Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion src/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export class PowerShellProcess {
private pid?: number;
private pidUpdateEmitter?: vscode.EventEmitter<number | undefined>;

// 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,
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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();
}
Expand Down
9 changes: 8 additions & 1 deletion src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!",
);
}

Expand Down
90 changes: 90 additions & 0 deletions test/core/process.test.ts
Original file line number Diff line number Diff line change
@@ -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<vscode.Terminal>({
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<vscode.Terminal>({
dispose: () => {
return;
},
});

const otherTerminal = stubInterface<vscode.Terminal>({
exitStatus: {
code: 1,
reason: vscode.TerminalExitReason.Process,
},
});
internals.onTerminalClose(otherTerminal);

assert.equal(process.getExitStatus(), undefined);
});
});
Loading