diff --git a/src/vs/base/node/encoding.ts b/src/vs/base/node/encoding.ts index 8da85d8ce39..3b459fa4037 100644 --- a/src/vs/base/node/encoding.ts +++ b/src/vs/base/node/encoding.ts @@ -4,8 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as iconv from 'iconv-lite'; -import { isLinux, isMacintosh } from 'vs/base/common/platform'; -import { exec } from 'child_process'; import { Readable, Writable } from 'stream'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -353,87 +351,3 @@ export function detectEncodingFromBuffer({ buffer, bytesRead }: IReadResult, aut return { seemsBinary, encoding }; } - -// https://ss64.com/nt/chcp.html -const windowsTerminalEncodings = { - '437': 'cp437', // United States - '850': 'cp850', // Multilingual(Latin I) - '852': 'cp852', // Slavic(Latin II) - '855': 'cp855', // Cyrillic(Russian) - '857': 'cp857', // Turkish - '860': 'cp860', // Portuguese - '861': 'cp861', // Icelandic - '863': 'cp863', // Canadian - French - '865': 'cp865', // Nordic - '866': 'cp866', // Russian - '869': 'cp869', // Modern Greek - '936': 'cp936', // Simplified Chinese - '1252': 'cp1252' // West European Latin -}; - -export async function resolveTerminalEncoding(verbose?: boolean): Promise { - let rawEncodingPromise: Promise; - - // Support a global environment variable to win over other mechanics - const cliEncodingEnv = process.env['VSCODE_CLI_ENCODING']; - if (cliEncodingEnv) { - if (verbose) { - console.log(`Found VSCODE_CLI_ENCODING variable: ${cliEncodingEnv}`); - } - - rawEncodingPromise = Promise.resolve(cliEncodingEnv); - } - - // Linux/Mac: use "locale charmap" command - else if (isLinux || isMacintosh) { - rawEncodingPromise = new Promise(resolve => { - if (verbose) { - console.log('Running "locale charmap" to detect terminal encoding...'); - } - - exec('locale charmap', (err, stdout, stderr) => resolve(stdout)); - }); - } - - // Windows: educated guess - else { - rawEncodingPromise = new Promise(resolve => { - if (verbose) { - console.log('Running "chcp" to detect terminal encoding...'); - } - - exec('chcp', (err, stdout, stderr) => { - if (stdout) { - const windowsTerminalEncodingKeys = Object.keys(windowsTerminalEncodings) as Array; - for (const key of windowsTerminalEncodingKeys) { - if (stdout.indexOf(key) >= 0) { - return resolve(windowsTerminalEncodings[key]); - } - } - } - - return resolve(undefined); - }); - }); - } - - const rawEncoding = await rawEncodingPromise; - if (verbose) { - console.log(`Detected raw terminal encoding: ${rawEncoding}`); - } - - if (!rawEncoding || rawEncoding.toLowerCase() === 'utf-8' || rawEncoding.toLowerCase() === UTF8) { - return UTF8; - } - - const iconvEncoding = toIconvLiteEncoding(rawEncoding); - if (iconv.encodingExists(iconvEncoding)) { - return iconvEncoding; - } - - if (verbose) { - console.log('Unsupported terminal encoding, falling back to UTF-8.'); - } - - return UTF8; -} diff --git a/src/vs/base/node/terminalEncoding.ts b/src/vs/base/node/terminalEncoding.ts new file mode 100644 index 00000000000..3077f3a9633 --- /dev/null +++ b/src/vs/base/node/terminalEncoding.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * This code is also used by standalone cli's. Avoid adding dependencies to keep the size of the cli small. + */ +import { exec } from 'child_process'; +import * as os from 'os'; + +const windowsTerminalEncodings = { + '437': 'cp437', // United States + '850': 'cp850', // Multilingual(Latin I) + '852': 'cp852', // Slavic(Latin II) + '855': 'cp855', // Cyrillic(Russian) + '857': 'cp857', // Turkish + '860': 'cp860', // Portuguese + '861': 'cp861', // Icelandic + '863': 'cp863', // Canadian - French + '865': 'cp865', // Nordic + '866': 'cp866', // Russian + '869': 'cp869', // Modern Greek + '936': 'cp936', // Simplified Chinese + '1252': 'cp1252' // West European Latin +}; + +function toIconvLiteEncoding(encodingName: string): string { + const normalizedEncodingName = encodingName.replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); + const mapped = JSCHARDET_TO_ICONV_ENCODINGS[normalizedEncodingName]; + + return mapped || normalizedEncodingName; +} + +const JSCHARDET_TO_ICONV_ENCODINGS: { [name: string]: string } = { + 'ibm866': 'cp866', + 'big5': 'cp950' +}; + +const UTF8 = 'utf8'; + + +export async function resolveTerminalEncoding(verbose?: boolean): Promise { + let rawEncodingPromise: Promise; + + // Support a global environment variable to win over other mechanics + const cliEncodingEnv = process.env['VSCODE_CLI_ENCODING']; + if (cliEncodingEnv) { + if (verbose) { + console.log(`Found VSCODE_CLI_ENCODING variable: ${cliEncodingEnv}`); + } + + rawEncodingPromise = Promise.resolve(cliEncodingEnv); + } + + // Windows: educated guess + else if (os.platform() === 'win32') { + rawEncodingPromise = new Promise(resolve => { + if (verbose) { + console.log('Running "chcp" to detect terminal encoding...'); + } + + exec('chcp', (err, stdout, stderr) => { + if (stdout) { + const windowsTerminalEncodingKeys = Object.keys(windowsTerminalEncodings) as Array; + for (const key of windowsTerminalEncodingKeys) { + if (stdout.indexOf(key) >= 0) { + return resolve(windowsTerminalEncodings[key]); + } + } + } + + return resolve(undefined); + }); + }); + } + // Linux/Mac: use "locale charmap" command + else { + rawEncodingPromise = new Promise(resolve => { + if (verbose) { + console.log('Running "locale charmap" to detect terminal encoding...'); + } + + exec('locale charmap', (err, stdout, stderr) => resolve(stdout)); + }); + } + + const rawEncoding = await rawEncodingPromise; + if (verbose) { + console.log(`Detected raw terminal encoding: ${rawEncoding}`); + } + + if (!rawEncoding || rawEncoding.toLowerCase() === 'utf-8' || rawEncoding.toLowerCase() === UTF8) { + return UTF8; + } + + return toIconvLiteEncoding(rawEncoding); +} diff --git a/src/vs/base/test/node/encoding/encoding.test.ts b/src/vs/base/test/node/encoding/encoding.test.ts index a78f2ed6b46..1931c76aa40 100644 --- a/src/vs/base/test/node/encoding/encoding.test.ts +++ b/src/vs/base/test/node/encoding/encoding.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import * as fs from 'fs'; import * as encoding from 'vs/base/node/encoding'; +import * as terminalEncoding from 'vs/base/node/terminalEncoding'; import { Readable } from 'stream'; import { getPathFromAmdModule } from 'vs/base/common/amd'; @@ -118,14 +119,14 @@ suite('Encoding', () => { }); test('resolve terminal encoding (detect)', async function () { - const enc = await encoding.resolveTerminalEncoding(); - assert.ok(encoding.encodingExists(enc)); + const enc = await terminalEncoding.resolveTerminalEncoding(); + assert.ok(enc.length > 0); }); test('resolve terminal encoding (environment)', async function () { process.env['VSCODE_CLI_ENCODING'] = 'utf16le'; - const enc = await encoding.resolveTerminalEncoding(); + const enc = await terminalEncoding.resolveTerminalEncoding(); assert.ok(encoding.encodingExists(enc)); assert.equal(enc, 'utf16le'); }); diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 5a6c573b36d..609395b535a 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -14,10 +14,10 @@ import product from 'vs/platform/product/common/product'; import * as paths from 'vs/base/common/path'; import { whenDeleted, writeFileSync } from 'vs/base/node/pfs'; import { findFreePort, randomPort } from 'vs/base/node/ports'; -import { resolveTerminalEncoding } from 'vs/base/node/encoding'; import { isWindows, isLinux } from 'vs/base/common/platform'; import { ProfilingSession, Target } from 'v8-inspect-profiler'; import { isString } from 'vs/base/common/types'; +import { hasStdinWithoutTty, stdinDataListener, getStdinFilePath, readFromStdin } from 'vs/platform/environment/node/stdin'; function shouldSpawnCliProcess(argv: ParsedArgs): boolean { return !!argv['install-source'] @@ -142,91 +142,55 @@ export async function main(argv: string[]): Promise { }); } - let stdinWithoutTty: boolean = false; - try { - stdinWithoutTty = !process.stdin.isTTY; // Via https://twitter.com/MylesBorins/status/782009479382626304 - } catch (error) { - // Windows workaround for https://github.com/nodejs/node/issues/11656 - } - - const readFromStdin = args._.some(a => a === '-'); - if (readFromStdin) { + const hasReadStdinArg = args._.some(a => a === '-'); + if (hasReadStdinArg) { // remove the "-" argument when we read from stdin args._ = args._.filter(a => a !== '-'); argv = argv.filter(a => a !== '-'); } - let stdinFilePath: string; - if (stdinWithoutTty) { + let stdinFilePath: string | undefined; + if (hasStdinWithoutTty()) { // 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) - if (args._.length === 0 && readFromStdin) { - // 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`); + if (args._.length === 0) { + if (hasReadStdinArg) { + stdinFilePath = getStdinFilePath(); - // open tmp file for writing - let stdinFileError: Error | undefined; - let stdinFileStream: fs.WriteStream; - try { - stdinFileStream = fs.createWriteStream(stdinFilePath); - } catch (error) { - stdinFileError = error; - } + // returns a file path where stdin input is written into (write in progress). + try { + readFromStdin(stdinFilePath, !!verbose); // throws error if file can not be written - if (!stdinFileError) { + // Make sure to open tmp file + addArg(argv, stdinFilePath); - // Pipe into tmp file using terminals encoding - resolveTerminalEncoding(verbose).then(async encoding => { - const iconv = await import('iconv-lite'); - const converterStream = iconv.decodeStream(encoding); - process.stdin.pipe(converterStream).pipe(stdinFileStream); - }); + // Enable --wait to get all data and ignore adding this to history + addArg(argv, '--wait'); + addArg(argv, '--skip-add-to-recently-opened'); + args.wait = true; - // Make sure to open tmp file - addArg(argv, stdinFilePath); - - // Enable --wait to get all data and ignore adding this to history - addArg(argv, '--wait'); - addArg(argv, '--skip-add-to-recently-opened'); - args.wait = true; - } - - if (verbose) { - if (stdinFileError) { - console.error(`Failed to create file to read via stdin: ${stdinFileError.toString()}`); - } else { console.log(`Reading from stdin via: ${stdinFilePath}`); + } catch (e) { + console.log(`Failed to create file to read via stdin: ${e.toString()}`); + stdinFilePath = undefined; } - } - } + } else { - // 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 Promise(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} -').`); + // 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. + processCallbacks.push(_ => stdinDataListener(1000).then(dataReceived => { + if (dataReceived) { + 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(undefined); - }; - - // wait for 1s maximum... - setTimeout(() => { - process.stdin.removeListener('data', dataListener); - - c(undefined); - }, 1000); - - // ...but finish early if we detect data - process.stdin.once('data', dataListener); - })); + })); + } } } diff --git a/src/vs/platform/environment/node/stdin.ts b/src/vs/platform/environment/node/stdin.ts new file mode 100644 index 00000000000..2cd928e2507 --- /dev/null +++ b/src/vs/platform/environment/node/stdin.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +/** + * This code is also used by standalone cli's. Avoid adding dependencies to keep the size of the cli small. + */ +import * as paths from 'vs/base/common/path'; +import * as fs from 'fs'; +import * as os from 'os'; +import { resolveTerminalEncoding } from 'vs/base/node/terminalEncoding'; + +export function hasStdinWithoutTty() { + try { + return !process.stdin.isTTY; // Via https://twitter.com/MylesBorins/status/782009479382626304 + } catch (error) { + // Windows workaround for https://github.com/nodejs/node/issues/11656 + } + return false; +} + +export function stdinDataListener(durationinMs: number): Promise { + return new Promise(c => { + const dataListener = () => c(true); + + // wait for 1s maximum... + setTimeout(() => { + process.stdin.removeListener('data', dataListener); + + c(false); + }, durationinMs); + + // ...but finish early if we detect data + process.stdin.once('data', dataListener); + }); +} + +export function getStdinFilePath(): string { + return paths.join(os.tmpdir(), `code-stdin-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 3)}.txt`); +} + +export function readFromStdin(targetPath: string, verbose: boolean): Promise { + // open tmp file for writing + const stdinFileStream = fs.createWriteStream(targetPath); + // Pipe into tmp file using terminals encoding + return resolveTerminalEncoding(verbose).then(async encoding => { + + const iconv = await import('iconv-lite'); + if (!iconv.encodingExists(encoding)) { + console.log(`Unsupported terminal encoding: ${encoding}, falling back to UTF-8.`); + encoding = 'utf8'; + } + const converterStream = iconv.decodeStream(encoding); + process.stdin.pipe(converterStream).pipe(stdinFileStream); + }); +}