From 676fe25cfdee04fbdb83f26efbbbb9773e2e6dd9 Mon Sep 17 00:00:00 2001 From: Andy Jordan Date: Thu, 20 Apr 2023 19:32:16 -0700 Subject: [PATCH] Handle end-of-support PowerShell with error message Significantly improves our handling of PowerShell failing to start because it is unsupported. --- src/features/GenerateBugReport.ts | 7 ++- src/process.ts | 14 +++-- src/session.ts | 88 +++++++++++++++++++++---------- 3 files changed, 75 insertions(+), 34 deletions(-) diff --git a/src/features/GenerateBugReport.ts b/src/features/GenerateBugReport.ts index fbb2cf26b2..4ec7cec571 100644 --- a/src/features/GenerateBugReport.ts +++ b/src/features/GenerateBugReport.ts @@ -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(";", ","); } diff --git a/src/process.ts b/src/process.ts index 24c1f51314..06d0f0d36e 100644 --- a/src/process.ts +++ b/src/process.ts @@ -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. @@ -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 { + 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 { if (!this.consoleTerminal) { return undefined; } @@ -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 { diff --git a/src/session.ts b/src/session.ts index 4d14cd0ce3..c28708669f 100644 --- a/src/session.ts +++ b/src/session.ts @@ -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, @@ -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 { @@ -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 => { - const getPSUri = vscode.Uri.parse("https://aka.ms/get-powershell-vscode"); - await vscode.env.openExternal(getPSUri); - }, - }, - ]); + void this.setSessionFailedGetPowerShell(message); } return foundPowerShell; @@ -791,6 +804,27 @@ Type 'help' to get help. void this.logger.writeAndShowError(message, ...additionalMessages); } + private async setSessionFailedOpenBug(message: string): Promise { + this.setSessionStatus("Initialization Error!", SessionStatus.Failed); + await this.logger.writeAndShowErrorWithActions(message, [{ + prompt: "Open an Issue", + action: async (): Promise => { + await vscode.commands.executeCommand("PowerShell.GenerateBugReport"); + }}] + ); + } + + private async setSessionFailedGetPowerShell(message: string): Promise { + this.setSessionStatus("Initialization Error!", SessionStatus.Failed); + await this.logger.writeAndShowErrorWithActions(message, [{ + prompt: "Open PowerShell Install Documentation", + action: async (): Promise => { + await vscode.env.openExternal( + vscode.Uri.parse("https://aka.ms/get-powershell-vscode")); + }}] + ); + } + private async changePowerShellDefaultVersion(exePath: IPowerShellExeDetails): Promise { this.suppressRestartPrompt = true; try {