Skip to main content

pistonite_cu/process/
child.rs

1use std::{process::ExitStatus, time::Duration};
2
3use tokio::{process::Child as TokioChild, task::JoinSet};
4
5use crate::{BoxedFuture, Context as _, co};
6
7/// A child process spawned with [`Command`](crate::Command)
8pub struct Child {
9    pub(crate) name: String,
10    pub(crate) inner: TokioChild,
11    pub(crate) io: ChildIo,
12}
13
14impl Child {
15    /// Block the thread and wait for the child to finish, and check if the ExitStatus is 0
16    ///
17    /// Note that currently any error that occurred in input/output tasks are treated
18    /// as warning and not hard error.
19    ///
20    /// # Blocking
21    /// This will block the current thread while trying to join the child.
22    /// Use [`co_wait_nz`](Self::co_wait_nz) to avoid blocking if in async context.
23    pub fn wait_nz(self) -> crate::Result<()> {
24        let status = wait_internal(&self.name, self.inner, self.io)?;
25        if !status.success() {
26            crate::bail!("{} exited with non-zero status", self.name);
27        }
28        Ok(())
29    }
30
31    /// Block the thread and wait for the child to finish,
32    /// and return its [`ExitStatus`]
33    ///
34    /// Note that currently any error that occurred in input/output tasks are treated
35    /// as warning and not hard error.
36    ///
37    /// # Blocking
38    /// This will block the current thread while trying to join the child.
39    /// Use [`co_wait`](Self::co_wait) to avoid blocking if in async context.
40    #[inline(always)]
41    pub fn wait(self) -> crate::Result<ExitStatus> {
42        wait_internal(&self.name, self.inner, self.io)
43    }
44
45    /// Wait for the child asynchronously using the current tokio runtime,
46    /// and check if the ExitStatus is 0.
47    ///
48    /// Note that currently any error that occurred in input/output tasks are treated
49    /// as warning and not hard error.
50    ///
51    /// # Panic
52    /// Will panic if called outside of a tokio runtime context
53    pub async fn co_wait_nz(self) -> crate::Result<()> {
54        let status = co_wait_internal(&self.name, self.inner, self.io).await?;
55        if !status.success() {
56            crate::bail!("{} exited with non-zero status", self.name);
57        }
58        Ok(())
59    }
60
61    /// Wait for the child asynchronously using the current tokio runtime,
62    /// and return its [`ExitStatus`]
63    ///
64    /// Note that currently any error that occurred in input/output tasks are treated
65    /// as warning and not hard error.
66    ///
67    /// # Panic
68    /// Will panic if called outside of a tokio runtime context
69    #[inline(always)]
70    pub async fn co_wait(self) -> crate::Result<ExitStatus> {
71        co_wait_internal(&self.name, self.inner, self.io).await
72    }
73
74    /// Create a wait guard that will automatically wait for the child
75    /// (and ignore the error) when going out of scope.
76    #[inline(always)]
77    pub fn wait_guard(self) -> ChildWaitGuard {
78        ChildWaitGuard { inner: Some(self) }
79    }
80
81    /// Wait for the child to exit for at least the timeout.
82    /// Return the exit status if exited
83    pub async fn co_wait_timeout(
84        &mut self,
85        timeout: Duration,
86    ) -> crate::Result<Option<ExitStatus>> {
87        let ms = 100;
88        let mut total_ms = 0;
89        loop {
90            match self.inner.try_wait() {
91                Ok(Some(s)) => return Ok(Some(s)),
92                Ok(None) => {}
93                Err(e) => {
94                    crate::rethrow!(e, "io error while waiting {}", self.name)
95                }
96            }
97            tokio::time::sleep(Duration::from_millis(ms)).await;
98            total_ms += ms;
99            if Duration::from_millis(total_ms) >= timeout {
100                break;
101            }
102        }
103        Ok(None)
104    }
105
106    pub fn wait_timeout(&mut self, timeout: Duration) -> crate::Result<Option<ExitStatus>> {
107        let ms = 100;
108        let mut total_ms = 0;
109        loop {
110            match self.inner.try_wait() {
111                Ok(Some(s)) => return Ok(Some(s)),
112                Ok(None) => {}
113                Err(e) => {
114                    crate::rethrow!(e, "io error while waiting {}", self.name)
115                }
116            }
117            std::thread::sleep(Duration::from_millis(ms));
118            total_ms += ms;
119            if Duration::from_millis(total_ms) >= timeout {
120                break;
121            }
122        }
123        Ok(None)
124    }
125
126    /// Kill the child and wait for it to exit asynchronously. Return the exit status
127    ///
128    /// # Panic
129    /// Will panic if called outside of a tokio runtime context
130    pub async fn co_kill(mut self) -> crate::Result<ExitStatus> {
131        let mut ms = 100;
132        for i in 0..5 {
133            crate::trace!("trying to kill child '{}', attempt {}", self.name, i + 1);
134            crate::check!(
135                self.inner.start_kill(),
136                "failed to send kill signal to child"
137            )?;
138            match self.inner.try_wait() {
139                Ok(Some(s)) => return Ok(s),
140                Ok(None) => {}
141                Err(e) => {
142                    crate::rethrow!(e, "io error while killing {}", self.name)
143                }
144            }
145            tokio::time::sleep(Duration::from_millis(ms)).await;
146            ms *= 4;
147        }
148        self.io.co_join(&self.name).await;
149        crate::bail!("failed to kill child '{}' after many attempts", self.name);
150    }
151
152    /// Kill the child and block the current thread until the child exits. Return the exit status.
153    ///
154    /// # Blocking
155    /// This will block the current thread while trying to join the child.
156    /// Use [`co_kill`](Self::co_kill) to avoid blocking if in async context.
157    pub fn kill(mut self) -> crate::Result<ExitStatus> {
158        let mut ms = 100;
159        for i in 0..5 {
160            crate::trace!("trying to kill child '{}', attempt {}", self.name, i + 1);
161            crate::check!(
162                self.inner.start_kill(),
163                "failed to send kill signal to child"
164            )?;
165            match self.inner.try_wait() {
166                Ok(Some(s)) => return Ok(s),
167                Ok(None) => {}
168                Err(e) => {
169                    crate::rethrow!(e, "io error while killing {}", self.name)
170                }
171            }
172            std::thread::sleep(Duration::from_millis(ms));
173            ms *= 4;
174        }
175        self.io.join(&self.name);
176        crate::bail!("failed to kill child '{}' after many attempts", self.name);
177    }
178}
179
180fn wait_internal(name: &str, mut child: TokioChild, io: ChildIo) -> crate::Result<ExitStatus> {
181    // consume the child by waiting
182    let wait_task = co::spawn(async move { child.wait().await });
183    // ensure the IO tasks are finished first, since blocking
184    // on child could dead lock if the child is waiting for IO
185    io.join(name);
186    crate::check!(wait_task.join()?, "io error while executing {name}")
187}
188
189async fn co_wait_internal(
190    name: &str,
191    mut child: TokioChild,
192    io: ChildIo,
193) -> crate::Result<ExitStatus> {
194    // consume the child by waiting
195    let wait_task = co::spawn(async move { child.wait().await });
196    // ensure the IO tasks are finished first, since blocking
197    // on child could dead lock if the child is waiting for IO
198    io.co_join(name).await;
199    crate::check!(
200        wait_task.co_join().await?,
201        "io error while executing {name}"
202    )
203}
204
205/// IO Task for a child
206pub(crate) struct ChildIo {
207    // (panicked, stdin_err)
208    inner: co::Handle<(bool, crate::Result<()>)>,
209}
210impl ChildIo {
211    pub fn start(
212        stdin: Option<BoxedFuture<crate::Result<()>>>,
213        stdout: Option<BoxedFuture<()>>,
214        stderr: Option<BoxedFuture<()>>,
215    ) -> Self {
216        let combined_future = async move {
217            let mut j = JoinSet::new();
218            if let Some(x) = stdin {
219                j.spawn(x);
220            }
221            if let Some(x) = stdout {
222                j.spawn(async move {
223                    x.await;
224                    Ok(())
225                });
226            }
227            if let Some(x) = stderr {
228                j.spawn(async move {
229                    x.await;
230                    Ok(())
231                });
232            }
233
234            let mut panicked = false;
235            let mut stdin_err = Ok(());
236            while let Some(x) = j.join_next().await {
237                match x {
238                    // could not join because panicked
239                    Err(_) => panicked = true,
240                    Ok(Err(e)) => {
241                        // this must be stdin because
242                        // the out tasks return ()
243                        stdin_err = Err(e);
244                    }
245                    Ok(Ok(())) => {}
246                }
247            }
248            (panicked, stdin_err)
249        };
250        let io_task = co::spawn(combined_future);
251        Self { inner: io_task }
252    }
253
254    pub fn join(self, name: &str) {
255        Self::do_join(name, self.inner.join_maybe_aborted())
256    }
257
258    pub async fn co_join(self, name: &str) {
259        Self::do_join(name, self.inner.co_join_maybe_aborted().await)
260    }
261
262    fn do_join(name: &str, result: crate::Result<Option<(bool, crate::Result<()>)>>) {
263        match result {
264            Ok(Some((panicked, stdin_err))) => {
265                if let Err(e) = stdin_err {
266                    crate::warn!("failed to write to stdin while executing {name}: {e:?}")
267                }
268                if panicked {
269                    crate::warn!("some io tasks panicked while executing {name}");
270                }
271            }
272            Ok(None) => {
273                // aborted
274            }
275            Err(_) => {
276                crate::warn!("some io tasks panicked while executing {name}");
277            }
278        }
279    }
280}
281
282/// A guard that automatically calls `wait` on a child
283/// when dropped. The result of the `wait` is ignored.
284///
285/// This can be constructed with [`Child::wait_guard`]
286pub struct ChildWaitGuard {
287    inner: Option<Child>,
288}
289impl Drop for ChildWaitGuard {
290    fn drop(&mut self) {
291        let Some(child) = self.inner.take() else {
292            return;
293        };
294        match child.wait() {
295            Err(e) => crate::trace!("wait guard: error while waiting for child: {e}"),
296            Ok(x) => crate::trace!("wait guard: child exited with status: {x}"),
297        };
298    }
299}
300impl ChildWaitGuard {
301    /// Call [`wait_nz`](Child::wait_nz) on the inner child
302    pub fn wait_nz(mut self) -> crate::Result<()> {
303        self.inner.take().unwrap().wait_nz()
304    }
305    /// Call [`wait`](Child::wait) on the inner child
306    pub fn wait(mut self) -> crate::Result<ExitStatus> {
307        self.inner.take().unwrap().wait()
308    }
309}