Skip to content

Commit

Permalink
Enable ability to trace DAP messages at client side
Browse files Browse the repository at this point in the history
  • Loading branch information
JustinGrote committed Oct 14, 2024
1 parent e61b612 commit d7ad116
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 7 deletions.
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1011,7 +1011,12 @@
"verbose"
],
"default": "off",
"markdownDescription": "Traces the communication between VS Code and the PowerShell Editor Services language server. **This setting is only meant for extension developers!**"
"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!**"
},
"powershell.trace.dap": {
"type": "boolean",
"default": false,
"markdownDescription": "Traces the communication between VS Code and the PowerShell Editor Services [DAP Server](https://microsoft.github.io/debug-adapter-protocol/). **only for extension developers and issue troubleshooting!**"
}
}
},
Expand Down
79 changes: 76 additions & 3 deletions src/features/DebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ import {
InputBoxOptions,
QuickPickItem,
QuickPickOptions,
DebugConfigurationProviderTriggerKind
DebugConfigurationProviderTriggerKind,
DebugAdapterTrackerFactory,
DebugAdapterTracker,
LogOutputChannel
} from "vscode";
import type { DebugProtocol } from "@vscode/debugprotocol";
import { NotificationType, RequestType } from "vscode-languageclient";
Expand Down Expand Up @@ -126,6 +129,7 @@ export class DebugSessionFeature extends LanguageClientConsumer
private tempSessionDetails: IEditorServicesSessionDetails | undefined;
private commands: Disposable[] = [];
private handlers: Disposable[] = [];
private adapterName = "PowerShell";

constructor(context: ExtensionContext, private sessionManager: SessionManager, private logger: ILogger) {
super();
Expand Down Expand Up @@ -165,12 +169,17 @@ export class DebugSessionFeature extends LanguageClientConsumer
DebugConfigurationProviderTriggerKind.Dynamic
];


for (const triggerKind of triggers) {
context.subscriptions.push(
debug.registerDebugConfigurationProvider("PowerShell", this, triggerKind));
debug.registerDebugConfigurationProvider(this.adapterName, this, triggerKind)
);
}

context.subscriptions.push(debug.registerDebugAdapterDescriptorFactory("PowerShell", this));
context.subscriptions.push(
debug.registerDebugAdapterTrackerFactory(this.adapterName, new PowerShellDebugAdapterTrackerFactory(this.adapterName)),
debug.registerDebugAdapterDescriptorFactory(this.adapterName, this)
);
}

public override onLanguageClientSet(languageClient: LanguageClient): void {
Expand Down Expand Up @@ -595,6 +604,70 @@ export class DebugSessionFeature extends LanguageClientConsumer
}
}

class PowerShellDebugAdapterTrackerFactory implements DebugAdapterTrackerFactory, Disposable {
disposables: Disposable[] = [];
dapLogEnabled: boolean = workspace.getConfiguration("powershell").get<boolean>("trace.dap") ?? false;
constructor(private adapterName = "PowerShell") {
this.disposables.push(workspace.onDidChangeConfiguration(change => {
if (
change.affectsConfiguration("powershell.trace.dap")
) {
this.dapLogEnabled = workspace.getConfiguration("powershell").get<boolean>("trace.dap") ?? false;
if (this.dapLogEnabled) {
// Trigger the output pane to appear. This gives the user time to position it before starting a debug.
this.log?.show(true);
}
}
}));
}

/* We want to use a shared output log for separate debug sessions as usually only one is running at a time and we
* dont need an output window for every debug session. We also want to leave it active so user can copy and paste
* even on run end. When user changes the setting and disables it getter will return undefined, which will result
* in a noop for the logging activities, effectively pausing logging but not disposing the output channel. If the
* user re-enables, then logging resumes.
*/
_log: LogOutputChannel | undefined;
get log(): LogOutputChannel | undefined {
if (this.dapLogEnabled && this._log === undefined) {
this._log = window.createOutputChannel(`${this.adapterName} Trace - DAP`, { log: true });
this.disposables.push(this._log);
}
return this.dapLogEnabled ? this._log : undefined;
}

createDebugAdapterTracker(session: DebugSession): DebugAdapterTracker {
const sessionInfo = `${this.adapterName} Debug Session: ${session.name} [${session.id}]`;
return {
onWillStartSession: () => this.log?.info(`Starting ${sessionInfo}. Set log level to trace to see DAP messages beyond errors`),
onWillStopSession: () => this.log?.info(`Stopping ${sessionInfo}`),
onExit: code => this.log?.info(`${sessionInfo} exited with code ${code}`),
onWillReceiveMessage: (m): void => {
this.log?.debug(`▶️${m.seq} ${m.type}: ${m.command}`);
if (m.arguments && (Array.isArray(m.arguments) ? m.arguments.length > 0 : Object.keys(m.arguments).length > 0)) {
this.log?.trace(`${m.seq}: ` + JSON.stringify(m.arguments, undefined, 2));
}
},
onDidSendMessage: (m):void => {
const responseSummary = m.request_seq !== undefined
? `${m.success ? "✅" : "❌"}${m.request_seq} ${m.type}(${m.seq}): ${m.command}`
: `◀️${m.seq} ${m.type}: ${m.event ?? m.command}`;
this.log?.debug(
responseSummary
);
if (m.body && (Array.isArray(m.body) ? m.body.length > 0 : Object.keys(m.body).length > 0)) {
this.log?.trace(`${m.seq}: ` + JSON.stringify(m.body, undefined, 2));
}
},
onError: e => this.log?.error(e),
};
}

dispose(): void {
this.disposables.forEach(d => d.dispose());
}
}

export class SpecifyScriptArgsFeature implements Disposable {
private command: Disposable;
private context: ExtensionContext;
Expand Down
3 changes: 1 addition & 2 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,8 +623,6 @@ export class SessionManager implements Middleware {
});
});
};


const clientOptions: LanguageClientOptions = {
documentSelector: this.documentSelector,
synchronize: {
Expand Down Expand Up @@ -660,6 +658,7 @@ export class SessionManager implements Middleware {
},
revealOutputChannelOn: RevealOutputChannelOn.Never,
middleware: this,
traceOutputChannel: vscode.window.createOutputChannel("PowerShell Trace - LSP", {log: true}),
};

const languageClient = new LanguageClient("powershell", "PowerShell Editor Services Client", connectFunc, clientOptions);
Expand Down
2 changes: 1 addition & 1 deletion test/features/DebugSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe("DebugSessionFeature", () => {
createDebugSessionFeatureStub({context: context});
assert.ok(registerFactoryStub.calledOnce, "Debug adapter factory method called");
assert.ok(registerProviderStub.calledTwice, "Debug config provider registered for both Initial and Dynamic");
assert.equal(context.subscriptions.length, 3, "DebugSessionFeature disposables populated");
assert.equal(context.subscriptions.length, 4, "DebugSessionFeature disposables populated");
// TODO: Validate the registration content, such as the language name
});
});
Expand Down

0 comments on commit d7ad116

Please sign in to comment.