mirror of
https://github.com/block/buzz.git
synced 2026-10-29 13:31:44 +00:00
fix(desktop): start the Canvas update socket inside the async runtime
The app aborted on every macOS and Linux launch. `ipc::start` runs synchronously from Tauri's `setup` hook, on the main thread with no Tokio runtime in context, and called `tokio::net::UnixListener::from_std` there. That call registers the socket with the reactor, so it panicked with "there is no reactor running" — and because `setup` runs inside tao's `did_finish_launching`, a non-unwinding ObjC callback, the panic escalated to `panic_cannot_unwind` and killed the process before a window ever appeared. Bind the socket synchronously as before (so stale-socket and permission failures still surface as a returned error), then hand the std listener to `spawn_serving`, which converts it inside the spawned task where the reactor is live. Socket cleanup on a failed conversion matches the existing accept error path. Covered by a unit test that calls `spawn_serving` from a plain sync context, exactly as `setup` does, and asserts the socket still accepts a connection; it reproduces the original panic when the conversion moves back to the call site. Verified with `just staging`: the app now boots past `did_finish_launching`, creates `CANVASES/.runtime/agent-updates.sock` with 0600 permissions, and reaches the frontend. Full Tauri test suite (3031), clippy, fmt, and the file-size gate pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
This commit is contained in:
@@ -3,7 +3,7 @@ use std::{
|
||||
fs,
|
||||
os::unix::{
|
||||
fs::{FileTypeExt, PermissionsExt},
|
||||
net::UnixStream as StdUnixStream,
|
||||
net::{UnixListener as StdUnixListener, UnixStream as StdUnixStream},
|
||||
},
|
||||
path::PathBuf,
|
||||
time::Duration,
|
||||
@@ -48,19 +48,45 @@ pub(super) fn start(app: AppHandle) -> Result<(), String> {
|
||||
.map_err(|error| format!("remove stale project canvas update socket: {error}"))?;
|
||||
}
|
||||
|
||||
let std_listener = std::os::unix::net::UnixListener::bind(&socket_path)
|
||||
let std_listener = StdUnixListener::bind(&socket_path)
|
||||
.map_err(|error| format!("bind project canvas update socket: {error}"))?;
|
||||
fs::set_permissions(&socket_path, fs::Permissions::from_mode(0o600))
|
||||
.map_err(|error| format!("secure project canvas update socket: {error}"))?;
|
||||
std_listener
|
||||
.set_nonblocking(true)
|
||||
.map_err(|error| format!("configure project canvas update socket: {error}"))?;
|
||||
let listener = tokio::net::UnixListener::from_std(std_listener)
|
||||
.map_err(|error| format!("start project canvas update socket: {error}"))?;
|
||||
tauri::async_runtime::spawn(run(listener, app, socket_path));
|
||||
let serve_path = socket_path.clone();
|
||||
spawn_serving(std_listener, socket_path, move |listener| {
|
||||
run(listener, app, serve_path)
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Hand a bound socket to the async runtime and serve it there.
|
||||
///
|
||||
/// `tokio::net::UnixListener::from_std` registers the socket with the Tokio
|
||||
/// reactor, so it only works from inside the runtime. `start` runs on the main
|
||||
/// thread from Tauri's `setup` hook, which is not in runtime context — doing
|
||||
/// the conversion there panics, and the panic crosses the non-unwinding
|
||||
/// `did_finish_launching` boundary, aborting the app before it opens a window.
|
||||
/// So the conversion has to happen in the spawned task, not at the call site.
|
||||
#[cfg(unix)]
|
||||
pub(super) fn spawn_serving<F, Fut>(std_listener: StdUnixListener, socket_path: PathBuf, serve: F)
|
||||
where
|
||||
F: FnOnce(tokio::net::UnixListener) -> Fut + Send + 'static,
|
||||
Fut: std::future::Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match tokio::net::UnixListener::from_std(std_listener) {
|
||||
Ok(listener) => serve(listener).await,
|
||||
Err(error) => {
|
||||
eprintln!("buzz-desktop: project Canvas update socket stopped: {error}");
|
||||
let _ = fs::remove_file(&socket_path);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub(super) fn start(_app: tauri::AppHandle) -> Result<(), String> {
|
||||
Ok(())
|
||||
|
||||
@@ -655,3 +655,32 @@ fn native_navigation_policy_blocks_external_document_navigation() {
|
||||
&"file:///tmp/secret".parse().unwrap()
|
||||
));
|
||||
}
|
||||
|
||||
// `start` runs on the main thread from Tauri's `setup` hook, with no Tokio
|
||||
// runtime in context. Registering the socket with the reactor there aborts the
|
||||
// whole app on launch, so the handoff has to survive a plain sync caller.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn agent_update_socket_serves_when_started_outside_the_async_runtime() {
|
||||
use std::{
|
||||
os::unix::net::{UnixListener as StdUnixListener, UnixStream as StdUnixStream},
|
||||
sync::mpsc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
let temp = TempDir::new().unwrap();
|
||||
let socket_path = temp.path().join("agent-updates.sock");
|
||||
let listener = StdUnixListener::bind(&socket_path).unwrap();
|
||||
listener.set_nonblocking(true).unwrap();
|
||||
|
||||
let (accepted, wait) = mpsc::channel();
|
||||
super::ipc::spawn_serving(listener, socket_path.clone(), move |listener| async move {
|
||||
if listener.accept().await.is_ok() {
|
||||
let _ = accepted.send(());
|
||||
}
|
||||
});
|
||||
|
||||
let _client = StdUnixStream::connect(&socket_path).unwrap();
|
||||
wait.recv_timeout(Duration::from_secs(5))
|
||||
.expect("socket bound before the runtime handoff should still accept connections");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user