use std::process::Command;
use std::sync::Arc;
use crate::proto::daemon::{
DaemonRequest, DaemonResponse, KeyValue, SpawnDaemonResponse, StatusCode,
};
use crate::ORIGINATOR_ENV_VAR;
use sysinfo::{Pid, ProcessRefreshKind, System};
use crate::daemon::registry::{self, TrackedEntry};
use super::util::{error_response, unix_now_seconds};
use super::DaemonState;
#[derive(Debug)]
struct SpawnedChild {
pid: u32,
created_at: f64,
}
fn shell_command(command: &str) -> Command {
#[cfg(windows)]
{
let mut cmd = Command::new("cmd.exe");
cmd.arg("/D").arg("/S").arg("/C").arg(command);
cmd
}
#[cfg(not(windows))]
{
let mut cmd = Command::new("/bin/sh");
cmd.arg("-c").arg(command);
cmd
}
}
fn process_created_at(pid: u32) -> Option<f64> {
let mut system = System::new();
let sysinfo_pid = Pid::from_u32(pid);
system.refresh_process_specifics(sysinfo_pid, ProcessRefreshKind::new());
system
.process(sysinfo_pid)
.map(|process| process.start_time() as f64)
}
fn canonical_env_pairs(env: &[KeyValue]) -> Vec<(String, String)> {
#[cfg(windows)]
{
use std::collections::BTreeMap;
let mut seen: BTreeMap<String, (String, String)> = BTreeMap::new();
for kv in env {
seen.insert(
kv.key.to_ascii_uppercase(),
(kv.key.clone(), kv.value.clone()),
);
}
seen.into_values().collect()
}
#[cfg(not(windows))]
{
env.iter()
.map(|kv| (kv.key.clone(), kv.value.clone()))
.collect()
}
}
fn spawn_and_track_detached(
command_text: &str,
cwd: &str,
env: &[KeyValue],
clear_inherited_env: bool,
originator: &str,
state: &DaemonState,
) -> Result<SpawnedChild, String> {
let mut command = shell_command(command_text);
if !cwd.is_empty() {
command.current_dir(cwd);
}
if clear_inherited_env {
command.env_clear();
}
if !env.is_empty() {
command.envs(canonical_env_pairs(env));
}
if !originator.is_empty() {
command.env(ORIGINATOR_ENV_VAR, originator);
}
let mut detached = crate::spawn_daemon_with_clear_env(&mut command, clear_inherited_env)
.map_err(|e| format!("failed to spawn detached command: {e}"))?;
let pid = detached.id();
let created_at = process_created_at(pid).unwrap_or_else(unix_now_seconds);
let created_at_ms = registry::created_at_to_ms(created_at);
let entry = TrackedEntry {
pid,
created_at_ms,
kind: "subprocess".to_string(),
command: command_text.to_string(),
cwd: cwd.to_string(),
originator: originator.to_string(),
containment: "detached".to_string(),
registered_at: unix_now_seconds(),
};
if let Err(e) = state.registry.register(entry) {
let _ = detached.kill();
let _ = detached.wait();
return Err(format!("registry error: {e}"));
}
let registry = Arc::clone(&state.registry);
std::thread::spawn(move || {
let _ = detached.wait();
let _ = registry.unregister_exact(pid, created_at_ms);
});
Ok(SpawnedChild { pid, created_at })
}
pub fn handle_spawn_daemon(request: &DaemonRequest, state: &DaemonState) -> DaemonResponse {
let Some(ref req) = request.spawn_daemon else {
return error_response(
request.id,
StatusCode::InvalidArgument,
"missing spawn_daemon payload".into(),
);
};
let command_text = req.command.trim();
if command_text.is_empty() {
return error_response(
request.id,
StatusCode::InvalidArgument,
"command must not be empty".into(),
);
}
let effective_originator = if req.originator.trim().is_empty() {
request.client_name.clone()
} else {
req.originator.clone()
};
match spawn_and_track_detached(
command_text,
&req.cwd,
&req.env,
req.clear_inherited_env,
&effective_originator,
state,
) {
Ok(spawned) => DaemonResponse {
request_id: request.id,
code: StatusCode::Ok as i32,
message: String::new(),
spawn_daemon: Some(SpawnDaemonResponse {
pid: spawned.pid,
created_at: spawned.created_at,
command: command_text.to_string(),
cwd: req.cwd.clone(),
originator: effective_originator,
containment: "detached".to_string(),
}),
..Default::default()
},
Err(message) => error_response(request.id, StatusCode::Internal, message),
}
}