From 79c8d1a7e02190ce0f60e6d75445303ba1031631 Mon Sep 17 00:00:00 2001 From: ruchirguitar <113586074+ruchirguitar@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:56:29 +0530 Subject: [PATCH] Fix #5236: cap the wait for open -a at two seconds A stalled `open -a` no longer holds the window: Edit Original waits at most two seconds for its exit status (enough to report an app it can't open), then leaves the launch running and reaps it on a thread of its own. Co-Authored-By: Claude Opus 5.5 --- apps/vectorcraft/src/host_services.rs | 39 ++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/apps/vectorcraft/src/host_services.rs b/apps/vectorcraft/src/host_services.rs index 4004050ff..2568ae03b 100644 --- a/apps/vectorcraft/src/host_services.rs +++ b/apps/vectorcraft/src/host_services.rs @@ -357,18 +357,39 @@ fn open_file_with(app: &str, path: &str) -> Result<(), String> { c.arg(path); c }; - // macOS: `open` returns once Launch Services has the request, so waiting for it is brief and - // reports an app it can't open (a folder named `.app` that isn't a bundle). Elsewhere the - // editor itself runs: waiting would last until it closes. + // macOS: `open` returns once Launch Services has the request, so a short wait reports an app + // it can't open (a folder named `.app` that isn't a bundle). The wait is capped so a stalled + // `open` never holds the window: past it, the launch is left to run. Elsewhere the editor + // itself runs: waiting would last until it closes. #[cfg(target_os = "macos")] { - let out = c.output().map_err(|e| format!("can't open {path} in {}: {e}", app.display()))?; - if out.status.success() { - return Ok(()); + const WAIT: std::time::Duration = std::time::Duration::from_secs(2); + let fail = |why: String| format!("can't open {path} in {}: {why}", app.display()); + let mut child = c.stderr(std::process::Stdio::piped()).spawn().map_err(|e| fail(e.to_string()))?; + let started = std::time::Instant::now(); + loop { + match child.try_wait() { + Ok(Some(status)) if status.success() => return Ok(()), + Ok(Some(status)) => { + let mut why = String::new(); + if let Some(mut err) = child.stderr.take() { + // Best effort: without its message the exit status says why. + let _ = std::io::Read::read_to_string(&mut err, &mut why); + } + let why = why.trim().to_string(); + return Err(fail(if why.is_empty() { status.to_string() } else { why })); + } + Ok(None) if started.elapsed() < WAIT => std::thread::sleep(std::time::Duration::from_millis(20)), + Ok(None) => { + // Reaped off the UI thread once it ends. + std::thread::spawn(move || { + let _ = child.wait(); + }); + return Ok(()); + } + Err(e) => return Err(fail(e.to_string())), + } } - let why = String::from_utf8_lossy(&out.stderr).trim().to_string(); - let why = if why.is_empty() { out.status.to_string() } else { why }; - Err(format!("can't open {path} in {}: {why}", app.display())) } #[cfg(not(target_os = "macos"))] c.spawn().map(|_| ()).map_err(|e| format!("can't open {path} in {}: {e}", app.display()))