Path: blob/main/src/vs/workbench/services/lifecycle/electron-browser/lifecycleService.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { handleVetos } from '../../../../platform/lifecycle/common/lifecycle.js';6import { ShutdownReason, ILifecycleService, IWillShutdownEventJoiner, WillShutdownJoinerOrder } from '../common/lifecycle.js';7import { IStorageService } from '../../../../platform/storage/common/storage.js';8import { ipcRenderer } from '../../../../base/parts/sandbox/electron-browser/globals.js';9import { ILogService } from '../../../../platform/log/common/log.js';10import { AbstractLifecycleService } from '../common/lifecycleService.js';11import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';12import { INativeHostService } from '../../../../platform/native/common/native.js';13import { Promises, disposableTimeout, raceCancellation } from '../../../../base/common/async.js';14import { toErrorMessage } from '../../../../base/common/errorMessage.js';15import { CancellationTokenSource } from '../../../../base/common/cancellation.js';1617export class NativeLifecycleService extends AbstractLifecycleService {1819private static readonly BEFORE_SHUTDOWN_WARNING_DELAY = 5000;20private static readonly WILL_SHUTDOWN_WARNING_DELAY = 800;2122constructor(23@INativeHostService private readonly nativeHostService: INativeHostService,24@IStorageService storageService: IStorageService,25@ILogService logService: ILogService26) {27super(logService, storageService);2829this.registerListeners();30}3132private registerListeners(): void {33const windowId = this.nativeHostService.windowId;3435// Main side indicates that window is about to unload, check for vetos36ipcRenderer.on('vscode:onBeforeUnload', async (event: unknown, reply: { okChannel: string; cancelChannel: string; reason: ShutdownReason }) => {37this.logService.trace(`[lifecycle] onBeforeUnload (reason: ${reply.reason})`);3839// trigger onBeforeShutdown events and veto collecting40const veto = await this.handleBeforeShutdown(reply.reason);4142// veto: cancel unload43if (veto) {44this.logService.trace('[lifecycle] onBeforeUnload prevented via veto');4546// Indicate as event47this._onShutdownVeto.fire();4849ipcRenderer.send(reply.cancelChannel, windowId);50}5152// no veto: allow unload53else {54this.logService.trace('[lifecycle] onBeforeUnload continues without veto');5556this.shutdownReason = reply.reason;57ipcRenderer.send(reply.okChannel, windowId);58}59});6061// Main side indicates that we will indeed shutdown62ipcRenderer.on('vscode:onWillUnload', async (event: unknown, reply: { replyChannel: string; reason: ShutdownReason }) => {63this.logService.trace(`[lifecycle] onWillUnload (reason: ${reply.reason})`);6465// trigger onWillShutdown events and joining66await this.handleWillShutdown(reply.reason);6768// trigger onDidShutdown event now that we know we will quit69this._onDidShutdown.fire();7071// acknowledge to main side72ipcRenderer.send(reply.replyChannel, windowId);73});74}7576protected async handleBeforeShutdown(reason: ShutdownReason): Promise<boolean> {77const logService = this.logService;7879const vetos: (boolean | Promise<boolean>)[] = [];80const pendingVetos = new Set<string>();8182let finalVeto: (() => boolean | Promise<boolean>) | undefined = undefined;83let finalVetoId: string | undefined = undefined;8485// before-shutdown event with veto support86this._onBeforeShutdown.fire({87reason,88veto(value, id) {89vetos.push(value);9091// Log any veto instantly92if (value === true) {93logService.info(`[lifecycle]: Shutdown was prevented (id: ${id})`);94}9596// Track promise completion97else if (value instanceof Promise) {98pendingVetos.add(id);99value.then(veto => {100if (veto === true) {101logService.info(`[lifecycle]: Shutdown was prevented (id: ${id})`);102}103}).finally(() => pendingVetos.delete(id));104}105},106finalVeto(value, id) {107if (!finalVeto) {108finalVeto = value;109finalVetoId = id;110} else {111throw new Error(`[lifecycle]: Final veto is already defined (id: ${id})`);112}113}114});115116const longRunningBeforeShutdownWarning = disposableTimeout(() => {117logService.warn(`[lifecycle] onBeforeShutdown is taking a long time, pending operations: ${Array.from(pendingVetos).join(', ')}`);118}, NativeLifecycleService.BEFORE_SHUTDOWN_WARNING_DELAY);119120try {121122// First: run list of vetos in parallel123let veto = await handleVetos(vetos, error => this.handleBeforeShutdownError(error, reason));124if (veto) {125return veto;126}127128// Second: run the final veto if defined129if (finalVeto) {130try {131pendingVetos.add(finalVetoId as unknown as string);132veto = await (finalVeto as () => Promise<boolean>)();133if (veto) {134logService.info(`[lifecycle]: Shutdown was prevented by final veto (id: ${finalVetoId})`);135}136} catch (error) {137veto = true; // treat error as veto138139this.handleBeforeShutdownError(error, reason);140}141}142143return veto;144} finally {145longRunningBeforeShutdownWarning.dispose();146}147}148149private handleBeforeShutdownError(error: Error, reason: ShutdownReason): void {150this.logService.error(`[lifecycle]: Error during before-shutdown phase (error: ${toErrorMessage(error)})`);151152this._onBeforeShutdownError.fire({ reason, error });153}154155protected async handleWillShutdown(reason: ShutdownReason): Promise<void> {156this._willShutdown = true;157158const joiners: Promise<void>[] = [];159const lastJoiners: (() => Promise<void>)[] = [];160const pendingJoiners = new Set<IWillShutdownEventJoiner>();161const cts = new CancellationTokenSource();162this._onWillShutdown.fire({163reason,164token: cts.token,165joiners: () => Array.from(pendingJoiners.values()),166join(promiseOrPromiseFn, joiner) {167pendingJoiners.add(joiner);168169if (joiner.order === WillShutdownJoinerOrder.Last) {170const promiseFn = typeof promiseOrPromiseFn === 'function' ? promiseOrPromiseFn : () => promiseOrPromiseFn;171lastJoiners.push(() => promiseFn().finally(() => pendingJoiners.delete(joiner)));172} else {173const promise = typeof promiseOrPromiseFn === 'function' ? promiseOrPromiseFn() : promiseOrPromiseFn;174promise.finally(() => pendingJoiners.delete(joiner));175joiners.push(promise);176}177},178force: () => {179cts.dispose(true);180}181});182183const longRunningWillShutdownWarning = disposableTimeout(() => {184this.logService.warn(`[lifecycle] onWillShutdown is taking a long time, pending operations: ${Array.from(pendingJoiners).map(joiner => joiner.id).join(', ')}`);185}, NativeLifecycleService.WILL_SHUTDOWN_WARNING_DELAY);186187try {188await raceCancellation(Promises.settled(joiners), cts.token);189} catch (error) {190this.logService.error(`[lifecycle]: Error during will-shutdown phase in default joiners (error: ${toErrorMessage(error)})`);191}192193try {194await raceCancellation(Promises.settled(lastJoiners.map(lastJoiner => lastJoiner())), cts.token);195} catch (error) {196this.logService.error(`[lifecycle]: Error during will-shutdown phase in last joiners (error: ${toErrorMessage(error)})`);197}198199longRunningWillShutdownWarning.dispose();200}201202shutdown(): Promise<void> {203return this.nativeHostService.closeWindow();204}205}206207registerSingleton(ILifecycleService, NativeLifecycleService, InstantiationType.Eager);208209210