Skip to content

Commit 679105b

Browse files
authored
feat: don't attach debugger to terminal until a process starts (#221)
* feat: don't attach debugger to terminal until a process starts As referenced in #199, the debugger terminal experience today is awkward in that a session is 'running' as long as the terminal is open, even when no Node.js processes are currently active. This PR addresses that. We reuse the `TerminalNodeLauncher` to set up the terminal, but do so outside of the debug adapter. Instead, when we see a process launches and enters debug mode, we register it inside of a 'delegate' collection, and then send the delegate ID into `vscode.debug.startDebugging`. This pulls up the debug session as if it was just launched insider the adapter, and from then on it's identical to any other debug session. This PR also adds an configuration option that configures the default launch config used for the debugger terminal, and there's churn from typing the `targetOrigin` away from `any` throughout the process, now that I've fully internalized what it is and what it's used for :) This implementation has some indirection, but within the adapter's architecture I believe this is the best general approach. Due to the way in which we launch processes and the way the VS Code terminal behaves (both of which are optimal), we can't ever know that we have a debuggable target exists until it knocks on our door with a CDP connection. At that point, we need to delegate it into a debug session in some form or another. That's what I've tried to do here with a minimal amount of changes or knowledge-leaks into the rest of the code. Fixes #199 * fix: use terminal attach as delegate mode instead of different type
1 parent 53a36eb commit 679105b

33 files changed

+734
-193
lines changed

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
"micromatch": "^4.0.2",
5858
"source-map": "^0.7.3",
5959
"split2": "^3.1.1",
60-
"typescript": "^3.7.2",
60+
"typescript": "^3.7.4",
6161
"vscode-nls": "^4.1.1",
6262
"ws": "^7.0.1"
6363
},

src/binder.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import * as errors from './dap/errors';
2424
import { ILauncher, ILaunchResult, ITarget } from './targets/targets';
2525
import { RawTelemetryReporterToDap } from './telemetry/telemetryReporter';
2626
import { filterErrorsReportedToTelemetry } from './telemetry/unhandledErrorReporter';
27+
import { ITargetOrigin } from './targets/targetOrigin';
2728
import { IAsyncStackPolicy, getAsyncStackPolicy } from './adapter/asyncStackPolicy';
2829

2930
const localize = nls.loadMessageBundle();
@@ -44,7 +45,7 @@ export class Binder implements IDisposable {
4445
private _onTargetListChangedEmitter = new EventEmitter<void>();
4546
readonly onTargetListChanged = this._onTargetListChangedEmitter.event;
4647
private _dap: Promise<Dap.Api>;
47-
private _targetOrigin: any;
48+
private _targetOrigin: ITargetOrigin;
4849
private _launchParams?: AnyLaunchConfiguration;
4950
private _rawTelemetryReporter: RawTelemetryReporterToDap | undefined;
5051
private _clientCapabilities: Dap.InitializeParams | undefined;
@@ -54,7 +55,7 @@ export class Binder implements IDisposable {
5455
delegate: IBinderDelegate,
5556
connection: DapConnection,
5657
launchers: ILauncher[],
57-
targetOrigin: any,
58+
targetOrigin: ITargetOrigin,
5859
) {
5960
this._delegate = delegate;
6061
this._dap = connection.dap();

src/build/generate-contributions.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*--------------------------------------------------------*/
44
import { Contributions, IConfigurationTypes, Configuration } from '../common/contributionUtils';
55
import {
6+
IMandatedConfiguration,
67
AnyLaunchConfiguration,
78
ResolvingConfiguration,
89
INodeAttachConfiguration,
@@ -14,20 +15,18 @@ import {
1415
IChromeBaseConfiguration,
1516
IChromeLaunchConfiguration,
1617
IChromeAttachConfiguration,
17-
INodeTerminalConfiguration,
18+
ITerminalLaunchConfiguration,
1819
baseDefaults,
1920
} from '../configuration';
2021
import { JSONSchema6 } from 'json-schema';
2122
import strings from './strings';
2223
import { walkObject, sortKeys } from '../common/objUtils';
2324

2425
type OmittedKeysFromAttributes =
25-
| 'type'
26-
| 'request'
27-
| 'internalConsoleOptions'
28-
| 'name'
26+
| keyof IMandatedConfiguration
2927
| 'rootPath'
30-
| '__workspaceFolder';
28+
| '__workspaceFolder'
29+
| '__workspaceCachePath';
3130

3231
type ConfigurationAttributes<T> = {
3332
[K in keyof Omit<T, OmittedKeysFromAttributes>]: JSONSchema6 &
@@ -468,7 +467,7 @@ const nodeLaunchConfig: IDebugger<INodeLaunchConfiguration> = {
468467
},
469468
};
470469

471-
const nodeTerminalConfiguration: IDebugger<INodeTerminalConfiguration> = {
470+
const nodeTerminalConfiguration: IDebugger<ITerminalLaunchConfiguration> = {
472471
type: Contributions.TerminalDebugType,
473472
request: 'launch',
474473
label: refString('debug.terminal.label'),
@@ -730,6 +729,12 @@ const configurationSchema: ConfigurationAttributes<IConfigurationTypes> = {
730729
default: true,
731730
description: refString('configuration.warnOnLongPrediction'),
732731
},
732+
[Configuration.TerminalDebugConfig]: {
733+
type: 'object',
734+
description: refString('configuration.terminalOptions'),
735+
default: {},
736+
properties: nodeTerminalConfiguration.configurationAttributes as { [key: string]: JSONSchema6 },
737+
},
733738
};
734739

735740
process.stdout.write(

src/build/strings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ const strings = {
176176
'Whether a loading prompt should be shown if breakpoint prediction takes a while.',
177177
'configuration.npmScriptLensLocation':
178178
'Where a "Run" and "Debug" code lens should be shown in your npm scripts. It may be on "all", scripts, on "top" of the script section, or "never".',
179+
'configuration.terminalOptions':
180+
'Default launch options for the JavaScript debug terminal and npm scripts.',
179181
};
180182

181183
export default strings;

src/common/contributionUtils.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
* Copyright (C) Microsoft Corporation. All rights reserved.
33
*--------------------------------------------------------*/
44

5-
import { WorkspaceConfiguration } from 'vscode';
5+
import { Command, WorkspaceConfiguration, WorkspaceFolder, commands } from 'vscode';
6+
import { ITerminalLaunchConfiguration } from '../configuration';
67

78
export const enum Contributions {
89
PrettyPrintCommand = 'extension.NAMESPACE(node-debug).prettyPrint',
@@ -27,6 +28,7 @@ export const enum Contributions {
2728
export const enum Configuration {
2829
NpmScriptLens = 'debug.javascript.codelens.npmScripts',
2930
WarnOnLongPrediction = 'debug.javascript.warnOnLongPrediction',
31+
TerminalDebugConfig = 'debug.javascript.terminalOptions',
3032
}
3133

3234
/**
@@ -35,8 +37,44 @@ export const enum Configuration {
3537
export interface IConfigurationTypes {
3638
[Configuration.NpmScriptLens]: 'all' | 'top' | 'never';
3739
[Configuration.WarnOnLongPrediction]: boolean;
40+
[Configuration.TerminalDebugConfig]: Partial<ITerminalLaunchConfiguration>;
3841
}
3942

43+
export interface ICommandTypes {
44+
[Contributions.DebugNpmScript]: { args: [WorkspaceFolder?]; out: void };
45+
[Contributions.PickProcessCommand]: { args: []; out: string | null };
46+
[Contributions.AttachProcessCommand]: { args: []; out: void };
47+
[Contributions.CreateDebuggerTerminal]: { args: [string?, WorkspaceFolder?]; out: void };
48+
}
49+
50+
/**
51+
* Typed guard for registering a command.
52+
*/
53+
export const registerCommand = <K extends keyof ICommandTypes>(
54+
ns: typeof commands,
55+
key: K,
56+
fn: (...args: ICommandTypes[K]['args']) => Promise<ICommandTypes[K]['out']>,
57+
) => ns.registerCommand(key, fn);
58+
59+
/**
60+
* Typed guard for running a command.
61+
*/
62+
export const runCommand = async <K extends keyof ICommandTypes>(
63+
ns: typeof commands,
64+
key: K,
65+
...args: ICommandTypes[K]['args']
66+
): Promise<ICommandTypes[K]['out']> => await ns.executeCommand(key, ...args);
67+
68+
/**
69+
* Typed guard for creating a {@link Command} interface.
70+
*/
71+
export const asCommand = <K extends keyof ICommandTypes>(command: {
72+
title: string;
73+
command: K;
74+
tooltip?: string;
75+
arguments: ICommandTypes[K]['args'];
76+
}): Command => command;
77+
4078
/**
4179
* Typed guard for reading a contributed config.
4280
*/

src/common/logging/logger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ export const assert = <T>(
187187
): assertion is T => {
188188
if (assertion === false || assertion === undefined || assertion === null) {
189189
logger.error(LogTag.RuntimeAssertion, message, { error: new Error('Assertion failed') });
190+
debugger; // break when running in development
190191
return false;
191192
}
192193

src/common/objUtils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ export const removeNulls = <V>(obj: { [key: string]: V | null }) =>
88
export const removeUndefined = <V>(obj: { [key: string]: V | undefined }) =>
99
filterValues(obj, (v): v is V => v !== undefined);
1010

11+
/**
12+
* Asserts that the value is never. If this function is reached, it throws.
13+
*/
14+
export const assertNever = (value: never, message: string): never => {
15+
debugger;
16+
throw new Error(message.replace('{value}', JSON.stringify(value)));
17+
};
18+
1119
/**
1220
* Filters the object by value.
1321
*/

0 commit comments

Comments
 (0)