fix(vt): detect windows pty child exit via registerWaitForSingleObject
This commit is contained in:
parent
a21b1318ed
commit
8027096f3e
2 changed files with 82 additions and 24 deletions
|
|
@ -30,8 +30,8 @@
|
||||||
.hash = "fuzzig-0.1.1-Ji0xivxIAQBD0g8O_NV_0foqoPf3elsg9Sc3pNfdVH4D",
|
.hash = "fuzzig-0.1.1-Ji0xivxIAQBD0g8O_NV_0foqoPf3elsg9Sc3pNfdVH4D",
|
||||||
},
|
},
|
||||||
.vaxis = .{
|
.vaxis = .{
|
||||||
.url = "git+https://github.com/neurocyte/libvaxis?ref=main#398f890e9015576673a9767d6d44877c1da34cfc",
|
.url = "git+https://github.com/neurocyte/libvaxis?ref=main#e6801b9c81fab5313bc35b349b294ba4b0a060ad",
|
||||||
.hash = "vaxis-0.5.1-BWNV_PQdCgBo6zTDEPmwNeTk9TM_qY0FjHWOx60YRYUB",
|
.hash = "vaxis-0.5.1-BWNV_L0eCgA4TNGahogJfmfebvJ-0sQXhOJAKn5WZmc6",
|
||||||
},
|
},
|
||||||
.zeit = .{
|
.zeit = .{
|
||||||
.url = "git+https://github.com/rockorager/zeit?ref=zig-0.15#ed2ca60db118414bda2b12df2039e33bad3b0b88",
|
.url = "git+https://github.com/rockorager/zeit?ref=zig-0.15#ed2ca60db118414bda2b12df2039e33bad3b0b88",
|
||||||
|
|
|
||||||
|
|
@ -709,11 +709,23 @@ const pty_posix = struct {
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Windows pty actor: reads ConPTY output pipe via tp.file_stream (IOCP overlapped I/O).
|
/// Windows pty actor: reads ConPTY output pipe via tp.file_stream (IOCP overlapped I/O).
|
||||||
/// Exit detection relies on error 109 (ERROR_BROKEN_PIPE) from the read stream,
|
///
|
||||||
/// which fires when the child process exits and the ConPTY closes the pipe.
|
/// Exit detection: ConPTY does NOT close the output pipe when the child process exits -
|
||||||
|
/// it keeps it open until ClosePseudoConsole is called. So a pending async read would
|
||||||
|
/// block forever. Instead we use RegisterWaitForSingleObject on the process handle;
|
||||||
|
/// when it fires the threadpool callback posts "child_exited" to this actor, which
|
||||||
|
/// cancels the stream and tears down cleanly.
|
||||||
const pty_windows = struct {
|
const pty_windows = struct {
|
||||||
const Parser = Terminal.Parser;
|
const Parser = Terminal.Parser;
|
||||||
const Receiver = tp.Receiver(*@This());
|
const Receiver = tp.Receiver(*@This());
|
||||||
|
const windows = std.os.windows;
|
||||||
|
|
||||||
|
// Context struct allocated on the heap and passed to the wait callback.
|
||||||
|
// Heap-allocated so its lifetime is independent of the actor.
|
||||||
|
const WaitCtx = struct {
|
||||||
|
self_pid: tp.pid,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
};
|
||||||
|
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
vt: *Terminal,
|
vt: *Terminal,
|
||||||
|
|
@ -721,6 +733,7 @@ const pty_windows = struct {
|
||||||
parser: Parser,
|
parser: Parser,
|
||||||
receiver: Receiver,
|
receiver: Receiver,
|
||||||
parent: tp.pid,
|
parent: tp.pid,
|
||||||
|
wait_handle: ?windows.HANDLE = null,
|
||||||
|
|
||||||
pub fn spawn(allocator: std.mem.Allocator, vt: *Terminal) !tp.pid {
|
pub fn spawn(allocator: std.mem.Allocator, vt: *Terminal) !tp.pid {
|
||||||
const self = try allocator.create(@This());
|
const self = try allocator.create(@This());
|
||||||
|
|
@ -737,6 +750,10 @@ const pty_windows = struct {
|
||||||
|
|
||||||
fn deinit(self: *@This()) void {
|
fn deinit(self: *@This()) void {
|
||||||
std.log.debug("terminal: pty actor (windows) deinit", .{});
|
std.log.debug("terminal: pty actor (windows) deinit", .{});
|
||||||
|
if (self.wait_handle) |wh| {
|
||||||
|
_ = UnregisterWait(wh);
|
||||||
|
self.wait_handle = null;
|
||||||
|
}
|
||||||
if (self.stream) |s| s.deinit();
|
if (self.stream) |s| s.deinit();
|
||||||
self.parser.buf.deinit();
|
self.parser.buf.deinit();
|
||||||
self.parent.deinit();
|
self.parent.deinit();
|
||||||
|
|
@ -753,9 +770,46 @@ const pty_windows = struct {
|
||||||
std.log.debug("terminal: pty stream start_read failed: {}", .{e});
|
std.log.debug("terminal: pty stream start_read failed: {}", .{e});
|
||||||
return tp.exit_error(e, @errorReturnTrace());
|
return tp.exit_error(e, @errorReturnTrace());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Register a one-shot wait on the process handle. When the child exits
|
||||||
|
// the threadpool fires on_child_exit, which sends "child_exited" to us.
|
||||||
|
// This is the only reliable way to detect ConPTY child exit without polling,
|
||||||
|
// since ConPTY keeps the output pipe open until ClosePseudoConsole.
|
||||||
|
const process_handle = self.vt.cmd.process_handle orelse {
|
||||||
|
std.log.debug("terminal: pty actor: no process handle to wait on", .{});
|
||||||
|
return tp.exit_error(error.NoProcessHandle, @errorReturnTrace());
|
||||||
|
};
|
||||||
|
const ctx = self.allocator.create(WaitCtx) catch |e|
|
||||||
|
return tp.exit_error(e, @errorReturnTrace());
|
||||||
|
ctx.* = .{
|
||||||
|
.self_pid = tp.self_pid().clone(),
|
||||||
|
.allocator = self.allocator,
|
||||||
|
};
|
||||||
|
var wh: windows.HANDLE = undefined;
|
||||||
|
// WT_EXECUTEONLYONCE: callback fires once then the wait is auto-unregistered.
|
||||||
|
const WT_EXECUTEONLYONCE: windows.ULONG = 0x00000008;
|
||||||
|
if (RegisterWaitForSingleObject(&wh, process_handle, on_child_exit, ctx, windows.INFINITE, WT_EXECUTEONLYONCE) == windows.FALSE) {
|
||||||
|
ctx.self_pid.deinit();
|
||||||
|
self.allocator.destroy(ctx);
|
||||||
|
std.log.debug("terminal: RegisterWaitForSingleObject failed", .{});
|
||||||
|
return tp.exit_error(error.RegisterWaitFailed, @errorReturnTrace());
|
||||||
|
}
|
||||||
|
self.wait_handle = wh;
|
||||||
|
|
||||||
tp.receive(&self.receiver);
|
tp.receive(&self.receiver);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Threadpool callback - called when the process handle becomes signaled.
|
||||||
|
/// Must be fast and non-blocking. Sends "child_exited" to the pty actor.
|
||||||
|
fn on_child_exit(ctx_ptr: ?*anyopaque, _: windows.BOOLEAN) callconv(.winapi) void {
|
||||||
|
const ctx: *WaitCtx = @ptrCast(@alignCast(ctx_ptr orelse return));
|
||||||
|
defer {
|
||||||
|
ctx.self_pid.deinit();
|
||||||
|
ctx.allocator.destroy(ctx);
|
||||||
|
}
|
||||||
|
ctx.self_pid.send(.{"child_exited"}) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
fn pty_receive(self: *@This(), _: tp.pid_ref, m: tp.message) tp.result {
|
fn pty_receive(self: *@This(), _: tp.pid_ref, m: tp.message) tp.result {
|
||||||
errdefer self.deinit();
|
errdefer self.deinit();
|
||||||
|
|
||||||
|
|
@ -763,16 +817,15 @@ const pty_windows = struct {
|
||||||
var err_code: i64 = 0;
|
var err_code: i64 = 0;
|
||||||
var err_msg: []const u8 = "";
|
var err_msg: []const u8 = "";
|
||||||
|
|
||||||
if (try m.match(.{ "stream", "pty_out", "read_complete", tp.extract(&bytes) })) {
|
if (try m.match(.{"child_exited"})) {
|
||||||
// Got output data from the child - process it, then arm next read.
|
self.wait_handle = null;
|
||||||
if (bytes.len == 0) {
|
if (self.stream) |s| s.cancel() catch {};
|
||||||
// Zero bytes = EOF on the pipe = child exited
|
const code = self.vt.cmd.wait();
|
||||||
const code = self.vt.cmd.wait();
|
std.log.debug("terminal: child exited (process wait), code={d}", .{code});
|
||||||
std.log.debug("terminal: ConPTY pipe EOF, process exited with code={d}", .{code});
|
self.vt.event_queue.push(.{ .exited = code });
|
||||||
self.vt.event_queue.push(.{ .exited = code });
|
self.parent.send(.{ "terminal_view", "output" }) catch {};
|
||||||
self.parent.send(.{ "terminal_view", "output" }) catch {};
|
return tp.exit_normal();
|
||||||
return tp.exit_normal();
|
} else if (try m.match(.{ "stream", "pty_out", "read_complete", tp.extract(&bytes) })) {
|
||||||
}
|
|
||||||
defer self.parent.send(.{ "terminal_view", "output" }) catch {};
|
defer self.parent.send(.{ "terminal_view", "output" }) catch {};
|
||||||
switch (self.vt.processOutput(&self.parser, bytes) catch |e| {
|
switch (self.vt.processOutput(&self.parser, bytes) catch |e| {
|
||||||
std.log.debug("terminal: processOutput error: {}", .{e});
|
std.log.debug("terminal: processOutput error: {}", .{e});
|
||||||
|
|
@ -784,21 +837,12 @@ const pty_windows = struct {
|
||||||
},
|
},
|
||||||
.running => {},
|
.running => {},
|
||||||
}
|
}
|
||||||
// Re-arm the read for next chunk
|
|
||||||
self.stream.?.start_read() catch |e| {
|
self.stream.?.start_read() catch |e| {
|
||||||
std.log.debug("terminal: pty stream re-arm failed: {}", .{e});
|
std.log.debug("terminal: pty stream re-arm failed: {}", .{e});
|
||||||
return tp.exit_normal();
|
return tp.exit_normal();
|
||||||
};
|
};
|
||||||
} else if (try m.match(.{ "stream", "pty_out", "read_error", 109, tp.extract(&err_msg) })) {
|
|
||||||
// ERROR_BROKEN_PIPE (109) = child process has exited and ConPTY closed the pipe.
|
|
||||||
const code = self.vt.cmd.wait();
|
|
||||||
std.log.debug("terminal: ConPTY pipe broken (child exited), code={d}", .{code});
|
|
||||||
self.vt.event_queue.push(.{ .exited = code });
|
|
||||||
self.parent.send(.{ "terminal_view", "output" }) catch {};
|
|
||||||
return tp.exit_normal();
|
|
||||||
} else if (try m.match(.{ "stream", "pty_out", "read_error", tp.extract(&err_code), tp.extract(&err_msg) })) {
|
} else if (try m.match(.{ "stream", "pty_out", "read_error", tp.extract(&err_code), tp.extract(&err_msg) })) {
|
||||||
// Other read error - treat as unexpected exit
|
std.log.debug("terminal: ConPTY stream error: {d} {s}", .{ err_code, err_msg });
|
||||||
std.log.debug("terminal: ConPTY read error: {d} {s}", .{ err_code, err_msg });
|
|
||||||
const code = self.vt.cmd.wait();
|
const code = self.vt.cmd.wait();
|
||||||
self.vt.event_queue.push(.{ .exited = code });
|
self.vt.event_queue.push(.{ .exited = code });
|
||||||
self.parent.send(.{ "terminal_view", "output" }) catch {};
|
self.parent.send(.{ "terminal_view", "output" }) catch {};
|
||||||
|
|
@ -811,4 +855,18 @@ const pty_windows = struct {
|
||||||
return tp.unexpected(m);
|
return tp.unexpected(m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Win32 extern declarations
|
||||||
|
extern "kernel32" fn RegisterWaitForSingleObject(
|
||||||
|
phNewWaitObject: *windows.HANDLE,
|
||||||
|
hObject: windows.HANDLE,
|
||||||
|
Callback: *const fn (?*anyopaque, windows.BOOLEAN) callconv(.winapi) void,
|
||||||
|
Context: ?*anyopaque,
|
||||||
|
dwMilliseconds: windows.DWORD,
|
||||||
|
dwFlags: windows.ULONG,
|
||||||
|
) callconv(.winapi) windows.BOOL;
|
||||||
|
|
||||||
|
extern "kernel32" fn UnregisterWait(
|
||||||
|
WaitHandle: windows.HANDLE,
|
||||||
|
) callconv(.winapi) windows.BOOL;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue