Skip to content

VS Code: The Startup Process Of The Desktop Application #37

@xwcoder

Description

@xwcoder

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.

Electron: Protocol

// 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:

  1. Create services, including the instantiationService as the root IoC container.
  2. Init some services
  3. Instantiate the CodeApplication class (vs/code/electron-main/app.ts) using instantiationService.createInstance() and call its startup() 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:

  1. Create an IPC server which is based Electron IPC API. const mainProcessElectronServer = new ElectronIPCServer();
  2. Create the shared process. const { sharedProcessReady, sharedProcessClient } = this.setupSharedProcess(machineId);
  3. 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.
  4. Init message channels. appInstantiationService.invokeFunction(accessor => this.initChannels(accessor, mainProcessElectronServer, sharedProcessClient));
  5. Setup protocol URL handlers.
  6. 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));
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions