Goby3 3.7.0
2026.11.12
Loading...
Searching...
No Matches
model.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_MODEL_H
25#define GOBY_MIDDLEWARE_TOOL_INTERFACE_MODEL_H
26
27#include <algorithm>
28#include <cctype>
29#include <cstdint>
30#include <cstdlib>
31#include <filesystem>
32#include <fstream>
33#include <functional>
34#include <map>
35#include <memory>
36#include <optional>
37#include <ostream>
38#include <regex>
39#include <set>
40#include <sstream>
41#include <string>
42#include <vector>
43
44#include <yaml-cpp/yaml.h>
45
48
49namespace goby
50{
51namespace middleware
52{
53namespace tool
54{
55namespace interface
56{
58struct Issue
59{
60 enum class Severity
61 {
62 ERROR,
63 WARNING,
64 INFO
65 };
66
68 struct Subject
69 {
70 std::string platform;
71 std::string module;
72 std::string application;
73 std::string thread;
74 std::string layer;
75 std::string group;
76 std::string scheme;
77 std::string type;
78 std::string necessity;
79 };
80
82 std::string code;
83 std::string message;
84 // e.g. "platforms[0].run[2]"
85 std::string location;
87};
88
89using Issues = std::vector<Issue>;
90
91inline void add_issue(Issues& issues, Issue::Severity severity, std::string code,
92 std::string message, std::string location = "")
93{
94 issues.push_back({severity, std::move(code), std::move(message), std::move(location), {}});
95}
96
97inline std::ostream& operator<<(std::ostream& os, const Issue& issue)
98{
99 switch (issue.severity)
100 {
101 case Issue::Severity::ERROR: os << "error"; break;
102 case Issue::Severity::WARNING: os << "warning"; break;
103 case Issue::Severity::INFO: os << "info"; break;
104 }
105 os << " [" << issue.code << "]";
106 if (!issue.location.empty())
107 os << " " << issue.location;
108 return os << ": " << issue.message;
109}
110
111inline bool has_errors(const Issues& issues)
112{
113 return std::any_of(issues.begin(), issues.end(),
114 [](const Issue& i) { return i.severity == Issue::Severity::ERROR; });
115}
116
119{
120 public:
121 int next(Layer layer) { return next_[layer]++; }
122
123 private:
124 std::map<Layer, int> next_;
125};
126
127struct Thread
128{
129 Thread(std::string n, bool k, std::set<std::string> b = std::set<std::string>())
130 : name(std::move(n)), known(k), bases(std::move(b))
131 {
132 }
133
134 Thread() : known(true) {}
135
136 std::string most_derived_name() const { return parent ? parent->most_derived_name() : name; }
137
138 std::string name;
139 bool known;
140 std::set<std::string> bases;
141
142 // set when a class derived from this one (rather than SimpleThread directly) is the thread
143 // that actually runs
144 const Thread* parent{nullptr};
145
146 std::set<PubSubEntry> interthread_publishes;
147 std::set<PubSubEntry> interthread_subscribes;
148};
149
151{
152 Application(std::string n) : name(std::move(n)) {}
153
154 std::string name;
155 std::map<std::string, std::shared_ptr<Thread>> threads;
156 // interprocess, intermodule and intervehicle (interthread entries live in each Thread)
157 std::map<Layer, std::set<PubSubEntry>> publishes;
158 std::map<Layer, std::set<PubSubEntry>> subscribes;
159
160 const std::set<PubSubEntry>& publishes_on(Layer layer) const
161 {
162 static const std::set<PubSubEntry> empty;
163 auto it = publishes.find(layer);
164 return it == publishes.end() ? empty : it->second;
165 }
166
167 const std::set<PubSubEntry>& subscribes_on(Layer layer) const
168 {
169 static const std::set<PubSubEntry> empty;
170 auto it = subscribes.find(layer);
171 return it == subscribes.end() ? empty : it->second;
172 }
173
176 using Binder = std::function<std::vector<PubSubEntry>(PubSubEntry)>;
177
179 void merge(const YAML::Node& yaml, PublishIndexer& indexer, const Binder& binder = Binder());
180
181 private:
182 std::string most_derived(const std::string& thread) const
183 {
184 auto it = threads.find(thread);
185 return it == threads.end() ? thread : it->second->most_derived_name();
186 }
187
188 void insert(std::set<PubSubEntry>& entries, PubSubEntry entry, PublishIndexer& indexer,
189 const Binder& binder)
190 {
191 for (auto& bound : binder ? binder(entry) : std::vector<PubSubEntry>{entry})
192 {
193 auto result = entries.insert(bound);
194 if (result.second && bound.direction == PubSubEntry::Direction::PUBLISH)
195 result.first->publish_index = indexer.next(bound.layer);
196 }
197 }
198};
199
200inline void Application::merge(const YAML::Node& yaml, PublishIndexer& indexer,
201 const Binder& binder)
202{
203 using Direction = PubSubEntry::Direction;
204
205 std::vector<std::pair<std::shared_ptr<Thread>, YAML::Node>> new_thread_yaml;
206 if (auto interthread_node = yaml["interthread"])
207 {
208 for (const auto& thread_node : interthread_node["threads"])
209 {
210 auto thread_name = thread_node["name"].as<std::string>();
211 bool known = !thread_node["known"] || thread_node["known"].as<bool>();
212 std::set<std::string> bases;
213 for (const auto& base : thread_node["bases"]) bases.insert(base.as<std::string>());
214
215 auto it = threads.find(thread_name);
216 if (it == threads.end())
217 it = threads
218 .emplace(thread_name, std::make_shared<Thread>(thread_name, known, bases))
219 .first;
220 else
221 it->second->bases.insert(bases.begin(), bases.end());
222 new_thread_yaml.emplace_back(it->second, thread_node);
223 }
224
225 // a thread that isn't a direct subclass of SimpleThread is drawn as its most derived class
226 for (auto& thread_p : threads)
227 {
228 auto& thread = *thread_p.second;
229 bool is_direct_thread_subclass =
230 std::any_of(thread.bases.begin(), thread.bases.end(), [](const std::string& b)
231 { return b.find("goby::middleware::SimpleThread") == 0; });
232 if (is_direct_thread_subclass)
233 continue;
234 for (const auto& base : thread.bases)
235 {
236 auto base_it = threads.find(base);
237 if (base_it != threads.end())
238 base_it->second->parent = &thread;
239 }
240 }
241
242 std::sort(new_thread_yaml.begin(), new_thread_yaml.end(),
243 [](const auto& a, const auto& b) { return a.first->name < b.first->name; });
244 for (auto& thread_yaml : new_thread_yaml)
245 {
246 auto& thread = *thread_yaml.first;
247 for (const auto& p : thread_yaml.second["publishes"])
248 insert(thread.interthread_publishes,
249 PubSubEntry(Layer::INTERTHREAD, Direction::PUBLISH, p,
250 thread.most_derived_name()),
251 indexer, binder);
252 for (const auto& s : thread_yaml.second["subscribes"])
253 insert(thread.interthread_subscribes,
254 PubSubEntry(Layer::INTERTHREAD, Direction::SUBSCRIBE, s,
255 thread.most_derived_name()),
256 indexer, binder);
257 }
258 }
259
261 {
262 auto layer_node = yaml[layer_name(layer)];
263 if (!layer_node)
264 continue;
265
266 for (const auto& p : layer_node["publishes"])
267 {
268 PubSubEntry entry(layer, Direction::PUBLISH, p);
269 entry.thread = most_derived(entry.thread);
270 insert(publishes[layer], entry, indexer, binder);
271 }
272 for (const auto& s : layer_node["subscribes"])
273 {
274 PubSubEntry entry(layer, Direction::SUBSCRIBE, s);
275 entry.thread = most_derived(entry.thread);
276 insert(subscribes[layer], entry, indexer, binder);
277 }
278 }
279
280 // the main thread of applications without interthread publish/subscribe
281 for (const auto* layer_map : {&publishes, &subscribes})
282 {
283 for (const auto& layer_p : *layer_map)
284 {
285 for (const auto& e : layer_p.second)
286 {
287 if (!threads.count(e.thread))
288 threads.emplace(e.thread,
289 std::make_shared<Thread>(e.thread, e.thread_is_known));
290 }
291 }
292 }
293}
294
295struct Module
296{
297 std::string name;
298 std::map<std::string, Application> applications;
299 // platform listed its interfaces directly, without modules:
300 bool not_a_module{false};
301};
302
303using KeyValues = std::vector<std::pair<std::string, std::string>>;
304
307{
308 // a project unit file, with ${var}s, in place of the built-in unit
309 std::optional<std::string> unit_template;
310 std::optional<std::string> user;
311 std::optional<std::string> group;
312 std::optional<std::string> wanted_by;
313 std::optional<std::string> description;
314 // a systemd Condition...= line, checked when the unit starts
315 std::optional<std::string> condition;
316 // appended to the [Unit], [Service] and [Install] sections of the built-in unit
317 std::optional<std::string> unit;
318 std::optional<std::string> service;
319 std::optional<std::string> install;
320 std::map<std::string, std::string> vars;
321 bool skip{false};
322};
323
326{
327 std::string module;
328 // goby_launch's name for the line, and ${app} in its config
329 std::string name;
330 // empty for a process (a non-Goby program, with no interface file)
331 std::string application;
332 bool process{false};
333 std::optional<std::string> exec;
334 std::optional<std::string> config;
335 std::optional<std::string> args;
336 std::optional<std::string> kill;
337 // a bash conditional ([[ ... ]]) deciding at launch whether the entry runs
338 std::optional<std::string> when;
340 bool enabled{true};
341 // gobyd's required_client list; "auto" stands for the derived list
342 std::optional<std::vector<std::string>> hold;
343 // goby_coroner's expected_name list; "auto" stands for the derived list
344 std::optional<std::vector<std::string>> expected;
345 // expected: false, leaving this entry out of an automatic expected list
346 bool not_expected{false};
348 std::string location;
349};
350
353{
354 std::string flags;
355 // defaults for Goby applications' entries
356 std::optional<std::string> config;
357 std::optional<std::string> args;
358 std::optional<std::string> kill;
360};
361
364{
365 // an existing umbrella unit to use instead of a generated <deployment>-<platform>.target
366 std::optional<std::string> target;
367 // start each unit after the one before it in run:, rather than all at once after gobyd
368 bool ordered{false};
370};
371
373{
374 std::string name;
375 // the name in the deployment file; differs from name for one of a platform's instances
376 std::string base_name;
377 std::vector<Module> modules;
378 // every run: entry, in order, including processes
379 std::vector<LaunchEntry> launch;
382
383 bool has_modules() const { return !(modules.size() == 1 && modules.front().not_a_module); }
384};
385
387{
388 std::string name;
389 // the deployment file, which relative paths in it (such as unit templates) are relative to
390 std::string file;
391 std::vector<Platform> platforms;
392 // continues numbering after the loaded publications, for placeholders added when drawing
394};
395
398{
399 // searched, in order, after the deployment file's own directory
400 std::vector<std::string> interface_dirs;
401 // goby's own installed interfaces (share/goby/interfaces), searched after interface_dirs
403 // ${NAME} values; these override the environment
404 std::map<std::string, std::string> defines;
405 // load each of a platform's instances: as <name><instance>; otherwise a platform with
406 // instances: is loaded once, its binds taking the numerics of every instance
408};
409
410namespace detail
411{
413struct Bind
414{
415 std::string location;
416 int uses{0};
417
418 // numeric: { group: <name or glob>, numeric: <values>, layer: <optional> }
419 std::regex group_pattern;
420 std::vector<std::string> numerics;
421 bool has_layer{false};
423
424 // publication: { dynamic: { layer, scheme, type }, publishes: [...], subscribes: [...] }
425 bool is_publication{false};
426 std::string select_scheme;
427 std::string select_type;
428 struct Concrete
429 {
430 std::string group;
431 std::string scheme;
432 std::string type;
433 };
434 std::vector<Concrete> publishes;
435 std::vector<Concrete> subscribes;
436};
437
438// one entry of a platform's or module's run: list (or of the older interfaces: list)
439struct RunRef
440{
441 // look the interface file up by the application it declares
442 std::string application;
443 // or read this file
444 std::string file;
445 // name the application instance (and its node) differently from the application
446 std::string name;
447 std::vector<std::string> plugins;
448 std::string location;
449 // an interfaces: entry that names its application, which merges it into that application
450 bool merge_intended{false};
451 std::vector<std::shared_ptr<Bind>> binds;
453};
454
456{
457 std::string name;
458 std::vector<RunRef> run;
459 bool not_a_module{false};
460};
461
463{
464 std::string location;
465 std::vector<ModuleRef> modules;
466 std::vector<std::shared_ptr<Bind>> binds;
467 std::vector<std::uint32_t> instances;
470};
471
475inline std::string expand_variables(const std::string& text,
476 const std::map<std::string, std::string>& defines,
477 std::set<std::string>& unset)
478{
479 std::string out;
480 for (std::size_t i = 0; i < text.size(); ++i)
481 {
482 if (text.compare(i, 3, "$${") == 0)
483 {
484 out += "${";
485 i += 2;
486 continue;
487 }
488 if (text.compare(i, 2, "${") != 0)
489 {
490 out += text[i];
491 continue;
492 }
493 auto close = text.find('}', i + 2);
494 if (close == std::string::npos)
495 {
496 out += text.substr(i);
497 break;
498 }
499 std::string expr = text.substr(i + 2, close - i - 2);
500 std::string name = expr, fallback;
501 bool has_fallback = false;
502 auto sep = expr.find(":-");
503 if (sep != std::string::npos)
504 {
505 name = expr.substr(0, sep);
506 fallback = expr.substr(sep + 2);
507 has_fallback = true;
508 }
509
510 if (name == "instance" || name == "app")
511 {
512 out += text.substr(i, close - i + 1);
513 i = close;
514 continue;
515 }
516
517 auto define_it = defines.find(name);
518 const char* env = std::getenv(name.c_str());
519 if (define_it != defines.end())
520 out += define_it->second;
521 else if (env)
522 out += env;
523 else if (has_fallback)
524 out += fallback;
525 else
526 {
527 unset.insert(name);
528 out += text.substr(i, close - i + 1);
529 }
530 i = close;
531 }
532 return out;
533}
534
535inline bool load_yaml_text(const std::string& text, const std::string& path,
536 const std::string& code, const std::string& location, Issues& issues,
537 YAML::Node& yaml)
538{
539 try
540 {
541 yaml = YAML::Load(text);
542 return true;
543 }
544 catch (const std::exception& e)
545 {
546 add_issue(issues, Issue::Severity::ERROR, code + "-invalid",
547 "cannot parse " + path + ": " + e.what(), location);
548 }
549 return false;
550}
551
552inline bool load_yaml(const std::string& path, const std::string& code, const std::string& location,
553 Issues& issues, YAML::Node& yaml)
554{
555 std::ifstream in(path);
556 if (!in.is_open())
557 {
558 add_issue(issues, Issue::Severity::ERROR, code + "-missing", "cannot open " + path,
559 location);
560 return false;
561 }
562 std::stringstream ss;
563 ss << in.rdbuf();
564 return load_yaml_text(ss.str(), path, code, location, issues, yaml);
565}
566
567inline std::vector<std::string> read_list(const YAML::Node& node)
568{
569 std::vector<std::string> values;
570 if (node && node.IsSequence())
571 for (const auto& v : node) values.push_back(v.as<std::string>());
572 else if (node && node.IsScalar())
573 values.push_back(node.as<std::string>());
574 return values;
575}
576
577inline bool layer_from_name(const std::string& name, Layer& layer)
578{
579 for (auto l :
581 {
582 if (layer_name(l) == name)
583 {
584 layer = l;
585 return true;
586 }
587 }
588 return false;
589}
590
591// "*" matches any run of characters; everything else is literal
592inline std::regex glob_regex(const std::string& glob)
593{
594 std::string re;
595 for (char c : glob)
596 {
597 if (c == '*')
598 re += ".*";
599 else if (std::string("\\^$.|?+()[]{}").find(c) != std::string::npos)
600 re += std::string("\\") + c;
601 else
602 re += c;
603 }
604 return std::regex(re);
605}
606
607// integer arithmetic (+, -, *, parentheses) on non-negative literals
609{
610 public:
611 explicit Arithmetic(std::string text) : text_(std::move(text)) {}
612
613 bool evaluate(long long& value)
614 {
615 pos_ = 0;
616 if (!expr(value))
617 return false;
618 skip_space();
619 return pos_ == text_.size();
620 }
621
622 private:
623 void skip_space()
624 {
625 while (pos_ < text_.size() && std::isspace(static_cast<unsigned char>(text_[pos_]))) ++pos_;
626 }
627 bool accept(char c)
628 {
629 skip_space();
630 if (pos_ < text_.size() && text_[pos_] == c)
631 {
632 ++pos_;
633 return true;
634 }
635 return false;
636 }
637 bool expr(long long& value)
638 {
639 if (!term(value))
640 return false;
641 for (;;)
642 {
643 long long rhs;
644 if (accept('+'))
645 {
646 if (!term(rhs))
647 return false;
648 value += rhs;
649 }
650 else if (accept('-'))
651 {
652 if (!term(rhs))
653 return false;
654 value -= rhs;
655 }
656 else
657 return true;
658 }
659 }
660 bool term(long long& value)
661 {
662 if (!factor(value))
663 return false;
664 long long rhs;
665 while (accept('*'))
666 {
667 if (!factor(rhs))
668 return false;
669 value *= rhs;
670 }
671 return true;
672 }
673 bool factor(long long& value)
674 {
675 if (accept('('))
676 return expr(value) && accept(')');
677 skip_space();
678 std::size_t start = pos_;
679 while (pos_ < text_.size() && std::isdigit(static_cast<unsigned char>(text_[pos_]))) ++pos_;
680 if (pos_ == start || pos_ - start > 10)
681 return false;
682 value = std::stoll(text_.substr(start, pos_ - start));
683 return true;
684 }
685
686 std::string text_;
687 std::size_t pos_{0};
688};
689
692inline bool eval_numeric(const std::string& text, const std::vector<std::uint32_t>& instances,
693 std::set<std::uint32_t>& values, std::string& error)
694{
695 static const std::regex range("\\s*([0-9]+)\\s*-\\s*([0-9]+)\\s*");
696 static const std::string instance_var = "${instance}";
697 auto in_range = [](long long v) { return v >= 0 && v <= Group::maximum_valid_group; };
698
699 std::smatch m;
700 if (std::regex_match(text, m, range))
701 {
702 auto first = std::stoll(m[1]), last = std::stoll(m[2]);
703 if (!in_range(first) || !in_range(last) || last < first || last - first > 100000)
704 {
705 error = "'" + text + "' is not a valid range of group numbers";
706 return false;
707 }
708 for (auto v = first; v <= last; ++v) values.insert(v);
709 return true;
710 }
711
712 bool uses_instance = text.find(instance_var) != std::string::npos;
713 if (uses_instance && instances.empty())
714 {
715 error = "'" + text + "' uses ${instance}, but its platform has no instances:";
716 return false;
717 }
718 for (auto instance : uses_instance ? instances : std::vector<std::uint32_t>{0})
719 {
720 std::string expanded = text;
721 for (auto pos = expanded.find(instance_var); pos != std::string::npos;
722 pos = expanded.find(instance_var, pos))
723 expanded.replace(pos, instance_var.size(), std::to_string(instance));
724 long long value;
725 if (!Arithmetic(expanded).evaluate(value) || !in_range(value))
726 {
727 error = "'" + text + "' is not a group number, a range (a-b) or arithmetic on " +
728 "${instance}";
729 return false;
730 }
731 values.insert(value);
732 }
733 return true;
734}
735
736inline std::vector<std::shared_ptr<Bind>> read_binds(const YAML::Node& node,
737 const std::string& location, Issues& issues)
738{
739 std::vector<std::shared_ptr<Bind>> binds;
740 if (!node)
741 return binds;
742 if (!node.IsSequence())
743 {
744 add_issue(issues, Issue::Severity::ERROR, "deployment-schema", "bind: must be a sequence",
745 location);
746 return binds;
747 }
748
749 int index = 0;
750 for (const auto& entry : node)
751 {
752 auto bind = std::make_shared<Bind>();
753 bind->location = location + "[" + std::to_string(index++) + "]";
754 auto fail = [&](const std::string& msg)
755 { add_issue(issues, Issue::Severity::ERROR, "deployment-schema", msg, bind->location); };
756
757 if (!entry.IsMap())
758 {
759 fail("a bind must be a map");
760 continue;
761 }
762 static const std::set<std::string> keys{"group", "numeric", "layer",
763 "dynamic", "publishes", "subscribes"};
764 bool ok = true;
765 for (const auto& kv : entry)
766 {
767 if (!keys.count(kv.first.as<std::string>()))
768 {
769 fail("unknown key '" + kv.first.as<std::string>() + "'");
770 ok = false;
771 }
772 }
773 if (!ok)
774 continue;
775
776 if (entry["dynamic"])
777 {
778 bind->is_publication = true;
779 auto select = entry["dynamic"];
780 if (!select.IsMap())
781 {
782 fail("dynamic: selects a dynamic publication or subscription by layer, scheme "
783 "and type");
784 continue;
785 }
786 if (select["layer"])
787 {
788 bind->has_layer = layer_from_name(select["layer"].as<std::string>(), bind->layer);
789 if (!bind->has_layer)
790 {
791 fail("unknown layer '" + select["layer"].as<std::string>() + "'");
792 continue;
793 }
794 }
795 if (select["scheme"])
796 bind->select_scheme = select["scheme"].as<std::string>();
797 if (select["type"])
798 bind->select_type = select["type"].as<std::string>();
799
800 for (auto kind : {"publishes", "subscribes"})
801 {
802 auto& concrete =
803 std::string(kind) == "publishes" ? bind->publishes : bind->subscribes;
804 for (const auto& c : entry[kind])
805 {
806 if (!c.IsMap() || !c["group"])
807 {
808 fail(std::string(kind) + " entries need group: (and type:, scheme: if "
809 "they differ from the dynamic entry's)");
810 ok = false;
811 continue;
812 }
813 concrete.push_back({c["group"].as<std::string>(),
814 c["scheme"] ? c["scheme"].as<std::string>() : "",
815 c["type"] ? c["type"].as<std::string>() : ""});
816 }
817 }
818 if (!ok)
819 continue;
820 if (bind->publishes.empty() && bind->subscribes.empty())
821 {
822 fail("a dynamic: bind needs publishes: or subscribes:");
823 continue;
824 }
825 }
826 else
827 {
828 if (!entry["group"] || !entry["numeric"])
829 {
830 fail("a bind needs group: and numeric:, or dynamic: with publishes: or "
831 "subscribes:");
832 continue;
833 }
834 bind->group_pattern = glob_regex(entry["group"].as<std::string>());
835 bind->numerics = read_list(entry["numeric"]);
836 if (entry["layer"])
837 {
838 bind->has_layer = layer_from_name(entry["layer"].as<std::string>(), bind->layer);
839 if (!bind->has_layer)
840 {
841 fail("unknown layer '" + entry["layer"].as<std::string>() + "'");
842 continue;
843 }
844 }
845 for (const auto& numeric : bind->numerics)
846 {
847 std::set<std::uint32_t> values;
848 std::string error;
849 if (!eval_numeric(numeric, {1}, values, error))
850 {
851 fail(error);
852 ok = false;
853 }
854 }
855 if (!ok)
856 continue;
857 }
858 binds.push_back(bind);
859 }
860 return binds;
861}
862
863inline Application::Binder make_binder(std::vector<std::shared_ptr<Bind>> binds,
864 std::vector<std::uint32_t> instances)
865{
866 return [binds, instances](PubSubEntry e) -> std::vector<PubSubEntry>
867 {
868 if (!e.dynamic)
869 return {e};
870
871 for (const auto& bind : binds)
872 {
873 if (bind->has_layer && bind->layer != e.layer)
874 continue;
875
876 if (!bind->is_publication)
877 {
878 if (!e.numeric_runtime || !std::regex_match(e.group, bind->group_pattern))
879 continue;
880 for (const auto& numeric : bind->numerics)
881 {
882 std::string error;
883 eval_numeric(numeric, instances, e.bound_numerics, error);
884 }
885 ++bind->uses;
886 return {e};
887 }
888
889 if (!e.unresolved() && !e.numeric_runtime)
890 continue;
891 if ((!bind->select_scheme.empty() && bind->select_scheme != e.scheme) ||
892 (!bind->select_type.empty() && bind->select_type != e.type))
893 continue;
894 const auto& concrete =
895 e.direction == PubSubEntry::Direction::PUBLISH ? bind->publishes : bind->subscribes;
896 if (concrete.empty())
897 continue;
898
899 ++bind->uses;
900 std::vector<PubSubEntry> bound;
901 for (const auto& c : concrete)
902 {
903 PubSubEntry b = e;
904 b.group = c.group;
905 if (!c.scheme.empty())
906 b.scheme = c.scheme;
907 if (!c.type.empty())
908 b.type = c.type;
909 b.numeric_runtime = false;
910 b.expr.clear();
911 bound.push_back(b);
912 }
913 return bound;
914 }
915 return {e};
916 };
917}
918
919inline bool valid_kill(const std::string& kill)
920{
921 return kill == "goby_terminate" || kill == "SIGTERM" || kill == "SIGINT" || kill == "SIGKILL";
922}
923
924inline KeyValues read_env(const YAML::Node& node, const std::string& location, Issues& issues)
925{
926 KeyValues env;
927 if (!node)
928 return env;
929 if (!node.IsMap())
930 {
931 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
932 "env: must be a map of NAME: value", location);
933 return env;
934 }
935 for (const auto& kv : node)
936 {
937 auto value = kv.second.as<std::string>();
938 // goby_launch splits its [directives] on spaces, commas and semicolons
939 if (value.find_first_of(" ,;") != std::string::npos)
940 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
941 "env: " + kv.first.as<std::string>() +
942 " contains a space, comma or semicolon, which goby_launch cannot pass",
943 location);
944 env.emplace_back(kv.first.as<std::string>(), value);
945 }
946 return env;
947}
948
949inline SystemdSettings read_systemd_settings(const YAML::Node& node, const std::string& location,
950 Issues& issues);
951
952// the launch keys of a run: entry
953inline bool read_launch(const YAML::Node& entry, RunRef& ref, Issues& issues)
954{
955 auto& launch = ref.launch;
956 launch.location = ref.location;
957 auto text = [&](const char* key, std::optional<std::string>& value)
958 {
959 if (entry[key])
960 value = entry[key].as<std::string>();
961 };
962
963 if (entry["process"])
964 {
965 if (entry["application"] || entry["file"] || entry["plugins"] || entry["bind"])
966 {
967 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
968 "a process: entry is not a Goby application, so it has no application:, "
969 "file:, plugins: or bind:",
970 ref.location);
971 return false;
972 }
973 launch.process = true;
974 launch.name = entry["process"].as<std::string>();
975 }
976 text("exec", launch.exec);
977 text("config", launch.config);
978 text("args", launch.args);
979 text("kill", launch.kill);
980 text("when", launch.when);
981 launch.env = read_env(entry["env"], ref.location + ".env", issues);
982 launch.systemd = read_systemd_settings(entry["systemd"], ref.location + ".systemd", issues);
983 if (entry["enabled"])
984 launch.enabled = entry["enabled"].as<bool>();
985 if (launch.kill && !valid_kill(*launch.kill))
986 {
987 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
988 "kill: must be goby_terminate, SIGTERM, SIGINT or SIGKILL", ref.location);
989 return false;
990 }
991 if (entry["hold"])
992 launch.hold = read_list(entry["hold"]);
993 if (auto expected = entry["expected"])
994 {
995 if (expected.IsScalar() && (expected.as<std::string>() == "false"))
996 launch.not_expected = true;
997 else
998 launch.expected = read_list(expected);
999 }
1000 return true;
1001}
1002
1003inline LaunchSettings read_launch_settings(const YAML::Node& node, const std::string& location,
1004 Issues& issues)
1005{
1006 LaunchSettings settings;
1007 if (!node)
1008 return settings;
1009 if (!node.IsMap())
1010 {
1011 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1012 "launch: must be a map (flags:, defaults:)", location);
1013 return settings;
1014 }
1015 for (const auto& kv : node)
1016 {
1017 auto key = kv.first.as<std::string>();
1018 if (key != "flags" && key != "defaults")
1019 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1020 "unknown key '" + key + "'", location);
1021 }
1022 if (node["flags"])
1023 settings.flags = node["flags"].as<std::string>();
1024 if (auto defaults = node["defaults"])
1025 {
1026 for (const auto& kv : defaults)
1027 {
1028 auto key = kv.first.as<std::string>();
1029 if (key == "config")
1030 settings.config = kv.second.as<std::string>();
1031 else if (key == "args")
1032 settings.args = kv.second.as<std::string>();
1033 else if (key == "kill")
1034 settings.kill = kv.second.as<std::string>();
1035 else if (key == "env")
1036 settings.env = read_env(kv.second, location + ".defaults.env", issues);
1037 else
1038 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1039 "unknown key '" + key + "' (defaults: takes config, args, kill, env)",
1040 location + ".defaults");
1041 }
1042 if (settings.kill && !valid_kill(*settings.kill))
1043 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1044 "kill: must be goby_terminate, SIGTERM, SIGINT or SIGKILL",
1045 location + ".defaults");
1046 }
1047 return settings;
1048}
1049
1050inline std::string with_instance(std::string text, const std::vector<std::uint32_t>& instances)
1051{
1052 if (instances.size() != 1)
1053 return text;
1054 static const std::string var = "${instance}";
1055 for (auto pos = text.find(var); pos != std::string::npos; pos = text.find(var, pos))
1056 text.replace(pos, var.size(), std::to_string(instances.front()));
1057 return text;
1058}
1059
1061 const std::vector<std::uint32_t>& instances)
1062{
1063 settings.flags = with_instance(settings.flags, instances);
1064 for (auto* field : {&settings.config, &settings.args})
1065 {
1066 if (*field)
1067 **field = with_instance(**field, instances);
1068 }
1069 for (auto& kv : settings.env) kv.second = with_instance(kv.second, instances);
1070 return settings;
1071}
1072
1073inline SystemdSettings read_systemd_settings(const YAML::Node& node, const std::string& location,
1074 Issues& issues)
1075{
1076 SystemdSettings settings;
1077 if (!node)
1078 return settings;
1079 if (!node.IsMap())
1080 {
1081 add_issue(issues, Issue::Severity::ERROR, "deployment-schema", "systemd: must be a map",
1082 location);
1083 return settings;
1084 }
1085 for (const auto& kv : node)
1086 {
1087 auto key = kv.first.as<std::string>();
1088 auto text = [&]() { return kv.second.as<std::string>(); };
1089 if (key == "template")
1090 settings.unit_template = text();
1091 else if (key == "user")
1092 settings.user = text();
1093 else if (key == "group")
1094 settings.group = text();
1095 else if (key == "wanted_by")
1096 settings.wanted_by = text();
1097 else if (key == "description")
1098 settings.description = text();
1099 else if (key == "condition")
1100 settings.condition = text();
1101 else if (key == "unit")
1102 settings.unit = text();
1103 else if (key == "service")
1104 settings.service = text();
1105 else if (key == "install")
1106 settings.install = text();
1107 else if (key == "skip")
1108 settings.skip = kv.second.as<bool>();
1109 else if (key == "vars" && kv.second.IsMap())
1110 {
1111 for (const auto& var : kv.second)
1112 settings.vars[var.first.as<std::string>()] = var.second.as<std::string>();
1113 }
1114 else
1115 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1116 "unknown key '" + key + "'", location);
1117 }
1118 return settings;
1119}
1120
1121inline PlatformSystemd read_platform_systemd(const YAML::Node& node, const std::string& location,
1122 Issues& issues)
1123{
1124 PlatformSystemd systemd;
1125 if (!node)
1126 return systemd;
1127 if (!node.IsMap())
1128 {
1129 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1130 "systemd: must be a map (target:, ordered:, defaults:)", location);
1131 return systemd;
1132 }
1133 for (const auto& kv : node)
1134 {
1135 auto key = kv.first.as<std::string>();
1136 if (key == "target")
1137 systemd.target = kv.second.as<std::string>();
1138 else if (key == "ordered")
1139 systemd.ordered = kv.second.as<bool>();
1140 else if (key == "defaults")
1141 systemd.defaults = read_systemd_settings(kv.second, location + ".defaults", issues);
1142 else
1143 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1144 "unknown key '" + key + "'", location);
1145 }
1146 return systemd;
1147}
1148
1150 const std::vector<std::uint32_t>& instances)
1151{
1152 for (auto* field : {&settings.unit_template, &settings.user, &settings.group,
1153 &settings.wanted_by, &settings.description, &settings.condition,
1154 &settings.unit, &settings.service, &settings.install})
1155 {
1156 if (*field)
1157 **field = with_instance(**field, instances);
1158 }
1159 for (auto& kv : settings.vars) kv.second = with_instance(kv.second, instances);
1160 return settings;
1161}
1162
1163inline LaunchEntry for_instance(LaunchEntry entry, const std::vector<std::uint32_t>& instances)
1164{
1165 entry.systemd = for_instance(entry.systemd, instances);
1166 for (auto* field : {&entry.exec, &entry.config, &entry.args, &entry.when})
1167 {
1168 if (*field)
1169 **field = with_instance(**field, instances);
1170 }
1171 for (auto& kv : entry.env) kv.second = with_instance(kv.second, instances);
1172 return entry;
1173}
1174
1175inline std::vector<RunRef> read_run(const YAML::Node& node, bool legacy,
1176 const std::string& location, Issues& issues)
1177{
1178 std::vector<RunRef> refs;
1179 if (!node.IsSequence())
1180 {
1181 add_issue(issues, Issue::Severity::ERROR, "deployment-schema", "must be a sequence",
1182 location);
1183 return refs;
1184 }
1185
1186 int index = 0;
1187 for (const auto& entry : node)
1188 {
1189 RunRef ref;
1190 ref.location = location + "[" + std::to_string(index++) + "]";
1191 ref.launch.location = ref.location;
1192 if (entry.IsScalar())
1193 {
1194 (legacy ? ref.file : ref.application) = entry.as<std::string>();
1195 }
1196 else if (entry.IsMap())
1197 {
1198 static const std::set<std::string> legacy_keys{"file", "application", "bind"};
1199 static const std::set<std::string> run_keys{
1200 "application", "file", "name", "plugins", "bind", "process",
1201 "exec", "config", "args", "env", "kill", "when",
1202 "enabled", "hold", "expected", "systemd"};
1203 const auto& allowed = legacy ? legacy_keys : run_keys;
1204 bool bad_key = false;
1205 for (const auto& kv : entry)
1206 {
1207 auto key = kv.first.as<std::string>();
1208 if (!allowed.count(key))
1209 {
1210 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1211 "unknown key '" + key + "'", ref.location);
1212 bad_key = true;
1213 }
1214 }
1215 if (bad_key)
1216 continue;
1217
1218 if (entry["file"])
1219 ref.file = entry["file"].as<std::string>();
1220 ref.binds = read_binds(entry["bind"], ref.location + ".bind", issues);
1221 if (legacy)
1222 {
1223 if (entry["application"])
1224 {
1225 ref.name = entry["application"].as<std::string>();
1226 ref.merge_intended = true;
1227 }
1228 }
1229 else
1230 {
1231 if (entry["application"])
1232 ref.application = entry["application"].as<std::string>();
1233 if (entry["name"])
1234 ref.name = entry["name"].as<std::string>();
1235 ref.plugins = read_list(entry["plugins"]);
1236 if (!read_launch(entry, ref, issues))
1237 continue;
1238 }
1239 if (ref.file.empty() && ref.application.empty() && !ref.launch.process)
1240 {
1241 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1242 legacy ? "an interfaces entry that is a map must have file:"
1243 : "a run entry must name an application:, a file: or a process:",
1244 ref.location);
1245 continue;
1246 }
1247 }
1248 else
1249 {
1250 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1251 "must be an application name or a map", ref.location);
1252 continue;
1253 }
1254 refs.push_back(ref);
1255 }
1256 return refs;
1257}
1258
1261inline bool read_applications(const YAML::Node& node, const std::string& location,
1262 std::vector<RunRef>& refs, Issues& issues)
1263{
1264 if (node["run"] && node["interfaces"])
1265 {
1266 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1267 "use run: or interfaces:, not both", location);
1268 return false;
1269 }
1270 if (node["run"])
1271 refs = read_run(node["run"], false, location + ".run", issues);
1272 else if (node["interfaces"])
1273 refs = read_run(node["interfaces"], true, location + ".interfaces", issues);
1274 else
1275 return false;
1276 return true;
1277}
1278
1281inline std::string declared_application(const std::string& path)
1282{
1283 try
1284 {
1285 auto yaml = YAML::LoadFile(path);
1286 if (yaml.IsMap() && yaml["application"] && yaml["application"].IsScalar())
1287 return yaml["application"].as<std::string>();
1288 }
1289 catch (const std::exception&)
1290 {
1291 }
1292 return "";
1293}
1294
1297{
1298 public:
1299 InterfaceFinder(std::vector<std::string> dirs) : dirs_(std::move(dirs)) {}
1300
1301 const std::vector<std::string>& dirs() const { return dirs_; }
1302
1303 std::string find_file(const std::string& file) const
1304 {
1305 namespace fs = std::filesystem;
1306 if (fs::path(file).is_absolute() || fs::exists(file))
1307 return fs::exists(file) ? file : "";
1308 for (const auto& dir : dirs_)
1309 {
1310 auto candidate = fs::path(dir) / file;
1311 if (fs::exists(candidate))
1312 return candidate.string();
1313 }
1314 return "";
1315 }
1316
1317 std::string find_application(const std::string& application, Issues& issues)
1318 {
1319 if (!indexed_)
1320 index();
1321 auto it = index_.find(application);
1322 if (it == index_.end())
1323 return "";
1324 const auto& files = it->second;
1325 if (files.size() > 1 && reported_shadowed_.insert(application).second)
1326 {
1327 std::string others;
1328 for (auto f = files.begin() + 1; f != files.end(); ++f)
1329 others += (others.empty() ? "" : ", ") + *f;
1330 add_issue(issues, Issue::Severity::INFO, "interface-shadowed",
1331 "application " + application + " is declared by " + files.front() +
1332 ", which is used, and by " + others);
1333 }
1334 return files.front();
1335 }
1336
1337 std::string searched() const
1338 {
1339 std::string out;
1340 for (const auto& dir : dirs_) out += (out.empty() ? "" : ", ") + dir;
1341 return out;
1342 }
1343
1344 private:
1345 void index()
1346 {
1347 namespace fs = std::filesystem;
1348 indexed_ = true;
1349 for (const auto& dir : dirs_)
1350 {
1351 std::error_code ec;
1352 if (!fs::is_directory(dir, ec))
1353 continue;
1354 std::vector<fs::path> files;
1355 for (fs::directory_iterator it(dir, ec), end; !ec && it != end; it.increment(ec))
1356 {
1357 auto ext = it->path().extension().string();
1358 if (ext == ".yml" || ext == ".yaml")
1359 files.push_back(it->path());
1360 }
1361 std::sort(files.begin(), files.end());
1362 for (const auto& file : files)
1363 {
1364 std::string application = declared_application(file.string());
1365 if (application.empty())
1366 continue;
1367 index_[application].push_back(file.string());
1368 }
1369 }
1370 }
1371
1372 std::vector<std::string> dirs_;
1373 bool indexed_{false};
1374 // application -> files declaring it, in search order
1375 std::map<std::string, std::vector<std::string>> index_;
1376 std::set<std::string> reported_shadowed_;
1377};
1378
1381inline void apply_necessity_overrides(const YAML::Node& node, Deployment& deployment,
1382 Issues& issues)
1383{
1384 if (!node)
1385 return;
1386 if (!node.IsSequence())
1387 {
1388 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1389 "necessity: must be a sequence", "necessity");
1390 return;
1391 }
1392
1393 static const std::vector<std::string> selectors{"platform", "module", "application", "thread",
1394 "layer", "group", "type"};
1395 int index = 0;
1396 for (const auto& entry : node)
1397 {
1398 std::string location = "necessity[" + std::to_string(index++) + "]";
1399 if (!entry.IsMap() || !entry["set"])
1400 {
1401 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1402 "a necessity override needs set: required|recommended|optional", location);
1403 continue;
1404 }
1405 auto set_name = entry["set"].as<std::string>();
1406 if (set_name != "required" && set_name != "recommended" && set_name != "optional")
1407 {
1408 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1409 "set: must be required, recommended or optional", location);
1410 continue;
1411 }
1412 std::map<std::string, std::regex> select;
1413 bool ok = true;
1414 for (const auto& kv : entry)
1415 {
1416 auto key = kv.first.as<std::string>();
1417 if (key == "set")
1418 continue;
1419 if (std::find(selectors.begin(), selectors.end(), key) == selectors.end())
1420 {
1421 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1422 "unknown key '" + key + "'", location);
1423 ok = false;
1424 continue;
1425 }
1426 try
1427 {
1428 select[key] = std::regex(kv.second.as<std::string>());
1429 }
1430 catch (const std::regex_error& e)
1431 {
1432 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1433 key + ": is not a valid regex: " + e.what(), location);
1434 ok = false;
1435 }
1436 }
1437 if (!ok)
1438 continue;
1439
1440 auto matches = [&](const std::string& key, const std::string& value)
1441 {
1442 auto it = select.find(key);
1443 return it == select.end() || std::regex_match(value, it->second);
1444 };
1445
1446 int uses = 0;
1447 auto apply = [&](std::set<PubSubEntry>& subs, const Platform& platform,
1448 const Module& module, const Application& app)
1449 {
1450 if (!(matches("platform", platform.name) || matches("platform", platform.base_name)) ||
1451 !matches("module", module.name) || !matches("application", app.name))
1452 return;
1453 std::set<PubSubEntry> updated;
1454 for (auto e : subs)
1455 {
1456 if (matches("thread", e.thread) && matches("layer", layer_name(e.layer)) &&
1457 matches("group", e.group) && matches("type", e.type))
1458 {
1459 e.necessity = necessity_from_name(set_name);
1460 e.necessity_overridden = true;
1461 ++uses;
1462 }
1463 updated.insert(e);
1464 }
1465 subs = updated;
1466 };
1467
1468 for (auto& platform : deployment.platforms)
1469 {
1470 for (auto& module : platform.modules)
1471 {
1472 for (auto& app_p : module.applications)
1473 {
1474 auto& app = app_p.second;
1475 for (auto& thread_p : app.threads)
1476 apply(thread_p.second->interthread_subscribes, platform, module, app);
1477 for (auto& layer_p : app.subscribes)
1478 apply(layer_p.second, platform, module, app);
1479 }
1480 }
1481 }
1482 if (uses == 0)
1483 add_issue(issues, Issue::Severity::WARNING, "necessity-unused",
1484 "this override matches no subscription", location);
1485 }
1486}
1487
1488inline std::vector<std::string> search_dirs(const std::string& deployment_path,
1489 const LoadParams& params)
1490{
1491 namespace fs = std::filesystem;
1492 std::vector<std::string> dirs;
1493 auto deployment_dir = fs::path(deployment_path).parent_path().string();
1494 dirs.push_back(deployment_dir.empty() ? "." : deployment_dir);
1495 dirs.insert(dirs.end(), params.interface_dirs.begin(), params.interface_dirs.end());
1496 if (const char* env = std::getenv("GOBY_INTERFACE_PATH"))
1497 {
1498 std::stringstream ss(env);
1499 for (std::string dir; std::getline(ss, dir, ':');)
1500 if (!dir.empty())
1501 dirs.push_back(dir);
1502 }
1503 if (!params.goby_interfaces_dir.empty())
1504 dirs.push_back(params.goby_interfaces_dir);
1505 dirs.push_back(".");
1506
1507 std::vector<std::string> unique;
1508 std::set<std::string> seen;
1509 for (const auto& dir : dirs)
1510 {
1511 std::error_code ec;
1512 auto canonical = fs::weakly_canonical(dir, ec).string();
1513 if (seen.insert(ec ? dir : canonical).second)
1514 unique.push_back(dir);
1515 }
1516 return unique;
1517}
1518
1519} // namespace detail
1520
1524inline Deployment load_deployment(const std::string& path, Issues& issues,
1525 const LoadParams& params = LoadParams())
1526{
1527 Deployment deployment;
1528 deployment.file = path;
1529
1530 auto defines = params.defines;
1531 if (!params.goby_interfaces_dir.empty() && !defines.count("GOBY_INTERFACES_DIR"))
1532 defines["GOBY_INTERFACES_DIR"] = params.goby_interfaces_dir;
1533
1534 std::ifstream in(path);
1535 if (!in.is_open())
1536 {
1537 add_issue(issues, Issue::Severity::ERROR, "deployment-missing", "cannot open " + path);
1538 return deployment;
1539 }
1540 std::stringstream raw;
1541 raw << in.rdbuf();
1542 std::set<std::string> unset;
1543 YAML::Node deploy_yaml;
1544 if (!detail::load_yaml_text(detail::expand_variables(raw.str(), defines, unset), path,
1545 "deployment", "", issues, deploy_yaml))
1546 return deployment;
1547 for (const auto& name : unset)
1548 add_issue(issues, Issue::Severity::INFO, "variable-unset",
1549 "${" + name +
1550 "} has no value here, so it is left for the shell (set it with -D " + name +
1551 "=value)");
1552
1553 detail::InterfaceFinder finder(detail::search_dirs(path, params));
1554 std::map<std::string, detail::PlatformRef> platform_refs;
1555
1556 if (!deploy_yaml.IsMap() || !deploy_yaml["deployment"])
1557 {
1558 if (deploy_yaml.IsMap() && deploy_yaml["application"])
1559 {
1560 deployment.name = deploy_yaml["application"].as<std::string>() + "_stub_deployment";
1561 detail::RunRef ref;
1562 ref.file = path;
1563 ref.location = "application";
1564 platform_refs["goby_platform"].modules.push_back({"nomodule", {ref}, true});
1565 }
1566 else
1567 {
1568 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1569 "must specify 'deployment: name' in a deployment file, or give a single "
1570 "interface file");
1571 return deployment;
1572 }
1573 }
1574 else
1575 {
1576 deployment.name = deploy_yaml["deployment"].as<std::string>();
1577
1578 auto platforms_node = deploy_yaml["platforms"];
1579 if (!platforms_node || !platforms_node.IsSequence())
1580 {
1581 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1582 "platforms: must be a sequence", "platforms");
1583 return deployment;
1584 }
1585
1586 int platform_index = 0;
1587 for (const auto& platform : platforms_node)
1588 {
1589 std::string location = "platforms[" + std::to_string(platform_index++) + "]";
1590 if (!platform["name"])
1591 {
1592 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1593 "each platform must have name:", location);
1594 continue;
1595 }
1596 auto platform_name = platform["name"].as<std::string>();
1597 auto& platform_ref = platform_refs[platform_name];
1598 platform_ref.location = location;
1599 platform_ref.binds = detail::read_binds(platform["bind"], location + ".bind", issues);
1600 platform_ref.launch_settings =
1601 detail::read_launch_settings(platform["launch"], location + ".launch", issues);
1602 platform_ref.systemd =
1603 detail::read_platform_systemd(platform["systemd"], location + ".systemd", issues);
1604 if (auto instances_node = platform["instances"])
1605 {
1606 for (const auto& instance : detail::read_list(instances_node))
1607 {
1608 std::set<std::uint32_t> values;
1609 std::string error;
1610 if (!detail::eval_numeric(instance, {}, values, error))
1611 add_issue(issues, Issue::Severity::ERROR, "deployment-schema", error,
1612 location + ".instances");
1613 platform_ref.instances.insert(platform_ref.instances.end(), values.begin(),
1614 values.end());
1615 }
1616 }
1617
1618 if (auto modules_node = platform["modules"])
1619 {
1620 int module_index = 0;
1621 for (const auto& module : modules_node)
1622 {
1623 std::string module_location =
1624 location + ".modules[" + std::to_string(module_index++) + "]";
1625 detail::ModuleRef module_ref;
1626 if (!module["name"] ||
1627 !detail::read_applications(module, module_location, module_ref.run, issues))
1628 {
1629 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1630 "each module must have name: and a run: (or interfaces:) list",
1631 module_location);
1632 continue;
1633 }
1634 module_ref.name = module["name"].as<std::string>();
1635 platform_ref.modules.push_back(module_ref);
1636 }
1637 }
1638 else
1639 {
1640 detail::ModuleRef module_ref{"nomodule", {}, true};
1641 if (!detail::read_applications(platform, location, module_ref.run, issues))
1642 {
1643 add_issue(issues, Issue::Severity::ERROR, "deployment-schema",
1644 "a platform must have modules:, or a run: (or interfaces:) list",
1645 location);
1646 continue;
1647 }
1648 platform_ref.modules = {module_ref};
1649 }
1650 }
1651 }
1652
1653 auto locate = [&](const detail::RunRef& ref, const std::string& application,
1654 const std::string& file) -> std::string
1655 {
1656 if (!file.empty())
1657 {
1658 auto found = finder.find_file(file);
1659 if (found.empty())
1660 add_issue(issues, Issue::Severity::ERROR, "interface-missing",
1661 "cannot find " + file + " (searched: " + finder.searched() + ")",
1662 ref.location);
1663 return found;
1664 }
1665 auto found = finder.find_application(application, issues);
1666 if (found.empty())
1667 add_issue(issues, Issue::Severity::ERROR, "interface-missing",
1668 "no interface file declares application " + application +
1669 " (searched: " + finder.searched() + ")",
1670 ref.location);
1671 return found;
1672 };
1673
1674 auto load_interface = [&](const std::string& file, const detail::RunRef& ref,
1675 YAML::Node& yaml) -> bool
1676 {
1677 if (!detail::load_yaml(file, "interface", ref.location, issues, yaml))
1678 return false;
1679 if (!yaml.IsMap() || !yaml["application"] || !yaml["application"].IsScalar())
1680 {
1681 add_issue(issues, Issue::Severity::ERROR, "interface-invalid",
1682 file + " has no application:", ref.location);
1683 return false;
1684 }
1685 return true;
1686 };
1687
1688 auto merge = [&](Application& app, const YAML::Node& yaml, const std::string& file,
1689 const detail::RunRef& ref, const Application::Binder& binder)
1690 {
1691 try
1692 {
1693 app.merge(yaml, deployment.indexer, binder);
1694 }
1695 catch (const std::exception& e)
1696 {
1697 add_issue(issues, Issue::Severity::ERROR, "interface-invalid", file + ": " + e.what(),
1698 ref.location);
1699 }
1700 };
1701
1702 auto load_platform = [&](const std::string& name, const detail::PlatformRef& platform_ref,
1703 const std::vector<std::uint32_t>& instances, Platform& platform)
1704 {
1705 platform.name = name;
1706 platform.launch_settings = detail::for_instance(platform_ref.launch_settings, instances);
1707 platform.systemd = platform_ref.systemd;
1708 platform.systemd.defaults = detail::for_instance(platform_ref.systemd.defaults, instances);
1709 for (const auto& module_ref : platform_ref.modules)
1710 {
1711 Module module;
1712 module.name = module_ref.name;
1713 module.not_a_module = module_ref.not_a_module;
1714 for (const auto& ref : module_ref.run)
1715 {
1716 auto launch = detail::for_instance(ref.launch, instances);
1717 launch.module = module.name;
1718 if (launch.process)
1719 {
1720 platform.launch.push_back(launch);
1721 continue;
1722 }
1723
1724 auto file = locate(ref, ref.application, ref.file);
1725 YAML::Node yaml;
1726 if (file.empty() || !load_interface(file, ref, yaml))
1727 {
1728 launch.application = ref.application;
1729 launch.name = ref.name.empty() ? ref.application : ref.name;
1730 if (launch.name.empty())
1731 launch.name = std::filesystem::path(ref.file).stem().string();
1732 platform.launch.push_back(launch);
1733 continue;
1734 }
1735
1736 std::string app_name =
1737 ref.name.empty() ? yaml["application"].as<std::string>() : ref.name;
1738 launch.application = yaml["application"].as<std::string>();
1739 launch.name = app_name;
1740 // a second entry for the same application (a plugin's interface file) is not a
1741 // second process
1742 bool already_launched = std::any_of(
1743 platform.launch.begin(), platform.launch.end(), [&](const LaunchEntry& e)
1744 { return e.module == launch.module && e.name == launch.name; });
1745 if (!already_launched)
1746 platform.launch.push_back(launch);
1747
1748 auto it = module.applications.find(app_name);
1749 if (it == module.applications.end())
1750 {
1751 it = module.applications.emplace(app_name, Application(app_name)).first;
1752 }
1753 else if (!ref.merge_intended)
1754 {
1755 add_issue(issues, Issue::Severity::WARNING, "application-duplicate",
1756 "application " + app_name + " is listed more than once on " +
1757 platform.name + (module.not_a_module ? "" : "/" + module.name) +
1758 "; the entries were merged (give each a distinct name:, or "
1759 "list plugins under plugins:)",
1760 ref.location);
1761 }
1762 auto binds = ref.binds;
1763 binds.insert(binds.end(), platform_ref.binds.begin(), platform_ref.binds.end());
1764 auto binder = detail::make_binder(binds, instances);
1765 merge(it->second, yaml, file, ref, binder);
1766
1767 for (const auto& plugin : ref.plugins)
1768 {
1769 bool is_file = std::filesystem::path(plugin).extension() == ".yml" ||
1770 std::filesystem::path(plugin).extension() == ".yaml";
1771 auto plugin_file = locate(ref, is_file ? "" : plugin, is_file ? plugin : "");
1772 YAML::Node plugin_yaml;
1773 if (!plugin_file.empty() && load_interface(plugin_file, ref, plugin_yaml))
1774 merge(it->second, plugin_yaml, plugin_file, ref, binder);
1775 }
1776 }
1777 platform.modules.push_back(std::move(module));
1778 }
1779 std::stable_sort(platform.modules.begin(), platform.modules.end(),
1780 [](const Module& a, const Module& b) { return a.name < b.name; });
1781 };
1782
1783 for (const auto& platform_p : platform_refs)
1784 {
1785 const auto& ref = platform_p.second;
1786 if (ref.instances.empty() || !params.expand_instances)
1787 {
1788 Platform platform;
1789 platform.base_name = platform_p.first;
1790 load_platform(platform_p.first, ref, ref.instances, platform);
1791 deployment.platforms.push_back(std::move(platform));
1792 }
1793 else
1794 {
1795 for (auto instance : ref.instances)
1796 {
1797 Platform platform;
1798 platform.base_name = platform_p.first;
1799 load_platform(platform_p.first + std::to_string(instance), ref, {instance},
1800 platform);
1801 deployment.platforms.push_back(std::move(platform));
1802 }
1803 }
1804 }
1805 std::stable_sort(deployment.platforms.begin(), deployment.platforms.end(),
1806 [](const Platform& a, const Platform& b) { return a.name < b.name; });
1807
1808 detail::apply_necessity_overrides(deploy_yaml["necessity"], deployment, issues);
1809
1810 std::vector<std::shared_ptr<detail::Bind>> all_binds;
1811 for (const auto& platform_p : platform_refs)
1812 {
1813 const auto& ref = platform_p.second;
1814 all_binds.insert(all_binds.end(), ref.binds.begin(), ref.binds.end());
1815 for (const auto& module_ref : ref.modules)
1816 for (const auto& run_ref : module_ref.run)
1817 all_binds.insert(all_binds.end(), run_ref.binds.begin(), run_ref.binds.end());
1818 }
1819 for (const auto& bind : all_binds)
1820 {
1821 if (bind->uses == 0)
1822 add_issue(issues, Issue::Severity::WARNING, "bind-unused",
1823 "this bind matches no dynamic publication or subscription", bind->location);
1824 }
1825
1826 return deployment;
1827}
1828
1829} // namespace interface
1830} // namespace tool
1831} // namespace middleware
1832} // namespace goby
1833
1834#endif
static constexpr std::uint32_t maximum_valid_group
Definition group.h:67
Hands out the per-layer publication numbers (P0, P1, ...) shown in diagrams.
Definition model.h:119
Finds interface files by path, or by the application: they declare.
Definition model.h:1297
std::string find_application(const std::string &application, Issues &issues)
Definition model.h:1317
std::string find_file(const std::string &file) const
Definition model.h:1303
InterfaceFinder(std::vector< std::string > dirs)
Definition model.h:1299
const std::vector< std::string > & dirs() const
Definition model.h:1301
detail namespace with internal helper functions
Definition json.hpp:263
LaunchSettings read_launch_settings(const YAML::Node &node, const std::string &location, Issues &issues)
Definition model.h:1003
bool load_yaml(const std::string &path, const std::string &code, const std::string &location, Issues &issues, YAML::Node &yaml)
Definition model.h:552
std::vector< std::string > read_list(const YAML::Node &node)
Definition model.h:567
std::string declared_application(const std::string &path)
The application an interface file declares, or "" if it is not one (generated interface files start w...
Definition model.h:1281
std::vector< std::shared_ptr< Bind > > read_binds(const YAML::Node &node, const std::string &location, Issues &issues)
Definition model.h:736
bool layer_from_name(const std::string &name, Layer &layer)
Definition model.h:577
bool read_applications(const YAML::Node &node, const std::string &location, std::vector< RunRef > &refs, Issues &issues)
Reads the application list of a platform or module: run: (by application name) or the older interface...
Definition model.h:1261
const Application * application(const LaunchUnit &unit, const LaunchEntry &e)
Definition launch.h:92
std::string expand_variables(const std::string &text, const std::map< std::string, std::string > &defines, std::set< std::string > &unset)
Replace ${NAME} and ${NAME:-default} from defines, then the environment.
Definition model.h:475
std::string with_instance(std::string text, const std::vector< std::uint32_t > &instances)
Definition model.h:1050
std::vector< std::string > search_dirs(const std::string &deployment_path, const LoadParams &params)
Definition model.h:1488
Application::Binder make_binder(std::vector< std::shared_ptr< Bind > > binds, std::vector< std::uint32_t > instances)
Definition model.h:863
void apply_necessity_overrides(const YAML::Node &node, Deployment &deployment, Issues &issues)
Apply a deployment's necessity: list, which sets the necessity of the subscriptions it selects whatev...
Definition model.h:1381
KeyValues read_env(const YAML::Node &node, const std::string &location, Issues &issues)
Definition model.h:924
LaunchSettings for_instance(LaunchSettings settings, const std::vector< std::uint32_t > &instances)
Definition model.h:1060
PlatformSystemd read_platform_systemd(const YAML::Node &node, const std::string &location, Issues &issues)
Definition model.h:1121
SystemdSettings read_systemd_settings(const YAML::Node &node, const std::string &location, Issues &issues)
Definition model.h:1073
bool valid_kill(const std::string &kill)
Definition model.h:919
std::regex glob_regex(const std::string &glob)
Definition model.h:592
bool read_launch(const YAML::Node &entry, RunRef &ref, Issues &issues)
Definition model.h:953
bool eval_numeric(const std::string &text, const std::vector< std::uint32_t > &instances, std::set< std::uint32_t > &values, std::string &error)
Evaluate a bind's numeric: "5", "151-153", or arithmetic with ${instance}, once per instance given.
Definition model.h:692
std::vector< RunRef > read_run(const YAML::Node &node, bool legacy, const std::string &location, Issues &issues)
Definition model.h:1175
bool load_yaml_text(const std::string &text, const std::string &path, const std::string &code, const std::string &location, Issues &issues, YAML::Node &yaml)
Definition model.h:535
std::string layer_name(Layer layer)
std::vector< Issue > Issues
Definition model.h:89
std::vector< std::pair< std::string, std::string > > KeyValues
Definition model.h:303
Deployment load_deployment(const std::string &path, Issues &issues, const LoadParams &params=LoadParams())
Load a deployment file, or a single interface file as a one-platform stub deployment.
Definition model.h:1524
goby::middleware::Necessity necessity_from_name(const std::string &s)
void add_issue(Issues &issues, Issue::Severity severity, std::string code, std::string message, std::string location="")
Definition model.h:91
bool has_errors(const Issues &issues)
Definition model.h:111
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
extern ::PROTOBUF_NAMESPACE_ID::internal::ExtensionIdentifier< ::PROTOBUF_NAMESPACE_ID::MessageOptions, ::PROTOBUF_NAMESPACE_ID::internal::MessageTypeTraits< ::goby::GobyMessageOptions >, 11, false > msg
STL namespace.
std::function< std::vector< PubSubEntry >(PubSubEntry)> Binder
Turns one entry from an interface file into the entries a deployment sees (a dynamic group with its b...
Definition model.h:176
std::map< Layer, std::set< PubSubEntry > > publishes
Definition model.h:157
void merge(const YAML::Node &yaml, PublishIndexer &indexer, const Binder &binder=Binder())
Add the contents of one interface file (a root node with application: and the layers)
Definition model.h:200
const std::set< PubSubEntry > & subscribes_on(Layer layer) const
Definition model.h:167
std::map< std::string, std::shared_ptr< Thread > > threads
Definition model.h:155
const std::set< PubSubEntry > & publishes_on(Layer layer) const
Definition model.h:160
std::map< Layer, std::set< PubSubEntry > > subscribes
Definition model.h:158
The publication or subscription an issue is about, when there is one.
Definition model.h:69
A problem found while loading or checking a deployment.
Definition model.h:59
How one run: entry is started by goby_launch.
Definition model.h:326
std::optional< std::string > args
Definition model.h:335
std::optional< std::vector< std::string > > hold
Definition model.h:342
std::optional< std::string > exec
Definition model.h:333
std::optional< std::string > config
Definition model.h:334
std::optional< std::string > kill
Definition model.h:336
std::optional< std::vector< std::string > > expected
Definition model.h:344
std::optional< std::string > when
Definition model.h:338
A platform's launch: settings.
Definition model.h:353
std::optional< std::string > config
Definition model.h:356
std::optional< std::string > kill
Definition model.h:358
std::optional< std::string > args
Definition model.h:357
Where to find interface files and the values of ${NAME} in a deployment file.
Definition model.h:398
std::vector< std::string > interface_dirs
Definition model.h:400
std::map< std::string, std::string > defines
Definition model.h:404
std::map< std::string, Application > applications
Definition model.h:298
A platform's systemd: settings.
Definition model.h:364
std::optional< std::string > target
Definition model.h:366
std::vector< LaunchEntry > launch
Definition model.h:379
One publication or subscription as written to (and read from) an interface file.
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
std::string most_derived_name() const
Definition model.h:136
Thread(std::string n, bool k, std::set< std::string > b=std::set< std::string >())
Definition model.h:129
std::set< PubSubEntry > interthread_publishes
Definition model.h:146
std::set< PubSubEntry > interthread_subscribes
Definition model.h:147
std::set< std::string > bases
Definition model.h:140
One bind: entry of a platform or of an application in its run: list.
Definition model.h:414
std::vector< std::string > numerics
Definition model.h:420
std::vector< std::shared_ptr< Bind > > binds
Definition model.h:466
std::vector< std::shared_ptr< Bind > > binds
Definition model.h:451