Skip to main content

nros_platform/board/
runtime.rs

1//! [`RuntimeCtx`] — Phase 212.N.1.
2//!
3//! Per-invocation runtime context handed to `BoardEntry::run`'s
4//! `setup` callback. Carries the overlay knobs the codegen
5//! `run_plan(runtime)` body reads:
6//!
7//! - **params** — `(key, value)` pairs from launch XML
8//!   `<param name="…" value="…"/>`, projected here by `nros sync` at BUILD
9//!   time. NOT from `--ros-args -p k:=v`: a generated entry reads no argv,
10//!   and the hosted `nros::init_with_args` parse refuses `-p` rather than
11//!   pretending to honour it (runtime parameters are RFC-0015 §9's channel).
12//! - **remaps** — `(from, to)` topic/service renames, from launch
13//!   `<remap from= to=/>` by the same build-time projection.
14//! - **env** — environment-style key/value pairs (POSIX `getenv`
15//!   shape) accessible from no_std boards via this struct rather
16//!   than a `libc::getenv` call.
17//! - **runtime** — `&mut dyn NodeDispatchRuntime` sink the
18//!   codegen-emitted `run_plan(runtime)` body forwards each Node
19//!   pkg's `register(runtime)` call into (Phase 212.N.7 step-3.2).
20//!   Populated by each `BoardEntry::run` impl after opening its
21//!   executor; defaults to a no-op sink when constructed via
22//!   [`RuntimeCtx::with_runtime`].
23//!
24//! ## no_std-safe shape
25//!
26//! Slice-of-tuples kept on the boot stack. No allocation, no
27//! `core::collections`. Codegen owns the storage and passes a
28//! `&mut RuntimeCtx<'_>` whose backing slices live in `static`s.
29//!
30//! Hosted boards (POSIX) may instead build a longer-lived owned
31//! variant on the heap; the trait surface is slice-based so
32//! both shapes work.
33
34use super::dispatch::DispatchStrategy;
35
36/// Layer-clean substitute for `nros::node_metadata::CallbackId` +
37/// `nros::node::CallbackCtx` at the [`NodeDispatchRuntime`] boundary
38/// (Phase 216.A.2). `nros-platform` sits below `nros` in the dep
39/// graph, so the trait surface cannot reference those types
40/// directly. The `nros`-side runtime impl wraps a real
41/// `(CallbackId, &mut CallbackCtx)` pair into this opaque shape
42/// before invoking [`NodeDispatchRuntime::signal_callback`]; the
43/// concrete dispatcher casts `ctx_ptr` back to
44/// `&mut nros::CallbackCtx<'_>` at the call site.
45///
46/// `#[repr(C)]` keeps the layout stable across the (same-language
47/// today, FFI-shaped tomorrow) `nros-platform` ↔ `nros` boundary.
48#[repr(C)]
49pub struct SignaledCallback<'a> {
50    /// Stable identifier string carried by `nros::CallbackId(&'a str)`.
51    pub cb_id: &'a str,
52
53    /// Erased pointer to the `nros::CallbackCtx<'_>` the dispatcher
54    /// will drive. The `nros`-side `NodeDispatchRuntime` impl casts
55    /// back to `&mut nros::CallbackCtx<'_>` before invoking the
56    /// component body.
57    pub ctx_ptr: *mut core::ffi::c_void,
58}
59
60// Phase 258 (Track 2, w5) — the opaque per-Node `extern "Rust" fn()` aliases
61// (~~`NodeRegisterFn`~~ / ~~`NodeInitFn`~~ / ~~`NodeDispatchFn`~~ / ~~`NodeTickFn`~~) are gone.
62// They anchored the retired ~~`register_dispatch_slot_dyn`~~ four-fn-ptr bridge
63// (owned-spin declarative register), which the install seam replaced — Rust
64// owned-spin now registers via `RuntimeCtx::runtime.executor_handle()` +
65// `nros::__macro::install_node_typed` like the C/C++ typed entries.
66
67/// Node runtime sink the codegen-emitted `run_plan(runtime)`
68/// body talks to (Phase 212.N.7 step-3.1).
69///
70/// Object-safe + `no_std`. The concrete impl
71/// (`ExecutorNodeRuntime` in `nros`) owns the live executor;
72/// `BoardEntry::run` installs it on the per-boot
73/// [`RuntimeCtx::runtime`] slot before invoking the user `setup`
74/// closure.
75///
76/// Phase 214.K.1 — renamed from `NodeRuntime` to disambiguate from
77/// the user-facing `nros::NodeRuntime` metadata-sink trait in
78/// `packages/api/nros/src/node.rs:112`. The two traits live at
79/// different layers (board-side dispatch sink vs user-side metadata
80/// declaration sink) and the previous shared name forced explicit
81/// `nros_platform::` / `nros::` qualification at every use site +
82/// produced confusing `impl NodeRuntime for X` ambiguity. The
83/// phase-214.K.1 `NodeRuntime` deprecation alias was removed in phase-313
84/// W1 (issue #0243); use `NodeDispatchRuntime` directly.
85pub trait NodeDispatchRuntime {
86    /// Drive the underlying executor for at most `timeout_ms`
87    /// milliseconds. `Ok(())` on a clean spin (including timeout);
88    /// `Err(())` if the executor surfaces a spin error.
89    ///
90    /// `Result<_, ()>` is deliberate: the board entry-point callers
91    /// (`nros-board-{freertos,nuttx,threadx}` spin loops) only
92    /// `{:?}`-print the error and `B::exit_failure()` — a typed enum
93    /// would carry no extra info across the trait boundary, since the
94    /// underlying `ExecutorError` from `nros::node_runtime` is mapped
95    /// to `()` at the impl site (`impl NodeDispatchRuntime for
96    /// ExecutorNodeRuntime`). `#[allow]` keeps the surface narrow.
97    #[allow(clippy::result_unit_err)]
98    fn spin_once(&mut self, timeout_ms: u32) -> Result<(), ()>;
99
100    /// phase-521 W6 (RFC-0113 D2) — the budget an entry loop passes to
101    /// [`spin_once`](Self::spin_once). The executor-backed runtime answers
102    /// with how long its park can safely run (until its next deadline when
103    /// every source of work can end it, else a 10 ms slice). The default is
104    /// that slice, for a runtime that cannot say.
105    fn entry_spin_budget_ms(&self) -> u32 {
106        10
107    }
108
109    /// Phase 264 W2 — register the REP-2002 lifecycle services on the underlying
110    /// executor + drive boot autostart. `autostart`: 0 = none, 1 = configure,
111    /// 2 = configure+activate. Default no-op (non-executor runtimes, or `nros`
112    /// built without `lifecycle-services`); the `ExecutorNodeRuntime` impl in
113    /// `nros` does the real work behind that feature. `nros::main!` calls this
114    /// (via [`RuntimeCtx::apply_lifecycle`]) when `system.toml` declares
115    /// `[lifecycle]`, mirroring the bake's `generate.rs::render_lifecycle_fn`.
116    /// issue 0460 — the `Err` carries a STATIC REASON, not `()`.
117    ///
118    /// A capability that fails to register is reported by the caller as
119    /// `RuntimeError::NodeRegister("lifecycle")`, which names WHICH capability
120    /// and nothing about WHY. On Zephyr that opaque string was the entire
121    /// diagnostic for three dead entries: the image printed nothing after
122    /// "Network ready" and the cause could only be guessed at. `&'static str`
123    /// rather than a typed error because this trait lives below `nros-node` and
124    /// cannot name `NodeError`.
125    fn apply_lifecycle(&mut self, autostart: u8) -> Result<(), &'static str> {
126        let _ = autostart;
127        Ok(())
128    }
129
130    /// Phase 264 W4b -- register the 6 ROS 2 parameter services on the underlying
131    /// executor and create its volatile RAM param store. Launch `<param>`
132    /// initials are NOT passed here (issue 1272): each node's are seeded on that
133    /// node as it is built, from the `params` slice `nros::main!` sets before
134    /// the node's `register` call. After this,
135    /// `ros2 param list/get/set` works against the running node; reconfigured values
136    /// live in RAM until the next boot (RFC-0004 §10; persistence is out of scope,
137    /// issue 0080). Default no-op (non-executor runtimes, or `nros` built without
138    /// `param-services`); the `ExecutorNodeRuntime` impl in `nros` does the real work
139    /// behind that feature. `nros::main!` calls this (via
140    /// [`RuntimeCtx::apply_param_services`]) when `system.toml` declares
141    /// `[param_services]`, mirroring the bake's `generate.rs::render_param_persistence_fn`.
142    /// issue 0460 — the `Err` carries a STATIC REASON, not `()`.
143    ///
144    /// A capability that fails to register is reported by the caller as
145    /// `RuntimeError::NodeRegister("lifecycle")`, which names WHICH capability
146    /// and nothing about WHY. On Zephyr that opaque string was the entire
147    /// diagnostic for three dead entries: the image printed nothing after
148    /// "Network ready" and the cause could only be guessed at. `&'static str`
149    /// rather than a typed error because this trait lives below `nros-node` and
150    /// cannot name `NodeError`.
151    fn apply_param_services(&mut self) -> Result<(), &'static str> {
152        Ok(())
153    }
154
155    /// phase-461 W6 -- build the parameter STORE and register no service: the
156    /// `params` capability axis. `nros::main!` calls this (via
157    /// [`RuntimeCtx::apply_param_store`]) before the per-node `register` calls
158    /// when the system declares `params` and not `param_services`. Default
159    /// no-op (non-executor runtimes, or `nros` built without `param-store`,
160    /// which the entry const-asserts against).
161    fn apply_param_store(&mut self) -> Result<(), &'static str> {
162        Ok(())
163    }
164
165    /// Phase 258 (Track 2, 2a) — raw `*mut Executor` (as `void*`) for the
166    /// owned-spin entry, so a Node pkg's `register(runtime)` wrapper can call
167    /// the uniform `__nros_component_<pkg>_install(.., executor, ..)` seam
168    /// (`nros::__macro::install_node_typed`) instead of the retired opaque-fn-ptr
169    /// ~~`register_dispatch_slot_dyn`~~ bridge. A pointer crosses the
170    /// `nros-platform` → `nros` layering wall cleanly (the concrete
171    /// `ExecutorNodeRuntime` lives in `nros`; this trait can't name it).
172    ///
173    /// Default `null` — sinks without a live executor (e.g.
174    /// [`NullNodeRuntime`], framework-dispatch-only runtimes) report no
175    /// handle; the install path treats null as a registration error.
176    fn executor_handle(&mut self) -> *mut core::ffi::c_void {
177        core::ptr::null_mut()
178    }
179
180    /// phase-446 W6 -- hand the executor the parameters each node's contract
181    /// DECLARES, so a node whose code declares a parameter its contract does
182    /// not (or with another type) refuses the boot naming the node, the
183    /// parameter and the contract.
184    ///
185    /// `nodes` is `(node FQN, contract)`; `params` is `(node FQN, name,
186    /// rcl_interfaces type code)`. Plain tuples because this trait lives below
187    /// `nros-params` and cannot name its types. `nros::main!` calls this before
188    /// any node registers. Default no-op: a runtime with no parameter store
189    /// has nothing to check.
190    fn apply_declared_params(
191        &mut self,
192        nodes: &'static [(&'static str, &'static str)],
193        params: &'static [(&'static str, &'static str, u8)],
194    ) -> Result<(), &'static str> {
195        let _ = (nodes, params);
196        Ok(())
197    }
198
199    /// Observability counters from hosted/runtime tests.
200    ///
201    /// Returns `(all_callbacks, message_callbacks)`. Implementations
202    /// that cannot observe callback dispatch keep the default zeros.
203    fn observed_callback_counts(&self) -> (usize, usize) {
204        (0, 0)
205    }
206
207    /// Hand a signaled callback to the framework-side dispatcher
208    /// (Phase 216.A.2). Only meaningful for `DispatchStrategy::Deferred`
209    /// (RTIC) runtimes — `Inline` runtimes drive callbacks
210    /// directly from `spin_once` and never call this. The default panic
211    /// surfaces the mis-wire loudly rather than silently dropping the
212    /// callback signal.
213    fn signal_callback(&mut self, _cb: SignaledCallback<'_>) {
214        panic!("signal_callback not implemented for Inline runtime");
215    }
216
217    /// Declare how this runtime delivers callbacks (Phase 216.A.2).
218    /// `nros check` (Phase 216.D.1) cross-validates each Node pkg's
219    /// `Node::DISPATCH` against this value. Defaults to `Inline` so
220    /// every existing impl reports the historical behavior unchanged.
221    fn dispatch_strategy(&self) -> DispatchStrategy {
222        DispatchStrategy::Inline
223    }
224}
225
226/// No-op [`NodeDispatchRuntime`] for tests / placeholders. Every call
227/// returns `Err(())` so callers that depend on a populated runtime
228/// fail loud rather than silently no-op.
229///
230/// `BoardEntry::run` impls replace this with a real
231/// `ExecutorNodeRuntime`-backed sink before invoking the user
232/// `setup` closure.
233#[derive(Debug, Default)]
234pub struct NullNodeRuntime;
235
236impl NodeDispatchRuntime for NullNodeRuntime {
237    fn spin_once(&mut self, _timeout_ms: u32) -> Result<(), ()> {
238        Err(())
239    }
240}
241
242/// Runtime context handed to `BoardEntry::run(setup)`.
243///
244/// All three overlay slices may be empty. A board's launch overlay
245/// typically populates `params` + `remaps`; `env` is rarely set on
246/// embedded.
247pub struct RuntimeCtx<'a> {
248    /// `<param name=… value=…/>` from launch XML, baked here by
249    /// `nros::main!` from the resolved model at BUILD time.
250    ///
251    /// NOT `-p name:=value` CLI overrides: a generated entry reads no argv,
252    /// and the hosted `nros::init_with_args` parse refuses `-p` loudly, so
253    /// launch is the only producer of these.
254    pub params: &'a [(&'a str, &'a str)],
255
256    /// Topic / service / action remaps: `(from, to)`, set per component by
257    /// `nros::main!` from the model's launch `<remap>` rules (phase-306 W3,
258    /// issue 0255). `nros::node!`'s `register` forwards them into
259    /// `install_node_typed_with_launch`, where entity creation expands
260    /// `~`/relative names and substitutes matching rules (exact-FQN match,
261    /// first rule wins).
262    ///
263    /// These are AUTHORITATIVE rules: `ExecutorSink::create_entity` passes
264    /// them as `nros_node::names::resolve_name_layered`'s authoritative tier,
265    /// beneath which the executor's `--ros-args` rules (installed by a hosted
266    /// `nros::Context`) can only apply to a name the launch projected no rule
267    /// for.
268    pub remaps: &'a [(&'a str, &'a str)],
269
270    /// Environment-style key/value pairs (mostly POSIX). Empty on
271    /// embedded boards.
272    pub env: &'a [(&'a str, &'a str)],
273
274    /// Phase 268 W1 — launch-injected node identity `(name, namespace)` for
275    /// the NEXT `register()` call, set by `nros::main!` per component before
276    /// each `<pkg>::register(runtime)?` call. `None` → the node's own
277    /// `create_node(NodeOptions::new("…"))` default stands (backward-compatible).
278    /// Reset to `None` after every register in the self-bringup arm so a prior
279    /// component's identity never leaks into the next (RFC-0046).
280    pub node_identity: Option<(&'static str, &'static str)>,
281
282    /// Issue #52 — per-node QoS overrides for the NEXT `register()` call, baked
283    /// by `nros::main!` from the model's
284    /// `qos_overrides.<topic>.<role>.<policy>` params. Same reset discipline as
285    /// `params`/`remaps`: `&[]` when the node has none, so a prior component's
286    /// table never leaks into the next.
287    ///
288    /// Primitive `(topic, role, policy, value)` codes — the SAME wire form the
289    /// C and C++ ABIs use — because `nros-platform` sits below `nros-rmw` in the
290    /// layer graph and a typed `QoSOverride` field here would invert it. The
291    /// register seam installs them via `Executor::set_node_qos_overrides`, which
292    /// decodes at entity-create time.
293    pub qos_overrides: &'static [(&'static str, u8, u8, u32)],
294
295    /// Node runtime sink. `BoardEntry::run` populates this with
296    /// the live `ExecutorNodeRuntime`-backed impl before invoking
297    /// the user `setup` closure. The codegen-emitted
298    /// `run_plan(runtime)` body calls `<pkg>::register(runtime)` once per Node
299    /// pkg, which installs through `runtime.executor_handle()` +
300    /// `nros::__macro::install_node_typed` (Phase 258, Track 2).
301    ///
302    /// Defaults to a [`NullNodeRuntime`] when the context is
303    /// built via [`RuntimeCtx::with_runtime`]. That sink errors
304    /// every call so test fixtures that forget to wire a real runtime
305    /// fail loud.
306    pub runtime: &'a mut dyn NodeDispatchRuntime,
307}
308
309impl core::fmt::Debug for RuntimeCtx<'_> {
310    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
311        f.debug_struct("RuntimeCtx")
312            .field("params", &self.params)
313            .field("remaps", &self.remaps)
314            .field("qos_overrides", &self.qos_overrides)
315            .field("env", &self.env)
316            .field("node_identity", &self.node_identity)
317            .field("runtime", &"<dyn NodeDispatchRuntime>")
318            .finish()
319    }
320}
321
322impl<'a> RuntimeCtx<'a> {
323    /// Build a [`RuntimeCtx`] with no params / remaps / env and the
324    /// given runtime sink. The common shape `BoardEntry::run`
325    /// constructs after opening its executor.
326    ///
327    /// For test fixtures that don't need a populated runtime, pass a
328    /// `&mut NullNodeRuntime` — every call against the sink
329    /// returns `Err(())`, surfacing the missing wiring.
330    pub fn with_runtime(runtime: &'a mut dyn NodeDispatchRuntime) -> Self {
331        Self {
332            params: &[],
333            remaps: &[],
334            env: &[],
335            node_identity: None,
336            qos_overrides: &[],
337            runtime,
338        }
339    }
340
341    /// Build a [`RuntimeCtx`] with explicit overlay slices + runtime
342    /// sink (Phase 212.N.7 step-3.2).
343    pub fn new(
344        runtime: &'a mut dyn NodeDispatchRuntime,
345        params: &'a [(&'a str, &'a str)],
346        remaps: &'a [(&'a str, &'a str)],
347        env: &'a [(&'a str, &'a str)],
348    ) -> Self {
349        Self {
350            params,
351            remaps,
352            env,
353            node_identity: None,
354            qos_overrides: &[],
355            runtime,
356        }
357    }
358
359    /// Phase 264 W2 — register lifecycle services + drive autostart on the runtime
360    /// sink (forwards to [`NodeDispatchRuntime::apply_lifecycle`]). `nros::main!`
361    /// emits this after the per-node `register` calls when `system.toml` declares
362    /// `[lifecycle]`. No-op unless `nros` is built with `lifecycle-services`.
363    pub fn apply_lifecycle(&mut self, autostart: u8) -> Result<(), &'static str> {
364        self.runtime.apply_lifecycle(autostart)
365    }
366
367    /// Phase 264 W4b -- register the ROS 2 parameter services and create the
368    /// volatile param store (forwards to
369    /// [`NodeDispatchRuntime::apply_param_services`]). `nros::main!` emits this
370    /// BEFORE the per-node `register` calls when `system.toml` declares
371    /// `[param_services]`, so the store exists when each node is built and that
372    /// node's launch `<param>` initials ([`RuntimeCtx::params`]) are seeded on it
373    /// (issue 1272). No-op unless `nros` is built with `param-services`.
374    pub fn apply_param_services(&mut self) -> Result<(), &'static str> {
375        self.runtime.apply_param_services()
376    }
377
378    /// phase-461 W6 -- build the parameter store without its services
379    /// (forwards to [`NodeDispatchRuntime::apply_param_store`]). Emitted where
380    /// [`Self::apply_param_services`] would be, for a system declaring
381    /// `params` alone. No-op unless `nros` is built with `param-store`.
382    pub fn apply_param_store(&mut self) -> Result<(), &'static str> {
383        self.runtime.apply_param_store()
384    }
385
386    /// Lookup a param by name; first match wins. Linear scan
387    /// because the slice is typically small (≤ a dozen entries).
388    pub fn param(&self, name: &str) -> Option<&'a str> {
389        self.params
390            .iter()
391            .find(|(k, _)| *k == name)
392            .map(|(_, v)| *v)
393    }
394
395    /// Lookup a remap by the original (`from`) name; returns the
396    /// rewritten name when remapped, else `None`.
397    pub fn remap(&self, from: &str) -> Option<&'a str> {
398        self.remaps
399            .iter()
400            .find(|(k, _)| *k == from)
401            .map(|(_, v)| *v)
402    }
403
404    /// Lookup an env entry by name.
405    pub fn env_var(&self, name: &str) -> Option<&'a str> {
406        self.env.iter().find(|(k, _)| *k == name).map(|(_, v)| *v)
407    }
408
409    /// phase-446 W6 -- the contract's declared parameters (forwards to
410    /// [`NodeDispatchRuntime::apply_declared_params`]). `nros::main!` emits
411    /// this before the per-node `register` calls.
412    pub fn apply_declared_params(
413        &mut self,
414        nodes: &'static [(&'static str, &'static str)],
415        params: &'static [(&'static str, &'static str, u8)],
416    ) -> Result<(), &'static str> {
417        self.runtime.apply_declared_params(nodes, params)
418    }
419}
420
421/// Error returned by the codegen-emitted `run_plan(runtime)` body
422/// (Phase 212.N.4) and by Node pkg `register(runtime)` wrappers
423/// (Phase 212.N.7 step-2).
424///
425/// `no_std`-safe — variants are string-typed so embedded Entry pkgs
426/// don't need to pull `thiserror`/`anyhow` to print. The
427/// out-of-tree `nros-build` codegen library re-exports this type so
428/// emitted code references `::nros_platform::RuntimeError`, NOT
429/// `::nros_build::RuntimeError` — the embedded Entry pkg's runtime
430/// path then doesn't need `nros-build` as a runtime dep (build-dep
431/// only).
432#[derive(Debug)]
433#[non_exhaustive]
434pub enum RuntimeError {
435    /// A node's `register(runtime)` call failed. The string carries the
436    /// node pkg name.
437    ///
438    /// Phase 212.N.12 hard-renamed the legacy `ComponentRegister` variant
439    /// to `NodeRegister` to match the rclcpp_components / ROS 2 launch.xml
440    /// `<node pkg=…>` convention.
441    NodeRegister(&'static str),
442
443    /// issue 0460 — a CAPABILITY (`[lifecycle]`, `[param_services]`) failed to
444    /// register, with the reason.
445    ///
446    /// Distinct from [`NodeRegister`](Self::NodeRegister) because reusing it
447    /// forced the capability name to be the whole message: three Zephyr entries
448    /// died reporting `NodeRegister("lifecycle")`, which says WHICH capability
449    /// and nothing about WHY, and the image printed nothing else. Two static
450    /// strings rather than a log call, because not every entry crate depends on
451    /// `log` — the first attempt emitted `::log::error!` from the macro and
452    /// broke `linux_rust_qos_entry` with `cannot find 'log' in the crate root`.
453    Capability {
454        /// `"lifecycle"` / `"param_services"`.
455        name: &'static str,
456        /// Why registration failed, from the runtime that attempted it.
457        reason: &'static str,
458    },
459
460    /// Issue 0095 — a node's `register` failed specifically because the
461    /// executor's fixed callback-entry table (`NROS_EXECUTOR_MAX_CBS`) is full.
462    /// Distinct from the opaque [`NodeRegister`](Self::NodeRegister) so the user
463    /// sees the actionable knob. The string carries the node pkg name.
464    ExecutorFull(&'static str),
465
466    /// The hosted Entry spin loop failed or did not observe the
467    /// requested runtime condition before its bounded test deadline.
468    Spin,
469
470    /// RFC-0065 D5 amendment — the image's `before_boot` step returned an
471    /// error, with its reason. Nothing else has run yet.
472    BeforeBoot(&'static str),
473}
474
475impl core::fmt::Display for RuntimeError {
476    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
477        match self {
478            Self::NodeRegister(msg) => write!(f, "node register failed: {msg}"),
479            Self::ExecutorFull(msg) => write!(
480                f,
481                "node '{msg}' register failed: executor callback table full — \
482                 raise NROS_EXECUTOR_MAX_CBS (build-time env, default 4)"
483            ),
484            Self::Capability { name, reason } => write!(
485                f,
486                "capability '[{name}]' failed to register: {reason} — the system \
487                 declares it, so the entry cannot run without it"
488            ),
489            Self::Spin => write!(f, "entry spin failed"),
490            Self::BeforeBoot(reason) => write!(f, "before_boot failed: {reason}"),
491        }
492    }
493}