@@ -19,7 +19,7 @@ use openbot_desktop_lib::{
1919 install, local_browser, local_eval, local_eval_state, preparation,
2020 problem::{LeftoverReset, Problem},
2121 provider, pull_metrics, quiet, recorder_external, recorder_host, recorder_install, stack,
22- telemetry, tray, windows as win,
22+ supervise, telemetry, tray, windows as win,
2323};
2424
2525const QUIT_CLEANUP_NOTICE_FILE: &str = ".openbot-quit-cleanup-notice";
@@ -37,6 +37,9 @@ use tauri::{Emitter, Manager};
3737struct Shell {
3838 /// Session-only local folder authority; shutdown retires it before stopping the API server.
3939 host_access: Mutex<Option<host_access::HostAccess>>,
40+ /// The person's browser, the one host process a Start runs (see `local_browser`). Held so Stop
41+ /// can let Chrome close its windows and profile before anything is forced.
42+ browser: Mutex<Option<std::process::Child>>,
4043 /// Native Linux engine API owned by this desktop session, kept through Stop/Start.
4144 #[cfg(target_os = "linux")]
4245 podman_api: Mutex<engine::local_api::Service>,
@@ -1462,7 +1465,10 @@ fn reset_leftover_database_with(
14621465 }
14631466 let attempt = StartAttempt::begin(shell)?;
14641467 let _startup = attempt.lock_current()?;
1465- if shell.containers.lock().unwrap().is_some() || shell.root.lock().unwrap().is_some() {
1468+ if shell.containers.lock().unwrap().is_some()
1469+ || shell.root.lock().unwrap().is_some()
1470+ || shell.browser.lock().unwrap().is_some()
1471+ {
14661472 return Err(Problem::plain("OpenBot still owns running services. Choose Stop OpenBot before resetting leftover data."));
14671473 }
14681474 if shell
@@ -1733,7 +1739,7 @@ async fn start_stack_inner<R: tauri::Runtime>(
17331739 let host_token =
17341740 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(rand::random::<[u8; 32]>());
17351741
1736- let (found, secrets, ports) = {
1742+ let (found, secrets, ports, local_browser ) = {
17371743 let _startup = attempt.lock_current()?;
17381744
17391745 // Belt and braces: a fetch that reported success and left something out is still not a
@@ -2006,7 +2012,7 @@ async fn start_stack_inner<R: tauri::Runtime>(
20062012 report(&app, "services", false, detail);
20072013 })?;
20082014
2009- (found, secrets, ports)
2015+ (found, secrets, ports, local_browser )
20102016 };
20112017
20122018 /*
@@ -2055,8 +2061,50 @@ async fn start_stack_inner<R: tauri::Runtime>(
20552061 report(&app, "answering", false, problem.said.clone());
20562062 return Err(problem);
20572063 }
2064+ /*
2065+ * The person's own browser, the one host process left (see `local_browser`).
2066+ *
2067+ * After the server answers: the server reaches it only when the person opens their browser, so
2068+ * it is not on the way to the window answering. Bun and the computer's packages are prepared the
2069+ * first time a Start has a browser to run, and reused after that, as the Intelligence sign-in
2070+ * helper prepares them. Installing OpenBot no longer does.
2071+ */
2072+ let browser_bun = match local_browser.as_ref() {
2073+ Some(_) => {
2074+ report_running(&app, "browser", "Preparing your browser.", None);
2075+ let prepare_root = root.clone();
2076+ let prepared = tauri::async_runtime::spawn_blocking(move || {
2077+ preparation::install_dependencies_if_needed(&prepare_root, || {
2078+ install::ensure_bun(&prepare_root, which_bun())
2079+ })
2080+ })
2081+ .await
2082+ .unwrap_or_else(|error| {
2083+ Err(Problem::with(
2084+ "OpenBot could not prepare your browser. Try Start again.",
2085+ error.to_string(),
2086+ ))
2087+ });
2088+ attempt.require_current()?;
2089+ Some(
2090+ prepared
2091+ .inspect_err(|problem| report(&app, "browser", false, problem.said.clone()))?,
2092+ )
2093+ }
2094+ None => None,
2095+ };
20582096 // Stop must not finish between accepting readiness and reporting a successful Start.
20592097 let _startup = attempt.lock_current()?;
2098+ if let (Some(browser), Some(bun)) = (local_browser.as_ref(), browser_bun.as_ref()) {
2099+ start_local_browser(&shell, &root, bun, &secrets, &ports, browser)
2100+ .inspect_err(|problem| report(&app, "browser", false, problem.said.clone()))?;
2101+ report(
2102+ &app,
2103+ "browser",
2104+ true,
2105+ format!("{} is ready.", browser.product.name()),
2106+ );
2107+ }
20602108 *shell.root.lock().unwrap() = Some(root.clone());
20612109 clear_recovery_required(&shell, &root);
20622110 let generation = shell.generation.load(std::sync::atomic::Ordering::SeqCst);
@@ -2088,6 +2136,9 @@ async fn start_stack_inner<R: tauri::Runtime>(
20882136 )
20892137 })?;
20902138 preparation::save_selected_root(&config, &root)?;
2139+ if let (Some(browser), Some(bun)) = (local_browser, browser_bun) {
2140+ supervise_local_browser(app.clone(), root.clone(), bun, secrets, browser, generation);
2141+ }
20912142 watch_connections(app.clone(), root, host_token, generation, ports.server);
20922143
20932144 report(&app, "answering", true, "OpenBot is ready.");
@@ -2561,7 +2612,12 @@ where
25612612 .unwrap()
25622613 .clone()
25632614 .unwrap_or_else(|| root.to_path_buf());
2564- let result = cleanup(&selected);
2615+ // Closed while it is still held, before `cleanup` ends whatever was recorded.
2616+ let result = match (close_held_browser(shell, &selected), cleanup(&selected)) {
2617+ (Ok(()), result) => result,
2618+ (Err(problem), Ok(_)) => Err(problem),
2619+ (Err(_), Err(problem)) => Err(problem),
2620+ };
25652621 let result = match (host_access_result, result) {
25662622 (Ok(()), result) => result,
25672623 (Err(error), Ok(_)) => Err(Problem::with(
@@ -5583,6 +5639,160 @@ fn publish_connection_failure<R: tauri::Runtime>(
55835639 Ok(true)
55845640}
55855641
5642+ /// Start the person's browser and hand it to this window and to the record a later window ends it
5643+ /// from. Called under the startup lock, so a Stop cannot finish between the spawn and the handoff.
5644+ fn start_local_browser(
5645+ shell: &Shell,
5646+ root: &Path,
5647+ bun: &Path,
5648+ secrets: &stack::Secrets,
5649+ ports: &openbot_env::Ports,
5650+ browser: &local_browser::LocalBrowser,
5651+ ) -> Result<(), Problem> {
5652+ let port = ports.browser.ok_or_else(|| {
5653+ Problem::plain("OpenBot did not choose a port for your browser. Try Start again.")
5654+ })?;
5655+ if let Some(problem) = stack::port_already_taken(&[("browser", port)]) {
5656+ return Err(problem.into());
5657+ }
5658+ let child = spawn_local_browser(root, bun, secrets, browser)?;
5659+ hold_local_browser(shell, root, child)
5660+ }
5661+
5662+ fn spawn_local_browser(
5663+ root: &Path,
5664+ bun: &Path,
5665+ secrets: &stack::Secrets,
5666+ browser: &local_browser::LocalBrowser,
5667+ ) -> Result<std::process::Child, Problem> {
5668+ stack::spawn_host_process(
5669+ stack::local_browser_process(),
5670+ root,
5671+ &root.join(".logs"),
5672+ bun,
5673+ secrets,
5674+ Some(browser),
5675+ )
5676+ .map_err(|error| {
5677+ Problem::with(
5678+ "OpenBot could not start your browser. Try Start again.",
5679+ error.to_string(),
5680+ )
5681+ })
5682+ }
5683+
5684+ /// Record it, then hold it. One that cannot be recorded is ended, rather than left running where no
5685+ /// later window could find it.
5686+ fn hold_local_browser(
5687+ shell: &Shell,
5688+ root: &Path,
5689+ child: std::process::Child,
5690+ ) -> Result<(), Problem> {
5691+ if let Err(problem) = stack::record_host_processes(root, &[(stack::LOCAL_BROWSER, child.id())])
5692+ {
5693+ let mut held = [(stack::LOCAL_BROWSER, child)];
5694+ if stack::stop_host_children(root, &mut held).is_err() {
5695+ let [(_, mut child)] = held;
5696+ let _ = child.kill();
5697+ let _ = child.wait();
5698+ }
5699+ return Err(problem);
5700+ }
5701+ *shell.browser.lock().unwrap() = Some(child);
5702+ Ok(())
5703+ }
5704+
5705+ /// The browser this window holds, closed so Chrome closes its windows and profile before anything
5706+ /// is forced (see `stack::stop_host_children`). Kept on failure, so another Stop can try again.
5707+ fn close_held_browser(shell: &Shell, root: &Path) -> Result<(), Problem> {
5708+ let mut slot = shell.browser.lock().unwrap();
5709+ let Some(child) = slot.take() else {
5710+ return Ok(());
5711+ };
5712+ let mut held = [(stack::LOCAL_BROWSER, child)];
5713+ match stack::stop_host_children(root, &mut held) {
5714+ Ok(_) => Ok(()),
5715+ Err(problem) => {
5716+ let [(_, child)] = held;
5717+ *slot = Some(child);
5718+ Err(problem)
5719+ }
5720+ }
5721+ }
5722+
5723+ /**
5724+ The person's browser, started again when it dies, as the host-process supervisor once did for all
5725+ four. It ends with the run it was started for, like [`watch_connections`].
5726+
5727+ Giving up is said, with the tail of its log, and OpenBot is left running: it runs without the
5728+ browser, and sending the window back to setup over it would stop everything else too.
5729+ */
5730+ fn supervise_local_browser<R: tauri::Runtime>(
5731+ app: tauri::AppHandle<R>,
5732+ root: PathBuf,
5733+ bun: PathBuf,
5734+ secrets: stack::Secrets,
5735+ browser: local_browser::LocalBrowser,
5736+ generation: u64,
5737+ ) -> std::thread::JoinHandle<()> {
5738+ std::thread::spawn(move || {
5739+ let mut watch = supervise::Watch::new(stack::LOCAL_BROWSER);
5740+ let current = |shell: &Shell| {
5741+ shell.generation.load(std::sync::atomic::Ordering::SeqCst) == generation
5742+ && shell.root.lock().unwrap().as_deref() == Some(root.as_path())
5743+ };
5744+ loop {
5745+ std::thread::sleep(std::time::Duration::from_secs(2));
5746+ let shell = app.state::<Shell>();
5747+ if !current(&shell) {
5748+ return;
5749+ }
5750+ let exited = match shell.browser.lock().unwrap().as_mut() {
5751+ // Stop took it.
5752+ None => return,
5753+ Some(child) => matches!(child.try_wait(), Ok(Some(_))),
5754+ };
5755+ if !exited {
5756+ continue;
5757+ }
5758+ if !watch.should_restart(std::time::Instant::now()) {
5759+ *shell.browser.lock().unwrap() = None;
5760+ report(
5761+ &app,
5762+ stack::LOCAL_BROWSER,
5763+ false,
5764+ format!(
5765+ "{}\n{}",
5766+ watch.gave_up(),
5767+ stack::tail_of(&root.join(".logs"), stack::LOCAL_BROWSER)
5768+ ),
5769+ );
5770+ return;
5771+ }
5772+ report(
5773+ &app,
5774+ stack::LOCAL_BROWSER,
5775+ false,
5776+ "Your browser stopped. Starting it again.",
5777+ );
5778+ std::thread::sleep(supervise::backoff(watch.restarts - 1));
5779+
5780+ // Asked again under the lock: a Stop, or another Start, may have happened meanwhile,
5781+ // and starting a browser into either is how an orphan is made.
5782+ let _startup = shell.startup.lock().unwrap();
5783+ if !current(&shell) {
5784+ return;
5785+ }
5786+ match spawn_local_browser(&root, &bun, &secrets, &browser)
5787+ .and_then(|child| hold_local_browser(&shell, &root, child))
5788+ {
5789+ Ok(()) => report(&app, stack::LOCAL_BROWSER, true, "started again"),
5790+ Err(problem) => report(&app, stack::LOCAL_BROWSER, false, problem.said),
5791+ }
5792+ }
5793+ })
5794+ }
5795+
55865796/**
55875797Watch a running OpenBot for a connection it reports refused, and send the window back to setup to
55885798refresh it.
0 commit comments