Skip to content

Commit d3ad02e

Browse files
committed
feat(gui): skip transient daemon child when daemon already running
The GUI startup path always spawned a `lan-mouse daemon` child process and immediately killed it on exit. If a daemon was already running — e.g. because the user had launched `lan-mouse daemon` standalone — the spawned child died early with `AlreadyRunning`, producing a noisy "service already running!" line and a brief phantom process in the tree. Probe the IPC socket via `lan-mouse-ipc::is_service_running` before spawning. If something's already listening, attach to it and skip the child entirely; the existing SIGINT/wait/SIGKILL teardown is gated on a `Some(child)` so an externally managed daemon outlives the GUI as expected. The probe is a one-shot connect; a stale socket file with no listener returns false and the daemon's own bind path cleans the file up. Inspired by the approach in feschber#407, layered on top of feschber#436's dedicated GuiLock so the singleton guarantee remains atomic and cross-platform — this just removes the transient-child noise on the headless-daemon-then-GUI path.
1 parent eb4e016 commit d3ad02e

2 files changed

Lines changed: 33 additions & 4 deletions

File tree

lan-mouse-ipc/src/lib.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,3 +425,19 @@ pub fn default_socket_path() -> Result<PathBuf, SocketPathError> {
425425
.join("Caches")
426426
.join(LAN_MOUSE_SOCKET_NAME))
427427
}
428+
429+
/// One-shot probe: is a daemon already listening on the IPC socket?
430+
/// A stale socket file with no listener returns `false` (the daemon's
431+
/// own listener bind path will clean it up).
432+
#[cfg(unix)]
433+
pub fn is_service_running() -> bool {
434+
let Ok(socket_path) = default_socket_path() else {
435+
return false;
436+
};
437+
std::os::unix::net::UnixStream::connect(socket_path).is_ok()
438+
}
439+
440+
#[cfg(windows)]
441+
pub fn is_service_running() -> bool {
442+
std::net::TcpStream::connect("127.0.0.1:5252").is_ok()
443+
}

src/main.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,28 @@ fn run() -> Result<(), LanMouseError> {
107107
}
108108
};
109109

110-
let mut service = start_service()?;
110+
// Skip the transient daemon child if a daemon is
111+
// already listening on the IPC socket — covers both
112+
// an externally-started `lan-mouse daemon` and the
113+
// case where a previous GUI's daemon outlived its
114+
// parent. The GUI then attaches to the existing
115+
// daemon and leaves it running on exit.
116+
let mut service = if lan_mouse_ipc::is_service_running() {
117+
log::info!("daemon already running; attaching to existing instance");
118+
None
119+
} else {
120+
Some(start_service()?)
121+
};
111122
let res = lan_mouse_gtk::run(gui_lock, config::local_commit());
112123

113124
// Bound the daemon-child cleanup so a wedged daemon
114125
// (CGEventTap stuck on macOS, hung syscall, etc.)
115126
// can't freeze the GUI on quit. SIGINT first, give it
116-
// a few seconds to exit cleanly, then SIGKILL.
127+
// a few seconds to exit cleanly, then SIGKILL. Only
128+
// runs when *we* spawned the daemon — an externally
129+
// managed daemon outlives the GUI.
117130
#[cfg(unix)]
118-
{
131+
if let Some(service) = service.as_mut() {
119132
let pid = service.id() as libc::pid_t;
120133
unsafe {
121134
libc::kill(pid, libc::SIGINT);
@@ -141,7 +154,7 @@ fn run() -> Result<(), LanMouseError> {
141154
}
142155
}
143156
#[cfg(not(unix))]
144-
{
157+
if let Some(service) = service.as_mut() {
145158
let _ = service.kill();
146159
let _ = service.wait();
147160
}

0 commit comments

Comments
 (0)