From 29bbfa92c10e744d4e51846d833ef543ab5abe4f Mon Sep 17 00:00:00 2001 From: deepak1556 Date: Wed, 2 Sep 2026 23:38:23 +0900 Subject: [PATCH] feat: bundle generated code cache into product --- build/darwin/create-universal-app.ts | 1 + build/gulpfile.vscode.ts | 21 +- build/lib/nodeCompileCache.ts | 155 ++++ build/lib/test/nodeCompileCache.test.ts | 143 ++++ build/next/index.ts | 16 + eslint.config.js | 4 +- src/bootstrap-fork.ts | 9 +- src/main.ts | 768 +---------------- src/mainImpl.ts | 772 ++++++++++++++++++ src/vs/base/node/nodeCompileCache.ts | 141 ++++ src/vs/code/electron-main/app.ts | 28 +- .../sharedProcess/sharedProcessMain.ts | 2 + .../platform/agentHost/node/agentHostMain.ts | 2 + .../agentHost/node/agentHostService.ts | 4 + src/vs/platform/terminal/node/ptyHostMain.ts | 2 + .../api/common/extHostExtensionService.ts | 3 + .../api/node/extHostExtensionService.ts | 5 + 17 files changed, 1306 insertions(+), 770 deletions(-) create mode 100644 build/lib/nodeCompileCache.ts create mode 100644 build/lib/test/nodeCompileCache.test.ts create mode 100644 src/mainImpl.ts create mode 100644 src/vs/base/node/nodeCompileCache.ts diff --git a/build/darwin/create-universal-app.ts b/build/darwin/create-universal-app.ts index 943c559884e526..927b7b0e192502 100644 --- a/build/darwin/create-universal-app.ts +++ b/build/darwin/create-universal-app.ts @@ -48,6 +48,7 @@ async function main(buildDir?: string) { const asarRelativePath = path.join('Contents', 'Resources', 'app', 'node_modules.asar'); const outAppPath = path.join(buildDir, `VSCode-darwin-${arch}`, appName); const productJsonPath = path.resolve(outAppPath, 'Contents', 'Resources', 'app', 'product.json'); + crossCopyPlatformDir(x64AppPath, arm64AppPath, path.join('Contents', 'Resources', 'app', 'node-compile-cache')); // Copilot SDK ships platform-specific native binaries that npm only installs // for the host architecture. The universal app merger requires both builds to diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index d48d82d52c9e5c..5197946aa7d1c5 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -39,6 +39,7 @@ import globCallback from 'glob'; import rceditCallback from 'rcedit'; import { spawnTsgo } from './lib/tsgo.ts'; import { runEsbuildTranspile, runEsbuildBundle } from './lib/esbuild.ts'; +import { generateNodeCompileCache, shouldGenerateNodeCompileCache } from './lib/nodeCompileCache.ts'; const glob = promisify(globCallback); @@ -158,6 +159,10 @@ const bootstrapEntryPoints = [ 'out-build/cli.js', 'out-build/bootstrap-fork.js' ]; +const bootstrapBundleEntryPoints = [ + ...bootstrapEntryPoints, + 'out-build/mainImpl.js' +]; const bundleVSCodeTask = task.define('bundle-vscode', task.series( util.rimraf('out-vscode'), @@ -172,7 +177,7 @@ const bundleVSCodeTask = task.define('bundle-vscode', task.series( src: 'out-build', entryPoints: [ ...vscodeEntryPoints, - ...bootstrapEntryPoints + ...bootstrapBundleEntryPoints ], resources: vscodeResources, skipTSBoilerplateRemoval: entryPoint => entryPoint === 'vs/code/electron-browser/workbench/workbench' || entryPoint === 'vs/sessions/electron-browser/sessions' @@ -700,6 +705,17 @@ function prepareCopilotRipgrepShimTask(platform: string, arch: string, destinati }; } +function generateNodeCompileCacheTask(platform: string, destinationFolderName: string) { + const outputDirectory = path.join(path.dirname(root), destinationFolderName); + + return () => generateNodeCompileCache( + platform, + outputDirectory, + util.getVersionedResourcesFolder(platform, commit!), + product + ); +} + const buildRoot = path.dirname(root); const BUILD_TARGETS = [ @@ -731,6 +747,9 @@ BUILD_TARGETS.forEach(buildTarget => { if (platform === 'win32') { packageTasks.push(patchWin32DependenciesTask(destinationFolderName)); } + if (shouldGenerateNodeCompileCache(platform, arch, product)) { + packageTasks.push(generateNodeCompileCacheTask(platform, destinationFolderName)); + } const vscodeTaskCI = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}-ci`, task.series(...packageTasks)); task.task(vscodeTaskCI); diff --git a/build/lib/nodeCompileCache.ts b/build/lib/nodeCompileCache.ts new file mode 100644 index 00000000000000..66dfaf4c1b507e --- /dev/null +++ b/build/lib/nodeCompileCache.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as cp from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +const nodeCompileCacheKinds = ['main', 'extension-host', 'shared-process', 'pty-host', 'agent-host'] as const; +const nodeCompileCacheTagPattern = /^v\d+\.\d+\.\d+-(arm64|x64)-[0-9a-f]{8}$/; +const nodeCompileCacheEntryPattern = /^[0-9a-f]{8}$/; + +export interface INodeCompileCacheProduct { + readonly applicationName: string; + readonly nameLong: string; + readonly nameShort: string; + readonly quality?: string; +} + +export interface INodeCompileCachePaths { + readonly application: string; + readonly cacheDirectory: string; +} + +export function shouldGenerateNodeCompileCache(platform: string, arch: string, product: INodeCompileCacheProduct): boolean { + if (!product.quality) { + return false; + } + + return platform === 'darwin' ? arch === 'arm64' : (platform === 'linux' || platform === 'win32') && arch === 'x64'; +} + +export function getNodeCompileCachePaths(platform: string, outputDirectory: string, versionedResourcesFolder: string, product: INodeCompileCacheProduct): INodeCompileCachePaths { + if (platform === 'darwin') { + const applicationRoot = path.join(outputDirectory, `${product.nameLong}.app`); + return { + application: path.join(applicationRoot, 'Contents', 'MacOS', product.nameShort), + cacheDirectory: path.join(applicationRoot, 'Contents', 'Resources', 'app', 'node-compile-cache') + }; + } + + const application = platform === 'win32' + ? path.join(outputDirectory, `${product.nameShort}.exe`) + : path.join(outputDirectory, product.applicationName); + + return { + application, + cacheDirectory: path.join(outputDirectory, versionedResourcesFolder, 'resources', 'app', 'node-compile-cache') + }; +} + +export function createNodeCompileCacheGenerationEnvironment(portableDirectory: string, parentEnvironment: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv { + const env = { ...parentEnvironment }; + delete env.ELECTRON_RUN_AS_NODE; + delete env.NODE_COMPILE_CACHE; + delete env.NODE_COMPILE_CACHE_PORTABLE; + delete env.NODE_COMPILE_CACHE_READONLY; + delete env.NODE_DISABLE_COMPILE_CACHE; + delete env.VSCODE_DEV; + delete env.VSCODE_MEASURE_NODE_COMPILE_CACHE; + delete env.VSCODE_NODE_COMPILE_CACHE_KIND; + delete env.VSCODE_NODE_COMPILE_CACHE_MEASUREMENTS; + delete env.VSCODE_NODE_COMPILE_CACHE_ROOT; + env.VSCODE_GENERATE_NODE_COMPILE_CACHE = '1'; + env.VSCODE_PORTABLE = portableDirectory; + return env; +} + +export async function generateNodeCompileCache(platform: string, outputDirectory: string, versionedResourcesFolder: string, product: INodeCompileCacheProduct): Promise { + const paths = getNodeCompileCachePaths(platform, outputDirectory, versionedResourcesFolder, product); + const temporaryDirectory = platform === 'win32' ? os.tmpdir() : '/tmp'; + const portableDirectory = await fs.promises.mkdtemp(path.join(temporaryDirectory, 'vscode-cache-')); + + await fs.promises.rm(paths.cacheDirectory, { recursive: true, force: true }); + + try { + const env = createNodeCompileCacheGenerationEnvironment(portableDirectory); + await runCacheGeneration(paths.application, paths.cacheDirectory, env); + const totalCacheEntryCount = await validateNodeCompileCache(paths.cacheDirectory, process.arch); + console.log(`Generated ${totalCacheEntryCount} total Node.js compile cache entries in ${paths.cacheDirectory}.`); + } finally { + await fs.promises.rm(portableDirectory, { recursive: true, force: true }); + } +} + +export async function validateNodeCompileCache(rootDirectory: string, architecture: string): Promise { + let totalCacheEntryCount = 0; + for (const kind of nodeCompileCacheKinds) { + const kindDirectory = path.join(rootDirectory, kind); + const entries = await fs.promises.readdir(kindDirectory, { withFileTypes: true }); + const tagDirectories = entries.filter(entry => entry.isDirectory() && nodeCompileCacheTagPattern.exec(entry.name)?.[1] === architecture); + if (tagDirectories.length !== 1) { + throw new Error(`Node.js compile cache generation produced ${tagDirectories.length} ${architecture} version-tag directories for ${kind} in ${kindDirectory}; expected exactly one.`); + } + if (!entries.some(entry => entry.isFile() && entry.name === '.ready')) { + throw new Error(`Node.js compile cache generation did not produce a readiness marker for ${kind} in ${kindDirectory}.`); + } + const unexpectedEntries = entries.filter(entry => entry.name !== '.ready' && entry.name !== tagDirectories[0].name); + if (unexpectedEntries.length > 0) { + throw new Error(`Node.js compile cache generation produced unexpected ${kind} entries in ${kindDirectory}: ${unexpectedEntries.map(entry => entry.name).join(', ')}.`); + } + + const cacheDirectory = path.join(kindDirectory, tagDirectories[0].name); + const cacheEntries = await fs.promises.readdir(cacheDirectory, { withFileTypes: true }); + const validCacheEntries = cacheEntries.filter(entry => entry.isFile() && nodeCompileCacheEntryPattern.test(entry.name)); + const cacheEntryCount = validCacheEntries.length; + if (cacheEntryCount === 0) { + throw new Error(`Node.js compile cache generation produced no ${kind} cache entries in ${cacheDirectory}.`); + } + const unexpectedCacheEntries = cacheEntries.filter(entry => !entry.isFile() || !nodeCompileCacheEntryPattern.test(entry.name)); + if (unexpectedCacheEntries.length > 0) { + throw new Error(`Node.js compile cache generation produced unexpected ${kind} cache entries in ${cacheDirectory}: ${unexpectedCacheEntries.map(entry => entry.name).join(', ')}.`); + } + totalCacheEntryCount += cacheEntryCount; + console.log(`Generated ${cacheEntryCount} ${kind} Node.js compile cache entries in ${cacheDirectory}.`); + await fs.promises.rm(path.join(kindDirectory, '.ready')); + } + return totalCacheEntryCount; +} + +function runCacheGeneration(application: string, cacheDirectory: string, env: NodeJS.ProcessEnv): Promise { + return new Promise((resolve, reject) => { + const child = cp.spawn(application, [], { + env, + stdio: ['ignore', 'pipe', 'pipe'] + }); + let output = ''; + let didTimeOut = false; + const timeout = setTimeout(() => { + didTimeOut = true; + child.kill(); + }, 120_000); + + child.stdout.on('data', chunk => output += chunk.toString()); + child.stderr.on('data', chunk => output += chunk.toString()); + child.on('error', error => { + clearTimeout(timeout); + reject(error); + }); + child.on('close', (code, signal) => { + clearTimeout(timeout); + if (didTimeOut) { + const missingKinds = nodeCompileCacheKinds.filter(kind => !fs.existsSync(path.join(cacheDirectory, kind, '.ready'))); + const missingKindsMessage = missingKinds.length > 0 ? ` Missing readiness markers: ${missingKinds.join(', ')}.` : ''; + reject(new Error(`Node.js compile cache generation timed out.${missingKindsMessage}\n${output}`)); + } else if (code !== 0) { + reject(new Error(`Node.js compile cache generation exited with code ${code} and signal ${signal ?? 'none'}.\n${output}`)); + } else { + resolve(); + } + }); + }); +} diff --git a/build/lib/test/nodeCompileCache.test.ts b/build/lib/test/nodeCompileCache.test.ts new file mode 100644 index 00000000000000..e617418b03e656 --- /dev/null +++ b/build/lib/test/nodeCompileCache.test.ts @@ -0,0 +1,143 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { suite, test } from 'node:test'; +import { createNodeCompileCacheGenerationEnvironment, getNodeCompileCachePaths, shouldGenerateNodeCompileCache, validateNodeCompileCache, type INodeCompileCacheProduct } from '../nodeCompileCache.ts'; + +const product: INodeCompileCacheProduct = { + applicationName: 'code-oss', + nameLong: 'Code - OSS', + nameShort: 'Code - OSS' +}; +const cacheKinds = ['main', 'extension-host', 'shared-process', 'pty-host', 'agent-host']; + +async function createCacheOutput(cacheDirectory: string, tag: string, unexpectedEntry?: string): Promise { + for (const kind of cacheKinds) { + const kindDirectory = path.join(cacheDirectory, kind); + const tagDirectory = path.join(kindDirectory, tag); + await fs.promises.mkdir(tagDirectory, { recursive: true }); + await fs.promises.writeFile(path.join(kindDirectory, '.ready'), ''); + await fs.promises.writeFile(path.join(tagDirectory, '12345678'), ''); + if (unexpectedEntry) { + await fs.promises.writeFile(path.join(kindDirectory, unexpectedEntry), ''); + } + } +} + +suite('Node compile cache', () => { + test('resolves packaged application and cache paths', () => { + assert.deepStrictEqual({ + darwin: getNodeCompileCachePaths('darwin', '/build/VSCode-darwin-x64', '', product), + linux: getNodeCompileCachePaths('linux', '/build/VSCode-linux-x64', '', product), + win32: getNodeCompileCachePaths('win32', 'C:\\build\\VSCode-win32-x64', '1234567890', product) + }, { + darwin: { + application: path.join('/build/VSCode-darwin-x64', 'Code - OSS.app', 'Contents', 'MacOS', 'Code - OSS'), + cacheDirectory: path.join('/build/VSCode-darwin-x64', 'Code - OSS.app', 'Contents', 'Resources', 'app', 'node-compile-cache') + }, + linux: { + application: path.join('/build/VSCode-linux-x64', 'code-oss'), + cacheDirectory: path.join('/build/VSCode-linux-x64', 'resources', 'app', 'node-compile-cache') + }, + win32: { + application: path.join('C:\\build\\VSCode-win32-x64', 'Code - OSS.exe'), + cacheDirectory: path.join('C:\\build\\VSCode-win32-x64', '1234567890', 'resources', 'app', 'node-compile-cache') + } + }); + }); + + test('generates caches only for native product build targets', () => { + assert.deepStrictEqual({ + oss: shouldGenerateNodeCompileCache('darwin', 'arm64', product), + darwinX64: shouldGenerateNodeCompileCache('darwin', 'x64', { ...product, quality: 'insider' }), + darwinArm64: shouldGenerateNodeCompileCache('darwin', 'arm64', { ...product, quality: 'insider' }), + linuxX64: shouldGenerateNodeCompileCache('linux', 'x64', { ...product, quality: 'insider' }), + linuxArm64: shouldGenerateNodeCompileCache('linux', 'arm64', { ...product, quality: 'insider' }), + win32X64: shouldGenerateNodeCompileCache('win32', 'x64', { ...product, quality: 'insider' }), + win32Arm64: shouldGenerateNodeCompileCache('win32', 'arm64', { ...product, quality: 'insider' }) + }, { + oss: false, + darwinX64: false, + darwinArm64: true, + linuxX64: true, + linuxArm64: false, + win32X64: true, + win32Arm64: false + }); + }); + + test('creates an isolated cache generation environment', () => { + const inheritedEnvironment = { + PATH: '/bin', + ELECTRON_RUN_AS_NODE: '1', + NODE_COMPILE_CACHE: '/node-cache', + NODE_COMPILE_CACHE_PORTABLE: '1', + NODE_COMPILE_CACHE_READONLY: '1', + NODE_DISABLE_COMPILE_CACHE: '1', + VSCODE_DEV: '1', + VSCODE_GENERATE_NODE_COMPILE_CACHE: '0', + VSCODE_MEASURE_NODE_COMPILE_CACHE: '1', + VSCODE_NODE_COMPILE_CACHE_KIND: 'main', + VSCODE_NODE_COMPILE_CACHE_MEASUREMENTS: '/measurements', + VSCODE_NODE_COMPILE_CACHE_ROOT: '/cache', + VSCODE_PORTABLE: '/inherited-portable' + }; + + assert.deepStrictEqual(createNodeCompileCacheGenerationEnvironment('/portable', inheritedEnvironment), { + PATH: '/bin', + VSCODE_GENERATE_NODE_COMPILE_CACHE: '1', + VSCODE_PORTABLE: '/portable' + }); + }); + + test('rejects cache output with unexpected role entries', async () => { + const cacheDirectory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'vscode-node-compile-cache-test-')); + + try { + await createCacheOutput(cacheDirectory, 'v24.20.0-x64-12345678', 'manifest.jsonl'); + + await assert.rejects( + validateNodeCompileCache(cacheDirectory, 'x64'), + /unexpected main entries.*manifest\.jsonl/ + ); + } finally { + await fs.promises.rm(cacheDirectory, { recursive: true, force: true }); + } + }); + + test('validates cache output and removes readiness markers', async () => { + const cacheDirectory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'vscode-node-compile-cache-test-')); + + try { + await createCacheOutput(cacheDirectory, 'v24.20.0-x64-12345678'); + + assert.strictEqual(await validateNodeCompileCache(cacheDirectory, 'x64'), cacheKinds.length); + for (const kind of cacheKinds) { + assert.strictEqual(fs.existsSync(path.join(cacheDirectory, kind, '.ready')), false); + } + } finally { + await fs.promises.rm(cacheDirectory, { recursive: true, force: true }); + } + }); + + test('rejects cache output with a UID-specific tag', async () => { + const cacheDirectory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'vscode-node-compile-cache-test-')); + + try { + await createCacheOutput(cacheDirectory, 'v24.20.0-x64-12345678-1000'); + + await assert.rejects( + validateNodeCompileCache(cacheDirectory, 'x64'), + /produced 0 x64 version-tag directories for main/ + ); + } finally { + await fs.promises.rm(cacheDirectory, { recursive: true, force: true }); + } + }); +}); diff --git a/build/next/index.ts b/build/next/index.ts index c03436b234e9de..35632ae712dfa6 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -141,6 +141,7 @@ const serverEntryPoints = [ // Bootstrap files per target const bootstrapEntryPointsDesktop = [ 'main', + 'mainImpl', 'cli', 'bootstrap-fork', ]; @@ -367,6 +368,18 @@ function cssExternalPlugin(): esbuild.Plugin { }; } +function mainImplExternalPlugin(): esbuild.Plugin { + return { + name: 'main-impl-external', + setup(build) { + build.onResolve({ filter: /^\.\/mainImpl\.js$/ }, args => ({ + path: args.path, + external: true, + })); + }, + }; +} + /** * esbuild plugin that transforms source files to inject build-time configuration. * This runs during onLoad so the transformation happens before esbuild processes the content, @@ -558,6 +571,9 @@ async function bundle(outDir: string, doMinify: boolean, doNls: boolean, doMangl const outPath = path.join(REPO_ROOT, outDir, `${entry}.js`); const bootstrapPlugins: esbuild.Plugin[] = [inlineMinimistPlugin(), contentMapperPlugin]; + if (entry === 'main') { + bootstrapPlugins.push(mainImplExternalPlugin()); + } if (doNls) { bootstrapPlugins.unshift(nlsPlugin({ baseDir: path.join(REPO_ROOT, SRC_DIR), diff --git a/eslint.config.js b/eslint.config.js index 5ae73c6f455bd3..65528ce79cdf8f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1589,6 +1589,8 @@ export default defineConfig( 'inspector', 'minimist', 'node:module', + 'node:url', + 'node:v8', 'native-keymap', 'net', 'node-pty', @@ -2088,7 +2090,7 @@ export default defineConfig( ] }, { - 'target': 'src/{bootstrap-cli.ts,bootstrap-esm.ts,bootstrap-fork.ts,bootstrap-import.ts,bootstrap-meta.ts,bootstrap-node.ts,bootstrap-server.ts,cli.ts,main.ts,server-cli.ts,server-main.ts}', + 'target': 'src/{bootstrap-cli.ts,bootstrap-esm.ts,bootstrap-fork.ts,bootstrap-import.ts,bootstrap-meta.ts,bootstrap-node.ts,bootstrap-server.ts,cli.ts,main.ts,mainImpl.ts,server-cli.ts,server-main.ts}', 'restrictions': [ 'vs/**/common/*', 'vs/**/node/*', diff --git a/src/bootstrap-fork.ts b/src/bootstrap-fork.ts index b87e855ba85fe5..0deb097a6d86d9 100644 --- a/src/bootstrap-fork.ts +++ b/src/bootstrap-fork.ts @@ -6,9 +6,16 @@ import * as performance from './vs/base/common/performance.js'; import { removeGlobalNodeJsModuleLookupPaths, devInjectNodeModuleLookupPath } from './bootstrap-node.js'; import { bootstrapESM } from './bootstrap-esm.js'; +import { enableNodeCompileCache, getNodeCompileCacheKindForUtilityProcess } from './vs/base/node/nodeCompileCache.js'; performance.mark('code/fork/start'); +const nodeCompileCacheKind = getNodeCompileCacheKindForUtilityProcess(process.env['VSCODE_CRASH_REPORTER_PROCESS_TYPE'] ?? ''); +const esmEntryPoint = process.env['VSCODE_ESM_ENTRYPOINT']; +if (nodeCompileCacheKind && esmEntryPoint) { + enableNodeCompileCache(nodeCompileCacheKind, new URL(`./${esmEntryPoint}.js`, import.meta.url).href); +} + //#region Helpers function pipeLoggingToParent(): void { @@ -226,4 +233,4 @@ if (process.env['VSCODE_PARENT_PID']) { await bootstrapESM(); // Load ESM entry point -await import([`./${process.env['VSCODE_ESM_ENTRYPOINT']}.js`].join('/') /* workaround: esbuild prints some strange warnings when trying to inline? */); +await import([`./${esmEntryPoint}.js`].join('/') /* workaround: esbuild prints some strange warnings when trying to inline? */); diff --git a/src/main.ts b/src/main.ts index 0db48b04ae1255..434de26f35b608 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,770 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'node:path'; -import * as fs from 'original-fs'; -import * as os from 'node:os'; -import { performance } from 'node:perf_hooks'; -import { configurePortable } from './bootstrap-node.js'; -import { bootstrapESM } from './bootstrap-esm.js'; -import { app, protocol, crashReporter, Menu, contentTracing } from 'electron'; -import minimist from 'minimist'; -import { product } from './bootstrap-meta.js'; -import { parse } from './vs/base/common/jsonc.js'; -import { getUserDataPath } from './vs/platform/environment/node/userDataPath.js'; -import * as perf from './vs/base/common/performance.js'; -import { resolveNLSConfiguration } from './vs/base/node/nls.js'; -import { getUNCHost, addUNCHostToAllowlist } from './vs/base/node/unc.js'; -import { INLSConfiguration } from './vs/nls.js'; -import { NativeParsedArgs } from './vs/platform/environment/common/argv.js'; +import { enableNodeCompileCache } from './vs/base/node/nodeCompileCache.js'; -perf.mark('code/didStartMain'); +enableNodeCompileCache('main', new URL('./mainImpl.js', import.meta.url).href); -perf.mark('code/willLoadMainBundle', { - // When built, the main bundle is a single JS file with all - // dependencies inlined. As such, we mark `willLoadMainBundle` - // as the start of the main bundle loading process. - startTime: Math.floor(performance.timeOrigin) -}); -perf.mark('code/didLoadMainBundle'); - -// Enable portable support -const portable = configurePortable(product); - -const args = parseCLIArgs(); -// Configure static command line arguments -perf.mark('code/willConfigureCommandlineSwitches'); -const argvConfig = configureCommandlineSwitchesSync(args); -perf.mark('code/didConfigureCommandlineSwitches'); -// Enable sandbox globally unless -// 1) disabled via command line using either -// `--no-sandbox` or `--disable-chromium-sandbox` argument. -// 2) argv.json contains `disable-chromium-sandbox: true`. -if (args['sandbox'] && - !args['disable-chromium-sandbox'] && - !argvConfig['disable-chromium-sandbox']) { - app.enableSandbox(); -} else if (app.commandLine.hasSwitch('no-sandbox') && - !app.commandLine.hasSwitch('disable-gpu-sandbox')) { - // Disable GPU sandbox whenever --no-sandbox is used. - app.commandLine.appendSwitch('disable-gpu-sandbox'); -} else { - app.commandLine.appendSwitch('no-sandbox'); - app.commandLine.appendSwitch('disable-gpu-sandbox'); -} - -// Set userData path before app 'ready' event -perf.mark('code/willGetUserDataPath'); -const userDataPath = getUserDataPath(args, product.nameShort ?? 'code-oss-dev'); -if (process.platform === 'win32') { - const userDataUNCHost = getUNCHost(userDataPath); - if (userDataUNCHost) { - addUNCHostToAllowlist(userDataUNCHost); // enables to use UNC paths in userDataPath - } -} -app.setPath('userData', userDataPath); -perf.mark('code/didGetUserDataPath'); - -if (process.platform === 'linux') { - const snapName = process.env['SNAP_INSTANCE_NAME']; - const installedDesktopName = product.linuxDesktopName && `/usr/share/applications/${product.linuxDesktopName}.desktop`; - if (snapName) { - app.setDesktopName(`${snapName}_${product.applicationName}.desktop`); - } else if (installedDesktopName && fs.existsSync(installedDesktopName)) { - app.setDesktopName(`${product.linuxDesktopName}.desktop`); - } -} - -// Resolve code cache path -const codeCachePath = getCodeCachePath(); - -// Disable default menu (https://github.com/electron/electron/issues/35512) -Menu.setApplicationMenu(null); - -// Configure crash reporter -perf.mark('code/willStartCrashReporter'); -// If a crash-reporter-directory is specified we store the crash reports -// in the specified directory and don't upload them to the crash server. -// -// Appcenter crash reporting is enabled if -// * enable-crash-reporter runtime argument is set to 'true' -// * --disable-crash-reporter command line parameter is not set -// -// Disable crash reporting in all other cases. -if (args['crash-reporter-directory'] || (argvConfig['enable-crash-reporter'] && !args['disable-crash-reporter'])) { - configureCrashReporter(); -} -perf.mark('code/didStartCrashReporter'); - -// Set logs path before app 'ready' event if running portable -// to ensure that no 'logs' folder is created on disk at a -// location outside of the portable directory -// (https://github.com/microsoft/vscode/issues/56651) -if (portable.isPortable) { - app.setAppLogsPath(path.join(userDataPath, 'logs')); -} - -// Register custom schemes with privileges -perf.mark('code/willRegisterSchemesAsPrivileged'); -protocol.registerSchemesAsPrivileged([ - { - scheme: 'vscode-webview', - privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true, allowServiceWorkers: true, codeCache: true } - }, - { - scheme: 'vscode-file', - privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true, codeCache: true } - }, - { - scheme: 'vscode-remote-resource', - privileges: { secure: true, supportFetchAPI: true, corsEnabled: true } - }, - { - scheme: 'vscode-managed-remote-resource', - privileges: { secure: true, supportFetchAPI: true, corsEnabled: true } - } -]); -perf.mark('code/didRegisterSchemesAsPrivileged'); - -// Global app listeners -perf.mark('code/willRegisterListeners'); -registerListeners(); -perf.mark('code/didRegisterListeners'); - -/** - * We can resolve the NLS configuration early if it is defined - * in argv.json before `app.ready` event. Otherwise we can only - * resolve NLS after `app.ready` event to resolve the OS locale. - */ -let nlsConfigurationPromise: Promise | undefined = undefined; - -// Use the most preferred OS language for language recommendation. -// The API might return an empty array on Linux, such as when -// the 'C' locale is the user's only configured locale. -// No matter the OS, if the array is empty, default back to 'en'. -// Note: this forces Chromium's locale init and costs ~90ms; deferring it past `app.ready` only relocates that cost. -perf.mark('code/willGetPreferredSystemLanguages'); -const osLocale = processZhLocale((app.getPreferredSystemLanguages()?.[0] ?? 'en').toLowerCase()); -perf.mark('code/didGetPreferredSystemLanguages'); -const userLocale = getUserDefinedLocale(argvConfig); -if (userLocale) { - nlsConfigurationPromise = resolveNLSConfiguration({ - userLocale, - osLocale, - commit: product.commit, - nlsMetadataHash: product.nlsMetadataHash, - userDataPath, - nlsMetadataPath: import.meta.dirname - }); -} - -// Pass in the locale to Electron so that the -// Windows Control Overlay is rendered correctly on Windows. -// For now, don't pass in the locale on macOS due to -// https://github.com/microsoft/vscode/issues/167543. -// If the locale is `qps-ploc`, the Microsoft -// Pseudo Language Language Pack is being used. -// In that case, use `en` as the Electron locale. - -if (process.platform === 'win32' || process.platform === 'linux') { - const electronLocale = (!userLocale || userLocale === 'qps-ploc') ? 'en' : userLocale; - app.commandLine.appendSwitch('lang', electronLocale); -} - -// Load our code once ready -perf.mark('code/willWaitForAppReady'); -app.once('ready', function () { - perf.mark('code/didWaitForAppReady'); - if (args['trace']) { - let traceOptions: Electron.TraceConfig | Electron.TraceCategoriesAndOptions; - if (args['trace-memory-infra']) { - const customCategories = args['trace-category-filter']?.split(',') || []; - customCategories.push('disabled-by-default-memory-infra', 'disabled-by-default-memory-infra.v8.code_stats'); - traceOptions = { - included_categories: customCategories, - excluded_categories: ['*'], - memory_dump_config: { - allowed_dump_modes: ['light', 'detailed'], - triggers: [ - { - type: 'periodic_interval', - mode: 'detailed', - min_time_between_dumps_ms: 10000 - }, - { - type: 'periodic_interval', - mode: 'light', - min_time_between_dumps_ms: 1000 - } - ] - } - }; - } else { - traceOptions = { - categoryFilter: args['trace-category-filter'] || '*', - traceOptions: args['trace-options'] || 'record-until-full,enable-sampling' - }; - } - - contentTracing.startRecording(traceOptions).finally(() => onReady()); - } else { - onReady(); - } -}); - -async function onReady() { - perf.mark('code/mainAppReady'); - - try { - const [, nlsConfig] = await Promise.all([ - mkdirpIgnoreError(codeCachePath), - resolveNlsConfiguration() - ]); - - await startup(codeCachePath, nlsConfig); - } catch (error) { - console.error(error); - } -} - -/** - * Main startup routine - */ -async function startup(codeCachePath: string | undefined, nlsConfig: INLSConfiguration): Promise { - process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig); - process.env['VSCODE_CODE_CACHE_PATH'] = codeCachePath || ''; - - // Bootstrap ESM - perf.mark('code/willBootstrapESM'); - await bootstrapESM(); - perf.mark('code/didBootstrapESM'); - - // Load Main - // Note: `out/main.js` is already compiled here, so this only executes the electron-main module graph. - perf.mark('code/willRunMainBundle'); - await import('./vs/code/electron-main/main.js'); - perf.mark('code/didRunMainBundle'); -} - -function configureCommandlineSwitchesSync(cliArgs: NativeParsedArgs) { - const SUPPORTED_ELECTRON_SWITCHES = [ - - // alias from us for --disable-gpu - 'disable-hardware-acceleration', - - // override for the color profile to use - 'force-color-profile', - - // disable LCD font rendering, a Chromium flag - 'disable-lcd-text', - - // bypass any specified proxy for the given semi-colon-separated list of hosts - 'proxy-bypass-list', - - 'remote-debugging-port' - ]; - - if (process.platform === 'linux') { - - // Force enable screen readers on Linux via this flag - SUPPORTED_ELECTRON_SWITCHES.push('force-renderer-accessibility'); - - // override which password-store is used on Linux - SUPPORTED_ELECTRON_SWITCHES.push('password-store'); - } - - const SUPPORTED_MAIN_PROCESS_SWITCHES = [ - - // Persistently enable proposed api via argv.json: https://github.com/microsoft/vscode/issues/99775 - 'enable-proposed-api', - - // Log level to use. Default is 'info'. Allowed values are 'error', 'warn', 'info', 'debug', 'trace', 'off'. - 'log-level', - - // Use an in-memory storage for secrets - 'use-inmemory-secretstorage', - - // Enables display tracking to restore maximized windows under RDP: https://github.com/electron/electron/issues/47016 - 'enable-rdp-display-tracking', - ]; - - // Read argv config - const argvConfig = readArgvConfigSync(); - - Object.keys(argvConfig).forEach(argvKey => { - const argvValue = argvConfig[argvKey]; - - // Append Electron flags to Electron - if (SUPPORTED_ELECTRON_SWITCHES.indexOf(argvKey) !== -1) { - if (argvValue === true || argvValue === 'true') { - if (argvKey === 'disable-hardware-acceleration') { - app.disableHardwareAcceleration(); // needs to be called explicitly - } else { - app.commandLine.appendSwitch(argvKey); - } - } else if (typeof argvValue === 'string' && argvValue) { - if (argvKey === 'password-store') { - // Password store - // TODO@TylerLeonhardt: Remove this migration in 3 months - let migratedArgvValue = argvValue; - if (argvValue === 'gnome' || argvValue === 'gnome-keyring') { - migratedArgvValue = 'gnome-libsecret'; - } - app.commandLine.appendSwitch(argvKey, migratedArgvValue); - } else { - app.commandLine.appendSwitch(argvKey, argvValue); - } - } - } - - // Append main process flags to process.argv - else if (SUPPORTED_MAIN_PROCESS_SWITCHES.indexOf(argvKey) !== -1) { - switch (argvKey) { - case 'enable-proposed-api': - if (Array.isArray(argvValue)) { - argvValue.forEach(id => id && typeof id === 'string' && process.argv.push('--enable-proposed-api', id)); - } else { - console.error(`Unexpected value for \`enable-proposed-api\` in argv.json. Expected array of extension ids.`); - } - break; - - case 'log-level': - if (typeof argvValue === 'string') { - process.argv.push('--log', argvValue); - } else if (Array.isArray(argvValue)) { - for (const value of argvValue) { - process.argv.push('--log', value); - } - } - break; - - case 'use-inmemory-secretstorage': - if (argvValue) { - process.argv.push('--use-inmemory-secretstorage'); - } - break; - - case 'enable-rdp-display-tracking': - if (argvValue) { - process.argv.push('--enable-rdp-display-tracking'); - } - break; - } - } - }); - - // Following features are enabled from the runtime: - // `NetAdapterMaxBufSizeFeature` - Specify the max buffer size for NetToMojoPendingBuffer, refs https://github.com/microsoft/vscode/issues/268800 - // `DocumentPolicyIncludeJSCallStacksInCrashReports` - https://www.electronjs.org/docs/latest/api/web-frame-main#framecollectjavascriptcallstack-experimental - // `EarlyEstablishGpuChannel` - Refs https://issues.chromium.org/issues/40208065 - // `EstablishGpuChannelAsync` - Refs https://issues.chromium.org/issues/40208065 - // `GlobalShortcutsPortal` - Enables Electron's `globalShortcut` (system-wide keybindings) on Linux Wayland via the XDG global shortcuts portal (no-op elsewhere) - const featuresToEnable = - `NetAdapterMaxBufSizeFeature:NetAdapterMaxBufSize/8192,DocumentPolicyIncludeJSCallStacksInCrashReports,EarlyEstablishGpuChannel,EstablishGpuChannelAsync${process.platform === 'linux' ? ',GlobalShortcutsPortal' : ''},${app.commandLine.getSwitchValue('enable-features')}`; - app.commandLine.appendSwitch('enable-features', featuresToEnable); - - // Following features are disabled from the runtime: - // `CalculateNativeWinOcclusion` - Disable native window occlusion tracker (https://groups.google.com/a/chromium.org/g/embedder-dev/c/ZF3uHHyWLKw/m/VDN2hDXMAAAJ) - const featuresToDisable = - `CalculateNativeWinOcclusion,${app.commandLine.getSwitchValue('disable-features')}`; - app.commandLine.appendSwitch('disable-features', featuresToDisable); - - // Blink features to configure. - // `FontMatchingCTMigration` - Siwtch font matching on macOS to Appkit (Refs https://github.com/microsoft/vscode/issues/224496#issuecomment-2270418470). - // `StandardizedBrowserZoom` - Disable zoom adjustment for bounding box (https://github.com/microsoft/vscode/issues/232750#issuecomment-2459495394) - const blinkFeaturesToDisable = - `FontMatchingCTMigration,StandardizedBrowserZoom,${app.commandLine.getSwitchValue('disable-blink-features')}`; - app.commandLine.appendSwitch('disable-blink-features', blinkFeaturesToDisable); - - // Support JS Flags - const jsFlags = getJSFlags(cliArgs, argvConfig); - if (jsFlags) { - app.commandLine.appendSwitch('js-flags', jsFlags); - } - - // Use portal version 4 that supports current_folder option - // to address https://github.com/microsoft/vscode/issues/213780 - // Runtime sets the default version to 3, refs https://github.com/electron/electron/pull/44426 - app.commandLine.appendSwitch('xdg-portal-required-version', '4'); - - // Increase the maximum number of active WebGL contexts as each terminal may - // use up to 2 - app.commandLine.appendSwitch('max-active-webgl-contexts', '32'); - - return argvConfig; -} - -interface IArgvConfig { - [key: string]: string | string[] | boolean | undefined; - readonly locale?: string; - readonly 'disable-lcd-text'?: boolean; - readonly 'proxy-bypass-list'?: string; - readonly 'disable-hardware-acceleration'?: boolean; - readonly 'force-color-profile'?: string; - readonly 'enable-crash-reporter'?: boolean; - readonly 'crash-reporter-id'?: string; - readonly 'enable-proposed-api'?: string[]; - readonly 'log-level'?: string | string[]; - readonly 'disable-chromium-sandbox'?: boolean; - readonly 'use-inmemory-secretstorage'?: boolean; - readonly 'enable-rdp-display-tracking'?: boolean; - readonly 'remote-debugging-port'?: string; - readonly 'js-flags'?: string; -} - -function readArgvConfigSync(): IArgvConfig { - - // Read or create the argv.json config file sync before app('ready') - const argvConfigPath = getArgvConfigPath(); - let argvConfig: IArgvConfig | undefined = undefined; - try { - argvConfig = parse(fs.readFileSync(argvConfigPath).toString()); - } catch (error) { - if (error && error.code === 'ENOENT') { - createDefaultArgvConfigSync(argvConfigPath); - } else { - console.warn(`Unable to read argv.json configuration file in ${argvConfigPath}, falling back to defaults (${error})`); - } - } - - // Fallback to default - if (!argvConfig) { - argvConfig = {}; - } - - return argvConfig; -} - -function createDefaultArgvConfigSync(argvConfigPath: string): void { - try { - - // Ensure argv config parent exists - const argvConfigPathDirname = path.dirname(argvConfigPath); - if (!fs.existsSync(argvConfigPathDirname)) { - fs.mkdirSync(argvConfigPathDirname); - } - - // Default argv content - const defaultArgvConfigContent = [ - '// This configuration file allows you to pass permanent command line arguments to VS Code.', - '// Only a subset of arguments is currently supported to reduce the likelihood of breaking', - '// the installation.', - '//', - '// PLEASE DO NOT CHANGE WITHOUT UNDERSTANDING THE IMPACT', - '//', - '// NOTE: Changing this file requires a restart of VS Code.', - '{', - ' // Use software rendering instead of hardware accelerated rendering.', - ' // This can help in cases where you see rendering issues in VS Code.', - ' // "disable-hardware-acceleration": true', - '}' - ]; - - // Create initial argv.json with default content - fs.writeFileSync(argvConfigPath, defaultArgvConfigContent.join('\n')); - } catch (error) { - console.error(`Unable to create argv.json configuration file in ${argvConfigPath}, falling back to defaults (${error})`); - } -} - -function getArgvConfigPath(): string { - const vscodePortable = process.env['VSCODE_PORTABLE']; - if (vscodePortable) { - return path.join(vscodePortable, 'argv.json'); - } - - let dataFolderName = product.dataFolderName; - if (process.env['VSCODE_DEV']) { - dataFolderName = `${dataFolderName}-dev`; - } - - return path.join(os.homedir(), dataFolderName!, 'argv.json'); -} - -function configureCrashReporter(): void { - let crashReporterDirectory = args['crash-reporter-directory']; - let submitURL = ''; - if (crashReporterDirectory) { - crashReporterDirectory = path.normalize(crashReporterDirectory); - - if (!path.isAbsolute(crashReporterDirectory)) { - console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory must be absolute.`); - app.exit(1); - } - - if (!fs.existsSync(crashReporterDirectory)) { - try { - fs.mkdirSync(crashReporterDirectory, { recursive: true }); - } catch (error) { - console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory does not seem to exist or cannot be created.`); - app.exit(1); - } - } - - // Crashes are stored in the crashDumps directory by default, so we - // need to change that directory to the provided one - console.log(`Found --crash-reporter-directory argument. Setting crashDumps directory to be '${crashReporterDirectory}'`); - app.setPath('crashDumps', crashReporterDirectory); - } - - // Otherwise we configure the crash reporter from product.json - else { - const appCenter = product.appCenter; - if (appCenter) { - const isWindows = (process.platform === 'win32'); - const isLinux = (process.platform === 'linux'); - const isDarwin = (process.platform === 'darwin'); - const crashReporterId = argvConfig['crash-reporter-id']; - const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - if (crashReporterId && uuidPattern.test(crashReporterId)) { - if (isWindows) { - switch (process.arch) { - case 'x64': - submitURL = appCenter['win32-x64']; - break; - case 'arm64': - submitURL = appCenter['win32-arm64']; - break; - } - } else if (isDarwin) { - if (product.darwinUniversalAssetId) { - submitURL = appCenter['darwin-universal']; - } else { - switch (process.arch) { - case 'x64': - submitURL = appCenter['darwin']; - break; - case 'arm64': - submitURL = appCenter['darwin-arm64']; - break; - } - } - } else if (isLinux) { - submitURL = appCenter['linux-x64']; - } - submitURL = submitURL.concat('&uid=', crashReporterId, '&iid=', crashReporterId, '&sid=', crashReporterId); - // Send the id for child node process that are explicitly starting crash reporter. - // For vscode this is ExtensionHost process currently. - const argv = process.argv; - const endOfArgsMarkerIndex = argv.indexOf('--'); - if (endOfArgsMarkerIndex === -1) { - argv.push('--crash-reporter-id', crashReporterId); - } else { - // if the we have an argument "--" (end of argument marker) - // we cannot add arguments at the end. rather, we add - // arguments before the "--" marker. - argv.splice(endOfArgsMarkerIndex, 0, '--crash-reporter-id', crashReporterId); - } - } - } - } - - // Start crash reporter for all processes - const productName = (product.crashReporter ? product.crashReporter.productName : undefined) || product.nameShort; - const companyName = (product.crashReporter ? product.crashReporter.companyName : undefined) || 'Microsoft'; - const uploadToServer = Boolean(!process.env['VSCODE_DEV'] && submitURL && !crashReporterDirectory); - crashReporter.start({ - companyName, - productName: process.env['VSCODE_DEV'] ? `${productName} Dev` : productName, - submitURL, - uploadToServer, - compress: true, - ignoreSystemCrashHandler: true - }); -} - -function getJSFlags(cliArgs: NativeParsedArgs, argvConfig: IArgvConfig): string | null { - const jsFlags: string[] = []; - - // Add any existing JS flags we already got from the command line - if (cliArgs['js-flags']) { - jsFlags.push(cliArgs['js-flags']); - } - - // Add JS flags from runtime arguments (argv.json) - if (typeof argvConfig['js-flags'] === 'string' && argvConfig['js-flags']) { - jsFlags.push(argvConfig['js-flags']); - } - - return jsFlags.length > 0 ? jsFlags.join(' ') : null; -} - -function parseCLIArgs(): NativeParsedArgs { - return minimist(process.argv, { - string: [ - 'user-data-dir', - 'locale', - 'js-flags', - 'crash-reporter-directory' - ], - boolean: [ - 'disable-chromium-sandbox', - ], - default: { - 'sandbox': true - }, - alias: { - 'no-sandbox': 'sandbox' - } - }); -} - -function registerListeners(): void { - - /** - * macOS: when someone drops a file to the not-yet running VSCode, the open-file event fires even before - * the app-ready event. We listen very early for open-file and remember this upon startup as path to open. - */ - const macOpenFiles: string[] = []; - (globalThis as { macOpenFiles?: string[] }).macOpenFiles = macOpenFiles; - app.on('open-file', function (event, path) { - macOpenFiles.push(path); - }); - - /** - * macOS: react to open-url requests. - */ - const openUrls: string[] = []; - const onOpenUrl = - function (event: { preventDefault: () => void }, url: string) { - event.preventDefault(); - - openUrls.push(url); - }; - - app.on('will-finish-launching', function () { - app.on('open-url', onOpenUrl); - }); - - (globalThis as { getOpenUrls?: () => string[] }).getOpenUrls = function () { - app.removeListener('open-url', onOpenUrl); - - return openUrls; - }; -} - -function getCodeCachePath(): string | undefined { - - // explicitly disabled via CLI args - if (process.argv.indexOf('--no-cached-data') > 0) { - return undefined; - } - - // running out of sources - if (process.env['VSCODE_DEV']) { - return undefined; - } - - // require commit id - const commit = product.commit; - if (!commit) { - return undefined; - } - - return path.join(userDataPath, 'CachedData', commit); -} - -async function mkdirpIgnoreError(dir: string | undefined): Promise { - if (typeof dir === 'string') { - try { - await fs.promises.mkdir(dir, { recursive: true }); - - return dir; - } catch (error) { - // ignore - } - } - - return undefined; -} - -//#region NLS Support - -function processZhLocale(appLocale: string): string { - if (appLocale.startsWith('zh')) { - const region = appLocale.split('-')[1]; - - // On Windows and macOS, Chinese languages returned by - // app.getPreferredSystemLanguages() start with zh-hans - // for Simplified Chinese or zh-hant for Traditional Chinese, - // so we can easily determine whether to use Simplified or Traditional. - // However, on Linux, Chinese languages returned by that same API - // are of the form zh-XY, where XY is a country code. - // For China (CN), Singapore (SG), and Malaysia (MY) - // country codes, assume they use Simplified Chinese. - // For other cases, assume they use Traditional. - if (['hans', 'cn', 'sg', 'my'].includes(region)) { - return 'zh-cn'; - } - - return 'zh-tw'; - } - - return appLocale; -} - -/** - * Resolve the NLS configuration - */ -async function resolveNlsConfiguration(): Promise { - perf.mark('code/willResolveNlsConfiguration'); - try { - - // First, we need to test a user defined locale. - // If it fails we try the app locale. - // If that fails we fall back to English. - - const nlsConfiguration = nlsConfigurationPromise ? await nlsConfigurationPromise : undefined; - if (nlsConfiguration) { - return nlsConfiguration; - } - - // Try to use the app locale which is only valid - // after the app ready event has been fired. - - let userLocale = app.getLocale(); - if (!userLocale) { - return { - userLocale: 'en', - osLocale, - resolvedLanguage: 'en', - defaultMessagesFile: path.join(import.meta.dirname, 'nls.messages.json'), - - // NLS: below 2 are a relic from old times only used by vscode-nls and deprecated - locale: 'en', - availableLanguages: {} - }; - } - - // See above the comment about the loader and case sensitiveness - userLocale = processZhLocale(userLocale.toLowerCase()); - - return await resolveNLSConfiguration({ - userLocale, - osLocale, - commit: product.commit, - nlsMetadataHash: product.nlsMetadataHash, - userDataPath, - nlsMetadataPath: import.meta.dirname - }); - } finally { - perf.mark('code/didResolveNlsConfiguration'); - } -} - -/** - * Language tags are case insensitive however an ESM loader is case sensitive - * To make this work on case preserving & insensitive FS we do the following: - * the language bundles have lower case language tags and we always lower case - * the locale we receive from the user or OS. - */ -function getUserDefinedLocale(argvConfig: IArgvConfig): string | undefined { - const locale = args['locale']; - if (locale) { - return locale.toLowerCase(); // a directly provided --locale always wins - } - - return typeof argvConfig?.locale === 'string' ? argvConfig.locale.toLowerCase() : undefined; -} - -//#endregion +await import('./mainImpl.js'); diff --git a/src/mainImpl.ts b/src/mainImpl.ts new file mode 100644 index 00000000000000..4cac0cb737730c --- /dev/null +++ b/src/mainImpl.ts @@ -0,0 +1,772 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'node:path'; +import * as fs from 'original-fs'; +import * as os from 'node:os'; +import { performance } from 'node:perf_hooks'; +import { configurePortable } from './bootstrap-node.js'; +import { bootstrapESM } from './bootstrap-esm.js'; +import { app, protocol, crashReporter, Menu, contentTracing } from 'electron'; +import minimist from 'minimist'; +import { product } from './bootstrap-meta.js'; +import { parse } from './vs/base/common/jsonc.js'; +import { getUserDataPath } from './vs/platform/environment/node/userDataPath.js'; +import * as perf from './vs/base/common/performance.js'; +import { resolveNLSConfiguration } from './vs/base/node/nls.js'; +import { getUNCHost, addUNCHostToAllowlist } from './vs/base/node/unc.js'; +import { INLSConfiguration } from './vs/nls.js'; +import { NativeParsedArgs } from './vs/platform/environment/common/argv.js'; + +perf.mark('code/didStartMain'); + +perf.mark('code/willLoadMainBundle', { + // When built, the main bundle is a single JS file with all + // dependencies inlined. As such, we mark `willLoadMainBundle` + // as the start of the main bundle loading process. + startTime: Math.floor(performance.timeOrigin) +}); +perf.mark('code/didLoadMainBundle'); + +// Enable portable support +const portable = configurePortable(product); + +const args = parseCLIArgs(); +// Configure static command line arguments +perf.mark('code/willConfigureCommandlineSwitches'); +const argvConfig = configureCommandlineSwitchesSync(args); +perf.mark('code/didConfigureCommandlineSwitches'); +// Enable sandbox globally unless +// 1) disabled via command line using either +// `--no-sandbox` or `--disable-chromium-sandbox` argument. +// 2) argv.json contains `disable-chromium-sandbox: true`. +if (args.sandbox && + !args['disable-chromium-sandbox'] && + !argvConfig['disable-chromium-sandbox']) { + app.enableSandbox(); +} else if (app.commandLine.hasSwitch('no-sandbox') && + !app.commandLine.hasSwitch('disable-gpu-sandbox')) { + // Disable GPU sandbox whenever --no-sandbox is used. + app.commandLine.appendSwitch('disable-gpu-sandbox'); +} else { + app.commandLine.appendSwitch('no-sandbox'); + app.commandLine.appendSwitch('disable-gpu-sandbox'); +} + +// Set userData path before app 'ready' event +perf.mark('code/willGetUserDataPath'); +const userDataPath = getUserDataPath(args, product.nameShort ?? 'code-oss-dev'); +if (process.platform === 'win32') { + const userDataUNCHost = getUNCHost(userDataPath); + if (userDataUNCHost) { + addUNCHostToAllowlist(userDataUNCHost); // enables to use UNC paths in userDataPath + } +} +app.setPath('userData', userDataPath); +perf.mark('code/didGetUserDataPath'); + +if (process.platform === 'linux') { + const snapName = process.env['SNAP_INSTANCE_NAME']; + const installedDesktopName = product.linuxDesktopName && `/usr/share/applications/${product.linuxDesktopName}.desktop`; + if (snapName) { + app.setDesktopName(`${snapName}_${product.applicationName}.desktop`); + } else if (installedDesktopName && fs.existsSync(installedDesktopName)) { + app.setDesktopName(`${product.linuxDesktopName}.desktop`); + } +} + +// Resolve code cache path +const codeCachePath = getCodeCachePath(); + +// Disable default menu (https://github.com/electron/electron/issues/35512) +Menu.setApplicationMenu(null); + +// Configure crash reporter +perf.mark('code/willStartCrashReporter'); +// If a crash-reporter-directory is specified we store the crash reports +// in the specified directory and don't upload them to the crash server. +// +// Appcenter crash reporting is enabled if +// * enable-crash-reporter runtime argument is set to 'true' +// * --disable-crash-reporter command line parameter is not set +// +// Disable crash reporting in all other cases. +if (args['crash-reporter-directory'] || (argvConfig['enable-crash-reporter'] && !args['disable-crash-reporter'])) { + configureCrashReporter(); +} +perf.mark('code/didStartCrashReporter'); + +// Set logs path before app 'ready' event if running portable +// to ensure that no 'logs' folder is created on disk at a +// location outside of the portable directory +// (https://github.com/microsoft/vscode/issues/56651) +if (portable.isPortable) { + app.setAppLogsPath(path.join(userDataPath, 'logs')); +} + +// Register custom schemes with privileges +perf.mark('code/willRegisterSchemesAsPrivileged'); +protocol.registerSchemesAsPrivileged([ + { + scheme: 'vscode-webview', + privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true, allowServiceWorkers: true, codeCache: true } + }, + { + scheme: 'vscode-file', + privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true, codeCache: true } + }, + { + scheme: 'vscode-remote-resource', + privileges: { secure: true, supportFetchAPI: true, corsEnabled: true } + }, + { + scheme: 'vscode-managed-remote-resource', + privileges: { secure: true, supportFetchAPI: true, corsEnabled: true } + } +]); +perf.mark('code/didRegisterSchemesAsPrivileged'); + +// Global app listeners +perf.mark('code/willRegisterListeners'); +registerListeners(); +perf.mark('code/didRegisterListeners'); + +/** + * We can resolve the NLS configuration early if it is defined + * in argv.json before `app.ready` event. Otherwise we can only + * resolve NLS after `app.ready` event to resolve the OS locale. + */ +let nlsConfigurationPromise: Promise | undefined = undefined; + +// Use the most preferred OS language for language recommendation. +// The API might return an empty array on Linux, such as when +// the 'C' locale is the user's only configured locale. +// No matter the OS, if the array is empty, default back to 'en'. +// Note: this forces Chromium's locale init and costs ~90ms; deferring it past `app.ready` only relocates that cost. +perf.mark('code/willGetPreferredSystemLanguages'); +const osLocale = processZhLocale((app.getPreferredSystemLanguages()?.[0] ?? 'en').toLowerCase()); +perf.mark('code/didGetPreferredSystemLanguages'); +const userLocale = getUserDefinedLocale(argvConfig); +if (userLocale) { + nlsConfigurationPromise = resolveNLSConfiguration({ + userLocale, + osLocale, + commit: product.commit, + nlsMetadataHash: product.nlsMetadataHash, + userDataPath, + nlsMetadataPath: import.meta.dirname + }); +} + +// Pass in the locale to Electron so that the +// Windows Control Overlay is rendered correctly on Windows. +// For now, don't pass in the locale on macOS due to +// https://github.com/microsoft/vscode/issues/167543. +// If the locale is `qps-ploc`, the Microsoft +// Pseudo Language Language Pack is being used. +// In that case, use `en` as the Electron locale. + +if (process.platform === 'win32' || process.platform === 'linux') { + const electronLocale = (!userLocale || userLocale === 'qps-ploc') ? 'en' : userLocale; + app.commandLine.appendSwitch('lang', electronLocale); +} + +// Load our code once ready +perf.mark('code/willWaitForAppReady'); +app.once('ready', function () { + perf.mark('code/didWaitForAppReady'); + if (args.trace) { + let traceOptions: Electron.TraceConfig | Electron.TraceCategoriesAndOptions; + if (args['trace-memory-infra']) { + const customCategories = args['trace-category-filter']?.split(',') || []; + customCategories.push('disabled-by-default-memory-infra', 'disabled-by-default-memory-infra.v8.code_stats'); + traceOptions = { + included_categories: customCategories, + excluded_categories: ['*'], + memory_dump_config: { + allowed_dump_modes: ['light', 'detailed'], + triggers: [ + { + type: 'periodic_interval', + mode: 'detailed', + min_time_between_dumps_ms: 10000 + }, + { + type: 'periodic_interval', + mode: 'light', + min_time_between_dumps_ms: 1000 + } + ] + } + }; + } else { + traceOptions = { + categoryFilter: args['trace-category-filter'] || '*', + traceOptions: args['trace-options'] || 'record-until-full,enable-sampling' + }; + } + + contentTracing.startRecording(traceOptions).finally(() => onReady()); + } else { + onReady(); + } +}); + +async function onReady() { + perf.mark('code/mainAppReady'); + + try { + const [, nlsConfig] = await Promise.all([ + mkdirpIgnoreError(codeCachePath), + resolveNlsConfiguration() + ]); + + await startup(codeCachePath, nlsConfig); + } catch (error) { + console.error(error); + } +} + +/** + * Main startup routine + */ +async function startup(codeCachePath: string | undefined, nlsConfig: INLSConfiguration): Promise { + process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig); + process.env['VSCODE_CODE_CACHE_PATH'] = codeCachePath || ''; + + // Bootstrap ESM + perf.mark('code/willBootstrapESM'); + await bootstrapESM(); + perf.mark('code/didBootstrapESM'); + + // Load Main + // Note: `out/main.js` is already compiled here, so this only executes the electron-main module graph. + perf.mark('code/willRunMainBundle'); + await import('./vs/code/electron-main/main.js'); + perf.mark('code/didRunMainBundle'); +} + +function configureCommandlineSwitchesSync(cliArgs: NativeParsedArgs) { + const SUPPORTED_ELECTRON_SWITCHES = [ + + // alias from us for --disable-gpu + 'disable-hardware-acceleration', + + // override for the color profile to use + 'force-color-profile', + + // disable LCD font rendering, a Chromium flag + 'disable-lcd-text', + + // bypass any specified proxy for the given semi-colon-separated list of hosts + 'proxy-bypass-list', + + 'remote-debugging-port' + ]; + + if (process.platform === 'linux') { + + // Force enable screen readers on Linux via this flag + SUPPORTED_ELECTRON_SWITCHES.push('force-renderer-accessibility'); + + // override which password-store is used on Linux + SUPPORTED_ELECTRON_SWITCHES.push('password-store'); + } + + const SUPPORTED_MAIN_PROCESS_SWITCHES = [ + + // Persistently enable proposed api via argv.json: https://github.com/microsoft/vscode/issues/99775 + 'enable-proposed-api', + + // Log level to use. Default is 'info'. Allowed values are 'error', 'warn', 'info', 'debug', 'trace', 'off'. + 'log-level', + + // Use an in-memory storage for secrets + 'use-inmemory-secretstorage', + + // Enables display tracking to restore maximized windows under RDP: https://github.com/electron/electron/issues/47016 + 'enable-rdp-display-tracking', + ]; + + // Read argv config + const argvConfig = readArgvConfigSync(); + + Object.keys(argvConfig).forEach(argvKey => { + const argvValue = argvConfig[argvKey]; + + // Append Electron flags to Electron + if (SUPPORTED_ELECTRON_SWITCHES.indexOf(argvKey) !== -1) { + if (argvValue === true || argvValue === 'true') { + if (argvKey === 'disable-hardware-acceleration') { + app.disableHardwareAcceleration(); // needs to be called explicitly + } else { + app.commandLine.appendSwitch(argvKey); + } + } else if (typeof argvValue === 'string' && argvValue) { + if (argvKey === 'password-store') { + // Password store + // TODO@TylerLeonhardt: Remove this migration in 3 months + let migratedArgvValue = argvValue; + if (argvValue === 'gnome' || argvValue === 'gnome-keyring') { + migratedArgvValue = 'gnome-libsecret'; + } + app.commandLine.appendSwitch(argvKey, migratedArgvValue); + } else { + app.commandLine.appendSwitch(argvKey, argvValue); + } + } + } + + // Append main process flags to process.argv + else if (SUPPORTED_MAIN_PROCESS_SWITCHES.indexOf(argvKey) !== -1) { + switch (argvKey) { + case 'enable-proposed-api': + if (Array.isArray(argvValue)) { + argvValue.forEach(id => id && typeof id === 'string' && process.argv.push('--enable-proposed-api', id)); + } else { + console.error(`Unexpected value for \`enable-proposed-api\` in argv.json. Expected array of extension ids.`); + } + break; + + case 'log-level': + if (typeof argvValue === 'string') { + process.argv.push('--log', argvValue); + } else if (Array.isArray(argvValue)) { + for (const value of argvValue) { + process.argv.push('--log', value); + } + } + break; + + case 'use-inmemory-secretstorage': + if (argvValue) { + process.argv.push('--use-inmemory-secretstorage'); + } + break; + + case 'enable-rdp-display-tracking': + if (argvValue) { + process.argv.push('--enable-rdp-display-tracking'); + } + break; + } + } + }); + + // Following features are enabled from the runtime: + // `NetAdapterMaxBufSizeFeature` - Specify the max buffer size for NetToMojoPendingBuffer, refs https://github.com/microsoft/vscode/issues/268800 + // `DocumentPolicyIncludeJSCallStacksInCrashReports` - https://www.electronjs.org/docs/latest/api/web-frame-main#framecollectjavascriptcallstack-experimental + // `EarlyEstablishGpuChannel` - Refs https://issues.chromium.org/issues/40208065 + // `EstablishGpuChannelAsync` - Refs https://issues.chromium.org/issues/40208065 + // `GlobalShortcutsPortal` - Enables Electron's `globalShortcut` (system-wide keybindings) on Linux Wayland via the XDG global shortcuts portal (no-op elsewhere) + const featuresToEnable = + `NetAdapterMaxBufSizeFeature:NetAdapterMaxBufSize/8192,DocumentPolicyIncludeJSCallStacksInCrashReports,EarlyEstablishGpuChannel,EstablishGpuChannelAsync${process.platform === 'linux' ? ',GlobalShortcutsPortal' : ''},${app.commandLine.getSwitchValue('enable-features')}`; + app.commandLine.appendSwitch('enable-features', featuresToEnable); + + // Following features are disabled from the runtime: + // `CalculateNativeWinOcclusion` - Disable native window occlusion tracker (https://groups.google.com/a/chromium.org/g/embedder-dev/c/ZF3uHHyWLKw/m/VDN2hDXMAAAJ) + const featuresToDisable = + `CalculateNativeWinOcclusion,${app.commandLine.getSwitchValue('disable-features')}`; + app.commandLine.appendSwitch('disable-features', featuresToDisable); + + // Blink features to configure. + // `FontMatchingCTMigration` - Siwtch font matching on macOS to Appkit (Refs https://github.com/microsoft/vscode/issues/224496#issuecomment-2270418470). + // `StandardizedBrowserZoom` - Disable zoom adjustment for bounding box (https://github.com/microsoft/vscode/issues/232750#issuecomment-2459495394) + const blinkFeaturesToDisable = + `FontMatchingCTMigration,StandardizedBrowserZoom,${app.commandLine.getSwitchValue('disable-blink-features')}`; + app.commandLine.appendSwitch('disable-blink-features', blinkFeaturesToDisable); + + // Support JS Flags + const jsFlags = getJSFlags(cliArgs, argvConfig); + if (jsFlags) { + app.commandLine.appendSwitch('js-flags', jsFlags); + } + + // Use portal version 4 that supports current_folder option + // to address https://github.com/microsoft/vscode/issues/213780 + // Runtime sets the default version to 3, refs https://github.com/electron/electron/pull/44426 + app.commandLine.appendSwitch('xdg-portal-required-version', '4'); + + // Increase the maximum number of active WebGL contexts as each terminal may + // use up to 2 + app.commandLine.appendSwitch('max-active-webgl-contexts', '32'); + + return argvConfig; +} + +interface IArgvConfig { + [key: string]: string | string[] | boolean | undefined; + readonly locale?: string; + readonly 'disable-lcd-text'?: boolean; + readonly 'proxy-bypass-list'?: string; + readonly 'disable-hardware-acceleration'?: boolean; + readonly 'force-color-profile'?: string; + readonly 'enable-crash-reporter'?: boolean; + readonly 'crash-reporter-id'?: string; + readonly 'enable-proposed-api'?: string[]; + readonly 'log-level'?: string | string[]; + readonly 'disable-chromium-sandbox'?: boolean; + readonly 'use-inmemory-secretstorage'?: boolean; + readonly 'enable-rdp-display-tracking'?: boolean; + readonly 'remote-debugging-port'?: string; + readonly 'js-flags'?: string; +} + +function readArgvConfigSync(): IArgvConfig { + + // Read or create the argv.json config file sync before app('ready') + const argvConfigPath = getArgvConfigPath(); + let argvConfig: IArgvConfig | undefined = undefined; + try { + argvConfig = parse(fs.readFileSync(argvConfigPath).toString()); + } catch (error) { + if (error && error.code === 'ENOENT') { + createDefaultArgvConfigSync(argvConfigPath); + } else { + console.warn(`Unable to read argv.json configuration file in ${argvConfigPath}, falling back to defaults (${error})`); + } + } + + // Fallback to default + if (!argvConfig) { + argvConfig = {}; + } + + return argvConfig; +} + +function createDefaultArgvConfigSync(argvConfigPath: string): void { + try { + + // Ensure argv config parent exists + const argvConfigPathDirname = path.dirname(argvConfigPath); + if (!fs.existsSync(argvConfigPathDirname)) { + fs.mkdirSync(argvConfigPathDirname); + } + + // Default argv content + const defaultArgvConfigContent = [ + '// This configuration file allows you to pass permanent command line arguments to VS Code.', + '// Only a subset of arguments is currently supported to reduce the likelihood of breaking', + '// the installation.', + '//', + '// PLEASE DO NOT CHANGE WITHOUT UNDERSTANDING THE IMPACT', + '//', + '// NOTE: Changing this file requires a restart of VS Code.', + '{', + ' // Use software rendering instead of hardware accelerated rendering.', + ' // This can help in cases where you see rendering issues in VS Code.', + ' // "disable-hardware-acceleration": true', + '}' + ]; + + // Create initial argv.json with default content + fs.writeFileSync(argvConfigPath, defaultArgvConfigContent.join('\n')); + } catch (error) { + console.error(`Unable to create argv.json configuration file in ${argvConfigPath}, falling back to defaults (${error})`); + } +} + +function getArgvConfigPath(): string { + const vscodePortable = process.env['VSCODE_PORTABLE']; + if (vscodePortable) { + return path.join(vscodePortable, 'argv.json'); + } + + let dataFolderName = product.dataFolderName; + if (process.env['VSCODE_DEV']) { + dataFolderName = `${dataFolderName}-dev`; + } + + return path.join(os.homedir(), dataFolderName!, 'argv.json'); +} + +function configureCrashReporter(): void { + let crashReporterDirectory = args['crash-reporter-directory']; + let submitURL = ''; + if (crashReporterDirectory) { + crashReporterDirectory = path.normalize(crashReporterDirectory); + + if (!path.isAbsolute(crashReporterDirectory)) { + console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory must be absolute.`); + app.exit(1); + } + + if (!fs.existsSync(crashReporterDirectory)) { + try { + fs.mkdirSync(crashReporterDirectory, { recursive: true }); + } catch (error) { + console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory does not seem to exist or cannot be created.`); + app.exit(1); + } + } + + // Crashes are stored in the crashDumps directory by default, so we + // need to change that directory to the provided one + console.log(`Found --crash-reporter-directory argument. Setting crashDumps directory to be '${crashReporterDirectory}'`); + app.setPath('crashDumps', crashReporterDirectory); + } + + // Otherwise we configure the crash reporter from product.json + else { + const appCenter = product.appCenter; + if (appCenter) { + const isWindows = (process.platform === 'win32'); + const isLinux = (process.platform === 'linux'); + const isDarwin = (process.platform === 'darwin'); + const crashReporterId = argvConfig['crash-reporter-id']; + const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + if (crashReporterId && uuidPattern.test(crashReporterId)) { + if (isWindows) { + switch (process.arch) { + case 'x64': + submitURL = appCenter['win32-x64']; + break; + case 'arm64': + submitURL = appCenter['win32-arm64']; + break; + } + } else if (isDarwin) { + if (product.darwinUniversalAssetId) { + submitURL = appCenter['darwin-universal']; + } else { + switch (process.arch) { + case 'x64': + submitURL = appCenter.darwin; + break; + case 'arm64': + submitURL = appCenter['darwin-arm64']; + break; + } + } + } else if (isLinux) { + submitURL = appCenter['linux-x64']; + } + submitURL = submitURL.concat('&uid=', crashReporterId, '&iid=', crashReporterId, '&sid=', crashReporterId); + // Send the id for child node process that are explicitly starting crash reporter. + // For vscode this is ExtensionHost process currently. + const argv = process.argv; + const endOfArgsMarkerIndex = argv.indexOf('--'); + if (endOfArgsMarkerIndex === -1) { + argv.push('--crash-reporter-id', crashReporterId); + } else { + // if the we have an argument "--" (end of argument marker) + // we cannot add arguments at the end. rather, we add + // arguments before the "--" marker. + argv.splice(endOfArgsMarkerIndex, 0, '--crash-reporter-id', crashReporterId); + } + } + } + } + + // Start crash reporter for all processes + const productName = (product.crashReporter ? product.crashReporter.productName : undefined) || product.nameShort; + const companyName = (product.crashReporter ? product.crashReporter.companyName : undefined) || 'Microsoft'; + const uploadToServer = Boolean(!process.env['VSCODE_DEV'] && submitURL && !crashReporterDirectory); + crashReporter.start({ + companyName, + productName: process.env['VSCODE_DEV'] ? `${productName} Dev` : productName, + submitURL, + uploadToServer, + compress: true, + ignoreSystemCrashHandler: true + }); +} + +function getJSFlags(cliArgs: NativeParsedArgs, argvConfig: IArgvConfig): string | null { + const jsFlags: string[] = []; + + // Add any existing JS flags we already got from the command line + if (cliArgs['js-flags']) { + jsFlags.push(cliArgs['js-flags']); + } + + // Add JS flags from runtime arguments (argv.json) + if (typeof argvConfig['js-flags'] === 'string' && argvConfig['js-flags']) { + jsFlags.push(argvConfig['js-flags']); + } + + return jsFlags.length > 0 ? jsFlags.join(' ') : null; +} + +function parseCLIArgs(): NativeParsedArgs { + return minimist(process.argv, { + string: [ + 'user-data-dir', + 'locale', + 'js-flags', + 'crash-reporter-directory' + ], + boolean: [ + 'disable-chromium-sandbox', + ], + default: { + 'sandbox': true + }, + alias: { + 'no-sandbox': 'sandbox' + } + }); +} + +function registerListeners(): void { + + /** + * macOS: when someone drops a file to the not-yet running VSCode, the open-file event fires even before + * the app-ready event. We listen very early for open-file and remember this upon startup as path to open. + */ + const macOpenFiles: string[] = []; + (globalThis as { macOpenFiles?: string[] }).macOpenFiles = macOpenFiles; + app.on('open-file', function (event, path) { + macOpenFiles.push(path); + }); + + /** + * macOS: react to open-url requests. + */ + const openUrls: string[] = []; + const onOpenUrl = + function (event: { preventDefault: () => void }, url: string) { + event.preventDefault(); + + openUrls.push(url); + }; + + app.on('will-finish-launching', function () { + app.on('open-url', onOpenUrl); + }); + + (globalThis as { getOpenUrls?: () => string[] }).getOpenUrls = function () { + app.removeListener('open-url', onOpenUrl); + + return openUrls; + }; +} + +function getCodeCachePath(): string | undefined { + + // explicitly disabled via CLI args + if (process.argv.indexOf('--no-cached-data') > 0) { + return undefined; + } + + // running out of sources + if (process.env['VSCODE_DEV']) { + return undefined; + } + + // require commit id + const commit = product.commit; + if (!commit) { + return undefined; + } + + return path.join(userDataPath, 'CachedData', commit); +} + +async function mkdirpIgnoreError(dir: string | undefined): Promise { + if (typeof dir === 'string') { + try { + await fs.promises.mkdir(dir, { recursive: true }); + + return dir; + } catch (error) { + // ignore + } + } + + return undefined; +} + +//#region NLS Support + +function processZhLocale(appLocale: string): string { + if (appLocale.startsWith('zh')) { + const region = appLocale.split('-')[1]; + + // On Windows and macOS, Chinese languages returned by + // app.getPreferredSystemLanguages() start with zh-hans + // for Simplified Chinese or zh-hant for Traditional Chinese, + // so we can easily determine whether to use Simplified or Traditional. + // However, on Linux, Chinese languages returned by that same API + // are of the form zh-XY, where XY is a country code. + // For China (CN), Singapore (SG), and Malaysia (MY) + // country codes, assume they use Simplified Chinese. + // For other cases, assume they use Traditional. + if (['hans', 'cn', 'sg', 'my'].includes(region)) { + return 'zh-cn'; + } + + return 'zh-tw'; + } + + return appLocale; +} + +/** + * Resolve the NLS configuration + */ +async function resolveNlsConfiguration(): Promise { + perf.mark('code/willResolveNlsConfiguration'); + try { + + // First, we need to test a user defined locale. + // If it fails we try the app locale. + // If that fails we fall back to English. + + const nlsConfiguration = nlsConfigurationPromise ? await nlsConfigurationPromise : undefined; + if (nlsConfiguration) { + return nlsConfiguration; + } + + // Try to use the app locale which is only valid + // after the app ready event has been fired. + + let userLocale = app.getLocale(); + if (!userLocale) { + return { + userLocale: 'en', + osLocale, + resolvedLanguage: 'en', + defaultMessagesFile: path.join(import.meta.dirname, 'nls.messages.json'), + + // NLS: below 2 are a relic from old times only used by vscode-nls and deprecated + locale: 'en', + availableLanguages: {} + }; + } + + // See above the comment about the loader and case sensitiveness + userLocale = processZhLocale(userLocale.toLowerCase()); + + return await resolveNLSConfiguration({ + userLocale, + osLocale, + commit: product.commit, + nlsMetadataHash: product.nlsMetadataHash, + userDataPath, + nlsMetadataPath: import.meta.dirname + }); + } finally { + perf.mark('code/didResolveNlsConfiguration'); + } +} + +/** + * Language tags are case insensitive however an ESM loader is case sensitive + * To make this work on case preserving & insensitive FS we do the following: + * the language bundles have lower case language tags and we always lower case + * the locale we receive from the user or OS. + */ +function getUserDefinedLocale(argvConfig: IArgvConfig): string | undefined { + const locale = args.locale; + if (locale) { + return locale.toLowerCase(); // a directly provided --locale always wins + } + + return typeof argvConfig?.locale === 'string' ? argvConfig.locale.toLowerCase() : undefined; +} + +//#endregion diff --git a/src/vs/base/node/nodeCompileCache.ts b/src/vs/base/node/nodeCompileCache.ts new file mode 100644 index 00000000000000..623af0058d27f9 --- /dev/null +++ b/src/vs/base/node/nodeCompileCache.ts @@ -0,0 +1,141 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; +import { constants, enableCompileCache, flushCompileCache } from 'node:module'; +import { pathToFileURL } from 'node:url'; +import { getHeapCodeStatistics } from 'node:v8'; +import { join } from '../common/path.js'; + +declare module 'node:module' { + interface EnableCompileCacheOptions { + readOnly?: boolean; + eager?: boolean | { moduleIdentifiers: string[] }; + } +} + +export const nodeCompileCacheKinds = ['main', 'extension-host', 'shared-process', 'pty-host', 'agent-host'] as const; +export type NodeCompileCacheKind = typeof nodeCompileCacheKinds[number]; + +let enabledKind: NodeCompileCacheKind | undefined; + +const utilityProcessCacheKinds: Readonly> = { + extensionHost: 'extension-host', + 'shared-process': 'shared-process', + ptyHost: 'pty-host', + agentHost: 'agent-host' +}; + +const applicationRoot = join(import.meta.dirname, '..'); +const eagerCompileCacheDependencyModuleIdentifiers: Readonly> = { + main: [], + 'extension-host': [ + join(applicationRoot, 'extensions/git/dist/main.js'), + join(applicationRoot, 'extensions/github-authentication/dist/extension.js'), + pathToFileURL(join(applicationRoot, 'extensions/github/dist/extension.js')).href, + join(applicationRoot, 'extensions/merge-conflict/dist/mergeConflictMain.js'), + join(applicationRoot, 'extensions/emmet/dist/node/emmetNodeMain.js') + ], + 'shared-process': [], + 'pty-host': [], + 'agent-host': [ + join(applicationRoot, 'node_modules.asar/@vscode/tree-sitter-wasm/wasm/tree-sitter.js'), + join(applicationRoot, 'node_modules.asar/@xterm/headless/lib-headless/xterm-headless.js'), + pathToFileURL(join(applicationRoot, 'node_modules.asar/@github/copilot-sdk/dist/client.js')).href, + join(applicationRoot, 'node_modules.asar/vscode-jsonrpc/lib/common/connection.js') + ] +}; + +export function getNodeCompileCacheKindForUtilityProcess(type: string): NodeCompileCacheKind | undefined { + return utilityProcessCacheKinds[type]; +} + +export function enableNodeCompileCache(kind: NodeCompileCacheKind, eagerEntryPointModuleIdentifier: string): boolean { + enabledKind = kind; + process.env['VSCODE_NODE_COMPILE_CACHE_ROOT'] = getNodeCompileCacheRoot(); + const cacheDirectory = getNodeCompileCacheDirectory(kind); + const isGeneratingCache = process.env['VSCODE_GENERATE_NODE_COMPILE_CACHE'] === '1'; + const runtimeCachePrefix = `${process.version}-${process.arch}-`; + const hasRuntimeCache = fs.existsSync(cacheDirectory) && fs.readdirSync(cacheDirectory).some(entry => entry.startsWith(runtimeCachePrefix)); + + if (process.env['VSCODE_DEV'] || (!isGeneratingCache && !hasRuntimeCache)) { + return false; + } + + const result = enableCompileCache({ + directory: cacheDirectory, + portable: true, + readOnly: !isGeneratingCache, + ...isGeneratingCache && { + eager: { + moduleIdentifiers: [ + eagerEntryPointModuleIdentifier, + ...eagerCompileCacheDependencyModuleIdentifiers[kind] + ] + } + } + }); + + if (result.status === constants.compileCacheStatus.FAILED) { + const message = `Unable to enable the packaged Node.js compile cache for ${kind}: ${result.message ?? 'unknown error'}`; + if (isGeneratingCache) { + throw new Error(message); + } + console.warn(message); + return false; + } + + process.env['VSCODE_NODE_COMPILE_CACHE_KIND'] = kind; + return isGeneratingCache; +} + +export function markNodeCompileCacheReady(): void { + const kind = enabledKind ?? process.env['VSCODE_NODE_COMPILE_CACHE_KIND'] as NodeCompileCacheKind | undefined; + if (!kind || !nodeCompileCacheKinds.includes(kind)) { + return; + } + + if (process.env['VSCODE_GENERATE_NODE_COMPILE_CACHE'] === '1') { + flushCompileCache(); + fs.writeFileSync(getNodeCompileCacheReadyMarkerPath(kind), ''); + } + + const measurementsDirectory = process.env['VSCODE_NODE_COMPILE_CACHE_MEASUREMENTS']; + if (measurementsDirectory) { + fs.writeFileSync(join(measurementsDirectory, `${kind}.json`), JSON.stringify({ + kind, + pid: process.pid, + heapCodeStatistics: getHeapCodeStatistics(), + memoryUsage: process.memoryUsage(), + resourceUsage: process.resourceUsage() + })); + } +} + +export async function waitForNodeCompileCacheReady(): Promise { + const pendingKinds = new Set(nodeCompileCacheKinds); + while (pendingKinds.size > 0) { + for (const kind of pendingKinds) { + if (fs.existsSync(getNodeCompileCacheReadyMarkerPath(kind))) { + pendingKinds.delete(kind); + } + } + if (pendingKinds.size > 0) { + await new Promise(resolve => setTimeout(resolve, 50)); + } + } +} + +function getNodeCompileCacheDirectory(kind: NodeCompileCacheKind): string { + return join(getNodeCompileCacheRoot(), kind); +} + +function getNodeCompileCacheReadyMarkerPath(kind: NodeCompileCacheKind): string { + return join(getNodeCompileCacheDirectory(kind), '.ready'); +} + +function getNodeCompileCacheRoot(): string { + return process.env['VSCODE_NODE_COMPILE_CACHE_ROOT'] ?? join(import.meta.dirname, '..', 'node-compile-cache'); +} diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index b9a22c34f22106..e2ffcb77c88971 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -152,6 +152,7 @@ import { ITerminalSandboxService, NullTerminalSandboxService } from '../../platf import ErrorTelemetry from '../../platform/telemetry/electron-main/errorTelemetry.js'; import { IProtocolMainService } from '../../platform/protocol/electron-main/protocol.js'; import { createRemoteResourceRequestHandler } from '../../platform/protocol/electron-main/remoteResourceProtocol.js'; +import { markNodeCompileCacheReady, waitForNodeCompileCacheReady } from '../../base/node/nodeCompileCache.js'; type OSProxyConfigEvent = { readonly success: boolean; @@ -762,7 +763,7 @@ export class CodeApplication extends Disposable { // cannot fully observe. const agentHostStarter = appInstantiationService.createInstance(ElectronAgentHostStarter, { machineId, sqmId, devDeviceId }); // This manager self-disposes after its lifecycle join; CodeApplication disposes before later shutdown listeners run. - appInstantiationService.createInstance(AgentHostProcessManager, agentHostStarter, process.platform); + const agentHostProcessManager = appInstantiationService.createInstance(AgentHostProcessManager, agentHostStarter, process.platform); // Metered connection telemetry appInstantiationService.invokeFunction(accessor => { @@ -790,7 +791,7 @@ export class CodeApplication extends Disposable { // Open Windows mark('code/willOpenFirstWindow'); - await appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, initialProtocolUrls)); + const windows = await appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, initialProtocolUrls)); mark('code/didOpenFirstWindow'); // Signal phase: after window open @@ -799,6 +800,17 @@ export class CodeApplication extends Disposable { // Post Open Windows Tasks this.afterWindowOpen(appInstantiationService); + const isGeneratingNodeCompileCache = process.env['VSCODE_GENERATE_NODE_COMPILE_CACHE'] === '1'; + const shouldStartCriticalNodeProcesses = isGeneratingNodeCompileCache || process.env['VSCODE_MEASURE_NODE_COMPILE_CACHE'] === '1'; + if (shouldStartCriticalNodeProcesses) { + await Promise.all([ + ...windows.map(window => window.ready()), + sharedProcessReady, + appInstantiationService.invokeFunction(accessor => accessor.get(ILocalPtyService).getLatency()), + agentHostProcessManager.start() + ]); + } + // Set lifecycle phase to `Eventually` after a short delay and when idle (min 2.5sec, max 5sec) const eventuallyPhaseScheduler = this._register(new RunOnceScheduler(() => { this._register(runWhenGlobalIdle(() => { @@ -808,9 +820,21 @@ export class CodeApplication extends Disposable { // Eventually Post Open Window Tasks this.eventuallyAfterWindowOpen(appInstantiationService); + + if (shouldStartCriticalNodeProcesses) { + markNodeCompileCacheReady(); + } }, 2500)); }, 2500)); eventuallyPhaseScheduler.schedule(); + + if (isGeneratingNodeCompileCache) { + await Promise.all([ + this.lifecycleMainService.when(LifecycleMainPhase.Eventually), + waitForNodeCompileCacheReady() + ]); + await this.lifecycleMainService.quit(); + } } private async setupProtocolUrlHandlers(accessor: ServicesAccessor, mainProcessElectronServer: ElectronIPCServer): Promise { diff --git a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts index d32bf32badc887..cce287d4488fd1 100644 --- a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts @@ -70,6 +70,7 @@ import { UserDataSyncService } from '../../../platform/userDataSync/common/userD import { UserDataSyncServiceChannel } from '../../../platform/userDataSync/common/userDataSyncServiceIpc.js'; import { UserDataSyncStoreManagementService, UserDataSyncStoreService } from '../../../platform/userDataSync/common/userDataSyncStoreService.js'; import { IUserDataProfileStorageService } from '../../../platform/userDataProfile/common/userDataProfileStorageService.js'; +import { markNodeCompileCacheReady } from '../../../base/node/nodeCompileCache.js'; import { SharedProcessUserDataProfileStorageService } from '../../../platform/userDataProfile/node/userDataProfileStorageService.js'; import { ActiveWindowManager } from '../../../platform/windows/node/windowTracker.js'; import { ISignService } from '../../../platform/sign/common/sign.js'; @@ -618,6 +619,7 @@ export async function main(configuration: ISharedProcessConfiguration): Promise< await sharedProcess.init(); process.parentPort.postMessage(SharedProcessLifecycle.initDone); + markNodeCompileCacheReady(); } catch (error) { process.parentPort.postMessage({ error: error.toString() }); } diff --git a/src/vs/platform/agentHost/node/agentHostMain.ts b/src/vs/platform/agentHost/node/agentHostMain.ts index f74c2cac73622c..70d1dd4ac01279 100644 --- a/src/vs/platform/agentHost/node/agentHostMain.ts +++ b/src/vs/platform/agentHost/node/agentHostMain.ts @@ -59,6 +59,7 @@ import { join } from '../../../base/common/path.js'; import ErrorTelemetry from '../../telemetry/node/errorTelemetry.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { AgentHostLaunchKindEnvVar, readAgentHostLaunchKind, type AgentHostLaunchKind } from '../common/agentHostTelemetry.js'; +import { markNodeCompileCacheReady } from '../../../base/node/nodeCompileCache.js'; // Entry point for the agent host utility process. // Sets up IPC, logging, and registers agent providers (Copilot). @@ -483,6 +484,7 @@ async function startAgentHost(): Promise { logService.error('Failed to start WebSocket server', err); }).finally(() => { agentService.markStartupComplete(); + markNodeCompileCacheReady(); }); process.once('exit', () => { diff --git a/src/vs/platform/agentHost/node/agentHostService.ts b/src/vs/platform/agentHost/node/agentHostService.ts index 1f1dc3e83c213b..92a076a9fed5dd 100644 --- a/src/vs/platform/agentHost/node/agentHostService.ts +++ b/src/vs/platform/agentHost/node/agentHostService.ts @@ -70,6 +70,10 @@ export class AgentHostProcessManager extends Disposable { } } + start(): Promise { + return this._ensureStarted(); + } + private _ensureStarted(): Promise { if (this._wasQuitRequested || this._store.isDisposed) { return Promise.reject(new Error('Agent Host process manager is shutting down.')); diff --git a/src/vs/platform/terminal/node/ptyHostMain.ts b/src/vs/platform/terminal/node/ptyHostMain.ts index 01887e8f9269e3..b67ca6b000773f 100644 --- a/src/vs/platform/terminal/node/ptyHostMain.ts +++ b/src/vs/platform/terminal/node/ptyHostMain.ts @@ -22,6 +22,7 @@ import { PtyService } from './ptyService.js'; import { isUtilityProcess } from '../../../base/parts/sandbox/node/electronTypes.js'; import { timeout } from '../../../base/common/async.js'; import { DisposableStore } from '../../../base/common/lifecycle.js'; +import { markNodeCompileCacheReady } from '../../../base/node/nodeCompileCache.js'; startPtyHost(); @@ -91,6 +92,7 @@ async function startPtyHost() { if (_isUtilityProcess) { server.registerChannel(TerminalIpcChannels.PtyHostWindow, ptyServiceChannel); } + markNodeCompileCacheReady(); // Clean up process.once('exit', () => { diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 36cec4d29bc698..42008549e96dff 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -816,9 +816,12 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme .then(() => { this._eagerExtensionsActivated.open(); this._logService.info(`Eager extensions activated`); + this._onEagerExtensionsActivated(); }); } + protected _onEagerExtensionsActivated(): void { } + // -- called by extensions public registerRemoteAuthorityResolver(authorityPrefix: string, resolver: vscode.RemoteAuthorityResolver): vscode.Disposable { diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index ab221e8aed7c7f..c95d2fda6c2a78 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -24,6 +24,7 @@ import { assertType } from '../../../base/common/types.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { BidirectionalMap } from '../../../base/common/map.js'; import { DisposableStore, toDisposable } from '../../../base/common/lifecycle.js'; +import { markNodeCompileCacheReady } from '../../../base/node/nodeCompileCache.js'; const require = nodeModule.createRequire(import.meta.url); class NodeModuleRequireInterceptor extends RequireInterceptor { @@ -237,4 +238,8 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { } } } + + protected override _onEagerExtensionsActivated(): void { + markNodeCompileCacheReady(); + } }