@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
1,253 lines • 59.1 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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