UNPKG

@sussudio/platform

Version:

Internal APIs for VS Code's service injection the base services.

1,253 lines 59.1 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc, d; if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function') r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); }; }; import { app, BrowserWindow, nativeImage, screen, systemPreferences, TouchBar } from 'electron'; import { DeferredPromise, RunOnceScheduler, timeout } from '@sussudio/base/common/async.mjs'; import { CancellationToken } from '@sussudio/base/common/cancellation.mjs'; import { toErrorMessage } from '@sussudio/base/common/errorMessage.mjs'; import { Emitter } from '@sussudio/base/common/event.mjs'; import { mnemonicButtonLabel } from '@sussudio/base/common/labels.mjs'; import { Disposable } from '@sussudio/base/common/lifecycle.mjs'; import { FileAccess, Schemas } from '@sussudio/base/common/network.mjs'; import { join } from '@sussudio/base/common/path.mjs'; import { getMarks, mark } from '@sussudio/base/common/performance.mjs'; import { isLinux, isMacintosh, isWindows } from '@sussudio/base/common/platform.mjs'; import { URI } from '@sussudio/base/common/uri.mjs'; import { localize } from 'vscode-nls.mjs'; import { IBackupMainService } from '../../backup/electron-main/backup.mjs'; import { IConfigurationService } from '../../configuration/common/configuration.mjs'; import { IDialogMainService } from '../../dialogs/electron-main/dialogMainService.mjs'; import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.mjs'; import { isLaunchedFromCli } from '../../environment/node/argvHelper.mjs'; import { IFileService } from '../../files/common/files.mjs'; import { ILifecycleMainService } from '../../lifecycle/electron-main/lifecycleMainService.mjs'; import { ILogService } from '../../log/common/log.mjs'; import { IProductService } from '../../product/common/productService.mjs'; import { IProtocolMainService } from '../../protocol/electron-main/protocol.mjs'; import { resolveMarketplaceHeaders } from '../../externalServices/common/marketplace.mjs'; import { IApplicationStorageMainService, IStorageMainService, } from '../../storage/electron-main/storageMainService.mjs'; import { ITelemetryService } from '../../telemetry/common/telemetry.mjs'; import { ThemeIcon } from '../../theme/common/themeService.mjs'; import { IThemeMainService } from '../../theme/electron-main/themeMainService.mjs'; import { getMenuBarVisibility, getTitleBarStyle, useWindowControlsOverlay, WindowMinimumSize, zoomLevelToZoomFactor, } from '../../window/common/window.mjs'; import { IWindowsMainService } from './windows.mjs'; import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, toWorkspaceIdentifier, } from '../../workspace/common/workspace.mjs'; import { IWorkspacesManagementMainService } from '../../workspaces/electron-main/workspacesManagementMainService.mjs'; import { defaultWindowState } from '../../window/electron-main/window.mjs'; import { Color } from '@sussudio/base/common/color.mjs'; import { IPolicyService } from '../../policy/common/policy.mjs'; import { IStateMainService } from '../../state/electron-main/state.mjs'; import { IUserDataProfilesMainService } from '../../userDataProfile/electron-main/userDataProfile.mjs'; import { INativeHostMainService } from '../../native/electron-main/nativeHostMainService.mjs'; import { OneDataSystemAppender } from '../../telemetry/node/1dsAppender.mjs'; import { TelemetryService } from '../../telemetry/common/telemetryService.mjs'; import { getPiiPathsFromEnvironment, isInternalTelemetry, supportsTelemetry, } from '../../telemetry/common/telemetryUtils.mjs'; import { resolveCommonProperties } from '../../telemetry/common/commonProperties.mjs'; import { hostname, release } from 'os'; import { resolveMachineId } from '../../telemetry/electron-main/telemetryUtils.mjs'; let CodeWindow = class CodeWindow extends Disposable { logService; environmentMainService; policyService; userDataProfilesService; fileService; applicationStorageMainService; storageMainService; configurationService; themeMainService; workspacesManagementMainService; backupMainService; telemetryService; dialogMainService; lifecycleMainService; productService; protocolMainService; windowsMainService; stateMainService; nativeHostMainService; static windowControlHeightStateStorageKey = 'windowControlHeight'; static sandboxState = undefined; //#region Events _onWillLoad = this._register(new Emitter()); onWillLoad = this._onWillLoad.event; _onDidSignalReady = this._register(new Emitter()); onDidSignalReady = this._onDidSignalReady.event; _onDidTriggerSystemContextMenu = this._register(new Emitter()); onDidTriggerSystemContextMenu = this._onDidTriggerSystemContextMenu.event; _onDidClose = this._register(new Emitter()); onDidClose = this._onDidClose.event; _onDidDestroy = this._register(new Emitter()); onDidDestroy = this._onDidDestroy.event; //#endregion //#region Properties _id; get id() { return this._id; } _win; get win() { return this._win; } _lastFocusTime = -1; get lastFocusTime() { return this._lastFocusTime; } get backupPath() { return this._config?.backupPath; } get openedWorkspace() { return this._config?.workspace; } get profile() { if (!this.config) { return undefined; } const profile = this.userDataProfilesService.profiles.find( (profile) => profile.id === this.config?.profiles.profile.id, ); if (this.isExtensionDevelopmentHost && profile) { return profile; } return ( this.userDataProfilesService.getProfileForWorkspace( this.config.workspace ?? toWorkspaceIdentifier(this.backupPath, this.isExtensionDevelopmentHost), ) ?? this.userDataProfilesService.defaultProfile ); } get remoteAuthority() { return this._config?.remoteAuthority; } _config; get config() { return this._config; } get isExtensionDevelopmentHost() { return !!this._config?.extensionDevelopmentPath; } get isExtensionTestHost() { return !!this._config?.extensionTestsPath; } get isExtensionDevelopmentTestFromCli() { return this.isExtensionDevelopmentHost && this.isExtensionTestHost && !this._config?.debugId; } //#endregion windowState; currentMenuBarVisibility; // TODO@electron workaround for https://github.com/electron/electron/issues/35360 // where on macOS the window will report a wrong state for `isFullScreen()` while // transitioning into and out of native full screen. transientIsNativeFullScreen = undefined; joinNativeFullScreenTransition = undefined; representedFilename; documentEdited; hasWindowControlOverlay = false; whenReadyCallbacks = []; touchBarGroups = []; currentHttpProxy = undefined; currentNoProxy = undefined; configObjectUrl = this._register(this.protocolMainService.createIPCObjectUrl()); pendingLoadConfig; wasLoaded = false; constructor( config, logService, environmentMainService, policyService, userDataProfilesService, fileService, applicationStorageMainService, storageMainService, configurationService, themeMainService, workspacesManagementMainService, backupMainService, telemetryService, dialogMainService, lifecycleMainService, productService, protocolMainService, windowsMainService, stateMainService, nativeHostMainService, ) { super(); this.logService = logService; this.environmentMainService = environmentMainService; this.policyService = policyService; this.userDataProfilesService = userDataProfilesService; this.fileService = fileService; this.applicationStorageMainService = applicationStorageMainService; this.storageMainService = storageMainService; this.configurationService = configurationService; this.themeMainService = themeMainService; this.workspacesManagementMainService = workspacesManagementMainService; this.backupMainService = backupMainService; this.telemetryService = telemetryService; this.dialogMainService = dialogMainService; this.lifecycleMainService = lifecycleMainService; this.productService = productService; this.protocolMainService = protocolMainService; this.windowsMainService = windowsMainService; this.stateMainService = stateMainService; this.nativeHostMainService = nativeHostMainService; //#region create browser window { // Load window state const [state, hasMultipleDisplays] = this.restoreWindowState(config.state); this.windowState = state; this.logService.trace('window#ctor: using window state', state); // In case we are maximized or fullscreen, only show later // after the call to maximize/fullscreen (see below) const isFullscreenOrMaximized = this.windowState.mode === 0 /* WindowMode.Maximized */ || this.windowState.mode === 3 /* WindowMode.Fullscreen */; if (typeof CodeWindow.sandboxState === 'undefined') { // we should only check this once so that we do not end up // with some windows in sandbox mode and some not! CodeWindow.sandboxState = this.stateMainService.getItem('window.experimental.useSandbox', false); } const windowSettings = this.configurationService.getValue('window'); let useSandbox = false; if (typeof windowSettings?.experimental?.useSandbox === 'boolean') { useSandbox = windowSettings.experimental.useSandbox; } else if (this.productService.quality === 'stable' && CodeWindow.sandboxState) { useSandbox = true; } else { useSandbox = typeof this.productService.quality === 'string' && this.productService.quality !== 'stable'; } const options = { width: this.windowState.width, height: this.windowState.height, x: this.windowState.x, y: this.windowState.y, backgroundColor: this.themeMainService.getBackgroundColor(), minWidth: WindowMinimumSize.WIDTH, minHeight: WindowMinimumSize.HEIGHT, show: !isFullscreenOrMaximized, title: this.productService.nameLong, webPreferences: { preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js').fsPath, additionalArguments: [`--vscode-window-config=${this.configObjectUrl.resource.toString()}`], v8CacheOptions: this.environmentMainService.useCodeCache ? 'bypassHeatCheck' : 'none', enableWebSQL: false, spellcheck: false, zoomFactor: zoomLevelToZoomFactor(windowSettings?.zoomLevel), autoplayPolicy: 'user-gesture-required', // Enable experimental css highlight api https://chromestatus.com/feature/5436441440026624 // Refs https://github.com/microsoft/vscode/issues/140098 enableBlinkFeatures: 'HighlightAPI', ...(useSandbox ? // Sandbox { sandbox: true, } : // No Sandbox { nodeIntegration: true, contextIsolation: false, }), }, experimentalDarkMode: true, }; // Apply icon to window // Linux: always // Windows: only when running out of sources, otherwise an icon is set by us on the executable if (isLinux) { options.icon = join(this.environmentMainService.appRoot, 'resources/linux/code.png'); } else if (isWindows && !this.environmentMainService.isBuilt) { options.icon = join(this.environmentMainService.appRoot, 'resources/win32/code_150x150.png'); } if (isMacintosh && !this.useNativeFullScreen()) { options.fullscreenable = false; // enables simple fullscreen mode } if (isMacintosh) { options.acceptFirstMouse = true; // enabled by default if (windowSettings?.clickThroughInactive === false) { options.acceptFirstMouse = false; } } const useNativeTabs = isMacintosh && windowSettings?.nativeTabs === true; if (useNativeTabs) { options.tabbingIdentifier = this.productService.nameShort; // this opts in to sierra tabs } const useCustomTitleStyle = getTitleBarStyle(this.configurationService) === 'custom'; if (useCustomTitleStyle) { options.titleBarStyle = 'hidden'; if (!isMacintosh) { options.frame = false; } if (useWindowControlsOverlay(this.configurationService)) { // This logic will not perfectly guess the right colors // to use on initialization, but prefer to keep things // simple as it is temporary and not noticeable const titleBarColor = this.themeMainService.getWindowSplash()?.colorInfo.titleBarBackground ?? this.themeMainService.getBackgroundColor(); const symbolColor = Color.fromHex(titleBarColor).isDarker() ? '#FFFFFF' : '#000000'; options.titleBarOverlay = { height: 29, color: titleBarColor, symbolColor, }; this.hasWindowControlOverlay = true; } } // Create the browser window mark('code/willCreateCodeBrowserWindow'); this._win = new BrowserWindow(options); mark('code/didCreateCodeBrowserWindow'); this._id = this._win.id; if (isMacintosh && useCustomTitleStyle) { this._win.setSheetOffset(22); // offset dialogs by the height of the custom title bar if we have any } // Update the window controls immediately based on cached values if (useCustomTitleStyle && ((isWindows && useWindowControlsOverlay(this.configurationService)) || isMacintosh)) { const cachedWindowControlHeight = this.stateMainService.getItem(CodeWindow.windowControlHeightStateStorageKey); if (cachedWindowControlHeight) { this.updateWindowControls({ height: cachedWindowControlHeight }); } } // Windows Custom System Context Menu // See https://github.com/electron/electron/issues/24893 // // The purpose of this is to allow for the context menu in the Windows Title Bar // // Currently, all mouse events in the title bar are captured by the OS // thus we need to capture them here with a window hook specific to Windows // and then forward them to the correct window. if (isWindows && useCustomTitleStyle) { const WM_INITMENU = 0x0116; // https://docs.microsoft.com/en-us/windows/win32/menurc/wm-initmenu // This sets up a listener for the window hook. This is a Windows-only API provided by electron. this._win.hookWindowMessage(WM_INITMENU, () => { const [x, y] = this._win.getPosition(); const cursorPos = screen.getCursorScreenPoint(); const cx = cursorPos.x - x; const cy = cursorPos.y - y; // In some cases, show the default system context menu // 1) The mouse position is not within the title bar // 2) The mouse position is within the title bar, but over the app icon // We do not know the exact title bar height but we make an estimate based on window height const shouldTriggerDefaultSystemContextMenu = () => { // Use the custom context menu when over the title bar, but not over the app icon // The app icon is estimated to be 30px wide // The title bar is estimated to be the max of 35px and 15% of the window height if (cx > 30 && cy >= 0 && cy <= Math.max(this._win.getBounds().height * 0.15, 35)) { return false; } return true; }; if (!shouldTriggerDefaultSystemContextMenu()) { // This is necessary to make sure the native system context menu does not show up. this._win.setEnabled(false); this._win.setEnabled(true); this._onDidTriggerSystemContextMenu.fire({ x: cx, y: cy }); } return 0; }); } // TODO@electron (Electron 4 regression): when running on multiple displays where the target display // to open the window has a larger resolution than the primary display, the window will not size // correctly unless we set the bounds again (https://github.com/microsoft/vscode/issues/74872) // // Extended to cover Windows as well as Mac (https://github.com/microsoft/vscode/issues/146499) // // However, when running with native tabs with multiple windows we cannot use this workaround // because there is a potential that the new window will be added as native tab instead of being // a window on its own. In that case calling setBounds() would cause https://github.com/microsoft/vscode/issues/75830 if ( (isMacintosh || isWindows) && hasMultipleDisplays && (!useNativeTabs || BrowserWindow.getAllWindows().length === 1) ) { if ( [this.windowState.width, this.windowState.height, this.windowState.x, this.windowState.y].every( (value) => typeof value === 'number', ) ) { this._win.setBounds({ width: this.windowState.width, height: this.windowState.height, x: this.windowState.x, y: this.windowState.y, }); } } if (isFullscreenOrMaximized) { mark('code/willMaximizeCodeWindow'); // this call may or may not show the window, depends // on the platform: currently on Windows and Linux will // show the window as active. To be on the safe side, // we show the window at the end of this block. this._win.maximize(); if (this.windowState.mode === 3 /* WindowMode.Fullscreen */) { this.setFullScreen(true); } // to reduce flicker from the default window size // to maximize or fullscreen, we only show after this._win.show(); mark('code/didMaximizeCodeWindow'); } this._lastFocusTime = Date.now(); // since we show directly, we need to set the last focus time too } //#endregion // Open devtools if instructed from command line args if (this.environmentMainService.args['open-devtools'] === true) { this._win.webContents.openDevTools(); } // respect configured menu bar visibility this.onConfigurationUpdated(); // macOS: touch bar support this.createTouchBar(); // Eventing this.registerListeners(); } setRepresentedFilename(filename) { if (isMacintosh) { this._win.setRepresentedFilename(filename); } else { this.representedFilename = filename; } } getRepresentedFilename() { if (isMacintosh) { return this._win.getRepresentedFilename(); } return this.representedFilename; } setDocumentEdited(edited) { if (isMacintosh) { this._win.setDocumentEdited(edited); } this.documentEdited = edited; } isDocumentEdited() { if (isMacintosh) { return this._win.isDocumentEdited(); } return !!this.documentEdited; } focus(options) { // macOS: Electron > 7.x changed its behaviour to not // bring the application to the foreground when a window // is focused programmatically. Only via `app.focus` and // the option `steal: true` can you get the previous // behaviour back. The only reason to use this option is // when a window is getting focused while the application // is not in the foreground. if (isMacintosh && options?.force) { app.focus({ steal: true }); } if (!this._win) { return; } if (this._win.isMinimized()) { this._win.restore(); } this._win.focus(); } readyState = 0 /* ReadyState.NONE */; setReady() { this.logService.trace(`window#load: window reported ready (id: ${this._id})`); this.readyState = 2 /* ReadyState.READY */; // inform all waiting promises that we are ready now while (this.whenReadyCallbacks.length) { this.whenReadyCallbacks.pop()(this); } // Events this._onDidSignalReady.fire(); } ready() { return new Promise((resolve) => { if (this.isReady) { return resolve(this); } // otherwise keep and call later when we are ready this.whenReadyCallbacks.push(resolve); }); } get isReady() { return this.readyState === 2 /* ReadyState.READY */; } get whenClosedOrLoaded() { return new Promise((resolve) => { function handle() { closeListener.dispose(); loadListener.dispose(); resolve(); } const closeListener = this.onDidClose(() => handle()); const loadListener = this.onWillLoad(() => handle()); }); } registerListeners() { // Window error conditions to handle this._win.on('unresponsive', () => this.onWindowError(1 /* WindowError.UNRESPONSIVE */)); this._win.webContents.on('render-process-gone', (event, details) => this.onWindowError(2 /* WindowError.PROCESS_GONE */, details), ); this._win.webContents.on('did-fail-load', (event, exitCode, reason) => this.onWindowError(3 /* WindowError.LOAD */, { reason, exitCode }), ); // Prevent windows/iframes from blocking the unload // through DOM events. We have our own logic for // unloading a window that should not be confused // with the DOM way. // (https://github.com/microsoft/vscode/issues/122736) this._win.webContents.on('will-prevent-unload', (event) => { event.preventDefault(); }); // Window close this._win.on('closed', () => { this._onDidClose.fire(); this.dispose(); }); // Remember that we loaded this._win.webContents.on('did-finish-load', () => { // Associate properties from the load request if provided if (this.pendingLoadConfig) { this._config = this.pendingLoadConfig; this.pendingLoadConfig = undefined; } }); // Window Focus this._win.on('focus', () => { this._lastFocusTime = Date.now(); }); // Window (Un)Maximize this._win.on('maximize', (e) => { if (this._config) { this._config.maximized = true; } app.emit('browser-window-maximize', e, this._win); }); this._win.on('unmaximize', (e) => { if (this._config) { this._config.maximized = false; } app.emit('browser-window-unmaximize', e, this._win); }); // Window Fullscreen this._win.on('enter-full-screen', () => { this.sendWhenReady('vscode:enterFullScreen', CancellationToken.None); this.joinNativeFullScreenTransition?.complete(); this.joinNativeFullScreenTransition = undefined; }); this._win.on('leave-full-screen', () => { this.sendWhenReady('vscode:leaveFullScreen', CancellationToken.None); this.joinNativeFullScreenTransition?.complete(); this.joinNativeFullScreenTransition = undefined; }); // Handle configuration changes this._register(this.configurationService.onDidChangeConfiguration((e) => this.onConfigurationUpdated(e))); // Handle Workspace events this._register( this.workspacesManagementMainService.onDidDeleteUntitledWorkspace((e) => this.onDidDeleteUntitledWorkspace(e)), ); // Inject headers when requests are incoming const urls = ['https://marketplace.visualstudio.com/*', 'https://*.vsassets.io/*']; this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, async (details, cb) => { const headers = await this.getMarketplaceHeaders(); cb({ cancel: false, requestHeaders: Object.assign(details.requestHeaders, headers) }); }); } marketplaceHeadersPromise; getMarketplaceHeaders() { if (!this.marketplaceHeadersPromise) { this.marketplaceHeadersPromise = resolveMarketplaceHeaders( this.productService.version, this.productService, this.environmentMainService, this.configurationService, this.fileService, this.applicationStorageMainService, this.telemetryService, ); } return this.marketplaceHeadersPromise; } async onWindowError(type, details) { switch (type) { case 2 /* WindowError.PROCESS_GONE */: this.logService.error( `CodeWindow: renderer process gone (reason: ${details?.reason || '<unknown>'}, code: ${ details?.exitCode || '<unknown>' })`, ); break; case 1 /* WindowError.UNRESPONSIVE */: this.logService.error('CodeWindow: detected unresponsive'); break; case 3 /* WindowError.LOAD */: this.logService.error( `CodeWindow: failed to load (reason: ${details?.reason || '<unknown>'}, code: ${ details?.exitCode || '<unknown>' })`, ); break; } this.telemetryService.publicLog2('windowerror', { type, reason: details?.reason, code: details?.exitCode }); // Inform User if non-recoverable switch (type) { case 1 /* WindowError.UNRESPONSIVE */: case 2 /* WindowError.PROCESS_GONE */: // If we run extension tests from CLI, we want to signal // back this state to the test runner by exiting with a // non-zero exit code. if (this.isExtensionDevelopmentTestFromCli) { this.lifecycleMainService.kill(1); return; } // If we run smoke tests, want to proceed with an orderly // shutdown as much as possible by destroying the window // and then calling the normal `quit` routine. if (this.environmentMainService.args['enable-smoke-test-driver']) { await this.destroyWindow(false, false); this.lifecycleMainService.quit(); // still allow for an orderly shutdown return; } // Unresponsive if (type === 1 /* WindowError.UNRESPONSIVE */) { if ( this.isExtensionDevelopmentHost || this.isExtensionTestHost || (this._win && this._win.webContents && this._win.webContents.isDevToolsOpened()) ) { // TODO@electron Workaround for https://github.com/microsoft/vscode/issues/56994 // In certain cases the window can report unresponsiveness because a breakpoint was hit // and the process is stopped executing. The most typical cases are: // - devtools are opened and debugging happens // - window is an extensions development host that is being debugged // - window is an extension test development host that is being debugged return; } // Show Dialog const result = await this.dialogMainService.showMessageBox( { title: this.productService.nameLong, type: 'warning', buttons: [ mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, '&&Reopen')), mnemonicButtonLabel(localize({ key: 'wait', comment: ['&& denotes a mnemonic'] }, '&&Keep Waiting')), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, '&&Close')), ], message: localize('appStalled', 'The window is not responding'), detail: localize('appStalledDetail', 'You can reopen or close the window or keep waiting.'), noLink: true, defaultId: 0, cancelId: 1, checkboxLabel: this._config?.workspace ? localize('doNotRestoreEditors', "Don't restore editors") : undefined, }, this._win, ); // Handle choice if (result.response !== 1 /* keep waiting */) { const reopen = result.response === 0; await this.destroyWindow(reopen, result.checkboxChecked); } } // Process gone else if (type === 2 /* WindowError.PROCESS_GONE */) { // Windows: running as admin with AppLocker enabled is unsupported // when sandbox: true. // we cannot detect AppLocker use currently, but make a // guess based on the reason and exit code. if ( isWindows && details?.reason === 'launch-failed' && details.exitCode === 18 && (await this.nativeHostMainService.isAdmin(undefined)) ) { await this.handleWindowsAdminCrash(details); } // Any other crash: offer to restart else { let message; if (!details) { message = localize('appGone', 'The window terminated unexpectedly'); } else { message = localize( 'appGoneDetails', "The window terminated unexpectedly (reason: '{0}', code: '{1}')", details.reason, details.exitCode ?? '<unknown>', ); } // Show Dialog const result = await this.dialogMainService.showMessageBox( { title: this.productService.nameLong, type: 'warning', buttons: [ this._config?.workspace ? mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, '&&Reopen')) : mnemonicButtonLabel( localize({ key: 'newWindow', comment: ['&& denotes a mnemonic'] }, '&&New Window'), ), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, '&&Close')), ], message, detail: this._config?.workspace ? localize( 'appGoneDetailWorkspace', 'We are sorry for the inconvenience. You can reopen the window to continue where you left off.', ) : localize( 'appGoneDetailEmptyWindow', 'We are sorry for the inconvenience. You can open a new empty window to start again.', ), noLink: true, defaultId: 0, checkboxLabel: this._config?.workspace ? localize('doNotRestoreEditors', "Don't restore editors") : undefined, }, this._win, ); // Handle choice const reopen = result.response === 0; await this.destroyWindow(reopen, result.checkboxChecked); } } break; } } async handleWindowsAdminCrash(details) { // Prepare telemetry event (TODO@bpasero remove me eventually) const appenders = []; const isInternal = isInternalTelemetry(this.productService, this.configurationService); if (supportsTelemetry(this.productService, this.environmentMainService)) { if (this.productService.aiConfig && this.productService.aiConfig.ariaKey) { appenders.push( new OneDataSystemAppender(isInternal, 'monacoworkbench', null, this.productService.aiConfig.ariaKey), ); } const { installSourcePath } = this.environmentMainService; const machineId = await resolveMachineId(this.stateMainService); const config = { appenders, sendErrorTelemetry: false, commonProperties: resolveCommonProperties( this.fileService, release(), hostname(), process.arch, this.productService.commit, this.productService.version, machineId, isInternal, installSourcePath, ), piiPaths: getPiiPathsFromEnvironment(this.environmentMainService), }; const telemetryService = new TelemetryService(config, this.configurationService, this.productService); await telemetryService.publicLog2('windowadminerror', { reason: details.reason, code: details.exitCode }); } // Inform user const result = await this.dialogMainService.showMessageBox( { title: this.productService.nameLong, type: 'error', buttons: [ mnemonicButtonLabel(localize({ key: 'learnMore', comment: ['&& denotes a mnemonic'] }, '&&Learn More')), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, '&&Close')), ], message: localize('appGoneAdminMessage', 'Running as administrator is not supported in your environment'), detail: localize( 'appGoneAdminDetail', 'We are sorry for the inconvenience. Please try again without administrator privileges.', this.productService.nameLong, ), noLink: true, defaultId: 0, }, this._win, ); if (result.response === 0) { await this.nativeHostMainService.openExternal(undefined, 'https://go.microsoft.com/fwlink/?linkid=2220179'); } // Ensure to await flush telemetry await Promise.all(appenders.map((appender) => appender.flush())); // Exit await this.destroyWindow(false, false); } async destroyWindow(reopen, skipRestoreEditors) { const workspace = this._config?.workspace; // check to discard editor state first if (skipRestoreEditors && workspace) { try { const workspaceStorage = this.storageMainService.workspaceStorage(workspace); await workspaceStorage.init(); workspaceStorage.delete('memento/workbench.parts.editor'); await workspaceStorage.close(); } catch (error) { this.logService.error(error); } } // 'close' event will not be fired on destroy(), so signal crash via explicit event this._onDidDestroy.fire(); // make sure to destroy the window as its renderer process is gone this._win?.destroy(); // ask the windows service to open a new fresh window if specified if (reopen && this._config) { // We have to reconstruct a openable from the current workspace let uriToOpen = undefined; let forceEmpty = undefined; if (isSingleFolderWorkspaceIdentifier(workspace)) { uriToOpen = { folderUri: workspace.uri }; } else if (isWorkspaceIdentifier(workspace)) { uriToOpen = { workspaceUri: workspace.configPath }; } else { forceEmpty = true; } // Delegate to windows service const [window] = await this.windowsMainService.open({ context: 5 /* OpenContext.API */, userEnv: this._config.userEnv, cli: { ...this.environmentMainService.args, _: [], // we pass in the workspace to open explicitly via `urisToOpen` }, urisToOpen: uriToOpen ? [uriToOpen] : undefined, forceEmpty, forceNewWindow: true, remoteAuthority: this.remoteAuthority, }); window.focus(); } } onDidDeleteUntitledWorkspace(workspace) { // Make sure to update our workspace config if we detect that it // was deleted if (this._config?.workspace?.id === workspace.id) { this._config.workspace = undefined; } } onConfigurationUpdated(e) { // Menubar const newMenuBarVisibility = this.getMenuBarVisibility(); if (newMenuBarVisibility !== this.currentMenuBarVisibility) { this.currentMenuBarVisibility = newMenuBarVisibility; this.setMenuBarVisibility(newMenuBarVisibility); } // Proxy let newHttpProxy = (this.configurationService.getValue('http.proxy') || '').trim() || ( process.env['https_proxy'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['HTTP_PROXY'] || '' ).trim() || // Not standardized. undefined; if (newHttpProxy?.endsWith('/')) { newHttpProxy = newHttpProxy.substr(0, newHttpProxy.length - 1); } const newNoProxy = (process.env['no_proxy'] || process.env['NO_PROXY'] || '').trim() || undefined; // Not standardized. if ( (newHttpProxy || '').indexOf('@') === -1 && (newHttpProxy !== this.currentHttpProxy || newNoProxy !== this.currentNoProxy) ) { this.currentHttpProxy = newHttpProxy; this.currentNoProxy = newNoProxy; const proxyRules = newHttpProxy || ''; const proxyBypassRules = newNoProxy ? `${newNoProxy},<local>` : '<local>'; this.logService.trace(`Setting proxy to '${proxyRules}', bypassing '${proxyBypassRules}'`); this._win.webContents.session.setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); } } addTabbedWindow(window) { if (isMacintosh && window.win) { this._win.addTabbedWindow(window.win); } } load(configuration, options = Object.create(null)) { this.logService.trace(`window#load: attempt to load window (id: ${this._id})`); // Clear Document Edited if needed if (this.isDocumentEdited()) { if (!options.isReload || !this.backupMainService.isHotExitEnabled()) { this.setDocumentEdited(false); } } // Clear Title and Filename if needed if (!options.isReload) { if (this.getRepresentedFilename()) { this.setRepresentedFilename(''); } this._win.setTitle(this.productService.nameLong); } // Update configuration values based on our window context // and set it into the config object URL for usage. this.updateConfiguration(configuration, options); // If this is the first time the window is loaded, we associate the paths // directly with the window because we assume the loading will just work if (this.readyState === 0 /* ReadyState.NONE */) { this._config = configuration; } // Otherwise, the window is currently showing a folder and if there is an // unload handler preventing the load, we cannot just associate the paths // because the loading might be vetoed. Instead we associate it later when // the window load event has fired. else { this.pendingLoadConfig = configuration; } // Indicate we are navigting now this.readyState = 1 /* ReadyState.NAVIGATING */; // Load URL this._win.loadURL( FileAccess.asBrowserUri( `vs/code/electron-sandbox/workbench/workbench${this.environmentMainService.isBuilt ? '' : '-dev'}.html`, ).toString(true), ); // Remember that we did load const wasLoaded = this.wasLoaded; this.wasLoaded = true; // Make window visible if it did not open in N seconds because this indicates an error // Only do this when running out of sources and not when running tests if (!this.environmentMainService.isBuilt && !this.environmentMainService.extensionTestsLocationURI) { this._register( new RunOnceScheduler(() => { if (this._win && !this._win.isVisible() && !this._win.isMinimized()) { this._win.show(); this.focus({ force: true }); this._win.webContents.openDevTools(); } }, 10000), ).schedule(); } // Event this._onWillLoad.fire({ workspace: configuration.workspace, reason: options.isReload ? 3 /* LoadReason.RELOAD */ : wasLoaded ? 2 /* LoadReason.LOAD */ : 1 /* LoadReason.INITIAL */, }); } updateConfiguration(configuration, options) { // If this window was loaded before from the command line // (as indicated by VSCODE_CLI environment), make sure to // preserve that user environment in subsequent loads, // unless the new configuration context was also a CLI // (for https://github.com/microsoft/vscode/issues/108571) // Also, preserve the environment if we're loading from an // extension development host that had its environment set // (for https://github.com/microsoft/vscode/issues/123508) const currentUserEnv = (this._config ?? this.pendingLoadConfig)?.userEnv; if (currentUserEnv) { const shouldPreserveLaunchCliEnvironment = isLaunchedFromCli(currentUserEnv) && !isLaunchedFromCli(configuration.userEnv); const shouldPreserveDebugEnvironmnet = this.isExtensionDevelopmentHost; if (shouldPreserveLaunchCliEnvironment || shouldPreserveDebugEnvironmnet) { configuration.userEnv = { ...currentUserEnv, ...configuration.userEnv }; // still allow to override certain environment as passed in } } // If named pipe was instantiated for the crashpad_handler process, reuse the same // pipe for new app instances connecting to the original app instance. // Ref: https://github.com/microsoft/vscode/issues/115874 if (process.env['CHROME_CRASHPAD_PIPE_NAME']) { Object.assign(configuration.userEnv, { CHROME_CRASHPAD_PIPE_NAME: process.env['CHROME_CRASHPAD_PIPE_NAME'], }); } // Add disable-extensions to the config, but do not preserve it on currentConfig or // pendingLoadConfig so that it is applied only on this load if (options.disableExtensions !== undefined) { configuration['disable-extensions'] = options.disableExtensions; } // Update window related properties configuration.fullscreen = this.isFullScreen; configuration.maximized = this._win.isMaximized(); configuration.partsSplash = this.themeMainService.getWindowSplash(); // Update with latest perf marks mark('code/willOpenNewWindow'); configuration.perfMarks = getMarks(); // Update in config object URL for usage in renderer this.configObjectUrl.update(configuration); } async reload(cli) { // Copy our current config for reuse const configuration = Object.assign({}, this._config); // Validate workspace configuration.workspace = await this.validateWorkspaceBeforeReload(configuration); // Delete some properties we do not want during reload delete configuration.filesToOpenOrCreate; delete configuration.filesToDiff; delete configuration.filesToMerge; delete configuration.filesToWait; // Some configuration things get inherited if the window is being reloaded and we are // in extension development mode. These options are all development related. if (this.isExtensionDevelopmentHost && cli) { configuration.verbose = cli.verbose; configuration.debugId = cli.debugId; configuration.extensionEnvironment = cli.extensionEnvironment; configuration['inspect-extensions'] = cli['inspect-extensions']; configuration['inspect-brk-extensions'] = cli['inspect-brk-extensions']; configuration['extensions-dir'] = cli['extensions-dir']; } configuration.accessibilitySupport = app.isAccessibilitySupportEnabled(); configuration.isInitialStartup = false; // since this is a reload configuration.policiesData = this.policyService.serialize(); // set policies data again configuration.continueOn = this.environmentMainService.continueOn; configuration.profiles = { all: this.userDataProfilesService.profiles, profile: this.profile || this.userDataProfilesService.defaultProfile, }; configuration.logLevel = this.logService.getLevel(); // Load config this.load(configuration, { isReload: true, disableExtensions: cli?.['disable-extensions'] }); } async validateWorkspaceBeforeReload(configuration) { // Multi folder if (isWorkspaceIdentifier(configuration.workspace)) { const configPath = configuration.workspace.configPath; if (configPath.scheme === Schemas.file) { const workspaceExists = await this.fileService.exists(configPath); if (!workspaceExists) { return undefined; } } } // Single folder else if (isSingleFolderWorkspaceIdentifier(configuration.workspace)) { const uri = configuration.workspace.uri; if (uri.scheme === Schemas.file) { const folderExists = await this.fileService.exists(uri); if (!folderExists) { return undefined; } } } // Workspace is valid return configuration.workspace; } serializeWindowState() { if (!this._win) { return defaultWindowState(); } // fullscreen gets special treatment if (this.isFullScreen) { let display; try { display = screen.getDisplayMatching(this.getBounds()); } catch (error) { // Electron has weird conditions under which it throws errors // e.g. https://github.com/microsoft/vscode/issues/100334 when // large numbers are passed in } const defaultState = defaultWindowState(); const res = { mode: 3 /* WindowMode.Fullscreen */, display: display ? display.id : undefined, // Still carry over window dimensions from previous sessions // if we can compute it in fullscreen state. // does not seem possible in all cases on Linux for example // (https://github.com/microsoft/vscode/issues/58218) so we // fallback to the defaults in that case. width: this.windowState.width || defaultState.width, height: this.windowState.height || defaultState.height, x: this.windowState.x || 0, y: this.windowState.y || 0, }; return res; } const state = Object.create(null); let mode; // get window mode if (!isMacintosh && this._win.isMaximized()) { mode = 0 /* WindowMode.Maximized */; } else { mode = 1 /* WindowMode.Normal */; } // we don't want to save minimized state, only maximized or normal if (mode === 0 /* WindowMode.Maximized */) { state.mode = 0 /* WindowMode.Maximized */; } else { state.mode = 1 /* WindowMode.Normal */; } // only consider non-minimized window states if (mode === 1 /* WindowMode.Normal */ || mode === 0 /* WindowMode.Maximized */) { let bounds; if (mode === 1 /* WindowMode.Normal */) { bounds = this.getBounds(); } else { bounds = this._win.getNormalBounds(); // make sure to persist the normal bounds when maximized to be able to restore them } state.x = bounds.x; state.y = bounds.y; state.width = bounds.width; state.height = bounds.height; } return state; } updateWindowControls(options) { // Cache the height for speeds lookups on startup if (options.height) { this.stateMainService.setItem(CodeWindow.windowControlHeightStateStorageKey, options.height); } // Windows: window control overlay (WCO) if (isWindows && this.hasWindowControlOverlay) { this._win.setTitleBarOverlay({ color: options.backgroundColor?.trim() === '' ? undefined : options.backgroundColor, symbolColor: options.foregroundColor?.trim() === '' ? undefined : options.foregroundColor, height: options.height ? options.height - 1 : undefined, // account for window border }); } // macOS: traffic lights else if (isMacintosh && options.height !== undefined) { const verticalOffset = (options.height - 15) / 2; // 15px is the height of the traffic lights this._win.setTrafficLightPosition({ x: verticalOffset, y: verticalOffset }); } } restoreWindowState(state) { mark('code/willRestoreCodeWindowState'); let hasMultipleDisplays = false; if (state) { try { const displays = screen.getAllDisplays(); hasMultipleDisplays = displays.length > 1; state = this.validateWindowState(state, displays); } catch (err) { this.logService.warn(`Unexpected error validating window state: ${err}\n${err.stack}`); // somehow display API can be picky about the state to validate } } mark('code/didRestoreCodeWindowState'); return [state || defaultWindowState(), hasMultipleDisplays]; } validateWindowState(state, displays) { this.logService.trace( `window#validateWindowState: validating window state on ${displays.length} display(s)`, state, ); if ( typeof state.x !== 'number' || typeof state.y !== 'number' || typeof state.width !== 'number' || typeof state.height !== 'number' ) { this.logService.trace('window#validateWindowState: unexpected type of state values'); return undefined; } if (state.width <= 0 || state.height <= 0) { this.logService.trace('window#validateWindowState: unexpected negative values'); return undefined; } // Single Monitor: be strict about x/y positioning // macOS & Linux: these OS seem to be pretty good in ensuring that a window is never outside of it's bounds. // Windows: it is possible to have a window with a size that makes it fall out of the window. our strategy // is to try as much as possible to keep the window in the monitor bounds. we are not as strict as // macOS and Linux and allow the window to exceed the monitor bounds as long as the window is still // some pixels (128) visible on the screen for the user to drag it back. if (displays.length === 1) { const displayWorkingArea = this.getWorkingArea(displays[0]); if (displayWorkingArea) { this.logService.trace('window#validateWindowState: 1 monitor working area', displayWorkingArea); function ensureStateInDisplayWorkingArea() { if (!state || typeof state.x !== 'number' || typeof state.y !== 'number' || !displayWorkingArea) { return; } if (state.x < displayWorkingArea.x) { // prevent window from falling out of the screen to the left state.x = displayWorkingArea.x; } if (state.y < displayWorkingArea.y) { // prevent window from falling out of the screen to the top state.y = displayWorkingArea.y; } } // ensure state is not outside display working area (top, left) ensureStateInDisplayWorkingArea(); if (state.width > displayWorkingArea.width) { // prevent window from exceeding display bounds width state.width = displayWorkingArea.width; } if (state.height > displayWorkingArea.height) { // prevent window from exceeding display bounds height state.height = displayWorkingArea.height; } if (state.x > displayWorkingArea.x + displayWorkingArea.width - 128) { // prevent window from falling out of the screen to the right with // 128px margin by positioning the window to the far right edge of // the screen state.x = displayWorkingArea.x + displayWorkingArea.width - state.width; } if (state.y > displayWorkingArea.y + displayWorkingArea.height - 128) { // prevent window from falling out of the screen to the bottom with // 128px margin by positioning the window to the far bottom edge of // the screen state.y = displayWorkingArea.y + displayWorkingArea.height - state.height; } // again ensure state is not outside display working area // (it may have changed from the previous validation step) ensureStateInDisplayWorkingArea(); } return state; } // Multi Montior (fullscreen): try to find the previously used display if (state.display && state.mode === 3 /* WindowMode.Fullscreen */) { const display = displays.find((d) => d.id === state.display); if (display && typeof display.bounds?.x === 'number' && typeof display.bounds?.y === 'number') { this.logService.trace('window#validateWindowState: restoring fullscreen to previous display'); const defaults = defaultWindowState(3 /* WindowMode.Fullscreen */); // make sure we have good values when the user restores the window defaults.x = display.bounds.x; // carefull to use displays x/y position so that the window ends up on the correct monitor defaults.y = display.bounds.y; return defaults; } } // Multi Monitor (non-fullscreen): ensure window is within display bounds let display; let displayWorkingArea; try { display = screen.getDisplayMatching({ x: state.x, y: state.y, width: state.width, height: state.height }); displayWorkingArea = this.getWorkingArea(display); } catch (error) { // Electron has weird conditions under which it throws errors // e.g. https://github.com/microsoft/vscode/issues/100334 when // large numbers are passed in } if ( display && // we have a display matching the desired bounds displayWorkingArea && // we have valid working area bounds state.x + state.width > displayWorkingArea.x && // prevent window from falling out of the screen to the left state.y + state.height > displayWorkingArea.y && // prevent windo