Skip to content

Commit 9cd6b91

Browse files
Enable ability to trace DAP messages at client side (#5064)
* Enable ability to trace DAP messages at client side * Update setting description --------- Co-authored-by: Andy Jordan <[email protected]>
1 parent e61b612 commit 9cd6b91

File tree

4 files changed

+84
-7
lines changed

4 files changed

+84
-7
lines changed

package.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -1011,7 +1011,12 @@
10111011
"verbose"
10121012
],
10131013
"default": "off",
1014-
"markdownDescription": "Traces the communication between VS Code and the PowerShell Editor Services language server. **This setting is only meant for extension developers!**"
1014+
"markdownDescription": "Traces the communication between VS Code and the PowerShell Editor Services [LSP Server](https://microsoft.github.io/language-server-protocol/). **only for extension developers and issue troubleshooting!**"
1015+
},
1016+
"powershell.trace.dap": {
1017+
"type": "boolean",
1018+
"default": false,
1019+
"markdownDescription": "Traces the communication between VS Code and the PowerShell Editor Services [DAP Server](https://microsoft.github.io/debug-adapter-protocol/). **This setting is only meant for extension developers and issue troubleshooting!**"
10151020
}
10161021
}
10171022
},

src/features/DebugSession.ts

+76-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ import {
2222
InputBoxOptions,
2323
QuickPickItem,
2424
QuickPickOptions,
25-
DebugConfigurationProviderTriggerKind
25+
DebugConfigurationProviderTriggerKind,
26+
DebugAdapterTrackerFactory,
27+
DebugAdapterTracker,
28+
LogOutputChannel
2629
} from "vscode";
2730
import type { DebugProtocol } from "@vscode/debugprotocol";
2831
import { NotificationType, RequestType } from "vscode-languageclient";
@@ -126,6 +129,7 @@ export class DebugSessionFeature extends LanguageClientConsumer
126129
private tempSessionDetails: IEditorServicesSessionDetails | undefined;
127130
private commands: Disposable[] = [];
128131
private handlers: Disposable[] = [];
132+
private adapterName = "PowerShell";
129133

130134
constructor(context: ExtensionContext, private sessionManager: SessionManager, private logger: ILogger) {
131135
super();
@@ -165,12 +169,17 @@ export class DebugSessionFeature extends LanguageClientConsumer
165169
DebugConfigurationProviderTriggerKind.Dynamic
166170
];
167171

172+
168173
for (const triggerKind of triggers) {
169174
context.subscriptions.push(
170-
debug.registerDebugConfigurationProvider("PowerShell", this, triggerKind));
175+
debug.registerDebugConfigurationProvider(this.adapterName, this, triggerKind)
176+
);
171177
}
172178

173-
context.subscriptions.push(debug.registerDebugAdapterDescriptorFactory("PowerShell", this));
179+
context.subscriptions.push(
180+
debug.registerDebugAdapterTrackerFactory(this.adapterName, new PowerShellDebugAdapterTrackerFactory(this.adapterName)),
181+
debug.registerDebugAdapterDescriptorFactory(this.adapterName, this)
182+
);
174183
}
175184

176185
public override onLanguageClientSet(languageClient: LanguageClient): void {
@@ -595,6 +604,70 @@ export class DebugSessionFeature extends LanguageClientConsumer
595604
}
596605
}
597606

607+
class PowerShellDebugAdapterTrackerFactory implements DebugAdapterTrackerFactory, Disposable {
608+
disposables: Disposable[] = [];
609+
dapLogEnabled: boolean = workspace.getConfiguration("powershell").get<boolean>("trace.dap") ?? false;
610+
constructor(private adapterName = "PowerShell") {
611+
this.disposables.push(workspace.onDidChangeConfiguration(change => {
612+
if (
613+
change.affectsConfiguration("powershell.trace.dap")
614+
) {
615+
this.dapLogEnabled = workspace.getConfiguration("powershell").get<boolean>("trace.dap") ?? false;
616+
if (this.dapLogEnabled) {
617+
// Trigger the output pane to appear. This gives the user time to position it before starting a debug.
618+
this.log?.show(true);
619+
}
620+
}
621+
}));
622+
}
623+
624+
/* We want to use a shared output log for separate debug sessions as usually only one is running at a time and we
625+
* dont need an output window for every debug session. We also want to leave it active so user can copy and paste
626+
* even on run end. When user changes the setting and disables it getter will return undefined, which will result
627+
* in a noop for the logging activities, effectively pausing logging but not disposing the output channel. If the
628+
* user re-enables, then logging resumes.
629+
*/
630+
_log: LogOutputChannel | undefined;
631+
get log(): LogOutputChannel | undefined {
632+
if (this.dapLogEnabled && this._log === undefined) {
633+
this._log = window.createOutputChannel(`${this.adapterName} Trace - DAP`, { log: true });
634+
this.disposables.push(this._log);
635+
}
636+
return this.dapLogEnabled ? this._log : undefined;
637+
}
638+
639+
createDebugAdapterTracker(session: DebugSession): DebugAdapterTracker {
640+
const sessionInfo = `${this.adapterName} Debug Session: ${session.name} [${session.id}]`;
641+
return {
642+
onWillStartSession: () => this.log?.info(`Starting ${sessionInfo}. Set log level to trace to see DAP messages beyond errors`),
643+
onWillStopSession: () => this.log?.info(`Stopping ${sessionInfo}`),
644+
onExit: code => this.log?.info(`${sessionInfo} exited with code ${code}`),
645+
onWillReceiveMessage: (m): void => {
646+
this.log?.debug(`▶️${m.seq} ${m.type}: ${m.command}`);
647+
if (m.arguments && (Array.isArray(m.arguments) ? m.arguments.length > 0 : Object.keys(m.arguments).length > 0)) {
648+
this.log?.trace(`${m.seq}: ` + JSON.stringify(m.arguments, undefined, 2));
649+
}
650+
},
651+
onDidSendMessage: (m):void => {
652+
const responseSummary = m.request_seq !== undefined
653+
? `${m.success ? "✅" : "❌"}${m.request_seq} ${m.type}(${m.seq}): ${m.command}`
654+
: `◀️${m.seq} ${m.type}: ${m.event ?? m.command}`;
655+
this.log?.debug(
656+
responseSummary
657+
);
658+
if (m.body && (Array.isArray(m.body) ? m.body.length > 0 : Object.keys(m.body).length > 0)) {
659+
this.log?.trace(`${m.seq}: ` + JSON.stringify(m.body, undefined, 2));
660+
}
661+
},
662+
onError: e => this.log?.error(e),
663+
};
664+
}
665+
666+
dispose(): void {
667+
this.disposables.forEach(d => d.dispose());
668+
}
669+
}
670+
598671
export class SpecifyScriptArgsFeature implements Disposable {
599672
private command: Disposable;
600673
private context: ExtensionContext;

src/session.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -623,8 +623,6 @@ export class SessionManager implements Middleware {
623623
});
624624
});
625625
};
626-
627-
628626
const clientOptions: LanguageClientOptions = {
629627
documentSelector: this.documentSelector,
630628
synchronize: {
@@ -660,6 +658,7 @@ export class SessionManager implements Middleware {
660658
},
661659
revealOutputChannelOn: RevealOutputChannelOn.Never,
662660
middleware: this,
661+
traceOutputChannel: vscode.window.createOutputChannel("PowerShell Trace - LSP", {log: true}),
663662
};
664663

665664
const languageClient = new LanguageClient("powershell", "PowerShell Editor Services Client", connectFunc, clientOptions);

test/features/DebugSession.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ describe("DebugSessionFeature", () => {
6969
createDebugSessionFeatureStub({context: context});
7070
assert.ok(registerFactoryStub.calledOnce, "Debug adapter factory method called");
7171
assert.ok(registerProviderStub.calledTwice, "Debug config provider registered for both Initial and Dynamic");
72-
assert.equal(context.subscriptions.length, 3, "DebugSessionFeature disposables populated");
72+
assert.equal(context.subscriptions.length, 4, "DebugSessionFeature disposables populated");
7373
// TODO: Validate the registration content, such as the language name
7474
});
7575
});

0 commit comments

Comments
 (0)