Skip to content

Commit

Permalink
feat: add basic network view, support experimental networking for node (
Browse files Browse the repository at this point in the history
#2053)

This adds a basic network tree view that shows requests and responses
in the debugee. It has a default "go to" action which opens a cURL
representation of the request, and context menu actions to open the
response body either in a text editor or the hex editor. It also has
actions to copy the URL and replay the request.

![](https://memes.peet.io/img/24-08-aa5f7f35-332f-4a60-9fbb-32ac79018b60.png)

I initially was hoping to turn this on by default for Node.js since
their networking support in 22.6.0 inspired this change, but their
functionality right now is very limited (we basically get the URL and
nothing else.) Therefore I added an option to turn it on, but it's not
on by default there.

I think I might end up toggling this off for the next stable release
for now until we can polish it some more, but it's ship it in nightly
to get some feedback!
  • Loading branch information
connor4312 authored Aug 8, 2024
1 parent ca1c645 commit 6e8a828
Show file tree
Hide file tree
Showing 18 changed files with 881 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This changelog records changes to stable releases since 1.50.2. "TBA" changes he

## Nightly (only)

- feat: add basic network view, support experimental networking for node ([#2051](https://github.com/microsoft/vscode-js-debug/issues/2051))
- feat: support "debug url" in terminals created through the `node-terminal` launch type ([#2049](https://github.com/microsoft/vscode-js-debug/issues/2049))
- fix: hover evaluation incorrectly showing undefined ([vscode#221503](https://github.com/microsoft/vscode/issues/221503))

Expand Down
3 changes: 2 additions & 1 deletion OPTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@
<h5>Default value:</h4><pre><code>true</pre></code><h4>enableDWARF</h4><p>Toggles whether the debugger will try to read DWARF debug symbols from WebAssembly, which can be resource intensive. Requires the <code>ms-vscode.wasm-dwarf-debugging</code> extension to function.</p>
<h5>Default value:</h4><pre><code>true</pre></code><h4>env</h4><p>Environment variables passed to the program. The value <code>null</code> removes the variable from the environment.</p>
<h5>Default value:</h4><pre><code>{}</pre></code><h4>envFile</h4><p>Absolute path to a file containing environment variable definitions.</p>
<h5>Default value:</h4><pre><code>null</pre></code><h4>killBehavior</h4><p>Configures how debug processes are killed when stopping the session. Can be:<br><br>- forceful (default): forcefully tears down the process tree. Sends SIGKILL on posix, or <code>taskkill.exe /F</code> on Windows.<br>- polite: gracefully tears down the process tree. It&#39;s possible that misbehaving processes continue to run after shutdown in this way. Sends SIGTERM on posix, or <code>taskkill.exe</code> with no <code>/F</code> (force) flag on Windows.<br>- none: no termination will happen.</p>
<h5>Default value:</h4><pre><code>null</pre></code><h4>experimentalNetworking</h4><p>Enable experimental inspection in Node.js. When set to <code>auto</code> this is enabled for versions of Node.js that support it. It can be set to <code>on</code> or <code>off</code> to enable or disable it explicitly.</p>
<h5>Default value:</h4><pre><code>"auto"</pre></code><h4>killBehavior</h4><p>Configures how debug processes are killed when stopping the session. Can be:<br><br>- forceful (default): forcefully tears down the process tree. Sends SIGKILL on posix, or <code>taskkill.exe /F</code> on Windows.<br>- polite: gracefully tears down the process tree. It&#39;s possible that misbehaving processes continue to run after shutdown in this way. Sends SIGTERM on posix, or <code>taskkill.exe</code> with no <code>/F</code> (force) flag on Windows.<br>- none: no termination will happen.</p>
<h5>Default value:</h4><pre><code>"forceful"</pre></code><h4>localRoot</h4><p>Path to the local directory containing the program.</p>
<h5>Default value:</h4><pre><code>null</pre></code><h4>nodeVersionHint</h4><p>Allows you to explicitly specify the Node version that&#39;s running, which can be used to disable or enable certain behaviors in cases where the automatic version detection does not work.</p>
<h5>Default value:</h4><pre><code>undefined</pre></code><h4>outFiles</h4><p>If source maps are enabled, these glob patterns specify the generated JavaScript files. If a pattern starts with <code>!</code> the files are excluded. If not specified, the generated code is expected in the same directory as its source.</p>
Expand Down
13 changes: 10 additions & 3 deletions package.nls.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"add.eventListener.breakpoint": "Toggle Event Listener Breakpoints",
"add.xhr.breakpoint": "Add XHR/fetch Breakpoint",
"breakpoint.xhr.contains":"Break when URL contains:",
"breakpoint.xhr.any":"Any XHR/fetch",
"breakpoint.xhr.contains": "Break when URL contains:",
"breakpoint.xhr.any": "Any XHR/fetch",
"edit.xhr.breakpoint": "Edit XHR/fetch Breakpoint",
"attach.node.process": "Attach to Node Process",
"base.cascadeTerminateToConfigurations.label": "A list of debug sessions which, when this debug session is terminated, will also be stopped.",
Expand Down Expand Up @@ -198,6 +198,7 @@
"node.versionHint.description": "Allows you to explicitly specify the Node version that's running, which can be used to disable or enable certain behaviors in cases where the automatic version detection does not work.",
"node.websocket.address.description": "Exact websocket address to attach to. If unspecified, it will be discovered from the address and port.",
"node.remote.host.header.description": "Explicit Host header to use when connecting to the websocket of inspector. If unspecified, the host header will be set to 'localhost'. This is useful when the inspector is running behind a proxy that only accept particular Host header.",
"node.experimentalNetworking.description": "Enable experimental inspection in Node.js. When set to `auto` this is enabled for versions of Node.js that support it. It can be set to `on` or `off` to enable or disable it explicitly.",
"openEdgeDevTools.label": "Open Browser Devtools",
"outFiles.description": "If source maps are enabled, these glob patterns specify the generated JavaScript files. If a pattern starts with `!` the files are excluded. If not specified, the generated code is expected in the same directory as its source.",
"pretty.print.script": "Pretty print for debugging",
Expand All @@ -222,5 +223,11 @@
"trace.description": "Configures what diagnostic output is produced.",
"trace.logFile.description": "Configures where on disk logs are written.",
"trace.stdio.description": "Whether to return trace data from the launched application or browser.",
"workspaceTrust.description": "Trust is required to debug code in this workspace."
"workspaceTrust.description": "Trust is required to debug code in this workspace.",
"commands.networkViewRequest.label": "View Request as cURL",
"commands.networkOpenBody.label": "Open Response Body",
"commands.networkOpenBodyInHexEditor.label": "Open Response Body in Hex Editor",
"commands.networkReplayXHR.label": "Replay Request",
"commands.networkCopyURI.label": "Copy Request URL",
"commands.networkClear.label": "Clear Network Log"
}
15 changes: 15 additions & 0 deletions src/adapter/debugAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,21 @@ export class DebugAdapter implements IDisposable {
this.dap.on('stepInTargets', params => this._stepInTargets(params));
this.dap.on('setDebuggerProperty', params => this._setDebuggerProperty(params));
this.dap.on('setSymbolOptions', params => this._setSymbolOptions(params));
this.dap.on('networkCall', params => this._doNetworkCall(params));
}

private async _doNetworkCall({ method, params }: Dap.NetworkCallParams) {
if (!this._thread) {
return Promise.resolve({});
}

// ugly casts :(
const networkDomain = this._thread.cdp().Network as unknown as Record<
string,
(method: unknown) => Promise<object>
>;

return networkDomain[method](params);
}

private _setDebuggerProperty(
Expand Down
21 changes: 21 additions & 0 deletions src/adapter/threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DebugType } from '../common/contributionUtils';
import { EventEmitter } from '../common/events';
import { HrTime } from '../common/hrnow';
import { ILogger, LogTag } from '../common/logging';
import { mirroredNetworkEvents } from '../common/networkEvents';
import { isInstanceOf, truthy } from '../common/objUtils';
import { Base0Position, Base1Position, Range } from '../common/positions';
import { IDeferred, delay, getDeferred } from '../common/promiseUtil';
Expand Down Expand Up @@ -739,6 +740,26 @@ export class Thread implements IVariableStoreLocationProvider {
} else this._revealObject(event.object);
});

if (!this.launchConfig.noDebug) {
// Use whether we can make a cookies request to feature-request the
// availability of networking.
this._cdp.Network.enable({}).then(r => {
if (!r) {
return;
}

this._dap.with(dap => {
dap.networkAvailable({});
for (const event of mirroredNetworkEvents) {
// the types don't work well with the overloads on Network.on, a cast is needed:
(this._cdp.Network.on as (ev: string, fn: (d: object) => void) => void)(event, data =>
dap.networkEvent({ data, event }),
);
}
});
});
}

this._cdp.Debugger.on('paused', async event => this._onPaused(event));
this._cdp.Debugger.on('resumed', () => this.onResumed());
this._cdp.Debugger.on('scriptParsed', event => this._onScriptParsed(event));
Expand Down
45 changes: 45 additions & 0 deletions src/build/dapCustom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,51 @@ const dapCustom: JSONSchema4 = {
description:
'Arguments for "setSymbolOptions" request. Properties are determined by debugger.',
},

...makeEvent(
'networkAvailable',
'Fired when we successfully enable CDP networking on the session.',
{},
),

...makeEvent(
'networkEvent',
'A wrapped CDP network event. There is little abstraction here because UI interacts literally with CDP at the moment.',
{
properties: {
event: {
type: 'string',
description: 'The CDP network event name',
},
data: {
type: 'object',
description: 'The CDP network data',
},
},
required: ['event', 'data'],
},
),

...makeRequest(
'networkCall',
'Makes a network call. There is little abstraction here because UI interacts literally with CDP at the moment.',
{
properties: {
method: {
type: 'string',
description: 'The HTTP method',
},
params: {
type: 'object',
description: 'The CDP call parameters',
},
},
required: ['method', 'params'],
},
{
type: 'object',
},
),
},
};

Expand Down
95 changes: 94 additions & 1 deletion src/build/generate-contributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
IConfigurationTypes,
allCommands,
allDebugTypes,
networkFilesystemScheme,
preferredDebugTypes,
} from '../common/contributionUtils';
import { knownToolToken } from '../common/knownTools';
Expand Down Expand Up @@ -79,7 +80,7 @@ type Menus = {
command: Commands;
title?: MappedReferenceString;
when?: string;
group?: 'navigation' | 'inline';
group?: 'navigation' | 'inline' | string;
}[];
};

Expand Down Expand Up @@ -653,6 +654,12 @@ const nodeLaunchConfig: IDebugger<INodeLaunchConfiguration> = {
default: KillBehavior.Forceful,
markdownDescription: refString('node.killBehavior.description'),
},
experimentalNetworking: {
type: 'string',
default: 'auto',
description: refString('node.experimentalNetworking.description'),
enum: ['auto', 'on', 'off'],
},
},
defaults: nodeLaunchConfigDefaults,
};
Expand Down Expand Up @@ -1350,6 +1357,32 @@ const commands: ReadonlyArray<{
title: refString('commands.disableSourceMapStepping.label'),
icon: '$(compass)',
},
{
command: Commands.NetworkViewRequest,
title: refString('commands.networkViewRequest.label'),
icon: '$(arrow-right)',
},
{
command: Commands.NetworkClear,
title: refString('commands.networkClear.label'),
icon: '$(clear-all)',
},
{
command: Commands.NetworkOpenBody,
title: refString('commands.networkOpenBody.label'),
},
{
command: Commands.NetworkOpenBodyHex,
title: refString('commands.networkOpenBodyInHexEditor.label'),
},
{
command: Commands.NetworkReplayXHR,
title: refString('commands.networkReplayXHR.label'),
},
{
command: Commands.NetworkCopyUri,
title: refString('commands.networkCopyURI.label'),
},
];

const menus: Menus = {
Expand Down Expand Up @@ -1406,6 +1439,30 @@ const menus: Menus = {
command: Commands.CallersGoToTarget,
when: 'false',
},
{
command: Commands.NetworkCopyUri,
when: 'false',
},
{
command: Commands.NetworkOpenBody,
when: 'false',
},
{
command: Commands.NetworkOpenBodyHex,
when: 'false',
},
{
command: Commands.NetworkReplayXHR,
when: 'false',
},
{
command: Commands.NetworkViewRequest,
when: 'false',
},
{
command: Commands.NetworkClear,
when: 'false',
},
{
command: Commands.EnableSourceMapStepping,
when: ContextKey.IsMapSteppingDisabled,
Expand Down Expand Up @@ -1497,6 +1554,11 @@ const menus: Menus = {
`view == workbench.debug.callStackView && ${ContextKey.IsMapSteppingDisabled}`,
),
},
{
command: Commands.NetworkClear,
group: 'navigation',
when: `view == ${CustomViews.Network}`,
},
],
'view/item/context': [
{
Expand Down Expand Up @@ -1541,6 +1603,31 @@ const menus: Menus = {
group: 'inline',
when: `view == ${CustomViews.ExcludedCallers}`,
},
{
command: Commands.NetworkViewRequest,
group: 'inline@1',
when: `view == ${CustomViews.Network}`,
},
{
command: Commands.NetworkOpenBody,
group: 'body@1',
when: `view == ${CustomViews.Network}`,
},
{
command: Commands.NetworkOpenBodyHex,
group: 'body@2',
when: `view == ${CustomViews.Network}`,
},
{
command: Commands.NetworkCopyUri,
group: 'other@1',
when: `view == ${CustomViews.Network}`,
},
{
command: Commands.NetworkReplayXHR,
group: 'other@2',
when: `view == ${CustomViews.Network}`,
},
],
'editor/title': [
{
Expand Down Expand Up @@ -1594,12 +1681,18 @@ const views = {
name: 'Excluded Callers',
when: forAnyDebugType('debugType', 'jsDebugHasExcludedCallers'),
},
{
id: CustomViews.Network,
name: 'Network',
when: ContextKey.NetworkAvailable,
},
],
};

const activationEvents = new Set([
'onDebugDynamicConfigurations',
'onDebugInitialConfigurations',
`onFileSystem:${networkFilesystemScheme}`,
...[...debuggers.map(dbg => dbg.type), ...preferredDebugTypes.values()].map(
t => `onDebugResolve:${t}`,
),
Expand Down
26 changes: 26 additions & 0 deletions src/common/contributionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type Dap from '../dap/api';
import type { IAutoAttachInfo } from '../targets/node/bootloader/environment';
import type { ExcludedCaller } from '../ui/excludedCallersUI';
import type { IStartProfileArguments } from '../ui/profiling/uiProfileManager';
import type { NetworkRequest } from '../ui/networkTree';

export const enum Contributions {
BrowserBreakpointsView = 'jsBrowserBreakpoints',
Expand All @@ -24,6 +25,7 @@ export const enum CustomViews {
EventListenerBreakpoints = 'jsBrowserBreakpoints',
XHRFetchBreakpoints = 'jsXHRBreakpoints',
ExcludedCallers = 'jsExcludedCallers',
Network = 'jsDebugNetworkTree',
}

export const enum Commands {
Expand Down Expand Up @@ -60,6 +62,14 @@ export const enum Commands {
CallersRemoveAll = 'extension.js-debug.callers.removeAll',
CallersAdd = 'extension.js-debug.callers.add',
//#endregion
//#region Network view
NetworkViewRequest = 'extension.js-debug.network.viewRequest',
NetworkCopyUri = 'extension.js-debug.network.copyUri',
NetworkOpenBody = 'extension.js-debug.network.openBody',
NetworkOpenBodyHex = 'extension.js-debug.network.openBodyInHex',
NetworkReplayXHR = 'extension.js-debug.network.replayXHR',
NetworkClear = 'extension.js-debug.network.clear',
//#endregion
}

export const enum DebugType {
Expand Down Expand Up @@ -120,6 +130,12 @@ const commandsObj: { [K in Commands]: null } = {
[Commands.CallersRemoveAll]: null,
[Commands.EnableSourceMapStepping]: null,
[Commands.DisableSourceMapStepping]: null,
[Commands.NetworkViewRequest]: null,
[Commands.NetworkCopyUri]: null,
[Commands.NetworkOpenBody]: null,
[Commands.NetworkOpenBodyHex]: null,
[Commands.NetworkReplayXHR]: null,
[Commands.NetworkClear]: null,
};

/**
Expand Down Expand Up @@ -223,8 +239,16 @@ export interface ICommandTypes {
[Commands.CallersRemoveAll](): void;
[Commands.EnableSourceMapStepping](): void;
[Commands.DisableSourceMapStepping](): void;
[Commands.NetworkViewRequest](request: NetworkRequest): void;
[Commands.NetworkCopyUri](request: NetworkRequest): void;
[Commands.NetworkOpenBody](request: NetworkRequest): void;
[Commands.NetworkOpenBodyHex](request: NetworkRequest): void;
[Commands.NetworkReplayXHR](request: NetworkRequest): void;
[Commands.NetworkClear](): void;
}

export const networkFilesystemScheme = 'jsDebugNetworkFs';

/**
* Typed guard for registering a command.
*/
Expand Down Expand Up @@ -278,13 +302,15 @@ export const enum ContextKey {
CanPrettyPrint = 'jsDebugCanPrettyPrint',
IsProfiling = 'jsDebugIsProfiling',
IsMapSteppingDisabled = 'jsDebugIsMapSteppingDisabled',
NetworkAvailable = 'jsDebugNetworkAvailable',
}

export interface IContextKeyTypes {
[ContextKey.HasExcludedCallers]: boolean;
[ContextKey.CanPrettyPrint]: string[];
[ContextKey.IsProfiling]: boolean;
[ContextKey.IsMapSteppingDisabled]: boolean;
[ContextKey.NetworkAvailable]: boolean;
}

export const setContextKey = async <K extends keyof IContextKeyTypes>(
Expand Down
Loading

0 comments on commit 6e8a828

Please sign in to comment.