Goby3 3.7.0
2026.11.12
Loading...
Searching...
No Matches
launch.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_LAUNCH_H
25#define GOBY_MIDDLEWARE_TOOL_INTERFACE_LAUNCH_H
26
27#include <algorithm>
28#include <map>
29#include <set>
30#include <sstream>
31#include <string>
32#include <vector>
33
35
36namespace goby
37{
38namespace middleware
39{
40namespace tool
41{
42namespace interface
43{
46{
47 const Platform* platform{nullptr};
48 // "" for a platform without modules
49 std::string module;
50
51 std::string name() const { return platform->name + (module.empty() ? "" : "." + module); }
52};
53
56{
57 const LaunchEntry* entry{nullptr};
58 std::string name;
59 std::string command;
60 // the command without its when: condition
62 // as given in the deployment (empty: goby_launch's default)
63 std::string kill;
65 bool goby_application{false};
66};
67
68namespace detail
69{
70inline std::string replace_all(std::string text, const std::string& from, const std::string& to)
71{
72 for (auto pos = text.find(from); pos != std::string::npos; pos = text.find(from, pos))
73 {
74 text.replace(pos, from.size(), to);
75 pos += to.size();
76 }
77 return text;
78}
79
80inline std::string first_word_basename(const std::string& command)
81{
82 auto word = command.substr(0, command.find(' '));
83 auto slash = word.rfind('/');
84 return slash == std::string::npos ? word : word.substr(slash + 1);
85}
86
87inline bool in_unit(const LaunchEntry& e, const LaunchUnit& unit)
88{
89 return unit.module.empty() || e.module == unit.module;
90}
91
92inline const Application* application(const LaunchUnit& unit, const LaunchEntry& e)
93{
94 for (const auto& module : unit.platform->modules)
95 {
96 if (!unit.module.empty() && module.name != unit.module)
97 continue;
98 auto it = module.applications.find(e.name);
99 if (it != module.applications.end())
100 return &it->second;
101 }
102 return nullptr;
103}
104
105inline bool requires_intervehicle(const Application& app)
106{
107 for (const auto& e : app.subscribes_on(Layer::INTERVEHICLE))
108 {
109 if (e.necessity == goby::middleware::Necessity::REQUIRED)
110 return true;
111 }
112 return false;
113}
114
117inline std::string when_command(const std::string& when)
118{
119 for (const std::string op : {" =~ ", " == ", " != "})
120 {
121 auto pos = when.find(op);
122 if (pos == std::string::npos)
123 continue;
124 auto lhs = when.substr(0, pos), rhs = when.substr(pos + op.size());
125 if (op == " =~ ")
126 return "echo \"" + lhs + "\" | grep -qE \"" + rhs + "\"";
127 return "test \"" + lhs + "\" " + (op == " == " ? "=" : "!=") + " \"" + rhs + "\"";
128 }
129 return when;
130}
131
132inline std::string escape_double_quoted(const std::string& text)
133{
134 std::string escaped;
135 for (char c : text)
136 {
137 if (c == '"' || c == '\\')
138 escaped += '\\';
139 escaped += c;
140 }
141 return escaped;
142}
143
144inline bool is_gobyd(const LaunchEntry& e)
145{
146 return !e.process && (e.application == "gobyd" || e.name == "gobyd");
147}
148} // namespace detail
149
152inline std::vector<LaunchUnit> launch_units(const Deployment& deployment)
153{
154 std::vector<LaunchUnit> units;
155 for (const auto& platform : deployment.platforms)
156 {
157 if (!platform.has_modules())
158 {
159 units.push_back({&platform, ""});
160 continue;
161 }
162 for (const auto& module : platform.modules) units.push_back({&platform, module.name});
163 }
164 return units;
165}
166
170inline std::vector<std::string> auto_hold(const LaunchUnit& unit)
171{
172 std::vector<std::string> clients;
173 for (const auto& e : unit.platform->launch)
174 {
175 if (!e.enabled || e.process || !detail::in_unit(e, unit))
176 continue;
177 const auto* app = detail::application(unit, e);
178 if ((app && detail::requires_intervehicle(*app)) ||
179 e.application == "goby_intervehicle_portal")
180 clients.push_back(e.name);
181 }
182 return clients;
183}
184
187inline std::vector<std::string> auto_expected(const LaunchUnit& unit, const LaunchEntry& coroner)
188{
189 std::vector<std::string> names;
190 for (const auto& e : unit.platform->launch)
191 {
192 if (!e.enabled || e.process || e.not_expected || &e == &coroner ||
193 !detail::in_unit(e, unit))
194 continue;
195 names.push_back(e.name);
196 }
197 return names;
198}
199
200namespace detail
201{
202// a list with "auto" in it, with the derived list in its place
203inline std::vector<std::string> expand_auto(const std::vector<std::string>& given,
204 const std::vector<std::string>& derived)
205{
206 std::vector<std::string> out;
207 for (const auto& item : given)
208 {
209 const auto& add = item == "auto" ? derived : std::vector<std::string>{item};
210 for (const auto& name : add)
211 {
212 if (std::find(out.begin(), out.end(), name) == out.end())
213 out.push_back(name);
214 }
215 }
216 return out;
217}
218} // namespace detail
219
221inline std::vector<LaunchLine> launch_lines(const LaunchUnit& unit)
222{
223 const auto& settings = unit.platform->launch_settings;
224 std::vector<LaunchLine> lines;
225 for (const auto& e : unit.platform->launch)
226 {
227 if (!detail::in_unit(e, unit))
228 continue;
229 LaunchLine line;
230 line.entry = &e;
231 line.name = e.name;
232 line.goby_application = !e.process;
233
234 auto pick = [&](const std::optional<std::string>& own,
235 const std::optional<std::string>& fallback) -> std::string
236 {
237 if (own)
238 return *own;
239 return line.goby_application && fallback ? *fallback : "";
240 };
241 auto app = [&](const std::string& text)
242 { return detail::replace_all(text, "${app}", e.name); };
243
244 std::string command = app(e.exec ? *e.exec : (e.process ? e.name : e.application));
245 for (const auto& part : {pick(e.config, settings.config), pick(e.args, settings.args)})
246 {
247 if (!part.empty())
248 command += " " + app(part);
249 }
250
251 if (e.hold)
252 {
253 std::string clients;
254 for (const auto& client : detail::expand_auto(*e.hold, auto_hold(unit)))
255 clients += (clients.empty() ? "" : " ") + std::string("required_client: \"") +
256 client + "\"";
257 if (!clients.empty())
258 command += " --hold '" + clients + "'";
259 }
260 if (e.expected)
261 {
262 for (const auto& name : detail::expand_auto(*e.expected, auto_expected(unit, e)))
263 command += " --expected_name " + name;
264 }
265 line.unconditional_command = command;
266 if (e.when)
267 command = "/bin/bash -c \"if " +
268 detail::escape_double_quoted(detail::when_command(*e.when)) + "; then exec " +
269 detail::escape_double_quoted(command) + "; else exec sleep infinity; fi\"";
270 line.command = command;
271
272 line.kill = pick(e.kill, settings.kill);
273 if (line.kill.empty() && e.process)
274 line.kill = "SIGTERM";
275
276 if (line.goby_application)
277 line.env = settings.env;
278 for (const auto& kv : e.env)
279 {
280 auto it = std::find_if(line.env.begin(), line.env.end(),
281 [&](const std::pair<std::string, std::string>& existing)
282 { return existing.first == kv.first; });
283 if (it == line.env.end())
284 line.env.push_back(kv);
285 else
286 it->second = kv.second;
287 }
288 for (auto& kv : line.env) kv.second = app(kv.second);
289 lines.push_back(line);
290 }
291 return lines;
292}
293
295inline std::string launch_line_text(const LaunchLine& line)
296{
297 std::vector<std::string> directives;
298 if (!line.kill.empty() && !(line.goby_application && line.kill == "goby_terminate"))
299 directives.push_back("kill=" + line.kill);
300 if (detail::first_word_basename(line.command) != line.name)
301 directives.push_back("name=" + line.name);
302 for (const auto& kv : line.env) directives.push_back("env=" + kv.first + "=" + kv.second);
303
304 std::string text;
305 if (!directives.empty())
306 {
307 text = "[";
308 for (std::size_t i = 0; i < directives.size(); ++i) text += (i ? ", " : "") + directives[i];
309 text += "] ";
310 }
311 return text + line.command;
312}
313
315inline std::string render_launch(const LaunchUnit& unit, const std::string& deployment_file)
316{
317 std::stringstream ss;
318 auto flags = unit.platform->launch_settings.flags;
319 if (flags.find("-p ") == std::string::npos && flags.find("-p\t") == std::string::npos)
320 flags += (flags.empty() ? "" : " ") + std::string("-p ") + unit.name();
321 ss << "#!/usr/bin/env -S goby_launch " << flags << "\n";
322 ss << "# Generated by 'goby interface launch' from " << deployment_file << " for "
323 << unit.name() << ": edit that file rather than this one.\n";
324 for (const auto& line : launch_lines(unit))
325 ss << (line.entry->enabled ? "" : "# (enabled: false) ") << launch_line_text(line) << "\n";
326 return ss.str();
327}
328
332inline void check_launch(const Deployment& deployment, Issues& issues)
333{
334 for (const auto& unit : launch_units(deployment))
335 {
336 bool seen_gobyd = std::none_of(
337 unit.platform->launch.begin(), unit.platform->launch.end(), [&](const LaunchEntry& e)
338 { return e.enabled && detail::in_unit(e, unit) && detail::is_gobyd(e); });
339 for (const auto& line : launch_lines(unit))
340 {
341 const auto& e = *line.entry;
342 if (!e.enabled)
343 continue;
344 auto text = launch_line_text(line);
345 if (text.find('#') != std::string::npos)
346 add_issue(issues, Issue::Severity::ERROR, "launch-hash",
347 e.name + ": goby_launch drops everything after a '#' on a line",
348 e.location);
349 // goby_launch reads [directives] up to the last ']' on the line
350 if (text[0] == '[' && line.command.find(']') != std::string::npos)
351 add_issue(issues, Issue::Severity::ERROR, "launch-bracket",
352 e.name + ": goby_launch would read the command's ']' as the end of the "
353 "line's [directives]",
354 e.location);
355 if (detail::is_gobyd(e))
356 seen_gobyd = true;
357 else if (!e.process && !seen_gobyd)
358 add_issue(issues, Issue::Severity::WARNING, "launch-order",
359 e.name + " is launched before gobyd on " + unit.name(), e.location);
360
361 if (e.hold && std::find(e.hold->begin(), e.hold->end(), "auto") == e.hold->end())
362 {
363 for (const auto& client : auto_hold(unit))
364 {
365 if (std::find(e.hold->begin(), e.hold->end(), client) == e.hold->end())
366 add_issue(issues, Issue::Severity::WARNING, "hold-incomplete",
367 "the hold leaves out " + client +
368 ", which needs its intervehicle subscriptions in place "
369 "first (hold: auto adds it)",
370 e.location);
371 }
372 }
373 if (e.expected &&
374 std::find(e.expected->begin(), e.expected->end(), "auto") == e.expected->end())
375 {
376 auto launched = auto_expected(unit, e);
377 std::set<std::string> given(e.expected->begin(), e.expected->end()),
378 actual(launched.begin(), launched.end());
379 std::string missing, extra;
380 for (const auto& name : actual)
381 if (!given.count(name))
382 missing += (missing.empty() ? "" : ", ") + name;
383 for (const auto& name : given)
384 if (!actual.count(name))
385 extra += (extra.empty() ? "" : ", ") + name;
386 if (!missing.empty() || !extra.empty())
387 add_issue(issues, Issue::Severity::INFO, "coroner-expected",
388 "the expected list " +
389 (missing.empty() ? "" : "leaves out " + missing) +
390 (!missing.empty() && !extra.empty() ? " and " : "") +
391 (extra.empty() ? "" : "names " + extra + ", not launched here"),
392 e.location);
393 }
394 }
395 }
396}
397
400{
401 int line_number{0};
402 std::string name;
403 std::string command;
404 std::string kill;
406};
407
408inline std::vector<ParsedLaunchLine> parse_launch_file(const std::string& text)
409{
410 std::vector<ParsedLaunchLine> lines;
411 std::stringstream in(text);
412 int number = 0;
413 for (std::string raw; std::getline(in, raw);)
414 {
415 ++number;
416 auto line = raw.substr(0, raw.find('#'));
417 auto first = line.find_first_not_of(" \t");
418 if (first == std::string::npos)
419 continue;
420 line = line.substr(first, line.find_last_not_of(" \t\r") - first + 1);
421
422 ParsedLaunchLine parsed;
423 parsed.line_number = number;
424 if (line[0] == '[')
425 {
426 auto close = line.find(']');
427 std::string directives = line.substr(1, close - 1);
428 line = close == std::string::npos ? "" : line.substr(close + 1);
429 line = line.substr(std::min(line.size(), line.find_first_not_of(" \t")));
430 for (auto& c : directives)
431 {
432 if (c == ',' || c == ';')
433 c = ' ';
434 }
435 std::stringstream ds(directives);
436 for (std::string directive; ds >> directive;)
437 {
438 auto eq = directive.find('=');
439 auto key = directive.substr(0, eq);
440 auto value = eq == std::string::npos ? "" : directive.substr(eq + 1);
441 if (key == "kill")
442 parsed.kill = value;
443 else if (key == "name")
444 parsed.name = value;
445 else if (key == "env")
446 {
447 auto env_eq = value.find('=');
448 parsed.env.emplace_back(value.substr(0, env_eq),
449 env_eq == std::string::npos ? ""
450 : value.substr(env_eq + 1));
451 }
452 }
453 }
454 parsed.command = line;
455 if (parsed.name.empty())
456 parsed.name = detail::first_word_basename(line);
457 lines.push_back(parsed);
458 }
459 return lines;
460}
461
464{
465 // launched-only, not-launched, env, kill
466 std::string kind;
467 std::string name;
468 int line_number{0};
469 std::string detail;
470};
471
473inline std::vector<LaunchDifference> check_launch_file(const LaunchUnit& unit,
474 const std::string& launch_text)
475{
476 auto parsed = parse_launch_file(launch_text);
477 std::vector<LaunchLine> expected;
478 for (const auto& line : launch_lines(unit))
479 {
480 if (line.entry->enabled)
481 expected.push_back(line);
482 }
483
484 // goby_launch kills with goby_terminate only when it finds exactly one gobyd to ask
485 auto default_kill = [](int gobyd_count)
486 { return gobyd_count == 1 ? "goby_terminate" : "SIGTERM"; };
487 int file_gobyds = std::count_if(parsed.begin(), parsed.end(), [](const ParsedLaunchLine& l)
488 { return detail::first_word_basename(l.command) == "gobyd"; });
489 int deployment_gobyds = std::count_if(expected.begin(), expected.end(), [](const LaunchLine& l)
490 { return detail::is_gobyd(*l.entry); });
491
492 auto env_text = [](KeyValues env)
493 {
494 std::sort(env.begin(), env.end());
495 std::string text;
496 for (const auto& kv : env) text += (text.empty() ? "" : " ") + kv.first + "=" + kv.second;
497 return text.empty() ? "(none)" : text;
498 };
499
500 std::vector<LaunchDifference> differences;
501 for (const auto& p : parsed)
502 {
503 auto it = std::find_if(expected.begin(), expected.end(),
504 [&](const LaunchLine& l) { return l.name == p.name; });
505 if (it == expected.end())
506 {
507 differences.push_back(
508 {"launched-only", p.name, p.line_number,
509 "line " + std::to_string(p.line_number) + " has no entry in the deployment"});
510 continue;
511 }
512 auto file_kill = p.kill.empty() ? default_kill(file_gobyds) : p.kill;
513 auto deployment_kill = it->kill.empty() ? default_kill(deployment_gobyds) : it->kill;
514 if (file_kill != deployment_kill)
515 differences.push_back(
516 {"kill", p.name, p.line_number,
517 file_kill + " in the launch file, " + deployment_kill + " in the deployment"});
518 if (env_text(p.env) != env_text(it->env))
519 differences.push_back({"env", p.name, p.line_number,
520 env_text(p.env) + " in the launch file, " + env_text(it->env) +
521 " in the deployment"});
522 }
523 for (const auto& l : expected)
524 {
525 if (std::none_of(parsed.begin(), parsed.end(),
526 [&](const ParsedLaunchLine& p) { return p.name == l.name; }))
527 differences.push_back(
528 {"not-launched", l.name, 0,
529 "in the deployment (" + l.entry->location + ") but not in the launch file"});
530 }
531 return differences;
532}
533
534} // namespace interface
535} // namespace tool
536} // namespace middleware
537} // namespace goby
538
539#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
bool in_unit(const LaunchEntry &e, const LaunchUnit &unit)
Definition launch.h:87
const Application * application(const LaunchUnit &unit, const LaunchEntry &e)
Definition launch.h:92
std::string first_word_basename(const std::string &command)
Definition launch.h:80
std::string escape_double_quoted(const std::string &text)
Definition launch.h:132
std::vector< std::string > expand_auto(const std::vector< std::string > &given, const std::vector< std::string > &derived)
Definition launch.h:203
bool is_gobyd(const LaunchEntry &e)
Definition launch.h:144
bool requires_intervehicle(const Application &app)
Definition launch.h:105
std::string when_command(const std::string &when)
A when: condition as a shell command: "A =~ REGEX", "A == B", "A != B", or a command whose exit statu...
Definition launch.h:117
void check_launch(const Deployment &deployment, Issues &issues)
Problems with a deployment's launch data: anything goby_launch would cut at a '#',...
Definition launch.h:332
std::vector< LaunchDifference > check_launch_file(const LaunchUnit &unit, const std::string &launch_text)
Compare a hand-written launch file with what the deployment would launch.
Definition launch.h:473
std::vector< ParsedLaunchLine > parse_launch_file(const std::string &text)
Definition launch.h:408
std::vector< Issue > Issues
Definition model.h:89
std::vector< std::pair< std::string, std::string > > KeyValues
Definition model.h:303
std::string render_launch(const LaunchUnit &unit, const std::string &deployment_file)
A goby_launch file for the unit.
Definition launch.h:315
std::vector< LaunchUnit > launch_units(const Deployment &deployment)
Every launch file a deployment makes: one per platform, or per module where a platform has modules.
Definition launch.h:152
std::string launch_line_text(const LaunchLine &line)
"[kill=..., name=..., env=...] command", as goby_launch reads it
Definition launch.h:295
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.
const std::set< PubSubEntry > & subscribes_on(Layer layer) const
Definition model.h:167
A difference between a launch file and a deployment's launch lines.
Definition launch.h:464
How one run: entry is started by goby_launch.
Definition model.h:326
One goby_launch line, worked out from a run: entry.
Definition launch.h:56
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
One line of an existing launch file, as goby_launch would read it.
Definition launch.h:400
std::vector< LaunchEntry > launch
Definition model.h:379