Skip to content
Open
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
54 changes: 0 additions & 54 deletions js/core/src/BlitConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,60 +504,6 @@ export class BlitConnection {
this.transport.send(buildCloseMessage(session.ptyId));
}

/**
* Wait for `sessionId` to reach `state === "exited"` or `state === "closed"`
* and return the captured `exitStatus`. Resolves immediately if the session
* has already finished. Resolves with `{ exitStatus: null, timedOut: false }`
* if the session is unknown — the caller has nothing else to wait on.
*
* If `options.timeoutMs` is provided, races the wait against the timeout and
* resolves with `{ exitStatus: null, timedOut: true }` if it elapses first.
*
* Honours {@link EXIT_STATUS_UNKNOWN}: callers that want a conventional
* shell exit code should pass the returned `exitStatus` through
* {@link exitCodeFromStatus}.
*/
awaitSessionExit(
sessionId: SessionId,
options?: { readonly timeoutMs?: number },
): Promise<{
readonly exitStatus: number | null;
readonly timedOut: boolean;
}> {
const current = this.sessionsById.get(sessionId);
if (!current || current.state === "exited" || current.state === "closed") {
return Promise.resolve({
exitStatus: current?.exitStatus ?? null,
timedOut: false,
});
}
return new Promise((resolve) => {
let unsubscribe: () => void = () => {};
let timer: ReturnType<typeof setTimeout> | null = null;
const finish = (result: {
exitStatus: number | null;
timedOut: boolean;
}): void => {
if (timer !== null) clearTimeout(timer);
unsubscribe();
resolve(result);
};
if (options?.timeoutMs !== undefined) {
timer = setTimeout(
() => finish({ exitStatus: null, timedOut: true }),
options.timeoutMs,
);
}
unsubscribe = this.subscribe(() => {
const session = this.sessionsById.get(sessionId);
if (!session) return;
if (session.state === "exited" || session.state === "closed") {
finish({ exitStatus: session.exitStatus, timedOut: false });
}
});
});
}

restartSession(sessionId: SessionId): void {
const session = this.sessionsById.get(sessionId);
if (
Expand Down
82 changes: 0 additions & 82 deletions js/core/src/__tests__/BlitConnection.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { BlitConnection } from "../BlitConnection";
import { EXIT_STATUS_UNKNOWN } from "../exit-status";
import { MockTransport } from "./mock-transport";
import type { BlitWasmModule } from "../TerminalStore";
import {
Expand Down Expand Up @@ -337,87 +336,6 @@ describe("BlitConnection", () => {
await promise;
});

// --- awaitSessionExit ---

it("awaitSessionExit resolves on EXITED", async () => {
transport.pushCreated(3, "");
const session = conn.getSnapshot().sessions[0];
const promise = conn.awaitSessionExit(session.id);
transport.pushExited(3, 0);
await expect(promise).resolves.toEqual({ exitStatus: 0, timedOut: false });
});

it("awaitSessionExit captures signal exit status", async () => {
transport.pushCreated(3, "");
const session = conn.getSnapshot().sessions[0];
const promise = conn.awaitSessionExit(session.id);
transport.pushExited(3, -15);
await expect(promise).resolves.toEqual({
exitStatus: -15,
timedOut: false,
});
});

it("awaitSessionExit resolves immediately when already exited", async () => {
transport.pushCreated(3, "");
const session = conn.getSnapshot().sessions[0];
transport.pushExited(3, 42);
await expect(conn.awaitSessionExit(session.id)).resolves.toEqual({
exitStatus: 42,
timedOut: false,
});
});

it("awaitSessionExit resolves on CLOSED without exitStatus", async () => {
transport.pushCreated(3, "");
const session = conn.getSnapshot().sessions[0];
const promise = conn.awaitSessionExit(session.id);
transport.pushClosed(3);
await expect(promise).resolves.toEqual({
exitStatus: null,
timedOut: false,
});
});

it("awaitSessionExit honours legacy EXITED frames as EXIT_STATUS_UNKNOWN", async () => {
transport.pushCreated(3, "");
const session = conn.getSnapshot().sessions[0];
const promise = conn.awaitSessionExit(session.id);
transport.pushExitedRaw(3);
await expect(promise).resolves.toEqual({
exitStatus: EXIT_STATUS_UNKNOWN,
timedOut: false,
});
});

it("awaitSessionExit times out and unsubscribes when the process keeps running", async () => {
transport.pushCreated(3, "");
const session = conn.getSnapshot().sessions[0];
vi.useFakeTimers();
try {
const promise = conn.awaitSessionExit(session.id, { timeoutMs: 50 });
vi.advanceTimersByTime(50);
await expect(promise).resolves.toEqual({
exitStatus: null,
timedOut: true,
});
// Late EXITED must not be observed by a discarded listener — if the
// subscription leaked, this would attempt to resolve the already-settled
// promise; we simply assert the snapshot still reflects the exit.
transport.pushExited(3, 7);
expect(conn.getSession(session.id)?.exitStatus).toBe(7);
} finally {
vi.useRealTimers();
}
});

it("awaitSessionExit returns immediately for an unknown session", async () => {
await expect(conn.awaitSessionExit("missing-session-id")).resolves.toEqual({
exitStatus: null,
timedOut: false,
});
});

// --- Send helpers ---

it("sendInput sends INPUT with session ptyId", () => {
Expand Down
Loading