Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle end-of-support PowerShell with error message #4532

Merged
merged 1 commit into from Apr 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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?");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth adding an aka.ms link to the install instructions here maybe?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So with this.setSessionFailedGetPowerShell this actually now shows an error window popup with a button "Open PowerShell Install Documentation" that opens the link https://aka.ms/get-powershell-vscode!

} 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