From f16f07fa26fd721897fba1693e17e8c932b5a767 Mon Sep 17 00:00:00 2001 From: Ben Buhse Date: Sun, 26 Apr 2026 10:23:48 -0500 Subject: [PATCH] Fix zombie processes from keybind spawns Previously, beansprout used std.process.Child.spawn without ever calling wait(), leaving exited children as zombies. This commit switches to a double-fork based off river-classic's spawn handling, where the actual spawned process gets orphaned and reparented to PID 1. This way, the parent (i.e. beansprout) only has to wait for the intermediate child. Also switch tokenizeShell and expandTilde to produce [:0]const u8 tokens so the argv array for execvpeZ can be built without copying each string. Fixes: #12 --- src/Config.zig | 4 ++-- src/XkbBindings.zig | 50 +++++++++++++++++++++++++++++++++++------- src/config/helpers.zig | 10 +++++---- src/utils.zig | 6 ++--- 4 files changed, 53 insertions(+), 17 deletions(-) diff --git a/src/Config.zig b/src/Config.zig index 51e5fd6..b89bfe3 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -151,8 +151,8 @@ fn addFallbackKeybinds(config: *Config) void { const fallbacks = [_]Entry{ .{ .{ .modifiers = .{ .ctrl = true, .mod1 = true }, .keysym = .Delete }, @unionInit(XkbBindings.Command, "exit_river", {}) }, .{ .{ .modifiers = .{ .mod4 = true, .shift = true }, .keysym = .R }, @unionInit(XkbBindings.Command, "reload_config", {}) }, - .{ .{ .modifiers = .{ .mod4 = true }, .keysym = .T }, .{ .spawn = utils.gpa.dupe([]const u8, &.{ - utils.gpa.dupe(u8, "foot") catch @panic("Out of memory"), + .{ .{ .modifiers = .{ .mod4 = true }, .keysym = .T }, .{ .spawn = utils.gpa.dupe([:0]const u8, &.{ + utils.gpa.dupeZ(u8, "foot") catch @panic("Out of memory"), }) catch @panic("Out of memory") } }, }; for (&fallbacks) |*entry| { diff --git a/src/XkbBindings.zig b/src/XkbBindings.zig index f5ea952..d18272d 100644 --- a/src/XkbBindings.zig +++ b/src/XkbBindings.zig @@ -5,7 +5,7 @@ const XkbBindings = @This(); pub const Command = union(enum) { - spawn: []const []const u8, + spawn: []const [:0]const u8, focus_next_window, focus_prev_window, focus_next_output, @@ -139,13 +139,7 @@ const XkbBinding = struct { // TODO: Should I log.warn when commands return early? switch (xkb_binding.command) { - .spawn => |cmd| { - var child = std.process.Child.init(cmd, utils.gpa); - child.env_map = &context.env; - _ = child.spawn() catch |err| { - log.err("Failed to spawn \"{s}\": {}", .{ cmd[0], err }); - }; - }, + .spawn => |cmd| spawnProcess(cmd), .focus_next_window => focusWindow(context, .next), .focus_prev_window => focusWindow(context, .prev), .focus_next_output => focusOutput(context, .next), @@ -403,6 +397,44 @@ const XkbBinding = struct { } } + // Borrowed and modified from river-classic + // https://codeberg.org/river/river-classic/src/commit/d72408df18310d5945147d485fe4bb66eef043d3/river/process.zig + fn spawnProcess(cmd: []const [:0]const u8) void { + const pid = posix.fork() catch |err| { + return log.err("Failed to fork \"{s}\": {}", .{ cmd[0], err }); + }; + + if (pid == 0) { + cleanupChild(); + + const c_argv = utils.gpa.allocSentinel(?[*:0]const u8, cmd.len, null) catch posix.exit(1); + for (cmd, 0..) |arg, i| c_argv[i] = arg.ptr; + + const pid2 = posix.fork() catch posix.exit(1); + if (pid2 == 0) { + posix.execvpeZ(c_argv[0].?, c_argv, std.c.environ) catch posix.exit(1); + } + + posix.exit(0); + } + + _ = posix.waitpid(pid, 0); + } + + // Borrowed and modified from river-classic + // https://codeberg.org/river/river-classic/src/commit/d72408df18310d5945147d485fe4bb66eef043d3/river/process.zig + fn cleanupChild() void { + _ = posix.setsid() catch unreachable; + if (posix.system.sigprocmask(posix.SIG.SETMASK, &posix.sigemptyset(), null) < 0) unreachable; + + const sig_dfl = posix.Sigaction{ + .handler = .{ .handler = posix.SIG.DFL }, + .mask = posix.sigemptyset(), + .flags = 0, + }; + posix.sigaction(posix.SIG.PIPE, &sig_dfl, null); + } + fn moveFloatingWindow(context: *Context, dx: i32, dy: i32) void { const seat = context.wm.seats.first() orelse return; const window = seat.focused_window orelse return; @@ -532,6 +564,8 @@ pub fn manage(xkb_bindings: *XkbBindings) void { const std = @import("std"); const assert = std.debug.assert; +const posix = std.posix; +const process = std.process; const wayland = @import("wayland"); const wl = wayland.client.wl; diff --git a/src/config/helpers.zig b/src/config/helpers.zig index ffc5643..7fcdedc 100644 --- a/src/config/helpers.zig +++ b/src/config/helpers.zig @@ -38,12 +38,13 @@ pub fn parseButton(s: []const u8) ?u32 { return fmt.parseInt(u32, s, 0) catch null; } -pub fn expandTilde(path: []const u8) ![]const u8 { +/// Expand a tilde in a string to the user's $HOME directory +pub fn expandTilde(path: []const u8) ![:0]const u8 { if (path.len > 0 and path[0] == '~') { - const home = std.posix.getenv("HOME") orelse return error.HomeNotSet; - return std.fmt.allocPrint(utils.gpa, "{s}{s}", .{ home, path[1..] }); + const home = posix.getenv("HOME") orelse return error.HomeNotSet; + return mem.concatWithSentinel(utils.gpa, u8, &.{ home, path[1..] }, 0); } - return utils.gpa.dupe(u8, path); + return utils.gpa.dupeZ(u8, path); } /// Check whether this machine's hostname matches the hostname property @@ -85,6 +86,7 @@ pub inline fn logWarnInvalidNode(node_name: []const u8) void { const std = @import("std"); const fmt = std.fmt; const mem = std.mem; +const posix = std.posix; const kdl = @import("kdl"); diff --git a/src/utils.zig b/src/utils.zig index 80a60d4..0718ed2 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -162,8 +162,8 @@ pub fn parseModifiers(s: []const u8) !?river.SeatV1.Modifiers { /// Split a string into tokens, respecting single and double quotes. /// Quoted sections have their quotes stripped. Returns an error if /// a quote is opened but never closed. -pub fn tokenizeShell(input: []const u8) ![][]const u8 { - var list: std.ArrayList([]const u8) = .empty; +pub fn tokenizeShell(input: []const u8) ![][:0]const u8 { + var list: std.ArrayList([:0]const u8) = .empty; errdefer { for (list.items) |item| gpa.free(item); list.deinit(gpa); @@ -190,7 +190,7 @@ pub fn tokenizeShell(input: []const u8) ![][]const u8 { i += 1; } } - try list.append(gpa, try token.toOwnedSlice(gpa)); + try list.append(gpa, try token.toOwnedSliceSentinel(gpa, 0)); } return list.toOwnedSlice(gpa); }