Skip to content

Commit e1b63cc

Browse files
committed
Refactor LanguageClientConsumer to wait properly
This makes a lot more sense and fixes a long-standing bug. Also cleaned up a lot while at it. 1. `SessionManager` is created and the features are injected into the session manager with the intention of them being able to "register" their language client event handlers in the period between when the client is created and when it is started. 2. For each feature, if it gets to a point where it needs the language client, we want them to wait on a promise for the "started" client to be provided from the session manager before continuing. 3. Once it is started, the session manager passes the client to `LanguageClientConsumer` which resolves the promise for the features so they can continue.
1 parent e831373 commit e1b63cc

15 files changed

+267
-394
lines changed

src/features/CodeActions.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,25 @@ import vscode = require("vscode");
55
import { ILogger } from "../logging";
66

77
export class CodeActionsFeature implements vscode.Disposable {
8-
private showDocumentationCommand: vscode.Disposable;
8+
private command: vscode.Disposable;
99

1010
constructor(private log: ILogger) {
1111
// NOTE: While not exposed to the user via package.json, this is
1212
// required as the server's code action sends across a command name.
1313
//
1414
// TODO: In the far future with LSP 3.19 the server can just set a URL
1515
// and this can go away. See https://github.com/microsoft/language-server-protocol/issues/1548
16-
this.showDocumentationCommand =
16+
this.command =
1717
vscode.commands.registerCommand("PowerShell.ShowCodeActionDocumentation", async (ruleName: string) => {
1818
await this.showRuleDocumentation(ruleName);
1919
});
2020
}
2121

2222
public dispose(): void {
23-
this.showDocumentationCommand.dispose();
23+
this.command.dispose();
2424
}
2525

26-
public async showRuleDocumentation(ruleId: string): Promise<void> {
26+
private async showRuleDocumentation(ruleId: string): Promise<void> {
2727
const pssaDocBaseURL = "https://docs.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/";
2828

2929
if (!ruleId) {

src/features/Console.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,8 @@ export class ConsoleFeature extends LanguageClientConsumer {
200200
} else {
201201
selectionRange = editor.document.lineAt(editor.selection.start.line).range;
202202
}
203-
204-
await this.languageClient?.sendRequest(EvaluateRequestType, {
203+
const client = await LanguageClientConsumer.getLanguageClient();
204+
await client.sendRequest(EvaluateRequestType, {
205205
expression: editor.document.getText(selectionRange),
206206
});
207207

@@ -217,19 +217,19 @@ export class ConsoleFeature extends LanguageClientConsumer {
217217
for (const command of this.commands) {
218218
command.dispose();
219219
}
220+
220221
for (const handler of this.handlers) {
221222
handler.dispose();
222223
}
223224
}
224225

225-
public override setLanguageClient(languageClient: LanguageClient): void {
226-
this.languageClient = languageClient;
226+
public override onLanguageClientSet(languageClient: LanguageClient): void {
227227
this.handlers = [
228-
this.languageClient.onRequest(
228+
languageClient.onRequest(
229229
ShowChoicePromptRequestType,
230230
(promptDetails) => showChoicePrompt(promptDetails)),
231231

232-
this.languageClient.onRequest(
232+
languageClient.onRequest(
233233
ShowInputPromptRequestType,
234234
(promptDetails) => showInputPrompt(promptDetails)),
235235
];

src/features/DebugSession.ts

+79-204
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export class DebugSessionFeature extends LanguageClientConsumer
126126
private sessionCount = 1;
127127
private tempDebugProcess: PowerShellProcess | undefined;
128128
private tempSessionDetails: IEditorServicesSessionDetails | undefined;
129+
private commands: Disposable[];
129130
private handlers: Disposable[] = [];
130131

131132
constructor(context: ExtensionContext, private sessionManager: SessionManager, private logger: ILogger) {
@@ -138,15 +139,34 @@ export class DebugSessionFeature extends LanguageClientConsumer
138139
context.subscriptions.push(debug.registerDebugConfigurationProvider("PowerShell", this, triggerKind));
139140
});
140141
context.subscriptions.push(debug.registerDebugAdapterDescriptorFactory("PowerShell", this));
142+
143+
// NOTE: While process and runspace IDs are numbers, command
144+
// substitutions in VS Code debug configurations are required to return
145+
// strings. Hence to the `toString()` on these.
146+
this.commands = [
147+
commands.registerCommand("PowerShell.PickPSHostProcess", async () => {
148+
149+
const processId = await this.pickPSHostProcess();
150+
return processId?.toString();
151+
}),
152+
commands.registerCommand("PowerShell.PickRunspace", async (processId) => {
153+
const runspace = await this.pickRunspace(processId);
154+
return runspace?.toString();
155+
}, this),
156+
];
141157
}
142158

143159
public dispose(): void {
160+
for (const command of this.commands) {
161+
command.dispose();
162+
}
163+
144164
for (const handler of this.handlers) {
145165
handler.dispose();
146166
}
147167
}
148168

149-
public override setLanguageClient(languageClient: LanguageClient): void {
169+
public override onLanguageClientSet(languageClient: LanguageClient): void {
150170
this.handlers = [
151171
languageClient.onNotification(
152172
StartDebuggerNotificationType,
@@ -477,7 +497,7 @@ export class DebugSessionFeature extends LanguageClientConsumer
477497

478498
// If nothing is set, prompt for the processId.
479499
if (!config.customPipeName && !config.processId) {
480-
config.processId = await commands.executeCommand("PowerShell.PickPSHostProcess");
500+
config.processId = await this.pickPSHostProcess();
481501
// No process selected. Cancel attach.
482502
if (!config.processId) {
483503
return PREVENT_DEBUG_START;
@@ -511,10 +531,66 @@ export class DebugSessionFeature extends LanguageClientConsumer
511531

512532
return config;
513533
}
534+
535+
private async pickPSHostProcess(): Promise<number | undefined> {
536+
// Start with the current PowerShell process in the list.
537+
const items: IProcessItem[] = [];
538+
539+
const client = await LanguageClientConsumer.getLanguageClient();
540+
const response = await client.sendRequest(GetPSHostProcessesRequestType, {});
541+
for (const process of response) {
542+
let windowTitle = "";
543+
if (process.mainWindowTitle) {
544+
windowTitle = `, ${process.mainWindowTitle}`;
545+
}
546+
547+
items.push({
548+
label: process.processName,
549+
description: `PID: ${process.processId.toString()}${windowTitle}`,
550+
processId: process.processId,
551+
});
552+
}
553+
554+
if (items.length === 0) {
555+
return Promise.reject("There are no PowerShell host processes to attach.");
556+
}
557+
558+
const options: QuickPickOptions = {
559+
placeHolder: "Select a PowerShell host process to attach.",
560+
matchOnDescription: true,
561+
matchOnDetail: true,
562+
};
563+
564+
const item = await window.showQuickPick(items, options);
565+
566+
return item?.processId ?? undefined;
567+
}
568+
569+
private async pickRunspace(processId: number): Promise<number | undefined> {
570+
const client = await LanguageClientConsumer.getLanguageClient();
571+
const response = await client.sendRequest(GetRunspaceRequestType, { processId });
572+
const items: IRunspaceItem[] = [];
573+
for (const runspace of response) {
574+
items.push({
575+
label: runspace.name,
576+
description: `ID: ${runspace.id} - ${runspace.availability}`,
577+
id: runspace.id,
578+
});
579+
}
580+
581+
const options: QuickPickOptions = {
582+
placeHolder: "Select PowerShell runspace to debug",
583+
matchOnDescription: true,
584+
matchOnDetail: true,
585+
};
586+
587+
const item = await window.showQuickPick(items, options);
588+
589+
return item?.id ?? undefined;
590+
}
514591
}
515592

516593
export class SpecifyScriptArgsFeature implements Disposable {
517-
518594
private command: Disposable;
519595
private context: ExtensionContext;
520596

@@ -572,111 +648,6 @@ interface IPSHostProcessInfo {
572648
export const GetPSHostProcessesRequestType =
573649
new RequestType<IGetPSHostProcessesArguments, IPSHostProcessInfo[], string>("powerShell/getPSHostProcesses");
574650

575-
export class PickPSHostProcessFeature extends LanguageClientConsumer {
576-
private command: Disposable;
577-
private waitingForClientToken?: CancellationTokenSource;
578-
private getLanguageClientResolve?: (value: LanguageClient) => void;
579-
580-
constructor(private logger: ILogger) {
581-
super();
582-
583-
this.command = commands.registerCommand("PowerShell.PickPSHostProcess", async () => {
584-
return this.pickPSHostProcess();
585-
});
586-
}
587-
588-
public override setLanguageClient(languageClient: LanguageClient): void {
589-
this.languageClient = languageClient;
590-
591-
if (this.waitingForClientToken && this.getLanguageClientResolve) {
592-
this.getLanguageClientResolve(this.languageClient);
593-
this.clearWaitingToken();
594-
}
595-
}
596-
597-
public dispose(): void {
598-
this.command.dispose();
599-
}
600-
601-
private getLanguageClient(): Promise<LanguageClient> {
602-
if (this.languageClient !== undefined) {
603-
return Promise.resolve(this.languageClient);
604-
} else {
605-
// If PowerShell isn't finished loading yet, show a loading message
606-
// until the LanguageClient is passed on to us
607-
this.waitingForClientToken = new CancellationTokenSource();
608-
609-
return new Promise<LanguageClient>(
610-
(resolve, reject) => {
611-
this.getLanguageClientResolve = resolve;
612-
613-
void window
614-
.showQuickPick(
615-
["Cancel"],
616-
{ placeHolder: "Attach to PowerShell host process: Please wait, starting PowerShell..." },
617-
this.waitingForClientToken?.token)
618-
.then((response) => {
619-
if (response === "Cancel") {
620-
this.clearWaitingToken();
621-
reject();
622-
}
623-
}, undefined);
624-
625-
// Cancel the loading prompt after 60 seconds
626-
setTimeout(() => {
627-
if (this.waitingForClientToken) {
628-
this.clearWaitingToken();
629-
reject();
630-
631-
void this.logger.writeAndShowError("Attach to PowerShell host process: PowerShell session took too long to start.");
632-
}
633-
}, 60000);
634-
},
635-
);
636-
}
637-
}
638-
639-
private async pickPSHostProcess(): Promise<number | undefined> {
640-
// We need the language client in order to send the request.
641-
await this.getLanguageClient();
642-
643-
// Start with the current PowerShell process in the list.
644-
const items: IProcessItem[] = [];
645-
646-
const response = await this.languageClient?.sendRequest(GetPSHostProcessesRequestType, {});
647-
for (const process of response ?? []) {
648-
let windowTitle = "";
649-
if (process.mainWindowTitle) {
650-
windowTitle = `, ${process.mainWindowTitle}`;
651-
}
652-
653-
items.push({
654-
label: process.processName,
655-
description: `PID: ${process.processId.toString()}${windowTitle}`,
656-
processId: process.processId,
657-
});
658-
}
659-
660-
if (items.length === 0) {
661-
return Promise.reject("There are no PowerShell host processes to attach to.");
662-
}
663-
664-
const options: QuickPickOptions = {
665-
placeHolder: "Select a PowerShell host process to attach to",
666-
matchOnDescription: true,
667-
matchOnDetail: true,
668-
};
669-
670-
const item = await window.showQuickPick(items, options);
671-
672-
return item?.processId ?? undefined;
673-
}
674-
675-
private clearWaitingToken(): void {
676-
this.waitingForClientToken?.dispose();
677-
this.waitingForClientToken = undefined;
678-
}
679-
}
680651

681652
interface IRunspaceItem extends QuickPickItem {
682653
id: number; // payload for the QuickPick UI
@@ -694,99 +665,3 @@ interface IRunspace {
694665

695666
export const GetRunspaceRequestType =
696667
new RequestType<IGetRunspaceRequestArguments, IRunspace[], string>("powerShell/getRunspace");
697-
698-
export class PickRunspaceFeature extends LanguageClientConsumer {
699-
private command: Disposable;
700-
private waitingForClientToken?: CancellationTokenSource;
701-
private getLanguageClientResolve?: (value: LanguageClient) => void;
702-
703-
constructor(private logger: ILogger) {
704-
super();
705-
706-
this.command = commands.registerCommand("PowerShell.PickRunspace",
707-
async (processId) => { return this.pickRunspace(processId); },
708-
this);
709-
}
710-
711-
public override setLanguageClient(languageClient: LanguageClient): void {
712-
this.languageClient = languageClient;
713-
714-
if (this.waitingForClientToken && this.getLanguageClientResolve) {
715-
this.getLanguageClientResolve(this.languageClient);
716-
this.clearWaitingToken();
717-
}
718-
}
719-
720-
public dispose(): void {
721-
this.command.dispose();
722-
}
723-
724-
private getLanguageClient(): Promise<LanguageClient> {
725-
if (this.languageClient) {
726-
return Promise.resolve(this.languageClient);
727-
} else {
728-
// If PowerShell isn't finished loading yet, show a loading message
729-
// until the LanguageClient is passed on to us
730-
this.waitingForClientToken = new CancellationTokenSource();
731-
732-
return new Promise<LanguageClient>(
733-
(resolve, reject) => {
734-
this.getLanguageClientResolve = resolve;
735-
736-
void window
737-
.showQuickPick(
738-
["Cancel"],
739-
{ placeHolder: "Attach to PowerShell host process: Please wait, starting PowerShell..." },
740-
this.waitingForClientToken?.token)
741-
.then((response) => {
742-
if (response === "Cancel") {
743-
this.clearWaitingToken();
744-
reject();
745-
}
746-
}, undefined);
747-
748-
// Cancel the loading prompt after 60 seconds
749-
setTimeout(() => {
750-
if (this.waitingForClientToken) {
751-
this.clearWaitingToken();
752-
reject();
753-
754-
void this.logger.writeAndShowError(
755-
"Attach to PowerShell host process: PowerShell session took too long to start.");
756-
}
757-
}, 60000);
758-
},
759-
);
760-
}
761-
}
762-
763-
private async pickRunspace(processId: number): Promise<number | undefined> {
764-
// We need the language client in order to send the request.
765-
await this.getLanguageClient();
766-
767-
const response = await this.languageClient?.sendRequest(GetRunspaceRequestType, { processId });
768-
const items: IRunspaceItem[] = [];
769-
for (const runspace of response ?? []) {
770-
items.push({
771-
label: runspace.name,
772-
description: `ID: ${runspace.id} - ${runspace.availability}`,
773-
id: runspace.id,
774-
});
775-
}
776-
777-
const options: QuickPickOptions = {
778-
placeHolder: "Select PowerShell runspace to debug",
779-
matchOnDescription: true,
780-
matchOnDetail: true,
781-
};
782-
783-
const item = await window.showQuickPick(items, options);
784-
785-
return item?.id ?? undefined;
786-
}
787-
788-
private clearWaitingToken(): void {
789-
this.waitingForClientToken?.dispose();
790-
this.waitingForClientToken = undefined;
791-
}
792-
}

0 commit comments

Comments
 (0)