use std::io;
use std::os::windows::io::{AsHandle, OwnedHandle};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use crate::backend::BackendKind;
use crate::command::Command;
pub(crate) use crate::core::job::KILL_EXIT_CODE;
use crate::core::job::{self, Job};
use crate::core::proc;
use crate::core::pseudocon::PseudoConsole;
use crate::core::wait::{spawn_root_watcher, LEGACY_CLOSE_GRACE};
use crate::error::{Error, Result};
use crate::size::Size;
pub(crate) const CLEAR_FEATURE: &str = "ClearPseudoConsole";
#[derive(Debug)]
pub(crate) struct Session {
console: PseudoConsole,
eof_on_root_exit: bool,
spawned: AtomicBool,
attached: AtomicBool,
}
impl Session {
pub(crate) const fn new(console: PseudoConsole, eof_on_root_exit: bool) -> Self {
Self {
console,
eof_on_root_exit,
spawned: AtomicBool::new(false),
attached: AtomicBool::new(false),
}
}
pub(crate) fn size(&self) -> Size {
self.console.size()
}
pub(crate) fn resize(&self, size: Size) -> Result<()> {
self.console.resize(size).map_err(Error::resize)
}
pub(crate) fn clear(&self) -> Result<()> {
if !self.console.supports_clear() {
return Err(Error::unsupported_feature(CLEAR_FEATURE));
}
self.console.clear().map_err(Error::clear)
}
pub(crate) fn supports_clear(&self) -> bool {
self.console.supports_clear()
}
#[cfg(test)]
pub(crate) fn supports_release(&self) -> bool {
self.console.supports_release()
}
#[cfg(test)]
pub(crate) fn reader_finished(&self) -> bool {
self.console.shared().reader_finished()
}
pub(crate) fn backend_kind(&self) -> &BackendKind {
self.console.backend_kind()
}
#[cfg(any(feature = "blocking", feature = "tokio"))]
pub(crate) fn request_close_after_input(&self) {
if self.attached.load(Ordering::SeqCst) {
self.console.shared().request_close_detached();
}
}
}
#[derive(Debug)]
pub(crate) struct RootChild {
pub(super) child: windows_spawn::Child,
pub(super) job: Arc<Job>,
pub(super) pid: u32,
pub(super) kill_on_drop: bool,
}
pub(crate) fn spawn_root(
session: &Session,
command: &mut Command,
kill_on_drop: bool,
) -> Result<RootChild> {
spawn_root_with_watcher_handle(session, command, kill_on_drop, duplicate_watcher_handle)
}
fn spawn_root_with_watcher_handle<F>(
session: &Session,
command: &mut Command,
kill_on_drop: bool,
duplicate: F,
) -> Result<RootChild>
where
F: FnOnce(&windows_spawn::Child) -> io::Result<OwnedHandle>,
{
if session.spawned.swap(true, Ordering::SeqCst) {
return Err(spawn_error(
command,
io::Error::new(
io::ErrorKind::AlreadyExists,
"this pseudoconsole already hosts a root child process",
),
));
}
let (job, child) = match create_child(session, command, kill_on_drop) {
Ok(started) => started,
Err(err) => {
session.spawned.store(false, Ordering::SeqCst);
return Err(spawn_error(command, err));
},
};
session.attached.store(true, Ordering::SeqCst);
let pid = child.id();
let released = match session.console.release_after_spawn() {
Ok(released) => released,
Err(err) => {
log_release_failure(&err);
false
},
};
let close_legacy = should_close_legacy_on_root_exit(released, session.eof_on_root_exit);
if let Err(err) = arm_root_watcher(session, &child, &job, close_legacy, duplicate) {
if let Err(kill_err) = job.terminate(KILL_EXIT_CODE) {
log_spawn_cleanup_failure(&kill_err);
}
session.console.shared().request_close_detached();
return Err(spawn_error(command, err));
}
Ok(RootChild {
child,
job,
pid,
kill_on_drop,
})
}
const fn should_close_legacy_on_root_exit(released: bool, eof_on_root_exit: bool) -> bool {
!released && eof_on_root_exit
}
fn create_child(
session: &Session,
command: &mut Command,
kill_on_drop: bool,
) -> io::Result<(Arc<Job>, windows_spawn::Child)> {
let job = Arc::new(job::create(kill_on_drop)?);
let pseudoconsole = session.console.spawn_capability()?;
let child = proc::spawn(command, &pseudoconsole, &job)?;
Ok((job, child))
}
fn arm_root_watcher<F>(
session: &Session,
child: &windows_spawn::Child,
job: &Arc<Job>,
close_legacy: bool,
duplicate: F,
) -> io::Result<()>
where
F: FnOnce(&windows_spawn::Child) -> io::Result<OwnedHandle>,
{
let watched = duplicate(child)?;
spawn_root_watcher(
watched,
Arc::downgrade(job),
Arc::clone(session.console.shared()),
LEGACY_CLOSE_GRACE,
close_legacy,
)
}
fn duplicate_watcher_handle(child: &windows_spawn::Child) -> io::Result<OwnedHandle> {
child.as_handle().try_clone_to_owned()
}
fn spawn_error(command: &Command, source: io::Error) -> Error {
Error::spawn(command.get_program().to_os_string(), source)
}
#[cfg(feature = "tracing")]
fn log_release_failure(err: &io::Error) {
tracing::warn!(
error = %err,
"ReleasePseudoConsole failed; the session falls back to the legacy shutdown path"
);
}
#[cfg(not(feature = "tracing"))]
const fn log_release_failure(_err: &io::Error) {}
#[cfg(feature = "tracing")]
fn log_spawn_cleanup_failure(err: &io::Error) {
tracing::error!(
error = %err,
"failed to terminate a child after root watcher setup failed"
);
}
#[cfg(not(feature = "tracing"))]
const fn log_spawn_cleanup_failure(_err: &io::Error) {}
#[cfg(test)]
mod tests {
use std::io;
#[cfg(any(feature = "blocking", feature = "tokio"))]
use std::mem::size_of;
#[cfg(any(feature = "blocking", feature = "tokio"))]
use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle, RawHandle};
#[cfg(any(feature = "blocking", feature = "tokio"))]
use std::sync::atomic::Ordering;
#[cfg(any(feature = "blocking", feature = "tokio"))]
use std::time::{Duration, Instant};
use super::should_close_legacy_on_root_exit;
#[cfg(feature = "tracing")]
use super::{log_release_failure, log_spawn_cleanup_failure};
#[cfg(any(feature = "blocking", feature = "tokio"))]
use super::{spawn_root, spawn_root_with_watcher_handle, Session};
#[cfg(any(feature = "blocking", feature = "tokio"))]
use crate::backend::ConPtyBackend;
#[cfg(any(feature = "blocking", feature = "tokio"))]
use crate::core::pipes::{create_sync_pipes, SyncPipes};
#[cfg(any(feature = "blocking", feature = "tokio"))]
use crate::core::pseudocon::PseudoConsole;
#[cfg(any(feature = "blocking", feature = "tokio"))]
use crate::error::ErrorKind;
#[cfg(any(feature = "blocking", feature = "tokio"))]
use crate::size::Size;
#[cfg(any(feature = "blocking", feature = "tokio"))]
use windows_sys::Win32::Foundation::{INVALID_HANDLE_VALUE, WAIT_TIMEOUT};
#[cfg(any(feature = "blocking", feature = "tokio"))]
use windows_sys::Win32::Storage::FileSystem::SYNCHRONIZE;
#[cfg(any(feature = "blocking", feature = "tokio"))]
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
TH32CS_SNAPPROCESS,
};
#[cfg(any(feature = "blocking", feature = "tokio"))]
use windows_sys::Win32::System::Threading::{OpenProcess, WaitForSingleObject};
#[cfg(any(feature = "blocking", feature = "tokio"))]
fn process_is_running(pid: u32) -> bool {
let raw = unsafe { OpenProcess(SYNCHRONIZE, 0, pid) };
if raw.is_null() {
return false;
}
let process = unsafe { OwnedHandle::from_raw_handle(raw as RawHandle) };
unsafe { WaitForSingleObject(process.as_raw_handle(), 0) == WAIT_TIMEOUT }
}
#[cfg(any(feature = "blocking", feature = "tokio"))]
fn direct_child_of(root: u32) -> Option<u32> {
let raw = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) };
if raw == INVALID_HANDLE_VALUE {
return None;
}
let snapshot = unsafe { OwnedHandle::from_raw_handle(raw as RawHandle) };
let mut entry = PROCESSENTRY32W {
dwSize: u32::try_from(size_of::<PROCESSENTRY32W>())
.expect("PROCESSENTRY32W size fits in u32"),
..Default::default()
};
let mut present = unsafe { Process32FirstW(snapshot.as_raw_handle(), &mut entry) } != 0;
while present {
if entry.th32ParentProcessID == root {
return Some(entry.th32ProcessID);
}
present = unsafe { Process32NextW(snapshot.as_raw_handle(), &mut entry) } != 0;
}
None
}
#[test]
fn legacy_close_requires_both_legacy_mode_and_eof_policy() {
assert!(should_close_legacy_on_root_exit(false, true));
assert!(!should_close_legacy_on_root_exit(false, false));
assert!(!should_close_legacy_on_root_exit(true, true));
assert!(!should_close_legacy_on_root_exit(true, false));
}
#[cfg(any(feature = "blocking", feature = "tokio"))]
#[test]
fn input_retirement_requests_close_only_after_a_child_attaches() {
let backend = ConPtyBackend::system().expect("ConPTY must be available");
let SyncPipes {
conout_read,
conout_write,
conin_read,
conin_write,
} = create_sync_pipes().expect("creating pipes must succeed");
let console = PseudoConsole::new(backend, Size::default(), conin_read, conout_write, false)
.expect("CreatePseudoConsole must succeed");
let shared = std::sync::Arc::clone(console.shared());
let session = Session::new(console, true);
drop(conout_read);
shared.notify_reader_closed();
session.request_close_after_input();
assert!(
!shared.is_closed(),
"an idle session must stay usable when its writer is dropped"
);
session.attached.store(true, Ordering::SeqCst);
session.request_close_after_input();
assert!(
shared.is_closed(),
"input retirement after attachment must claim pseudoconsole close"
);
drop(conin_write);
}
#[cfg(any(feature = "blocking", feature = "tokio"))]
#[test]
fn watcher_handle_duplication_failure_retires_and_kills_the_tree() {
let backend = ConPtyBackend::system().expect("ConPTY must be available");
let SyncPipes {
conout_read,
conout_write,
conin_read,
conin_write,
} = create_sync_pipes().expect("creating pipes must succeed");
let console = PseudoConsole::new(backend, Size::default(), conin_read, conout_write, false)
.expect("CreatePseudoConsole must succeed");
let session = Session::new(console, true);
let mut command = crate::command::Command::new("cmd.exe");
command.args(["/D", "/S", "/C"]).raw_arg(
"start \"\" /b ping.exe -t 127.0.0.1 >nul & + ping.exe -n 30 127.0.0.1 >nul",
);
let mut observed = None;
let error = spawn_root_with_watcher_handle(&session, &mut command, false, |child| {
let deadline = Instant::now() + Duration::from_secs(5);
let mut descendant = direct_child_of(child.id());
while descendant.is_none() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
descendant = direct_child_of(child.id());
}
let root = child.id();
let descendant =
descendant.ok_or_else(|| io::Error::other("no descendant appeared"))?;
observed = Some((root, descendant));
Err(io::Error::other(
"injected watcher handle duplication failure",
))
})
.expect_err("watcher handle duplication failure must fail the spawn");
assert_eq!(error.kind(), ErrorKind::Spawn);
assert_eq!(
error.io_error().map(io::Error::kind),
Some(io::ErrorKind::Other)
);
assert!(error
.io_error()
.is_some_and(|source| source.to_string().contains("injected watcher")));
let mut second = crate::command::Command::new("cmd.exe");
let reused = spawn_root(&session, &mut second, false)
.expect_err("a post-creation failure must retire the pseudoconsole");
assert_eq!(reused.kind(), ErrorKind::Spawn);
assert_eq!(
reused.io_error().map(io::Error::kind),
Some(io::ErrorKind::AlreadyExists)
);
let (root, descendant) = observed.expect("the injected failure observed both pids");
let deadline = Instant::now() + Duration::from_secs(5);
while (process_is_running(root) || process_is_running(descendant))
&& Instant::now() < deadline
{
std::thread::sleep(Duration::from_millis(10));
}
assert!(
!process_is_running(root),
"the root process survived cleanup"
);
assert!(
!process_is_running(descendant),
"the descendant process survived cleanup"
);
drop(conin_write);
drop(conout_read);
}
#[cfg(feature = "tracing")]
#[test]
fn lifecycle_fallback_failures_are_logged() {
let error = io::Error::other("injected lifecycle failure");
let events = crate::tracing_test_support::count_events(|| {
log_release_failure(&error);
log_spawn_cleanup_failure(&error);
});
assert_eq!(events, 2);
}
}