UNPKG

@sussudio/platform

Version:

Internal APIs for VS Code's service injection the base services.

428 lines (427 loc) 15.9 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc, d; if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function') r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); }; }; import { app, BrowserWindow } from 'electron'; import { validatedIpcMain } from '@sussudio/base/parts/ipc/electron-main/ipcMain.mjs'; import { Barrier, Promises, timeout } from '@sussudio/base/common/async.mjs'; import { Emitter } from '@sussudio/base/common/event.mjs'; import { Disposable, DisposableStore } from '@sussudio/base/common/lifecycle.mjs'; import { isMacintosh, isWindows } from '@sussudio/base/common/platform.mjs'; import { cwd } from '@sussudio/base/common/process.mjs'; import { assertIsDefined } from '@sussudio/base/common/types.mjs'; import { createDecorator } from '../../instantiation/common/instantiation.mjs'; import { ILogService } from '../../log/common/log.mjs'; import { IStateMainService } from '../../state/electron-main/state.mjs'; import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.mjs'; export const ILifecycleMainService = createDecorator('lifecycleMainService'); let LifecycleMainService = class LifecycleMainService extends Disposable { logService; stateMainService; environmentMainService; static QUIT_AND_RESTART_KEY = 'lifecycle.quitAndRestart'; _onBeforeShutdown = this._register(new Emitter()); onBeforeShutdown = this._onBeforeShutdown.event; _onWillShutdown = this._register(new Emitter()); onWillShutdown = this._onWillShutdown.event; _onWillLoadWindow = this._register(new Emitter()); onWillLoadWindow = this._onWillLoadWindow.event; _onBeforeCloseWindow = this._register(new Emitter()); onBeforeCloseWindow = this._onBeforeCloseWindow.event; _quitRequested = false; get quitRequested() { return this._quitRequested; } _wasRestarted = false; get wasRestarted() { return this._wasRestarted; } _phase = 1 /* LifecycleMainPhase.Starting */; get phase() { return this._phase; } windowToCloseRequest = new Set(); oneTimeListenerTokenGenerator = 0; windowCounter = 0; pendingQuitPromise = undefined; pendingQuitPromiseResolve = undefined; pendingWillShutdownPromise = undefined; mapWindowIdToPendingUnload = new Map(); phaseWhen = new Map(); constructor(logService, stateMainService, environmentMainService) { super(); this.logService = logService; this.stateMainService = stateMainService; this.environmentMainService = environmentMainService; this.resolveRestarted(); this.when(2 /* LifecycleMainPhase.Ready */).then(() => this.registerListeners()); } resolveRestarted() { this._wasRestarted = !!this.stateMainService.getItem(LifecycleMainService.QUIT_AND_RESTART_KEY); if (this._wasRestarted) { // remove the marker right after if found this.stateMainService.removeItem(LifecycleMainService.QUIT_AND_RESTART_KEY); } } registerListeners() { // before-quit: an event that is fired if application quit was // requested but before any window was closed. const beforeQuitListener = () => { if (this._quitRequested) { return; } this.trace('Lifecycle#app.on(before-quit)'); this._quitRequested = true; // Emit event to indicate that we are about to shutdown this.trace('Lifecycle#onBeforeShutdown.fire()'); this._onBeforeShutdown.fire(); // macOS: can run without any window open. in that case we fire // the onWillShutdown() event directly because there is no veto // to be expected. if (isMacintosh && this.windowCounter === 0) { this.fireOnWillShutdown(1 /* ShutdownReason.QUIT */); } }; app.addListener('before-quit', beforeQuitListener); // window-all-closed: an event that only fires when the last window // was closed. We override this event to be in charge if app.quit() // should be called or not. const windowAllClosedListener = () => { this.trace('Lifecycle#app.on(window-all-closed)'); // Windows/Linux: we quit when all windows have closed // Mac: we only quit when quit was requested if (this._quitRequested || !isMacintosh) { app.quit(); } }; app.addListener('window-all-closed', windowAllClosedListener); // will-quit: an event that is fired after all windows have been // closed, but before actually quitting. app.once('will-quit', (e) => { this.trace('Lifecycle#app.on(will-quit)'); // Prevent the quit until the shutdown promise was resolved e.preventDefault(); // Start shutdown sequence const shutdownPromise = this.fireOnWillShutdown(1 /* ShutdownReason.QUIT */); // Wait until shutdown is signaled to be complete shutdownPromise.finally(() => { // Resolve pending quit promise now without veto this.resolvePendingQuitPromise(false /* no veto */); // Quit again, this time do not prevent this, since our // will-quit listener is only installed "once". Also // remove any listener we have that is no longer needed app.removeListener('before-quit', beforeQuitListener); app.removeListener('window-all-closed', windowAllClosedListener); app.quit(); }); }); } fireOnWillShutdown(reason) { if (this.pendingWillShutdownPromise) { return this.pendingWillShutdownPromise; // shutdown is already running } this.trace('Lifecycle#onWillShutdown.fire()'); const joiners = []; this._onWillShutdown.fire({ reason, join(promise) { joiners.push(promise); }, }); this.pendingWillShutdownPromise = (async () => { // Settle all shutdown event joiners try { await Promises.settled(joiners); } catch (error) { this.logService.error(error); } // Then, always make sure at the end // the state service is flushed. try { await this.stateMainService.close(); } catch (error) { this.logService.error(error); } })(); return this.pendingWillShutdownPromise; } set phase(value) { if (value < this.phase) { throw new Error('Lifecycle cannot go backwards'); } if (this._phase === value) { return; } this.trace(`lifecycle (main): phase changed (value: ${value})`); this._phase = value; const barrier = this.phaseWhen.get(this._phase); if (barrier) { barrier.open(); this.phaseWhen.delete(this._phase); } } async when(phase) { if (phase <= this._phase) { return; } let barrier = this.phaseWhen.get(phase); if (!barrier) { barrier = new Barrier(); this.phaseWhen.set(phase, barrier); } await barrier.wait(); } registerWindow(window) { const windowListeners = new DisposableStore(); // track window count this.windowCounter++; // Window Will Load windowListeners.add( window.onWillLoad((e) => this._onWillLoadWindow.fire({ window, workspace: e.workspace, reason: e.reason })), ); // Window Before Closing: Main -> Renderer const win = assertIsDefined(window.win); win.on('close', (e) => { // The window already acknowledged to be closed const windowId = window.id; if (this.windowToCloseRequest.has(windowId)) { this.windowToCloseRequest.delete(windowId); return; } this.trace(`Lifecycle#window.on('close') - window ID ${window.id}`); // Otherwise prevent unload and handle it from window e.preventDefault(); this.unload(window, 1 /* UnloadReason.CLOSE */).then((veto) => { if (veto) { this.windowToCloseRequest.delete(windowId); return; } this.windowToCloseRequest.add(windowId); // Fire onBeforeCloseWindow before actually closing this.trace(`Lifecycle#onBeforeCloseWindow.fire() - window ID ${windowId}`); this._onBeforeCloseWindow.fire(window); // No veto, close window now window.close(); }); }); // Window After Closing win.on('closed', () => { this.trace(`Lifecycle#window.on('closed') - window ID ${window.id}`); // update window count this.windowCounter--; // clear window listeners windowListeners.dispose(); // if there are no more code windows opened, fire the onWillShutdown event, unless // we are on macOS where it is perfectly fine to close the last window and // the application continues running (unless quit was actually requested) if (this.windowCounter === 0 && (!isMacintosh || this._quitRequested)) { this.fireOnWillShutdown(1 /* ShutdownReason.QUIT */); } }); } async reload(window, cli) { // Only reload when the window has not vetoed this const veto = await this.unload(window, 3 /* UnloadReason.RELOAD */); if (!veto) { window.reload(cli); } } unload(window, reason) { // Ensure there is only 1 unload running at the same time const pendingUnloadPromise = this.mapWindowIdToPendingUnload.get(window.id); if (pendingUnloadPromise) { return pendingUnloadPromise; } // Start unload and remember in map until finished const unloadPromise = this.doUnload(window, reason).finally(() => { this.mapWindowIdToPendingUnload.delete(window.id); }); this.mapWindowIdToPendingUnload.set(window.id, unloadPromise); return unloadPromise; } async doUnload(window, reason) { // Always allow to unload a window that is not yet ready if (!window.isReady) { return false; } this.trace(`Lifecycle#unload() - window ID ${window.id}`); // first ask the window itself if it vetos the unload const windowUnloadReason = this._quitRequested ? 2 /* UnloadReason.QUIT */ : reason; const veto = await this.onBeforeUnloadWindowInRenderer(window, windowUnloadReason); if (veto) { this.trace(`Lifecycle#unload() - veto in renderer (window ID ${window.id})`); return this.handleWindowUnloadVeto(veto); } // finally if there are no vetos, unload the renderer await this.onWillUnloadWindowInRenderer(window, windowUnloadReason); return false; } handleWindowUnloadVeto(veto) { if (!veto) { return false; // no veto } // a veto resolves any pending quit with veto this.resolvePendingQuitPromise(true /* veto */); // a veto resets the pending quit request flag this._quitRequested = false; return true; // veto } resolvePendingQuitPromise(veto) { if (this.pendingQuitPromiseResolve) { this.pendingQuitPromiseResolve(veto); this.pendingQuitPromiseResolve = undefined; this.pendingQuitPromise = undefined; } } onBeforeUnloadWindowInRenderer(window, reason) { return new Promise((resolve) => { const oneTimeEventToken = this.oneTimeListenerTokenGenerator++; const okChannel = `vscode:ok${oneTimeEventToken}`; const cancelChannel = `vscode:cancel${oneTimeEventToken}`; validatedIpcMain.once(okChannel, () => { resolve(false); // no veto }); validatedIpcMain.once(cancelChannel, () => { resolve(true); // veto }); window.send('vscode:onBeforeUnload', { okChannel, cancelChannel, reason }); }); } onWillUnloadWindowInRenderer(window, reason) { return new Promise((resolve) => { const oneTimeEventToken = this.oneTimeListenerTokenGenerator++; const replyChannel = `vscode:reply${oneTimeEventToken}`; validatedIpcMain.once(replyChannel, () => resolve()); window.send('vscode:onWillUnload', { replyChannel, reason }); }); } quit(willRestart) { this.trace(`Lifecycle#quit() - begin (willRestart: ${willRestart})`); if (this.pendingQuitPromise) { this.trace('Lifecycle#quit() - returning pending quit promise'); return this.pendingQuitPromise; } // Remember if we are about to restart if (willRestart) { this.stateMainService.setItem(LifecycleMainService.QUIT_AND_RESTART_KEY, true); } this.pendingQuitPromise = new Promise((resolve) => { // Store as field to access it from a window cancellation this.pendingQuitPromiseResolve = resolve; // Calling app.quit() will trigger the close handlers of each opened window // and only if no window vetoed the shutdown, we will get the will-quit event this.trace('Lifecycle#quit() - calling app.quit()'); app.quit(); }); return this.pendingQuitPromise; } trace(msg) { if (this.environmentMainService.args['enable-smoke-test-driver']) { this.logService.info(msg); // helps diagnose issues with exiting from smoke tests } else { this.logService.trace(msg); } } async relaunch(options) { this.trace('Lifecycle#relaunch()'); const args = process.argv.slice(1); if (options?.addArgs) { args.push(...options.addArgs); } if (options?.removeArgs) { for (const a of options.removeArgs) { const idx = args.indexOf(a); if (idx >= 0) { args.splice(idx, 1); } } } const quitListener = () => { // Windows: we are about to restart and as such we need to restore the original // current working directory we had on startup to get the exact same startup // behaviour. As such, we briefly change back to that directory and then when // Code starts it will set it back to the installation directory again. try { if (isWindows) { const currentWorkingDir = cwd(); if (currentWorkingDir !== process.cwd()) { process.chdir(currentWorkingDir); } } } catch (err) { this.logService.error(err); } // relaunch after we are sure there is no veto this.trace('Lifecycle#relaunch() - calling app.relaunch()'); app.relaunch({ args }); }; app.once('quit', quitListener); // `app.relaunch()` does not quit automatically, so we quit first, // check for vetoes and then relaunch from the `app.on('quit')` event const veto = await this.quit(true /* will restart */); if (veto) { app.removeListener('quit', quitListener); } } async kill(code) { this.trace('Lifecycle#kill()'); // Give main process participants a chance to orderly shutdown await this.fireOnWillShutdown(2 /* ShutdownReason.KILL */); // From extension tests we have seen issues where calling app.exit() // with an opened window can lead to native crashes (Linux). As such, // we should make sure to destroy any opened window before calling // `app.exit()`. // // Note: Electron implements a similar logic here: // https://github.com/electron/electron/blob/fe5318d753637c3903e23fc1ed1b263025887b6a/spec-main/window-helpers.ts#L5 await Promise.race([ // Still do not block more than 1s timeout(1000), // Destroy any opened window: we do not unload windows here because // there is a chance that the unload is veto'd or long running due // to a participant within the window. this is not wanted when we // are asked to kill the application. (async () => { for (const window of BrowserWindow.getAllWindows()) { if (window && !window.isDestroyed()) { let whenWindowClosed; if (window.webContents && !window.webContents.isDestroyed()) { whenWindowClosed = new Promise((resolve) => window.once('closed', resolve)); } else { whenWindowClosed = Promise.resolve(); } window.destroy(); await whenWindowClosed; } } })(), ]); // Now exit either after 1s or all windows destroyed app.exit(code); } }; LifecycleMainService = __decorate( [__param(0, ILogService), __param(1, IStateMainService), __param(2, IEnvironmentMainService)], LifecycleMainService, ); export { LifecycleMainService };