Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/code/node/cli.ts
3292 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { ChildProcess, spawn, SpawnOptions, StdioOptions } from 'child_process';
7
import { chmodSync, existsSync, readFileSync, statSync, truncateSync, unlinkSync } from 'fs';
8
import { homedir, release, tmpdir } from 'os';
9
import type { ProfilingSession, Target } from 'v8-inspect-profiler';
10
import { Event } from '../../base/common/event.js';
11
import { isAbsolute, resolve, join, dirname } from '../../base/common/path.js';
12
import { IProcessEnvironment, isMacintosh, isWindows } from '../../base/common/platform.js';
13
import { randomPort } from '../../base/common/ports.js';
14
import { whenDeleted, writeFileSync } from '../../base/node/pfs.js';
15
import { findFreePort } from '../../base/node/ports.js';
16
import { watchFileContents } from '../../platform/files/node/watcher/nodejs/nodejsWatcherLib.js';
17
import { NativeParsedArgs } from '../../platform/environment/common/argv.js';
18
import { buildHelpMessage, buildStdinMessage, buildVersionMessage, NATIVE_CLI_COMMANDS, OPTIONS } from '../../platform/environment/node/argv.js';
19
import { addArg, parseCLIProcessArgv } from '../../platform/environment/node/argvHelper.js';
20
import { getStdinFilePath, hasStdinWithoutTty, readFromStdin, stdinDataListener } from '../../platform/environment/node/stdin.js';
21
import { createWaitMarkerFileSync } from '../../platform/environment/node/wait.js';
22
import product from '../../platform/product/common/product.js';
23
import { CancellationTokenSource } from '../../base/common/cancellation.js';
24
import { isUNC, randomPath } from '../../base/common/extpath.js';
25
import { Utils } from '../../platform/profiling/common/profiling.js';
26
import { FileAccess } from '../../base/common/network.js';
27
import { cwd } from '../../base/common/process.js';
28
import { addUNCHostToAllowlist } from '../../base/node/unc.js';
29
import { URI } from '../../base/common/uri.js';
30
import { DeferredPromise } from '../../base/common/async.js';
31
32
function shouldSpawnCliProcess(argv: NativeParsedArgs): boolean {
33
return !!argv['install-source']
34
|| !!argv['list-extensions']
35
|| !!argv['install-extension']
36
|| !!argv['uninstall-extension']
37
|| !!argv['update-extensions']
38
|| !!argv['locate-extension']
39
|| !!argv['add-mcp']
40
|| !!argv['telemetry'];
41
}
42
43
export async function main(argv: string[]): Promise<any> {
44
let args: NativeParsedArgs;
45
46
try {
47
args = parseCLIProcessArgv(argv);
48
} catch (err) {
49
console.error(err.message);
50
return;
51
}
52
53
for (const subcommand of NATIVE_CLI_COMMANDS) {
54
if (args[subcommand]) {
55
if (!product.tunnelApplicationName) {
56
console.error(`'${subcommand}' command not supported in ${product.applicationName}`);
57
return;
58
}
59
const env: IProcessEnvironment = {
60
...process.env
61
};
62
// bootstrap-esm.js determines the electron environment based
63
// on the following variable. For the server we need to unset
64
// it to prevent importing any electron specific modules.
65
// Refs https://github.com/microsoft/vscode/issues/221883
66
delete env['ELECTRON_RUN_AS_NODE'];
67
68
const tunnelArgs = argv.slice(argv.indexOf(subcommand) + 1); // all arguments behind `tunnel`
69
return new Promise((resolve, reject) => {
70
let tunnelProcess: ChildProcess;
71
const stdio: StdioOptions = ['ignore', 'pipe', 'pipe'];
72
if (process.env['VSCODE_DEV']) {
73
tunnelProcess = spawn('cargo', ['run', '--', subcommand, ...tunnelArgs], { cwd: join(getAppRoot(), 'cli'), stdio, env });
74
} else {
75
const appPath = process.platform === 'darwin'
76
// ./Contents/MacOS/Electron => ./Contents/Resources/app/bin/code-tunnel-insiders
77
? join(dirname(dirname(process.execPath)), 'Resources', 'app')
78
: dirname(process.execPath);
79
const tunnelCommand = join(appPath, 'bin', `${product.tunnelApplicationName}${isWindows ? '.exe' : ''}`);
80
tunnelProcess = spawn(tunnelCommand, [subcommand, ...tunnelArgs], { cwd: cwd(), stdio, env });
81
}
82
83
tunnelProcess.stdout!.pipe(process.stdout);
84
tunnelProcess.stderr!.pipe(process.stderr);
85
tunnelProcess.on('exit', resolve);
86
tunnelProcess.on('error', reject);
87
});
88
}
89
}
90
91
// Help (general)
92
if (args.help) {
93
const executable = `${product.applicationName}${isWindows ? '.exe' : ''}`;
94
console.log(buildHelpMessage(product.nameLong, executable, product.version, OPTIONS));
95
}
96
97
// Help (chat)
98
else if (args.chat?.help) {
99
const executable = `${product.applicationName}${isWindows ? '.exe' : ''}`;
100
console.log(buildHelpMessage(product.nameLong, executable, product.version, OPTIONS.chat.options, { isChat: true }));
101
}
102
103
// Version Info
104
else if (args.version) {
105
console.log(buildVersionMessage(product.version, product.commit));
106
}
107
108
// Shell integration
109
else if (args['locate-shell-integration-path']) {
110
let file: string;
111
switch (args['locate-shell-integration-path']) {
112
// Usage: `[[ "$TERM_PROGRAM" == "vscode" ]] && . "$(code --locate-shell-integration-path bash)"`
113
case 'bash': file = 'shellIntegration-bash.sh'; break;
114
// Usage: `if ($env:TERM_PROGRAM -eq "vscode") { . "$(code --locate-shell-integration-path pwsh)" }`
115
case 'pwsh': file = 'shellIntegration.ps1'; break;
116
// Usage: `[[ "$TERM_PROGRAM" == "vscode" ]] && . "$(code --locate-shell-integration-path zsh)"`
117
case 'zsh': file = 'shellIntegration-rc.zsh'; break;
118
// Usage: `string match -q "$TERM_PROGRAM" "vscode"; and . (code --locate-shell-integration-path fish)`
119
case 'fish': file = 'shellIntegration.fish'; break;
120
default: throw new Error('Error using --locate-shell-integration-path: Invalid shell type');
121
}
122
console.log(join(getAppRoot(), 'out', 'vs', 'workbench', 'contrib', 'terminal', 'common', 'scripts', file));
123
}
124
125
// Extensions Management
126
else if (shouldSpawnCliProcess(args)) {
127
128
// We do not bundle `cliProcessMain.js` into this file because
129
// it is rather large and only needed for very few CLI operations.
130
// This has the downside that we need to know if we run OSS or
131
// built, because our location on disk is different if built.
132
133
let cliProcessMain: string;
134
if (process.env['VSCODE_DEV']) {
135
cliProcessMain = './cliProcessMain.js';
136
} else {
137
cliProcessMain = './vs/code/node/cliProcessMain.js';
138
}
139
140
const cli = await import(cliProcessMain);
141
await cli.main(args);
142
143
return;
144
}
145
146
// Write File
147
else if (args['file-write']) {
148
const argsFile = args._[0];
149
if (!argsFile || !isAbsolute(argsFile) || !existsSync(argsFile) || !statSync(argsFile).isFile()) {
150
throw new Error('Using --file-write with invalid arguments.');
151
}
152
153
let source: string | undefined;
154
let target: string | undefined;
155
try {
156
const argsContents: { source: string; target: string } = JSON.parse(readFileSync(argsFile, 'utf8'));
157
source = argsContents.source;
158
target = argsContents.target;
159
} catch (error) {
160
throw new Error('Using --file-write with invalid arguments.');
161
}
162
163
// Windows: set the paths as allowed UNC paths given
164
// they are explicitly provided by the user as arguments
165
if (isWindows) {
166
for (const path of [source, target]) {
167
if (typeof path === 'string' && isUNC(path)) {
168
addUNCHostToAllowlist(URI.file(path).authority);
169
}
170
}
171
}
172
173
// Validate
174
if (
175
!source || !target || source === target || // make sure source and target are provided and are not the same
176
!isAbsolute(source) || !isAbsolute(target) || // make sure both source and target are absolute paths
177
!existsSync(source) || !statSync(source).isFile() || // make sure source exists as file
178
!existsSync(target) || !statSync(target).isFile() // make sure target exists as file
179
) {
180
throw new Error('Using --file-write with invalid arguments.');
181
}
182
183
try {
184
185
// Check for readonly status and chmod if so if we are told so
186
let targetMode: number = 0;
187
let restoreMode = false;
188
if (!!args['file-chmod']) {
189
targetMode = statSync(target).mode;
190
if (!(targetMode & 0o200 /* File mode indicating writable by owner */)) {
191
chmodSync(target, targetMode | 0o200);
192
restoreMode = true;
193
}
194
}
195
196
// Write source to target
197
const data = readFileSync(source);
198
if (isWindows) {
199
// On Windows we use a different strategy of saving the file
200
// by first truncating the file and then writing with r+ mode.
201
// This helps to save hidden files on Windows
202
// (see https://github.com/microsoft/vscode/issues/931) and
203
// prevent removing alternate data streams
204
// (see https://github.com/microsoft/vscode/issues/6363)
205
truncateSync(target, 0);
206
writeFileSync(target, data, { flag: 'r+' });
207
} else {
208
writeFileSync(target, data);
209
}
210
211
// Restore previous mode as needed
212
if (restoreMode) {
213
chmodSync(target, targetMode);
214
}
215
} catch (error) {
216
error.message = `Error using --file-write: ${error.message}`;
217
throw error;
218
}
219
}
220
221
// Just Code
222
else {
223
const env: IProcessEnvironment = {
224
...process.env,
225
'ELECTRON_NO_ATTACH_CONSOLE': '1'
226
};
227
228
delete env['ELECTRON_RUN_AS_NODE'];
229
230
const processCallbacks: ((child: ChildProcess) => Promise<void>)[] = [];
231
232
if (args.verbose) {
233
env['ELECTRON_ENABLE_LOGGING'] = '1';
234
}
235
236
if (args.verbose || args.status) {
237
processCallbacks.push(async child => {
238
child.stdout?.on('data', (data: Buffer) => console.log(data.toString('utf8').trim()));
239
child.stderr?.on('data', (data: Buffer) => console.log(data.toString('utf8').trim()));
240
241
await Event.toPromise(Event.fromNodeEventEmitter(child, 'exit'));
242
});
243
}
244
245
// Handle --transient option
246
if (args['transient']) {
247
const tempParentDir = randomPath(tmpdir(), 'vscode');
248
const tempUserDataDir = join(tempParentDir, 'data');
249
const tempExtensionsDir = join(tempParentDir, 'extensions');
250
251
addArg(argv, '--user-data-dir', tempUserDataDir);
252
addArg(argv, '--extensions-dir', tempExtensionsDir);
253
254
console.log(`State is temporarily stored. Relaunch this state with: ${product.applicationName} --user-data-dir "${tempUserDataDir}" --extensions-dir "${tempExtensionsDir}"`);
255
}
256
257
const hasReadStdinArg = args._.some(arg => arg === '-') || args.chat?._.some(arg => arg === '-');
258
if (hasReadStdinArg) {
259
// remove the "-" argument when we read from stdin
260
args._ = args._.filter(a => a !== '-');
261
argv = argv.filter(a => a !== '-');
262
}
263
264
let stdinFilePath: string | undefined;
265
if (hasStdinWithoutTty()) {
266
267
// Read from stdin: we require a single "-" argument to be passed in order to start reading from
268
// stdin. We do this because there is no reliable way to find out if data is piped to stdin. Just
269
// checking for stdin being connected to a TTY is not enough (https://github.com/microsoft/vscode/issues/40351)
270
271
if (hasReadStdinArg) {
272
stdinFilePath = getStdinFilePath();
273
274
try {
275
const readFromStdinDone = new DeferredPromise<void>();
276
await readFromStdin(stdinFilePath, !!args.verbose, () => readFromStdinDone.complete());
277
if (!args.wait) {
278
279
// if `--wait` is not provided, we keep this process alive
280
// for at least as long as the stdin stream is open to
281
// ensure that we read all the data.
282
// the downside is that the Code CLI process will then not
283
// terminate until stdin is closed, but users can always
284
// pass `--wait` to prevent that from happening (this is
285
// actually what we enforced until v1.85.x but then was
286
// changed to not enforce it anymore).
287
// a solution in the future would possibly be to exit, when
288
// the Code process exits. this would require some careful
289
// solution though in case Code is already running and this
290
// is a second instance telling the first instance what to
291
// open.
292
293
processCallbacks.push(() => readFromStdinDone.p);
294
}
295
296
if (args.chat) {
297
// Make sure to add tmp file as context to chat
298
addArg(argv, '--add-file', stdinFilePath);
299
} else {
300
// Make sure to open tmp file as editor but ignore
301
// it in the "recently open" list
302
addArg(argv, stdinFilePath);
303
addArg(argv, '--skip-add-to-recently-opened');
304
}
305
306
console.log(`Reading from stdin via: ${stdinFilePath}`);
307
} catch (e) {
308
console.log(`Failed to create file to read via stdin: ${e.toString()}`);
309
stdinFilePath = undefined;
310
}
311
} else {
312
313
// If the user pipes data via stdin but forgot to add the "-" argument, help by printing a message
314
// if we detect that data flows into via stdin after a certain timeout.
315
processCallbacks.push(_ => stdinDataListener(1000).then(dataReceived => {
316
if (dataReceived) {
317
console.log(buildStdinMessage(product.applicationName, !!args.chat));
318
}
319
}));
320
}
321
}
322
323
const isMacOSBigSurOrNewer = isMacintosh && release() > '20.0.0';
324
325
// If we are started with --wait create a random temporary file
326
// and pass it over to the starting instance. We can use this file
327
// to wait for it to be deleted to monitor that the edited file
328
// is closed and then exit the waiting process.
329
let waitMarkerFilePath: string | undefined;
330
if (args.wait) {
331
waitMarkerFilePath = createWaitMarkerFileSync(args.verbose);
332
if (waitMarkerFilePath) {
333
addArg(argv, '--waitMarkerFilePath', waitMarkerFilePath);
334
}
335
336
// When running with --wait, we want to continue running CLI process
337
// until either:
338
// - the wait marker file has been deleted (e.g. when closing the editor)
339
// - the launched process terminates (e.g. due to a crash)
340
processCallbacks.push(async child => {
341
let childExitPromise;
342
if (isMacOSBigSurOrNewer) {
343
// On Big Sur, we resolve the following promise only when the child,
344
// i.e. the open command, exited with a signal or error. Otherwise, we
345
// wait for the marker file to be deleted or for the child to error.
346
childExitPromise = new Promise<void>(resolve => {
347
// Only resolve this promise if the child (i.e. open) exited with an error
348
child.on('exit', (code, signal) => {
349
if (code !== 0 || signal) {
350
resolve();
351
}
352
});
353
});
354
} else {
355
// On other platforms, we listen for exit in case the child exits before the
356
// marker file is deleted.
357
childExitPromise = Event.toPromise(Event.fromNodeEventEmitter(child, 'exit'));
358
}
359
try {
360
await Promise.race([
361
whenDeleted(waitMarkerFilePath!),
362
Event.toPromise(Event.fromNodeEventEmitter(child, 'error')),
363
childExitPromise
364
]);
365
} finally {
366
if (stdinFilePath) {
367
unlinkSync(stdinFilePath); // Make sure to delete the tmp stdin file if we have any
368
}
369
}
370
});
371
}
372
373
// If we have been started with `--prof-startup` we need to find free ports to profile
374
// the main process, the renderer, and the extension host. We also disable v8 cached data
375
// to get better profile traces. Last, we listen on stdout for a signal that tells us to
376
// stop profiling.
377
if (args['prof-startup']) {
378
const profileHost = '127.0.0.1';
379
const portMain = await findFreePort(randomPort(), 10, 3000);
380
const portRenderer = await findFreePort(portMain + 1, 10, 3000);
381
const portExthost = await findFreePort(portRenderer + 1, 10, 3000);
382
383
// fail the operation when one of the ports couldn't be acquired.
384
if (portMain * portRenderer * portExthost === 0) {
385
throw new Error('Failed to find free ports for profiler. Make sure to shutdown all instances of the editor first.');
386
}
387
388
const filenamePrefix = randomPath(homedir(), 'prof');
389
390
addArg(argv, `--inspect-brk=${profileHost}:${portMain}`);
391
addArg(argv, `--remote-debugging-port=${profileHost}:${portRenderer}`);
392
addArg(argv, `--inspect-brk-extensions=${profileHost}:${portExthost}`);
393
addArg(argv, `--prof-startup-prefix`, filenamePrefix);
394
addArg(argv, `--no-cached-data`);
395
396
writeFileSync(filenamePrefix, argv.slice(-6).join('|'));
397
398
processCallbacks.push(async _child => {
399
400
class Profiler {
401
static async start(name: string, filenamePrefix: string, opts: { port: number; tries?: number; target?: (targets: Target[]) => Target }) {
402
const profiler = await import('v8-inspect-profiler');
403
404
let session: ProfilingSession;
405
try {
406
session = await profiler.startProfiling({ ...opts, host: profileHost });
407
} catch (err) {
408
console.error(`FAILED to start profiling for '${name}' on port '${opts.port}'`);
409
}
410
411
return {
412
async stop() {
413
if (!session) {
414
return;
415
}
416
let suffix = '';
417
const result = await session.stop();
418
if (!process.env['VSCODE_DEV']) {
419
// when running from a not-development-build we remove
420
// absolute filenames because we don't want to reveal anything
421
// about users. We also append the `.txt` suffix to make it
422
// easier to attach these files to GH issues
423
result.profile = Utils.rewriteAbsolutePaths(result.profile, 'piiRemoved');
424
suffix = '.txt';
425
}
426
427
writeFileSync(`${filenamePrefix}.${name}.cpuprofile${suffix}`, JSON.stringify(result.profile, undefined, 4));
428
}
429
};
430
}
431
}
432
433
try {
434
// load and start profiler
435
const mainProfileRequest = Profiler.start('main', filenamePrefix, { port: portMain });
436
const extHostProfileRequest = Profiler.start('extHost', filenamePrefix, { port: portExthost, tries: 300 });
437
const rendererProfileRequest = Profiler.start('renderer', filenamePrefix, {
438
port: portRenderer,
439
tries: 200,
440
target: function (targets) {
441
return targets.filter(target => {
442
if (!target.webSocketDebuggerUrl) {
443
return false;
444
}
445
if (target.type === 'page') {
446
return target.url.indexOf('workbench/workbench.html') > 0 || target.url.indexOf('workbench/workbench-dev.html') > 0;
447
} else {
448
return true;
449
}
450
})[0];
451
}
452
});
453
454
const main = await mainProfileRequest;
455
const extHost = await extHostProfileRequest;
456
const renderer = await rendererProfileRequest;
457
458
// wait for the renderer to delete the marker file
459
await whenDeleted(filenamePrefix);
460
461
// stop profiling
462
await main.stop();
463
await renderer.stop();
464
await extHost.stop();
465
466
// re-create the marker file to signal that profiling is done
467
writeFileSync(filenamePrefix, '');
468
469
} catch (e) {
470
console.error('Failed to profile startup. Make sure to quit Code first.');
471
}
472
});
473
}
474
475
const options: SpawnOptions = {
476
detached: true,
477
env
478
};
479
480
if (!args.verbose) {
481
options['stdio'] = 'ignore';
482
}
483
484
let child: ChildProcess;
485
if (!isMacOSBigSurOrNewer) {
486
if (!args.verbose && args.status) {
487
options['stdio'] = ['ignore', 'pipe', 'ignore']; // restore ability to see output when --status is used
488
}
489
490
// We spawn process.execPath directly
491
child = spawn(process.execPath, argv.slice(2), options);
492
} else {
493
// On Big Sur, we spawn using the open command to obtain behavior
494
// similar to if the app was launched from the dock
495
// https://github.com/microsoft/vscode/issues/102975
496
497
// The following args are for the open command itself, rather than for VS Code:
498
// -n creates a new instance.
499
// Without -n, the open command re-opens the existing instance as-is.
500
// -g starts the new instance in the background.
501
// Later, Electron brings the instance to the foreground.
502
// This way, Mac does not automatically try to foreground the new instance, which causes
503
// focusing issues when the new instance only sends data to a previous instance and then closes.
504
const spawnArgs = ['-n', '-g'];
505
// -a opens the given application.
506
spawnArgs.push('-a', process.execPath); // -a: opens a specific application
507
508
if (args.verbose || args.status) {
509
spawnArgs.push('--wait-apps'); // `open --wait-apps`: blocks until the launched app is closed (even if they were already running)
510
511
// The open command only allows for redirecting stderr and stdout to files,
512
// so we make it redirect those to temp files, and then use a logger to
513
// redirect the file output to the console
514
for (const outputType of args.verbose ? ['stdout', 'stderr'] : ['stdout']) {
515
516
// Tmp file to target output to
517
const tmpName = randomPath(tmpdir(), `code-${outputType}`);
518
writeFileSync(tmpName, '');
519
spawnArgs.push(`--${outputType}`, tmpName);
520
521
// Listener to redirect content to stdout/stderr
522
processCallbacks.push(async child => {
523
try {
524
const stream = outputType === 'stdout' ? process.stdout : process.stderr;
525
526
const cts = new CancellationTokenSource();
527
child.on('close', () => {
528
// We must dispose the token to stop watching,
529
// but the watcher might still be reading data.
530
setTimeout(() => cts.dispose(true), 200);
531
});
532
await watchFileContents(tmpName, chunk => stream.write(chunk), () => { /* ignore */ }, cts.token);
533
} finally {
534
unlinkSync(tmpName);
535
}
536
});
537
}
538
}
539
540
for (const e in env) {
541
// Ignore the _ env var, because the open command
542
// ignores it anyway.
543
// Pass the rest of the env vars in to fix
544
// https://github.com/microsoft/vscode/issues/134696.
545
if (e !== '_') {
546
spawnArgs.push('--env');
547
spawnArgs.push(`${e}=${env[e]}`);
548
}
549
}
550
551
spawnArgs.push('--args', ...argv.slice(2)); // pass on our arguments
552
553
if (env['VSCODE_DEV']) {
554
// If we're in development mode, replace the . arg with the
555
// vscode source arg. Because the OSS app isn't bundled,
556
// it needs the full vscode source arg to launch properly.
557
const curdir = '.';
558
const launchDirIndex = spawnArgs.indexOf(curdir);
559
if (launchDirIndex !== -1) {
560
spawnArgs[launchDirIndex] = resolve(curdir);
561
}
562
}
563
564
// We already passed over the env variables
565
// using the --env flags, so we can leave them out here.
566
// Also, we don't need to pass env._, which is different from argv._
567
child = spawn('open', spawnArgs, { ...options, env: {} });
568
}
569
570
return Promise.all(processCallbacks.map(callback => callback(child)));
571
}
572
}
573
574
function getAppRoot() {
575
return dirname(FileAccess.asFileUri('').fsPath);
576
}
577
578
function eventuallyExit(code: number): void {
579
setTimeout(() => process.exit(code), 0);
580
}
581
582
main(process.argv)
583
.then(() => eventuallyExit(0))
584
.then(null, err => {
585
console.error(err.message || err.stack || err);
586
eventuallyExit(1);
587
});
588
589