mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-18 01:04:41 +08:00
Fix remote Agent Host dependency packaging (#326019)
* Fix remote Agent Host dependencies Include fs-copyfile in REH packages and verify that startup imports are declared in the remote manifest. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Smoke test packaged remote Agent Host Launch the packaged Agent Host with its bundled Node after native REH builds and require it to bind a WebSocket listener. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { type ChildProcess, fork } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { setTimeout as delay } from 'timers/promises';
|
||||
|
||||
const startupTimeoutMs = 30_000;
|
||||
const shutdownTimeoutMs = 5_000;
|
||||
const readyPattern = /Agent host server listening on \S+/;
|
||||
|
||||
async function main(serverRoot: string | undefined): Promise<void> {
|
||||
if (!serverRoot) {
|
||||
throw new Error('Usage: node smokeTestAgentHost.ts <server-root>');
|
||||
}
|
||||
|
||||
const nodePath = path.join(serverRoot, process.platform === 'win32' ? 'node.exe' : 'node');
|
||||
const bootstrapPath = path.join(serverRoot, 'out', 'bootstrap-fork.js');
|
||||
for (const requiredPath of [nodePath, bootstrapPath]) {
|
||||
if (!fs.existsSync(requiredPath)) {
|
||||
throw new Error(`Packaged Agent Host dependency not found: ${requiredPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-agent-host-smoke-'));
|
||||
const logsPath = path.join(temporaryRoot, 'logs');
|
||||
const userDataPath = path.join(temporaryRoot, 'user-data');
|
||||
fs.mkdirSync(logsPath, { recursive: true });
|
||||
fs.mkdirSync(userDataPath, { recursive: true });
|
||||
|
||||
const child = fork(bootstrapPath, [
|
||||
'--type=agentHost',
|
||||
'--logsPath', logsPath,
|
||||
'--user-data-dir', userDataPath,
|
||||
'--disable-telemetry',
|
||||
], {
|
||||
cwd: serverRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
VSCODE_DEV: undefined,
|
||||
VSCODE_ESM_ENTRYPOINT: 'vs/platform/agentHost/node/agentHostMain',
|
||||
VSCODE_AGENT_HOST_CONNECTION_TOKEN: undefined,
|
||||
VSCODE_AGENT_HOST_HOST: '127.0.0.1',
|
||||
VSCODE_AGENT_HOST_PORT: '0',
|
||||
VSCODE_AGENT_HOST_SOCKET_PATH: undefined,
|
||||
VSCODE_NLS_CONFIG: JSON.stringify({
|
||||
userLocale: 'en',
|
||||
osLocale: 'en',
|
||||
resolvedLanguage: 'en',
|
||||
defaultMessagesFile: path.join(serverRoot, 'out', 'nls.messages.json'),
|
||||
}),
|
||||
VSCODE_PIPE_LOGGING: 'false',
|
||||
VSCODE_VERBOSE_LOGGING: 'false',
|
||||
},
|
||||
execPath: nodePath,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
try {
|
||||
await waitForReady(child);
|
||||
console.log('Packaged Agent Host started successfully.');
|
||||
} finally {
|
||||
await stopProcess(child);
|
||||
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function waitForReady(child: ChildProcess): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let output = '';
|
||||
let settled = false;
|
||||
|
||||
const finish = (error?: Error) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearTimeout(timeout);
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
const appendOutput = (data: Buffer) => {
|
||||
const text = data.toString();
|
||||
process.stdout.write(text);
|
||||
output = `${output}${text}`.slice(-64 * 1024);
|
||||
if (readyPattern.test(output)) {
|
||||
finish();
|
||||
}
|
||||
};
|
||||
|
||||
child.stdout?.on('data', appendOutput);
|
||||
child.stderr?.on('data', appendOutput);
|
||||
child.once('error', error => finish(error));
|
||||
child.once('exit', (code, signal) => {
|
||||
finish(new Error(`Packaged Agent Host exited before becoming ready (code: ${code}, signal: ${signal}).\n${output}`));
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
finish(new Error(`Timed out after ${startupTimeoutMs}ms waiting for the packaged Agent Host to start.\n${output}`));
|
||||
}, startupTimeoutMs);
|
||||
});
|
||||
}
|
||||
|
||||
async function stopProcess(child: ChildProcess): Promise<void> {
|
||||
if (child.exitCode !== null || child.signalCode !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const exited = new Promise<void>(resolve => child.once('exit', () => resolve()));
|
||||
child.kill();
|
||||
await Promise.race([exited, delay(shutdownTimeoutMs)]);
|
||||
if (child.exitCode === null && child.signalCode === null) {
|
||||
child.kill('SIGKILL');
|
||||
await exited;
|
||||
}
|
||||
}
|
||||
|
||||
main(process.argv[2]).catch(error => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -200,6 +200,10 @@ steps:
|
||||
AZURE_DEVOPS_EXT_PAT: $(System.AccessToken)
|
||||
displayName: Build server
|
||||
|
||||
- ${{ if eq(parameters.VSCODE_ARCH, 'arm64') }}:
|
||||
- script: node build/azure-pipelines/common/smokeTestAgentHost.ts "$(Agent.BuildDirectory)/vscode-server-darwin-$(VSCODE_ARCH)"
|
||||
displayName: 🧪 Smoke test packaged Agent Host
|
||||
|
||||
- script: |
|
||||
set -e
|
||||
npm run gulp vscode-reh-web-darwin-$(VSCODE_ARCH)-min-ci
|
||||
|
||||
@@ -290,6 +290,10 @@ steps:
|
||||
AZURE_DEVOPS_EXT_PAT: $(System.AccessToken)
|
||||
displayName: Build server
|
||||
|
||||
- ${{ if eq(parameters.VSCODE_ARCH, 'x64') }}:
|
||||
- script: node build/azure-pipelines/common/smokeTestAgentHost.ts "$(Agent.BuildDirectory)/vscode-server-linux-$(VSCODE_ARCH)"
|
||||
displayName: 🧪 Smoke test packaged Agent Host
|
||||
|
||||
- script: |
|
||||
set -e
|
||||
npm run gulp vscode-reh-web-linux-$(VSCODE_ARCH)-min-ci
|
||||
|
||||
@@ -229,6 +229,10 @@ steps:
|
||||
AZURE_DEVOPS_EXT_PAT: $(System.AccessToken)
|
||||
displayName: Build server
|
||||
|
||||
- ${{ if eq(parameters.VSCODE_ARCH, 'x64') }}:
|
||||
- powershell: node build/azure-pipelines/common/smokeTestAgentHost.ts "$(Agent.BuildDirectory)\vscode-server-win32-$(VSCODE_ARCH)"
|
||||
displayName: 🧪 Smoke test packaged Agent Host
|
||||
|
||||
- powershell: |
|
||||
. build/azure-pipelines/win32/exec.ps1
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 fs from 'fs';
|
||||
import { builtinModules } from 'module';
|
||||
import path from 'path';
|
||||
import { suite, test } from 'node:test';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
const repositoryRoot = path.resolve(import.meta.dirname, '../../..');
|
||||
const agentHostEntryPoints = [
|
||||
'src/vs/platform/agentHost/node/agentHostMain.ts',
|
||||
'src/vs/platform/agentHost/node/agentHostServerMain.ts',
|
||||
];
|
||||
|
||||
suite('Agent Host dependencies', () => {
|
||||
test('runtime packages are included in the remote server', () => {
|
||||
const remotePackageJson = JSON.parse(fs.readFileSync(path.join(repositoryRoot, 'remote/package.json'), 'utf8')) as {
|
||||
dependencies?: Record<string, string>;
|
||||
optionalDependencies?: Record<string, string>;
|
||||
};
|
||||
const packagedDependencies = new Set([
|
||||
...Object.keys(remotePackageJson.dependencies ?? {}),
|
||||
...Object.keys(remotePackageJson.optionalDependencies ?? {}),
|
||||
]);
|
||||
const runtimeImports = collectRuntimePackageImports(
|
||||
agentHostEntryPoints.map(entryPoint => path.join(repositoryRoot, entryPoint))
|
||||
);
|
||||
const missingDependencies = [...runtimeImports]
|
||||
.filter(([packageName]) => !packagedDependencies.has(packageName))
|
||||
.map(([packageName, importers]) => `${packageName}: ${[...importers].sort().map(importer => path.relative(repositoryRoot, importer)).join(', ')}`)
|
||||
.sort();
|
||||
|
||||
assert.deepStrictEqual(missingDependencies, []);
|
||||
});
|
||||
});
|
||||
|
||||
function collectRuntimePackageImports(entryPoints: readonly string[]): Map<string, Set<string>> {
|
||||
const pendingFiles = [...entryPoints];
|
||||
const visitedFiles = new Set<string>();
|
||||
const packageImports = new Map<string, Set<string>>();
|
||||
const builtInModules = new Set([...builtinModules, ...builtinModules.map(moduleName => `node:${moduleName}`)]);
|
||||
|
||||
while (pendingFiles.length > 0) {
|
||||
const file = pendingFiles.pop()!;
|
||||
if (visitedFiles.has(file)) {
|
||||
continue;
|
||||
}
|
||||
visitedFiles.add(file);
|
||||
|
||||
const sourceFile = ts.createSourceFile(file, fs.readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
||||
for (const moduleSpecifier of getRuntimeModuleSpecifiers(sourceFile)) {
|
||||
if (moduleSpecifier.startsWith('.')) {
|
||||
const importedFile = resolveSourceImport(file, moduleSpecifier);
|
||||
if (importedFile) {
|
||||
pendingFiles.push(importedFile);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (builtInModules.has(moduleSpecifier)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const packageName = getPackageName(moduleSpecifier);
|
||||
let importers = packageImports.get(packageName);
|
||||
if (!importers) {
|
||||
importers = new Set<string>();
|
||||
packageImports.set(packageName, importers);
|
||||
}
|
||||
importers.add(file);
|
||||
}
|
||||
}
|
||||
|
||||
return packageImports;
|
||||
}
|
||||
|
||||
function getRuntimeModuleSpecifiers(sourceFile: ts.SourceFile): string[] {
|
||||
const moduleSpecifiers: string[] = [];
|
||||
for (const statement of sourceFile.statements) {
|
||||
if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) && isRuntimeImport(statement)) {
|
||||
moduleSpecifiers.push(statement.moduleSpecifier.text);
|
||||
} else if (ts.isExportDeclaration(statement) && !statement.isTypeOnly && statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) {
|
||||
moduleSpecifiers.push(statement.moduleSpecifier.text);
|
||||
}
|
||||
}
|
||||
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (
|
||||
ts.isCallExpression(node)
|
||||
&& ts.isIdentifier(node.expression)
|
||||
&& (node.expression.text === 'require' || node.expression.text === 'nativeRequire')
|
||||
&& node.arguments.length === 1
|
||||
&& ts.isStringLiteral(node.arguments[0])
|
||||
) {
|
||||
moduleSpecifiers.push(node.arguments[0].text);
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(sourceFile);
|
||||
|
||||
return moduleSpecifiers;
|
||||
}
|
||||
|
||||
function isRuntimeImport(statement: ts.ImportDeclaration): boolean {
|
||||
const clause = statement.importClause;
|
||||
if (!clause) {
|
||||
return true;
|
||||
}
|
||||
if (clause.isTypeOnly) {
|
||||
return false;
|
||||
}
|
||||
if (clause.name || !clause.namedBindings || ts.isNamespaceImport(clause.namedBindings)) {
|
||||
return true;
|
||||
}
|
||||
return clause.namedBindings.elements.some(element => !element.isTypeOnly);
|
||||
}
|
||||
|
||||
function resolveSourceImport(importer: string, moduleSpecifier: string): string | undefined {
|
||||
const unresolvedPath = path.resolve(path.dirname(importer), moduleSpecifier);
|
||||
const candidates = [
|
||||
unresolvedPath,
|
||||
unresolvedPath.replace(/\.js$/, '.ts'),
|
||||
unresolvedPath.replace(/\.js$/, '.tsx'),
|
||||
path.join(unresolvedPath, 'index.ts'),
|
||||
];
|
||||
return candidates.find(candidate => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
|
||||
}
|
||||
|
||||
function getPackageName(moduleSpecifier: string): string {
|
||||
const segments = moduleSpecifier.split('/');
|
||||
return moduleSpecifier.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0];
|
||||
}
|
||||
Generated
+20
@@ -16,6 +16,7 @@
|
||||
"@parcel/watcher": "^2.5.6",
|
||||
"@vscode/copilot-api": "^0.4.2",
|
||||
"@vscode/deviceid": "^0.1.1",
|
||||
"@vscode/fs-copyfile": "2.0.0",
|
||||
"@vscode/iconv-lite-umd": "0.7.1",
|
||||
"@vscode/native-watchdog": "^1.4.6",
|
||||
"@vscode/proxy-agent": "^0.44.0",
|
||||
@@ -644,6 +645,25 @@
|
||||
"uuid": "^14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/fs-copyfile": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/fs-copyfile/-/fs-copyfile-2.0.0.tgz",
|
||||
"integrity": "sha512-ARb4+9rN905WjJtQ2mSBG/q4pjJkSRun/MkfCeRkk7h/5J8w4vd18NCePFJ/ZucIwXx/7mr9T6nz9Vtt1tk7hg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/fs-copyfile/node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vscode/iconv-lite-umd": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.1.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"@parcel/watcher": "^2.5.6",
|
||||
"@vscode/copilot-api": "^0.4.2",
|
||||
"@vscode/deviceid": "^0.1.1",
|
||||
"@vscode/fs-copyfile": "2.0.0",
|
||||
"@vscode/iconv-lite-umd": "0.7.1",
|
||||
"@vscode/native-watchdog": "^1.4.6",
|
||||
"@vscode/proxy-agent": "^0.44.0",
|
||||
@@ -65,6 +66,7 @@
|
||||
"node-pty@1.2.0-beta.13": true,
|
||||
"@parcel/watcher@2.5.6": true,
|
||||
"@vscode/deviceid@0.1.5": true,
|
||||
"@vscode/fs-copyfile@2.0.0": true,
|
||||
"@vscode/native-watchdog@1.4.6": true,
|
||||
"@vscode/spdlog@0.15.8": true,
|
||||
"@vscode/windows-registry@1.2.0": true,
|
||||
|
||||
Reference in New Issue
Block a user