use super::ProcessService;
use crate::paths;
use anyhow::{Context, Result};
use mecha10_cli_core::utils::strip_node_scope;
use mecha10_core::wire_identifier::split_wire_identifier;
use std::collections::HashMap;
impl ProcessService {
pub fn spawn_node_runner(
&mut self,
node_identifier: &str,
project_name: &str,
project_env: Option<HashMap<String, String>>,
) -> Result<u32> {
let runner_path = Self::resolve_node_runner_path();
let env = project_env.unwrap_or_default();
let (base_identifier, instance_id, camera_id) = split_wire_identifier(node_identifier);
let args = node_runner_args(project_name, base_identifier, instance_id, camera_id);
let args: Vec<&str> = args.iter().map(String::as_str).collect();
self.spawn_with_env(node_identifier, &runner_path, &args, env)
}
pub fn spawn_node_direct(
&mut self,
node_identifier: &str,
_project_name: &str,
project_env: Option<HashMap<String, String>>,
) -> Result<u32> {
let (base_identifier, instance_id, camera_id) = split_wire_identifier(node_identifier);
let node_name = strip_node_scope(base_identifier);
let is_local_node = self.is_local_project_node(node_name);
let mut env = HashMap::new();
env.insert("NODE_NAME".to_string(), base_identifier.to_string());
if let Some(instance_id) = instance_id {
env.insert("NODE_INSTANCE_ID".to_string(), instance_id.to_string());
}
if let Some(camera_id) = camera_id {
env.insert("MECHA10_CAMERA_ID".to_string(), camera_id.to_string());
}
if let Ok(rust_log) = std::env::var("RUST_LOG") {
env.insert("RUST_LOG".to_string(), rust_log);
}
if let Some(project_vars) = project_env {
tracing::debug!(
"🔀 spawn_node_direct for '{}': merging {} project vars",
node_name,
project_vars.len()
);
for (key, value) in &project_vars {
tracing::debug!(
" Adding: {} = {}",
key,
if key == "ROBOT_API_KEY" { "<redacted>" } else { value }
);
}
env.extend(project_vars);
} else {
tracing::warn!("⚠️ spawn_node_direct for '{}': No project_env provided!", node_name);
}
tracing::debug!(
"🏁 spawn_node_direct for '{}': final env has {} vars, is_local: {}",
node_name,
env.len(),
is_local_node
);
if is_local_node {
self.spawn_local_node_with_identifier(node_identifier, node_name, env)
} else {
self.spawn_framework_node(node_identifier, node_name, env)
}
}
fn spawn_framework_node(
&mut self,
node_identifier: &str,
node_name: &str,
env: HashMap<String, String>,
) -> Result<u32> {
use super::super::binary_manager::BinaryManager;
let (base_identifier, _instance_id, _camera_id) = split_wire_identifier(node_identifier);
let version = self.get_node_version_from_config(base_identifier);
let binary_manager = BinaryManager::new()?;
let binary_path = tokio::task::block_in_place(|| {
tokio::runtime::Handle::current()
.block_on(binary_manager.resolve_with_version(node_name, version.as_deref()))
})?;
tracing::info!(
"🚀 Spawning framework node '{}' from {}",
node_name,
binary_path.display()
);
self.spawn_with_env(node_identifier, &binary_path.to_string_lossy(), &[], env)
}
fn spawn_local_node_with_identifier(
&mut self,
node_identifier: &str,
node_name: &str,
env: HashMap<String, String>,
) -> Result<u32> {
use std::process::Command;
tracing::info!("🔨 Building local node: {}", node_name);
let build_output = Command::new("cargo")
.args(["build", "-p", node_name])
.output()
.context("Failed to run cargo build")?;
if !build_output.status.success() {
let stderr = String::from_utf8_lossy(&build_output.stderr);
anyhow::bail!("Failed to build node '{}': {}", node_name, stderr);
}
tracing::info!("✅ Built local node: {}", node_name);
let binary_path = paths::target_path("debug", node_name);
if !std::path::Path::new(&binary_path).exists() {
anyhow::bail!(
"Binary not found at '{}'. Build may have failed or the package name differs from node name.",
binary_path
);
}
self.spawn_with_env(node_identifier, &binary_path, &[], env)
}
}
fn node_runner_args(
project_name: &str,
base_identifier: &str,
instance_id: Option<&str>,
camera_id: Option<&str>,
) -> Vec<String> {
let mut args = vec![
"--project".to_string(),
project_name.to_string(),
base_identifier.to_string(),
];
if let Some(instance_id) = instance_id {
args.push("--instance-id".to_string());
args.push(instance_id.to_string());
}
if let Some(camera_id) = camera_id {
args.push("--camera-id".to_string());
args.push(camera_id.to_string());
}
args
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn node_runner_args_passes_the_scoped_identifier_through_unchanged() {
let args = node_runner_args("rover-robot", "@mecha10/simulator", None, None);
assert_eq!(args, ["--project", "rover-robot", "@mecha10/simulator"]);
}
#[test]
fn node_runner_args_leaves_unscoped_node_name_untouched() {
let args = node_runner_args("rover-robot", "my-node", None, None);
assert_eq!(args, ["--project", "rover-robot", "my-node"]);
}
#[test]
fn node_runner_args_appends_instance_id_flag_when_present() {
let args = node_runner_args("rover-robot", "@mecha10/camera", Some("real"), None);
assert_eq!(
args,
["--project", "rover-robot", "@mecha10/camera", "--instance-id", "real"]
);
}
#[test]
fn node_runner_args_appends_camera_id_flag_when_present() {
let args = node_runner_args("rover-robot", "@mecha10/video-streamer", None, Some("camera"));
assert_eq!(
args,
[
"--project",
"rover-robot",
"@mecha10/video-streamer",
"--camera-id",
"camera"
]
);
}
#[test]
fn node_runner_args_appends_both_flags_when_both_present() {
let args = node_runner_args(
"rover-robot",
"@mecha10/video-streamer",
Some("sim-streamer"),
Some("simulation_camera"),
);
assert_eq!(
args,
[
"--project",
"rover-robot",
"@mecha10/video-streamer",
"--instance-id",
"sim-streamer",
"--camera-id",
"simulation_camera"
]
);
}
}