use std::fmt;
use std::os::windows::io::{AsHandle, BorrowedHandle};
use crate::core::job::Job;
use crate::core::session::{RootChild, KILL_EXIT_CODE};
use crate::error::{Error, Result};
use crate::status::ExitStatus;
use std::sync::Arc;
pub(crate) struct ChildCore {
child: windows_spawn::Child,
job: Arc<Job>,
pid: u32,
kill_on_drop: bool,
status: Option<ExitStatus>,
}
impl ChildCore {
pub(crate) fn from_root(root: RootChild) -> Self {
Self {
child: root.child,
job: root.job,
pid: root.pid,
kill_on_drop: root.kill_on_drop,
status: None,
}
}
pub(crate) const fn id(&self) -> u32 {
self.pid
}
#[cfg(feature = "tokio")]
pub(crate) const fn status(&self) -> Option<ExitStatus> {
self.status
}
#[cfg(feature = "blocking")]
pub(crate) fn wait_blocking(&mut self) -> Result<ExitStatus> {
if let Some(status) = self.status {
return Ok(status);
}
let status = ExitStatus::from_raw(
self.child
.wait()
.and_then(raw_exit_code)
.map_err(Error::wait)?,
);
self.status = Some(status);
Ok(status)
}
pub(crate) fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
if let Some(status) = self.status {
return Ok(Some(status));
}
let Some(status) = self.child.try_wait().map_err(Error::wait)? else {
return Ok(None);
};
let status = ExitStatus::from_raw(raw_exit_code(status).map_err(Error::wait)?);
self.status = Some(status);
Ok(Some(status))
}
#[cfg(feature = "tokio")]
pub(crate) fn cache_exit_code(&mut self, code: u32) -> ExitStatus {
let status = ExitStatus::from_raw(code);
self.status = Some(status);
status
}
pub(crate) fn kill(&self) -> Result<()> {
self.job.terminate(KILL_EXIT_CODE).map_err(Error::kill)
}
}
impl AsHandle for ChildCore {
fn as_handle(&self) -> BorrowedHandle<'_> {
self.child.as_handle()
}
}
fn raw_exit_code(status: std::process::ExitStatus) -> std::io::Result<u32> {
status
.code()
.map(|code| u32::from_ne_bytes(code.to_ne_bytes()))
.ok_or_else(|| std::io::Error::other("Windows child status did not contain an exit code"))
}
impl fmt::Debug for ChildCore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Child")
.field("pid", &self.pid)
.field("kill_on_drop", &self.kill_on_drop)
.field("status", &self.status)
.finish_non_exhaustive()
}
}
impl Drop for ChildCore {
fn drop(&mut self) {
if self.kill_on_drop {
if let Err(err) = self.job.terminate(KILL_EXIT_CODE) {
log_drop_kill_failure(&err);
}
}
}
}
#[cfg(feature = "tracing")]
fn log_drop_kill_failure(err: &std::io::Error) {
let logged = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
tracing::error!(error = %err, "failed to terminate the child job during drop");
}));
if let Err(payload) = logged {
std::mem::forget(payload);
}
}
#[cfg(not(feature = "tracing"))]
const fn log_drop_kill_failure(_err: &std::io::Error) {}
#[cfg(all(test, feature = "tracing"))]
mod tests {
use super::log_drop_kill_failure;
#[test]
fn drop_kill_failure_is_logged() {
let events = crate::tracing_test_support::count_events(|| {
log_drop_kill_failure(&std::io::Error::other("injected kill failure"));
});
assert_eq!(events, 1);
}
}