diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index 7794ebf1017..d22fc2749b4 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -45,6 +45,13 @@ export enum RimRafMode { MOVE } +/** + * Allows to delete the provied path (either file or folder) recursively + * with the options: + * - `UNLINK`: direct removal from disk + * - `MOVE`: faster variant that first moves the target to temp dir and then + * deletes it in the background without waiting for that to finish. + */ export async function rimraf(path: string, mode = RimRafMode.UNLINK): Promise { if (isRootOrDriveLetter(path)) { throw new Error('rimraf - will refuse to recursively delete root'); @@ -93,44 +100,55 @@ export function rimrafSync(path: string): void { //#region readdir with NFC support (macos) -export async function readdir(path: string): Promise { - return handleDirectoryChildren(await fs.promises.readdir(path)); -} - -export async function readdirWithFileTypes(path: string): Promise { - const children = await fs.promises.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 (isMacintosh) { - for (const child of children) { - child.name = normalizeNFC(child.name); - } - } - - return children; +/** + * Drop-in replacement of `fs.readdir` with support + * for converting from macOS NFD unicon form to NFC + * (https://github.com/nodejs/node/issues/2165) + */ +export async function readdir(path: string): Promise; +export async function readdir(path: string, options: { withFileTypes: true }): Promise; +export async function readdir(path: string, options?: { withFileTypes: true }): Promise<(string | fs.Dirent)[]> { + return handleDirectoryChildren(await (options ? fs.promises.readdir(path, options) : fs.promises.readdir(path))); } +/** + * Drop-in replacement of `fs.readdirSync` with support + * for converting from macOS NFD unicon form to NFC + * (https://github.com/nodejs/node/issues/2165) + */ 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 (isMacintosh) { - return children.map(child => normalizeNFC(child)); - } +function handleDirectoryChildren(children: string[]): string[]; +function handleDirectoryChildren(children: fs.Dirent[]): fs.Dirent[]; +function handleDirectoryChildren(children: (string | fs.Dirent)[]): (string | fs.Dirent)[]; +function handleDirectoryChildren(children: (string | fs.Dirent)[]): (string | fs.Dirent)[] { + return children.map(child => { - return children; + // Mac: uses NFD unicode form on disk, but we want NFC + // See also https://github.com/nodejs/node/issues/2165 + + if (typeof child === 'string') { + return isMacintosh ? normalizeNFC(child) : child; + } + + child.name = isMacintosh ? normalizeNFC(child.name) : child.name; + + return child; + }); } +/** + * A convinience method to read all children of a path that + * are directories. + */ export async function readDirsInDir(dirPath: string): Promise { const children = await readdir(dirPath); const directories: string[] = []; for (const child of children) { - if (await SymlinkSupport.dirExists(join(dirPath, child))) { + if (await SymlinkSupport.existsDirectory(join(dirPath, child))) { directories.push(child); } } @@ -142,7 +160,11 @@ export async function readDirsInDir(dirPath: string): Promise { //#region whenDeleted() -export function whenDeleted(path: string): Promise { +/** + * A `Promise` that resolves when the provided `path` + * is deleted from disk. + */ +export function whenDeleted(path: string, intervalMs = 1000): Promise { // Complete when wait marker file is deleted return new Promise(resolve => { @@ -159,7 +181,7 @@ export function whenDeleted(path: string): Promise { } }); } - }, 1000); + }, intervalMs); }); } @@ -243,7 +265,17 @@ export namespace SymlinkSupport { } } - export async function fileExists(path: string): Promise { + /** + * Figures out if the `path` exists and is a file with support + * for symlinks. + * + * Note: this will return `false` for a symlink that exists on + * disk but is dangling (pointing to a non-existing path). + * + * Use `exists` if you only care about the path existing on disk + * or not without support for symbolic links. + */ + export async function existsFile(path: string): Promise { try { const { stat, symbolicLink } = await SymlinkSupport.stat(path); @@ -255,7 +287,17 @@ export namespace SymlinkSupport { return false; } - export async function dirExists(path: string): Promise { + /** + * Figures out if the `path` exists and is a directory with support for + * symlinks. + * + * Note: this will return `false` for a symlink that exists on + * disk but is dangling (pointing to a non-existing path). + * + * Use `exists` if you only care about the path existing on disk + * or not without support for symbolic links. + */ + export async function existsDirectory(path: string): Promise { try { const { stat, symbolicLink } = await SymlinkSupport.stat(path); @@ -272,11 +314,13 @@ export namespace SymlinkSupport { //#region Write File -// 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> = new Map(); - +/** + * Same as `fs.writeFile` but with an additional call to + * `fs.fdatasync` after writing to ensure changes are + * flushed to disk. + * + * In addition, multiple writes to the same path are queued. + */ export function writeFile(path: string, data: string, options?: IWriteFileOptions): Promise; export function writeFile(path: string, data: Buffer, options?: IWriteFileOptions): Promise; export function writeFile(path: string, data: Uint8Array, options?: IWriteFileOptions): Promise; @@ -291,6 +335,11 @@ export function writeFile(path: string, data: string | Buffer | Uint8Array, opti }); } +// 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> = new Map(); + function toQueueKey(path: string): string { let queueKey = path; if (isWindows || isMacintosh) { @@ -368,6 +417,11 @@ function doWriteFileAndFlush(path: string, data: string | Buffer | Uint8Array, o }); } +/** + * Same as `fs.writeFileSync` but with an additional call to + * `fs.fdatasyncSync` after writing to ensure changes are + * flushed to disk. + */ export function writeFileSync(path: string, data: string | Buffer, options?: IWriteFileOptions): void { const ensuredOptions = ensureWriteOptions(options); @@ -410,6 +464,11 @@ function ensureWriteOptions(options?: IWriteFileOptions): IEnsuredWriteFileOptio //#region Move / Copy +/** + * A drop-in replacement for `fs.rename` that: + * - updates the `mtime` of the `source` after the operation + * - allows to move across multiple disks + */ export async function move(source: string, target: string): Promise { if (source === target) { return; // simulate node.js behaviour here and do a no-op if paths match @@ -464,6 +523,16 @@ export async function move(source: string, target: string): Promise { } } +/** + * Recursively copies all of `source` to `target`. + * + * Note: symbolic links are currently not preserved but followed and copies + * as files and folders. + */ +export async function copy(source: string, target: string): Promise { + return doCopy(source, target); +} + // When copying a file or folder, we want to preserve the mode // it had and as such provide it when creating. However, modes // can go beyond what we expect (see link below), so we mask it. @@ -473,7 +542,7 @@ export async function move(source: string, target: string): Promise { // it's implementation and check wether this mask is still needed. const COPY_MODE_MASK = 0o777; -export async function copy(source: string, target: string, handledSourcesIn?: { [path: string]: boolean }): Promise { +async function doCopy(source: string, target: string, handledSourcesIn?: { [path: string]: boolean }): Promise { // Keep track of paths already copied to prevent // cycles from symbolic links to cause issues @@ -500,7 +569,7 @@ export async function copy(source: string, target: string, handledSourcesIn?: { 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), handledSources); + await doCopy(join(source, file), join(target, file), handledSources); } } diff --git a/src/vs/base/node/powershell.ts b/src/vs/base/node/powershell.ts index f99e1162de9..e2c804344b7 100644 --- a/src/vs/base/node/powershell.ts +++ b/src/vs/base/node/powershell.ts @@ -38,7 +38,7 @@ class PossiblePowerShellExe implements IPossiblePowerShellExe { public async exists(): Promise { if (this.knownToExist === undefined) { - this.knownToExist = await pfs.SymlinkSupport.fileExists(this.exePath); + this.knownToExist = await pfs.SymlinkSupport.existsFile(this.exePath); } return this.knownToExist; } @@ -100,7 +100,7 @@ async function findPSCoreWindowsInstallation( const powerShellInstallBaseDir = path.join(programFilesPath, 'PowerShell'); // Ensure the base directory exists - if (!await pfs.SymlinkSupport.dirExists(powerShellInstallBaseDir)) { + if (!await pfs.SymlinkSupport.existsDirectory(powerShellInstallBaseDir)) { return null; } @@ -142,7 +142,7 @@ async function findPSCoreWindowsInstallation( // Now look for the file const exePath = path.join(powerShellInstallBaseDir, item, 'pwsh.exe'); - if (!await pfs.SymlinkSupport.fileExists(exePath)) { + if (!await pfs.SymlinkSupport.existsFile(exePath)) { continue; } @@ -169,7 +169,7 @@ async function findPSCoreMsix({ findPreview }: { findPreview?: boolean } = {}): // Find the base directory for MSIX application exe shortcuts const msixAppDir = path.join(env.LOCALAPPDATA, 'Microsoft', 'WindowsApps'); - if (!await pfs.SymlinkSupport.dirExists(msixAppDir)) { + if (!await pfs.SymlinkSupport.existsDirectory(msixAppDir)) { return null; } diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index 2c1a25ad725..817e3c369e5 100644 --- a/src/vs/base/test/node/pfs/pfs.test.ts +++ b/src/vs/base/test/node/pfs/pfs.test.ts @@ -8,7 +8,7 @@ import * as fs from 'fs'; import { tmpdir } from 'os'; import { join, sep } from 'vs/base/common/path'; import { generateUuid } from 'vs/base/common/uuid'; -import { copy, exists, move, readdir, readDirsInDir, readdirWithFileTypes, rimraf, RimRafMode, rimrafSync, SymlinkSupport, writeFile, writeFileSync } from 'vs/base/node/pfs'; +import { copy, exists, move, readdir, readDirsInDir, rimraf, RimRafMode, rimrafSync, SymlinkSupport, writeFile, writeFileSync } from 'vs/base/node/pfs'; import { timeout } from 'vs/base/common/async'; import { getPathFromAmdModule } from 'vs/base/common/amd'; import { canNormalize } from 'vs/base/common/normalization'; @@ -276,7 +276,7 @@ flakySuite('PFS', function () { } }); - test('readdirWithFileTypes', async () => { + test('readdir (with file types)', async () => { if (canNormalize && typeof process.versions['electron'] !== 'undefined' /* needs electron */) { const newDir = join(testDir, 'öäü'); await fs.promises.mkdir(newDir, { recursive: true }); @@ -285,7 +285,7 @@ flakySuite('PFS', function () { assert.ok(fs.existsSync(newDir)); - const children = await readdirWithFileTypes(testDir); + const children = await readdir(testDir, { withFileTypes: true }); assert.strictEqual(children.some(n => n.name === 'öäü'), true); // Mac always converts to NFD, so assert.strictEqual(children.some(n => n.isDirectory()), true); diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index 4544c8f4edd..74787b6d3e7 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -10,7 +10,7 @@ import { FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, File import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { isLinux, isWindows } from 'vs/base/common/platform'; -import { SymlinkSupport, move, copy, rimraf, RimRafMode, exists, readdirWithFileTypes } from 'vs/base/node/pfs'; +import { SymlinkSupport, move, copy, rimraf, RimRafMode, exists, readdir } from 'vs/base/node/pfs'; import { normalize, basename, dirname } from 'vs/base/common/path'; import { joinPath } from 'vs/base/common/resources'; import { isEqual } from 'vs/base/common/extpath'; @@ -95,7 +95,7 @@ export class DiskFileSystemProvider extends Disposable implements async readdir(resource: URI): Promise<[string, FileType][]> { try { - const children = await readdirWithFileTypes(this.toFilePath(resource)); + const children = await readdir(this.toFilePath(resource), { withFileTypes: true }); const result: [string, FileType][] = []; await Promise.all(children.map(async child => { diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 97b12c744be..406fd62d878 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -261,7 +261,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain const paths = await this.dialogMainService.pickFileFolder(options); if (paths) { this.sendPickerTelemetry(paths, options.telemetryEventName || 'openFileFolder', options.telemetryExtraData); - this.doOpenPicked(await Promise.all(paths.map(async path => (await SymlinkSupport.dirExists(path)) ? { folderUri: URI.file(path) } : { fileUri: URI.file(path) })), options, windowId); + this.doOpenPicked(await Promise.all(paths.map(async path => (await SymlinkSupport.existsDirectory(path)) ? { folderUri: URI.file(path) } : { fileUri: URI.file(path) })), options, windowId); } } diff --git a/src/vs/workbench/api/node/extHostOutputService.ts b/src/vs/workbench/api/node/extHostOutputService.ts index 2204c403d32..d18f945141c 100644 --- a/src/vs/workbench/api/node/extHostOutputService.ts +++ b/src/vs/workbench/api/node/extHostOutputService.ts @@ -86,7 +86,7 @@ export class ExtHostOutputService2 extends ExtHostOutputService { private async _doCreateOutChannel(name: string): Promise { try { const outputDirPath = join(this._logsLocation.fsPath, `output_logging_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`); - const exists = await SymlinkSupport.dirExists(outputDirPath); + const exists = await SymlinkSupport.existsDirectory(outputDirPath); if (!exists) { await promises.mkdir(outputDirPath, { recursive: true }); } diff --git a/src/vs/workbench/contrib/terminal/node/terminal.ts b/src/vs/workbench/contrib/terminal/node/terminal.ts index 8802e61a467..b949c038eac 100644 --- a/src/vs/workbench/contrib/terminal/node/terminal.ts +++ b/src/vs/workbench/contrib/terminal/node/terminal.ts @@ -15,7 +15,7 @@ import { enumeratePowerShellInstallations } from 'vs/base/node/powershell'; let detectedDistro = LinuxDistro.Unknown; if (platform.isLinux) { const file = '/etc/os-release'; - SymlinkSupport.fileExists(file).then(async exists => { + SymlinkSupport.existsFile(file).then(async exists => { if (!exists) { return; } diff --git a/src/vs/workbench/services/extensions/node/extensionPoints.ts b/src/vs/workbench/services/extensions/node/extensionPoints.ts index 6b2890ada83..5506bddc81d 100644 --- a/src/vs/workbench/services/extensions/node/extensionPoints.ts +++ b/src/vs/workbench/services/extensions/node/extensionPoints.ts @@ -145,7 +145,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { return { values: undefined, default: `${basename}.nls.json` }; }); } else { - localizedMessages = pfs.SymlinkSupport.fileExists(basename + '.nls' + extension).then(exists => { + localizedMessages = pfs.SymlinkSupport.existsFile(basename + '.nls' + extension).then(exists => { if (!exists) { return undefined; } @@ -221,7 +221,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { return new Promise<{ localized: string; original: string | null; }>((c, e) => { function loop(basename: string, locale: string): void { let toCheck = `${basename}.nls.${locale}.json`; - pfs.SymlinkSupport.fileExists(toCheck).then(exists => { + pfs.SymlinkSupport.existsFile(toCheck).then(exists => { if (exists) { c({ localized: toCheck, original: `${basename}.nls.json` }); } @@ -598,7 +598,7 @@ export class ExtensionScanner { const isBuiltin = input.isBuiltin; const isUnderDevelopment = input.isUnderDevelopment; - return pfs.SymlinkSupport.fileExists(path.join(absoluteFolderPath, MANIFEST_FILE)).then((exists) => { + return pfs.SymlinkSupport.existsFile(path.join(absoluteFolderPath, MANIFEST_FILE)).then((exists) => { if (exists) { const nlsConfig = ExtensionScannerInput.createNLSConfig(input); return this.scanExtension(input.ourVersion, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig).then((extensionDescription) => { diff --git a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts index 0f2b63541b1..1935dbb5adc 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts @@ -879,7 +879,7 @@ suite('ExtHostSearch', () => { }); test('basic sibling clause', async () => { - mockPFS.readdir = (_path: string) => { + mockPFS.readdir = (_path: string): any => { if (_path === rootFolderA.fsPath) { return Promise.resolve([ 'file1.js', @@ -922,7 +922,7 @@ suite('ExtHostSearch', () => { }); test('multiroot sibling clause', async () => { - mockPFS.readdir = (_path: string) => { + mockPFS.readdir = (_path: string): any => { if (_path === joinPath(rootFolderA, 'folder').fsPath) { return Promise.resolve([ 'fileA.scss',