diff --git a/.gitignore b/.gitignore index a51acf522c2f..cefa2212f557 100644 --- a/.gitignore +++ b/.gitignore @@ -39,7 +39,3 @@ tmp/** test-results.xml uitests/out/** !build/ - -# TODO (GH-8925) This is temporary. We will remove it when we adjust -# tests to not leave the file behind. -src/test/get-pip.py diff --git a/news/2 Fixes/8890.md b/news/2 Fixes/8890.md deleted file mode 100644 index 276c04796cff..000000000000 --- a/news/2 Fixes/8890.md +++ /dev/null @@ -1 +0,0 @@ -Fixes to FileSystem code to use bitwise operations. diff --git a/news/3 Code Health/6911.md b/news/3 Code Health/6911.md deleted file mode 100644 index f7a40ecad97d..000000000000 --- a/news/3 Code Health/6911.md +++ /dev/null @@ -1 +0,0 @@ -Use the new VS Code filesystem API as much as possible. diff --git a/src/client/common/editor.ts b/src/client/common/editor.ts index 15927c59194c..f777de96e246 100644 --- a/src/client/common/editor.ts +++ b/src/client/common/editor.ts @@ -1,10 +1,10 @@ import { Diff, diff_match_patch } from 'diff-match-patch'; +import * as fs from 'fs-extra'; import { injectable } from 'inversify'; import * as md5 from 'md5'; import { EOL } from 'os'; import * as path from 'path'; import { Position, Range, TextDocument, TextEdit, Uri, WorkspaceEdit } from 'vscode'; -import { IFileSystem } from './platform/types'; import { IEditorUtils } from './types'; // Code borrowed from goFormat.ts (Go Extension for VS Code) @@ -80,11 +80,7 @@ export function getTextEditsFromPatch(before: string, patch: string): TextEdit[] return textEdits; } -export function getWorkspaceEditsFromPatch( - filePatches: string[], - fs: IFileSystem, - workspaceRoot?: string -): WorkspaceEdit { +export function getWorkspaceEditsFromPatch(filePatches: string[], workspaceRoot?: string): WorkspaceEdit { const workspaceEdit = new WorkspaceEdit(); filePatches.forEach(patch => { const indexOfAtAt = patch.indexOf('@@'); @@ -111,7 +107,7 @@ export function getWorkspaceEditsFromPatch( let fileName = fileNameLines[0].substring(fileNameLines[0].indexOf(' a') + 3).trim(); fileName = workspaceRoot && !path.isAbsolute(fileName) ? path.resolve(workspaceRoot, fileName) : fileName; - if (!fs.fileExistsSync(fileName)) { + if (!fs.existsSync(fileName)) { return; } @@ -127,7 +123,7 @@ export function getWorkspaceEditsFromPatch( throw new Error('Unable to parse Patch string'); } - const fileSource = fs.readFileSync(fileName); + const fileSource = fs.readFileSync(fileName).toString('utf8'); const fileUri = Uri.file(fileName); // Add line feeds and build the text edits @@ -230,25 +226,24 @@ function getTextEditsInternal(before: string, diffs: [number, string][], startLi return edits; } -export async function getTempFileWithDocumentContents( - document: TextDocument, - fs: IFileSystem -): Promise { - // Don't create file in temp folder since external utilities - // look into configuration files in the workspace and are not able - // to find custom rules if file is saved in a random disk location. - // This means temp file has to be created in the same folder - // as the original one and then removed. - - const ext = path.extname(document.uri.fsPath); - const filename = `${document.uri.fsPath}.${md5(document.uri.fsPath)}${ext}`; - await ( - fs.writeFile(filename, document.getText()) - .catch(err => { - throw Error(`Failed to create a temporary file, ${err.message}`); - }) - ); - return filename; +export function getTempFileWithDocumentContents(document: TextDocument): Promise { + return new Promise((resolve, reject) => { + const ext = path.extname(document.uri.fsPath); + // Don't create file in temp folder since external utilities + // look into configuration files in the workspace and are not able + // to find custom rules if file is saved in a random disk location. + // This means temp file has to be created in the same folder + // as the original one and then removed. + + // tslint:disable-next-line:no-require-imports + const fileName = `${document.uri.fsPath}.${md5(document.uri.fsPath)}${ext}`; + fs.writeFile(fileName, document.getText(), ex => { + if (ex) { + reject(`Failed to create a temporary file, ${ex.message}`); + } + resolve(fileName); + }); + }); } /** diff --git a/src/client/common/envFileParser.ts b/src/client/common/envFileParser.ts new file mode 100644 index 000000000000..f1cfda52b430 --- /dev/null +++ b/src/client/common/envFileParser.ts @@ -0,0 +1,63 @@ +import * as fs from 'fs-extra'; +import { IS_WINDOWS } from './platform/constants'; +import { PathUtils } from './platform/pathUtils'; +import { EnvironmentVariablesService } from './variables/environment'; +import { EnvironmentVariables } from './variables/types'; +function parseEnvironmentVariables(contents: string): EnvironmentVariables | undefined { + if (typeof contents !== 'string' || contents.length === 0) { + return undefined; + } + + const env: EnvironmentVariables = {}; + contents.split('\n').forEach(line => { + const match = line.match(/^\s*([\w\.\-]+)\s*=\s*(.*)?\s*$/); + if (match !== null) { + let value = typeof match[2] === 'string' ? match[2] : ''; + if (value.length > 0 && value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') { + value = value.replace(/\\n/gm, '\n'); + } + env[match[1]] = value.replace(/(^['"]|['"]$)/g, ''); + } + }); + return env; +} + +export function parseEnvFile(envFile: string, mergeWithProcessEnvVars: boolean = true): EnvironmentVariables { + const buffer = fs.readFileSync(envFile, 'utf8'); + const env = parseEnvironmentVariables(buffer)!; + return mergeWithProcessEnvVars ? mergeEnvVariables(env, process.env) : mergePythonPath(env, process.env.PYTHONPATH as string); +} + +/** + * Merge the target environment variables into the source. + * Note: The source variables are modified and returned (i.e. it modifies value passed in). + * @export + * @param {EnvironmentVariables} targetEnvVars target environment variables. + * @param {EnvironmentVariables} [sourceEnvVars=process.env] source environment variables (defaults to current process variables). + * @returns {EnvironmentVariables} + */ +export function mergeEnvVariables(targetEnvVars: EnvironmentVariables, sourceEnvVars: EnvironmentVariables = process.env): EnvironmentVariables { + const service = new EnvironmentVariablesService(new PathUtils(IS_WINDOWS)); + service.mergeVariables(sourceEnvVars, targetEnvVars); + if (sourceEnvVars.PYTHONPATH) { + service.appendPythonPath(targetEnvVars, sourceEnvVars.PYTHONPATH); + } + return targetEnvVars; +} + +/** + * Merge the target PYTHONPATH value into the env variables passed. + * Note: The env variables passed in are modified and returned (i.e. it modifies value passed in). + * @export + * @param {EnvironmentVariables} env target environment variables. + * @param {string | undefined} [currentPythonPath] PYTHONPATH value. + * @returns {EnvironmentVariables} + */ +export function mergePythonPath(env: EnvironmentVariables, currentPythonPath: string | undefined): EnvironmentVariables { + if (typeof currentPythonPath !== 'string' || currentPythonPath.length === 0) { + return env; + } + const service = new EnvironmentVariablesService(new PathUtils(IS_WINDOWS)); + service.appendPythonPath(env, currentPythonPath); + return env; +} diff --git a/src/client/common/installer/moduleInstaller.ts b/src/client/common/installer/moduleInstaller.ts index 417a004a141f..21724e53afd7 100644 --- a/src/client/common/installer/moduleInstaller.ts +++ b/src/client/common/installer/moduleInstaller.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as fs from 'fs'; import { injectable } from 'inversify'; import * as path from 'path'; import { OutputChannel, window } from 'vscode'; @@ -9,10 +10,9 @@ import { IServiceContainer } from '../../ioc/types'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { STANDARD_OUTPUT_CHANNEL } from '../constants'; -import { IFileSystem } from '../platform/types'; import { ITerminalServiceFactory } from '../terminal/types'; import { ExecutionInfo, IConfigurationService, IOutputChannel } from '../types'; -import { isResource } from '../utils/misc'; +import { isResource, noop } from '../utils/misc'; import { InterpreterUri } from './types'; @injectable() @@ -38,10 +38,7 @@ export abstract class ModuleInstaller { if (!interpreter || interpreter.type !== InterpreterType.Unknown) { await terminalService.sendCommand(pythonPath, args); } else if (settings.globalModuleInstallation) { - const dirname = path.dirname(pythonPath); - const fs = this.serviceContainer.get(IFileSystem); - const isWritable = ! await fs.isDirReadonly(dirname); - if (isWritable) { + if (await this.isPathWritableAsync(path.dirname(pythonPath))) { await terminalService.sendCommand(pythonPath, args); } else { this.elevatedInstall(pythonPath, args); @@ -71,6 +68,19 @@ export abstract class ModuleInstaller { } return args; } + private async isPathWritableAsync(directoryPath: string): Promise { + const filePath = `${directoryPath}${path.sep}___vscpTest___`; + return new Promise(resolve => { + fs.open(filePath, fs.constants.O_CREAT | fs.constants.O_RDWR, (error, fd) => { + if (!error) { + fs.close(fd, () => { + fs.unlink(filePath, noop); + }); + } + return resolve(!error); + }); + }); + } private elevatedInstall(execPath: string, args: string[]) { const options = { diff --git a/src/client/common/net/fileDownloader.ts b/src/client/common/net/fileDownloader.ts index 281538a47a5d..53162c965304 100644 --- a/src/client/common/net/fileDownloader.ts +++ b/src/client/common/net/fileDownloader.ts @@ -3,11 +3,12 @@ 'use strict'; +import { WriteStream } from 'fs'; import { inject, injectable } from 'inversify'; import * as requestTypes from 'request'; import { Progress, ProgressLocation } from 'vscode'; import { IApplicationShell } from '../application/types'; -import { IFileSystem, WriteStream } from '../platform/types'; +import { IFileSystem } from '../platform/types'; import { DownloadOptions, IFileDownloader, IHttpClient } from '../types'; import { Http } from '../utils/localize'; import { noop } from '../utils/misc'; diff --git a/src/client/common/platform/fileSystem.ts b/src/client/common/platform/fileSystem.ts index af2ea15439b8..b1413e5b422d 100644 --- a/src/client/common/platform/fileSystem.ts +++ b/src/client/common/platform/fileSystem.ts @@ -1,552 +1,207 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - 'use strict'; import { createHash } from 'crypto'; -import * as fsextra from 'fs-extra'; +import * as fileSystem from 'fs'; +import * as fs from 'fs-extra'; import * as glob from 'glob'; -import { injectable } from 'inversify'; -import * as fspath from 'path'; -import * as tmpMod from 'tmp'; -import * as util from 'util'; -import * as vscode from 'vscode'; -import { getOSType, OSType } from '../utils/platform'; -import { - FileStat, FileType, - IFileSystem, IFileSystemPaths, IFileSystemUtils, IRawFileSystem, - ITempFileSystem, - TemporaryFile, WriteStream -} from './types'; - -// tslint:disable:max-classes-per-file -// tslint:disable:no-suspicious-comment - -const ENCODING: string = 'utf8'; - -const FILE_NOT_FOUND = vscode.FileSystemError.FileNotFound().name; -const FILE_EXISTS = vscode.FileSystemError.FileExists().name; - -function isFileNotFoundError(err: Error): boolean { - return err.name === FILE_NOT_FOUND; -} - -function isFileExistsError(err: Error): boolean { - return err.name === FILE_EXISTS; -} - -function convertFileStat(stat: fsextra.Stats): FileStat { - let fileType = FileType.Unknown; - if (stat.isFile()) { - fileType = fileType | FileType.File; - } - if (stat.isDirectory()) { - fileType = fileType | FileType.Directory; - } - if (stat.isSymbolicLink()) { - fileType = fileType | FileType.SymbolicLink; - } - return { - type: fileType, - size: stat.size, - ctime: stat.ctimeMs, - mtime: stat.mtimeMs - }; -} - -// The parts of node's 'path' module used by FileSystemPath. -interface INodePath { - join(...filenames: string[]): string; - dirname(filename: string): string; - normalize(filename: string): string; -} - -// Eventually we will merge PathUtils into FileSystemPath. - -// The file path operations used by the extension. -export class FileSystemPaths implements IFileSystemPaths { - constructor( - protected readonly isCaseSensitive: boolean, - protected readonly raw: INodePath - ) { } - // Create a new object using common-case default values. - // We do not use an alternate constructor because defaults in the - // constructor runs counter to our approach. - public static withDefaults(): FileSystemPaths { - return new FileSystemPaths( - (getOSType() === OSType.Windows), - fspath - ); - } - - public join(...filenames: string[]): string { - return this.raw.join(...filenames); - } +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import * as tmp from 'tmp'; +import { FileStat } from 'vscode'; +import { createDeferred } from '../utils/async'; +import { IFileSystem, IPlatformService, TemporaryFile } from './types'; - public dirname(filename: string): string { - return this.raw.dirname(filename); - } - - public normCase(filename: string): string { - filename = this.raw.normalize(filename); - return this.isCaseSensitive ? filename.toUpperCase() : filename; - } -} - -//tslint:disable-next-line:no-any -type TempCallback = (err: any, path: string, fd: number, cleanupCallback: () => void) => void; -// The parts of the 'tmp' module used by TempFileSystem. -interface IRawTmp { - file(options: tmpMod.Options, cb: TempCallback): void; -} +@injectable() +export class FileSystem implements IFileSystem { + constructor(@inject(IPlatformService) private platformService: IPlatformService) {} -// The operations on temporary files/directoryes used by the extension. -export class TempFileSystem { - constructor( - protected readonly raw: IRawTmp - ) { } - // Create a new object using common-case default values. - public static withDefaults(): TempFileSystem { - return new TempFileSystem( - tmpMod - ); + public get directorySeparatorChar(): string { + return path.sep; } - - public async createFile(suffix?: string, dir?: string): Promise { - const options = { - postfix: suffix, - dir: dir - }; - // We could use util.promisify() here. The tmp.file() callback - // makes it a bit complicated though. - return new Promise((resolve, reject) => { - this.raw.file(options, (err, tmpFile, _fd, cleanupCallback) => { - if (err) { - return reject(err); + public async stat(filePath: string): Promise { + // Do not import vscode directly, as this isn't available in the Debugger Context. + // If stat is used in debugger context, it will fail, however theres a separate PR that will resolve this. + // tslint:disable-next-line: no-require-imports + const vscode = require('vscode'); + return vscode.workspace.fs.stat(vscode.Uri.file(filePath)); + } + + public objectExists(filePath: string, statCheck: (s: fs.Stats) => boolean): Promise { + return new Promise(resolve => { + fs.stat(filePath, (error, stats) => { + if (error) { + return resolve(false); } - resolve({ - filePath: tmpFile, - dispose: cleanupCallback - }); + return resolve(statCheck(stats)); }); }); } -} - -// This is the parts of the vscode.workspace.fs API that we use here. -// See: https://code.visualstudio.com/api/references/vscode-api#FileSystem -// Note that we have used all the API functions *except* "rename()". -interface IVSCodeFileSystemAPI { - copy(source: vscode.Uri, target: vscode.Uri, options?: {overwrite: boolean}): Thenable; - createDirectory(uri: vscode.Uri): Thenable; - delete(uri: vscode.Uri, options?: {recursive: boolean; useTrash: boolean}): Thenable; - readDirectory(uri: vscode.Uri): Thenable<[string, FileType][]>; - readFile(uri: vscode.Uri): Thenable; - stat(uri: vscode.Uri): Thenable; - writeFile(uri: vscode.Uri, content: Uint8Array): Thenable; -} - -// This is the parts of the 'fs-extra' module that we use in RawFileSystem. -interface IRawFSExtra { - chmod(filePath: string, mode: string | number): Promise; - lstat(filename: string): Promise; - - // non-async - statSync(filename: string): fsextra.Stats; - readFileSync(path: string, encoding: string): string; - createWriteStream(filePath: string): fsextra.WriteStream; -} -interface IRawPath { - dirname(filename: string): string; -} - -// Later we will drop "FileSystem", switching usage to -// "FileSystemUtils" and then rename "RawFileSystem" to "FileSystem". - -// The low-level filesystem operations used by the extension. -export class RawFileSystem implements IRawFileSystem { - constructor( - protected readonly paths: IRawPath, - protected readonly vscfs: IVSCodeFileSystemAPI, - protected readonly fsExtra: IRawFSExtra - ) { } - - // Create a new object using common-case default values. - public static withDefaults( - paths?: IRawPath, - vscfs?: IVSCodeFileSystemAPI, - fsExtra?: IRawFSExtra - ): RawFileSystem{ - return new RawFileSystem( - paths || FileSystemPaths.withDefaults(), - vscfs || vscode.workspace.fs, - fsExtra || fsextra - ); + public fileExists(filePath: string): Promise { + return this.objectExists(filePath, stats => stats.isFile()); } - - //**************************** - // VS Code API - - public async readText(filename: string): Promise { - const uri = vscode.Uri.file(filename); - const data = Buffer.from( - await this.vscfs.readFile(uri)); - return data.toString(ENCODING); + public fileExistsSync(filePath: string): boolean { + return fs.existsSync(filePath); } - - public async writeText(filename: string, text: string): Promise { - const uri = vscode.Uri.file(filename); - const data = Buffer.from(text); - await this.vscfs.writeFile(uri, data); + /** + * Reads the contents of the file using utf8 and returns the string contents. + * @param {string} filePath + * @returns {Promise} + * @memberof FileSystem + */ + public readFile(filePath: string): Promise { + return fs.readFile(filePath).then(buffer => buffer.toString()); } - public async rmtree(dirname: string): Promise { - const uri = vscode.Uri.file(dirname); - // TODO (https://github.com/microsoft/vscode/issues/84177) - // The docs say "throws - FileNotFound when uri doesn't exist". - // However, it happily does nothing (at least for remote-over-SSH). - // So we have to manually stat, just to be sure. - await this.vscfs.stat(uri); - return this.vscfs.delete(uri, { - recursive: true, - useTrash: false - }); + public async writeFile(filePath: string, data: {}, options: string | fs.WriteFileOptions = { encoding: 'utf8' }): Promise { + await fs.writeFile(filePath, data, options); } - public async rmfile(filename: string): Promise { - const uri = vscode.Uri.file(filename); - return this.vscfs.delete(uri, { - recursive: false, - useTrash: false - }); + public directoryExists(filePath: string): Promise { + return this.objectExists(filePath, stats => stats.isDirectory()); } - public async stat(filename: string): Promise { - const uri = vscode.Uri.file(filename); - return this.vscfs.stat(uri); + public createDirectory(directoryPath: string): Promise { + return fs.mkdirp(directoryPath); } - public async listdir(dirname: string): Promise<[string, FileType][]> { - const uri = vscode.Uri.file(dirname); - return this.vscfs.readDirectory(uri); + public deleteDirectory(directoryPath: string): Promise { + const deferred = createDeferred(); + fs.rmdir(directoryPath, err => (err ? deferred.reject(err) : deferred.resolve())); + return deferred.promise; } - public async mkdirp(dirname: string): Promise { - // TODO https://github.com/microsoft/vscode/issues/84175 - // Hopefully VS Code provides this method in their API - // so we don't have to roll our own. - const stack = [dirname]; - while (stack.length > 0) { - const current = stack.pop() || ''; - const uri = vscode.Uri.file(current); - try { - await this.vscfs.createDirectory(uri); - } catch (err) { - if (isFileExistsError(err)) { - // already done! - return; - } - // According to the docs, FileNotFound means the parent - // does not exist. - // See: https://code.visualstudio.com/api/references/vscode-api#FileSystemProvider - if (!isFileNotFoundError(err)) { - // Fail for anything else. - throw err; // re-throw + public getSubDirectories(rootDir: string): Promise { + return new Promise(resolve => { + fs.readdir(rootDir, async (error, files) => { + if (error) { + return resolve([]); } - // Try creating the parent first. - const parent = this.paths.dirname(current); - if (parent === '' || parent === current) { - // This shouldn't ever happen. - throw err; - } - stack.push(current); - stack.push(parent); - } - } - } - - public async copyFile(src: string, dest: string): Promise { - const srcURI = vscode.Uri.file(src); - const destURI = vscode.Uri.file(dest); - // TODO (https://github.com/microsoft/vscode/issues/84177) - // The docs say "throws - FileNotFound when parent of - // destination doesn't exist". However, it happily creates - // the parent directory (at least for remote-over-SSH). - // So we have to manually stat, just to be sure. - await this.vscfs.stat(vscode.Uri.file(this.paths.dirname(dest))); - await this.vscfs.copy(srcURI, destURI, { - overwrite: true + const subDirs = (await Promise.all( + files.map(async name => { + const fullPath = path.join(rootDir, name); + try { + if ((await fs.stat(fullPath)).isDirectory()) { + return fullPath; + } + // tslint:disable-next-line:no-empty + } catch (ex) {} + }) + )).filter(dir => dir !== undefined) as string[]; + resolve(subDirs); + }); }); } - //**************************** - // fs-extra - - public async chmod(filename: string, mode: string | number): Promise { - // TODO https://github.com/microsoft/vscode/issues/84175 - // This functionality has been requested for the VS Code API. - return this.fsExtra.chmod(filename, mode); - } - - public async lstat(filename: string): Promise { - // TODO https://github.com/microsoft/vscode/issues/84175 - // This functionality has been requested for the VS Code API. - const stat = await this.fsExtra.lstat(filename); - return convertFileStat(stat); - } - - //**************************** - // non-async (fs-extra) - - public statSync(filename: string): FileStat { - // TODO https://github.com/microsoft/vscode/issues/84175 - // This functionality has been requested for the VS Code API. - const stat = this.fsExtra.statSync(filename); - return convertFileStat(stat); - } - - public readTextSync(filename: string): string { - // TODO https://github.com/microsoft/vscode/issues/84175 - // This functionality has been requested for the VS Code API. - return this.fsExtra.readFileSync(filename, ENCODING); - } - - public createWriteStream(filename: string): WriteStream { - // TODO https://github.com/microsoft/vscode/issues/84175 - // This functionality has been requested for the VS Code API. - return this.fsExtra.createWriteStream(filename); - } -} - -// High-level filesystem operations used by the extension. -export class FileSystemUtils implements IFileSystemUtils { - constructor( - public readonly raw: IRawFileSystem, - public readonly paths: IFileSystemPaths, - public readonly tmp: ITempFileSystem, - protected readonly getHash: (data: string) => string, - protected readonly globFile: (pat: string) => Promise - ) { } - // Create a new object using common-case default values. - public static withDefaults( - raw?: IRawFileSystem, - paths?: IFileSystemPaths, - tmp?: ITempFileSystem, - getHash?: (data: string) => string, - globFile?: (pat: string) => Promise - ): FileSystemUtils { - paths = paths || FileSystemPaths.withDefaults(); - return new FileSystemUtils( - raw || RawFileSystem.withDefaults(paths), - paths, - tmp || TempFileSystem.withDefaults(), - getHash || getHashString, - globFile || util.promisify(glob) - ); - } - - //**************************** - // aliases - - public async createDirectory(dirname: string): Promise { - return this.raw.mkdirp(dirname); - } - - public async deleteDirectory(dirname: string): Promise { - return this.raw.rmtree(dirname); - } - - public async deleteFile(filename: string): Promise { - return this.raw.rmfile(filename); - } - - //**************************** - // helpers - - public arePathsSame(path1: string, path2: string): boolean { - if (path1 === path2) { - return true; - } - path1 = this.paths.normCase(path1); - path2 = this.paths.normCase(path2); - return path1 === path2; - } - - public async pathExists( - filename: string, - fileType?: FileType - ): Promise { - let stat: FileStat; - try { - stat = await this._stat(filename); - } catch (err) { + public async getFiles(rootDir: string): Promise { + const files = await fs.readdir(rootDir); + return files.filter(async f => { + const fullPath = path.join(rootDir, f); + if ((await fs.stat(fullPath)).isFile()) { + return true; + } return false; - } - - if (fileType === undefined) { - return true; - } - if (fileType === FileType.Unknown) { - // FileType.Unknown == 0, hence do not use bitwise operations. - return stat.type === FileType.Unknown; - } - return (stat.type & fileType) === fileType; - } - public async fileExists(filename: string): Promise { - return this.pathExists(filename, FileType.File); - } - public async directoryExists(dirname: string): Promise { - return this.pathExists(dirname, FileType.Directory); - } - - public async listdir(dirname: string): Promise<[string, FileType][]> { - try { - return await this.raw.listdir(dirname); - } catch { - return []; - } - } - public async getSubDirectories(dirname: string): Promise { - return (await this.listdir(dirname)) - .filter(([_name, fileType]) => fileType & FileType.Directory) - .map(([name, _fileType]) => this.paths.join(dirname, name)); - } - public async getFiles(dirname: string): Promise { - return (await this.listdir(dirname)) - .filter(([_name, fileType]) => fileType & FileType.File) - .map(([name, _fileType]) => this.paths.join(dirname, name)); - } - - public async isDirReadonly(dirname: string): Promise { - let tmpFile: TemporaryFile; - try { - tmpFile = await this.tmp.createFile('___vscpTest___', dirname); - } catch { - // Use a stat call to ensure the directory exists. - await this._stat(dirname); - return true; - } - tmpFile.dispose(); - return false; - } - - public async getFileHash(filename: string): Promise { - const stat = await this.raw.lstat(filename); - const data = `${stat.ctime}-${stat.mtime}`; - return this.getHash(data); - } - - public async search(globPattern: string): Promise { - const files = await this.globFile(globPattern); - return Array.isArray(files) ? files : []; - } - - public async _stat(filename: string): Promise { - return this.raw.stat(filename); - } -} - -// We *could* use ICryptoUtils, but it's a bit overkill, issue tracked -// in https://github.com/microsoft/vscode-python/issues/8438. -function getHashString(data: string): string { - const hash = createHash('sha512') - .update(data); - return hash.digest('hex'); -} - -// more aliases (to cause less churn) -@injectable() -export class FileSystem implements IFileSystem { - protected utils: FileSystemUtils; - constructor() { - this.utils = FileSystemUtils.withDefaults(); + }); } - //**************************** - // wrappers - - public async createDirectory(dirname: string): Promise { - return this.utils.createDirectory(dirname); - } - public async deleteDirectory(dirname: string): Promise { - return this.utils.deleteDirectory(dirname); - } - public async deleteFile(filename: string): Promise { - return this.utils.deleteFile(filename); - } public arePathsSame(path1: string, path2: string): boolean { - return this.utils.arePathsSame(path1, path2); - } - public async pathExists(filename: string): Promise { - return this.utils.pathExists(filename); - } - public async fileExists(filename: string): Promise { - return this.utils.fileExists(filename); - } - public async directoryExists(dirname: string): Promise { - return this.utils.directoryExists(dirname); - } - public async listdir(dirname: string): Promise<[string, FileType][]> { - return this.utils.listdir(dirname); - } - public async getSubDirectories(dirname: string): Promise { - return this.utils.getSubDirectories(dirname); - } - public async getFiles(dirname: string): Promise { - return this.utils.getFiles(dirname); - } - public async isDirReadonly(dirname: string): Promise { - return this.utils.isDirReadonly(dirname); - } - public async getFileHash(filename: string): Promise { - return this.utils.getFileHash(filename); - } - public async search(globPattern: string): Promise { - return this.utils.search(globPattern); - } - - public fileExistsSync(filename: string): boolean { - try { - this.utils.raw.statSync(filename); - } catch { - return false; + path1 = path.normalize(path1); + path2 = path.normalize(path2); + if (this.platformService.isWindows) { + return path1.toUpperCase() === path2.toUpperCase(); + } else { + return path1 === path2; } - return true; } - //**************************** - // aliases - - public async stat(filePath: string): Promise { - return this.utils._stat(filePath); + public appendFileSync(filename: string, data: {}, encoding: string): void; + public appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: number; flag?: string }): void; + // tslint:disable-next-line:unified-signatures + public appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: string; flag?: string }): void; + public appendFileSync(filename: string, data: {}, optionsOrEncoding: {}): void { + return fs.appendFileSync(filename, data, optionsOrEncoding); } - public async readFile(filename: string): Promise { - return this.utils.raw.readText(filename); + public getRealPath(filePath: string): Promise { + return new Promise(resolve => { + fs.realpath(filePath, (err, realPath) => { + resolve(err ? filePath : realPath); + }); + }); } - public async writeFile(filename: string, data: {}): Promise { - return this.utils.raw.writeText(filename, data); + public copyFile(src: string, dest: string): Promise { + const deferred = createDeferred(); + const rs = fs.createReadStream(src).on('error', err => { + deferred.reject(err); + }); + const ws = fs + .createWriteStream(dest) + .on('error', err => { + deferred.reject(err); + }) + .on('close', () => { + deferred.resolve(); + }); + rs.pipe(ws); + return deferred.promise; } - public async chmod(filename: string, mode: string): Promise { - return this.utils.raw.chmod(filename, mode); + public deleteFile(filename: string): Promise { + const deferred = createDeferred(); + fs.unlink(filename, err => (err ? deferred.reject(err) : deferred.resolve())); + return deferred.promise; } - public async copyFile(src: string, dest: string): Promise { - return this.utils.raw.copyFile(src, dest); + public getFileHash(filePath: string): Promise { + return new Promise((resolve, reject) => { + fs.lstat(filePath, (err, stats) => { + if (err) { + reject(err); + } else { + const actual = createHash('sha512') + .update(`${stats.ctimeMs}-${stats.mtimeMs}`) + .digest('hex'); + resolve(actual); + } + }); + }); } - - public readFileSync(filename: string): string { - return this.utils.raw.readTextSync(filename); + public search(globPattern: string): Promise { + return new Promise((resolve, reject) => { + glob(globPattern, (ex, files) => { + if (ex) { + return reject(ex); + } + resolve(Array.isArray(files) ? files : []); + }); + }); + } + public createTemporaryFile(extension: string): Promise { + return new Promise((resolve, reject) => { + tmp.file({ postfix: extension }, (err, tmpFile, _, cleanupCallback) => { + if (err) { + return reject(err); + } + resolve({ filePath: tmpFile, dispose: cleanupCallback }); + }); + }); } - public createWriteStream(filename: string): WriteStream { - return this.utils.raw.createWriteStream(filename); + public createWriteStream(filePath: string): fileSystem.WriteStream { + return fileSystem.createWriteStream(filePath); } - public async createTemporaryFile(suffix: string): Promise { - return this.utils.tmp.createFile(suffix); + public chmod(filePath: string, mode: string): Promise { + return new Promise((resolve, reject) => { + fileSystem.chmod(filePath, mode, (err: NodeJS.ErrnoException | null) => { + if (err) { + return reject(err); + } + resolve(); + }); + }); } } diff --git a/src/client/common/platform/types.ts b/src/client/common/platform/types.ts index a2a5ba2c575c..586b5557a070 100644 --- a/src/client/common/platform/types.ts +++ b/src/client/common/platform/types.ts @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; - import * as fs from 'fs'; +import * as fsextra from 'fs-extra'; import { SemVer } from 'semver'; -import * as vscode from 'vscode'; +import { Disposable, FileStat } from 'vscode'; import { Architecture, OSType } from '../utils/platform'; export enum RegistryHive { @@ -33,140 +32,34 @@ export interface IPlatformService { getVersion(): Promise; } -export type TemporaryFile = vscode.Disposable & { - filePath: string; -}; -export type TemporaryDirectory = vscode.Disposable & { - path: string; -}; -export interface ITempFileSystem { - createFile(suffix?: string, dir?: string): Promise; -} - -// Eventually we will merge IPathUtils into IFileSystemPath. - -export interface IFileSystemPaths { - join(...filenames: string[]): string; - dirname(filename: string): string; - normCase(filename: string): string; -} - -export import FileType = vscode.FileType; -export type FileStat = vscode.FileStat; -export type WriteStream = fs.WriteStream; - -// Later we will drop "IFileSystem", switching usage to -// "IFileSystemUtils" and then rename "IRawFileSystem" to "IFileSystem". - -// The low-level filesystem operations on which the extension depends. -export interface IRawFileSystem { - // Get information about a file (resolve symlinks). - stat(filename: string): Promise; - // Get information about a file (do not resolve synlinks). - lstat(filename: string): Promise; - // Change a file's permissions. - chmod(filename: string, mode: string | number): Promise; - - //*********************** - // files - - // Return the text of the given file (decoded from UTF-8). - readText(filename: string): Promise; - // Write the given text to the file (UTF-8 encoded). - writeText(filename: string, data: {}): Promise; - // Copy a file. - copyFile(src: string, dest: string): Promise; - // Delete a file. - rmfile(filename: string): Promise; - - //*********************** - // directories - - // Create the directory and any missing parent directories. - mkdirp(dirname: string): Promise; - // Delete the directory and everything in it. - rmtree(dirname: string): Promise; - // Return the contents of the directory. - listdir(dirname: string): Promise<[string, FileType][]>; - - //*********************** - // not async - - // Get information about a file (resolve symlinks). - statSync(filename: string): FileStat; - // Return the text of the given file (decoded from UTF-8). - readTextSync(filename: string): string; - // Create a streaming wrappr around an open file. - createWriteStream(filename: string): WriteStream; -} - -// High-level filesystem operations used by the extension. -export interface IFileSystemUtils { - readonly raw: IRawFileSystem; - readonly paths: IFileSystemPaths; - readonly tmp: ITempFileSystem; - - //*********************** - // aliases +export type TemporaryFile = { filePath: string } & Disposable; +export type TemporaryDirectory = { path: string } & Disposable; - createDirectory(dirname: string): Promise; - deleteDirectory(dirname: string): Promise; - deleteFile(filename: string): Promise; - - //*********************** - // helpers - - // Determine if the file exists, optionally requiring the type. - pathExists(filename: string, fileType?: FileType): Promise; - // Determine if the regular file exists. - fileExists(filename: string): Promise; - // Determine if the directory exists. - directoryExists(dirname: string): Promise; - // Get the paths of all immediate subdirectories. - getSubDirectories(dirname: string): Promise; - // Get the paths of all immediately contained files. - getFiles(dirname: string): Promise; - // Determine if the directory is read-only. - isDirReadonly(dirname: string): Promise; - // Generate the sha512 hash for the file (based on timestamps). - getFileHash(filename: string): Promise; - // Get the paths of all files matching the pattern. - search(globPattern: string): Promise; - - //*********************** - // helpers (non-async) - - // Decide if the two filenames are equivalent. - arePathsSame(path1: string, path2: string): boolean; // Move to IPathUtils. -} - -// more aliases (to cause less churn) export const IFileSystem = Symbol('IFileSystem'); export interface IFileSystem { - createDirectory(dirname: string): Promise; - deleteDirectory(dirname: string): Promise; - deleteFile(filename: string): Promise; - pathExists(filename: string, fileType?: FileType): Promise; - fileExists(filename: string): Promise; - directoryExists(dirname: string): Promise; - getSubDirectories(dirname: string): Promise; - getFiles(dirname: string): Promise; - isDirReadonly(dirname: string): Promise; - getFileHash(filename: string): Promise; - search(globPattern: string): Promise; - arePathsSame(path1: string, path2: string): boolean; - + directorySeparatorChar: string; stat(filePath: string): Promise; - readFile(filename: string): Promise; - writeFile(filename: string, data: {}): Promise; - chmod(filename: string, mode: string): Promise; + objectExists(path: string, statCheck: (s: fs.Stats) => boolean): Promise; + fileExists(path: string): Promise; + fileExistsSync(path: string): boolean; + directoryExists(path: string): Promise; + createDirectory(path: string): Promise; + deleteDirectory(path: string): Promise; + getSubDirectories(rootDir: string): Promise; + getFiles(rootDir: string): Promise; + arePathsSame(path1: string, path2: string): boolean; + readFile(filePath: string): Promise; + writeFile(filePath: string, data: {}, options?: string | fsextra.WriteFileOptions): Promise; + appendFileSync(filename: string, data: {}, encoding: string): void; + appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: number; flag?: string }): void; + // tslint:disable-next-line:unified-signatures + appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: string; flag?: string }): void; + getRealPath(path: string): Promise; copyFile(src: string, dest: string): Promise; - createTemporaryFile(suffix: string): Promise; - - //*********************** - // non-async - - fileExistsSync(filename: string): boolean; - readFileSync(filename: string): string; - createWriteStream(filename: string): WriteStream; + deleteFile(filename: string): Promise; + getFileHash(filePath: string): Promise; + search(globPattern: string): Promise; + createTemporaryFile(extension: string): Promise; + createWriteStream(path: string): fs.WriteStream; + chmod(path: string, mode: string): Promise; } diff --git a/src/client/common/utils/fs.ts b/src/client/common/utils/fs.ts new file mode 100644 index 000000000000..4ab1e19686c8 --- /dev/null +++ b/src/client/common/utils/fs.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as fs from 'fs'; +import * as path from 'path'; +import * as tmp from 'tmp'; + +export function fsExistsAsync(filePath: string): Promise { + return new Promise(resolve => { + fs.exists(filePath, exists => { + return resolve(exists); + }); + }); +} +export function fsReaddirAsync(root: string): Promise { + return new Promise(resolve => { + // Now look for Interpreters in this directory + fs.readdir(root, (err, subDirs) => { + if (err) { + return resolve([]); + } + resolve(subDirs.map(subDir => path.join(root, subDir))); + }); + }); +} + +export function getSubDirectories(rootDir: string): Promise { + return new Promise(resolve => { + fs.readdir(rootDir, (error, files) => { + if (error) { + return resolve([]); + } + const subDirs: string[] = []; + files.forEach(name => { + const fullPath = path.join(rootDir, name); + try { + if (fs.statSync(fullPath).isDirectory()) { + subDirs.push(fullPath); + } + } + // tslint:disable-next-line:no-empty one-line + catch (ex) { } + }); + resolve(subDirs); + }); + }); +} + +export function createTemporaryFile(extension: string, temporaryDirectory?: string): Promise<{ filePath: string; cleanupCallback: Function }> { + // tslint:disable-next-line:no-any + const options: any = { postfix: extension }; + if (temporaryDirectory) { + options.dir = temporaryDirectory; + } + + return new Promise<{ filePath: string; cleanupCallback: Function }>((resolve, reject) => { + tmp.file(options, (err, tmpFile, _fd, cleanupCallback) => { + if (err) { + return reject(err); + } + resolve({ filePath: tmpFile, cleanupCallback: cleanupCallback }); + }); + }); +} diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index f6b5c4f65d7c..3b59b7537f14 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -3,10 +3,9 @@ 'use strict'; +import * as fs from 'fs'; import * as path from 'path'; import { EXTENSION_ROOT_DIR } from '../../constants'; -import { FileSystem } from '../platform/fileSystem'; -import { IFileSystem } from '../platform/types'; // External callers of localize use these tables to retrieve localized values. export namespace Diagnostics { @@ -494,15 +493,14 @@ function getString(key: string, defValue?: string) { return result; } -function load(fs?: IFileSystem) { - fs = fs ? fs : new FileSystem(); +function load() { // Figure out our current locale. loadedLocale = parseLocale(); // Find the nls file that matches (if there is one) const nlsFile = path.join(EXTENSION_ROOT_DIR, `package.nls.${loadedLocale}.json`); - if (fs.fileExistsSync(nlsFile)) { - const contents = fs.readFileSync(nlsFile); + if (fs.existsSync(nlsFile)) { + const contents = fs.readFileSync(nlsFile, 'utf8'); loadedCollection = JSON.parse(contents); } else { // If there isn't one, at least remember that we looked so we don't try to load a second time @@ -512,8 +510,8 @@ function load(fs?: IFileSystem) { // Get the default collection if necessary. Strings may be in the default or the locale json if (!defaultCollection) { const defaultNlsFile = path.join(EXTENSION_ROOT_DIR, 'package.nls.json'); - if (fs.fileExistsSync(defaultNlsFile)) { - const contents = fs.readFileSync(defaultNlsFile); + if (fs.existsSync(defaultNlsFile)) { + const contents = fs.readFileSync(defaultNlsFile, 'utf8'); defaultCollection = JSON.parse(contents); } else { defaultCollection = {}; diff --git a/src/client/common/variables/environment.ts b/src/client/common/variables/environment.ts index fd905ad83786..92921345a6f6 100644 --- a/src/client/common/variables/environment.ts +++ b/src/client/common/variables/environment.ts @@ -1,37 +1,28 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as fs from 'fs-extra'; import { inject, injectable } from 'inversify'; import * as path from 'path'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -import { IFileSystem } from '../platform/types'; import { IPathUtils } from '../types'; import { EnvironmentVariables, IEnvironmentVariablesService } from './types'; @injectable() export class EnvironmentVariablesService implements IEnvironmentVariablesService { private readonly pathVariable: 'PATH' | 'Path'; - constructor( - @inject(IPathUtils) pathUtils: IPathUtils, - @inject(IFileSystem) private readonly fs: IFileSystem - ) { + constructor(@inject(IPathUtils) pathUtils: IPathUtils) { this.pathVariable = pathUtils.getPathVariableName(); } - public async parseFile( - filePath?: string, - baseVars?: EnvironmentVariables - ): Promise { - if (!filePath) { + public async parseFile(filePath?: string, baseVars?: EnvironmentVariables): Promise { + if (!filePath || !await fs.pathExists(filePath)) { return; } - if (!await this.fs.fileExists(filePath)) { + if (!fs.lstatSync(filePath).isFile()) { return; } - return parseEnvFile( - await this.fs.readFile(filePath), - baseVars - ); + return parseEnvFile(await fs.readFile(filePath), baseVars); } public mergeVariables(source: EnvironmentVariables, target: EnvironmentVariables) { if (!target) { diff --git a/src/client/datascience/interactive-common/interactiveBase.ts b/src/client/datascience/interactive-common/interactiveBase.ts index f22ce9432869..5b6db22996c6 100644 --- a/src/client/datascience/interactive-common/interactiveBase.ts +++ b/src/client/datascience/interactive-common/interactiveBase.ts @@ -634,7 +634,7 @@ export abstract class InteractiveBase extends WebViewHost { diff --git a/src/client/datascience/interactive-ipynb/nativeEditor.ts b/src/client/datascience/interactive-ipynb/nativeEditor.ts index 45fbe20425ea..a03896a1ee65 100644 --- a/src/client/datascience/interactive-ipynb/nativeEditor.ts +++ b/src/client/datascience/interactive-ipynb/nativeEditor.ts @@ -788,7 +788,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor { tempFile = await this.fileSystem.createTemporaryFile('.ipynb'); // Translate the cells into a notebook - await this.fileSystem.writeFile(tempFile.filePath, await this.generateNotebookContent(cells)); + await this.fileSystem.writeFile(tempFile.filePath, await this.generateNotebookContent(cells), { encoding: 'utf-8' }); // Import this file and show it const contents = await this.importer.importFromFile(tempFile.filePath, this.file.fsPath); diff --git a/src/client/datascience/jupyter/kernels/kernelService.ts b/src/client/datascience/jupyter/kernels/kernelService.ts index 3b5a83d9849c..9e06a076da9d 100644 --- a/src/client/datascience/jupyter/kernels/kernelService.ts +++ b/src/client/datascience/jupyter/kernels/kernelService.ts @@ -296,7 +296,7 @@ export class KernelService { specModel.metadata.interpreter = interpreter as any; // Update the kernel.json with our new stuff. - await this.fileSystem.writeFile(kernel.specFile, JSON.stringify(specModel, undefined, 2)); + await this.fileSystem.writeFile(kernel.specFile, JSON.stringify(specModel, undefined, 2), { flag: 'w', encoding: 'utf8' }); kernel.metadata = specModel.metadata; traceInfo(`Kernel successfully registered for ${interpreter.path} with the name=${name} and spec can be found here ${kernel.specFile}`); diff --git a/src/client/debugger/debugAdapter/main.ts b/src/client/debugger/debugAdapter/main.ts index c078ee92bb8e..0403dc5c7905 100644 --- a/src/client/debugger/debugAdapter/main.ts +++ b/src/client/debugger/debugAdapter/main.ts @@ -9,7 +9,6 @@ if ((Reflect as any).metadata === undefined) { require('reflect-metadata'); } -import * as fsextra from 'fs-extra'; import { Socket } from 'net'; import { EOL } from 'os'; import * as path from 'path'; @@ -21,6 +20,7 @@ import { DebugProtocol } from 'vscode-debugprotocol'; import { EXTENSION_ROOT_DIR } from '../../common/constants'; import '../../common/extensions'; import { isNotInstalledError } from '../../common/helpers'; +import { IFileSystem } from '../../common/platform/types'; import { ICurrentProcess, IDisposable, IDisposableRegistry } from '../../common/types'; import { createDeferred, Deferred, sleep } from '../../common/utils/async'; import { noop } from '../../common/utils/misc'; @@ -47,10 +47,7 @@ export class PythonDebugger extends DebugSession { public debugServer?: BaseDebugServer; public client = createDeferred(); private supportsRunInTerminalRequest: boolean = false; - constructor( - private readonly serviceContainer: IServiceContainer, - private readonly fileExistsSync = fsextra.existsSync - ) { + constructor(private readonly serviceContainer: IServiceContainer) { super(false); } public shutdown(): void { @@ -109,7 +106,8 @@ export class PythonDebugger extends DebugSession { } protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { - if ((typeof args.module !== 'string' || args.module.length === 0) && args.program && !this.fileExistsSync(args.program)) { + const fs = this.serviceContainer.get(IFileSystem); + if ((typeof args.module !== 'string' || args.module.length === 0) && args.program && !fs.fileExistsSync(args.program)) { return this.sendErrorResponse(response, { format: `File does not exist. "${args.program}"`, id: 1 }, undefined, undefined, ErrorDestination.User); } diff --git a/src/client/debugger/debugAdapter/serviceRegistry.ts b/src/client/debugger/debugAdapter/serviceRegistry.ts index 0feccf1baa85..2c015eb99297 100644 --- a/src/client/debugger/debugAdapter/serviceRegistry.ts +++ b/src/client/debugger/debugAdapter/serviceRegistry.ts @@ -5,6 +5,9 @@ import { Container } from 'inversify'; import { SocketServer } from '../../common/net/socket/socketServer'; +import { FileSystem } from '../../common/platform/fileSystem'; +import { PlatformService } from '../../common/platform/platformService'; +import { IFileSystem, IPlatformService } from '../../common/platform/types'; import { CurrentProcess } from '../../common/process/currentProcess'; import { BufferDecoder } from '../../common/process/decoder'; import { IBufferDecoder, IProcessServiceFactory } from '../../common/process/types'; @@ -34,6 +37,8 @@ function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IDebugStreamProvider, DebugStreamProvider); serviceManager.addSingleton(IProtocolLogger, ProtocolLogger); serviceManager.add(IProtocolParser, ProtocolParser); + serviceManager.addSingleton(IFileSystem, FileSystem); + serviceManager.addSingleton(IPlatformService, PlatformService); serviceManager.addSingleton(ISocketServer, SocketServer); serviceManager.addSingleton(IProtocolMessageWriter, ProtocolMessageWriter); serviceManager.addSingleton(IBufferDecoder, BufferDecoder); diff --git a/src/client/formatters/baseFormatter.ts b/src/client/formatters/baseFormatter.ts index a8a2112c9d89..bd84428f8481 100644 --- a/src/client/formatters/baseFormatter.ts +++ b/src/client/formatters/baseFormatter.ts @@ -1,3 +1,4 @@ +import * as fs from 'fs-extra'; import * as path from 'path'; import * as vscode from 'vscode'; import { IApplicationShell, IWorkspaceService } from '../common/application/types'; @@ -5,7 +6,6 @@ import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; import '../common/extensions'; import { isNotInstalledError } from '../common/helpers'; import { traceError } from '../common/logger'; -import { IFileSystem } from '../common/platform/types'; import { IPythonToolExecutionService } from '../common/process/types'; import { IDisposableRegistry, IInstaller, IOutputChannel, Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; @@ -15,17 +15,11 @@ import { IFormatterHelper } from './types'; export abstract class BaseFormatter { protected readonly outputChannel: vscode.OutputChannel; protected readonly workspace: IWorkspaceService; - private readonly fs: IFileSystem; private readonly helper: IFormatterHelper; - constructor( - public Id: string, - private product: Product, - protected serviceContainer: IServiceContainer - ) { + constructor(public Id: string, private product: Product, protected serviceContainer: IServiceContainer) { this.outputChannel = serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); this.helper = serviceContainer.get(IFormatterHelper); - this.fs = serviceContainer.get(IFileSystem); this.workspace = serviceContainer.get(IWorkspaceService); } @@ -109,13 +103,13 @@ export abstract class BaseFormatter { private async createTempFile(document: vscode.TextDocument): Promise { return document.isDirty - ? getTempFileWithDocumentContents(document, this.fs) + ? getTempFileWithDocumentContents(document) : document.fileName; } private deleteTempFile(originalFile: string, tempFile: string): Promise { if (originalFile !== tempFile) { - return this.fs.deleteFile(tempFile); + return fs.unlink(tempFile); } return Promise.resolve(); } diff --git a/src/client/interpreter/locators/helpers.ts b/src/client/interpreter/locators/helpers.ts index 47bf6dd6fd2d..bf1a0a8424dd 100644 --- a/src/client/interpreter/locators/helpers.ts +++ b/src/client/interpreter/locators/helpers.ts @@ -2,28 +2,20 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { traceError } from '../../common/logger'; import { IS_WINDOWS } from '../../common/platform/constants'; -import { FileSystem } from '../../common/platform/fileSystem'; import { IFileSystem } from '../../common/platform/types'; +import { fsReaddirAsync } from '../../common/utils/fs'; import { IInterpreterLocatorHelper, InterpreterType, PythonInterpreter } from '../contracts'; import { IPipEnvServiceHelper } from './types'; const CheckPythonInterpreterRegEx = IS_WINDOWS ? /^python(\d+(.\d+)?)?\.exe$/ : /^python(\d+(.\d+)?)?$/; -export async function lookForInterpretersInDirectory( - pathToCheck: string, - fs: IFileSystem = new FileSystem() -): Promise { - const files = await ( - fs.getFiles(pathToCheck) - .catch(err => { - traceError('Python Extension (lookForInterpretersInDirectory.fs.getFiles):', err); - return [] as string[]; - }) - ); - return files.filter(filename => { - const name = path.basename(filename); - return CheckPythonInterpreterRegEx.test(name); - }); +export function lookForInterpretersInDirectory(pathToCheck: string): Promise { + return fsReaddirAsync(pathToCheck) + .then(subDirs => subDirs.filter(fileName => CheckPythonInterpreterRegEx.test(path.basename(fileName)))) + .catch(err => { + traceError('Python Extension (lookForInterpretersInDirectory.fsReaddirAsync):', err); + return [] as string[]; + }); } @injectable() diff --git a/src/client/interpreter/locators/services/KnownPathsService.ts b/src/client/interpreter/locators/services/KnownPathsService.ts index b6081fa61bf0..ee033e322bc7 100644 --- a/src/client/interpreter/locators/services/KnownPathsService.ts +++ b/src/client/interpreter/locators/services/KnownPathsService.ts @@ -2,8 +2,9 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Uri } from 'vscode'; -import { IFileSystem, IPlatformService } from '../../../common/platform/types'; +import { IPlatformService } from '../../../common/platform/types'; import { ICurrentProcess, IPathUtils } from '../../../common/types'; +import { fsExistsAsync } from '../../../common/utils/fs'; import { IServiceContainer } from '../../../ioc/types'; import { IInterpreterHelper, IKnownSearchPathsForInterpreters, InterpreterType, PythonInterpreter } from '../../contracts'; import { lookForInterpretersInDirectory } from '../helpers'; @@ -15,14 +16,12 @@ const flatten = require('lodash/flatten') as typeof import('lodash/flatten'); */ @injectable() export class KnownPathsService extends CacheableLocatorService { - private readonly fs: IFileSystem; public constructor( @inject(IKnownSearchPathsForInterpreters) private knownSearchPaths: IKnownSearchPathsForInterpreters, @inject(IInterpreterHelper) private helper: IInterpreterHelper, @inject(IServiceContainer) serviceContainer: IServiceContainer ) { super('KnownPathsService', serviceContainer); - this.fs = serviceContainer.get(IFileSystem); } /** @@ -73,12 +72,9 @@ export class KnownPathsService extends CacheableLocatorService { /** * Return the interpreters in the given directory. */ - private async getInterpretersInDirectory(dir: string): Promise { - if (await this.fs.directoryExists(dir)) { - return lookForInterpretersInDirectory(dir, this.fs); - } else { - return []; - } + private getInterpretersInDirectory(dir: string) { + return fsExistsAsync(dir) + .then(exists => exists ? lookForInterpretersInDirectory(dir) : Promise.resolve([])); } } diff --git a/src/client/interpreter/locators/services/baseVirtualEnvService.ts b/src/client/interpreter/locators/services/baseVirtualEnvService.ts index 1dec370789fd..4b46de4a2261 100644 --- a/src/client/interpreter/locators/services/baseVirtualEnvService.ts +++ b/src/client/interpreter/locators/services/baseVirtualEnvService.ts @@ -40,7 +40,7 @@ export class BaseVirtualEnvService extends CacheableLocatorService { return this.fileSystem.getSubDirectories(pathToCheck) .then(subDirs => Promise.all(this.getProspectiveDirectoriesForLookup(subDirs))) .then(dirs => dirs.filter(dir => dir.length > 0)) - .then(dirs => Promise.all(dirs.map(d => lookForInterpretersInDirectory(d, this.fileSystem)))) + .then(dirs => Promise.all(dirs.map(lookForInterpretersInDirectory))) .then(pathsWithInterpreters => flatten(pathsWithInterpreters)) .then(interpreters => Promise.all(interpreters.map(interpreter => this.getVirtualEnvDetails(interpreter, resource)))) .then(interpreters => interpreters.filter(interpreter => !!interpreter).map(interpreter => interpreter!)) diff --git a/src/client/interpreter/locators/services/windowsRegistryService.ts b/src/client/interpreter/locators/services/windowsRegistryService.ts index 70af04c649e8..e99f29a1be66 100644 --- a/src/client/interpreter/locators/services/windowsRegistryService.ts +++ b/src/client/interpreter/locators/services/windowsRegistryService.ts @@ -1,11 +1,10 @@ // tslint:disable:no-require-imports no-var-requires underscore-consistent-invocation +import * as fs from 'fs-extra'; import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Uri } from 'vscode'; import { traceError } from '../../../common/logger'; -import { - IFileSystem, IPlatformService, IRegistry, RegistryHive -} from '../../../common/platform/types'; +import { IPlatformService, IRegistry, RegistryHive } from '../../../common/platform/types'; import { IPathUtils } from '../../../common/types'; import { Architecture } from '../../../common/utils/platform'; import { parsePythonVersion } from '../../../common/utils/version'; @@ -39,8 +38,7 @@ export class WindowsRegistryService extends CacheableLocatorService { @inject(IRegistry) private registry: IRegistry, @inject(IPlatformService) private readonly platform: IPlatformService, @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(WindowsStoreInterpreter) private readonly windowsStoreInterpreter: IWindowsStoreInterpreter, - @inject(IFileSystem) private readonly fs: IFileSystem + @inject(WindowsStoreInterpreter) private readonly windowsStoreInterpreter: IWindowsStoreInterpreter ) { super('WindowsRegistryService', serviceContainer); this.pathUtils = serviceContainer.get(IPathUtils); @@ -160,7 +158,7 @@ export class WindowsRegistryService extends CacheableLocatorService { }) .then(interpreter => interpreter - ? this.fs + ? fs .pathExists(interpreter.path) .catch(() => false) .then(exists => (exists ? interpreter : null)) diff --git a/src/client/providers/jediProxy.ts b/src/client/providers/jediProxy.ts index 8194b7760830..19a55328f85a 100644 --- a/src/client/providers/jediProxy.ts +++ b/src/client/providers/jediProxy.ts @@ -3,6 +3,7 @@ // tslint:disable:no-var-requires no-require-imports no-any import { ChildProcess } from 'child_process'; +import * as fs from 'fs-extra'; import * as path from 'path'; // @ts-ignore import * as pidusage from 'pidusage'; @@ -10,7 +11,6 @@ import { CancellationToken, CancellationTokenSource, CompletionItemKind, Disposa import { isTestExecution } from '../common/constants'; import '../common/extensions'; import { IS_WINDOWS } from '../common/platform/constants'; -import { IFileSystem } from '../common/platform/types'; import { IPythonExecutionFactory } from '../common/process/types'; import { BANNER_NAME_PROPOSE_LS, IConfigurationService, ILogger, IPythonExtensionBanner, IPythonSettings } from '../common/types'; import { createDeferred, Deferred } from '../common/utils/async'; @@ -640,7 +640,6 @@ export class JediProxy implements Disposable { if (lines.length === 0) { return ''; } - const fs = this.serviceContainer.get(IFileSystem); const exists = await fs.pathExists(lines[0]); return exists ? lines[0] : ''; } catch { diff --git a/src/client/providers/renameProvider.ts b/src/client/providers/renameProvider.ts index 481c804bf83d..a29de3281be6 100644 --- a/src/client/providers/renameProvider.ts +++ b/src/client/providers/renameProvider.ts @@ -6,7 +6,6 @@ import { import { EXTENSION_ROOT_DIR, STANDARD_OUTPUT_CHANNEL } from '../common/constants'; import { getWorkspaceEditsFromPatch } from '../common/editor'; import { traceError } from '../common/logger'; -import { IFileSystem } from '../common/platform/types'; import { IConfigurationService, IInstaller, IOutputChannel, Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { RefactorProxy } from '../refactor/proxy'; @@ -20,13 +19,9 @@ type RenameResponse = { export class PythonRenameProvider implements RenameProvider { private readonly outputChannel: OutputChannel; private readonly configurationService: IConfigurationService; - private readonly fs: IFileSystem; - constructor( - private serviceContainer: IServiceContainer - ) { + constructor(private serviceContainer: IServiceContainer) { this.outputChannel = serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); this.configurationService = serviceContainer.get(IConfigurationService); - this.fs = serviceContainer.get(IFileSystem); } @captureTelemetry(EventName.REFACTOR_RENAME) public provideRenameEdits(document: TextDocument, position: Position, newName: string, _token: CancellationToken): ProviderResult { @@ -62,7 +57,7 @@ export class PythonRenameProvider implements RenameProvider { const proxy = new RefactorProxy(EXTENSION_ROOT_DIR, pythonSettings, workspaceRoot, this.serviceContainer); return proxy.rename(document, newName, document.uri.fsPath, range).then(response => { const fileDiffs = response.results.map(fileChanges => fileChanges.diff); - return getWorkspaceEditsFromPatch(fileDiffs, this.fs, workspaceRoot); + return getWorkspaceEditsFromPatch(fileDiffs, workspaceRoot); }).catch(reason => { if (reason === 'Not installed') { const installer = this.serviceContainer.get(IInstaller); diff --git a/src/client/sourceMapSupport.ts b/src/client/sourceMapSupport.ts index b878df21a217..02907eaaf5ae 100644 --- a/src/client/sourceMapSupport.ts +++ b/src/client/sourceMapSupport.ts @@ -59,9 +59,6 @@ export class SourceMapSupport { } } protected async rename(sourceFile: string, targetFile: string) { - // SourceMapSupport is initialized before the extension, so we - // do not have access to IFileSystem yet and have to use Node's - // "fs" directly. const fsExists = promisify(fs.exists); const fsRename = promisify(fs.rename); if (await fsExists(targetFile)) { diff --git a/src/client/testing/common/managers/testConfigurationManager.ts b/src/client/testing/common/managers/testConfigurationManager.ts index ad693d5b9a0a..ca4d7ce43ebb 100644 --- a/src/client/testing/common/managers/testConfigurationManager.ts +++ b/src/client/testing/common/managers/testConfigurationManager.ts @@ -1,9 +1,9 @@ import * as path from 'path'; import { OutputChannel, QuickPickItem, Uri } from 'vscode'; import { IApplicationShell } from '../../../common/application/types'; -import { IFileSystem } from '../../../common/platform/types'; import { IInstaller, ILogger, IOutputChannel } from '../../../common/types'; import { createDeferred } from '../../../common/utils/async'; +import { getSubDirectories } from '../../../common/utils/fs'; import { IServiceContainer } from '../../../ioc/types'; import { ITestConfigSettingsService, ITestConfigurationManager } from '../../types'; import { TEST_OUTPUT_CHANNEL, UNIT_TEST_PRODUCTS } from '../constants'; @@ -13,10 +13,7 @@ export abstract class TestConfigurationManager implements ITestConfigurationMana protected readonly outputChannel: OutputChannel; protected readonly installer: IInstaller; protected readonly testConfigSettingsService: ITestConfigSettingsService; - private readonly fs: IFileSystem; - - constructor( - protected workspace: Uri, + constructor(protected workspace: Uri, protected product: UnitTestProduct, protected readonly serviceContainer: IServiceContainer, cfg?: ITestConfigSettingsService @@ -24,12 +21,9 @@ export abstract class TestConfigurationManager implements ITestConfigurationMana this.outputChannel = serviceContainer.get(IOutputChannel, TEST_OUTPUT_CHANNEL); this.installer = serviceContainer.get(IInstaller); this.testConfigSettingsService = cfg ? cfg : serviceContainer.get(ITestConfigSettingsService); - this.fs = serviceContainer.get(IFileSystem); } - public abstract configure(wkspace: Uri): Promise; public abstract requiresUserToConfigure(wkspace: Uri): Promise; - public async enable() { // Disable other test frameworks. await Promise.all(UNIT_TEST_PRODUCTS @@ -107,7 +101,7 @@ export abstract class TestConfigurationManager implements ITestConfigurationMana return def.promise; } protected getTestDirs(rootDir: string): Promise { - return this.fs.getSubDirectories(rootDir).then(subDirs => { + return getSubDirectories(rootDir).then(subDirs => { subDirs.sort(); // Find out if there are any dirs with the name test and place them on the top. diff --git a/src/client/workspaceSymbols/parser.ts b/src/client/workspaceSymbols/parser.ts index 3522e66c6a66..54bd50571360 100644 --- a/src/client/workspaceSymbols/parser.ts +++ b/src/client/workspaceSymbols/parser.ts @@ -1,7 +1,6 @@ import * as path from 'path'; import * as vscode from 'vscode'; -import { FileSystem } from '../common/platform/fileSystem'; -import { IFileSystem } from '../common/platform/types'; +import { fsExistsAsync } from '../common/utils/fs'; import { ITag } from './contracts'; // tslint:disable:no-require-imports no-var-requires no-suspicious-comment @@ -108,11 +107,9 @@ export function parseTags( workspaceFolder: string, tagFile: string, query: string, - token: vscode.CancellationToken, - fs?: IFileSystem + token: vscode.CancellationToken ): Promise { - fs = fs ? fs : new FileSystem(); - return fs.fileExists(tagFile).then(exists => { + return fsExistsAsync(tagFile).then(exists => { if (!exists) { return Promise.resolve([]); } diff --git a/src/client/workspaceSymbols/provider.ts b/src/client/workspaceSymbols/provider.ts index ee2460be6e8c..019bcd25592d 100644 --- a/src/client/workspaceSymbols/provider.ts +++ b/src/client/workspaceSymbols/provider.ts @@ -43,13 +43,7 @@ export class WorkspaceSymbolProvider implements IWorspaceSymbolProvider { .filter(generator => generator !== undefined && generator.enabled) .map(async generator => { // load tags - const items = await parseTags( - generator!.workspaceFolder.fsPath, - generator!.tagFilePath, - query, - token, - this.fs - ); + const items = await parseTags(generator!.workspaceFolder.fsPath, generator!.tagFilePath, query, token); if (!Array.isArray(items)) { return []; } diff --git a/src/test/common/crypto.unit.test.ts b/src/test/common/crypto.unit.test.ts index e97b5b4adaed..2d1ea5257cf9 100644 --- a/src/test/common/crypto.unit.test.ts +++ b/src/test/common/crypto.unit.test.ts @@ -4,23 +4,20 @@ 'use strict'; import { assert, expect } from 'chai'; -import * as fsextra from 'fs-extra'; import * as path from 'path'; import { CryptoUtils } from '../../client/common/crypto'; +import { FileSystem } from '../../client/common/platform/fileSystem'; +import { PlatformService } from '../../client/common/platform/platformService'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../constants'; // tslint:disable-next-line: max-func-body-length suite('Crypto Utils', async () => { let crypto: CryptoUtils; + const fs = new FileSystem(new PlatformService()); + const file = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'common', 'randomWords.txt'); setup(() => { crypto = new CryptoUtils(); }); - async function getWordList(): Promise { - const file = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'common', 'randomWords.txt'); - const words = await fsextra.readFile(file, 'utf8'); - return words.split('\n'); - } - test('If hashFormat equals `number`, method createHash() returns a number', async () => { const hash = crypto.createHash('blabla', 'number'); assert.typeOf(hash, 'number', 'Type should be a number'); @@ -56,7 +53,8 @@ suite('Crypto Utils', async () => { assert.notEqual(hash1, hash2, 'Hashes should be different strings'); }); test('If hashFormat equals `number`, ensure numbers are uniformly distributed on scale from 0 to 100', async () => { - const wordList = await getWordList(); + const words = await fs.readFile(file); + const wordList = words.split('\n'); const buckets: number[] = Array(100).fill(0); const hashes = Array(10).fill(0); for (const w of wordList) { @@ -75,7 +73,8 @@ suite('Crypto Utils', async () => { } }); test('If hashFormat equals `number`, on a scale of 0 to 100, small difference in the input on average produce large differences (about 33) in the output ', async () => { - const wordList = await getWordList(); + const words = await fs.readFile(file); + const wordList = words.split('\n'); const buckets: number[] = Array(100).fill(0); let hashes: number[] = []; let totalDifference = 0; diff --git a/src/test/common/net/fileDownloader.unit.test.ts b/src/test/common/net/fileDownloader.unit.test.ts index c82ddd023cc2..d3b31e1ffc86 100644 --- a/src/test/common/net/fileDownloader.unit.test.ts +++ b/src/test/common/net/fileDownloader.unit.test.ts @@ -95,7 +95,7 @@ suite('File Downloader', () => { httpClient = mock(HttpClient); appShell = mock(ApplicationShell); when(httpClient.downloadFile(anything())).thenCall(request); - fs = new FileSystem(); + fs = new FileSystem(new PlatformService()); }); teardown(() => { rewiremock.disable(); diff --git a/src/test/common/platform/filesystem.functional.test.ts b/src/test/common/platform/filesystem.functional.test.ts deleted file mode 100644 index 0834c7f4a4f3..000000000000 --- a/src/test/common/platform/filesystem.functional.test.ts +++ /dev/null @@ -1,764 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// tslint:disable:no-require-imports no-var-requires max-func-body-length chai-vague-errors - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as fsextra from 'fs-extra'; -import * as net from 'net'; -import * as path from 'path'; -import * as tmpMod from 'tmp'; -import { - FileSystem, FileSystemPaths, FileSystemUtils, RawFileSystem, - TempFileSystem -} from '../../../client/common/platform/fileSystem'; -import { - FileType, - IFileSystemPaths, IFileSystemUtils, IRawFileSystem, ITempFileSystem, - TemporaryFile -} from '../../../client/common/platform/types'; -import { sleep } from '../../../client/common/utils/async'; - -const assertArrays = require('chai-arrays'); -use(assertArrays); -use(chaiAsPromised); - -// Note: all functional tests that trigger the VS Code "fs" API are -// found in filesystem.test.ts. - -export const WINDOWS = /^win/.test(process.platform); - -export const DOES_NOT_EXIST = 'this file does not exist'; - -export async function assertDoesNotExist(filename: string) { - await expect( - fsextra.stat(filename) - ).to.eventually.be.rejected; -} - -export async function assertExists(filename: string) { - await expect( - fsextra.stat(filename) - ).to.not.eventually.be.rejected; -} - -export class FSFixture { - public tempDir: tmpMod.SynchrounousResult | undefined; - public sockServer: net.Server | undefined; - - public async cleanUp() { - if (this.tempDir) { - const tempDir = this.tempDir; - this.tempDir = undefined; - try { - tempDir.removeCallback(); - } catch { - // The "unsafeCleanup: true" option is supposed - // to support a non-empty directory, but apparently - // that isn't always the case. (see #8804) - await fsextra.remove(tempDir.name); - } - } - if (this.sockServer) { - const srv = this.sockServer; - await new Promise(resolve => srv.close(resolve)); - this.sockServer = undefined; - } - } - - public async resolve(relname: string, mkdirs = true): Promise { - if (!this.tempDir) { - this.tempDir = tmpMod.dirSync({ - prefix: 'pyvsc-fs-tests-', - unsafeCleanup: true - }); - } - relname = path.normalize(relname); - const filename = path.join(this.tempDir.name, relname); - if (mkdirs) { - await fsextra.mkdirp( - path.dirname(filename)); - } - return filename; - } - - public async createFile(relname: string, text = ''): Promise { - const filename = await this.resolve(relname); - await fsextra.writeFile(filename, text); - return filename; - } - - public async createDirectory(relname: string): Promise { - const dirname = await this.resolve(relname); - await fsextra.mkdir(dirname); - return dirname; - } - - public async createSymlink(relname: string, source: string): Promise { - const symlink = await this.resolve(relname); - await fsextra.ensureSymlink(source, symlink); - return symlink; - } - - public async createSocket(relname: string): Promise { - if (!this.sockServer) { - this.sockServer = net.createServer(); - } - const srv = this.sockServer!; - const filename = await this.resolve(relname); - await new Promise(resolve => srv!.listen(filename, 0, resolve)); - return filename; - } -} - -suite('FileSystem - Temporary files', () => { - let tmp: ITempFileSystem; - setup(() => { - tmp = TempFileSystem.withDefaults(); - }); - - suite('createFile', () => { - test('TemporaryFile is populated properly', async () => { - const tempfile = await tmp.createFile('.tmp'); - await assertExists(tempfile.filePath); - tempfile.dispose(); - - await assertDoesNotExist(tempfile.filePath); - expect(tempfile.filePath.endsWith('.tmp')).to.equal(true, `bad suffix on ${tempfile.filePath}`); - }); - - test('fails if the target temp directory does not exist', async () => { - const promise = tmp.createFile('.tmp', DOES_NOT_EXIST); - - await expect(promise).to.eventually.be.rejected; - }); - }); -}); - -suite('FileSystem paths', () => { - let fspath: IFileSystemPaths; - setup(() => { - fspath = FileSystemPaths.withDefaults(); - }); - - suite('join', () => { - test('parts get joined by path.sep', () => { - const expected = path.join('x', 'y', 'z', 'spam.py'); - - const result = fspath.join( - 'x', - path.sep === '\\' ? 'y\\z' : 'y/z', - 'spam.py' - ); - - expect(result).to.equal(expected); - }); - }); - - suite('normCase', () => { - test('forward-slash', () => { - const filename = 'X/Y/Z/SPAM.PY'; - const expected = WINDOWS ? 'X\\Y\\Z\\SPAM.PY' : filename; - - const result = fspath.normCase(filename); - - expect(result).to.equal(expected); - }); - - test('backslash is not changed', () => { - const filename = 'X\\Y\\Z\\SPAM.PY'; - const expected = filename; - - const result = fspath.normCase(filename); - - expect(result).to.equal(expected); - }); - - test('lower-case', () => { - const filename = 'x\\y\\z\\spam.py'; - const expected = WINDOWS ? 'X\\Y\\Z\\SPAM.PY' : filename; - - const result = fspath.normCase(filename); - - expect(result).to.equal(expected); - }); - - test('upper-case stays upper-case', () => { - const filename = 'X\\Y\\Z\\SPAM.PY'; - const expected = 'X\\Y\\Z\\SPAM.PY'; - - const result = fspath.normCase(filename); - - expect(result).to.equal(expected); - }); - }); -}); - -suite('Raw FileSystem', () => { - let filesystem: IRawFileSystem; - let fix: FSFixture; - setup(() => { - filesystem = RawFileSystem.withDefaults(); - fix = new FSFixture(); - }); - teardown(async () => { - await fix.cleanUp(); - }); - - suite('chmod (non-Windows)', () => { - suiteSetup(function () { - // On Windows, chmod won't have any effect on the file itself. - if (WINDOWS) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - } - }); - async function checkMode(filename: string, expected: number) { - const stat = await fsextra.stat(filename); - expect(stat.mode & 0o777).to.equal(expected); - } - - test('the file mode gets updated (string)', async () => { - const filename = await fix.createFile('spam.py', '...'); - await fsextra.chmod(filename, 0o644); - - await filesystem.chmod(filename, '755'); - - await checkMode(filename, 0o755); - }); - - test('the file mode gets updated (number)', async () => { - const filename = await fix.createFile('spam.py', '...'); - await fsextra.chmod(filename, 0o644); - - await filesystem.chmod(filename, 0o755); - - await checkMode(filename, 0o755); - }); - - test('the file mode gets updated for a directory', async () => { - const dirname = await fix.createDirectory('spam'); - await fsextra.chmod(dirname, 0o755); - - await filesystem.chmod(dirname, 0o700); - - await checkMode(dirname, 0o700); - }); - - test('nothing happens if the file mode already matches', async () => { - const filename = await fix.createFile('spam.py', '...'); - await fsextra.chmod(filename, 0o644); - - await filesystem.chmod(filename, 0o644); - - await checkMode(filename, 0o644); - }); - - test('fails if the file does not exist', async () => { - const promise = filesystem.chmod(DOES_NOT_EXIST, 0o755); - - await expect(promise).to.eventually.be.rejected; - }); - }); - - suite('lstat', () => { - test('for symlinks, gives the link info', async () => { - const filename = await fix.createFile('x/y/z/spam.py', '...'); - const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); - const old = await fsextra.lstat(symlink); - const expected = { - type: FileType.SymbolicLink, - size: old.size, - ctime: old.ctimeMs, - mtime: old.mtimeMs - }; - - const stat = await filesystem.lstat(symlink); - - expect(stat).to.deep.equal(expected); - }); - - test('for normal files, gives the file info', async () => { - const filename = await fix.createFile('x/y/z/spam.py', '...'); - const old = await fsextra.stat(filename); - const expected = { - type: FileType.File, - size: old.size, - ctime: old.ctimeMs, - mtime: old.mtimeMs - }; - - const stat = await filesystem.lstat(filename); - - expect(stat).to.deep.equal(expected); - }); - - test('fails if the file does not exist', async () => { - const promise = filesystem.lstat(DOES_NOT_EXIST); - - await expect(promise).to.eventually.be.rejected; - }); - }); - - suite('statSync', () => { - test('gets the info for an existing file', async () => { - const filename = await fix.createFile('x/y/z/spam.py', '...'); - const old = await fsextra.stat(filename); - const expected = { - type: FileType.File, - size: old.size, - ctime: old.ctimeMs, - mtime: old.mtimeMs - }; - - const stat = filesystem.statSync(filename); - - expect(stat).to.deep.equal(expected); - }); - - test('gets the info for an existing directory', async () => { - const dirname = await fix.createDirectory('x/y/z/spam'); - const old = await fsextra.stat(dirname); - const expected = { - type: FileType.Directory, - size: old.size, - ctime: old.ctimeMs, - mtime: old.mtimeMs - }; - - const stat = filesystem.statSync(dirname); - - expect(stat).to.deep.equal(expected); - }); - - test('for symlinks, gets the info for the linked file', async () => { - const filename = await fix.createFile('x/y/z/spam.py', '...'); - const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); - const old = await fsextra.stat(filename); - const expected = { - type: FileType.File, - size: old.size, - ctime: old.ctimeMs, - mtime: old.mtimeMs - }; - - const stat = filesystem.statSync(symlink); - - expect(stat).to.deep.equal(expected); - }); - - test('fails if the file does not exist', async () => { - expect( - () => filesystem.statSync(DOES_NOT_EXIST) - ).to.throw(Error); - }); - }); - - suite('readTextSync', () => { - test('returns contents of a file', async () => { - const expected = ''; - const filename = await fix.createFile('x/y/z/spam.py', expected); - - const content = filesystem.readTextSync(filename); - - expect(content).to.be.equal(expected); - }); - - test('always UTF-8', async () => { - const expected = '... 😁 ...'; - const filename = await fix.createFile('x/y/z/spam.py', expected); - - const text = filesystem.readTextSync(filename); - - expect(text).to.equal(expected); - }); - - test('throws an exception if file does not exist', async () => { - expect( - () => filesystem.readTextSync(DOES_NOT_EXIST) - ).to.throw(Error); - }); - }); - - suite('createWriteStream', () => { - setup(function() { - // Tests disabled due to CI failures: https://github.com/microsoft/vscode-python/issues/8804 - // tslint:disable-next-line:no-invalid-this - return this.skip(); - }); - - test('returns the correct WriteStream', async () => { - const filename = await fix.resolve('x/y/z/spam.py'); - const expected = fsextra.createWriteStream(filename); - - const stream = filesystem.createWriteStream(filename); - - expect(stream.path).to.deep.equal(expected.path); - }); - - test('creates the file if missing', async () => { - const filename = await fix.resolve('x/y/z/spam.py'); - await assertDoesNotExist(filename); - const data = 'line1\nline2\n'; - - const stream = filesystem.createWriteStream(filename); - stream.write(data); - stream.destroy(); - - const actual = await fsextra.readFile(filename) - .then(buffer => buffer.toString()); - expect(actual).to.equal(data); - }); - - test('always UTF-8', async () => { - const filename = await fix.resolve('x/y/z/spam.py'); - const data = '... 😁 ...'; - - const stream = filesystem.createWriteStream(filename); - stream.write(data); - stream.destroy(); - - const actual = await fsextra.readFile(filename) - .then(buffer => buffer.toString()); - expect(actual).to.equal(data); - }); - - test('overwrites existing file', async () => { - const filename = await fix.createFile('x/y/z/spam.py', '...'); - const data = 'line1\nline2\n'; - - const stream = filesystem.createWriteStream(filename); - stream.write(data); - stream.destroy(); - - const actual = await fsextra.readFile(filename) - .then(buffer => buffer.toString()); - expect(actual).to.equal(data); - }); - }); -}); - -suite('FileSystem Utils', () => { - let utils: IFileSystemUtils; - let fix: FSFixture; - setup(() => { - utils = FileSystemUtils.withDefaults(); - fix = new FSFixture(); - }); - teardown(async () => { - await fix.cleanUp(); - }); - - suite('arePathsSame', () => { - test('identical', () => { - const filename = 'x/y/z/spam.py'; - - const result = utils.arePathsSame(filename, filename); - - expect(result).to.equal(true); - }); - - test('not the same', () => { - const file1 = 'x/y/z/spam.py'; - const file2 = 'a/b/c/spam.py'; - - const result = utils.arePathsSame(file1, file2); - - expect(result).to.equal(false); - }); - - test('with different separators', () => { - const file1 = 'x/y/z/spam.py'; - const file2 = 'x\\y\\z\\spam.py'; - const expected = WINDOWS; - - const result = utils.arePathsSame(file1, file2); - - expect(result).to.equal(expected); - }); - - test('with different case', () => { - const file1 = 'x/y/z/spam.py'; - const file2 = 'x/Y/z/Spam.py'; - const expected = WINDOWS; - - const result = utils.arePathsSame(file1, file2); - - expect(result).to.equal(expected); - }); - }); - - suite('getFileHash', () => { - setup(function() { - // Tests disabled due to CI failures: https://github.com/microsoft/vscode-python/issues/8804 - // tslint:disable-next-line:no-invalid-this - return this.skip(); - }); - - test('Getting hash for a file should return non-empty string', async () => { - const filename = await fix.createFile('x/y/z/spam.py'); - - const hash = await utils.getFileHash(filename); - - expect(hash).to.not.equal(''); - }); - - test('the returned hash is stable', async () => { - const filename = await fix.createFile('x/y/z/spam.py'); - - const hash1 = await utils.getFileHash(filename); - const hash2 = await utils.getFileHash(filename); - const hash3 = await utils.getFileHash(filename); - - expect(hash1).to.equal(hash2); - expect(hash1).to.equal(hash3); - expect(hash2).to.equal(hash3); - }); - - test('the returned hash changes with modification', async () => { - const filename = await fix.createFile('x/y/z/spam.py', 'original text'); - - const hash1 = await utils.getFileHash(filename); - await sleep(10); - await fsextra.writeFile(filename, 'new text'); - const hash2 = await utils.getFileHash(filename); - - expect(hash1).to.not.equal(hash2); - }); - - test('the returned hash is unique', async () => { - const file1 = await fix.createFile('spam.py'); - await sleep(10); // milliseconds - const file2 = await fix.createFile('x/y/z/spam.py'); - await sleep(10); // milliseconds - const file3 = await fix.createFile('eggs.py'); - - const hash1 = await utils.getFileHash(file1); - const hash2 = await utils.getFileHash(file2); - const hash3 = await utils.getFileHash(file3); - - expect(hash1).to.not.equal(hash2); - expect(hash1).to.not.equal(hash3); - expect(hash2).to.not.equal(hash3); - }); - - test('Getting hash for non existent file should throw error', async () => { - const promise = utils.getFileHash(DOES_NOT_EXIST); - - await expect(promise).to.eventually.be.rejected; - }); - }); - - suite('search', () => { - setup(function() { - // Tests disabled due to CI failures: https://github.com/microsoft/vscode-python/issues/8804 - // tslint:disable-next-line:no-invalid-this - return this.skip(); - }); - - test('found matches', async () => { - const pattern = await fix.resolve(`x/y/z/spam.*`); - const expected: string[] = [ - await fix.createFile('x/y/z/spam.py'), - await fix.createFile('x/y/z/spam.pyc'), - await fix.createFile('x/y/z/spam.so'), - await fix.createDirectory('x/y/z/spam.data') - ]; - // non-matches - await fix.createFile('x/spam.py'); - await fix.createFile('x/y/z/eggs.py'); - await fix.createFile('x/y/z/spam-all.py'); - await fix.createFile('x/y/z/spam'); - await fix.createFile('x/spam.py'); - - const files = await utils.search(pattern); - - expect(files.sort()).to.deep.equal(expected.sort()); - }); - - test('no matches', async () => { - const pattern = await fix.resolve(`x/y/z/spam.*`); - - const files = await utils.search(pattern); - - expect(files).to.deep.equal([]); - }); - }); -}); - -suite('FileSystem - legacy aliases', () => { - const fileToAppendTo = path.join(__dirname, 'created_for_testing_dummy.txt'); - setup(() => { - cleanTestFiles(); - }); - teardown(cleanTestFiles); - function cleanTestFiles() { - if (fsextra.existsSync(fileToAppendTo)) { - fsextra.unlinkSync(fileToAppendTo); - } - } - - suite('Case sensitivity', () => { - const path1 = 'c:\\users\\Peter Smith\\my documents\\test.txt'; - const path2 = 'c:\\USERS\\Peter Smith\\my documents\\test.TXT'; - const path3 = 'c:\\USERS\\Peter Smith\\my documents\\test.exe'; - let filesystem: FileSystem; - setup(() => { - filesystem = new FileSystem(); - }); - - test('windows', function() { - if (!WINDOWS) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - } - - const same12 = filesystem.arePathsSame(path1, path2); - const same11 = filesystem.arePathsSame(path1, path1); - const same22 = filesystem.arePathsSame(path2, path2); - const same13 = filesystem.arePathsSame(path1, path3); - - expect(same12).to.be.equal(true, 'file paths do not match (windows)'); - expect(same11).to.be.equal(true, '1. file paths do not match'); - expect(same22).to.be.equal(true, '2. file paths do not match'); - expect(same13).to.be.equal(false, '2. file paths do not match'); - }); - - test('non-windows', function() { - if (WINDOWS) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - } - - const same12 = filesystem.arePathsSame(path1, path2); - const same11 = filesystem.arePathsSame(path1, path1); - const same22 = filesystem.arePathsSame(path2, path2); - const same13 = filesystem.arePathsSame(path1, path3); - - expect(same12).to.be.equal(false, 'file match (non windows)'); - expect(same11).to.be.equal(true, '1. file paths do not match'); - expect(same22).to.be.equal(true, '2. file paths do not match'); - expect(same13).to.be.equal(false, '2. file paths do not match'); - }); - }); - - test('Check existence of files synchronously', async () => { - const filesystem = new FileSystem(); - - expect(filesystem.fileExistsSync(__filename)).to.be.equal(true, 'file not found'); - }); - - test('Test searching for files', async () => { - const searchPattern = `${path.basename(__filename, __filename.substring(__filename.length - 3))}.*`; - const filesystem = new FileSystem(); - - const files = await filesystem.search(path.join(__dirname, searchPattern)); - - expect(files).to.be.array(); - expect(files.length).to.be.at.least(1); - const expectedFileName = __filename.replace(/\\/g, '/'); - const fileName = files[0].replace(/\\/g, '/'); - expect(fileName).to.equal(expectedFileName); - }); - - test('Ensure creating a temporary file results in a unique temp file path', async () => { - const filesystem = new FileSystem(); - - const tempFile = await filesystem.createTemporaryFile('.tmp'); - const tempFile2 = await filesystem.createTemporaryFile('.tmp'); - - tempFile.dispose(); - tempFile2.dispose(); - expect(tempFile.filePath).to.not.equal(tempFile2.filePath, 'Temp files must be unique, implementation of createTemporaryFile is off.'); - }); - - test('Ensure writing to a temp file is supported via file stream', async () => { - const filesystem = new FileSystem(); - - await filesystem.createTemporaryFile('.tmp').then((tf: TemporaryFile) => { - expect(tf).to.not.equal(undefined, 'Error trying to create a temporary file'); - const writeStream = filesystem.createWriteStream(tf.filePath); - writeStream.write('hello', 'utf8', (err: Error | null | undefined) => { - expect(err).to.equal(undefined, `Failed to write to a temp file, error is ${err}`); - }); - }, (failReason) => { - expect(failReason).to.equal('No errors occurred', `Failed to create a temporary file with error ${failReason}`); - }); - }); - - test('Ensure chmod works against a temporary file', async () => { - const filesystem = new FileSystem(); - - await filesystem.createTemporaryFile('.tmp').then(async (fl: TemporaryFile) => { - await filesystem.chmod(fl.filePath, '7777').then( - (_success: void) => { - // cannot check for success other than we got here, chmod in Windows won't have any effect on the file itself. - }, - (failReason) => { - expect(failReason).to.equal('There was no error using chmod', `Failed to perform chmod operation successfully, got error ${failReason}`); - }); - }); - }); - - test('Getting hash for non existent file should throw error', async () => { - const filesystem = new FileSystem(); - - const promise = filesystem.getFileHash('some unknown file'); - - await expect(promise).to.eventually.be.rejected; - }); - - test('Getting hash for a file should return non-empty string', async () => { - const filesystem = new FileSystem(); - - const hash = await filesystem.getFileHash(__filename); - - expect(hash).to.be.length.greaterThan(0); - }); - - suite('createTemporaryFile', () => { - test('TemporaryFile is populated properly', async () => { - const filesystem = new FileSystem(); - - const tempfile = await filesystem.createTemporaryFile('.tmp'); - - await assertExists(tempfile.filePath); - tempfile.dispose(); - expect(tempfile.filePath.endsWith('.tmp')).to.equal(true, tempfile.filePath); - }); - - test('Ensure creating a temporary file results in a unique temp file path', async () => { - const filesystem = new FileSystem(); - - const tempfile1 = await filesystem.createTemporaryFile('.tmp'); - const tempfile2 = await filesystem.createTemporaryFile('.tmp'); - - tempfile1.dispose(); - tempfile2.dispose(); - expect(tempfile1.filePath).to.not.equal(tempfile2.filePath); - }); - - test('Ensure writing to a temp file is supported via file stream', async () => { - const filesystem = new FileSystem(); - const tempfile = await filesystem.createTemporaryFile('.tmp'); - const stream = filesystem.createWriteStream(tempfile.filePath); - const data = '...'; - - stream.write(data, 'utf8'); - - const actual = await fsextra.readFile(tempfile.filePath, 'utf8'); - expect(actual).to.equal(data); - }); - - test('Ensure chmod works against a temporary file', async () => { - const filesystem = new FileSystem(); - - const tempfile = await filesystem.createTemporaryFile('.tmp'); - - await expect( - fsextra.chmod(tempfile.filePath, '7777') - ).to.not.eventually.be.rejected; - }); - }); -}); diff --git a/src/test/common/platform/filesystem.test.ts b/src/test/common/platform/filesystem.test.ts deleted file mode 100644 index 83f0f54a6cf9..000000000000 --- a/src/test/common/platform/filesystem.test.ts +++ /dev/null @@ -1,568 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { expect } from 'chai'; -import * as fsextra from 'fs-extra'; -import * as path from 'path'; -import { - FileSystem, FileSystemUtils, RawFileSystem -} from '../../../client/common/platform/fileSystem'; -import { - FileStat, FileType, - IFileSystemUtils, IRawFileSystem -} from '../../../client/common/platform/types'; -import { - assertDoesNotExist, assertExists, DOES_NOT_EXIST, FSFixture, WINDOWS -} from './filesystem.functional.test'; - -// Note: all functional tests that do not trigger the VS Code "fs" API -// are found in filesystem.functional.test.ts. - -// tslint:disable:max-func-body-length chai-vague-errors -// tslint:disable:no-suspicious-comment - -suite('Raw FileSystem', () => { - let filesystem: IRawFileSystem; - let fix: FSFixture; - setup(async () => { - filesystem = RawFileSystem.withDefaults(); - fix = new FSFixture(); - - await assertDoesNotExist(DOES_NOT_EXIST); - }); - teardown(async () => { - await fix.cleanUp(); - }); - - suite('readText', () => { - test('returns contents of a file', async () => { - const expected = ''; - const filename = await fix.createFile('x/y/z/spam.py', expected); - - const content = await filesystem.readText(filename); - - expect(content).to.be.equal(expected); - }); - - test('always UTF-8', async () => { - const expected = '... 😁 ...'; - const filename = await fix.createFile('x/y/z/spam.py', expected); - - const text = await filesystem.readText(filename); - - expect(text).to.equal(expected); - }); - - test('returns garbage if encoding is UCS-2', async () => { - const filename = await fix.resolve('spam.py'); - // There are probably cases where this would fail too. - // However, the extension never has to deal with non-UTF8 - // cases, so it doesn't matter too much. - const original = '... 😁 ...'; - await fsextra.writeFile(filename, original, { encoding: 'ucs2' }); - - const text = await filesystem.readText(filename); - - expect(text).to.equal('.\u0000.\u0000.\u0000 \u0000=�\u0001� \u0000.\u0000.\u0000.\u0000'); - }); - - test('throws an exception if file does not exist', async () => { - const promise = filesystem.readText(DOES_NOT_EXIST); - - await expect(promise).to.eventually.be.rejected; - }); - }); - - suite('writeText', () => { - test('creates the file if missing', async () => { - const filename = await fix.resolve('x/y/z/spam.py'); - await assertDoesNotExist(filename); - const data = 'line1\nline2\n'; - - await filesystem.writeText(filename, data); - - const actual = await fsextra.readFile(filename) - .then(buffer => buffer.toString()); - expect(actual).to.equal(data); - }); - - test('always UTF-8', async () => { - const filename = await fix.resolve('x/y/z/spam.py'); - const data = '... 😁 ...'; - - await filesystem.writeText(filename, data); - - const actual = await fsextra.readFile(filename) - .then(buffer => buffer.toString()); - expect(actual).to.equal(data); - }); - - test('overwrites existing file', async () => { - const filename = await fix.createFile('x/y/z/spam.py', '...'); - const data = 'line1\nline2\n'; - - await filesystem.writeText(filename, data); - - const actual = await fsextra.readFile(filename) - .then(buffer => buffer.toString()); - expect(actual).to.equal(data); - }); - }); - - suite('rmtree', () => { - test('deletes the directory and everything in it', async () => { - const dirname = await fix.createDirectory('x'); - const filename = await fix.createFile('x/y/z/spam.py'); - await assertExists(filename); - - await filesystem.rmtree(dirname); - - await assertDoesNotExist(dirname); - }); - - test('fails if the directory does not exist', async () => { - const promise = filesystem.rmtree(DOES_NOT_EXIST); - - await expect(promise).to.eventually.be.rejected; - }); - }); - - suite('rmfile', () => { - test('deletes the file', async () => { - const filename = await fix.createFile('x/y/z/spam.py', '...'); - await assertExists(filename); - - await filesystem.rmfile(filename); - - await assertDoesNotExist(filename); - }); - - test('fails if the file does not exist', async () => { - const promise = filesystem.rmfile(DOES_NOT_EXIST); - - await expect(promise).to.eventually.be.rejected; - }); - }); - - suite('stat', () => { - function convertStat(old: fsextra.Stats, filetype: FileType): FileStat { - return { - type: filetype, - size: old.size, - // TODO (https://github.com/microsoft/vscode/issues/84177) - // FileStat.ctime and FileStat.mtime only have 1-second resolution. - // So for now we round to the nearest integer. - // TODO (https://github.com/microsoft/vscode/issues/84177) - // FileStat.ctime is consistently 0 instead of the actual ctime. - ctime: 0, - //ctime: Math.round(old.ctimeMs), - mtime: Math.round(old.mtimeMs) - }; - } - - test('gets the info for an existing file', async () => { - const filename = await fix.createFile('x/y/z/spam.py', '...'); - const old = await fsextra.stat(filename); - const expected = convertStat(old, FileType.File); - - const stat = await filesystem.stat(filename); - - expect(stat).to.deep.equal(expected); - }); - - test('gets the info for an existing directory', async () => { - const dirname = await fix.createDirectory('x/y/z/spam'); - const old = await fsextra.stat(dirname); - const expected = convertStat(old, FileType.Directory); - - const stat = await filesystem.stat(dirname); - - expect(stat).to.deep.equal(expected); - }); - - test('for symlinks, gets the info for the linked file', async () => { - const filename = await fix.createFile('x/y/z/spam.py', '...'); - const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); - const old = await fsextra.stat(filename); - const expected = convertStat(old, FileType.SymbolicLink | FileType.File); - - const stat = await filesystem.stat(symlink); - - expect(stat).to.deep.equal(expected); - }); - - test('fails if the file does not exist', async () => { - const promise = filesystem.stat(DOES_NOT_EXIST); - - await expect(promise).to.eventually.be.rejected; - }); - }); - - suite('listdir', () => { - test('mixed', async () => { - // Create the target directory and its contents. - const dirname = await fix.createDirectory('x/y/z'); - await fix.createFile('x/y/z/__init__.py', ''); - const script = await fix.createFile('x/y/z/__main__.py', '