use std::io;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use crate::backend::BackendKind;
use crate::command::Command;
use crate::core::job::Job;
pub(crate) use crate::core::job::KILL_EXIT_CODE;
use crate::core::proc::{self, SpawnedChild};
use crate::core::pseudocon::PseudoConsole;
use crate::core::wait::{spawn_root_watcher, ProcessWaiter, 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) waiter: ProcessWaiter,
pub(super) job: Arc<Job>,
pub(super) pid: u32,
pub(super) kill_on_drop: bool,
}
pub(crate) fn spawn_root(
session: &Session,
command: &Command,
kill_on_drop: bool,
) -> Result<RootChild> {
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, spawned) = 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 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, &spawned, &job, close_legacy) {
if let Err(kill_err) = job.terminate(KILL_EXIT_CODE) {
log_spawn_cleanup_failure(&kill_err);
}
return Err(spawn_error(command, err));
}
Ok(RootChild {
waiter: ProcessWaiter::new(spawned.process),
job,
pid: spawned.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: &Command,
kill_on_drop: bool,
) -> io::Result<(Arc<Job>, SpawnedChild)> {
let job = Arc::new(Job::create(kill_on_drop)?);
let spawned = proc::spawn(command, session.console.hpcon(), &job)?;
Ok((job, spawned))
}
fn arm_root_watcher(
session: &Session,
spawned: &SpawnedChild,
job: &Arc<Job>,
close_legacy: bool,
) -> io::Result<()> {
use std::os::windows::io::AsHandle;
let watched = spawned.process.as_handle().try_clone_to_owned()?;
spawn_root_watcher(
watched,
Arc::downgrade(job),
Arc::clone(session.console.shared()),
LEGACY_CLOSE_GRACE,
close_legacy,
)
}
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 {
#[cfg(feature = "tracing")]
use std::io;
#[cfg(any(feature = "blocking", feature = "tokio"))]
use std::sync::atomic::Ordering;
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 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::size::Size;
#[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 = super::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(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);
}
}