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}