Skip to main content

browser_commander/browser/
browser_process.rs

1//! The spawned browser process handle shared by the real-browser launcher and
2//! [`LaunchResult`](super::launcher::LaunchResult).
3//!
4//! The process itself is a [`ManagedProcess`] from
5//! [`utilities::subprocess`](crate::utilities::subprocess), so the browser is
6//! started through command-stream like every other subprocess in the crate
7//! (issue #104). [`BrowserProcess`] is a cheap, clonable view of it; the
8//! browser is stopped once the last clone is dropped.
9
10use std::fmt;
11use std::future::Future;
12use std::pin::Pin;
13use std::sync::Arc;
14use std::time::Duration;
15
16use crate::utilities::ManagedProcess;
17
18/// A future resolving with a process's exit code.
19pub(crate) type ExitFuture = Pin<Box<dyn Future<Output = i32> + Send + 'static>>;
20
21/// What the launcher needs from a spawned process. Implemented by
22/// [`ManagedProcess`] and by the fakes in the launcher's unit tests.
23pub(crate) trait ProcessControl: Send + Sync {
24    fn pid(&self) -> Option<u32>;
25    fn exit_code(&self) -> Option<i32>;
26    fn kill(&self) -> bool;
27    fn exited(&self) -> ExitFuture;
28}
29
30impl ProcessControl for ManagedProcess {
31    fn pid(&self) -> Option<u32> {
32        ManagedProcess::pid(self)
33    }
34
35    fn exit_code(&self) -> Option<i32> {
36        ManagedProcess::exit_code(self)
37    }
38
39    fn kill(&self) -> bool {
40        ManagedProcess::kill(self)
41    }
42
43    fn exited(&self) -> ExitFuture {
44        Box::pin(ManagedProcess::exited(self))
45    }
46}
47
48/// Shuts down whatever a launch started: the browser and, for a temporary
49/// profile, its directory. Idempotent.
50#[async_trait::async_trait]
51pub(crate) trait BrowserCloser: Send + Sync {
52    async fn close(&self) -> anyhow::Result<()>;
53}
54
55/// A spawned installed-browser process.
56///
57/// Clones share the same process. Dropping the last clone stops the browser;
58/// call [`LaunchResult::close`](super::launcher::LaunchResult::close) or
59/// [`RealBrowserLaunchResult::close`](super::real_browser::RealBrowserLaunchResult::close)
60/// for a graceful shutdown that also removes a temporary profile.
61#[derive(Clone)]
62pub struct BrowserProcess {
63    inner: Arc<dyn ProcessControl>,
64}
65
66impl BrowserProcess {
67    pub(crate) fn from_control(inner: Arc<dyn ProcessControl>) -> Self {
68        Self { inner }
69    }
70
71    pub(crate) fn from_managed(process: ManagedProcess) -> Self {
72        Self::from_control(Arc::new(process))
73    }
74
75    /// Operating-system process identifier (`0` if it is not known).
76    pub fn id(&self) -> u32 {
77        self.pid().unwrap_or(0)
78    }
79
80    /// Operating-system process identifier.
81    pub fn pid(&self) -> Option<u32> {
82        self.inner.pid()
83    }
84
85    /// Exit code once the browser has exited (`128 + signal` for a signal).
86    pub fn exit_code(&self) -> Option<i32> {
87        self.inner.exit_code()
88    }
89
90    /// Whether the browser is still running.
91    pub fn is_running(&self) -> bool {
92        self.exit_code().is_none()
93    }
94
95    /// Ask the browser to stop (`SIGTERM`, then `SIGKILL` after a grace
96    /// period). Returns whether a running process was signalled; it does not
97    /// wait - use [`wait`](Self::wait) for that.
98    pub fn kill(&self) -> bool {
99        if !self.is_running() {
100            return false;
101        }
102        self.inner.kill()
103    }
104
105    /// Wait for the browser to exit and return its exit code.
106    pub async fn wait(&self) -> i32 {
107        self.exited().await
108    }
109
110    /// Wait up to `timeout` for the browser to exit; `None` if it is still
111    /// running afterwards.
112    pub async fn wait_timeout(&self, timeout: Duration) -> Option<i32> {
113        if let Some(code) = self.exit_code() {
114            return Some(code);
115        }
116        tokio::time::timeout(timeout, self.exited()).await.ok()
117    }
118
119    /// A `'static` future resolving with the exit code, for use in tasks.
120    pub fn exited(&self) -> impl Future<Output = i32> + Send + 'static {
121        self.inner.exited()
122    }
123}
124
125impl fmt::Debug for BrowserProcess {
126    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
127        formatter
128            .debug_struct("BrowserProcess")
129            .field("pid", &self.pid())
130            .field("exit_code", &self.exit_code())
131            .finish()
132    }
133}
134
135#[cfg(test)]
136pub(crate) mod fake {
137    //! A scripted process for launcher tests.
138
139    use super::{BrowserProcess, ExitFuture, ProcessControl};
140    use std::sync::atomic::{AtomicUsize, Ordering};
141    use std::sync::Arc;
142    use tokio::sync::watch;
143
144    /// A process that exits when killed, or when the test says so.
145    pub(crate) struct FakeProcess {
146        exit: watch::Sender<Option<i32>>,
147        pub(crate) kills: AtomicUsize,
148        /// Whether `kill` makes the process exit (a stuck browser does not).
149        pub(crate) exits_on_kill: bool,
150    }
151
152    impl FakeProcess {
153        pub(crate) fn new() -> Arc<Self> {
154            Arc::new(Self {
155                exit: watch::channel(None).0,
156                kills: AtomicUsize::new(0),
157                exits_on_kill: true,
158            })
159        }
160
161        pub(crate) fn exit(&self, code: i32) {
162            self.exit.send_replace(Some(code));
163        }
164
165        pub(crate) fn kill_count(&self) -> usize {
166            self.kills.load(Ordering::SeqCst)
167        }
168
169        pub(crate) fn handle(self: &Arc<Self>) -> BrowserProcess {
170            BrowserProcess::from_control(Arc::clone(self) as Arc<dyn ProcessControl>)
171        }
172    }
173
174    impl ProcessControl for FakeProcess {
175        fn pid(&self) -> Option<u32> {
176            Some(4242)
177        }
178
179        fn exit_code(&self) -> Option<i32> {
180            *self.exit.borrow()
181        }
182
183        fn kill(&self) -> bool {
184            self.kills.fetch_add(1, Ordering::SeqCst);
185            if self.exits_on_kill {
186                self.exit(143);
187            }
188            true
189        }
190
191        fn exited(&self) -> ExitFuture {
192            let mut exit = self.exit.subscribe();
193            Box::pin(async move {
194                match exit.wait_for(Option::is_some).await {
195                    Ok(code) => code.unwrap_or(1),
196                    Err(_) => 1,
197                }
198            })
199        }
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use crate::utilities::{start_process, StartProcessOptions};
207
208    #[tokio::test]
209    async fn wraps_a_managed_process() {
210        let process = start_process("sleep", &["30"], StartProcessOptions::default())
211            .await
212            .unwrap();
213        let browser = BrowserProcess::from_managed(process);
214        assert!(browser.is_running());
215        assert_ne!(browser.id(), 0);
216        assert!(browser
217            .wait_timeout(Duration::from_millis(50))
218            .await
219            .is_none());
220        assert!(browser.kill());
221        let code = browser.wait_timeout(Duration::from_secs(5)).await;
222        assert_eq!(code, Some(128 + 15));
223        assert!(!browser.kill());
224    }
225
226    #[tokio::test]
227    async fn fake_process_exits_when_killed() {
228        let fake = fake::FakeProcess::new();
229        let process = fake.handle();
230        let exited = process.exited();
231        process.kill();
232        assert_eq!(exited.await, 143);
233        assert_eq!(fake.kill_count(), 1);
234    }
235}