diff --git a/src/process.ts b/src/process.ts index 7ca46148f4..81c92cc4ad 100644 --- a/src/process.ts +++ b/src/process.ts @@ -22,6 +22,10 @@ export class PowerShellProcess { private pid?: number; private pidUpdateEmitter?: vscode.EventEmitter; + // Captured when the terminal closes so callers can report why the process + // exited (e.g. a non-zero exit code) instead of a generic failure message. + private exitStatus?: vscode.TerminalExitStatus; + constructor( public exePath: string, private bundledModulesPath: string, @@ -223,6 +227,13 @@ export class PowerShellProcess { return await this.consoleTerminal?.processId; } + // Returns the exit status of the terminal, if it has closed. This is + // captured before the terminal is disposed so callers can report a non-zero + // exit code when the process terminates before connecting. + public getExitStatus(): vscode.TerminalExitStatus | undefined { + return this.exitStatus; + } + public showTerminal(preserveFocus?: boolean): void { this.consoleTerminal?.show(preserveFocus); } @@ -343,8 +354,16 @@ export class PowerShellProcess { return; } + // Capture the exit status before disposing so it can be reported. A + // non-zero code means the process failed to start; `undefined` means + // the user (or VS Code) closed the terminal. + this.exitStatus = terminal.exitStatus; + const code = this.exitStatus?.code; + this.logger.writeWarning( - `PowerShell process terminated or Extension Terminal was closed, PID: ${this.pid}`, + `PowerShell process terminated or Extension Terminal was closed${ + code !== undefined ? ` with exit code: ${code}` : "" + }, PID: ${this.pid}`, ); this.dispose(); } diff --git a/src/session.ts b/src/session.ts index adcb815e4f..437ed2911b 100644 --- a/src/session.ts +++ b/src/session.ts @@ -791,8 +791,15 @@ export class SessionManager implements Middleware { ); } else { shouldUpdate = false; + // If the process terminated before connecting, VS Code gives us the + // terminal's exit code, which is far more actionable than a generic + // failure. A `undefined` code means it timed out or was closed + // without reporting one. + const exitCode = powerShellProcess.getExitStatus()?.code; void this.setSessionFailedOpenBug( - "PowerShell Language Server process didn't start!", + exitCode !== undefined && exitCode !== 0 + ? `PowerShell Language Server process exited with code: ${exitCode} before connecting! Check the PowerShell output for errors.` + : "PowerShell Language Server process didn't start!", ); } diff --git a/test/core/process.test.ts b/test/core/process.test.ts new file mode 100644 index 0000000000..dab3074e34 --- /dev/null +++ b/test/core/process.test.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as assert from "assert"; +import Sinon from "sinon"; +import * as vscode from "vscode"; +import { PowerShellProcess } from "../../src/process"; +import { stubInterface, testLogger } from "../utils"; + +describe("PowerShellProcess", () => { + afterEach(() => { + Sinon.restore(); + }); + + function createProcess(): PowerShellProcess { + return new PowerShellProcess( + "pwsh", + "modules", + false, + false, + testLogger, + vscode.Uri.file("C:/tmp"), + "", + vscode.Uri.file("C:/tmp/session.json"), + ); + } + + // These pokes mirror the `manager as unknown as {...}` style used in + // `session.test.ts` to exercise otherwise private startup behavior. + function internalsOf(process: PowerShellProcess): { + consoleTerminal: vscode.Terminal | undefined; + onTerminalClose: (terminal: vscode.Terminal) => void; + } { + return process as unknown as { + consoleTerminal: vscode.Terminal | undefined; + onTerminalClose: (terminal: vscode.Terminal) => void; + }; + } + + it("has no exit status before its terminal closes", () => { + const process = createProcess(); + assert.equal(process.getExitStatus(), undefined); + }); + + it("captures and logs the exit status when its terminal closes", () => { + const warnSpy = Sinon.spy(testLogger, "writeWarning"); + const process = createProcess(); + const exitStatus: vscode.TerminalExitStatus = { + code: 1, + reason: vscode.TerminalExitReason.Process, + }; + const terminal = stubInterface({ + exitStatus, + dispose: () => { + return; + }, + }); + + // Track the terminal, then simulate VS Code firing its close event. + const internals = internalsOf(process); + internals.consoleTerminal = terminal; + internals.onTerminalClose(terminal); + + assert.deepEqual(process.getExitStatus(), exitStatus); + assert.ok( + warnSpy.calledWithMatch("exit code: 1"), + "should log the non-zero exit code", + ); + }); + + it("ignores close events from unrelated terminals", () => { + const process = createProcess(); + const internals = internalsOf(process); + internals.consoleTerminal = stubInterface({ + dispose: () => { + return; + }, + }); + + const otherTerminal = stubInterface({ + exitStatus: { + code: 1, + reason: vscode.TerminalExitReason.Process, + }, + }); + internals.onTerminalClose(otherTerminal); + + assert.equal(process.getExitStatus(), undefined); + }); +});