browser_commander/browser/parity/
reference.rs1use std::path::{Path, PathBuf};
4use std::sync::{Arc, Mutex};
5use std::time::Duration;
6
7use anyhow::{bail, Result};
8use serde_json::Value;
9
10use super::{server::ProbeServer, MeasureParityOptions};
11use crate::browser::profile_directory::{create_temporary_user_data_dir, remove_user_data_dir};
12use crate::utilities::{start_process, ManagedProcess, StartProcessOptions};
13
14struct Reference {
15 directory: PathBuf,
16 child: Option<ManagedProcess>,
17}
18
19pub fn build_reference_args(
21 directory: &Path,
22 url: &str,
23 headless: bool,
24 extra_args: &[String],
25) -> Vec<String> {
26 let mut args = vec![format!("--user-data-dir={}", directory.display())];
27 if headless {
28 args.push("--headless=new".into());
29 }
30 args.extend_from_slice(extra_args);
31 args.push(url.into());
32 args
33}
34
35impl Reference {
36 async fn close(&mut self) -> Result<()> {
37 if let Some(child) = self.child.take() {
38 child.kill();
39 child.wait_timeout(Duration::from_secs(6)).await;
40 }
41 remove_user_data_dir(&self.directory).await
42 }
43}
44
45impl Drop for Reference {
46 fn drop(&mut self) {
47 let directory = self.directory.clone();
48 if let Some(child) = self.child.take() {
49 child.kill();
50 if let Ok(runtime) = tokio::runtime::Handle::try_current() {
51 runtime.spawn(async move {
52 child.wait_timeout(Duration::from_secs(6)).await;
53 let _ = remove_user_data_dir(&directory).await;
54 });
55 return;
56 }
57 }
58 let _ = std::fs::remove_dir_all(directory);
59 }
60}
61
62pub(super) async fn capture(
63 server: &ProbeServer,
64 executable: &Path,
65 options: &MeasureParityOptions,
66) -> Result<Value> {
67 let mut reference = Reference {
68 directory: create_temporary_user_data_dir(None)?,
69 child: None,
70 };
71 let args = build_reference_args(
72 &reference.directory,
73 &server.reference_url,
74 options.launch.headless,
75 &options.reference_args,
76 );
77 let stderr = Arc::new(Mutex::new(Vec::<u8>::new()));
78 let tail = stderr.clone();
79 reference.child = Some(
80 start_process(
81 &executable.to_string_lossy(),
82 &args,
83 StartProcessOptions {
84 kill_grace: Duration::from_secs(3),
85 on_stderr: vec![Arc::new(move |chunk| {
86 if let Ok(mut tail) = tail.lock() {
87 tail.extend_from_slice(chunk);
88 let remove = tail.len().saturating_sub(4000);
89 tail.drain(..remove);
90 }
91 })],
92 ..Default::default()
93 },
94 )
95 .await?,
96 );
97 let child = reference.child.as_ref().unwrap();
98 let exited = async {
99 let code = child.wait().await;
100 if code == 0 {
101 std::future::pending::<()>().await;
102 }
103 bail!(
104 "reference browser {} exited with code {code} before reporting",
105 executable.display()
106 );
107 };
108 let captured = tokio::select! {
109 report = server.report(true, options.timeout) => report,
110 result = exited => result,
111 };
112 let captured = captured.map_err(|error| {
113 let tail = stderr
114 .lock()
115 .map(|bytes| String::from_utf8_lossy(&bytes).into_owned())
116 .unwrap_or_default();
117 anyhow::anyhow!("{error}\nbrowser stderr:\n{tail}")
118 });
119 let closed = reference.close().await;
120 let report = captured?;
121 closed?;
122 Ok(report)
123}
124
125#[cfg(all(test, unix))]
126mod tests {
127 use super::*;
128
129 #[tokio::test]
130 async fn reports_early_reference_exit_with_stderr() -> Result<()> {
131 let server = ProbeServer::start().await?;
132 let error = tokio::time::timeout(
134 Duration::from_secs(3),
135 capture(
136 &server,
137 Path::new("/bin/sh"),
138 &MeasureParityOptions::default(),
139 ),
140 )
141 .await?
142 .unwrap_err();
143 let text = error.to_string();
144 assert!(text.contains("exited with code"), "{text}");
145 assert!(text.contains("browser stderr:"), "{text}");
146 Ok(())
147 }
148
149 #[tokio::test]
150 async fn cancellation_reaps_reference_before_removing_its_profile() -> Result<()> {
151 let directory = create_temporary_user_data_dir(None)?;
152 let path = directory.clone();
153 let (ready, started) = tokio::sync::oneshot::channel();
154 let task = tokio::spawn(async move {
155 let reference = Reference {
156 directory,
157 child: Some(
158 start_process(
159 "/bin/sh",
160 &["-c", "exec sleep 30"],
161 StartProcessOptions {
162 kill_grace: Duration::from_millis(50),
163 ..Default::default()
164 },
165 )
166 .await
167 .unwrap(),
168 ),
169 };
170 ready
171 .send(reference.child.as_ref().unwrap().pid().unwrap())
172 .unwrap();
173 std::future::pending::<()>().await;
174 drop(reference);
175 });
176 let pid = started.await?;
177 task.abort();
178 let _ = task.await;
179 tokio::time::timeout(Duration::from_secs(7), async {
180 while path.exists() {
181 tokio::time::sleep(Duration::from_millis(20)).await;
182 }
183 })
184 .await?;
185 let result = crate::utilities::run_command(
186 "/bin/kill",
187 &["-0".to_owned(), pid.to_string()],
188 crate::utilities::RunCommandOptions {
189 check: false,
190 ..Default::default()
191 },
192 )
193 .await?;
194 assert_ne!(
195 result.code, 0,
196 "reference process {pid} survived cancellation"
197 );
198 Ok(())
199 }
200}