diff options
Diffstat (limited to 'src/gui.zig')
| -rw-r--r-- | src/gui.zig | 69 |
1 files changed, 43 insertions, 26 deletions
diff --git a/src/gui.zig b/src/gui.zig index c1881c7c..2653a056 100644 --- a/src/gui.zig +++ b/src/gui.zig @@ -280,7 +280,7 @@ fn normCell(norm: f32, win: f32, cell: f32) u16 { const Pty = struct { fd: c_int, pid: libc.pid_t }; -const Msg = union(enum) { output: struct { pane: u8, bytes: []u8 }, eof: u8 }; +const Msg = union(enum) { output: struct { pane: u8, gen: u32, bytes: []u8 }, eof: struct { pane: u8, gen: u32, fd: c_int } }; const Queue = struct { gpa: std.mem.Allocator, @@ -342,7 +342,7 @@ const Queue = struct { } }; -fn readPtyThread(gpa: std.mem.Allocator, fd: c_int, pane: u8, q: *Queue) void { +fn readPtyThread(gpa: std.mem.Allocator, fd: c_int, pane: u8, gen: u32, q: *Queue) void { var buf: [0x10000]u8 = undefined; while (true) { const n = libc.read(fd, &buf, buf.len); @@ -352,13 +352,13 @@ fn readPtyThread(gpa: std.mem.Allocator, fd: c_int, pane: u8, q: *Queue) void { } if (n == 0) break; const bytes = gpa.dupe(u8, buf[0..@intCast(n)]) catch break; - q.push(.{ .output = .{ .pane = pane, .bytes = bytes } }); + q.push(.{ .output = .{ .pane = pane, .gen = gen, .bytes = bytes } }); } - q.push(.{ .eof = pane }); + q.push(.{ .eof = .{ .pane = pane, .gen = gen, .fd = fd } }); } -fn spawnReader(gpa: std.mem.Allocator, pt: Pty, pane: u8, q: *Queue) void { - const th = std.Thread.spawn(.{}, readPtyThread, .{ gpa, pt.fd, pane, q }) catch return; +fn spawnReader(gpa: std.mem.Allocator, pt: Pty, pane: u8, gen: u32, q: *Queue) void { + const th = std.Thread.spawn(.{}, readPtyThread, .{ gpa, pt.fd, pane, gen, q }) catch return; th.detach(); } @@ -627,6 +627,9 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { writeFile(pardes.bash_rc); var ptys: [pardes.MAX_PANES]?Pty = @splat(null); + // per-slot spawn generation: drops a dead shell's late output/eof when its + // pane id has been respawned (see drainEffects .spawn) + var gens: [pardes.MAX_PANES]u32 = @splat(0); defer for (&ptys) |*slot| if (slot.*) |pt| { _ = libc.close(pt.fd); slot.* = null; @@ -636,8 +639,8 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { // initial spawns BEFORE any worker thread exists: forkpty from a // multithreaded process can wedge the child before exec (see tty.zig). - drainEffects(core, &ptys, gpa, &queue, &g, false); - for (&ptys, 0..) |*slot, id| if (slot.*) |pt| spawnReader(gpa, pt, @intCast(id), &queue); + drainEffects(core, &ptys, &gens, gpa, &queue, &g, false); + for (&ptys, 0..) |*slot, id| if (slot.*) |pt| spawnReader(gpa, pt, @intCast(id), gens[id], &queue); _ = c.SDL_StartTextInput(window); @@ -663,22 +666,23 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { var msgs = queue.take(); for (msgs.items) |m| switch (m) { .output => |o| { - core.update(.{ .output = .{ .pane = o.pane, .bytes = o.bytes } }); + if (gens[o.pane] == o.gen) + core.update(.{ .output = .{ .pane = o.pane, .bytes = o.bytes } }); gpa.free(o.bytes); }, - .eof => |id| { - if (ptys[id]) |pt| { - _ = libc.close(pt.fd); - ptys[id] = null; + .eof => |e| { + _ = libc.close(e.fd); // the dead reader's master — stale or current + if (gens[e.pane] == e.gen) { + ptys[e.pane] = null; + core.update(.{ .eof = .{ .pane = e.pane } }); } - core.update(.{ .eof = .{ .pane = id } }); }, }; msgs.deinit(gpa); // 3. steamdeck: poll gamepad axes into virtual cursor / wheel events pollGamepad(&g, core); // 4. effects - drainEffects(core, &ptys, gpa, &queue, &g, true); + drainEffects(core, &ptys, &gens, gpa, &queue, &g, true); if (core.quit) break; // 5. live cwd for tags/look: cheap /proc readlink per pane, per frame pollCwds(core, &ptys); @@ -900,14 +904,17 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void { writeFile(pardes.bash_rc); var ptys: [pardes.MAX_PANES]?Pty = @splat(null); + // per-slot spawn generation: drops a dead shell's late output/eof when its + // pane id has been respawned (see drainEffects .spawn) + var gens: [pardes.MAX_PANES]u32 = @splat(0); defer for (&ptys) |*slot| if (slot.*) |pt| { _ = libc.close(pt.fd); slot.* = null; }; var queue: Queue = .{ .gpa = gpa, .sdl_wake = false }; defer queue.close(); - drainEffects(core, &ptys, gpa, &queue, null, false); - for (&ptys, 0..) |*slot, id| if (slot.*) |pt| spawnReader(gpa, pt, @intCast(id), &queue); + drainEffects(core, &ptys, &gens, gpa, &queue, null, false); + for (&ptys, 0..) |*slot, id| if (slot.*) |pt| spawnReader(gpa, pt, @intCast(id), gens[id], &queue); setStdinRaw() catch {}; // stdin may be a pipe, not a pty — best effort @@ -927,21 +934,22 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void { var msgs = queue.take(); for (msgs.items) |m| switch (m) { .output => |o| { - core.update(.{ .output = .{ .pane = o.pane, .bytes = o.bytes } }); + if (gens[o.pane] == o.gen) + core.update(.{ .output = .{ .pane = o.pane, .bytes = o.bytes } }); gpa.free(o.bytes); n_events += 1; }, - .eof => |id| { - if (ptys[id]) |pt| { - _ = libc.close(pt.fd); - ptys[id] = null; + .eof => |e| { + _ = libc.close(e.fd); // the dead reader's master — stale or current + if (gens[e.pane] == e.gen) { + ptys[e.pane] = null; + core.update(.{ .eof = .{ .pane = e.pane } }); } - core.update(.{ .eof = .{ .pane = id } }); n_events += 1; }, }; msgs.deinit(gpa); - drainEffects(core, &ptys, gpa, &queue, null, true); + drainEffects(core, &ptys, &gens, gpa, &queue, null, true); pollCwds(core, &ptys); if (n_events == 0) continue; // idle tick: nothing changed, no frame _ = frame_arena.reset(.retain_capacity); @@ -1418,6 +1426,7 @@ fn pollGamepad(g: *Gui, core: *pardes.Pardes) void { fn drainEffects( core: *pardes.Pardes, ptys: *[pardes.MAX_PANES]?Pty, + gens: *[pardes.MAX_PANES]u32, gpa: std.mem.Allocator, queue: *Queue, g: ?*Gui, // null in grid test mode (no SDL: clipboard effects are no-ops) @@ -1425,7 +1434,15 @@ fn drainEffects( ) void { while (core.nextEffect()) |effect| switch (effect) { .spawn => |sp| { - std.debug.assert(ptys[sp.pane] == null); + // the core reuses pane ids and there is no close effect: a deleted + // pane's shell lives in its slot until a respawn lands here. Kill + // it; its detached reader wakes on child death and the gen-guarded + // eof closes the old fd (not here — the reader still reads it). + if (ptys[sp.pane]) |old| { + _ = libc.kill(old.pid, libc.SIG.KILL); + ptys[sp.pane] = null; + } + gens[sp.pane] +%= 1; const cwd = sp.cwd.slice(); var cwd_buf: [256:0]u8 = undefined; var cwd_z: ?[*:0]const u8 = null; @@ -1439,7 +1456,7 @@ fn drainEffects( // report the pane's starting directory back to the core (tags) var lbuf: [1024]u8 = undefined; if (look.shellCwd(pt.pid, &lbuf)) |wd| core.setCwd(sp.pane, wd); - if (threads_ok) spawnReader(gpa, pt, sp.pane, queue); + if (threads_ok) spawnReader(gpa, pt, sp.pane, gens[sp.pane], queue); }, .write => |w| { if (ptys[w.pane]) |pt| writeFd(pt.fd, w.bytes.slice()); |
