Path: blob/main/src/vs/workbench/services/lifecycle/electron-browser/lifecycleService.ts
5256 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, ...args: unknown[]) => {37const reply = args[0] as { okChannel: string; cancelChannel: string; reason: ShutdownReason };38this.logService.trace(`[lifecycle] onBeforeUnload (reason: ${reply.reason})`);3940// trigger onBeforeShutdown events and veto collecting41const veto = await this.handleBeforeShutdown(reply.reason);4243// veto: cancel unload44if (veto) {45this.logService.trace('[lifecycle] onBeforeUnload prevented via veto');4647// Indicate as event48this._onShutdownVeto.fire();4950ipcRenderer.send(reply.cancelChannel, windowId);51}5253// no veto: allow unload54else {55this.logService.trace('[lifecycle] onBeforeUnload continues without veto');5657this.shutdownReason = reply.reason;58ipcRenderer.send(reply.okChannel, windowId);59}60});6162// Main side indicates that we will indeed shutdown63ipcRenderer.on('vscode:onWillUnload', async (event: unknown, ...args: unknown[]) => {64const reply = args[0] as { replyChannel: string; reason: ShutdownReason };65this.logService.trace(`[lifecycle] onWillUnload (reason: ${reply.reason})`);6667// trigger onWillShutdown events and joining68await this.handleWillShutdown(reply.reason);6970// trigger onDidShutdown event now that we know we will quit71this._onDidShutdown.fire();7273// acknowledge to main side74ipcRenderer.send(reply.replyChannel, windowId);75});76}7778protected async handleBeforeShutdown(reason: ShutdownReason): Promise<boolean> {79const logService = this.logService;8081const vetos: (boolean | Promise<boolean>)[] = [];82const pendingVetos = new Set<string>();8384let finalVeto: (() => boolean | Promise<boolean>) | undefined = undefined;85let finalVetoId: string | undefined = undefined;8687// before-shutdown event with veto support88this._onBeforeShutdown.fire({89reason,90veto(value, id) {91vetos.push(value);9293// Log any veto instantly94if (value === true) {95logService.info(`[lifecycle]: Shutdown was prevented (id: ${id})`);96}9798// Track promise completion99else if (value instanceof Promise) {100pendingVetos.add(id);101value.then(veto => {102if (veto === true) {103logService.info(`[lifecycle]: Shutdown was prevented (id: ${id})`);104}105}).finally(() => pendingVetos.delete(id));106}107},108finalVeto(value, id) {109if (!finalVeto) {110finalVeto = value;111finalVetoId = id;112} else {113throw new Error(`[lifecycle]: Final veto is already defined (id: ${id})`);114}115}116});117118const longRunningBeforeShutdownWarning = disposableTimeout(() => {119logService.warn(`[lifecycle] onBeforeShutdown is taking a long time, pending operations: ${Array.from(pendingVetos).join(', ')}`);120}, NativeLifecycleService.BEFORE_SHUTDOWN_WARNING_DELAY);121122try {123124// First: run list of vetos in parallel125let veto = await handleVetos(vetos, error => this.handleBeforeShutdownError(error, reason));126if (veto) {127return veto;128}129130// Second: run the final veto if defined131if (finalVeto) {132try {133pendingVetos.add(finalVetoId as unknown as string);134veto = await (finalVeto as () => Promise<boolean>)();135if (veto) {136logService.info(`[lifecycle]: Shutdown was prevented by final veto (id: ${finalVetoId})`);137}138} catch (error) {139veto = true; // treat error as veto140141this.handleBeforeShutdownError(error, reason);142}143}144145return veto;146} finally {147longRunningBeforeShutdownWarning.dispose();148}149}150151private handleBeforeShutdownError(error: Error, reason: ShutdownReason): void {152this.logService.error(`[lifecycle]: Error during before-shutdown phase (error: ${toErrorMessage(error)})`);153154this._onBeforeShutdownError.fire({ reason, error });155}156157protected async handleWillShutdown(reason: ShutdownReason): Promise<void> {158this._willShutdown = true;159160const joiners: Promise<void>[] = [];161const lastJoiners: (() => Promise<void>)[] = [];162const pendingJoiners = new Set<IWillShutdownEventJoiner>();163const cts = new CancellationTokenSource();164this._onWillShutdown.fire({165reason,166token: cts.token,167joiners: () => Array.from(pendingJoiners.values()),168join(promiseOrPromiseFn, joiner) {169pendingJoiners.add(joiner);170171if (joiner.order === WillShutdownJoinerOrder.Last) {172const promiseFn = typeof promiseOrPromiseFn === 'function' ? promiseOrPromiseFn : () => promiseOrPromiseFn;173lastJoiners.push(() => promiseFn().finally(() => pendingJoiners.delete(joiner)));174} else {175const promise = typeof promiseOrPromiseFn === 'function' ? promiseOrPromiseFn() : promiseOrPromiseFn;176promise.finally(() => pendingJoiners.delete(joiner));177joiners.push(promise);178}179},180force: () => {181cts.dispose(true);182}183});184185const longRunningWillShutdownWarning = disposableTimeout(() => {186this.logService.warn(`[lifecycle] onWillShutdown is taking a long time, pending operations: ${Array.from(pendingJoiners).map(joiner => joiner.id).join(', ')}`);187}, NativeLifecycleService.WILL_SHUTDOWN_WARNING_DELAY);188189try {190await raceCancellation(Promises.settled(joiners), cts.token);191} catch (error) {192this.logService.error(`[lifecycle]: Error during will-shutdown phase in default joiners (error: ${toErrorMessage(error)})`);193}194195try {196await raceCancellation(Promises.settled(lastJoiners.map(lastJoiner => lastJoiner())), cts.token);197} catch (error) {198this.logService.error(`[lifecycle]: Error during will-shutdown phase in last joiners (error: ${toErrorMessage(error)})`);199}200201longRunningWillShutdownWarning.dispose();202}203204shutdown(): Promise<void> {205return this.nativeHostService.closeWindow();206}207}208209registerSingleton(ILifecycleService, NativeLifecycleService, InstantiationType.Eager);210211212