diff --git a/Cargo.lock b/Cargo.lock index 11093dad..764f69f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2293,9 +2293,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "log", "once_cell", @@ -2317,9 +2317,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", @@ -2674,7 +2674,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3352,7 +3352,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/crates/running-process-platform-internal/src/foreground.rs b/crates/running-process-platform-internal/src/foreground.rs index aee7bad1..adaca2ac 100644 --- a/crates/running-process-platform-internal/src/foreground.rs +++ b/crates/running-process-platform-internal/src/foreground.rs @@ -33,198 +33,27 @@ pub fn output(command: &mut Command) -> io::Result { command.output() } -/// Replace the current Unix process image using the supplied native command. -/// Success never returns; failure returns the original OS error. This does not -/// spawn a child or change the caller's requested argv, environment, stdio, -/// pre-exec hooks, process group, or session policy. As with `CommandExt::exec`, -/// failed execution may leave setup changes applied to the current process. -#[cfg(unix)] -pub fn exec(command: &mut Command) -> io::Error { - use std::os::unix::process::CommandExt; - command.exec() -} +// Host-specific forms (Unix `exec`) come from the selected platform tree. +// The Windows tree exports none, so the glob is empty there by design. +#[allow(unused_imports)] +pub use crate::foreground_imp::*; #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; - #[test] - fn spawn_preserves_native_missing_program_error() { - let directory = tempfile::tempdir().expect("private fixture directory"); - let mut command = Command::new(directory.path().join("absent-executable")); - let error = spawn(&mut command).expect_err("no child should be created"); - assert_eq!(error.kind(), io::ErrorKind::NotFound); - } - - #[cfg(unix)] - #[test] - fn spawn_transfers_child_and_preserves_configured_output() { - use std::io::{Read, Seek}; - use std::process::Stdio; - use std::time::{Duration, Instant}; - let mut destination = tempfile::tempfile().expect("output file"); - let mut command = Command::new("/bin/sh"); - command - .args(["-c", "printf '%s' \"$MARKER\"; exit 7"]) - .env("MARKER", "caller-configured-output") - .stdin(Stdio::null()) - .stderr(Stdio::null()) - .stdout(Stdio::from(destination.try_clone().unwrap())); - let mut child = spawn(&mut command).expect("foreground spawn"); - assert!( - child.stdout.is_none(), - "file binding must not become a capture pipe" - ); - let deadline = Instant::now() + Duration::from_secs(5); - let status = loop { - match child.try_wait() { - Ok(Some(status)) => break status, - Ok(None) if Instant::now() < deadline => { - std::thread::sleep(Duration::from_millis(10)); - } - outcome => { - let _ = child.kill(); - let _ = child.wait(); - panic!("foreground fixture did not complete: {outcome:?}"); - } - } - }; - assert_eq!(status.code(), Some(7)); - destination.rewind().unwrap(); - let mut bytes = Vec::new(); - destination.read_to_end(&mut bytes).unwrap(); - assert_eq!(bytes, b"caller-configured-output"); + /// Build a fixed fixture command for the per-host foreground tests. The + /// platform trees reuse this so every fixture `Command` is constructed in + /// this reviewed escape-hatch module, not a new spawn site. + pub(crate) fn fixture_command(program: impl AsRef) -> Command { + Command::new(program) } - #[cfg(unix)] #[test] - fn exec_returns_native_missing_image_error() { + fn spawn_preserves_native_missing_program_error() { let directory = tempfile::tempdir().expect("private fixture directory"); let mut command = Command::new(directory.path().join("absent-executable")); - // No stdio/cwd/env/hook changes: the failed exec leaves this test's - // process configuration untouched, and never runs another image. - let error = exec(&mut command); + let error = spawn(&mut command).expect_err("no child should be created"); assert_eq!(error.kind(), io::ErrorKind::NotFound); - assert_eq!(error.raw_os_error(), Some(libc::ENOENT)); - } - - #[cfg(unix)] - #[test] - fn output_preserves_cwd_environment_literal_argv_and_native_nonzero() { - let directory = tempfile::tempdir().expect("fixture cwd"); - let cwd = directory.path().canonicalize().expect("physical cwd"); - let mut command = Command::new("/bin/sh"); - command.args(["-c", "[ \"$#\" = 2 ] && [ -z \"$2\" ] || exit 9; printf '%s\\n%s\\n%s\\n' \"$PWD\" \"$MARKER\" \"$1\"; exit 7", "foreground", "$literal; not a command", ""]); - command.current_dir(&cwd).env("MARKER", "foreground-env"); - let output = output(&mut command).expect("foreground output"); - assert_eq!(output.status.code(), Some(7)); - let expected = format!( - "{}\nforeground-env\n$literal; not a command\n", - cwd.display() - ); - assert_eq!(output.stdout, expected.as_bytes()); - assert!(output.stderr.is_empty()); - } - - #[cfg(windows)] - #[test] - fn output_preserves_windows_environment_cwd_and_nonzero_status() { - let directory = tempfile::tempdir().expect("fixture cwd"); - let mut command = Command::new("cmd.exe"); - // /D disables user AutoRun commands; CD emits the actual child cwd. - command.args(["/D", "/C", "cd & echo %MARKER% & exit /b 7"]); - command - .current_dir(directory.path()) - .env("MARKER", "foreground-env"); - let captured = output(&mut command).expect("foreground output"); - assert_eq!(captured.status.code(), Some(7)); - let text = String::from_utf8_lossy(&captured.stdout); - let mut lines = text.lines(); - let actual = std::path::Path::new(lines.next().expect("cwd line")); - assert_eq!( - actual.canonicalize().unwrap(), - directory.path().canonicalize().unwrap() - ); - assert_eq!( - lines.next().expect("environment line").trim(), - "foreground-env" - ); - } - - #[cfg(target_os = "linux")] - #[test] - fn foreground_keeps_parent_process_group_and_session() { - let parent = std::fs::read_to_string("/proc/self/stat").expect("parent stat"); - let mut command = Command::new("/bin/sh"); - command.args(["-c", "cut -d' ' -f5,6 /proc/self/stat"]); - let output = output(&mut command).expect("foreground child"); - let parent_fields: Vec<_> = parent - .rsplit_once(") ") - .expect("stat") - .1 - .split_whitespace() - .collect(); - let child = String::from_utf8_lossy(&output.stdout); - assert_eq!( - child.trim(), - format!("{} {}", parent_fields[2], parent_fields[3]) - ); - } - - #[cfg(unix)] - #[test] - fn both_execution_forms_preserve_explicit_stdio_overrides() { - use std::io::{Read, Seek, Write}; - use std::process::Stdio; - for capture in [false, true] { - let mut input = tempfile::tempfile().unwrap(); - input.write_all(b"caller-controlled input").unwrap(); - input.rewind().unwrap(); - let mut destination = tempfile::tempfile().unwrap(); - let mut command = Command::new("/bin/sh"); - command - .args(["-c", "cat; exit 7"]) - .stdin(Stdio::from(input)) - .stdout(Stdio::from(destination.try_clone().unwrap())) - .stderr(Stdio::null()); - if capture { - let captured = output(&mut command).unwrap(); - assert_eq!(captured.status.code(), Some(7)); - assert!(captured.stdout.is_empty()); - assert!(captured.stderr.is_empty()); - } else { - assert_eq!(status(&mut command).unwrap().code(), Some(7)); - } - destination.rewind().unwrap(); - let mut bytes = Vec::new(); - destination.read_to_end(&mut bytes).unwrap(); - assert_eq!(bytes, b"caller-controlled input"); - } - } - - #[cfg(target_os = "linux")] - #[test] - fn foreground_does_not_close_an_inheritable_high_descriptor() { - use std::io::{Seek, Write}; - use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; - let mut file = tempfile::tempfile().expect("fixture file"); - file.write_all(b"inherited-resource").unwrap(); - file.rewind().unwrap(); - // SAFETY: F_DUPFD duplicates the live borrowed descriptor; it does - // not access userspace memory or change the source descriptor. - let descriptor = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_DUPFD, 64) }; - assert!(descriptor >= 64); - // SAFETY: F_DUPFD returned a distinct owned descriptor. - let descriptor = unsafe { OwnedFd::from_raw_fd(descriptor) }; - let mut command = Command::new("/bin/sh"); - command.args([ - "-c", - "cat \"/proc/self/fd/$1\"", - "foreground", - &descriptor.as_raw_fd().to_string(), - ]); - let captured = output(&mut command).expect("inherited descriptor read"); - assert!(captured.status.success()); - assert_eq!(captured.stdout, b"inherited-resource"); } } diff --git a/crates/running-process-platform-internal/src/lib.rs b/crates/running-process-platform-internal/src/lib.rs index 30cdde79..e13af800 100644 --- a/crates/running-process-platform-internal/src/lib.rs +++ b/crates/running-process-platform-internal/src/lib.rs @@ -75,6 +75,9 @@ cfg_select! { // Re-export the selected implementation once from this allowed host-selector // root. Neutral capability facades re-export only crate-root names and never // name the private `platform_imp` alias themselves. +pub(crate) use platform_imp::foreground as foreground_imp; +pub(crate) use platform_imp::{PRIORITY_NICE_HIGH, PRIORITY_NICE_LOW}; + pub use platform_imp::{ assign_child_to_windows_job, cancel_capture_reader, canonical_environment_pairs, capture_reader_done, compat_shell_command, configure_exact_trace, configure_process_command, diff --git a/crates/running-process-platform-internal/src/platform_linux.rs b/crates/running-process-platform-internal/src/platform_linux.rs index 9eda5ecd..6a218611 100644 --- a/crates/running-process-platform-internal/src/platform_linux.rs +++ b/crates/running-process-platform-internal/src/platform_linux.rs @@ -1,5 +1,13 @@ //! Linux implementation root for the process capability. +#[path = "platform_linux/foreground.rs"] +pub(crate) mod foreground; + +/// Native niceness for the portable `ProcessPriority::Low` intent. +pub(crate) const PRIORITY_NICE_LOW: i32 = 10; +/// Native niceness for the portable `ProcessPriority::High` intent. +pub(crate) const PRIORITY_NICE_HIGH: i32 = -5; + #[path = "platform_linux/autostart.rs"] pub(crate) mod autostart; diff --git a/crates/running-process-platform-internal/src/platform_linux/foreground.rs b/crates/running-process-platform-internal/src/platform_linux/foreground.rs new file mode 100644 index 00000000..d97df4ab --- /dev/null +++ b/crates/running-process-platform-internal/src/platform_linux/foreground.rs @@ -0,0 +1,163 @@ +//! Linux foreground execution: Unix process-image replacement. + +use std::io; +use std::process::Command; + +/// Replace the current Unix process image using the supplied native command. +/// Success never returns; failure returns the original OS error. This does not +/// spawn a child or change the caller's requested argv, environment, stdio, +/// pre-exec hooks, process group, or session policy. As with `CommandExt::exec`, +/// failed execution may leave setup changes applied to the current process. +pub fn exec(command: &mut Command) -> io::Error { + use std::os::unix::process::CommandExt; + command.exec() +} + +#[cfg(test)] +mod tests { + use super::exec; + use crate::foreground::{output, spawn, status}; + use std::io; + use crate::foreground::tests::fixture_command; + + #[test] + fn spawn_transfers_child_and_preserves_configured_output() { + use std::io::{Read, Seek}; + use std::process::Stdio; + use std::time::{Duration, Instant}; + let mut destination = tempfile::tempfile().expect("output file"); + let mut command = fixture_command("/bin/sh"); + command + .args(["-c", "printf '%s' \"$MARKER\"; exit 7"]) + .env("MARKER", "caller-configured-output") + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .stdout(Stdio::from(destination.try_clone().unwrap())); + let mut child = spawn(&mut command).expect("foreground spawn"); + assert!( + child.stdout.is_none(), + "file binding must not become a capture pipe" + ); + let deadline = Instant::now() + Duration::from_secs(5); + let status = loop { + match child.try_wait() { + Ok(Some(status)) => break status, + Ok(None) if Instant::now() < deadline => { + std::thread::sleep(Duration::from_millis(10)); + } + outcome => { + let _ = child.kill(); + let _ = child.wait(); + panic!("foreground fixture did not complete: {outcome:?}"); + } + } + }; + assert_eq!(status.code(), Some(7)); + destination.rewind().unwrap(); + let mut bytes = Vec::new(); + destination.read_to_end(&mut bytes).unwrap(); + assert_eq!(bytes, b"caller-configured-output"); + } + + #[test] + fn exec_returns_native_missing_image_error() { + let directory = tempfile::tempdir().expect("private fixture directory"); + let mut command = fixture_command(directory.path().join("absent-executable")); + // No stdio/cwd/env/hook changes: the failed exec leaves this test's + // process configuration untouched, and never runs another image. + let error = exec(&mut command); + assert_eq!(error.kind(), io::ErrorKind::NotFound); + assert_eq!(error.raw_os_error(), Some(libc::ENOENT)); + } + + #[test] + fn output_preserves_cwd_environment_literal_argv_and_native_nonzero() { + let directory = tempfile::tempdir().expect("fixture cwd"); + let cwd = directory.path().canonicalize().expect("physical cwd"); + let mut command = fixture_command("/bin/sh"); + command.args(["-c", "[ \"$#\" = 2 ] && [ -z \"$2\" ] || exit 9; printf '%s\\n%s\\n%s\\n' \"$PWD\" \"$MARKER\" \"$1\"; exit 7", "foreground", "$literal; not a command", ""]); + command.current_dir(&cwd).env("MARKER", "foreground-env"); + let output = output(&mut command).expect("foreground output"); + assert_eq!(output.status.code(), Some(7)); + let expected = format!( + "{}\nforeground-env\n$literal; not a command\n", + cwd.display() + ); + assert_eq!(output.stdout, expected.as_bytes()); + assert!(output.stderr.is_empty()); + } + + #[test] + fn both_execution_forms_preserve_explicit_stdio_overrides() { + use std::io::{Read, Seek, Write}; + use std::process::Stdio; + for capture in [false, true] { + let mut input = tempfile::tempfile().unwrap(); + input.write_all(b"caller-controlled input").unwrap(); + input.rewind().unwrap(); + let mut destination = tempfile::tempfile().unwrap(); + let mut command = fixture_command("/bin/sh"); + command + .args(["-c", "cat; exit 7"]) + .stdin(Stdio::from(input)) + .stdout(Stdio::from(destination.try_clone().unwrap())) + .stderr(Stdio::null()); + if capture { + let captured = output(&mut command).unwrap(); + assert_eq!(captured.status.code(), Some(7)); + assert!(captured.stdout.is_empty()); + assert!(captured.stderr.is_empty()); + } else { + assert_eq!(status(&mut command).unwrap().code(), Some(7)); + } + destination.rewind().unwrap(); + let mut bytes = Vec::new(); + destination.read_to_end(&mut bytes).unwrap(); + assert_eq!(bytes, b"caller-controlled input"); + } + } + + #[test] + fn foreground_keeps_parent_process_group_and_session() { + let parent = std::fs::read_to_string("/proc/self/stat").expect("parent stat"); + let mut command = fixture_command("/bin/sh"); + command.args(["-c", "cut -d' ' -f5,6 /proc/self/stat"]); + let output = output(&mut command).expect("foreground child"); + let parent_fields: Vec<_> = parent + .rsplit_once(") ") + .expect("stat") + .1 + .split_whitespace() + .collect(); + let child = String::from_utf8_lossy(&output.stdout); + assert_eq!( + child.trim(), + format!("{} {}", parent_fields[2], parent_fields[3]) + ); + } + + #[test] + fn foreground_does_not_close_an_inheritable_high_descriptor() { + use std::io::{Seek, Write}; + use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; + let mut file = tempfile::tempfile().expect("fixture file"); + file.write_all(b"inherited-resource").unwrap(); + file.rewind().unwrap(); + // SAFETY: F_DUPFD duplicates the live borrowed descriptor; it does + // not access userspace memory or change the source descriptor. + let descriptor = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_DUPFD, 64) }; + assert!(descriptor >= 64); + // SAFETY: F_DUPFD returned a distinct owned descriptor. + let descriptor = unsafe { OwnedFd::from_raw_fd(descriptor) }; + let mut command = fixture_command("/bin/sh"); + command.args([ + "-c", + "cat \"/proc/self/fd/$1\"", + "foreground", + &descriptor.as_raw_fd().to_string(), + ]); + let captured = output(&mut command).expect("inherited descriptor read"); + assert!(captured.status.success()); + assert_eq!(captured.stdout, b"inherited-resource"); + } +} diff --git a/crates/running-process-platform-internal/src/platform_macos.rs b/crates/running-process-platform-internal/src/platform_macos.rs index b1ba7347..749daa68 100644 --- a/crates/running-process-platform-internal/src/platform_macos.rs +++ b/crates/running-process-platform-internal/src/platform_macos.rs @@ -16,6 +16,14 @@ pub(crate) use independent_io::open_regular as independent_open_regular; #[cfg(feature = "independent-spawn")] pub(crate) const INDEPENDENT_ZERO_WRITE_PENDING: bool = false; +#[path = "platform_macos/foreground.rs"] +pub(crate) mod foreground; + +/// Native niceness for the portable `ProcessPriority::Low` intent. +pub(crate) const PRIORITY_NICE_LOW: i32 = 10; +/// Native niceness for the portable `ProcessPriority::High` intent. +pub(crate) const PRIORITY_NICE_HIGH: i32 = -5; + #[path = "platform_macos/autostart.rs"] pub(crate) mod autostart; diff --git a/crates/running-process-platform-internal/src/platform_macos/foreground.rs b/crates/running-process-platform-internal/src/platform_macos/foreground.rs new file mode 100644 index 00000000..4f868856 --- /dev/null +++ b/crates/running-process-platform-internal/src/platform_macos/foreground.rs @@ -0,0 +1,119 @@ +//! macOS foreground execution: Unix process-image replacement. + +use std::io; +use std::process::Command; + +/// Replace the current Unix process image using the supplied native command. +/// Success never returns; failure returns the original OS error. This does not +/// spawn a child or change the caller's requested argv, environment, stdio, +/// pre-exec hooks, process group, or session policy. As with `CommandExt::exec`, +/// failed execution may leave setup changes applied to the current process. +pub fn exec(command: &mut Command) -> io::Error { + use std::os::unix::process::CommandExt; + command.exec() +} + +#[cfg(test)] +mod tests { + use super::exec; + use crate::foreground::{output, spawn, status}; + use std::io; + use crate::foreground::tests::fixture_command; + + #[test] + fn spawn_transfers_child_and_preserves_configured_output() { + use std::io::{Read, Seek}; + use std::process::Stdio; + use std::time::{Duration, Instant}; + let mut destination = tempfile::tempfile().expect("output file"); + let mut command = fixture_command("/bin/sh"); + command + .args(["-c", "printf '%s' \"$MARKER\"; exit 7"]) + .env("MARKER", "caller-configured-output") + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .stdout(Stdio::from(destination.try_clone().unwrap())); + let mut child = spawn(&mut command).expect("foreground spawn"); + assert!( + child.stdout.is_none(), + "file binding must not become a capture pipe" + ); + let deadline = Instant::now() + Duration::from_secs(5); + let status = loop { + match child.try_wait() { + Ok(Some(status)) => break status, + Ok(None) if Instant::now() < deadline => { + std::thread::sleep(Duration::from_millis(10)); + } + outcome => { + let _ = child.kill(); + let _ = child.wait(); + panic!("foreground fixture did not complete: {outcome:?}"); + } + } + }; + assert_eq!(status.code(), Some(7)); + destination.rewind().unwrap(); + let mut bytes = Vec::new(); + destination.read_to_end(&mut bytes).unwrap(); + assert_eq!(bytes, b"caller-configured-output"); + } + + #[test] + fn exec_returns_native_missing_image_error() { + let directory = tempfile::tempdir().expect("private fixture directory"); + let mut command = fixture_command(directory.path().join("absent-executable")); + // No stdio/cwd/env/hook changes: the failed exec leaves this test's + // process configuration untouched, and never runs another image. + let error = exec(&mut command); + assert_eq!(error.kind(), io::ErrorKind::NotFound); + assert_eq!(error.raw_os_error(), Some(libc::ENOENT)); + } + + #[test] + fn output_preserves_cwd_environment_literal_argv_and_native_nonzero() { + let directory = tempfile::tempdir().expect("fixture cwd"); + let cwd = directory.path().canonicalize().expect("physical cwd"); + let mut command = fixture_command("/bin/sh"); + command.args(["-c", "[ \"$#\" = 2 ] && [ -z \"$2\" ] || exit 9; printf '%s\\n%s\\n%s\\n' \"$PWD\" \"$MARKER\" \"$1\"; exit 7", "foreground", "$literal; not a command", ""]); + command.current_dir(&cwd).env("MARKER", "foreground-env"); + let output = output(&mut command).expect("foreground output"); + assert_eq!(output.status.code(), Some(7)); + let expected = format!( + "{}\nforeground-env\n$literal; not a command\n", + cwd.display() + ); + assert_eq!(output.stdout, expected.as_bytes()); + assert!(output.stderr.is_empty()); + } + + #[test] + fn both_execution_forms_preserve_explicit_stdio_overrides() { + use std::io::{Read, Seek, Write}; + use std::process::Stdio; + for capture in [false, true] { + let mut input = tempfile::tempfile().unwrap(); + input.write_all(b"caller-controlled input").unwrap(); + input.rewind().unwrap(); + let mut destination = tempfile::tempfile().unwrap(); + let mut command = fixture_command("/bin/sh"); + command + .args(["-c", "cat; exit 7"]) + .stdin(Stdio::from(input)) + .stdout(Stdio::from(destination.try_clone().unwrap())) + .stderr(Stdio::null()); + if capture { + let captured = output(&mut command).unwrap(); + assert_eq!(captured.status.code(), Some(7)); + assert!(captured.stdout.is_empty()); + assert!(captured.stderr.is_empty()); + } else { + assert_eq!(status(&mut command).unwrap().code(), Some(7)); + } + destination.rewind().unwrap(); + let mut bytes = Vec::new(); + destination.read_to_end(&mut bytes).unwrap(); + assert_eq!(bytes, b"caller-controlled input"); + } + } +} diff --git a/crates/running-process-platform-internal/src/platform_win.rs b/crates/running-process-platform-internal/src/platform_win.rs index efd8af68..99d575c4 100644 --- a/crates/running-process-platform-internal/src/platform_win.rs +++ b/crates/running-process-platform-internal/src/platform_win.rs @@ -19,6 +19,14 @@ pub(crate) use independent_io::open_regular as independent_open_regular; #[cfg(feature = "independent-spawn")] pub(crate) const INDEPENDENT_ZERO_WRITE_PENDING: bool = true; +#[path = "platform_win/foreground.rs"] +pub(crate) mod foreground; + +/// Native niceness for the portable `ProcessPriority::Low` intent. +pub(crate) const PRIORITY_NICE_LOW: i32 = 1; +/// Native niceness for the portable `ProcessPriority::High` intent. +pub(crate) const PRIORITY_NICE_HIGH: i32 = -15; + #[path = "platform_win/autostart.rs"] pub(crate) mod autostart; diff --git a/crates/running-process-platform-internal/src/platform_win/foreground.rs b/crates/running-process-platform-internal/src/platform_win/foreground.rs new file mode 100644 index 00000000..e5faa640 --- /dev/null +++ b/crates/running-process-platform-internal/src/platform_win/foreground.rs @@ -0,0 +1,34 @@ +//! Windows foreground execution. +//! +//! Windows has no in-place process-image replacement, so this tree exports no +//! `exec`; the neutral facade's spawn/status/output cover the host. + +#[cfg(test)] +mod tests { + use crate::foreground::output; + use crate::foreground::tests::fixture_command; + + #[test] + fn output_preserves_windows_environment_cwd_and_nonzero_status() { + let directory = tempfile::tempdir().expect("fixture cwd"); + let mut command = fixture_command("cmd.exe"); + // /D disables user AutoRun commands; CD emits the actual child cwd. + command.args(["/D", "/C", "cd & echo %MARKER% & exit /b 7"]); + command + .current_dir(directory.path()) + .env("MARKER", "foreground-env"); + let captured = output(&mut command).expect("foreground output"); + assert_eq!(captured.status.code(), Some(7)); + let text = String::from_utf8_lossy(&captured.stdout); + let mut lines = text.lines(); + let actual = std::path::Path::new(lines.next().expect("cwd line")); + assert_eq!( + actual.canonicalize().unwrap(), + directory.path().canonicalize().unwrap() + ); + assert_eq!( + lines.next().expect("environment line").trim(), + "foreground-env" + ); + } +} diff --git a/crates/running-process-platform-internal/src/semantic_priority.rs b/crates/running-process-platform-internal/src/semantic_priority.rs index b2a338e2..2df4a9de 100644 --- a/crates/running-process-platform-internal/src/semantic_priority.rs +++ b/crates/running-process-platform-internal/src/semantic_priority.rs @@ -21,8 +21,8 @@ impl ProcessPriority { match self { Self::Normal => None, Self::Idle => Some(19), - Self::Low => Some(if cfg!(windows) { 1 } else { 10 }), - Self::High => Some(if cfg!(windows) { -15 } else { -5 }), + Self::Low => Some(crate::PRIORITY_NICE_LOW), + Self::High => Some(crate::PRIORITY_NICE_HIGH), } } } diff --git a/docs/api_snapshot_rust.txt b/docs/api_snapshot_rust.txt index b5aedb50..317a3a39 100644 --- a/docs/api_snapshot_rust.txt +++ b/docs/api_snapshot_rust.txt @@ -5,7 +5,7 @@ ## running_process re-exports - pub use async_process::{ AsyncCapturedOutput, AsyncProcess, AsyncProcessBuilder, AsyncProcessSession, AsyncProcessSessionChunk, AsyncProcessSessionControl, AsyncProcessSessionEvent, AsyncProcessSessionOptions, AsyncProcessSessionOutput, AsyncStdio, }; + pub use async_process::{ AsyncCapturedOutput, AsyncProcess, AsyncProcessBuilder, AsyncProcessSession, AsyncProcessSessionChunk, AsyncProcessSessionControl, AsyncProcessSessionEvent, AsyncProcessSessionOptions, AsyncProcessSessionOutput, AsyncStdio, ProcessTreeKill, }; pub use blocking_island::dispatch_blocking as blocking_island_dispatch; pub use bounded::{ run_command, run_command_bounded, run_std_command_bounded, run_std_command_bounded_with_options, BoundedRunOptions, }; pub use console_detect::{monitor_console_windows, ConsoleWindowInfo}; @@ -15,10 +15,18 @@ pub use observer::{ CapabilitySupport, CaptureSource, CategoryCapability, DumpResult, EventCategory, ObservationGrade, ObservationPolicy, ObserverCapabilities, ObserverConfig, ObserverEvent, ObserverEventKind, ObserverSubscriber, ProcessEvent, ProcessEventKind, ProcessIdentity, ProcessObservation, ProcessObservationCapabilities, ProcessObservationError, ProcessWatch, ProcessWatchConfigurationError, ProcessWatchCursor, ProcessWatchGap, ProcessWatchLoss, ProcessWatchMatch, ProcessWatchRead, ProcessWatchSubscriber, StackCapture, StackDump, }; pub use originator::{ find_declared_daemon_pids, find_processes_by_originator, OriginatorProcessInfo, }; pub use output_log::{ CursorRead, OutputCursor, OutputLog, OutputRecord, SharedOutputCursor, SharedOutputLog, }; + pub use running_process_platform_internal::ProcessLiveness; + pub use running_process_platform_internal::ProcessPriority; + pub use running_process_platform_internal::SpawnAdmission; + pub use running_process_platform_internal::exit_code as native_exit_code; + pub use running_process_platform_internal::foreground; pub use running_process_platform_internal::platform::executable as platform_executable; pub use running_process_platform_internal::platform::process::current_executable_build_id; + pub use running_process_platform_internal::platform::process::{ ProcessInspectError, ProcessInspectErrorKind, }; + pub use running_process_platform_internal::process_executable_path; + pub use running_process_platform_internal::process_same_executable_path; pub use rust_debug::{render_rust_debug_traces, RustDebugScopeGuard}; - pub use spawn::{ spawn, spawn_daemon, spawn_daemon_breaking_away_from_job, spawn_daemon_breaking_away_with_env_policy, spawn_daemon_with_clear_env, spawn_daemon_with_env_policy, spawn_daemon_with_stdio, spawn_daemon_with_stdio_and_env_policy, spawn_with_env_policy, DaemonChild, DaemonStdio, DaemonStdioSource, EnvironmentPolicy, SpawnStdio, SpawnedChild, StdioSource, DAEMON_MARKER_ENV_VAR, }; + pub use spawn::{ spawn, spawn_daemon, spawn_daemon_breaking_away_from_job, spawn_daemon_breaking_away_with_env_policy, spawn_daemon_with_clear_env, spawn_daemon_with_env_policy, spawn_daemon_with_environment, spawn_daemon_with_explicit_environment, spawn_daemon_with_stdio, spawn_daemon_with_stdio_and_env_policy, spawn_with_env_policy, spawn_with_environment, spawn_with_explicit_environment, DaemonChild, DaemonStdio, DaemonStdioSource, EnvironmentPolicy, SpawnStdio, SpawnedChild, SpawnedChildControl, StdioSource, SyncEnvironment, DAEMON_MARKER_ENV_VAR, }; pub use spawn::{spawn_tokio, TokioSpawnOptions}; pub use spawn_contract::{IndependentBackend, SpawnLifetime, SpawnMode, SpawnOptions}; pub use spawn_dispatch::{spawn_with_options, SpawnExit, SpawnHandle}; diff --git a/lints/running-process-platform-boundary/src/baseline.txt b/lints/running-process-platform-boundary/src/baseline.txt index ea68db9b..79c23933 100644 --- a/lints/running-process-platform-boundary/src/baseline.txt +++ b/lints/running-process-platform-boundary/src/baseline.txt @@ -430,17 +430,3 @@ crates/running-process/src/tests.rs native_import libc 14 crates/running-process/src/tests.rs native_import libc 15 crates/running-process/src/tests.rs native_import libc 16 crates/running-process/src/tests.rs native_import std::os::windows 0 -crates/running-process-platform-internal/src/foreground.rs attr_cfg target_os 0 -crates/running-process-platform-internal/src/foreground.rs attr_cfg target_os 1 -crates/running-process-platform-internal/src/foreground.rs attr_cfg unix 0 -crates/running-process-platform-internal/src/foreground.rs attr_cfg unix 1 -crates/running-process-platform-internal/src/foreground.rs attr_cfg unix 2 -crates/running-process-platform-internal/src/foreground.rs attr_cfg unix 3 -crates/running-process-platform-internal/src/foreground.rs attr_cfg unix 4 -crates/running-process-platform-internal/src/foreground.rs attr_cfg windows 0 -crates/running-process-platform-internal/src/foreground.rs native_import libc 0 -crates/running-process-platform-internal/src/foreground.rs native_import libc 1 -crates/running-process-platform-internal/src/foreground.rs native_import libc 2 -crates/running-process-platform-internal/src/foreground.rs native_import std::os::unix 0 -crates/running-process-platform-internal/src/semantic_priority.rs cfg_macro windows 0 -crates/running-process-platform-internal/src/semantic_priority.rs cfg_macro windows 1