cli.ts 12.8 KB
Newer Older
J
Joao Moreno 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import { spawn, ChildProcess } from 'child_process';
J
Joao Moreno 已提交
7
import { TPromise } from 'vs/base/common/winjs.base';
J
Joao Moreno 已提交
8
import { assign } from 'vs/base/common/objects';
J
Joao Moreno 已提交
9 10
import { parseCLIProcessArgv, buildHelpMessage } from 'vs/platform/environment/node/argv';
import { ParsedArgs } from 'vs/platform/environment/common/environment';
11 12
import product from 'vs/platform/node/product';
import pkg from 'vs/platform/node/package';
13 14
import * as paths from 'path';
import * as os from 'os';
15
import * as fs from 'fs';
B
Benjamin Pasero 已提交
16
import { whenDeleted } from 'vs/base/node/pfs';
17
import { findFreePort, randomPort } from 'vs/base/node/ports';
18 19
import { resolveTerminalEncoding } from 'vs/base/node/encoding';
import * as iconv from 'iconv-lite';
20
import { writeFileAndFlushSync } from 'vs/base/node/extfs';
21
import { isWindows } from 'vs/base/common/platform';
22
import { ProfilingSession } from 'v8-inspect-profiler';
B
Benjamin Pasero 已提交
23
import { createWaitMarkerFile } from 'vs/code/node/wait';
24

J
Joao Moreno 已提交
25
function shouldSpawnCliProcess(argv: ParsedArgs): boolean {
26 27 28 29
	return !!argv['install-source']
		|| !!argv['list-extensions']
		|| !!argv['install-extension']
		|| !!argv['uninstall-extension'];
J
Joao Moreno 已提交
30 31 32 33 34 35
}

interface IMainCli {
	main: (argv: ParsedArgs) => TPromise<void>;
}

J
Joao Moreno 已提交
36
export async function main(argv: string[]): Promise<any> {
J
Joao Moreno 已提交
37
	let args: ParsedArgs;
J
Joao Moreno 已提交
38

J
Joao Moreno 已提交
39 40 41 42 43 44 45
	try {
		args = parseCLIProcessArgv(argv);
	} catch (err) {
		console.error(err.message);
		return TPromise.as(null);
	}

B
Benjamin Pasero 已提交
46
	// Help
J
Joao Moreno 已提交
47
	if (args.help) {
J
Joao Moreno 已提交
48
		console.log(buildHelpMessage(product.nameLong, product.applicationName, pkg.version));
B
Benjamin Pasero 已提交
49 50 51 52
	}

	// Version Info
	else if (args.version) {
53
		console.log(`${pkg.version}\n${product.commit}\n${process.arch}`);
B
Benjamin Pasero 已提交
54 55 56 57
	}

	// Extensions Management
	else if (shouldSpawnCliProcess(args)) {
J
Joao Moreno 已提交
58
		const mainCli = new TPromise<IMainCli>(c => require(['vs/code/node/cliProcessMain'], c));
J
Joao Moreno 已提交
59
		return mainCli.then(cli => cli.main(args));
B
Benjamin Pasero 已提交
60 61
	}

B
Benjamin Pasero 已提交
62 63
	// Write File
	else if (args['file-write']) {
64 65 66 67 68 69 70 71 72 73
		const source = args._[0];
		const target = args._[1];

		// Validate
		if (
			!source || !target || source === target ||					// make sure source and target are provided and are not the same
			!paths.isAbsolute(source) || !paths.isAbsolute(target) ||	// make sure both source and target are absolute paths
			!fs.existsSync(source) || !fs.statSync(source).isFile() ||	// make sure source exists as file
			!fs.existsSync(target) || !fs.statSync(target).isFile()		// make sure target exists as file
		) {
B
Benjamin Pasero 已提交
74
			return TPromise.wrapError(new Error('Using --file-write with invalid arguments.'));
75 76 77
		}

		try {
78 79 80 81

			// Check for readonly status and chmod if so if we are told so
			let targetMode: number;
			let restoreMode = false;
B
Benjamin Pasero 已提交
82
			if (!!args['file-chmod']) {
83 84 85 86 87 88 89 90
				targetMode = fs.statSync(target).mode;
				if (!(targetMode & 128) /* readonly */) {
					fs.chmodSync(target, targetMode | 128);
					restoreMode = true;
				}
			}

			// Write source to target
91
			const data = fs.readFileSync(source);
92 93 94 95 96 97 98 99 100 101
			if (isWindows) {
				// On Windows we use a different strategy of saving the file
				// by first truncating the file and then writing with r+ mode.
				// This helps to save hidden files on Windows
				// (see https://github.com/Microsoft/vscode/issues/931) and
				// prevent removing alternate data streams
				// (see https://github.com/Microsoft/vscode/issues/6363)
				fs.truncateSync(target, 0);
				writeFileAndFlushSync(target, data, { flag: 'r+' });
			} else {
102 103
				writeFileAndFlushSync(target, data);
			}
104 105 106 107 108

			// Restore previous mode as needed
			if (restoreMode) {
				fs.chmodSync(target, targetMode);
			}
109
		} catch (error) {
B
Benjamin Pasero 已提交
110
			return TPromise.wrapError(new Error(`Using --file-write resulted in an error: ${error}`));
111 112 113 114 115
		}

		return TPromise.as(null);
	}

B
Benjamin Pasero 已提交
116 117
	// Just Code
	else {
118
		const env = assign({}, process.env, {
119
			'VSCODE_CLI': '1', // this will signal Code that it was spawned from this module
J
Joao Moreno 已提交
120
			'ELECTRON_NO_ATTACH_CONSOLE': '1'
121
		});
B
Benjamin Pasero 已提交
122

B
Benjamin Pasero 已提交
123
		delete env['ELECTRON_RUN_AS_NODE'];
J
Joao Moreno 已提交
124

125
		const processCallbacks: ((child: ChildProcess) => Thenable<any>)[] = [];
126

127
		const verbose = args.verbose || args.status || typeof args['upload-logs'] !== 'undefined';
B
Benjamin Pasero 已提交
128
		if (verbose) {
B
Benjamin Pasero 已提交
129
			env['ELECTRON_ENABLE_LOGGING'] = '1';
130 131 132 133

			processCallbacks.push(child => {
				child.stdout.on('data', (data: Buffer) => console.log(data.toString('utf8').trim()));
				child.stderr.on('data', (data: Buffer) => console.log(data.toString('utf8').trim()));
134

M
Matt Bierner 已提交
135
				return new TPromise<void>(c => child.once('exit', () => c()));
136
			});
B
Benjamin Pasero 已提交
137 138
		}

139
		let stdinWithoutTty: boolean;
140
		try {
141
			stdinWithoutTty = !process.stdin.isTTY; // Via https://twitter.com/MylesBorins/status/782009479382626304
142 143 144 145
		} catch (error) {
			// Windows workaround for https://github.com/nodejs/node/issues/11656
		}

146 147 148 149 150 151 152
		const readFromStdin = args._.some(a => a === '-');
		if (readFromStdin) {
			// remove the "-" argument when we read from stdin
			args._ = args._.filter(a => a !== '-');
			argv = argv.filter(a => a !== '-');
		}

153
		let stdinFilePath: string;
154 155 156 157 158
		if (stdinWithoutTty) {

			// Read from stdin: we require a single "-" argument to be passed in order to start reading from
			// stdin. We do this because there is no reliable way to find out if data is piped to stdin. Just
			// checking for stdin being connected to a TTY is not enough (https://github.com/Microsoft/vscode/issues/40351)
159
			if (args._.length === 0 && readFromStdin) {
160 161 162 163 164 165 166 167 168 169 170 171

				// prepare temp file to read stdin to
				stdinFilePath = paths.join(os.tmpdir(), `code-stdin-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 3)}.txt`);

				// open tmp file for writing
				let stdinFileError: Error;
				let stdinFileStream: fs.WriteStream;
				try {
					stdinFileStream = fs.createWriteStream(stdinFilePath);
				} catch (error) {
					stdinFileError = error;
				}
172

173
				if (!stdinFileError) {
174

175
					// Pipe into tmp file using terminals encoding
176
					resolveTerminalEncoding(verbose).then(encoding => {
177 178 179 180 181 182 183 184 185 186 187 188
						const converterStream = iconv.decodeStream(encoding);
						process.stdin.pipe(converterStream).pipe(stdinFileStream);
					});

					// Make sure to open tmp file
					argv.push(stdinFilePath);

					// Enable --wait to get all data and ignore adding this to history
					argv.push('--wait');
					argv.push('--skip-add-to-recently-opened');
					args.wait = true;
				}
189

190 191 192 193 194 195 196
				if (verbose) {
					if (stdinFileError) {
						console.error(`Failed to create file to read via stdin: ${stdinFileError.toString()}`);
					} else {
						console.log(`Reading from stdin via: ${stdinFilePath}`);
					}
				}
197 198
			}

199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
			// If the user pipes data via stdin but forgot to add the "-" argument, help by printing a message
			// if we detect that data flows into via stdin after a certain timeout.
			else if (args._.length === 0) {
				processCallbacks.push(child => new TPromise(c => {
					const dataListener = () => {
						if (isWindows) {
							console.log(`Run with '${product.applicationName} -' to read output from another program (e.g. 'echo Hello World | ${product.applicationName} -').`);
						} else {
							console.log(`Run with '${product.applicationName} -' to read from stdin (e.g. 'ps aux | grep code | ${product.applicationName} -').`);
						}

						c(void 0);
					};

					// wait for 1s maximum...
					setTimeout(() => {
						process.stdin.removeListener('data', dataListener);

						c(void 0);
					}, 1000);

					// ...but finish early if we detect data
					process.stdin.once('data', dataListener);
				}));
223 224 225
			}
		}

226 227 228 229 230 231
		// If we are started with --wait create a random temporary file
		// and pass it over to the starting instance. We can use this file
		// to wait for it to be deleted to monitor that the edited file
		// is closed and then exit the waiting process.
		let waitMarkerFilePath: string;
		if (args.wait) {
B
Benjamin Pasero 已提交
232 233
			waitMarkerFilePath = await createWaitMarkerFile(verbose);
			if (waitMarkerFilePath) {
234 235 236 237
				argv.push('--waitMarkerFilePath', waitMarkerFilePath);
			}
		}

238 239 240 241 242
		// If we have been started with `--prof-startup` we need to find free ports to profile
		// the main process, the renderer, and the extension host. We also disable v8 cached data
		// to get better profile traces. Last, we listen on stdout for a signal that tells us to
		// stop profiling.
		if (args['prof-startup']) {
243 244 245
			const portMain = await findFreePort(randomPort(), 10, 3000);
			const portRenderer = await findFreePort(portMain + 1, 10, 3000);
			const portExthost = await findFreePort(portRenderer + 1, 10, 3000);
246

247 248 249
			// fail the operation when one of the ports couldn't be accquired.
			if (portMain * portRenderer * portExthost === 0) {
				throw new Error('Failed to find free ports for profiler. Make sure to shutdown all instances of the editor first.');
250 251
			}

252
			const filenamePrefix = paths.join(os.homedir(), 'prof-' + Math.random().toString(16).slice(-4));
253 254 255 256 257 258 259

			argv.push(`--inspect-brk=${portMain}`);
			argv.push(`--remote-debugging-port=${portRenderer}`);
			argv.push(`--inspect-brk-extensions=${portExthost}`);
			argv.push(`--prof-startup-prefix`, filenamePrefix);
			argv.push(`--no-cached-data`);

260
			fs.writeFileSync(filenamePrefix, argv.slice(-6).join('|'));
261

262
			processCallbacks.push(async _child => {
263 264

				class Profiler {
265
					static async start(name: string, filenamePrefix: string, opts: { port: number, tries?: number, chooseTab?: Function }) {
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
						const profiler = await import('v8-inspect-profiler');

						let session: ProfilingSession;
						try {
							session = await profiler.startProfiling(opts);
						} catch (err) {
							console.error(`FAILED to start profiling for '${name}' on port '${opts.port}'`);
						}

						return {
							async stop() {
								if (!session) {
									return;
								}
								let suffix = '';
								let profile = await session.stop();
								if (!process.env['VSCODE_DEV']) {
									// when running from a not-development-build we remove
									// absolute filenames because we don't want to reveal anything
									// about users. We also append the `.txt` suffix to make it
									// easier to attach these files to GH issues
									profile = profiler.rewriteAbsolutePaths(profile, 'piiRemoved');
									suffix = '.txt';
								}

291
								await profiler.writeProfile(profile, `${filenamePrefix}.${name}.cpuprofile${suffix}`);
292 293 294 295 296
							}
						};
					}
				}

297 298
				try {
					// load and start profiler
J
Johannes Rieken 已提交
299 300 301
					const mainProfileRequest = Profiler.start('main', filenamePrefix, { port: portMain });
					const extHostProfileRequest = Profiler.start('extHost', filenamePrefix, { port: portExthost, tries: 300 });
					const rendererProfileRequest = Profiler.start('renderer', filenamePrefix, {
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
						port: portRenderer,
						tries: 200,
						chooseTab: function (targets) {
							return targets.find(target => {
								if (!target.webSocketDebuggerUrl) {
									return false;
								}
								if (target.type === 'page') {
									return target.url.indexOf('workbench/workbench.html') > 0;
								} else {
									return true;
								}
							});
						}
					});
317

J
Johannes Rieken 已提交
318 319 320 321
					const main = await mainProfileRequest;
					const extHost = await extHostProfileRequest;
					const renderer = await rendererProfileRequest;

322 323
					// wait for the renderer to delete the
					// marker file
324
					await whenDeleted(filenamePrefix);
325

326 327 328 329
					// stop profiling
					await main.stop();
					await renderer.stop();
					await extHost.stop();
330

331 332 333
					// re-create the marker file to signal that profiling is done
					fs.writeFileSync(filenamePrefix, '');

334 335 336
				} catch (e) {
					console.error('Failed to profile startup. Make sure to quit Code first.');
				}
337 338 339
			});
		}

340 341
		if (args['js-flags']) {
			const match = /max_old_space_size=(\d+)/g.exec(args['js-flags']);
P
Peng Lyu 已提交
342 343
			if (match && !args['max-memory']) {
				argv.push(`--max-memory=${match[1]}`);
344 345 346
			}
		}

B
Benjamin Pasero 已提交
347
		const options = {
D
Daniel Imms 已提交
348
			detached: true,
349
			env
350
		};
B
Benjamin Pasero 已提交
351

B
Benjamin Pasero 已提交
352
		if (typeof args['upload-logs'] !== 'undefined') {
353
			options['stdio'] = ['pipe', 'pipe', 'pipe'];
M
Matt Bierner 已提交
354
		} else if (!verbose) {
355 356 357
			options['stdio'] = 'ignore';
		}

J
Joao Moreno 已提交
358
		const child = spawn(process.execPath, argv.slice(2), options);
359

360 361 362 363 364 365 366
		if (args.wait && waitMarkerFilePath) {
			return new TPromise<void>(c => {

				// Complete when process exits
				child.once('exit', () => c(null));

				// Complete when wait marker file is deleted
367
				whenDeleted(waitMarkerFilePath).then(c, c);
368 369 370 371 372 373
			}).then(() => {

				// Make sure to delete the tmp stdin file if we have any
				if (stdinFilePath) {
					fs.unlinkSync(stdinFilePath);
				}
374 375
			});
		}
376 377

		return TPromise.join(processCallbacks.map(callback => callback(child)));
J
Joao Moreno 已提交
378 379
	}

J
Joao Moreno 已提交
380
	return TPromise.as(null);
J
Joao Moreno 已提交
381 382
}

383 384 385 386
function eventuallyExit(code: number): void {
	setTimeout(() => process.exit(code), 0);
}

J
Joao Moreno 已提交
387
main(process.argv)
388
	.then(() => eventuallyExit(0))
J
Joao Moreno 已提交
389
	.then(null, err => {
J
Joao Moreno 已提交
390
		console.error(err.message || err.stack || err);
391
		eventuallyExit(1);
J
Joao Moreno 已提交
392
	});