running_process_platform_internal/
lib.rs1use std::cfg_select;
9use std::ffi::{OsStr, OsString};
10use std::io;
11use std::path::PathBuf;
12use std::process::{ExitStatus, Output, Stdio};
13
14use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
15use tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command};
16
17pub mod platform;
22
23cfg_select! {
26 target_os = "windows" => {
27 mod platform_win;
28 pub(crate) use platform_win as platform_imp;
29 }
30 target_os = "linux" => {
31 mod platform_linux;
32 pub(crate) use platform_linux as platform_imp;
33 }
34 target_os = "macos" => {
35 mod platform_macos;
36 pub(crate) use platform_macos as platform_imp;
37 }
38}
39
40pub fn configure_compat_tokio_command(
45 command: &mut Command,
46 show_console: bool,
47 kill_when_owner_dies: bool,
48) -> io::Result<()> {
49 platform_imp::configure_compat_tokio_command(command, show_console, kill_when_owner_dies)
50}
51
52pub fn after_compat_tokio_spawn(child: &Child, kill_when_owner_dies: bool) {
54 platform_imp::after_compat_tokio_spawn(child, kill_when_owner_dies)
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum StreamMode {
60 Inherit,
62 Piped,
64 Null,
66}
67
68impl StreamMode {
69 fn apply(self) -> Stdio {
70 match self {
71 Self::Inherit => Stdio::inherit(),
72 Self::Piped => Stdio::piped(),
73 Self::Null => Stdio::null(),
74 }
75 }
76}
77
78#[derive(Debug, Clone)]
80pub struct SpawnSpec {
81 program: OsString,
82 args: Vec<OsString>,
83 current_dir: Option<PathBuf>,
84 env: Vec<(OsString, OsString)>,
85 clear_env: bool,
86 stdin: StreamMode,
87 stdout: StreamMode,
88 stderr: StreamMode,
89 create_process_group: bool,
90 kill_when_owner_dies: bool,
91}
92
93impl SpawnSpec {
94 pub fn new(program: impl Into<OsString>) -> Self {
96 Self {
97 program: program.into(),
98 args: Vec::new(),
99 current_dir: None,
100 env: Vec::new(),
101 clear_env: false,
102 stdin: StreamMode::Inherit,
103 stdout: StreamMode::Inherit,
104 stderr: StreamMode::Inherit,
105 create_process_group: false,
106 kill_when_owner_dies: false,
107 }
108 }
109
110 pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
112 self.args.push(arg.into());
113 self
114 }
115
116 pub fn current_dir(mut self, path: impl Into<PathBuf>) -> Self {
118 self.current_dir = Some(path.into());
119 self
120 }
121
122 pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
124 self.env.push((key.into(), value.into()));
125 self
126 }
127
128 pub fn clear_env(mut self, clear: bool) -> Self {
130 self.clear_env = clear;
131 self
132 }
133
134 pub fn stdin(mut self, mode: StreamMode) -> Self {
136 self.stdin = mode;
137 self
138 }
139
140 pub fn stdout(mut self, mode: StreamMode) -> Self {
142 self.stdout = mode;
143 self
144 }
145
146 pub fn stderr(mut self, mode: StreamMode) -> Self {
148 self.stderr = mode;
149 self
150 }
151
152 pub fn create_process_group(mut self, create: bool) -> Self {
161 self.create_process_group = create;
162 self
163 }
164
165 pub fn kill_when_owner_dies(mut self, kill: bool) -> Self {
172 self.kill_when_owner_dies = kill;
173 self
174 }
175
176 pub async fn spawn(self) -> io::Result<PlatformChild> {
178 let mut command = Command::new(&self.program);
179 command.args(&self.args);
180 if let Some(current_dir) = self.current_dir.as_deref() {
181 command.current_dir(current_dir);
182 }
183 if self.clear_env {
184 command.env_clear();
185 }
186 for (key, value) in &self.env {
187 command.env(key, value);
188 }
189 command
190 .stdin(self.stdin.apply())
191 .stdout(self.stdout.apply())
192 .stderr(self.stderr.apply());
193 platform_imp::configure_command(
194 &mut command,
195 self.create_process_group,
196 self.kill_when_owner_dies,
197 )?;
198
199 let child = command.spawn()?;
200 platform_imp::after_spawn(&child, self.kill_when_owner_dies);
201 Ok(PlatformChild::new(child, self.create_process_group))
202 }
203}
204
205pub struct PlatformChild {
207 child: Child,
208 stdin: Option<ChildStdin>,
209 stdout: Option<ChildStdout>,
210 stderr: Option<ChildStderr>,
211 signal: PlatformEmergencySignal,
212}
213
214impl PlatformChild {
215 fn new(mut child: Child, own_process_group: bool) -> Self {
216 let signal = PlatformEmergencySignal {
217 pid: child.id(),
218 own_process_group,
219 };
220 Self {
221 stdin: child.stdin.take(),
222 stdout: child.stdout.take(),
223 stderr: child.stderr.take(),
224 child,
225 signal,
226 }
227 }
228
229 pub fn id(&self) -> Option<u32> {
231 self.child.id()
232 }
233
234 pub async fn wait(&mut self) -> io::Result<ExitStatus> {
236 self.child.wait().await
237 }
238
239 pub async fn kill(&mut self) -> io::Result<()> {
241 self.child.kill().await
242 }
243
244 pub async fn wait_with_output(self) -> io::Result<Output> {
246 let Self {
247 mut child,
248 stdin,
249 stdout,
250 stderr,
251 ..
252 } = self;
253 drop(stdin);
256 let (status, stdout, stderr) = tokio::try_join!(
257 child.wait(),
258 read_owned_to_end(stdout),
259 read_owned_to_end(stderr),
260 )?;
261 Ok(Output {
262 status,
263 stdout,
264 stderr,
265 })
266 }
267
268 pub async fn write_stdin(&mut self, bytes: &[u8]) -> io::Result<()> {
270 let stdin = self.stdin.as_mut().ok_or_else(stdin_not_piped)?;
271 stdin.write_all(bytes).await?;
272 stdin.flush().await
273 }
274
275 pub fn close_stdin(&mut self) {
280 drop(self.stdin.take());
281 }
282
283 pub async fn read_stdout_to_end(&mut self) -> io::Result<Vec<u8>> {
285 let stdout = self.stdout.as_mut().ok_or_else(stdout_not_piped)?;
286 let mut bytes = Vec::new();
287 stdout.read_to_end(&mut bytes).await?;
288 Ok(bytes)
289 }
290
291 pub async fn read_stderr_to_end(&mut self) -> io::Result<Vec<u8>> {
293 let stderr = self.stderr.as_mut().ok_or_else(stderr_not_piped)?;
294 let mut bytes = Vec::new();
295 stderr.read_to_end(&mut bytes).await?;
296 Ok(bytes)
297 }
298
299 pub fn into_actor_parts(
305 self,
306 ) -> (
307 PlatformLifecycle,
308 PlatformEmergencySignal,
309 Option<PlatformStdin>,
310 Option<PlatformOutput>,
311 Option<PlatformOutput>,
312 ) {
313 (
314 PlatformLifecycle { child: self.child },
315 self.signal,
316 self.stdin.map(|stdin| PlatformStdin { stdin }),
317 self.stdout.map(PlatformOutput::stdout),
318 self.stderr.map(PlatformOutput::stderr),
319 )
320 }
321}
322
323pub struct PlatformLifecycle {
325 child: Child,
326}
327
328impl PlatformLifecycle {
329 pub async fn wait(&mut self) -> io::Result<ExitStatus> {
331 self.child.wait().await
332 }
333}
334
335pub struct PlatformEmergencySignal {
340 pid: Option<u32>,
341 own_process_group: bool,
342}
343
344impl PlatformEmergencySignal {
345 pub fn kill(&self) -> io::Result<()> {
347 platform_imp::signal_process(self.target()?)
348 }
349
350 pub fn terminate_group_soft(&self) -> io::Result<bool> {
359 if !self.own_process_group {
360 return Ok(false);
361 }
362 platform_imp::signal_process_group(self.target()?).map(|()| true)
363 }
364
365 fn target(&self) -> io::Result<u32> {
366 self.pid.ok_or_else(|| {
367 io::Error::new(
368 io::ErrorKind::BrokenPipe,
369 "child process no longer has an emergency signal target",
370 )
371 })
372 }
373}
374
375pub struct PlatformStdin {
377 stdin: ChildStdin,
378}
379
380impl PlatformStdin {
381 pub async fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
383 self.stdin.write_all(bytes).await?;
384 self.stdin.flush().await
385 }
386}
387
388pub struct PlatformOutput {
390 reader: OutputReader,
391}
392
393enum OutputReader {
394 Stdout(ChildStdout),
395 Stderr(ChildStderr),
396}
397
398impl PlatformOutput {
399 fn stdout(stdout: ChildStdout) -> Self {
400 Self {
401 reader: OutputReader::Stdout(stdout),
402 }
403 }
404
405 fn stderr(stderr: ChildStderr) -> Self {
406 Self {
407 reader: OutputReader::Stderr(stderr),
408 }
409 }
410
411 pub async fn read_to_end(self) -> io::Result<Vec<u8>> {
413 match self.reader {
414 OutputReader::Stdout(stdout) => read_owned_to_end(Some(stdout)).await,
415 OutputReader::Stderr(stderr) => read_owned_to_end(Some(stderr)).await,
416 }
417 }
418
419 pub async fn read_chunk(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
424 match &mut self.reader {
425 OutputReader::Stdout(stdout) => stdout.read(buffer).await,
426 OutputReader::Stderr(stderr) => stderr.read(buffer).await,
427 }
428 }
429}
430
431fn stdin_not_piped() -> io::Error {
432 io::Error::new(io::ErrorKind::BrokenPipe, "child stdin is not piped")
433}
434
435fn stdout_not_piped() -> io::Error {
436 io::Error::new(io::ErrorKind::BrokenPipe, "child stdout is not piped")
437}
438
439fn stderr_not_piped() -> io::Error {
440 io::Error::new(io::ErrorKind::BrokenPipe, "child stderr is not piped")
441}
442
443async fn read_owned_to_end<R>(reader: Option<R>) -> io::Result<Vec<u8>>
444where
445 R: AsyncRead + Unpin,
446{
447 let Some(mut reader) = reader else {
448 return Ok(Vec::new());
449 };
450 let mut bytes = Vec::new();
451 reader.read_to_end(&mut bytes).await?;
452 Ok(bytes)
453}
454
455pub fn shell_spec(command: impl AsRef<OsStr>) -> SpawnSpec {
457 platform_imp::shell_spec(command.as_ref())
458}
459
460#[cfg(test)]
461mod tests {
462 use super::{shell_spec, SpawnSpec, StreamMode};
463
464 fn fixture_command() -> SpawnSpec {
465 #[cfg(windows)]
466 {
467 shell_spec("echo async-platform-internal")
468 }
469 #[cfg(not(windows))]
470 {
471 shell_spec("printf async-platform-internal")
472 }
473 }
474
475 #[tokio::test]
476 async fn blessed_spawn_captures_output_without_sync_wait() {
477 let output = fixture_command()
478 .stdout(StreamMode::Piped)
479 .stderr(StreamMode::Piped)
480 .spawn()
481 .await
482 .expect("spawn")
483 .wait_with_output()
484 .await
485 .expect("wait with output");
486
487 assert!(output.status.success());
488 let expected = if cfg!(windows) {
489 b"async-platform-internal\r\n".as_slice()
490 } else {
491 b"async-platform-internal".as_slice()
492 };
493 assert_eq!(output.stdout, expected);
494 assert!(output.stderr.is_empty());
495 }
496
497 #[tokio::test]
498 async fn blessed_spawn_reports_missing_program() {
499 let result = SpawnSpec::new("running-process-program-that-does-not-exist")
500 .spawn()
501 .await;
502 assert!(result.is_err());
503 }
504
505 #[tokio::test]
506 async fn one_shot_output_closes_owned_stdin() {
507 #[cfg(windows)]
508 let spec = shell_spec("more > nul & echo done");
509 #[cfg(not(windows))]
510 let spec = shell_spec("cat > /dev/null; printf done");
511
512 let output = tokio::time::timeout(
513 std::time::Duration::from_secs(2),
514 spec.stdin(StreamMode::Piped)
515 .stdout(StreamMode::Piped)
516 .stderr(StreamMode::Piped)
517 .spawn()
518 .await
519 .expect("spawn")
520 .wait_with_output(),
521 )
522 .await
523 .expect("stdin is closed for one-shot output")
524 .expect("output succeeds");
525
526 let expected = if cfg!(windows) {
527 b"done\r\n".as_slice()
528 } else {
529 b"done".as_slice()
530 };
531 assert_eq!(output.stdout, expected);
532 }
533}