Skip to content

Commit 3f7a3cc

Browse files
committed
feat: runVSCodeCommand as workaround for CVE-2024-27980
1 parent 40ecedf commit 3f7a3cc

File tree

9 files changed

+176
-84
lines changed

9 files changed

+176
-84
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
### 2.3.10 | 2024-01-19
4+
5+
- Add `runVSCodeCommand` method and workaround for Node CVE-2024-27980
6+
37
### 2.3.9 | 2024-01-19
48

59
- Fix archive extraction on Windows failing when run under Electron

README.md

+4-8
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
![Test Status Badge](https://github.com/microsoft/vscode-test/workflows/Tests/badge.svg)
44

5-
This module helps you test VS Code extensions.
5+
This module helps you test VS Code extensions. Note that new extensions may want to use the [VS Code Test CLI](https://github.com/microsoft/vscode-test-cli/blob/main/README.md), which leverages this module, for a richer editing and execution experience.
66

77
Supported:
88

@@ -13,10 +13,10 @@ Supported:
1313

1414
## Usage
1515

16-
See [./sample](./sample) for a runnable sample, with [Azure DevOps Pipelines](https://github.com/microsoft/vscode-test/blob/master/sample/azure-pipelines.yml) and [Travis CI](https://github.com/microsoft/vscode-test/blob/master/.travis.yml) configuration.
16+
See [./sample](./sample) for a runnable sample, with [Azure DevOps Pipelines](https://github.com/microsoft/vscode-test/blob/main/sample/azure-pipelines.yml) and [Github ACtions](https://github.com/microsoft/vscode-test/blob/main/sample/.travis.yml) configuration.
1717

1818
```ts
19-
import { runTests } from '@vscode/test-electron';
19+
import { runTests, runVSCodeCommand, downloadAndUnzipVSCode } from '@vscode/test-electron';
2020

2121
async function go() {
2222
try {
@@ -82,11 +82,7 @@ async function go() {
8282
/**
8383
* Install Python extension
8484
*/
85-
const [cli, ...args] = resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath);
86-
cp.spawnSync(cli, [...args, '--install-extension', 'ms-python.python'], {
87-
encoding: 'utf-8',
88-
stdio: 'inherit',
89-
});
85+
await runVSCodeCommand(['--install-extension', 'ms-python.python'], { version: '1.35.0' });
9086

9187
/**
9288
* - Add additional launch flags for VS Code

lib/download.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ describe('sane downloads', () => {
4949
}
5050

5151
if (platform === systemDefaultPlatform) {
52-
const version = spawnSync(exePath, ['--version']);
52+
const shell = process.platform === 'win32';
53+
const version = spawnSync(shell ? `"${exePath}"` : exePath, ['--version'], { shell });
5354
expect(version.status).to.equal(0);
5455
expect(version.stdout.toString().trim()).to.not.be.empty;
5556
}

lib/download.ts

+58-7
Original file line numberDiff line numberDiff line change
@@ -173,13 +173,64 @@ export type DownloadPlatform = StringLiteralUnion<
173173
>;
174174

175175
export interface DownloadOptions {
176-
readonly cachePath: string;
177-
readonly version: DownloadVersion;
178-
readonly platform: DownloadPlatform;
179-
readonly extensionDevelopmentPath?: string | string[];
180-
readonly reporter?: ProgressReporter;
181-
readonly extractSync?: boolean;
182-
readonly timeout?: number;
176+
/**
177+
* The VS Code version to download. Valid versions are:
178+
* - `'stable'`
179+
* - `'insiders'`
180+
* - `'1.32.0'`, `'1.31.1'`, etc
181+
*
182+
* Defaults to `stable`, which is latest stable version.
183+
*
184+
* *If a local copy exists at `.vscode-test/vscode-<VERSION>`, skip download.*
185+
*/
186+
version: DownloadVersion;
187+
188+
/**
189+
* The VS Code platform to download. If not specified, it defaults to the
190+
* current platform.
191+
*
192+
* Possible values are:
193+
* - `win32-x64-archive`
194+
* - `win32-arm64-archive `
195+
* - `darwin`
196+
* - `darwin-arm64`
197+
* - `linux-x64`
198+
* - `linux-arm64`
199+
* - `linux-armhf`
200+
*/
201+
platform: DownloadPlatform;
202+
203+
/**
204+
* Path where the downloaded VS Code instance is stored.
205+
* Defaults to `.vscode-test` within your working directory folder.
206+
*/
207+
cachePath: string;
208+
209+
/**
210+
* Absolute path to the extension root. Passed to `--extensionDevelopmentPath`.
211+
* Must include a `package.json` Extension Manifest.
212+
*/
213+
extensionDevelopmentPath?: string | string[];
214+
215+
/**
216+
* Progress reporter to use while VS Code is downloaded. Defaults to a
217+
* console reporter. A {@link SilentReporter} is also available, and you
218+
* may implement your own.
219+
*/
220+
reporter?: ProgressReporter;
221+
222+
/**
223+
* Whether the downloaded zip should be synchronously extracted. Should be
224+
* omitted unless you're experiencing issues installing VS Code versions.
225+
*/
226+
extractSync?: boolean;
227+
228+
/**
229+
* Number of milliseconds after which to time out if no data is received from
230+
* the remote when downloading VS Code. Note that this is an 'idle' timeout
231+
* and does not enforce the total time VS Code may take to download.
232+
*/
233+
timeout?: number;
183234
}
184235

185236
interface IDownload {

lib/index.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
export { download, downloadAndUnzipVSCode } from './download';
7-
export { runTests } from './runTest';
8-
export { resolveCliPathFromVSCodeExecutablePath, resolveCliArgsFromVSCodeExecutablePath } from './util';
6+
export { download, downloadAndUnzipVSCode, DownloadOptions } from './download';
7+
export { runTests, TestOptions } from './runTest';
8+
export {
9+
resolveCliPathFromVSCodeExecutablePath,
10+
resolveCliArgsFromVSCodeExecutablePath,
11+
runVSCodeCommand,
12+
VSCodeCommandError,
13+
RunVSCodeCommandOptions,
14+
} from './util';
915
export * from './progress';

lib/runTest.ts

+5-51
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@
55

66
import * as cp from 'child_process';
77
import * as path from 'path';
8-
import { downloadAndUnzipVSCode, DownloadVersion, DownloadPlatform, defaultCachePath } from './download';
9-
import { ProgressReporter } from './progress';
8+
import { DownloadOptions, defaultCachePath, downloadAndUnzipVSCode } from './download';
109
import { killTree } from './util';
1110

12-
export interface TestOptions {
11+
export interface TestOptions extends Partial<DownloadOptions> {
1312
/**
1413
* The VS Code executable path used for testing.
1514
*
@@ -18,33 +17,6 @@ export interface TestOptions {
1817
*/
1918
vscodeExecutablePath?: string;
2019

21-
/**
22-
* The VS Code version to download. Valid versions are:
23-
* - `'stable'`
24-
* - `'insiders'`
25-
* - `'1.32.0'`, `'1.31.1'`, etc
26-
*
27-
* Defaults to `stable`, which is latest stable version.
28-
*
29-
* *If a local copy exists at `.vscode-test/vscode-<VERSION>`, skip download.*
30-
*/
31-
version?: DownloadVersion;
32-
33-
/**
34-
* The VS Code platform to download. If not specified, it defaults to the
35-
* current platform.
36-
*
37-
* Possible values are:
38-
* - `win32-x64-archive`
39-
* - `win32-arm64-archive `
40-
* - `darwin`
41-
* - `darwin-arm64`
42-
* - `linux-x64`
43-
* - `linux-arm64`
44-
* - `linux-armhf`
45-
*/
46-
platform?: DownloadPlatform;
47-
4820
/**
4921
* Whether VS Code should be launched using default settings and extensions
5022
* installed on this machine. If `false`, then separate directories will be
@@ -95,26 +67,6 @@ export interface TestOptions {
9567
* See `code --help` for possible arguments.
9668
*/
9769
launchArgs?: string[];
98-
99-
/**
100-
* Progress reporter to use while VS Code is downloaded. Defaults to a
101-
* console reporter. A {@link SilentReporter} is also available, and you
102-
* may implement your own.
103-
*/
104-
reporter?: ProgressReporter;
105-
106-
/**
107-
* Whether the downloaded zip should be synchronously extracted. Should be
108-
* omitted unless you're experiencing issues installing VS Code versions.
109-
*/
110-
extractSync?: boolean;
111-
112-
/**
113-
* Number of milliseconds after which to time out if no data is received from
114-
* the remote when downloading VS Code. Note that this is an 'idle' timeout
115-
* and does not enforce the total time VS Code may take to download.
116-
*/
117-
timeout?: number;
11870
}
11971

12072
/**
@@ -185,7 +137,9 @@ async function innerRunTests(
185137
}
186138
): Promise<number> {
187139
const fullEnv = Object.assign({}, process.env, testRunnerEnv);
188-
const cmd = cp.spawn(executable, args, { env: fullEnv });
140+
const shell = process.platform === 'win32';
141+
const cmd = cp.spawn(shell ? `"${executable}"` : executable, args, { env: fullEnv, shell });
142+
189143
let exitRequested = false;
190144
const ctrlc1 = () => {
191145
process.removeListener(SIGINT, ctrlc1);

lib/util.ts

+52-3
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { ChildProcess, spawn } from 'child_process';
6+
import { ChildProcess, SpawnOptions, spawn } from 'child_process';
7+
import { createHash } from 'crypto';
78
import { readFileSync } from 'fs';
89
import * as createHttpProxyAgent from 'http-proxy-agent';
910
import * as https from 'https';
1011
import * as createHttpsProxyAgent from 'https-proxy-agent';
1112
import * as path from 'path';
1213
import { URL } from 'url';
13-
import { DownloadPlatform } from './download';
14+
import { DownloadOptions, DownloadPlatform, downloadAndUnzipVSCode } from './download';
1415
import * as request from './request';
1516
import { TestOptions, getProfileArguments } from './runTest';
16-
import { createHash } from 'crypto';
1717

1818
export let systemDefaultPlatform: DownloadPlatform;
1919

@@ -176,6 +176,7 @@ export function resolveCliPathFromVSCodeExecutablePath(
176176
* cp.spawnSync(cli, [...args, '--install-extension', '<EXTENSION-ID-OR-PATH-TO-VSIX>'], {
177177
* encoding: 'utf-8',
178178
* stdio: 'inherit'
179+
* shell: process.platform === 'win32',
179180
* });
180181
* ```
181182
*
@@ -195,6 +196,54 @@ export function resolveCliArgsFromVSCodeExecutablePath(
195196
return args;
196197
}
197198

199+
export type RunVSCodeCommandOptions = Partial<DownloadOptions> & { spawn?: SpawnOptions };
200+
201+
export class VSCodeCommandError extends Error {
202+
constructor(
203+
args: string[],
204+
public readonly exitCode: number | null,
205+
public readonly stderr: string,
206+
public stdout: string
207+
) {
208+
super(`'code ${args.join(' ')}' failed with exit code ${exitCode}:\n\n${stderr}\n\n${stdout}`);
209+
}
210+
}
211+
212+
/**
213+
* Runs a VS Code command, and returns its output
214+
* @throws a {@link VSCodeCommandError} if the command fails
215+
*/
216+
export async function runVSCodeCommand(args: string[], options: RunVSCodeCommandOptions = {}) {
217+
const vscodeExecutablePath = await downloadAndUnzipVSCode(options);
218+
const [cli, ...baseArgs] = resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath);
219+
220+
const shell = process.platform === 'win32';
221+
222+
return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
223+
let stdout = '';
224+
let stderr = '';
225+
226+
const child = spawn(shell ? `"${cli}"` : cli, [...baseArgs, ...args], {
227+
stdio: 'pipe',
228+
shell,
229+
windowsHide: true,
230+
...options.spawn,
231+
});
232+
233+
child.stdout?.setEncoding('utf-8').on('data', (data) => (stdout += data));
234+
child.stderr?.setEncoding('utf-8').on('data', (data) => (stderr += data));
235+
236+
child.on('error', reject);
237+
child.on('exit', (code) => {
238+
if (code !== 0) {
239+
reject(new VSCodeCommandError(args, code, stderr, stdout));
240+
} else {
241+
resolve({ stdout, stderr });
242+
}
243+
});
244+
});
245+
}
246+
198247
/** Predicates whether arg is undefined or null */
199248
export function isDefined<T>(arg: T | undefined | null): arg is T {
200249
return arg != null;

sample/.github/workflows/ci.yml

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Run VSCode Extension Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout code
16+
uses: actions/checkout@v2
17+
18+
- name: Setup Node.js environment
19+
uses: actions/setup-node@v2
20+
with:
21+
node-version: ${{ matrix.node-version }}
22+
23+
- name: Install dependencies
24+
run: yarn install
25+
26+
- name: Compile
27+
run: yarn compile
28+
29+
- name: Run tests
30+
run: xvfb-run -a yarn test
31+
if: runner.os == 'Linux'
32+
33+
- name: Run tests
34+
run: yarn test
35+
if: runner.os != 'Linux'

0 commit comments

Comments
 (0)