Files
simplex-chat/plans/2026-05-13-desktop-single-instance.md
T
shGitHubEvgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
c165663555 desktop: prevent duplicate launches (#6979)
* desktop: prevent duplicate launches

Acquires a file lock and listens on a loopback ServerSocket in dataDir.
A second launch signals the running instance to restore its window and
exits silently. See plans/2026-05-13-desktop-single-instance.md.

* desktop: un-minimize window in showWindow

toFront() does not un-minimize a JFrame on any AWT platform. Clear the
ICONIFIED bit so a minimized window restores; preserves MAXIMIZED_BOTH.
Also fixes the same case when restoring from the tray icon.

* desktop: move showWindow from DesktopTray to DesktopApp

It has callers outside the tray (single-instance signal) and belongs
next to simplexWindowState, which it operates on.

* simplify

* refactor

* desktop: start show-file watcher when choosing minimize from first-close dialog

The handleCloseRequest path already starts the watcher when minimizing to
tray; the Ask-dialog path did not, so the first-time user who picks
"Minimize to tray" got a hidden window with no signal handling — a
duplicate launch would not restore it.

* desktop: always watch for duplicate-launch signal, drop hung-instance alert

The watcher now runs for the JVM lifetime once the lock is acquired,
not only when minimized to tray. Duplicate launches always restore the
primary's window (un-minimize, un-tray-hide, toFront) instead of being
silently dropped when the primary is not minimized.

Drops the "may be hung, start anyway?" popup and the two strings — that
fallback was needed only because the watcher could miss signals. With
the always-on watcher there is no scenario where the primary fails to
consume simplex.show, so the escape hatch becomes dead code.

* desktop: alert when primary's watcher doesn't consume the show file

Restores the "another instance may be running" alert. Every duplicate
launch waits up to 1s for the primary's watcher to delete the show file
it just created. If the file is consumed within the window, the
duplicate exits silently. If still there after 1s the primary is hung
and the alert fires.

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
2026-05-18 09:15:20 +01:00

1.8 KiB

Desktop single instance - restore on duplicate launch

Problem

After tray support (#6970), the desktop app can minimize to tray. The process stays alive holding the database. When the user clicks the app launcher again (forgetting about the tray), a second process starts and either crashes on the SQLite lock or runs in a degraded state.

Design

Two files in dataDir: simplex.started (lock file) and simplex.show (signal file).

Startup

  1. Try FileChannel.tryLock(0, 1, false) on simplex.started.
  2. Lock acquired: delete stale simplex.show if present (leftover from crash), start a daemon WatchService on dataDir for ENTRY_CREATE, start the app normally.
  3. Lock taken (another process holds it): create simplex.show, exit. The running instance detects it and shows its window.
  4. Lock fails (IOException, filesystem doesn't support locks, etc.): start normally but disable minimize-to-tray. Close quits the app. No worse than before tray support existed.

Signal handling

While the lock is held, the daemon watcher runs for the JVM lifetime. When simplex.show appears it deletes the file and posts showWindow() to the EDT. showWindow() sets windowVisible = true, clears ICONIFIED, and brings the window to front — restores from tray, from taskbar-minimize, or just raises if visible-but-behind.

Minimize-to-tray is only available when singleInstanceLock is held. If the lock couldn't be acquired (case 4), close always quits - preventing the scenario where two tray'd instances fight over the database.

Crash recovery

The OS releases the file lock when the process dies. simplex.show may be left behind but is harmless - the next startup (step 2) deletes it.

Scope

Linux, Windows, macOS. Per-data-directory - separate installs with different dataDir run independently.