Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ShutdownReason, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { AbstractLifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycleService';
|
||||
import { localize } from 'vs/nls';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { addDisposableListener } from 'vs/base/browser/dom';
|
||||
|
||||
export class BrowserLifecycleService extends AbstractLifecycleService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private beforeUnloadDisposable: IDisposable | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
@ILogService readonly logService: ILogService
|
||||
) {
|
||||
super(logService);
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
|
||||
// beforeUnload
|
||||
this.beforeUnloadDisposable = addDisposableListener(window, 'beforeunload', (e: BeforeUnloadEvent) => this.onBeforeUnload(e));
|
||||
}
|
||||
|
||||
private onBeforeUnload(event: BeforeUnloadEvent): void {
|
||||
this.logService.info('[lifecycle] onBeforeUnload triggered');
|
||||
|
||||
this.doShutdown(() => {
|
||||
// Veto handling
|
||||
event.preventDefault();
|
||||
event.returnValue = localize('lifecycleVeto', "Changes that you made may not be saved. Please check press 'Cancel' and try again.");
|
||||
});
|
||||
}
|
||||
|
||||
shutdown(): void {
|
||||
this.logService.info('[lifecycle] shutdown triggered');
|
||||
|
||||
// Remove beforeunload listener that would prevent shutdown
|
||||
this.beforeUnloadDisposable?.dispose();
|
||||
|
||||
// Handle shutdown without veto support
|
||||
this.doShutdown();
|
||||
}
|
||||
|
||||
private doShutdown(handleVeto?: () => void): void {
|
||||
const logService = this.logService;
|
||||
|
||||
let veto = false;
|
||||
|
||||
// Before Shutdown
|
||||
this._onBeforeShutdown.fire({
|
||||
veto(value) {
|
||||
if (typeof handleVeto === 'function') {
|
||||
if (value === true) {
|
||||
veto = true;
|
||||
} else if (value instanceof Promise && !veto) {
|
||||
logService.error('[lifecycle] Long running onBeforeShutdown currently not supported in the web');
|
||||
veto = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
reason: ShutdownReason.QUIT
|
||||
});
|
||||
|
||||
// Veto: handle if provided
|
||||
if (veto && typeof handleVeto === 'function') {
|
||||
handleVeto();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// No Veto: continue with Will Shutdown
|
||||
this._onWillShutdown.fire({
|
||||
join() {
|
||||
logService.error('[lifecycle] Long running onWillShutdown currently not supported in the web');
|
||||
},
|
||||
reason: ShutdownReason.QUIT
|
||||
});
|
||||
|
||||
// Finally end with Shutdown event
|
||||
this._onShutdown.fire();
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(ILifecycleService, BrowserLifecycleService);
|
||||
@@ -0,0 +1,189 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export const ILifecycleService = createDecorator<ILifecycleService>('lifecycleService');
|
||||
|
||||
/**
|
||||
* An event that is send out when the window is about to close. Clients have a chance to veto
|
||||
* the closing by either calling veto with a boolean "true" directly or with a promise that
|
||||
* resolves to a boolean. Returning a promise is useful in cases of long running operations
|
||||
* on shutdown.
|
||||
*
|
||||
* Note: It is absolutely important to avoid long running promises if possible. Please try hard
|
||||
* to return a boolean directly. Returning a promise has quite an impact on the shutdown sequence!
|
||||
*/
|
||||
export interface BeforeShutdownEvent {
|
||||
|
||||
/**
|
||||
* Allows to veto the shutdown. The veto can be a long running operation but it
|
||||
* will block the application from closing.
|
||||
*/
|
||||
veto(value: boolean | Promise<boolean>): void;
|
||||
|
||||
/**
|
||||
* The reason why the application will be shutting down.
|
||||
*/
|
||||
readonly reason: ShutdownReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* An event that is send out when the window closes. Clients have a chance to join the closing
|
||||
* by providing a promise from the join method. Returning a promise is useful in cases of long
|
||||
* running operations on shutdown.
|
||||
*
|
||||
* Note: It is absolutely important to avoid long running promises if possible. Please try hard
|
||||
* to return a boolean directly. Returning a promise has quite an impact on the shutdown sequence!
|
||||
*/
|
||||
export interface WillShutdownEvent {
|
||||
|
||||
/**
|
||||
* Allows to join the shutdown. The promise can be a long running operation but it
|
||||
* will block the application from closing.
|
||||
*/
|
||||
join(promise: Promise<void>): void;
|
||||
|
||||
/**
|
||||
* The reason why the application is shutting down.
|
||||
*/
|
||||
readonly reason: ShutdownReason;
|
||||
}
|
||||
|
||||
export const enum ShutdownReason {
|
||||
|
||||
/** Window is closed */
|
||||
CLOSE = 1,
|
||||
|
||||
/** Application is quit */
|
||||
QUIT = 2,
|
||||
|
||||
/** Window is reloaded */
|
||||
RELOAD = 3,
|
||||
|
||||
/** Other configuration loaded into window */
|
||||
LOAD = 4
|
||||
}
|
||||
|
||||
export const enum StartupKind {
|
||||
NewWindow = 1,
|
||||
ReloadedWindow = 3,
|
||||
ReopenedWindow = 4,
|
||||
}
|
||||
|
||||
export function StartupKindToString(startupKind: StartupKind): string {
|
||||
switch (startupKind) {
|
||||
case StartupKind.NewWindow: return 'NewWindow';
|
||||
case StartupKind.ReloadedWindow: return 'ReloadedWindow';
|
||||
case StartupKind.ReopenedWindow: return 'ReopenedWindow';
|
||||
}
|
||||
}
|
||||
|
||||
export const enum LifecyclePhase {
|
||||
|
||||
/**
|
||||
* The first phase signals that we are about to startup getting ready.
|
||||
*/
|
||||
Starting = 1,
|
||||
|
||||
/**
|
||||
* Services are ready and the view is about to restore its state.
|
||||
*/
|
||||
Ready = 2,
|
||||
|
||||
/**
|
||||
* Views, panels and editors have restored. For editors this means, that
|
||||
* they show their contents fully.
|
||||
*/
|
||||
Restored = 3,
|
||||
|
||||
/**
|
||||
* The last phase after views, panels and editors have restored and
|
||||
* some time has passed (few seconds).
|
||||
*/
|
||||
Eventually = 4
|
||||
}
|
||||
|
||||
export function LifecyclePhaseToString(phase: LifecyclePhase) {
|
||||
switch (phase) {
|
||||
case LifecyclePhase.Starting: return 'Starting';
|
||||
case LifecyclePhase.Ready: return 'Ready';
|
||||
case LifecyclePhase.Restored: return 'Restored';
|
||||
case LifecyclePhase.Eventually: return 'Eventually';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A lifecycle service informs about lifecycle events of the
|
||||
* application, such as shutdown.
|
||||
*/
|
||||
export interface ILifecycleService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/**
|
||||
* Value indicates how this window got loaded.
|
||||
*/
|
||||
readonly startupKind: StartupKind;
|
||||
|
||||
/**
|
||||
* A flag indicating in what phase of the lifecycle we currently are.
|
||||
*/
|
||||
phase: LifecyclePhase;
|
||||
|
||||
/**
|
||||
* Fired before shutdown happens. Allows listeners to veto against the
|
||||
* shutdown to prevent it from happening.
|
||||
*
|
||||
* The event carries a shutdown reason that indicates how the shutdown was triggered.
|
||||
*/
|
||||
readonly onBeforeShutdown: Event<BeforeShutdownEvent>;
|
||||
|
||||
/**
|
||||
* Fired when no client is preventing the shutdown from happening (from onBeforeShutdown).
|
||||
* Can be used to save UI state even if that is long running through the WillShutdownEvent#join()
|
||||
* method.
|
||||
*
|
||||
* The event carries a shutdown reason that indicates how the shutdown was triggered.
|
||||
*/
|
||||
readonly onWillShutdown: Event<WillShutdownEvent>;
|
||||
|
||||
/**
|
||||
* Fired when the shutdown is about to happen after long running shutdown operations
|
||||
* have finished (from onWillShutdown). This is the right place to dispose resources.
|
||||
*/
|
||||
readonly onShutdown: Event<void>;
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves when a certain lifecycle phase
|
||||
* has started.
|
||||
*/
|
||||
when(phase: LifecyclePhase): Promise<void>;
|
||||
|
||||
/**
|
||||
* Triggers a shutdown of the workbench. Depending on native or web, this can have
|
||||
* different implementations and behaviour.
|
||||
*
|
||||
* **Note:** this should normally not be called. See related methods in `IHostService`
|
||||
* and `INativeHostService` to close a window or quit the application.
|
||||
*/
|
||||
shutdown(): void;
|
||||
}
|
||||
|
||||
export const NullLifecycleService: ILifecycleService = {
|
||||
|
||||
_serviceBrand: undefined,
|
||||
|
||||
onBeforeShutdown: Event.None,
|
||||
onWillShutdown: Event.None,
|
||||
onShutdown: Event.None,
|
||||
|
||||
phase: LifecyclePhase.Restored,
|
||||
startupKind: StartupKind.NewWindow,
|
||||
|
||||
async when() { },
|
||||
shutdown() { }
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { Barrier } from 'vs/base/common/async';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ILifecycleService, BeforeShutdownEvent, WillShutdownEvent, StartupKind, LifecyclePhase, LifecyclePhaseToString } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { mark } from 'vs/base/common/performance';
|
||||
|
||||
export abstract class AbstractLifecycleService extends Disposable implements ILifecycleService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
protected readonly _onBeforeShutdown = this._register(new Emitter<BeforeShutdownEvent>());
|
||||
readonly onBeforeShutdown = this._onBeforeShutdown.event;
|
||||
|
||||
protected readonly _onWillShutdown = this._register(new Emitter<WillShutdownEvent>());
|
||||
readonly onWillShutdown = this._onWillShutdown.event;
|
||||
|
||||
protected readonly _onShutdown = this._register(new Emitter<void>());
|
||||
readonly onShutdown = this._onShutdown.event;
|
||||
|
||||
protected _startupKind: StartupKind = StartupKind.NewWindow;
|
||||
get startupKind(): StartupKind { return this._startupKind; }
|
||||
|
||||
private _phase: LifecyclePhase = LifecyclePhase.Starting;
|
||||
get phase(): LifecyclePhase { return this._phase; }
|
||||
|
||||
private readonly phaseWhen = new Map<LifecyclePhase, Barrier>();
|
||||
|
||||
constructor(
|
||||
@ILogService protected readonly logService: ILogService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
set phase(value: LifecyclePhase) {
|
||||
if (value < this.phase) {
|
||||
throw new Error('Lifecycle cannot go backwards');
|
||||
}
|
||||
|
||||
if (this._phase === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.trace(`lifecycle: phase changed (value: ${value})`);
|
||||
|
||||
this._phase = value;
|
||||
mark(`LifecyclePhase/${LifecyclePhaseToString(value)}`);
|
||||
|
||||
const barrier = this.phaseWhen.get(this._phase);
|
||||
if (barrier) {
|
||||
barrier.open();
|
||||
this.phaseWhen.delete(this._phase);
|
||||
}
|
||||
}
|
||||
|
||||
async when(phase: LifecyclePhase): Promise<void> {
|
||||
if (phase <= this._phase) {
|
||||
return;
|
||||
}
|
||||
|
||||
let barrier = this.phaseWhen.get(phase);
|
||||
if (!barrier) {
|
||||
barrier = new Barrier();
|
||||
this.phaseWhen.set(phase, barrier);
|
||||
}
|
||||
|
||||
await barrier.wait();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses to implement the explicit shutdown method.
|
||||
*/
|
||||
abstract shutdown(): void;
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { handleVetos } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { ShutdownReason, StartupKind, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
||||
import { IStorageService, StorageScope, WillSaveStateReason } from 'vs/platform/storage/common/storage';
|
||||
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { AbstractLifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycleService';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
|
||||
|
||||
export class NativeLifecycleService extends AbstractLifecycleService {
|
||||
|
||||
private static readonly LAST_SHUTDOWN_REASON_KEY = 'lifecyle.lastShutdownReason';
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private shutdownReason: ShutdownReason | undefined;
|
||||
|
||||
constructor(
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@INativeHostService private readonly nativeHostService: INativeHostService,
|
||||
@IStorageService readonly storageService: IStorageService,
|
||||
@ILogService readonly logService: ILogService
|
||||
) {
|
||||
super(logService);
|
||||
|
||||
this._startupKind = this.resolveStartupKind();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private resolveStartupKind(): StartupKind {
|
||||
const lastShutdownReason = this.storageService.getNumber(NativeLifecycleService.LAST_SHUTDOWN_REASON_KEY, StorageScope.WORKSPACE);
|
||||
this.storageService.remove(NativeLifecycleService.LAST_SHUTDOWN_REASON_KEY, StorageScope.WORKSPACE);
|
||||
|
||||
let startupKind: StartupKind;
|
||||
if (lastShutdownReason === ShutdownReason.RELOAD) {
|
||||
startupKind = StartupKind.ReloadedWindow;
|
||||
} else if (lastShutdownReason === ShutdownReason.LOAD) {
|
||||
startupKind = StartupKind.ReopenedWindow;
|
||||
} else {
|
||||
startupKind = StartupKind.NewWindow;
|
||||
}
|
||||
|
||||
this.logService.trace(`lifecycle: starting up (startup kind: ${this._startupKind})`);
|
||||
|
||||
return startupKind;
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
const windowId = this.nativeHostService.windowId;
|
||||
|
||||
// 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})`);
|
||||
|
||||
// trigger onBeforeShutdown events and veto collecting
|
||||
this.handleBeforeShutdown(reply.reason).then(veto => {
|
||||
if (veto) {
|
||||
this.logService.trace('lifecycle: onBeforeUnload prevented via veto');
|
||||
|
||||
ipcRenderer.send(reply.cancelChannel, windowId);
|
||||
} else {
|
||||
this.logService.trace('lifecycle: onBeforeUnload continues without veto');
|
||||
|
||||
this.shutdownReason = reply.reason;
|
||||
ipcRenderer.send(reply.okChannel, windowId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 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})`);
|
||||
|
||||
// trigger onWillShutdown events and joining
|
||||
await this.handleWillShutdown(reply.reason);
|
||||
|
||||
// trigger onShutdown event now that we know we will quit
|
||||
this._onShutdown.fire();
|
||||
|
||||
// acknowledge to main side
|
||||
ipcRenderer.send(reply.replyChannel, windowId);
|
||||
});
|
||||
|
||||
// Save shutdown reason to retrieve on next startup
|
||||
this.storageService.onWillSaveState(e => {
|
||||
if (e.reason === WillSaveStateReason.SHUTDOWN) {
|
||||
this.storageService.store(NativeLifecycleService.LAST_SHUTDOWN_REASON_KEY, this.shutdownReason, StorageScope.WORKSPACE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private handleBeforeShutdown(reason: ShutdownReason): Promise<boolean> {
|
||||
const vetos: (boolean | Promise<boolean>)[] = [];
|
||||
|
||||
this._onBeforeShutdown.fire({
|
||||
veto(value) {
|
||||
vetos.push(value);
|
||||
},
|
||||
reason
|
||||
});
|
||||
|
||||
return handleVetos(vetos, error => this.onShutdownError(reason, error));
|
||||
}
|
||||
|
||||
private async handleWillShutdown(reason: ShutdownReason): Promise<void> {
|
||||
const joiners: Promise<void>[] = [];
|
||||
|
||||
this._onWillShutdown.fire({
|
||||
join(promise) {
|
||||
if (promise) {
|
||||
joiners.push(promise);
|
||||
}
|
||||
},
|
||||
reason
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(joiners);
|
||||
} catch (error) {
|
||||
this.onShutdownError(reason, error);
|
||||
}
|
||||
}
|
||||
|
||||
private onShutdownError(reason: ShutdownReason, error: Error): void {
|
||||
let message: string;
|
||||
switch (reason) {
|
||||
case ShutdownReason.CLOSE:
|
||||
message = localize('errorClose', "An unexpected error was thrown while attempting to close the window ({0}).", toErrorMessage(error));
|
||||
break;
|
||||
case ShutdownReason.QUIT:
|
||||
message = localize('errorQuit', "An unexpected error was thrown while attempting to quit the application ({0}).", toErrorMessage(error));
|
||||
break;
|
||||
case ShutdownReason.RELOAD:
|
||||
message = localize('errorReload', "An unexpected error was thrown while attempting to reload the window ({0}).", toErrorMessage(error));
|
||||
break;
|
||||
case ShutdownReason.LOAD:
|
||||
message = localize('errorLoad', "An unexpected error was thrown while attempting to change the workspace of the window ({0}).", toErrorMessage(error));
|
||||
break;
|
||||
}
|
||||
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message,
|
||||
sticky: true
|
||||
});
|
||||
|
||||
onUnexpectedError(error);
|
||||
}
|
||||
|
||||
shutdown(): void {
|
||||
this.nativeHostService.closeWindow();
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(ILifecycleService, NativeLifecycleService);
|
||||
Reference in New Issue
Block a user