ziglang / zig

General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.
https://ziglang.org
MIT License
33.78k stars 2.47k forks source link

Terminating `zig build run` does not terminate child processes #20853

Open MadLittleMods opened 1 month ago

MadLittleMods commented 1 month ago

Zig Version

0.13.0

Steps to Reproduce and Observed Behavior

  1. Using the standard zig init boilerplate
  2. Write a little program that keeps running and doesn't exit: src/main.zig

    const std = @import("std");
    
    pub fn main() void {
        while (true) {
            std.log.info("Hello, World!", .{});
            std.time.sleep(1 * std.time.ns_per_s);
        }
    }
  3. zig build run
  4. Notice the three processes that have spawned (the original zig build run, the expanded zig build, and running the final binary)
    $ ps aux | grep zig
    USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
    eric     3972336  0.0  0.0 434540 35328 pts/21   Sl+  01:20   0:00 zig build run
    eric     3972354  0.0  0.0 282024  2816 pts/21   Sl+  01:20   0:00 /home/eric/Documents/code/zig/random/kill-build-hello-world-test/.zig-cache/o/cdb0a375ac43b8fc9e3e09e439f80b53/build /home/eric/zig/0.13.0/files/zig /home/eric/Documents/code/zig/random/kill-build-hello-world-test /home/eric/Documents/code/zig/random/kill-build-hello-world-test/.zig-cache /home/eric/.cache/zig --seed 0x4ec1d056 -Z148e1dbdd164c915 run
    eric     3972408  0.0  0.0   1064   512 pts/21   S+   01:20   0:00 /home/eric/Documents/code/zig/random/kill-build-hello-world-test/zig-out/bin/kill-build-hello-world-test
  5. Kill the zig build run parent process (kill 3972336)
  6. Notice that the program keeps running and outputting text
  7. If you instead kill the main binary (kill 3972408), all of the processes are cleaned up


Also tested with the latest master (0.14.0-dev.655+d30d37e35)

Expected Behavior

My expected/desired behavior is that when killing the zig build run process, all of the child processes are cleaned up (no orphaned child processes).

Note: There is no automatic propagation of signals (SIGTERM or otherwise) to children in the process tree so the behavior is as expected in POSIX land. We would need to add our own signal handlers to make this work.

Related issue: https://github.com/ziglang/zig/issues/18340


My personal use case is wanting to use zig build run in a child process runner. The only thing I can access there is child.kill() or child.id (process ID) so I'm unable to clean up all of the processes unless I split the build from running the binary.

 test "run and kill main process" {
    const allocator = std.testing.allocator;

    const main_argv = [_][]const u8{ "zig", "build", "run" };
    var main_process = std.process.Child.init(&main_argv, allocator);
    // Prevent writing to `stdout` so the test runner doesn't hang,
    // see https://github.com/ziglang/zig/issues/15091
    main_process.stdin_behavior = .Ignore;
    main_process.stdout_behavior = .Ignore;
    main_process.stderr_behavior = .Ignore;

    try main_process.spawn();

    std.time.sleep(3 * std.time.ns_per_s);

    const main_term = try main_process.kill();
    // std.debug.print("main_process.id {}", .{main_process.id});

    try std.testing.expectEqual(std.process.Child.Term{ .Signal = std.posix.SIG.TERM }, main_term);
}
matklad commented 1 month ago

That's a hard problem in general, and I believe no actually good solution exists on Unix. Things I sadly know about the issue, in case it is helpful to anyone:

Isn't systems programming wonderful!