From 97c939b981bc7fbf1a58511a9d83d9f1e12a8f16 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Fri, 2 Oct 2026 00:50:41 -0300 Subject: An Edit filter that can never be reaped no longer holds the Edit's answer: at the limit the command's group is killed and reaped off the answer's path, the stdin writer owning its input, so a filter writing its own ctl with > or >> answers EIO at 10 s instead of hanging; a running Edit keeps its own copies of the names it reports, and the panes it changes refuse Del, delete, rmdir, Undo, Redo, Get and Zerox (busy, EBUSY); e and r say unreadable files in Get's words; the reference says both sides of a filter's own ctl write need <> Co-Authored-By: Claude Opus 5.5 --- src/selection_pipe.zig | 74 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 14 deletions(-) (limited to 'src/selection_pipe.zig') diff --git a/src/selection_pipe.zig b/src/selection_pipe.zig index 66444b04..c3e5e3a6 100644 --- a/src/selection_pipe.zig +++ b/src/selection_pipe.zig @@ -214,17 +214,53 @@ pub const Tasks = struct { } }; +/// The stdin writer's own: its copy of the input, freed by the writer when +/// it is done, so a writer stuck on a command that never reads (and cannot +/// be killed, below) can be let go of without its input dying under it. const WriterContext = struct { + gpa: std.mem.Allocator, io: std.Io, file: std.Io.File, - input: []const u8, - ok: bool = false, + input: []u8, }; fn writeInput(context: *WriterContext) void { - defer context.file.close(context.io); + defer { + context.file.close(context.io); + context.gpa.free(context.input); + context.gpa.destroy(context); + } context.file.writeStreamingAll(context.io, context.input) catch return; - context.ok = true; +} + +/// Kills the command's group, then `abandon`s it. +fn abandonNow(io: std.Io, child: *std.process.Child, pid: std.posix.pid_t) void { + // Before the shell is reaped, while its group id cannot be another's. + std.posix.kill(-pid, .KILL) catch {}; + std.posix.kill(pid, .KILL) catch {}; + abandon(io, child.*); + child.id = null; + child.stdout = null; + child.stderr = null; +} + +/// A command given up on, killed and reaped where nothing waits for it: a +/// process in uninterruptible sleep (a write to a file of this very +/// session, through its mount, waiting on the request whose answer is +/// waiting on this command) dies only when that request is answered, and +/// the answer must not wait on its death. +fn abandon(io: std.Io, child: std.process.Child) void { + const Reap = struct { + fn run(i: std.Io, c: std.process.Child) void { + var reaped = c; + reaped.kill(i); + } + }; + const thread = std.Thread.spawn(.{}, Reap.run, .{ io, child }) catch { + var c = child; + return c.kill(io); + }; + thread.detach(); } /// Run one POSIX shell command with exact stdin, concurrently draining stdout @@ -350,23 +386,33 @@ pub fn runIn( }) catch return fail.k(.spawn); const pid = child.id.?; - var writer_context: WriterContext = .{ + const writer_context = gpa.create(WriterContext) catch { + abandonNow(io, &child, pid); + return fail.k(.spawn); + }; + writer_context.* = .{ + .gpa = gpa, .io = io, .file = child.stdin.?, - .input = input, + .input = gpa.dupe(u8, input) catch { + gpa.destroy(writer_context); + abandonNow(io, &child, pid); + return fail.k(.spawn); + }, }; child.stdin = null; // writer_context owns and closes this endpoint - var writer: ?std.Thread = std.Thread.spawn(.{}, writeInput, .{&writer_context}) catch { + var writer: ?std.Thread = std.Thread.spawn(.{}, writeInput, .{writer_context}) catch { writer_context.file.close(io); - child.kill(io); + gpa.free(writer_context.input); + gpa.destroy(writer_context); + abandonNow(io, &child, pid); return fail.k(.spawn); }; - // On every early return kill first, unblocking a command which never read - // stdin, then join the short-lived writer before its borrowed input dies. - defer if (writer) |thread| thread.join(); - defer child.kill(io); - // Before the shell is reaped, while its group id cannot be another's. - defer if (child.id != null) std.posix.kill(-pid, .KILL) catch {}; + // On every early return (a timeout, a ceiling) the answer goes at once: + // the group is killed and the shell left to be reaped elsewhere + // (abandon), and the writer, which owns its input, let go. + defer if (writer) |thread| thread.detach(); + defer if (child.id != null) abandonNow(io, &child, pid); defer if (token != 0) Running.remove(pid); if (token != 0 and !Running.add(token, pid)) return fail.k(.signal); -- cgit v1.3