diff --git a/packages/core/src/browser/common-frontend-contribution.ts b/packages/core/src/browser/common-frontend-contribution.ts index 60f1b0b5dccda..4b1f0acf6894c 100644 --- a/packages/core/src/browser/common-frontend-contribution.ts +++ b/packages/core/src/browser/common-frontend-contribution.ts @@ -56,6 +56,7 @@ import { QuickInputService, QuickPick, QuickPickItem } from './quick-input'; import { AsyncLocalizationProvider } from '../common/i18n/localization'; import { nls } from '../common/nls'; import { confirmExit } from './dialogs'; +import { WindowService } from './window/window-service'; export namespace CommonMenus { @@ -349,6 +350,9 @@ export class CommonFrontendContribution implements FrontendApplicationContributi @inject(AuthenticationService) protected readonly authenticationService: AuthenticationService; + @inject(WindowService) + protected readonly windowService: WindowService; + async configure(app: FrontendApplication): Promise { const configDirUri = await this.environments.getConfigDirUri(); // Global settings @@ -1041,10 +1045,10 @@ export class CommonFrontendContribution implements FrontendApplicationContributi for (const additionalLanguage of ['en', ...availableLanguages]) { items.push({ label: additionalLanguage, - execute: () => { - if (additionalLanguage !== nls.locale) { + execute: async () => { + if (additionalLanguage !== nls.locale && await this.windowService.safeToShutDown()) { window.localStorage.setItem(nls.localeId, additionalLanguage); - window.location.reload(); + this.windowService.reload(); } } }); diff --git a/packages/core/src/browser/window/default-window-service.ts b/packages/core/src/browser/window/default-window-service.ts index 60c92b9093b2d..02f62c3843f7f 100644 --- a/packages/core/src/browser/window/default-window-service.ts +++ b/packages/core/src/browser/window/default-window-service.ts @@ -27,6 +27,7 @@ import { confirmExit } from '../dialogs'; export class DefaultWindowService implements WindowService, FrontendApplicationContribution { protected frontendApplication: FrontendApplication; + protected allowVetoes = true; protected onUnloadEmitter = new Emitter(); get onUnload(): Event { @@ -54,21 +55,31 @@ export class DefaultWindowService implements WindowService, FrontendApplicationC this.openNewWindow(DEFAULT_WINDOW_HASH); } + /** + * Returns a list of actions that {@link FrontendApplicationContribution}s would like to take before shutdown + * It is expected that this will succeed - i.e. return an empty array - at most once per session. If no vetoes are received + * during any cycle, no further checks will be made. In that case, shutdown should proceed unconditionally. + */ protected collectContributionUnloadVetoes(): OnWillStopAction[] { const vetoes = []; - const shouldConfirmExit = this.corePreferences['application.confirmExit']; - for (const contribution of this.contributions.getContributions()) { - const veto = contribution.onWillStop?.(this.frontendApplication); - if (veto && shouldConfirmExit !== 'never') { // Ignore vetoes if we should not prompt for exit. - if (OnWillStopAction.is(veto)) { - vetoes.push(veto); - } else { - vetoes.push({ reason: 'No reason given', action: () => false }); + if (this.allowVetoes) { + const shouldConfirmExit = this.corePreferences['application.confirmExit']; + for (const contribution of this.contributions.getContributions()) { + const veto = contribution.onWillStop?.(this.frontendApplication); + if (veto && shouldConfirmExit !== 'never') { // Ignore vetoes if we should not prompt for exit or if we have already run vetoes. + if (OnWillStopAction.is(veto)) { + vetoes.push(veto); + } else { + vetoes.push({ reason: 'No reason given', action: () => false }); + } } } - } - if (vetoes.length === 0 && shouldConfirmExit === 'always') { - vetoes.push({ reason: 'application.confirmExit preference', action: () => confirmExit() }); + if (vetoes.length === 0 && shouldConfirmExit === 'always') { + vetoes.push({ reason: 'application.confirmExit preference', action: () => confirmExit() }); + } + if (vetoes.length === 0) { + this.allowVetoes = false; + } } return vetoes; } @@ -94,6 +105,7 @@ export class DefaultWindowService implements WindowService, FrontendApplicationC const resolvedVetoes = await Promise.allSettled(vetoes.map(({ action }) => action())); if (resolvedVetoes.every(resolution => resolution.status === 'rejected' || resolution.value === true)) { console.debug('OnWillStop actions resolved; allowing shutdown'); + this.allowVetoes = false; return true; } else { return false; diff --git a/packages/core/src/browser/window/window-service.ts b/packages/core/src/browser/window/window-service.ts index ac76c3cb534d1..aee3ea8c99e68 100644 --- a/packages/core/src/browser/window/window-service.ts +++ b/packages/core/src/browser/window/window-service.ts @@ -45,6 +45,9 @@ export interface WindowService { /** * Checks `FrontendApplicationContribution#willStop` for impediments to shutdown and runs any actions returned. * Can be used safely in browser and Electron when triggering reload or shutdown programmatically. + * Should _only_ be called before a shutdown - if this returns `true`, `FrontendApplicationContribution#willStop` + * will not be called again in the current session. I.e. if this return `true`, the shutdown should proceed without + * further condition. */ safeToShutDown(): Promise; diff --git a/packages/core/src/electron-common/messaging/electron-messages.ts b/packages/core/src/electron-common/messaging/electron-messages.ts index 22217548d3728..5a01665af1269 100644 --- a/packages/core/src/electron-common/messaging/electron-messages.ts +++ b/packages/core/src/electron-common/messaging/electron-messages.ts @@ -28,8 +28,18 @@ export const CLOSE_REQUESTED_SIGNAL = 'close-requested'; export const RELOAD_REQUESTED_SIGNAL = 'reload-requested'; export enum StopReason { + /** + * Closing the window with no prospect of restart. + */ Close, + /** + * Reload without closing the window. + */ Reload, + /** + * Reload that includes closing the window. + */ + Restart, // eslint-disable-line @typescript-eslint/no-shadow } export interface CloseRequestArguments { diff --git a/packages/core/src/electron-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts index cde5c16c64d0f..18642faa165a3 100644 --- a/packages/core/src/electron-main/electron-main-application.ts +++ b/packages/core/src/electron-main/electron-main-application.ts @@ -457,14 +457,15 @@ export class ElectronMainApplication { } event.preventDefault(); - const doExit = () => { - this.closeIsConfirmed.add(electronWindow.id); - electronWindow.close(); - }; - this.handleStopRequest(electronWindow, doExit, StopReason.Close); + this.handleStopRequest(electronWindow, () => this.doCloseWindow(electronWindow), StopReason.Close); }); } + protected doCloseWindow(electronWindow: BrowserWindow): void { + this.closeIsConfirmed.add(electronWindow.id); + electronWindow.close(); + } + protected async handleStopRequest(electronWindow: BrowserWindow, onSafeCallback: () => unknown, reason: StopReason): Promise { const safeToClose = await this.checkSafeToStop(electronWindow, reason); if (safeToClose) { @@ -607,7 +608,7 @@ export class ElectronMainApplication { }); this.restarting = false; }); - window.close(); + this.handleStopRequest(window, () => this.doCloseWindow(window), StopReason.Restart); } protected async handleReload(event: Electron.IpcMainEvent): Promise { diff --git a/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts b/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts index 7589243827900..49ecbc8a3a9f4 100755 --- a/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts +++ b/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts @@ -78,6 +78,7 @@ import { } from '@theia/plugin-ext/lib/main/browser/callhierarchy/callhierarchy-type-converters'; import { CustomEditorOpener } from '@theia/plugin-ext/lib/main/browser/custom-editors/custom-editor-opener'; import { nls } from '@theia/core/lib/common/nls'; +import { WindowService } from '@theia/core/lib/browser/window/window-service'; export namespace VscodeCommands { export const OPEN: Command = { @@ -137,6 +138,8 @@ export class PluginVscodeCommandsContribution implements CommandContribution { protected readonly callHierarchyProvider: CallHierarchyServiceProvider; @inject(MonacoTextModelService) protected readonly textModelService: MonacoTextModelService; + @inject(WindowService) + protected readonly windowService: WindowService; private async openWith(commandId: string, resource: URI, columnOrOptions?: ViewColumn | TextDocumentShowOptions, openerId?: string): Promise { if (!resource) { @@ -477,7 +480,7 @@ export class PluginVscodeCommandsContribution implements CommandContribution { commands.registerCommand({ id: 'workbench.action.reloadWindow' }, { execute: () => { - window.location.reload(); + this.windowService.reload(); } });