Path: blob/main/src/vs/workbench/services/lifecycle/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 { ShutdownReason, ILifecycleService, StartupKind } from '../common/lifecycle.js';6import { ILogService } from '../../../../platform/log/common/log.js';7import { AbstractLifecycleService } from '../common/lifecycleService.js';8import { localize } from '../../../../nls.js';9import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';10import { IDisposable } from '../../../../base/common/lifecycle.js';11import { addDisposableListener, EventType } from '../../../../base/browser/dom.js';12import { IStorageService, WillSaveStateReason } from '../../../../platform/storage/common/storage.js';13import { CancellationToken } from '../../../../base/common/cancellation.js';14import { mainWindow } from '../../../../base/browser/window.js';1516export class BrowserLifecycleService extends AbstractLifecycleService {1718private beforeUnloadListener: IDisposable | undefined = undefined;19private unloadListener: IDisposable | undefined = undefined;2021private ignoreBeforeUnload = false;2223private didUnload = false;2425constructor(26@ILogService logService: ILogService,27@IStorageService storageService: IStorageService28) {29super(logService, storageService);3031this.registerListeners();32}3334private registerListeners(): void {3536// Listen to `beforeUnload` to support to veto37this.beforeUnloadListener = addDisposableListener(mainWindow, EventType.BEFORE_UNLOAD, (e: BeforeUnloadEvent) => this.onBeforeUnload(e));3839// Listen to `pagehide` to support orderly shutdown40// We explicitly do not listen to `unload` event41// which would disable certain browser caching.42// We currently do not handle the `persisted` property43// (https://github.com/microsoft/vscode/issues/136216)44this.unloadListener = addDisposableListener(mainWindow, EventType.PAGE_HIDE, () => this.onUnload());45}4647private onBeforeUnload(event: BeforeUnloadEvent): void {4849// Before unload ignored (once)50if (this.ignoreBeforeUnload) {51this.logService.info('[lifecycle] onBeforeUnload triggered but ignored once');5253this.ignoreBeforeUnload = false;54}5556// Before unload with veto support57else {58this.logService.info('[lifecycle] onBeforeUnload triggered and handled with veto support');5960this.doShutdown(() => this.vetoBeforeUnload(event));61}62}6364private vetoBeforeUnload(event: BeforeUnloadEvent): void {65event.preventDefault();66event.returnValue = localize('lifecycleVeto', "Changes that you made may not be saved. Please check press 'Cancel' and try again.");67}6869withExpectedShutdown(reason: ShutdownReason): Promise<void>;70withExpectedShutdown(reason: { disableShutdownHandling: true }, callback: Function): void;71withExpectedShutdown(reason: ShutdownReason | { disableShutdownHandling: true }, callback?: Function): Promise<void> | void {7273// Standard shutdown74if (typeof reason === 'number') {75this.shutdownReason = reason;7677// Ensure UI state is persisted78return this.storageService.flush(WillSaveStateReason.SHUTDOWN);79}8081// Before unload handling ignored for duration of callback82else {83this.ignoreBeforeUnload = true;84try {85callback?.();86} finally {87this.ignoreBeforeUnload = false;88}89}90}9192async shutdown(): Promise<void> {93this.logService.info('[lifecycle] shutdown triggered');9495// An explicit shutdown renders our unload96// event handlers disabled, so dispose them.97this.beforeUnloadListener?.dispose();98this.unloadListener?.dispose();99100// Ensure UI state is persisted101await this.storageService.flush(WillSaveStateReason.SHUTDOWN);102103// Handle shutdown without veto support104this.doShutdown();105}106107private doShutdown(vetoShutdown?: () => void): void {108const logService = this.logService;109110// Optimistically trigger a UI state flush111// without waiting for it. The browser does112// not guarantee that this is being executed113// but if a dialog opens, we have a chance114// to succeed.115this.storageService.flush(WillSaveStateReason.SHUTDOWN);116117let veto = false;118119function handleVeto(vetoResult: boolean | Promise<boolean>, id: string) {120if (typeof vetoShutdown !== 'function') {121return; // veto handling disabled122}123124if (vetoResult instanceof Promise) {125logService.error(`[lifecycle] Long running operations before shutdown are unsupported in the web (id: ${id})`);126127veto = true; // implicitly vetos since we cannot handle promises in web128}129130if (vetoResult === true) {131logService.info(`[lifecycle]: Unload was prevented (id: ${id})`);132133veto = true;134}135}136137// Before Shutdown138this._onBeforeShutdown.fire({139reason: ShutdownReason.QUIT,140veto(value, id) {141handleVeto(value, id);142},143finalVeto(valueFn, id) {144handleVeto(valueFn(), id); // in browser, trigger instantly because we do not support async anyway145}146});147148// Veto: handle if provided149if (veto && typeof vetoShutdown === 'function') {150return vetoShutdown();151}152153// No veto, continue to shutdown154return this.onUnload();155}156157private onUnload(): void {158if (this.didUnload) {159return; // only once160}161162this.didUnload = true;163this._willShutdown = true;164165// Register a late `pageshow` listener specifically on unload166this._register(addDisposableListener(mainWindow, EventType.PAGE_SHOW, (e: PageTransitionEvent) => this.onLoadAfterUnload(e)));167168// First indicate will-shutdown169const logService = this.logService;170this._onWillShutdown.fire({171reason: ShutdownReason.QUIT,172joiners: () => [], // Unsupported in web173token: CancellationToken.None, // Unsupported in web174join(promise, joiner) {175if (typeof promise === 'function') {176promise();177}178logService.error(`[lifecycle] Long running operations during shutdown are unsupported in the web (id: ${joiner.id})`);179},180force: () => { /* No-Op in web */ },181});182183// Finally end with did-shutdown184this._onDidShutdown.fire();185}186187private onLoadAfterUnload(event: PageTransitionEvent): void {188189// We only really care about page-show events190// where the browser indicates to us that the191// page was restored from cache and not freshly192// loaded.193const wasRestoredFromCache = event.persisted;194if (!wasRestoredFromCache) {195return;196}197198// At this point, we know that the page was restored from199// cache even though it was unloaded before,200// so in order to get back to a functional workbench, we201// currently can only reload the window202// Docs: https://web.dev/bfcache/#optimize-your-pages-for-bfcache203// Refs: https://github.com/microsoft/vscode/issues/136035204this.withExpectedShutdown({ disableShutdownHandling: true }, () => mainWindow.location.reload());205}206207protected override doResolveStartupKind(): StartupKind | undefined {208let startupKind = super.doResolveStartupKind();209if (typeof startupKind !== 'number') {210const timing = performance.getEntriesByType('navigation').at(0) as PerformanceNavigationTiming | undefined;211if (timing?.type === 'reload') {212// MDN: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming/type#value213startupKind = StartupKind.ReloadedWindow;214}215}216217return startupKind;218}219}220221registerSingleton(ILifecycleService, BrowserLifecycleService, InstantiationType.Eager);222223224