Archived
1
0

Squashed 'lib/vscode/' content from commit e5a624b788

git-subtree-dir: lib/vscode
git-subtree-split: e5a624b788d92b8d34d1392e4c4d9789406efe8f
This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
commit be3e823608
4649 changed files with 1311795 additions and 0 deletions

64
src/vs/base/node/cpuUsage.sh Executable file
View File

@ -0,0 +1,64 @@
#!/bin/bash
function get_total_cpu_time() {
# Read the first line of /proc/stat and remove the cpu prefix
CPU=(`sed -n 's/^cpu\s//p' /proc/stat`)
# Sum all of the values in CPU to get total time
for VALUE in "${CPU[@]}"; do
let $1=$1+$VALUE
done
}
TOTAL_TIME_BEFORE=0
get_total_cpu_time TOTAL_TIME_BEFORE
# Loop over the arguments, which are a list of PIDs
# The 13th and 14th words in /proc/<PID>/stat are the user and system time
# the process has used, so sum these to get total process run time
declare -a PROCESS_BEFORE_TIMES
ITER=0
for PID in "$@"; do
if [ -f /proc/$PID/stat ]
then
PROCESS_STATS=`cat /proc/$PID/stat`
PROCESS_STAT_ARRAY=($PROCESS_STATS)
let PROCESS_TIME_BEFORE="${PROCESS_STAT_ARRAY[13]}+${PROCESS_STAT_ARRAY[14]}"
else
let PROCESS_TIME_BEFORE=0
fi
PROCESS_BEFORE_TIMES[$ITER]=$PROCESS_TIME_BEFORE
((++ITER))
done
# Wait for a second
sleep 1
TOTAL_TIME_AFTER=0
get_total_cpu_time TOTAL_TIME_AFTER
# Check the user and system time sum of each process again and compute the change
# in process time used over total system time
ITER=0
for PID in "$@"; do
if [ -f /proc/$PID/stat ]
then
PROCESS_STATS=`cat /proc/$PID/stat`
PROCESS_STAT_ARRAY=($PROCESS_STATS)
let PROCESS_TIME_AFTER="${PROCESS_STAT_ARRAY[13]}+${PROCESS_STAT_ARRAY[14]}"
else
let PROCESS_TIME_AFTER=${PROCESS_BEFORE_TIMES[$ITER]}
fi
PROCESS_TIME_BEFORE=${PROCESS_BEFORE_TIMES[$ITER]}
let PROCESS_DELTA=$PROCESS_TIME_AFTER-$PROCESS_TIME_BEFORE
let TOTAL_DELTA=$TOTAL_TIME_AFTER-$TOTAL_TIME_BEFORE
CPU_USAGE=`echo "$((100*$PROCESS_DELTA/$TOTAL_DELTA))"`
# Parent script reads from stdout, so echo result to be read
echo $CPU_USAGE
((++ITER))
done

View File

@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as crypto from 'crypto';
import { once } from 'vs/base/common/functional';
export async function checksum(path: string, sha1hash: string | undefined): Promise<void> {
const checksumPromise = new Promise<string | undefined>((resolve, reject) => {
const input = fs.createReadStream(path);
const hash = crypto.createHash('sha1');
input.pipe(hash);
const done = once((err?: Error, result?: string) => {
input.removeAllListeners();
hash.removeAllListeners();
if (err) {
reject(err);
} else {
resolve(result);
}
});
input.once('error', done);
input.once('end', done);
hash.once('error', done);
hash.once('data', (data: Buffer) => done(undefined, data.toString('hex')));
});
const hash = await checksumPromise;
if (hash !== sha1hash) {
throw new Error('Hash mismatch');
}
}

View File

@ -0,0 +1,62 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as sd from 'string_decoder';
import { CharCode } from 'vs/base/common/charCode';
/**
* Convenient way to iterate over output line by line. This helper accommodates for the fact that
* a buffer might not end with new lines all the way.
*
* To use:
* - call the write method
* - forEach() over the result to get the lines
*/
export class LineDecoder {
private stringDecoder: sd.StringDecoder;
private remaining: string | null;
constructor(encoding: string = 'utf8') {
this.stringDecoder = new sd.StringDecoder(encoding);
this.remaining = null;
}
write(buffer: Buffer): string[] {
const result: string[] = [];
const value = this.remaining
? this.remaining + this.stringDecoder.write(buffer)
: this.stringDecoder.write(buffer);
if (value.length < 1) {
return result;
}
let start = 0;
let ch: number;
let idx = start;
while (idx < value.length) {
ch = value.charCodeAt(idx);
if (ch === CharCode.CarriageReturn || ch === CharCode.LineFeed) {
result.push(value.substring(start, idx));
idx++;
if (idx < value.length) {
const lastChar = ch;
ch = value.charCodeAt(idx);
if ((lastChar === CharCode.CarriageReturn && ch === CharCode.LineFeed) || (lastChar === CharCode.LineFeed && ch === CharCode.CarriageReturn)) {
idx++;
}
}
start = idx;
} else {
idx++;
}
}
this.remaining = start < value.length ? value.substr(start) : null;
return result;
}
end(): string | null {
return this.remaining;
}
}

View File

@ -0,0 +1,91 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import { rtrim } from 'vs/base/common/strings';
import { sep, join, normalize, dirname, basename } from 'vs/base/common/path';
import { readdirSync } from 'vs/base/node/pfs';
import { promisify } from 'util';
/**
* Copied from: https://github.com/microsoft/vscode-node-debug/blob/master/src/node/pathUtilities.ts#L83
*
* Given an absolute, normalized, and existing file path 'realcase' returns the exact path that the file has on disk.
* On a case insensitive file system, the returned path might differ from the original path by character casing.
* On a case sensitive file system, the returned path will always be identical to the original path.
* In case of errors, null is returned. But you cannot use this function to verify that a path exists.
* realcaseSync does not handle '..' or '.' path segments and it does not take the locale into account.
*/
export function realcaseSync(path: string): string | null {
const dir = dirname(path);
if (path === dir) { // end recursion
return path;
}
const name = (basename(path) /* can be '' for windows drive letters */ || path).toLowerCase();
try {
const entries = readdirSync(dir);
const found = entries.filter(e => e.toLowerCase() === name); // use a case insensitive search
if (found.length === 1) {
// on a case sensitive filesystem we cannot determine here, whether the file exists or not, hence we need the 'file exists' precondition
const prefix = realcaseSync(dir); // recurse
if (prefix) {
return join(prefix, found[0]);
}
} else if (found.length > 1) {
// must be a case sensitive $filesystem
const ix = found.indexOf(name);
if (ix >= 0) { // case sensitive
const prefix = realcaseSync(dir); // recurse
if (prefix) {
return join(prefix, found[ix]);
}
}
}
} catch (error) {
// silently ignore error
}
return null;
}
export async function realpath(path: string): Promise<string> {
try {
return await promisify(fs.realpath)(path);
} catch (error) {
// We hit an error calling fs.realpath(). Since fs.realpath() is doing some path normalization
// we now do a similar normalization and then try again if we can access the path with read
// permissions at least. If that succeeds, we return that path.
// fs.realpath() is resolving symlinks and that can fail in certain cases. The workaround is
// to not resolve links but to simply see if the path is read accessible or not.
const normalizedPath = normalizePath(path);
await promisify(fs.access)(normalizedPath, fs.constants.R_OK);
return normalizedPath;
}
}
export function realpathSync(path: string): string {
try {
return fs.realpathSync(path);
} catch (error) {
// We hit an error calling fs.realpathSync(). Since fs.realpathSync() is doing some path normalization
// we now do a similar normalization and then try again if we can access the path with read
// permissions at least. If that succeeds, we return that path.
// fs.realpath() is resolving symlinks and that can fail in certain cases. The workaround is
// to not resolve links but to simply see if the path is read accessible or not.
const normalizedPath = normalizePath(path);
fs.accessSync(normalizedPath, fs.constants.R_OK); // throws in case of an error
return normalizedPath;
}
}
function normalizePath(path: string): string {
return rtrim(normalize(path), sep);
}

101
src/vs/base/node/id.ts Normal file
View File

@ -0,0 +1,101 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as errors from 'vs/base/common/errors';
import * as uuid from 'vs/base/common/uuid';
import { networkInterfaces } from 'os';
import { TernarySearchTree } from 'vs/base/common/map';
import { getMac } from 'vs/base/node/macAddress';
// http://www.techrepublic.com/blog/data-center/mac-address-scorecard-for-common-virtual-machine-platforms/
// VMware ESX 3, Server, Workstation, Player 00-50-56, 00-0C-29, 00-05-69
// Microsoft Hyper-V, Virtual Server, Virtual PC 00-03-FF
// Parallels Desktop, Workstation, Server, Virtuozzo 00-1C-42
// Virtual Iron 4 00-0F-4B
// Red Hat Xen 00-16-3E
// Oracle VM 00-16-3E
// XenSource 00-16-3E
// Novell Xen 00-16-3E
// Sun xVM VirtualBox 08-00-27
export const virtualMachineHint: { value(): number } = new class {
private _virtualMachineOUIs?: TernarySearchTree<string, boolean>;
private _value?: number;
private _isVirtualMachineMacAdress(mac: string): boolean {
if (!this._virtualMachineOUIs) {
this._virtualMachineOUIs = TernarySearchTree.forStrings<boolean>();
// dash-separated
this._virtualMachineOUIs.set('00-50-56', true);
this._virtualMachineOUIs.set('00-0C-29', true);
this._virtualMachineOUIs.set('00-05-69', true);
this._virtualMachineOUIs.set('00-03-FF', true);
this._virtualMachineOUIs.set('00-1C-42', true);
this._virtualMachineOUIs.set('00-16-3E', true);
this._virtualMachineOUIs.set('08-00-27', true);
// colon-separated
this._virtualMachineOUIs.set('00:50:56', true);
this._virtualMachineOUIs.set('00:0C:29', true);
this._virtualMachineOUIs.set('00:05:69', true);
this._virtualMachineOUIs.set('00:03:FF', true);
this._virtualMachineOUIs.set('00:1C:42', true);
this._virtualMachineOUIs.set('00:16:3E', true);
this._virtualMachineOUIs.set('08:00:27', true);
}
return !!this._virtualMachineOUIs.findSubstr(mac);
}
value(): number {
if (this._value === undefined) {
let vmOui = 0;
let interfaceCount = 0;
const interfaces = networkInterfaces();
for (let name in interfaces) {
if (Object.prototype.hasOwnProperty.call(interfaces, name)) {
for (const { mac, internal } of interfaces[name]) {
if (!internal) {
interfaceCount += 1;
if (this._isVirtualMachineMacAdress(mac.toUpperCase())) {
vmOui += 1;
}
}
}
}
}
this._value = interfaceCount > 0
? vmOui / interfaceCount
: 0;
}
return this._value;
}
};
let machineId: Promise<string>;
export async function getMachineId(): Promise<string> {
if (!machineId) {
machineId = (async () => {
const id = await getMacMachineId();
return id || uuid.generateUuid(); // fallback, generate a UUID
})();
}
return machineId;
}
async function getMacMachineId(): Promise<string | undefined> {
try {
const crypto = await import('crypto');
const macAddress = await getMac();
return crypto.createHash('sha256').update(macAddress, 'utf8').digest('hex');
} catch (err) {
errors.onUnexpectedError(err);
return undefined;
}
}

24
src/vs/base/node/languagePacks.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface NLSConfiguration {
locale: string;
availableLanguages: {
[key: string]: string;
};
pseudo?: boolean;
_languagePackSupport?: boolean;
}
export interface InternalNLSConfiguration extends NLSConfiguration {
_languagePackId: string;
_translationsConfigFile: string;
_cacheRoot: string;
_resolvedLanguagePackCoreLocation: string;
_corruptedFile: string;
_languagePackSupport?: boolean;
}
export function getNLSConfiguration(commit: string, userDataPath: string, metaDataFile: string, locale: string): Promise<NLSConfiguration>;

View File

@ -0,0 +1,314 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
//@ts-check
/**
* @param {NodeRequire} nodeRequire
* @param {typeof import('path')} path
* @param {typeof import('fs')} fs
* @param {typeof import('../common/performance')} perf
*/
function factory(nodeRequire, path, fs, perf) {
/**
* @param {string} file
* @returns {Promise<boolean>}
*/
function exists(file) {
return new Promise(c => fs.exists(file, c));
}
/**
* @param {string} file
* @returns {Promise<void>}
*/
function touch(file) {
return new Promise((c, e) => { const d = new Date(); fs.utimes(file, d, d, err => err ? e(err) : c()); });
}
/**
* @param {string} file
* @returns {Promise<object>}
*/
function lstat(file) {
return new Promise((c, e) => fs.lstat(file, (err, stats) => err ? e(err) : c(stats)));
}
/**
* @param {string} dir
* @returns {Promise<string[]>}
*/
function readdir(dir) {
return new Promise((c, e) => fs.readdir(dir, (err, files) => err ? e(err) : c(files)));
}
/**
* @param {string} dir
* @returns {Promise<string>}
*/
function mkdirp(dir) {
return new Promise((c, e) => fs.mkdir(dir, { recursive: true }, err => (err && err.code !== 'EEXIST') ? e(err) : c(dir)));
}
/**
* @param {string} dir
* @returns {Promise<void>}
*/
function rmdir(dir) {
return new Promise((c, e) => fs.rmdir(dir, err => err ? e(err) : c(undefined)));
}
/**
* @param {string} file
* @returns {Promise<void>}
*/
function unlink(file) {
return new Promise((c, e) => fs.unlink(file, err => err ? e(err) : c(undefined)));
}
/**
* @param {string} location
* @returns {Promise<void>}
*/
function rimraf(location) {
return lstat(location).then(stat => {
if (stat.isDirectory() && !stat.isSymbolicLink()) {
return readdir(location)
.then(children => Promise.all(children.map(child => rimraf(path.join(location, child)))))
.then(() => rmdir(location));
} else {
return unlink(location);
}
}, err => {
if (err.code === 'ENOENT') {
return undefined;
}
throw err;
});
}
function readFile(file) {
return new Promise(function (resolve, reject) {
fs.readFile(file, 'utf8', function (err, data) {
if (err) {
reject(err);
return;
}
resolve(data);
});
});
}
/**
* @param {string} file
* @param {string} content
* @returns {Promise<void>}
*/
function writeFile(file, content) {
return new Promise(function (resolve, reject) {
fs.writeFile(file, content, 'utf8', function (err) {
if (err) {
reject(err);
return;
}
resolve();
});
});
}
/**
* @param {string} userDataPath
* @returns {object}
*/
function getLanguagePackConfigurations(userDataPath) {
const configFile = path.join(userDataPath, 'languagepacks.json');
try {
return nodeRequire(configFile);
} catch (err) {
// Do nothing. If we can't read the file we have no
// language pack config.
}
return undefined;
}
/**
* @param {object} config
* @param {string} locale
*/
function resolveLanguagePackLocale(config, locale) {
try {
while (locale) {
if (config[locale]) {
return locale;
} else {
const index = locale.lastIndexOf('-');
if (index > 0) {
locale = locale.substring(0, index);
} else {
return undefined;
}
}
}
} catch (err) {
console.error('Resolving language pack configuration failed.', err);
}
return undefined;
}
/**
* @param {string} commit
* @param {string} userDataPath
* @param {string} metaDataFile
* @param {string} locale
*/
function getNLSConfiguration(commit, userDataPath, metaDataFile, locale) {
if (locale === 'pseudo') {
return Promise.resolve({ locale: locale, availableLanguages: {}, pseudo: true });
}
if (process.env['VSCODE_DEV']) {
return Promise.resolve({ locale: locale, availableLanguages: {} });
}
// We have a built version so we have extracted nls file. Try to find
// the right file to use.
// Check if we have an English or English US locale. If so fall to default since that is our
// English translation (we don't ship *.nls.en.json files)
if (locale && (locale === 'en' || locale === 'en-us')) {
return Promise.resolve({ locale: locale, availableLanguages: {} });
}
const initialLocale = locale;
perf.mark('nlsGeneration:start');
const defaultResult = function (locale) {
perf.mark('nlsGeneration:end');
return Promise.resolve({ locale: locale, availableLanguages: {} });
};
try {
if (!commit) {
return defaultResult(initialLocale);
}
const configs = getLanguagePackConfigurations(userDataPath);
if (!configs) {
return defaultResult(initialLocale);
}
locale = resolveLanguagePackLocale(configs, locale);
if (!locale) {
return defaultResult(initialLocale);
}
const packConfig = configs[locale];
let mainPack;
if (!packConfig || typeof packConfig.hash !== 'string' || !packConfig.translations || typeof (mainPack = packConfig.translations['vscode']) !== 'string') {
return defaultResult(initialLocale);
}
return exists(mainPack).then(fileExists => {
if (!fileExists) {
return defaultResult(initialLocale);
}
const packId = packConfig.hash + '.' + locale;
const cacheRoot = path.join(userDataPath, 'clp', packId);
const coreLocation = path.join(cacheRoot, commit);
const translationsConfigFile = path.join(cacheRoot, 'tcf.json');
const corruptedFile = path.join(cacheRoot, 'corrupted.info');
const result = {
locale: initialLocale,
availableLanguages: { '*': locale },
_languagePackId: packId,
_translationsConfigFile: translationsConfigFile,
_cacheRoot: cacheRoot,
_resolvedLanguagePackCoreLocation: coreLocation,
_corruptedFile: corruptedFile
};
return exists(corruptedFile).then(corrupted => {
// The nls cache directory is corrupted.
let toDelete;
if (corrupted) {
toDelete = rimraf(cacheRoot);
} else {
toDelete = Promise.resolve(undefined);
}
return toDelete.then(() => {
return exists(coreLocation).then(fileExists => {
if (fileExists) {
// We don't wait for this. No big harm if we can't touch
touch(coreLocation).catch(() => { });
perf.mark('nlsGeneration:end');
return result;
}
return mkdirp(coreLocation).then(() => {
return Promise.all([readFile(metaDataFile), readFile(mainPack)]);
}).then(values => {
const metadata = JSON.parse(values[0]);
const packData = JSON.parse(values[1]).contents;
const bundles = Object.keys(metadata.bundles);
const writes = [];
for (const bundle of bundles) {
const modules = metadata.bundles[bundle];
const target = Object.create(null);
for (const module of modules) {
const keys = metadata.keys[module];
const defaultMessages = metadata.messages[module];
const translations = packData[module];
let targetStrings;
if (translations) {
targetStrings = [];
for (let i = 0; i < keys.length; i++) {
const elem = keys[i];
const key = typeof elem === 'string' ? elem : elem.key;
let translatedMessage = translations[key];
if (translatedMessage === undefined) {
translatedMessage = defaultMessages[i];
}
targetStrings.push(translatedMessage);
}
} else {
targetStrings = defaultMessages;
}
target[module] = targetStrings;
}
writes.push(writeFile(path.join(coreLocation, bundle.replace(/\//g, '!') + '.nls.json'), JSON.stringify(target)));
}
writes.push(writeFile(translationsConfigFile, JSON.stringify(packConfig.translations)));
return Promise.all(writes);
}).then(() => {
perf.mark('nlsGeneration:end');
return result;
}).catch(err => {
console.error('Generating translation files failed.', err);
return defaultResult(locale);
});
});
});
});
});
} catch (err) {
console.error('Generating translation files failed.', err);
return defaultResult(locale);
}
}
return {
getNLSConfiguration
};
}
if (typeof define === 'function') {
// amd
define(['path', 'fs', 'vs/base/common/performance'], function (path, fs, perf) { return factory(require.__$__nodeRequire, path, fs, perf); });
} else if (typeof module === 'object' && typeof module.exports === 'object') {
const path = require('path');
const fs = require('fs');
const perf = require('../common/performance');
module.exports = factory(require, path, fs, perf);
} else {
throw new Error('Unknown context');
}

View File

@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { networkInterfaces } from 'os';
const invalidMacAddresses = new Set([
'00:00:00:00:00:00',
'ff:ff:ff:ff:ff:ff',
'ac:de:48:00:11:22'
]);
function validateMacAddress(candidate: string): boolean {
const tempCandidate = candidate.replace(/\-/g, ':').toLowerCase();
return !invalidMacAddresses.has(tempCandidate);
}
export function getMac(): Promise<string> {
return new Promise(async (resolve, reject) => {
const timeout = setTimeout(() => reject('Unable to retrieve mac address (timeout after 10s)'), 10000);
try {
resolve(await doGetMac());
} catch (error) {
reject(error);
} finally {
clearTimeout(timeout);
}
});
}
function doGetMac(): Promise<string> {
return new Promise((resolve, reject) => {
try {
const ifaces = networkInterfaces();
for (const [, infos] of Object.entries(ifaces)) {
for (const info of infos) {
if (validateMacAddress(info.mac)) {
return resolve(info.mac);
}
}
}
reject('Unable to retrieve mac address (unexpected format)');
} catch (err) {
reject(err);
}
});
}

16
src/vs/base/node/paths.ts Normal file
View File

@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { FileAccess } from 'vs/base/common/network';
interface IPaths {
getAppDataPath(platform: string): string;
getDefaultUserDataPath(platform: string): string;
}
const pathsPath = FileAccess.asFileUri('paths', require).fsPath;
const paths = require.__$__nodeRequire<IPaths>(pathsPath);
export const getAppDataPath = paths.getAppDataPath;
export const getDefaultUserDataPath = paths.getDefaultUserDataPath;

559
src/vs/base/node/pfs.ts Normal file
View File

@ -0,0 +1,559 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { join } from 'vs/base/common/path';
import { Queue } from 'vs/base/common/async';
import * as fs from 'fs';
import * as os from 'os';
import * as platform from 'vs/base/common/platform';
import { Event } from 'vs/base/common/event';
import { promisify } from 'util';
import { isRootOrDriveLetter } from 'vs/base/common/extpath';
import { generateUuid } from 'vs/base/common/uuid';
import { normalizeNFC } from 'vs/base/common/normalization';
// See https://github.com/microsoft/vscode/issues/30180
const WIN32_MAX_FILE_SIZE = 300 * 1024 * 1024; // 300 MB
const GENERAL_MAX_FILE_SIZE = 16 * 1024 * 1024 * 1024; // 16 GB
// See https://github.com/v8/v8/blob/5918a23a3d571b9625e5cce246bdd5b46ff7cd8b/src/heap/heap.cc#L149
const WIN32_MAX_HEAP_SIZE = 700 * 1024 * 1024; // 700 MB
const GENERAL_MAX_HEAP_SIZE = 700 * 2 * 1024 * 1024; // 1400 MB
export const MAX_FILE_SIZE = process.arch === 'ia32' ? WIN32_MAX_FILE_SIZE : GENERAL_MAX_FILE_SIZE;
export const MAX_HEAP_SIZE = process.arch === 'ia32' ? WIN32_MAX_HEAP_SIZE : GENERAL_MAX_HEAP_SIZE;
export enum RimRafMode {
/**
* Slow version that unlinks each file and folder.
*/
UNLINK,
/**
* Fast version that first moves the file/folder
* into a temp directory and then deletes that
* without waiting for it.
*/
MOVE
}
export async function rimraf(path: string, mode = RimRafMode.UNLINK): Promise<void> {
if (isRootOrDriveLetter(path)) {
throw new Error('rimraf - will refuse to recursively delete root');
}
// delete: via unlink
if (mode === RimRafMode.UNLINK) {
return rimrafUnlink(path);
}
// delete: via move
return rimrafMove(path);
}
async function rimrafUnlink(path: string): Promise<void> {
try {
const stat = await lstat(path);
// Folder delete (recursive) - NOT for symbolic links though!
if (stat.isDirectory() && !stat.isSymbolicLink()) {
// Children
const children = await readdir(path);
await Promise.all(children.map(child => rimrafUnlink(join(path, child))));
// Folder
await promisify(fs.rmdir)(path);
}
// Single file delete
else {
// chmod as needed to allow for unlink
const mode = stat.mode;
if (!(mode & 128)) { // 128 === 0200
await chmod(path, mode | 128);
}
return unlink(path);
}
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
}
async function rimrafMove(path: string): Promise<void> {
try {
const pathInTemp = join(os.tmpdir(), generateUuid());
try {
await rename(path, pathInTemp);
} catch (error) {
return rimrafUnlink(path); // if rename fails, delete without tmp dir
}
// Delete but do not return as promise
rimrafUnlink(pathInTemp);
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
}
export function rimrafSync(path: string): void {
if (isRootOrDriveLetter(path)) {
throw new Error('rimraf - will refuse to recursively delete root');
}
try {
const stat = fs.lstatSync(path);
// Folder delete (recursive) - NOT for symbolic links though!
if (stat.isDirectory() && !stat.isSymbolicLink()) {
// Children
const children = readdirSync(path);
children.map(child => rimrafSync(join(path, child)));
// Folder
fs.rmdirSync(path);
}
// Single file delete
else {
// chmod as needed to allow for unlink
const mode = stat.mode;
if (!(mode & 128)) { // 128 === 0200
fs.chmodSync(path, mode | 128);
}
return fs.unlinkSync(path);
}
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
}
export async function readdir(path: string): Promise<string[]> {
return handleDirectoryChildren(await promisify(fs.readdir)(path));
}
export async function readdirWithFileTypes(path: string): Promise<fs.Dirent[]> {
const children = await promisify(fs.readdir)(path, { withFileTypes: true });
// Mac: uses NFD unicode form on disk, but we want NFC
// See also https://github.com/nodejs/node/issues/2165
if (platform.isMacintosh) {
for (const child of children) {
child.name = normalizeNFC(child.name);
}
}
return children;
}
export function readdirSync(path: string): string[] {
return handleDirectoryChildren(fs.readdirSync(path));
}
function handleDirectoryChildren(children: string[]): string[] {
// Mac: uses NFD unicode form on disk, but we want NFC
// See also https://github.com/nodejs/node/issues/2165
if (platform.isMacintosh) {
return children.map(child => normalizeNFC(child));
}
return children;
}
export function exists(path: string): Promise<boolean> {
return promisify(fs.exists)(path);
}
export function chmod(path: string, mode: number): Promise<void> {
return promisify(fs.chmod)(path, mode);
}
export function stat(path: string): Promise<fs.Stats> {
return promisify(fs.stat)(path);
}
export interface IStatAndLink {
// The stats of the file. If the file is a symbolic
// link, the stats will be of that target file and
// not the link itself.
// If the file is a symbolic link pointing to a non
// existing file, the stat will be of the link and
// the `dangling` flag will indicate this.
stat: fs.Stats;
// Will be provided if the resource is a symbolic link
// on disk. Use the `dangling` flag to find out if it
// points to a resource that does not exist on disk.
symbolicLink?: { dangling: boolean };
}
export async function statLink(path: string): Promise<IStatAndLink> {
// First stat the link
let lstats: fs.Stats | undefined;
try {
lstats = await lstat(path);
// Return early if the stat is not a symbolic link at all
if (!lstats.isSymbolicLink()) {
return { stat: lstats };
}
} catch (error) {
/* ignore - use stat() instead */
}
// If the stat is a symbolic link or failed to stat, use fs.stat()
// which for symbolic links will stat the target they point to
try {
const stats = await stat(path);
return { stat: stats, symbolicLink: lstats?.isSymbolicLink() ? { dangling: false } : undefined };
} catch (error) {
// If the link points to a non-existing file we still want
// to return it as result while setting dangling: true flag
if (error.code === 'ENOENT' && lstats) {
return { stat: lstats, symbolicLink: { dangling: true } };
}
throw error;
}
}
export function lstat(path: string): Promise<fs.Stats> {
return promisify(fs.lstat)(path);
}
export function rename(oldPath: string, newPath: string): Promise<void> {
return promisify(fs.rename)(oldPath, newPath);
}
export function renameIgnoreError(oldPath: string, newPath: string): Promise<void> {
return new Promise(resolve => fs.rename(oldPath, newPath, () => resolve()));
}
export function unlink(path: string): Promise<void> {
return promisify(fs.unlink)(path);
}
export function symlink(target: string, path: string, type?: string): Promise<void> {
return promisify(fs.symlink)(target, path, type);
}
export function truncate(path: string, len: number): Promise<void> {
return promisify(fs.truncate)(path, len);
}
export function readFile(path: string): Promise<Buffer>;
export function readFile(path: string, encoding: string): Promise<string>;
export function readFile(path: string, encoding?: string): Promise<Buffer | string> {
return promisify(fs.readFile)(path, encoding);
}
export async function mkdirp(path: string, mode?: number): Promise<void> {
return promisify(fs.mkdir)(path, { mode, recursive: true });
}
// According to node.js docs (https://nodejs.org/docs/v6.5.0/api/fs.html#fs_fs_writefile_file_data_options_callback)
// it is not safe to call writeFile() on the same path multiple times without waiting for the callback to return.
// Therefor we use a Queue on the path that is given to us to sequentialize calls to the same path properly.
const writeFilePathQueues: Map<string, Queue<void>> = new Map();
export function writeFile(path: string, data: string, options?: IWriteFileOptions): Promise<void>;
export function writeFile(path: string, data: Buffer, options?: IWriteFileOptions): Promise<void>;
export function writeFile(path: string, data: Uint8Array, options?: IWriteFileOptions): Promise<void>;
export function writeFile(path: string, data: string | Buffer | Uint8Array, options?: IWriteFileOptions): Promise<void>;
export function writeFile(path: string, data: string | Buffer | Uint8Array, options?: IWriteFileOptions): Promise<void> {
const queueKey = toQueueKey(path);
return ensureWriteFileQueue(queueKey).queue(() => {
const ensuredOptions = ensureWriteOptions(options);
return new Promise((resolve, reject) => doWriteFileAndFlush(path, data, ensuredOptions, error => error ? reject(error) : resolve()));
});
}
function toQueueKey(path: string): string {
let queueKey = path;
if (platform.isWindows || platform.isMacintosh) {
queueKey = queueKey.toLowerCase(); // accommodate for case insensitive file systems
}
return queueKey;
}
function ensureWriteFileQueue(queueKey: string): Queue<void> {
const existingWriteFileQueue = writeFilePathQueues.get(queueKey);
if (existingWriteFileQueue) {
return existingWriteFileQueue;
}
const writeFileQueue = new Queue<void>();
writeFilePathQueues.set(queueKey, writeFileQueue);
const onFinish = Event.once(writeFileQueue.onFinished);
onFinish(() => {
writeFilePathQueues.delete(queueKey);
writeFileQueue.dispose();
});
return writeFileQueue;
}
export interface IWriteFileOptions {
mode?: number;
flag?: string;
}
interface IEnsuredWriteFileOptions extends IWriteFileOptions {
mode: number;
flag: string;
}
let canFlush = true;
// Calls fs.writeFile() followed by a fs.sync() call to flush the changes to disk
// We do this in cases where we want to make sure the data is really on disk and
// not in some cache.
//
// See https://github.com/nodejs/node/blob/v5.10.0/lib/fs.js#L1194
function doWriteFileAndFlush(path: string, data: string | Buffer | Uint8Array, options: IEnsuredWriteFileOptions, callback: (error: Error | null) => void): void {
if (!canFlush) {
return fs.writeFile(path, data, { mode: options.mode, flag: options.flag }, callback);
}
// Open the file with same flags and mode as fs.writeFile()
fs.open(path, options.flag, options.mode, (openError, fd) => {
if (openError) {
return callback(openError);
}
// It is valid to pass a fd handle to fs.writeFile() and this will keep the handle open!
fs.writeFile(fd, data, writeError => {
if (writeError) {
return fs.close(fd, () => callback(writeError)); // still need to close the handle on error!
}
// Flush contents (not metadata) of the file to disk
fs.fdatasync(fd, (syncError: Error | null) => {
// In some exotic setups it is well possible that node fails to sync
// In that case we disable flushing and warn to the console
if (syncError) {
console.warn('[node.js fs] fdatasync is now disabled for this session because it failed: ', syncError);
canFlush = false;
}
return fs.close(fd, closeError => callback(closeError));
});
});
});
}
export function writeFileSync(path: string, data: string | Buffer, options?: IWriteFileOptions): void {
const ensuredOptions = ensureWriteOptions(options);
if (!canFlush) {
return fs.writeFileSync(path, data, { mode: ensuredOptions.mode, flag: ensuredOptions.flag });
}
// Open the file with same flags and mode as fs.writeFile()
const fd = fs.openSync(path, ensuredOptions.flag, ensuredOptions.mode);
try {
// It is valid to pass a fd handle to fs.writeFile() and this will keep the handle open!
fs.writeFileSync(fd, data);
// Flush contents (not metadata) of the file to disk
try {
fs.fdatasyncSync(fd);
} catch (syncError) {
console.warn('[node.js fs] fdatasyncSync is now disabled for this session because it failed: ', syncError);
canFlush = false;
}
} finally {
fs.closeSync(fd);
}
}
function ensureWriteOptions(options?: IWriteFileOptions): IEnsuredWriteFileOptions {
if (!options) {
return { mode: 0o666, flag: 'w' };
}
return {
mode: typeof options.mode === 'number' ? options.mode : 0o666,
flag: typeof options.flag === 'string' ? options.flag : 'w'
};
}
export async function readDirsInDir(dirPath: string): Promise<string[]> {
const children = await readdir(dirPath);
const directories: string[] = [];
for (const child of children) {
if (await dirExists(join(dirPath, child))) {
directories.push(child);
}
}
return directories;
}
export async function dirExists(path: string): Promise<boolean> {
try {
const fileStat = await stat(path);
return fileStat.isDirectory();
} catch (error) {
return false;
}
}
export async function fileExists(path: string): Promise<boolean> {
try {
const fileStat = await stat(path);
return fileStat.isFile();
} catch (error) {
return false;
}
}
export function whenDeleted(path: string): Promise<void> {
// Complete when wait marker file is deleted
return new Promise<void>(resolve => {
let running = false;
const interval = setInterval(() => {
if (!running) {
running = true;
fs.exists(path, exists => {
running = false;
if (!exists) {
clearInterval(interval);
resolve(undefined);
}
});
}
}, 1000);
});
}
export async function move(source: string, target: string): Promise<void> {
if (source === target) {
return Promise.resolve();
}
async function updateMtime(path: string): Promise<void> {
const stat = await lstat(path);
if (stat.isDirectory() || stat.isSymbolicLink()) {
return Promise.resolve(); // only for files
}
const fd = await promisify(fs.open)(path, 'a');
try {
await promisify(fs.futimes)(fd, stat.atime, new Date());
} catch (error) {
//ignore
}
return promisify(fs.close)(fd);
}
try {
await rename(source, target);
await updateMtime(target);
} catch (error) {
// In two cases we fallback to classic copy and delete:
//
// 1.) The EXDEV error indicates that source and target are on different devices
// In this case, fallback to using a copy() operation as there is no way to
// rename() between different devices.
//
// 2.) The user tries to rename a file/folder that ends with a dot. This is not
// really possible to move then, at least on UNC devices.
if (source.toLowerCase() !== target.toLowerCase() && error.code === 'EXDEV' || source.endsWith('.')) {
await copy(source, target);
await rimraf(source, RimRafMode.MOVE);
await updateMtime(target);
} else {
throw error;
}
}
}
export async function copy(source: string, target: string, copiedSourcesIn?: { [path: string]: boolean }): Promise<void> {
const copiedSources = copiedSourcesIn ? copiedSourcesIn : Object.create(null);
const fileStat = await stat(source);
if (!fileStat.isDirectory()) {
return doCopyFile(source, target, fileStat.mode & 511);
}
if (copiedSources[source]) {
return Promise.resolve(); // escape when there are cycles (can happen with symlinks)
}
copiedSources[source] = true; // remember as copied
// Create folder
await mkdirp(target, fileStat.mode & 511);
// Copy each file recursively
const files = await readdir(source);
for (let i = 0; i < files.length; i++) {
const file = files[i];
await copy(join(source, file), join(target, file), copiedSources);
}
}
async function doCopyFile(source: string, target: string, mode: number): Promise<void> {
return new Promise((resolve, reject) => {
const reader = fs.createReadStream(source);
const writer = fs.createWriteStream(target, { mode });
let finished = false;
const finish = (error?: Error) => {
if (!finished) {
finished = true;
// in error cases, pass to callback
if (error) {
return reject(error);
}
// we need to explicitly chmod because of https://github.com/nodejs/node/issues/1104
fs.chmod(target, mode, error => error ? reject(error) : resolve());
}
};
// handle errors properly
reader.once('error', error => finish(error));
writer.once('error', error => finish(error));
// we are done (underlying fd has been closed)
writer.once('close', () => finish());
// start piping
reader.pipe(writer);
});
}

128
src/vs/base/node/ports.ts Normal file
View File

@ -0,0 +1,128 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as net from 'net';
/**
* @returns Returns a random port between 1025 and 65535.
*/
export function randomPort(): number {
const min = 1025;
const max = 65535;
return min + Math.floor((max - min) * Math.random());
}
/**
* Given a start point and a max number of retries, will find a port that
* is openable. Will return 0 in case no free port can be found.
*/
export function findFreePort(startPort: number, giveUpAfter: number, timeout: number): Promise<number> {
let done = false;
return new Promise(resolve => {
const timeoutHandle = setTimeout(() => {
if (!done) {
done = true;
return resolve(0);
}
}, timeout);
doFindFreePort(startPort, giveUpAfter, (port) => {
if (!done) {
done = true;
clearTimeout(timeoutHandle);
return resolve(port);
}
});
});
}
function doFindFreePort(startPort: number, giveUpAfter: number, clb: (port: number) => void): void {
if (giveUpAfter === 0) {
return clb(0);
}
const client = new net.Socket();
// If we can connect to the port it means the port is already taken so we continue searching
client.once('connect', () => {
dispose(client);
return doFindFreePort(startPort + 1, giveUpAfter - 1, clb);
});
client.once('data', () => {
// this listener is required since node.js 8.x
});
client.once('error', (err: Error & { code?: string }) => {
dispose(client);
// If we receive any non ECONNREFUSED error, it means the port is used but we cannot connect
if (err.code !== 'ECONNREFUSED') {
return doFindFreePort(startPort + 1, giveUpAfter - 1, clb);
}
// Otherwise it means the port is free to use!
return clb(startPort);
});
client.connect(startPort, '127.0.0.1');
}
/**
* Uses listen instead of connect. Is faster, but if there is another listener on 0.0.0.0 then this will take 127.0.0.1 from that listener.
*/
export function findFreePortFaster(startPort: number, giveUpAfter: number, timeout: number): Promise<number> {
let resolved: boolean = false;
let timeoutHandle: NodeJS.Timeout | undefined = undefined;
let countTried: number = 1;
const server = net.createServer({ pauseOnConnect: true });
function doResolve(port: number, resolve: (port: number) => void) {
if (!resolved) {
resolved = true;
server.removeAllListeners();
server.close();
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
resolve(port);
}
}
return new Promise<number>(resolve => {
timeoutHandle = setTimeout(() => {
doResolve(0, resolve);
}, timeout);
server.on('listening', () => {
doResolve(startPort, resolve);
});
server.on('error', err => {
if (err && ((<any>err).code === 'EADDRINUSE' || (<any>err).code === 'EACCES') && (countTried < giveUpAfter)) {
startPort++;
countTried++;
server.listen(startPort, '127.0.0.1');
} else {
doResolve(0, resolve);
}
});
server.on('close', () => {
doResolve(0, resolve);
});
server.listen(startPort, '127.0.0.1');
});
}
function dispose(socket: net.Socket): void {
try {
socket.removeAllListeners('connect');
socket.removeAllListeners('error');
socket.end();
socket.destroy();
socket.unref();
} catch (error) {
console.error(error); // otherwise this error would get lost in the callback chain
}
}

View File

@ -0,0 +1,489 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'vs/base/common/path';
import * as fs from 'fs';
import { promisify } from 'util';
import * as cp from 'child_process';
import * as nls from 'vs/nls';
import * as Types from 'vs/base/common/types';
import { IStringDictionary } from 'vs/base/common/collections';
import * as Objects from 'vs/base/common/objects';
import * as extpath from 'vs/base/common/extpath';
import * as Platform from 'vs/base/common/platform';
import { LineDecoder } from 'vs/base/node/decoder';
import { CommandOptions, ForkOptions, SuccessData, Source, TerminateResponse, TerminateResponseCode, Executable } from 'vs/base/common/processes';
import { FileAccess } from 'vs/base/common/network';
export { CommandOptions, ForkOptions, SuccessData, Source, TerminateResponse, TerminateResponseCode };
export type ValueCallback<T> = (value: T | Promise<T>) => void;
export type ErrorCallback = (error?: any) => void;
export type ProgressCallback<T> = (progress: T) => void;
export interface LineData {
line: string;
source: Source;
}
function getWindowsCode(status: number): TerminateResponseCode {
switch (status) {
case 0:
return TerminateResponseCode.Success;
case 1:
return TerminateResponseCode.AccessDenied;
case 128:
return TerminateResponseCode.ProcessNotFound;
default:
return TerminateResponseCode.Unknown;
}
}
function terminateProcess(process: cp.ChildProcess, cwd?: string): Promise<TerminateResponse> {
if (Platform.isWindows) {
try {
const options: any = {
stdio: ['pipe', 'pipe', 'ignore']
};
if (cwd) {
options.cwd = cwd;
}
const killProcess = cp.execFile('taskkill', ['/T', '/F', '/PID', process.pid.toString()], options);
return new Promise((resolve, reject) => {
killProcess.once('error', (err) => {
resolve({ success: false, error: err });
});
killProcess.once('exit', (code, signal) => {
if (code === 0) {
resolve({ success: true });
} else {
resolve({ success: false, code: code !== null ? code : TerminateResponseCode.Unknown });
}
});
});
} catch (err) {
return Promise.resolve({ success: false, error: err, code: err.status ? getWindowsCode(err.status) : TerminateResponseCode.Unknown });
}
} else if (Platform.isLinux || Platform.isMacintosh) {
try {
const cmd = FileAccess.asFileUri('vs/base/node/terminateProcess.sh', require).fsPath;
return new Promise((resolve, reject) => {
cp.execFile(cmd, [process.pid.toString()], { encoding: 'utf8', shell: true } as cp.ExecFileOptions, (err, stdout, stderr) => {
if (err) {
resolve({ success: false, error: err });
} else {
resolve({ success: true });
}
});
});
} catch (err) {
return Promise.resolve({ success: false, error: err });
}
} else {
process.kill('SIGKILL');
}
return Promise.resolve({ success: true });
}
export function getWindowsShell(environment: Platform.IProcessEnvironment = process.env as Platform.IProcessEnvironment): string {
return environment['comspec'] || 'cmd.exe';
}
export abstract class AbstractProcess<TProgressData> {
private cmd: string;
private args: string[];
private options: CommandOptions | ForkOptions;
protected shell: boolean;
private childProcess: cp.ChildProcess | null;
protected childProcessPromise: Promise<cp.ChildProcess> | null;
private pidResolve: ValueCallback<number> | undefined;
protected terminateRequested: boolean;
private static WellKnowCommands: IStringDictionary<boolean> = {
'ant': true,
'cmake': true,
'eslint': true,
'gradle': true,
'grunt': true,
'gulp': true,
'jake': true,
'jenkins': true,
'jshint': true,
'make': true,
'maven': true,
'msbuild': true,
'msc': true,
'nmake': true,
'npm': true,
'rake': true,
'tsc': true,
'xbuild': true
};
public constructor(executable: Executable);
public constructor(cmd: string, args: string[] | undefined, shell: boolean, options: CommandOptions | undefined);
public constructor(arg1: string | Executable, arg2?: string[], arg3?: boolean, arg4?: CommandOptions) {
if (arg2 !== undefined && arg3 !== undefined && arg4 !== undefined) {
this.cmd = <string>arg1;
this.args = arg2;
this.shell = arg3;
this.options = arg4;
} else {
const executable = <Executable>arg1;
this.cmd = executable.command;
this.shell = executable.isShellCommand;
this.args = executable.args.slice(0);
this.options = executable.options || {};
}
this.childProcess = null;
this.childProcessPromise = null;
this.terminateRequested = false;
if (this.options.env) {
const newEnv: IStringDictionary<string> = Object.create(null);
Object.keys(process.env).forEach((key) => {
newEnv[key] = process.env[key]!;
});
Object.keys(this.options.env).forEach((key) => {
newEnv[key] = this.options.env![key]!;
});
this.options.env = newEnv;
}
}
public getSanitizedCommand(): string {
let result = this.cmd.toLowerCase();
const index = result.lastIndexOf(path.sep);
if (index !== -1) {
result = result.substring(index + 1);
}
if (AbstractProcess.WellKnowCommands[result]) {
return result;
}
return 'other';
}
public start(pp: ProgressCallback<TProgressData>): Promise<SuccessData> {
if (Platform.isWindows && ((this.options && this.options.cwd && extpath.isUNC(this.options.cwd)) || !this.options && extpath.isUNC(process.cwd()))) {
return Promise.reject(new Error(nls.localize('TaskRunner.UNC', 'Can\'t execute a shell command on a UNC drive.')));
}
return this.useExec().then((useExec) => {
let cc: ValueCallback<SuccessData>;
let ee: ErrorCallback;
const result = new Promise<any>((c, e) => {
cc = c;
ee = e;
});
if (useExec) {
let cmd: string = this.cmd;
if (this.args) {
cmd = cmd + ' ' + this.args.join(' ');
}
this.childProcess = cp.exec(cmd, this.options, (error, stdout, stderr) => {
this.childProcess = null;
const err: any = error;
// This is tricky since executing a command shell reports error back in case the executed command return an
// error or the command didn't exist at all. So we can't blindly treat an error as a failed command. So we
// always parse the output and report success unless the job got killed.
if (err && err.killed) {
ee({ killed: this.terminateRequested, stdout: stdout.toString(), stderr: stderr.toString() });
} else {
this.handleExec(cc, pp, error, stdout as any, stderr as any);
}
});
} else {
let childProcess: cp.ChildProcess | null = null;
const closeHandler = (data: any) => {
this.childProcess = null;
this.childProcessPromise = null;
this.handleClose(data, cc, pp, ee);
const result: SuccessData = {
terminated: this.terminateRequested
};
if (Types.isNumber(data)) {
result.cmdCode = <number>data;
}
cc(result);
};
if (this.shell && Platform.isWindows) {
const options: any = Objects.deepClone(this.options);
options.windowsVerbatimArguments = true;
options.detached = false;
let quotedCommand: boolean = false;
let quotedArg: boolean = false;
const commandLine: string[] = [];
let quoted = this.ensureQuotes(this.cmd);
commandLine.push(quoted.value);
quotedCommand = quoted.quoted;
if (this.args) {
this.args.forEach((elem) => {
quoted = this.ensureQuotes(elem);
commandLine.push(quoted.value);
quotedArg = quotedArg && quoted.quoted;
});
}
const args: string[] = [
'/s',
'/c',
];
if (quotedCommand) {
if (quotedArg) {
args.push('"' + commandLine.join(' ') + '"');
} else if (commandLine.length > 1) {
args.push('"' + commandLine[0] + '"' + ' ' + commandLine.slice(1).join(' '));
} else {
args.push('"' + commandLine[0] + '"');
}
} else {
args.push(commandLine.join(' '));
}
childProcess = cp.spawn(getWindowsShell(), args, options);
} else {
if (this.cmd) {
childProcess = cp.spawn(this.cmd, this.args, this.options);
}
}
if (childProcess) {
this.childProcess = childProcess;
this.childProcessPromise = Promise.resolve(childProcess);
if (this.pidResolve) {
this.pidResolve(Types.isNumber(childProcess.pid) ? childProcess.pid : -1);
this.pidResolve = undefined;
}
childProcess.on('error', (error: Error) => {
this.childProcess = null;
ee({ terminated: this.terminateRequested, error: error });
});
if (childProcess.pid) {
this.childProcess.on('close', closeHandler);
this.handleSpawn(childProcess, cc!, pp, ee!, true);
}
}
}
return result;
});
}
protected abstract handleExec(cc: ValueCallback<SuccessData>, pp: ProgressCallback<TProgressData>, error: Error | null, stdout: Buffer, stderr: Buffer): void;
protected abstract handleSpawn(childProcess: cp.ChildProcess, cc: ValueCallback<SuccessData>, pp: ProgressCallback<TProgressData>, ee: ErrorCallback, sync: boolean): void;
protected handleClose(data: any, cc: ValueCallback<SuccessData>, pp: ProgressCallback<TProgressData>, ee: ErrorCallback): void {
// Default is to do nothing.
}
private static readonly regexp = /^[^"].* .*[^"]/;
private ensureQuotes(value: string) {
if (AbstractProcess.regexp.test(value)) {
return {
value: '"' + value + '"', //`"${value}"`,
quoted: true
};
} else {
return {
value: value,
quoted: value.length > 0 && value[0] === '"' && value[value.length - 1] === '"'
};
}
}
public get pid(): Promise<number> {
if (this.childProcessPromise) {
return this.childProcessPromise.then(childProcess => childProcess.pid, err => -1);
} else {
return new Promise<number>((resolve) => {
this.pidResolve = resolve;
});
}
}
public terminate(): Promise<TerminateResponse> {
if (!this.childProcessPromise) {
return Promise.resolve<TerminateResponse>({ success: true });
}
return this.childProcessPromise.then((childProcess) => {
this.terminateRequested = true;
return terminateProcess(childProcess, this.options.cwd).then(response => {
if (response.success) {
this.childProcess = null;
}
return response;
});
}, (err) => {
return { success: true };
});
}
private useExec(): Promise<boolean> {
return new Promise<boolean>(resolve => {
if (!this.shell || !Platform.isWindows) {
return resolve(false);
}
const cmdShell = cp.spawn(getWindowsShell(), ['/s', '/c']);
cmdShell.on('error', (error: Error) => {
return resolve(true);
});
cmdShell.on('exit', (data: any) => {
return resolve(false);
});
});
}
}
export class LineProcess extends AbstractProcess<LineData> {
private stdoutLineDecoder: LineDecoder | null;
private stderrLineDecoder: LineDecoder | null;
public constructor(executable: Executable);
public constructor(cmd: string, args: string[], shell: boolean, options: CommandOptions);
public constructor(arg1: string | Executable, arg2?: string[], arg3?: boolean | ForkOptions, arg4?: CommandOptions) {
super(<any>arg1, arg2, <any>arg3, arg4);
this.stdoutLineDecoder = null;
this.stderrLineDecoder = null;
}
protected handleExec(cc: ValueCallback<SuccessData>, pp: ProgressCallback<LineData>, error: Error, stdout: Buffer, stderr: Buffer) {
[stdout, stderr].forEach((buffer: Buffer, index: number) => {
const lineDecoder = new LineDecoder();
const lines = lineDecoder.write(buffer);
lines.forEach((line) => {
pp({ line: line, source: index === 0 ? Source.stdout : Source.stderr });
});
const line = lineDecoder.end();
if (line) {
pp({ line: line, source: index === 0 ? Source.stdout : Source.stderr });
}
});
cc({ terminated: this.terminateRequested, error: error });
}
protected handleSpawn(childProcess: cp.ChildProcess, cc: ValueCallback<SuccessData>, pp: ProgressCallback<LineData>, ee: ErrorCallback, sync: boolean): void {
const stdoutLineDecoder = new LineDecoder();
const stderrLineDecoder = new LineDecoder();
childProcess.stdout!.on('data', (data: Buffer) => {
const lines = stdoutLineDecoder.write(data);
lines.forEach(line => pp({ line: line, source: Source.stdout }));
});
childProcess.stderr!.on('data', (data: Buffer) => {
const lines = stderrLineDecoder.write(data);
lines.forEach(line => pp({ line: line, source: Source.stderr }));
});
this.stdoutLineDecoder = stdoutLineDecoder;
this.stderrLineDecoder = stderrLineDecoder;
}
protected handleClose(data: any, cc: ValueCallback<SuccessData>, pp: ProgressCallback<LineData>, ee: ErrorCallback): void {
const stdoutLine = this.stdoutLineDecoder ? this.stdoutLineDecoder.end() : null;
if (stdoutLine) {
pp({ line: stdoutLine, source: Source.stdout });
}
const stderrLine = this.stderrLineDecoder ? this.stderrLineDecoder.end() : null;
if (stderrLine) {
pp({ line: stderrLine, source: Source.stderr });
}
}
}
export interface IQueuedSender {
send: (msg: any) => void;
}
// Wrapper around process.send() that will queue any messages if the internal node.js
// queue is filled with messages and only continue sending messages when the internal
// queue is free again to consume messages.
// On Windows we always wait for the send() method to return before sending the next message
// to workaround https://github.com/nodejs/node/issues/7657 (IPC can freeze process)
export function createQueuedSender(childProcess: cp.ChildProcess): IQueuedSender {
let msgQueue: string[] = [];
let useQueue = false;
const send = function (msg: any): void {
if (useQueue) {
msgQueue.push(msg); // add to the queue if the process cannot handle more messages
return;
}
const result = childProcess.send(msg, (error: Error | null) => {
if (error) {
console.error(error); // unlikely to happen, best we can do is log this error
}
useQueue = false; // we are good again to send directly without queue
// now send all the messages that we have in our queue and did not send yet
if (msgQueue.length > 0) {
const msgQueueCopy = msgQueue.slice(0);
msgQueue = [];
msgQueueCopy.forEach(entry => send(entry));
}
});
if (!result || Platform.isWindows /* workaround https://github.com/nodejs/node/issues/7657 */) {
useQueue = true;
}
};
return { send };
}
export namespace win32 {
export async function findExecutable(command: string, cwd?: string, paths?: string[]): Promise<string> {
// If we have an absolute path then we take it.
if (path.isAbsolute(command)) {
return command;
}
if (cwd === undefined) {
cwd = process.cwd();
}
const dir = path.dirname(command);
if (dir !== '.') {
// We have a directory and the directory is relative (see above). Make the path absolute
// to the current working directory.
return path.join(cwd, command);
}
if (paths === undefined && Types.isString(process.env.PATH)) {
paths = process.env.PATH.split(path.delimiter);
}
// No PATH environment. Make path absolute to the cwd.
if (paths === undefined || paths.length === 0) {
return path.join(cwd, command);
}
async function fileExists(path: string): Promise<boolean> {
if (await promisify(fs.exists)(path)) {
return !((await promisify(fs.stat)(path)).isDirectory());
}
return false;
}
// We have a simple file name. We get the path variable from the env
// and try to find the executable on the path.
for (let pathEntry of paths) {
// The path entry is absolute.
let fullPath: string;
if (path.isAbsolute(pathEntry)) {
fullPath = path.join(pathEntry, command);
} else {
fullPath = path.join(cwd, pathEntry, command);
}
if (await fileExists(fullPath)) {
return fullPath;
}
let withExtension = fullPath + '.com';
if (await fileExists(withExtension)) {
return withExtension;
}
withExtension = fullPath + '.exe';
if (await fileExists(withExtension)) {
return withExtension;
}
}
return path.join(cwd, command);
}
}

39
src/vs/base/node/ps.sh Executable file
View File

@ -0,0 +1,39 @@
#!/bin/sh
PAGESIZE=`getconf PAGESIZE`;
TOTAL_MEMORY=`cat /proc/meminfo | head -n 1 | awk '{print $2}'`;
# Mimic the output of ps -ax -o pid=,ppid=,pcpu=,pmem=,command=
# Read all numeric subdirectories in /proc
for pid in `cd /proc && ls -d [0-9]*`
do {
if [ -e /proc/$pid/stat ]
then
echo $pid;
# ppid is the word at index 4 in the stat file for the process
awk '{print $4}' /proc/$pid/stat;
# pcpu - calculation will be done later, this is a placeholder value
echo "0.0"
# pmem - ratio of the process's working set size to total memory.
# use the page size to convert to bytes, total memory is in KB
# multiplied by 100 to get percentage, extra 10 to be able to move
# the decimal over by one place
RESIDENT_SET_SIZE=`awk '{print $24}' /proc/$pid/stat`;
PERCENT_MEMORY=$(((1000 * $PAGESIZE * $RESIDENT_SET_SIZE) / ($TOTAL_MEMORY * 1024)));
if [ $PERCENT_MEMORY -lt 10 ]
then
# replace the last character with 0. the last character
echo $PERCENT_MEMORY | sed 's/.$/0.&/'; #pmem
else
# insert . before the last character
echo $PERCENT_MEMORY | sed 's/.$/.&/';
fi
# cmdline
xargs -0 < /proc/$pid/cmdline;
fi
} | tr "\n" "\t"; # Replace newlines with tab so that all info for a process is shown on one line
echo; # But add new lines between processes
done

259
src/vs/base/node/ps.ts Normal file
View File

@ -0,0 +1,259 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { exec } from 'child_process';
import { ProcessItem } from 'vs/base/common/processes';
import { FileAccess } from 'vs/base/common/network';
export function listProcesses(rootPid: number): Promise<ProcessItem> {
return new Promise((resolve, reject) => {
let rootItem: ProcessItem | undefined;
const map = new Map<number, ProcessItem>();
function addToTree(pid: number, ppid: number, cmd: string, load: number, mem: number) {
const parent = map.get(ppid);
if (pid === rootPid || parent) {
const item: ProcessItem = {
name: findName(cmd),
cmd,
pid,
ppid,
load,
mem
};
map.set(pid, item);
if (pid === rootPid) {
rootItem = item;
}
if (parent) {
if (!parent.children) {
parent.children = [];
}
parent.children.push(item);
if (parent.children.length > 1) {
parent.children = parent.children.sort((a, b) => a.pid - b.pid);
}
}
}
}
function findName(cmd: string): string {
const SHARED_PROCESS_HINT = /--disable-blink-features=Auxclick/;
const WINDOWS_WATCHER_HINT = /\\watcher\\win32\\CodeHelper\.exe/;
const WINDOWS_CRASH_REPORTER = /--crashes-directory/;
const WINDOWS_PTY = /\\pipe\\winpty-control/;
const WINDOWS_CONSOLE_HOST = /conhost\.exe/;
const TYPE = /--type=([a-zA-Z-]+)/;
// find windows file watcher
if (WINDOWS_WATCHER_HINT.exec(cmd)) {
return 'watcherService ';
}
// find windows crash reporter
if (WINDOWS_CRASH_REPORTER.exec(cmd)) {
return 'electron-crash-reporter';
}
// find windows pty process
if (WINDOWS_PTY.exec(cmd)) {
return 'winpty-process';
}
//find windows console host process
if (WINDOWS_CONSOLE_HOST.exec(cmd)) {
return 'console-window-host (Windows internal process)';
}
// find "--type=xxxx"
let matches = TYPE.exec(cmd);
if (matches && matches.length === 2) {
if (matches[1] === 'renderer') {
if (SHARED_PROCESS_HINT.exec(cmd)) {
return 'shared-process';
}
return `window`;
}
return matches[1];
}
// find all xxxx.js
const JS = /[a-zA-Z-]+\.js/g;
let result = '';
do {
matches = JS.exec(cmd);
if (matches) {
result += matches + ' ';
}
} while (matches);
if (result) {
if (cmd.indexOf('node ') < 0 && cmd.indexOf('node.exe') < 0) {
return `electron_node ${result}`;
}
}
return cmd;
}
if (process.platform === 'win32') {
const cleanUNCPrefix = (value: string): string => {
if (value.indexOf('\\\\?\\') === 0) {
return value.substr(4);
} else if (value.indexOf('\\??\\') === 0) {
return value.substr(4);
} else if (value.indexOf('"\\\\?\\') === 0) {
return '"' + value.substr(5);
} else if (value.indexOf('"\\??\\') === 0) {
return '"' + value.substr(5);
} else {
return value;
}
};
(import('windows-process-tree')).then(windowsProcessTree => {
windowsProcessTree.getProcessList(rootPid, (processList) => {
windowsProcessTree.getProcessCpuUsage(processList, (completeProcessList) => {
const processItems: Map<number, ProcessItem> = new Map();
completeProcessList.forEach(process => {
const commandLine = cleanUNCPrefix(process.commandLine || '');
processItems.set(process.pid, {
name: findName(commandLine),
cmd: commandLine,
pid: process.pid,
ppid: process.ppid,
load: process.cpu || 0,
mem: process.memory || 0
});
});
rootItem = processItems.get(rootPid);
if (rootItem) {
processItems.forEach(item => {
const parent = processItems.get(item.ppid);
if (parent) {
if (!parent.children) {
parent.children = [];
}
parent.children.push(item);
}
});
processItems.forEach(item => {
if (item.children) {
item.children = item.children.sort((a, b) => a.pid - b.pid);
}
});
resolve(rootItem);
} else {
reject(new Error(`Root process ${rootPid} not found`));
}
});
}, windowsProcessTree.ProcessDataFlag.CommandLine | windowsProcessTree.ProcessDataFlag.Memory);
});
} else { // OS X & Linux
function calculateLinuxCpuUsage() {
// Flatten rootItem to get a list of all VSCode processes
let processes = [rootItem];
const pids: number[] = [];
while (processes.length) {
const process = processes.shift();
if (process) {
pids.push(process.pid);
if (process.children) {
processes = processes.concat(process.children);
}
}
}
// The cpu usage value reported on Linux is the average over the process lifetime,
// recalculate the usage over a one second interval
// JSON.stringify is needed to escape spaces, https://github.com/nodejs/node/issues/6803
let cmd = JSON.stringify(FileAccess.asFileUri('vs/base/node/cpuUsage.sh', require).fsPath);
cmd += ' ' + pids.join(' ');
exec(cmd, {}, (err, stdout, stderr) => {
if (err || stderr) {
reject(err || new Error(stderr.toString()));
} else {
const cpuUsage = stdout.toString().split('\n');
for (let i = 0; i < pids.length; i++) {
const processInfo = map.get(pids[i])!;
processInfo.load = parseFloat(cpuUsage[i]);
}
if (!rootItem) {
reject(new Error(`Root process ${rootPid} not found`));
return;
}
resolve(rootItem);
}
});
}
exec('which ps', {}, (err, stdout, stderr) => {
if (err || stderr) {
if (process.platform !== 'linux') {
reject(err || new Error(stderr.toString()));
} else {
const cmd = JSON.stringify(FileAccess.asFileUri('vs/base/node/ps.sh', require).fsPath);
exec(cmd, {}, (err, stdout, stderr) => {
if (err || stderr) {
reject(err || new Error(stderr.toString()));
} else {
parsePsOutput(stdout, addToTree);
calculateLinuxCpuUsage();
}
});
}
} else {
const ps = stdout.toString().trim();
const args = '-ax -o pid=,ppid=,pcpu=,pmem=,command=';
// Set numeric locale to ensure '.' is used as the decimal separator
exec(`${ps} ${args}`, { maxBuffer: 1000 * 1024, env: { LC_NUMERIC: 'en_US.UTF-8' } }, (err, stdout, stderr) => {
// Silently ignoring the screen size is bogus error. See https://github.com/microsoft/vscode/issues/98590
if (err || (stderr && !stderr.includes('screen size is bogus'))) {
reject(err || new Error(stderr.toString()));
} else {
parsePsOutput(stdout, addToTree);
if (process.platform === 'linux') {
calculateLinuxCpuUsage();
} else {
if (!rootItem) {
reject(new Error(`Root process ${rootPid} not found`));
} else {
resolve(rootItem);
}
}
}
});
}
});
}
});
}
function parsePsOutput(stdout: string, addToTree: (pid: number, ppid: number, cmd: string, load: number, mem: number) => void): void {
const PID_CMD = /^\s*([0-9]+)\s+([0-9]+)\s+([0-9]+\.[0-9]+)\s+([0-9]+\.[0-9]+)\s+(.+)$/;
const lines = stdout.toString().split('\n');
for (const line of lines) {
const matches = PID_CMD.exec(line.trim());
if (matches && matches.length === 6) {
addToTree(parseInt(matches[1]), parseInt(matches[2]), matches[5], parseFloat(matches[3]), parseFloat(matches[4]));
}
}
}

View File

@ -0,0 +1,101 @@
/*---------------------------------------------------------------------------------------------
* 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 { isWindows } from 'vs/base/common/platform';
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<string> {
let rawEncodingPromise: Promise<string | undefined>;
// 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 (isWindows) {
rawEncodingPromise = new Promise<string | undefined>(resolve => {
if (verbose) {
console.log('Running "chcp" to detect terminal encoding...');
}
exec('chcp', (err, stdout, stderr) => {
if (stdout) {
if (verbose) {
console.log(`Output from "chcp" command is: ${stdout}`);
}
const windowsTerminalEncodingKeys = Object.keys(windowsTerminalEncodings) as Array<keyof typeof windowsTerminalEncodings>;
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<string>(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);
}

View File

@ -0,0 +1,12 @@
#!/bin/bash
terminateTree() {
for cpid in $(/usr/bin/pgrep -P $1); do
terminateTree $cpid
done
kill -9 $1 > /dev/null 2>&1
}
for pid in $*; do
terminateTree $pid
done

192
src/vs/base/node/watcher.ts Normal file
View File

@ -0,0 +1,192 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { join, basename } from 'vs/base/common/path';
import { watch } from 'fs';
import { isMacintosh } from 'vs/base/common/platform';
import { normalizeNFC } from 'vs/base/common/normalization';
import { toDisposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { exists, readdir } from 'vs/base/node/pfs';
export function watchFile(path: string, onChange: (type: 'added' | 'changed' | 'deleted', path: string) => void, onError: (error: string) => void): IDisposable {
return doWatchNonRecursive({ path, isDirectory: false }, onChange, onError);
}
export function watchFolder(path: string, onChange: (type: 'added' | 'changed' | 'deleted', path: string) => void, onError: (error: string) => void): IDisposable {
return doWatchNonRecursive({ path, isDirectory: true }, onChange, onError);
}
export const CHANGE_BUFFER_DELAY = 100;
function doWatchNonRecursive(file: { path: string, isDirectory: boolean }, onChange: (type: 'added' | 'changed' | 'deleted', path: string) => void, onError: (error: string) => void): IDisposable {
const originalFileName = basename(file.path);
const mapPathToStatDisposable = new Map<string, IDisposable>();
let disposed = false;
let watcherDisposables: IDisposable[] = [toDisposable(() => {
mapPathToStatDisposable.forEach(disposable => dispose(disposable));
mapPathToStatDisposable.clear();
})];
try {
// Creating watcher can fail with an exception
const watcher = watch(file.path);
watcherDisposables.push(toDisposable(() => {
watcher.removeAllListeners();
watcher.close();
}));
// Folder: resolve children to emit proper events
const folderChildren: Set<string> = new Set<string>();
if (file.isDirectory) {
readdir(file.path).then(children => children.forEach(child => folderChildren.add(child)));
}
watcher.on('error', (code: number, signal: string) => {
if (!disposed) {
onError(`Failed to watch ${file.path} for changes using fs.watch() (${code}, ${signal})`);
}
});
watcher.on('change', (type, raw) => {
if (disposed) {
return; // ignore if already disposed
}
// Normalize file name
let changedFileName: string = '';
if (raw) { // https://github.com/microsoft/vscode/issues/38191
changedFileName = raw.toString();
if (isMacintosh) {
// Mac: uses NFD unicode form on disk, but we want NFC
// See also https://github.com/nodejs/node/issues/2165
changedFileName = normalizeNFC(changedFileName);
}
}
if (!changedFileName || (type !== 'change' && type !== 'rename')) {
return; // ignore unexpected events
}
// File path: use path directly for files and join with changed file name otherwise
const changedFilePath = file.isDirectory ? join(file.path, changedFileName) : file.path;
// File
if (!file.isDirectory) {
if (type === 'rename' || changedFileName !== originalFileName) {
// The file was either deleted or renamed. Many tools apply changes to files in an
// atomic way ("Atomic Save") by first renaming the file to a temporary name and then
// renaming it back to the original name. Our watcher will detect this as a rename
// and then stops to work on Mac and Linux because the watcher is applied to the
// inode and not the name. The fix is to detect this case and trying to watch the file
// again after a certain delay.
// In addition, we send out a delete event if after a timeout we detect that the file
// does indeed not exist anymore.
const timeoutHandle = setTimeout(async () => {
const fileExists = await exists(changedFilePath);
if (disposed) {
return; // ignore if disposed by now
}
// File still exists, so emit as change event and reapply the watcher
if (fileExists) {
onChange('changed', changedFilePath);
watcherDisposables = [doWatchNonRecursive(file, onChange, onError)];
}
// File seems to be really gone, so emit a deleted event
else {
onChange('deleted', changedFilePath);
}
}, CHANGE_BUFFER_DELAY);
// Very important to dispose the watcher which now points to a stale inode
// and wire in a new disposable that tracks our timeout that is installed
dispose(watcherDisposables);
watcherDisposables = [toDisposable(() => clearTimeout(timeoutHandle))];
} else {
onChange('changed', changedFilePath);
}
}
// Folder
else {
// Children add/delete
if (type === 'rename') {
// Cancel any previous stats for this file path if existing
const statDisposable = mapPathToStatDisposable.get(changedFilePath);
if (statDisposable) {
dispose(statDisposable);
}
// Wait a bit and try see if the file still exists on disk to decide on the resulting event
const timeoutHandle = setTimeout(async () => {
mapPathToStatDisposable.delete(changedFilePath);
const fileExists = await exists(changedFilePath);
if (disposed) {
return; // ignore if disposed by now
}
// Figure out the correct event type:
// File Exists: either 'added' or 'changed' if known before
// File Does not Exist: always 'deleted'
let type: 'added' | 'deleted' | 'changed';
if (fileExists) {
if (folderChildren.has(changedFileName)) {
type = 'changed';
} else {
type = 'added';
folderChildren.add(changedFileName);
}
} else {
folderChildren.delete(changedFileName);
type = 'deleted';
}
onChange(type, changedFilePath);
}, CHANGE_BUFFER_DELAY);
mapPathToStatDisposable.set(changedFilePath, toDisposable(() => clearTimeout(timeoutHandle)));
}
// Other events
else {
// Figure out the correct event type: if this is the
// first time we see this child, it can only be added
let type: 'added' | 'changed';
if (folderChildren.has(changedFileName)) {
type = 'changed';
} else {
type = 'added';
folderChildren.add(changedFileName);
}
onChange(type, changedFilePath);
}
}
});
} catch (error) {
exists(file.path).then(exists => {
if (exists && !disposed) {
onError(`Failed to watch ${file.path} for changes using fs.watch() (${error.toString()})`);
}
});
}
return toDisposable(() => {
disposed = true;
watcherDisposables = dispose(watcherDisposables);
});
}

250
src/vs/base/node/zip.ts Normal file
View File

@ -0,0 +1,250 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as path from 'vs/base/common/path';
import { createWriteStream, WriteStream } from 'fs';
import { Readable } from 'stream';
import { Sequencer, createCancelablePromise } from 'vs/base/common/async';
import { mkdirp, rimraf } from 'vs/base/node/pfs';
import { open as _openZip, Entry, ZipFile } from 'yauzl';
import * as yazl from 'yazl';
import { CancellationToken } from 'vs/base/common/cancellation';
import { assertIsDefined } from 'vs/base/common/types';
export interface IExtractOptions {
overwrite?: boolean;
/**
* Source path within the ZIP archive. Only the files contained in this
* path will be extracted.
*/
sourcePath?: string;
}
interface IOptions {
sourcePathRegex: RegExp;
}
export type ExtractErrorType = 'CorruptZip' | 'Incomplete';
export class ExtractError extends Error {
readonly type?: ExtractErrorType;
readonly cause: Error;
constructor(type: ExtractErrorType | undefined, cause: Error) {
let message = cause.message;
switch (type) {
case 'CorruptZip': message = `Corrupt ZIP: ${message}`; break;
}
super(message);
this.type = type;
this.cause = cause;
}
}
function modeFromEntry(entry: Entry) {
const attr = entry.externalFileAttributes >> 16 || 33188;
return [448 /* S_IRWXU */, 56 /* S_IRWXG */, 7 /* S_IRWXO */]
.map(mask => attr & mask)
.reduce((a, b) => a + b, attr & 61440 /* S_IFMT */);
}
function toExtractError(err: Error): ExtractError {
if (err instanceof ExtractError) {
return err;
}
let type: ExtractErrorType | undefined = undefined;
if (/end of central directory record signature not found/.test(err.message)) {
type = 'CorruptZip';
}
return new ExtractError(type, err);
}
function extractEntry(stream: Readable, fileName: string, mode: number, targetPath: string, options: IOptions, token: CancellationToken): Promise<void> {
const dirName = path.dirname(fileName);
const targetDirName = path.join(targetPath, dirName);
if (!targetDirName.startsWith(targetPath)) {
return Promise.reject(new Error(nls.localize('invalid file', "Error extracting {0}. Invalid file.", fileName)));
}
const targetFileName = path.join(targetPath, fileName);
let istream: WriteStream;
token.onCancellationRequested(() => {
if (istream) {
istream.destroy();
}
});
return Promise.resolve(mkdirp(targetDirName)).then(() => new Promise<void>((c, e) => {
if (token.isCancellationRequested) {
return;
}
try {
istream = createWriteStream(targetFileName, { mode });
istream.once('close', () => c());
istream.once('error', e);
stream.once('error', e);
stream.pipe(istream);
} catch (error) {
e(error);
}
}));
}
function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, token: CancellationToken): Promise<void> {
let last = createCancelablePromise<void>(() => Promise.resolve());
let extractedEntriesCount = 0;
token.onCancellationRequested(() => {
last.cancel();
zipfile.close();
});
return new Promise((c, e) => {
const throttler = new Sequencer();
const readNextEntry = (token: CancellationToken) => {
if (token.isCancellationRequested) {
return;
}
extractedEntriesCount++;
zipfile.readEntry();
};
zipfile.once('error', e);
zipfile.once('close', () => last.then(() => {
if (token.isCancellationRequested || zipfile.entryCount === extractedEntriesCount) {
c();
} else {
e(new ExtractError('Incomplete', new Error(nls.localize('incompleteExtract', "Incomplete. Found {0} of {1} entries", extractedEntriesCount, zipfile.entryCount))));
}
}, e));
zipfile.readEntry();
zipfile.on('entry', (entry: Entry) => {
if (token.isCancellationRequested) {
return;
}
if (!options.sourcePathRegex.test(entry.fileName)) {
readNextEntry(token);
return;
}
const fileName = entry.fileName.replace(options.sourcePathRegex, '');
// directory file names end with '/'
if (/\/$/.test(fileName)) {
const targetFileName = path.join(targetPath, fileName);
last = createCancelablePromise(token => mkdirp(targetFileName).then(() => readNextEntry(token)).then(undefined, e));
return;
}
const stream = openZipStream(zipfile, entry);
const mode = modeFromEntry(entry);
last = createCancelablePromise(token => throttler.queue(() => stream.then(stream => extractEntry(stream, fileName, mode, targetPath, options, token).then(() => readNextEntry(token)))).then(null, e));
});
});
}
function openZip(zipFile: string, lazy: boolean = false): Promise<ZipFile> {
return new Promise<ZipFile>((resolve, reject) => {
_openZip(zipFile, lazy ? { lazyEntries: true } : undefined!, (error?: Error, zipfile?: ZipFile) => {
if (error) {
reject(toExtractError(error));
} else {
resolve(assertIsDefined(zipfile));
}
});
});
}
function openZipStream(zipFile: ZipFile, entry: Entry): Promise<Readable> {
return new Promise<Readable>((resolve, reject) => {
zipFile.openReadStream(entry, (error?: Error, stream?: Readable) => {
if (error) {
reject(toExtractError(error));
} else {
resolve(assertIsDefined(stream));
}
});
});
}
export interface IFile {
path: string;
contents?: Buffer | string;
localPath?: string;
}
export function zip(zipPath: string, files: IFile[]): Promise<string> {
return new Promise<string>((c, e) => {
const zip = new yazl.ZipFile();
files.forEach(f => {
if (f.contents) {
zip.addBuffer(typeof f.contents === 'string' ? Buffer.from(f.contents, 'utf8') : f.contents, f.path);
} else if (f.localPath) {
zip.addFile(f.localPath, f.path);
}
});
zip.end();
const zipStream = createWriteStream(zipPath);
zip.outputStream.pipe(zipStream);
zip.outputStream.once('error', e);
zipStream.once('error', e);
zipStream.once('finish', () => c(zipPath));
});
}
export function extract(zipPath: string, targetPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise<void> {
const sourcePathRegex = new RegExp(options.sourcePath ? `^${options.sourcePath}` : '');
let promise = openZip(zipPath, true);
if (options.overwrite) {
promise = promise.then(zipfile => rimraf(targetPath).then(() => zipfile));
}
return promise.then(zipfile => extractZip(zipfile, targetPath, { sourcePathRegex }, token));
}
function read(zipPath: string, filePath: string): Promise<Readable> {
return openZip(zipPath).then(zipfile => {
return new Promise<Readable>((c, e) => {
zipfile.on('entry', (entry: Entry) => {
if (entry.fileName === filePath) {
openZipStream(zipfile, entry).then(stream => c(stream), err => e(err));
}
});
zipfile.once('close', () => e(new Error(nls.localize('notFound', "{0} not found inside zip.", filePath))));
});
});
}
export function buffer(zipPath: string, filePath: string): Promise<Buffer> {
return read(zipPath, filePath).then(stream => {
return new Promise<Buffer>((c, e) => {
const buffers: Buffer[] = [];
stream.once('error', e);
stream.on('data', (b: Buffer) => buffers.push(b));
stream.on('end', () => c(Buffer.concat(buffers)));
});
});
}