From 17e4420c49d6e0ca78b2a68edd979a08ed0598fc Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 6 May 2026 16:16:25 -0500 Subject: [PATCH] WIP: Add single-instance listener via app-launcher Registers a TCP socket listener (via SingleInstance.listen from app-launcher) so that a secondary transient instance launched by the OS can hand off its arguments to the already-running primary instance and exit immediately, before the splash screen ever appears. This feature is implemented in scijava-desktop -- not in scijava-common or app-launcher directly -- because OS-level integration features of scijava-desktop like file type associations and URI scheme registrations result in a new application process being launched each time a file or link of an associated type is opened. But for more complex applications like those the SciJava application framework is intended to build, users typically expect the already-running application to handle the newly opened file or link, rather than multiple instances stacking up. Bundling the single instance listener here keeps the responsibility clearly located and avoids scijava-common depending on app-launcher or vice versa -- scijava-common is intended to be launcher-agnostic, while app-launcher is designed as a minimal bootstrap tool that should not depend on the full framework it launches. When arguments arrive from a secondary instance, the receiveArgs callback runs an appropriate subset of the launch sequence: 1. processArgs: dispatches args to registered ConsoleArgument handlers, some of which queue deferred work to StartupService rather than acting immediately because they need the UI to be visible 2. execMains: handles any --main flags 3. executeOperations: drains whatever was queued by processArgs The feature is opt-in via the scijava.app.single-instance system property, so that a launched application only becomes a single instance under appropriate conditions, as determined by the launch sequence. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 13 +++++++- pom.xml | 6 ++++ .../desktop/DefaultDesktopService.java | 32 +++++++++++++++++++ .../org/scijava/desktop/DesktopService.java | 4 +++ 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b5da8c0..51e851f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Unified desktop integration for SciJava applications. ## Features -The scijava-desktop component provides three kinds of desktop integration: +The scijava-desktop component provides four kinds of desktop integration: 1. **URI Link Scheme Registration & Handling** - Register custom URI schemes (e.g., `myapp://`) with the operating system @@ -22,6 +22,17 @@ The scijava-desktop component provides three kinds of desktop integration: - Associate file types with your application - Platform-specific MIME type handling +4. **Single-Instance Enforcement** + - Prevents multiple application instances when the OS re-launches the binary + - Uses a TCP socket listener via the `SingleInstance` feature of the + [SciJava App Launcher](https://github.com/scijava/app-launcher) to let a + secondary transient instance hand off its arguments to the already-running + primary instance and exit immediately -- before the splash screen appears + - Enabled via the `scijava.app.single-instance` system property + - Useful in conjunction with file type associations and URI scheme + registration, both of which trigger the OS to launch another copy of the + application binary when a registered file type or link is clicked + ## Platform Support - **Linux**: Full support for URI schemes and desktop icons via `.desktop` files diff --git a/pom.xml b/pom.xml index 6446688..7912073 100644 --- a/pom.xml +++ b/pom.xml @@ -104,10 +104,16 @@ 11 bsd_2 SciJava developers. + 2.4.0-SNAPSHOT + true + + org.scijava + app-launcher + org.scijava scijava-common diff --git a/src/main/java/org/scijava/desktop/DefaultDesktopService.java b/src/main/java/org/scijava/desktop/DefaultDesktopService.java index 35f9ba1..cd2b221 100644 --- a/src/main/java/org/scijava/desktop/DefaultDesktopService.java +++ b/src/main/java/org/scijava/desktop/DefaultDesktopService.java @@ -28,9 +28,12 @@ */ package org.scijava.desktop; +import org.scijava.console.ConsoleService; import org.scijava.event.ContextCreatedEvent; import org.scijava.event.EventHandler; +import org.scijava.launcher.SingleInstance; import org.scijava.log.LogService; +import org.scijava.main.MainService; import org.scijava.object.LazyObjects; import org.scijava.object.ObjectService; import org.scijava.platform.PlatformService; @@ -39,6 +42,7 @@ import org.scijava.prefs.PrefService; import org.scijava.service.AbstractService; import org.scijava.service.Service; +import org.scijava.startup.StartupService; import org.scijava.thread.ThreadService; import java.io.BufferedReader; @@ -68,6 +72,15 @@ public class DefaultDesktopService extends AbstractService implements DesktopSer @Parameter private ThreadService threadService; + @Parameter + private ConsoleService consoleService; + + @Parameter + private MainService mainService; + + @Parameter + private StartupService startupService; + @Parameter(required = false) private PrefService prefs; @@ -165,6 +178,15 @@ public String getDescription(final String extension) { return descriptions.get(extension); } + // -- Service methods -- + + @Override + public void initialize() { + if (isSingleInstanceEnabled()) { + startupService.addOperation(() -> SingleInstance.listen(0, this::receiveArgs)); + } + } + // -- Event handlers -- @EventHandler @@ -279,6 +301,16 @@ private Stream desktopPlatforms() { .map(p -> (DesktopIntegrationProvider) p); } + /** + * Receives arguments from a secondary transient application instance + * and handles them via standard SciJava Common service methods. + */ + private void receiveArgs(String[] args) { + consoleService.processArgs(args); + mainService.execMains(); + startupService.executeOperations(); + } + private void maybeAutoInstallDesktopIntegrations() { // Auto-install desktop integrations on first run. diff --git a/src/main/java/org/scijava/desktop/DesktopService.java b/src/main/java/org/scijava/desktop/DesktopService.java index 879328f..5608711 100644 --- a/src/main/java/org/scijava/desktop/DesktopService.java +++ b/src/main/java/org/scijava/desktop/DesktopService.java @@ -41,6 +41,10 @@ */ public interface DesktopService extends SciJavaService { + default boolean isSingleInstanceEnabled() { + return Boolean.getBoolean("scijava.app.single-instance"); + } + /** * Applies desktop integration settings. *