mecha10_cli/services/process/
spawn_node.rs1use super::ProcessService;
9use crate::paths;
10use anyhow::{Context, Result};
11use mecha10_cli_core::utils::strip_node_scope;
12use mecha10_core::wire_identifier::split_wire_identifier;
13use std::collections::HashMap;
14
15impl ProcessService {
16 pub fn spawn_node_runner(
20 &mut self,
21 node_identifier: &str,
22 project_name: &str,
23 project_env: Option<HashMap<String, String>>,
24 ) -> Result<u32> {
25 let runner_path = Self::resolve_node_runner_path();
26
27 let env = project_env.unwrap_or_default();
28
29 let (base_identifier, instance_id, camera_id) = split_wire_identifier(node_identifier);
30
31 let args = node_runner_args(project_name, base_identifier, instance_id, camera_id);
32 let args: Vec<&str> = args.iter().map(String::as_str).collect();
33 self.spawn_with_env(node_identifier, &runner_path, &args, env)
34 }
35
36 pub fn spawn_node_direct(
41 &mut self,
42 node_identifier: &str,
43 _project_name: &str,
44 project_env: Option<HashMap<String, String>>,
45 ) -> Result<u32> {
46 let (base_identifier, instance_id, camera_id) = split_wire_identifier(node_identifier);
47 let node_name = strip_node_scope(base_identifier);
48
49 let is_local_node = self.is_local_project_node(node_name);
50
51 let mut env = HashMap::new();
52 env.insert("NODE_NAME".to_string(), base_identifier.to_string());
53 if let Some(instance_id) = instance_id {
54 env.insert("NODE_INSTANCE_ID".to_string(), instance_id.to_string());
55 }
56 if let Some(camera_id) = camera_id {
57 env.insert("MECHA10_CAMERA_ID".to_string(), camera_id.to_string());
58 }
59 if let Ok(rust_log) = std::env::var("RUST_LOG") {
60 env.insert("RUST_LOG".to_string(), rust_log);
61 }
62
63 if let Some(project_vars) = project_env {
64 tracing::debug!(
65 "🔀 spawn_node_direct for '{}': merging {} project vars",
66 node_name,
67 project_vars.len()
68 );
69 for (key, value) in &project_vars {
70 tracing::debug!(
71 " Adding: {} = {}",
72 key,
73 if key == "ROBOT_API_KEY" { "<redacted>" } else { value }
74 );
75 }
76 env.extend(project_vars);
77 } else {
78 tracing::warn!("⚠️ spawn_node_direct for '{}': No project_env provided!", node_name);
79 }
80
81 tracing::debug!(
82 "🏁 spawn_node_direct for '{}': final env has {} vars, is_local: {}",
83 node_name,
84 env.len(),
85 is_local_node
86 );
87
88 if is_local_node {
89 self.spawn_local_node_with_identifier(node_identifier, node_name, env)
90 } else {
91 self.spawn_framework_node(node_identifier, node_name, env)
92 }
93 }
94
95 fn spawn_framework_node(
98 &mut self,
99 node_identifier: &str,
100 node_name: &str,
101 env: HashMap<String, String>,
102 ) -> Result<u32> {
103 use super::super::binary_manager::BinaryManager;
104
105 let (base_identifier, _instance_id, _camera_id) = split_wire_identifier(node_identifier);
106 let version = self.get_node_version_from_config(base_identifier);
107
108 let binary_manager = BinaryManager::new()?;
109
110 let binary_path = tokio::task::block_in_place(|| {
111 tokio::runtime::Handle::current()
112 .block_on(binary_manager.resolve_with_version(node_name, version.as_deref()))
113 })?;
114
115 tracing::info!(
116 "🚀 Spawning framework node '{}' from {}",
117 node_name,
118 binary_path.display()
119 );
120
121 self.spawn_with_env(node_identifier, &binary_path.to_string_lossy(), &[], env)
122 }
123
124 fn spawn_local_node_with_identifier(
127 &mut self,
128 node_identifier: &str,
129 node_name: &str,
130 env: HashMap<String, String>,
131 ) -> Result<u32> {
132 use std::process::Command;
133
134 tracing::info!("🔨 Building local node: {}", node_name);
135
136 let build_output = Command::new("cargo")
137 .args(["build", "-p", node_name])
138 .output()
139 .context("Failed to run cargo build")?;
140
141 if !build_output.status.success() {
142 let stderr = String::from_utf8_lossy(&build_output.stderr);
143 anyhow::bail!("Failed to build node '{}': {}", node_name, stderr);
144 }
145
146 tracing::info!("✅ Built local node: {}", node_name);
147
148 let binary_path = paths::target_path("debug", node_name);
149
150 if !std::path::Path::new(&binary_path).exists() {
151 anyhow::bail!(
152 "Binary not found at '{}'. Build may have failed or the package name differs from node name.",
153 binary_path
154 );
155 }
156
157 self.spawn_with_env(node_identifier, &binary_path, &[], env)
158 }
159}
160
161fn node_runner_args(
166 project_name: &str,
167 base_identifier: &str,
168 instance_id: Option<&str>,
169 camera_id: Option<&str>,
170) -> Vec<String> {
171 let mut args = vec![
172 "--project".to_string(),
173 project_name.to_string(),
174 base_identifier.to_string(),
175 ];
176 if let Some(instance_id) = instance_id {
177 args.push("--instance-id".to_string());
178 args.push(instance_id.to_string());
179 }
180 if let Some(camera_id) = camera_id {
181 args.push("--camera-id".to_string());
182 args.push(camera_id.to_string());
183 }
184 args
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn node_runner_args_passes_the_scoped_identifier_through_unchanged() {
193 let args = node_runner_args("rover-robot", "@mecha10/simulator", None, None);
194 assert_eq!(args, ["--project", "rover-robot", "@mecha10/simulator"]);
195 }
196
197 #[test]
198 fn node_runner_args_leaves_unscoped_node_name_untouched() {
199 let args = node_runner_args("rover-robot", "my-node", None, None);
200 assert_eq!(args, ["--project", "rover-robot", "my-node"]);
201 }
202
203 #[test]
206 fn node_runner_args_appends_instance_id_flag_when_present() {
207 let args = node_runner_args("rover-robot", "@mecha10/camera", Some("real"), None);
208 assert_eq!(
209 args,
210 ["--project", "rover-robot", "@mecha10/camera", "--instance-id", "real"]
211 );
212 }
213
214 #[test]
216 fn node_runner_args_appends_camera_id_flag_when_present() {
217 let args = node_runner_args("rover-robot", "@mecha10/video-streamer", None, Some("camera"));
218 assert_eq!(
219 args,
220 [
221 "--project",
222 "rover-robot",
223 "@mecha10/video-streamer",
224 "--camera-id",
225 "camera"
226 ]
227 );
228 }
229
230 #[test]
231 fn node_runner_args_appends_both_flags_when_both_present() {
232 let args = node_runner_args(
233 "rover-robot",
234 "@mecha10/video-streamer",
235 Some("sim-streamer"),
236 Some("simulation_camera"),
237 );
238 assert_eq!(
239 args,
240 [
241 "--project",
242 "rover-robot",
243 "@mecha10/video-streamer",
244 "--instance-id",
245 "sim-streamer",
246 "--camera-id",
247 "simulation_camera"
248 ]
249 );
250 }
251}