mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-21 02:11:11 +00:00
1527 lines
69 KiB
TypeScript
1527 lines
69 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { app, powerMonitor, protocol, session, Session, systemPreferences, WebFrameMain } from 'electron';
|
|
import { addUNCHostToAllowlist, disableUNCAccessRestrictions } from '../../base/node/unc.js';
|
|
import { validatedIpcMain } from '../../base/parts/ipc/electron-main/ipcMain.js';
|
|
import { hostname, release } from 'os';
|
|
import { VSBuffer } from '../../base/common/buffer.js';
|
|
import { toErrorMessage } from '../../base/common/errorMessage.js';
|
|
import { Event } from '../../base/common/event.js';
|
|
import { parse } from '../../base/common/jsonc.js';
|
|
import { getPathLabel } from '../../base/common/labels.js';
|
|
import { Disposable, DisposableStore } from '../../base/common/lifecycle.js';
|
|
import { Schemas, VSCODE_AUTHORITY } from '../../base/common/network.js';
|
|
import { join, posix } from '../../base/common/path.js';
|
|
import { IProcessEnvironment, isLinux, isLinuxSnap, isMacintosh, isWindows, OS } from '../../base/common/platform.js';
|
|
import { assertType } from '../../base/common/types.js';
|
|
import { URI } from '../../base/common/uri.js';
|
|
import { generateUuid } from '../../base/common/uuid.js';
|
|
import { registerContextMenuListener } from '../../base/parts/contextmenu/electron-main/contextmenu.js';
|
|
import { getDelayedChannel, ProxyChannel, StaticRouter } from '../../base/parts/ipc/common/ipc.js';
|
|
import { Server as ElectronIPCServer } from '../../base/parts/ipc/electron-main/ipc.electron.js';
|
|
import { Client as MessagePortClient } from '../../base/parts/ipc/electron-main/ipc.mp.js';
|
|
import { Server as NodeIPCServer } from '../../base/parts/ipc/node/ipc.net.js';
|
|
import { IProxyAuthService, ProxyAuthService } from '../../platform/native/electron-main/auth.js';
|
|
import { localize } from '../../nls.js';
|
|
import { IBackupMainService } from '../../platform/backup/electron-main/backup.js';
|
|
import { BackupMainService } from '../../platform/backup/electron-main/backupMainService.js';
|
|
import { IConfigurationService } from '../../platform/configuration/common/configuration.js';
|
|
import { ElectronExtensionHostDebugBroadcastChannel } from '../../platform/debug/electron-main/extensionHostDebugIpc.js';
|
|
import { IDiagnosticsService } from '../../platform/diagnostics/common/diagnostics.js';
|
|
import { DiagnosticsMainService, IDiagnosticsMainService } from '../../platform/diagnostics/electron-main/diagnosticsMainService.js';
|
|
import { DialogMainService, IDialogMainService } from '../../platform/dialogs/electron-main/dialogMainService.js';
|
|
import { IEncryptionMainService } from '../../platform/encryption/common/encryptionService.js';
|
|
import { EncryptionMainService } from '../../platform/encryption/electron-main/encryptionMainService.js';
|
|
import { NativeBrowserElementsMainService, INativeBrowserElementsMainService } from '../../platform/browserElements/electron-main/nativeBrowserElementsMainService.js';
|
|
import { ipcBrowserViewChannelName } from '../../platform/browserView/common/browserView.js';
|
|
import { BrowserViewMainService, IBrowserViewMainService } from '../../platform/browserView/electron-main/browserViewMainService.js';
|
|
import { NativeParsedArgs } from '../../platform/environment/common/argv.js';
|
|
import { IEnvironmentMainService } from '../../platform/environment/electron-main/environmentMainService.js';
|
|
import { isLaunchedFromCli } from '../../platform/environment/node/argvHelper.js';
|
|
import { getResolvedShellEnv } from '../../platform/shell/node/shellEnv.js';
|
|
import { IExtensionHostStarter, ipcExtensionHostStarterChannelName } from '../../platform/extensions/common/extensionHostStarter.js';
|
|
import { ExtensionHostStarter } from '../../platform/extensions/electron-main/extensionHostStarter.js';
|
|
import { IExternalTerminalMainService } from '../../platform/externalTerminal/electron-main/externalTerminal.js';
|
|
import { LinuxExternalTerminalService, MacExternalTerminalService, WindowsExternalTerminalService } from '../../platform/externalTerminal/node/externalTerminalService.js';
|
|
import { LOCAL_FILE_SYSTEM_CHANNEL_NAME } from '../../platform/files/common/diskFileSystemProviderClient.js';
|
|
import { IFileService } from '../../platform/files/common/files.js';
|
|
import { DiskFileSystemProviderChannel } from '../../platform/files/electron-main/diskFileSystemProviderServer.js';
|
|
import { DiskFileSystemProvider } from '../../platform/files/node/diskFileSystemProvider.js';
|
|
import { SyncDescriptor } from '../../platform/instantiation/common/descriptors.js';
|
|
import { IInstantiationService, ServicesAccessor } from '../../platform/instantiation/common/instantiation.js';
|
|
import { ServiceCollection } from '../../platform/instantiation/common/serviceCollection.js';
|
|
import { ProcessMainService } from '../../platform/process/electron-main/processMainService.js';
|
|
import { IKeyboardLayoutMainService, KeyboardLayoutMainService } from '../../platform/keyboardLayout/electron-main/keyboardLayoutMainService.js';
|
|
import { ILaunchMainService, LaunchMainService } from '../../platform/launch/electron-main/launchMainService.js';
|
|
import { ILifecycleMainService, LifecycleMainPhase, ShutdownReason } from '../../platform/lifecycle/electron-main/lifecycleMainService.js';
|
|
import { ILoggerService, ILogService } from '../../platform/log/common/log.js';
|
|
import { IMenubarMainService, MenubarMainService } from '../../platform/menubar/electron-main/menubarMainService.js';
|
|
import { INativeHostMainService, NativeHostMainService } from '../../platform/native/electron-main/nativeHostMainService.js';
|
|
import { IProductService } from '../../platform/product/common/productService.js';
|
|
import { getRemoteAuthority } from '../../platform/remote/common/remoteHosts.js';
|
|
import { SharedProcess } from '../../platform/sharedProcess/electron-main/sharedProcess.js';
|
|
import { ISignService } from '../../platform/sign/common/sign.js';
|
|
import { IStateService } from '../../platform/state/node/state.js';
|
|
import { StorageDatabaseChannel } from '../../platform/storage/electron-main/storageIpc.js';
|
|
import { ApplicationStorageMainService, IApplicationStorageMainService, IStorageMainService, StorageMainService } from '../../platform/storage/electron-main/storageMainService.js';
|
|
import { resolveCommonProperties } from '../../platform/telemetry/common/commonProperties.js';
|
|
import { ITelemetryService, TelemetryLevel } from '../../platform/telemetry/common/telemetry.js';
|
|
import { TelemetryAppenderClient } from '../../platform/telemetry/common/telemetryIpc.js';
|
|
import { ITelemetryServiceConfig, TelemetryService } from '../../platform/telemetry/common/telemetryService.js';
|
|
import { getPiiPathsFromEnvironment, getTelemetryLevel, isInternalTelemetry, NullTelemetryService, supportsTelemetry } from '../../platform/telemetry/common/telemetryUtils.js';
|
|
import { IUpdateService } from '../../platform/update/common/update.js';
|
|
import { UpdateChannel } from '../../platform/update/common/updateIpc.js';
|
|
import { DarwinUpdateService } from '../../platform/update/electron-main/updateService.darwin.js';
|
|
import { LinuxUpdateService } from '../../platform/update/electron-main/updateService.linux.js';
|
|
import { SnapUpdateService } from '../../platform/update/electron-main/updateService.snap.js';
|
|
import { Win32UpdateService } from '../../platform/update/electron-main/updateService.win32.js';
|
|
import { IOpenURLOptions, IURLService } from '../../platform/url/common/url.js';
|
|
import { URLHandlerChannelClient, URLHandlerRouter } from '../../platform/url/common/urlIpc.js';
|
|
import { NativeURLService } from '../../platform/url/common/urlService.js';
|
|
import { ElectronURLListener } from '../../platform/url/electron-main/electronUrlListener.js';
|
|
import { IWebviewManagerService } from '../../platform/webview/common/webviewManagerService.js';
|
|
import { WebviewMainService } from '../../platform/webview/electron-main/webviewMainService.js';
|
|
import { isFolderToOpen, isWorkspaceToOpen, IWindowOpenable } from '../../platform/window/common/window.js';
|
|
import { getAllWindowsExcludingOffscreen, IWindowsMainService, OpenContext } from '../../platform/windows/electron-main/windows.js';
|
|
import { ICodeWindow } from '../../platform/window/electron-main/window.js';
|
|
import { WindowsMainService } from '../../platform/windows/electron-main/windowsMainService.js';
|
|
import { ActiveWindowManager } from '../../platform/windows/node/windowTracker.js';
|
|
import { hasWorkspaceFileExtension } from '../../platform/workspace/common/workspace.js';
|
|
import { IWorkspacesService } from '../../platform/workspaces/common/workspaces.js';
|
|
import { IWorkspacesHistoryMainService, WorkspacesHistoryMainService } from '../../platform/workspaces/electron-main/workspacesHistoryMainService.js';
|
|
import { WorkspacesMainService } from '../../platform/workspaces/electron-main/workspacesMainService.js';
|
|
import { IWorkspacesManagementMainService, WorkspacesManagementMainService } from '../../platform/workspaces/electron-main/workspacesManagementMainService.js';
|
|
import { IPolicyService } from '../../platform/policy/common/policy.js';
|
|
import { PolicyChannel } from '../../platform/policy/common/policyIpc.js';
|
|
import { IUserDataProfilesMainService } from '../../platform/userDataProfile/electron-main/userDataProfile.js';
|
|
import { IExtensionsProfileScannerService } from '../../platform/extensionManagement/common/extensionsProfileScannerService.js';
|
|
import { IExtensionsScannerService } from '../../platform/extensionManagement/common/extensionsScannerService.js';
|
|
import { ExtensionsScannerService } from '../../platform/extensionManagement/node/extensionsScannerService.js';
|
|
import { UserDataProfilesHandler } from '../../platform/userDataProfile/electron-main/userDataProfilesHandler.js';
|
|
import { ProfileStorageChangesListenerChannel } from '../../platform/userDataProfile/electron-main/userDataProfileStorageIpc.js';
|
|
import { Promises, RunOnceScheduler, runWhenGlobalIdle } from '../../base/common/async.js';
|
|
import { resolveMachineId, resolveSqmId, resolveDevDeviceId, validateDevDeviceId } from '../../platform/telemetry/electron-main/telemetryUtils.js';
|
|
import { ExtensionsProfileScannerService } from '../../platform/extensionManagement/node/extensionsProfileScannerService.js';
|
|
import { LoggerChannel } from '../../platform/log/electron-main/logIpc.js';
|
|
import { ILoggerMainService } from '../../platform/log/electron-main/loggerService.js';
|
|
import { IInitialProtocolUrls, IProtocolUrl } from '../../platform/url/electron-main/url.js';
|
|
import { IUtilityProcessWorkerMainService, UtilityProcessWorkerMainService } from '../../platform/utilityProcess/electron-main/utilityProcessWorkerMainService.js';
|
|
import { ipcUtilityProcessWorkerChannelName } from '../../platform/utilityProcess/common/utilityProcessWorkerService.js';
|
|
import { ILocalPtyService, LocalReconnectConstants, TerminalIpcChannels, TerminalSettingId } from '../../platform/terminal/common/terminal.js';
|
|
import { ElectronPtyHostStarter } from '../../platform/terminal/electron-main/electronPtyHostStarter.js';
|
|
import { PtyHostService } from '../../platform/terminal/node/ptyHostService.js';
|
|
import { NODE_REMOTE_RESOURCE_CHANNEL_NAME, NODE_REMOTE_RESOURCE_IPC_METHOD_NAME, NodeRemoteResourceResponse, NodeRemoteResourceRouter } from '../../platform/remote/common/electronRemoteResources.js';
|
|
import { Lazy } from '../../base/common/lazy.js';
|
|
import { IAuxiliaryWindowsMainService } from '../../platform/auxiliaryWindow/electron-main/auxiliaryWindows.js';
|
|
import { AuxiliaryWindowsMainService } from '../../platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.js';
|
|
import { normalizeNFC } from '../../base/common/normalization.js';
|
|
import { ICSSDevelopmentService, CSSDevelopmentService } from '../../platform/cssDev/node/cssDevService.js';
|
|
import { INativeMcpDiscoveryHelperService, NativeMcpDiscoveryHelperChannelName } from '../../platform/mcp/common/nativeMcpDiscoveryHelper.js';
|
|
import { NativeMcpDiscoveryHelperService } from '../../platform/mcp/node/nativeMcpDiscoveryHelperService.js';
|
|
import { IMcpGatewayService, McpGatewayChannelName } from '../../platform/mcp/common/mcpGateway.js';
|
|
import { McpGatewayService } from '../../platform/mcp/node/mcpGatewayService.js';
|
|
import { McpGatewayChannel } from '../../platform/mcp/node/mcpGatewayChannel.js';
|
|
import { IWebContentExtractorService } from '../../platform/webContentExtractor/common/webContentExtractor.js';
|
|
import { NativeWebContentExtractorService } from '../../platform/webContentExtractor/electron-main/webContentExtractorService.js';
|
|
import ErrorTelemetry from '../../platform/telemetry/electron-main/errorTelemetry.js';
|
|
|
|
/**
|
|
* The main VS Code application. There will only ever be one instance,
|
|
* even if the user starts many instances (e.g. from the command line).
|
|
*/
|
|
export class CodeApplication extends Disposable {
|
|
|
|
private static readonly SECURITY_PROTOCOL_HANDLING_CONFIRMATION_SETTING_KEY = {
|
|
[Schemas.file]: 'security.promptForLocalFileProtocolHandling' as const,
|
|
[Schemas.vscodeRemote]: 'security.promptForRemoteFileProtocolHandling' as const
|
|
};
|
|
|
|
private windowsMainService: IWindowsMainService | undefined;
|
|
private auxiliaryWindowsMainService: IAuxiliaryWindowsMainService | undefined;
|
|
private nativeHostMainService: INativeHostMainService | undefined;
|
|
|
|
constructor(
|
|
private readonly mainProcessNodeIpcServer: NodeIPCServer,
|
|
private readonly userEnv: IProcessEnvironment,
|
|
@IInstantiationService private readonly mainInstantiationService: IInstantiationService,
|
|
@ILogService private readonly logService: ILogService,
|
|
@ILoggerService private readonly loggerService: ILoggerService,
|
|
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,
|
|
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
|
|
@IConfigurationService private readonly configurationService: IConfigurationService,
|
|
@IStateService private readonly stateService: IStateService,
|
|
@IFileService private readonly fileService: IFileService,
|
|
@IProductService private readonly productService: IProductService,
|
|
@IUserDataProfilesMainService private readonly userDataProfilesMainService: IUserDataProfilesMainService
|
|
) {
|
|
super();
|
|
|
|
this.configureSession();
|
|
this.registerListeners();
|
|
}
|
|
|
|
private configureSession(): void {
|
|
|
|
//#region Security related measures (https://electronjs.org/docs/tutorial/security)
|
|
//
|
|
// !!! DO NOT CHANGE without consulting the documentation !!!
|
|
//
|
|
|
|
const isUrlFromWindow = (requestingUrl?: string | undefined) => requestingUrl?.startsWith(`${Schemas.vscodeFileResource}://${VSCODE_AUTHORITY}`);
|
|
const isUrlFromWebview = (requestingUrl: string | undefined) => requestingUrl?.startsWith(`${Schemas.vscodeWebview}://`);
|
|
|
|
const alwaysAllowedPermissions = new Set(['pointerLock', 'notifications']);
|
|
|
|
const allowedPermissionsInWebview = new Set([
|
|
...alwaysAllowedPermissions,
|
|
'clipboard-read',
|
|
'clipboard-sanitized-write',
|
|
// TODO(deepak1556): Should be removed once migration is complete
|
|
// https://github.com/microsoft/vscode/issues/239228
|
|
'deprecated-sync-clipboard-read',
|
|
]);
|
|
|
|
const allowedPermissionsInCore = new Set([
|
|
...alwaysAllowedPermissions,
|
|
'media',
|
|
'local-fonts',
|
|
// TODO(deepak1556): Should be removed once migration is complete
|
|
// https://github.com/microsoft/vscode/issues/239228
|
|
'deprecated-sync-clipboard-read',
|
|
]);
|
|
|
|
session.defaultSession.setPermissionRequestHandler((_webContents, permission, callback, details) => {
|
|
if (isUrlFromWebview(details.requestingUrl)) {
|
|
return callback(allowedPermissionsInWebview.has(permission));
|
|
}
|
|
if (isUrlFromWindow(details.requestingUrl)) {
|
|
return callback(allowedPermissionsInCore.has(permission));
|
|
}
|
|
return callback(false);
|
|
});
|
|
|
|
session.defaultSession.setPermissionCheckHandler((_webContents, permission, _origin, details) => {
|
|
if (isUrlFromWebview(details.requestingUrl)) {
|
|
return allowedPermissionsInWebview.has(permission);
|
|
}
|
|
if (isUrlFromWindow(details.requestingUrl)) {
|
|
return allowedPermissionsInCore.has(permission);
|
|
}
|
|
return false;
|
|
});
|
|
|
|
//#endregion
|
|
|
|
//#region Request filtering
|
|
|
|
// Block all SVG requests from unsupported origins
|
|
const supportedSvgSchemes = new Set([Schemas.file, Schemas.vscodeFileResource, Schemas.vscodeRemoteResource, Schemas.vscodeManagedRemoteResource, 'devtools']);
|
|
|
|
// But allow them if they are made from inside an webview
|
|
const isSafeFrame = (requestFrame: WebFrameMain | null | undefined): boolean => {
|
|
for (let frame: WebFrameMain | null | undefined = requestFrame; frame; frame = frame.parent) {
|
|
if (frame.url.startsWith(`${Schemas.vscodeWebview}://`)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const isSvgRequestFromSafeContext = (details: Electron.OnBeforeRequestListenerDetails | Electron.OnHeadersReceivedListenerDetails): boolean => {
|
|
return details.resourceType === 'xhr' || isSafeFrame(details.frame);
|
|
};
|
|
|
|
const isAllowedVsCodeFileRequest = (details: Electron.OnBeforeRequestListenerDetails) => {
|
|
const frame = details.frame;
|
|
if (!frame || !this.windowsMainService) {
|
|
return false;
|
|
}
|
|
|
|
// Check to see if the request comes from one of the main windows (or shared process) and not from embedded content
|
|
const windows = getAllWindowsExcludingOffscreen();
|
|
for (const window of windows) {
|
|
if (frame.processId === window.webContents.mainFrame.processId) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
const isAllowedWebviewRequest = (uri: URI, details: Electron.OnBeforeRequestListenerDetails): boolean => {
|
|
if (uri.path !== '/index.html') {
|
|
return true; // Only restrict top level page of webviews: index.html
|
|
}
|
|
|
|
const frame = details.frame;
|
|
if (!frame || !this.windowsMainService) {
|
|
return false;
|
|
}
|
|
|
|
// Check to see if the request comes from one of the main editor windows.
|
|
for (const window of this.windowsMainService.getWindows()) {
|
|
if (window.win) {
|
|
if (frame.processId === window.win.webContents.mainFrame.processId) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
session.defaultSession.webRequest.onBeforeRequest((details, callback) => {
|
|
const uri = URI.parse(details.url);
|
|
if (uri.scheme === Schemas.vscodeWebview) {
|
|
if (!isAllowedWebviewRequest(uri, details)) {
|
|
this.logService.error('Blocked vscode-webview request', details.url);
|
|
return callback({ cancel: true });
|
|
}
|
|
}
|
|
|
|
if (uri.scheme === Schemas.vscodeFileResource) {
|
|
if (!isAllowedVsCodeFileRequest(details)) {
|
|
this.logService.error('Blocked vscode-file request', details.url);
|
|
return callback({ cancel: true });
|
|
}
|
|
}
|
|
|
|
// Block most svgs
|
|
if (uri.path.endsWith('.svg')) {
|
|
const isSafeResourceUrl = supportedSvgSchemes.has(uri.scheme);
|
|
if (!isSafeResourceUrl) {
|
|
return callback({ cancel: !isSvgRequestFromSafeContext(details) });
|
|
}
|
|
}
|
|
|
|
return callback({ cancel: false });
|
|
});
|
|
|
|
// Configure SVG header content type properly
|
|
// https://github.com/microsoft/vscode/issues/97564
|
|
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
|
const responseHeaders = details.responseHeaders as Record<string, (string) | (string[])>;
|
|
const contentTypes = (responseHeaders['content-type'] || responseHeaders['Content-Type']);
|
|
|
|
if (contentTypes && Array.isArray(contentTypes)) {
|
|
const uri = URI.parse(details.url);
|
|
if (uri.path.endsWith('.svg')) {
|
|
if (supportedSvgSchemes.has(uri.scheme)) {
|
|
responseHeaders['Content-Type'] = ['image/svg+xml'];
|
|
|
|
return callback({ cancel: false, responseHeaders });
|
|
}
|
|
}
|
|
|
|
// remote extension schemes have the following format
|
|
// http://127.0.0.1:<port>/vscode-remote-resource?path=
|
|
if (!uri.path.endsWith(Schemas.vscodeRemoteResource) && contentTypes.some(contentType => contentType.toLowerCase().includes('image/svg'))) {
|
|
return callback({ cancel: !isSvgRequestFromSafeContext(details) });
|
|
}
|
|
}
|
|
|
|
return callback({ cancel: false });
|
|
});
|
|
|
|
//#endregion
|
|
|
|
//#region Allow CORS for the PRSS CDN
|
|
|
|
// https://github.com/microsoft/vscode-remote-release/issues/9246
|
|
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
|
if (details.url.startsWith('https://vscode.download.prss.microsoft.com/')) {
|
|
const responseHeaders = details.responseHeaders ?? Object.create(null);
|
|
|
|
if (responseHeaders['Access-Control-Allow-Origin'] === undefined) {
|
|
responseHeaders['Access-Control-Allow-Origin'] = ['*'];
|
|
return callback({ cancel: false, responseHeaders });
|
|
}
|
|
}
|
|
|
|
return callback({ cancel: false });
|
|
});
|
|
|
|
//#endregion
|
|
|
|
//#region Code Cache
|
|
|
|
type SessionWithCodeCachePathSupport = Session & {
|
|
/**
|
|
* Sets code cache directory. By default, the directory will be `Code Cache` under
|
|
* the respective user data folder.
|
|
*/
|
|
setCodeCachePath?(path: string): void;
|
|
};
|
|
|
|
const defaultSession = session.defaultSession as unknown as SessionWithCodeCachePathSupport;
|
|
if (typeof defaultSession.setCodeCachePath === 'function' && this.environmentMainService.codeCachePath) {
|
|
// Make sure to partition Chrome's code cache folder
|
|
// in the same way as our code cache path to help
|
|
// invalidate caches that we know are invalid
|
|
// (https://github.com/microsoft/vscode/issues/120655)
|
|
defaultSession.setCodeCachePath(join(this.environmentMainService.codeCachePath, 'chrome'));
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region UNC Host Allowlist (Windows)
|
|
|
|
if (isWindows) {
|
|
if (this.configurationService.getValue('security.restrictUNCAccess') === false) {
|
|
disableUNCAccessRestrictions();
|
|
} else {
|
|
addUNCHostToAllowlist(this.configurationService.getValue('security.allowedUNCHosts'));
|
|
}
|
|
}
|
|
|
|
//#endregion
|
|
}
|
|
|
|
private registerListeners(): void {
|
|
|
|
// Dispose on shutdown
|
|
Event.once(this.lifecycleMainService.onWillShutdown)(() => this.dispose());
|
|
|
|
// Contextmenu via IPC support
|
|
registerContextMenuListener();
|
|
|
|
// Accessibility change event
|
|
app.on('accessibility-support-changed', (event, accessibilitySupportEnabled) => {
|
|
this.windowsMainService?.sendToAll('vscode:accessibilitySupportChanged', accessibilitySupportEnabled);
|
|
});
|
|
|
|
// macOS dock activate
|
|
app.on('activate', async (event, hasVisibleWindows) => {
|
|
this.logService.trace('app#activate');
|
|
|
|
// Mac only event: open new window when we get activated
|
|
if (!hasVisibleWindows) {
|
|
await this.windowsMainService?.openEmptyWindow({ context: OpenContext.DOCK });
|
|
}
|
|
});
|
|
|
|
//#region Security related measures (https://electronjs.org/docs/tutorial/security)
|
|
//
|
|
// !!! DO NOT CHANGE without consulting the documentation !!!
|
|
//
|
|
app.on('web-contents-created', (event, contents) => {
|
|
|
|
// Auxiliary Window: delegate to `AuxiliaryWindow` class
|
|
if (contents?.opener?.url.startsWith(`${Schemas.vscodeFileResource}://${VSCODE_AUTHORITY}/`)) {
|
|
this.logService.trace('[aux window] app.on("web-contents-created"): Registering auxiliary window');
|
|
|
|
this.auxiliaryWindowsMainService?.registerWindow(contents);
|
|
}
|
|
|
|
// Handle any in-page navigation
|
|
contents.on('will-navigate', event => {
|
|
if (BrowserViewMainService.isBrowserViewWebContents(contents)) {
|
|
return; // Allow navigation in integrated browser views
|
|
}
|
|
|
|
this.logService.error('webContents#will-navigate: Prevented webcontent navigation');
|
|
|
|
event.preventDefault(); // Prevent any in-page navigation
|
|
});
|
|
|
|
// All Windows: only allow about:blank auxiliary windows to open
|
|
// For all other URLs, delegate to the OS.
|
|
contents.setWindowOpenHandler(details => {
|
|
|
|
// about:blank windows can open as window witho our default options
|
|
if (details.url === 'about:blank') {
|
|
this.logService.trace('[aux window] webContents#setWindowOpenHandler: Allowing auxiliary window to open on about:blank');
|
|
|
|
return {
|
|
action: 'allow',
|
|
overrideBrowserWindowOptions: this.auxiliaryWindowsMainService?.createWindow(details)
|
|
};
|
|
}
|
|
|
|
// Any other URL: delegate to OS
|
|
else {
|
|
this.logService.trace(`webContents#setWindowOpenHandler: Prevented opening window with URL ${details.url}}`);
|
|
|
|
this.nativeHostMainService?.openExternal(undefined, details.url);
|
|
|
|
return { action: 'deny' };
|
|
}
|
|
});
|
|
});
|
|
|
|
//#endregion
|
|
|
|
let macOpenFileURIs: IWindowOpenable[] = [];
|
|
let runningTimeout: Timeout | undefined = undefined;
|
|
app.on('open-file', (event, path) => {
|
|
path = normalizeNFC(path); // macOS only: normalize paths to NFC form
|
|
|
|
this.logService.trace('app#open-file: ', path);
|
|
event.preventDefault();
|
|
|
|
// Keep in array because more might come!
|
|
macOpenFileURIs.push(hasWorkspaceFileExtension(path) ? { workspaceUri: URI.file(path) } : { fileUri: URI.file(path) });
|
|
|
|
// Clear previous handler if any
|
|
if (runningTimeout !== undefined) {
|
|
clearTimeout(runningTimeout);
|
|
runningTimeout = undefined;
|
|
}
|
|
|
|
// Handle paths delayed in case more are coming!
|
|
runningTimeout = setTimeout(async () => {
|
|
await this.windowsMainService?.open({
|
|
context: OpenContext.DOCK /* can also be opening from finder while app is running */,
|
|
cli: this.environmentMainService.args,
|
|
urisToOpen: macOpenFileURIs,
|
|
gotoLineMode: false,
|
|
preferNewWindow: true /* dropping on the dock or opening from finder prefers to open in a new window */
|
|
});
|
|
|
|
macOpenFileURIs = [];
|
|
runningTimeout = undefined;
|
|
}, 100);
|
|
});
|
|
|
|
app.on('new-window-for-tab', async () => {
|
|
await this.windowsMainService?.openEmptyWindow({ context: OpenContext.DESKTOP }); //macOS native tab "+" button
|
|
});
|
|
|
|
//#region Bootstrap IPC Handlers
|
|
|
|
validatedIpcMain.handle('vscode:fetchShellEnv', event => {
|
|
|
|
// Prefer to use the args and env from the target window
|
|
// when resolving the shell env. It is possible that
|
|
// a first window was opened from the UI but a second
|
|
// from the CLI and that has implications for whether to
|
|
// resolve the shell environment or not.
|
|
//
|
|
// Window can be undefined for e.g. the shared process
|
|
// that is not part of our windows registry!
|
|
const window = this.windowsMainService?.getWindowByWebContents(event.sender); // Note: this can be `undefined` for the shared process
|
|
let args: NativeParsedArgs;
|
|
let env: IProcessEnvironment;
|
|
if (window?.config) {
|
|
args = window.config;
|
|
env = { ...process.env, ...window.config.userEnv };
|
|
} else {
|
|
args = this.environmentMainService.args;
|
|
env = process.env;
|
|
}
|
|
|
|
// Resolve shell env
|
|
return this.resolveShellEnvironment(args, env, false);
|
|
});
|
|
|
|
validatedIpcMain.on('vscode:toggleDevTools', event => event.sender.toggleDevTools());
|
|
validatedIpcMain.on('vscode:openDevTools', event => event.sender.openDevTools());
|
|
|
|
validatedIpcMain.on('vscode:reloadWindow', event => event.sender.reload());
|
|
|
|
validatedIpcMain.handle('vscode:notifyZoomLevel', async (event, zoomLevel: number | undefined) => {
|
|
const window = this.windowsMainService?.getWindowByWebContents(event.sender);
|
|
if (window) {
|
|
window.notifyZoomLevel(zoomLevel);
|
|
}
|
|
});
|
|
|
|
//#endregion
|
|
}
|
|
|
|
async startup(): Promise<void> {
|
|
this.logService.debug('Starting VS Code');
|
|
this.logService.debug(`from: ${this.environmentMainService.appRoot}`);
|
|
this.logService.debug('args:', this.environmentMainService.args);
|
|
|
|
// Make sure we associate the program with the app user model id
|
|
// This will help Windows to associate the running program with
|
|
// any shortcut that is pinned to the taskbar and prevent showing
|
|
// two icons in the taskbar for the same app.
|
|
const win32AppUserModelId = this.productService.win32AppUserModelId;
|
|
if (isWindows && win32AppUserModelId) {
|
|
app.setAppUserModelId(win32AppUserModelId);
|
|
}
|
|
|
|
// Fix native tabs on macOS 10.13
|
|
// macOS enables a compatibility patch for any bundle ID beginning with
|
|
// "com.microsoft.", which breaks native tabs for VS Code when using this
|
|
// identifier (from the official build).
|
|
// Explicitly opt out of the patch here before creating any windows.
|
|
// See: https://github.com/microsoft/vscode/issues/35361#issuecomment-399794085
|
|
try {
|
|
if (isMacintosh && this.configurationService.getValue('window.nativeTabs') === true && !systemPreferences.getUserDefault('NSUseImprovedLayoutPass', 'boolean')) {
|
|
systemPreferences.setUserDefault('NSUseImprovedLayoutPass', 'boolean', true);
|
|
}
|
|
} catch (error) {
|
|
this.logService.error(error);
|
|
}
|
|
|
|
// Main process server (electron IPC based)
|
|
const mainProcessElectronServer = new ElectronIPCServer();
|
|
Event.once(this.lifecycleMainService.onWillShutdown)(e => {
|
|
if (e.reason === ShutdownReason.KILL) {
|
|
// When we go down abnormally, make sure to free up
|
|
// any IPC we accept from other windows to reduce
|
|
// the chance of doing work after we go down. Kill
|
|
// is special in that it does not orderly shutdown
|
|
// windows.
|
|
mainProcessElectronServer.dispose();
|
|
}
|
|
});
|
|
|
|
// Resolve unique machine ID
|
|
const [machineId, sqmId, devDeviceId] = await Promise.all([
|
|
resolveMachineId(this.stateService, this.logService),
|
|
resolveSqmId(this.stateService, this.logService),
|
|
resolveDevDeviceId(this.stateService, this.logService)
|
|
]);
|
|
|
|
// Shared process
|
|
const { sharedProcessReady, sharedProcessClient } = this.setupSharedProcess(machineId, sqmId, devDeviceId);
|
|
|
|
// Services
|
|
const appInstantiationService = await this.initServices(machineId, sqmId, devDeviceId, sharedProcessReady);
|
|
|
|
// Error telemetry
|
|
appInstantiationService.invokeFunction(accessor => this._register(new ErrorTelemetry(accessor.get(ILogService), accessor.get(ITelemetryService))));
|
|
|
|
// Auth Handler
|
|
appInstantiationService.invokeFunction(accessor => accessor.get(IProxyAuthService));
|
|
|
|
// Transient profiles handler
|
|
this._register(appInstantiationService.createInstance(UserDataProfilesHandler));
|
|
|
|
// Init Channels
|
|
appInstantiationService.invokeFunction(accessor => this.initChannels(accessor, mainProcessElectronServer, sharedProcessClient));
|
|
|
|
// Setup Protocol URL Handlers
|
|
const initialProtocolUrls = await appInstantiationService.invokeFunction(accessor => this.setupProtocolUrlHandlers(accessor, mainProcessElectronServer));
|
|
|
|
// Setup vscode-remote-resource protocol handler
|
|
this.setupManagedRemoteResourceUrlHandler(mainProcessElectronServer);
|
|
|
|
// Signal phase: ready - before opening first window
|
|
this.lifecycleMainService.phase = LifecycleMainPhase.Ready;
|
|
|
|
// Open Windows
|
|
await appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, initialProtocolUrls));
|
|
|
|
// Signal phase: after window open
|
|
this.lifecycleMainService.phase = LifecycleMainPhase.AfterWindowOpen;
|
|
|
|
// Post Open Windows Tasks
|
|
this.afterWindowOpen(appInstantiationService);
|
|
|
|
// Set lifecycle phase to `Eventually` after a short delay and when idle (min 2.5sec, max 5sec)
|
|
const eventuallyPhaseScheduler = this._register(new RunOnceScheduler(() => {
|
|
this._register(runWhenGlobalIdle(() => {
|
|
|
|
// Signal phase: eventually
|
|
this.lifecycleMainService.phase = LifecycleMainPhase.Eventually;
|
|
|
|
// Eventually Post Open Window Tasks
|
|
this.eventuallyAfterWindowOpen();
|
|
}, 2500));
|
|
}, 2500));
|
|
eventuallyPhaseScheduler.schedule();
|
|
}
|
|
|
|
private async setupProtocolUrlHandlers(accessor: ServicesAccessor, mainProcessElectronServer: ElectronIPCServer): Promise<IInitialProtocolUrls | undefined> {
|
|
const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService);
|
|
const urlService = accessor.get(IURLService);
|
|
const nativeHostMainService = this.nativeHostMainService = accessor.get(INativeHostMainService);
|
|
const dialogMainService = accessor.get(IDialogMainService);
|
|
|
|
// Install URL handlers that deal with protocl URLs either
|
|
// from this process by opening windows and/or by forwarding
|
|
// the URLs into a window process to be handled there.
|
|
|
|
const app = this;
|
|
urlService.registerHandler({
|
|
async handleURL(uri: URI, options?: IOpenURLOptions): Promise<boolean> {
|
|
return app.handleProtocolUrl(windowsMainService, dialogMainService, urlService, uri, options);
|
|
}
|
|
});
|
|
|
|
const activeWindowManager = this._register(new ActiveWindowManager({
|
|
onDidOpenMainWindow: nativeHostMainService.onDidOpenMainWindow,
|
|
onDidFocusMainWindow: nativeHostMainService.onDidFocusMainWindow,
|
|
getActiveWindowId: () => nativeHostMainService.getActiveWindowId(-1)
|
|
}));
|
|
const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id));
|
|
const urlHandlerRouter = new URLHandlerRouter(activeWindowRouter, this.logService);
|
|
const urlHandlerChannel = mainProcessElectronServer.getChannel('urlHandler', urlHandlerRouter);
|
|
urlService.registerHandler(new URLHandlerChannelClient(urlHandlerChannel));
|
|
|
|
const initialProtocolUrls = await this.resolveInitialProtocolUrls(windowsMainService, dialogMainService);
|
|
this._register(new ElectronURLListener(initialProtocolUrls?.urls, urlService, windowsMainService, this.environmentMainService, this.productService, this.logService));
|
|
|
|
return initialProtocolUrls;
|
|
}
|
|
|
|
private setupManagedRemoteResourceUrlHandler(mainProcessElectronServer: ElectronIPCServer) {
|
|
const notFound = (): Electron.ProtocolResponse => ({ statusCode: 404, data: 'Not found' });
|
|
const remoteResourceChannel = new Lazy(() => mainProcessElectronServer.getChannel(
|
|
NODE_REMOTE_RESOURCE_CHANNEL_NAME,
|
|
new NodeRemoteResourceRouter(),
|
|
));
|
|
|
|
protocol.registerBufferProtocol(Schemas.vscodeManagedRemoteResource, (request, callback) => {
|
|
const url = URI.parse(request.url);
|
|
if (!url.authority.startsWith('window:')) {
|
|
return callback(notFound());
|
|
}
|
|
|
|
remoteResourceChannel.value.call<NodeRemoteResourceResponse>(NODE_REMOTE_RESOURCE_IPC_METHOD_NAME, [url]).then(
|
|
r => callback({ ...r, data: Buffer.from(r.body, 'base64') }),
|
|
err => {
|
|
this.logService.warn('error dispatching remote resource call', err);
|
|
callback({ statusCode: 500, data: String(err) });
|
|
});
|
|
});
|
|
}
|
|
|
|
private async resolveInitialProtocolUrls(windowsMainService: IWindowsMainService, dialogMainService: IDialogMainService): Promise<IInitialProtocolUrls | undefined> {
|
|
|
|
/**
|
|
* Protocol URL handling on startup is complex, refer to
|
|
* {@link IInitialProtocolUrls} for an explainer.
|
|
*/
|
|
|
|
// Windows/Linux: protocol handler invokes CLI with --open-url
|
|
const protocolUrlsFromCommandLine = this.environmentMainService.args['open-url'] ? this.environmentMainService.args._urls || [] : [];
|
|
if (protocolUrlsFromCommandLine.length > 0) {
|
|
this.logService.trace('app#resolveInitialProtocolUrls() protocol urls from command line:', protocolUrlsFromCommandLine);
|
|
}
|
|
|
|
// macOS: open-url events that were received before the app is ready
|
|
const protocolUrlsFromEvent = ((global as { getOpenUrls?: () => string[] }).getOpenUrls?.() || []);
|
|
if (protocolUrlsFromEvent.length > 0) {
|
|
this.logService.trace(`app#resolveInitialProtocolUrls() protocol urls from macOS 'open-url' event:`, protocolUrlsFromEvent);
|
|
}
|
|
|
|
if (protocolUrlsFromCommandLine.length + protocolUrlsFromEvent.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
const protocolUrls = [
|
|
...protocolUrlsFromCommandLine,
|
|
...protocolUrlsFromEvent
|
|
].map(url => {
|
|
try {
|
|
return { uri: URI.parse(url), originalUrl: url };
|
|
} catch {
|
|
this.logService.trace('app#resolveInitialProtocolUrls() protocol url failed to parse:', url);
|
|
|
|
return undefined;
|
|
}
|
|
});
|
|
|
|
const openables: IWindowOpenable[] = [];
|
|
const urls: IProtocolUrl[] = [];
|
|
for (const protocolUrl of protocolUrls) {
|
|
if (!protocolUrl) {
|
|
continue; // invalid
|
|
}
|
|
|
|
const windowOpenable = this.getWindowOpenableFromProtocolUrl(protocolUrl.uri);
|
|
if (windowOpenable) {
|
|
if (await this.shouldBlockOpenable(windowOpenable, windowsMainService, dialogMainService)) {
|
|
this.logService.trace('app#resolveInitialProtocolUrls() protocol url was blocked:', protocolUrl.uri.toString(true));
|
|
|
|
continue; // blocked
|
|
} else {
|
|
this.logService.trace('app#resolveInitialProtocolUrls() protocol url will be handled as window to open:', protocolUrl.uri.toString(true), windowOpenable);
|
|
|
|
openables.push(windowOpenable); // handled as window to open
|
|
}
|
|
} else {
|
|
this.logService.trace('app#resolveInitialProtocolUrls() protocol url will be passed to active window for handling:', protocolUrl.uri.toString(true));
|
|
|
|
urls.push(protocolUrl); // handled within active window
|
|
}
|
|
}
|
|
|
|
return { urls, openables };
|
|
}
|
|
|
|
private async shouldBlockOpenable(openable: IWindowOpenable, windowsMainService: IWindowsMainService, dialogMainService: IDialogMainService): Promise<boolean> {
|
|
let openableUri: URI;
|
|
let message: string;
|
|
if (isWorkspaceToOpen(openable)) {
|
|
openableUri = openable.workspaceUri;
|
|
message = localize('confirmOpenMessageWorkspace', "An external application wants to open '{0}' in {1}. Do you want to open this workspace file?", openableUri.scheme === Schemas.file ? getPathLabel(openableUri, { os: OS, tildify: this.environmentMainService }) : openableUri.toString(true), this.productService.nameShort);
|
|
} else if (isFolderToOpen(openable)) {
|
|
openableUri = openable.folderUri;
|
|
message = localize('confirmOpenMessageFolder', "An external application wants to open '{0}' in {1}. Do you want to open this folder?", openableUri.scheme === Schemas.file ? getPathLabel(openableUri, { os: OS, tildify: this.environmentMainService }) : openableUri.toString(true), this.productService.nameShort);
|
|
} else {
|
|
openableUri = openable.fileUri;
|
|
message = localize('confirmOpenMessageFileOrFolder', "An external application wants to open '{0}' in {1}. Do you want to open this file or folder?", openableUri.scheme === Schemas.file ? getPathLabel(openableUri, { os: OS, tildify: this.environmentMainService }) : openableUri.toString(true), this.productService.nameShort);
|
|
}
|
|
|
|
if (openableUri.scheme !== Schemas.file && openableUri.scheme !== Schemas.vscodeRemote) {
|
|
|
|
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
//
|
|
// NOTE: we currently only ask for confirmation for `file` and `vscode-remote`
|
|
// authorities here. There is an additional confirmation for `extension.id`
|
|
// authorities from within the window.
|
|
//
|
|
// IF YOU ARE PLANNING ON ADDING ANOTHER AUTHORITY HERE, MAKE SURE TO ALSO
|
|
// ADD IT TO THE CONFIRMATION CODE BELOW OR INSIDE THE WINDOW!
|
|
//
|
|
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
|
|
return false;
|
|
}
|
|
|
|
const askForConfirmation = this.configurationService.getValue<unknown>(CodeApplication.SECURITY_PROTOCOL_HANDLING_CONFIRMATION_SETTING_KEY[openableUri.scheme]);
|
|
if (askForConfirmation === false) {
|
|
return false; // not blocked via settings
|
|
}
|
|
|
|
const { response, checkboxChecked } = await dialogMainService.showMessageBox({
|
|
type: 'warning',
|
|
buttons: [
|
|
localize({ key: 'open', comment: ['&& denotes a mnemonic'] }, "&&Yes"),
|
|
localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&No")
|
|
],
|
|
message,
|
|
detail: localize('confirmOpenDetail', "If you did not initiate this request, it may represent an attempted attack on your system. Unless you took an explicit action to initiate this request, you should press 'No'"),
|
|
checkboxLabel: openableUri.scheme === Schemas.file ? localize('doNotAskAgainLocal', "Allow opening local paths without asking") : localize('doNotAskAgainRemote', "Allow opening remote paths without asking"),
|
|
cancelId: 1
|
|
});
|
|
|
|
if (response !== 0) {
|
|
return true; // blocked by user choice
|
|
}
|
|
|
|
if (checkboxChecked) {
|
|
// Due to https://github.com/microsoft/vscode/issues/195436, we can only
|
|
// update settings from within a window. But we do not know if a window
|
|
// is about to open or can already handle the request, so we have to send
|
|
// to any current window and any newly opening window.
|
|
const request = { channel: 'vscode:disablePromptForProtocolHandling', args: openableUri.scheme === Schemas.file ? 'local' : 'remote' };
|
|
windowsMainService.sendToFocused(request.channel, request.args);
|
|
windowsMainService.sendToOpeningWindow(request.channel, request.args);
|
|
}
|
|
|
|
return false; // not blocked by user choice
|
|
}
|
|
|
|
private getWindowOpenableFromProtocolUrl(uri: URI): IWindowOpenable | undefined {
|
|
if (!uri.path) {
|
|
return undefined;
|
|
}
|
|
|
|
// File path
|
|
if (uri.authority === Schemas.file) {
|
|
const fileUri = URI.file(uri.fsPath);
|
|
|
|
if (hasWorkspaceFileExtension(fileUri)) {
|
|
return { workspaceUri: fileUri };
|
|
}
|
|
|
|
return { fileUri };
|
|
}
|
|
|
|
// Remote path
|
|
else if (uri.authority === Schemas.vscodeRemote) {
|
|
|
|
// Example conversion:
|
|
// From: vscode://vscode-remote/wsl+ubuntu/mnt/c/GitDevelopment/monaco
|
|
// To: vscode-remote://wsl+ubuntu/mnt/c/GitDevelopment/monaco
|
|
|
|
const secondSlash = uri.path.indexOf(posix.sep, 1 /* skip over the leading slash */);
|
|
let authority: string;
|
|
let path: string;
|
|
if (secondSlash !== -1) {
|
|
authority = uri.path.substring(1, secondSlash);
|
|
path = uri.path.substring(secondSlash);
|
|
} else {
|
|
authority = uri.path.substring(1);
|
|
path = '/';
|
|
}
|
|
|
|
let query = uri.query;
|
|
const params = new URLSearchParams(uri.query);
|
|
if (params.get('windowId') === '_blank') {
|
|
// Make sure to unset any `windowId=_blank` here
|
|
// https://github.com/microsoft/vscode/issues/191902
|
|
params.delete('windowId');
|
|
query = params.toString();
|
|
}
|
|
|
|
const remoteUri = URI.from({ scheme: Schemas.vscodeRemote, authority, path, query, fragment: uri.fragment });
|
|
|
|
if (hasWorkspaceFileExtension(path)) {
|
|
return { workspaceUri: remoteUri };
|
|
}
|
|
|
|
if (/:[\d]+$/.test(path)) {
|
|
// path with :line:column syntax
|
|
return { fileUri: remoteUri };
|
|
}
|
|
|
|
return { folderUri: remoteUri };
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
private async handleProtocolUrl(windowsMainService: IWindowsMainService, dialogMainService: IDialogMainService, urlService: IURLService, uri: URI, options?: IOpenURLOptions): Promise<boolean> {
|
|
this.logService.trace('app#handleProtocolUrl():', uri.toString(true), options);
|
|
|
|
// Support 'workspace' URLs (https://github.com/microsoft/vscode/issues/124263)
|
|
if (uri.scheme === this.productService.urlProtocol && uri.path === 'workspace') {
|
|
uri = uri.with({
|
|
authority: 'file',
|
|
path: URI.parse(uri.query).path,
|
|
query: ''
|
|
});
|
|
}
|
|
|
|
let shouldOpenInNewWindow = false;
|
|
|
|
// We should handle the URI in a new window if the URL contains `windowId=_blank`
|
|
const params = new URLSearchParams(uri.query);
|
|
if (params.get('windowId') === '_blank') {
|
|
this.logService.trace(`app#handleProtocolUrl() found 'windowId=_blank' as parameter, setting shouldOpenInNewWindow=true:`, uri.toString(true));
|
|
|
|
params.delete('windowId');
|
|
uri = uri.with({ query: params.toString() });
|
|
|
|
shouldOpenInNewWindow = true;
|
|
}
|
|
|
|
// or if no window is open (macOS only)
|
|
else if (isMacintosh && windowsMainService.getWindowCount() === 0) {
|
|
this.logService.trace(`app#handleProtocolUrl() running on macOS with no window open, setting shouldOpenInNewWindow=true:`, uri.toString(true));
|
|
|
|
shouldOpenInNewWindow = true;
|
|
}
|
|
|
|
// Pass along whether the application is being opened via a Continue On flow
|
|
const continueOn = params.get('continueOn');
|
|
if (continueOn !== null) {
|
|
this.logService.trace(`app#handleProtocolUrl() found 'continueOn' as parameter:`, uri.toString(true));
|
|
|
|
params.delete('continueOn');
|
|
uri = uri.with({ query: params.toString() });
|
|
|
|
this.environmentMainService.continueOn = continueOn ?? undefined;
|
|
}
|
|
|
|
// Check if the protocol URL is a window openable to open...
|
|
const windowOpenableFromProtocolUrl = this.getWindowOpenableFromProtocolUrl(uri);
|
|
if (windowOpenableFromProtocolUrl) {
|
|
if (await this.shouldBlockOpenable(windowOpenableFromProtocolUrl, windowsMainService, dialogMainService)) {
|
|
this.logService.trace('app#handleProtocolUrl() protocol url was blocked:', uri.toString(true));
|
|
|
|
return true; // If openable should be blocked, behave as if it's handled
|
|
} else {
|
|
this.logService.trace('app#handleProtocolUrl() opening protocol url as window:', windowOpenableFromProtocolUrl, uri.toString(true));
|
|
|
|
const window = (await windowsMainService.open({
|
|
context: OpenContext.LINK,
|
|
cli: { ...this.environmentMainService.args },
|
|
urisToOpen: [windowOpenableFromProtocolUrl],
|
|
forceNewWindow: shouldOpenInNewWindow,
|
|
gotoLineMode: true
|
|
// remoteAuthority: will be determined based on windowOpenableFromProtocolUrl
|
|
})).at(0);
|
|
|
|
window?.focus(); // this should help ensuring that the right window gets focus when multiple are opened
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// ...or if we should open in a new window and then handle it within that window
|
|
if (shouldOpenInNewWindow) {
|
|
this.logService.trace('app#handleProtocolUrl() opening empty window and passing in protocol url:', uri.toString(true));
|
|
|
|
const window = (await windowsMainService.open({
|
|
context: OpenContext.LINK,
|
|
cli: { ...this.environmentMainService.args },
|
|
forceNewWindow: true,
|
|
forceEmpty: true,
|
|
gotoLineMode: true,
|
|
remoteAuthority: getRemoteAuthority(uri)
|
|
})).at(0);
|
|
|
|
await window?.ready();
|
|
|
|
return urlService.open(uri, options);
|
|
}
|
|
|
|
this.logService.trace('app#handleProtocolUrl(): not handled', uri.toString(true), options);
|
|
|
|
return false;
|
|
}
|
|
|
|
private setupSharedProcess(machineId: string, sqmId: string, devDeviceId: string): { sharedProcessReady: Promise<MessagePortClient>; sharedProcessClient: Promise<MessagePortClient> } {
|
|
const sharedProcess = this._register(this.mainInstantiationService.createInstance(SharedProcess, machineId, sqmId, devDeviceId));
|
|
|
|
this._register(sharedProcess.onDidCrash(() => this.windowsMainService?.sendToFocused('vscode:reportSharedProcessCrash')));
|
|
|
|
const sharedProcessClient = (async () => {
|
|
this.logService.trace('Main->SharedProcess#connect');
|
|
|
|
const port = await sharedProcess.connect();
|
|
|
|
this.logService.trace('Main->SharedProcess#connect: connection established');
|
|
|
|
return new MessagePortClient(port, 'main');
|
|
})();
|
|
|
|
const sharedProcessReady = (async () => {
|
|
await sharedProcess.whenReady();
|
|
|
|
return sharedProcessClient;
|
|
})();
|
|
|
|
return { sharedProcessReady, sharedProcessClient };
|
|
}
|
|
|
|
private async initServices(machineId: string, sqmId: string, devDeviceId: string, sharedProcessReady: Promise<MessagePortClient>): Promise<IInstantiationService> {
|
|
const services = new ServiceCollection();
|
|
|
|
// Update
|
|
switch (process.platform) {
|
|
case 'win32':
|
|
services.set(IUpdateService, new SyncDescriptor(Win32UpdateService));
|
|
break;
|
|
|
|
case 'linux':
|
|
if (isLinuxSnap) {
|
|
services.set(IUpdateService, new SyncDescriptor(SnapUpdateService, [process.env['SNAP'], process.env['SNAP_REVISION']]));
|
|
} else {
|
|
services.set(IUpdateService, new SyncDescriptor(LinuxUpdateService));
|
|
}
|
|
break;
|
|
|
|
case 'darwin':
|
|
services.set(IUpdateService, new SyncDescriptor(DarwinUpdateService));
|
|
break;
|
|
}
|
|
|
|
// Windows
|
|
services.set(IWindowsMainService, new SyncDescriptor(WindowsMainService, [machineId, sqmId, devDeviceId, this.userEnv], false));
|
|
services.set(IAuxiliaryWindowsMainService, new SyncDescriptor(AuxiliaryWindowsMainService, undefined, false));
|
|
|
|
// Dialogs
|
|
const dialogMainService = new DialogMainService(this.logService, this.productService);
|
|
services.set(IDialogMainService, dialogMainService);
|
|
|
|
// Launch
|
|
services.set(ILaunchMainService, new SyncDescriptor(LaunchMainService, undefined, false /* proxied to other processes */));
|
|
|
|
// Diagnostics
|
|
services.set(IDiagnosticsMainService, new SyncDescriptor(DiagnosticsMainService, undefined, false /* proxied to other processes */));
|
|
services.set(IDiagnosticsService, ProxyChannel.toService(getDelayedChannel(sharedProcessReady.then(client => client.getChannel('diagnostics')))));
|
|
|
|
// Encryption
|
|
services.set(IEncryptionMainService, new SyncDescriptor(EncryptionMainService));
|
|
|
|
// Browser Elements
|
|
services.set(INativeBrowserElementsMainService, new SyncDescriptor(NativeBrowserElementsMainService, undefined, false /* proxied to other processes */));
|
|
|
|
// Browser View
|
|
services.set(IBrowserViewMainService, new SyncDescriptor(BrowserViewMainService, undefined, false /* proxied to other processes */));
|
|
|
|
// Keyboard Layout
|
|
services.set(IKeyboardLayoutMainService, new SyncDescriptor(KeyboardLayoutMainService));
|
|
|
|
// Native Host
|
|
services.set(INativeHostMainService, new SyncDescriptor(NativeHostMainService, undefined, false /* proxied to other processes */));
|
|
|
|
// Web Contents Extractor
|
|
services.set(IWebContentExtractorService, new SyncDescriptor(NativeWebContentExtractorService, undefined, false /* proxied to other processes */));
|
|
|
|
// Webview Manager
|
|
services.set(IWebviewManagerService, new SyncDescriptor(WebviewMainService));
|
|
|
|
// Menubar
|
|
services.set(IMenubarMainService, new SyncDescriptor(MenubarMainService));
|
|
|
|
// Extension Host Starter
|
|
services.set(IExtensionHostStarter, new SyncDescriptor(ExtensionHostStarter));
|
|
|
|
// Storage
|
|
services.set(IStorageMainService, new SyncDescriptor(StorageMainService));
|
|
services.set(IApplicationStorageMainService, new SyncDescriptor(ApplicationStorageMainService));
|
|
|
|
// Terminal
|
|
const ptyHostStarter = new ElectronPtyHostStarter({
|
|
graceTime: LocalReconnectConstants.GraceTime,
|
|
shortGraceTime: LocalReconnectConstants.ShortGraceTime,
|
|
scrollback: this.configurationService.getValue<number>(TerminalSettingId.PersistentSessionScrollback) ?? 100
|
|
}, this.configurationService, this.environmentMainService, this.lifecycleMainService, this.logService);
|
|
const ptyHostService = new PtyHostService(
|
|
ptyHostStarter,
|
|
this.configurationService,
|
|
this.logService,
|
|
this.loggerService
|
|
);
|
|
services.set(ILocalPtyService, ptyHostService);
|
|
|
|
// External terminal
|
|
if (isWindows) {
|
|
services.set(IExternalTerminalMainService, new SyncDescriptor(WindowsExternalTerminalService));
|
|
} else if (isMacintosh) {
|
|
services.set(IExternalTerminalMainService, new SyncDescriptor(MacExternalTerminalService));
|
|
} else if (isLinux) {
|
|
services.set(IExternalTerminalMainService, new SyncDescriptor(LinuxExternalTerminalService));
|
|
}
|
|
|
|
// Backups
|
|
const backupMainService = new BackupMainService(this.environmentMainService, this.configurationService, this.logService, this.stateService);
|
|
services.set(IBackupMainService, backupMainService);
|
|
|
|
// Workspaces
|
|
const workspacesManagementMainService = new WorkspacesManagementMainService(this.environmentMainService, this.logService, this.userDataProfilesMainService, backupMainService, dialogMainService);
|
|
services.set(IWorkspacesManagementMainService, workspacesManagementMainService);
|
|
services.set(IWorkspacesService, new SyncDescriptor(WorkspacesMainService, undefined, false /* proxied to other processes */));
|
|
services.set(IWorkspacesHistoryMainService, new SyncDescriptor(WorkspacesHistoryMainService, undefined, false));
|
|
|
|
// URL handling
|
|
services.set(IURLService, new SyncDescriptor(NativeURLService, undefined, false /* proxied to other processes */));
|
|
|
|
// Telemetry
|
|
if (supportsTelemetry(this.productService, this.environmentMainService)) {
|
|
const isInternal = isInternalTelemetry(this.productService, this.configurationService);
|
|
const channel = getDelayedChannel(sharedProcessReady.then(client => client.getChannel('telemetryAppender')));
|
|
const appender = new TelemetryAppenderClient(channel);
|
|
const commonProperties = resolveCommonProperties(release(), hostname(), process.arch, this.productService.commit, this.productService.version, machineId, sqmId, devDeviceId, isInternal, this.productService.date);
|
|
const piiPaths = getPiiPathsFromEnvironment(this.environmentMainService);
|
|
const config: ITelemetryServiceConfig = { appenders: [appender], commonProperties, piiPaths, sendErrorTelemetry: true };
|
|
|
|
services.set(ITelemetryService, new SyncDescriptor(TelemetryService, [config], false));
|
|
} else {
|
|
services.set(ITelemetryService, NullTelemetryService);
|
|
}
|
|
|
|
// Default Extensions Profile Init
|
|
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true));
|
|
services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService, undefined, true));
|
|
|
|
// Utility Process Worker
|
|
services.set(IUtilityProcessWorkerMainService, new SyncDescriptor(UtilityProcessWorkerMainService, undefined, true));
|
|
|
|
// Proxy Auth
|
|
services.set(IProxyAuthService, new SyncDescriptor(ProxyAuthService));
|
|
|
|
// MCP
|
|
services.set(INativeMcpDiscoveryHelperService, new SyncDescriptor(NativeMcpDiscoveryHelperService));
|
|
services.set(IMcpGatewayService, new SyncDescriptor(McpGatewayService));
|
|
|
|
|
|
// Dev Only: CSS service (for ESM)
|
|
services.set(ICSSDevelopmentService, new SyncDescriptor(CSSDevelopmentService, undefined, true));
|
|
|
|
// Init services that require it
|
|
await Promises.settled([
|
|
backupMainService.initialize(),
|
|
workspacesManagementMainService.initialize()
|
|
]);
|
|
|
|
return this.mainInstantiationService.createChild(services);
|
|
}
|
|
|
|
private initChannels(accessor: ServicesAccessor, mainProcessElectronServer: ElectronIPCServer, sharedProcessClient: Promise<MessagePortClient>): void {
|
|
|
|
// Channels registered to node.js are exposed to second instances
|
|
// launching because that is the only way the second instance
|
|
// can talk to the first instance. Electron IPC does not work
|
|
// across apps until `requestSingleInstance` APIs are adopted.
|
|
|
|
const disposables = this._register(new DisposableStore());
|
|
|
|
const launchChannel = ProxyChannel.fromService(accessor.get(ILaunchMainService), disposables, { disableMarshalling: true });
|
|
this.mainProcessNodeIpcServer.registerChannel('launch', launchChannel);
|
|
|
|
const diagnosticsChannel = ProxyChannel.fromService(accessor.get(IDiagnosticsMainService), disposables, { disableMarshalling: true });
|
|
this.mainProcessNodeIpcServer.registerChannel('diagnostics', diagnosticsChannel);
|
|
|
|
// Policies (main & shared process)
|
|
const policyChannel = disposables.add(new PolicyChannel(accessor.get(IPolicyService)));
|
|
mainProcessElectronServer.registerChannel('policy', policyChannel);
|
|
sharedProcessClient.then(client => client.registerChannel('policy', policyChannel));
|
|
|
|
// Local Files
|
|
const diskFileSystemProvider = this.fileService.getProvider(Schemas.file);
|
|
assertType(diskFileSystemProvider instanceof DiskFileSystemProvider);
|
|
const fileSystemProviderChannel = disposables.add(new DiskFileSystemProviderChannel(diskFileSystemProvider, this.logService, this.environmentMainService));
|
|
mainProcessElectronServer.registerChannel(LOCAL_FILE_SYSTEM_CHANNEL_NAME, fileSystemProviderChannel);
|
|
sharedProcessClient.then(client => client.registerChannel(LOCAL_FILE_SYSTEM_CHANNEL_NAME, fileSystemProviderChannel));
|
|
|
|
// User Data Profiles
|
|
const userDataProfilesService = ProxyChannel.fromService(accessor.get(IUserDataProfilesMainService), disposables);
|
|
mainProcessElectronServer.registerChannel('userDataProfiles', userDataProfilesService);
|
|
sharedProcessClient.then(client => client.registerChannel('userDataProfiles', userDataProfilesService));
|
|
|
|
// Update
|
|
const updateChannel = new UpdateChannel(accessor.get(IUpdateService));
|
|
mainProcessElectronServer.registerChannel('update', updateChannel);
|
|
|
|
// Process
|
|
const processChannel = ProxyChannel.fromService(new ProcessMainService(this.logService, accessor.get(IDiagnosticsService), accessor.get(IDiagnosticsMainService)), disposables);
|
|
mainProcessElectronServer.registerChannel('process', processChannel);
|
|
|
|
// Encryption
|
|
const encryptionChannel = ProxyChannel.fromService(accessor.get(IEncryptionMainService), disposables);
|
|
mainProcessElectronServer.registerChannel('encryption', encryptionChannel);
|
|
|
|
// Browser Elements
|
|
const browserElementsChannel = ProxyChannel.fromService(accessor.get(INativeBrowserElementsMainService), disposables);
|
|
mainProcessElectronServer.registerChannel('browserElements', browserElementsChannel);
|
|
sharedProcessClient.then(client => client.registerChannel('browserElements', browserElementsChannel));
|
|
|
|
// Browser View
|
|
const browserViewChannel = ProxyChannel.fromService(accessor.get(IBrowserViewMainService), disposables);
|
|
mainProcessElectronServer.registerChannel(ipcBrowserViewChannelName, browserViewChannel);
|
|
|
|
// Signing
|
|
const signChannel = ProxyChannel.fromService(accessor.get(ISignService), disposables);
|
|
mainProcessElectronServer.registerChannel('sign', signChannel);
|
|
|
|
// Keyboard Layout
|
|
const keyboardLayoutChannel = ProxyChannel.fromService(accessor.get(IKeyboardLayoutMainService), disposables);
|
|
mainProcessElectronServer.registerChannel('keyboardLayout', keyboardLayoutChannel);
|
|
|
|
// Native host (main & shared process)
|
|
this.nativeHostMainService = accessor.get(INativeHostMainService);
|
|
const nativeHostChannel = ProxyChannel.fromService(this.nativeHostMainService, disposables);
|
|
mainProcessElectronServer.registerChannel('nativeHost', nativeHostChannel);
|
|
sharedProcessClient.then(client => client.registerChannel('nativeHost', nativeHostChannel));
|
|
|
|
// Web Content Extractor
|
|
const webContentExtractorChannel = ProxyChannel.fromService(accessor.get(IWebContentExtractorService), disposables);
|
|
mainProcessElectronServer.registerChannel('webContentExtractor', webContentExtractorChannel);
|
|
|
|
// Workspaces
|
|
const workspacesChannel = ProxyChannel.fromService(accessor.get(IWorkspacesService), disposables);
|
|
mainProcessElectronServer.registerChannel('workspaces', workspacesChannel);
|
|
|
|
// Menubar
|
|
const menubarChannel = ProxyChannel.fromService(accessor.get(IMenubarMainService), disposables);
|
|
mainProcessElectronServer.registerChannel('menubar', menubarChannel);
|
|
|
|
// URL handling
|
|
const urlChannel = ProxyChannel.fromService(accessor.get(IURLService), disposables);
|
|
mainProcessElectronServer.registerChannel('url', urlChannel);
|
|
|
|
// Webview Manager
|
|
const webviewChannel = ProxyChannel.fromService(accessor.get(IWebviewManagerService), disposables);
|
|
mainProcessElectronServer.registerChannel('webview', webviewChannel);
|
|
|
|
// Storage (main & shared process)
|
|
const storageChannel = disposables.add((new StorageDatabaseChannel(this.logService, accessor.get(IStorageMainService))));
|
|
mainProcessElectronServer.registerChannel('storage', storageChannel);
|
|
sharedProcessClient.then(client => client.registerChannel('storage', storageChannel));
|
|
|
|
// Profile Storage Changes Listener (shared process)
|
|
const profileStorageListener = disposables.add((new ProfileStorageChangesListenerChannel(accessor.get(IStorageMainService), accessor.get(IUserDataProfilesMainService), this.logService)));
|
|
sharedProcessClient.then(client => client.registerChannel('profileStorageListener', profileStorageListener));
|
|
|
|
// Terminal
|
|
const ptyHostChannel = ProxyChannel.fromService(accessor.get(ILocalPtyService), disposables);
|
|
mainProcessElectronServer.registerChannel(TerminalIpcChannels.LocalPty, ptyHostChannel);
|
|
|
|
// External Terminal
|
|
const externalTerminalChannel = ProxyChannel.fromService(accessor.get(IExternalTerminalMainService), disposables);
|
|
mainProcessElectronServer.registerChannel('externalTerminal', externalTerminalChannel);
|
|
|
|
// MCP
|
|
const mcpDiscoveryChannel = ProxyChannel.fromService(accessor.get(INativeMcpDiscoveryHelperService), disposables);
|
|
mainProcessElectronServer.registerChannel(NativeMcpDiscoveryHelperChannelName, mcpDiscoveryChannel);
|
|
const mcpGatewayChannel = this._register(new McpGatewayChannel(mainProcessElectronServer, accessor.get(IMcpGatewayService)));
|
|
mainProcessElectronServer.registerChannel(McpGatewayChannelName, mcpGatewayChannel);
|
|
|
|
// Logger
|
|
const loggerChannel = new LoggerChannel(accessor.get(ILoggerMainService),);
|
|
mainProcessElectronServer.registerChannel('logger', loggerChannel);
|
|
sharedProcessClient.then(client => client.registerChannel('logger', loggerChannel));
|
|
|
|
// Extension Host Debug Broadcasting
|
|
const electronExtensionHostDebugBroadcastChannel = new ElectronExtensionHostDebugBroadcastChannel(accessor.get(IWindowsMainService));
|
|
mainProcessElectronServer.registerChannel('extensionhostdebugservice', electronExtensionHostDebugBroadcastChannel);
|
|
|
|
// Extension Host Starter
|
|
const extensionHostStarterChannel = ProxyChannel.fromService(accessor.get(IExtensionHostStarter), disposables);
|
|
mainProcessElectronServer.registerChannel(ipcExtensionHostStarterChannelName, extensionHostStarterChannel);
|
|
|
|
// Utility Process Worker
|
|
const utilityProcessWorkerChannel = ProxyChannel.fromService(accessor.get(IUtilityProcessWorkerMainService), disposables);
|
|
mainProcessElectronServer.registerChannel(ipcUtilityProcessWorkerChannelName, utilityProcessWorkerChannel);
|
|
}
|
|
|
|
private async openFirstWindow(accessor: ServicesAccessor, initialProtocolUrls: IInitialProtocolUrls | undefined): Promise<ICodeWindow[]> {
|
|
const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService);
|
|
this.auxiliaryWindowsMainService = accessor.get(IAuxiliaryWindowsMainService);
|
|
|
|
const context = isLaunchedFromCli(process.env) ? OpenContext.CLI : OpenContext.DESKTOP;
|
|
const args = this.environmentMainService.args;
|
|
|
|
// First check for windows from protocol links to open
|
|
if (initialProtocolUrls) {
|
|
|
|
// Openables can open as windows directly
|
|
if (initialProtocolUrls.openables.length > 0) {
|
|
return windowsMainService.open({
|
|
context,
|
|
cli: args,
|
|
urisToOpen: initialProtocolUrls.openables,
|
|
gotoLineMode: true,
|
|
initialStartup: true
|
|
// remoteAuthority: will be determined based on openables
|
|
});
|
|
}
|
|
|
|
// Protocol links with `windowId=_blank` on startup
|
|
// should be handled in a special way:
|
|
// We take the first one of these and open an empty
|
|
// window for it. This ensures we are not restoring
|
|
// all windows of the previous session.
|
|
// If there are any more URLs like these, they will
|
|
// be handled from the URL listeners installed later.
|
|
|
|
if (initialProtocolUrls.urls.length > 0) {
|
|
for (const protocolUrl of initialProtocolUrls.urls) {
|
|
const params = new URLSearchParams(protocolUrl.uri.query);
|
|
if (params.get('windowId') === '_blank') {
|
|
|
|
// It is important here that we remove `windowId=_blank` from
|
|
// this URL because here we open an empty window for it.
|
|
|
|
params.delete('windowId');
|
|
protocolUrl.originalUrl = protocolUrl.uri.toString(true);
|
|
protocolUrl.uri = protocolUrl.uri.with({ query: params.toString() });
|
|
|
|
return windowsMainService.open({
|
|
context,
|
|
cli: args,
|
|
forceNewWindow: true,
|
|
forceEmpty: true,
|
|
gotoLineMode: true,
|
|
initialStartup: true
|
|
// remoteAuthority: will be determined based on openables
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const macOpenFiles: string[] = (global as { macOpenFiles?: string[] }).macOpenFiles ?? [];
|
|
const hasCliArgs = args._.length;
|
|
const hasFolderURIs = !!args['folder-uri'];
|
|
const hasFileURIs = !!args['file-uri'];
|
|
const noRecentEntry = args['skip-add-to-recently-opened'] === true;
|
|
const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined;
|
|
const remoteAuthority = args.remote || undefined;
|
|
const forceProfile = args.profile;
|
|
const forceTempProfile = args['profile-temp'];
|
|
|
|
// Started without file/folder arguments
|
|
if (!hasCliArgs && !hasFolderURIs && !hasFileURIs) {
|
|
|
|
// Force new window
|
|
if (args['new-window'] || forceProfile || forceTempProfile) {
|
|
return windowsMainService.open({
|
|
context,
|
|
cli: args,
|
|
forceNewWindow: true,
|
|
forceEmpty: true,
|
|
noRecentEntry,
|
|
waitMarkerFileURI,
|
|
initialStartup: true,
|
|
remoteAuthority,
|
|
forceProfile,
|
|
forceTempProfile
|
|
});
|
|
}
|
|
|
|
// mac: open-file event received on startup
|
|
if (macOpenFiles.length) {
|
|
return windowsMainService.open({
|
|
context: OpenContext.DOCK,
|
|
cli: args,
|
|
urisToOpen: macOpenFiles.map(path => {
|
|
path = normalizeNFC(path); // macOS only: normalize paths to NFC form
|
|
|
|
return (hasWorkspaceFileExtension(path) ? { workspaceUri: URI.file(path) } : { fileUri: URI.file(path) });
|
|
}),
|
|
noRecentEntry,
|
|
waitMarkerFileURI,
|
|
initialStartup: true,
|
|
// remoteAuthority: will be determined based on macOpenFiles
|
|
});
|
|
}
|
|
}
|
|
|
|
// default: read paths from cli
|
|
return windowsMainService.open({
|
|
context,
|
|
cli: args,
|
|
forceNewWindow: args['new-window'],
|
|
diffMode: args.diff,
|
|
mergeMode: args.merge,
|
|
noRecentEntry,
|
|
waitMarkerFileURI,
|
|
gotoLineMode: args.goto,
|
|
initialStartup: true,
|
|
remoteAuthority,
|
|
forceProfile,
|
|
forceTempProfile
|
|
});
|
|
}
|
|
|
|
private afterWindowOpen(instantiationService: IInstantiationService): void {
|
|
|
|
// Windows: mutex
|
|
this.installMutex();
|
|
|
|
// Remote Authorities
|
|
protocol.registerHttpProtocol(Schemas.vscodeRemoteResource, (request, callback) => {
|
|
callback({
|
|
url: request.url.replace(/^vscode-remote-resource:/, 'http:'),
|
|
method: request.method
|
|
});
|
|
});
|
|
|
|
// Start to fetch shell environment (if needed) after window has opened
|
|
// Since this operation can take a long time, we want to warm it up while
|
|
// the window is opening.
|
|
// We also show an error to the user in case this fails.
|
|
this.resolveShellEnvironment(this.environmentMainService.args, process.env, true);
|
|
|
|
// Crash reporter
|
|
this.updateCrashReporterEnablement();
|
|
|
|
// macOS: rosetta translation warning
|
|
if (isMacintosh && app.runningUnderARM64Translation) {
|
|
this.windowsMainService?.sendToFocused('vscode:showTranslatedBuildWarning');
|
|
}
|
|
|
|
// Power telemetry
|
|
instantiationService.invokeFunction(accessor => {
|
|
const telemetryService = accessor.get(ITelemetryService);
|
|
|
|
type PowerEvent = {
|
|
readonly idleState: string;
|
|
readonly idleTime: number;
|
|
readonly thermalState: string;
|
|
readonly onBattery: boolean;
|
|
};
|
|
type PowerEventClassification = {
|
|
idleState: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The system idle state (active, idle, locked, unknown).' };
|
|
idleTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The system idle time in seconds.' };
|
|
thermalState: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The system thermal state (unknown, nominal, fair, serious, critical).' };
|
|
onBattery: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Whether the system is running on battery power.' };
|
|
owner: 'chrmarti';
|
|
comment: 'Tracks OS power suspend and resume events for reliability insights.';
|
|
};
|
|
|
|
const getPowerEventData = (): PowerEvent => ({
|
|
idleState: powerMonitor.getSystemIdleState(60),
|
|
idleTime: powerMonitor.getSystemIdleTime(),
|
|
thermalState: powerMonitor.getCurrentThermalState(),
|
|
onBattery: powerMonitor.isOnBatteryPower()
|
|
});
|
|
|
|
this._register(Event.fromNodeEventEmitter(powerMonitor, 'suspend')(() => {
|
|
telemetryService.publicLog2<PowerEvent, PowerEventClassification>('power.suspend', getPowerEventData());
|
|
}));
|
|
|
|
this._register(Event.fromNodeEventEmitter(powerMonitor, 'resume')(() => {
|
|
telemetryService.publicLog2<PowerEvent, PowerEventClassification>('power.resume', getPowerEventData());
|
|
}));
|
|
});
|
|
}
|
|
|
|
private async installMutex(): Promise<void> {
|
|
const win32MutexName = this.productService.win32MutexName;
|
|
if (isWindows && win32MutexName) {
|
|
try {
|
|
const WindowsMutex = await import('@vscode/windows-mutex');
|
|
const mutex = new WindowsMutex.Mutex(win32MutexName);
|
|
Event.once(this.lifecycleMainService.onWillShutdown)(() => mutex.release());
|
|
} catch (error) {
|
|
this.logService.error(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async resolveShellEnvironment(args: NativeParsedArgs, env: IProcessEnvironment, notifyOnError: boolean): Promise<typeof process.env> {
|
|
try {
|
|
return await getResolvedShellEnv(this.configurationService, this.logService, args, env);
|
|
} catch (error) {
|
|
const errorMessage = toErrorMessage(error);
|
|
if (notifyOnError) {
|
|
this.windowsMainService?.sendToFocused('vscode:showResolveShellEnvError', errorMessage);
|
|
} else {
|
|
this.logService.error(errorMessage);
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
private async updateCrashReporterEnablement(): Promise<void> {
|
|
|
|
// If enable-crash-reporter argv is undefined then this is a fresh start,
|
|
// based on `telemetry.enableCrashreporter` settings, generate a UUID which
|
|
// will be used as crash reporter id and also update the json file.
|
|
|
|
try {
|
|
const argvContent = await this.fileService.readFile(this.environmentMainService.argvResource);
|
|
const argvString = argvContent.value.toString();
|
|
const argvJSON = parse<{ 'enable-crash-reporter'?: boolean }>(argvString);
|
|
const telemetryLevel = getTelemetryLevel(this.configurationService);
|
|
const enableCrashReporter = telemetryLevel >= TelemetryLevel.CRASH;
|
|
|
|
// Initial startup
|
|
if (argvJSON['enable-crash-reporter'] === undefined) {
|
|
const additionalArgvContent = [
|
|
'',
|
|
' // Allows to disable crash reporting.',
|
|
' // Should restart the app if the value is changed.',
|
|
` "enable-crash-reporter": ${enableCrashReporter},`,
|
|
'',
|
|
' // Unique id used for correlating crash reports sent from this instance.',
|
|
' // Do not edit this value.',
|
|
` "crash-reporter-id": "${generateUuid()}"`,
|
|
'}'
|
|
];
|
|
const newArgvString = argvString.substring(0, argvString.length - 2).concat(',\n', additionalArgvContent.join('\n'));
|
|
|
|
await this.fileService.writeFile(this.environmentMainService.argvResource, VSBuffer.fromString(newArgvString));
|
|
}
|
|
|
|
// Subsequent startup: update crash reporter value if changed
|
|
else {
|
|
const newArgvString = argvString.replace(/"enable-crash-reporter": .*,/, `"enable-crash-reporter": ${enableCrashReporter},`);
|
|
if (newArgvString !== argvString) {
|
|
await this.fileService.writeFile(this.environmentMainService.argvResource, VSBuffer.fromString(newArgvString));
|
|
}
|
|
}
|
|
} catch (error) {
|
|
this.logService.error(error);
|
|
|
|
// Inform the user via notification
|
|
this.windowsMainService?.sendToFocused('vscode:showArgvParseWarning');
|
|
}
|
|
}
|
|
|
|
private eventuallyAfterWindowOpen(): void {
|
|
|
|
// Validate Device ID is up to date (delay this as it has shown significant perf impact)
|
|
// Refs: https://github.com/microsoft/vscode/issues/234064
|
|
validateDevDeviceId(this.stateService, this.logService);
|
|
}
|
|
}
|