Skip to content

Commit

Permalink
Handle PowerShell failing to start with actionable fixes (#4532)
Browse files Browse the repository at this point in the history
Significantly improves our handling of PowerShell failing to start for various reasons.
  • Loading branch information
andyleejordan committed Apr 24, 2023
1 parent 266ac53 commit 4bf0aa0
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 34 deletions.
7 changes: 3 additions & 4 deletions src/features/GenerateBugReport.ts
Expand Up @@ -33,10 +33,9 @@ export class GenerateBugReportFeature implements vscode.Disposable {
if (this.sessionManager.PowerShellExeDetails === undefined) {
return "Session's PowerShell details are unknown!";
}

const powerShellExePath = this.sessionManager.PowerShellExeDetails.exePath;
const powerShellArgs = [ "-NoProfile", "-Command", "$PSVersionTable | Out-String" ];
const child = child_process.spawnSync(powerShellExePath, powerShellArgs);
const child = child_process.spawnSync(
this.sessionManager.PowerShellExeDetails.exePath,
["-NoProfile", "-NoLogo", "-Command", "$PSVersionTable | Out-String"]);
// Replace semicolons as they'll cause the URI component to truncate
return child.stdout.toString().trim().replace(";", ",");
}
Expand Down
14 changes: 11 additions & 3 deletions src/process.ts
Expand Up @@ -8,6 +8,7 @@ import { ILogger } from "./logging";
import Settings = require("./settings");
import utils = require("./utils");
import { IEditorServicesSessionDetails } from "./session";
import { promisify } from "util";

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

// This function should only be used after a failure has occurred because it is slow!
public async getVersionCli(): Promise<string> {
const exec = promisify(cp.execFile);
const { stdout } = await exec(this.exePath, ["-NoProfile", "-NoLogo", "-Command", "$PSVersionTable.PSVersion.ToString()"]);
return stdout.trim();
}

// Returns the process Id of the consoleTerminal
public async getPid(): Promise<number | undefined> {
if (!this.consoleTerminal) { return undefined; }
Expand All @@ -148,13 +156,13 @@ export class PowerShellProcess {
// Clean up the session file
this.logger.write("Terminating PowerShell process...");

await PowerShellProcess.deleteSessionFile(this.sessionFilePath);
this.consoleTerminal?.dispose();
this.consoleTerminal = undefined;

this.consoleCloseSubscription?.dispose();
this.consoleCloseSubscription = undefined;

this.consoleTerminal?.dispose();
this.consoleTerminal = undefined;
await PowerShellProcess.deleteSessionFile(this.sessionFilePath);
}

public sendKeyPress(): void {
Expand Down
88 changes: 61 additions & 27 deletions src/session.ts
Expand Up @@ -24,7 +24,7 @@ import {
OperatingSystem, PowerShellExeFinder
} from "./platform";
import { LanguageClientConsumer } from "./languageClientConsumer";
import { SemVer } from "semver";
import { SemVer, satisfies } from "semver";

export enum SessionStatus {
NeverStarted,
Expand Down Expand Up @@ -458,35 +458,57 @@ export class SessionManager implements Middleware {
try {
this.sessionDetails = await languageServerProcess.start("EditorServices");
} catch (err) {
this.setSessionFailure("PowerShell process failed to start: ", err instanceof Error ? err.message : "unknown");
// We should kill the process in case it's stuck.
void languageServerProcess.dispose();

// PowerShell never started, probably a bad version!
const version = await languageServerProcess.getVersionCli();
let shouldUpdate = true;
if (satisfies(version, "<5.1.0")) {
void this.setSessionFailedGetPowerShell(`PowerShell ${version} is not supported, please update!`);
} else if (satisfies(version, ">=5.1.0 <6.0.0")) {
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?");
} else if (satisfies(version, ">=6.0.0 <7.2.0")) {
void this.setSessionFailedGetPowerShell(`PowerShell ${version} has reached end-of-support, please update!`);
} else {
shouldUpdate = false;
void this.setSessionFailedOpenBug("PowerShell language server process didn't start!");
}
if (shouldUpdate) {
// Run the update notifier since it won't run later as we failed
// to start, but we have enough details to do so now.
const versionDetails: IPowerShellVersionDetails = {
"version": version,
"edition": "", // Unused by UpdatePowerShell
"commit": version, // Actually used by UpdatePowerShell
"architecture": process.arch // Best guess based off Code's architecture
};
const updater = new UpdatePowerShell(this, this.sessionSettings, this.logger, versionDetails);
void updater.checkForUpdate();
}
return;
}

if (this.sessionDetails?.status === "started") {
if (this.sessionDetails.status === "started") { // Successful server start with a session file
this.logger.write("Language server started.");
try {
await this.startLanguageClient(this.sessionDetails);
return languageServerProcess;
} catch (err) {
this.setSessionFailure("Language client failed to start: ", err instanceof Error ? err.message : "unknown");
void this.setSessionFailedOpenBug("Language client failed to start: " + (err instanceof Error ? err.message : "unknown"));
}
} else if (this.sessionDetails?.status === "failed") {
} else if (this.sessionDetails.status === "failed") { // Server started but indicated it failed
if (this.sessionDetails.reason === "unsupported") {
this.setSessionFailure(
"PowerShell language features are only supported on PowerShell version 5.1 and 7+. " +
`The current version is ${this.sessionDetails.powerShellVersion}.`);
void this.setSessionFailedGetPowerShell(`PowerShell ${this.sessionDetails.powerShellVersion} is not supported, please update!`);
} else if (this.sessionDetails.reason === "languageMode") {
this.setSessionFailure(
"PowerShell language features are disabled due to an unsupported LanguageMode: " +
`${this.sessionDetails.detail}`);
this.setSessionFailure(`PowerShell language features are disabled due to an unsupported LanguageMode: ${this.sessionDetails.detail}`);
} else {
this.setSessionFailure(
`PowerShell could not be started for an unknown reason '${this.sessionDetails.reason}'`);
void this.setSessionFailedOpenBug(`PowerShell could not be started for an unknown reason: ${this.sessionDetails.reason}`);
}
} else {
this.setSessionFailure(
`Unknown session status '${this.sessionDetails?.status}' with reason '${this.sessionDetails?.reason}`);
void this.setSessionFailedOpenBug(`PowerShell could not be started with an unknown status: ${this.sessionDetails.status}, and reason: ${this.sessionDetails.reason}`);
}

return languageServerProcess;
return;
}

private async findPowerShell(): Promise<IPowerShellExeDetails | undefined> {
Expand Down Expand Up @@ -523,16 +545,7 @@ export class SessionManager implements Middleware {
+ " Do you have PowerShell installed?"
+ " You can also configure custom PowerShell installations"
+ " with the 'powershell.powerShellAdditionalExePaths' setting.";

await this.logger.writeAndShowErrorWithActions(message, [
{
prompt: "Get PowerShell",
action: async (): Promise<void> => {
const getPSUri = vscode.Uri.parse("https://aka.ms/get-powershell-vscode");
await vscode.env.openExternal(getPSUri);
},
},
]);
void this.setSessionFailedGetPowerShell(message);
}

return foundPowerShell;
Expand Down Expand Up @@ -791,6 +804,27 @@ Type 'help' to get help.
void this.logger.writeAndShowError(message, ...additionalMessages);
}

private async setSessionFailedOpenBug(message: string): Promise<void> {
this.setSessionStatus("Initialization Error!", SessionStatus.Failed);
await this.logger.writeAndShowErrorWithActions(message, [{
prompt: "Open an Issue",
action: async (): Promise<void> => {
await vscode.commands.executeCommand("PowerShell.GenerateBugReport");
}}]
);
}

private async setSessionFailedGetPowerShell(message: string): Promise<void> {
this.setSessionStatus("Initialization Error!", SessionStatus.Failed);
await this.logger.writeAndShowErrorWithActions(message, [{
prompt: "Open PowerShell Install Documentation",
action: async (): Promise<void> => {
await vscode.env.openExternal(
vscode.Uri.parse("https://aka.ms/get-powershell-vscode"));
}}]
);
}

private async changePowerShellDefaultVersion(exePath: IPowerShellExeDetails): Promise<void> {
this.suppressRestartPrompt = true;
try {
Expand Down

0 comments on commit 4bf0aa0

Please sign in to comment.