Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
871
lib/vscode/src/vs/code/electron-main/app.ts
Normal file
871
lib/vscode/src/vs/code/electron-main/app.ts
Normal file
@@ -0,0 +1,871 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { app, ipcMain as ipc, systemPreferences, contentTracing, protocol, IpcMainEvent, BrowserWindow, dialog, session } from 'electron';
|
||||
import { IProcessEnvironment, isWindows, isMacintosh } from 'vs/base/common/platform';
|
||||
import { WindowsMainService } from 'vs/platform/windows/electron-main/windowsMainService';
|
||||
import { IWindowOpenable } from 'vs/platform/windows/common/windows';
|
||||
import { OpenContext } from 'vs/platform/windows/node/window';
|
||||
import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
|
||||
import { getShellEnvironment } from 'vs/code/node/shellEnv';
|
||||
import { IUpdateService } from 'vs/platform/update/common/update';
|
||||
import { UpdateChannel } from 'vs/platform/update/electron-main/updateIpc';
|
||||
import { Server as ElectronIPCServer } from 'vs/base/parts/ipc/electron-main/ipc.electron-main';
|
||||
import { Client } from 'vs/base/parts/ipc/common/ipc.net';
|
||||
import { Server, connect } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { SharedProcess } from 'vs/code/electron-main/sharedProcess';
|
||||
import { LaunchMainService, ILaunchMainService } from 'vs/platform/launch/electron-main/launchMainService';
|
||||
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IStateService } from 'vs/platform/state/node/state';
|
||||
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IURLService } from 'vs/platform/url/common/url';
|
||||
import { URLHandlerChannelClient, URLHandlerRouter } from 'vs/platform/url/common/urlIpc';
|
||||
import { ITelemetryService, machineIdKey } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc';
|
||||
import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService';
|
||||
import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties';
|
||||
import { getDelayedChannel, StaticRouter, createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/common/ipc';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
import { ProxyAuthHandler } from 'vs/code/electron-main/auth';
|
||||
import { ProxyAuthHandler2 } from 'vs/code/electron-main/auth2';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { hasWorkspaceFileExtension, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { WorkspacesService } from 'vs/platform/workspaces/electron-main/workspacesService';
|
||||
import { getMachineId } from 'vs/base/node/id';
|
||||
import { Win32UpdateService } from 'vs/platform/update/electron-main/updateService.win32';
|
||||
import { LinuxUpdateService } from 'vs/platform/update/electron-main/updateService.linux';
|
||||
import { DarwinUpdateService } from 'vs/platform/update/electron-main/updateService.darwin';
|
||||
import { IssueMainService, IIssueMainService } from 'vs/platform/issue/electron-main/issueMainService';
|
||||
import { LoggerChannel } from 'vs/platform/log/common/logIpc';
|
||||
import { setUnexpectedErrorHandler, onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { ElectronURLListener } from 'vs/platform/url/electron-main/electronUrlListener';
|
||||
import { serve as serveDriver } from 'vs/platform/driver/electron-main/driver';
|
||||
import { IMenubarMainService, MenubarMainService } from 'vs/platform/menubar/electron-main/menubarMainService';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { registerContextMenuListener } from 'vs/base/parts/contextmenu/electron-main/contextmenu';
|
||||
import { sep, posix } from 'vs/base/common/path';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { localize } from 'vs/nls';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { SnapUpdateService } from 'vs/platform/update/electron-main/updateService.snap';
|
||||
import { IStorageMainService, StorageMainService } from 'vs/platform/storage/node/storageMainService';
|
||||
import { GlobalStorageDatabaseChannel } from 'vs/platform/storage/node/storageIpc';
|
||||
import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainService';
|
||||
import { IBackupMainService } from 'vs/platform/backup/electron-main/backup';
|
||||
import { WorkspacesHistoryMainService, IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService';
|
||||
import { NativeURLService } from 'vs/platform/url/common/urlService';
|
||||
import { WorkspacesMainService, IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService';
|
||||
import { statSync } from 'fs';
|
||||
import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService';
|
||||
import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc';
|
||||
import { ElectronExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/electron-main/extensionHostDebugIpc';
|
||||
import { INativeHostMainService, NativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService';
|
||||
import { ISharedProcessMainService, SharedProcessMainService } from 'vs/platform/ipc/electron-main/sharedProcessMainService';
|
||||
import { IDialogMainService, DialogMainService } from 'vs/platform/dialogs/electron-main/dialogs';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { mnemonicButtonLabel, getPathLabel } from 'vs/base/common/labels';
|
||||
import { WebviewMainService } from 'vs/platform/webview/electron-main/webviewMainService';
|
||||
import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { stripComments } from 'vs/base/common/json';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { EncryptionMainService, IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService';
|
||||
import { ActiveWindowManager } from 'vs/platform/windows/common/windowTracker';
|
||||
|
||||
export class CodeApplication extends Disposable {
|
||||
private windowsMainService: IWindowsMainService | undefined;
|
||||
private dialogMainService: IDialogMainService | undefined;
|
||||
private nativeHostMainService: INativeHostMainService | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly mainIpcServer: Server,
|
||||
private readonly userEnv: IProcessEnvironment,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IEnvironmentMainService private readonly environmentService: IEnvironmentMainService,
|
||||
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IStateService private readonly stateService: IStateService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
|
||||
// We handle uncaught exceptions here to prevent electron from opening a dialog to the user
|
||||
setUnexpectedErrorHandler(err => this.onUnexpectedError(err));
|
||||
process.on('uncaughtException', err => this.onUnexpectedError(err));
|
||||
process.on('unhandledRejection', (reason: unknown) => onUnexpectedError(reason));
|
||||
|
||||
// Dispose on shutdown
|
||||
this.lifecycleMainService.onWillShutdown(() => this.dispose());
|
||||
|
||||
// Contextmenu via IPC support
|
||||
registerContextMenuListener();
|
||||
|
||||
// Accessibility change event
|
||||
app.on('accessibility-support-changed', (event, accessibilitySupportEnabled) => {
|
||||
if (this.windowsMainService) {
|
||||
this.windowsMainService.sendToAll('vscode:accessibilitySupportChanged', accessibilitySupportEnabled);
|
||||
}
|
||||
});
|
||||
|
||||
// macOS dock activate
|
||||
app.on('activate', (event, hasVisibleWindows) => {
|
||||
this.logService.trace('app#activate');
|
||||
|
||||
// Mac only event: open new window when we get activated
|
||||
if (!hasVisibleWindows && this.windowsMainService) {
|
||||
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('remote-require', (event, sender, module) => {
|
||||
this.logService.trace('app#on(remote-require): prevented');
|
||||
|
||||
event.preventDefault();
|
||||
});
|
||||
app.on('remote-get-global', (event, sender, module) => {
|
||||
this.logService.trace(`App#on(remote-get-global): prevented on ${module}`);
|
||||
|
||||
event.preventDefault();
|
||||
});
|
||||
app.on('remote-get-builtin', (event, sender, module) => {
|
||||
this.logService.trace(`App#on(remote-get-builtin): prevented on ${module}`);
|
||||
|
||||
if (module !== 'clipboard') {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
app.on('remote-get-current-window', event => {
|
||||
this.logService.trace(`App#on(remote-get-current-window): prevented`);
|
||||
|
||||
event.preventDefault();
|
||||
});
|
||||
app.on('remote-get-current-web-contents', event => {
|
||||
if (this.environmentService.args.driver) {
|
||||
return; // the driver needs access to web contents
|
||||
}
|
||||
|
||||
this.logService.trace(`App#on(remote-get-current-web-contents): prevented`);
|
||||
|
||||
event.preventDefault();
|
||||
});
|
||||
app.on('web-contents-created', (event, contents) => {
|
||||
contents.on('will-attach-webview', (event, webPreferences, params) => {
|
||||
|
||||
const isValidWebviewSource = (source: string | undefined): boolean => {
|
||||
if (!source) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uri = URI.parse(source);
|
||||
if (uri.scheme === Schemas.vscodeWebview) {
|
||||
return uri.path === '/index.html' || uri.path === '/electron-browser/index.html';
|
||||
}
|
||||
|
||||
const srcUri = uri.fsPath.toLowerCase();
|
||||
const rootUri = URI.file(this.environmentService.appRoot).fsPath.toLowerCase();
|
||||
|
||||
return srcUri.startsWith(rootUri + sep);
|
||||
};
|
||||
|
||||
// Ensure defaults
|
||||
delete webPreferences.preload;
|
||||
webPreferences.nodeIntegration = false;
|
||||
|
||||
// Verify URLs being loaded
|
||||
// https://github.com/electron/electron/issues/21553
|
||||
if (isValidWebviewSource(params.src) && isValidWebviewSource((webPreferences as { preloadURL: string }).preloadURL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete (webPreferences as { preloadURL: string | undefined }).preloadURL; // https://github.com/electron/electron/issues/21553
|
||||
|
||||
// Otherwise prevent loading
|
||||
this.logService.error('webContents#web-contents-created: Prevented webview attach');
|
||||
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
contents.on('will-navigate', event => {
|
||||
this.logService.error('webContents#will-navigate: Prevented webcontent navigation');
|
||||
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
contents.on('new-window', (event, url) => {
|
||||
event.preventDefault(); // prevent code that wants to open links
|
||||
|
||||
if (this.nativeHostMainService) {
|
||||
this.nativeHostMainService.openExternal(undefined, url);
|
||||
}
|
||||
});
|
||||
|
||||
session.defaultSession.setPermissionRequestHandler((webContents, permission /* 'media' | 'geolocation' | 'notifications' | 'midiSysex' | 'pointerLock' | 'fullscreen' | 'openExternal' */, callback) => {
|
||||
return callback(false);
|
||||
});
|
||||
|
||||
session.defaultSession.setPermissionCheckHandler((webContents, permission /* 'media' */) => {
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
//#endregion
|
||||
|
||||
let macOpenFileURIs: IWindowOpenable[] = [];
|
||||
let runningTimeout: NodeJS.Timeout | null = null;
|
||||
app.on('open-file', (event, path) => {
|
||||
this.logService.trace('app#open-file: ', path);
|
||||
event.preventDefault();
|
||||
|
||||
// Keep in array because more might come!
|
||||
macOpenFileURIs.push(this.getWindowOpenableFromPathSync(path));
|
||||
|
||||
// Clear previous handler if any
|
||||
if (runningTimeout !== null) {
|
||||
clearTimeout(runningTimeout);
|
||||
runningTimeout = null;
|
||||
}
|
||||
|
||||
// Handle paths delayed in case more are coming!
|
||||
runningTimeout = setTimeout(() => {
|
||||
if (this.windowsMainService) {
|
||||
this.windowsMainService.open({
|
||||
context: OpenContext.DOCK /* can also be opening from finder while app is running */,
|
||||
cli: this.environmentService.args,
|
||||
urisToOpen: macOpenFileURIs,
|
||||
gotoLineMode: false,
|
||||
preferNewWindow: true /* dropping on the dock or opening from finder prefers to open in a new window */
|
||||
});
|
||||
|
||||
macOpenFileURIs = [];
|
||||
runningTimeout = null;
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
app.on('new-window-for-tab', () => {
|
||||
if (this.windowsMainService) {
|
||||
this.windowsMainService.openEmptyWindow({ context: OpenContext.DESKTOP }); //macOS native tab "+" button
|
||||
}
|
||||
});
|
||||
|
||||
ipc.on('vscode:fetchShellEnv', async (event: IpcMainEvent) => {
|
||||
const webContents = event.sender;
|
||||
|
||||
try {
|
||||
const shellEnv = await getShellEnvironment(this.logService, this.environmentService);
|
||||
|
||||
if (!webContents.isDestroyed()) {
|
||||
webContents.send('vscode:acceptShellEnv', shellEnv);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!webContents.isDestroyed()) {
|
||||
webContents.send('vscode:acceptShellEnv', {});
|
||||
}
|
||||
|
||||
this.logService.error('Error fetching shell env', error);
|
||||
}
|
||||
});
|
||||
|
||||
ipc.on('vscode:toggleDevTools', (event: IpcMainEvent) => event.sender.toggleDevTools());
|
||||
ipc.on('vscode:openDevTools', (event: IpcMainEvent) => event.sender.openDevTools());
|
||||
|
||||
ipc.on('vscode:reloadWindow', (event: IpcMainEvent) => event.sender.reload());
|
||||
|
||||
// Some listeners after window opened
|
||||
(async () => {
|
||||
await this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen);
|
||||
|
||||
// Keyboard layout changes (after window opened)
|
||||
const nativeKeymap = await import('native-keymap');
|
||||
nativeKeymap.onDidChangeKeyboardLayout(() => {
|
||||
if (this.windowsMainService) {
|
||||
this.windowsMainService.sendToAll('vscode:keyboardLayoutChanged');
|
||||
}
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
private onUnexpectedError(err: Error): void {
|
||||
if (err) {
|
||||
|
||||
// take only the message and stack property
|
||||
const friendlyError = {
|
||||
message: err.message,
|
||||
stack: err.stack
|
||||
};
|
||||
|
||||
// handle on client side
|
||||
if (this.windowsMainService) {
|
||||
this.windowsMainService.sendToFocused('vscode:reportError', JSON.stringify(friendlyError));
|
||||
}
|
||||
}
|
||||
|
||||
this.logService.error(`[uncaught exception in main]: ${err}`);
|
||||
if (err.stack) {
|
||||
this.logService.error(err.stack);
|
||||
}
|
||||
}
|
||||
|
||||
async startup(): Promise<void> {
|
||||
this.logService.debug('Starting VS Code');
|
||||
this.logService.debug(`from: ${this.environmentService.appRoot}`);
|
||||
this.logService.debug('args:', this.environmentService.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 = product.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<boolean>('window.nativeTabs') === true && !systemPreferences.getUserDefault('NSUseImprovedLayoutPass', 'boolean')) {
|
||||
systemPreferences.setUserDefault('NSUseImprovedLayoutPass', 'boolean', true as any);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
}
|
||||
|
||||
// Create Electron IPC Server
|
||||
const electronIpcServer = new ElectronIPCServer();
|
||||
|
||||
// Resolve unique machine ID
|
||||
this.logService.trace('Resolving machine identifier...');
|
||||
const machineId = await this.resolveMachineId();
|
||||
this.logService.trace(`Resolved machine identifier: ${machineId}`);
|
||||
|
||||
// Spawn shared process after the first window has opened and 3s have passed
|
||||
const sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv);
|
||||
const sharedProcessClient = sharedProcess.whenIpcReady().then(() => {
|
||||
this.logService.trace('Shared process: IPC ready');
|
||||
|
||||
return connect(this.environmentService.sharedIPCHandle, 'main');
|
||||
});
|
||||
const sharedProcessReady = sharedProcess.whenReady().then(() => {
|
||||
this.logService.trace('Shared process: init ready');
|
||||
|
||||
return sharedProcessClient;
|
||||
});
|
||||
this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => {
|
||||
this._register(new RunOnceScheduler(async () => {
|
||||
sharedProcess.spawn(await getShellEnvironment(this.logService, this.environmentService));
|
||||
}, 3000)).schedule();
|
||||
});
|
||||
|
||||
// Services
|
||||
const appInstantiationService = await this.createServices(machineId, sharedProcess, sharedProcessReady);
|
||||
|
||||
// Create driver
|
||||
if (this.environmentService.driverHandle) {
|
||||
const server = await serveDriver(electronIpcServer, this.environmentService.driverHandle, this.environmentService, appInstantiationService);
|
||||
|
||||
this.logService.info('Driver started at:', this.environmentService.driverHandle);
|
||||
this._register(server);
|
||||
}
|
||||
|
||||
// Setup Auth Handler
|
||||
if (this.configurationService.getValue('window.enableExperimentalProxyLoginDialog') !== true) {
|
||||
this._register(new ProxyAuthHandler());
|
||||
} else {
|
||||
this._register(appInstantiationService.createInstance(ProxyAuthHandler2));
|
||||
}
|
||||
|
||||
// Open Windows
|
||||
const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient));
|
||||
|
||||
// Post Open Windows Tasks
|
||||
appInstantiationService.invokeFunction(accessor => this.afterWindowOpen(accessor));
|
||||
|
||||
// Tracing: Stop tracing after windows are ready if enabled
|
||||
if (this.environmentService.args.trace) {
|
||||
this.stopTracingEventually(windows);
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveMachineId(): Promise<string> {
|
||||
|
||||
// We cache the machineId for faster lookups on startup
|
||||
// and resolve it only once initially if not cached or we need to replace the macOS iBridge device
|
||||
let machineId = this.stateService.getItem<string>(machineIdKey);
|
||||
if (!machineId || (isMacintosh && machineId === '6c9d2bc8f91b89624add29c0abeae7fb42bf539fa1cdb2e3e57cd668fa9bcead')) {
|
||||
machineId = await getMachineId();
|
||||
|
||||
this.stateService.setItem(machineIdKey, machineId);
|
||||
}
|
||||
|
||||
return machineId;
|
||||
}
|
||||
|
||||
private async createServices(machineId: string, sharedProcess: SharedProcess, sharedProcessReady: Promise<Client<string>>): Promise<IInstantiationService> {
|
||||
const services = new ServiceCollection();
|
||||
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
services.set(IUpdateService, new SyncDescriptor(Win32UpdateService));
|
||||
break;
|
||||
|
||||
case 'linux':
|
||||
if (process.env.SNAP && process.env.SNAP_REVISION) {
|
||||
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;
|
||||
}
|
||||
|
||||
services.set(IWindowsMainService, new SyncDescriptor(WindowsMainService, [machineId, this.userEnv]));
|
||||
services.set(IDialogMainService, new SyncDescriptor(DialogMainService));
|
||||
services.set(ISharedProcessMainService, new SyncDescriptor(SharedProcessMainService, [sharedProcess]));
|
||||
services.set(ILaunchMainService, new SyncDescriptor(LaunchMainService));
|
||||
services.set(IDiagnosticsService, createChannelSender(getDelayedChannel(sharedProcessReady.then(client => client.getChannel('diagnostics')))));
|
||||
|
||||
services.set(IIssueMainService, new SyncDescriptor(IssueMainService, [machineId, this.userEnv]));
|
||||
services.set(IEncryptionMainService, new SyncDescriptor(EncryptionMainService, [machineId]));
|
||||
services.set(INativeHostMainService, new SyncDescriptor(NativeHostMainService));
|
||||
services.set(IWebviewManagerService, new SyncDescriptor(WebviewMainService));
|
||||
services.set(IWorkspacesService, new SyncDescriptor(WorkspacesService));
|
||||
services.set(IMenubarMainService, new SyncDescriptor(MenubarMainService));
|
||||
|
||||
const storageMainService = new StorageMainService(this.logService, this.environmentService);
|
||||
services.set(IStorageMainService, storageMainService);
|
||||
this.lifecycleMainService.onWillShutdown(e => e.join(storageMainService.close()));
|
||||
|
||||
const backupMainService = new BackupMainService(this.environmentService, this.configurationService, this.logService);
|
||||
services.set(IBackupMainService, backupMainService);
|
||||
|
||||
services.set(IWorkspacesHistoryMainService, new SyncDescriptor(WorkspacesHistoryMainService));
|
||||
services.set(IURLService, new SyncDescriptor(NativeURLService));
|
||||
services.set(IWorkspacesMainService, new SyncDescriptor(WorkspacesMainService));
|
||||
|
||||
// Telemetry
|
||||
if (!this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) {
|
||||
const channel = getDelayedChannel(sharedProcessReady.then(client => client.getChannel('telemetryAppender')));
|
||||
const appender = new TelemetryAppenderClient(channel);
|
||||
const commonProperties = resolveCommonProperties(product.commit, product.version, machineId, product.msftInternalDomains, this.environmentService.installSourcePath);
|
||||
const piiPaths = this.environmentService.extensionsPath ? [this.environmentService.appRoot, this.environmentService.extensionsPath] : [this.environmentService.appRoot];
|
||||
const config: ITelemetryServiceConfig = { appender, commonProperties, piiPaths, sendErrorTelemetry: true };
|
||||
|
||||
services.set(ITelemetryService, new SyncDescriptor(TelemetryService, [config]));
|
||||
} else {
|
||||
services.set(ITelemetryService, NullTelemetryService);
|
||||
}
|
||||
|
||||
// Init services that require it
|
||||
await backupMainService.initialize();
|
||||
|
||||
return this.instantiationService.createChild(services);
|
||||
}
|
||||
|
||||
private stopTracingEventually(windows: ICodeWindow[]): void {
|
||||
this.logService.info(`Tracing: waiting for windows to get ready...`);
|
||||
|
||||
let recordingStopped = false;
|
||||
const stopRecording = async (timeout: boolean) => {
|
||||
if (recordingStopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
recordingStopped = true; // only once
|
||||
|
||||
const path = await contentTracing.stopRecording(joinPath(this.environmentService.userHome, `${product.applicationName}-${Math.random().toString(16).slice(-4)}.trace.txt`).fsPath);
|
||||
|
||||
if (!timeout) {
|
||||
if (this.dialogMainService) {
|
||||
this.dialogMainService.showMessageBox({
|
||||
type: 'info',
|
||||
message: localize('trace.message', "Successfully created trace."),
|
||||
detail: localize('trace.detail', "Please create an issue and manually attach the following file:\n{0}", path),
|
||||
buttons: [localize('trace.ok', "OK")]
|
||||
}, withNullAsUndefined(BrowserWindow.getFocusedWindow()));
|
||||
}
|
||||
} else {
|
||||
this.logService.info(`Tracing: data recorded (after 30s timeout) to ${path}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Wait up to 30s before creating the trace anyways
|
||||
const timeoutHandle = setTimeout(() => stopRecording(true), 30000);
|
||||
|
||||
// Wait for all windows to get ready and stop tracing then
|
||||
Promise.all(windows.map(window => window.ready())).then(() => {
|
||||
clearTimeout(timeoutHandle);
|
||||
stopRecording(false);
|
||||
});
|
||||
}
|
||||
|
||||
private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise<Client<string>>): ICodeWindow[] {
|
||||
|
||||
// Register more Main IPC services
|
||||
const launchMainService = accessor.get(ILaunchMainService);
|
||||
const launchChannel = createChannelReceiver(launchMainService, { disableMarshalling: true });
|
||||
this.mainIpcServer.registerChannel('launch', launchChannel);
|
||||
|
||||
// Register more Electron IPC services
|
||||
const updateService = accessor.get(IUpdateService);
|
||||
const updateChannel = new UpdateChannel(updateService);
|
||||
electronIpcServer.registerChannel('update', updateChannel);
|
||||
|
||||
const issueMainService = accessor.get(IIssueMainService);
|
||||
const issueChannel = createChannelReceiver(issueMainService);
|
||||
electronIpcServer.registerChannel('issue', issueChannel);
|
||||
|
||||
const encryptionMainService = accessor.get(IEncryptionMainService);
|
||||
const encryptionChannel = createChannelReceiver(encryptionMainService);
|
||||
electronIpcServer.registerChannel('encryption', encryptionChannel);
|
||||
|
||||
const nativeHostMainService = this.nativeHostMainService = accessor.get(INativeHostMainService);
|
||||
const nativeHostChannel = createChannelReceiver(this.nativeHostMainService);
|
||||
electronIpcServer.registerChannel('nativeHost', nativeHostChannel);
|
||||
sharedProcessClient.then(client => client.registerChannel('nativeHost', nativeHostChannel));
|
||||
|
||||
const sharedProcessMainService = accessor.get(ISharedProcessMainService);
|
||||
const sharedProcessChannel = createChannelReceiver(sharedProcessMainService);
|
||||
electronIpcServer.registerChannel('sharedProcess', sharedProcessChannel);
|
||||
|
||||
const workspacesService = accessor.get(IWorkspacesService);
|
||||
const workspacesChannel = createChannelReceiver(workspacesService);
|
||||
electronIpcServer.registerChannel('workspaces', workspacesChannel);
|
||||
|
||||
const menubarMainService = accessor.get(IMenubarMainService);
|
||||
const menubarChannel = createChannelReceiver(menubarMainService);
|
||||
electronIpcServer.registerChannel('menubar', menubarChannel);
|
||||
|
||||
const urlService = accessor.get(IURLService);
|
||||
const urlChannel = createChannelReceiver(urlService);
|
||||
electronIpcServer.registerChannel('url', urlChannel);
|
||||
|
||||
const webviewManagerService = accessor.get(IWebviewManagerService);
|
||||
const webviewChannel = createChannelReceiver(webviewManagerService);
|
||||
electronIpcServer.registerChannel('webview', webviewChannel);
|
||||
|
||||
const storageMainService = accessor.get(IStorageMainService);
|
||||
const storageChannel = this._register(new GlobalStorageDatabaseChannel(this.logService, storageMainService));
|
||||
electronIpcServer.registerChannel('storage', storageChannel);
|
||||
sharedProcessClient.then(client => client.registerChannel('storage', storageChannel));
|
||||
|
||||
const loggerChannel = new LoggerChannel(accessor.get(ILogService));
|
||||
electronIpcServer.registerChannel('logger', loggerChannel);
|
||||
sharedProcessClient.then(client => client.registerChannel('logger', loggerChannel));
|
||||
|
||||
// ExtensionHost Debug broadcast service
|
||||
const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService);
|
||||
electronIpcServer.registerChannel(ExtensionHostDebugBroadcastChannel.ChannelName, new ElectronExtensionHostDebugBroadcastChannel(windowsMainService));
|
||||
|
||||
// Signal phase: ready (services set)
|
||||
this.lifecycleMainService.phase = LifecycleMainPhase.Ready;
|
||||
|
||||
// Propagate to clients
|
||||
this.dialogMainService = accessor.get(IDialogMainService);
|
||||
|
||||
// Check for initial URLs to handle from protocol link invocations
|
||||
const pendingWindowOpenablesFromProtocolLinks: IWindowOpenable[] = [];
|
||||
const pendingProtocolLinksToHandle = coalesce([
|
||||
|
||||
// Windows/Linux: protocol handler invokes CLI with --open-url
|
||||
...this.environmentService.args['open-url'] ? this.environmentService.args._urls || [] : [],
|
||||
|
||||
// macOS: open-url events
|
||||
...((<any>global).getOpenUrls() || []) as string[]
|
||||
].map(pendingUrlToHandle => {
|
||||
try {
|
||||
return URI.parse(pendingUrlToHandle);
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
})).filter(pendingUriToHandle => {
|
||||
// if URI should be blocked, filter it out
|
||||
if (this.shouldBlockURI(pendingUriToHandle)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// filter out any protocol link that wants to open as window so that
|
||||
// we open the right set of windows on startup and not restore the
|
||||
// previous workspace too.
|
||||
const windowOpenable = this.getWindowOpenableFromProtocolLink(pendingUriToHandle);
|
||||
if (windowOpenable) {
|
||||
pendingWindowOpenablesFromProtocolLinks.push(windowOpenable);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Create a URL handler to open file URIs in the active window
|
||||
const app = this;
|
||||
const environmentService = this.environmentService;
|
||||
urlService.registerHandler({
|
||||
async handleURL(uri: URI): Promise<boolean> {
|
||||
// if URI should be blocked, behave as if it's handled
|
||||
if (app.shouldBlockURI(uri)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for URIs to open in window
|
||||
const windowOpenableFromProtocolLink = app.getWindowOpenableFromProtocolLink(uri);
|
||||
if (windowOpenableFromProtocolLink) {
|
||||
windowsMainService.open({
|
||||
context: OpenContext.API,
|
||||
cli: { ...environmentService.args },
|
||||
urisToOpen: [windowOpenableFromProtocolLink],
|
||||
gotoLineMode: true
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we have not yet handled the URI and we have no window opened (macOS only)
|
||||
// we first open a window and then try to open that URI within that window
|
||||
if (isMacintosh && windowsMainService.getWindowCount() === 0) {
|
||||
const [window] = windowsMainService.open({
|
||||
context: OpenContext.API,
|
||||
cli: { ...environmentService.args },
|
||||
forceEmpty: true,
|
||||
gotoLineMode: true
|
||||
});
|
||||
|
||||
await window.ready();
|
||||
|
||||
return urlService.open(uri);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Create a URL handler which forwards to the last active window
|
||||
const activeWindowManager = new ActiveWindowManager({
|
||||
onDidOpenWindow: nativeHostMainService.onDidOpenWindow,
|
||||
onDidFocusWindow: nativeHostMainService.onDidFocusWindow,
|
||||
getActiveWindowId: () => nativeHostMainService.getActiveWindowId(-1)
|
||||
});
|
||||
const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id));
|
||||
const urlHandlerRouter = new URLHandlerRouter(activeWindowRouter);
|
||||
const urlHandlerChannel = electronIpcServer.getChannel('urlHandler', urlHandlerRouter);
|
||||
urlService.registerHandler(new URLHandlerChannelClient(urlHandlerChannel));
|
||||
|
||||
// Watch Electron URLs and forward them to the UrlService
|
||||
this._register(new ElectronURLListener(pendingProtocolLinksToHandle, urlService, windowsMainService, this.environmentService));
|
||||
|
||||
// Open our first window
|
||||
const args = this.environmentService.args;
|
||||
const macOpenFiles: string[] = (<any>global).macOpenFiles;
|
||||
const context = !!process.env['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP;
|
||||
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;
|
||||
|
||||
// check for a pending window to open from URI
|
||||
// e.g. when running code with --open-uri from
|
||||
// a protocol handler
|
||||
if (pendingWindowOpenablesFromProtocolLinks.length > 0) {
|
||||
return windowsMainService.open({
|
||||
context,
|
||||
cli: args,
|
||||
urisToOpen: pendingWindowOpenablesFromProtocolLinks,
|
||||
gotoLineMode: true,
|
||||
initialStartup: true
|
||||
});
|
||||
}
|
||||
|
||||
// new window if "-n"
|
||||
if (args['new-window'] && !hasCliArgs && !hasFolderURIs && !hasFileURIs) {
|
||||
return windowsMainService.open({
|
||||
context,
|
||||
cli: args,
|
||||
forceNewWindow: true,
|
||||
forceEmpty: true,
|
||||
noRecentEntry,
|
||||
waitMarkerFileURI,
|
||||
initialStartup: true
|
||||
});
|
||||
}
|
||||
|
||||
// mac: open-file event received on startup
|
||||
if (macOpenFiles.length && !hasCliArgs && !hasFolderURIs && !hasFileURIs) {
|
||||
return windowsMainService.open({
|
||||
context: OpenContext.DOCK,
|
||||
cli: args,
|
||||
urisToOpen: macOpenFiles.map(file => this.getWindowOpenableFromPathSync(file)),
|
||||
noRecentEntry,
|
||||
waitMarkerFileURI,
|
||||
initialStartup: true
|
||||
});
|
||||
}
|
||||
|
||||
// default: read paths from cli
|
||||
return windowsMainService.open({
|
||||
context,
|
||||
cli: args,
|
||||
forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']),
|
||||
diffMode: args.diff,
|
||||
noRecentEntry,
|
||||
waitMarkerFileURI,
|
||||
gotoLineMode: args.goto,
|
||||
initialStartup: true
|
||||
});
|
||||
}
|
||||
|
||||
private shouldBlockURI(uri: URI): boolean {
|
||||
if (uri.authority === Schemas.file && isWindows) {
|
||||
const res = dialog.showMessageBoxSync({
|
||||
title: product.nameLong,
|
||||
type: 'question',
|
||||
buttons: [
|
||||
mnemonicButtonLabel(localize({ key: 'open', comment: ['&& denotes a mnemonic'] }, "&&Yes")),
|
||||
mnemonicButtonLabel(localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&No")),
|
||||
],
|
||||
cancelId: 1,
|
||||
message: localize('confirmOpenMessage', "An external application wants to open '{0}' in {1}. Do you want to open this file or folder?", getPathLabel(uri.fsPath), product.nameShort),
|
||||
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'"),
|
||||
noLink: true
|
||||
});
|
||||
|
||||
if (res === 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private getWindowOpenableFromProtocolLink(uri: URI): IWindowOpenable | undefined {
|
||||
if (!uri.path) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// File path
|
||||
if (uri.authority === Schemas.file) {
|
||||
// we configure as fileUri, but later validation will
|
||||
// make sure to open as folder or workspace if possible
|
||||
return { fileUri: URI.file(uri.fsPath) };
|
||||
}
|
||||
|
||||
// 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 */);
|
||||
if (secondSlash !== -1) {
|
||||
const authority = uri.path.substring(1, secondSlash);
|
||||
const path = uri.path.substring(secondSlash);
|
||||
const remoteUri = URI.from({ scheme: Schemas.vscodeRemote, authority, path, query: uri.query, fragment: uri.fragment });
|
||||
|
||||
if (hasWorkspaceFileExtension(path)) {
|
||||
return { workspaceUri: remoteUri };
|
||||
} else {
|
||||
return { folderUri: remoteUri };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getWindowOpenableFromPathSync(path: string): IWindowOpenable {
|
||||
try {
|
||||
const fileStat = statSync(path);
|
||||
if (fileStat.isDirectory()) {
|
||||
return { folderUri: URI.file(path) };
|
||||
}
|
||||
|
||||
if (hasWorkspaceFileExtension(path)) {
|
||||
return { workspaceUri: URI.file(path) };
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore errors
|
||||
}
|
||||
|
||||
return { fileUri: URI.file(path) };
|
||||
}
|
||||
|
||||
private async afterWindowOpen(accessor: ServicesAccessor): Promise<void> {
|
||||
|
||||
// Signal phase: after window open
|
||||
this.lifecycleMainService.phase = LifecycleMainPhase.AfterWindowOpen;
|
||||
|
||||
// Remote Authorities
|
||||
this.handleRemoteAuthorities();
|
||||
|
||||
// Initialize update service
|
||||
const updateService = accessor.get(IUpdateService);
|
||||
if (updateService instanceof Win32UpdateService || updateService instanceof LinuxUpdateService || updateService instanceof DarwinUpdateService) {
|
||||
updateService.initialize();
|
||||
}
|
||||
|
||||
// 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 fileService = accessor.get(IFileService);
|
||||
const argvContent = await fileService.readFile(this.environmentService.argvResource);
|
||||
const argvString = argvContent.value.toString();
|
||||
const argvJSON = JSON.parse(stripComments(argvString));
|
||||
if (argvJSON['enable-crash-reporter'] === undefined) {
|
||||
const enableCrashReporter = this.configurationService.getValue<boolean>('telemetry.enableCrashReporter') ?? true;
|
||||
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 fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(newArgvString));
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
}
|
||||
|
||||
// Start to fetch shell environment after window has opened
|
||||
getShellEnvironment(this.logService, this.environmentService);
|
||||
}
|
||||
|
||||
private handleRemoteAuthorities(): void {
|
||||
protocol.registerHttpProtocol(Schemas.vscodeRemoteResource, (request, callback) => {
|
||||
callback({
|
||||
url: request.url.replace(/^vscode-remote-resource:/, 'http:'),
|
||||
method: request.method
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
102
lib/vscode/src/vs/code/electron-main/auth.ts
Normal file
102
lib/vscode/src/vs/code/electron-main/auth.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { FileAccess } from 'vs/base/common/network';
|
||||
import { BrowserWindow, BrowserWindowConstructorOptions, app, AuthInfo, WebContents, Event as ElectronEvent } from 'electron';
|
||||
|
||||
type LoginEvent = {
|
||||
event: ElectronEvent;
|
||||
webContents: WebContents;
|
||||
req: Request;
|
||||
authInfo: AuthInfo;
|
||||
cb: (username: string, password: string) => void;
|
||||
};
|
||||
|
||||
type Credentials = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export class ProxyAuthHandler extends Disposable {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private retryCount = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
const onLogin = Event.fromNodeEventEmitter<LoginEvent>(app, 'login', (event, webContents, req, authInfo, cb) => ({ event, webContents, req, authInfo, cb }));
|
||||
this._register(onLogin(this.onLogin, this));
|
||||
}
|
||||
|
||||
private onLogin({ event, authInfo, cb }: LoginEvent): void {
|
||||
if (!authInfo.isProxy) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.retryCount++ > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const opts: BrowserWindowConstructorOptions = {
|
||||
alwaysOnTop: true,
|
||||
skipTaskbar: true,
|
||||
resizable: false,
|
||||
width: 450,
|
||||
height: 225,
|
||||
show: true,
|
||||
title: 'VS Code',
|
||||
webPreferences: {
|
||||
preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath,
|
||||
sandbox: true,
|
||||
contextIsolation: true,
|
||||
enableWebSQL: false,
|
||||
enableRemoteModule: false,
|
||||
spellcheck: false,
|
||||
devTools: false
|
||||
}
|
||||
};
|
||||
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow) {
|
||||
opts.parent = focusedWindow;
|
||||
opts.modal = true;
|
||||
}
|
||||
|
||||
const win = new BrowserWindow(opts);
|
||||
const windowUrl = FileAccess.asBrowserUri('vs/code/electron-sandbox/proxy/auth.html', require);
|
||||
const proxyUrl = `${authInfo.host}:${authInfo.port}`;
|
||||
const title = localize('authRequire', "Proxy Authentication Required");
|
||||
const message = localize('proxyauth', "The proxy {0} requires authentication.", proxyUrl);
|
||||
|
||||
const onWindowClose = () => cb('', '');
|
||||
win.on('close', onWindowClose);
|
||||
|
||||
win.setMenu(null);
|
||||
win.webContents.on('did-finish-load', () => {
|
||||
const data = { title, message };
|
||||
win.webContents.send('vscode:openProxyAuthDialog', data);
|
||||
});
|
||||
win.webContents.on('ipc-message', (event, channel, credentials: Credentials) => {
|
||||
if (channel === 'vscode:proxyAuthResponse') {
|
||||
const { username, password } = credentials;
|
||||
cb(username, password);
|
||||
win.removeListener('close', onWindowClose);
|
||||
win.close();
|
||||
}
|
||||
});
|
||||
win.loadURL(windowUrl.toString(true));
|
||||
}
|
||||
}
|
||||
241
lib/vscode/src/vs/code/electron-main/auth2.ts
Normal file
241
lib/vscode/src/vs/code/electron-main/auth2.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { hash } from 'vs/base/common/hash';
|
||||
import { app, AuthInfo, WebContents, Event as ElectronEvent, AuthenticationResponseDetails } from 'electron';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
|
||||
import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService';
|
||||
import { IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
|
||||
interface ElectronAuthenticationResponseDetails extends AuthenticationResponseDetails {
|
||||
firstAuthAttempt?: boolean; // https://github.com/electron/electron/blob/84a42a050e7d45225e69df5bd2d2bf9f1037ea41/shell/browser/login_handler.cc#L70
|
||||
}
|
||||
|
||||
type LoginEvent = {
|
||||
event: ElectronEvent;
|
||||
authInfo: AuthInfo;
|
||||
req: ElectronAuthenticationResponseDetails;
|
||||
|
||||
callback: (username?: string, password?: string) => void;
|
||||
};
|
||||
|
||||
type Credentials = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
enum ProxyAuthState {
|
||||
|
||||
/**
|
||||
* Initial state: we will try to use stored credentials
|
||||
* first to reply to the auth challenge.
|
||||
*/
|
||||
Initial = 1,
|
||||
|
||||
/**
|
||||
* We used stored credentials and are still challenged,
|
||||
* so we will show a login dialog next.
|
||||
*/
|
||||
StoredCredentialsUsed,
|
||||
|
||||
/**
|
||||
* Finally, if we showed a login dialog already, we will
|
||||
* not show any more login dialogs until restart to reduce
|
||||
* the UI noise.
|
||||
*/
|
||||
LoginDialogShown
|
||||
}
|
||||
|
||||
export class ProxyAuthHandler2 extends Disposable {
|
||||
|
||||
private static PROXY_CREDENTIALS_SERVICE_KEY = `${product.urlProtocol}.proxy-credentials`;
|
||||
|
||||
private pendingProxyResolve: Promise<Credentials | undefined> | undefined = undefined;
|
||||
|
||||
private state = ProxyAuthState.Initial;
|
||||
|
||||
private sessionCredentials: Credentials | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
|
||||
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,
|
||||
@IEncryptionMainService private readonly encryptionMainService: IEncryptionMainService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
const onLogin = Event.fromNodeEventEmitter<LoginEvent>(app, 'login', (event: ElectronEvent, webContents: WebContents, req: ElectronAuthenticationResponseDetails, authInfo: AuthInfo, callback) => ({ event, webContents, req, authInfo, callback }));
|
||||
this._register(onLogin(this.onLogin, this));
|
||||
}
|
||||
|
||||
private async onLogin({ event, authInfo, req, callback }: LoginEvent): Promise<void> {
|
||||
if (!authInfo.isProxy) {
|
||||
return; // only for proxy
|
||||
}
|
||||
|
||||
if (!this.pendingProxyResolve && this.state === ProxyAuthState.LoginDialogShown && req.firstAuthAttempt) {
|
||||
this.logService.trace('auth#onLogin (proxy) - exit - proxy dialog already shown');
|
||||
|
||||
return; // only one dialog per session at max (except when firstAuthAttempt: false which indicates a login problem)
|
||||
}
|
||||
|
||||
// Signal we handle this event on our own, otherwise
|
||||
// Electron will ignore our provided credentials.
|
||||
event.preventDefault();
|
||||
|
||||
let credentials: Credentials | undefined = undefined;
|
||||
if (!this.pendingProxyResolve) {
|
||||
this.logService.trace('auth#onLogin (proxy) - no pending proxy handling found, starting new');
|
||||
|
||||
this.pendingProxyResolve = this.resolveProxyCredentials(authInfo);
|
||||
try {
|
||||
credentials = await this.pendingProxyResolve;
|
||||
} finally {
|
||||
this.pendingProxyResolve = undefined;
|
||||
}
|
||||
} else {
|
||||
this.logService.trace('auth#onLogin (proxy) - pending proxy handling found');
|
||||
|
||||
credentials = await this.pendingProxyResolve;
|
||||
}
|
||||
|
||||
// According to Electron docs, it is fine to call back without
|
||||
// username or password to signal that the authentication was handled
|
||||
// by us, even though without having credentials received:
|
||||
//
|
||||
// > If `callback` is called without a username or password, the authentication
|
||||
// > request will be cancelled and the authentication error will be returned to the
|
||||
// > page.
|
||||
callback(credentials?.username, credentials?.password);
|
||||
}
|
||||
|
||||
private async resolveProxyCredentials(authInfo: AuthInfo): Promise<Credentials | undefined> {
|
||||
this.logService.trace('auth#resolveProxyCredentials (proxy) - enter');
|
||||
|
||||
try {
|
||||
const credentials = await this.doResolveProxyCredentials(authInfo);
|
||||
if (credentials) {
|
||||
this.logService.trace('auth#resolveProxyCredentials (proxy) - got credentials');
|
||||
|
||||
return credentials;
|
||||
} else {
|
||||
this.logService.trace('auth#resolveProxyCredentials (proxy) - did not get credentials');
|
||||
}
|
||||
} finally {
|
||||
this.logService.trace('auth#resolveProxyCredentials (proxy) - exit');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async doResolveProxyCredentials(authInfo: AuthInfo): Promise<Credentials | undefined> {
|
||||
this.logService.trace('auth#doResolveProxyCredentials - enter', authInfo);
|
||||
|
||||
// Compute a hash over the authentication info to be used
|
||||
// with the credentials store to return the right credentials
|
||||
// given the properties of the auth request
|
||||
// (see https://github.com/microsoft/vscode/issues/109497)
|
||||
const authInfoHash = String(hash({ scheme: authInfo.scheme, host: authInfo.host, port: authInfo.port }));
|
||||
|
||||
// Find any previously stored credentials
|
||||
let storedUsername: string | undefined = undefined;
|
||||
let storedPassword: string | undefined = undefined;
|
||||
try {
|
||||
const encryptedSerializedProxyCredentials = await this.nativeHostMainService.getPassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash);
|
||||
if (encryptedSerializedProxyCredentials) {
|
||||
const credentials: Credentials = JSON.parse(await this.encryptionMainService.decrypt(encryptedSerializedProxyCredentials));
|
||||
|
||||
storedUsername = credentials.username;
|
||||
storedPassword = credentials.password;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(error); // handle errors by asking user for login via dialog
|
||||
}
|
||||
|
||||
// Reply with stored credentials unless we used them already.
|
||||
// In that case we need to show a login dialog again because
|
||||
// they seem invalid.
|
||||
if (this.state !== ProxyAuthState.StoredCredentialsUsed && typeof storedUsername === 'string' && typeof storedPassword === 'string') {
|
||||
this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - found stored credentials to use');
|
||||
this.state = ProxyAuthState.StoredCredentialsUsed;
|
||||
|
||||
return { username: storedUsername, password: storedPassword };
|
||||
}
|
||||
|
||||
// Find suitable window to show dialog: prefer to show it in the
|
||||
// active window because any other network request will wait on
|
||||
// the credentials and we want the user to present the dialog.
|
||||
const window = this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow();
|
||||
if (!window) {
|
||||
this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - no opened window found to show dialog in');
|
||||
|
||||
return undefined; // unexpected
|
||||
}
|
||||
|
||||
this.logService.trace(`auth#doResolveProxyCredentials (proxy) - asking window ${window.id} to handle proxy login`);
|
||||
|
||||
// Open proxy dialog
|
||||
const payload = {
|
||||
authInfo,
|
||||
username: this.sessionCredentials?.username ?? storedUsername, // prefer to show already used username (if any) over stored
|
||||
password: this.sessionCredentials?.password ?? storedPassword, // prefer to show already used password (if any) over stored
|
||||
replyChannel: `vscode:proxyAuthResponse:${generateUuid()}`
|
||||
};
|
||||
window.sendWhenReady('vscode:openProxyAuthenticationDialog', payload);
|
||||
this.state = ProxyAuthState.LoginDialogShown;
|
||||
|
||||
// Handle reply
|
||||
const loginDialogCredentials = await new Promise<Credentials | undefined>(resolve => {
|
||||
const proxyAuthResponseHandler = async (event: ElectronEvent, channel: string, reply: Credentials & { remember: boolean } | undefined /* canceled */) => {
|
||||
if (channel === payload.replyChannel) {
|
||||
this.logService.trace(`auth#doResolveProxyCredentials - exit - received credentials from window ${window.id}`);
|
||||
window.win.webContents.off('ipc-message', proxyAuthResponseHandler);
|
||||
|
||||
// We got credentials from the window
|
||||
if (reply) {
|
||||
const credentials: Credentials = { username: reply.username, password: reply.password };
|
||||
|
||||
// Update stored credentials based on `remember` flag
|
||||
try {
|
||||
if (reply.remember) {
|
||||
const encryptedSerializedCredentials = await this.encryptionMainService.encrypt(JSON.stringify(credentials));
|
||||
await this.nativeHostMainService.setPassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash, encryptedSerializedCredentials);
|
||||
} else {
|
||||
await this.nativeHostMainService.deletePassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(error); // handle gracefully
|
||||
}
|
||||
|
||||
resolve({ username: credentials.username, password: credentials.password });
|
||||
}
|
||||
|
||||
// We did not get any credentials from the window (e.g. cancelled)
|
||||
else {
|
||||
resolve(undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.win.webContents.on('ipc-message', proxyAuthResponseHandler);
|
||||
});
|
||||
|
||||
// Remember credentials for the session in case
|
||||
// the credentials are wrong and we show the dialog
|
||||
// again
|
||||
this.sessionCredentials = loginDialogCredentials;
|
||||
|
||||
return loginDialogCredentials;
|
||||
}
|
||||
}
|
||||
513
lib/vscode/src/vs/code/electron-main/main.ts
Normal file
513
lib/vscode/src/vs/code/electron-main/main.ts
Normal file
@@ -0,0 +1,513 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/platform/update/common/update.config.contribution';
|
||||
import { app, dialog } from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import { isWindows, IProcessEnvironment, isMacintosh } from 'vs/base/common/platform';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
import { parseMainProcessArgv, addArg } from 'vs/platform/environment/node/argvHelper';
|
||||
import { createWaitMarkerFile } from 'vs/platform/environment/node/waitMarkerFile';
|
||||
import { mkdirp } from 'vs/base/node/pfs';
|
||||
import { LifecycleMainService, ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
|
||||
import { Server, serve, connect, XDG_RUNTIME_DIR } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ILaunchMainService } from 'vs/platform/launch/electron-main/launchMainService';
|
||||
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
|
||||
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { ILogService, ConsoleLogMainService, MultiplexLogService, getLogLevel } from 'vs/platform/log/common/log';
|
||||
import { StateService } from 'vs/platform/state/node/stateService';
|
||||
import { IStateService } from 'vs/platform/state/node/state';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ConfigurationService } from 'vs/platform/configuration/common/configurationService';
|
||||
import { IRequestService } from 'vs/platform/request/common/request';
|
||||
import { RequestMainService } from 'vs/platform/request/electron-main/requestMainService';
|
||||
import { CodeApplication } from 'vs/code/electron-main/app';
|
||||
import { localize } from 'vs/nls';
|
||||
import { mnemonicButtonLabel } from 'vs/base/common/labels';
|
||||
import { SpdLogService } from 'vs/platform/log/node/spdlogService';
|
||||
import { BufferLogService } from 'vs/platform/log/common/bufferLog';
|
||||
import { setUnexpectedErrorHandler } from 'vs/base/common/errors';
|
||||
import { IThemeMainService, ThemeMainService } from 'vs/platform/theme/electron-main/themeMainService';
|
||||
import { Client } from 'vs/base/parts/ipc/common/ipc.net';
|
||||
import { once } from 'vs/base/common/functional';
|
||||
import { ISignService } from 'vs/platform/sign/common/sign';
|
||||
import { SignService } from 'vs/platform/sign/node/signService';
|
||||
import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { ITunnelService } from 'vs/platform/remote/common/tunnel';
|
||||
import { TunnelService } from 'vs/platform/remote/node/tunnelService';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { IPathWithLineAndColumn, isValidBasename, parseLineAndColumnAware, sanitizeFilePath } from 'vs/base/common/extpath';
|
||||
import { isNumber } from 'vs/base/common/types';
|
||||
import { rtrim, trim } from 'vs/base/common/strings';
|
||||
import { basename, resolve } from 'vs/base/common/path';
|
||||
import { coalesce, distinct } from 'vs/base/common/arrays';
|
||||
import { EnvironmentMainService, IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
|
||||
|
||||
class ExpectedError extends Error {
|
||||
readonly isExpected = true;
|
||||
}
|
||||
|
||||
class CodeMain {
|
||||
|
||||
main(): void {
|
||||
|
||||
// Set the error handler early enough so that we are not getting the
|
||||
// default electron error dialog popping up
|
||||
setUnexpectedErrorHandler(err => console.error(err));
|
||||
|
||||
// Parse arguments
|
||||
let args: NativeParsedArgs;
|
||||
try {
|
||||
args = parseMainProcessArgv(process.argv);
|
||||
args = this.validatePaths(args);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
app.exit(1);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If we are started with --wait create a random temporary file
|
||||
// and pass it over to the starting instance. We can use this file
|
||||
// to wait for it to be deleted to monitor that the edited file
|
||||
// is closed and then exit the waiting process.
|
||||
//
|
||||
// Note: we are not doing this if the wait marker has been already
|
||||
// added as argument. This can happen if Code was started from CLI.
|
||||
if (args.wait && !args.waitMarkerFilePath) {
|
||||
const waitMarkerFilePath = createWaitMarkerFile(args.verbose);
|
||||
if (waitMarkerFilePath) {
|
||||
addArg(process.argv, '--waitMarkerFilePath', waitMarkerFilePath);
|
||||
args.waitMarkerFilePath = waitMarkerFilePath;
|
||||
}
|
||||
}
|
||||
|
||||
// Launch
|
||||
this.startup(args);
|
||||
}
|
||||
|
||||
private async startup(args: NativeParsedArgs): Promise<void> {
|
||||
|
||||
// We need to buffer the spdlog logs until we are sure
|
||||
// we are the only instance running, otherwise we'll have concurrent
|
||||
// log file access on Windows (https://github.com/microsoft/vscode/issues/41218)
|
||||
const bufferLogService = new BufferLogService();
|
||||
|
||||
const [instantiationService, instanceEnvironment, environmentService] = this.createServices(args, bufferLogService);
|
||||
try {
|
||||
|
||||
// Init services
|
||||
await instantiationService.invokeFunction(async accessor => {
|
||||
const configurationService = accessor.get(IConfigurationService);
|
||||
const stateService = accessor.get(IStateService);
|
||||
|
||||
try {
|
||||
await this.initServices(environmentService, configurationService as ConfigurationService, stateService as StateService);
|
||||
} catch (error) {
|
||||
|
||||
// Show a dialog for errors that can be resolved by the user
|
||||
this.handleStartupDataDirError(environmentService, error);
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// Startup
|
||||
await instantiationService.invokeFunction(async accessor => {
|
||||
const logService = accessor.get(ILogService);
|
||||
const lifecycleMainService = accessor.get(ILifecycleMainService);
|
||||
const fileService = accessor.get(IFileService);
|
||||
const configurationService = accessor.get(IConfigurationService);
|
||||
|
||||
const mainIpcServer = await this.doStartup(args, logService, environmentService, lifecycleMainService, instantiationService, true);
|
||||
|
||||
bufferLogService.logger = new SpdLogService('main', environmentService.logsPath, bufferLogService.getLevel());
|
||||
once(lifecycleMainService.onWillShutdown)(() => {
|
||||
fileService.dispose();
|
||||
(configurationService as ConfigurationService).dispose();
|
||||
});
|
||||
|
||||
return instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment).startup();
|
||||
});
|
||||
} catch (error) {
|
||||
instantiationService.invokeFunction(this.quit, error);
|
||||
}
|
||||
}
|
||||
|
||||
private createServices(args: NativeParsedArgs, bufferLogService: BufferLogService): [IInstantiationService, IProcessEnvironment, IEnvironmentMainService] {
|
||||
const services = new ServiceCollection();
|
||||
|
||||
const environmentService = new EnvironmentMainService(args);
|
||||
const instanceEnvironment = this.patchEnvironment(environmentService); // Patch `process.env` with the instance's environment
|
||||
services.set(IEnvironmentService, environmentService);
|
||||
services.set(IEnvironmentMainService, environmentService);
|
||||
|
||||
const logService = new MultiplexLogService([new ConsoleLogMainService(getLogLevel(environmentService)), bufferLogService]);
|
||||
process.once('exit', () => logService.dispose());
|
||||
services.set(ILogService, logService);
|
||||
|
||||
const fileService = new FileService(logService);
|
||||
services.set(IFileService, fileService);
|
||||
const diskFileSystemProvider = new DiskFileSystemProvider(logService);
|
||||
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
|
||||
|
||||
services.set(IConfigurationService, new ConfigurationService(environmentService.settingsResource, fileService));
|
||||
services.set(ILifecycleMainService, new SyncDescriptor(LifecycleMainService));
|
||||
services.set(IStateService, new SyncDescriptor(StateService));
|
||||
services.set(IRequestService, new SyncDescriptor(RequestMainService));
|
||||
services.set(IThemeMainService, new SyncDescriptor(ThemeMainService));
|
||||
services.set(ISignService, new SyncDescriptor(SignService));
|
||||
services.set(IProductService, { _serviceBrand: undefined, ...product });
|
||||
services.set(ITunnelService, new SyncDescriptor(TunnelService));
|
||||
|
||||
return [new InstantiationService(services, true), instanceEnvironment, environmentService];
|
||||
}
|
||||
|
||||
private initServices(environmentService: IEnvironmentMainService, configurationService: ConfigurationService, stateService: StateService): Promise<unknown> {
|
||||
|
||||
// Environment service (paths)
|
||||
const environmentServiceInitialization = Promise.all<void | undefined>([
|
||||
environmentService.extensionsPath,
|
||||
environmentService.nodeCachedDataDir,
|
||||
environmentService.logsPath,
|
||||
environmentService.globalStorageHome.fsPath,
|
||||
environmentService.workspaceStorageHome.fsPath,
|
||||
environmentService.backupHome
|
||||
].map((path): undefined | Promise<void> => path ? mkdirp(path) : undefined));
|
||||
|
||||
// Configuration service
|
||||
const configurationServiceInitialization = configurationService.initialize();
|
||||
|
||||
// State service
|
||||
const stateServiceInitialization = stateService.init();
|
||||
|
||||
return Promise.all([environmentServiceInitialization, configurationServiceInitialization, stateServiceInitialization]);
|
||||
}
|
||||
|
||||
private patchEnvironment(environmentService: IEnvironmentMainService): IProcessEnvironment {
|
||||
const instanceEnvironment: IProcessEnvironment = {
|
||||
VSCODE_IPC_HOOK: environmentService.mainIPCHandle
|
||||
};
|
||||
|
||||
['VSCODE_NLS_CONFIG', 'VSCODE_LOGS', 'VSCODE_PORTABLE'].forEach(key => {
|
||||
const value = process.env[key];
|
||||
if (typeof value === 'string') {
|
||||
instanceEnvironment[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
Object.assign(process.env, instanceEnvironment);
|
||||
|
||||
return instanceEnvironment;
|
||||
}
|
||||
|
||||
private async doStartup(args: NativeParsedArgs, logService: ILogService, environmentService: IEnvironmentMainService, lifecycleMainService: ILifecycleMainService, instantiationService: IInstantiationService, retry: boolean): Promise<Server> {
|
||||
|
||||
// Try to setup a server for running. If that succeeds it means
|
||||
// we are the first instance to startup. Otherwise it is likely
|
||||
// that another instance is already running.
|
||||
let server: Server;
|
||||
try {
|
||||
server = await serve(environmentService.mainIPCHandle);
|
||||
once(lifecycleMainService.onWillShutdown)(() => server.dispose());
|
||||
} catch (error) {
|
||||
|
||||
// Handle unexpected errors (the only expected error is EADDRINUSE that
|
||||
// indicates a second instance of Code is running)
|
||||
if (error.code !== 'EADDRINUSE') {
|
||||
|
||||
// Show a dialog for errors that can be resolved by the user
|
||||
this.handleStartupDataDirError(environmentService, error);
|
||||
|
||||
// Any other runtime error is just printed to the console
|
||||
throw error;
|
||||
}
|
||||
|
||||
// there's a running instance, let's connect to it
|
||||
let client: Client<string>;
|
||||
try {
|
||||
client = await connect(environmentService.mainIPCHandle, 'main');
|
||||
} catch (error) {
|
||||
|
||||
// Handle unexpected connection errors by showing a dialog to the user
|
||||
if (!retry || isWindows || error.code !== 'ECONNREFUSED') {
|
||||
if (error.code === 'EPERM') {
|
||||
this.showStartupWarningDialog(
|
||||
localize('secondInstanceAdmin', "A second instance of {0} is already running as administrator.", product.nameShort),
|
||||
localize('secondInstanceAdminDetail', "Please close the other instance and try again.")
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
// it happens on Linux and OS X that the pipe is left behind
|
||||
// let's delete it, since we can't connect to it and then
|
||||
// retry the whole thing
|
||||
try {
|
||||
fs.unlinkSync(environmentService.mainIPCHandle);
|
||||
} catch (error) {
|
||||
logService.warn('Could not delete obsolete instance handle', error);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return this.doStartup(args, logService, environmentService, lifecycleMainService, instantiationService, false);
|
||||
}
|
||||
|
||||
// Tests from CLI require to be the only instance currently
|
||||
if (environmentService.extensionTestsLocationURI && !environmentService.debugExtensionHost.break) {
|
||||
const msg = 'Running extension tests from the command line is currently only supported if no other instance of Code is running.';
|
||||
logService.error(msg);
|
||||
client.dispose();
|
||||
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
// Show a warning dialog after some timeout if it takes long to talk to the other instance
|
||||
// Skip this if we are running with --wait where it is expected that we wait for a while.
|
||||
// Also skip when gathering diagnostics (--status) which can take a longer time.
|
||||
let startupWarningDialogHandle: NodeJS.Timeout | undefined = undefined;
|
||||
if (!args.wait && !args.status) {
|
||||
startupWarningDialogHandle = setTimeout(() => {
|
||||
this.showStartupWarningDialog(
|
||||
localize('secondInstanceNoResponse', "Another instance of {0} is running but not responding", product.nameShort),
|
||||
localize('secondInstanceNoResponseDetail', "Please close all other instances and try again.")
|
||||
);
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
const launchService = createChannelSender<ILaunchMainService>(client.getChannel('launch'), { disableMarshalling: true });
|
||||
|
||||
// Process Info
|
||||
if (args.status) {
|
||||
return instantiationService.invokeFunction(async () => {
|
||||
|
||||
// Create a diagnostic service connected to the existing shared process
|
||||
const sharedProcessClient = await connect(environmentService.sharedIPCHandle, 'main');
|
||||
const diagnosticsChannel = sharedProcessClient.getChannel('diagnostics');
|
||||
const diagnosticsService = createChannelSender<IDiagnosticsService>(diagnosticsChannel);
|
||||
const mainProcessInfo = await launchService.getMainProcessInfo();
|
||||
const remoteDiagnostics = await launchService.getRemoteDiagnostics({ includeProcesses: true, includeWorkspaceMetadata: true });
|
||||
const diagnostics = await diagnosticsService.getDiagnostics(mainProcessInfo, remoteDiagnostics);
|
||||
console.log(diagnostics);
|
||||
|
||||
throw new ExpectedError();
|
||||
});
|
||||
}
|
||||
|
||||
// Windows: allow to set foreground
|
||||
if (isWindows) {
|
||||
await this.windowsAllowSetForegroundWindow(launchService, logService);
|
||||
}
|
||||
|
||||
// Send environment over...
|
||||
logService.trace('Sending env to running instance...');
|
||||
await launchService.start(args, process.env as IProcessEnvironment);
|
||||
|
||||
// Cleanup
|
||||
client.dispose();
|
||||
|
||||
// Now that we started, make sure the warning dialog is prevented
|
||||
if (startupWarningDialogHandle) {
|
||||
clearTimeout(startupWarningDialogHandle);
|
||||
}
|
||||
|
||||
throw new ExpectedError('Sent env to running instance. Terminating...');
|
||||
}
|
||||
|
||||
// Print --status usage info
|
||||
if (args.status) {
|
||||
logService.warn('Warning: The --status argument can only be used if Code is already running. Please run it again after Code has started.');
|
||||
|
||||
throw new ExpectedError('Terminating...');
|
||||
}
|
||||
|
||||
// Set the VSCODE_PID variable here when we are sure we are the first
|
||||
// instance to startup. Otherwise we would wrongly overwrite the PID
|
||||
process.env['VSCODE_PID'] = String(process.pid);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
private handleStartupDataDirError(environmentService: IEnvironmentMainService, error: NodeJS.ErrnoException): void {
|
||||
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
||||
const directories = [environmentService.userDataPath];
|
||||
|
||||
if (environmentService.extensionsPath) {
|
||||
directories.push(environmentService.extensionsPath);
|
||||
}
|
||||
|
||||
if (XDG_RUNTIME_DIR) {
|
||||
directories.push(XDG_RUNTIME_DIR);
|
||||
}
|
||||
|
||||
this.showStartupWarningDialog(
|
||||
localize('startupDataDirError', "Unable to write program user data."),
|
||||
localize('startupUserDataAndExtensionsDirErrorDetail', "Please make sure the following directories are writeable:\n\n{0}", directories.join('\n'))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private showStartupWarningDialog(message: string, detail: string): void {
|
||||
// use sync variant here because we likely exit after this method
|
||||
// due to startup issues and otherwise the dialog seems to disappear
|
||||
// https://github.com/microsoft/vscode/issues/104493
|
||||
dialog.showMessageBoxSync({
|
||||
title: product.nameLong,
|
||||
type: 'warning',
|
||||
buttons: [mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))],
|
||||
message,
|
||||
detail,
|
||||
noLink: true
|
||||
});
|
||||
}
|
||||
|
||||
private async windowsAllowSetForegroundWindow(launchService: ILaunchMainService, logService: ILogService): Promise<void> {
|
||||
if (isWindows) {
|
||||
const processId = await launchService.getMainProcessId();
|
||||
|
||||
logService.trace('Sending some foreground love to the running instance:', processId);
|
||||
|
||||
try {
|
||||
(await import('windows-foreground-love')).allowSetForegroundWindow(processId);
|
||||
} catch (error) {
|
||||
logService.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private quit(accessor: ServicesAccessor, reason?: ExpectedError | Error): void {
|
||||
const logService = accessor.get(ILogService);
|
||||
const lifecycleMainService = accessor.get(ILifecycleMainService);
|
||||
|
||||
let exitCode = 0;
|
||||
|
||||
if (reason) {
|
||||
if ((reason as ExpectedError).isExpected) {
|
||||
if (reason.message) {
|
||||
logService.trace(reason.message);
|
||||
}
|
||||
} else {
|
||||
exitCode = 1; // signal error to the outside
|
||||
|
||||
if (reason.stack) {
|
||||
logService.error(reason.stack);
|
||||
} else {
|
||||
logService.error(`Startup error: ${reason.toString()}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleMainService.kill(exitCode);
|
||||
}
|
||||
|
||||
//#region Helpers
|
||||
|
||||
private validatePaths(args: NativeParsedArgs): NativeParsedArgs {
|
||||
|
||||
// Track URLs if they're going to be used
|
||||
if (args['open-url']) {
|
||||
args._urls = args._;
|
||||
args._ = [];
|
||||
}
|
||||
|
||||
// Normalize paths and watch out for goto line mode
|
||||
if (!args['remote']) {
|
||||
const paths = this.doValidatePaths(args._, args.goto);
|
||||
args._ = paths;
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
private doValidatePaths(args: string[], gotoLineMode?: boolean): string[] {
|
||||
const cwd = process.env['VSCODE_CWD'] || process.cwd();
|
||||
const result = args.map(arg => {
|
||||
let pathCandidate = String(arg);
|
||||
|
||||
let parsedPath: IPathWithLineAndColumn | undefined = undefined;
|
||||
if (gotoLineMode) {
|
||||
parsedPath = parseLineAndColumnAware(pathCandidate);
|
||||
pathCandidate = parsedPath.path;
|
||||
}
|
||||
|
||||
if (pathCandidate) {
|
||||
pathCandidate = this.preparePath(cwd, pathCandidate);
|
||||
}
|
||||
|
||||
const sanitizedFilePath = sanitizeFilePath(pathCandidate, cwd);
|
||||
|
||||
const filePathBasename = basename(sanitizedFilePath);
|
||||
if (filePathBasename /* can be empty if code is opened on root */ && !isValidBasename(filePathBasename)) {
|
||||
return null; // do not allow invalid file names
|
||||
}
|
||||
|
||||
if (gotoLineMode && parsedPath) {
|
||||
parsedPath.path = sanitizedFilePath;
|
||||
|
||||
return this.toPath(parsedPath);
|
||||
}
|
||||
|
||||
return sanitizedFilePath;
|
||||
});
|
||||
|
||||
const caseInsensitive = isWindows || isMacintosh;
|
||||
const distinctPaths = distinct(result, path => path && caseInsensitive ? path.toLowerCase() : (path || ''));
|
||||
|
||||
return coalesce(distinctPaths);
|
||||
}
|
||||
|
||||
private preparePath(cwd: string, path: string): string {
|
||||
|
||||
// Trim trailing quotes
|
||||
if (isWindows) {
|
||||
path = rtrim(path, '"'); // https://github.com/microsoft/vscode/issues/1498
|
||||
}
|
||||
|
||||
// Trim whitespaces
|
||||
path = trim(trim(path, ' '), '\t');
|
||||
|
||||
if (isWindows) {
|
||||
|
||||
// Resolve the path against cwd if it is relative
|
||||
path = resolve(cwd, path);
|
||||
|
||||
// Trim trailing '.' chars on Windows to prevent invalid file names
|
||||
path = rtrim(path, '.');
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private toPath(pathWithLineAndCol: IPathWithLineAndColumn): string {
|
||||
const segments = [pathWithLineAndCol.path];
|
||||
|
||||
if (isNumber(pathWithLineAndCol.line)) {
|
||||
segments.push(String(pathWithLineAndCol.line));
|
||||
}
|
||||
|
||||
if (isNumber(pathWithLineAndCol.column)) {
|
||||
segments.push(String(pathWithLineAndCol.column));
|
||||
}
|
||||
|
||||
return segments.join(':');
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
// Main Startup
|
||||
const code = new CodeMain();
|
||||
code.main();
|
||||
168
lib/vscode/src/vs/code/electron-main/sharedProcess.ts
Normal file
168
lib/vscode/src/vs/code/electron-main/sharedProcess.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
|
||||
import { BrowserWindow, ipcMain, WebContents, Event as ElectronEvent } from 'electron';
|
||||
import { ISharedProcess } from 'vs/platform/ipc/electron-main/sharedProcessMainService';
|
||||
import { Barrier } from 'vs/base/common/async';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
|
||||
import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService';
|
||||
import { toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { FileAccess } from 'vs/base/common/network';
|
||||
|
||||
export class SharedProcess implements ISharedProcess {
|
||||
|
||||
private barrier = new Barrier();
|
||||
|
||||
private window: BrowserWindow | null = null;
|
||||
|
||||
private readonly _whenReady: Promise<void>;
|
||||
|
||||
constructor(
|
||||
private readonly machineId: string,
|
||||
private userEnv: NodeJS.ProcessEnv,
|
||||
@IEnvironmentMainService private readonly environmentService: IEnvironmentMainService,
|
||||
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IThemeMainService private readonly themeMainService: IThemeMainService
|
||||
) {
|
||||
// overall ready promise when shared process signals initialization is done
|
||||
this._whenReady = new Promise<void>(c => ipcMain.once('vscode:shared-process->electron-main=init-done', () => c(undefined)));
|
||||
}
|
||||
|
||||
@memoize
|
||||
private get _whenIpcReady(): Promise<void> {
|
||||
this.window = new BrowserWindow({
|
||||
show: false,
|
||||
backgroundColor: this.themeMainService.getBackgroundColor(),
|
||||
webPreferences: {
|
||||
preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath,
|
||||
nodeIntegration: true,
|
||||
enableWebSQL: false,
|
||||
enableRemoteModule: false,
|
||||
spellcheck: false,
|
||||
nativeWindowOpen: true,
|
||||
images: false,
|
||||
webgl: false,
|
||||
disableBlinkFeatures: 'Auxclick' // do NOT change, allows us to identify this window as shared-process in the process explorer
|
||||
}
|
||||
});
|
||||
const config = {
|
||||
appRoot: this.environmentService.appRoot,
|
||||
machineId: this.machineId,
|
||||
nodeCachedDataDir: this.environmentService.nodeCachedDataDir,
|
||||
userEnv: this.userEnv,
|
||||
windowId: this.window.id
|
||||
};
|
||||
|
||||
const windowUrl = FileAccess
|
||||
.asBrowserUri('vs/code/electron-browser/sharedProcess/sharedProcess.html', require)
|
||||
.with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` });
|
||||
this.window.loadURL(windowUrl.toString(true));
|
||||
|
||||
// Prevent the window from dying
|
||||
const onClose = (e: ElectronEvent) => {
|
||||
this.logService.trace('SharedProcess#close prevented');
|
||||
|
||||
// We never allow to close the shared process unless we get explicitly disposed()
|
||||
e.preventDefault();
|
||||
|
||||
// Still hide the window though if visible
|
||||
if (this.window && this.window.isVisible()) {
|
||||
this.window.hide();
|
||||
}
|
||||
};
|
||||
|
||||
this.window.on('close', onClose);
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
this.lifecycleMainService.onWillShutdown(() => {
|
||||
disposables.dispose();
|
||||
|
||||
// Shut the shared process down when we are quitting
|
||||
//
|
||||
// Note: because we veto the window close, we must first remove our veto.
|
||||
// Otherwise the application would never quit because the shared process
|
||||
// window is refusing to close!
|
||||
//
|
||||
if (this.window) {
|
||||
this.window.removeListener('close', onClose);
|
||||
}
|
||||
|
||||
// Electron seems to crash on Windows without this setTimeout :|
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (this.window) {
|
||||
this.window.close();
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore, as electron is already shutting down
|
||||
}
|
||||
|
||||
this.window = null;
|
||||
}, 0);
|
||||
});
|
||||
|
||||
return new Promise<void>(c => {
|
||||
// send payload once shared process is ready to receive it
|
||||
disposables.add(Event.once(Event.fromNodeEventEmitter(ipcMain, 'vscode:shared-process->electron-main=ready-for-payload', ({ sender }: { sender: WebContents }) => sender))(sender => {
|
||||
sender.send('vscode:electron-main->shared-process=payload', {
|
||||
sharedIPCHandle: this.environmentService.sharedIPCHandle,
|
||||
args: this.environmentService.args,
|
||||
logLevel: this.logService.getLevel(),
|
||||
backupWorkspacesPath: this.environmentService.backupWorkspacesPath,
|
||||
nodeCachedDataDir: this.environmentService.nodeCachedDataDir
|
||||
});
|
||||
|
||||
// signal exit to shared process when we get disposed
|
||||
disposables.add(toDisposable(() => sender.send('vscode:electron-main->shared-process=exit')));
|
||||
|
||||
// complete IPC-ready promise when shared process signals this to us
|
||||
ipcMain.once('vscode:shared-process->electron-main=ipc-ready', () => c(undefined));
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
spawn(userEnv: NodeJS.ProcessEnv): void {
|
||||
this.userEnv = { ...this.userEnv, ...userEnv };
|
||||
this.barrier.open();
|
||||
}
|
||||
|
||||
async whenReady(): Promise<void> {
|
||||
await this.barrier.wait();
|
||||
await this._whenReady;
|
||||
}
|
||||
|
||||
async whenIpcReady(): Promise<void> {
|
||||
await this.barrier.wait();
|
||||
await this._whenIpcReady;
|
||||
}
|
||||
|
||||
toggle(): void {
|
||||
if (!this.window || this.window.isVisible()) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
|
||||
show(): void {
|
||||
if (this.window) {
|
||||
this.window.show();
|
||||
this.window.webContents.openDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
if (this.window) {
|
||||
this.window.webContents.closeDevTools();
|
||||
this.window.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
1329
lib/vscode/src/vs/code/electron-main/window.ts
Normal file
1329
lib/vscode/src/vs/code/electron-main/window.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user