Skip to main content

rpi_plugin_sdk/
process.rs

1//! Own a process tree so inherited pipes cannot outlive their transport.
2//! The optional `process` feature keeps OS-specific dependencies out of the
3//! SDK's default ABI-only build.
4use std::process::{Child, Command};
5
6#[cfg(windows)]
7#[derive(Debug)]
8pub struct ProcessTree(std::os::windows::io::OwnedHandle);
9
10#[cfg(windows)]
11impl ProcessTree {
12    pub fn prepare(command: &mut Command) {
13        use std::os::windows::process::CommandExt;
14        use windows_sys::Win32::System::Threading::{CREATE_NO_WINDOW, CREATE_SUSPENDED};
15        // Assign the job before server code can spawn descendants or inherit pipes.
16        command.creation_flags(CREATE_NO_WINDOW | CREATE_SUSPENDED);
17    }
18    pub fn attach(child: &Child) -> Result<Self, String> {
19        use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};
20        use windows_sys::Win32::System::JobObjects::*;
21        // SAFETY: Windows creates an owned job handle; the initialized limits
22        // and Child's live process handle remain valid throughout these calls.
23        // OwnedHandle closes the job on every success or error path.
24        unsafe {
25            let job = CreateJobObjectW(std::ptr::null(), std::ptr::null());
26            if job.is_null() {
27                return Err(std::io::Error::last_os_error().to_string());
28            }
29            let tree = Self(OwnedHandle::from_raw_handle(job));
30            let mut limits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed();
31            limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
32            if SetInformationJobObject(
33                job,
34                JobObjectExtendedLimitInformation,
35                &limits as *const _ as *const _,
36                std::mem::size_of_val(&limits) as u32,
37            ) == 0
38                || AssignProcessToJobObject(job, child.as_raw_handle()) == 0
39            {
40                return Err(format!(
41                    "protect MCP process tree: {}",
42                    std::io::Error::last_os_error()
43                ));
44            }
45            resume_child(child.id())?;
46            Ok(tree)
47        }
48    }
49    pub fn kill(&self) {
50        use std::os::windows::io::AsRawHandle;
51        // SAFETY: self owns the job handle for the duration of this call.
52        unsafe {
53            windows_sys::Win32::System::JobObjects::TerminateJobObject(self.0.as_raw_handle(), 1);
54        }
55    }
56}
57
58#[cfg(unix)]
59#[derive(Debug)]
60pub struct ProcessTree(i32);
61#[cfg(unix)]
62impl ProcessTree {
63    pub fn prepare(command: &mut Command) {
64        use std::os::unix::process::CommandExt;
65        command.process_group(0);
66    }
67    pub fn attach(child: &Child) -> Result<Self, String> {
68        Ok(Self(child.id() as i32))
69    }
70    pub fn kill(&self) {
71        // SAFETY: kill accepts a scalar process-group ID and signal; no Rust
72        // memory is exposed. prepare creates the child's dedicated group.
73        unsafe {
74            libc::kill(-self.0, libc::SIGKILL);
75        }
76    }
77}
78
79#[cfg(windows)]
80fn resume_child(pid: u32) -> Result<(), String> {
81    use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};
82    use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
83    use windows_sys::Win32::System::Diagnostics::ToolHelp::*;
84    use windows_sys::Win32::System::Threading::{OpenThread, ResumeThread, THREAD_SUSPEND_RESUME};
85    // SAFETY: successful OS calls return owned handles, and enumeration writes
86    // only to the initialized local entry. RAII closes every snapshot/thread.
87    unsafe {
88        let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
89        if snapshot == INVALID_HANDLE_VALUE {
90            return Err(format!(
91                "snapshot MCP thread: {}",
92                std::io::Error::last_os_error()
93            ));
94        }
95        let snapshot = OwnedHandle::from_raw_handle(snapshot);
96        let mut entry: THREADENTRY32 = std::mem::zeroed();
97        entry.dwSize = std::mem::size_of_val(&entry) as u32;
98        let mut found = Thread32First(snapshot.as_raw_handle(), &mut entry);
99        let mut result = Err("MCP primary thread not found".into());
100        while found != 0 {
101            if entry.th32OwnerProcessID == pid {
102                let thread = OpenThread(THREAD_SUSPEND_RESUME, 0, entry.th32ThreadID);
103                if thread.is_null() {
104                    result = Err(format!(
105                        "open MCP thread: {}",
106                        std::io::Error::last_os_error()
107                    ));
108                } else {
109                    let thread = OwnedHandle::from_raw_handle(thread);
110                    let resumed = ResumeThread(thread.as_raw_handle());
111                    result = if resumed == u32::MAX {
112                        Err(format!(
113                            "resume MCP thread: {}",
114                            std::io::Error::last_os_error()
115                        ))
116                    } else {
117                        Ok(())
118                    };
119                }
120                break;
121            }
122            found = Thread32Next(snapshot.as_raw_handle(), &mut entry);
123        }
124        result
125    }
126}
127
128/// Query whether a PID still names a running process. Access failures are
129/// reported rather than treating an unqueryable process as dead.
130#[cfg(windows)]
131pub fn process_is_running(pid: u32) -> std::io::Result<bool> {
132    use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};
133    use windows_sys::Win32::System::Threading::{
134        GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
135    };
136    // SAFETY: these APIs take scalar IDs and valid owned handles; the exit
137    // code pointer is a writable local, and RAII closes the process handle.
138    unsafe {
139        let process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
140        if process.is_null() {
141            let error = std::io::Error::last_os_error();
142            return if error.raw_os_error() == Some(87) {
143                Ok(false)
144            } else {
145                Err(error)
146            };
147        }
148        let process = OwnedHandle::from_raw_handle(process);
149        let mut code = 0;
150        if GetExitCodeProcess(process.as_raw_handle(), &mut code) == 0 {
151            return Err(std::io::Error::last_os_error());
152        }
153        Ok(code == 259) // STILL_ACTIVE
154    }
155}
156
157#[cfg(all(test, windows))]
158mod tests {
159    #[test]
160    fn queries_live_and_missing_processes() {
161        assert!(super::process_is_running(std::process::id()).unwrap());
162        assert!(!super::process_is_running(0).unwrap());
163    }
164}