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(
55 &mut self,
56 node_identifier: &str,
57 project_name: &str,
58 project_env: Option<HashMap<String, String>>,
59 ) -> Result<u32> {
60 let runner_path = Self::resolve_node_runner_path();
61
62 let env = project_env.unwrap_or_default();
63
64 let (base_identifier, instance_id, camera_id) = split_wire_identifier(node_identifier);
65
66 let args = node_runner_args(project_name, base_identifier, instance_id, camera_id);
67 let args: Vec<&str> = args.iter().map(String::as_str).collect();
68 self.spawn_with_env(node_identifier, &runner_path, &args, env)
69 }
70
71 pub fn spawn_node_direct(
94 &mut self,
95 node_identifier: &str,
96 _project_name: &str,
97 project_env: Option<HashMap<String, String>>,
98 ) -> Result<u32> {
99 let (base_identifier, instance_id, camera_id) = split_wire_identifier(node_identifier);
100 let node_name = strip_node_scope(base_identifier);
101
102 let is_local_node = self.is_local_project_node(node_name);
103
104 let mut env = HashMap::new();
105 env.insert("NODE_NAME".to_string(), base_identifier.to_string());
106 if let Some(instance_id) = instance_id {
107 env.insert("NODE_INSTANCE_ID".to_string(), instance_id.to_string());
108 }
109 if let Some(camera_id) = camera_id {
110 env.insert("MECHA10_CAMERA_ID".to_string(), camera_id.to_string());
111 }
112 if let Ok(rust_log) = std::env::var("RUST_LOG") {
113 env.insert("RUST_LOG".to_string(), rust_log);
114 }
115
116 if let Some(project_vars) = project_env {
117 tracing::debug!(
118 "🔀 spawn_node_direct for '{}': merging {} project vars",
119 node_name,
120 project_vars.len()
121 );
122 for (key, value) in &project_vars {
123 tracing::debug!(
124 " Adding: {} = {}",
125 key,
126 if key == "ROBOT_API_KEY" { "<redacted>" } else { value }
127 );
128 }
129 env.extend(project_vars);
130 } else {
131 tracing::warn!("⚠️ spawn_node_direct for '{}': No project_env provided!", node_name);
132 }
133
134 tracing::debug!(
135 "🏁 spawn_node_direct for '{}': final env has {} vars, is_local: {}",
136 node_name,
137 env.len(),
138 is_local_node
139 );
140
141 if is_local_node {
142 self.spawn_local_node_with_identifier(node_identifier, node_name, env)
143 } else {
144 self.spawn_framework_node(node_identifier, node_name, env)
145 }
146 }
147
148 fn spawn_framework_node(
157 &mut self,
158 node_identifier: &str,
159 node_name: &str,
160 env: HashMap<String, String>,
161 ) -> Result<u32> {
162 use super::super::binary_manager::BinaryManager;
163
164 let (base_identifier, _instance_id, _camera_id) = split_wire_identifier(node_identifier);
165 let version = self.get_node_version_from_config(base_identifier);
166
167 let binary_manager = BinaryManager::new()?;
168
169 let binary_path = tokio::task::block_in_place(|| {
170 tokio::runtime::Handle::current()
171 .block_on(binary_manager.resolve_with_version(node_name, version.as_deref()))
172 })?;
173
174 tracing::info!(
175 "🚀 Spawning framework node '{}' from {}",
176 node_name,
177 binary_path.display()
178 );
179
180 self.spawn_with_env(node_identifier, &binary_path.to_string_lossy(), &[], env)
181 }
182
183 fn spawn_local_node_with_identifier(
188 &mut self,
189 node_identifier: &str,
190 node_name: &str,
191 env: HashMap<String, String>,
192 ) -> Result<u32> {
193 use std::process::Command;
194
195 tracing::info!("🔨 Building local node: {}", node_name);
196
197 let build_output = Command::new("cargo")
198 .args(["build", "-p", node_name])
199 .output()
200 .context("Failed to run cargo build")?;
201
202 if !build_output.status.success() {
203 let stderr = String::from_utf8_lossy(&build_output.stderr);
204 anyhow::bail!("Failed to build node '{}': {}", node_name, stderr);
205 }
206
207 tracing::info!("✅ Built local node: {}", node_name);
208
209 let binary_path = paths::target_path("debug", node_name);
210
211 if !std::path::Path::new(&binary_path).exists() {
212 anyhow::bail!(
213 "Binary not found at '{}'. Build may have failed or the package name differs from node name.",
214 binary_path
215 );
216 }
217
218 self.spawn_with_env(node_identifier, &binary_path, &[], env)
219 }
220}
221
222fn node_runner_args(
234 project_name: &str,
235 base_identifier: &str,
236 instance_id: Option<&str>,
237 camera_id: Option<&str>,
238) -> Vec<String> {
239 let mut args = vec![
240 "--project".to_string(),
241 project_name.to_string(),
242 base_identifier.to_string(),
243 ];
244 if let Some(instance_id) = instance_id {
245 args.push("--instance-id".to_string());
246 args.push(instance_id.to_string());
247 }
248 if let Some(camera_id) = camera_id {
249 args.push("--camera-id".to_string());
250 args.push(camera_id.to_string());
251 }
252 args
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn node_runner_args_passes_the_scoped_identifier_through_unchanged() {
261 let args = node_runner_args("rover-robot", "@mecha10/simulator", None, None);
262 assert_eq!(args, ["--project", "rover-robot", "@mecha10/simulator"]);
263 }
264
265 #[test]
266 fn node_runner_args_leaves_unscoped_node_name_untouched() {
267 let args = node_runner_args("rover-robot", "my-node", None, None);
268 assert_eq!(args, ["--project", "rover-robot", "my-node"]);
269 }
270
271 #[test]
274 fn node_runner_args_appends_instance_id_flag_when_present() {
275 let args = node_runner_args("rover-robot", "@mecha10/camera", Some("real"), None);
276 assert_eq!(
277 args,
278 ["--project", "rover-robot", "@mecha10/camera", "--instance-id", "real"]
279 );
280 }
281
282 #[test]
284 fn node_runner_args_appends_camera_id_flag_when_present() {
285 let args = node_runner_args("rover-robot", "@mecha10/video-streamer", None, Some("camera"));
286 assert_eq!(
287 args,
288 [
289 "--project",
290 "rover-robot",
291 "@mecha10/video-streamer",
292 "--camera-id",
293 "camera"
294 ]
295 );
296 }
297
298 #[test]
299 fn node_runner_args_appends_both_flags_when_both_present() {
300 let args = node_runner_args(
301 "rover-robot",
302 "@mecha10/video-streamer",
303 Some("sim-streamer"),
304 Some("simulation_camera"),
305 );
306 assert_eq!(
307 args,
308 [
309 "--project",
310 "rover-robot",
311 "@mecha10/video-streamer",
312 "--instance-id",
313 "sim-streamer",
314 "--camera-id",
315 "simulation_camera"
316 ]
317 );
318 }
319}