Skip to main content

frost_exec/
sys.rs

1//! Platform abstraction layer.
2//!
3//! Isolates all OS-specific system calls (fork, exec, pipe, dup2, open,
4//! close, wait) behind a clean interface so the rest of frost-exec
5//! remains portable across Unix variants and architectures.
6
7use std::ffi::CString;
8use std::os::fd::{IntoRawFd, RawFd};
9
10use nix::fcntl::OFlag;
11use nix::sys::stat::Mode;
12use nix::sys::wait::{self, WaitPidFlag, WaitStatus};
13use nix::unistd::{self, ForkResult, Pid};
14
15// ── Pipe ────────────────────────────────────────────────────────────
16
17/// A pipe pair as raw file descriptors.
18pub struct Pipe {
19    pub read: RawFd,
20    pub write: RawFd,
21}
22
23/// Create a pipe, returning raw file descriptors.
24pub fn pipe() -> Result<Pipe, nix::errno::Errno> {
25    let (rd, wr) = unistd::pipe()?;
26    Ok(Pipe {
27        read: rd.into_raw_fd(),
28        write: wr.into_raw_fd(),
29    })
30}
31
32// ── File descriptor operations ──────────────────────────────────────
33
34/// Duplicate `src` onto `dst`.
35pub fn dup2(src: RawFd, dst: RawFd) -> Result<RawFd, nix::errno::Errno> {
36    unistd::dup2(src, dst)
37}
38
39/// Close a file descriptor.
40pub fn close(fd: RawFd) -> Result<(), nix::errno::Errno> {
41    unistd::close(fd)
42}
43
44/// Open a file, returning a raw file descriptor.
45pub fn open(path: &std::ffi::CStr, flags: OFlag, mode: Mode) -> Result<RawFd, nix::errno::Errno> {
46    let owned = nix::fcntl::open(path, flags, mode)?;
47    Ok(owned.into_raw_fd())
48}
49
50/// Duplicate `src` onto `dst`, then close `src` if they differ.
51pub fn dup2_and_close(src: RawFd, dst: RawFd) -> Result<(), nix::errno::Errno> {
52    if src != dst {
53        dup2(src, dst)?;
54        close(src)?;
55    }
56    Ok(())
57}
58
59// ── Fork ────────────────────────────────────────────────────────────
60
61/// Result of a fork operation.
62pub enum ForkOutcome {
63    Child,
64    Parent { child_pid: Pid },
65}
66
67/// Fork the current process.
68///
69/// # Safety
70///
71/// Caller must ensure fork safety — no async-signal-unsafe operations
72/// in the child between `fork()` and `exec()`.
73pub unsafe fn fork() -> Result<ForkOutcome, nix::errno::Errno> {
74    match unsafe { unistd::fork() }? {
75        ForkResult::Child => Ok(ForkOutcome::Child),
76        ForkResult::Parent { child } => Ok(ForkOutcome::Parent { child_pid: child }),
77    }
78}
79
80// ── Exec ────────────────────────────────────────────────────────────
81
82/// Replace the current process image with a new program.
83///
84/// If `argv[0]` contains a `/`, uses it as a direct path. Otherwise
85/// searches `PATH` (extracted from `envp`) for the binary.
86///
87/// Uses `execve(2)` on all platforms — no reliance on platform-specific
88/// variants like `execvpe` (Linux-only) or `execvp` (inherits process
89/// env instead of using the shell's `envp`).
90///
91/// Does not return on success.
92pub fn exec(argv: &[CString], envp: &[CString]) -> nix::errno::Errno {
93    let Some(cmd) = argv.first() else {
94        return nix::errno::Errno::ENOENT;
95    };
96
97    if cmd.as_bytes().contains(&b'/') {
98        // Direct path — exec immediately.
99        match unistd::execve(cmd, argv, envp) {
100            Ok(infallible) => match infallible {},
101            Err(e) => e,
102        }
103    } else {
104        // Search PATH from the shell's environment.
105        let path_val = envp
106            .iter()
107            .find_map(|entry| {
108                let bytes = entry.as_bytes();
109                bytes
110                    .starts_with(b"PATH=")
111                    .then(|| String::from_utf8_lossy(&bytes[5..]).into_owned())
112            })
113            .unwrap_or_else(|| "/usr/bin:/bin".into());
114
115        let cmd_str = cmd.to_string_lossy();
116        let mut last_err = nix::errno::Errno::ENOENT;
117
118        for dir in path_val.split(':') {
119            let Ok(full_path) = CString::new(format!("{dir}/{cmd_str}")) else {
120                continue;
121            };
122            match unistd::execve(&full_path, argv, envp) {
123                Ok(infallible) => match infallible {},
124                Err(nix::errno::Errno::ENOENT | nix::errno::Errno::EACCES) => continue,
125                Err(e) => {
126                    last_err = e;
127                    break;
128                }
129            }
130        }
131        last_err
132    }
133}
134
135// ── Wait ────────────────────────────────────────────────────────────
136
137/// Outcome of waiting for a child process.
138pub enum ChildStatus {
139    /// Exited normally with a status code.
140    Exited(i32),
141    /// Killed by a signal (value is 128 + signal number).
142    Signaled(i32),
143    /// Stopped (e.g. SIGTSTP).
144    Stopped,
145    /// Still alive (non-blocking wait only).
146    Running,
147}
148
149/// Wait for a specific child process (blocking).
150pub fn wait_pid(pid: Pid) -> Result<ChildStatus, nix::errno::Errno> {
151    match wait::waitpid(pid, None)? {
152        WaitStatus::Exited(_, code) => Ok(ChildStatus::Exited(code)),
153        WaitStatus::Signaled(_, sig, _) => Ok(ChildStatus::Signaled(128 + sig as i32)),
154        WaitStatus::Stopped(_, _) => Ok(ChildStatus::Stopped),
155        _ => Ok(ChildStatus::Exited(0)),
156    }
157}
158
159/// Wait for a child process (non-blocking).
160pub fn try_wait_pid(pid: Pid) -> Result<ChildStatus, nix::errno::Errno> {
161    match wait::waitpid(pid, Some(WaitPidFlag::WNOHANG | WaitPidFlag::WUNTRACED))? {
162        WaitStatus::Exited(_, code) => Ok(ChildStatus::Exited(code)),
163        WaitStatus::Signaled(_, sig, _) => Ok(ChildStatus::Signaled(128 + sig as i32)),
164        WaitStatus::Stopped(_, _) => Ok(ChildStatus::Stopped),
165        WaitStatus::StillAlive => Ok(ChildStatus::Running),
166        _ => Ok(ChildStatus::Running),
167    }
168}