Skip to content

Commit

Permalink
Add support for remote file system operations
Browse files Browse the repository at this point in the history
  • Loading branch information
digeff committed Sep 14, 2020
1 parent 8581c0b commit 2554968
Show file tree
Hide file tree
Showing 38 changed files with 398 additions and 98 deletions.
10 changes: 5 additions & 5 deletions src/binder.ts
Expand Up @@ -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 {};
Expand Down Expand Up @@ -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>(ILogger).setup(resolveLoggerOptions(dap, params.trace));

const cts =
Expand Down
51 changes: 51 additions & 0 deletions src/build/dapCustom.ts
Expand Up @@ -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' },
Expand Down
1 change: 1 addition & 0 deletions src/build/generate-contributions.ts
Expand Up @@ -51,6 +51,7 @@ type OmittedKeysFromAttributes =
| '__workspaceFolder'
| '__workspaceCachePath'
| '__autoExpandGetters'
| '__remoteFilePrefix'
| '__sessionId';

type DescribedAttribute<T> = JSONSchema6 &
Expand Down
72 changes: 68 additions & 4 deletions src/common/fsUtils.ts
Expand Up @@ -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;

Expand Down Expand Up @@ -106,8 +107,71 @@ export function readFileRaw(path: string): Promise<Buffer> {
return fs.promises.readFile(path).catch(() => Buffer.alloc(0));
}

export function exists(path: string): Promise<boolean> {
return new Promise(cb => {
fs.exists(path, cb);
});
export interface IFsUtils {
exists(path: string): Promise<boolean>;
}

export class LocalFsUtils implements IFsUtils {
public constructor(private readonly fs: FsPromises) {}

public async exists(path: string): Promise<boolean> {
// 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<boolean> {
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<boolean> {
return (this.shouldUseRemoteFileSystem(path) ? this.remoteFsUtils : this.localFsUtils).exists(
path,
);
}

public shouldUseRemoteFileSystem(path: string) {
return path.startsWith(this.remoteFilePrefix);
}
}
11 changes: 6 additions & 5 deletions src/common/sourceMaps/sourceMapResolutionUtils.ts
Expand Up @@ -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) {
Expand Down Expand Up @@ -135,18 +136,18 @@ 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;
}

// 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',
);
Expand Down
6 changes: 3 additions & 3 deletions src/common/sourceUtils.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}

Expand Down
6 changes: 3 additions & 3 deletions src/common/urlUtils.ts
Expand Up @@ -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';

Expand Down Expand Up @@ -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<string>(['localhost', '127.0.0.1', '::1']);
Expand Down
6 changes: 6 additions & 0 deletions src/configuration.ts
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -778,6 +783,7 @@ export const baseDefaults: IBaseConfiguration = {
// Should always be determined upstream;
__workspaceFolder: '',
__autoExpandGetters: false,
__remoteFilePrefix: undefined,
__breakOnConditionalError: false,
customDescriptionGenerator: undefined,
};
Expand Down
28 changes: 28 additions & 0 deletions src/dap/api.d.ts
Expand Up @@ -923,6 +923,18 @@ export namespace Dap {
*/
launchUnelevatedRequest(params: LaunchUnelevatedParams): Promise<LaunchUnelevatedResult>;

/**
* Check if file exists on remote file system, used in VS.
*/
on(
request: 'remoteFileExists',
handler: (params: RemoteFileExistsParams) => Promise<RemoteFileExistsResult | Error>,
): () => void;
/**
* Check if file exists on remote file system, used in VS.
*/
remoteFileExistsRequest(params: RemoteFileExistsParams): Promise<RemoteFileExistsResult>;

/**
* Gets all defined breakpoints.
*/
Expand Down Expand Up @@ -1588,6 +1600,11 @@ export namespace Dap {
*/
launchUnelevated(params: LaunchUnelevatedParams): Promise<LaunchUnelevatedResult>;

/**
* Check if file exists on remote file system, used in VS.
*/
remoteFileExists(params: RemoteFileExistsParams): Promise<RemoteFileExistsResult>;

/**
* Gets all defined breakpoints.
*/
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions src/dap/telemetryClassification.d.ts
Expand Up @@ -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' };
Expand Down
3 changes: 3 additions & 0 deletions src/extension.ts
Expand Up @@ -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({
Expand Down Expand Up @@ -93,6 +95,7 @@ export function activate(context: vscode.ExtensionContext) {
context,
services.get(DelegateLauncherFactory),
services.get(TerminalLinkHandler),
services.get<LocalFsUtils>(FSUtils),
);
registerNpmScriptLens(context);
registerProfilingCommand(context, services);
Expand Down
5 changes: 5 additions & 0 deletions src/ioc-extras.ts
Expand Up @@ -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.
*/
Expand Down

0 comments on commit 2554968

Please sign in to comment.