STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1/**
2 * state_script_check — validate a dynamic-state script against a config, and exit 0 or 1.
3 *
4 * Hermetic. Opens no sockets, touches no boards, starts no services, reads exactly the two files
5 * it is given. It is in the same family as cppcheck, not in the family of things
6 * daq-server/CLAUDE.md forbids running.
7 *
8 * This exists so the config editor can say "line 4: unknown actuator" at the operator's desk,
9 * between sessions, when no sequencer is running. The web backend spawns it on save. It is early
10 * warning only — the sequencer parses the script itself at startup and is the authority on whether
11 * a state becomes enterable, so a stale checker binary degrades the editing experience and never
12 * the stand.
13 *
14 * state_script_check --config <config.toml> --state <"State Name"> [--transitions <csv>] <script>
15 *
16 * Output is one diagnostic per line: `<line>:<col>: <CODE> <message>`, which is what the backend
17 * parses back into the editor's gutter.
18 */
19
20#include <cstdio>
21#include <fstream>
22#include <iostream>
23#include <sstream>
24#include <string>
25#include <vector>
26
27#include "config/Config.hpp"
31
32namespace {
33
34int usage() {
35 std::cerr << "usage: state_script_check --config <config.toml> --state <name> "
36 "[--transitions <state_transitions.csv>] <script-file>\n";
37 return 2;
38}
39
40/**
41 * Sends anything written to std::cout to stderr for as long as it is alive.
42 *
43 * StateMachine logs its "Loaded 20 state(s)" banner on std::cout, which is fine in a service and
44 * ruinous here: stdout is this tool's machine-readable channel and the backend parses every line
45 * of it into the editor's gutter. Rather than change logging that other services rely on, the two
46 * calls that emit it are wrapped.
47 */
49public:
50 CoutToStderr() : saved_(std::cout.rdbuf(std::cerr.rdbuf())) {
51 }
53 std::cout.rdbuf(saved_);
54 }
55
56private:
57 std::streambuf* saved_;
58};
59
60bool readFile(const std::string& path, std::string& out) {
61 std::ifstream f(path, std::ios::binary);
62 if (!f.is_open())
63 return false;
64 std::ostringstream ss;
65 ss << f.rdbuf();
66 out = ss.str();
67 return true;
68}
69
70} // namespace
71
72int main(int argc, char** argv) {
73 std::string config_path, state_name, transitions_path, script_path;
74
75 for (int i = 1; i < argc; i++) {
76 const std::string a = argv[i];
77 auto next = [&](std::string& dst) {
78 if (i + 1 >= argc)
79 return false;
80 dst = argv[++i];
81 return true;
82 };
83 if (a == "--config") {
84 if (!next(config_path))
85 return usage();
86 } else if (a == "--state") {
87 if (!next(state_name))
88 return usage();
89 } else if (a == "--transitions") {
90 if (!next(transitions_path))
91 return usage();
92 } else if (a == "-h" || a == "--help") {
93 return usage();
94 } else if (!a.empty() && a[0] == '-') {
95 std::cerr << "unknown option " << a << "\n";
96 return usage();
97 } else {
98 script_path = a;
99 }
100 }
101 if (config_path.empty() || state_name.empty() || script_path.empty())
102 return usage();
103
104 std::string config_text;
105 if (!readFile(config_path, config_text)) {
106 std::cerr << "cannot read config: " << config_path << "\n";
107 return 2;
108 }
109 std::string script_text;
110 if (!readFile(script_path, script_text)) {
111 std::cerr << "cannot read script: " << script_path << "\n";
112 return 2;
113 }
114
115 const fsw::config::Config cfg = fsw::config::load_from_string(config_text);
116 if (cfg.states.empty()) {
117 // load_from_string swallows a TOML parse error and hands back a default Config, so "no
118 // states" is indistinguishable here from "the file did not parse". Either way the answer
119 // the operator needs is the same: this config cannot be checked against.
120 std::cerr << "config declares no [[states]] (or failed to parse): " << config_path << "\n";
121 return 2;
122 }
123
124 auto build = fsw::script::build_slug_tables(cfg);
125 for (const auto& b : build.bad)
126 std::cerr << "config: " << b.where << " \"" << b.canonical
127 << "\" has no usable slug (got \"" << b.slug << "\") — a script cannot name it\n";
128 for (const auto& c : build.collisions)
129 std::cerr << "config: " << c.where << "s \"" << c.first << "\" and \"" << c.second
130 << "\" both slug to " << c.slug << " — ambiguous\n";
131 if (!build.ok())
132 return 2;
133
134 // Which states this one may transition to. Without a transitions CSV every transition_to would
135 // be reported as not allowed, which would be a confidently wrong answer — so refuse instead.
137 const std::string csv =
138 transitions_path.empty() ? cfg.state_machine.transitions_csv : transitions_path;
139 bool csv_ok = false;
140 {
141 CoutToStderr quiet; // keep StateMachine's banner off the diagnostics channel
143 csv_ok = sm.load(csv);
144 }
145 if (!csv_ok) {
146 std::cerr << "cannot read state_transitions.csv: " << csv
147 << " (pass --transitions to point at it)\n";
148 return 2;
149 }
151 if (from == sequencer::State::UNKNOWN) {
152 std::cerr << "config declares no state named \"" << state_name << "\"\n";
153 return 2;
154 }
155 for (const auto& s : cfg.states) {
156 if (s.name.empty())
157 continue;
159 if (to != sequencer::State::UNKNOWN && sm.isAllowed(from, to))
160 build.tables.allowed_transitions.insert(fsw::script::slugify(s.name));
161 }
162
163 fsw::script::ParseResult parsed = fsw::script::parse(script_text);
164 std::vector<fsw::script::Diagnostic> issues = parsed.diagnostics;
165 if (issues.empty())
166 issues = fsw::script::validate(parsed.program, build.tables);
167
168 for (const auto& d : issues)
169 std::cout << d.line << ":" << d.col << ": " << fsw::script::diag_name(d.code) << " "
170 << d.message << "\n";
171
172 return issues.empty() ? 0 : 1;
173}
Parses state_transitions.csv and validates state transitions.
bool load(const std::string &csv_path)
Load from a state_transitions.csv file.
static State fromName(const std::string &name)
Translate a CSV name (e.g.
bool isAllowed(State from, State to) const
Returns true if transitioning from→to is allowed.
static void loadStatesFromConfig(const std::string &config_content)
Adopt the [[states]] entries from a config as the source of truth for state names and semantics.
*brief create JSON pointer *sa reference_tokens const string_t const string_t & b
Definition json.hpp:13878
*brief create JSON pointer *sa reference_tokens const string_t & a
Definition json.hpp:13877
bool readFile(const std::string &path, std::string &out)
Definition main.cpp:60
Config load_from_string(const std::string &text)
Parse config from an in-memory TOML string (SequencerService / ActuatorCommander hold it so).
Definition Config.cpp:458
const char * diag_name(Diag d)
Definition Parser.cpp:56
std::string slugify(const std::string &name)
Canonical name -> slug: trim, uppercase, collapse internal whitespace runs to a single '_'.
Definition Slug.cpp:8
ParseResult parse(const std::string &source)
Lex and parse, applying every check that does not need config.
Definition Parser.cpp:813
TableBuild build_slug_tables(const fsw::config::Config &cfg)
Build the actuator / sensor / state slug tables from a loaded config.
std::vector< Diagnostic > validate(const ScriptProgram &program, const SlugTables &tables)
Config-dependent checks.
Definition Validate.cpp:69
State
System state IDs — must match the TypeScript SystemState enum in web-gui/shared/types....
STL namespace.
The whole parsed config.toml, restricted to what C++ consumes.
Definition Config.hpp:264
std::vector< Diagnostic > diagnostics
int main()