From 254c5c7c867a4d952b5324aaeda2babf98a88344 Mon Sep 17 00:00:00 2001 From: Raymond Zhao Date: Tue, 14 Sep 2021 10:03:46 -0700 Subject: [PATCH] Add prototype of mac open command (#131213) Co-authored-by: Benjamin Pasero --- src/vs/base/node/watcher.ts | 60 +++++++++++++++++++++++++ src/vs/code/node/cli.ts | 89 ++++++++++++++++++++++++++++++++++--- 2 files changed, 143 insertions(+), 6 deletions(-) diff --git a/src/vs/base/node/watcher.ts b/src/vs/base/node/watcher.ts index 0b6947dde41d2..58adf3749438d 100644 --- a/src/vs/base/node/watcher.ts +++ b/src/vs/base/node/watcher.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { watch } from 'fs'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { isEqualOrParent } from 'vs/base/common/extpath'; import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { normalizeNFC } from 'vs/base/common/normalization'; @@ -202,3 +203,62 @@ function doWatchNonRecursive(file: { path: string, isDirectory: boolean }, onCha watcherDisposables = dispose(watcherDisposables); }); } + +/** + * Watch the provided `path` for changes and return + * the data in chunks of `Uint8Array` for further use. + */ +export async function watchFileContents(path: string, onData: (chunk: Uint8Array) => void, token: CancellationToken, bufferSize = 512): Promise { + const handle = await Promises.open(path, 'r'); + const buffer = Buffer.allocUnsafe(bufferSize); + + const cts = new CancellationTokenSource(token); + + let error: Error | undefined = undefined; + let isReading = false; + + const watcher = watchFile(path, async type => { + if (type === 'changed') { + + if (isReading) { + return; // return early if we are already reading the output + } + + isReading = true; + + try { + // Consume the new contents of the file until finished + // everytime there is a change event signalling a change + while (!cts.token.isCancellationRequested) { + const { bytesRead } = await Promises.read(handle, buffer, 0, bufferSize, null); + if (!bytesRead || cts.token.isCancellationRequested) { + break; + } + + onData(buffer.slice(0, bytesRead)); + } + } catch (err) { + error = new Error(err); + cts.dispose(true); + } finally { + isReading = false; + } + } + }, err => { + error = new Error(err); + cts.dispose(true); + }); + + return new Promise((resolve, reject) => { + cts.token.onCancellationRequested(async () => { + watcher.dispose(); + await Promises.close(handle); + + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); +} diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index de50b33b195c8..5f6acd3639568 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -5,21 +5,23 @@ import { ChildProcess, spawn, SpawnOptions } from 'child_process'; import { chmodSync, existsSync, readFileSync, statSync, truncateSync, unlinkSync } from 'fs'; -import { homedir } from 'os'; +import { homedir, tmpdir } from 'os'; import type { ProfilingSession, Target } from 'v8-inspect-profiler'; import { Event } from 'vs/base/common/event'; -import { isAbsolute, join } from 'vs/base/common/path'; -import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; +import { isAbsolute, join, resolve } from 'vs/base/common/path'; +import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform'; import { randomPort } from 'vs/base/common/ports'; import { isString } from 'vs/base/common/types'; import { whenDeleted, writeFileSync } from 'vs/base/node/pfs'; import { findFreePort } from 'vs/base/node/ports'; +import { watchFileContents } from 'vs/base/node/watcher'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { buildHelpMessage, buildVersionMessage, OPTIONS } from 'vs/platform/environment/node/argv'; import { addArg, parseCLIProcessArgv } from 'vs/platform/environment/node/argvHelper'; import { getStdinFilePath, hasStdinWithoutTty, readFromStdin, stdinDataListener } from 'vs/platform/environment/node/stdin'; import { createWaitMarkerFile } from 'vs/platform/environment/node/wait'; import product from 'vs/platform/product/common/product'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; function shouldSpawnCliProcess(argv: NativeParsedArgs): boolean { return !!argv['install-source'] @@ -30,6 +32,10 @@ function shouldSpawnCliProcess(argv: NativeParsedArgs): boolean { || !!argv['telemetry']; } +function createFileName(dir: string, prefix: string): string { + return join(dir, `${prefix}-${Math.random().toString(16).slice(-4)}`); +} + interface IMainCli { main: (argv: NativeParsedArgs) => Promise; } @@ -205,10 +211,29 @@ export async function main(argv: string[]): Promise { // - the wait marker file has been deleted (e.g. when closing the editor) // - the launched process terminates (e.g. due to a crash) processCallbacks.push(async child => { + let childExitPromise; + if (isMacintosh) { + // On macOS, we resolve the following promise only when the child, + // i.e. the open command, exited with a signal or error. Otherwise, we + // wait for the marker file to be deleted or for the child to error. + childExitPromise = new Promise((resolve) => { + // Only resolve this promise if the child (i.e. open) exited with an error + child.on('exit', (code, signal) => { + if (code !== 0 || signal) { + resolve(); + } + }); + }); + } else { + // On other platforms, we listen for exit in case the child exits before the + // marker file is deleted. + childExitPromise = Event.toPromise(Event.fromNodeEventEmitter(child, 'exit')); + } try { await Promise.race([ whenDeleted(waitMarkerFilePath!), - Event.toPromise(Event.fromNodeEventEmitter(child, 'exit')) + Event.toPromise(Event.fromNodeEventEmitter(child, 'error')), + childExitPromise ]); } finally { if (stdinFilePath) { @@ -232,7 +257,7 @@ export async function main(argv: string[]): Promise { throw new Error('Failed to find free ports for profiler. Make sure to shutdown all instances of the editor first.'); } - const filenamePrefix = join(homedir(), 'prof-' + Math.random().toString(16).slice(-4)); + const filenamePrefix = createFileName(homedir(), 'prof'); addArg(argv, `--inspect-brk=${portMain}`); addArg(argv, `--remote-debugging-port=${portRenderer}`); @@ -337,7 +362,59 @@ export async function main(argv: string[]): Promise { options['stdio'] = 'ignore'; } - const child = spawn(process.execPath, argv.slice(2), options); + let child: ChildProcess; + if (!isMacintosh) { + // We spawn process.execPath directly + child = spawn(process.execPath, argv.slice(2), options); + } else { + // On mac, we spawn using the open command to obtain behavior + // similar to if the app was launched from the dock + // https://github.com/microsoft/vscode/issues/102975 + + const spawnArgs = ['-n']; // -n: launches even when opened already + spawnArgs.push('-a', process.execPath); // -a: opens a specific application + + if (verbose) { + spawnArgs.push('--wait-apps'); // `open --wait-apps`: blocks until the launched app is closed (even if they were already running) + + // The open command only allows for redirecting stderr and stdout to files, + // so we make it redirect those to temp files, and then use a logger to + // redirect the file output to the console + for (const outputType of ['stdout', 'stderr']) { + + // Tmp file to target output to + const tmpName = createFileName(tmpdir(), `code-${outputType}`); + writeFileSync(tmpName, ''); + spawnArgs.push(`--${outputType}`, tmpName); + + // Listener to redirect content to stdout/stderr + processCallbacks.push(async (child: ChildProcess) => { + try { + const stream = outputType === 'stdout' ? process.stdout : process.stderr; + + const cts = new CancellationTokenSource(); + child.on('close', () => cts.dispose(true)); + await watchFileContents(tmpName, chunk => stream.write(chunk), cts.token); + } finally { + unlinkSync(tmpName); + } + }); + } + } + + spawnArgs.push('--args', ...argv.slice(2)); // pass on our arguments + + if (env['VSCODE_DEV']) { + // If we're in development mode, replace the . arg with the + // vscode source arg. Because the OSS app isn't bundled, + // it needs the full vscode source arg to launch properly. + const curdir = '.'; + const launchDirIndex = spawnArgs.indexOf(curdir); + spawnArgs[launchDirIndex] = resolve(curdir); + } + + child = spawn('open', spawnArgs, options); + } return Promise.all(processCallbacks.map(callback => callback(child))); }