Merge PR #6920: fix(tui): prevent Windows pane-close deadlock (@roboomp)
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed native Windows terminal panes freezing their host during forced closure by skipping the impossible stdout-drain wait after ConPTY disconnects ([#6917](https://github.com/can1357/oh-my-pi/issues/6917)).
|
||||
|
||||
## [17.1.8] - 2026-07-28
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1587,7 +1587,7 @@ export class ProcessTerminal implements Terminal {
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
void postmortem.quit(129);
|
||||
void postmortem.quit(129, { drainStdout: false });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { afterEach, describe, expect, it } from "bun:test";
|
||||
import { afterEach, describe, expect, it, vi } from "bun:test";
|
||||
import { postmortem } from "@oh-my-pi/pi-utils";
|
||||
import {
|
||||
createProcessTerminalRenderHarness,
|
||||
type ProcessTerminalRenderHarness,
|
||||
} from "./process-terminal-render-harness";
|
||||
|
||||
const PLATFORM_DESCRIPTOR = Object.getOwnPropertyDescriptor(process, "platform");
|
||||
|
||||
// Geometry-reflow contract for the *real* terminal driven through the *real*
|
||||
// renderer. These exercise the seam VirtualTerminal cannot model: the OS channel
|
||||
// (SIGWINCH) and the DEC 2048 in-band channel disagreeing. The observable
|
||||
@@ -14,6 +17,8 @@ describe("ProcessTerminal geometry reflow through the renderer", () => {
|
||||
afterEach(() => {
|
||||
harness?.dispose();
|
||||
harness = undefined;
|
||||
if (PLATFORM_DESCRIPTOR) Object.defineProperty(process, "platform", PLATFORM_DESCRIPTOR);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("reflows to the OS width on resize when in-band resize is inactive", async () => {
|
||||
@@ -102,6 +107,17 @@ describe("ProcessTerminal geometry reflow through the renderer", () => {
|
||||
expect(harness.signals.at(-1)).toEqual({ pid: process.pid, signal: "SIGHUP" });
|
||||
});
|
||||
|
||||
it("does not wait for terminal output to drain after input ends on Windows", async () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
||||
const quit = vi.spyOn(postmortem, "quit").mockResolvedValue(undefined);
|
||||
harness = createProcessTerminalRenderHarness(100, 30);
|
||||
|
||||
await harness.endInput();
|
||||
|
||||
expect(quit).toHaveBeenCalledWith(129, { drainStdout: false });
|
||||
expect(harness.signals).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("stops rendering and raises SIGHUP when terminal output fails", async () => {
|
||||
harness = createProcessTerminalRenderHarness(100, 30);
|
||||
await harness.settle();
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
### Changed
|
||||
|
||||
- Replaced the central logger's Winston dispatch with a byte-compatible local dispatcher while retaining the existing `winston-daily-rotate-file` rotation and retention behavior.
|
||||
### Added
|
||||
|
||||
- Added a `postmortem.quit` option for shutdown paths where terminal output has already disconnected and cannot drain.
|
||||
|
||||
## [17.1.8] - 2026-07-28
|
||||
|
||||
|
||||
@@ -319,14 +319,20 @@ export function cleanup(): Promise<void> {
|
||||
return runCleanup(Reason.MANUAL);
|
||||
}
|
||||
|
||||
async function runQuit(code: number, exitMode: "guarded" | "native"): Promise<void> {
|
||||
/** Controls how manual process shutdown handles terminal output. */
|
||||
export interface QuitOptions {
|
||||
/** Wait for buffered stdout before exiting; disable after the terminal has disconnected. */
|
||||
drainStdout?: boolean;
|
||||
}
|
||||
|
||||
async function runQuit(code: number, exitMode: "guarded" | "native", options: QuitOptions = {}): Promise<void> {
|
||||
await runCleanup(Reason.MANUAL);
|
||||
|
||||
if (!isMainThread) {
|
||||
return; // Workers: cleanup done, let worker exit naturally
|
||||
}
|
||||
|
||||
if (process.stdout.writableLength > 0) {
|
||||
if (options.drainStdout !== false && process.stdout.writableLength > 0) {
|
||||
const { promise, resolve } = Promise.withResolvers<void>();
|
||||
process.stdout.once("drain", resolve);
|
||||
await Promise.race([promise, Bun.sleep(5000)]);
|
||||
@@ -343,9 +349,9 @@ async function runQuit(code: number, exitMode: "guarded" | "native"): Promise<vo
|
||||
/**
|
||||
* Runs all cleanup callbacks and exits through the current `process.exit`.
|
||||
*
|
||||
* In main thread: waits for stdout drain, then calls `process.exit()`.
|
||||
* In main thread: waits for stdout drain unless disabled, then calls `process.exit()`.
|
||||
* In workers: runs cleanup only (process.exit would kill entire process).
|
||||
*/
|
||||
export function quit(code: number = 0): Promise<void> {
|
||||
return runQuit(code, "guarded");
|
||||
export function quit(code: number = 0, options: QuitOptions = {}): Promise<void> {
|
||||
return runQuit(code, "guarded", options);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user