-
Notifications
You must be signed in to change notification settings - Fork 4
Description
VS Code: The Startup Process Of The Desktop Application
In this post, we will only briefly cover the process from the app entry to open the browser window and briefly cover the important services
As an Electron App, the entry file is defined in package.json, here is "main": "./out/main”
, the corresponding source file is src/main.js
. As the entry point, there are some important works to do, especially these must be done before ready
event.
Enable sandbox globally.
// src/main.js
// main branch
app.enableSandbox()
Register two custom schemes.
// src/main.js
protocol.registerSchemesAsPrivileged([
{
scheme: 'vscode-webview',
privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true, allowServiceWorkers: true, }
},
{
scheme: 'vscode-file',
privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true }
}
]);
Register some global listeners
// src/main.js
registerListeners();
function registerListeners() {
...
app.on('will-finish-launching', function () {
app.on('open-url', onOpenUrl);
});
global['getOpenUrls'] = function () {
app.removeListener('open-url', onOpenUrl);
return openUrls;
};
}
Load the main module (vs/code/electron-main/main.ts
) using the private AMD loader to startup the app in ready
event.
// src/main.js
app.once('ready', function () {
...
onReady();
});
async function onReady() {
...
const [, nlsConfig] = await Promise.all([mkdirpIgnoreError(codeCachePath), resolveNlsConfiguration()]);
startup(codeCachePath, nlsConfig);
}
function startup(codeCachePath, nlsConfig) {
...
require('./bootstrap-amd').load('vs/code/electron-main/main', () => {
perf.mark('code/didLoadMainBundle');
});
}
In vs/code/electron-main/main.ts
, the application news an instance of CodeMain
and runs CodeMain.main()
. The main()
calls this.startup()
to startup the application
// vs/code/electron-main/main.ts
// Main Startup
const code = new CodeMain();
code.main();
class CodeMain {
main(): void {
try {
this.startup();
} catch (error) {
console.error(error.message);
app.exit(1);
}
}
...
}
In startup()
method, the application does this:
- Create services, including the
instantiationService
as the root IoC container. - Init some services
- Instantiate the
CodeApplication
class (vs/code/electron-main/app.ts
) usinginstantiationService.createInstance()
and call itsstartup()
method.
// vs/code/electron-main/main.ts
class CodeMain {
...
private async startup(): Promise<void> {
...
// Create services
const [instantiationService, ...] = this.createServices();
// Init services
await this.initServices(environmentMainService, userDataProfilesMainService, configurationService, stateMainService, productService);
// Startup
await instantiationService.invokeFunction(async accessor => {
const logService = accessor.get(ILogService);
const lifecycleMainService = accessor.get(ILifecycleMainService);
const fileService = accessor.get(IFileService);
...
return instantiationService.createInstance(CodeApplication, mainProcessNodeIpcServer, instanceEnvironment).startup();
});
}
private createServices(): [IInstantiationService, IProcessEnvironment, IEnvironmentMainService, ConfigurationService, StateService, BufferLogger, IProductService, UserDataProfilesMainService] {
const services = new ServiceCollection();
...
// Lifecycle
services.set(ILifecycleMainService, new SyncDescriptor(LifecycleMainService, undefined, false));
...
// Protocol (instantiated early and not using sync descriptor for security reasons)
services.set(IProtocolMainService, new ProtocolMainService(environmentMainService, userDataProfilesMainService, logService));
return [new InstantiationService(services, true), instanceEnvironment, environmentMainService, configurationService, stateService, bufferLogger, productService, userDataProfilesMainService];
}
private async initServices(environmentMainService: IEnvironmentMainService, userDataProfilesMainService: UserDataProfilesMainService, configurationService: ConfigurationService, stateService: StateService, productService: IProductService): Promise<void> {
...
// Initialize user data profiles after initializing the state
userDataProfilesMainService.init();
}
}
The IProtocolMainService
(vs/platform/protocol/electron-main/protocolMainService.ts
) registers thevscode-file://
protocol handler to handle the request resource, and register the file://
handler to forbidden the file://
protocol.
// vs/platform/protocol/electron-main/protocolMainService.ts
export class ProtocolMainService extends Disposable implements IProtocolMainService {
constructor() {
super();
...
this.handleProtocols();
}
private handleProtocols(): void {
const { defaultSession } = session;
// Register vscode-file:// handler
defaultSession.protocol.registerFileProtocol(Schemas.vscodeFileResource, (request, callback) => this.handleResourceRequest(request, callback));
// Block any file:// access
defaultSession.protocol.interceptFileProtocol(Schemas.file, (request, callback) => this.handleFileRequest(request, callback));
}
private handleFileRequest(request: Electron.ProtocolRequest, callback: ProtocolCallback) {
const uri = URI.parse(request.url);
this.logService.error(`Refused to load resource ${uri.fsPath} from ${Schemas.file}: protocol (original URL: ${request.url})`);
return callback({ error: -3 /* ABORTED */ });
}
private handleResourceRequest(request: Electron.ProtocolRequest, callback: ProtocolCallback): void {
const path = this.requestToNormalizedFilePath(request);
let headers: Record<string, string> | undefined;
if (this.environmentService.crossOriginIsolated) {
if (basename(path) === 'workbench.html' || basename(path) === 'workbench-dev.html') {
headers = COI.CoopAndCoep;
} else {
headers = COI.getHeadersFromQuery(request.url);
}
}
// first check by validRoots
if (this.validRoots.findSubstr(path)) {
return callback({ path, headers });
}
// then check by validExtensions
if (this.validExtensions.has(extname(path).toLowerCase())) {
return callback({ path });
}
// finally block to load the resource
this.logService.error(`${Schemas.vscodeFileResource}: Refused to load resource ${path} from ${Schemas.vscodeFileResource}: protocol (original URL: ${request.url})`);
return callback({ error: -3 /* ABORTED */ });
}
}
In the constructor of CodeApplication
class, the application configures the security settings of the session and registers some listeners.
// vs/code/electron-main/app.ts
export class CodeApplication extends Disposable {
constructor(...) {
super();
this.configureSession();
this.registerListeners();
}
private configureSession(): void {
...
const isUrlFromWebview = (requestingUrl: string | undefined) => requestingUrl?.startsWith(`${Schemas.vscodeWebview}://`);
session.defaultSession.setPermissionRequestHandler((_webContents, permission /* 'media' | 'geolocation' | 'notifications' | 'midiSysex' | 'pointerLock' | 'fullscreen' | 'openExternal' */, callback, details) => {
if (isUrlFromWebview(details.requestingUrl)) {
return callback(allowedPermissionsInWebview.has(permission));
}
return callback(false);
});
...
}
private registerListeners(): void {
...
// We handle uncaught exceptions here to prevent electron from opening a dialog to the user
setUnexpectedErrorHandler(error => this.onUnexpectedError(error));
process.on('uncaughtException', error => onUnexpectedError(error));
process.on('unhandledRejection', (reason: unknown) => onUnexpectedError(reason));
// 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 });
}
});
...
}
}
In CodeApplication.startup()
, the application does these:
- Create an IPC server which is based Electron IPC API.
const mainProcessElectronServer = new ElectronIPCServer();
- Create the shared process.
const { sharedProcessReady, sharedProcessClient } = this.setupSharedProcess(machineId);
- Create an new IoC Container named
appInstantiationService
as the child of the root IoC container and registers a series of services into the new IoC container. - Init message channels.
appInstantiationService.invokeFunction(accessor => this.initChannels(accessor, mainProcessElectronServer, sharedProcessClient));
- Setup protocol URL handlers.
- Open window, the user interface.
await appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, initialProtocolUrls));
// vs/code/electron-main/app.ts
export class CodeApplication extends Disposable {
constructor(
...
@IInstantiationService private readonly mainInstantiationService: IInstantiationService,
...
) {
...
}
async startup(): Promise<void> {
// Main process server (electron IPC based)
const mainProcessElectronServer = new ElectronIPCServer();
// Shared process
const { sharedProcessReady, sharedProcessClient } = this.setupSharedProcess(machineId);
// Services
const appInstantiationService = await this.initServices(machineId, sharedProcessReady);
// Init Channels
appInstantiationService.invokeFunction(accessor => this.initChannels(accessor, mainProcessElectronServer, sharedProcessClient));
// Setup Protocol URL Handlers
const initialProtocolUrls = appInstantiationService.invokeFunction(accessor => this.setupProtocolUrlHandlers(accessor, mainProcessElectronServer));
// Open Windows
await appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, initialProtocolUrls));
}
}
The application registers many importance services in the new IoC container appInstantiationService
, including IUpdateService
, IWindowsMainService
, ILaunchMainService
, IKeyboardLayoutMainService
, IMenubarMainService
, IExtensionHostStarter
, IExternalTerminalMainService
, and IUtilityProcessWorkerMainService
, etc.
// vs/code/electron-main/app.ts
export class CodeApplication extends Disposable {
...
private async initServices(machineId: 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, this.userEnv], false));
...
// Launch
services.set(ILaunchMainService, new SyncDescriptor(LaunchMainService, undefined, false /* proxied to other processes */));
// Keyboard Layout
services.set(IKeyboardLayoutMainService, new SyncDescriptor(KeyboardLayoutMainService));
// Menubar
services.set(IMenubarMainService, new SyncDescriptor(MenubarMainService));
// Extension Host Starter
services.set(IExtensionHostStarter, new SyncDescriptor(ExtensionHostStarter));
// 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));
}
// Utility Process Worker
services.set(IUtilityProcessWorkerMainService, new SyncDescriptor(UtilityProcessWorkerMainService, undefined, true));
....
return this.mainInstantiationService.createChild(services);
}
}
IWindowsMainService
(vs/platform/windows/electron-main/windowsMainService.ts
) is the windows service used to manage windows. CodeApplication.openFirstWindow()
invokes IWindowsMainService.open()
to open a window to show the user interface (workbench)
// vs/code/electron-main/app.ts
export class CodeApplication extends Disposable {
...
async startup(): Promise<void> {
...
// Open Windows
await appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, initialProtocolUrls));
...
}
private async openFirstWindow(accessor: ServicesAccessor, initialProtocolUrls: IInitialProtocolUrls | undefined): Promise<ICodeWindow[]> {
const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService);
....
return windowsMainService.open({
context,
cli: args,
urisToOpen: initialProtocolUrls.openables,
gotoLineMode: true,
initialStartup: true
});
}
}
In the implementation of IWindowsMainService.open()
, the application create an instance of CodeWindow
and call its load()
method to open the page finally.
// vs/platform/windows/electron-main/windowsMainService.ts
export class WindowsMainService extends Disposable implements IWindowsMainService {
...
constructor(
...
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
}
async open(openConfig: IOpenConfiguration): Promise<ICodeWindow[]> {
...
// Open based on config
const { windows: usedWindows, filesOpenedInWindow } = await this.doOpen(openConfig, workspacesToOpen, foldersToOpen, ...);
...
return usedWindows;
}
private async doOpen(): Promise<{ windows: ICodeWindow[]; filesOpenedInWindow: ICodeWindow | undefined }> {
...
await this.openInBrowserWindow({
userEnv: openConfig.userEnv,
cli: openConfig.cli,
initialStartup: openConfig.initialStartup,
filesToOpen,
forceNewWindow: true,
remoteAuthority: filesToOpen.remoteAuthority,
forceNewTabbedWindow: openConfig.forceNewTabbedWindow,
forceProfile: openConfig.forceProfile,
forceTempProfile: openConfig.forceTempProfile
})
...
}
private async openInBrowserWindow(options: IOpenBrowserWindowOptions): Promise<ICodeWindow> {
...
const createdWindow = window = this.instantiationService.createInstance(CodeWindow, {
state,
extensionDevelopmentPath: configuration.extensionDevelopmentPath,
isExtensionTestHost: !!configuration.extensionTestsPath
});
await this.doOpenInBrowserWindow(window, configuration, options, defaultProfile);
...
return window;
}
private async doOpenInBrowserWindow(window: ICodeWindow, ...): Promise<void> {
...
window.load(configuration);
}
}
CodeWindow
(vs/platform/windows/electron-main/windowImpl.ts
) is an abstraction and wrapper based on Electron’s BrowserWindow
. When instantiated it creates an instance of BrowserWindow
with the preload script vs/base/parts/sandbox/electron-sandbox/preload.js
.
// vs/platform/windows/electron-main/windowImpl.ts
export class CodeWindow extends Disposable implements ICodeWindow {
constructor(...) {
...
const options: BrowserWindowConstructorOptions & { experimentalDarkMode: boolean } = {
width: this.windowState.width,
height: this.windowState.height,
x: this.windowState.x,
y: this.windowState.y,
backgroundColor: this.themeMainService.getBackgroundColor(),
minWidth: WindowMinimumSize.WIDTH,
minHeight: WindowMinimumSize.HEIGHT,
show: !isFullscreenOrMaximized, // reduce flicker by showing later
title: this.productService.nameLong,
webPreferences: {
preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-sandbox/preload.js').fsPath,
additionalArguments: [`--vscode-window-config=${this.configObjectUrl.resource.toString()}`],
v8CacheOptions: this.environmentMainService.useCodeCache ? 'bypassHeatCheck' : 'none',
enableWebSQL: false,
spellcheck: false,
zoomFactor: zoomLevelToZoomFactor(windowSettings?.zoomLevel),
autoplayPolicy: 'user-gesture-required',
// Enable experimental css highlight api https://chromestatus.com/feature/5436441440026624
// Refs https://github.com/microsoft/vscode/issues/140098
enableBlinkFeatures: 'HighlightAPI',
sandbox: true
},
experimentalDarkMode: true
};
this._win = new BrowserWindow(options);
...
}
}
The preload.js
exposes a global variable named vscode
to provide process communication and other utilities.
The CodeWindow.load()
uses the BrowserWindow.loadURL()
to open the web page which is vs/code/electron-sandbox/workbench/workbench.html
or vs/code/electron-sandbox/workbench/workbench-dev.html
based on environment.
// vs/platform/windows/electron-main/windowImpl.ts
load(configuration: INativeWindowConfiguration, options: ILoadOptions = Object.create(null)): void {
this._win.loadURL(FileAccess.asBrowserUri(`vs/code/electron-sandbox/workbench/workbench${this.environmentMainService.isBuilt ? '' : '-dev'}.html`).toString(true));
}