Skip to main content

browser_automation_cli/
lifecycle.rs

1//! One-shot lifecycle: INIT → EXECUTA → FINALIZE → EXIT.
2//!
3//! # Flow
4//!
5//! 1. `Lifecycle::new` creates cancel token and resource ledger
6//! 2. Commands may mark Chrome PID / profile in `ResourceLedger`
7//! 3. `Lifecycle::finalize` is idempotent and safe to call multiple times
8//! 4. `Drop` also calls finalize as a safety net
9//!
10//! # Safety
11//!
12//! On Unix, finalize may send `SIGTERM`/`SIGKILL` to a recorded Chrome PID as
13//! last resort when primary Browser.close reap did not clear the ledger.
14
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::sync::Arc;
17
18use tokio_util::sync::CancellationToken;
19
20/// Ledger of resources owned by this invocation (browser, profile, temp dirs).
21#[derive(Debug, Default)]
22pub struct ResourceLedger {
23    /// True when this process launched Chrome and still owns the residual flag.
24    pub chrome_launched: bool,
25    /// Optional OS process id of launched Chrome for last-resort kill.
26    pub chrome_pid: Option<u32>,
27    /// Temporary profile directory created for this invocation, if any.
28    pub profile_dir: Option<std::path::PathBuf>,
29}
30
31/// Runtime token for cooperative cancel and FINALIZE.
32#[derive(Clone)]
33pub struct Lifecycle {
34    /// Cancellation token shared with async tasks.
35    pub cancel: CancellationToken,
36    /// Whether FINALIZE already completed.
37    pub finalize_done: Arc<AtomicBool>,
38    /// Owned resources for residual cleanup.
39    pub ledger: Arc<std::sync::Mutex<ResourceLedger>>,
40}
41
42impl Lifecycle {
43    /// Create a fresh lifecycle for one process invocation.
44    pub fn new() -> Self {
45        Self {
46            cancel: CancellationToken::new(),
47            finalize_done: Arc::new(AtomicBool::new(false)),
48            ledger: Arc::new(std::sync::Mutex::new(ResourceLedger::default())),
49        }
50    }
51
52    /// Idempotent FINALIZE: last-resort residual kill if chrome still flagged.
53    ///
54    /// Primary reap is `OneShotSession::shutdown` (Browser.close + wait_or_kill).
55    pub fn finalize(&self) {
56        if self
57            .finalize_done
58            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
59            .is_err()
60        {
61            return;
62        }
63        self.cancel.cancel();
64        if let Ok(mut ledger) = self.ledger.lock() {
65            if ledger.chrome_launched {
66                if let Some(pid) = ledger.chrome_pid.take() {
67                    #[cfg(unix)]
68                    unsafe {
69                        let _ = libc::kill(pid as i32, libc::SIGTERM);
70                        let _ = libc::kill(pid as i32, libc::SIGKILL);
71                    }
72                    #[cfg(windows)]
73                    {
74                        let _ = pid;
75                    }
76                }
77            }
78            ledger.chrome_launched = false;
79            ledger.profile_dir = None;
80        }
81    }
82}
83
84impl Default for Lifecycle {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90impl Drop for Lifecycle {
91    fn drop(&mut self) {
92        self.finalize();
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn finalize_is_idempotent() {
102        let lc = Lifecycle::new();
103        lc.finalize();
104        lc.finalize();
105        assert!(lc.finalize_done.load(Ordering::SeqCst));
106    }
107}