Skip to content

Commit 4bf0aa0

Browse files
Handle PowerShell failing to start with actionable fixes (#4532)
Significantly improves our handling of PowerShell failing to start for various reasons.
1 parent 266ac53 commit 4bf0aa0

File tree

3 files changed

+75
-34
lines changed

3 files changed

+75
-34
lines changed

src/features/GenerateBugReport.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,9 @@ export class GenerateBugReportFeature implements vscode.Disposable {
3333
if (this.sessionManager.PowerShellExeDetails === undefined) {
3434
return "Session's PowerShell details are unknown!";
3535
}
36-
37-
const powerShellExePath = this.sessionManager.PowerShellExeDetails.exePath;
38-
const powerShellArgs = [ "-NoProfile", "-Command", "$PSVersionTable | Out-String" ];
39-
const child = child_process.spawnSync(powerShellExePath, powerShellArgs);
36+
const child = child_process.spawnSync(
37+
this.sessionManager.PowerShellExeDetails.exePath,
38+
["-NoProfile", "-NoLogo", "-Command", "$PSVersionTable | Out-String"]);
4039
// Replace semicolons as they'll cause the URI component to truncate
4140
return child.stdout.toString().trim().replace(";", ",");
4241
}

src/process.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ILogger } from "./logging";
88
import Settings = require("./settings");
99
import utils = require("./utils");
1010
import { IEditorServicesSessionDetails } from "./session";
11+
import { promisify } from "util";
1112

1213
export class PowerShellProcess {
1314
// This is used to warn the user that the extension is taking longer than expected to startup.
@@ -134,6 +135,13 @@ export class PowerShellProcess {
134135
return sessionDetails;
135136
}
136137

138+
// This function should only be used after a failure has occurred because it is slow!
139+
public async getVersionCli(): Promise<string> {
140+
const exec = promisify(cp.execFile);
141+
const { stdout } = await exec(this.exePath, ["-NoProfile", "-NoLogo", "-Command", "$PSVersionTable.PSVersion.ToString()"]);
142+
return stdout.trim();
143+
}
144+
137145
// Returns the process Id of the consoleTerminal
138146
public async getPid(): Promise<number | undefined> {
139147
if (!this.consoleTerminal) { return undefined; }
@@ -148,13 +156,13 @@ export class PowerShellProcess {
148156
// Clean up the session file
149157
this.logger.write("Terminating PowerShell process...");
150158

151-
await PowerShellProcess.deleteSessionFile(this.sessionFilePath);
159+
this.consoleTerminal?.dispose();
160+
this.consoleTerminal = undefined;
152161

153162
this.consoleCloseSubscription?.dispose();
154163
this.consoleCloseSubscription = undefined;
155164

156-
this.consoleTerminal?.dispose();
157-
this.consoleTerminal = undefined;
165+
await PowerShellProcess.deleteSessionFile(this.sessionFilePath);
158166
}
159167

160168
public sendKeyPress(): void {

src/session.ts

+61-27
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
OperatingSystem, PowerShellExeFinder
2525
} from "./platform";
2626
import { LanguageClientConsumer } from "./languageClientConsumer";
27-
import { SemVer } from "semver";
27+
import { SemVer, satisfies } from "semver";
2828

2929
export enum SessionStatus {
3030
NeverStarted,
@@ -458,35 +458,57 @@ export class SessionManager implements Middleware {
458458
try {
459459
this.sessionDetails = await languageServerProcess.start("EditorServices");
460460
} catch (err) {
461-
this.setSessionFailure("PowerShell process failed to start: ", err instanceof Error ? err.message : "unknown");
461+
// We should kill the process in case it's stuck.
462+
void languageServerProcess.dispose();
463+
464+
// PowerShell never started, probably a bad version!
465+
const version = await languageServerProcess.getVersionCli();
466+
let shouldUpdate = true;
467+
if (satisfies(version, "<5.1.0")) {
468+
void this.setSessionFailedGetPowerShell(`PowerShell ${version} is not supported, please update!`);
469+
} else if (satisfies(version, ">=5.1.0 <6.0.0")) {
470+
void this.setSessionFailedGetPowerShell("It looks like you're trying to use Windows PowerShell, which is supported on a best-effort basis. Can you try PowerShell 7?");
471+
} else if (satisfies(version, ">=6.0.0 <7.2.0")) {
472+
void this.setSessionFailedGetPowerShell(`PowerShell ${version} has reached end-of-support, please update!`);
473+
} else {
474+
shouldUpdate = false;
475+
void this.setSessionFailedOpenBug("PowerShell language server process didn't start!");
476+
}
477+
if (shouldUpdate) {
478+
// Run the update notifier since it won't run later as we failed
479+
// to start, but we have enough details to do so now.
480+
const versionDetails: IPowerShellVersionDetails = {
481+
"version": version,
482+
"edition": "", // Unused by UpdatePowerShell
483+
"commit": version, // Actually used by UpdatePowerShell
484+
"architecture": process.arch // Best guess based off Code's architecture
485+
};
486+
const updater = new UpdatePowerShell(this, this.sessionSettings, this.logger, versionDetails);
487+
void updater.checkForUpdate();
488+
}
489+
return;
462490
}
463491

464-
if (this.sessionDetails?.status === "started") {
492+
if (this.sessionDetails.status === "started") { // Successful server start with a session file
465493
this.logger.write("Language server started.");
466494
try {
467495
await this.startLanguageClient(this.sessionDetails);
496+
return languageServerProcess;
468497
} catch (err) {
469-
this.setSessionFailure("Language client failed to start: ", err instanceof Error ? err.message : "unknown");
498+
void this.setSessionFailedOpenBug("Language client failed to start: " + (err instanceof Error ? err.message : "unknown"));
470499
}
471-
} else if (this.sessionDetails?.status === "failed") {
500+
} else if (this.sessionDetails.status === "failed") { // Server started but indicated it failed
472501
if (this.sessionDetails.reason === "unsupported") {
473-
this.setSessionFailure(
474-
"PowerShell language features are only supported on PowerShell version 5.1 and 7+. " +
475-
`The current version is ${this.sessionDetails.powerShellVersion}.`);
502+
void this.setSessionFailedGetPowerShell(`PowerShell ${this.sessionDetails.powerShellVersion} is not supported, please update!`);
476503
} else if (this.sessionDetails.reason === "languageMode") {
477-
this.setSessionFailure(
478-
"PowerShell language features are disabled due to an unsupported LanguageMode: " +
479-
`${this.sessionDetails.detail}`);
504+
this.setSessionFailure(`PowerShell language features are disabled due to an unsupported LanguageMode: ${this.sessionDetails.detail}`);
480505
} else {
481-
this.setSessionFailure(
482-
`PowerShell could not be started for an unknown reason '${this.sessionDetails.reason}'`);
506+
void this.setSessionFailedOpenBug(`PowerShell could not be started for an unknown reason: ${this.sessionDetails.reason}`);
483507
}
484508
} else {
485-
this.setSessionFailure(
486-
`Unknown session status '${this.sessionDetails?.status}' with reason '${this.sessionDetails?.reason}`);
509+
void this.setSessionFailedOpenBug(`PowerShell could not be started with an unknown status: ${this.sessionDetails.status}, and reason: ${this.sessionDetails.reason}`);
487510
}
488-
489-
return languageServerProcess;
511+
return;
490512
}
491513

492514
private async findPowerShell(): Promise<IPowerShellExeDetails | undefined> {
@@ -523,16 +545,7 @@ export class SessionManager implements Middleware {
523545
+ " Do you have PowerShell installed?"
524546
+ " You can also configure custom PowerShell installations"
525547
+ " with the 'powershell.powerShellAdditionalExePaths' setting.";
526-
527-
await this.logger.writeAndShowErrorWithActions(message, [
528-
{
529-
prompt: "Get PowerShell",
530-
action: async (): Promise<void> => {
531-
const getPSUri = vscode.Uri.parse("https://aka.ms/get-powershell-vscode");
532-
await vscode.env.openExternal(getPSUri);
533-
},
534-
},
535-
]);
548+
void this.setSessionFailedGetPowerShell(message);
536549
}
537550

538551
return foundPowerShell;
@@ -791,6 +804,27 @@ Type 'help' to get help.
791804
void this.logger.writeAndShowError(message, ...additionalMessages);
792805
}
793806

807+
private async setSessionFailedOpenBug(message: string): Promise<void> {
808+
this.setSessionStatus("Initialization Error!", SessionStatus.Failed);
809+
await this.logger.writeAndShowErrorWithActions(message, [{
810+
prompt: "Open an Issue",
811+
action: async (): Promise<void> => {
812+
await vscode.commands.executeCommand("PowerShell.GenerateBugReport");
813+
}}]
814+
);
815+
}
816+
817+
private async setSessionFailedGetPowerShell(message: string): Promise<void> {
818+
this.setSessionStatus("Initialization Error!", SessionStatus.Failed);
819+
await this.logger.writeAndShowErrorWithActions(message, [{
820+
prompt: "Open PowerShell Install Documentation",
821+
action: async (): Promise<void> => {
822+
await vscode.env.openExternal(
823+
vscode.Uri.parse("https://aka.ms/get-powershell-vscode"));
824+
}}]
825+
);
826+
}
827+
794828
private async changePowerShellDefaultVersion(exePath: IPowerShellExeDetails): Promise<void> {
795829
this.suppressRestartPrompt = true;
796830
try {

0 commit comments

Comments
 (0)