tastty-driver 0.1.0

Terminal automation driver built on tastty
//! Persistent terminal driver session.

mod inspection;
mod io;
mod lifecycle;
mod signal;
mod spawn;
mod wait;

pub use spawn::DriverOptions;
use std::sync::Arc;

use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::{self, JoinHandle};
use std::time::Duration;

use crate::ExitStatus;
use crate::observer::IoObserver;
use crate::wait::exit::ExitNotifier;
use crate::wait::output::OutputNotifier;
use tastty::Terminal;

const REAPER_POLL_INTERVAL: Duration = Duration::from_millis(50);

/// Persistent terminal driver session.
pub struct Session {
    terminal: Arc<Terminal>,
    observer: Option<Arc<dyn IoObserver>>,
    exit_notifier: Arc<ExitNotifier>,
    output_notifier: Arc<OutputNotifier>,
    output_timer: Option<JoinHandle<()>>,
    reaper_shutdown: Arc<AtomicBool>,
    reaper: Option<JoinHandle<()>>,
}

impl Session {
    /// Clone of the shared terminal handle. The wait module uses this to
    /// drive both the synchronous wait loop and the async wait future
    /// without taking a `&Session` reference, so the async future can
    /// outlive the [`Session`] that produced it.
    pub(crate) fn terminal_handle(&self) -> Arc<Terminal> {
        Arc::clone(&self.terminal)
    }

    /// Clone of the session's [`OutputNotifier`]. The synchronous
    /// [`Session::wait`] path uses this to block on tick wakes; async
    /// wait futures take their own [`Arc`] so they can outlive the
    /// [`Session`] (the timer thread is joined on session drop, but the
    /// notifier itself is reference-counted, so a future still holding an
    /// Arc just observes no further ticks after the session is gone).
    pub(crate) fn output_notifier(&self) -> Arc<OutputNotifier> {
        Arc::clone(&self.output_notifier)
    }
}

impl Drop for Session {
    fn drop(&mut self) {
        self.reaper_shutdown.store(true, Ordering::Release);
        if let Some(handle) = self.reaper.take() {
            drop(handle.join());
        }
        self.output_notifier.shutdown();
        if let Some(handle) = self.output_timer.take() {
            drop(handle.join());
        }
    }
}

fn exit_reaper(
    terminal: Arc<Terminal>,
    exit: Arc<ExitNotifier>,
    output: Arc<OutputNotifier>,
    observer: Option<Arc<dyn IoObserver>>,
    shutdown: Arc<AtomicBool>,
) {
    loop {
        let shutting_down = shutdown.load(Ordering::Acquire);
        match terminal.try_wait() {
            Ok(Some(status)) => {
                let status = ExitStatus::from_tastty(status);
                exit.notify_exit(status);
                // Kick the output notifier so any pending wait future or
                // synchronous waiter re-probes its condition. Without this,
                // a waiter blocked on tick wakes alone after a clean exit
                // (no further parser ticks fire post-EOF) would only
                // observe `try_wait` -> Some on its next deadline-driven
                // wake, adding latency to the ProcessExitedBeforeMatch
                // surface for non-Exit/non-Stable conditions.
                output.notify_tick();
                if let Some(obs) = &observer {
                    obs.on_exit(status);
                }
                return;
            }
            Ok(None) => {}
            Err(_) => {
                // Surface to every wait-for-exit consumer (sync and async)
                // as a Failed transition. The original syscall error is
                // dropped here, mirroring the pre-redesign reaper which
                // bailed silently with `Err(_) => return`.
                exit.notify_failure();
                output.notify_tick();
                return;
            }
        }
        // Final check after the post-shutdown poll catches the race where the
        // child exited between the last poll and shutdown being signalled.
        // If we still have not observed an exit, wake any pending consumers
        // with a Failed transition: nobody else will reap this notifier
        // after the reaper exits.
        if shutting_down {
            exit.notify_failure();
            output.notify_tick();
            return;
        }
        thread::sleep(REAPER_POLL_INTERVAL);
    }
}