diff --git a/lib/std/Build/EmulatableRunStep.zig b/lib/std/Build/EmulatableRunStep.zig index 5517f7f9aa..d4b5238524 100644 --- a/lib/std/Build/EmulatableRunStep.zig +++ b/lib/std/Build/EmulatableRunStep.zig @@ -26,7 +26,7 @@ builder: *std.Build, exe: *CompileStep, /// Set this to `null` to ignore the exit code for the purpose of determining a successful execution -expected_exit_code: ?u8 = 0, +expected_term: ?std.ChildProcess.Term = .{ .Exited = 0 }, /// Override this field to modify the environment env_map: ?*EnvMap, @@ -131,7 +131,7 @@ fn make(step: *Step) !void { try RunStep.runCommand( argv_list.items, self.builder, - self.expected_exit_code, + self.expected_term, self.stdout_action, self.stderr_action, .Inherit, diff --git a/lib/std/Build/RunStep.zig b/lib/std/Build/RunStep.zig index d3f48e4e87..1aae37d2f3 100644 --- a/lib/std/Build/RunStep.zig +++ b/lib/std/Build/RunStep.zig @@ -35,7 +35,7 @@ stderr_action: StdIoAction = .inherit, stdin_behavior: std.ChildProcess.StdIo = .Inherit, /// Set this to `null` to ignore the exit code for the purpose of determining a successful execution -expected_exit_code: ?u8 = 0, +expected_term: ?std.ChildProcess.Term = .{ .Exited = 0 }, /// Print the command before running it print: bool, @@ -290,7 +290,7 @@ fn make(step: *Step) !void { try runCommand( argv_list.items, self.builder, - self.expected_exit_code, + self.expected_term, self.stdout_action, self.stderr_action, self.stdin_behavior, @@ -304,10 +304,55 @@ fn make(step: *Step) !void { } } +fn formatTerm( + term: ?std.ChildProcess.Term, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, +) !void { + _ = fmt; + _ = options; + if (term) |t| switch (t) { + .Exited => |code| try writer.print("exited with code {}", .{code}), + .Signal => |sig| try writer.print("terminated with signal {}", .{sig}), + .Stopped => |sig| try writer.print("stopped with signal {}", .{sig}), + .Unknown => |code| try writer.print("terminated for unknown reason with code {}", .{code}), + } else { + try writer.writeAll("exited with any code"); + } +} +fn fmtTerm(term: ?std.ChildProcess.Term) std.fmt.Formatter(formatTerm) { + return .{ .data = term }; +} + +fn termMatches(expected: ?std.ChildProcess.Term, actual: std.ChildProcess.Term) bool { + return if (expected) |e| switch (e) { + .Exited => |expected_code| switch (actual) { + .Exited => |actual_code| expected_code == actual_code, + else => false, + }, + .Signal => |expected_sig| switch (actual) { + .Signal => |actual_sig| expected_sig == actual_sig, + else => false, + }, + .Stopped => |expected_sig| switch (actual) { + .Stopped => |actual_sig| expected_sig == actual_sig, + else => false, + }, + .Unknown => |expected_code| switch (actual) { + .Unknown => |actual_code| expected_code == actual_code, + else => false, + }, + } else switch (actual) { + .Exited => true, + else => false, + }; +} + pub fn runCommand( argv: []const []const u8, builder: *std.Build, - expected_exit_code: ?u8, + expected_term: ?std.ChildProcess.Term, stdout_action: StdIoAction, stderr_action: StdIoAction, stdin_behavior: std.ChildProcess.StdIo, @@ -369,32 +414,14 @@ pub fn runCommand( return err; }; - switch (term) { - .Exited => |code| blk: { - const expected_code = expected_exit_code orelse break :blk; - - if (code != expected_code) { - if (builder.prominent_compile_errors) { - std.debug.print("Run step exited with error code {} (expected {})\n", .{ - code, - expected_code, - }); - } else { - std.debug.print("The following command exited with error code {} (expected {}):\n", .{ - code, - expected_code, - }); - printCmd(cwd, argv); - } - - return error.UnexpectedExitCode; - } - }, - else => { - std.debug.print("The following command terminated unexpectedly:\n", .{}); + if (!termMatches(expected_term, term)) { + if (builder.prominent_compile_errors) { + std.debug.print("Run step {} (expected {})\n", .{ fmtTerm(term), fmtTerm(expected_term) }); + } else { + std.debug.print("The following command {} (expected {}):\n", .{ fmtTerm(term), fmtTerm(expected_term) }); printCmd(cwd, argv); - return error.UncleanExit; - }, + } + return error.UnexpectedExit; } switch (stderr_action) { diff --git a/lib/std/os.zig b/lib/std/os.zig index c5eeb34b1c..bd6719ec8f 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -7056,3 +7056,21 @@ pub fn timerfd_gettime(fd: i32) TimerFdGetError!linux.itimerspec { else => |err| return unexpectedErrno(err), }; } + +pub const have_sigpipe_support = @hasDecl(@This(), "SIG") and @hasDecl(SIG, "PIPE"); + +fn noopSigHandler(_: c_int) callconv(.C) void {} + +pub fn maybeIgnoreSigpipe() void { + if (have_sigpipe_support and !std.options.keep_sigpipe) { + const act = Sigaction{ + // We set handler to a noop function instead of SIG.IGN so we don't leak our + // signal disposition to a child process + .handler = .{ .handler = noopSigHandler }, + .mask = empty_sigset, + .flags = 0, + }; + sigaction(SIG.PIPE, &act, null) catch |err| + std.debug.panic("failed to install noop SIGPIPE handler with '{s}'", .{@errorName(err)}); + } +} diff --git a/lib/std/start.zig b/lib/std/start.zig index ea221d1539..6edebde122 100644 --- a/lib/std/start.zig +++ b/lib/std/start.zig @@ -496,6 +496,7 @@ fn callMainWithArgs(argc: usize, argv: [*][*:0]u8, envp: [][*:0]u8) u8 { std.os.environ = envp; std.debug.maybeEnableSegfaultHandler(); + std.os.maybeIgnoreSigpipe(); return initEventLoopAndCallMain(); } diff --git a/lib/std/std.zig b/lib/std/std.zig index e02be2ebaf..5b0963ba20 100644 --- a/lib/std/std.zig +++ b/lib/std/std.zig @@ -167,6 +167,22 @@ pub const options = struct { options_override.crypto_always_getrandom else false; + + /// By default Zig disables SIGPIPE by setting a "no-op" handler for it. Set this option + /// to `true` to prevent that. + /// + /// Note that we use a "no-op" handler instead of SIG_IGN because it will not be inherited by + /// any child process. + /// + /// SIGPIPE is triggered when a process attempts to write to a broken pipe. By default, SIGPIPE + /// will terminate the process instead of exiting. It doesn't trigger the panic handler so in many + /// cases it's unclear why the process was terminated. By capturing SIGPIPE instead, functions that + /// write to broken pipes will return the EPIPE error (error.BrokenPipe) and the program can handle + /// it like any other error. + pub const keep_sigpipe: bool = if (@hasDecl(options_override, "keep_sigpipe")) + options_override.keep_sigpipe + else + false; }; // This forces the start.zig file to be imported, and the comptime logic inside that diff --git a/test/link/macho/dead_strip_dylibs/build.zig b/test/link/macho/dead_strip_dylibs/build.zig index 8b62cec6e6..af2f5cf0dc 100644 --- a/test/link/macho/dead_strip_dylibs/build.zig +++ b/test/link/macho/dead_strip_dylibs/build.zig @@ -29,7 +29,7 @@ pub fn build(b: *std.Build) void { exe.dead_strip_dylibs = true; const run_cmd = exe.run(); - run_cmd.expected_exit_code = @bitCast(u8, @as(i8, -2)); // should fail + run_cmd.expected_term = .{ .Exited = @bitCast(u8, @as(i8, -2)) }; // should fail test_step.dependOn(&run_cmd.step); } } diff --git a/test/src/compare_output.zig b/test/src/compare_output.zig index edd48321c9..3bda3bdacd 100644 --- a/test/src/compare_output.zig +++ b/test/src/compare_output.zig @@ -168,7 +168,7 @@ pub const CompareOutputContext = struct { run.addArgs(case.cli_args); run.stderr_action = .ignore; run.stdout_action = .ignore; - run.expected_exit_code = 126; + run.expected_term = .{ .Exited = 126 }; self.step.dependOn(&run.step); }, diff --git a/test/standalone.zig b/test/standalone.zig index 81eb1b0042..ed0d2c2d30 100644 --- a/test/standalone.zig +++ b/test/standalone.zig @@ -84,6 +84,9 @@ pub fn addCases(cases: *tests.StandaloneContext) void { cases.addBuildFile("test/standalone/pie/build.zig", .{}); } cases.addBuildFile("test/standalone/issue_12706/build.zig", .{}); + if (std.os.have_sigpipe_support) { + cases.addBuildFile("test/standalone/sigpipe/build.zig", .{}); + } // Ensure the development tools are buildable. Alphabetically sorted. // No need to build `tools/spirv/grammar.zig`. diff --git a/test/standalone/sigpipe/breakpipe.zig b/test/standalone/sigpipe/breakpipe.zig new file mode 100644 index 0000000000..3623451db5 --- /dev/null +++ b/test/standalone/sigpipe/breakpipe.zig @@ -0,0 +1,21 @@ +const std = @import("std"); +const build_options = @import("build_options"); + +pub const std_options = if (build_options.keep_sigpipe) struct { + pub const keep_sigpipe = true; +} else struct { + // intentionally not setting keep_sigpipe to ensure the default behavior is equivalent to false +}; + +pub fn main() !void { + const pipe = try std.os.pipe(); + std.os.close(pipe[0]); + _ = std.os.write(pipe[1], "a") catch |err| switch (err) { + error.BrokenPipe => { + try std.io.getStdOut().writer().writeAll("BrokenPipe\n"); + std.os.exit(123); + }, + else => |e| return e, + }; + unreachable; +} diff --git a/test/standalone/sigpipe/build.zig b/test/standalone/sigpipe/build.zig new file mode 100644 index 0000000000..763df5fe46 --- /dev/null +++ b/test/standalone/sigpipe/build.zig @@ -0,0 +1,35 @@ +const std = @import("std"); +const os = std.os; + +pub fn build(b: *std.build.Builder) !void { + const test_step = b.step("test", "Run the tests"); + + // This test runs "breakpipe" as a child process and that process + // depends on inheriting a SIGPIPE disposition of "default". + { + const act = os.Sigaction{ + .handler = .{ .handler = os.SIG.DFL }, + .mask = os.empty_sigset, + .flags = 0, + }; + try os.sigaction(os.SIG.PIPE, &act, null); + } + + for ([_]bool{ false, true }) |keep_sigpipe| { + const options = b.addOptions(); + options.addOption(bool, "keep_sigpipe", keep_sigpipe); + const exe = b.addExecutable(.{ + .name = "breakpipe", + .root_source_file = .{ .path = "breakpipe.zig" }, + }); + exe.addOptions("build_options", options); + const run = exe.run(); + if (keep_sigpipe) { + run.expected_term = .{ .Signal = std.os.SIG.PIPE }; + } else { + run.stdout_action = .{ .expect_exact = "BrokenPipe\n" }; + run.expected_term = .{ .Exited = 123 }; + } + test_step.dependOn(&run.step); + } +}