From f09369ee17c4612b0e165f347a9fe8f7fd15836a Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Fri, 14 Oct 2022 22:31:01 +0200 Subject: [PATCH] Only allow a single running instance Can be overridden by `--allow-multiple-instances` CLI argument closes #1265 closes #527 --- README.md | 4 ++ public/electron.js | 159 ++++++++++++++++++++++++++------------------- 2 files changed, 95 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 881ac670..44376a81 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,10 @@ See [available settings](https://github.com/mifi/lossless-cut/blob/master/public LosslessCut --settings-json '{captureFormat:"jpeg", "keyframeCut":true}' ``` +### Multiple instances + +By default, only a single running instance of LosslessCut is allowed. If you start a new LosslessCut instance from the command line, it will instead pass the list of files onto the already running instance. You can override this behavior by passing `--allow-multiple-instances` via the command line. Running multiple instances is experimental. + ## Developing See the [developer notes](developer-notes.md). diff --git a/public/electron.js b/public/electron.js index a0504998..32237614 100644 --- a/public/electron.js +++ b/public/electron.js @@ -121,100 +121,123 @@ function updateMenu() { menu(app, mainWindow, newVersion); } +function openFilesEventually(paths) { + if (rendererReady) openFiles(paths); + else filesToOpen = paths; +} // https://github.com/electron/electron/issues/3657 // https://github.com/mifi/lossless-cut/issues/357 // https://github.com/mifi/lossless-cut/issues/639 // https://github.com/mifi/lossless-cut/issues/591 -function parseCliArgs() { +function parseCliArgs(rawArgv = process.argv) { const ignoreFirstArgs = isDev ? 2 : 1; // production: First arg is the LosslessCut executable // dev: First 2 args are electron and the electron.js - const argsWithoutAppName = process.argv.length > ignoreFirstArgs ? process.argv.slice(ignoreFirstArgs) : []; + const argsWithoutAppName = rawArgv.length > ignoreFirstArgs ? rawArgv.slice(ignoreFirstArgs) : []; return yargsParser(argsWithoutAppName); } -// This method will be called when Electron has finished -// initialization and is ready to create browser windows. -// Some APIs can only be used after this event occurs. -app.on('ready', async () => { - // https://github.com/electron/electron/issues/23757 - // https://github.com/electron/electron/pull/28489 - // TODO I think this can be removed when we are on electron 12 or 14 - if (isDev) { - electron.protocol.registerFileProtocol('file', (request, callback) => { - const pathname = decodeURIComponent(request.url.replace('file:///', '')); - callback(pathname); - }); - } +const argv = parseCliArgs(); - await configStore.init(); +if (!argv.allowMultipleInstances && !app.requestSingleInstanceLock()) { + app.quit(); +} else { + // On macOS, the system enforces single instance automatically when users try to open a second instance of your app in Finder, and the open-file and open-url events will be emitted for that. + // However when users start your app in command line, the system's single instance mechanism will be bypassed, and you have to use this method to ensure single instance. + // This can be tested with one terminal: npx electron . + // and another terminal: npx electron . path/to/file.mp4 + app.on('second-instance', (event, commandLine) => { + // Someone tried to run a second instance, we should focus our window. + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + } - const argv = parseCliArgs(); - logger.info('CLI arguments', argv); - // Only if no files to open already (open-file might have already added some files) - if (filesToOpen.length === 0) filesToOpen = argv._; - const { settingsJson } = argv; + const argv2 = parseCliArgs(commandLine); + if (argv2._) openFilesEventually(argv2._); + }); - if (settingsJson != null) { - logger.info('initializing settings', settingsJson); - Object.entries(JSON5.parse(settingsJson)).forEach(([key, value]) => { - configStore.set(key, value); - }); - } + // This method will be called when Electron has finished + // initialization and is ready to create browser windows. + // Some APIs can only be used after this event occurs. + app.on('ready', async () => { + // https://github.com/electron/electron/issues/23757 + // https://github.com/electron/electron/pull/28489 + // TODO I think this can be removed when we are on electron 12 or 14 + if (isDev) { + electron.protocol.registerFileProtocol('file', (request, callback) => { + const pathname = decodeURIComponent(request.url.replace('file:///', '')); + callback(pathname); + }); + } - if (isDev) { - const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer'); // eslint-disable-line global-require,import/no-extraneous-dependencies + await configStore.init(); - installExtension(REACT_DEVELOPER_TOOLS) - .then(name => logger.info('Added Extension', name)) - .catch(err => logger.error('Failed to add extension', err)); - } + logger.info('CLI arguments', argv); + // Only if no files to open already (open-file might have already added some files) + if (filesToOpen.length === 0) filesToOpen = argv._; + const { settingsJson } = argv; - createWindow(); - updateMenu(); + if (settingsJson != null) { + logger.info('initializing settings', settingsJson); + Object.entries(JSON5.parse(settingsJson)).forEach(([key, value]) => { + configStore.set(key, value); + }); + } - if (!process.windowsStore && !process.mas) { - newVersion = await checkNewVersion(); - // newVersion = '1.2.3'; - if (newVersion) updateMenu(); - } -}); + if (isDev) { + const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer'); // eslint-disable-line global-require,import/no-extraneous-dependencies -// Quit when all windows are closed. -app.on('window-all-closed', () => { - app.quit(); -}); + installExtension(REACT_DEVELOPER_TOOLS) + .then(name => logger.info('Added Extension', name)) + .catch(err => logger.error('Failed to add extension', err)); + } -app.on('activate', () => { - // On OS X it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (mainWindow === null) { createWindow(); - } -}); + updateMenu(); -ipcMain.on('renderer-ready', () => { - rendererReady = true; - if (filesToOpen.length > 0) openFiles(filesToOpen); -}); + if (!process.windowsStore && !process.mas) { + newVersion = await checkNewVersion(); + // newVersion = '1.2.3'; + if (newVersion) updateMenu(); + } + }); -// Mac OS open with LosslessCut -// Emitted when the user wants to open a file with the application. The open-file event is usually emitted when the application is already open and the OS wants to reuse the application to open the file. -app.on('open-file', (event, path) => { - if (rendererReady) openFiles([path]); - else filesToOpen = [path]; - event.preventDefault(); // recommended in docs https://www.electronjs.org/docs/latest/api/app#event-open-file-macos -}); + // Quit when all windows are closed. + app.on('window-all-closed', () => { + app.quit(); + }); -ipcMain.on('setAskBeforeClose', (e, val) => { - askBeforeClose = val; -}); + app.on('activate', () => { + // On OS X it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (mainWindow === null) { + createWindow(); + } + }); -ipcMain.on('setLanguage', (e, language) => { - i18n.changeLanguage(language).then(() => updateMenu()).catch((err) => logger.error('Failed to set language', err)); -}); + ipcMain.on('renderer-ready', () => { + rendererReady = true; + if (filesToOpen.length > 0) openFiles(filesToOpen); + }); + + // Mac OS open with LosslessCut + // Emitted when the user wants to open a file with the application. The open-file event is usually emitted when the application is already open and the OS wants to reuse the application to open the file. + app.on('open-file', (event, path) => { + openFilesEventually([path]); + event.preventDefault(); // recommended in docs https://www.electronjs.org/docs/latest/api/app#event-open-file-macos + }); + + ipcMain.on('setAskBeforeClose', (e, val) => { + askBeforeClose = val; + }); + + ipcMain.on('setLanguage', (e, language) => { + i18n.changeLanguage(language).then(() => updateMenu()).catch((err) => logger.error('Failed to set language', err)); + }); +} function focusWindow() { try {