From 6c9dab1259f603521918091e9ade540a99389043 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 10 Dec 2020 09:16:58 +0100 Subject: [PATCH] web - register external opener to prevent unload on expected href changes --- src/vs/workbench/browser/web.main.ts | 7 +++ src/vs/workbench/browser/window.ts | 56 +++++++++++++++++++ src/vs/workbench/electron-sandbox/window.ts | 2 +- .../lifecycle/browser/lifecycleService.ts | 17 ++++++ 4 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 src/vs/workbench/browser/window.ts diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 08c6a397ade..088c5561594 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -61,6 +61,7 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { UriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentityService'; +import { BrowserWindow } from 'vs/workbench/browser/window'; class BrowserMain extends Disposable { @@ -103,6 +104,12 @@ class BrowserMain extends Disposable { // Startup const instantiationService = workbench.startup(); + // Window + this._register(instantiationService.createInstance(BrowserWindow)); + + // Logging + services.logService.trace('workbench configuration', JSON.stringify(this.configuration)); + // Return API Facade return instantiationService.invokeFunction(accessor => { const commandService = accessor.get(ICommandService); diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts new file mode 100644 index 00000000000..c1595223ccc --- /dev/null +++ b/src/vs/workbench/browser/window.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { windowOpenNoOpener } from 'vs/base/browser/dom'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; +import { BrowserLifecycleService } from 'vs/workbench/services/lifecycle/browser/lifecycleService'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; + +export class BrowserWindow extends Disposable { + + constructor( + @IOpenerService private readonly openerService: IOpenerService, + @ILifecycleService private readonly lifecycleService: BrowserLifecycleService + ) { + super(); + + this.create(); + } + + private create(): void { + + // Handle open calls + this.setupOpenHandlers(); + } + + private setupOpenHandlers(): void { + + // Block window.open() calls + window.open = function (): Window | null { + throw new Error('Prevented call to window.open(). Use IOpenerService instead!'); + }; + + // We need to ignore the `beforeunload` event while + // we handle external links to open specifically for + // the case of application protocols that e.g. invoke + // vscode itself. We do not want to open these links + // in a new window because that would leave a blank + // window to the user, but using `window.location.href` + // will trigger the `beforeunload`. + this.openerService.setExternalOpener({ + openExternal: async (href: string) => { + if (matchesScheme(href, Schemas.http) || matchesScheme(href, Schemas.https)) { + windowOpenNoOpener(href); + } else { + this.lifecycleService.withExpectedUnload(() => window.location.href = href); + } + + return true; + } + }); + } +} diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 8c4309244ee..9242ca0b6d6 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -125,7 +125,7 @@ export class NativeWindow extends Disposable { // React to editor input changes this._register(this.editorService.onDidActiveEditorChange(() => this.updateTouchbarMenu())); - // prevent opening a real URL inside the shell + // prevent opening a real URL inside the window [EventType.DRAG_OVER, EventType.DROP].forEach(event => { window.document.body.addEventListener(event, (e: DragEvent) => { EventHelper.stop(e); diff --git a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts index d9e5aed88ce..609563ad05e 100644 --- a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts @@ -16,6 +16,7 @@ export class BrowserLifecycleService extends AbstractLifecycleService { declare readonly _serviceBrand: undefined; private beforeUnloadDisposable: IDisposable | undefined = undefined; + private expectedUnload = false; constructor( @ILogService readonly logService: ILogService @@ -32,6 +33,13 @@ export class BrowserLifecycleService extends AbstractLifecycleService { } private onBeforeUnload(event: BeforeUnloadEvent): void { + if (this.expectedUnload) { + this.logService.info('[lifecycle] onBeforeUnload expected, ignoring once'); + + this.expectedUnload = false; + return; // ignore expected unload only once + } + this.logService.info('[lifecycle] onBeforeUnload triggered'); this.doShutdown(() => { @@ -41,6 +49,15 @@ export class BrowserLifecycleService extends AbstractLifecycleService { }); } + withExpectedUnload(callback: Function): void { + this.expectedUnload = true; + try { + callback(); + } finally { + this.expectedUnload = false; + } + } + shutdown(): void { this.logService.info('[lifecycle] shutdown triggered');