diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 564721dc09b..a8a86fd763c 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -145,8 +145,7 @@ class BrowserMain extends Disposable { // Workbench Lifecycle this._register(workbench.onBeforeShutdown(event => { if (storageService.hasPendingUpdate) { - logService.warn('Unload veto: pending storage update'); - event.veto(true); // prevent data loss from pending storage update + event.veto(true, 'veto.pendingStorageUpdate'); // prevent data loss from pending storage update } })); this._register(workbench.onWillShutdown(() => storageService.close())); diff --git a/src/vs/workbench/contrib/backup/common/backupTracker.ts b/src/vs/workbench/contrib/backup/common/backupTracker.ts index d8f35fc9dae..463d69e2355 100644 --- a/src/vs/workbench/contrib/backup/common/backupTracker.ts +++ b/src/vs/workbench/contrib/backup/common/backupTracker.ts @@ -43,7 +43,7 @@ export abstract class BackupTracker extends Disposable { this._register(this.workingCopyService.onDidChangeContent(workingCopy => this.onDidChangeContent(workingCopy))); // Lifecycle (handled in subclasses) - this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason))); + this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason), 'veto.backups')); } private onDidRegister(workingCopy: IWorkingCopy): void { diff --git a/src/vs/workbench/contrib/tasks/electron-browser/taskService.ts b/src/vs/workbench/contrib/tasks/electron-browser/taskService.ts index 64fe0389f65..3df8b849992 100644 --- a/src/vs/workbench/contrib/tasks/electron-browser/taskService.ts +++ b/src/vs/workbench/contrib/tasks/electron-browser/taskService.ts @@ -116,7 +116,7 @@ export class TaskService extends AbstractTaskService { preferencesService, viewDescriptorService, logService); - this._register(lifecycleService.onBeforeShutdown(event => event.veto(this.beforeShutdown()))); + this._register(lifecycleService.onBeforeShutdown(event => event.veto(this.beforeShutdown(), 'veto.tasks'))); } protected getTaskSystem(): ITaskSystem { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index f82474d5f55..83b4cf26c03 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -123,7 +123,7 @@ export class TerminalService implements ITerminalService { this._activeTabIndex = 0; this._isShuttingDown = false; this._findState = new FindReplaceState(); - lifecycleService.onBeforeShutdown(async event => event.veto(this._onBeforeShutdown())); + lifecycleService.onBeforeShutdown(async event => event.veto(this._onBeforeShutdown(), 'veto.terminal')); lifecycleService.onShutdown(() => this._onShutdown()); this._terminalFocusContextKey = KEYBINDING_CONTEXT_TERMINAL_FOCUS.bindTo(this._contextKeyService); this._terminalShellTypeContextKey = KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE.bindTo(this._contextKeyService); diff --git a/src/vs/workbench/electron-browser/desktop.main.ts b/src/vs/workbench/electron-browser/desktop.main.ts index dd3d59cb4ea..c20dc166cde 100644 --- a/src/vs/workbench/electron-browser/desktop.main.ts +++ b/src/vs/workbench/electron-browser/desktop.main.ts @@ -145,7 +145,7 @@ class DesktopMain extends Disposable { // Workbench Lifecycle this._register(workbench.onShutdown(() => this.dispose())); - this._register(workbench.onWillShutdown(event => event.join(storageService.close()))); + this._register(workbench.onWillShutdown(event => event.join(storageService.close(), 'join.closeStorage'))); } private onWindowResize(e: Event, retry: boolean, workbench: Workbench): void { diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index dc8f60572cc..0ed5c1ff869 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -117,7 +117,7 @@ class DesktopMain extends Disposable { // Workbench Lifecycle this._register(workbench.onShutdown(() => this.dispose())); - this._register(workbench.onWillShutdown(event => event.join(storageService.close()))); + this._register(workbench.onWillShutdown(event => event.join(storageService.close(), 'join.closeStorage'))); } private onWindowResize(e: Event, retry: boolean, workbench: Workbench): void { diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index 34fa2c3b059..101ece12361 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -651,7 +651,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { // to communicate this back to the main side to terminate the debug session if (this._isExtensionDevHost && !this._isExtensionDevTestFromCli && !this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) { this._extensionHostDebugService.terminateSession(this._environmentService.debugExtensionHost.debugId); - event.join(timeout(100 /* wait a bit for IPC to get delivered */)); + event.join(timeout(100 /* wait a bit for IPC to get delivered */), 'join.extensionDevelopment'); } } } diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index 61c461c75bf..88bd1f4f797 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -126,8 +126,7 @@ export class BrowserHostService extends Disposable implements IHostService { case HostShutdownReason.Keyboard: const confirmBeforeClose = this.configurationService.getValue<'always' | 'keyboardOnly' | 'never'>('window.confirmBeforeClose'); if (confirmBeforeClose === 'always' || (confirmBeforeClose === 'keyboardOnly' && this.shutdownReason === HostShutdownReason.Keyboard)) { - this.logService.warn(`Unload veto: window.confirmBeforeClose=${confirmBeforeClose}`); - e.veto(true); + e.veto(true, 'veto.confirmBeforeClose'); } break; diff --git a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts index 77c97f2fd5d..fe323702d86 100644 --- a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts @@ -77,12 +77,17 @@ export class BrowserLifecycleService extends AbstractLifecycleService { // Before Shutdown this._onBeforeShutdown.fire({ - veto(value) { + veto(value, id) { if (typeof handleVeto === 'function') { + if (value instanceof Promise) { + logService.error(`[lifecycle] Long running operations before shutdown are unsupported in the web (id: ${id})`); + + value = true; // implicitly vetos since we cannot handle promises in web + } + if (value === true) { - veto = true; - } else if (value instanceof Promise && !veto) { - logService.error('[lifecycle] Long running onBeforeShutdown currently not supported in the web'); + logService.info(`[lifecycle]: Unload was prevented (id: ${id})`); + veto = true; } } @@ -99,8 +104,8 @@ export class BrowserLifecycleService extends AbstractLifecycleService { // No Veto: continue with Will Shutdown this._onWillShutdown.fire({ - join() { - logService.error('[lifecycle] Long running onWillShutdown currently not supported in the web'); + join(promise, id) { + logService.error(`[lifecycle] Long running operations during shutdown are unsupported in the web (id: ${id})`); }, reason: ShutdownReason.QUIT }); diff --git a/src/vs/workbench/services/lifecycle/common/lifecycle.ts b/src/vs/workbench/services/lifecycle/common/lifecycle.ts index 876edba7db3..355cd49265c 100644 --- a/src/vs/workbench/services/lifecycle/common/lifecycle.ts +++ b/src/vs/workbench/services/lifecycle/common/lifecycle.ts @@ -22,8 +22,11 @@ export interface BeforeShutdownEvent { /** * Allows to veto the shutdown. The veto can be a long running operation but it * will block the application from closing. + * + * @param id to identify the veto operation in case it takes very long or never + * completes. */ - veto(value: boolean | Promise): void; + veto(value: boolean | Promise, id: string): void; /** * The reason why the application will be shutting down. @@ -44,8 +47,11 @@ export interface WillShutdownEvent { /** * Allows to join the shutdown. The promise can be a long running operation but it * will block the application from closing. + * + * @param id to identify the join operation in case it takes very long or never + * completes. */ - join(promise: Promise): void; + join(promise: Promise, id: string): void; /** * The reason why the application is shutting down. diff --git a/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts b/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts index e9fb6c211d0..61716c228f4 100644 --- a/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts @@ -16,11 +16,15 @@ import { AbstractLifecycleService } from 'vs/workbench/services/lifecycle/common import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import Severity from 'vs/base/common/severity'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; +import { disposableTimeout } from 'vs/base/common/async'; export class NativeLifecycleService extends AbstractLifecycleService { private static readonly LAST_SHUTDOWN_REASON_KEY = 'lifecyle.lastShutdownReason'; + private static readonly BEFORE_SHUTDOWN_WARNING_DELAY = 5000; + private static readonly WILL_SHUTDOWN_WARNING_DELAY = 5000; + declare readonly _serviceBrand: undefined; private shutdownReason: ShutdownReason | undefined; @@ -51,7 +55,7 @@ export class NativeLifecycleService extends AbstractLifecycleService { startupKind = StartupKind.NewWindow; } - this.logService.trace(`lifecycle: starting up (startup kind: ${this._startupKind})`); + this.logService.trace(`[lifecycle] starting up (startup kind: ${this._startupKind})`); return startupKind; } @@ -61,16 +65,16 @@ export class NativeLifecycleService extends AbstractLifecycleService { // Main side indicates that window is about to unload, check for vetos ipcRenderer.on('vscode:onBeforeUnload', (event: unknown, reply: { okChannel: string, cancelChannel: string, reason: ShutdownReason }) => { - this.logService.trace(`lifecycle: onBeforeUnload (reason: ${reply.reason})`); + this.logService.trace(`[lifecycle] onBeforeUnload (reason: ${reply.reason})`); // trigger onBeforeShutdown events and veto collecting this.handleBeforeShutdown(reply.reason).then(veto => { if (veto) { - this.logService.trace('lifecycle: onBeforeUnload prevented via veto'); + this.logService.trace('[lifecycle] onBeforeUnload prevented via veto'); ipcRenderer.send(reply.cancelChannel, windowId); } else { - this.logService.trace('lifecycle: onBeforeUnload continues without veto'); + this.logService.trace('[lifecycle] onBeforeUnload continues without veto'); this.shutdownReason = reply.reason; ipcRenderer.send(reply.okChannel, windowId); @@ -80,7 +84,7 @@ export class NativeLifecycleService extends AbstractLifecycleService { // Main side indicates that we will indeed shutdown ipcRenderer.on('vscode:onWillUnload', async (event: unknown, reply: { replyChannel: string, reason: ShutdownReason }) => { - this.logService.trace(`lifecycle: onWillUnload (reason: ${reply.reason})`); + this.logService.trace(`[lifecycle] onWillUnload (reason: ${reply.reason})`); // trigger onWillShutdown events and joining await this.handleWillShutdown(reply.reason); @@ -100,35 +104,71 @@ export class NativeLifecycleService extends AbstractLifecycleService { }); } - private handleBeforeShutdown(reason: ShutdownReason): Promise { + private async handleBeforeShutdown(reason: ShutdownReason): Promise { + const logService = this.logService; const vetos: (boolean | Promise)[] = []; + const pendingVetos = new Set(); this._onBeforeShutdown.fire({ - veto(value) { + veto(value, id) { vetos.push(value); - }, - reason - }); - return handleVetos(vetos, error => this.onShutdownError(reason, error)); - } + // Log any veto instantly + if (value === true) { + logService.info(`[lifecycle]: Shutdown was prevented (id: ${id})`); + } - private async handleWillShutdown(reason: ShutdownReason): Promise { - const joiners: Promise[] = []; - - this._onWillShutdown.fire({ - join(promise) { - if (promise) { - joiners.push(promise); + // Track promise completion + else if (value instanceof Promise) { + pendingVetos.add(id); + value.then(veto => { + if (veto === true) { + logService.info(`[lifecycle]: Shutdown was prevented (id: ${id})`); + } + }).finally(() => pendingVetos.delete(id)); } }, reason }); + const longRunningBeforeShutdownWarning = disposableTimeout(() => { + logService.warn(`[lifecycle] onBeforeShutdown is taking a long time, pending operations: ${Array.from(pendingVetos).join(', ')}`); + }, NativeLifecycleService.BEFORE_SHUTDOWN_WARNING_DELAY); + + try { + return await handleVetos(vetos, error => this.onShutdownError(reason, error)); + } finally { + longRunningBeforeShutdownWarning.dispose(); + } + } + + private async handleWillShutdown(reason: ShutdownReason): Promise { + const joiners: Promise[] = []; + const pendingJoiners = new Set(); + + this._onWillShutdown.fire({ + join(promise, id) { + if (promise) { + joiners.push(promise); + + // Track promise completion + pendingJoiners.add(id); + promise.finally(() => pendingJoiners.delete(id)); + } + }, + reason + }); + + const longRunningWillShutdownWarning = disposableTimeout(() => { + this.logService.warn(`[lifecycle] onWillShutdown is taking a long time, pending operations: ${Array.from(pendingJoiners).join(', ')}`); + }, NativeLifecycleService.WILL_SHUTDOWN_WARNING_DELAY); + try { await Promise.all(joiners); } catch (error) { this.onShutdownError(reason, error); + } finally { + longRunningWillShutdownWarning.dispose(); } } diff --git a/src/vs/workbench/services/textfile/browser/browserTextFileService.ts b/src/vs/workbench/services/textfile/browser/browserTextFileService.ts index 3e67665f82c..941b946dcb2 100644 --- a/src/vs/workbench/services/textfile/browser/browserTextFileService.ts +++ b/src/vs/workbench/services/textfile/browser/browserTextFileService.ts @@ -14,13 +14,11 @@ export class BrowserTextFileService extends AbstractTextFileService { super.registerListeners(); // Lifecycle - this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason))); + this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason), 'veto.textFiles')); } protected onBeforeShutdown(reason: ShutdownReason): boolean { if (this.files.models.some(model => model.hasState(TextFileEditorModelState.PENDING_SAVE))) { - this.logService.warn('Unload veto: pending file saves'); - return true; // files are pending to be saved: veto } diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index 41b78ea3741..8daa50d6e3e 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -271,7 +271,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat const manualSyncTask = await this.userDataSyncService.createManualSyncTask(); const disposable = isWeb ? Disposable.None /* In web long running shutdown handlers will not work */ - : this.lifecycleService.onBeforeShutdown(e => e.veto(this.onBeforeShutdown(manualSyncTask))); + : this.lifecycleService.onBeforeShutdown(e => e.veto(this.onBeforeShutdown(manualSyncTask), 'veto.settingsSync')); try { await this.syncBeforeTurningOn(title, manualSyncTask); diff --git a/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts b/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts index c48fc29d852..77847a0bd67 100644 --- a/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; import { URI } from 'vs/base/common/uri'; -import * as nls from 'vs/nls'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; import { IWorkspacesService, isUntitledWorkspace, IWorkspaceIdentifier, hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; @@ -66,7 +66,7 @@ export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingServi this.lifecycleService.onBeforeShutdown(e => { const saveOperation = this.saveUntitledBeforeShutdown(e.reason); if (saveOperation) { - e.veto(saveOperation); + e.veto(saveOperation, 'veto.untitledWorkspace'); } }); } @@ -92,16 +92,14 @@ export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingServi CANCEL } - const buttons: { label: string; result: ConfirmResult; }[] = [ - { label: mnemonicButtonLabel(nls.localize('save', "Save")), result: ConfirmResult.SAVE }, - { label: mnemonicButtonLabel(nls.localize('doNotSave', "Don't Save")), result: ConfirmResult.DONT_SAVE }, - { label: nls.localize('cancel', "Cancel"), result: ConfirmResult.CANCEL } + const buttons = [ + { label: mnemonicButtonLabel(localize('save', "Save")), result: ConfirmResult.SAVE }, + { label: mnemonicButtonLabel(localize('doNotSave', "Don't Save")), result: ConfirmResult.DONT_SAVE }, + { label: localize('cancel', "Cancel"), result: ConfirmResult.CANCEL } ]; - const message = nls.localize('saveWorkspaceMessage', "Do you want to save your workspace configuration as a file?"); - const detail = nls.localize('saveWorkspaceDetail', "Save your workspace if you plan to open it again."); - const cancelId = 2; - - const { choice } = await this.dialogService.show(Severity.Warning, message, buttons.map(button => button.label), { detail, cancelId }); + const message = localize('saveWorkspaceMessage', "Do you want to save your workspace configuration as a file?"); + const detail = localize('saveWorkspaceDetail', "Save your workspace if you plan to open it again."); + const { choice } = await this.dialogService.show(Severity.Warning, message, buttons.map(button => button.label), { detail, cancelId: 2 }); switch (buttons[choice].result) { @@ -111,7 +109,7 @@ export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingServi // Don't Save: delete workspace case ConfirmResult.DONT_SAVE: - this.workspacesService.deleteUntitledWorkspace(workspaceIdentifier); + await this.workspacesService.deleteUntitledWorkspace(workspaceIdentifier); return false; // Save: save workspace, but do not veto unload if path provided @@ -126,13 +124,13 @@ export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingServi // Make sure to add the new workspace to the history to find it again const newWorkspaceIdentifier = await this.workspacesService.getWorkspaceIdentifier(newWorkspacePath); - this.workspacesService.addRecentlyOpened([{ + await this.workspacesService.addRecentlyOpened([{ label: this.labelService.getWorkspaceLabel(newWorkspaceIdentifier, { verbose: true }), workspace: newWorkspaceIdentifier }]); // Delete the untitled one - this.workspacesService.deleteUntitledWorkspace(workspaceIdentifier); + await this.workspacesService.deleteUntitledWorkspace(workspaceIdentifier); } catch (error) { // ignore } @@ -149,10 +147,10 @@ export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingServi if (windows.some(window => !!window.workspace && this.uriIdentityService.extUri.isEqual(window.workspace.configPath, path))) { await this.dialogService.show( Severity.Info, - nls.localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(path)), - [nls.localize('ok', "OK")], + localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(path)), + [localize('ok', "OK")], { - detail: nls.localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again.") + detail: localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again.") } );