mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-01 09:35:41 -05:00
Merge from vscode 2b0b9136329c181a9e381463a1f7dc3a2d105a34 (#4880)
This commit is contained in:
@@ -4,13 +4,13 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { dirname, basename } from 'vs/base/common/path';
|
||||
import { dirname } from 'vs/base/common/path';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import * as json from 'vs/base/common/json';
|
||||
import * as extfs from 'vs/base/node/extfs';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { readlink, statLink } from 'vs/base/node/pfs';
|
||||
import { watchFolder, watchFile } from 'vs/base/node/watcher';
|
||||
|
||||
export interface IConfigurationChangeEvent<T> {
|
||||
config: T;
|
||||
@@ -48,11 +48,9 @@ export class ConfigWatcher<T> implements IConfigWatcher<T>, IDisposable {
|
||||
private timeoutHandle: NodeJS.Timer | null;
|
||||
private disposables: IDisposable[];
|
||||
private readonly _onDidUpdateConfiguration: Emitter<IConfigurationChangeEvent<T>>;
|
||||
private configName: string;
|
||||
|
||||
constructor(private _path: string, private options: IConfigOptions<T> = { defaultConfig: Object.create(null), onError: error => console.error(error) }) {
|
||||
this.disposables = [];
|
||||
this.configName = basename(this._path);
|
||||
|
||||
this._onDidUpdateConfiguration = new Emitter<IConfigurationChangeEvent<T>>();
|
||||
this.disposables.push(this._onDidUpdateConfiguration);
|
||||
@@ -61,15 +59,15 @@ export class ConfigWatcher<T> implements IConfigWatcher<T>, IDisposable {
|
||||
this.initAsync();
|
||||
}
|
||||
|
||||
public get path(): string {
|
||||
get path(): string {
|
||||
return this._path;
|
||||
}
|
||||
|
||||
public get hasParseErrors(): boolean {
|
||||
get hasParseErrors(): boolean {
|
||||
return this.parseErrors && this.parseErrors.length > 0;
|
||||
}
|
||||
|
||||
public get onDidUpdateConfiguration(): Event<IConfigurationChangeEvent<T>> {
|
||||
get onDidUpdateConfiguration(): Event<IConfigurationChangeEvent<T>> {
|
||||
return this._onDidUpdateConfiguration.event;
|
||||
}
|
||||
|
||||
@@ -126,50 +124,31 @@ export class ConfigWatcher<T> implements IConfigWatcher<T>, IDisposable {
|
||||
this.watch(parentFolder, true);
|
||||
|
||||
// Check if the path is a symlink and watch its target if so
|
||||
fs.lstat(this._path, (err, stat) => {
|
||||
if (err || stat.isDirectory()) {
|
||||
return; // path is not a valid file
|
||||
}
|
||||
|
||||
// We found a symlink
|
||||
if (stat.isSymbolicLink()) {
|
||||
fs.readlink(this._path, (err, realPath) => {
|
||||
if (err) {
|
||||
return; // path is not a valid symlink
|
||||
}
|
||||
|
||||
this.watch(realPath, false);
|
||||
});
|
||||
}
|
||||
});
|
||||
this.handleSymbolicLink().then(undefined, error => { /* ignore error */ });
|
||||
}
|
||||
|
||||
private watch(path: string, isParentFolder: boolean): void {
|
||||
private async handleSymbolicLink(): Promise<void> {
|
||||
const { stat, isSymbolicLink } = await statLink(this._path);
|
||||
if (isSymbolicLink && !stat.isDirectory()) {
|
||||
const realPath = await readlink(this._path);
|
||||
|
||||
this.watch(realPath, false);
|
||||
}
|
||||
}
|
||||
|
||||
private watch(path: string, isFolder: boolean): void {
|
||||
if (this.disposed) {
|
||||
return; // avoid watchers that will never get disposed by checking for being disposed
|
||||
}
|
||||
|
||||
this.disposables.push(extfs.watch(path,
|
||||
(type, file) => this.onConfigFileChange(type, file, isParentFolder),
|
||||
(error: string) => this.options.onError(error)
|
||||
));
|
||||
if (isFolder) {
|
||||
this.disposables.push(watchFolder(path, (type, path) => path === this._path ? this.onConfigFileChange() : undefined, error => this.options.onError(error)));
|
||||
} else {
|
||||
this.disposables.push(watchFile(path, (type, path) => this.onConfigFileChange(), error => this.options.onError(error)));
|
||||
}
|
||||
}
|
||||
|
||||
private onConfigFileChange(eventType: string, filename: string | undefined, isParentFolder: boolean): void {
|
||||
if (isParentFolder) {
|
||||
|
||||
// Windows: in some cases the filename contains artifacts from the absolute path
|
||||
// see https://github.com/nodejs/node/issues/19170
|
||||
// As such, we have to ensure that the filename basename is used for comparison.
|
||||
if (isWindows && filename && filename !== this.configName) {
|
||||
filename = basename(filename);
|
||||
}
|
||||
|
||||
if (filename !== this.configName) {
|
||||
return; // a change to a sibling file that is not our config file
|
||||
}
|
||||
}
|
||||
|
||||
private onConfigFileChange(): void {
|
||||
if (this.timeoutHandle) {
|
||||
global.clearTimeout(this.timeoutHandle);
|
||||
this.timeoutHandle = null;
|
||||
@@ -179,7 +158,7 @@ export class ConfigWatcher<T> implements IConfigWatcher<T>, IDisposable {
|
||||
this.timeoutHandle = global.setTimeout(() => this.reload(), this.options.changeBufferDelay || 0);
|
||||
}
|
||||
|
||||
public reload(callback?: (config: T) => void): void {
|
||||
reload(callback?: (config: T) => void): void {
|
||||
this.loadAsync(currentConfig => {
|
||||
if (!objects.equals(currentConfig, this.cache)) {
|
||||
this.updateCache(currentConfig);
|
||||
@@ -193,7 +172,7 @@ export class ConfigWatcher<T> implements IConfigWatcher<T>, IDisposable {
|
||||
});
|
||||
}
|
||||
|
||||
public getConfig(): T {
|
||||
getConfig(): T {
|
||||
this.ensureLoaded();
|
||||
|
||||
return this.cache;
|
||||
@@ -205,7 +184,7 @@ export class ConfigWatcher<T> implements IConfigWatcher<T>, IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
dispose(): void {
|
||||
this.disposed = true;
|
||||
this.disposables = dispose(this.disposables);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ for PID in "$@"; do
|
||||
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" | bc -l`
|
||||
CPU_USAGE=`echo "$((100*$PROCESS_DELTA/$TOTAL_DELTA))"`
|
||||
|
||||
# Parent script reads from stdout, so echo result to be read
|
||||
echo $CPU_USAGE
|
||||
|
||||
@@ -23,7 +23,7 @@ export class LineDecoder {
|
||||
this.remaining = null;
|
||||
}
|
||||
|
||||
public write(buffer: Buffer): string[] {
|
||||
write(buffer: Buffer): string[] {
|
||||
const result: string[] = [];
|
||||
const value = this.remaining
|
||||
? this.remaining + this.stringDecoder.write(buffer)
|
||||
@@ -56,7 +56,7 @@ export class LineDecoder {
|
||||
return result;
|
||||
}
|
||||
|
||||
public end(): string | null {
|
||||
end(): string | null {
|
||||
return this.remaining;
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export function toDecodeStream(readable: Readable, options: IDecodeStreamOptions
|
||||
readable.pipe(new class extends Writable {
|
||||
|
||||
private _decodeStream: NodeJS.ReadWriteStream;
|
||||
private _decodeStreamConstruction: Promise<any>;
|
||||
private _decodeStreamConstruction: Promise<void>;
|
||||
private _buffer: Buffer[] = [];
|
||||
private _bytesBuffered = 0;
|
||||
|
||||
|
||||
@@ -1,707 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as paths from 'vs/base/common/path';
|
||||
import { nfcall } from 'vs/base/common/async';
|
||||
import { normalizeNFC } from 'vs/base/common/normalization';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import * as uuid from 'vs/base/common/uuid';
|
||||
import { encode, encodeStream } from 'vs/base/node/encoding';
|
||||
import * as flow from 'vs/base/node/flow';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
const loop = flow.loop;
|
||||
|
||||
export function readdirSync(path: 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 fs.readdirSync(path).map(c => normalizeNFC(c));
|
||||
}
|
||||
|
||||
return fs.readdirSync(path);
|
||||
}
|
||||
|
||||
export function readdir(path: string, callback: (error: Error | null, files: string[]) => void): void {
|
||||
// Mac: uses NFD unicode form on disk, but we want NFC
|
||||
// See also https://github.com/nodejs/node/issues/2165
|
||||
if (platform.isMacintosh) {
|
||||
return fs.readdir(path, (error, children) => {
|
||||
if (error) {
|
||||
return callback(error, []);
|
||||
}
|
||||
|
||||
return callback(null, children.map(c => normalizeNFC(c)));
|
||||
});
|
||||
}
|
||||
|
||||
return fs.readdir(path, callback);
|
||||
}
|
||||
|
||||
export interface IStatAndLink {
|
||||
stat: fs.Stats;
|
||||
isSymbolicLink: boolean;
|
||||
}
|
||||
|
||||
export function statLink(path: string, callback: (error: Error | null, statAndIsLink: IStatAndLink | null) => void): void {
|
||||
fs.lstat(path, (error, lstat) => {
|
||||
if (error || lstat.isSymbolicLink()) {
|
||||
fs.stat(path, (error, stat) => {
|
||||
if (error) {
|
||||
return callback(error, null);
|
||||
}
|
||||
|
||||
callback(null, { stat, isSymbolicLink: lstat && lstat.isSymbolicLink() });
|
||||
});
|
||||
} else {
|
||||
callback(null, { stat: lstat, isSymbolicLink: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function copy(source: string, target: string, callback: (error: Error | null) => void, copiedSourcesIn?: { [path: string]: boolean }): void {
|
||||
const copiedSources = copiedSourcesIn ? copiedSourcesIn : Object.create(null);
|
||||
|
||||
fs.stat(source, (error, stat) => {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if (!stat.isDirectory()) {
|
||||
return doCopyFile(source, target, stat.mode & 511, callback);
|
||||
}
|
||||
|
||||
if (copiedSources[source]) {
|
||||
return callback(null); // escape when there are cycles (can happen with symlinks)
|
||||
}
|
||||
|
||||
copiedSources[source] = true; // remember as copied
|
||||
|
||||
const proceed = function () {
|
||||
readdir(source, (err, files) => {
|
||||
loop(files, (file: string, clb: (error: Error | null, result: string[]) => void) => {
|
||||
copy(paths.join(source, file), paths.join(target, file), (error: Error) => clb(error, []), copiedSources);
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
mkdirp(target, stat.mode & 511).then(proceed, proceed);
|
||||
});
|
||||
}
|
||||
|
||||
function doCopyFile(source: string, target: string, mode: number, callback: (error: Error) => void): void {
|
||||
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) {
|
||||
callback(error);
|
||||
}
|
||||
|
||||
// we need to explicitly chmod because of https://github.com/nodejs/node/issues/1104
|
||||
else {
|
||||
fs.chmod(target, mode, callback);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
export function mkdirp(path: string, mode?: number, token?: CancellationToken): Promise<boolean> {
|
||||
const mkdir = (): Promise<null> => {
|
||||
return nfcall(fs.mkdir, path, mode).then(undefined, (mkdirErr: NodeJS.ErrnoException) => {
|
||||
|
||||
// ENOENT: a parent folder does not exist yet
|
||||
if (mkdirErr.code === 'ENOENT') {
|
||||
return Promise.reject(mkdirErr);
|
||||
}
|
||||
|
||||
// Any other error: check if folder exists and
|
||||
// return normally in that case if its a folder
|
||||
return nfcall(fs.stat, path).then((stat: fs.Stats) => {
|
||||
if (!stat.isDirectory()) {
|
||||
return Promise.reject(new Error(`'${path}' exists and is not a directory.`));
|
||||
}
|
||||
|
||||
return null;
|
||||
}, statErr => {
|
||||
return Promise.reject(mkdirErr); // bubble up original mkdir error
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// stop at root
|
||||
if (path === paths.dirname(path)) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// recursively mkdir
|
||||
return mkdir().then(undefined, (err: NodeJS.ErrnoException) => {
|
||||
|
||||
// Respect cancellation
|
||||
if (token && token.isCancellationRequested) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
// ENOENT: a parent folder does not exist yet, continue
|
||||
// to create the parent folder and then try again.
|
||||
if (err.code === 'ENOENT') {
|
||||
return mkdirp(paths.dirname(path), mode).then(mkdir);
|
||||
}
|
||||
|
||||
// Any other error
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
// Deletes the given path by first moving it out of the workspace. This has two benefits. For one, the operation can return fast because
|
||||
// after the rename, the contents are out of the workspace although not yet deleted. The greater benefit however is that this operation
|
||||
// will fail in case any file is used by another process. fs.unlink() in node will not bail if a file unlinked is used by another process.
|
||||
// However, the consequences are bad as outlined in all the related bugs from https://github.com/joyent/node/issues/7164
|
||||
export function del(path: string, tmpFolder: string, callback: (error: Error | null) => void, done?: (error: Error | null) => void): void {
|
||||
fs.exists(path, exists => {
|
||||
if (!exists) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
fs.stat(path, (err, stat) => {
|
||||
if (err || !stat) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Special windows workaround: A file or folder that ends with a "." cannot be moved to another place
|
||||
// because it is not a valid file name. In this case, we really have to do the deletion without prior move.
|
||||
if (path[path.length - 1] === '.' || strings.endsWith(path, './') || strings.endsWith(path, '.\\')) {
|
||||
return rmRecursive(path, callback);
|
||||
}
|
||||
|
||||
const pathInTemp = paths.join(tmpFolder, uuid.generateUuid());
|
||||
fs.rename(path, pathInTemp, (error: Error | null) => {
|
||||
if (error) {
|
||||
return rmRecursive(path, callback); // if rename fails, delete without tmp dir
|
||||
}
|
||||
|
||||
// Return early since the move succeeded
|
||||
callback(null);
|
||||
|
||||
// do the heavy deletion outside the callers callback
|
||||
rmRecursive(pathInTemp, error => {
|
||||
if (done) {
|
||||
done(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function rmRecursive(path: string, callback: (error: Error | null) => void): void {
|
||||
if (path === paths.win32.sep || path === paths.posix.sep) {
|
||||
return callback(new Error('Will not delete root!'));
|
||||
}
|
||||
|
||||
fs.exists(path, exists => {
|
||||
if (!exists) {
|
||||
callback(null);
|
||||
} else {
|
||||
fs.lstat(path, (err, stat) => {
|
||||
if (err || !stat) {
|
||||
callback(err);
|
||||
} else if (!stat.isDirectory() || stat.isSymbolicLink() /* !!! never recurse into links when deleting !!! */) {
|
||||
const mode = stat.mode;
|
||||
if (!(mode & 128)) { // 128 === 0200
|
||||
fs.chmod(path, mode | 128, (err: Error) => { // 128 === 0200
|
||||
if (err) {
|
||||
callback(err);
|
||||
} else {
|
||||
fs.unlink(path, callback);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
fs.unlink(path, callback);
|
||||
}
|
||||
} else {
|
||||
readdir(path, (err, children) => {
|
||||
if (err || !children) {
|
||||
callback(err);
|
||||
} else if (children.length === 0) {
|
||||
fs.rmdir(path, callback);
|
||||
} else {
|
||||
let firstError: Error | null = null;
|
||||
let childrenLeft = children.length;
|
||||
children.forEach(child => {
|
||||
rmRecursive(paths.join(path, child), (err: Error) => {
|
||||
childrenLeft--;
|
||||
if (err) {
|
||||
firstError = firstError || err;
|
||||
}
|
||||
|
||||
if (childrenLeft === 0) {
|
||||
if (firstError) {
|
||||
callback(firstError);
|
||||
} else {
|
||||
fs.rmdir(path, callback);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function delSync(path: string): void {
|
||||
if (path === paths.win32.sep || path === paths.posix.sep) {
|
||||
throw new Error('Will not delete root!');
|
||||
}
|
||||
|
||||
try {
|
||||
const stat = fs.lstatSync(path);
|
||||
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
||||
readdirSync(path).forEach(child => delSync(paths.join(path, child)));
|
||||
fs.rmdirSync(path);
|
||||
} else {
|
||||
fs.unlinkSync(path);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return; // not found
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export function mv(source: string, target: string, callback: (error: Error | null) => void): void {
|
||||
if (source === target) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
function updateMtime(err: Error | null): void {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
fs.lstat(target, (error, stat) => {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if (stat.isDirectory() || stat.isSymbolicLink()) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
fs.open(target, 'a', null, (err: Error, fd: number) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
fs.futimes(fd, stat.atime, new Date(), (err: Error) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
fs.close(fd, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Try native rename()
|
||||
fs.rename(source, target, (err: Error) => {
|
||||
if (!err) {
|
||||
return updateMtime(null);
|
||||
}
|
||||
|
||||
// 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 (err && source.toLowerCase() !== target.toLowerCase() && ((<any>err).code === 'EXDEV') || strings.endsWith(source, '.')) {
|
||||
return copy(source, target, (err: Error) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
rmRecursive(source, updateMtime);
|
||||
});
|
||||
}
|
||||
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
export interface IWriteFileOptions {
|
||||
mode?: number;
|
||||
flag?: string;
|
||||
encoding?: {
|
||||
charset: string;
|
||||
addBOM: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface IEnsuredWriteFileOptions extends IWriteFileOptions {
|
||||
mode: number;
|
||||
flag: string;
|
||||
}
|
||||
|
||||
let canFlush = true;
|
||||
export function writeFileAndFlush(path: string, data: string | Buffer | NodeJS.ReadableStream | Uint8Array, options: IWriteFileOptions, callback: (error?: Error) => void): void {
|
||||
const ensuredOptions = ensureWriteOptions(options);
|
||||
|
||||
if (typeof data === 'string' || Buffer.isBuffer(data) || data instanceof Uint8Array) {
|
||||
doWriteFileAndFlush(path, data, ensuredOptions, callback);
|
||||
} else {
|
||||
doWriteFileStreamAndFlush(path, data, ensuredOptions, callback);
|
||||
}
|
||||
}
|
||||
|
||||
function doWriteFileStreamAndFlush(path: string, reader: NodeJS.ReadableStream, options: IEnsuredWriteFileOptions, callback: (error?: Error) => void): void {
|
||||
|
||||
// finish only once
|
||||
let finished = false;
|
||||
const finish = (error?: Error) => {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
|
||||
// in error cases we need to manually close streams
|
||||
// if the write stream was successfully opened
|
||||
if (error) {
|
||||
if (isOpen) {
|
||||
writer.once('close', () => callback(error));
|
||||
writer.destroy();
|
||||
} else {
|
||||
callback(error);
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise just return without error
|
||||
else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// create writer to target. we set autoClose: false because we want to use the streams
|
||||
// file descriptor to call fs.fdatasync to ensure the data is flushed to disk
|
||||
const writer = fs.createWriteStream(path, { mode: options.mode, flags: options.flag, autoClose: false });
|
||||
|
||||
// Event: 'open'
|
||||
// Purpose: save the fd for later use and start piping
|
||||
// Notes: will not be called when there is an error opening the file descriptor!
|
||||
let fd: number;
|
||||
let isOpen: boolean;
|
||||
writer.once('open', descriptor => {
|
||||
fd = descriptor;
|
||||
isOpen = true;
|
||||
|
||||
// if an encoding is provided, we need to pipe the stream through
|
||||
// an encoder stream and forward the encoding related options
|
||||
if (options.encoding) {
|
||||
reader = reader.pipe(encodeStream(options.encoding.charset, { addBOM: options.encoding.addBOM }));
|
||||
}
|
||||
|
||||
// start data piping only when we got a successful open. this ensures that we do
|
||||
// not consume the stream when an error happens and helps to fix this issue:
|
||||
// https://github.com/Microsoft/vscode/issues/42542
|
||||
reader.pipe(writer);
|
||||
});
|
||||
|
||||
// Event: 'error'
|
||||
// Purpose: to return the error to the outside and to close the write stream (does not happen automatically)
|
||||
reader.once('error', error => finish(error));
|
||||
writer.once('error', error => finish(error));
|
||||
|
||||
// Event: 'finish'
|
||||
// Purpose: use fs.fdatasync to flush the contents to disk
|
||||
// Notes: event is called when the writer has finished writing to the underlying resource. we must call writer.close()
|
||||
// because we have created the WriteStream with autoClose: false
|
||||
writer.once('finish', () => {
|
||||
|
||||
// flush to disk
|
||||
if (canFlush && isOpen) {
|
||||
fs.fdatasync(fd, (syncError: Error) => {
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
writer.destroy();
|
||||
});
|
||||
} else {
|
||||
writer.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// Event: 'close'
|
||||
// Purpose: signal we are done to the outside
|
||||
// Notes: event is called when the writer's filedescriptor is closed
|
||||
writer.once('close', () => finish());
|
||||
}
|
||||
|
||||
// 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) => void): void {
|
||||
if (options.encoding) {
|
||||
data = encode(data instanceof Uint8Array ? Buffer.from(data) : data, options.encoding.charset, { addBOM: options.encoding.addBOM });
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
||||
// 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 writeFileAndFlushSync(path: string, data: string | Buffer, options?: IWriteFileOptions): void {
|
||||
const ensuredOptions = ensureWriteOptions(options);
|
||||
|
||||
if (ensuredOptions.encoding) {
|
||||
data = encode(data, ensuredOptions.encoding.charset, { addBOM: ensuredOptions.encoding.addBOM });
|
||||
}
|
||||
|
||||
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',
|
||||
encoding: options.encoding
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = paths.dirname(path);
|
||||
if (path === dir) { // end recursion
|
||||
return path;
|
||||
}
|
||||
|
||||
const name = (paths.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 paths.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 paths.join(prefix, found[ix]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// silently ignore error
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export function realpath(path: string, callback: (error: Error | null, realpath: string) => void): void {
|
||||
return fs.realpath(path, (error, realpath) => {
|
||||
if (!error) {
|
||||
return callback(null, realpath);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
return fs.access(normalizedPath, fs.constants.R_OK, error => {
|
||||
return callback(error, normalizedPath);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function normalizePath(path: string): string {
|
||||
return strings.rtrim(paths.normalize(path), paths.sep);
|
||||
}
|
||||
|
||||
export function watch(path: string, onChange: (type: string, path?: string) => void, onError: (error: string) => void): IDisposable {
|
||||
try {
|
||||
const watcher = fs.watch(path);
|
||||
|
||||
watcher.on('change', (type, raw) => {
|
||||
let file: string | undefined;
|
||||
if (raw) { // https://github.com/Microsoft/vscode/issues/38191
|
||||
file = raw.toString();
|
||||
if (platform.isMacintosh) {
|
||||
// Mac: uses NFD unicode form on disk, but we want NFC
|
||||
// See also https://github.com/nodejs/node/issues/2165
|
||||
file = normalizeNFC(file);
|
||||
}
|
||||
}
|
||||
|
||||
onChange(type, file);
|
||||
});
|
||||
|
||||
watcher.on('error', (code: number, signal: string) => onError(`Failed to watch ${path} for changes (${code}, ${signal})`));
|
||||
|
||||
return toDisposable(() => {
|
||||
watcher.removeAllListeners();
|
||||
watcher.close();
|
||||
});
|
||||
} catch (error) {
|
||||
fs.exists(path, exists => {
|
||||
if (exists) {
|
||||
onError(`Failed to watch ${path} for changes (${error.toString()})`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Disposable.None;
|
||||
}
|
||||
|
||||
export function sanitizeFilePath(candidate: string, cwd: string): string {
|
||||
|
||||
// Special case: allow to open a drive letter without trailing backslash
|
||||
if (platform.isWindows && strings.endsWith(candidate, ':')) {
|
||||
candidate += paths.sep;
|
||||
}
|
||||
|
||||
// Ensure absolute
|
||||
if (!paths.isAbsolute(candidate)) {
|
||||
candidate = paths.join(cwd, candidate);
|
||||
}
|
||||
|
||||
// Ensure normalized
|
||||
candidate = paths.normalize(candidate);
|
||||
|
||||
// Ensure no trailing slash/backslash
|
||||
if (platform.isWindows) {
|
||||
candidate = strings.rtrim(candidate, paths.sep);
|
||||
|
||||
// Special case: allow to open drive root ('C:\')
|
||||
if (strings.endsWith(candidate, ':')) {
|
||||
candidate += paths.sep;
|
||||
}
|
||||
|
||||
} else {
|
||||
candidate = strings.rtrim(candidate, paths.sep);
|
||||
|
||||
// Special case: allow to open root ('/')
|
||||
if (!candidate) {
|
||||
candidate = paths.sep;
|
||||
}
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
91
src/vs/base/node/extpath.ts
Normal file
91
src/vs/base/node/extpath.ts
Normal 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);
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
|
||||
/**
|
||||
* Executes the given function (fn) over the given array of items (list) in parallel and returns the resulting errors and results as
|
||||
* array to the callback (callback). The resulting errors and results are evaluated by calling the provided callback function.
|
||||
*/
|
||||
export function parallel<T, E>(list: T[], fn: (item: T, callback: (err: Error | null, result: E | null) => void) => void, callback: (err: Array<Error | null> | null, result: E[]) => void): void {
|
||||
const results = new Array(list.length);
|
||||
const errors = new Array<Error | null>(list.length);
|
||||
let didErrorOccur = false;
|
||||
let doneCount = 0;
|
||||
|
||||
if (list.length === 0) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
list.forEach((item, index) => {
|
||||
fn(item, (error, result) => {
|
||||
if (error) {
|
||||
didErrorOccur = true;
|
||||
results[index] = null;
|
||||
errors[index] = error;
|
||||
} else {
|
||||
results[index] = result;
|
||||
errors[index] = null;
|
||||
}
|
||||
|
||||
if (++doneCount === list.length) {
|
||||
return callback(didErrorOccur ? errors : null, results);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given function (fn) over the given array of items (param) in sequential order and returns the first occurred error or the result as
|
||||
* array to the callback (callback). The resulting errors and results are evaluated by calling the provided callback function. The first param can
|
||||
* either be a function that returns an array of results to loop in async fashion or be an array of items already.
|
||||
*/
|
||||
export function loop<T, E>(param: (callback: (error: Error, result: T[]) => void) => void, fn: (item: T, callback: (error: Error | null, result: E | null) => void, index: number, total: number) => void, callback: (error: Error | null, result: E[] | null) => void): void;
|
||||
export function loop<T, E>(param: T[], fn: (item: T, callback: (error: Error | null, result: E | null) => void, index: number, total: number) => void, callback: (error: Error | null, result: E[] | null) => void): void;
|
||||
export function loop<E>(param: any, fn: (item: any, callback: (error: Error | null, result: E | null) => void, index: number, total: number) => void, callback: (error: Error | null, result: E[] | null) => void): void {
|
||||
|
||||
// Assert
|
||||
assert.ok(param, 'Missing first parameter');
|
||||
assert.ok(typeof (fn) === 'function', 'Second parameter must be a function that is called for each element');
|
||||
assert.ok(typeof (callback) === 'function', 'Third parameter must be a function that is called on error and success');
|
||||
|
||||
// Param is function, execute to retrieve array
|
||||
if (typeof (param) === 'function') {
|
||||
try {
|
||||
param((error: Error, result: E[]) => {
|
||||
if (error) {
|
||||
callback(error, null);
|
||||
} else {
|
||||
loop(result, fn, callback);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
callback(error, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Expect the param to be an array and loop over it
|
||||
else {
|
||||
const results: E[] = [];
|
||||
|
||||
const looper: (i: number) => void = function (i: number): void {
|
||||
|
||||
// Still work to do
|
||||
if (i < param.length) {
|
||||
|
||||
// Execute function on array element
|
||||
try {
|
||||
fn(param[i], (error: any, result: E) => {
|
||||
|
||||
// A method might only send a boolean value as return value (e.g. fs.exists), support this case gracefully
|
||||
if (error === true || error === false) {
|
||||
result = error;
|
||||
error = null;
|
||||
}
|
||||
|
||||
// Quit looping on error
|
||||
if (error) {
|
||||
callback(error, null);
|
||||
}
|
||||
|
||||
// Otherwise push result on stack and continue looping
|
||||
else {
|
||||
if (result) { //Could be that provided function is not returning a result
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
process.nextTick(() => {
|
||||
looper(i + 1);
|
||||
});
|
||||
}
|
||||
}, i, param.length);
|
||||
} catch (error) {
|
||||
callback(error, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Done looping, pass back results too callback function
|
||||
else {
|
||||
callback(null, results);
|
||||
}
|
||||
};
|
||||
|
||||
// Start looping with first element in array
|
||||
looper(0);
|
||||
}
|
||||
}
|
||||
|
||||
function Sequence(sequences: { (...param: any[]): void; }[]): void {
|
||||
|
||||
// Assert
|
||||
assert.ok(sequences.length > 1, 'Need at least one error handler and one function to process sequence');
|
||||
sequences.forEach((sequence) => {
|
||||
assert.ok(typeof (sequence) === 'function');
|
||||
});
|
||||
|
||||
// Execute in Loop
|
||||
const errorHandler = sequences.splice(0, 1)[0]; //Remove error handler
|
||||
let sequenceResult: any = null;
|
||||
|
||||
loop(sequences, (sequence, clb) => {
|
||||
const sequenceFunction = function (error: any, result: any): void {
|
||||
|
||||
// A method might only send a boolean value as return value (e.g. fs.exists), support this case gracefully
|
||||
if (error === true || error === false) {
|
||||
result = error;
|
||||
error = null;
|
||||
}
|
||||
|
||||
// Handle Error and Result
|
||||
if (error) {
|
||||
clb(error, null);
|
||||
} else {
|
||||
sequenceResult = result; //Remember result of sequence
|
||||
clb(null, null); //Don't pass on result to Looper as we are not aggregating it
|
||||
}
|
||||
};
|
||||
|
||||
// We call the sequence function setting "this" to be the callback we define here
|
||||
// and we pass in the "sequenceResult" as first argument. Doing all this avoids having
|
||||
// to pass in a callback to the sequence because the callback is already "this".
|
||||
try {
|
||||
sequence.call(sequenceFunction, sequenceResult);
|
||||
} catch (error) {
|
||||
clb(error, null);
|
||||
}
|
||||
}, (error, result) => {
|
||||
if (error) {
|
||||
errorHandler(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a variable list of functions to execute in sequence. The first function must be the error handler and the
|
||||
* following functions can do arbitrary work. "this" must be used as callback value for async functions to continue
|
||||
* through the sequence:
|
||||
* sequence(
|
||||
* function errorHandler(error) {
|
||||
* clb(error, null);
|
||||
* },
|
||||
*
|
||||
* function doSomethingAsync() {
|
||||
* fs.doAsync(path, this);
|
||||
* },
|
||||
*
|
||||
* function done(result) {
|
||||
* clb(null, result);
|
||||
* }
|
||||
* );
|
||||
*/
|
||||
export function sequence(errorHandler: (error: Error) => void, ...sequences: Function[]): void;
|
||||
export function sequence(sequences: Function[]): void;
|
||||
export function sequence(sequences: any): void {
|
||||
Sequence((Array.isArray(sequences)) ? sequences : Array.prototype.slice.call(arguments));
|
||||
}
|
||||
@@ -3,68 +3,200 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as extfs from 'vs/base/node/extfs';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { nfcall, Queue } from 'vs/base/common/async';
|
||||
import { join, dirname } 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 { endsWith } from 'vs/base/common/strings';
|
||||
import { promisify } from 'util';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { isRootOrDriveLetter } from 'vs/base/common/extpath';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { normalizeNFC } from 'vs/base/common/normalization';
|
||||
import { encode, encodeStream } from 'vs/base/node/encoding';
|
||||
|
||||
export function readdir(path: string): Promise<string[]> {
|
||||
return nfcall(extfs.readdir, path);
|
||||
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 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 new Promise(c => fs.exists(path, c));
|
||||
return promisify(fs.exists)(path);
|
||||
}
|
||||
|
||||
export function chmod(path: string, mode: number): Promise<boolean> {
|
||||
return nfcall(fs.chmod, path, mode);
|
||||
}
|
||||
|
||||
export import mkdirp = extfs.mkdirp;
|
||||
|
||||
export function rimraf(path: string): Promise<void> {
|
||||
return lstat(path).then(stat => {
|
||||
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
||||
return readdir(path)
|
||||
.then(children => Promise.all(children.map(child => rimraf(join(path, child)))))
|
||||
.then(() => rmdir(path));
|
||||
} else {
|
||||
return unlink(path);
|
||||
}
|
||||
}, (err: NodeJS.ErrnoException) => {
|
||||
if (err.code === 'ENOENT') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
export function realpath(path: string): Promise<string> {
|
||||
return nfcall(extfs.realpath, 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 nfcall(fs.stat, path);
|
||||
return promisify(fs.stat)(path);
|
||||
}
|
||||
|
||||
export function statLink(path: string): Promise<{ stat: fs.Stats, isSymbolicLink: boolean }> {
|
||||
return nfcall(extfs.statLink, path);
|
||||
export interface IStatAndLink {
|
||||
stat: fs.Stats;
|
||||
isSymbolicLink: boolean;
|
||||
}
|
||||
|
||||
export async function statLink(path: string): Promise<IStatAndLink> {
|
||||
|
||||
// First stat the link
|
||||
let linkStat: fs.Stats | undefined;
|
||||
let linkStatError: NodeJS.ErrnoException | undefined;
|
||||
try {
|
||||
linkStat = await lstat(path);
|
||||
} catch (error) {
|
||||
linkStatError = error;
|
||||
}
|
||||
|
||||
// Then stat the target and return that
|
||||
const isLink = !!(linkStat && linkStat.isSymbolicLink());
|
||||
if (linkStatError || isLink) {
|
||||
const fileStat = await stat(path);
|
||||
|
||||
return { stat: fileStat, isSymbolicLink: isLink };
|
||||
}
|
||||
|
||||
return { stat: linkStat!, isSymbolicLink: false };
|
||||
}
|
||||
|
||||
export function lstat(path: string): Promise<fs.Stats> {
|
||||
return nfcall(fs.lstat, path);
|
||||
}
|
||||
|
||||
export function move(oldPath: string, newPath: string): Promise<void> {
|
||||
return nfcall(extfs.mv, oldPath, newPath);
|
||||
return promisify(fs.lstat)(path);
|
||||
}
|
||||
|
||||
export function rename(oldPath: string, newPath: string): Promise<void> {
|
||||
return nfcall(fs.rename, oldPath, newPath);
|
||||
return promisify(fs.rename)(oldPath, newPath);
|
||||
}
|
||||
|
||||
export function renameIgnoreError(oldPath: string, newPath: string): Promise<void> {
|
||||
@@ -73,30 +205,26 @@ export function renameIgnoreError(oldPath: string, newPath: string): Promise<voi
|
||||
});
|
||||
}
|
||||
|
||||
export function rmdir(path: string): Promise<void> {
|
||||
return nfcall(fs.rmdir, path);
|
||||
}
|
||||
|
||||
export function unlink(path: string): Promise<void> {
|
||||
return nfcall(fs.unlink, path);
|
||||
return promisify(fs.unlink)(path);
|
||||
}
|
||||
|
||||
export function symlink(target: string, path: string, type?: string): Promise<void> {
|
||||
return nfcall<void>(fs.symlink, target, path, type);
|
||||
return promisify(fs.symlink)(target, path, type);
|
||||
}
|
||||
|
||||
export function readlink(path: string): Promise<string> {
|
||||
return nfcall<string>(fs.readlink, path);
|
||||
return promisify(fs.readlink)(path);
|
||||
}
|
||||
|
||||
export function truncate(path: string, len: number): Promise<void> {
|
||||
return nfcall(fs.truncate, path, len);
|
||||
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 nfcall(fs.readFile, path, encoding);
|
||||
return promisify(fs.readFile)(path, encoding);
|
||||
}
|
||||
|
||||
// According to node.js docs (https://nodejs.org/docs/v6.5.0/api/fs.html#fs_fs_writefile_file_data_options_callback)
|
||||
@@ -104,15 +232,15 @@ export function readFile(path: string, encoding?: string): Promise<Buffer | stri
|
||||
// Therefor we use a Queue on the path that is given to us to sequentialize calls to the same path properly.
|
||||
const writeFilePathQueue: { [path: string]: Queue<void> } = Object.create(null);
|
||||
|
||||
export function writeFile(path: string, data: string, options?: extfs.IWriteFileOptions): Promise<void>;
|
||||
export function writeFile(path: string, data: Buffer, options?: extfs.IWriteFileOptions): Promise<void>;
|
||||
export function writeFile(path: string, data: Uint8Array, options?: extfs.IWriteFileOptions): Promise<void>;
|
||||
export function writeFile(path: string, data: NodeJS.ReadableStream, options?: extfs.IWriteFileOptions): Promise<void>;
|
||||
export function writeFile(path: string, data: any, options?: extfs.IWriteFileOptions): Promise<void>;
|
||||
export function writeFile(path: string, data: any, options?: extfs.IWriteFileOptions): any {
|
||||
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: NodeJS.ReadableStream, options?: IWriteFileOptions): Promise<void>;
|
||||
export function writeFile(path: string, data: string | Buffer | NodeJS.ReadableStream | Uint8Array, options?: IWriteFileOptions): Promise<void>;
|
||||
export function writeFile(path: string, data: string | Buffer | NodeJS.ReadableStream | Uint8Array, options?: IWriteFileOptions): Promise<void> {
|
||||
const queueKey = toQueueKey(path);
|
||||
|
||||
return ensureWriteFileQueue(queueKey).queue(() => nfcall(extfs.writeFileAndFlush, path, data, options));
|
||||
return ensureWriteFileQueue(queueKey).queue(() => writeFileAndFlush(path, data, options));
|
||||
}
|
||||
|
||||
function toQueueKey(path: string): string {
|
||||
@@ -140,43 +268,235 @@ function ensureWriteFileQueue(queueKey: string): Queue<void> {
|
||||
return writeFileQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a dir and return only subfolders
|
||||
*/
|
||||
export function readDirsInDir(dirPath: string): Promise<string[]> {
|
||||
return readdir(dirPath).then(children => {
|
||||
return Promise.all(children.map(c => dirExists(join(dirPath, c)))).then(exists => {
|
||||
return children.filter((_, i) => exists[i]);
|
||||
export interface IWriteFileOptions {
|
||||
mode?: number;
|
||||
flag?: string;
|
||||
encoding?: {
|
||||
charset: string;
|
||||
addBOM: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface IEnsuredWriteFileOptions extends IWriteFileOptions {
|
||||
mode: number;
|
||||
flag: string;
|
||||
}
|
||||
|
||||
let canFlush = true;
|
||||
function writeFileAndFlush(path: string, data: string | Buffer | NodeJS.ReadableStream | Uint8Array, options: IWriteFileOptions | undefined): Promise<void> {
|
||||
const ensuredOptions = ensureWriteOptions(options);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof data === 'string' || Buffer.isBuffer(data) || data instanceof Uint8Array) {
|
||||
doWriteFileAndFlush(path, data, ensuredOptions, error => error ? reject(error) : resolve());
|
||||
} else {
|
||||
doWriteFileStreamAndFlush(path, data, ensuredOptions, error => error ? reject(error) : resolve());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function doWriteFileStreamAndFlush(path: string, reader: NodeJS.ReadableStream, options: IEnsuredWriteFileOptions, callback: (error?: Error) => void): void {
|
||||
|
||||
// finish only once
|
||||
let finished = false;
|
||||
const finish = (error?: Error) => {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
|
||||
// in error cases we need to manually close streams
|
||||
// if the write stream was successfully opened
|
||||
if (error) {
|
||||
if (isOpen) {
|
||||
writer.once('close', () => callback(error));
|
||||
writer.destroy();
|
||||
} else {
|
||||
callback(error);
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise just return without error
|
||||
else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// create writer to target. we set autoClose: false because we want to use the streams
|
||||
// file descriptor to call fs.fdatasync to ensure the data is flushed to disk
|
||||
const writer = fs.createWriteStream(path, { mode: options.mode, flags: options.flag, autoClose: false });
|
||||
|
||||
// Event: 'open'
|
||||
// Purpose: save the fd for later use and start piping
|
||||
// Notes: will not be called when there is an error opening the file descriptor!
|
||||
let fd: number;
|
||||
let isOpen: boolean;
|
||||
writer.once('open', descriptor => {
|
||||
fd = descriptor;
|
||||
isOpen = true;
|
||||
|
||||
// if an encoding is provided, we need to pipe the stream through
|
||||
// an encoder stream and forward the encoding related options
|
||||
if (options.encoding) {
|
||||
reader = reader.pipe(encodeStream(options.encoding.charset, { addBOM: options.encoding.addBOM }));
|
||||
}
|
||||
|
||||
// start data piping only when we got a successful open. this ensures that we do
|
||||
// not consume the stream when an error happens and helps to fix this issue:
|
||||
// https://github.com/Microsoft/vscode/issues/42542
|
||||
reader.pipe(writer);
|
||||
});
|
||||
|
||||
// Event: 'error'
|
||||
// Purpose: to return the error to the outside and to close the write stream (does not happen automatically)
|
||||
reader.once('error', error => finish(error));
|
||||
writer.once('error', error => finish(error));
|
||||
|
||||
// Event: 'finish'
|
||||
// Purpose: use fs.fdatasync to flush the contents to disk
|
||||
// Notes: event is called when the writer has finished writing to the underlying resource. we must call writer.close()
|
||||
// because we have created the WriteStream with autoClose: false
|
||||
writer.once('finish', () => {
|
||||
|
||||
// flush to disk
|
||||
if (canFlush && isOpen) {
|
||||
fs.fdatasync(fd, (syncError: Error) => {
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
writer.destroy();
|
||||
});
|
||||
} else {
|
||||
writer.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// Event: 'close'
|
||||
// Purpose: signal we are done to the outside
|
||||
// Notes: event is called when the writer's filedescriptor is closed
|
||||
writer.once('close', () => finish());
|
||||
}
|
||||
|
||||
// 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) => void): void {
|
||||
if (options.encoding) {
|
||||
data = encode(data instanceof Uint8Array ? Buffer.from(data) : data, options.encoding.charset, { addBOM: options.encoding.addBOM });
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
||||
// 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));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* `path` exists and is a directory
|
||||
*/
|
||||
export function dirExists(path: string): Promise<boolean> {
|
||||
return stat(path).then(stat => stat.isDirectory(), () => false);
|
||||
}
|
||||
export function writeFileSync(path: string, data: string | Buffer, options?: IWriteFileOptions): void {
|
||||
const ensuredOptions = ensureWriteOptions(options);
|
||||
|
||||
/**
|
||||
* `path` exists and is a file.
|
||||
*/
|
||||
export function fileExists(path: string): Promise<boolean> {
|
||||
return stat(path).then(stat => stat.isFile(), () => false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a path from disk.
|
||||
*/
|
||||
let _tmpDir: string | null = null;
|
||||
function getTmpDir(): string {
|
||||
if (!_tmpDir) {
|
||||
_tmpDir = os.tmpdir();
|
||||
if (ensuredOptions.encoding) {
|
||||
data = encode(data, ensuredOptions.encoding.charset, { addBOM: ensuredOptions.encoding.addBOM });
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
return _tmpDir;
|
||||
}
|
||||
export function del(path: string, tmp = getTmpDir()): Promise<void> {
|
||||
return nfcall(extfs.del, path, tmp);
|
||||
|
||||
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',
|
||||
encoding: options.encoding
|
||||
};
|
||||
}
|
||||
|
||||
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> {
|
||||
@@ -200,6 +520,154 @@ export function whenDeleted(path: string): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
export function copy(source: string, target: string): Promise<void> {
|
||||
return nfcall(extfs.copy, source, target);
|
||||
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' || endsWith(source, '.')) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
export async function mkdirp(path: string, mode?: number, token?: CancellationToken): Promise<void> {
|
||||
const mkdir = async () => {
|
||||
try {
|
||||
await promisify(fs.mkdir)(path, mode);
|
||||
} catch (error) {
|
||||
|
||||
// ENOENT: a parent folder does not exist yet
|
||||
if (error.code === 'ENOENT') {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Any other error: check if folder exists and
|
||||
// return normally in that case if its a folder
|
||||
try {
|
||||
const fileStat = await stat(path);
|
||||
if (!fileStat.isDirectory()) {
|
||||
return Promise.reject(new Error(`'${path}' exists and is not a directory.`));
|
||||
}
|
||||
} catch (statError) {
|
||||
throw error; // rethrow original error
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// stop at root
|
||||
if (path === dirname(path)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
try {
|
||||
await mkdir();
|
||||
} catch (error) {
|
||||
|
||||
// Respect cancellation
|
||||
if (token && token.isCancellationRequested) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// ENOENT: a parent folder does not exist yet, continue
|
||||
// to create the parent folder and then try again.
|
||||
if (error.code === 'ENOENT') {
|
||||
await mkdirp(dirname(path), mode);
|
||||
|
||||
return mkdir();
|
||||
}
|
||||
|
||||
// Any other error
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
@@ -4,20 +4,9 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { exec } from 'child_process';
|
||||
|
||||
import { ProcessItem } from 'vs/base/common/processes';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
|
||||
export interface ProcessItem {
|
||||
name: string;
|
||||
cmd: string;
|
||||
pid: number;
|
||||
ppid: number;
|
||||
load: number;
|
||||
mem: number;
|
||||
|
||||
children?: ProcessItem[];
|
||||
}
|
||||
|
||||
export function listProcesses(rootPid: number): Promise<ProcessItem> {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -181,7 +170,7 @@ export function listProcesses(rootPid: number): Promise<ProcessItem> {
|
||||
exec(CMD, { maxBuffer: 1000 * 1024, env: { LC_NUMERIC: 'en_US.UTF-8' } }, (err, stdout, stderr) => {
|
||||
|
||||
if (err || stderr) {
|
||||
reject(err || stderr.toString());
|
||||
reject(err || new Error(stderr.toString()));
|
||||
} else {
|
||||
|
||||
const lines = stdout.toString().split('\n');
|
||||
@@ -214,7 +203,7 @@ export function listProcesses(rootPid: number): Promise<ProcessItem> {
|
||||
|
||||
exec(cmd, {}, (err, stdout, stderr) => {
|
||||
if (err || stderr) {
|
||||
reject(err || stderr.toString());
|
||||
reject(err || new Error(stderr.toString()));
|
||||
} else {
|
||||
const cpuUsage = stdout.toString().split('\n');
|
||||
for (let i = 0; i < pids.length; i++) {
|
||||
|
||||
@@ -64,7 +64,7 @@ export interface IStorage extends IDisposable {
|
||||
getNumber(key: string, fallbackValue: number): number;
|
||||
getNumber(key: string, fallbackValue?: number): number | undefined;
|
||||
|
||||
set(key: string, value: string | boolean | number): Promise<void>;
|
||||
set(key: string, value: string | boolean | number | undefined | null): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
|
||||
close(): Promise<void>;
|
||||
@@ -152,7 +152,7 @@ export class Storage extends Disposable implements IStorage {
|
||||
return this.cache.size;
|
||||
}
|
||||
|
||||
init(): Promise<void> {
|
||||
async init(): Promise<void> {
|
||||
if (this.state !== StorageState.None) {
|
||||
return Promise.resolve(); // either closed or already initialized
|
||||
}
|
||||
@@ -166,9 +166,7 @@ export class Storage extends Disposable implements IStorage {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.database.getItems().then(items => {
|
||||
this.cache = items;
|
||||
});
|
||||
this.cache = await this.database.getItems();
|
||||
}
|
||||
|
||||
get(key: string, fallbackValue: string): string;
|
||||
@@ -207,7 +205,7 @@ export class Storage extends Disposable implements IStorage {
|
||||
return parseInt(value, 10);
|
||||
}
|
||||
|
||||
set(key: string, value: string | boolean | number): Promise<void> {
|
||||
set(key: string, value: string | boolean | number | null | undefined): Promise<void> {
|
||||
if (this.state === StorageState.Closed) {
|
||||
return Promise.resolve(); // Return early if we are already closed
|
||||
}
|
||||
@@ -262,7 +260,7 @@ export class Storage extends Disposable implements IStorage {
|
||||
return this.flushDelayer.trigger(() => this.flushPending());
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
async close(): Promise<void> {
|
||||
if (this.state === StorageState.Closed) {
|
||||
return Promise.resolve(); // return if already closed
|
||||
}
|
||||
@@ -276,8 +274,13 @@ export class Storage extends Disposable implements IStorage {
|
||||
//
|
||||
// Recovery: we pass our cache over as recovery option in case
|
||||
// the DB is not healthy.
|
||||
const onDone = () => this.database.close(() => this.cache);
|
||||
return this.flushDelayer.trigger(() => this.flushPending(), 0 /* as soon as possible */).then(onDone, onDone);
|
||||
try {
|
||||
await this.flushDelayer.trigger(() => this.flushPending(), 0 /* as soon as possible */);
|
||||
} catch (error) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
await this.database.close(() => this.cache);
|
||||
}
|
||||
|
||||
private flushPending(): Promise<void> {
|
||||
@@ -344,24 +347,25 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
|
||||
this.whenConnected = this.connect(path);
|
||||
}
|
||||
|
||||
getItems(): Promise<Map<string, string>> {
|
||||
return this.whenConnected.then(connection => {
|
||||
const items = new Map<string, string>();
|
||||
async getItems(): Promise<Map<string, string>> {
|
||||
const connection = await this.whenConnected;
|
||||
|
||||
return this.all(connection, 'SELECT * FROM ItemTable').then(rows => {
|
||||
rows.forEach(row => items.set(row.key, row.value));
|
||||
const items = new Map<string, string>();
|
||||
|
||||
if (this.logger.isTracing) {
|
||||
this.logger.trace(`[storage ${this.name}] getItems(): ${items.size} rows`);
|
||||
}
|
||||
const rows = await this.all(connection, 'SELECT * FROM ItemTable');
|
||||
rows.forEach(row => items.set(row.key, row.value));
|
||||
|
||||
return items;
|
||||
});
|
||||
});
|
||||
if (this.logger.isTracing) {
|
||||
this.logger.trace(`[storage ${this.name}] getItems(): ${items.size} rows`);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
updateItems(request: IUpdateRequest): Promise<void> {
|
||||
return this.whenConnected.then(connection => this.doUpdateItems(connection, request));
|
||||
async updateItems(request: IUpdateRequest): Promise<void> {
|
||||
const connection = await this.whenConnected;
|
||||
|
||||
return this.doUpdateItems(connection, request);
|
||||
}
|
||||
|
||||
private doUpdateItems(connection: IDatabaseConnection, request: IUpdateRequest): Promise<void> {
|
||||
@@ -452,10 +456,12 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
|
||||
});
|
||||
}
|
||||
|
||||
close(recovery?: () => Map<string, string>): Promise<void> {
|
||||
async close(recovery?: () => Map<string, string>): Promise<void> {
|
||||
this.logger.trace(`[storage ${this.name}] close()`);
|
||||
|
||||
return this.whenConnected.then(connection => this.doClose(connection, recovery));
|
||||
const connection = await this.whenConnected;
|
||||
|
||||
return this.doClose(connection, recovery);
|
||||
}
|
||||
|
||||
private doClose(connection: IDatabaseConnection, recovery?: () => Map<string, string>): Promise<void> {
|
||||
@@ -529,24 +535,23 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
|
||||
return `${path}.backup`;
|
||||
}
|
||||
|
||||
checkIntegrity(full: boolean): Promise<string> {
|
||||
async checkIntegrity(full: boolean): Promise<string> {
|
||||
this.logger.trace(`[storage ${this.name}] checkIntegrity(full: ${full})`);
|
||||
|
||||
return this.whenConnected.then(connection => {
|
||||
return this.get(connection, full ? 'PRAGMA integrity_check' : 'PRAGMA quick_check').then(row => {
|
||||
const integrity = full ? row['integrity_check'] : row['quick_check'];
|
||||
const connection = await this.whenConnected;
|
||||
const row = await this.get(connection, full ? 'PRAGMA integrity_check' : 'PRAGMA quick_check');
|
||||
|
||||
if (connection.isErroneous) {
|
||||
return `${integrity} (last error: ${connection.lastError})`;
|
||||
}
|
||||
const integrity = full ? row['integrity_check'] : row['quick_check'];
|
||||
|
||||
if (connection.isInMemory) {
|
||||
return `${integrity} (in-memory!)`;
|
||||
}
|
||||
if (connection.isErroneous) {
|
||||
return `${integrity} (last error: ${connection.lastError})`;
|
||||
}
|
||||
|
||||
return integrity;
|
||||
});
|
||||
});
|
||||
if (connection.isInMemory) {
|
||||
return `${integrity} (in-memory!)`;
|
||||
}
|
||||
|
||||
return integrity;
|
||||
}
|
||||
|
||||
private connect(path: string, retryOnBusy: boolean = true): Promise<IDatabaseConnection> {
|
||||
|
||||
192
src/vs/base/node/watcher.ts
Normal file
192
src/vs/base/node/watcher.ts
Normal 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: '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);
|
||||
});
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import * as nls from 'vs/nls';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import { createWriteStream, WriteStream } from 'fs';
|
||||
import { Readable } from 'stream';
|
||||
import { nfcall, ninvoke, Sequencer, createCancelablePromise } from 'vs/base/common/async';
|
||||
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';
|
||||
@@ -17,7 +17,7 @@ import { Event } from 'vs/base/common/event';
|
||||
export interface IExtractOptions {
|
||||
overwrite?: boolean;
|
||||
|
||||
/**
|
||||
/**
|
||||
* Source path within the ZIP archive. Only the files contained in this
|
||||
* path will be extracted.
|
||||
*/
|
||||
@@ -153,7 +153,7 @@ function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, tok
|
||||
return;
|
||||
}
|
||||
|
||||
const stream = ninvoke(zipfile, zipfile.openReadStream, entry);
|
||||
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));
|
||||
@@ -162,8 +162,27 @@ function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, tok
|
||||
}
|
||||
|
||||
function openZip(zipFile: string, lazy: boolean = false): Promise<ZipFile> {
|
||||
return nfcall<ZipFile>(_openZip, zipFile, lazy ? { lazyEntries: true } : undefined)
|
||||
.then(undefined, err => Promise.reject(toExtractError(err)));
|
||||
return new Promise((resolve, reject) => {
|
||||
_openZip(zipFile, lazy ? { lazyEntries: true } : undefined, (error?: Error, zipfile?: ZipFile) => {
|
||||
if (error) {
|
||||
reject(toExtractError(error));
|
||||
} else {
|
||||
resolve(zipfile);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function openZipStream(zipFile: ZipFile, entry: Entry): Promise<Readable> {
|
||||
return new Promise((resolve, reject) => {
|
||||
zipFile.openReadStream(entry, (error?: Error, stream?: Readable) => {
|
||||
if (error) {
|
||||
reject(toExtractError(error));
|
||||
} else {
|
||||
resolve(stream);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export interface IFile {
|
||||
@@ -210,7 +229,7 @@ function read(zipPath: string, filePath: string): Promise<Readable> {
|
||||
return new Promise<Readable>((c, e) => {
|
||||
zipfile.on('entry', (entry: Entry) => {
|
||||
if (entry.fileName === filePath) {
|
||||
ninvoke<Readable>(zipfile, zipfile.openReadStream, entry).then(stream => c(stream), err => e(err));
|
||||
openZipStream(zipfile, entry).then(stream => c(stream), err => e(err));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -224,7 +243,7 @@ export function buffer(zipPath: string, filePath: string): Promise<Buffer> {
|
||||
return new Promise<Buffer>((c, e) => {
|
||||
const buffers: Buffer[] = [];
|
||||
stream.once('error', e);
|
||||
stream.on('data', b => buffers.push(b as Buffer));
|
||||
stream.on('data', (b: Buffer) => buffers.push(b));
|
||||
stream.on('end', () => c(Buffer.concat(buffers)));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user