From a0d76bb9834b63a02fba8017a6306511fe1ab4fe Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 2 Feb 2021 15:04:36 +0100 Subject: [PATCH 01/31] fs - drop pfs.mkdirp in favor of node.js --- src/vs/base/node/pfs.ts | 6 +---- src/vs/base/node/zip.ts | 8 +++---- .../parts/storage/test/node/storage.test.ts | 7 +++--- src/vs/base/test/node/crypto.test.ts | 5 +++-- src/vs/base/test/node/extpath.test.ts | 5 +++-- src/vs/base/test/node/pfs/pfs.test.ts | 22 ++++++------------- src/vs/base/test/node/zip/zip.test.ts | 5 +++-- src/vs/code/electron-main/main.ts | 5 ++--- src/vs/code/node/cliProcessMain.ts | 4 ++-- .../electron-main/backupMainService.test.ts | 6 ++--- src/vs/platform/state/test/node/state.test.ts | 5 +++-- .../platform/storage/node/storageService.ts | 5 +++-- .../storage/test/node/storageService.test.ts | 5 +++-- .../electron-main/updateService.win32.ts | 2 +- .../workspacesManagementMainService.ts | 6 ++--- .../workspacesManagementMainService.test.ts | 2 +- .../api/node/extHostOutputService.ts | 5 +++-- .../electron-browser/backupTracker.test.ts | 7 +++--- .../backupFileService.test.ts | 8 +++---- .../cachedExtensionScanner.ts | 3 ++- .../electron-browser/commonProperties.test.ts | 21 +++++++++--------- 21 files changed, 70 insertions(+), 72 deletions(-) diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index 34e136962f1..443c16dfcbb 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -288,10 +288,6 @@ export function readFile(path: string, encoding?: string): Promise { - return promisify(fs.mkdir)(path, { mode, recursive: true }); -} - // According to node.js docs (https://nodejs.org/docs/v6.5.0/api/fs.html#fs_fs_writefile_file_data_options_callback) // it is not safe to call writeFile() on the same path multiple times without waiting for the callback to return. // Therefor we use a Queue on the path that is given to us to sequentialize calls to the same path properly. @@ -558,7 +554,7 @@ export async function copy(source: string, target: string, handledSourcesIn?: { } // Create folder - await mkdirp(target, stat.mode & COPY_MODE_MASK); + await fs.promises.mkdir(target, { recursive: true, mode: stat.mode & COPY_MODE_MASK }); // Copy each file recursively const files = await readdir(source); diff --git a/src/vs/base/node/zip.ts b/src/vs/base/node/zip.ts index 4d98c0f29ce..0d74501d997 100644 --- a/src/vs/base/node/zip.ts +++ b/src/vs/base/node/zip.ts @@ -5,10 +5,10 @@ import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; -import { createWriteStream, WriteStream } from 'fs'; +import { promises, createWriteStream, WriteStream } from 'fs'; import { Readable } from 'stream'; import { Sequencer, createCancelablePromise } from 'vs/base/common/async'; -import { mkdirp, rimraf } from 'vs/base/node/pfs'; +import { rimraf } from 'vs/base/node/pfs'; import { open as _openZip, Entry, ZipFile } from 'yauzl'; import * as yazl from 'yazl'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -86,7 +86,7 @@ function extractEntry(stream: Readable, fileName: string, mode: number, targetPa } }); - return Promise.resolve(mkdirp(targetDirName)).then(() => new Promise((c, e) => { + return Promise.resolve(promises.mkdir(targetDirName, { recursive: true })).then(() => new Promise((c, e) => { if (token.isCancellationRequested) { return; } @@ -149,7 +149,7 @@ function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, tok // directory file names end with '/' if (/\/$/.test(fileName)) { const targetFileName = path.join(targetPath, fileName); - last = createCancelablePromise(token => mkdirp(targetFileName).then(() => readNextEntry(token)).then(undefined, e)); + last = createCancelablePromise(token => promises.mkdir(targetFileName, { recursive: true }).then(() => readNextEntry(token)).then(undefined, e)); return; } diff --git a/src/vs/base/parts/storage/test/node/storage.test.ts b/src/vs/base/parts/storage/test/node/storage.test.ts index 69e4d180080..9dffb5329ae 100644 --- a/src/vs/base/parts/storage/test/node/storage.test.ts +++ b/src/vs/base/parts/storage/test/node/storage.test.ts @@ -7,8 +7,9 @@ import { SQLiteStorageDatabase, ISQLiteStorageDatabaseOptions } from 'vs/base/pa import { Storage, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/parts/storage/common/storage'; import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; +import { promises } from 'fs'; import { strictEqual, ok } from 'assert'; -import { mkdirp, writeFile, exists, unlink, rimraf } from 'vs/base/node/pfs'; +import { writeFile, exists, unlink, rimraf } from 'vs/base/node/pfs'; import { timeout } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import { isWindows } from 'vs/base/common/platform'; @@ -22,7 +23,7 @@ flakySuite('Storage Library', function () { setup(function () { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storagelibrary'); - return mkdirp(testDir); + return promises.mkdir(testDir, { recursive: true }); }); teardown(function () { @@ -294,7 +295,7 @@ flakySuite('SQLite Storage Library', function () { setup(function () { testdir = getRandomTestPath(tmpdir(), 'vsctests', 'storagelibrary'); - return mkdirp(testdir); + return promises.mkdir(testdir, { recursive: true }); }); teardown(function () { diff --git a/src/vs/base/test/node/crypto.test.ts b/src/vs/base/test/node/crypto.test.ts index 16cfc58fe26..cb185077fc2 100644 --- a/src/vs/base/test/node/crypto.test.ts +++ b/src/vs/base/test/node/crypto.test.ts @@ -6,7 +6,8 @@ import { checksum } from 'vs/base/node/crypto'; import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { mkdirp, rimraf, writeFile } from 'vs/base/node/pfs'; +import { promises } from 'fs'; +import { rimraf, writeFile } from 'vs/base/node/pfs'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; suite('Crypto', () => { @@ -16,7 +17,7 @@ suite('Crypto', () => { setup(function () { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'crypto'); - return mkdirp(testDir); + return promises.mkdir(testDir, { recursive: true }); }); teardown(function () { diff --git a/src/vs/base/test/node/extpath.test.ts b/src/vs/base/test/node/extpath.test.ts index 056110be8f8..a1c8a7f94d2 100644 --- a/src/vs/base/test/node/extpath.test.ts +++ b/src/vs/base/test/node/extpath.test.ts @@ -5,7 +5,8 @@ import * as assert from 'assert'; import { tmpdir } from 'os'; -import { mkdirp, rimraf } from 'vs/base/node/pfs'; +import { promises } from 'fs'; +import { rimraf } from 'vs/base/node/pfs'; import { realcaseSync, realpath, realpathSync } from 'vs/base/node/extpath'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; @@ -15,7 +16,7 @@ flakySuite('Extpath', () => { setup(() => { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'extpath'); - return mkdirp(testDir, 493); + return promises.mkdir(testDir, { recursive: true }); }); teardown(() => { diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index eeb380a62fe..770a60615ca 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, mkdirp, move, readdir, readDirsInDir, readdirWithFileTypes, readFile, renameIgnoreError, rimraf, RimRafMode, rimrafSync, statLink, writeFile, writeFileSync } from 'vs/base/node/pfs'; +import { copy, exists, move, readdir, readDirsInDir, readdirWithFileTypes, readFile, renameIgnoreError, rimraf, RimRafMode, rimrafSync, statLink, 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'; @@ -22,7 +22,7 @@ flakySuite('PFS', function () { setup(() => { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'pfs'); - return mkdirp(testDir, 493); + return fs.promises.mkdir(testDir, { recursive: true }); }); teardown(() => { @@ -204,7 +204,7 @@ flakySuite('PFS', function () { const id3 = generateUuid(); const copyTarget = join(testDir, id3); - await mkdirp(symbolicLinkTarget, 493); + await fs.promises.mkdir(symbolicLinkTarget, { recursive: true }); fs.symlinkSync(symbolicLinkTarget, symbolicLink, 'junction'); @@ -215,14 +215,6 @@ flakySuite('PFS', function () { assert.ok(!fs.existsSync(copyTarget)); }); - test('mkdirp', async () => { - const newDir = join(testDir, generateUuid()); - - await mkdirp(newDir, 493); - - assert.ok(fs.existsSync(newDir)); - }); - test('readDirsInDir', async () => { fs.mkdirSync(join(testDir, 'somefolder1')); fs.mkdirSync(join(testDir, 'somefolder2')); @@ -244,7 +236,7 @@ flakySuite('PFS', function () { const id2 = generateUuid(); const symbolicLink = join(testDir, id2); - await mkdirp(directory, 493); + await fs.promises.mkdir(directory, { recursive: true }); fs.symlinkSync(directory, symbolicLink, 'junction'); @@ -263,7 +255,7 @@ flakySuite('PFS', function () { const id2 = generateUuid(); const symbolicLink = join(testDir, id2); - await mkdirp(directory, 493); + await fs.promises.mkdir(directory, { recursive: true }); fs.symlinkSync(directory, symbolicLink, 'junction'); @@ -279,7 +271,7 @@ flakySuite('PFS', function () { const id = generateUuid(); const newDir = join(testDir, 'pfs', id, 'öäü'); - await mkdirp(newDir, 493); + await fs.promises.mkdir(newDir, { recursive: true }); assert.ok(fs.existsSync(newDir)); @@ -291,7 +283,7 @@ flakySuite('PFS', function () { test('readdirWithFileTypes', async () => { if (canNormalize && typeof process.versions['electron'] !== 'undefined' /* needs electron */) { const newDir = join(testDir, 'öäü'); - await mkdirp(newDir, 493); + await fs.promises.mkdir(newDir, { recursive: true }); await writeFile(join(testDir, 'somefile.txt'), 'contents'); diff --git a/src/vs/base/test/node/zip/zip.test.ts b/src/vs/base/test/node/zip/zip.test.ts index a98b2609fbb..360f7dfd04b 100644 --- a/src/vs/base/test/node/zip/zip.test.ts +++ b/src/vs/base/test/node/zip/zip.test.ts @@ -6,8 +6,9 @@ import * as assert from 'assert'; import * as path from 'vs/base/common/path'; import { tmpdir } from 'os'; +import { promises } from 'fs'; import { extract } from 'vs/base/node/zip'; -import { rimraf, exists, mkdirp } from 'vs/base/node/pfs'; +import { rimraf, exists } from 'vs/base/node/pfs'; import { getPathFromAmdModule } from 'vs/base/common/amd'; import { createCancelablePromise } from 'vs/base/common/async'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; @@ -19,7 +20,7 @@ suite('Zip', () => { setup(() => { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'zip'); - return mkdirp(testDir); + return promises.mkdir(testDir, { recursive: true }); }); teardown(() => { diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index f0d8b6d2dd0..2a01fa11d0c 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -5,13 +5,12 @@ import 'vs/platform/update/common/update.config.contribution'; import { app, dialog } from 'electron'; -import { unlinkSync } from 'fs'; +import { promises, unlinkSync } from 'fs'; import { localize } from 'vs/nls'; import { isWindows, IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; import product from 'vs/platform/product/common/product'; import { parseMainProcessArgv, addArg } from 'vs/platform/environment/node/argvHelper'; import { createWaitMarkerFile } from 'vs/platform/environment/node/waitMarkerFile'; -import { mkdirp } from 'vs/base/node/pfs'; import { LifecycleMainService, ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { createChannelSender } from 'vs/base/parts/ipc/common/ipc'; import { Server as NodeIPCServer, serve as nodeIPCServe, connect as nodeIPCConnect, XDG_RUNTIME_DIR } from 'vs/base/parts/ipc/node/ipc.net'; @@ -186,7 +185,7 @@ class CodeMain { environmentService.globalStorageHome.fsPath, environmentService.workspaceStorageHome.fsPath, environmentService.backupHome - ].map((path): undefined | Promise => path ? mkdirp(path) : undefined)); + ].map((path): undefined | Promise => path ? promises.mkdir(path, { recursive: true }) : undefined)); // Configuration service const configurationServiceInitialization = configurationService.initialize(); diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index ee189f83229..0adac0a9244 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -28,7 +28,7 @@ import { RequestService } from 'vs/platform/request/node/requestService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; -import { mkdirp, writeFile } from 'vs/base/node/pfs'; +import { writeFile } from 'vs/base/node/pfs'; import { IStateService } from 'vs/platform/state/node/state'; import { StateService } from 'vs/platform/state/node/stateService'; import { ILogService, getLogLevel, LogLevel, ConsoleLogService, MultiplexLogService } from 'vs/platform/log/common/log'; @@ -99,7 +99,7 @@ class CliMain extends Disposable { services.set(INativeEnvironmentService, environmentService); // Init folders - await Promise.all([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath].map(path => path ? mkdirp(path) : undefined)); + await Promise.all([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath].map(path => path ? fs.promises.mkdir(path, { recursive: true }) : undefined)); // Log const logLevel = getLogLevel(environmentService); diff --git a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index 75b2cac3755..dbde7225804 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -106,7 +106,7 @@ flakySuite('BackupMainService', () => { environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS)); - await pfs.mkdirp(backupHome); + await fs.promises.mkdir(backupHome, { recursive: true }); configService = new TestConfigurationService(); service = new class TestBackupMainService extends BackupMainService { @@ -669,8 +669,8 @@ flakySuite('BackupMainService', () => { assert.strictEqual(((await service.getDirtyWorkspaces()).length), 0); try { - await pfs.mkdirp(path.join(folderBackupPath, Schemas.file)); - await pfs.mkdirp(path.join(workspaceBackupPath, Schemas.untitled)); + await fs.promises.mkdir(path.join(folderBackupPath, Schemas.file), { recursive: true }); + await fs.promises.mkdir(path.join(workspaceBackupPath, Schemas.untitled), { recursive: true }); } catch (error) { // ignore - folder might exist already } diff --git a/src/vs/platform/state/test/node/state.test.ts b/src/vs/platform/state/test/node/state.test.ts index dc4acc6aebe..01d2b4e3942 100644 --- a/src/vs/platform/state/test/node/state.test.ts +++ b/src/vs/platform/state/test/node/state.test.ts @@ -5,10 +5,11 @@ import * as assert from 'assert'; import { tmpdir } from 'os'; +import { promises } from 'fs'; import { join } from 'vs/base/common/path'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { FileStorage } from 'vs/platform/state/node/stateService'; -import { mkdirp, rimraf, writeFileSync } from 'vs/base/node/pfs'; +import { rimraf, writeFileSync } from 'vs/base/node/pfs'; flakySuite('StateService', () => { @@ -17,7 +18,7 @@ flakySuite('StateService', () => { setup(() => { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'stateservice'); - return mkdirp(testDir); + return promises.mkdir(testDir, { recursive: true }); }); teardown(() => { diff --git a/src/vs/platform/storage/node/storageService.ts b/src/vs/platform/storage/node/storageService.ts index b50c1481c87..c469537d3a1 100644 --- a/src/vs/platform/storage/node/storageService.ts +++ b/src/vs/platform/storage/node/storageService.ts @@ -10,7 +10,8 @@ import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/ import { Storage, IStorageDatabase, IStorage, StorageHint } from 'vs/base/parts/storage/common/storage'; import { mark } from 'vs/base/common/performance'; import { join } from 'vs/base/common/path'; -import { copy, exists, mkdirp, writeFile } from 'vs/base/node/pfs'; +import { copy, exists, writeFile } from 'vs/base/node/pfs'; +import { promises } from 'fs'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; import { assertIsDefined } from 'vs/base/common/types'; @@ -138,7 +139,7 @@ export class NativeStorageService extends AbstractStorageService { return { path: workspaceStorageFolderPath, wasCreated: false }; } - await mkdirp(workspaceStorageFolderPath); + await promises.mkdir(workspaceStorageFolderPath, { recursive: true }); // Write metadata into folder this.ensureWorkspaceStorageFolderMeta(payload); diff --git a/src/vs/platform/storage/test/node/storageService.test.ts b/src/vs/platform/storage/test/node/storageService.test.ts index 6677ab24d58..fed89b3eda6 100644 --- a/src/vs/platform/storage/test/node/storageService.test.ts +++ b/src/vs/platform/storage/test/node/storageService.test.ts @@ -7,7 +7,8 @@ import { strictEqual } from 'assert'; import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { NativeStorageService } from 'vs/platform/storage/node/storageService'; import { tmpdir } from 'os'; -import { mkdirp, rimraf } from 'vs/base/node/pfs'; +import { promises } from 'fs'; +import { rimraf } from 'vs/base/node/pfs'; import { NullLogService } from 'vs/platform/log/common/log'; import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; @@ -22,7 +23,7 @@ flakySuite('NativeStorageService', function () { setup(() => { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice'); - return mkdirp(testDir); + return promises.mkdir(testDir, { recursive: true }); }); teardown(() => { diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index d1c834165b4..5f069c9ca81 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -56,7 +56,7 @@ export class Win32UpdateService extends AbstractUpdateService { @memoize get cachePath(): Promise { const result = path.join(tmpdir(), `vscode-update-${product.target}-${process.arch}`); - return pfs.mkdirp(result).then(() => result); + return fs.promises.mkdir(result, { recursive: true }).then(() => result); } constructor( diff --git a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts index 306d1daa5c0..c9d22ba0af3 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts @@ -6,8 +6,8 @@ import { toWorkspaceFolders, IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IUntitledWorkspaceInfo, getStoredWorkspaceFolder, IEnterWorkspaceResult, isUntitledWorkspace, isWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { join, dirname } from 'vs/base/common/path'; -import { mkdirp, writeFile, rimrafSync, readdirSync, writeFileSync } from 'vs/base/node/pfs'; -import { readFileSync, existsSync, mkdirSync, statSync, Stats } from 'fs'; +import { writeFile, rimrafSync, readdirSync, writeFileSync } from 'vs/base/node/pfs'; +import { promises, readFileSync, existsSync, mkdirSync, statSync, Stats } from 'fs'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { Event, Emitter } from 'vs/base/common/event'; import { ILogService } from 'vs/platform/log/common/log'; @@ -140,7 +140,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority); const configPath = workspace.configPath.fsPath; - await mkdirp(dirname(configPath)); + await promises.mkdir(dirname(configPath), { recursive: true }); await writeFile(configPath, JSON.stringify(storedWorkspace, null, '\t')); return workspace; diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts index 8c12101c15f..fc4b803c38a 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts @@ -99,7 +99,7 @@ suite('WorkspacesManagementMainService', () => { service = new WorkspacesManagementMainService(environmentService, new NullLogService(), new TestBackupMainService(), new TestDialogMainService()); - return pfs.mkdirp(untitledWorkspacesHomePath); + return fs.promises.mkdir(untitledWorkspacesHomePath, { recursive: true }); }); teardown(() => { diff --git a/src/vs/workbench/api/node/extHostOutputService.ts b/src/vs/workbench/api/node/extHostOutputService.ts index aad00356e37..358507bdf22 100644 --- a/src/vs/workbench/api/node/extHostOutputService.ts +++ b/src/vs/workbench/api/node/extHostOutputService.ts @@ -9,7 +9,8 @@ import { URI } from 'vs/base/common/uri'; import { join } from 'vs/base/common/path'; import { OutputAppender } from 'vs/workbench/services/output/node/outputAppender'; import { toLocalISOString } from 'vs/base/common/date'; -import { dirExists, mkdirp } from 'vs/base/node/pfs'; +import { dirExists } from 'vs/base/node/pfs'; +import { promises } from 'fs'; import { AbstractExtHostOutputChannel, ExtHostPushOutputChannel, ExtHostOutputService, LazyOutputChannel } from 'vs/workbench/api/common/extHostOutput'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; @@ -87,7 +88,7 @@ export class ExtHostOutputService2 extends ExtHostOutputService { const outputDirPath = join(this._logsLocation.fsPath, `output_logging_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`); const exists = await dirExists(outputDirPath); if (!exists) { - await mkdirp(outputDirPath); + await promises.mkdir(outputDirPath, { recursive: true }); } const fileName = `${this._namePool++}-${name.replace(/[\\/:\*\?"<>\|]/g, '')}`; const file = URI.file(join(outputDirPath, `${fileName}.log`)); diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts index 4206f885950..6a3caeeda6f 100644 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts +++ b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts @@ -6,8 +6,9 @@ import * as assert from 'assert'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { tmpdir } from 'os'; +import { promises } from 'fs'; import { join } from 'vs/base/common/path'; -import { mkdirp, rimraf, writeFile } from 'vs/base/node/pfs'; +import { rimraf, writeFile } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { hashPath } from 'vs/workbench/services/backup/electron-browser/backupFileService'; @@ -106,8 +107,8 @@ flakySuite('BackupTracker (native)', function () { disposables.add(registerTestFileEditor()); - await mkdirp(backupHome); - await mkdirp(workspaceBackupPath); + await promises.mkdir(backupHome, { recursive: true }); + await promises.mkdir(workspaceBackupPath, { recursive: true }); return writeFile(workspacesJsonPath, ''); }); diff --git a/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts index 821ae915ec7..7bdf144cad8 100644 --- a/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts +++ b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts @@ -7,9 +7,9 @@ import * as assert from 'assert'; import { isWindows } from 'vs/base/common/platform'; import { createHash } from 'crypto'; import { tmpdir } from 'os'; -import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { promises, existsSync, readFileSync, writeFileSync } from 'fs'; import { dirname, join } from 'vs/base/common/path'; -import { mkdirp, readdirSync, rimraf, writeFile } from 'vs/base/node/pfs'; +import { readdirSync, rimraf, writeFile } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { BackupFilesModel } from 'vs/workbench/services/backup/common/backupFileService'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; @@ -140,7 +140,7 @@ suite('BackupFileService', () => { service = new NodeTestBackupFileService(testDir, workspaceBackupPath); - await mkdirp(backupHome); + await promises.mkdir(backupHome, { recursive: true }); return writeFile(workspacesJsonPath, ''); }); @@ -639,7 +639,7 @@ suite('BackupFileService', () => { }); test('resolve', async () => { - await mkdirp(dirname(fooBackupPath)); + await promises.mkdir(dirname(fooBackupPath), { recursive: true }); writeFileSync(fooBackupPath, 'foo'); const model = new BackupFilesModel(service.fileService); diff --git a/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts index f974db68e45..5c2f3f57209 100644 --- a/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; import * as errors from 'vs/base/common/errors'; @@ -164,7 +165,7 @@ export class CachedExtensionScanner { const cacheFile = path.join(cacheFolder, cacheKey); try { - await pfs.mkdirp(cacheFolder); + await fs.promises.mkdir(cacheFolder, { recursive: true }); } catch (err) { // That's ok... } diff --git a/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts b/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts index 627abe2546f..258b5005a5a 100644 --- a/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts +++ b/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts @@ -2,14 +2,15 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; -import * as path from 'vs/base/common/path'; -import * as os from 'os'; import * as fs from 'fs'; +import { join } from 'vs/base/common/path'; +import { release, tmpdir } from 'os'; import { resolveWorkbenchCommonProperties } from 'vs/workbench/services/telemetry/electron-sandbox/workbenchCommonProperties'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { IStorageService, StorageScope, InMemoryStorageService, StorageTarget } from 'vs/platform/storage/common/storage'; -import { mkdirp, rimraf } from 'vs/base/node/pfs'; +import { rimraf } from 'vs/base/node/pfs'; import { timeout } from 'vs/base/common/async'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; @@ -18,8 +19,8 @@ import { Schemas } from 'vs/base/common/network'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; suite('Telemetry - common properties', function () { - const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'telemetryservice'); - const installSource = path.join(parentDir, 'installSource'); + const parentDir = getRandomTestPath(tmpdir(), 'vsctests', 'telemetryservice'); + const installSource = join(parentDir, 'installSource'); const commit: string = (undefined)!; const version: string = (undefined)!; @@ -43,9 +44,9 @@ suite('Telemetry - common properties', function () { }); test('default', async function () { - await mkdirp(parentDir); + await fs.promises.mkdir(parentDir, { recursive: true }); fs.writeFileSync(installSource, 'my.install.source'); - const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, os.release(), commit, version, 'someMachineId', undefined, installSource); + const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), commit, version, 'someMachineId', undefined, installSource); assert.ok('commitHash' in props); assert.ok('sessionID' in props); assert.ok('timestamp' in props); @@ -66,7 +67,7 @@ suite('Telemetry - common properties', function () { assert.ok('common.instanceId' in props, 'instanceId'); assert.ok('common.machineId' in props, 'machineId'); fs.unlinkSync(installSource); - const props_1 = await resolveWorkbenchCommonProperties(testStorageService, testFileService, os.release(), commit, version, 'someMachineId', undefined, installSource); + const props_1 = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), commit, version, 'someMachineId', undefined, installSource); assert.ok(!('common.source' in props_1)); }); @@ -74,14 +75,14 @@ suite('Telemetry - common properties', function () { testStorageService.store('telemetry.lastSessionDate', new Date().toUTCString(), StorageScope.GLOBAL, StorageTarget.MACHINE); - const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, os.release(), commit, version, 'someMachineId', undefined, installSource); + const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), commit, version, 'someMachineId', undefined, installSource); assert.ok('common.lastSessionDate' in props); // conditional, see below assert.ok('common.isNewSession' in props); assert.equal(props['common.isNewSession'], 0); }); test('values chance on ask', async function () { - const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, os.release(), commit, version, 'someMachineId', undefined, installSource); + const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), commit, version, 'someMachineId', undefined, installSource); let value1 = props['common.sequence']; let value2 = props['common.sequence']; assert.ok(value1 !== value2, 'seq'); From 970d920f5bd699d8a54e8cc5a5b42bc8efd97f9d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 2 Feb 2021 16:02:40 +0100 Subject: [PATCH 02/31] fs - modernize `move` and add comments --- src/vs/base/node/pfs.ts | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index 443c16dfcbb..80b9f0107be 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -482,23 +482,33 @@ export function whenDeleted(path: string): Promise { export async function move(source: string, target: string): Promise { if (source === target) { - return; + return; // simulate node.js behaviour here and do a no-op if paths match } + // We have been updating `mtime` for move operations since the + // beginning for reasons that are no longer quite clear, but changing + // this could be risky as well. As such, trying to reason about it: + // It is very common as developer to have file watchers enabled that watch + // the current workspace for changes. Updating the `mtime` might make it + // easier for these watchers to recognize an actual change. Since changing + // a source code file also updates the `mtime`, moving a file should do so + // as well because conceptually it is a change of a similar category. async function updateMtime(path: string): Promise { - const stat = await lstat(path); - if (stat.isDirectory() || stat.isSymbolicLink()) { - return; // only for files - } - - const fd = await promisify(fs.open)(path, 'a'); try { - await promisify(fs.futimes)(fd, stat.atime, new Date()); - } catch (error) { - //ignore - } + const stat = await fs.promises.lstat(path); + if (stat.isDirectory() || stat.isSymbolicLink()) { + return; // only for files + } - return promisify(fs.close)(fd); + const fh = await fs.promises.open(path, 'a'); + try { + await fh.utimes(stat.atime, new Date()); + } finally { + await fh.close(); + } + } catch (error) { + // Ignore any error + } } try { From 0677d5e6f6263d8dff861630fefce7b73aac046f Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 2 Feb 2021 17:33:41 +0100 Subject: [PATCH 03/31] fs - sort pfs and introduce SymlinkSupport namespace --- src/vs/base/node/pfs.ts | 355 ++++++++++-------- src/vs/base/node/powershell.ts | 8 +- src/vs/base/test/node/pfs/pfs.test.ts | 8 +- .../backup/electron-main/backupMainService.ts | 16 +- .../files/node/diskFileSystemProvider.ts | 4 +- .../node/watcher/nodejs/watcherService.ts | 4 +- .../electron-main/nativeHostMainService.ts | 4 +- .../api/node/extHostOutputService.ts | 4 +- .../contrib/terminal/node/terminal.ts | 4 +- .../extensions/node/extensionPoints.ts | 6 +- 10 files changed, 228 insertions(+), 185 deletions(-) diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index 80b9f0107be..b71cbd95d71 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -14,6 +14,8 @@ import { isRootOrDriveLetter } from 'vs/base/common/extpath'; import { generateUuid } from 'vs/base/common/uuid'; import { normalizeNFC } from 'vs/base/common/normalization'; +//#region Constants + // See https://github.com/microsoft/vscode/issues/30180 const WIN32_MAX_FILE_SIZE = 300 * 1024 * 1024; // 300 MB const GENERAL_MAX_FILE_SIZE = 16 * 1024 * 1024 * 1024; // 16 GB @@ -25,6 +27,10 @@ const GENERAL_MAX_HEAP_SIZE = 700 * 2 * 1024 * 1024; // 1400 MB export const MAX_FILE_SIZE = process.arch === 'ia32' ? WIN32_MAX_FILE_SIZE : GENERAL_MAX_FILE_SIZE; export const MAX_HEAP_SIZE = process.arch === 'ia32' ? WIN32_MAX_HEAP_SIZE : GENERAL_MAX_HEAP_SIZE; +//#endregion + +//#region rimraf + export enum RimRafMode { /** @@ -142,6 +148,10 @@ export function rimrafSync(path: string): void { } } +//#endregion + +//#region readdir with NFC support (macos) + export async function readdir(path: string): Promise { return handleDirectoryChildren(await promisify(fs.readdir)(path)); } @@ -174,119 +184,148 @@ function handleDirectoryChildren(children: string[]): string[] { return children; } -export function exists(path: string): Promise { - return promisify(fs.exists)(path); -} +export async function readDirsInDir(dirPath: string): Promise { + const children = await readdir(dirPath); + const directories: string[] = []; -export function chmod(path: string, mode: number): Promise { - return promisify(fs.chmod)(path, mode); -} - -export function stat(path: string): Promise { - return promisify(fs.stat)(path); -} - -export interface IStatAndLink { - - // The stats of the file. If the file is a symbolic - // link, the stats will be of that target file and - // not the link itself. - // If the file is a symbolic link pointing to a non - // existing file, the stat will be of the link and - // the `dangling` flag will indicate this. - stat: fs.Stats; - - // Will be provided if the resource is a symbolic link - // on disk. Use the `dangling` flag to find out if it - // points to a resource that does not exist on disk. - symbolicLink?: { dangling: boolean }; -} - -export async function statLink(path: string): Promise { - - // First stat the link - let lstats: fs.Stats | undefined; - try { - lstats = await lstat(path); - - // Return early if the stat is not a symbolic link at all - if (!lstats.isSymbolicLink()) { - return { stat: lstats }; + for (const child of children) { + if (await SymlinkSupport.dirExists(join(dirPath, child))) { + directories.push(child); } - } catch (error) { - /* ignore - use stat() instead */ } - // If the stat is a symbolic link or failed to stat, use fs.stat() - // which for symbolic links will stat the target they point to - try { - const stats = await stat(path); + return directories; +} - return { stat: stats, symbolicLink: lstats?.isSymbolicLink() ? { dangling: false } : undefined }; - } catch (error) { +//#endregion - // If the link points to a non-existing file we still want - // to return it as result while setting dangling: true flag - if (error.code === 'ENOENT' && lstats) { - return { stat: lstats, symbolicLink: { dangling: true } }; - } +export function whenDeleted(path: string): Promise { - // Windows: workaround a node.js bug where reparse points - // are not supported (https://github.com/nodejs/node/issues/36790) - if (isWindows && error.code === 'EACCES' && lstats) { - try { - const stats = await stat(await readlink(path)); + // Complete when wait marker file is deleted + return new Promise(resolve => { + let running = false; + const interval = setInterval(() => { + if (!running) { + running = true; + fs.access(path, err => { + running = false; - return { stat: stats, symbolicLink: lstats.isSymbolicLink() ? { dangling: false } : undefined }; - } catch (error) { - - // If the link points to a non-existing file we still want - // to return it as result while setting dangling: true flag - if (error.code === 'ENOENT') { - return { stat: lstats, symbolicLink: { dangling: true } }; - } - - throw error; + if (err) { + clearInterval(interval); + resolve(undefined); + } + }); } + }, 1000); + }); +} + +//#region Methods with symbolic links support + +export namespace SymlinkSupport { + + export interface IStats { + + // The stats of the file. If the file is a symbolic + // link, the stats will be of that target file and + // not the link itself. + // If the file is a symbolic link pointing to a non + // existing file, the stat will be of the link and + // the `dangling` flag will indicate this. + stat: fs.Stats; + + // Will be provided if the resource is a symbolic link + // on disk. Use the `dangling` flag to find out if it + // points to a resource that does not exist on disk. + symbolicLink?: { dangling: boolean }; + } + + /** + * Resolves the `fs.Stats` of the provided path. If the path is a + * symbolic link, the `fs.Stats` will be from the target it points + * to. If the target does not exist, `dangling: true` will be returned + * as `symbolicLink` value. + */ + export async function stat(path: string): Promise { + + // First stat the link + let lstats: fs.Stats | undefined; + try { + lstats = await lstat(path); + + // Return early if the stat is not a symbolic link at all + if (!lstats.isSymbolicLink()) { + return { stat: lstats }; + } + } catch (error) { + /* ignore - use stat() instead */ } - throw error; + // If the stat is a symbolic link or failed to stat, use fs.stat() + // which for symbolic links will stat the target they point to + try { + const stats = await fs.promises.stat(path); + + return { stat: stats, symbolicLink: lstats?.isSymbolicLink() ? { dangling: false } : undefined }; + } catch (error) { + + // If the link points to a non-existing file we still want + // to return it as result while setting dangling: true flag + if (error.code === 'ENOENT' && lstats) { + return { stat: lstats, symbolicLink: { dangling: true } }; + } + + // Windows: workaround a node.js bug where reparse points + // are not supported (https://github.com/nodejs/node/issues/36790) + if (isWindows && error.code === 'EACCES' && lstats) { + try { + const stats = await fs.promises.stat(await readlink(path)); + + return { stat: stats, symbolicLink: lstats.isSymbolicLink() ? { dangling: false } : undefined }; + } catch (error) { + + // If the link points to a non-existing file we still want + // to return it as result while setting dangling: true flag + if (error.code === 'ENOENT') { + return { stat: lstats, symbolicLink: { dangling: true } }; + } + + throw error; + } + } + + throw error; + } + } + + export async function fileExists(path: string): Promise { + try { + const { stat, symbolicLink } = await SymlinkSupport.stat(path); + + return stat.isFile() && symbolicLink?.dangling !== true; + } catch (error) { + // Ignore, path might not exist + } + + return false; + } + + export async function dirExists(path: string): Promise { + try { + const { stat, symbolicLink } = await SymlinkSupport.stat(path); + + return stat.isDirectory() && symbolicLink?.dangling !== true; + } catch (error) { + // Ignore, path might not exist + } + + return false; } } -export function lstat(path: string): Promise { - return promisify(fs.lstat)(path); -} +//#endregion -export function rename(oldPath: string, newPath: string): Promise { - return promisify(fs.rename)(oldPath, newPath); -} - -export function renameIgnoreError(oldPath: string, newPath: string): Promise { - return new Promise(resolve => fs.rename(oldPath, newPath, () => resolve())); -} - -export function readlink(path: string): Promise { - return promisify(fs.readlink)(path); -} - -export function unlink(path: string): Promise { - return promisify(fs.unlink)(path); -} - -export function symlink(target: string, path: string, type?: string): Promise { - return promisify(fs.symlink)(target, path, type); -} - -export function truncate(path: string, len: number): Promise { - return promisify(fs.truncate)(path, len); -} - -export function readFile(path: string): Promise; -export function readFile(path: string, encoding: string): Promise; -export function readFile(path: string, encoding?: string): Promise { - return promisify(fs.readFile)(path, encoding); -} +//#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. @@ -422,70 +461,16 @@ function ensureWriteOptions(options?: IWriteFileOptions): IEnsuredWriteFileOptio }; } -export async function readDirsInDir(dirPath: string): Promise { - const children = await readdir(dirPath); - const directories: string[] = []; +//#endregion - for (const child of children) { - if (await dirExists(join(dirPath, child))) { - directories.push(child); - } - } - - return directories; -} - -export async function dirExists(path: string): Promise { - try { - const { stat, symbolicLink } = await statLink(path); - - return stat.isDirectory() && symbolicLink?.dangling !== true; - } catch (error) { - // Ignore, path might not exist - } - - return false; -} - -export async function fileExists(path: string): Promise { - try { - const { stat, symbolicLink } = await statLink(path); - - return stat.isFile() && symbolicLink?.dangling !== true; - } catch (error) { - // Ignore, path might not exist - } - - return false; -} - -export function whenDeleted(path: string): Promise { - - // Complete when wait marker file is deleted - return new Promise(resolve => { - let running = false; - const interval = setInterval(() => { - if (!running) { - running = true; - fs.exists(path, exists => { - running = false; - - if (!exists) { - clearInterval(interval); - resolve(undefined); - } - }); - } - }, 1000); - }); -} +//#region Move / Copy 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 } - // We have been updating `mtime` for move operations since the + // We have been updating `mtime` for move operations for files since the // beginning for reasons that are no longer quite clear, but changing // this could be risky as well. As such, trying to reason about it: // It is very common as developer to have file watchers enabled that watch @@ -554,7 +539,7 @@ export async function copy(source: string, target: string, handledSourcesIn?: { handledSources[source] = true; } - const { stat, symbolicLink } = await statLink(source); + const { stat, symbolicLink } = await SymlinkSupport.stat(source); if (symbolicLink?.dangling) { return; // skip over dangling symbolic links (https://github.com/microsoft/vscode/issues/111621) } @@ -605,3 +590,61 @@ async function doCopyFile(source: string, target: string, mode: number): Promise reader.pipe(writer); }); } + +//#endregion + +//#region Async FS Methods + +export async function exists(path: string): Promise { + try { + await fs.promises.access(path); + + return true; + } catch { + return false; + } +} + +export function chmod(path: string, mode: number): Promise { + return promisify(fs.chmod)(path, mode); +} + +export function stat(path: string): Promise { + return promisify(fs.stat)(path); +} + +export function lstat(path: string): Promise { + return promisify(fs.lstat)(path); +} + +export function rename(oldPath: string, newPath: string): Promise { + return promisify(fs.rename)(oldPath, newPath); +} + +export function renameIgnoreError(oldPath: string, newPath: string): Promise { + return new Promise(resolve => fs.rename(oldPath, newPath, () => resolve())); +} + +export function readlink(path: string): Promise { + return promisify(fs.readlink)(path); +} + +export function unlink(path: string): Promise { + return promisify(fs.unlink)(path); +} + +export function symlink(target: string, path: string, type?: string): Promise { + return promisify(fs.symlink)(target, path, type); +} + +export function truncate(path: string, len: number): Promise { + return promisify(fs.truncate)(path, len); +} + +export function readFile(path: string): Promise; +export function readFile(path: string, encoding: string): Promise; +export function readFile(path: string, encoding?: string): Promise { + return promisify(fs.readFile)(path, encoding); +} + +//#endregion diff --git a/src/vs/base/node/powershell.ts b/src/vs/base/node/powershell.ts index 29a7f594452..f99e1162de9 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.fileExists(this.exePath); + this.knownToExist = await pfs.SymlinkSupport.fileExists(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.dirExists(powerShellInstallBaseDir)) { + if (!await pfs.SymlinkSupport.dirExists(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.fileExists(exePath)) { + if (!await pfs.SymlinkSupport.fileExists(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.dirExists(msixAppDir)) { + if (!await pfs.SymlinkSupport.dirExists(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 770a60615ca..31d8e6057ad 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, readFile, renameIgnoreError, rimraf, RimRafMode, rimrafSync, statLink, writeFile, writeFileSync } from 'vs/base/node/pfs'; +import { copy, exists, move, readdir, readDirsInDir, readdirWithFileTypes, readFile, renameIgnoreError, 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'; @@ -240,10 +240,10 @@ flakySuite('PFS', function () { fs.symlinkSync(directory, symbolicLink, 'junction'); - let statAndIsLink = await statLink(directory); + let statAndIsLink = await SymlinkSupport.stat(directory); assert.ok(!statAndIsLink?.symbolicLink); - statAndIsLink = await statLink(symbolicLink); + statAndIsLink = await SymlinkSupport.stat(symbolicLink); assert.ok(statAndIsLink?.symbolicLink); assert.ok(!statAndIsLink?.symbolicLink?.dangling); }); @@ -261,7 +261,7 @@ flakySuite('PFS', function () { await rimraf(directory); - const statAndIsLink = await statLink(symbolicLink); + const statAndIsLink = await SymlinkSupport.stat(symbolicLink); assert.ok(statAndIsLink?.symbolicLink); assert.ok(statAndIsLink?.symbolicLink?.dangling); }); diff --git a/src/vs/platform/backup/electron-main/backupMainService.ts b/src/vs/platform/backup/electron-main/backupMainService.ts index 42f5fb335cd..1f3b67af11a 100644 --- a/src/vs/platform/backup/electron-main/backupMainService.ts +++ b/src/vs/platform/backup/electron-main/backupMainService.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; -import * as crypto from 'crypto'; -import * as path from 'vs/base/common/path'; -import * as platform from 'vs/base/common/platform'; +import { createHash } from 'crypto'; +import { join } from 'vs/base/common/path'; +import { isLinux } from 'vs/base/common/platform'; import { writeFileSync, writeFile, readFile, readdir, exists, rimraf, rename, RimRafMode } from 'vs/base/node/pfs'; import { IBackupMainService, IWorkspaceBackupInfo, isWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup'; import { IBackupWorkspacesFormat, IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; @@ -35,7 +35,7 @@ export class BackupMainService implements IBackupMainService { // - ignore path casing on Windows/macOS // - respect path casing on Linux private readonly backupUriComparer = extUriBiasedIgnorePathCase; - private readonly backupPathComparer = { isEqual: (pathA: string, pathB: string) => isEqual(pathA, pathB, !platform.isLinux) }; + private readonly backupPathComparer = { isEqual: (pathA: string, pathB: string) => isEqual(pathA, pathB, !isLinux) }; constructor( @IEnvironmentMainService environmentService: IEnvironmentMainService, @@ -204,7 +204,7 @@ export class BackupMainService implements IBackupMainService { } private getBackupPath(oldFolderHash: string): string { - return path.join(this.backupHome, oldFolderHash); + return join(this.backupHome, oldFolderHash); } private async validateWorkspaces(rootWorkspaces: IWorkspaceBackupInfo[]): Promise { @@ -406,7 +406,7 @@ export class BackupMainService implements IBackupMainService { for (const backupSchema of backupSchemas) { try { - const backupSchemaChildren = await readdir(path.join(backupPath, backupSchema)); + const backupSchemaChildren = await readdir(join(backupPath, backupSchema)); if (backupSchemaChildren.length > 0) { return true; } @@ -454,11 +454,11 @@ export class BackupMainService implements IBackupMainService { if (folderUri.scheme === Schemas.file) { // for backward compatibility, use the fspath as key - key = platform.isLinux ? folderUri.fsPath : folderUri.fsPath.toLowerCase(); + key = isLinux ? folderUri.fsPath : folderUri.fsPath.toLowerCase(); } else { key = folderUri.toString().toLowerCase(); } - return crypto.createHash('md5').update(key).digest('hex'); + return createHash('md5').update(key).digest('hex'); } } diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index 75811f569e9..baed33b3335 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 { statLink, unlink, move, copy, readFile, truncate, rimraf, RimRafMode, exists, readdirWithFileTypes } from 'vs/base/node/pfs'; +import { SymlinkSupport, unlink, move, copy, readFile, truncate, rimraf, RimRafMode, exists, readdirWithFileTypes } 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'; @@ -80,7 +80,7 @@ export class DiskFileSystemProvider extends Disposable implements async stat(resource: URI): Promise { try { - const { stat, symbolicLink } = await statLink(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly + const { stat, symbolicLink } = await SymlinkSupport.stat(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly return { type: this.toType(stat, symbolicLink), diff --git a/src/vs/platform/files/node/watcher/nodejs/watcherService.ts b/src/vs/platform/files/node/watcher/nodejs/watcherService.ts index 16c5428714b..2a68be01737 100644 --- a/src/vs/platform/files/node/watcher/nodejs/watcherService.ts +++ b/src/vs/platform/files/node/watcher/nodejs/watcherService.ts @@ -5,7 +5,7 @@ import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; import { Disposable } from 'vs/base/common/lifecycle'; -import { statLink } from 'vs/base/node/pfs'; +import { SymlinkSupport } from 'vs/base/node/pfs'; import { realpath } from 'vs/base/node/extpath'; import { watchFolder, watchFile, CHANGE_BUFFER_DELAY } from 'vs/base/node/watcher'; import { FileChangeType } from 'vs/platform/files/common/files'; @@ -35,7 +35,7 @@ export class FileWatcher extends Disposable { private async startWatching(): Promise { try { - const { stat, symbolicLink } = await statLink(this.path); + const { stat, symbolicLink } = await SymlinkSupport.stat(this.path); if (this.isDisposed) { return; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 50c4460ad4a..97b12c744be 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -15,7 +15,7 @@ import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { AddFirstParameterToFunctions } from 'vs/base/common/types'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; -import { dirExists } from 'vs/base/node/pfs'; +import { SymlinkSupport } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -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 dirExists(path)) ? { folderUri: URI.file(path) } : { fileUri: URI.file(path) })), options, windowId); + this.doOpenPicked(await Promise.all(paths.map(async path => (await SymlinkSupport.dirExists(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 358507bdf22..2204c403d32 100644 --- a/src/vs/workbench/api/node/extHostOutputService.ts +++ b/src/vs/workbench/api/node/extHostOutputService.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { join } from 'vs/base/common/path'; import { OutputAppender } from 'vs/workbench/services/output/node/outputAppender'; import { toLocalISOString } from 'vs/base/common/date'; -import { dirExists } from 'vs/base/node/pfs'; +import { SymlinkSupport } from 'vs/base/node/pfs'; import { promises } from 'fs'; import { AbstractExtHostOutputChannel, ExtHostPushOutputChannel, ExtHostOutputService, LazyOutputChannel } from 'vs/workbench/api/common/extHostOutput'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; @@ -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 dirExists(outputDirPath); + const exists = await SymlinkSupport.dirExists(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 ff36c4e6331..9be699a7ea9 100644 --- a/src/vs/workbench/contrib/terminal/node/terminal.ts +++ b/src/vs/workbench/contrib/terminal/node/terminal.ts @@ -5,7 +5,7 @@ import * as os from 'os'; import * as platform from 'vs/base/common/platform'; -import { readFile, fileExists, stat, lstat } from 'vs/base/node/pfs'; +import { readFile, SymlinkSupport, stat, lstat } from 'vs/base/node/pfs'; import { LinuxDistro, IShellDefinition } from 'vs/workbench/contrib/terminal/common/terminal'; import { coalesce } from 'vs/base/common/arrays'; import { normalize, basename } from 'vs/base/common/path'; @@ -14,7 +14,7 @@ import { enumeratePowerShellInstallations } from 'vs/base/node/powershell'; let detectedDistro = LinuxDistro.Unknown; if (platform.isLinux) { const file = '/etc/os-release'; - fileExists(file).then(async exists => { + SymlinkSupport.fileExists(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 c19a1f5ba58..bef6b6e9235 100644 --- a/src/vs/workbench/services/extensions/node/extensionPoints.ts +++ b/src/vs/workbench/services/extensions/node/extensionPoints.ts @@ -144,7 +144,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { return { values: undefined, default: `${basename}.nls.json` }; }); } else { - localizedMessages = pfs.fileExists(basename + '.nls' + extension).then(exists => { + localizedMessages = pfs.SymlinkSupport.fileExists(basename + '.nls' + extension).then(exists => { if (!exists) { return undefined; } @@ -220,7 +220,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.fileExists(toCheck).then(exists => { + pfs.SymlinkSupport.fileExists(toCheck).then(exists => { if (exists) { c({ localized: toCheck, original: `${basename}.nls.json` }); } @@ -597,7 +597,7 @@ export class ExtensionScanner { const isBuiltin = input.isBuiltin; const isUnderDevelopment = input.isUnderDevelopment; - return pfs.fileExists(path.join(absoluteFolderPath, MANIFEST_FILE)).then((exists) => { + return pfs.SymlinkSupport.fileExists(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) => { From 9c4d2480630d76a8812a54565e0308f5f3322dfb Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 2 Feb 2021 17:47:07 +0100 Subject: [PATCH 04/31] fs - avoid one exists() call --- src/vs/base/node/pfs.ts | 4 ++++ .../textfile/electron-browser/nativeTextFileService.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index b71cbd95d71..9925d41e489 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -199,6 +199,8 @@ export async function readDirsInDir(dirPath: string): Promise { //#endregion +//#region whenDeleted() + export function whenDeleted(path: string): Promise { // Complete when wait marker file is deleted @@ -220,6 +222,8 @@ export function whenDeleted(path: string): Promise { }); } +//#endregion + //#region Methods with symbolic links support export namespace SymlinkSupport { diff --git a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts index a3b6d2200d7..60d73593f0e 100644 --- a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts +++ b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts @@ -108,7 +108,7 @@ export class NativeTextFileService extends AbstractTextFileService { // check for overwriteReadonly property (only supported for local file://) try { - if (options?.overwriteReadonly && resource.scheme === Schemas.file && await this.fileService.exists(resource)) { + if (options?.overwriteReadonly && resource.scheme === Schemas.file) { const fileStat = await stat(resource.fsPath); // try to change mode to writeable From 944c4b4fced64fbc6ad894918c384f283a9e5eb7 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 3 Feb 2021 09:17:12 +0100 Subject: [PATCH 05/31] fs - drop our custom rimraf solution and use node.js --- src/vs/base/node/pfs.ts | 74 ++++------------------------------------- 1 file changed, 6 insertions(+), 68 deletions(-) diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index 9925d41e489..032443831de 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -6,7 +6,7 @@ import * as fs from 'fs'; import { tmpdir } from 'os'; import { join } from 'vs/base/common/path'; -import { Promises, Queue } from 'vs/base/common/async'; +import { Queue } from 'vs/base/common/async'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { Event } from 'vs/base/common/event'; import { promisify } from 'util'; @@ -51,59 +51,26 @@ export async function rimraf(path: string, mode = RimRafMode.UNLINK): Promise { - 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 Promises.settled(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 & fs.constants.S_IWUSR)) { - await chmod(path, mode | fs.constants.S_IWUSR); - } - - return unlink(path); - } - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; - } - } -} - async function rimrafMove(path: string): Promise { try { const pathInTemp = join(tmpdir(), generateUuid()); try { await rename(path, pathInTemp); } catch (error) { - return rimrafUnlink(path); // if rename fails, delete without tmp dir + return fs.promises.rmdir(path, { recursive: true }); // if rename fails, delete without tmp dir } // Delete but do not return as promise - rimrafUnlink(pathInTemp); + fs.promises.rmdir(pathInTemp, { recursive: true }).catch(error => {/* ignore */ }); } catch (error) { if (error.code !== 'ENOENT') { throw error; @@ -116,36 +83,7 @@ export function rimrafSync(path: string): void { 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 & fs.constants.S_IWUSR)) { - fs.chmodSync(path, mode | fs.constants.S_IWUSR); - } - - return fs.unlinkSync(path); - } - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; - } - } + fs.rmdirSync(path, { recursive: true }); } //#endregion From e2f7159745c98feadc3774b489eda52d4e0dfc1c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 3 Feb 2021 09:34:03 +0100 Subject: [PATCH 06/31] fs - remove custom rimraf solutions --- src/vs/base/node/languagePacks.js | 75 ++++--------------------------- 1 file changed, 9 insertions(+), 66 deletions(-) diff --git a/src/vs/base/node/languagePacks.js b/src/vs/base/node/languagePacks.js index be9ca50b1bf..24586971b2d 100644 --- a/src/vs/base/node/languagePacks.js +++ b/src/vs/base/node/languagePacks.js @@ -2,9 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; //@ts-check +'use strict'; /** * @param {NodeRequire} nodeRequire @@ -30,22 +30,6 @@ function factory(nodeRequire, path, fs, perf) { return new Promise((c, e) => { const d = new Date(); fs.utimes(file, d, d, err => err ? e(err) : c()); }); } - /** - * @param {string} file - * @returns {Promise} - */ - function lstat(file) { - return new Promise((c, e) => fs.lstat(file, (err, stats) => err ? e(err) : c(stats))); - } - - /** - * @param {string} dir - * @returns {Promise} - */ - function readdir(dir) { - return new Promise((c, e) => fs.readdir(dir, (err, files) => err ? e(err) : c(files))); - } - /** * @param {string} dir * @returns {Promise} @@ -54,53 +38,20 @@ function factory(nodeRequire, path, fs, perf) { return new Promise((c, e) => fs.mkdir(dir, { recursive: true }, err => (err && err.code !== 'EEXIST') ? e(err) : c(dir))); } - /** - * @param {string} dir - * @returns {Promise} - */ - function rmdir(dir) { - return new Promise((c, e) => fs.rmdir(dir, err => err ? e(err) : c(undefined))); - } - - /** - * @param {string} file - * @returns {Promise} - */ - function unlink(file) { - return new Promise((c, e) => fs.unlink(file, err => err ? e(err) : c(undefined))); - } - /** * @param {string} location * @returns {Promise} */ function rimraf(location) { - return lstat(location).then(stat => { - if (stat.isDirectory() && !stat.isSymbolicLink()) { - return readdir(location) - .then(children => Promise.all(children.map(child => rimraf(path.join(location, child))))) - .then(() => rmdir(location)); - } else { - return unlink(location); - } - }, err => { - if (err.code === 'ENOENT') { - return undefined; - } - throw err; - }); + return new Promise((c, e) => fs.rmdir(location, { recursive: true }, err => (err && err.code !== 'ENOENT') ? e(err) : c())); } + /** + * @param {string} file + * @returns {Promise} + */ function readFile(file) { - return new Promise(function (resolve, reject) { - fs.readFile(file, 'utf8', function (err, data) { - if (err) { - reject(err); - return; - } - resolve(data); - }); - }); + return new Promise((c, e) => fs.readFile(file, 'utf8', (err, data) => err ? e(err) : c(data))); } /** @@ -109,18 +60,9 @@ function factory(nodeRequire, path, fs, perf) { * @returns {Promise} */ function writeFile(file, content) { - return new Promise(function (resolve, reject) { - fs.writeFile(file, content, 'utf8', function (err) { - if (err) { - reject(err); - return; - } - resolve(); - }); - }); + return new Promise((c, e) => fs.writeFile(file, content, 'utf8', err => err ? e(err) : c())); } - /** * @param {string} userDataPath * @returns {object} @@ -303,6 +245,7 @@ function factory(nodeRequire, path, fs, perf) { if (typeof define === 'function') { // amd + // @ts-ignore define(['path', 'fs', 'vs/base/common/performance'], function (path, fs, perf) { return factory(require.__$__nodeRequire, path, fs, perf); }); } else if (typeof module === 'object' && typeof module.exports === 'object') { const path = require('path'); From 194e1c5f1c8c32e2a0af7ca8e56d1d8c6c6b397e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 3 Feb 2021 09:43:05 +0100 Subject: [PATCH 07/31] fs - retry async rimraf unlink --- src/vs/base/node/pfs.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index 032443831de..5abce01efc1 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -53,7 +53,7 @@ export async function rimraf(path: string, mode = RimRafMode.UNLINK): Promise { try { await rename(path, pathInTemp); } catch (error) { - return fs.promises.rmdir(path, { recursive: true }); // if rename fails, delete without tmp dir + return rimrafUnlink(path); // if rename fails, delete without tmp dir } // Delete but do not return as promise - fs.promises.rmdir(pathInTemp, { recursive: true }).catch(error => {/* ignore */ }); + rimrafUnlink(pathInTemp).catch(error => {/* ignore */ }); } catch (error) { if (error.code !== 'ENOENT') { throw error; @@ -78,6 +78,10 @@ async function rimrafMove(path: string): Promise { } } +async function rimrafUnlink(path: string): Promise { + return fs.promises.rmdir(path, { recursive: true, maxRetries: 3 }); +} + export function rimrafSync(path: string): void { if (isRootOrDriveLetter(path)) { throw new Error('rimraf - will refuse to recursively delete root'); From a17c656c3308c957077958e38f51e09e0407e478 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 3 Feb 2021 10:32:30 +0100 Subject: [PATCH 08/31] fs - some polish around file services --- src/vs/code/node/cliProcessMain.ts | 13 +++++----- .../backup/electron-main/backupMainService.ts | 24 +++++++++---------- .../files/node/diskFileSystemProvider.ts | 6 +++-- .../extensionsAutoProfiler.ts | 10 ++++---- 4 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 0adac0a9244..7e1fbe14cbb 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -28,7 +28,6 @@ import { RequestService } from 'vs/platform/request/node/requestService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; -import { writeFile } from 'vs/base/node/pfs'; import { IStateService } from 'vs/platform/state/node/state'; import { StateService } from 'vs/platform/state/node/stateService'; import { ILogService, getLogLevel, LogLevel, ConsoleLogService, MultiplexLogService } from 'vs/platform/log/common/log'; @@ -46,6 +45,7 @@ import { LocalizationsService } from 'vs/platform/localizations/node/localizatio import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { VSBuffer } from 'vs/base/common/buffer'; class CliMain extends Disposable { @@ -73,6 +73,7 @@ class CliMain extends Disposable { return instantiationService.invokeFunction(async accessor => { const logService = accessor.get(ILogService); + const fileService = accessor.get(IFileService); const environmentService = accessor.get(INativeEnvironmentService); const extensionManagementCLIService = accessor.get(IExtensionManagementCLIService); @@ -83,7 +84,7 @@ class CliMain extends Disposable { this.registerErrorHandler(logService); // Run based on argv - await this.doRun(environmentService, extensionManagementCLIService); + await this.doRun(environmentService, extensionManagementCLIService, fileService); // Flush the remaining data in AI adapter (with 1s timeout) return raceTimeout(combinedAppender(...appenders).flush(), 1000); @@ -182,11 +183,11 @@ class CliMain extends Disposable { }); } - private async doRun(environmentService: INativeEnvironmentService, extensionManagementCLIService: IExtensionManagementCLIService): Promise { + private async doRun(environmentService: INativeEnvironmentService, extensionManagementCLIService: IExtensionManagementCLIService, fileService: IFileService): Promise { // Install Source if (this.argv['install-source']) { - return this.setInstallSource(environmentService, this.argv['install-source']); + return this.setInstallSource(environmentService, fileService, this.argv['install-source']); } // List Extensions @@ -219,8 +220,8 @@ class CliMain extends Disposable { return inputs.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(process.cwd(), input)) : input); } - private setInstallSource(environmentService: INativeEnvironmentService, installSource: string): Promise { - return writeFile(environmentService.installSourcePath, installSource.slice(0, 30)); + private async setInstallSource(environmentService: INativeEnvironmentService, fileService: IFileService, installSource: string): Promise { + await fileService.writeFile(URI.file(environmentService.installSourcePath), VSBuffer.fromString(installSource.slice(0, 30))); } } diff --git a/src/vs/platform/backup/electron-main/backupMainService.ts b/src/vs/platform/backup/electron-main/backupMainService.ts index 1f3b67af11a..65596efb4b1 100644 --- a/src/vs/platform/backup/electron-main/backupMainService.ts +++ b/src/vs/platform/backup/electron-main/backupMainService.ts @@ -151,8 +151,8 @@ export class BackupMainService implements IBackupMainService { if (fs.existsSync(moveFromPath)) { try { fs.renameSync(moveFromPath, backupPath); - } catch (ex) { - this.logService.error(`Backup: Could not move backup folder to new location: ${ex.toString()}`); + } catch (error) { + this.logService.error(`Backup: Could not move backup folder to new location: ${error.toString()}`); } } } @@ -312,8 +312,8 @@ export class BackupMainService implements IBackupMainService { if (await exists(backupPath)) { await rimraf(backupPath, RimRafMode.MOVE); } - } catch (ex) { - this.logService.error(`Backup: Could not delete stale backup: ${ex.toString()}`); + } catch (error) { + this.logService.error(`Backup: Could not delete stale backup: ${error.toString()}`); } } @@ -329,8 +329,8 @@ export class BackupMainService implements IBackupMainService { const newEmptyWindowBackupPath = this.getBackupPath(newBackupFolder); try { await rename(backupPath, newEmptyWindowBackupPath); - } catch (ex) { - this.logService.error(`Backup: Could not rename backup folder: ${ex.toString()}`); + } catch (error) { + this.logService.error(`Backup: Could not rename backup folder: ${error.toString()}`); return false; } this.emptyWindows.push({ backupFolder: newBackupFolder }); @@ -350,8 +350,8 @@ export class BackupMainService implements IBackupMainService { const newEmptyWindowBackupPath = this.getBackupPath(newBackupFolder); try { fs.renameSync(backupPath, newEmptyWindowBackupPath); - } catch (ex) { - this.logService.error(`Backup: Could not rename backup folder: ${ex.toString()}`); + } catch (error) { + this.logService.error(`Backup: Could not rename backup folder: ${error.toString()}`); return false; } this.emptyWindows.push({ backupFolder: newBackupFolder }); @@ -424,16 +424,16 @@ export class BackupMainService implements IBackupMainService { private saveSync(): void { try { writeFileSync(this.workspacesJsonPath, JSON.stringify(this.serializeBackups())); - } catch (ex) { - this.logService.error(`Backup: Could not save workspaces.json: ${ex.toString()}`); + } catch (error) { + this.logService.error(`Backup: Could not save workspaces.json: ${error.toString()}`); } } private async save(): Promise { try { await writeFile(this.workspacesJsonPath, JSON.stringify(this.serializeBackups())); - } catch (ex) { - this.logService.error(`Backup: Could not save workspaces.json: ${ex.toString()}`); + } catch (error) { + this.logService.error(`Backup: Could not save workspaces.json: ${error.toString()}`); } } diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index baed33b3335..c212f37c152 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -212,7 +212,7 @@ export class DiskFileSystemProvider extends Disposable implements let flags: string | undefined = undefined; if (opts.create) { - if (isWindows && await exists(filePath)) { + if (isWindows) { try { // On Windows and if the file exists, we use a different strategy of saving the file // by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows @@ -223,7 +223,9 @@ export class DiskFileSystemProvider extends Disposable implements // After a successful truncate() the flag can be set to 'r+' which will not truncate. flags = 'r+'; } catch (error) { - this.logService.trace(error); + if (error.code !== 'ENOENT') { + this.logService.trace(error); + } } } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts index d8f182cffbb..96b09731b4e 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts @@ -11,7 +11,6 @@ import { ILogService } from 'vs/platform/log/common/log'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; import { joinPath } from 'vs/base/common/resources'; -import { writeFile } from 'vs/base/node/pfs'; import { IExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { localize } from 'vs/nls'; @@ -22,6 +21,8 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { createSlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions'; import { ExtensionHostProfiler } from 'vs/workbench/services/extensions/electron-browser/extensionHostProfiler'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { VSBuffer } from 'vs/base/common/buffer'; export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchContribution { @@ -36,7 +37,8 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont @INotificationService private readonly _notificationService: INotificationService, @IEditorService private readonly _editorService: IEditorService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @INativeWorkbenchEnvironmentService private readonly _environmentServie: INativeWorkbenchEnvironmentService + @INativeWorkbenchEnvironmentService private readonly _environmentServie: INativeWorkbenchEnvironmentService, + @IFileService private readonly _fileService: IFileService ) { super(); this._register(_extensionService.onDidChangeResponsiveChange(this._onDidChangeResponsiveChange, this)); @@ -139,8 +141,8 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont // print message to log - const path = joinPath(this._environmentServie.tmpDir, `exthost-${Math.random().toString(16).slice(2, 8)}.cpuprofile`).fsPath; - await writeFile(path, JSON.stringify(profile.data)); + const path = joinPath(this._environmentServie.tmpDir, `exthost-${Math.random().toString(16).slice(2, 8)}.cpuprofile`); + await this._fileService.writeFile(path, VSBuffer.fromString(JSON.stringify(profile.data))); this._logService.warn(`UNRESPONSIVE extension host, '${top.id}' took ${top!.percentage}% of ${duration / 1e3}ms, saved PROFILE here: '${path}'`, data); From dbf6147c01ed2554ec70b025a916d38d4204f2eb Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 3 Feb 2021 11:20:14 +0100 Subject: [PATCH 09/31] fs - remove promise based methods from pfs and replace with fs.promises --- src/vs/base/node/pfs.ts | 50 ++----------------- src/vs/base/parts/storage/node/storage.ts | 13 +++-- .../parts/storage/test/node/storage.test.ts | 4 +- src/vs/base/test/node/pfs/pfs.test.ts | 8 +-- .../contrib/languagePackCachedDataCleaner.ts | 5 +- .../contrib/nodeCachedDataCleaner.ts | 5 +- .../contrib/storageDataCleaner.ts | 5 +- .../backup/electron-main/backupMainService.ts | 6 +-- .../electron-main/backupMainService.test.ts | 28 +++++------ .../node/extensionDownloader.ts | 4 +- .../node/extensionManagementService.ts | 3 +- .../node/extensionsScanner.ts | 11 ++-- .../files/node/diskFileSystemProvider.ts | 10 ++-- .../electron-browser/diskFileService.test.ts | 18 +++---- .../localizations/node/localizations.ts | 7 +-- src/vs/platform/state/node/stateService.ts | 4 +- .../electron-main/updateService.win32.ts | 4 +- .../api/node/extHostTunnelService.ts | 8 +-- .../contrib/cli/node/cli.contribution.ts | 11 ++-- .../electron-browser/startupProfiler.ts | 9 ++-- .../contrib/terminal/node/terminal.ts | 11 ++-- .../terminal/node/terminalEnvironment.ts | 9 ++-- .../contrib/terminal/node/terminalProcess.ts | 5 +- .../cachedExtensionScanner.ts | 8 +-- .../extensions/node/extensionPoints.ts | 11 ++-- .../keyboardMapperTestUtils.ts | 7 +-- .../electron-browser/nativeTextFileService.ts | 9 ++-- .../nativeTextFileService.io.test.ts | 13 +++-- .../colorRegistry.releaseTest.ts | 5 +- 29 files changed, 134 insertions(+), 157 deletions(-) diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index 5abce01efc1..f9ec3033870 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -64,7 +64,7 @@ async function rimrafMove(path: string): Promise { try { const pathInTemp = join(tmpdir(), generateUuid()); try { - await rename(path, pathInTemp); + await fs.promises.rename(path, pathInTemp); } catch (error) { return rimrafUnlink(path); // if rename fails, delete without tmp dir } @@ -197,7 +197,7 @@ export namespace SymlinkSupport { // First stat the link let lstats: fs.Stats | undefined; try { - lstats = await lstat(path); + lstats = await fs.promises.lstat(path); // Return early if the stat is not a symbolic link at all if (!lstats.isSymbolicLink()) { @@ -225,7 +225,7 @@ export namespace SymlinkSupport { // are not supported (https://github.com/nodejs/node/issues/36790) if (isWindows && error.code === 'EACCES' && lstats) { try { - const stats = await fs.promises.stat(await readlink(path)); + const stats = await fs.promises.stat(await fs.promises.readlink(path)); return { stat: stats, symbolicLink: lstats.isSymbolicLink() ? { dangling: false } : undefined }; } catch (error) { @@ -443,7 +443,7 @@ export async function move(source: string, target: string): Promise { } try { - await rename(source, target); + await fs.promises.rename(source, target); await updateMtime(target); } catch (error) { @@ -551,46 +551,4 @@ export async function exists(path: string): Promise { } } -export function chmod(path: string, mode: number): Promise { - return promisify(fs.chmod)(path, mode); -} - -export function stat(path: string): Promise { - return promisify(fs.stat)(path); -} - -export function lstat(path: string): Promise { - return promisify(fs.lstat)(path); -} - -export function rename(oldPath: string, newPath: string): Promise { - return promisify(fs.rename)(oldPath, newPath); -} - -export function renameIgnoreError(oldPath: string, newPath: string): Promise { - return new Promise(resolve => fs.rename(oldPath, newPath, () => resolve())); -} - -export function readlink(path: string): Promise { - return promisify(fs.readlink)(path); -} - -export function unlink(path: string): Promise { - return promisify(fs.unlink)(path); -} - -export function symlink(target: string, path: string, type?: string): Promise { - return promisify(fs.symlink)(target, path, type); -} - -export function truncate(path: string, len: number): Promise { - return promisify(fs.truncate)(path, len); -} - -export function readFile(path: string): Promise; -export function readFile(path: string, encoding: string): Promise; -export function readFile(path: string, encoding?: string): Promise { - return promisify(fs.readFile)(path, encoding); -} - //#endregion diff --git a/src/vs/base/parts/storage/node/storage.ts b/src/vs/base/parts/storage/node/storage.ts index b99c08a358f..410158d5c5b 100644 --- a/src/vs/base/parts/storage/node/storage.ts +++ b/src/vs/base/parts/storage/node/storage.ts @@ -4,11 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import type { Database, Statement } from 'vscode-sqlite3'; +import { promises } from 'fs'; import { Event } from 'vs/base/common/event'; import { timeout } from 'vs/base/common/async'; import { mapToString, setToString } from 'vs/base/common/map'; import { basename } from 'vs/base/common/path'; -import { copy, renameIgnoreError, unlink } from 'vs/base/node/pfs'; +import { copy } from 'vs/base/node/pfs'; import { IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage'; interface IDatabaseConnection { @@ -186,7 +187,7 @@ export class SQLiteStorageDatabase implements IStorageDatabase { // Delete the existing DB. If the path does not exist or fails to // be deleted, we do not try to recover anymore because we assume // that the path is no longer writeable for us. - return unlink(this.path).then(() => { + return promises.unlink(this.path).then(() => { // Re-open the DB fresh return this.doConnect(this.path).then(recoveryConnection => { @@ -272,8 +273,12 @@ export class SQLiteStorageDatabase implements IStorageDatabase { // folder is really not writeable for us. // try { - await unlink(path); - await renameIgnoreError(this.toBackupPath(path), path); + await promises.unlink(path); + try { + await promises.rename(this.toBackupPath(path), path); + } catch (error) { + // ignore + } return await this.doConnect(path); } catch (error) { diff --git a/src/vs/base/parts/storage/test/node/storage.test.ts b/src/vs/base/parts/storage/test/node/storage.test.ts index 9dffb5329ae..46193801f71 100644 --- a/src/vs/base/parts/storage/test/node/storage.test.ts +++ b/src/vs/base/parts/storage/test/node/storage.test.ts @@ -9,7 +9,7 @@ import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; import { promises } from 'fs'; import { strictEqual, ok } from 'assert'; -import { writeFile, exists, unlink, rimraf } from 'vs/base/node/pfs'; +import { writeFile, exists, rimraf } from 'vs/base/node/pfs'; import { timeout } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import { isWindows } from 'vs/base/common/platform'; @@ -476,7 +476,7 @@ flakySuite('SQLite Storage Library', function () { // on shutdown. await storage.checkIntegrity(true).then(null, error => { } /* error is expected here but we do not want to fail */); - await unlink(backupPath); // also test that the recovery DB is backed up properly + await promises.unlink(backupPath); // also test that the recovery DB is backed up properly let recoveryCalled = false; await storage.close(() => { diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index 31d8e6057ad..2c1a25ad725 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, readFile, renameIgnoreError, rimraf, RimRafMode, rimrafSync, SymlinkSupport, writeFile, writeFileSync } from 'vs/base/node/pfs'; +import { copy, exists, move, readdir, readDirsInDir, readdirWithFileTypes, 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'; @@ -36,7 +36,7 @@ flakySuite('PFS', function () { await writeFile(testFile, 'Hello World', (null!)); - assert.strictEqual((await readFile(testFile)).toString(), 'Hello World'); + assert.strictEqual((await fs.promises.readFile(testFile)).toString(), 'Hello World'); }); test('writeFile - parallel write on different files works', async () => { @@ -153,10 +153,6 @@ flakySuite('PFS', function () { assert.ok(!fs.existsSync(testDir)); }); - test('moveIgnoreError', () => { - return renameIgnoreError(join(testDir, 'foo'), join(testDir, 'bar')); - }); - test('copy, move and delete', async () => { const id = generateUuid(); const id2 = generateUuid(); diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts index e060bcc6a01..41a73455b5b 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import * as path from 'vs/base/common/path'; import * as pfs from 'vs/base/node/pfs'; import { IStringDictionary } from 'vs/base/common/collections'; @@ -52,7 +53,7 @@ export class LanguagePackCachedDataCleaner extends Disposable { : 1000 * 60 * 60 * 24 * 30 * 3; // roughly 3 months try { const installed: IStringDictionary = Object.create(null); - const metaData: LanguagePackFile = JSON.parse(await pfs.readFile(path.join(this._environmentService.userDataPath, 'languagepacks.json'), 'utf8')); + const metaData: LanguagePackFile = JSON.parse(await fs.promises.readFile(path.join(this._environmentService.userDataPath, 'languagepacks.json'), 'utf8')); for (let locale of Object.keys(metaData)) { const entry = metaData[locale]; installed[`${entry.hash}.${locale}`] = true; @@ -80,7 +81,7 @@ export class LanguagePackCachedDataCleaner extends Disposable { continue; } const candidate = path.join(folder, entry); - const stat = await pfs.stat(candidate); + const stat = await fs.promises.stat(candidate); if (stat.isDirectory()) { const diff = now - stat.mtime.getTime(); if (diff > maxAge) { diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts index a9808801094..7c37511ca73 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { promises } from 'fs'; import { basename, dirname, join } from 'vs/base/common/path'; import { onUnexpectedError } from 'vs/base/common/errors'; import { toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { readdir, rimraf, stat } from 'vs/base/node/pfs'; +import { readdir, rimraf } from 'vs/base/node/pfs'; import product from 'vs/platform/product/common/product'; export class NodeCachedDataCleaner { @@ -54,7 +55,7 @@ export class NodeCachedDataCleaner { if (entry !== nodeCachedDataCurrent) { const path = join(nodeCachedDataRootDir, entry); - deletes.push(stat(path).then(stats => { + deletes.push(promises.stat(path).then(stats => { // stat check // * only directories // * only when old enough diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts index 66b8d79b5fa..fc26cb229c3 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { promises } from 'fs'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { join } from 'vs/base/common/path'; -import { readdir, readFile, rimraf } from 'vs/base/node/pfs'; +import { readdir, rimraf } from 'vs/base/node/pfs'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IBackupWorkspacesFormat } from 'vs/platform/backup/node/backup'; @@ -32,7 +33,7 @@ export class StorageDataCleaner extends Disposable { try { // Leverage the backup workspace file to find out which empty workspace is currently in use to // determine which empty workspace storage can safely be deleted - const contents = await readFile(this.backupWorkspacesPath, 'utf8'); + const contents = await promises.readFile(this.backupWorkspacesPath, 'utf8'); const workspaces = JSON.parse(contents) as IBackupWorkspacesFormat; const emptyWorkspaces = workspaces.emptyWorkspaceInfos.map(info => info.backupFolder); diff --git a/src/vs/platform/backup/electron-main/backupMainService.ts b/src/vs/platform/backup/electron-main/backupMainService.ts index 65596efb4b1..ac5e6cd2eb1 100644 --- a/src/vs/platform/backup/electron-main/backupMainService.ts +++ b/src/vs/platform/backup/electron-main/backupMainService.ts @@ -7,7 +7,7 @@ import * as fs from 'fs'; import { createHash } from 'crypto'; import { join } from 'vs/base/common/path'; import { isLinux } from 'vs/base/common/platform'; -import { writeFileSync, writeFile, readFile, readdir, exists, rimraf, rename, RimRafMode } from 'vs/base/node/pfs'; +import { writeFileSync, writeFile, readdir, exists, rimraf, RimRafMode } from 'vs/base/node/pfs'; import { IBackupMainService, IWorkspaceBackupInfo, isWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup'; import { IBackupWorkspacesFormat, IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; @@ -49,7 +49,7 @@ export class BackupMainService implements IBackupMainService { async initialize(): Promise { let backups: IBackupWorkspacesFormat; try { - backups = JSON.parse(await readFile(this.workspacesJsonPath, 'utf8')); // invalid JSON or permission issue can happen here + backups = JSON.parse(await fs.promises.readFile(this.workspacesJsonPath, 'utf8')); // invalid JSON or permission issue can happen here } catch (error) { backups = Object.create(null); } @@ -328,7 +328,7 @@ export class BackupMainService implements IBackupMainService { // Rename backupPath to new empty window backup path const newEmptyWindowBackupPath = this.getBackupPath(newBackupFolder); try { - await rename(backupPath, newEmptyWindowBackupPath); + await fs.promises.rename(backupPath, newEmptyWindowBackupPath); } catch (error) { this.logService.error(`Backup: Could not rename backup folder: ${error.toString()}`); return false; diff --git a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index dbde7225804..958f744ecef 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -445,7 +445,7 @@ flakySuite('BackupMainService', () => { await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)); await service.initialize(); - const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); @@ -461,7 +461,7 @@ flakySuite('BackupMainService', () => { }; await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)); await service.initialize(); - const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); @@ -483,7 +483,7 @@ flakySuite('BackupMainService', () => { await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)); await service.initialize(); - const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.strictEqual(json.rootURIWorkspaces.length, platform.isLinux ? 3 : 1); if (platform.isLinux) { @@ -499,7 +499,7 @@ flakySuite('BackupMainService', () => { service.registerFolderBackupSync(fooFile); service.registerFolderBackupSync(barFile); assertEqualUris(service.getFolderBackupPaths(), [fooFile, barFile]); - const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.deepStrictEqual(json.folderURIWorkspaces, [fooFile.toString(), barFile.toString()]); }); @@ -514,7 +514,7 @@ flakySuite('BackupMainService', () => { assert.strictEqual(ws1.workspace.id, service.getWorkspaceBackups()[0].workspace.id); assert.strictEqual(ws2.workspace.id, service.getWorkspaceBackups()[1].workspace.id); - const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [fooFile.toString(), barFile.toString()]); @@ -527,7 +527,7 @@ flakySuite('BackupMainService', () => { service.registerFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase())); assertEqualUris(service.getFolderBackupPaths(), [URI.file(fooFile.fsPath.toUpperCase())]); - const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.deepStrictEqual(json.folderURIWorkspaces, [URI.file(fooFile.fsPath.toUpperCase()).toString()]); }); @@ -537,7 +537,7 @@ flakySuite('BackupMainService', () => { service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(upperFooPath)); assertEqualUris(service.getWorkspaceBackups().map(b => b.workspace.configPath), [URI.file(upperFooPath)]); - const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [URI.file(upperFooPath).toString()]); }); @@ -548,12 +548,12 @@ flakySuite('BackupMainService', () => { service.registerFolderBackupSync(barFile); service.unregisterFolderBackupSync(fooFile); - const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); assert.deepStrictEqual(json.folderURIWorkspaces, [barFile.toString()]); service.unregisterFolderBackupSync(barFile); - const content = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); assert.deepStrictEqual(json2.folderURIWorkspaces, []); }); @@ -565,12 +565,12 @@ flakySuite('BackupMainService', () => { service.registerWorkspaceBackupSync(ws2); service.unregisterWorkspaceBackupSync(ws1.workspace); - const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); assert.deepStrictEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [barFile.toString()]); service.unregisterWorkspaceBackupSync(ws2.workspace); - const content = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); assert.deepStrictEqual(json2.rootURIWorkspaces, []); }); @@ -580,12 +580,12 @@ flakySuite('BackupMainService', () => { service.registerEmptyWindowBackupSync('bar'); service.unregisterEmptyWindowBackupSync('foo'); - const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); assert.deepStrictEqual(json.emptyWorkspaceInfos, [{ backupFolder: 'bar' }]); service.unregisterEmptyWindowBackupSync('bar'); - const content = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); assert.deepStrictEqual(json2.emptyWorkspaceInfos, []); }); @@ -599,7 +599,7 @@ flakySuite('BackupMainService', () => { await service.initialize(); service.unregisterFolderBackupSync(barFile); service.unregisterEmptyWindowBackupSync('test'); - const content = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(content)); assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); diff --git a/src/vs/platform/extensionManagement/node/extensionDownloader.ts b/src/vs/platform/extensionManagement/node/extensionDownloader.ts index 029bf44d49f..3df9b337cf7 100644 --- a/src/vs/platform/extensionManagement/node/extensionDownloader.ts +++ b/src/vs/platform/extensionManagement/node/extensionDownloader.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { promises } from 'fs'; import { Disposable } from 'vs/base/common/lifecycle'; -import { rename } from 'vs/base/node/pfs'; import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -63,7 +63,7 @@ export class ExtensionsDownloader extends Disposable { private async rename(from: URI, to: URI, retryUntil: number): Promise { try { - await rename(from.fsPath, to.fsPath); + await promises.rename(from.fsPath, to.fsPath); } catch (error) { if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) { this.logService.info(`Failed renaming ${from} to ${to} with 'EPERM' error. Trying again...`); diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index b7616154195..953b17f3b7f 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; import * as pfs from 'vs/base/node/pfs'; @@ -130,7 +131,7 @@ export class ExtensionManagementService extends Disposable implements IExtension const collectFilesFromDirectory = async (dir: string): Promise => { let entries = await pfs.readdir(dir); entries = entries.map(e => path.join(dir, e)); - const stats = await Promise.all(entries.map(e => pfs.stat(e))); + const stats = await Promise.all(entries.map(e => fs.promises.stat(e))); let promise: Promise = Promise.resolve([]); stats.forEach((stat, index) => { const entry = entries[index]; diff --git a/src/vs/platform/extensionManagement/node/extensionsScanner.ts b/src/vs/platform/extensionManagement/node/extensionsScanner.ts index e889257ba95..d3dcc08fe69 100644 --- a/src/vs/platform/extensionManagement/node/extensionsScanner.ts +++ b/src/vs/platform/extensionManagement/node/extensionsScanner.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import * as semver from 'vs/base/common/semver/semver'; import { Disposable } from 'vs/base/common/lifecycle'; import * as pfs from 'vs/base/node/pfs'; @@ -138,7 +139,7 @@ export class ExtensionsScanner extends Disposable { metadata.isMachineScoped = metadata.isMachineScoped || undefined; metadata.isBuiltin = metadata.isBuiltin || undefined; const manifestPath = path.join(local.location.fsPath, 'package.json'); - const raw = await pfs.readFile(manifestPath, 'utf8'); + const raw = await fs.promises.readFile(manifestPath, 'utf8'); const { manifest } = await this.parseManifest(raw); (manifest as ILocalExtensionManifest).__metadata = metadata; await pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t')); @@ -153,7 +154,7 @@ export class ExtensionsScanner extends Disposable { return this.uninstalledFileLimiter.queue(async () => { let raw: string | undefined; try { - raw = await pfs.readFile(this.uninstalledPath, 'utf8'); + raw = await fs.promises.readFile(this.uninstalledPath, 'utf8'); } catch (err) { if (err.code !== 'ENOENT') { throw err; @@ -211,7 +212,7 @@ export class ExtensionsScanner extends Disposable { private async rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise { try { - await pfs.rename(extractPath, renamePath); + await fs.promises.rename(extractPath, renamePath); } catch (error) { if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) { this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identifier.id); @@ -345,9 +346,9 @@ export class ExtensionsScanner extends Disposable { private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IMetadata | null; }> { const promises = [ - pfs.readFile(path.join(extensionPath, 'package.json'), 'utf8') + fs.promises.readFile(path.join(extensionPath, 'package.json'), 'utf8') .then(raw => this.parseManifest(raw)), - pfs.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8') + fs.promises.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8') .then(undefined, err => err.code !== 'ENOENT' ? Promise.reject(err) : '{}') .then(raw => JSON.parse(raw)) ]; diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index c212f37c152..805ad200e1c 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { mkdir, open, close, read, write, fdatasync, Dirent, Stats } from 'fs'; +import { mkdir, open, close, read, write, fdatasync, Dirent, Stats, promises } from 'fs'; import { promisify } from 'util'; import { IDisposable, Disposable, toDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle'; import { FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, IFileSystemProviderWithFileFolderCopyCapability } from 'vs/platform/files/common/files'; 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, unlink, move, copy, readFile, truncate, rimraf, RimRafMode, exists, readdirWithFileTypes } from 'vs/base/node/pfs'; +import { SymlinkSupport, move, copy, rimraf, RimRafMode, exists, readdirWithFileTypes } 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'; @@ -151,7 +151,7 @@ export class DiskFileSystemProvider extends Disposable implements try { const filePath = this.toFilePath(resource); - return await readFile(filePath); + return await promises.readFile(filePath); } catch (error) { throw this.toFileSystemProviderError(error); } @@ -218,7 +218,7 @@ export class DiskFileSystemProvider extends Disposable implements // by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows // (see https://github.com/microsoft/vscode/issues/931) and prevent removing alternate data streams // (see https://github.com/microsoft/vscode/issues/6363) - await truncate(filePath, 0); + await promises.truncate(filePath, 0); // After a successful truncate() the flag can be set to 'r+' which will not truncate. flags = 'r+'; @@ -420,7 +420,7 @@ export class DiskFileSystemProvider extends Disposable implements if (opts.recursive) { await rimraf(filePath, RimRafMode.MOVE); } else { - await unlink(filePath); + await promises.unlink(filePath); } } diff --git a/src/vs/platform/files/test/electron-browser/diskFileService.test.ts b/src/vs/platform/files/test/electron-browser/diskFileService.test.ts index 5b989febfa8..9fbbcbf8694 100644 --- a/src/vs/platform/files/test/electron-browser/diskFileService.test.ts +++ b/src/vs/platform/files/test/electron-browser/diskFileService.test.ts @@ -11,9 +11,9 @@ import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemPro import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { join, basename, dirname, posix } from 'vs/base/common/path'; import { getPathFromAmdModule } from 'vs/base/common/amd'; -import { copy, rimraf, symlink, rimrafSync } from 'vs/base/node/pfs'; +import { copy, rimraf, rimrafSync } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; -import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, createReadStream } from 'fs'; +import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, createReadStream, promises } from 'fs'; import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange, FileChangesEvent, FileOperationError, etag, IStat, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { NullLogService } from 'vs/platform/log/common/log'; import { isLinux, isWindows } from 'vs/base/common/platform'; @@ -408,7 +408,7 @@ flakySuite('Disk File Service', function () { test('resolve - folder symbolic link', async () => { const link = URI.file(join(testDir, 'deep-link')); - await symlink(join(testDir, 'deep'), link.fsPath, 'junction'); + await promises.symlink(join(testDir, 'deep'), link.fsPath, 'junction'); const resolved = await service.resolve(link); assert.strictEqual(resolved.children!.length, 4); @@ -418,7 +418,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('resolve - file symbolic link', async () => { const link = URI.file(join(testDir, 'lorem.txt-linked')); - await symlink(join(testDir, 'lorem.txt'), link.fsPath); + await promises.symlink(join(testDir, 'lorem.txt'), link.fsPath); const resolved = await service.resolve(link); assert.strictEqual(resolved.isDirectory, false); @@ -426,7 +426,7 @@ flakySuite('Disk File Service', function () { }); test('resolve - symbolic link pointing to non-existing file does not break', async () => { - await symlink(join(testDir, 'foo'), join(testDir, 'bar'), 'junction'); + await promises.symlink(join(testDir, 'foo'), join(testDir, 'bar'), 'junction'); const resolved = await service.resolve(URI.file(testDir)); assert.strictEqual(resolved.isDirectory, true); @@ -477,7 +477,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (exists)', async () => { const target = URI.file(join(testDir, 'lorem.txt')); const link = URI.file(join(testDir, 'lorem.txt-linked')); - await symlink(target.fsPath, link.fsPath); + await promises.symlink(target.fsPath, link.fsPath); const source = await service.resolve(link); @@ -499,7 +499,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (pointing to non-existing file)', async () => { const target = URI.file(join(testDir, 'foo')); const link = URI.file(join(testDir, 'bar')); - await symlink(target.fsPath, link.fsPath); + await promises.symlink(target.fsPath, link.fsPath); let event: FileOperationEvent; disposables.add(service.onDidRunOperation(e => event = e)); @@ -2010,7 +2010,7 @@ flakySuite('Disk File Service', function () { (runWatchTests && !isWindows /* windows: cannot create file symbolic link without elevated context */ ? test : test.skip)('watch - file symbolic link', async () => { const toWatch = URI.file(join(testDir, 'lorem.txt-linked')); - await symlink(join(testDir, 'lorem.txt'), toWatch.fsPath); + await promises.symlink(join(testDir, 'lorem.txt'), toWatch.fsPath); const promise = assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]); setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes'), 50); @@ -2138,7 +2138,7 @@ flakySuite('Disk File Service', function () { (runWatchTests ? test : test.skip)('watch - folder (non recursive) - symbolic link - change file', async () => { const watchDir = URI.file(join(testDir, 'deep-link')); - await symlink(join(testDir, 'deep'), watchDir.fsPath, 'junction'); + await promises.symlink(join(testDir, 'deep'), watchDir.fsPath, 'junction'); const file = URI.file(join(watchDir.fsPath, 'index.html')); writeFileSync(file.fsPath, 'Init'); diff --git a/src/vs/platform/localizations/node/localizations.ts b/src/vs/platform/localizations/node/localizations.ts index c4206bd5601..02f3df5a911 100644 --- a/src/vs/platform/localizations/node/localizations.ts +++ b/src/vs/platform/localizations/node/localizations.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as pfs from 'vs/base/node/pfs'; +import { writeFile } from 'vs/base/node/pfs'; +import { promises } from 'fs'; import { createHash } from 'crypto'; import { IExtensionManagementService, ILocalExtension, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -157,7 +158,7 @@ class LanguagePacksCache extends Disposable { private withLanguagePacks(fn: (languagePacks: { [language: string]: ILanguagePack }) => T | null = () => null): Promise { return this.languagePacksFileLimiter.queue(() => { let result: T | null = null; - return pfs.readFile(this.languagePacksFilePath, 'utf8') + return promises.readFile(this.languagePacksFilePath, 'utf8') .then(undefined, err => err.code === 'ENOENT' ? Promise.resolve('{}') : Promise.reject(err)) .then<{ [language: string]: ILanguagePack }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; } }) .then(languagePacks => { result = fn(languagePacks); return languagePacks; }) @@ -171,7 +172,7 @@ class LanguagePacksCache extends Disposable { this.initializedCache = true; const raw = JSON.stringify(this.languagePacks); this.logService.debug('Writing language packs', raw); - return pfs.writeFile(this.languagePacksFilePath, raw); + return writeFile(this.languagePacksFilePath, raw); }) .then(() => result, error => this.logService.error(error)); }); diff --git a/src/vs/platform/state/node/stateService.ts b/src/vs/platform/state/node/stateService.ts index cb46b77fac6..3799c66d4b0 100644 --- a/src/vs/platform/state/node/stateService.ts +++ b/src/vs/platform/state/node/stateService.ts @@ -6,7 +6,7 @@ import * as path from 'vs/base/common/path'; import * as fs from 'fs'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { writeFileSync, readFile } from 'vs/base/node/pfs'; +import { writeFileSync } from 'vs/base/node/pfs'; import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types'; import { IStateService } from 'vs/platform/state/node/state'; import { ILogService } from 'vs/platform/log/common/log'; @@ -58,7 +58,7 @@ export class FileStorage { private async loadAsync(): Promise { try { - this.lastFlushedSerializedDatabase = (await readFile(this.dbPath)).toString(); + this.lastFlushedSerializedDatabase = (await fs.promises.readFile(this.dbPath)).toString(); return JSON.parse(this.lastFlushedSerializedDatabase); } catch (error) { diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 5f069c9ca81..c92a0931dcc 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -146,7 +146,7 @@ export class Win32UpdateService extends AbstractUpdateService { return this.requestService.request({ url }, CancellationToken.None) .then(context => this.fileService.writeFile(URI.file(downloadPath), context.stream)) .then(hash ? () => checksum(downloadPath, update.hash) : () => undefined) - .then(() => pfs.rename(downloadPath, updatePackagePath)) + .then(() => fs.promises.rename(downloadPath, updatePackagePath)) .then(() => updatePackagePath); }); }).then(packagePath => { @@ -196,7 +196,7 @@ export class Win32UpdateService extends AbstractUpdateService { const promises = versions.filter(filter).map(async one => { try { - await pfs.unlink(path.join(cachePath, one)); + await fs.promises.unlink(path.join(cachePath, one)); } catch (err) { // ignore } diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index b7a05876859..7d2daea716d 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -267,8 +267,8 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe let tcp: string = ''; let tcp6: string = ''; try { - tcp = await pfs.readFile('/proc/net/tcp', 'utf8'); - tcp6 = await pfs.readFile('/proc/net/tcp6', 'utf8'); + tcp = await fs.promises.readFile('/proc/net/tcp', 'utf8'); + tcp6 = await fs.promises.readFile('/proc/net/tcp6', 'utf8'); } catch (e) { // File reading error. No additional handling needed. } @@ -286,10 +286,10 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe try { const pid: number = Number(childName); const childUri = resources.joinPath(URI.file('/proc'), childName); - const childStat = await pfs.stat(childUri.fsPath); + const childStat = await fs.promises.stat(childUri.fsPath); if (childStat.isDirectory() && !isNaN(pid)) { const cwd = await promisify(fs.readlink)(resources.joinPath(childUri, 'cwd').fsPath); - const cmd = await pfs.readFile(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8'); + const cmd = await fs.promises.readFile(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8'); processes.push({ pid, cwd, cmd }); } } catch (e) { diff --git a/src/vs/workbench/contrib/cli/node/cli.contribution.ts b/src/vs/workbench/contrib/cli/node/cli.contribution.ts index 30972a41158..74293fcf252 100644 --- a/src/vs/workbench/contrib/cli/node/cli.contribution.ts +++ b/src/vs/workbench/contrib/cli/node/cli.contribution.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; +import * as cp from 'child_process'; import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; -import * as cp from 'child_process'; import * as pfs from 'vs/base/node/pfs'; import * as extpath from 'vs/base/node/extpath'; import { promisify } from 'util'; @@ -74,9 +75,9 @@ class InstallAction extends Action2 { if (!isAvailable || isInstalled) { return Promise.resolve(null); } else { - return pfs.unlink(target) + return fs.promises.unlink(target) .then(undefined, ignore('ENOENT', null)) - .then(() => pfs.symlink(getSource(), target)) + .then(() => fs.promises.symlink(getSource(), target)) .then(undefined, err => { if (err.code === 'EACCES' || err.code === 'ENOENT') { return new Promise((resolve, reject) => { @@ -111,7 +112,7 @@ class InstallAction extends Action2 { } private isInstalled(target: string): Promise { - return pfs.lstat(target) + return fs.promises.lstat(target) .then(stat => stat.isSymbolicLink()) .then(() => extpath.realpath(target)) .then(link => link === getSource()) @@ -149,7 +150,7 @@ class UninstallAction extends Action2 { } const uninstall = () => { - return pfs.unlink(target) + return fs.promises.unlink(target) .then(undefined, ignore('ENOENT', null)); }; diff --git a/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts b/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts index 78832061e97..e0157e09a78 100644 --- a/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts +++ b/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { dirname, join, basename } from 'vs/base/common/path'; -import { exists, readdir, readFile, rimraf } from 'vs/base/node/pfs'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { promises } from 'fs'; import { localize } from 'vs/nls'; +import { dirname, join, basename } from 'vs/base/common/path'; +import { exists, readdir, rimraf } from 'vs/base/node/pfs'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -52,7 +53,7 @@ export class StartupProfiler implements IWorkbenchContribution { const prefix = basename(profileFilenamePrefix); const removeArgs: string[] = ['--prof-startup']; - const markerFile = readFile(profileFilenamePrefix).then(value => removeArgs.push(...value.toString().split('|'))) + const markerFile = promises.readFile(profileFilenamePrefix).then(value => removeArgs.push(...value.toString().split('|'))) .then(() => rimraf(profileFilenamePrefix)) // (1) delete the file to tell the main process to stop profiling .then(() => new Promise(resolve => { // (2) wait for main that recreates the fail to signal profiling has stopped const check = () => { diff --git a/src/vs/workbench/contrib/terminal/node/terminal.ts b/src/vs/workbench/contrib/terminal/node/terminal.ts index 9be699a7ea9..8802e61a467 100644 --- a/src/vs/workbench/contrib/terminal/node/terminal.ts +++ b/src/vs/workbench/contrib/terminal/node/terminal.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as os from 'os'; +import * as fs from 'fs'; import * as platform from 'vs/base/common/platform'; -import { readFile, SymlinkSupport, stat, lstat } from 'vs/base/node/pfs'; +import { SymlinkSupport } from 'vs/base/node/pfs'; import { LinuxDistro, IShellDefinition } from 'vs/workbench/contrib/terminal/common/terminal'; import { coalesce } from 'vs/base/common/arrays'; import { normalize, basename } from 'vs/base/common/path'; @@ -18,7 +19,7 @@ if (platform.isLinux) { if (!exists) { return; } - const buffer = await readFile(file); + const buffer = await fs.promises.readFile(file); const contents = buffer.toString(); if (/NAME="?Fedora"?/.test(contents)) { detectedDistro = LinuxDistro.Fedora; @@ -86,7 +87,7 @@ async function detectAvailableWindowsShells(): Promise { } async function detectAvailableUnixShells(): Promise { - const contents = await readFile('/etc/shells', 'utf8'); + const contents = await fs.promises.readFile('/etc/shells', 'utf8'); const shells = contents.split('\n').filter(e => e.trim().indexOf('#') !== 0 && e.trim().length > 0); return shells.map(e => { return { @@ -105,7 +106,7 @@ async function validateShellPaths(label: string, potentialPaths: string[]): Prom return validateShellPaths(label, potentialPaths); } try { - const result = await stat(normalize(current)); + const result = await fs.promises.stat(normalize(current)); if (result.isFile() || result.isSymbolicLink()) { return { label, @@ -117,7 +118,7 @@ async function validateShellPaths(label: string, potentialPaths: string[]): Prom // throw 'permission denied' using 'stat' but don't throw // using 'lstat' try { - const result = await lstat(normalize(current)); + const result = await fs.promises.lstat(normalize(current)); if (result.isFile() || result.isSymbolicLink()) { return { label, diff --git a/src/vs/workbench/contrib/terminal/node/terminalEnvironment.ts b/src/vs/workbench/contrib/terminal/node/terminalEnvironment.ts index f124b98c928..9a4c932546a 100644 --- a/src/vs/workbench/contrib/terminal/node/terminalEnvironment.ts +++ b/src/vs/workbench/contrib/terminal/node/terminalEnvironment.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; -import { readFile, exists } from 'vs/base/node/pfs'; +import * as fs from 'fs'; import * as path from 'vs/base/common/path'; +import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; +import { exists } from 'vs/base/node/pfs'; import { isString } from 'vs/base/common/types'; import { getCaseInsensitive } from 'vs/base/common/objects'; @@ -26,7 +27,7 @@ export async function getMainProcessParentEnv(baseEnvironment: IProcessEnvironme let name: string = codeProcessName; do { pid = ppid; - const status = await readFile(`/proc/${pid}/status`, 'utf8'); + const status = await fs.promises.readFile(`/proc/${pid}/status`, 'utf8'); const splitByLine = status.split('\n'); splitByLine.forEach(line => { if (line.indexOf('Name:') === 0) { @@ -37,7 +38,7 @@ export async function getMainProcessParentEnv(baseEnvironment: IProcessEnvironme } }); } while (name === codeProcessName); - const rawEnv = await readFile(`/proc/${pid}/environ`, 'utf8'); + const rawEnv = await fs.promises.readFile(`/proc/${pid}/environ`, 'utf8'); const env: IProcessEnvironment = {}; rawEnv.split('\0').forEach(e => { const i = e.indexOf('='); diff --git a/src/vs/workbench/contrib/terminal/node/terminalProcess.ts b/src/vs/workbench/contrib/terminal/node/terminalProcess.ts index 79bb093ca17..59ba95e9397 100644 --- a/src/vs/workbench/contrib/terminal/node/terminalProcess.ts +++ b/src/vs/workbench/contrib/terminal/node/terminalProcess.ts @@ -14,7 +14,6 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalLaunchError, FlowControlConstants } from 'vs/workbench/contrib/terminal/common/terminal'; import { exec } from 'child_process'; import { ILogService } from 'vs/platform/log/common/log'; -import { stat } from 'vs/base/node/pfs'; import { findExecutable } from 'vs/workbench/contrib/terminal/node/terminalEnvironment'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; @@ -122,7 +121,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private async _validateCwd(): Promise { try { - const result = await stat(this._initialCwd); + const result = await fs.promises.stat(this._initialCwd); if (!result.isDirectory()) { return { message: localize('launchFail.cwdNotDirectory', "Starting directory (cwd) \"{0}\" is not a directory", this._initialCwd.toString()) }; } @@ -140,7 +139,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess throw new Error('IShellLaunchConfig.executable not set'); } try { - const result = await stat(slc.executable); + const result = await fs.promises.stat(slc.executable); if (!result.isFile() && !result.isSymbolicLink()) { return { message: localize('launchFail.executableIsNotFileOrSymlink', "Path to shell executable \"{0}\" is not a file of a symlink", slc.executable) }; } diff --git a/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts index 5c2f3f57209..792a4cc3816 100644 --- a/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts @@ -151,7 +151,7 @@ export class CachedExtensionScanner { const cacheFile = path.join(cacheFolder, cacheKey); try { - const cacheRawContents = await pfs.readFile(cacheFile, 'utf8'); + const cacheRawContents = await fs.promises.readFile(cacheFile, 'utf8'); return JSON.parse(cacheRawContents); } catch (err) { // That's ok... @@ -184,7 +184,7 @@ export class CachedExtensionScanner { } try { - const folderStat = await pfs.stat(input.absoluteFolderPath); + const folderStat = await fs.promises.stat(input.absoluteFolderPath); input.mtime = folderStat.mtime.getTime(); } catch (err) { // That's ok... @@ -224,7 +224,7 @@ export class CachedExtensionScanner { private static async _readTranslationConfig(): Promise { if (platform.translationsConfigFile) { try { - const content = await pfs.readFile(platform.translationsConfigFile, 'utf8'); + const content = await fs.promises.readFile(platform.translationsConfigFile, 'utf8'); return JSON.parse(content) as Translations; } catch (err) { // no problemo @@ -263,7 +263,7 @@ export class CachedExtensionScanner { const builtInExtensions = Promise.resolve(productService.builtInExtensions || []); const controlFilePath = joinPath(environmentService.userHome, '.vscode-oss-dev', 'extensions', 'control.json').fsPath; - const controlFile = pfs.readFile(controlFilePath, 'utf8') + const controlFile = fs.promises.readFile(controlFilePath, 'utf8') .then(raw => JSON.parse(raw), () => ({} as any)); const input = new ExtensionScannerInput(version, commit, locale, devMode, getExtraDevSystemExtensionsRoot(), true, false, translations); diff --git a/src/vs/workbench/services/extensions/node/extensionPoints.ts b/src/vs/workbench/services/extensions/node/extensionPoints.ts index bef6b6e9235..6b2890ada83 100644 --- a/src/vs/workbench/services/extensions/node/extensionPoints.ts +++ b/src/vs/workbench/services/extensions/node/extensionPoints.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; import * as semver from 'vs/base/common/semver/semver'; @@ -57,7 +58,7 @@ class ExtensionManifestParser extends ExtensionManifestHandler { } public parse(): Promise { - return pfs.readFile(this._absoluteManifestPath).then((manifestContents) => { + return fs.promises.readFile(this._absoluteManifestPath).then((manifestContents) => { const errors: json.ParseError[] = []; const manifest = ExtensionManifestParser._fastParseJSON(manifestContents.toString(), errors); if (json.getNodeType(manifest) !== 'object') { @@ -127,7 +128,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { let translationPath = this._nlsConfig.translations[translationId]; let localizedMessages: Promise; if (translationPath) { - localizedMessages = pfs.readFile(translationPath, 'utf8').then((content) => { + localizedMessages = fs.promises.readFile(translationPath, 'utf8').then((content) => { let errors: json.ParseError[] = []; let translationBundle: TranslationBundle = json.parse(content, errors); if (errors.length > 0) { @@ -152,7 +153,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { if (!messageBundle.localized) { return { values: undefined, default: messageBundle.original }; } - return pfs.readFile(messageBundle.localized, 'utf8').then(messageBundleContent => { + return fs.promises.readFile(messageBundle.localized, 'utf8').then(messageBundleContent => { let errors: json.ParseError[] = []; let messages: MessageBag = json.parse(messageBundleContent, errors); if (errors.length > 0) { @@ -201,7 +202,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { private static resolveOriginalMessageBundle(originalMessageBundle: string | null, errors: json.ParseError[]) { return new Promise<{ [key: string]: string; } | null>((c, e) => { if (originalMessageBundle) { - pfs.readFile(originalMessageBundle).then(originalBundleContent => { + fs.promises.readFile(originalMessageBundle).then(originalBundleContent => { c(json.parse(originalBundleContent.toString(), errors)); }, (err) => { c(null); @@ -548,7 +549,7 @@ export class ExtensionScanner { let obsolete: { [folderName: string]: boolean; } = {}; if (!isBuiltin) { try { - const obsoleteFileContents = await pfs.readFile(path.join(absoluteFolderPath, '.obsolete'), 'utf8'); + const obsoleteFileContents = await fs.promises.readFile(path.join(absoluteFolderPath, '.obsolete'), 'utf8'); obsolete = JSON.parse(obsoleteFileContents); } catch (err) { // Don't care diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts b/src/vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts index acdefd4504b..4369c32cc63 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts +++ b/src/vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts @@ -5,10 +5,11 @@ import * as assert from 'assert'; import * as path from 'vs/base/common/path'; +import { promises } from 'fs'; import { getPathFromAmdModule } from 'vs/base/common/amd'; import { Keybinding, ResolvedKeybinding, SimpleKeybinding } from 'vs/base/common/keyCodes'; import { ScanCodeBinding } from 'vs/base/common/scanCode'; -import { readFile, writeFile } from 'vs/base/node/pfs'; +import { writeFile } from 'vs/base/node/pfs'; import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; import { IKeyboardMapper } from 'vs/platform/keyboardLayout/common/keyboardMapper'; @@ -52,7 +53,7 @@ export function assertResolveUserBinding(mapper: IKeyboardMapper, parts: (Simple } export function readRawMapping(file: string): Promise { - return readFile(getPathFromAmdModule(require, `vs/workbench/services/keybinding/test/electron-browser/${file}.js`)).then((buff) => { + return promises.readFile(getPathFromAmdModule(require, `vs/workbench/services/keybinding/test/electron-browser/${file}.js`)).then((buff) => { let contents = buff.toString(); let func = new Function('define', contents); let rawMappings: T | null = null; @@ -66,7 +67,7 @@ export function readRawMapping(file: string): Promise { export function assertMapping(writeFileIfDifferent: boolean, mapper: IKeyboardMapper, file: string): Promise { const filePath = path.normalize(getPathFromAmdModule(require, `vs/workbench/services/keybinding/test/electron-browser/${file}`)); - return readFile(filePath).then((buff) => { + return promises.readFile(filePath).then((buff) => { const expected = buff.toString().replace(/\r\n/g, '\n'); const actual = mapper.dumpDebugInfo().replace(/\r\n/g, '\n'); if (actual !== expected && writeFileIfDifferent) { diff --git a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts index 60d73593f0e..8f12705e0ec 100644 --- a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts +++ b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { promises } from 'fs'; import { localize } from 'vs/nls'; import { AbstractTextFileService } from 'vs/workbench/services/textfile/browser/textFileService'; import { ITextFileService, ITextFileStreamContent, ITextFileContent, IReadTextFileOptions, IWriteTextFileOptions } from 'vs/workbench/services/textfile/common/textfiles'; @@ -10,7 +11,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { URI } from 'vs/base/common/uri'; import { IFileStatWithMetadata, FileOperationError, FileOperationResult, IFileService, ByteSize } from 'vs/platform/files/common/files'; import { Schemas } from 'vs/base/common/network'; -import { stat, chmod, MAX_FILE_SIZE, MAX_HEAP_SIZE } from 'vs/base/node/pfs'; +import { MAX_FILE_SIZE, MAX_HEAP_SIZE } from 'vs/base/node/pfs'; import { join } from 'vs/base/common/path'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; import { UTF8, UTF8_with_bom } from 'vs/workbench/services/textfile/common/encoding'; @@ -109,10 +110,10 @@ export class NativeTextFileService extends AbstractTextFileService { // check for overwriteReadonly property (only supported for local file://) try { if (options?.overwriteReadonly && resource.scheme === Schemas.file) { - const fileStat = await stat(resource.fsPath); + const fileStat = await promises.stat(resource.fsPath); // try to change mode to writeable - await chmod(resource.fsPath, fileStat.mode | 0o200 /* File mode indicating writable by owner (fs.constants.S_IWUSR) */); + await promises.chmod(resource.fsPath, fileStat.mode | 0o200 /* File mode indicating writable by owner (fs.constants.S_IWUSR) */); } } catch (error) { // ignore and simply retry the operation @@ -131,7 +132,7 @@ export class NativeTextFileService extends AbstractTextFileService { if ((error).fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED) { let isReadonly = false; try { - const fileStat = await stat(resource.fsPath); + const fileStat = await promises.stat(resource.fsPath); if (!(fileStat.mode & 0o200 /* File mode indicating writable by owner (fs.constants.S_IWUSR) */)) { isReadonly = true; } diff --git a/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.io.test.ts b/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.io.test.ts index 59579ff1348..bd27a73b378 100644 --- a/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.io.test.ts @@ -3,17 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { tmpdir } from 'os'; +import { promises } from 'fs'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IFileService } from 'vs/platform/files/common/files'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { Schemas } from 'vs/base/common/network'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { rimraf, copy, readFile, exists, stat } from 'vs/base/node/pfs'; +import { rimraf, copy, exists } from 'vs/base/node/pfs'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService } from 'vs/platform/log/common/log'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; -import { tmpdir } from 'os'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { getPathFromAmdModule } from 'vs/base/common/amd'; import { detectEncodingByBOM } from 'vs/workbench/services/textfile/test/node/encoding/encoding.test'; @@ -29,6 +30,12 @@ flakySuite('Files - NativeTextFileService i/o', function () { let service: ITextFileService; let testDir: string; + function readFile(path: string): Promise; + function readFile(path: string, encoding: string): Promise; + function readFile(path: string, encoding?: string): Promise { + return promises.readFile(path, encoding); + } + createSuite({ setup: async () => { const instantiationService = workbenchInstantiationService(); @@ -64,7 +71,7 @@ flakySuite('Files - NativeTextFileService i/o', function () { }, exists, - stat, + stat: promises.stat, readFile, detectEncodingByBOM }); diff --git a/src/vs/workbench/test/electron-browser/colorRegistry.releaseTest.ts b/src/vs/workbench/test/electron-browser/colorRegistry.releaseTest.ts index 3ba7c797579..0480a1563ab 100644 --- a/src/vs/workbench/test/electron-browser/colorRegistry.releaseTest.ts +++ b/src/vs/workbench/test/electron-browser/colorRegistry.releaseTest.ts @@ -5,8 +5,8 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IColorRegistry, Extensions, ColorContribution } from 'vs/platform/theme/common/colorRegistry'; - import { asText } from 'vs/platform/request/common/request'; +import * as fs from 'fs'; import * as pfs from 'vs/base/node/pfs'; import * as path from 'vs/base/common/path'; import * as assert from 'assert'; @@ -17,7 +17,6 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import 'vs/workbench/workbench.desktop.main'; import { NullLogService } from 'vs/platform/log/common/log'; - interface ColorInfo { description: string; offset: number; @@ -106,7 +105,7 @@ async function getColorsFromExtension(): Promise<{ [id: string]: string }> { let result: { [id: string]: string } = Object.create(null); for (let folder of extFolders) { try { - let packageJSON = JSON.parse((await pfs.readFile(path.join(extPath, folder, 'package.json'))).toString()); + let packageJSON = JSON.parse((await fs.promises.readFile(path.join(extPath, folder, 'package.json'))).toString()); let contributes = packageJSON['contributes']; if (contributes) { let colors = contributes['colors']; From b28cd23d521059938e08515618cdc670f3baa79b Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 3 Feb 2021 11:29:26 +0100 Subject: [PATCH 10/31] fs - more removal of promisify utility --- src/vs/base/node/extpath.ts | 5 ++--- src/vs/base/node/pfs.ts | 5 ++--- src/vs/base/node/processes.ts | 6 +++--- src/vs/platform/files/node/diskFileSystemProvider.ts | 4 ++-- src/vs/workbench/api/node/extHostTunnelService.ts | 3 +-- .../contrib/performance/electron-browser/startupTimings.ts | 5 ++--- src/vs/workbench/services/extensions/node/proxyResolver.ts | 3 +-- 7 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/vs/base/node/extpath.ts b/src/vs/base/node/extpath.ts index 0096544ada5..b43c407d519 100644 --- a/src/vs/base/node/extpath.ts +++ b/src/vs/base/node/extpath.ts @@ -7,7 +7,6 @@ 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 @@ -53,7 +52,7 @@ export function realcaseSync(path: string): string | null { export async function realpath(path: string): Promise { try { - return await promisify(fs.realpath)(path); + return await fs.promises.realpath(path); } catch (error) { // We hit an error calling fs.realpath(). Since fs.realpath() is doing some path normalization @@ -63,7 +62,7 @@ export async function realpath(path: string): Promise { // 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); + await fs.promises.access(normalizedPath, fs.constants.R_OK); return normalizedPath; } diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index f9ec3033870..7794ebf1017 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -9,7 +9,6 @@ import { join } from 'vs/base/common/path'; import { Queue } from 'vs/base/common/async'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { Event } from 'vs/base/common/event'; -import { promisify } from 'util'; import { isRootOrDriveLetter } from 'vs/base/common/extpath'; import { generateUuid } from 'vs/base/common/uuid'; import { normalizeNFC } from 'vs/base/common/normalization'; @@ -95,11 +94,11 @@ export function rimrafSync(path: string): void { //#region readdir with NFC support (macos) export async function readdir(path: string): Promise { - return handleDirectoryChildren(await promisify(fs.readdir)(path)); + return handleDirectoryChildren(await fs.promises.readdir(path)); } export async function readdirWithFileTypes(path: string): Promise { - const children = await promisify(fs.readdir)(path, { withFileTypes: true }); + 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 diff --git a/src/vs/base/node/processes.ts b/src/vs/base/node/processes.ts index de16ce24d00..72c1a613a28 100644 --- a/src/vs/base/node/processes.ts +++ b/src/vs/base/node/processes.ts @@ -5,7 +5,7 @@ import * as path from 'vs/base/common/path'; import * as fs from 'fs'; -import { promisify } from 'util'; +import * as pfs from 'vs/base/node/pfs'; import * as cp from 'child_process'; import * as nls from 'vs/nls'; import * as Types from 'vs/base/common/types'; @@ -456,8 +456,8 @@ export namespace win32 { } async function fileExists(path: string): Promise { - if (await promisify(fs.exists)(path)) { - return !((await promisify(fs.stat)(path)).isDirectory()); + if (await pfs.exists(path)) { + return !((await fs.promises.stat(path)).isDirectory()); } return false; } diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index 805ad200e1c..4544c8f4edd 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { mkdir, open, close, read, write, fdatasync, Dirent, Stats, promises } from 'fs'; +import { open, close, read, write, fdatasync, Dirent, Stats, promises } from 'fs'; import { promisify } from 'util'; import { IDisposable, Disposable, toDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle'; import { FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, IFileSystemProviderWithFileFolderCopyCapability } from 'vs/platform/files/common/files'; @@ -400,7 +400,7 @@ export class DiskFileSystemProvider extends Disposable implements async mkdir(resource: URI): Promise { try { - await promisify(mkdir)(this.toFilePath(resource)); + await promises.mkdir(this.toFilePath(resource)); } catch (error) { throw this.toFileSystemProviderError(error); } diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index 7d2daea716d..5b079fa0cf1 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -18,7 +18,6 @@ import { IExtHostTunnelService, TunnelDto } from 'vs/workbench/api/common/extHos import { Event, Emitter } from 'vs/base/common/event'; import { TunnelOptions, TunnelCreationOptions } from 'vs/platform/remote/common/tunnel'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { promisify } from 'util'; import { MovingAverage } from 'vs/base/common/numbers'; import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { ILogService } from 'vs/platform/log/common/log'; @@ -288,7 +287,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe const childUri = resources.joinPath(URI.file('/proc'), childName); const childStat = await fs.promises.stat(childUri.fsPath); if (childStat.isDirectory() && !isNaN(pid)) { - const cwd = await promisify(fs.readlink)(resources.joinPath(childUri, 'cwd').fsPath); + const cwd = await fs.promises.readlink(resources.joinPath(childUri, 'cwd').fsPath); const cmd = await fs.promises.readFile(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8'); processes.push({ pid, cwd, cmd }); } diff --git a/src/vs/workbench/contrib/performance/electron-browser/startupTimings.ts b/src/vs/workbench/contrib/performance/electron-browser/startupTimings.ts index 47c85ed5304..2bbb3f6ece4 100644 --- a/src/vs/workbench/contrib/performance/electron-browser/startupTimings.ts +++ b/src/vs/workbench/contrib/performance/electron-browser/startupTimings.ts @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { appendFile } from 'fs'; +import { promises } from 'fs'; import { timeout } from 'vs/base/common/async'; -import { promisify } from 'util'; import { onUnexpectedError } from 'vs/base/common/errors'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; @@ -58,7 +57,7 @@ export class StartupTimings implements IWorkbenchContribution { this._timerService.whenReady(), timeout(15000), // wait: cached data creation, telemetry sending ]).then(() => { - return promisify(appendFile)(appendTo, `${this._timerService.startupMetrics.ellapsed}\t${this._productService.nameShort}\t${(this._productService.commit || '').slice(0, 10) || '0000000000'}\t${sessionId}\t${standardStartupError === undefined ? 'standard_start' : 'NO_standard_start : ' + standardStartupError}\n`); + return promises.appendFile(appendTo, `${this._timerService.startupMetrics.ellapsed}\t${this._productService.nameShort}\t${(this._productService.commit || '').slice(0, 10) || '0000000000'}\t${sessionId}\t${standardStartupError === undefined ? 'standard_start' : 'NO_standard_start : ' + standardStartupError}\n`); }).then(() => { this._nativeHostService.quit(); }).catch(err => { diff --git a/src/vs/workbench/services/extensions/node/proxyResolver.ts b/src/vs/workbench/services/extensions/node/proxyResolver.ts index d520ce8bd05..36aa0fbd61c 100644 --- a/src/vs/workbench/services/extensions/node/proxyResolver.ts +++ b/src/vs/workbench/services/extensions/node/proxyResolver.ts @@ -18,7 +18,6 @@ import { MainThreadTelemetryShape, IInitData } from 'vs/workbench/api/common/ext import { toErrorMessage } from 'vs/base/common/errorMessage'; import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService'; import { URI } from 'vs/base/common/uri'; -import { promisify } from 'util'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; @@ -540,7 +539,7 @@ const linuxCaCertificatePaths = [ async function readLinuxCaCertificates() { for (const certPath of linuxCaCertificatePaths) { try { - const content = await promisify(fs.readFile)(certPath, { encoding: 'utf8' }); + const content = await fs.promises.readFile(certPath, { encoding: 'utf8' }); const certs = new Set(content.split(/(?=-----BEGIN CERTIFICATE-----)/g) .filter(pem => !!pem.length)); return { From aca5d3ea2e9f75a47c0fdfe126bb4abebd610704 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 3 Feb 2021 12:07:48 +0100 Subject: [PATCH 11/31] fs - merge readdir functions into one --- src/vs/base/node/pfs.ts | 139 +++++++++++++----- src/vs/base/node/powershell.ts | 8 +- src/vs/base/test/node/pfs/pfs.test.ts | 6 +- .../files/node/diskFileSystemProvider.ts | 4 +- .../electron-main/nativeHostMainService.ts | 2 +- .../api/node/extHostOutputService.ts | 2 +- .../contrib/terminal/node/terminal.ts | 2 +- .../extensions/node/extensionPoints.ts | 6 +- .../api/extHostSearch.test.ts | 4 +- 9 files changed, 121 insertions(+), 52 deletions(-) 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', From 1cab95f5b6c8da52774c6316137f21e8232e8008 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 3 Feb 2021 12:09:51 +0100 Subject: [PATCH 12/31] fs - fix compile error for now --- src/vs/base/node/pfs.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index d22fc2749b4..09218e68356 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -620,3 +620,10 @@ export async function exists(path: string): Promise { } //#endregion + +// TODO@bpasero remove me +export function readFile(path: string): Promise; +export function readFile(path: string, encoding: string): Promise; +export function readFile(path: string, encoding?: string): Promise { + return fs.promises.readFile(path, encoding); +} From 495ed05511720e5b575dc5598b59b30f992755ab Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 3 Feb 2021 13:47:45 +0100 Subject: [PATCH 13/31] fs - preserve symlinks when copying them (#114881) --- src/vs/base/node/pfs.ts | 79 ++++++++++++++------------- src/vs/base/test/node/pfs/pfs.test.ts | 24 ++++++-- 2 files changed, 60 insertions(+), 43 deletions(-) diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index 09218e68356..93e0c72cb8a 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -530,79 +530,80 @@ export async function move(source: string, target: string): Promise { * as files and folders. */ export async function copy(source: string, target: string): Promise { - return doCopy(source, target); + return doCopy(source, target, new Set()); } // 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. // (https://github.com/nodejs/node-v0.x-archive/issues/3045#issuecomment-4862588) -// -// The `copy` method is very old so we should probably revisit -// it's implementation and check wether this mask is still needed. const COPY_MODE_MASK = 0o777; -async function doCopy(source: string, target: string, handledSourcesIn?: { [path: string]: boolean }): Promise { +async function doCopy(source: string, target: string, handledSourcePaths: Set): Promise { // Keep track of paths already copied to prevent // cycles from symbolic links to cause issues - const handledSources = handledSourcesIn ?? Object.create(null); - if (handledSources[source]) { + if (handledSourcePaths.has(source)) { return; } else { - handledSources[source] = true; + handledSourcePaths.add(source); } const { stat, symbolicLink } = await SymlinkSupport.stat(source); - if (symbolicLink?.dangling) { - return; // skip over dangling symbolic links (https://github.com/microsoft/vscode/issues/111621) + + // Symlink + if (symbolicLink) { + if (symbolicLink.dangling) { + return; // do not copy dangling symbolic links (https://github.com/microsoft/vscode/issues/111621) + } + + try { + return await doCopySymlink(source, target); + } catch (error) { + // in any case of an error fallback to normal copy via dereferencing + console.warn('[node.js fs] copy of symlink failed: ', error); + } } - if (!stat.isDirectory()) { + // Folder + if (stat.isDirectory()) { + return doCopyDirectory(source, target, stat.mode & COPY_MODE_MASK, handledSourcePaths); + } + + // File or file-like + else if (stat.isFile() || stat.isCharacterDevice() || stat.isBlockDevice()) { return doCopyFile(source, target, stat.mode & COPY_MODE_MASK); } +} + +async function doCopyDirectory(source: string, target: string, mode: number, handledSourcePaths: Set): Promise { // Create folder - await fs.promises.mkdir(target, { recursive: true, mode: stat.mode & COPY_MODE_MASK }); + await fs.promises.mkdir(target, { recursive: true, mode }); // Copy each file recursively const files = await readdir(source); - for (let i = 0; i < files.length; i++) { - const file = files[i]; - await doCopy(join(source, file), join(target, file), handledSources); + for (const file of files) { + await doCopy(join(source, file), join(target, file), handledSourcePaths); } } async function doCopyFile(source: string, target: string, mode: number): Promise { - 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; + // Copy file + await fs.promises.copyFile(source, target); - // in error cases, pass to callback - if (error) { - return reject(error); - } + // restore mode (https://github.com/nodejs/node/issues/1104) + await fs.promises.chmod(target, mode); +} - // we need to explicitly chmod because of https://github.com/nodejs/node/issues/1104 - fs.chmod(target, mode, error => error ? reject(error) : resolve()); - } - }; +async function doCopySymlink(source: string, target: string): Promise { - // handle errors properly - reader.once('error', error => finish(error)); - writer.once('error', error => finish(error)); + // Figure out link target + const linkTarget = await fs.promises.readlink(source); - // we are done (underlying fd has been closed) - writer.once('close', () => finish()); - - // start piping - reader.pipe(writer); - }); + // Create symlink + await fs.promises.symlink(linkTarget, target); } //#endregion diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index 817e3c369e5..8a04095618a 100644 --- a/src/vs/base/test/node/pfs/pfs.test.ts +++ b/src/vs/base/test/node/pfs/pfs.test.ts @@ -190,23 +190,39 @@ flakySuite('PFS', function () { assert.ok(!fs.existsSync(parentDir)); }); - test('copy skips over dangling symbolic links', async () => { + test('copy handles symbolic links', async () => { const id1 = generateUuid(); const symbolicLinkTarget = join(testDir, id1); const id2 = generateUuid(); - const symbolicLink = join(testDir, id2); + const symLink = join(testDir, id2); const id3 = generateUuid(); const copyTarget = join(testDir, id3); await fs.promises.mkdir(symbolicLinkTarget, { recursive: true }); - fs.symlinkSync(symbolicLinkTarget, symbolicLink, 'junction'); + fs.symlinkSync(symbolicLinkTarget, symLink, 'junction'); + // Copy preserves symlinks + + await copy(symLink, copyTarget); + + assert.ok(fs.existsSync(copyTarget)); + + const { symbolicLink } = await SymlinkSupport.stat(copyTarget); + assert.ok(symbolicLink); + assert.ok(!symbolicLink.dangling); + + const target = await fs.promises.readlink(copyTarget); + assert.strictEqual(target, symbolicLinkTarget); + + // Copy ignores dangling symlinks + + await rimraf(copyTarget); await rimraf(symbolicLinkTarget); - await copy(symbolicLink, copyTarget); // this should not throw + await copy(symLink, copyTarget); // this should not throw assert.ok(!fs.existsSync(copyTarget)); }); From 2d31774a528659027a7561d316e5aa7731ce5e1a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 3 Feb 2021 16:37:10 +0100 Subject: [PATCH 14/31] fs - provide an option to `copy` to preserve symlinks or not --- src/vs/base/node/pfs.ts | 34 +++++++++++-------- src/vs/base/parts/storage/node/storage.ts | 2 +- src/vs/base/test/node/pfs/pfs.test.ts | 18 +++++++--- .../files/node/diskFileSystemProvider.ts | 2 +- .../electron-browser/diskFileService.test.ts | 2 +- .../platform/storage/node/storageService.ts | 2 +- .../nativeTextFileService.io.test.ts | 2 +- 7 files changed, 38 insertions(+), 24 deletions(-) diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index 93e0c72cb8a..b74af510405 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -514,7 +514,7 @@ export async function move(source: string, target: string): Promise { // 2.) The user tries to rename a file/folder that ends with a dot. This is not // really possible to move then, at least on UNC devices. if (source.toLowerCase() !== target.toLowerCase() && error.code === 'EXDEV' || source.endsWith('.')) { - await copy(source, target); + await copy(source, target, { preserveSymlinks: false /* copying to another device */ }); await rimraf(source, RimRafMode.MOVE); await updateMtime(target); } else { @@ -526,11 +526,12 @@ 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. + * The options `preserveSymlinks` configures how symbolic + * links should be handled when encountered. Set to + * `false` to not preserve them and `true` otherwise. */ -export async function copy(source: string, target: string): Promise { - return doCopy(source, target, new Set()); +export async function copy(source: string, target: string, options: { preserveSymlinks: boolean }): Promise { + return doCopy(source, target, options, new Set()); } // When copying a file or folder, we want to preserve the mode @@ -539,7 +540,7 @@ export async function copy(source: string, target: string): Promise { // (https://github.com/nodejs/node-v0.x-archive/issues/3045#issuecomment-4862588) const COPY_MODE_MASK = 0o777; -async function doCopy(source: string, target: string, handledSourcePaths: Set): Promise { +async function doCopy(source: string, target: string, options: { preserveSymlinks: boolean }, handledSourcePaths: Set): Promise { // Keep track of paths already copied to prevent // cycles from symbolic links to cause issues @@ -557,26 +558,29 @@ async function doCopy(source: string, target: string, handledSourcePaths: Set): Promise { +async function doCopyDirectory(source: string, target: string, mode: number, options: { preserveSymlinks: boolean }, handledSourcePaths: Set,): Promise { // Create folder await fs.promises.mkdir(target, { recursive: true, mode }); @@ -584,7 +588,7 @@ async function doCopyDirectory(source: string, target: string, mode: number, han // Copy each file recursively const files = await readdir(source); for (const file of files) { - await doCopy(join(source, file), join(target, file), handledSourcePaths); + await doCopy(join(source, file), join(target, file), options, handledSourcePaths); } } diff --git a/src/vs/base/parts/storage/node/storage.ts b/src/vs/base/parts/storage/node/storage.ts index 410158d5c5b..e497862ddaf 100644 --- a/src/vs/base/parts/storage/node/storage.ts +++ b/src/vs/base/parts/storage/node/storage.ts @@ -217,7 +217,7 @@ export class SQLiteStorageDatabase implements IStorageDatabase { private backup(): Promise { const backupPath = this.toBackupPath(this.path); - return copy(this.path, backupPath); + return copy(this.path, backupPath, { preserveSymlinks: false }); } private toBackupPath(path: string): string { diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index 8a04095618a..0a6abbf8430 100644 --- a/src/vs/base/test/node/pfs/pfs.test.ts +++ b/src/vs/base/test/node/pfs/pfs.test.ts @@ -161,7 +161,7 @@ flakySuite('PFS', function () { const targetDir = join(parentDir, id); const targetDir2 = join(parentDir, id2); - await copy(sourceDir, targetDir); + await copy(sourceDir, targetDir, { preserveSymlinks: true }); assert.ok(fs.existsSync(targetDir)); assert.ok(fs.existsSync(join(targetDir, 'index.html'))); @@ -204,9 +204,9 @@ flakySuite('PFS', function () { fs.symlinkSync(symbolicLinkTarget, symLink, 'junction'); - // Copy preserves symlinks + // Copy preserves symlinks if configured as such - await copy(symLink, copyTarget); + await copy(symLink, copyTarget, { preserveSymlinks: true }); assert.ok(fs.existsSync(copyTarget)); @@ -217,12 +217,22 @@ flakySuite('PFS', function () { const target = await fs.promises.readlink(copyTarget); assert.strictEqual(target, symbolicLinkTarget); + // Copy does not preserve symlinks if configured as such + + await rimraf(copyTarget); + await copy(symLink, copyTarget, { preserveSymlinks: false }); + + assert.ok(fs.existsSync(copyTarget)); + + const { symbolicLink: symbolicLink2 } = await SymlinkSupport.stat(copyTarget); + assert.ok(!symbolicLink2); + // Copy ignores dangling symlinks await rimraf(copyTarget); await rimraf(symbolicLinkTarget); - await copy(symLink, copyTarget); // this should not throw + await copy(symLink, copyTarget, { preserveSymlinks: true }); // this should not throw assert.ok(!fs.existsSync(copyTarget)); }); diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index 74787b6d3e7..4017e98c06c 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -465,7 +465,7 @@ export class DiskFileSystemProvider extends Disposable implements await this.validateTargetDeleted(from, to, 'copy', opts.overwrite); // Copy - await copy(fromFilePath, toFilePath); + await copy(fromFilePath, toFilePath, { preserveSymlinks: true }); } catch (error) { // rewrite some typical errors that can happen especially around symlinks diff --git a/src/vs/platform/files/test/electron-browser/diskFileService.test.ts b/src/vs/platform/files/test/electron-browser/diskFileService.test.ts index 9fbbcbf8694..667e135bca6 100644 --- a/src/vs/platform/files/test/electron-browser/diskFileService.test.ts +++ b/src/vs/platform/files/test/electron-browser/diskFileService.test.ts @@ -147,7 +147,7 @@ flakySuite('Disk File Service', function () { const sourceDir = getPathFromAmdModule(require, './fixtures/service'); - await copy(sourceDir, testDir); + await copy(sourceDir, testDir, { preserveSymlinks: false }); }); teardown(() => { diff --git a/src/vs/platform/storage/node/storageService.ts b/src/vs/platform/storage/node/storageService.ts index c469537d3a1..4d7cdac0f9b 100644 --- a/src/vs/platform/storage/node/storageService.ts +++ b/src/vs/platform/storage/node/storageService.ts @@ -268,7 +268,7 @@ export class NativeStorageService extends AbstractStorageService { const newWorkspaceStoragePath = join(result.path, NativeStorageService.WORKSPACE_STORAGE_NAME); // Copy current storage over to new workspace storage - await copy(assertIsDefined(this.workspaceStoragePath), newWorkspaceStoragePath); + await copy(assertIsDefined(this.workspaceStoragePath), newWorkspaceStoragePath, { preserveSymlinks: false }); // Recreate and init workspace storage return this.createWorkspaceStorage(newWorkspaceStoragePath).init(); diff --git a/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.io.test.ts b/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.io.test.ts index bd27a73b378..176ea48d83a 100644 --- a/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.io.test.ts @@ -57,7 +57,7 @@ flakySuite('Files - NativeTextFileService i/o', function () { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'textfileservice'); const sourceDir = getPathFromAmdModule(require, './fixtures'); - await copy(sourceDir, testDir); + await copy(sourceDir, testDir, { preserveSymlinks: false }); return { service, testDir }; }, From ef0657580f95f0d8afb101d5d624a4cfc75eef24 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 3 Feb 2021 18:03:42 +0100 Subject: [PATCH 15/31] fs - preserve symlinks properly --- src/vs/base/node/pfs.ts | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index b74af510405..6201d050044 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -7,9 +7,9 @@ import * as fs from 'fs'; import { tmpdir } from 'os'; import { join } from 'vs/base/common/path'; import { Queue } from 'vs/base/common/async'; -import { isMacintosh, isWindows } from 'vs/base/common/platform'; +import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { Event } from 'vs/base/common/event'; -import { isRootOrDriveLetter } from 'vs/base/common/extpath'; +import { isEqualOrParent, isRootOrDriveLetter } from 'vs/base/common/extpath'; import { generateUuid } from 'vs/base/common/uuid'; import { normalizeNFC } from 'vs/base/common/normalization'; @@ -523,6 +523,12 @@ export async function move(source: string, target: string): Promise { } } +interface ICopyPayload { + readonly root: { source: string, target: string }; + readonly options: { preserveSymlinks: boolean }; + readonly handledSourcePaths: Set; +} + /** * Recursively copies all of `source` to `target`. * @@ -531,7 +537,7 @@ export async function move(source: string, target: string): Promise { * `false` to not preserve them and `true` otherwise. */ export async function copy(source: string, target: string, options: { preserveSymlinks: boolean }): Promise { - return doCopy(source, target, options, new Set()); + return doCopy(source, target, { root: { source, target }, options, handledSourcePaths: new Set() }); } // When copying a file or folder, we want to preserve the mode @@ -540,14 +546,14 @@ export async function copy(source: string, target: string, options: { preserveSy // (https://github.com/nodejs/node-v0.x-archive/issues/3045#issuecomment-4862588) const COPY_MODE_MASK = 0o777; -async function doCopy(source: string, target: string, options: { preserveSymlinks: boolean }, handledSourcePaths: Set): Promise { +async function doCopy(source: string, target: string, payload: ICopyPayload): Promise { // Keep track of paths already copied to prevent // cycles from symbolic links to cause issues - if (handledSourcePaths.has(source)) { + if (payload.handledSourcePaths.has(source)) { return; } else { - handledSourcePaths.add(source); + payload.handledSourcePaths.add(source); } const { stat, symbolicLink } = await SymlinkSupport.stat(source); @@ -559,9 +565,9 @@ async function doCopy(source: string, target: string, options: { preserveSymlink } // Try to re-create the symlink unless `preserveSymlinks: false` - if (options.preserveSymlinks) { + if (payload.options.preserveSymlinks) { try { - return await doCopySymlink(source, target); + return await doCopySymlink(source, target, payload); } catch (error) { // in any case of an error fallback to normal copy via dereferencing console.warn('[node.js fs] copy of symlink failed: ', error); @@ -571,7 +577,7 @@ async function doCopy(source: string, target: string, options: { preserveSymlink // Folder if (stat.isDirectory()) { - return doCopyDirectory(source, target, stat.mode & COPY_MODE_MASK, options, handledSourcePaths); + return doCopyDirectory(source, target, stat.mode & COPY_MODE_MASK, payload); } // File or file-like @@ -580,7 +586,7 @@ async function doCopy(source: string, target: string, options: { preserveSymlink } } -async function doCopyDirectory(source: string, target: string, mode: number, options: { preserveSymlinks: boolean }, handledSourcePaths: Set,): Promise { +async function doCopyDirectory(source: string, target: string, mode: number, payload: ICopyPayload): Promise { // Create folder await fs.promises.mkdir(target, { recursive: true, mode }); @@ -588,7 +594,7 @@ async function doCopyDirectory(source: string, target: string, mode: number, opt // Copy each file recursively const files = await readdir(source); for (const file of files) { - await doCopy(join(source, file), join(target, file), options, handledSourcePaths); + await doCopy(join(source, file), join(target, file), payload); } } @@ -601,10 +607,18 @@ async function doCopyFile(source: string, target: string, mode: number): Promise await fs.promises.chmod(target, mode); } -async function doCopySymlink(source: string, target: string): Promise { +async function doCopySymlink(source: string, target: string, payload: ICopyPayload): Promise { // Figure out link target - const linkTarget = await fs.promises.readlink(source); + let linkTarget = await fs.promises.readlink(source); + + // Special case: the symlink points to a target that is + // actually within the path that is being copied. In that + // case we want the symlink to point to the target and + // not the source + if (isEqualOrParent(linkTarget, payload.root.source, !isLinux)) { + linkTarget = join(payload.root.target, linkTarget.substr(payload.root.source.length + 1)); + } // Create symlink await fs.promises.symlink(linkTarget, target); From f45816452265c60b7a65f7392686feb7acc96cac Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 4 Feb 2021 09:57:56 +0100 Subject: [PATCH 16/31] fs - add test for symbolic link handling when link points to source --- src/vs/base/node/languagePacks.js | 1 + src/vs/base/node/pfs.ts | 2 - src/vs/base/test/node/pfs/pfs.test.ts | 43 +++++++++++++++++++ .../platform/storage/node/storageService.ts | 2 +- 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/vs/base/node/languagePacks.js b/src/vs/base/node/languagePacks.js index 24586971b2d..f47191157c1 100644 --- a/src/vs/base/node/languagePacks.js +++ b/src/vs/base/node/languagePacks.js @@ -243,6 +243,7 @@ function factory(nodeRequire, path, fs, perf) { } +// @ts-ignore if (typeof define === 'function') { // amd // @ts-ignore diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index 6201d050044..3b284df15df 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -165,8 +165,6 @@ export async function readDirsInDir(dirPath: string): Promise { * is deleted from disk. */ export function whenDeleted(path: string, intervalMs = 1000): Promise { - - // Complete when wait marker file is deleted return new Promise(resolve => { let running = false; const interval = setInterval(() => { diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index 0a6abbf8430..7925e7a823c 100644 --- a/src/vs/base/test/node/pfs/pfs.test.ts +++ b/src/vs/base/test/node/pfs/pfs.test.ts @@ -237,6 +237,49 @@ flakySuite('PFS', function () { assert.ok(!fs.existsSync(copyTarget)); }); + test('copy handles symbolic links when the reference is inside source', async () => { + + // Source Folder + const sourceFolder = join(testDir, generateUuid(), 'copy-test'); // copy-test + const sourceLinkTestFolder = join(sourceFolder, 'link-test'); // copy-test/link-test + const sourceLinkMD5JSFolder = join(sourceLinkTestFolder, 'md5'); // copy-test/link-test/md5 + const sourceLinkMD5JSFile = join(sourceLinkMD5JSFolder, 'md5.js'); // copy-test/link-test/md5/md5.js + await fs.promises.mkdir(sourceLinkMD5JSFolder, { recursive: true }); + await writeFile(sourceLinkMD5JSFile, 'Hello from MD5'); + + const sourceLinkMD5JSFolderLinked = join(sourceLinkTestFolder, 'md5-linked'); // copy-test/link-test/md5-linked + await fs.promises.symlink(sourceLinkMD5JSFolder, sourceLinkMD5JSFolderLinked); + + // Target Folder + const targetLinkTestFolder = join(sourceFolder, 'link-test copy'); // copy-test/link-test copy + const targetLinkMD5JSFolder = join(targetLinkTestFolder, 'md5'); // copy-test/link-test copy/md5 + const targetLinkMD5JSFile = join(targetLinkMD5JSFolder, 'md5.js'); // copy-test/link-test copy/md5/md5.js + const targetLinkMD5JSFolderLinked = join(targetLinkTestFolder, 'md5-linked'); // copy-test/link-test copy/md5-linked + + // Copy with `preserveSymlinks: true` and verify result + await copy(sourceLinkTestFolder, targetLinkTestFolder, { preserveSymlinks: true }); + + assert.ok(fs.existsSync(targetLinkTestFolder)); + assert.ok(fs.existsSync(targetLinkMD5JSFolder)); + assert.ok(fs.existsSync(targetLinkMD5JSFile)); + assert.ok(fs.existsSync(targetLinkMD5JSFolderLinked)); + assert.ok(fs.lstatSync(targetLinkMD5JSFolderLinked).isSymbolicLink()); + + const linkTarget = await fs.promises.readlink(targetLinkMD5JSFolderLinked); + assert.strictEqual(linkTarget, targetLinkMD5JSFolder); + + await fs.promises.rmdir(targetLinkTestFolder, { recursive: true }); + + // Copy with `preserveSymlinks: false` and verify result + await copy(sourceLinkTestFolder, targetLinkTestFolder, { preserveSymlinks: false }); + + assert.ok(fs.existsSync(targetLinkTestFolder)); + assert.ok(fs.existsSync(targetLinkMD5JSFolder)); + assert.ok(fs.existsSync(targetLinkMD5JSFile)); + assert.ok(fs.existsSync(targetLinkMD5JSFolderLinked)); + assert.ok(fs.lstatSync(targetLinkMD5JSFolderLinked).isDirectory()); + }); + test('readDirsInDir', async () => { fs.mkdirSync(join(testDir, 'somefolder1')); fs.mkdirSync(join(testDir, 'somefolder2')); diff --git a/src/vs/platform/storage/node/storageService.ts b/src/vs/platform/storage/node/storageService.ts index 4d7cdac0f9b..111bbf4874c 100644 --- a/src/vs/platform/storage/node/storageService.ts +++ b/src/vs/platform/storage/node/storageService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { promises } from 'fs'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { StorageScope, WillSaveStateReason, logStorage, IS_NEW_KEY, AbstractStorageService } from 'vs/platform/storage/common/storage'; @@ -11,7 +12,6 @@ import { Storage, IStorageDatabase, IStorage, StorageHint } from 'vs/base/parts/ import { mark } from 'vs/base/common/performance'; import { join } from 'vs/base/common/path'; import { copy, exists, writeFile } from 'vs/base/node/pfs'; -import { promises } from 'fs'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; import { assertIsDefined } from 'vs/base/common/types'; From d62c307366e320ec31c7e0309ce12abd3f332f26 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 4 Feb 2021 10:54:27 +0100 Subject: [PATCH 17/31] fs - fix tests on windows --- src/vs/base/test/node/pfs/pfs.test.ts | 58 ++++++++++++++++----------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index 7925e7a823c..62bb9c7e3ee 100644 --- a/src/vs/base/test/node/pfs/pfs.test.ts +++ b/src/vs/base/test/node/pfs/pfs.test.ts @@ -14,6 +14,7 @@ import { getPathFromAmdModule } from 'vs/base/common/amd'; import { canNormalize } from 'vs/base/common/normalization'; import { VSBuffer } from 'vs/base/common/buffer'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { isWindows } from 'vs/base/common/platform'; flakySuite('PFS', function () { @@ -205,27 +206,31 @@ flakySuite('PFS', function () { fs.symlinkSync(symbolicLinkTarget, symLink, 'junction'); // Copy preserves symlinks if configured as such + // + // Windows: this test does not work because creating symlinks + // requires priviledged permissions (admin). + if (!isWindows) { + await copy(symLink, copyTarget, { preserveSymlinks: true }); - await copy(symLink, copyTarget, { preserveSymlinks: true }); + assert.ok(fs.existsSync(copyTarget)); - assert.ok(fs.existsSync(copyTarget)); + const { symbolicLink } = await SymlinkSupport.stat(copyTarget); + assert.ok(symbolicLink); + assert.ok(!symbolicLink.dangling); - const { symbolicLink } = await SymlinkSupport.stat(copyTarget); - assert.ok(symbolicLink); - assert.ok(!symbolicLink.dangling); + const target = await fs.promises.readlink(copyTarget); + assert.strictEqual(target, symbolicLinkTarget); - const target = await fs.promises.readlink(copyTarget); - assert.strictEqual(target, symbolicLinkTarget); + // Copy does not preserve symlinks if configured as such - // Copy does not preserve symlinks if configured as such + await rimraf(copyTarget); + await copy(symLink, copyTarget, { preserveSymlinks: false }); - await rimraf(copyTarget); - await copy(symLink, copyTarget, { preserveSymlinks: false }); + assert.ok(fs.existsSync(copyTarget)); - assert.ok(fs.existsSync(copyTarget)); - - const { symbolicLink: symbolicLink2 } = await SymlinkSupport.stat(copyTarget); - assert.ok(!symbolicLink2); + const { symbolicLink: symbolicLink2 } = await SymlinkSupport.stat(copyTarget); + assert.ok(!symbolicLink2); + } // Copy ignores dangling symlinks @@ -248,7 +253,7 @@ flakySuite('PFS', function () { await writeFile(sourceLinkMD5JSFile, 'Hello from MD5'); const sourceLinkMD5JSFolderLinked = join(sourceLinkTestFolder, 'md5-linked'); // copy-test/link-test/md5-linked - await fs.promises.symlink(sourceLinkMD5JSFolder, sourceLinkMD5JSFolderLinked); + fs.symlinkSync(sourceLinkMD5JSFolder, sourceLinkMD5JSFolderLinked, 'junction'); // Target Folder const targetLinkTestFolder = join(sourceFolder, 'link-test copy'); // copy-test/link-test copy @@ -257,18 +262,23 @@ flakySuite('PFS', function () { const targetLinkMD5JSFolderLinked = join(targetLinkTestFolder, 'md5-linked'); // copy-test/link-test copy/md5-linked // Copy with `preserveSymlinks: true` and verify result - await copy(sourceLinkTestFolder, targetLinkTestFolder, { preserveSymlinks: true }); + // + // Windows: this test does not work because creating symlinks + // requires priviledged permissions (admin). + if (!isWindows) { + await copy(sourceLinkTestFolder, targetLinkTestFolder, { preserveSymlinks: true }); - assert.ok(fs.existsSync(targetLinkTestFolder)); - assert.ok(fs.existsSync(targetLinkMD5JSFolder)); - assert.ok(fs.existsSync(targetLinkMD5JSFile)); - assert.ok(fs.existsSync(targetLinkMD5JSFolderLinked)); - assert.ok(fs.lstatSync(targetLinkMD5JSFolderLinked).isSymbolicLink()); + assert.ok(fs.existsSync(targetLinkTestFolder)); + assert.ok(fs.existsSync(targetLinkMD5JSFolder)); + assert.ok(fs.existsSync(targetLinkMD5JSFile)); + assert.ok(fs.existsSync(targetLinkMD5JSFolderLinked)); + assert.ok(fs.lstatSync(targetLinkMD5JSFolderLinked).isSymbolicLink()); - const linkTarget = await fs.promises.readlink(targetLinkMD5JSFolderLinked); - assert.strictEqual(linkTarget, targetLinkMD5JSFolder); + const linkTarget = await fs.promises.readlink(targetLinkMD5JSFolderLinked); + assert.strictEqual(linkTarget, targetLinkMD5JSFolder); - await fs.promises.rmdir(targetLinkTestFolder, { recursive: true }); + await fs.promises.rmdir(targetLinkTestFolder, { recursive: true }); + } // Copy with `preserveSymlinks: false` and verify result await copy(sourceLinkTestFolder, targetLinkTestFolder, { preserveSymlinks: false }); From 265d5da0b346f9bfbbc2206f003e65ce8afa1c8c Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 4 Feb 2021 11:20:41 +0100 Subject: [PATCH 18/31] remove unused buildunit-files, fyi @dbaeumer --- src/vs/base/common/buildunit.json | 16 ---------------- src/vs/buildunit.json | 13 ------------- 2 files changed, 29 deletions(-) delete mode 100644 src/vs/base/common/buildunit.json delete mode 100644 src/vs/buildunit.json diff --git a/src/vs/base/common/buildunit.json b/src/vs/base/common/buildunit.json deleted file mode 100644 index 50e3d750676..00000000000 --- a/src/vs/base/common/buildunit.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "vs/base", - "dependencies": [ - { - "name": "vs", - "internal": false - } - ], - "libs": [ - "lib.core.d.ts" - ], - "sources": [ - "**/*.ts" - ], - "declares": [] -} diff --git a/src/vs/buildunit.json b/src/vs/buildunit.json deleted file mode 100644 index 88fe75c5552..00000000000 --- a/src/vs/buildunit.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "vs", - "dependencies": [ - ], - "libs": [ - "lib.core.d.ts" - ], - "sources": [ - ], - "declares": [ - "vs/nls.d.ts" - ] -} \ No newline at end of file From 4bdba123d24be75d87e371288a770c216be1005e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 4 Feb 2021 11:25:50 +0100 Subject: [PATCH 19/31] use PersistKeySet --- build/azure-pipelines/win32/import-esrp-auth-cert.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines/win32/import-esrp-auth-cert.ps1 b/build/azure-pipelines/win32/import-esrp-auth-cert.ps1 index f11f878c83f..e9a1d5a8ae8 100644 --- a/build/azure-pipelines/win32/import-esrp-auth-cert.ps1 +++ b/build/azure-pipelines/win32/import-esrp-auth-cert.ps1 @@ -3,7 +3,7 @@ $ErrorActionPreference = "Stop" $CertBytes = [System.Convert]::FromBase64String($CertBase64) $CertCollection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection -$CertCollection.Import($CertBytes, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable) +$CertCollection.Import($CertBytes, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable -bxor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet) $CertStore = New-Object System.Security.Cryptography.X509Certificates.X509Store("My","LocalMachine") $CertStore.Open("ReadWrite") From 30ba42cf46253db932b907c4a958aabbd6ee7e7e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 4 Feb 2021 11:29:58 +0100 Subject: [PATCH 20/31] fs - fix leftover todo --- src/vs/base/node/pfs.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index 3b284df15df..99258377daa 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -637,10 +637,3 @@ export async function exists(path: string): Promise { } //#endregion - -// TODO@bpasero remove me -export function readFile(path: string): Promise; -export function readFile(path: string, encoding: string): Promise; -export function readFile(path: string, encoding?: string): Promise { - return fs.promises.readFile(path, encoding); -} From 6250b9fde91236d2d75027e288ffa01a1bccbf0d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 4 Feb 2021 11:37:53 +0100 Subject: [PATCH 21/31] :up: distro --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 958ca329ab1..0a82a4a914c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.54.0", - "distro": "3b4f6074b6501e7e9f09a53dcaffce592d264b77", + "distro": "a1836302c2aaab6ded8ad4a0871f74dbc42359f5", "author": { "name": "Microsoft Corporation" }, From 66a1a9bee371cd71b93962eb5bedbd54ee544aa6 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 4 Feb 2021 11:48:30 +0100 Subject: [PATCH 22/31] fs - :lipstick: symbolic links handling --- src/vs/base/node/pfs.ts | 6 +++--- src/vs/platform/files/node/watcher/nodejs/watcherService.ts | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index 99258377daa..d854c68b523 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -242,16 +242,16 @@ export namespace SymlinkSupport { // Windows: workaround a node.js bug where reparse points // are not supported (https://github.com/nodejs/node/issues/36790) - if (isWindows && error.code === 'EACCES' && lstats) { + if (isWindows && error.code === 'EACCES') { try { const stats = await fs.promises.stat(await fs.promises.readlink(path)); - return { stat: stats, symbolicLink: lstats.isSymbolicLink() ? { dangling: false } : undefined }; + return { stat: stats, symbolicLink: { dangling: false } }; } catch (error) { // If the link points to a non-existing file we still want // to return it as result while setting dangling: true flag - if (error.code === 'ENOENT') { + if (error.code === 'ENOENT' && lstats) { return { stat: lstats, symbolicLink: { dangling: true } }; } diff --git a/src/vs/platform/files/node/watcher/nodejs/watcherService.ts b/src/vs/platform/files/node/watcher/nodejs/watcherService.ts index 2a68be01737..d983a1501ca 100644 --- a/src/vs/platform/files/node/watcher/nodejs/watcherService.ts +++ b/src/vs/platform/files/node/watcher/nodejs/watcherService.ts @@ -47,6 +47,10 @@ export class FileWatcher extends Disposable { pathToWatch = await realpath(pathToWatch); } catch (error) { this.onError(error); + + if (symbolicLink.dangling) { + return; // give up if symbolic link is dangling + } } } From bd20a720fba05f87ddb53c11c6af66936fd88a55 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 4 Feb 2021 12:00:18 +0100 Subject: [PATCH 23/31] Rename "Existing Tunnels" --- src/vs/workbench/contrib/remote/browser/tunnelView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 05cb916dfbf..c3b06ba9784 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -106,7 +106,7 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { } if (this.model.detected.size > 0) { groups.push({ - label: nls.localize('remote.tunnelsView.detected', "Existing Tunnels"), + label: nls.localize('remote.tunnelsView.detected', "Static Ports"), tunnelType: TunnelType.Detected, items: this.detected }); From 914cef30d507e88847535b77f7d048195b9220a5 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 4 Feb 2021 13:33:54 +0100 Subject: [PATCH 24/31] integration tests - disable minimap to reduce error output (#115747) --- .../emmet/src/test/test-fixtures/.vscode/settings.json | 3 +++ .../vscode-api-tests/testWorkspace/.vscode/settings.json | 5 +++-- .../vscode-api-tests/testWorkspace2/.vscode/settings.json | 3 +++ 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 extensions/emmet/src/test/test-fixtures/.vscode/settings.json create mode 100644 extensions/vscode-api-tests/testWorkspace2/.vscode/settings.json diff --git a/extensions/emmet/src/test/test-fixtures/.vscode/settings.json b/extensions/emmet/src/test/test-fixtures/.vscode/settings.json new file mode 100644 index 00000000000..b196f4685ef --- /dev/null +++ b/extensions/emmet/src/test/test-fixtures/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.minimap.enabled": false // see https://github.com/microsoft/vscode/issues/115747 +} diff --git a/extensions/vscode-api-tests/testWorkspace/.vscode/settings.json b/extensions/vscode-api-tests/testWorkspace/.vscode/settings.json index e9f6fb82156..31b22c56533 100644 --- a/extensions/vscode-api-tests/testWorkspace/.vscode/settings.json +++ b/extensions/vscode-api-tests/testWorkspace/.vscode/settings.json @@ -4,5 +4,6 @@ }, "files.exclude": { "**/files-exclude/**": true - } -} \ No newline at end of file + }, + "editor.minimap.enabled": false // see https://github.com/microsoft/vscode/issues/115747 +} diff --git a/extensions/vscode-api-tests/testWorkspace2/.vscode/settings.json b/extensions/vscode-api-tests/testWorkspace2/.vscode/settings.json new file mode 100644 index 00000000000..b196f4685ef --- /dev/null +++ b/extensions/vscode-api-tests/testWorkspace2/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.minimap.enabled": false // see https://github.com/microsoft/vscode/issues/115747 +} From 72e8e9f5bef8c4a8fdb110eb4a3635ad3711d61e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 4 Feb 2021 13:34:08 +0100 Subject: [PATCH 25/31] fs - do not log an error when watching results in ENOENT --- src/vs/platform/files/node/watcher/nodejs/watcherService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/files/node/watcher/nodejs/watcherService.ts b/src/vs/platform/files/node/watcher/nodejs/watcherService.ts index d983a1501ca..b645715d160 100644 --- a/src/vs/platform/files/node/watcher/nodejs/watcherService.ts +++ b/src/vs/platform/files/node/watcher/nodejs/watcherService.ts @@ -74,7 +74,9 @@ export class FileWatcher extends Disposable { }, error => this.onError(error))); } } catch (error) { - this.onError(error); + if (error.code !== 'ENOENT') { + this.onError(error); + } } } From 2c00f1afc97b336c0ff8eb03deef914a6dae4618 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 4 Feb 2021 13:41:34 +0100 Subject: [PATCH 26/31] Add validation message for portsAttributes Fixes microsoft/vscode-remote-release#4365 --- src/vs/workbench/contrib/remote/common/remote.contribution.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index 606ec342c8f..4d0c88698d6 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -172,7 +172,9 @@ Registry.as(ConfigurationExtensions.Configuration) } }, markdownDescription: localize('remote.portsAttributes', "Set default properties that are applied when a specific port number is forwarded. For example:\n\n```\n\"3000\": {\n \"label\": \"Labeled Port\"\n},\n\"40000-55000\": {\n \"onAutoForward\": \"ignore\"\n}\n```"), - defaultSnippets: [{ body: { '${1:3000}': { label: '${2:My Port}', onAutoForward: 'notify' } } }] + defaultSnippets: [{ body: { '${1:3000}': { label: '${2:My Port}', onAutoForward: 'notify' } } }], + errorMessage: localize('remote.portsAttributes.patternError', "Must be a port number or a range of port numbers"), + additionalProperties: false } } }); From 0494790a5ee41c0ff859bb132dac6d484716e121 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 4 Feb 2021 13:55:46 +0100 Subject: [PATCH 27/31] fix https://github.com/microsoft/vscode/issues/115746 --- extensions/vscode-api-tests/package.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 4daf425a7b9..162b2dce4e3 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -37,6 +37,14 @@ } } }, + "views": { + "remote": [ + { + "id": "test.treeId", + "when": "never" + } + ] + }, "configurationDefaults": { "[abcLang]": { "editor.lineNumbers": "off", From 70d9246943b1108431aa8c1bd27c3cebb95cbe70 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 4 Feb 2021 14:06:45 +0100 Subject: [PATCH 28/31] fs - use our readdir method everywhere --- .../diagnostics/node/diagnosticsService.ts | 108 +++++++++--------- 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/src/vs/platform/diagnostics/node/diagnosticsService.ts b/src/vs/platform/diagnostics/node/diagnosticsService.ts index 2f27114408f..c975790fa99 100644 --- a/src/vs/platform/diagnostics/node/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/node/diagnosticsService.ts @@ -5,7 +5,7 @@ import * as osLib from 'os'; import { virtualMachineHint } from 'vs/base/node/id'; import { IMachineInfo, WorkspaceStats, WorkspaceStatItem, PerformanceInfo, SystemInfo, IRemoteDiagnosticInfo, IRemoteDiagnosticError, isRemoteDiagnosticError, IWorkspaceInformation } from 'vs/platform/diagnostics/common/diagnostics'; -import { readdir, exists, readFile } from 'fs'; +import { Dirent, exists, readFile } from 'fs'; import { join, basename } from 'vs/base/common/path'; import { parse, ParseError, getNodeType } from 'vs/base/common/json'; import { listProcesses } from 'vs/base/node/ps'; @@ -19,6 +19,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Iterable } from 'vs/base/common/iterator'; import { Schemas } from 'vs/base/common/network'; import { ByteSize } from 'vs/platform/files/common/files'; +import { readdir } from 'vs/base/node/pfs'; export const ID = 'diagnosticsService'; export const IDiagnosticsService = createDecorator(ID); @@ -79,68 +80,69 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P function collect(root: string, dir: string, filter: string[], token: { count: number, maxReached: boolean }): Promise { const relativePath = dir.substring(root.length + 1); - return new Promise(resolve => { - readdir(dir, { withFileTypes: true }, async (err, files) => { + return new Promise(async resolve => { + let files: Dirent[]; + try { + files = await readdir(dir, { withFileTypes: true }); + } catch (error) { // Ignore folders that can't be read - if (err) { - resolve(); - return; - } - - if (token.count >= MAX_FILES) { - token.count += files.length; - token.maxReached = true; - resolve(); - return; - } - - let pending = files.length; - if (pending === 0) { - resolve(); - return; - } - - let filesToRead = files; - if (token.count + files.length > MAX_FILES) { - token.maxReached = true; - pending = MAX_FILES - token.count; - filesToRead = files.slice(0, pending); - } + resolve(); + return; + } + if (token.count >= MAX_FILES) { token.count += files.length; + token.maxReached = true; + resolve(); + return; + } - for (const file of filesToRead) { - if (file.isDirectory()) { - if (!filter.includes(file.name)) { - await collect(root, join(dir, file.name), filter, token); - } + let pending = files.length; + if (pending === 0) { + resolve(); + return; + } - if (--pending === 0) { - resolve(); - return; - } - } else { - const index = file.name.lastIndexOf('.'); - if (index >= 0) { - const fileType = file.name.substring(index + 1); - if (fileType) { - fileTypes.set(fileType, (fileTypes.get(fileType) ?? 0) + 1); - } - } + let filesToRead = files; + if (token.count + files.length > MAX_FILES) { + token.maxReached = true; + pending = MAX_FILES - token.count; + filesToRead = files.slice(0, pending); + } - for (const configFile of configFilePatterns) { - if (configFile.relativePathPattern?.test(relativePath) !== false && configFile.filePattern.test(file.name)) { - configFiles.set(configFile.tag, (configFiles.get(configFile.tag) ?? 0) + 1); - } - } + token.count += files.length; - if (--pending === 0) { - resolve(); - return; + for (const file of filesToRead) { + if (file.isDirectory()) { + if (!filter.includes(file.name)) { + await collect(root, join(dir, file.name), filter, token); + } + + if (--pending === 0) { + resolve(); + return; + } + } else { + const index = file.name.lastIndexOf('.'); + if (index >= 0) { + const fileType = file.name.substring(index + 1); + if (fileType) { + fileTypes.set(fileType, (fileTypes.get(fileType) ?? 0) + 1); } } + + for (const configFile of configFilePatterns) { + if (configFile.relativePathPattern?.test(relativePath) !== false && configFile.filePattern.test(file.name)) { + configFiles.set(configFile.tag, (configFiles.get(configFile.tag) ?? 0) + 1); + } + } + + if (--pending === 0) { + resolve(); + return; + } } - }); + } }); } From 2b6bd236136327334c5fe7438d9b9de3765c2c85 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 4 Feb 2021 14:10:00 +0100 Subject: [PATCH 29/31] Fix #115690 --- .../contrib/extensions/browser/extensions.contribution.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index bd2454e646d..5ef55d54b7a 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -677,8 +677,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi notificationService.prompt( Severity.Info, message, - actions, - { sticky: true } + actions ); } }); From be820b8f425aa07a3514933e766666b022c6df35 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 4 Feb 2021 14:23:17 +0100 Subject: [PATCH 30/31] Fix custom iconLabel hover flickering on description Fixes #114230 --- src/vs/base/browser/ui/iconLabel/iconLabel.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index 137f5d179a3..8e20443ade7 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -231,11 +231,13 @@ export class IconLabel extends Disposable { } tokenSource = new CancellationTokenSource(); function mouseLeaveOrDown(this: HTMLElement, e: MouseEvent): any { - isHovering = false; - hoverOptions = undefined; - tokenSource.dispose(true); - mouseLeaveDisposable.dispose(); - mouseDownDisposable.dispose(); + if ((e).fromElement === htmlElement) { + isHovering = false; + hoverOptions = undefined; + tokenSource.dispose(true); + mouseLeaveDisposable.dispose(); + mouseDownDisposable.dispose(); + } } const mouseLeaveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_LEAVE, true)(mouseLeaveOrDown.bind(htmlElement)); const mouseDownDisposable = domEvent(htmlElement, dom.EventType.MOUSE_DOWN, true)(mouseLeaveOrDown.bind(htmlElement)); From cb537dd4e82fb076f7095b969065e57b44acf3fc Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 4 Feb 2021 14:27:28 +0100 Subject: [PATCH 31/31] fs - provide a fallback when readdir with filetypes fails (#115645) --- src/vs/base/node/pfs.ts | 52 ++++++++++++++++--- .../diagnostics/node/diagnosticsService.ts | 6 +-- .../files/node/diskFileSystemProvider.ts | 6 +-- 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index d854c68b523..2733fb9432b 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -100,15 +100,55 @@ export function rimrafSync(path: string): void { //#region readdir with NFC support (macos) +export interface IDirent { + name: string; + + isFile(): boolean; + isDirectory(): boolean; + isSymbolicLink(): boolean; +} + /** * 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))); +export async function readdir(path: string, options: { withFileTypes: true }): Promise; +export async function readdir(path: string, options?: { withFileTypes: true }): Promise<(string | IDirent)[]> { + return handleDirectoryChildren(await (options ? safeReaddirWithFileTypes(path) : fs.promises.readdir(path))); +} + +async function safeReaddirWithFileTypes(path: string): Promise { + try { + return await fs.promises.readdir(path, { withFileTypes: true }); + } catch (error) { + console.warn('[node.js fs] readdir with filetypes failed with error: ', error); + } + + // Fallback to manually reading and resolving each + // children of the folder in case we hit an error + // previously. + // This can only really happen on exotic file systems + // such as explained in #115645 where we get entries + // from `readdir` that we can later not `lstat`. + const result: IDirent[] = []; + const children = await readdir(path); + for (const child of children) { + try { + const lstat = await fs.promises.lstat(join(path, child)); + result.push({ + name: child, + isFile: () => lstat.isFile(), + isDirectory: () => lstat.isDirectory(), + isSymbolicLink: () => lstat.isSymbolicLink() + }); + } catch (error) { + console.warn('[node.js fs] unexpected error from lstat after readdir: ', error); + } + } + + return result; } /** @@ -121,9 +161,9 @@ export function readdirSync(path: string): string[] { } 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)[] { +function handleDirectoryChildren(children: IDirent[]): IDirent[]; +function handleDirectoryChildren(children: (string | IDirent)[]): (string | IDirent)[]; +function handleDirectoryChildren(children: (string | IDirent)[]): (string | IDirent)[] { return children.map(child => { // Mac: uses NFD unicode form on disk, but we want NFC diff --git a/src/vs/platform/diagnostics/node/diagnosticsService.ts b/src/vs/platform/diagnostics/node/diagnosticsService.ts index c975790fa99..cc61cc0c478 100644 --- a/src/vs/platform/diagnostics/node/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/node/diagnosticsService.ts @@ -5,7 +5,7 @@ import * as osLib from 'os'; import { virtualMachineHint } from 'vs/base/node/id'; import { IMachineInfo, WorkspaceStats, WorkspaceStatItem, PerformanceInfo, SystemInfo, IRemoteDiagnosticInfo, IRemoteDiagnosticError, isRemoteDiagnosticError, IWorkspaceInformation } from 'vs/platform/diagnostics/common/diagnostics'; -import { Dirent, exists, readFile } from 'fs'; +import { exists, readFile } from 'fs'; import { join, basename } from 'vs/base/common/path'; import { parse, ParseError, getNodeType } from 'vs/base/common/json'; import { listProcesses } from 'vs/base/node/ps'; @@ -19,7 +19,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Iterable } from 'vs/base/common/iterator'; import { Schemas } from 'vs/base/common/network'; import { ByteSize } from 'vs/platform/files/common/files'; -import { readdir } from 'vs/base/node/pfs'; +import { IDirent, readdir } from 'vs/base/node/pfs'; export const ID = 'diagnosticsService'; export const IDiagnosticsService = createDecorator(ID); @@ -81,7 +81,7 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P const relativePath = dir.substring(root.length + 1); return new Promise(async resolve => { - let files: Dirent[]; + let files: IDirent[]; try { files = await readdir(dir, { withFileTypes: true }); } catch (error) { diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index 4017e98c06c..a7216230eb6 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { open, close, read, write, fdatasync, Dirent, Stats, promises } from 'fs'; +import { open, close, read, write, fdatasync, Stats, promises } from 'fs'; import { promisify } from 'util'; import { IDisposable, Disposable, toDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle'; import { FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, IFileSystemProviderWithFileFolderCopyCapability } from 'vs/platform/files/common/files'; 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, readdir } from 'vs/base/node/pfs'; +import { SymlinkSupport, move, copy, rimraf, RimRafMode, exists, readdir, IDirent } 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'; @@ -119,7 +119,7 @@ export class DiskFileSystemProvider extends Disposable implements } } - private toType(entry: Stats | Dirent, symbolicLink?: { dangling: boolean }): FileType { + private toType(entry: Stats | IDirent, symbolicLink?: { dangling: boolean }): FileType { // Signal file type by checking for file / directory, except: // - symbolic links pointing to non-existing files are FileType.Unknown