Skip to content

Commit cdd373e

Browse files
committed
feat(gui): respawn daemon if it dies between probe and connect
The is_service_running() probe answers "is a daemon listening right now"; the GUI's IPC connect happens after GTK initialization finishes, milliseconds-to-seconds later. A daemon that crashed during that window would leave the GUI hanging in connect()'s retry loop forever. Two changes close the gap: - lan-mouse-ipc: add `try_connect()`, a one-shot non-retrying connect that exposes the underlying I/O error so callers can branch on "no daemon" vs "real failure". - lan-mouse-gtk: when main.rs has flagged the daemon as externally managed (we skipped spawning a child), `build_ui` probes with try_connect first; on failure it spawns a fresh daemon (fire-and-forget — matches the externally-managed daemon semantics) and falls back to the retrying connect() to bind against the new instance. The else branch (we *did* spawn a child) keeps the existing retrying connect() so the normal startup window where the daemon hasn't bound yet still works as before.
1 parent 9aac308 commit cdd373e

4 files changed

Lines changed: 101 additions & 13 deletions

File tree

lan-mouse-gtk/src/lib.rs

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,11 @@ pub(crate) fn request_quit_with_backstop(app: &adw::Application) {
8484
app.quit();
8585
}
8686

87-
pub fn run(gui_lock: Option<GuiLock>, local_commit: [u8; 8]) -> Result<(), GtkError> {
87+
pub fn run(
88+
gui_lock: Option<GuiLock>,
89+
local_commit: [u8; 8],
90+
external_daemon: bool,
91+
) -> Result<(), GtkError> {
8892
log::debug!("running gtk frontend");
8993
LOCAL_COMMIT
9094
.set(local_commit)
@@ -114,20 +118,23 @@ pub fn run(gui_lock: Option<GuiLock>, local_commit: [u8; 8]) -> Result<(), GtkEr
114118
let ret = std::thread::Builder::new()
115119
.stack_size(8 * 1024 * 1024) // https://gitlab.gnome.org/GNOME/gtk/-/commit/52dbb3f372b2c3ea339e879689c1de535ba2c2c3 -> caused crash on windows
116120
.name("gtk".into())
117-
.spawn(move || gtk_main(show_rx))
121+
.spawn(move || gtk_main(show_rx, external_daemon))
118122
.unwrap()
119123
.join()
120124
.unwrap();
121125
#[cfg(not(windows))]
122-
let ret = gtk_main(show_rx);
126+
let ret = gtk_main(show_rx, external_daemon);
123127

124128
match ret {
125129
glib::ExitCode::SUCCESS => Ok(()),
126130
e => Err(GtkError::NonZeroExitCode(e.value())),
127131
}
128132
}
129133

130-
fn gtk_main(show_rx: Option<async_channel::Receiver<()>>) -> glib::ExitCode {
134+
fn gtk_main(
135+
show_rx: Option<async_channel::Receiver<()>>,
136+
external_daemon: bool,
137+
) -> glib::ExitCode {
131138
#[cfg(target_os = "macos")]
132139
{
133140
configure_macos_bundle_environment();
@@ -145,7 +152,7 @@ fn gtk_main(show_rx: Option<async_channel::Receiver<()>>) -> glib::ExitCode {
145152
setup_actions(app);
146153
setup_menu(app);
147154
});
148-
app.connect_activate(move |app| build_ui(app, show_rx.clone()));
155+
app.connect_activate(move |app| build_ui(app, show_rx.clone(), external_daemon));
149156

150157
let args: Vec<&'static str> = vec![];
151158
app.run_with_args(&args)
@@ -257,7 +264,11 @@ fn setup_menu(app: &adw::Application) {
257264
app.set_menubar(Some(&menu))
258265
}
259266

260-
fn build_ui(app: &Application, show_rx: Option<async_channel::Receiver<()>>) {
267+
fn build_ui(
268+
app: &Application,
269+
show_rx: Option<async_channel::Receiver<()>>,
270+
external_daemon: bool,
271+
) {
261272
// Defense in depth: if `activate` fires a second time in the same
262273
// process — the GApplication DBus single-instance hand-off does
263274
// this on Linux, and `kAEReopenApplication` does this on macOS —
@@ -268,7 +279,44 @@ fn build_ui(app: &Application, show_rx: Option<async_channel::Receiver<()>>) {
268279
}
269280

270281
log::debug!("connecting to lan-mouse-socket");
271-
let (mut frontend_rx, frontend_tx) = match lan_mouse_ipc::connect() {
282+
// Two paths into the IPC socket:
283+
//
284+
// - main.rs spawned a daemon child (external_daemon == false):
285+
// use the retrying connect(), which handles the racy startup
286+
// window where the daemon hasn't bound yet.
287+
//
288+
// - main.rs detected an existing daemon and skipped spawning
289+
// (external_daemon == true): probe with try_connect; if it
290+
// fails (the daemon died between is_service_running and now)
291+
// spawn a fresh one ourselves and fall back to the retrying
292+
// connect(). The respawned daemon is fire-and-forget — the
293+
// GUI no longer manages its lifecycle, matching the
294+
// externally-managed-daemon semantics that put us on this
295+
// branch in the first place.
296+
let conn_result = if external_daemon {
297+
match lan_mouse_ipc::try_connect() {
298+
Ok(conn) => Ok(conn),
299+
Err(probe_err) => {
300+
log::warn!(
301+
"could not attach to existing daemon ({probe_err}) — spawning a new one"
302+
);
303+
if let Err(spawn_err) = process::Command::new(
304+
env::current_exe().expect("could not determine executable path"),
305+
)
306+
.args(env::args().skip(1))
307+
.arg("daemon")
308+
.spawn()
309+
{
310+
log::error!("failed to spawn replacement daemon: {spawn_err}");
311+
process::exit(1);
312+
}
313+
lan_mouse_ipc::connect()
314+
}
315+
}
316+
} else {
317+
lan_mouse_ipc::connect()
318+
};
319+
let (mut frontend_rx, frontend_tx) = match conn_result {
272320
Ok(conn) => conn,
273321
Err(e) => {
274322
log::error!("{e}");

lan-mouse-ipc/src/connect.rs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,52 @@ impl FrontendRequestWriter {
4747

4848
pub fn connect() -> Result<(FrontendEventReader, FrontendRequestWriter), ConnectionError> {
4949
let rx = wait_for_service()?;
50+
make_connection(rx)
51+
}
52+
53+
/// One-shot connect with no retry. Use when the caller has already
54+
/// probed the IPC socket via [`crate::is_service_running`] and is
55+
/// prepared to take a recovery path on failure (e.g. spawn a fresh
56+
/// daemon and then call [`connect`]).
57+
#[cfg(unix)]
58+
pub fn try_connect() -> Result<(FrontendEventReader, FrontendRequestWriter), ConnectionError> {
59+
let socket_path = crate::default_socket_path()?;
60+
let rx = UnixStream::connect(&socket_path)?;
61+
make_connection(rx)
62+
}
63+
64+
#[cfg(windows)]
65+
pub fn try_connect() -> Result<(FrontendEventReader, FrontendRequestWriter), ConnectionError> {
66+
let rx = TcpStream::connect("127.0.0.1:5252")?;
67+
make_connection(rx)
68+
}
69+
70+
#[cfg(unix)]
71+
fn make_connection(
72+
rx: UnixStream,
73+
) -> Result<(FrontendEventReader, FrontendRequestWriter), ConnectionError> {
74+
let tx = rx.try_clone()?;
75+
let buf_reader = BufReader::new(rx);
76+
let lines = buf_reader.lines();
77+
let line_writer = LineWriter::new(tx);
78+
Ok((
79+
FrontendEventReader { lines },
80+
FrontendRequestWriter { line_writer },
81+
))
82+
}
83+
84+
#[cfg(windows)]
85+
fn make_connection(
86+
rx: TcpStream,
87+
) -> Result<(FrontendEventReader, FrontendRequestWriter), ConnectionError> {
5088
let tx = rx.try_clone()?;
5189
let buf_reader = BufReader::new(rx);
5290
let lines = buf_reader.lines();
5391
let line_writer = LineWriter::new(tx);
54-
let reader = FrontendEventReader { lines };
55-
let writer = FrontendRequestWriter { line_writer };
56-
Ok((reader, writer))
92+
Ok((
93+
FrontendEventReader { lines },
94+
FrontendRequestWriter { line_writer },
95+
))
5796
}
5897

5998
/// wait for the lan-mouse socket to come online

lan-mouse-ipc/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ mod connect_async;
2121
mod gui_lock;
2222
mod listen;
2323

24-
pub use connect::{FrontendEventReader, FrontendRequestWriter, connect};
24+
pub use connect::{FrontendEventReader, FrontendRequestWriter, connect, try_connect};
2525
pub use connect_async::{AsyncFrontendEventReader, AsyncFrontendRequestWriter, connect_async};
2626
pub use gui_lock::{GuiLock, GuiLockError};
2727
pub use listen::AsyncFrontendListener;

src/main.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,14 @@ fn run() -> Result<(), LanMouseError> {
113113
// case where a previous GUI's daemon outlived its
114114
// parent. The GUI then attaches to the existing
115115
// daemon and leaves it running on exit.
116-
let mut service = if lan_mouse_ipc::is_service_running() {
116+
let external_daemon = lan_mouse_ipc::is_service_running();
117+
let mut service = if external_daemon {
117118
log::info!("daemon already running; attaching to existing instance");
118119
None
119120
} else {
120121
Some(start_service()?)
121122
};
122-
let res = lan_mouse_gtk::run(gui_lock, config::local_commit());
123+
let res = lan_mouse_gtk::run(gui_lock, config::local_commit(), external_daemon);
123124

124125
// Bound the daemon-child cleanup so a wedged daemon
125126
// (CGEventTap stuck on macOS, hung syscall, etc.)

0 commit comments

Comments
 (0)