chore(phase0): name validation, golden frames, daemon into library tree, TSan daemon lane
Item 8a — ConfigValidator warns on non-identifier variable/event/alarm/ command names ([A-Za-z_][A-Za-z0-9_]*): language bindings expose names as kwargs/attributes, so 'Chamber Pressure' would be unusable in the planned Python client. Warning not error — the wire doesn't care. Tested (4 warning sites + good-name negative). Item 4 tail — golden frames for S5F1 (Binary ALCD / U4 ALID / ASCII ALTX) and a composed S6F11 (the production-critical report shape), bytes hand- computed from E5 encoding rules: external pins on message composition. Item 7 — equipment_service.hpp moved to include/secsgem/daemon/ (apps/ include-path hack removed) and a TSan daemon lane added locally + in CI. tools/tsan.supp suppresses races whose accesses sit entirely inside the UNinstrumented system libgrpc/libgpr/libabsl (epoll wakeups, absl Mutex GraphCycles bookkeeping); our frames stay fully checked. The lane earned its keep on first run: it caught a REAL threading-contract violation — a daemon test reading model stores from the test thread while the io thread serviced posted writes — fixed to use read_sync, exactly per the documented contract. Now TSan-clean under halt_on_error=1 in the full production threading shape. Suites: core 470/3068, daemon Release+TSan 125/125 each. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+30
-3
@@ -3,6 +3,7 @@
|
||||
#include <yaml-cpp/yaml.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <set>
|
||||
#include <string>
|
||||
@@ -117,6 +118,26 @@ std::optional<std::string> as_nonempty_string(const YAML::Node& n,
|
||||
return s;
|
||||
}
|
||||
|
||||
// API-facing names (variables, events, alarms, commands) should be valid
|
||||
// identifiers: language bindings expose them as kwargs/attributes
|
||||
// (eq.set(chamber_pressure=...)), where "Chamber Pressure" or "temp-2" can't
|
||||
// be written. Warning, not error — the wire doesn't care, only bindings do.
|
||||
void warn_if_not_identifier(const std::optional<std::string>& s, Sink& sink,
|
||||
const std::string& path, const YAML::Node& n) {
|
||||
if (!s) return;
|
||||
bool ok = !s->empty() &&
|
||||
(std::isalpha(static_cast<unsigned char>((*s)[0])) || (*s)[0] == '_');
|
||||
for (std::size_t i = 1; ok && i < s->size(); ++i) {
|
||||
const auto c = static_cast<unsigned char>((*s)[i]);
|
||||
ok = std::isalnum(c) || (*s)[i] == '_';
|
||||
}
|
||||
if (!ok)
|
||||
sink.warn(path, "`" + *s + "` is not identifier-safe " +
|
||||
"([A-Za-z_][A-Za-z0-9_]*); language bindings expose " +
|
||||
"names as kwargs/attributes",
|
||||
n);
|
||||
}
|
||||
|
||||
// Validate one entry that has the same (id, name, units, type, value)
|
||||
// shape as SVID/DVID/ECID. Returns the parsed id on success so the
|
||||
// caller can dedupe.
|
||||
@@ -124,7 +145,8 @@ std::optional<uint32_t> validate_typed_entry(
|
||||
const YAML::Node& entry, Sink& sink, const std::string& path) {
|
||||
auto id = as_int_in_range<uint32_t>(entry["id"], sink, path + ".id",
|
||||
0, UINT32_MAX);
|
||||
as_nonempty_string(entry["name"], sink, path + ".name");
|
||||
warn_if_not_identifier(as_nonempty_string(entry["name"], sink, path + ".name"),
|
||||
sink, path + ".name", entry["name"]);
|
||||
if (auto tn = entry["type"]) {
|
||||
auto t = tn.as<std::string>();
|
||||
if (!valid_secs_type(t)) {
|
||||
@@ -195,7 +217,9 @@ void validate_equipment_block(YAML::Node& root, Sink& sink) {
|
||||
const auto path = "ceids[" + std::to_string(i) + "]";
|
||||
auto id = as_int_in_range<uint32_t>(ces[i]["id"], sink, path + ".id",
|
||||
0, UINT32_MAX);
|
||||
as_nonempty_string(ces[i]["name"], sink, path + ".name");
|
||||
warn_if_not_identifier(
|
||||
as_nonempty_string(ces[i]["name"], sink, path + ".name"),
|
||||
sink, path + ".name", ces[i]["name"]);
|
||||
if (id && !ceids.insert(*id).second) {
|
||||
sink.error(path + ".id",
|
||||
"duplicate CEID " + std::to_string(*id),
|
||||
@@ -221,7 +245,9 @@ void validate_equipment_block(YAML::Node& root, Sink& sink) {
|
||||
// Optional local key for name-based APIs (not on the wire). If
|
||||
// present it must be non-empty.
|
||||
if (als[i]["name"])
|
||||
as_nonempty_string(als[i]["name"], sink, path + ".name");
|
||||
warn_if_not_identifier(
|
||||
as_nonempty_string(als[i]["name"], sink, path + ".name"),
|
||||
sink, path + ".name", als[i]["name"]);
|
||||
if (id && !alarm_ids.insert(*id).second) {
|
||||
sink.error(path + ".id",
|
||||
"duplicate ALID " + std::to_string(*id), als[i]);
|
||||
@@ -256,6 +282,7 @@ void validate_equipment_block(YAML::Node& root, Sink& sink) {
|
||||
for (std::size_t i = 0; i < cmds.size(); ++i) {
|
||||
const auto path = "host_commands[" + std::to_string(i) + "]";
|
||||
auto name = as_nonempty_string(cmds[i]["name"], sink, path + ".name");
|
||||
warn_if_not_identifier(name, sink, path + ".name", cmds[i]["name"]);
|
||||
if (auto ack = cmds[i]["ack"]) {
|
||||
auto s = ack.as<std::string>();
|
||||
if (!valid_hcack(s))
|
||||
|
||||
Reference in New Issue
Block a user