From 2554968845430480f274a07d49402dc4cd1ad267 Mon Sep 17 00:00:00 2001 From: digeff Date: Wed, 9 Sep 2020 16:03:36 -0700 Subject: [PATCH] Add support for remote file system operations --- src/binder.ts | 10 +-- src/build/dapCustom.ts | 51 +++++++++++++ src/build/generate-contributions.ts | 1 + src/common/fsUtils.ts | 72 +++++++++++++++++-- .../sourceMaps/sourceMapResolutionUtils.ts | 11 +-- src/common/sourceUtils.ts | 6 +- src/common/urlUtils.ts | 6 +- src/configuration.ts | 6 ++ src/dap/api.d.ts | 28 ++++++++ src/dap/telemetryClassification.d.ts | 2 + src/extension.ts | 3 + src/ioc-extras.ts | 5 ++ src/ioc.ts | 12 +++- src/targets/browser/browserPathResolver.ts | 14 ++-- src/targets/node/autoAttachLauncher.ts | 6 +- src/targets/node/nodeAttacher.ts | 13 ++-- src/targets/node/nodeLauncher.ts | 12 ++-- src/targets/node/nodeLauncherBase.ts | 4 ++ src/targets/node/nodeSourcePathResolver.ts | 12 +++- src/targets/node/nvmResolver.ts | 12 ++-- src/targets/node/terminalNodeLauncher.ts | 18 ++--- src/targets/sourcePathResolverFactory.ts | 6 +- src/test/browser/browserPathResolverTest.ts | 22 +++++- .../nodeConfigurationProvider.test.ts | 8 ++- src/test/goldenText.ts | 16 ++++- .../node/node-source-path-resolver.test.ts | 13 +++- src/test/node/process-tree.test.ts | 6 +- src/test/node/runtimeVersion.test.ts | 22 ++++-- src/test/profiling/profiling.test.ts | 3 +- src/test/sources/pretty-print.test.ts | 2 +- src/test/testRunner.ts | 6 +- src/test/ui/pickAttach.test.ts | 17 +++-- src/ui/autoAttach.ts | 10 ++- .../nodeDebugConfigurationResolver.ts | 15 ++-- src/ui/debugTerminalUI.ts | 13 +++- src/ui/processPicker.ts | 17 +++-- src/ui/processTree/darwinProcessTree.ts | 8 ++- src/ui/processTree/processTree.ts | 8 ++- 38 files changed, 398 insertions(+), 98 deletions(-) diff --git a/src/binder.ts b/src/binder.ts index 72f38c1e5..71586cd24 100644 --- a/src/binder.ts +++ b/src/binder.ts @@ -121,15 +121,15 @@ export class Binder implements IDisposable { dap, ), ); - dap.on('launch', params => - this._boot( + dap.on('launch', params => { + return this._boot( applyDefaults( params as AnyResolvingConfiguration, this._rootServices.get(ExtensionLocation), ), dap, - ), - ); + ); + }); dap.on('terminate', async () => { await this._disconnect(); return {}; @@ -208,7 +208,7 @@ export class Binder implements IDisposable { private async _boot(params: AnyLaunchConfiguration, dap: Dap.Api) { warnNightly(dap); this.reportBootTelemetry(params); - provideLaunchParams(this._rootServices, params); + provideLaunchParams(this._rootServices, params, dap); this._rootServices.get(ILogger).setup(resolveLoggerOptions(dap, params.trace)); const cts = diff --git a/src/build/dapCustom.ts b/src/build/dapCustom.ts index e619a78c7..ceab79b7a 100644 --- a/src/build/dapCustom.ts +++ b/src/build/dapCustom.ts @@ -668,6 +668,57 @@ const dapCustom: JSONSchema4 = { ], }, + RemoteFileExistsRequest: { + allOf: [ + { $ref: '#/definitions/Request' }, + { + type: 'object', + description: 'Check if file exists on remote file system, used in VS.', + properties: { + command: { + type: 'string', + enum: ['remoteFileExists'], + }, + arguments: { + $ref: '#/definitions/RemoteFileExistsArguments', + }, + }, + required: ['command', 'arguments'], + }, + ], + }, + RemoteFileExistsArguments: { + type: 'object', + description: "Arguments for 'RemoteFileExists' request.", + properties: { + localFilePath: { + type: 'string', + }, + }, + }, + RemoteFileExistsResponse: { + allOf: [ + { $ref: '#/definitions/Response' }, + { + type: 'object', + description: "Response to 'LaunchUnelevated' request.", + required: ['body'], + properties: { + body: { + type: 'object', + required: ['doesExists'], + properties: { + doesExists: { + type: 'boolean', + description: 'Does the file exist on the remote file system.', + }, + }, + }, + }, + }, + ], + }, + GetBreakpointsRequest: { allOf: [ { $ref: '#/definitions/Request' }, diff --git a/src/build/generate-contributions.ts b/src/build/generate-contributions.ts index 059f1ef4a..b12614979 100644 --- a/src/build/generate-contributions.ts +++ b/src/build/generate-contributions.ts @@ -51,6 +51,7 @@ type OmittedKeysFromAttributes = | '__workspaceFolder' | '__workspaceCachePath' | '__autoExpandGetters' + | '__remoteFilePrefix' | '__sessionId'; type DescribedAttribute = JSONSchema6 & diff --git a/src/common/fsUtils.ts b/src/common/fsUtils.ts index e7d521598..c03dc7b42 100644 --- a/src/common/fsUtils.ts +++ b/src/common/fsUtils.ts @@ -5,6 +5,7 @@ import * as fs from 'fs'; import * as util from 'util'; import { FsPromises } from '../ioc-extras'; +import Dap from '../dap/api'; export const fsModule = fs; @@ -106,8 +107,71 @@ export function readFileRaw(path: string): Promise { return fs.promises.readFile(path).catch(() => Buffer.alloc(0)); } -export function exists(path: string): Promise { - return new Promise(cb => { - fs.exists(path, cb); - }); +export interface IFsUtils { + exists(path: string): Promise; +} + +export class LocalFsUtils implements IFsUtils { + public constructor(private readonly fs: FsPromises) {} + + public async exists(path: string): Promise { + // Check if the file exists in the current directory. + try { + await this.fs.access(path, fs.constants.F_OK); + return true; + } catch { + return false; + } + } +} + +export class RemoteFsThroughDapUtils implements IFsUtils { + public constructor(private readonly dap: Dap.Api) {} + + public async exists(path: string): Promise { + try { + // Custom request + const { doesExists } = await this.dap.remoteFileExistsRequest({ + localFilePath: path, + }); + return doesExists; + } catch { + return false; + } + } +} + +/** + * Notes: remoteFilePrefix = '' // will do all fs operations thorugh DAP requests + * remoteFilePrefix = undefined // will do all operations thorugh Local Node.fs + */ +export class LocalAndRemoteFsUtils implements IFsUtils { + private constructor( + private readonly remoteFilePrefix: string, + private readonly localFsUtils: IFsUtils, + private readonly remoteFsUtils: IFsUtils, + ) {} + + public static create( + remoteFilePrefix: string | undefined, + fsPromises: FsPromises, + dap: Dap.Api, + ): IFsUtils { + const localFsUtils = new LocalFsUtils(fsPromises); + if (remoteFilePrefix !== undefined) { + return new this(remoteFilePrefix, localFsUtils, new RemoteFsThroughDapUtils(dap)); + } else { + return localFsUtils; + } + } + + public async exists(path: string): Promise { + return (this.shouldUseRemoteFileSystem(path) ? this.remoteFsUtils : this.localFsUtils).exists( + path, + ); + } + + public shouldUseRemoteFileSystem(path: string) { + return path.startsWith(this.remoteFilePrefix); + } } diff --git a/src/common/sourceMaps/sourceMapResolutionUtils.ts b/src/common/sourceMaps/sourceMapResolutionUtils.ts index 54764676f..a76551ffa 100644 --- a/src/common/sourceMaps/sourceMapResolutionUtils.ts +++ b/src/common/sourceMaps/sourceMapResolutionUtils.ts @@ -10,6 +10,7 @@ import { PathMapping } from '../../configuration'; import { ILogger, LogTag } from '../logging'; import { filterObject } from '../objUtils'; import { fixDriveLetterAndSlashes, properJoin, properResolve } from '../pathUtils'; +import { LocalFsUtils } from '../fsUtils'; export function getFullSourceEntry(sourceRoot: string | undefined, sourcePath: string): string { if (!sourceRoot) { @@ -135,11 +136,10 @@ export const defaultPathMappingResolver: PathMappingResolver = async ( * A path mapping resolver that resolves to the nearest folder containing * a package.json if there's no more precise match in the mapping. */ -export const moduleAwarePathMappingResolver = (compiledPath: string): PathMappingResolver => async ( - sourceRoot, - pathMapping, - logger, -) => { +export const moduleAwarePathMappingResolver = ( + fsUtils: LocalFsUtils, + compiledPath: string, +): PathMappingResolver => async (sourceRoot, pathMapping, logger) => { // 1. Handle cases where we know the path is already absolute on disk. if (process.platform === 'win32' && /^[a-z]:/i.test(sourceRoot)) { return sourceRoot; @@ -147,6 +147,7 @@ export const moduleAwarePathMappingResolver = (compiledPath: string): PathMappin // 2. It's a unix-style path. Get the root of this package containing the compiled file. const implicit = await utils.nearestDirectoryContaining( + fsUtils, path.dirname(compiledPath), 'package.json', ); diff --git a/src/common/sourceUtils.ts b/src/common/sourceUtils.ts index 066f2496e..87ad3de98 100644 --- a/src/common/sourceUtils.ts +++ b/src/common/sourceUtils.ts @@ -6,10 +6,10 @@ import { Parser } from 'acorn'; import { generate } from 'astring'; import { NullablePosition, Position, SourceMapConsumer, SourceMapGenerator } from 'source-map'; import * as ts from 'typescript'; +import { SourceMap } from './sourceMaps/sourceMap'; import { LineColumn } from '../adapter/breakpoints/breakpointBase'; -import * as fsUtils from './fsUtils'; import { Hasher } from './hash'; -import { SourceMap } from './sourceMaps/sourceMap'; +import * as fs from 'fs'; export async function prettyPrintAsSourceMap( fileName: string, @@ -241,7 +241,7 @@ export async function checkContentHash( } if (!contentHash) { - const exists = await fsUtils.exists(absolutePath); + const exists = fs.existsSync(absolutePath); return exists ? absolutePath : undefined; } diff --git a/src/common/urlUtils.ts b/src/common/urlUtils.ts index 3b0da0265..a206aa2ac 100644 --- a/src/common/urlUtils.ts +++ b/src/common/urlUtils.ts @@ -9,10 +9,10 @@ import Cdp from '../cdp/api'; import { AnyChromiumConfiguration } from '../configuration'; import { BrowserTargetType } from '../targets/browser/browserTargets'; import { MapUsingProjection } from './datastructure/mapUsingProjection'; -import { exists } from './fsUtils'; import { memoize } from './objUtils'; import { fixDriveLetterAndSlashes, forceForwardSlashes } from './pathUtils'; import { escapeRegexSpecialChars, isRegexSpecialChar } from './stringUtils'; +import { LocalFsUtils } from './fsUtils'; let isCaseSensitive = process.platform !== 'win32'; @@ -81,8 +81,8 @@ export const nearestDirectoryWhere = async ( /** * Returns the closest parent directory that contains a file with the given name. */ -export const nearestDirectoryContaining = (rootDir: string, file: string) => - nearestDirectoryWhere(rootDir, p => exists(path.join(p, file))); +export const nearestDirectoryContaining = (fsUtils: LocalFsUtils, rootDir: string, file: string) => + nearestDirectoryWhere(rootDir, p => fsUtils.exists(path.join(p, file))); // todo: not super correct, and most node libraries don't handle this accurately const knownLoopbacks = new Set(['localhost', '127.0.0.1', '::1']); diff --git a/src/configuration.ts b/src/configuration.ts index a1d1862b0..d2ae2ec3d 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -186,6 +186,11 @@ export interface IBaseConfiguration extends IMandatedConfiguration { */ __autoExpandGetters: boolean; + /** + * If a file starts with this prefix, we'll consider it a remote file, and perform it's operation thorugh DAP requests + */ + __remoteFilePrefix: string | undefined; + /** * Whether to stop if a conditional breakpoint throws an error. */ @@ -778,6 +783,7 @@ export const baseDefaults: IBaseConfiguration = { // Should always be determined upstream; __workspaceFolder: '', __autoExpandGetters: false, + __remoteFilePrefix: undefined, __breakOnConditionalError: false, customDescriptionGenerator: undefined, }; diff --git a/src/dap/api.d.ts b/src/dap/api.d.ts index aca1a1c74..4a40884a8 100644 --- a/src/dap/api.d.ts +++ b/src/dap/api.d.ts @@ -923,6 +923,18 @@ export namespace Dap { */ launchUnelevatedRequest(params: LaunchUnelevatedParams): Promise; + /** + * Check if file exists on remote file system, used in VS. + */ + on( + request: 'remoteFileExists', + handler: (params: RemoteFileExistsParams) => Promise, + ): () => void; + /** + * Check if file exists on remote file system, used in VS. + */ + remoteFileExistsRequest(params: RemoteFileExistsParams): Promise; + /** * Gets all defined breakpoints. */ @@ -1588,6 +1600,11 @@ export namespace Dap { */ launchUnelevated(params: LaunchUnelevatedParams): Promise; + /** + * Check if file exists on remote file system, used in VS. + */ + remoteFileExists(params: RemoteFileExistsParams): Promise; + /** * Gets all defined breakpoints. */ @@ -2648,6 +2665,17 @@ export namespace Dap { data?: string; } + export interface RemoteFileExistsParams { + localFilePath?: string; + } + + export interface RemoteFileExistsResult { + /** + * Does the file exist on the remote file system. + */ + doesExists: boolean; + } + export interface RestartFrameParams { /** * Restart this stackframe. diff --git a/src/dap/telemetryClassification.d.ts b/src/dap/telemetryClassification.d.ts index 88eeeef64..0c7bc4d0a 100644 --- a/src/dap/telemetryClassification.d.ts +++ b/src/dap/telemetryClassification.d.ts @@ -108,6 +108,8 @@ interface IDAPOperationClassification { '!launchvscode.errors': { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth' }; launchunelevated: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; '!launchunelevated.errors': { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth' }; + remotefileexists: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; + '!remotefileexists.errors': { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth' }; getbreakpoints: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; '!getbreakpoints.errors': { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth' }; revealpage: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; diff --git a/src/extension.ts b/src/extension.ts index 0f0f2468d..f7a6fd92d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -27,6 +27,8 @@ import { registerRevealPage } from './ui/revealPage'; import { TerminalLinkHandler } from './ui/terminalLinkHandler'; import { toggleSkippingFile } from './ui/toggleSkippingFile'; import { VSCodeSessionManager } from './ui/vsCodeSessionManager'; +import { LocalFsUtils } from './common/fsUtils'; +import { FSUtils } from './ioc-extras'; export function activate(context: vscode.ExtensionContext) { const services = createGlobalContainer({ @@ -93,6 +95,7 @@ export function activate(context: vscode.ExtensionContext) { context, services.get(DelegateLauncherFactory), services.get(TerminalLinkHandler), + services.get(FSUtils), ); registerNpmScriptLens(context); registerProfilingCommand(context, services); diff --git a/src/ioc-extras.ts b/src/ioc-extras.ts index 10c09d918..4a11b8779 100644 --- a/src/ioc-extras.ts +++ b/src/ioc-extras.ts @@ -53,6 +53,11 @@ export const Execa = Symbol('execa'); */ export const FS = Symbol('FS'); +/** + * Injection for the `FsUtils` module. + */ +export const FSUtils = Symbol('FsUtils'); + /** * Location the extension is running. */ diff --git a/src/ioc.ts b/src/ioc.ts index 32486fe44..40c4b776e 100644 --- a/src/ioc.ts +++ b/src/ioc.ts @@ -65,6 +65,7 @@ import { StoragePath, trackDispose, VSCodeApi, + FSUtils, } from './ioc-extras'; import { BrowserAttacher } from './targets/browser/browserAttacher'; import { ChromeLauncher } from './targets/browser/chromeLauncher'; @@ -90,6 +91,7 @@ import { ILauncher, ITarget } from './targets/targets'; import { DapTelemetryReporter } from './telemetry/dapTelemetryReporter'; import { NullTelemetryReporter } from './telemetry/nullTelemetryReporter'; import { ITelemetryReporter } from './telemetry/telemetryReporter'; +import { LocalAndRemoteFsUtils } from './common/fsUtils'; /** * Contains IOC container factories for the extension. We use Inverisfy, which @@ -303,7 +305,11 @@ export const createGlobalContainer = (options: { return container; }; -export const provideLaunchParams = (container: Container, params: AnyLaunchConfiguration) => { +export const provideLaunchParams = ( + container: Container, + params: AnyLaunchConfiguration, + dap: Dap.Api, +) => { container.bind(AnyLaunchConfiguration).toConstantValue(params); container.bind(SourcePathResolverFactory).toSelf().inSingletonScope(); @@ -312,4 +318,8 @@ export const provideLaunchParams = (container: Container, params: AnyLaunchConfi .bind(ISourcePathResolver) .toDynamicValue(ctx => ctx.container.get(SourcePathResolverFactory).create(params)) .inSingletonScope(); + + container + .bind(FSUtils) + .toConstantValue(LocalAndRemoteFsUtils.create(params.__remoteFilePrefix, fsPromises, dap)); }; diff --git a/src/targets/browser/browserPathResolver.ts b/src/targets/browser/browserPathResolver.ts index 162f4a3eb..412c482bc 100644 --- a/src/targets/browser/browserPathResolver.ts +++ b/src/targets/browser/browserPathResolver.ts @@ -4,7 +4,6 @@ import * as path from 'path'; import * as utils from '../../common/urlUtils'; -import * as fsUtils from '../../common/fsUtils'; import { ISourcePathResolverOptions, SourcePathResolverBase } from '../sourcePathResolver'; import { IUrlResolution } from '../../common/sourcePathResolver'; import { @@ -24,6 +23,8 @@ import { import { injectable, inject } from 'inversify'; import { IVueFileMapper, VueHandling } from '../../adapter/vueFileMapper'; import { ILogger } from '../../common/logging'; +import { IFsUtils } from '../../common/fsUtils'; +import { FSUtils } from '../../ioc-extras'; interface IOptions extends ISourcePathResolverOptions { baseUrl?: string; @@ -35,6 +36,7 @@ interface IOptions extends ISourcePathResolverOptions { export class BrowserSourcePathResolver extends SourcePathResolverBase { constructor( @inject(IVueFileMapper) private readonly vueMapper: IVueFileMapper, + @inject(FSUtils) private readonly fsUtils: IFsUtils, options: IOptions, logger: ILogger, ) { @@ -87,12 +89,12 @@ export class BrowserSourcePathResolver extends SourcePathResolverBase // to a location on disk. if (utils.isFileUrl(url)) { const abs = utils.fileUrlToAbsolutePath(url); - if (await fsUtils.exists(abs)) { + if (await this.fsUtils.exists(abs)) { return abs; } const net = utils.fileUrlToNetworkPath(url); - if (await fsUtils.exists(net)) { + if (await this.fsUtils.exists(net)) { return net; } } @@ -123,7 +125,7 @@ export class BrowserSourcePathResolver extends SourcePathResolverBase this.options.pathMapping, this.logger, ); - if (clientPath && (await fsUtils.exists(clientPath))) { + if (clientPath && (await this.fsUtils.exists(clientPath))) { return clientPath; } @@ -164,8 +166,8 @@ export class BrowserSourcePathResolver extends SourcePathResolverBase if ( this.options.clientID === 'visualstudio' && fullSourceEntry.startsWith('webpack:///') && - !(await fsUtils.exists(mappedFullSourceEntry)) && - (await fsUtils.exists(clientAppPath)) + !(await this.fsUtils.exists(mappedFullSourceEntry)) && + (await this.fsUtils.exists(clientAppPath)) ) { return clientAppPath; } else { diff --git a/src/targets/node/autoAttachLauncher.ts b/src/targets/node/autoAttachLauncher.ts index a13b350d9..65c66a128 100644 --- a/src/targets/node/autoAttachLauncher.ts +++ b/src/targets/node/autoAttachLauncher.ts @@ -16,7 +16,7 @@ import { forceForwardSlashes } from '../../common/pathUtils'; import { AnyLaunchConfiguration, ITerminalLaunchConfiguration } from '../../configuration'; import { ErrorCodes } from '../../dap/errors'; import { ProtocolError } from '../../dap/protocolError'; -import { ExtensionContext, FS, FsPromises } from '../../ioc-extras'; +import { ExtensionContext, FS, FsPromises, FSUtils } from '../../ioc-extras'; import { ITarget } from '../targets'; import { BootloaderEnvironment, @@ -35,6 +35,7 @@ import { IProcessTelemetry, IRunData, NodeLauncherBase } from './nodeLauncherBas import { StubProgram } from './program'; import { ITerminalLauncherLike } from './terminalNodeLauncher'; import { WatchDog } from './watchdogSpawn'; +import { LocalFsUtils } from '../../common/fsUtils'; /** * A special launcher whose launchProgram is a no-op. Used in attach attachment @@ -50,8 +51,9 @@ export class AutoAttachLauncher extends NodeLauncherBase { constructor( - pathProvider: NodeBinaryProvider, - logger: ILogger, + @inject(FSUtils) fsUtils: LocalFsUtils, + @inject(INodeBinaryProvider) pathProvider: NodeBinaryProvider, + @inject(ILogger) logger: ILogger, private readonly restarters = new RestartPolicyFactory(), ) { - super(pathProvider, logger); + super(pathProvider, logger, fsUtils); } /** diff --git a/src/targets/node/nodeLauncher.ts b/src/targets/node/nodeLauncher.ts index 3fb4c6b74..add00ed57 100644 --- a/src/targets/node/nodeLauncher.ts +++ b/src/targets/node/nodeLauncher.ts @@ -7,7 +7,7 @@ import { basename, extname, resolve } from 'path'; import { IBreakpointsPredictor } from '../../adapter/breakpointPredictor'; import Cdp from '../../cdp/api'; import { DebugType } from '../../common/contributionUtils'; -import { exists, readfile } from '../../common/fsUtils'; +import { readfile, LocalFsUtils } from '../../common/fsUtils'; import { ILogger, LogTag } from '../../common/logging'; import { fixDriveLetterAndSlashes } from '../../common/pathUtils'; import { delay } from '../../common/promiseUtil'; @@ -28,13 +28,14 @@ import { IProgramLauncher } from './processLauncher'; import { CombinedProgram, WatchDogProgram } from './program'; import { IRestartPolicy, RestartPolicyFactory } from './restartPolicy'; import { WatchDog } from './watchdogSpawn'; +import { FSUtils } from '../../ioc-extras'; /** * Tries to get the "program" entrypoint from the config. It a program * is explicitly provided, it grabs that, otherwise it looks for the first * existent path within the launch arguments. */ -const tryGetProgramFromArgs = async (config: INodeLaunchConfiguration) => { +const tryGetProgramFromArgs = async (fsUtils: LocalFsUtils, config: INodeLaunchConfiguration) => { if (typeof config.stopOnEntry === 'string') { return resolve(config.cwd, config.stopOnEntry); } @@ -50,7 +51,7 @@ const tryGetProgramFromArgs = async (config: INodeLaunchConfiguration) => { } const candidate = resolve(config.cwd, arg); - if (await exists(candidate)) { + if (await fsUtils.exists(candidate)) { return candidate; } } @@ -68,8 +69,9 @@ export class NodeLauncher extends NodeLauncherBase { @inject(IBreakpointsPredictor) private readonly bpPredictor: IBreakpointsPredictor, @multiInject(IProgramLauncher) private readonly launchers: ReadonlyArray, @inject(RestartPolicyFactory) private readonly restarters: RestartPolicyFactory, + @inject(FSUtils) fsUtils: LocalFsUtils, ) { - super(pathProvider, logger); + super(pathProvider, logger, fsUtils); } /** @@ -257,7 +259,7 @@ export class NodeLauncher extends NodeLauncherBase { // the get-go, but in our scenario the bootloader is the first thing // which is run and something we don't want to break in. We just // do our best to find the entrypoint from the run params. - const program = await tryGetProgramFromArgs(run.params); + const program = await tryGetProgramFromArgs(this.fsUtils, run.params); if (!program) { this.logger.warn(LogTag.Runtime, 'Could not resolve program entrypointfrom args'); return; diff --git a/src/targets/node/nodeLauncherBase.ts b/src/targets/node/nodeLauncherBase.ts index 34a6f7f89..e4bc76634 100644 --- a/src/targets/node/nodeLauncherBase.ts +++ b/src/targets/node/nodeLauncherBase.ts @@ -42,6 +42,8 @@ import { import { NodeSourcePathResolver } from './nodeSourcePathResolver'; import { INodeTargetLifecycleHooks, NodeTarget } from './nodeTarget'; import { IProgram } from './program'; +import { FSUtils } from '../../ioc-extras'; +import { LocalFsUtils } from '../../common/fsUtils'; /** * Telemetry received from the nested process. @@ -129,6 +131,7 @@ export abstract class NodeLauncherBase implement constructor( @inject(INodeBinaryProvider) private readonly pathProvider: NodeBinaryProvider, @inject(ILogger) protected readonly logger: ILogger, + @inject(FSUtils) protected readonly fsUtils: LocalFsUtils, ) {} /** @@ -154,6 +157,7 @@ export abstract class NodeLauncherBase implement context, logger, pathResolver: new NodeSourcePathResolver( + this.fsUtils, { resolveSourceMapLocations: resolved.resolveSourceMapLocations, basePath: resolved.cwd, diff --git a/src/targets/node/nodeSourcePathResolver.ts b/src/targets/node/nodeSourcePathResolver.ts index e9c1ae110..46cdd4209 100644 --- a/src/targets/node/nodeSourcePathResolver.ts +++ b/src/targets/node/nodeSourcePathResolver.ts @@ -14,12 +14,22 @@ import { import { IUrlResolution } from '../../common/sourcePathResolver'; import * as urlUtils from '../../common/urlUtils'; import { ISourcePathResolverOptions, SourcePathResolverBase } from '../sourcePathResolver'; +import { LocalFsUtils } from '../../common/fsUtils'; +import { ILogger } from '../../common/logging'; interface IOptions extends ISourcePathResolverOptions { basePath?: string; } export class NodeSourcePathResolver extends SourcePathResolverBase { + public constructor( + private readonly fsUtils: LocalFsUtils, + protected readonly options: IOptions, + protected readonly logger: ILogger, + ) { + super(options, logger); + } + async urlToAbsolutePath({ url, map }: IUrlResolution): Promise { // replace any query string, which can be generated by webpack bundles url = url.replace(/\?.+/, ''); @@ -80,7 +90,7 @@ export class NodeSourcePathResolver extends SourcePathResolverBase { map.sourceRoot, map.metadata.compiledPath, { '/': this.options.basePath }, - moduleAwarePathMappingResolver(map.metadata.compiledPath), + moduleAwarePathMappingResolver(this.fsUtils, map.metadata.compiledPath), this.logger, ), url, diff --git a/src/targets/node/nvmResolver.ts b/src/targets/node/nvmResolver.ts index b13e7691f..999caeba0 100644 --- a/src/targets/node/nvmResolver.ts +++ b/src/targets/node/nvmResolver.ts @@ -6,9 +6,10 @@ import * as path from 'path'; import * as fs from 'fs'; import { nvmHomeNotFound, nvmNotFound, nvmVersionNotFound, nvsNotFound } from '../../dap/errors'; import { ProtocolError } from '../../dap/protocolError'; -import { injectable } from 'inversify'; -import { exists } from '../../common/fsUtils'; +import { injectable, inject } from 'inversify'; import { some } from '../../common/promiseUtil'; +import { LocalFsUtils } from '../../common/fsUtils'; +import { FSUtils } from '../../ioc-extras'; /** * Resolves the location of Node installation querying an nvm installation. @@ -39,6 +40,7 @@ const enum Vars { @injectable() export class NvmResolver implements INvmResolver { constructor( + @inject(FSUtils) private readonly fsUtils: LocalFsUtils, private readonly env = process.env, private readonly arch = process.arch, private readonly platform = process.platform, @@ -84,7 +86,7 @@ export class NvmResolver implements INvmResolver { throw new ProtocolError(nvmNotFound()); } - if (!directory || !(await exists(directory))) { + if (!directory || !(await this.fsUtils.exists(directory))) { throw new ProtocolError(nvmVersionNotFound(version, versionManagers.join('/'))); } @@ -97,7 +99,7 @@ export class NvmResolver implements INvmResolver { * This detects that. */ private async getBinaryInFolder(dir: string) { - if (await some(['node64.exe', 'node64'].map(exe => exists(path.join(dir, exe))))) { + if (await some(['node64.exe', 'node64'].map(exe => this.fsUtils.exists(path.join(dir, exe))))) { return 'node64'; } @@ -129,7 +131,7 @@ export class NvmResolver implements INvmResolver { if (!nvmHome) { // if NVM_DIR is not set. Probe for '.nvm' directory instead const nvmDir = path.join(this.env['HOME'] || '', '.nvm'); - if (await exists(nvmDir)) { + if (await this.fsUtils.exists(nvmDir)) { nvmHome = nvmDir; } } diff --git a/src/targets/node/terminalNodeLauncher.ts b/src/targets/node/terminalNodeLauncher.ts index 30df0a2e4..370074e63 100644 --- a/src/targets/node/terminalNodeLauncher.ts +++ b/src/targets/node/terminalNodeLauncher.ts @@ -2,27 +2,28 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ -import { randomBytes } from 'crypto'; import { inject, injectable } from 'inversify'; -import { tmpdir } from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; import { DebugType } from '../../common/contributionUtils'; import { EventEmitter } from '../../common/events'; -import { ILogger } from '../../common/logging'; import { AnyLaunchConfiguration, ITerminalLaunchConfiguration } from '../../configuration'; import { ErrorCodes } from '../../dap/errors'; import { ProtocolError } from '../../dap/protocolError'; -import { FS, FsPromises } from '../../ioc-extras'; -import { IStopMetadata, ITarget } from '../targets'; +import { tmpdir } from 'os'; +import { randomBytes } from 'crypto'; +import { FS, FsPromises, FSUtils } from '../../ioc-extras'; import { hideDebugInfoFromConsole, INodeBinaryProvider, - NodeBinary, NodeBinaryProvider, + NodeBinary, } from './nodeBinaryProvider'; -import { IProcessTelemetry, IRunData, NodeLauncherBase } from './nodeLauncherBase'; +import { ILogger } from '../../common/logging'; +import { LocalFsUtils } from '../../common/fsUtils'; import { IProgram } from './program'; +import { IStopMetadata, ITarget } from '../targets'; +import { NodeLauncherBase, IProcessTelemetry, IRunData } from './nodeLauncherBase'; class VSCodeTerminalProcess implements IProgram { public readonly stopped: Promise; @@ -73,8 +74,9 @@ export class TerminalNodeLauncher extends NodeLauncherBase { - let fsExistStub: SinonStub<[fs.PathLike, (cb: boolean) => void], void>; + let fsExistStub: SinonStub<[PathLike, (cb: boolean) => void], void>; const testVueMapper: IVueFileMapper = { lookup: async url => path.join(testFixturesDir, 'web', 'looked up', url), getVueHandling: url => @@ -43,6 +43,7 @@ describe('browserPathResolver.urlToAbsolutePath', () => { describe('vue', () => { const resolver = new BrowserSourcePathResolver( testVueMapper, + new LocalFsUtils(fsPromises), { pathMapping: { '/': path.join(testFixturesDir, 'web') }, clientID: 'vscode', @@ -127,9 +128,23 @@ describe('browserPathResolver.urlToAbsolutePath', () => { }); }); + class FakeLocalFsUtils implements IFsUtils { + exists(path: string): Promise { + switch (path) { + case 'c:\\Users\\user\\Source\\Repos\\Angular Project\\ClientApp\\src\\app\\app.component.html': + return Promise.resolve(true); + case 'c:\\Users\\user\\Source\\Repos\\Angular Project\\wwwroot\\src\\app\\app.component.html': + return Promise.resolve(false); + default: + throw Error(`Unknown path ${path}`); + } + } + } + describe('absolutePathToUrl', () => { const resolver = new BrowserSourcePathResolver( testVueMapper, + new LocalFsUtils(fsPromises), { pathMapping: { '/': path.join(testFixturesDir, 'web'), @@ -170,6 +185,7 @@ describe('browserPathResolver.urlToAbsolutePath', () => { const resolver = new BrowserSourcePathResolver( testVueMapper, + new FakeLocalFsUtils(), { pathMapping: { '/': webRoot }, clientID: client, diff --git a/src/test/extension/nodeConfigurationProvider.test.ts b/src/test/extension/nodeConfigurationProvider.test.ts index 08366f58d..59526793d 100644 --- a/src/test/extension/nodeConfigurationProvider.test.ts +++ b/src/test/extension/nodeConfigurationProvider.test.ts @@ -12,6 +12,8 @@ import { INodeLaunchConfiguration } from '../../configuration'; import { NodeConfigurationResolver } from '../../ui/configuration/nodeDebugConfigurationResolver'; import { testFixturesDir } from '../test'; import { createFileTree } from '../createFileTree'; +import { LocalFsUtils } from '../../common/fsUtils'; +import { promises as fsPromises } from 'fs'; describe('NodeDebugConfigurationProvider', () => { let provider: NodeConfigurationResolver; @@ -24,7 +26,11 @@ describe('NodeDebugConfigurationProvider', () => { beforeEach(() => { nvmResolver = { resolveNvmVersionPath: stub() }; - provider = new NodeConfigurationResolver({ logPath: testFixturesDir } as any, nvmResolver); + provider = new NodeConfigurationResolver( + { logPath: testFixturesDir } as any, + nvmResolver, + new LocalFsUtils(fsPromises), + ); EnvironmentVars.platform = 'linux'; }); diff --git a/src/test/goldenText.ts b/src/test/goldenText.ts index 28091f93f..ee8ef2fdb 100644 --- a/src/test/goldenText.ts +++ b/src/test/goldenText.ts @@ -50,8 +50,20 @@ export class GoldenText { const match = frame.match(/^(.*):(\d+):(\d+)$/); if (!match) return null; const filePath = match[1]; - if (filePath === __filename) continue; - return filePath; + + // We need to consider .ts files because we may have source-map-support to convert stack traces to .ts + + if ( + filePath === __filename || + filePath === __filename.replace(/[\\/]out([\\/].*).js$/, '$1.ts') + ) + continue; + + if (filePath.endsWith('.ts')) { + return filePath.replace(/([\\/]src[\\/].*).ts$/, `${path.sep}out$1.js`); + } else { + return filePath; + } } return null; } diff --git a/src/test/node/node-source-path-resolver.test.ts b/src/test/node/node-source-path-resolver.test.ts index f379a4c98..cc5bef7ac 100644 --- a/src/test/node/node-source-path-resolver.test.ts +++ b/src/test/node/node-source-path-resolver.test.ts @@ -7,6 +7,10 @@ import { expect } from 'chai'; import { join, resolve } from 'path'; import { setCaseSensitivePaths, resetCaseSensitivePaths } from '../../common/urlUtils'; import { Logger } from '../../common/logging/logger'; +import { promises as fsPromises } from 'fs'; +import { LocalFsUtils } from '../../common/fsUtils'; + +const fsUtils = new LocalFsUtils(fsPromises); describe('node source path resolver', () => { describe('url to path', () => { @@ -19,7 +23,7 @@ describe('node source path resolver', () => { }; it('resolves absolute', async () => { - const r = new NodeSourcePathResolver(defaultOptions, await Logger.test()); + const r = new NodeSourcePathResolver(fsUtils, defaultOptions, await Logger.test()); expect(await r.urlToAbsolutePath({ url: 'file:///src/index.js' })).to.equal( resolve('/src/index.js'), ); @@ -27,6 +31,7 @@ describe('node source path resolver', () => { it('normalizes roots (win -> posix) ', async () => { const r = new NodeSourcePathResolver( + fsUtils, { ...defaultOptions, remoteRoot: 'C:\\Source', @@ -42,6 +47,7 @@ describe('node source path resolver', () => { it('normalizes roots (posix -> win) ', async () => { const r = new NodeSourcePathResolver( + fsUtils, { ...defaultOptions, remoteRoot: '/dev/src', @@ -56,7 +62,7 @@ describe('node source path resolver', () => { }); it('places relative paths in node_internals', async () => { - const r = new NodeSourcePathResolver(defaultOptions, await Logger.test()); + const r = new NodeSourcePathResolver(fsUtils, defaultOptions, await Logger.test()); expect( await r.urlToAbsolutePath({ @@ -66,7 +72,7 @@ describe('node source path resolver', () => { }); it('applies source map overrides', async () => { - const r = new NodeSourcePathResolver(defaultOptions, await Logger.test()); + const r = new NodeSourcePathResolver(fsUtils, defaultOptions, await Logger.test()); expect( await r.urlToAbsolutePath({ @@ -128,6 +134,7 @@ describe('node source path resolver', () => { setCaseSensitivePaths(caseSensitive); const resolver = new NodeSourcePathResolver( + fsUtils, { ...defaultOptions, resolveSourceMapLocations: locs, diff --git a/src/test/node/process-tree.test.ts b/src/test/node/process-tree.test.ts index 46e7c38c0..56547f4a0 100644 --- a/src/test/node/process-tree.test.ts +++ b/src/test/node/process-tree.test.ts @@ -10,6 +10,8 @@ import { IProcess, IProcessTree, processTree } from '../../ui/processTree/proces import { ReadableStreamBuffer } from 'stream-buffers'; import { DarwinProcessTree } from '../../ui/processTree/darwinProcessTree'; import { WindowsProcessTree } from '../../ui/processTree/windowsProcessTree'; +import { LocalFsUtils } from '../../common/fsUtils'; +import { promises as fsPromises } from 'fs'; const fakeChildProcess = (stdoutData: string) => { const ee: any = new EventEmitter(); @@ -54,7 +56,7 @@ describe('process tree', () => { it('works for darwin', async () => { await assertParses( - new DarwinProcessTree(), + new DarwinProcessTree(new LocalFsUtils(fsPromises)), ' PID PPID BINARY COMMAND\n 380 1 /usr/sbin/cfpref /usr/sbin/cfprefsd agent\n 381 1 /usr/libexec/Use /usr/libexec/UserEventAgent (Aqua)\n 383 1 /usr/sbin/distno /usr/sbin/distnoted agent\n 384 1 /usr/libexec/USB /usr/libexec/USBAgent\n 387 1 /System/Library/ /System/Library/Frameworks/CoreTelephony.framework/Support/CommCenter -L\n 389 1 /usr/libexec/lsd /usr/libexec/lsd\n 390 1 /usr/libexec/tru /usr/libexec/trustd --agent\n 391 1 /usr/libexec/sec /usr/libexec/secd\n 392 2 /System/Library/ /System/Library/PrivateFrameworks/CloudKitDaemon.framework/Support/cloudd', [ { pid: 380, ppid: 1, command: 'usr/sbin/cfpref', args: '/usr/sbin/cfprefsd agent' }, @@ -87,7 +89,7 @@ describe('process tree', () => { it('works for posix', async () => { await assertParses( - new PosixProcessTree(), + new PosixProcessTree(new LocalFsUtils(fsPromises)), ' PID PPID BINARY COMMAND\n 351 1 systemd /lib/systemd/systemd --user\n 352 351 (sd-pam) (sd-pam)\n 540 1 sh sh /home/connor/.vscode-server-insiders/bin/bbf00d8ea6aa7e825ca3393364d746fe401d3299/server.sh --host=127.0.0.1 --enable-remote-auto-shutdown --port=0\n 548 540 node /home/connor/.vscode-server-insiders/bin/bbf00d8ea6aa7e825ca3393364d746fe401d3299/node /home/connor/.vscode-server-insiders/bin/bbf00d8ea6aa7e825ca3393364d746fe401d3299/out/vs/server/main.js --host=127.0.0.1 --enable-remote-auto-shutdown --port=0\n 6557 6434 sshd sshd: connor@notty\n 6558 6557 bash bash\n 7281 7199 sshd sshd: connor@pts/0\n 7282 7281 bash -bash\n 9880 99219 bash /bin/bash', [ { pid: 351, ppid: 1, command: '/lib/systemd/systemd', args: '--user' }, diff --git a/src/test/node/runtimeVersion.test.ts b/src/test/node/runtimeVersion.test.ts index 7a5f3676d..95855dfb1 100644 --- a/src/test/node/runtimeVersion.test.ts +++ b/src/test/node/runtimeVersion.test.ts @@ -8,12 +8,16 @@ import { NvmResolver, INvmResolver } from '../../targets/node/nvmResolver'; import { expect } from 'chai'; import * as path from 'path'; import { ProtocolError } from '../../dap/protocolError'; +import { promises as fsPromises } from 'fs'; +import { LocalFsUtils } from '../../common/fsUtils'; + +const fsUtils = new LocalFsUtils(fsPromises); describe('runtimeVersion', () => { let resolver: INvmResolver; it('fails if no nvm/s present', async () => { - resolver = new NvmResolver({}, 'x64', 'linux'); + resolver = new NvmResolver(fsUtils, {}, 'x64', 'linux'); await expect(resolver.resolveNvmVersionPath('13')).to.eventually.be.rejectedWith( ProtocolError, /requires Node.js version manager/, @@ -29,6 +33,7 @@ describe('runtimeVersion', () => { }); resolver = new NvmResolver( + fsUtils, { NVS_HOME: path.join(testFixturesDir, 'nvs'), NVM_DIR: path.join(testFixturesDir, 'nvm') }, 'x64', 'linux', @@ -49,7 +54,12 @@ describe('runtimeVersion', () => { }); it('requires nvs for a specific architecture', async () => { - resolver = new NvmResolver({ NVM_DIR: path.join(testFixturesDir, 'nvm') }, 'x64', 'linux'); + resolver = new NvmResolver( + fsUtils, + { NVM_DIR: path.join(testFixturesDir, 'nvm') }, + 'x64', + 'linux', + ); await expect(resolver.resolveNvmVersionPath('13.11/x64')).to.eventually.be.rejectedWith( ProtocolError, /architecture requires 'nvs' to be installed/, @@ -74,7 +84,7 @@ describe('runtimeVersion', () => { 'node/13.invalid/x64/bin/node': '', }); - resolver = new NvmResolver({ NVS_HOME: testFixturesDir }, 'x64', 'linux'); + resolver = new NvmResolver(fsUtils, { NVS_HOME: testFixturesDir }, 'x64', 'linux'); }); it('gets an exact match', async () => { @@ -114,7 +124,7 @@ describe('runtimeVersion', () => { }); it('omits the bin directory on windows', async () => { - resolver = new NvmResolver({ NVS_HOME: testFixturesDir }, 'x64', 'win32'); + resolver = new NvmResolver(fsUtils, { NVS_HOME: testFixturesDir }, 'x64', 'win32'); const { directory } = await resolver.resolveNvmVersionPath('13.3.0'); expect(directory).to.equal(path.join(testFixturesDir, 'node/13.3.0/x64')); }); @@ -129,7 +139,7 @@ describe('runtimeVersion', () => { 'v13.invalid/node.exe': '', }); - resolver = new NvmResolver({ NVM_HOME: testFixturesDir }, 'x64', 'win32'); + resolver = new NvmResolver(fsUtils, { NVM_HOME: testFixturesDir }, 'x64', 'win32'); }); it('gets an exact match', async () => { @@ -166,7 +176,7 @@ describe('runtimeVersion', () => { 'versions/node/v13.invalid/bin/node': '', }); - resolver = new NvmResolver({ NVM_DIR: testFixturesDir }, 'x64', 'linux'); + resolver = new NvmResolver(fsUtils, { NVM_DIR: testFixturesDir }, 'x64', 'linux'); }); it('gets an exact match', async () => { diff --git a/src/test/profiling/profiling.test.ts b/src/test/profiling/profiling.test.ts index a9de3e3ab..19a327fa9 100644 --- a/src/test/profiling/profiling.test.ts +++ b/src/test/profiling/profiling.test.ts @@ -364,7 +364,8 @@ describe('profiling', () => { disposable.dispose(); }); - it('profiles from launch', async () => { + it('profiles from launch', async function () { + this.timeout(20 * 1000); // 20 seconds timeout vscode.debug.startDebugging(undefined, { type: DebugType.Node, request: 'launch', diff --git a/src/test/sources/pretty-print.test.ts b/src/test/sources/pretty-print.test.ts index 1abb5a622..c7f2e113a 100644 --- a/src/test/sources/pretty-print.test.ts +++ b/src/test/sources/pretty-print.test.ts @@ -12,7 +12,7 @@ describe('pretty print sources', () => { return () => p.dap.continue({ threadId }); } - itIntegrates('base', async ({ r }) => { + itIntegrates('base', async function ({ r }) { const p = await r.launchUrl('pretty/pretty.html'); const source = { path: p.workspacePath('web/pretty/ugly.js') }; await p.dap.setBreakpoints({ source, breakpoints: [{ line: 5, column: 1 }] }); diff --git a/src/test/testRunner.ts b/src/test/testRunner.ts index 439917ff4..c565779e1 100644 --- a/src/test/testRunner.ts +++ b/src/test/testRunner.ts @@ -72,9 +72,9 @@ export async function run(): Promise { runner.useColors(true); // todo: retry failing tests https://github.com/microsoft/vscode-pwa/issues/28 - if (process.env.RETRY_TESTS) { - runner.retries(Number(process.env.RETRY_TESTS)); - } + // if (process.env.RETRY_TESTS) { + // runner.retries(Number(process.env.RETRY_TESTS)); + // } if (process.env.FRAMEWORK_TESTS) { runner.addFile(join(__dirname, 'framework/reactTest')); diff --git a/src/test/ui/pickAttach.test.ts b/src/test/ui/pickAttach.test.ts index 9fbc9d53d..66652a885 100644 --- a/src/test/ui/pickAttach.test.ts +++ b/src/test/ui/pickAttach.test.ts @@ -18,6 +18,9 @@ import * as path from 'path'; import { tmpdir } from 'os'; import del from 'del'; import { forceForwardSlashes } from '../../common/pathUtils'; +import { LocalFsUtils } from '../../common/fsUtils'; +import { promises as fsPromises } from 'fs'; +import { delay } from '../../common/promiseUtil'; describe('pick and attach', () => { const testDir = path.join(tmpdir(), 'js-debug-pick-and-attach'); @@ -46,7 +49,7 @@ describe('pick and attach', () => { child = spawn('node', ['foo.js'], { cwd: testDir }); const config = { ...nodeAttachConfigDefaults, processId: `${child.pid}:1234` }; - await resolveProcessId(config, true); + await resolveProcessId(new LocalFsUtils(fsPromises), config, true); expect(removePrivatePrefix(config.cwd!)).to.equal(testDir); }); @@ -58,7 +61,7 @@ describe('pick and attach', () => { child = spawn('node', ['foo.js'], { cwd: path.join(testDir, 'nested') }); const config = { ...nodeAttachConfigDefaults, processId: `${child.pid}:1234` }; - await resolveProcessId(config, true); + await resolveProcessId(new LocalFsUtils(fsPromises), config, true); expect(removePrivatePrefix(config.cwd!)).to.equal(testDir); }); @@ -77,7 +80,7 @@ describe('pick and attach', () => { child = spawn('node', ['foo.js'], { cwd: path.join(testDir, 'nested') }); const config = { ...nodeAttachConfigDefaults, processId: `${child.pid}:1234` }; - await resolveProcessId(config, true); + await resolveProcessId(new LocalFsUtils(fsPromises), config, true); expect(removePrivatePrefix(config.cwd!)).to.equal(path.join(testDir, 'nested')); }); } @@ -92,17 +95,19 @@ describe('pick and attach', () => { .on('data', (line: string) => (attached = attached || line.includes('Debugger attached'))); }); - it('end to end', async function () { + it.only('end to end', async function () { this.timeout(30 * 1000); const createQuickPick = sandbox.spy(vscode.window, 'createQuickPick'); vscode.commands.executeCommand(Commands.AttachProcess); + await delay(2000); const picker = await eventuallyOk(() => { expect(createQuickPick.called).to.be.true; return createQuickPick.getCall(0).returnValue; - }); + }, 10 * 1000); + await delay(2000); const item = await eventuallyOk(() => { const i = picker.items.find(item => (item as any).pidAndPort === `${child.pid}:${port}`); if (!i) { @@ -112,7 +117,9 @@ describe('pick and attach', () => { }, 10 * 1000); picker.selectedItems = [item]; + await delay(2000); await vscode.commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); + await delay(2000); await eventuallyOk( () => expect(attached).to.equal(true, 'expected to have attached'), 10 * 1000, diff --git a/src/ui/autoAttach.ts b/src/ui/autoAttach.ts index ba6dbcef9..f0f42de87 100644 --- a/src/ui/autoAttach.ts +++ b/src/ui/autoAttach.ts @@ -13,6 +13,7 @@ import { } from '../targets/node/autoAttachLauncher'; import { NodeBinaryProvider } from '../targets/node/nodeBinaryProvider'; import { launchVirtualTerminalParent } from './debugTerminalUI'; +import { LocalFsUtils } from '../common/fsUtils'; const localize = nls.loadMessageBundle(); @@ -30,7 +31,14 @@ export function registerAutoAttach( launcher = (async () => { const logger = new ProxyLogger(); - const inst = new AutoAttachLauncher(new NodeBinaryProvider(logger, fs), logger, context, fs); + // TODO: Figure out how to inject FsUtils + const inst = new AutoAttachLauncher( + new NodeBinaryProvider(logger, fs), + logger, + context, + fs, + new LocalFsUtils(fs), + ); await launchVirtualTerminalParent(delegate, inst); inst.onTargetListChanged(() => { diff --git a/src/ui/configuration/nodeDebugConfigurationResolver.ts b/src/ui/configuration/nodeDebugConfigurationResolver.ts index c379d3899..094dfdede 100644 --- a/src/ui/configuration/nodeDebugConfigurationResolver.ts +++ b/src/ui/configuration/nodeDebugConfigurationResolver.ts @@ -28,6 +28,7 @@ import { INvmResolver } from '../../targets/node/nvmResolver'; import { fixInspectFlags } from '../configurationUtils'; import { resolveProcessId } from '../processPicker'; import { BaseConfigurationResolver } from './baseConfigurationResolver'; +import { LocalFsUtils } from '../../common/fsUtils'; const localize = nls.loadMessageBundle(); @@ -45,6 +46,7 @@ export class NodeConfigurationResolver extends BaseConfigurationResolver, ) { const logger = new ProxyLogger(); - const launcher = new TerminalNodeLauncher(new NodeBinaryProvider(logger, fs), logger, fs); + const launcher = new TerminalNodeLauncher( + new NodeBinaryProvider(logger, fs), + logger, + fs, + fsUtils, + ); launcher.onTerminalCreated(terminal => { linkHandler.enableHandlingInTerminal(terminal); }); diff --git a/src/ui/processPicker.ts b/src/ui/processPicker.ts index 3840fab0d..0974f316f 100644 --- a/src/ui/processPicker.ts +++ b/src/ui/processPicker.ts @@ -15,6 +15,8 @@ import { processTree, analyseArguments } from './processTree/processTree'; import { readConfig, Configuration } from '../common/contributionUtils'; import { nearestDirectoryContaining } from '../common/urlUtils'; import { isSubdirectoryOf } from '../common/pathUtils'; +import { LocalFsUtils } from '../common/fsUtils'; +import { promises as fsPromises } from 'fs'; const INSPECTOR_PORT_DEFAULT = 9229; @@ -45,7 +47,8 @@ export async function attachProcess() { processId, }; - await resolveProcessId(config, true); + // TODO: Figure out how to inject FsUtils + await resolveProcessId(new LocalFsUtils(fsPromises), config, true); await vscode.debug.startDebugging( config.cwd ? vscode.workspace.getWorkspaceFolder(vscode.Uri.file(config.cwd)) : undefined, config, @@ -57,7 +60,11 @@ export async function attachProcess() { * appropriately. Returns true if the configuration was updated, false * if it was cancelled. */ -export async function resolveProcessId(config: ResolvingNodeAttachConfiguration, setCwd = false) { +export async function resolveProcessId( + fsUtils: LocalFsUtils, + config: ResolvingNodeAttachConfiguration, + setCwd = false, +) { // we resolve Process Picker early (before VS Code) so that we can probe the process for its protocol const processId = config.processId?.trim(); const result = processId && decodePidAndPort(processId); @@ -79,14 +86,14 @@ export async function resolveProcessId(config: ResolvingNodeAttachConfiguration, delete config.processId; if (setCwd) { - const inferredWd = await inferWorkingDirectory(result.pid); + const inferredWd = await inferWorkingDirectory(fsUtils, result.pid); if (inferredWd) { config.cwd = inferredWd; } } } -async function inferWorkingDirectory(processId?: number) { +async function inferWorkingDirectory(fsUtils: LocalFsUtils, processId?: number) { const inferredWd = processId && (await processTree.getWorkingDirectory(processId)); // If we couldn't infer the working directory, just use the first workspace folder @@ -94,7 +101,7 @@ async function inferWorkingDirectory(processId?: number) { return vscode.workspace.workspaceFolders?.[0].uri.fsPath; } - const packageRoot = await nearestDirectoryContaining(inferredWd, 'package.json'); + const packageRoot = await nearestDirectoryContaining(fsUtils, inferredWd, 'package.json'); if (!packageRoot) { return inferredWd; } diff --git a/src/ui/processTree/darwinProcessTree.ts b/src/ui/processTree/darwinProcessTree.ts index 2608081f2..ad7a5e59a 100644 --- a/src/ui/processTree/darwinProcessTree.ts +++ b/src/ui/processTree/darwinProcessTree.ts @@ -6,9 +6,13 @@ import { IProcess } from './processTree'; import { BaseProcessTree } from './baseProcessTree'; import { spawnAsync, ChildProcessError } from '../../common/processUtils'; import { isAbsolute } from 'path'; -import { exists } from '../../common/fsUtils'; +import { LocalFsUtils } from '../../common/fsUtils'; export class DarwinProcessTree extends BaseProcessTree { + public constructor(private readonly fsUtils: LocalFsUtils) { + super(); + } + public async getWorkingDirectory(processId: number) { try { const { stdout } = await spawnAsync('lsof', [ @@ -25,7 +29,7 @@ export class DarwinProcessTree extends BaseProcessTree { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const cwd = stdout.trim().split('\n').pop()!.slice(1); - return cwd && isAbsolute(cwd) && (await exists(cwd)) ? cwd : undefined; + return cwd && isAbsolute(cwd) && (await this.fsUtils.exists(cwd)) ? cwd : undefined; } catch (e) { if (e instanceof ChildProcessError) { return undefined; diff --git a/src/ui/processTree/processTree.ts b/src/ui/processTree/processTree.ts index 768ed5606..adcf52527 100644 --- a/src/ui/processTree/processTree.ts +++ b/src/ui/processTree/processTree.ts @@ -5,6 +5,8 @@ import { WindowsProcessTree } from './windowsProcessTree'; import { DarwinProcessTree } from './darwinProcessTree'; import { PosixProcessTree } from './posixProcessTree'; +import { LocalFsUtils } from '../../common/fsUtils'; +import { promises as fsPromises } from 'fs'; /** * IProcess is parsed from the {@link IProcessTree} @@ -53,12 +55,14 @@ export interface IProcessTree { /** * The process tree implementation for the current platform. */ +// TODO: Figure out how to inject the fsUtils here +const fsUtils = new LocalFsUtils(fsPromises); export const processTree: IProcessTree = process.platform === 'win32' ? new WindowsProcessTree() : process.platform === 'darwin' - ? new DarwinProcessTree() - : new PosixProcessTree(); + ? new DarwinProcessTree(fsUtils) + : new PosixProcessTree(fsUtils); /* * Analyse the given command line arguments and extract debug port and protocol from it.