Goby3 3.7.0
2026.11.12
Loading...
Searching...
No Matches
systemd.h
Go to the documentation of this file.
1// Copyright 2026:
2// GobySoft, LLC (2013-)
3// Community contributors (see AUTHORS file)
4// File authors:
5// Toby Schneider <toby@gobysoft.org>
6//
7//
8// This file is part of the Goby Underwater Autonomy Project Libraries
9// ("The Goby Libraries").
10//
11// The Goby Libraries are free software: you can redistribute them and/or modify
12// them under the terms of the GNU Lesser General Public License as published by
13// the Free Software Foundation, either version 2.1 of the License, or
14// (at your option) any later version.
15//
16// The Goby Libraries are distributed in the hope that they will be useful,
17// but WITHOUT ANY WARRANTY; without even the implied warranty of
18// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19// GNU Lesser General Public License for more details.
20//
21// You should have received a copy of the GNU Lesser General Public License
22// along with Goby. If not, see <http://www.gnu.org/licenses/>.
23
24#ifndef GOBY_MIDDLEWARE_TOOL_INTERFACE_SYSTEMD_H
25#define GOBY_MIDDLEWARE_TOOL_INTERFACE_SYSTEMD_H
26
27#include <filesystem>
28#include <fstream>
29#include <map>
30#include <optional>
31#include <regex>
32#include <sstream>
33#include <string>
34#include <vector>
35
37
38namespace goby
39{
40namespace middleware
41{
42namespace tool
43{
44namespace interface
45{
47{
48 std::string file;
49 std::string text;
50};
51
52namespace detail
53{
54// systemd unit names: letters, digits and ":_.-@"
55inline std::string unit_name(const std::string& name)
56{
57 std::string out;
58 for (unsigned char c : name)
59 out += (std::isalnum(c) || std::string(":_.-@").find(c) != std::string::npos) ? c : '_';
60 return out;
61}
62
65inline std::optional<bool> evaluate_when(const std::string& when)
66{
67 for (const std::string op : {" =~ ", " == ", " != "})
68 {
69 auto pos = when.find(op);
70 if (pos == std::string::npos)
71 continue;
72 auto lhs = when.substr(0, pos), rhs = when.substr(pos + op.size());
73 if ((lhs + rhs).find("${") != std::string::npos)
74 return std::nullopt;
75 if (op == " =~ ")
76 return std::regex_search(lhs, std::regex(rhs, std::regex::extended));
77 return (lhs == rhs) == (op == " == ");
78 }
79 return std::nullopt;
80}
81
82inline SystemdSettings merged(const SystemdSettings& defaults, const SystemdSettings& own)
83{
84 SystemdSettings s = own;
85 auto fill = [](std::optional<std::string>& field, const std::optional<std::string>& fallback)
86 {
87 if (!field)
88 field = fallback;
89 };
90 fill(s.unit_template, defaults.unit_template);
91 fill(s.user, defaults.user);
92 fill(s.group, defaults.group);
93 fill(s.wanted_by, defaults.wanted_by);
94 fill(s.description, defaults.description);
95 fill(s.condition, defaults.condition);
96 fill(s.unit, defaults.unit);
97 fill(s.service, defaults.service);
98 fill(s.install, defaults.install);
99 for (const auto& kv : defaults.vars) s.vars.emplace(kv.first, kv.second);
100 return s;
101}
102
103// systemd runs ExecStart without a shell, so a command that needs one is handed to bash
104inline std::string exec_start(const std::string& command)
105{
106 static const std::vector<std::string> shell_syntax{"<(", "$(", "|", "&&", ";", "`", ">"};
107 bool needs_shell =
108 std::any_of(shell_syntax.begin(), shell_syntax.end(),
109 [&](const std::string& s) { return command.find(s) != std::string::npos; });
110 if (!needs_shell)
111 return command;
112 // '$' is doubled so that systemd passes it on to bash
113 auto quoted = replace_all(replace_all(command, "'", "'\\''"), "$", "$$");
114 return "/bin/bash -c '" + quoted + "'";
115}
116
117// replaces ${name} for each var; anything else (systemd's own $MAINPID, %i) is left alone
118inline std::string substitute(std::string text, const std::map<std::string, std::string>& vars)
119{
120 for (const auto& kv : vars) text = replace_all(text, "${" + kv.first + "}", kv.second);
121 return text;
122}
123} // namespace detail
124
126inline std::string systemd_target(const Deployment& deployment, const LaunchUnit& unit)
127{
128 if (unit.platform->systemd.target)
129 return *unit.platform->systemd.target;
130 return detail::unit_name(deployment.name + "-" + unit.name()) + ".target";
131}
132
135inline std::vector<SystemdUnitFile> render_systemd(const Deployment& deployment,
136 const LaunchUnit& unit, Issues& issues)
137{
138 const auto& platform = *unit.platform;
139 auto prefix = unit.module.empty() ? "" : detail::unit_name(unit.module) + "-";
140 auto service_name = [&](const std::string& name)
141 { return prefix + detail::unit_name(name) + ".service"; };
142 auto target = systemd_target(deployment, unit);
143 auto instance = platform.name.substr(std::min(platform.base_name.size(), platform.name.size()));
144
145 auto lines = launch_lines(unit);
146 std::string gobyd_service;
147 for (const auto& line : lines)
148 {
149 if (line.entry->enabled && detail::is_gobyd(*line.entry))
150 gobyd_service = service_name(line.name);
151 }
152
153 std::vector<SystemdUnitFile> units;
154 std::vector<std::string> services;
155 // entries that goby_launch would start but that get no unit here
156 std::vector<std::string> dropped;
157 std::string previous;
158 for (const auto& line : lines)
159 {
160 const auto& e = *line.entry;
161 auto settings = detail::merged(platform.systemd.defaults, e.systemd);
162 if (!e.enabled || settings.skip)
163 continue;
164 if (e.when)
165 {
166 auto decided = detail::evaluate_when(*e.when);
167 if (!decided)
168 {
169 add_issue(issues, Issue::Severity::WARNING, "systemd-when",
170 e.name + ": when: '" + *e.when +
171 "' cannot be decided before launch, so no unit is written; set its "
172 "values with -D, or give systemd: { condition: ... }",
173 e.location);
174 }
175 if (!decided || !*decided)
176 {
177 dropped.push_back(line.name);
178 continue;
179 }
180 }
181
182 auto service = service_name(line.name);
183 auto kill =
184 line.kill.empty() ? (line.goby_application ? "goby_terminate" : "SIGTERM") : line.kill;
185 bool needs_gobyd = line.goby_application && !detail::is_gobyd(e) && !gobyd_service.empty();
186 auto description = settings.description.value_or(line.name + " (" + unit.name() + " of " +
187 deployment.name + ")");
188
189 std::string environment;
190 for (const auto& kv : line.env)
191 environment += "Environment=\"" + kv.first + "=" + kv.second + "\"\n";
192
193 std::string text;
194 if (settings.unit_template)
195 {
196 auto path = std::filesystem::path(*settings.unit_template);
197 if (path.is_relative())
198 path = std::filesystem::path(deployment.file).parent_path() / path;
199 std::ifstream in(path);
200 if (!in.is_open())
201 {
202 add_issue(issues, Issue::Severity::ERROR, "systemd-template",
203 e.name + ": cannot open template " + path.string(), e.location);
204 continue;
205 }
206 std::stringstream ss;
207 ss << in.rdbuf();
208
209 auto config = line.goby_application && !e.config
210 ? platform.launch_settings.config.value_or("")
211 : e.config.value_or("");
212 auto args = line.goby_application && !e.args
213 ? platform.launch_settings.args.value_or("")
214 : e.args.value_or("");
215 std::map<std::string, std::string> vars{
216 {"name", line.name},
217 {"unit_name", service},
218 {"application", e.application},
219 {"platform", platform.name},
220 {"instance", instance},
221 {"deployment", deployment.name},
222 {"description", description},
223 {"exec", e.exec.value_or(e.process ? e.name : e.application)},
224 {"command", line.unconditional_command},
225 {"exec_start", detail::exec_start(line.unconditional_command)},
226 {"config", detail::replace_all(config, "${app}", line.name)},
227 {"args", detail::replace_all(args, "${app}", line.name)},
228 {"env", environment},
229 {"kill", kill},
230 {"user", settings.user.value_or("")},
231 {"group", settings.group.value_or("")},
232 {"wanted_by", settings.wanted_by.value_or(target)},
233 {"target", target},
234 {"unit", settings.unit.value_or("")},
235 {"service", settings.service.value_or("")},
236 {"install", settings.install.value_or("")}};
237 for (const auto& kv : settings.vars) vars[kv.first] = kv.second;
238 text = detail::substitute(ss.str(), vars);
239 }
240 else
241 {
242 std::stringstream ss;
243 ss << "# Generated by 'goby interface systemd' from " << deployment.file
244 << ": edit that file rather than this one.\n";
245 ss << "[Unit]\n";
246 ss << "Description=" << description << "\n";
247 ss << "PartOf=" << target << "\n";
248 if (needs_gobyd)
249 ss << "After=" << gobyd_service << "\nBindsTo=" << gobyd_service << "\n";
250 if (platform.systemd.ordered && !previous.empty() &&
251 !(needs_gobyd && previous == gobyd_service))
252 ss << "After=" << previous << "\n";
253 if (settings.condition)
254 ss << *settings.condition << "\n";
255 if (settings.unit)
256 ss << *settings.unit << "\n";
257
258 ss << "\n[Service]\n";
259 if (settings.user)
260 ss << "User=" << *settings.user << "\n";
261 if (settings.group)
262 ss << "Group=" << *settings.group << "\n";
263 ss << environment;
264 ss << "ExecStart=" << detail::exec_start(line.unconditional_command) << "\n";
265 if (line.goby_application && kill == "goby_terminate")
266 ss << "ExecStop=goby_terminate --target_pid $MAINPID\n";
267 else
268 ss << "KillSignal=" << (kill == "goby_terminate" ? "SIGTERM" : kill) << "\n";
269 ss << "Restart=on-failure\n";
270 if (settings.service)
271 ss << *settings.service << "\n";
272
273 ss << "\n[Install]\n";
274 ss << "WantedBy=" << settings.wanted_by.value_or(target) << "\n";
275 if (settings.install)
276 ss << *settings.install << "\n";
277 text = ss.str();
278 }
279
280 if (text.find("${") != std::string::npos)
281 add_issue(issues, Issue::Severity::INFO, "systemd-unexpanded",
282 service + " still has a ${...} that systemd will read as its own variable",
283 e.location);
284 units.push_back({service, text});
285 services.push_back(service);
286 previous = service;
287 }
288
289 // gobyd would hold, or goby_coroner report dead, an application that is never started
290 for (const auto& line : lines)
291 {
292 const auto& e = *line.entry;
293 std::vector<std::string> waits_for;
294 if (e.hold)
295 waits_for = detail::expand_auto(*e.hold, auto_hold(unit));
296 if (e.expected)
297 {
298 auto expected = detail::expand_auto(*e.expected, auto_expected(unit, e));
299 waits_for.insert(waits_for.end(), expected.begin(), expected.end());
300 }
301 for (const auto& name : dropped)
302 {
303 if (std::find(waits_for.begin(), waits_for.end(), name) != waits_for.end())
304 add_issue(issues, Issue::Severity::WARNING, "systemd-dropped",
305 line.name + " waits for " + name + ", which has no unit", e.location);
306 }
307 }
308
309 if (!platform.systemd.target)
310 {
311 std::stringstream ss;
312 ss << "# Generated by 'goby interface systemd' from " << deployment.file
313 << ": edit that file rather than this one.\n";
314 ss << "[Unit]\n";
315 ss << "Description=" << unit.name() << " of " << deployment.name << "\n";
316 if (!services.empty())
317 {
318 ss << "Wants=";
319 for (std::size_t i = 0; i < services.size(); ++i) ss << (i ? " " : "") << services[i];
320 ss << "\n";
321 }
322 ss << "\n[Install]\nWantedBy=multi-user.target\n";
323 units.insert(units.begin(), {target, ss.str()});
324 }
325 return units;
326}
327
328} // namespace interface
329} // namespace tool
330} // namespace middleware
331} // namespace goby
332
333#endif
detail namespace with internal helper functions
Definition json.hpp:263
std::string replace_all(std::string text, const std::string &from, const std::string &to)
Definition launch.h:70
std::string unit_name(const std::string &name)
Definition systemd.h:55
std::vector< std::string > expand_auto(const std::vector< std::string > &given, const std::vector< std::string > &derived)
Definition launch.h:203
std::string exec_start(const std::string &command)
Definition systemd.h:104
bool is_gobyd(const LaunchEntry &e)
Definition launch.h:144
SystemdSettings merged(const SystemdSettings &defaults, const SystemdSettings &own)
Definition systemd.h:82
std::string substitute(std::string text, const std::map< std::string, std::string > &vars)
Definition systemd.h:118
std::optional< bool > evaluate_when(const std::string &when)
A when: condition decided now, from the values -D and the environment gave the deployment; nullopt wh...
Definition systemd.h:65
std::vector< Issue > Issues
Definition model.h:89
std::string systemd_target(const Deployment &deployment, const LaunchUnit &unit)
The target (generated, or the platform's systemd: target:) that a unit's services belong to.
Definition systemd.h:126
std::vector< SystemdUnitFile > render_systemd(const Deployment &deployment, const LaunchUnit &unit, Issues &issues)
The systemd units that start a launch unit: one service per enabled entry and, unless the platform na...
Definition systemd.h:135
std::vector< std::string > auto_hold(const LaunchUnit &unit)
gobyd's hold: applications with a required intervehicle subscription, which must be subscribed before...
Definition launch.h:170
void add_issue(Issues &issues, Issue::Severity severity, std::string code, std::string message, std::string location="")
Definition model.h:91
std::vector< std::string > auto_expected(const LaunchUnit &unit, const LaunchEntry &coroner)
goby_coroner's expected names: every Goby application in the unit but the coroner itself and those ma...
Definition launch.h:187
std::vector< LaunchLine > launch_lines(const LaunchUnit &unit)
The goby_launch lines for a unit, in run: order (disabled entries included)
Definition launch.h:221
The global namespace for the Goby project.
extern ::PROTOBUF_NAMESPACE_ID::internal::ExtensionIdentifier< ::PROTOBUF_NAMESPACE_ID::FieldOptions, ::PROTOBUF_NAMESPACE_ID::internal::MessageTypeTraits< ::goby::GobyFieldOptions >, 11, false > field
What one launch file starts: a platform, or one module (one gobyd) of a platform.
Definition launch.h:46
std::string std::string name() const
Definition launch.h:51
std::optional< std::string > target
Definition model.h:366
systemd: settings, on a run: entry or as a platform's defaults
Definition model.h:307
std::optional< std::string > condition
Definition model.h:315
std::optional< std::string > service
Definition model.h:318
std::optional< std::string > install
Definition model.h:319
std::map< std::string, std::string > vars
Definition model.h:320
std::optional< std::string > description
Definition model.h:313
std::optional< std::string > unit_template
Definition model.h:309
std::optional< std::string > wanted_by
Definition model.h:312