mod support; use agentos_execution::{ v8_runtime::map_bridge_method, CreateJavascriptContextRequest, GuestRuntimeConfig, JavascriptExecution, JavascriptExecutionEvent, JavascriptExecutionLimits, JavascriptExecutionResult, JavascriptSyncRpcRequest, StartJavascriptExecutionRequest,};use base64::Engine;use serde::Deserialize;use serde_json::{json, Value};use std::collections::{BTreeMap, VecDeque};use std::fs;use std::io::{Read, Write};use std::os::unix::fs::symlink;use std::os::unix::fs::PermissionsExt;use std::os::unix::process::CommandExt;use std::os::unix::process::ExitStatusExt;use std::path::Path;use std::process::{Child, ChildStdin, Command, Stdio};use std::sync::mpsc::{self, Receiver, Sender, TryRecvError};use std::thread;use std::time::{Duration, Instant};use tempfile::tempdir; /*US-040 execution-test audit Deleted coverage:- `tests/javascript.rs`: removed because the file only exercised the old `legacy-js-tests` host-Node guest path (`loader.mjs`, `runner.mjs`, import-cache mutation, and `Command::new("node")` process behavior). The V8 isolate path no longer uses that guest execution model.- `permission_flags::node_permission_flags_do_not_expose_workspace_root_or_entrypoint_parent_writes`: removed because its JavaScript assertions depended on host-Node permission flags emitted for guest JS launches. V8 guest JS now stays in-process, while the remaining permission-flag tests still cover the real host-Node launches that remain for Python and WASM.- `benchmark::javascript_benchmark_harness_covers_required_startup_and_import_scenarios`: removed because it depended on pre-V8 benchmark marker behavior from the old startup harness instead of validating the current V8 execution path. The stable artifact and markdown benchmark tests remain.*/ // Timing-sensitive assertions flake under the CPU contention of a parallel test// run (see CLAUDE.md > Testing). Gated off by default; the nightly timing lane// sets AGENTOS_RUN_TIMING_TESTS=1 to enforce them.fn run_timing_sensitive_tests() -> bool { std::env::var_os("AGENTOS_RUN_TIMING_TESTS").is_some()} fn write_fixture(path: &Path, contents: &str) { if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("create fixture parent dirs"); } fs::write(path, contents).expect("write fixture");} fn run_host_node_json(cwd: &Path, entrypoint: &Path) -> Value { let output = Command::new("node") .arg(entrypoint) .current_dir(cwd) .output() .expect("run host node"); assert!( output.status.success(), "host node failed with status {:?}\nstdout:\n{}\nstderr:\n{}", output.status.code(), String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); serde_json::from_slice(&output.stdout).expect("parse host JSON")} fn write_fake_node_binary(path: &Path, log_path: &Path) { let script = format!( "#!/bin/sh\nset -eu\nprintf 'guest-node-invoked\\n' >> \"{}\"\nexit 99\n", log_path.display() ); fs::write(path, script).expect("write fake node binary"); let mut permissions = fs::metadata(path) .expect("fake node metadata") .permissions(); permissions.set_mode(0o755); fs::set_permissions(path, permissions).expect("chmod fake node binary");} #[derive(Debug, Deserialize, Default)]#[serde(rename_all = "camelCase")]struct TestJavascriptChildProcessSpawnOptions { #[serde(default)] argv0: Option<String>, #[serde(default)] cwd: Option<String>, #[serde(default)] env: BTreeMap<String, String>, #[serde(default)] internal_bootstrap_env: BTreeMap<String, String>, #[serde(default)] shell: bool, #[serde(default)] timeout: Option<u64>, #[serde(default, rename = "killSignal")] kill_signal: Option<String>,} #[derive(Debug, Deserialize)]struct TestJavascriptChildProcessSpawnRequest { command: String, #[serde(default)] args: Vec<String>, #[serde(default)] options: TestJavascriptChildProcessSpawnOptions,} type TestJavascriptChildProcessSpawnSyncRequest = ( TestJavascriptChildProcessSpawnRequest, Option<usize>, Option<Vec<u8>>,); #[derive(Debug, Deserialize, Default)]#[serde(rename_all = "camelCase")]struct TestLegacyJavascriptChildProcessSpawnOptions { #[serde(default)] argv0: Option<String>, #[serde(default)] cwd: Option<String>, #[serde(default)] env: BTreeMap<String, String>, #[serde(default)] input: Option<Value>, #[serde(default)] shell: bool, #[serde(default, rename = "maxBuffer")] max_buffer: Option<usize>, #[serde(default)] timeout: Option<u64>, #[serde(default, rename = "killSignal")] kill_signal: Option<String>,} enum HostChildOutputEvent { Stdout(Vec<u8>), Stderr(Vec<u8>), StreamClosed,} struct HostChildRecord { child: Child, stdin: Option<ChildStdin>, output_events: Receiver<HostChildOutputEvent>, pending_events: VecDeque<Value>, exit_status: Option<(Option<i32>, Option<String>)>, open_streams: usize,} #[derive(Default)]struct HostChildProcessHarness { next_child_id: usize, children: BTreeMap<String, HostChildRecord>,} impl HostChildProcessHarness { fn handle_request( &mut self, host_cwd: &Path, request: JavascriptSyncRpcRequest, ) -> Result<Value, String> { match request.method.as_str() { "child_process.spawn" => self.spawn(host_cwd, &request.args), "child_process.spawn_sync" => self.spawn_sync(host_cwd, &request.args), "child_process.poll" => self.poll(&request.args), "child_process.write_stdin" => self.write_stdin(&request.args), "child_process.close_stdin" => self.close_stdin(&request.args), "child_process.kill" => self.kill(&request.args), "fs.writeFileSync" => self.write_file(host_cwd, &request.args), other => Err(format!("unsupported sync RPC method: {other}")), } } fn spawn(&mut self, host_cwd: &Path, args: &[Value]) -> Result<Value, String> { let request = parse_test_child_process_spawn_request(args)?; let child_id = { self.next_child_id += 1; format!("child-{}", self.next_child_id) }; let mut command = if request.options.shell { let mut command = Command::new("/bin/sh"); command.arg("-c").arg(&request.command); command.args(&request.args); command } else { let mut command = Command::new(self.map_guest_path(host_cwd, &request.command)); command.args( request .args .iter() .map(|arg| self.map_guest_path(host_cwd, arg)), ); command }; if let Some(argv0) = request.options.argv0.as_deref() { command.arg0(argv0); } let child_cwd = request .options .cwd .as_deref() .map(|cwd| std::path::PathBuf::from(self.map_guest_path(host_cwd, cwd))) .unwrap_or_else(|| host_cwd.to_path_buf()); command .current_dir(child_cwd) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .env_clear() .envs(&request.options.env) .envs(&request.options.internal_bootstrap_env); let mut child = command .spawn() .map_err(|error| format!("spawn {} failed: {error}", request.command))?; let stdin = child.stdin.take(); let stdout = child .stdout .take() .ok_or_else(|| String::from("spawned child stdout pipe missing"))?; let stderr = child .stderr .take() .ok_or_else(|| String::from("spawned child stderr pipe missing"))?; let (output_sender, output_events) = mpsc::channel(); spawn_output_reader(stdout, output_sender.clone(), true); spawn_output_reader(stderr, output_sender, false); let pid = child.id(); self.children.insert( child_id.clone(), HostChildRecord { child, stdin, output_events, pending_events: VecDeque::new(), exit_status: None, open_streams: 2, }, ); Ok(json!({ "childId": child_id, "pid": pid, "command": request.command, "args": request.args, })) } fn poll(&mut self, args: &[Value]) -> Result<Value, String> { let child_id = args .first() .and_then(Value::as_str) .ok_or_else(|| String::from("child_process.poll missing child id"))?; let wait_ms = args.get(1).and_then(Value::as_u64).unwrap_or_default(); let child = self .children .get_mut(child_id) .ok_or_else(|| format!("unknown child process {child_id}"))?; let deadline = std::time::Instant::now() + Duration::from_millis(wait_ms); loop { drain_child_output(child); if let Some(event) = child.pending_events.pop_front() { return Ok(event); } if child.exit_status.is_none() { if let Some(status) = child .child .try_wait() .map_err(|error| format!("try_wait {child_id} failed: {error}"))? { child.exit_status = Some(( status.code(), status.signal().map(|signal| match signal { 9 => String::from("SIGKILL"), 15 => String::from("SIGTERM"), other => format!("SIG{other}"), }), )); } } if let Some((exit_code, signal)) = child.exit_status.as_ref() { if child.pending_events.is_empty() && child.open_streams == 0 { let exit_code = *exit_code; let signal = signal.clone(); self.children.remove(child_id); return Ok(json!({ "type": "exit", "exitCode": exit_code, "signal": signal, })); } } if std::time::Instant::now() >= deadline { return Ok(Value::Null); } thread::sleep(Duration::from_millis(5)); } } /// Translate the host-child harness's durable output state into the /// evented stream protocol used by the production sidecar. The JavaScript /// child-process shim no longer issues recurring `child_process.poll` /// bridge calls, so this standalone conformance harness must emulate the /// sidecar's readiness pump instead of waiting for a guest poll request. fn drain_stream_events(&mut self) -> Result<Vec<(&'static str, Value)>, String> { const MAX_EVENTS_PER_TURN: usize = 256; let child_ids: Vec<String> = self.children.keys().cloned().collect(); let mut stream_events = Vec::new(); for child_id in child_ids { while stream_events.len() < MAX_EVENTS_PER_TURN { let event = self.poll(&[Value::String(child_id.clone()), json!(0)])?; let Some(event_type) = event.get("type").and_then(Value::as_str) else { break; }; match event_type { "stdout" | "stderr" => stream_events.push(( if event_type == "stdout" { "child_stdout" } else { "child_stderr" }, json!({ "sessionId": child_id, "data": event.get("data").cloned().unwrap_or(Value::Null), }), )), "exit" => { stream_events.push(( "child_exit", json!({ "sessionId": child_id, "code": event .get("exitCode") .and_then(Value::as_i64) .unwrap_or(1), "signal": Value::Null, }), )); break; } other => return Err(format!("unknown child event type {other}")), } } if stream_events.len() >= MAX_EVENTS_PER_TURN { break; } } Ok(stream_events) } fn spawn_sync(&mut self, host_cwd: &Path, args: &[Value]) -> Result<Value, String> { let (request, max_buffer, input) = parse_test_child_process_spawn_sync_request(args)?; let mut command = if request.options.shell { let mut command = Command::new("/bin/sh"); command.arg("-c").arg(&request.command); command.args(&request.args); command } else { let mut command = Command::new(self.map_guest_path(host_cwd, &request.command)); command.args( request .args .iter() .map(|arg| self.map_guest_path(host_cwd, arg)), ); command }; if let Some(argv0) = request.options.argv0.as_deref() { command.arg0(argv0); } let child_cwd = request .options .cwd .as_deref() .map(|cwd| std::path::PathBuf::from(self.map_guest_path(host_cwd, cwd))) .unwrap_or_else(|| host_cwd.to_path_buf()); command .current_dir(child_cwd) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .env_clear() .envs(&request.options.env) .envs(&request.options.internal_bootstrap_env); let mut child = command .spawn() .map_err(|error| format!("spawnSync {} failed: {error}", request.command))?; if let Some(input) = input.as_deref() { let stdin = child .stdin .as_mut() .ok_or_else(|| String::from("spawnSync child stdin pipe missing"))?; stdin .write_all(input) .map_err(|error| format!("write spawnSync stdin failed: {error}"))?; } child.stdin.take(); let mut timed_out = false; if let Some(timeout_ms) = request.options.timeout { let deadline = Instant::now() + Duration::from_millis(timeout_ms); loop { match child .try_wait() .map_err(|error| format!("try_wait for {} failed: {error}", request.command))? { Some(_) => break, None if !timed_out && Instant::now() >= deadline => { let signal = request .options .kill_signal .clone() .unwrap_or_else(|| String::from("SIGTERM")); let status = Command::new("kill") .arg(format!( "-{}", signal.strip_prefix("SIG").unwrap_or(&signal) )) .arg(child.id().to_string()) .status() .map_err(|error| { format!("send {signal} to {} failed: {error}", request.command) })?; if !status.success() { return Err(format!( "send {signal} to {} failed with {status}", request.command )); } timed_out = true; } None => thread::sleep(Duration::from_millis(5)), } } } let output = child .wait_with_output() .map_err(|error| format!("wait_with_output for {} failed: {error}", request.command))?; let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); let max_buffer = max_buffer.unwrap_or(1024 * 1024); let max_buffer_exceeded = output.stdout.len() > max_buffer || output.stderr.len() > max_buffer; let signal = output.status.signal().map(|signal| match signal { 9 => String::from("SIGKILL"), 15 => String::from("SIGTERM"), other => format!("SIG{other}"), }); Ok(json!({ "stdout": stdout, "stderr": stderr, "code": output.status.code(), "signal": signal, "timedOut": timed_out, "maxBufferExceeded": max_buffer_exceeded, })) } fn write_stdin(&mut self, args: &[Value]) -> Result<Value, String> { let child_id = args .first() .and_then(Value::as_str) .ok_or_else(|| String::from("child_process.write_stdin missing child id"))?; let chunk = decode_guest_bytes( args.get(1) .ok_or_else(|| String::from("child_process.write_stdin missing chunk"))?, )?; let child = self .children .get_mut(child_id) .ok_or_else(|| format!("unknown child process {child_id}"))?; if let Some(stdin) = child.stdin.as_mut() { stdinLines 1–500 of 7117. Load more
mod support; use agentos_execution::{ v8_runtime::map_bridge_method, CreateJavascriptContextRequest, GuestRuntimeConfig, JavascriptExecution, JavascriptExecutionEvent, JavascriptExecutionLimits, JavascriptExecutionResult, JavascriptSyncRpcRequest, StartJavascriptExecutionRequest, }; use base64::Engine; use serde::Deserialize; use serde_json::{json, Value}; use std::collections::{BTreeMap, VecDeque}; use std::fs; use std::io::{Read, Write}; use std::os::unix::fs::symlink; use std::os::unix::fs::PermissionsExt; use std::os::unix::process::CommandExt; use std::os::unix::process::ExitStatusExt; use std::path::Path; use std::process::{Child, ChildStdin, Command, Stdio}; use std::sync::mpsc::{self, Receiver, Sender, TryRecvError}; use std::thread; use std::time::{Duration, Instant}; use tempfile::tempdir; /* US-040 execution-test audit Deleted coverage: - `tests/javascript.rs`: removed because the file only exercised the old `legacy-js-tests` host-Node guest path (`loader.mjs`, `runner.mjs`, import-cache mutation, and `Command::new("node")` process behavior). The V8 isolate path no longer uses that guest execution model. - `permission_flags::node_permission_flags_do_not_expose_workspace_root_or_entrypoint_parent_writes`: removed because its JavaScript assertions depended on host-Node permission flags emitted for guest JS launches. V8 guest JS now stays in-process, while the remaining permission-flag tests still cover the real host-Node launches that remain for Python and WASM. - `benchmark::javascript_benchmark_harness_covers_required_startup_and_import_scenarios`: removed because it depended on pre-V8 benchmark marker behavior from the old startup harness instead of validating the current V8 execution path. The stable artifact and markdown benchmark tests remain. */ // Timing-sensitive assertions flake under the CPU contention of a parallel test // run (see CLAUDE.md > Testing). Gated off by default; the nightly timing lane // sets AGENTOS_RUN_TIMING_TESTS=1 to enforce them. fn run_timing_sensitive_tests() -> bool { std::env::var_os("AGENTOS_RUN_TIMING_TESTS").is_some() } fn write_fixture(path: &Path, contents: &str) { if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("create fixture parent dirs"); } fs::write(path, contents).expect("write fixture"); } fn run_host_node_json(cwd: &Path, entrypoint: &Path) -> Value { let output = Command::new("node") .arg(entrypoint) .current_dir(cwd) .output() .expect("run host node"); assert!( output.status.success(), "host node failed with status {:?}\nstdout:\n{}\nstderr:\n{}", output.status.code(), String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); serde_json::from_slice(&output.stdout).expect("parse host JSON") } fn write_fake_node_binary(path: &Path, log_path: &Path) { let script = format!( "#!/bin/sh\nset -eu\nprintf 'guest-node-invoked\\n' >> \"{}\"\nexit 99\n", log_path.display() ); fs::write(path, script).expect("write fake node binary"); let mut permissions = fs::metadata(path) .expect("fake node metadata") .permissions(); permissions.set_mode(0o755); fs::set_permissions(path, permissions).expect("chmod fake node binary"); } #[derive(Debug, Deserialize, Default)] #[serde(rename_all = "camelCase")] struct TestJavascriptChildProcessSpawnOptions { #[serde(default)] argv0: Option<String>, #[serde(default)] cwd: Option<String>, #[serde(default)] env: BTreeMap<String, String>, #[serde(default)] internal_bootstrap_env: BTreeMap<String, String>, #[serde(default)] shell: bool, #[serde(default)] timeout: Option<u64>, #[serde(default, rename = "killSignal")] kill_signal: Option<String>, } #[derive(Debug, Deserialize)] struct TestJavascriptChildProcessSpawnRequest { command: String, #[serde(default)] args: Vec<String>, #[serde(default)] options: TestJavascriptChildProcessSpawnOptions, } type TestJavascriptChildProcessSpawnSyncRequest = ( TestJavascriptChildProcessSpawnRequest, Option<usize>, Option<Vec<u8>>, ); #[derive(Debug, Deserialize, Default)] #[serde(rename_all = "camelCase")] struct TestLegacyJavascriptChildProcessSpawnOptions { #[serde(default)] argv0: Option<String>, #[serde(default)] cwd: Option<String>, #[serde(default)] env: BTreeMap<String, String>, #[serde(default)] input: Option<Value>, #[serde(default)] shell: bool, #[serde(default, rename = "maxBuffer")] max_buffer: Option<usize>, #[serde(default)] timeout: Option<u64>, #[serde(default, rename = "killSignal")] kill_signal: Option<String>, } enum HostChildOutputEvent { Stdout(Vec<u8>), Stderr(Vec<u8>), StreamClosed, } struct HostChildRecord { child: Child, stdin: Option<ChildStdin>, output_events: Receiver<HostChildOutputEvent>, pending_events: VecDeque<Value>, exit_status: Option<(Option<i32>, Option<String>)>, open_streams: usize, } #[derive(Default)] struct HostChildProcessHarness { next_child_id: usize, children: BTreeMap<String, HostChildRecord>, } impl HostChildProcessHarness { fn handle_request( &mut self, host_cwd: &Path, request: JavascriptSyncRpcRequest, ) -> Result<Value, String> { match request.method.as_str() { "child_process.spawn" => self.spawn(host_cwd, &request.args), "child_process.spawn_sync" => self.spawn_sync(host_cwd, &request.args), "child_process.poll" => self.poll(&request.args), "child_process.write_stdin" => self.write_stdin(&request.args), "child_process.close_stdin" => self.close_stdin(&request.args), "child_process.kill" => self.kill(&request.args), "fs.writeFileSync" => self.write_file(host_cwd, &request.args), other => Err(format!("unsupported sync RPC method: {other}")), } } fn spawn(&mut self, host_cwd: &Path, args: &[Value]) -> Result<Value, String> { let request = parse_test_child_process_spawn_request(args)?; let child_id = { self.next_child_id += 1; format!("child-{}", self.next_child_id) }; let mut command = if request.options.shell { let mut command = Command::new("/bin/sh"); command.arg("-c").arg(&request.command); command.args(&request.args); command } else { let mut command = Command::new(self.map_guest_path(host_cwd, &request.command)); command.args( request .args .iter() .map(|arg| self.map_guest_path(host_cwd, arg)), ); command }; if let Some(argv0) = request.options.argv0.as_deref() { command.arg0(argv0); } let child_cwd = request .options .cwd .as_deref() .map(|cwd| std::path::PathBuf::from(self.map_guest_path(host_cwd, cwd))) .unwrap_or_else(|| host_cwd.to_path_buf()); command .current_dir(child_cwd) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .env_clear() .envs(&request.options.env) .envs(&request.options.internal_bootstrap_env); let mut child = command .spawn() .map_err(|error| format!("spawn {} failed: {error}", request.command))?; let stdin = child.stdin.take(); let stdout = child .stdout .take() .ok_or_else(|| String::from("spawned child stdout pipe missing"))?; let stderr = child .stderr .take() .ok_or_else(|| String::from("spawned child stderr pipe missing"))?; let (output_sender, output_events) = mpsc::channel(); spawn_output_reader(stdout, output_sender.clone(), true); spawn_output_reader(stderr, output_sender, false); let pid = child.id(); self.children.insert( child_id.clone(), HostChildRecord { child, stdin, output_events, pending_events: VecDeque::new(), exit_status: None, open_streams: 2, }, ); Ok(json!({ "childId": child_id, "pid": pid, "command": request.command, "args": request.args, })) } fn poll(&mut self, args: &[Value]) -> Result<Value, String> { let child_id = args .first() .and_then(Value::as_str) .ok_or_else(|| String::from("child_process.poll missing child id"))?; let wait_ms = args.get(1).and_then(Value::as_u64).unwrap_or_default(); let child = self .children .get_mut(child_id) .ok_or_else(|| format!("unknown child process {child_id}"))?; let deadline = std::time::Instant::now() + Duration::from_millis(wait_ms); loop { drain_child_output(child); if let Some(event) = child.pending_events.pop_front() { return Ok(event); } if child.exit_status.is_none() { if let Some(status) = child .child .try_wait() .map_err(|error| format!("try_wait {child_id} failed: {error}"))? { child.exit_status = Some(( status.code(), status.signal().map(|signal| match signal { 9 => String::from("SIGKILL"), 15 => String::from("SIGTERM"), other => format!("SIG{other}"), }), )); } } if let Some((exit_code, signal)) = child.exit_status.as_ref() { if child.pending_events.is_empty() && child.open_streams == 0 { let exit_code = *exit_code; let signal = signal.clone(); self.children.remove(child_id); return Ok(json!({ "type": "exit", "exitCode": exit_code, "signal": signal, })); } } if std::time::Instant::now() >= deadline { return Ok(Value::Null); } thread::sleep(Duration::from_millis(5)); } } /// Translate the host-child harness's durable output state into the /// evented stream protocol used by the production sidecar. The JavaScript /// child-process shim no longer issues recurring `child_process.poll` /// bridge calls, so this standalone conformance harness must emulate the /// sidecar's readiness pump instead of waiting for a guest poll request. fn drain_stream_events(&mut self) -> Result<Vec<(&'static str, Value)>, String> { const MAX_EVENTS_PER_TURN: usize = 256; let child_ids: Vec<String> = self.children.keys().cloned().collect(); let mut stream_events = Vec::new(); for child_id in child_ids { while stream_events.len() < MAX_EVENTS_PER_TURN { let event = self.poll(&[Value::String(child_id.clone()), json!(0)])?; let Some(event_type) = event.get("type").and_then(Value::as_str) else { break; }; match event_type { "stdout" | "stderr" => stream_events.push(( if event_type == "stdout" { "child_stdout" } else { "child_stderr" }, json!({ "sessionId": child_id, "data": event.get("data").cloned().unwrap_or(Value::Null), }), )), "exit" => { stream_events.push(( "child_exit", json!({ "sessionId": child_id, "code": event .get("exitCode") .and_then(Value::as_i64) .unwrap_or(1), "signal": Value::Null, }), )); break; } other => return Err(format!("unknown child event type {other}")), } } if stream_events.len() >= MAX_EVENTS_PER_TURN { break; } } Ok(stream_events) } fn spawn_sync(&mut self, host_cwd: &Path, args: &[Value]) -> Result<Value, String> { let (request, max_buffer, input) = parse_test_child_process_spawn_sync_request(args)?; let mut command = if request.options.shell { let mut command = Command::new("/bin/sh"); command.arg("-c").arg(&request.command); command.args(&request.args); command } else { let mut command = Command::new(self.map_guest_path(host_cwd, &request.command)); command.args( request .args .iter() .map(|arg| self.map_guest_path(host_cwd, arg)), ); command }; if let Some(argv0) = request.options.argv0.as_deref() { command.arg0(argv0); } let child_cwd = request .options .cwd .as_deref() .map(|cwd| std::path::PathBuf::from(self.map_guest_path(host_cwd, cwd))) .unwrap_or_else(|| host_cwd.to_path_buf()); command .current_dir(child_cwd) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .env_clear() .envs(&request.options.env) .envs(&request.options.internal_bootstrap_env); let mut child = command .spawn() .map_err(|error| format!("spawnSync {} failed: {error}", request.command))?; if let Some(input) = input.as_deref() { let stdin = child .stdin .as_mut() .ok_or_else(|| String::from("spawnSync child stdin pipe missing"))?; stdin .write_all(input) .map_err(|error| format!("write spawnSync stdin failed: {error}"))?; } child.stdin.take(); let mut timed_out = false; if let Some(timeout_ms) = request.options.timeout { let deadline = Instant::now() + Duration::from_millis(timeout_ms); loop { match child .try_wait() .map_err(|error| format!("try_wait for {} failed: {error}", request.command))? { Some(_) => break, None if !timed_out && Instant::now() >= deadline => { let signal = request .options .kill_signal .clone() .unwrap_or_else(|| String::from("SIGTERM")); let status = Command::new("kill") .arg(format!( "-{}", signal.strip_prefix("SIG").unwrap_or(&signal) )) .arg(child.id().to_string()) .status() .map_err(|error| { format!("send {signal} to {} failed: {error}", request.command) })?; if !status.success() { return Err(format!( "send {signal} to {} failed with {status}", request.command )); } timed_out = true; } None => thread::sleep(Duration::from_millis(5)), } } } let output = child .wait_with_output() .map_err(|error| format!("wait_with_output for {} failed: {error}", request.command))?; let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); let max_buffer = max_buffer.unwrap_or(1024 * 1024); let max_buffer_exceeded = output.stdout.len() > max_buffer || output.stderr.len() > max_buffer; let signal = output.status.signal().map(|signal| match signal { 9 => String::from("SIGKILL"), 15 => String::from("SIGTERM"), other => format!("SIG{other}"), }); Ok(json!({ "stdout": stdout, "stderr": stderr, "code": output.status.code(), "signal": signal, "timedOut": timed_out, "maxBufferExceeded": max_buffer_exceeded, })) } fn write_stdin(&mut self, args: &[Value]) -> Result<Value, String> { let child_id = args .first() .and_then(Value::as_str) .ok_or_else(|| String::from("child_process.write_stdin missing child id"))?; let chunk = decode_guest_bytes( args.get(1) .ok_or_else(|| String::from("child_process.write_stdin missing chunk"))?, )?; let child = self .children .get_mut(child_id) .ok_or_else(|| format!("unknown child process {child_id}"))?; if let Some(stdin) = child.stdin.as_mut() { stdin