use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use crate::utilities::ManagedProcess;
pub(crate) type ExitFuture = Pin<Box<dyn Future<Output = i32> + Send + 'static>>;
pub(crate) trait ProcessControl: Send + Sync {
fn pid(&self) -> Option<u32>;
fn exit_code(&self) -> Option<i32>;
fn kill(&self) -> bool;
fn exited(&self) -> ExitFuture;
}
impl ProcessControl for ManagedProcess {
fn pid(&self) -> Option<u32> {
ManagedProcess::pid(self)
}
fn exit_code(&self) -> Option<i32> {
ManagedProcess::exit_code(self)
}
fn kill(&self) -> bool {
ManagedProcess::kill(self)
}
fn exited(&self) -> ExitFuture {
Box::pin(ManagedProcess::exited(self))
}
}
#[async_trait::async_trait]
pub(crate) trait BrowserCloser: Send + Sync {
async fn close(&self) -> anyhow::Result<()>;
}
#[derive(Clone)]
pub struct BrowserProcess {
inner: Arc<dyn ProcessControl>,
}
impl BrowserProcess {
pub(crate) fn from_control(inner: Arc<dyn ProcessControl>) -> Self {
Self { inner }
}
pub(crate) fn from_managed(process: ManagedProcess) -> Self {
Self::from_control(Arc::new(process))
}
pub fn id(&self) -> u32 {
self.pid().unwrap_or(0)
}
pub fn pid(&self) -> Option<u32> {
self.inner.pid()
}
pub fn exit_code(&self) -> Option<i32> {
self.inner.exit_code()
}
pub fn is_running(&self) -> bool {
self.exit_code().is_none()
}
pub fn kill(&self) -> bool {
if !self.is_running() {
return false;
}
self.inner.kill()
}
pub async fn wait(&self) -> i32 {
self.exited().await
}
pub async fn wait_timeout(&self, timeout: Duration) -> Option<i32> {
if let Some(code) = self.exit_code() {
return Some(code);
}
tokio::time::timeout(timeout, self.exited()).await.ok()
}
pub fn exited(&self) -> impl Future<Output = i32> + Send + 'static {
self.inner.exited()
}
}
impl fmt::Debug for BrowserProcess {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("BrowserProcess")
.field("pid", &self.pid())
.field("exit_code", &self.exit_code())
.finish()
}
}
#[cfg(test)]
pub(crate) mod fake {
use super::{BrowserProcess, ExitFuture, ProcessControl};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use tokio::sync::watch;
pub(crate) struct FakeProcess {
exit: watch::Sender<Option<i32>>,
pub(crate) kills: AtomicUsize,
pub(crate) exits_on_kill: bool,
}
impl FakeProcess {
pub(crate) fn new() -> Arc<Self> {
Arc::new(Self {
exit: watch::channel(None).0,
kills: AtomicUsize::new(0),
exits_on_kill: true,
})
}
pub(crate) fn exit(&self, code: i32) {
self.exit.send_replace(Some(code));
}
pub(crate) fn kill_count(&self) -> usize {
self.kills.load(Ordering::SeqCst)
}
pub(crate) fn handle(self: &Arc<Self>) -> BrowserProcess {
BrowserProcess::from_control(Arc::clone(self) as Arc<dyn ProcessControl>)
}
}
impl ProcessControl for FakeProcess {
fn pid(&self) -> Option<u32> {
Some(4242)
}
fn exit_code(&self) -> Option<i32> {
*self.exit.borrow()
}
fn kill(&self) -> bool {
self.kills.fetch_add(1, Ordering::SeqCst);
if self.exits_on_kill {
self.exit(143);
}
true
}
fn exited(&self) -> ExitFuture {
let mut exit = self.exit.subscribe();
Box::pin(async move {
match exit.wait_for(Option::is_some).await {
Ok(code) => code.unwrap_or(1),
Err(_) => 1,
}
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utilities::{start_process, StartProcessOptions};
#[tokio::test]
async fn wraps_a_managed_process() {
let process = start_process("sleep", &["30"], StartProcessOptions::default())
.await
.unwrap();
let browser = BrowserProcess::from_managed(process);
assert!(browser.is_running());
assert_ne!(browser.id(), 0);
assert!(browser
.wait_timeout(Duration::from_millis(50))
.await
.is_none());
assert!(browser.kill());
let code = browser.wait_timeout(Duration::from_secs(5)).await;
assert_eq!(code, Some(128 + 15));
assert!(!browser.kill());
}
#[tokio::test]
async fn fake_process_exits_when_killed() {
let fake = fake::FakeProcess::new();
let process = fake.handle();
let exited = process.exited();
process.kill();
assert_eq!(exited.await, 143);
assert_eq!(fake.kill_count(), 1);
}
}