optimization_job_api/
process.rs1use std::{
3 io::{self, Read, Write},
4 process::{Child, Command, ExitStatus, Stdio},
5 thread,
6 time::{Duration, Instant},
7};
8pub fn group(command: &mut Command) {
9 #[cfg(unix)]
10 {
11 use std::os::unix::process::CommandExt;
12 command.process_group(0);
13 }
14}
15pub fn terminate_process_group(child: &mut Child) {
16 #[cfg(unix)]
17 {
18 if let Ok(pid) = i32::try_from(child.id()) {
19 unsafe {
20 libc::kill(-pid, libc::SIGKILL);
21 }
22 }
23 }
24 let _ = child.kill();
25}
26pub fn drain(mut reader: impl Read, maximum: usize) -> io::Result<(Vec<u8>, bool)> {
28 let mut out = Vec::new();
29 let mut overflow = false;
30 let mut buf = [0u8; 8192];
31 loop {
32 let n = reader.read(&mut buf)?;
33 if n == 0 {
34 break;
35 }
36 let keep = n.min(maximum.saturating_sub(out.len()));
37 out.extend_from_slice(&buf[..keep]);
38 overflow |= keep < n;
39 }
40 Ok((out, overflow))
41}
42pub fn bounded_command(
43 argv: &[String],
44 input: Vec<u8>,
45 timeout: Duration,
46 maximum: usize,
47) -> io::Result<(ExitStatus, Vec<u8>, Vec<u8>)> {
48 bounded_command_until(argv, input, timeout, maximum, &|| false)
49}
50pub fn bounded_command_until(
54 argv: &[String],
55 input: Vec<u8>,
56 timeout: Duration,
57 maximum: usize,
58 abort: &dyn Fn() -> bool,
59) -> io::Result<(ExitStatus, Vec<u8>, Vec<u8>)> {
60 let exe = argv.first().ok_or_else(|| io::Error::other("empty argv"))?;
61 let mut command = Command::new(exe);
62 command
63 .args(&argv[1..])
64 .stdin(Stdio::piped())
65 .stdout(Stdio::piped())
66 .stderr(Stdio::piped());
67 group(&mut command);
68 let mut child = command.spawn()?;
69 let mut stdin = child.stdin.take().unwrap();
70 let writer = thread::spawn(move || stdin.write_all(&input));
71 let stdout = child.stdout.take().unwrap();
72 let stderr = child.stderr.take().unwrap();
73 let a = thread::spawn(move || drain(stdout, maximum));
74 let b = thread::spawn(move || drain(stderr, maximum));
75 let start = Instant::now();
76 let mut aborted = false;
77 let finished = loop {
78 if exited(&mut child)? {
79 break true;
80 }
81 if abort() {
82 aborted = true;
83 break false;
84 }
85 if start.elapsed() >= timeout {
86 break false;
87 }
88 thread::sleep(Duration::from_millis(10));
89 };
90 terminate_process_group(&mut child);
95 let status = child.wait();
96 let write_result = writer
97 .join()
98 .map_err(|_| io::Error::other("stdin thread panicked"))?;
99 let (out, oa) = a
100 .join()
101 .map_err(|_| io::Error::other("stdout thread panicked"))??;
102 let (err, ob) = b
103 .join()
104 .map_err(|_| io::Error::other("stderr thread panicked"))??;
105 if aborted {
106 return Err(io::Error::new(
107 io::ErrorKind::Interrupted,
108 "command interrupted",
109 ));
110 }
111 let status = finished
112 .then_some(status?)
113 .ok_or_else(|| io::Error::new(io::ErrorKind::TimedOut, "command deadline exceeded"))?;
114 write_result?;
115 if oa || ob {
116 return Err(io::Error::other("command stream exceeds configured cap"));
117 }
118 Ok((status, out, err))
119}
120#[cfg(target_os = "linux")]
123fn exited(child: &mut Child) -> io::Result<bool> {
124 let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
125 let rc = unsafe {
126 libc::waitid(
127 libc::P_PID,
128 child.id() as libc::id_t,
129 &mut info,
130 libc::WEXITED | libc::WNOWAIT | libc::WNOHANG,
131 )
132 };
133 if rc < 0 {
134 return Err(io::Error::last_os_error());
135 }
136 Ok(unsafe { info.si_pid() } != 0)
137}
138#[cfg(not(target_os = "linux"))]
139fn exited(child: &mut Child) -> io::Result<bool> {
140 Ok(child.try_wait()?.is_some())
141}
142pub fn drain_file(
144 mut reader: impl Read,
145 mut file: std::fs::File,
146 maximum: usize,
147) -> io::Result<()> {
148 let mut kept = 0;
149 let mut buf = [0u8; 8192];
150 loop {
151 let n = reader.read(&mut buf)?;
152 if n == 0 {
153 break;
154 }
155 let k = n.min(maximum.saturating_sub(kept));
156 file.write_all(&buf[..k])?;
157 kept += k;
158 }
159 file.sync_data()
160}
161#[cfg(target_os = "linux")]
165pub fn group_stopped(pgid: u32) -> io::Result<bool> {
166 let rc = unsafe { libc::kill(-(pgid as i32), 0) };
168 if rc < 0 {
169 let e = io::Error::last_os_error();
170 return if e.raw_os_error() == Some(libc::ESRCH) {
171 Ok(true)
172 } else {
173 Err(e)
174 };
175 }
176 let namespace = std::fs::read_link("/proc/self/ns/pid")?;
177 let own_status = std::fs::read_to_string("/proc/self/status")?;
178 let depth = own_status
179 .lines()
180 .find_map(|l| l.strip_prefix("NSpgid:"))
181 .ok_or_else(|| io::Error::other("NSpgid unavailable"))?
182 .split_whitespace()
183 .count();
184 for entry in std::fs::read_dir("/proc")? {
185 let entry = entry?;
186 if entry.file_name().to_string_lossy().parse::<u32>().is_err() {
187 continue;
188 }
189 let vanished = |e: &io::Error| {
191 e.kind() == io::ErrorKind::NotFound || e.raw_os_error() == Some(libc::ESRCH)
192 };
193 let status = match std::fs::read_to_string(entry.path().join("status")) {
194 Ok(s) => s,
195 Err(e) if vanished(&e) => continue,
196 Err(e) => return Err(e),
197 };
198 let groups: Vec<_> = status
199 .lines()
200 .find_map(|l| l.strip_prefix("NSpgid:"))
201 .unwrap_or("")
202 .split_whitespace()
203 .collect();
204 if groups.len() != depth || groups.last().and_then(|s| s.parse::<u32>().ok()) != Some(pgid)
205 {
206 continue;
207 }
208 let other = match std::fs::read_link(entry.path().join("ns/pid")) {
209 Ok(n) => n,
210 Err(e) if vanished(&e) => continue,
211 Err(e) => return Err(e),
212 };
213 if other != namespace {
214 continue;
215 }
216 let state = status
217 .lines()
218 .find_map(|l| l.strip_prefix("State:"))
219 .and_then(|s| s.split_whitespace().next());
220 if state.is_none() {
221 return Err(io::Error::other("process state unavailable"));
222 }
223 if !matches!(state, Some("Z" | "X")) {
224 return Ok(false);
225 }
226 }
227 Ok(true)
228}
229#[cfg(not(target_os = "linux"))]
230pub fn group_stopped(_: u32) -> io::Result<bool> {
231 Err(io::Error::other("Linux process verification required"))
232}