Skip to content

Commit 5db448e

Browse files
committed
refactor stdin code
1 parent 7e68a32 commit 5db448e

5 files changed

Lines changed: 191 additions & 158 deletions

File tree

src/vs/base/node/encoding.ts

Lines changed: 0 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as iconv from 'iconv-lite';
7-
import { isLinux, isMacintosh } from 'vs/base/common/platform';
8-
import { exec } from 'child_process';
97
import { Readable, Writable } from 'stream';
108
import { VSBuffer } from 'vs/base/common/buffer';
119

@@ -353,87 +351,3 @@ export function detectEncodingFromBuffer({ buffer, bytesRead }: IReadResult, aut
353351

354352
return { seemsBinary, encoding };
355353
}
356-
357-
// https://ss64.com/nt/chcp.html
358-
const windowsTerminalEncodings = {
359-
'437': 'cp437', // United States
360-
'850': 'cp850', // Multilingual(Latin I)
361-
'852': 'cp852', // Slavic(Latin II)
362-
'855': 'cp855', // Cyrillic(Russian)
363-
'857': 'cp857', // Turkish
364-
'860': 'cp860', // Portuguese
365-
'861': 'cp861', // Icelandic
366-
'863': 'cp863', // Canadian - French
367-
'865': 'cp865', // Nordic
368-
'866': 'cp866', // Russian
369-
'869': 'cp869', // Modern Greek
370-
'936': 'cp936', // Simplified Chinese
371-
'1252': 'cp1252' // West European Latin
372-
};
373-
374-
export async function resolveTerminalEncoding(verbose?: boolean): Promise<string> {
375-
let rawEncodingPromise: Promise<string>;
376-
377-
// Support a global environment variable to win over other mechanics
378-
const cliEncodingEnv = process.env['VSCODE_CLI_ENCODING'];
379-
if (cliEncodingEnv) {
380-
if (verbose) {
381-
console.log(`Found VSCODE_CLI_ENCODING variable: ${cliEncodingEnv}`);
382-
}
383-
384-
rawEncodingPromise = Promise.resolve(cliEncodingEnv);
385-
}
386-
387-
// Linux/Mac: use "locale charmap" command
388-
else if (isLinux || isMacintosh) {
389-
rawEncodingPromise = new Promise<string>(resolve => {
390-
if (verbose) {
391-
console.log('Running "locale charmap" to detect terminal encoding...');
392-
}
393-
394-
exec('locale charmap', (err, stdout, stderr) => resolve(stdout));
395-
});
396-
}
397-
398-
// Windows: educated guess
399-
else {
400-
rawEncodingPromise = new Promise<string>(resolve => {
401-
if (verbose) {
402-
console.log('Running "chcp" to detect terminal encoding...');
403-
}
404-
405-
exec('chcp', (err, stdout, stderr) => {
406-
if (stdout) {
407-
const windowsTerminalEncodingKeys = Object.keys(windowsTerminalEncodings) as Array<keyof typeof windowsTerminalEncodings>;
408-
for (const key of windowsTerminalEncodingKeys) {
409-
if (stdout.indexOf(key) >= 0) {
410-
return resolve(windowsTerminalEncodings[key]);
411-
}
412-
}
413-
}
414-
415-
return resolve(undefined);
416-
});
417-
});
418-
}
419-
420-
const rawEncoding = await rawEncodingPromise;
421-
if (verbose) {
422-
console.log(`Detected raw terminal encoding: ${rawEncoding}`);
423-
}
424-
425-
if (!rawEncoding || rawEncoding.toLowerCase() === 'utf-8' || rawEncoding.toLowerCase() === UTF8) {
426-
return UTF8;
427-
}
428-
429-
const iconvEncoding = toIconvLiteEncoding(rawEncoding);
430-
if (iconv.encodingExists(iconvEncoding)) {
431-
return iconvEncoding;
432-
}
433-
434-
if (verbose) {
435-
console.log('Unsupported terminal encoding, falling back to UTF-8.');
436-
}
437-
438-
return UTF8;
439-
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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+
/**
7+
* This code is also used by standalone cli's. Avoid adding dependencies to keep the size of the cli small.
8+
*/
9+
import { exec } from 'child_process';
10+
import * as os from 'os';
11+
12+
const windowsTerminalEncodings = {
13+
'437': 'cp437', // United States
14+
'850': 'cp850', // Multilingual(Latin I)
15+
'852': 'cp852', // Slavic(Latin II)
16+
'855': 'cp855', // Cyrillic(Russian)
17+
'857': 'cp857', // Turkish
18+
'860': 'cp860', // Portuguese
19+
'861': 'cp861', // Icelandic
20+
'863': 'cp863', // Canadian - French
21+
'865': 'cp865', // Nordic
22+
'866': 'cp866', // Russian
23+
'869': 'cp869', // Modern Greek
24+
'936': 'cp936', // Simplified Chinese
25+
'1252': 'cp1252' // West European Latin
26+
};
27+
28+
function toIconvLiteEncoding(encodingName: string): string {
29+
const normalizedEncodingName = encodingName.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
30+
const mapped = JSCHARDET_TO_ICONV_ENCODINGS[normalizedEncodingName];
31+
32+
return mapped || normalizedEncodingName;
33+
}
34+
35+
const JSCHARDET_TO_ICONV_ENCODINGS: { [name: string]: string } = {
36+
'ibm866': 'cp866',
37+
'big5': 'cp950'
38+
};
39+
40+
const UTF8 = 'utf8';
41+
42+
43+
export async function resolveTerminalEncoding(verbose?: boolean): Promise<string> {
44+
let rawEncodingPromise: Promise<string>;
45+
46+
// Support a global environment variable to win over other mechanics
47+
const cliEncodingEnv = process.env['VSCODE_CLI_ENCODING'];
48+
if (cliEncodingEnv) {
49+
if (verbose) {
50+
console.log(`Found VSCODE_CLI_ENCODING variable: ${cliEncodingEnv}`);
51+
}
52+
53+
rawEncodingPromise = Promise.resolve(cliEncodingEnv);
54+
}
55+
56+
// Windows: educated guess
57+
else if (os.platform() === 'win32') {
58+
rawEncodingPromise = new Promise<string>(resolve => {
59+
if (verbose) {
60+
console.log('Running "chcp" to detect terminal encoding...');
61+
}
62+
63+
exec('chcp', (err, stdout, stderr) => {
64+
if (stdout) {
65+
const windowsTerminalEncodingKeys = Object.keys(windowsTerminalEncodings) as Array<keyof typeof windowsTerminalEncodings>;
66+
for (const key of windowsTerminalEncodingKeys) {
67+
if (stdout.indexOf(key) >= 0) {
68+
return resolve(windowsTerminalEncodings[key]);
69+
}
70+
}
71+
}
72+
73+
return resolve(undefined);
74+
});
75+
});
76+
}
77+
// Linux/Mac: use "locale charmap" command
78+
else {
79+
rawEncodingPromise = new Promise<string>(resolve => {
80+
if (verbose) {
81+
console.log('Running "locale charmap" to detect terminal encoding...');
82+
}
83+
84+
exec('locale charmap', (err, stdout, stderr) => resolve(stdout));
85+
});
86+
}
87+
88+
const rawEncoding = await rawEncodingPromise;
89+
if (verbose) {
90+
console.log(`Detected raw terminal encoding: ${rawEncoding}`);
91+
}
92+
93+
if (!rawEncoding || rawEncoding.toLowerCase() === 'utf-8' || rawEncoding.toLowerCase() === UTF8) {
94+
return UTF8;
95+
}
96+
97+
return toIconvLiteEncoding(rawEncoding);
98+
}

src/vs/base/test/node/encoding/encoding.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import * as assert from 'assert';
77
import * as fs from 'fs';
88
import * as encoding from 'vs/base/node/encoding';
9+
import * as terminalEncoding from 'vs/base/node/terminalEncoding';
910
import { Readable } from 'stream';
1011
import { getPathFromAmdModule } from 'vs/base/common/amd';
1112

@@ -118,14 +119,14 @@ suite('Encoding', () => {
118119
});
119120

120121
test('resolve terminal encoding (detect)', async function () {
121-
const enc = await encoding.resolveTerminalEncoding();
122-
assert.ok(encoding.encodingExists(enc));
122+
const enc = await terminalEncoding.resolveTerminalEncoding();
123+
assert.ok(enc.length > 0);
123124
});
124125

125126
test('resolve terminal encoding (environment)', async function () {
126127
process.env['VSCODE_CLI_ENCODING'] = 'utf16le';
127128

128-
const enc = await encoding.resolveTerminalEncoding();
129+
const enc = await terminalEncoding.resolveTerminalEncoding();
129130
assert.ok(encoding.encodingExists(enc));
130131
assert.equal(enc, 'utf16le');
131132
});

src/vs/code/node/cli.ts

Lines changed: 33 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ import product from 'vs/platform/product/common/product';
1414
import * as paths from 'vs/base/common/path';
1515
import { whenDeleted, writeFileSync } from 'vs/base/node/pfs';
1616
import { findFreePort, randomPort } from 'vs/base/node/ports';
17-
import { resolveTerminalEncoding } from 'vs/base/node/encoding';
1817
import { isWindows, isLinux } from 'vs/base/common/platform';
1918
import { ProfilingSession, Target } from 'v8-inspect-profiler';
2019
import { isString } from 'vs/base/common/types';
20+
import { hasStdinWithoutTty, stdinDataListener, getStdinFilePath, readFromStdin } from 'vs/platform/environment/node/stdin';
2121

2222
function shouldSpawnCliProcess(argv: ParsedArgs): boolean {
2323
return !!argv['install-source']
@@ -142,91 +142,55 @@ export async function main(argv: string[]): Promise<any> {
142142
});
143143
}
144144

145-
let stdinWithoutTty: boolean = false;
146-
try {
147-
stdinWithoutTty = !process.stdin.isTTY; // Via https://twitter.com/MylesBorins/status/782009479382626304
148-
} catch (error) {
149-
// Windows workaround for https://github.com/nodejs/node/issues/11656
150-
}
151-
152-
const readFromStdin = args._.some(a => a === '-');
153-
if (readFromStdin) {
145+
const hasReadStdinArg = args._.some(a => a === '-');
146+
if (hasReadStdinArg) {
154147
// remove the "-" argument when we read from stdin
155148
args._ = args._.filter(a => a !== '-');
156149
argv = argv.filter(a => a !== '-');
157150
}
158151

159-
let stdinFilePath: string;
160-
if (stdinWithoutTty) {
152+
let stdinFilePath: string | undefined;
153+
if (hasStdinWithoutTty()) {
161154

162155
// Read from stdin: we require a single "-" argument to be passed in order to start reading from
163156
// stdin. We do this because there is no reliable way to find out if data is piped to stdin. Just
164157
// checking for stdin being connected to a TTY is not enough (https://github.com/Microsoft/vscode/issues/40351)
165-
if (args._.length === 0 && readFromStdin) {
166-
167-
// prepare temp file to read stdin to
168-
stdinFilePath = paths.join(os.tmpdir(), `code-stdin-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 3)}.txt`);
169158

170-
// open tmp file for writing
171-
let stdinFileError: Error | undefined;
172-
let stdinFileStream: fs.WriteStream;
173-
try {
174-
stdinFileStream = fs.createWriteStream(stdinFilePath);
175-
} catch (error) {
176-
stdinFileError = error;
177-
}
159+
if (args._.length === 0) {
160+
if (hasReadStdinArg) {
161+
stdinFilePath = getStdinFilePath();
178162

179-
if (!stdinFileError) {
163+
// returns a file path where stdin input is written into (write in progress).
164+
try {
165+
readFromStdin(stdinFilePath, !!verbose); // throws error if file can not be written
180166

181-
// Pipe into tmp file using terminals encoding
182-
resolveTerminalEncoding(verbose).then(async encoding => {
183-
const iconv = await import('iconv-lite');
184-
const converterStream = iconv.decodeStream(encoding);
185-
process.stdin.pipe(converterStream).pipe(stdinFileStream);
186-
});
167+
// Make sure to open tmp file
168+
addArg(argv, stdinFilePath);
187169

188-
// Make sure to open tmp file
189-
addArg(argv, stdinFilePath);
190-
191-
// Enable --wait to get all data and ignore adding this to history
192-
addArg(argv, '--wait');
193-
addArg(argv, '--skip-add-to-recently-opened');
194-
args.wait = true;
195-
}
170+
// Enable --wait to get all data and ignore adding this to history
171+
addArg(argv, '--wait');
172+
addArg(argv, '--skip-add-to-recently-opened');
173+
args.wait = true;
196174

197-
if (verbose) {
198-
if (stdinFileError) {
199-
console.error(`Failed to create file to read via stdin: ${stdinFileError.toString()}`);
200-
} else {
201175
console.log(`Reading from stdin via: ${stdinFilePath}`);
176+
} catch (e) {
177+
console.log(`Failed to create file to read via stdin: ${e.toString()}`);
178+
stdinFilePath = undefined;
202179
}
203-
}
204-
}
205-
206-
// If the user pipes data via stdin but forgot to add the "-" argument, help by printing a message
207-
// if we detect that data flows into via stdin after a certain timeout.
208-
else if (args._.length === 0) {
209-
processCallbacks.push(child => new Promise(c => {
210-
const dataListener = () => {
211-
if (isWindows) {
212-
console.log(`Run with '${product.applicationName} -' to read output from another program (e.g. 'echo Hello World | ${product.applicationName} -').`);
213-
} else {
214-
console.log(`Run with '${product.applicationName} -' to read from stdin (e.g. 'ps aux | grep code | ${product.applicationName} -').`);
180+
} else {
181+
182+
// If the user pipes data via stdin but forgot to add the "-" argument, help by printing a message
183+
// if we detect that data flows into via stdin after a certain timeout.
184+
processCallbacks.push(_ => stdinDataListener(1000).then(dataReceived => {
185+
if (dataReceived) {
186+
if (isWindows) {
187+
console.log(`Run with '${product.applicationName} -' to read output from another program (e.g. 'echo Hello World | ${product.applicationName} -').`);
188+
} else {
189+
console.log(`Run with '${product.applicationName} -' to read from stdin (e.g. 'ps aux | grep code | ${product.applicationName} -').`);
190+
}
215191
}
216-
217-
c(undefined);
218-
};
219-
220-
// wait for 1s maximum...
221-
setTimeout(() => {
222-
process.stdin.removeListener('data', dataListener);
223-
224-
c(undefined);
225-
}, 1000);
226-
227-
// ...but finish early if we detect data
228-
process.stdin.once('data', dataListener);
229-
}));
192+
}));
193+
}
230194
}
231195
}
232196

0 commit comments

Comments
 (0)