From 52ffd46a6abc2c233cb49d968baa469ab262d570 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Wed, 9 Apr 2025 08:14:17 +0100 Subject: [PATCH] Desktop: Resolves #12006: Add a new menu item to launch the primary instance from the secondary one --- .eslintignore | 3 ++- .gitignore | 3 ++- packages/app-desktop/ElectronAppWrapper.ts | 2 +- packages/app-desktop/bridge.ts | 8 +++++++- packages/app-desktop/commands/index.ts | 6 ++++-- .../commands/openPrimaryAppInstance.ts | 19 +++++++++++++++++++ ...nstance.ts => openSecondaryAppInstance.ts} | 6 +++--- packages/app-desktop/gui/MenuBar.tsx | 6 ++++-- packages/app-desktop/gui/menuCommandNames.ts | 3 ++- packages/utils/execCommand.ts | 12 ++++++++---- readme/apps/multiple_instances.md | 18 +++++++++++++++--- 11 files changed, 67 insertions(+), 19 deletions(-) create mode 100644 packages/app-desktop/commands/openPrimaryAppInstance.ts rename packages/app-desktop/commands/{newAppInstance.ts => openSecondaryAppInstance.ts} (75%) diff --git a/.eslintignore b/.eslintignore index 351518d5bb..24a0bd1a93 100644 --- a/.eslintignore +++ b/.eslintignore @@ -158,9 +158,10 @@ packages/app-desktop/commands/exportFolders.js packages/app-desktop/commands/exportNotes.js packages/app-desktop/commands/focusElement.js packages/app-desktop/commands/index.js -packages/app-desktop/commands/newAppInstance.js packages/app-desktop/commands/openNoteInNewWindow.js +packages/app-desktop/commands/openPrimaryAppInstance.js packages/app-desktop/commands/openProfileDirectory.js +packages/app-desktop/commands/openSecondaryAppInstance.js packages/app-desktop/commands/replaceMisspelling.js packages/app-desktop/commands/restoreNoteRevision.js packages/app-desktop/commands/startExternalEditing.js diff --git a/.gitignore b/.gitignore index 6a4604fb7b..ab0fe5484a 100644 --- a/.gitignore +++ b/.gitignore @@ -133,9 +133,10 @@ packages/app-desktop/commands/exportFolders.js packages/app-desktop/commands/exportNotes.js packages/app-desktop/commands/focusElement.js packages/app-desktop/commands/index.js -packages/app-desktop/commands/newAppInstance.js packages/app-desktop/commands/openNoteInNewWindow.js +packages/app-desktop/commands/openPrimaryAppInstance.js packages/app-desktop/commands/openProfileDirectory.js +packages/app-desktop/commands/openSecondaryAppInstance.js packages/app-desktop/commands/replaceMisspelling.js packages/app-desktop/commands/restoreNoteRevision.js packages/app-desktop/commands/startExternalEditing.js diff --git a/packages/app-desktop/ElectronAppWrapper.ts b/packages/app-desktop/ElectronAppWrapper.ts index 53c6aa7bf6..078ed82d29 100644 --- a/packages/app-desktop/ElectronAppWrapper.ts +++ b/packages/app-desktop/ElectronAppWrapper.ts @@ -655,7 +655,7 @@ export default class ElectronAppWrapper { // might still be there for a short while. await msleep(1000); this.ipcLogger_.warn('restartAltInstance: App is gone - restarting it'); - void bridge().launchNewAppInstance(this.env()); + void bridge().launchAltAppInstance(this.env()); } else { this.ipcLogger_.warn('restartAltInstance: Could not restart calling app because it was still open'); } diff --git a/packages/app-desktop/bridge.ts b/packages/app-desktop/bridge.ts index 9d4302673e..ade7e0785c 100644 --- a/packages/app-desktop/bridge.ts +++ b/packages/app-desktop/bridge.ts @@ -523,12 +523,18 @@ export class Bridge { } } - public async launchNewAppInstance(env: string) { + public async launchAltAppInstance(env: string) { const cmd = this.appLaunchCommand(env, 'alt1'); await execCommand([cmd.execPath].concat(cmd.args), { detached: true }); } + public async launchMainAppInstance(env: string) { + const cmd = this.appLaunchCommand(env, ''); + + await execCommand([cmd.execPath].concat(cmd.args), { detached: true }); + } + public async restart() { // Note that in this case we are not sending the "appClose" event // to notify services and component that the app is about to close diff --git a/packages/app-desktop/commands/index.ts b/packages/app-desktop/commands/index.ts index eadafe80eb..3161eceedd 100644 --- a/packages/app-desktop/commands/index.ts +++ b/packages/app-desktop/commands/index.ts @@ -6,7 +6,8 @@ import * as exportDeletionLog from './exportDeletionLog'; import * as exportFolders from './exportFolders'; import * as exportNotes from './exportNotes'; import * as focusElement from './focusElement'; -import * as newAppInstance from './newAppInstance'; +import * as openSecondaryAppInstance from './openSecondaryAppInstance'; +import * as openPrimaryAppInstance from './openPrimaryAppInstance'; import * as openNoteInNewWindow from './openNoteInNewWindow'; import * as openProfileDirectory from './openProfileDirectory'; import * as replaceMisspelling from './replaceMisspelling'; @@ -29,7 +30,8 @@ const index: any[] = [ exportFolders, exportNotes, focusElement, - newAppInstance, + openSecondaryAppInstance, + openPrimaryAppInstance, openNoteInNewWindow, openProfileDirectory, replaceMisspelling, diff --git a/packages/app-desktop/commands/openPrimaryAppInstance.ts b/packages/app-desktop/commands/openPrimaryAppInstance.ts new file mode 100644 index 0000000000..2baf8bd761 --- /dev/null +++ b/packages/app-desktop/commands/openPrimaryAppInstance.ts @@ -0,0 +1,19 @@ +import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService'; +import { _ } from '@joplin/lib/locale'; +import bridge from '../services/bridge'; +import Setting from '@joplin/lib/models/Setting'; + +export const declaration: CommandDeclaration = { + name: 'openPrimaryAppInstance', + label: () => _('Open primary app instance...'), +}; + +export const runtime = (): CommandRuntime => { + return { + execute: async (_context: CommandContext) => { + await bridge().launchMainAppInstance(Setting.value('env')); + }, + + enabledCondition: 'isAltInstance', + }; +}; diff --git a/packages/app-desktop/commands/newAppInstance.ts b/packages/app-desktop/commands/openSecondaryAppInstance.ts similarity index 75% rename from packages/app-desktop/commands/newAppInstance.ts rename to packages/app-desktop/commands/openSecondaryAppInstance.ts index 5443a64c8e..b77a0078d8 100644 --- a/packages/app-desktop/commands/newAppInstance.ts +++ b/packages/app-desktop/commands/openSecondaryAppInstance.ts @@ -4,14 +4,14 @@ import bridge from '../services/bridge'; import Setting from '@joplin/lib/models/Setting'; export const declaration: CommandDeclaration = { - name: 'newAppInstance', - label: () => _('New application instance...'), + name: 'openSecondaryAppInstance', + label: () => _('Open secondary app instance...'), }; export const runtime = (): CommandRuntime => { return { execute: async (_context: CommandContext) => { - await bridge().launchNewAppInstance(Setting.value('env')); + await bridge().launchAltAppInstance(Setting.value('env')); }, enabledCondition: '!isAltInstance', diff --git a/packages/app-desktop/gui/MenuBar.tsx b/packages/app-desktop/gui/MenuBar.tsx index 977a0a2c23..34d60c6a40 100644 --- a/packages/app-desktop/gui/MenuBar.tsx +++ b/packages/app-desktop/gui/MenuBar.tsx @@ -555,7 +555,8 @@ function useMenu(props: Props) { const newFolderItem = menuItemDic.newFolder; const newSubFolderItem = menuItemDic.newSubFolder; const printItem = menuItemDic.print; - const newAppInstance = menuItemDic.newAppInstance; + const openSecondaryAppInstance = menuItemDic.openSecondaryAppInstance; + const openPrimaryAppInstance = menuItemDic.openPrimaryAppInstance; const switchProfileItem = { label: _('Switch profile'), submenu: switchProfileMenuItems, @@ -723,7 +724,8 @@ function useMenu(props: Props) { type: 'separator', }, switchProfileItem, - newAppInstance, + openSecondaryAppInstance, + openPrimaryAppInstance, ], }; diff --git a/packages/app-desktop/gui/menuCommandNames.ts b/packages/app-desktop/gui/menuCommandNames.ts index e04b001ed0..8602ba8e22 100644 --- a/packages/app-desktop/gui/menuCommandNames.ts +++ b/packages/app-desktop/gui/menuCommandNames.ts @@ -48,7 +48,8 @@ export default function() { 'toggleTabMovesFocus', 'editor.deleteLine', 'editor.duplicateLine', - 'newAppInstance', + 'openSecondaryAppInstance', + 'openPrimaryAppInstance', // We cannot put the undo/redo commands in the menu because they are // editor-specific commands. If we put them there it will break the // undo/redo in regular text fields. diff --git a/packages/utils/execCommand.ts b/packages/utils/execCommand.ts index 458e5d7234..694f8aa3e9 100644 --- a/packages/utils/execCommand.ts +++ b/packages/utils/execCommand.ts @@ -14,13 +14,17 @@ interface ExecCommandOptions { } export default async (command: string | string[], options: ExecCommandOptions | null = null): Promise => { + const detached = options ? options.detached : false; + + // When launching a detached executable it's better not to pipe the stdout and stderr, as this + // will most likely cause an EPIPE error. + options = { - showInput: true, - showStdout: true, - showStderr: true, + showInput: !detached, + showStdout: !detached, + showStderr: !detached, quiet: false, env: {}, - detached: false, ...options, }; diff --git a/readme/apps/multiple_instances.md b/readme/apps/multiple_instances.md index e687a9a2fb..5c046e526e 100644 --- a/readme/apps/multiple_instances.md +++ b/readme/apps/multiple_instances.md @@ -17,9 +17,9 @@ Each instance is completely isolated, operating as a standalone version of Jopli Currently, Joplin supports up to **two running instances**: -1. **Main Instance**: The primary application instance, with full access to all Joplin features. +1. **Primary Instance**: The primary application instance, with full access to all Joplin features. -2. **Alternative Instance**: A secondary application instance that functions independently. However, it does not support the **Web Clipper service**, which can only run in the main instance. +2. **Secondary Instance**: A secondary application instance that functions independently. However, it does not support the **Web Clipper service**, which can only run in the main instance. ## How to Launch a Second Instance @@ -29,8 +29,20 @@ To start a second instance of Joplin: 2. Navigate to the menu and select: -**File** => **New application instance...** +**File** => **Open secondary app instance...** 3. A new instance of Joplin will open with its own profile. This second instance operates independently, allowing you to customise it as needed. + +## Caveats + +### Launching the primary instance when the secondary instance is active + +Technically, the secondary instance is still initiated from the same executable file, which might confuse the operating system. Most operating systems reasonably assume that if you attempt to launch a GUI application that is already running, your intention is to bring that application into focus. + +In practical terms, this means the following: + +If you close the primary instance while the secondary instance remains open, and then attempt to reopen the primary instance—for instance, by clicking on its icon—the operating system will most likely refocus on the secondary instance instead of launching the primary one. To address this issue, the secondary instance includes a menu item labelled **Open primary app instance...**. Clicking on this option will explicitly launch the primary instance. + +In the same way, the secondary instance should generally be launched only from the first one, using the **Open secondary app instance...** menu item.