| author | |
| committer | |
| log | adfc019d6070cf322048bdb36d991354711c4636 |
| tree | 1c13de6e54ee6852c566087e242e6e1f732478ca |
| parent | f10950526ea781ee2d15df74398527420cca13a1 |
| parent | dafefe9c9d3ffd484915ead0474c7e772f1dfcfb |
| signature |
ignore SIGPIPE by default10 files changed, 153 insertions(+), 32 deletions(-)
lib/std/Build/EmulatableRunStep.zig+2-2| ... | ... | @@ -26,7 +26,7 @@ builder: *std.Build, |
| 26 | 26 | exe: *CompileStep, |
| 27 | 27 | |
| 28 | 28 | /// Set this to `null` to ignore the exit code for the purpose of determining a successful execution |
| 29 | expected_exit_code: ?u8 = 0, | |
| 29 | expected_term: ?std.ChildProcess.Term = .{ .Exited = 0 }, | |
| 30 | 30 | |
| 31 | 31 | /// Override this field to modify the environment |
| 32 | 32 | env_map: ?*EnvMap, |
| ... | ... | @@ -131,7 +131,7 @@ fn make(step: *Step) !void { |
| 131 | 131 | try RunStep.runCommand( |
| 132 | 132 | argv_list.items, |
| 133 | 133 | self.builder, |
| 134 | self.expected_exit_code, | |
| 134 | self.expected_term, | |
| 135 | 135 | self.stdout_action, |
| 136 | 136 | self.stderr_action, |
| 137 | 137 | .Inherit, |
lib/std/Build/RunStep.zig+55-28| ... | ... | @@ -35,7 +35,7 @@ stderr_action: StdIoAction = .inherit, |
| 35 | 35 | stdin_behavior: std.ChildProcess.StdIo = .Inherit, |
| 36 | 36 | |
| 37 | 37 | /// Set this to `null` to ignore the exit code for the purpose of determining a successful execution |
| 38 | expected_exit_code: ?u8 = 0, | |
| 38 | expected_term: ?std.ChildProcess.Term = .{ .Exited = 0 }, | |
| 39 | 39 | |
| 40 | 40 | /// Print the command before running it |
| 41 | 41 | print: bool, |
| ... | ... | @@ -290,7 +290,7 @@ fn make(step: *Step) !void { |
| 290 | 290 | try runCommand( |
| 291 | 291 | argv_list.items, |
| 292 | 292 | self.builder, |
| 293 | self.expected_exit_code, | |
| 293 | self.expected_term, | |
| 294 | 294 | self.stdout_action, |
| 295 | 295 | self.stderr_action, |
| 296 | 296 | self.stdin_behavior, |
| ... | ... | @@ -304,10 +304,55 @@ fn make(step: *Step) !void { |
| 304 | 304 | } |
| 305 | 305 | } |
| 306 | 306 | |
| 307 | fn formatTerm( | |
| 308 | term: ?std.ChildProcess.Term, | |
| 309 | comptime fmt: []const u8, | |
| 310 | options: std.fmt.FormatOptions, | |
| 311 | writer: anytype, | |
| 312 | ) !void { | |
| 313 | _ = fmt; | |
| 314 | _ = options; | |
| 315 | if (term) |t| switch (t) { | |
| 316 | .Exited => |code| try writer.print("exited with code {}", .{code}), | |
| 317 | .Signal => |sig| try writer.print("terminated with signal {}", .{sig}), | |
| 318 | .Stopped => |sig| try writer.print("stopped with signal {}", .{sig}), | |
| 319 | .Unknown => |code| try writer.print("terminated for unknown reason with code {}", .{code}), | |
| 320 | } else { | |
| 321 | try writer.writeAll("exited with any code"); | |
| 322 | } | |
| 323 | } | |
| 324 | fn fmtTerm(term: ?std.ChildProcess.Term) std.fmt.Formatter(formatTerm) { | |
| 325 | return .{ .data = term }; | |
| 326 | } | |
| 327 | ||
| 328 | fn termMatches(expected: ?std.ChildProcess.Term, actual: std.ChildProcess.Term) bool { | |
| 329 | return if (expected) |e| switch (e) { | |
| 330 | .Exited => |expected_code| switch (actual) { | |
| 331 | .Exited => |actual_code| expected_code == actual_code, | |
| 332 | else => false, | |
| 333 | }, | |
| 334 | .Signal => |expected_sig| switch (actual) { | |
| 335 | .Signal => |actual_sig| expected_sig == actual_sig, | |
| 336 | else => false, | |
| 337 | }, | |
| 338 | .Stopped => |expected_sig| switch (actual) { | |
| 339 | .Stopped => |actual_sig| expected_sig == actual_sig, | |
| 340 | else => false, | |
| 341 | }, | |
| 342 | .Unknown => |expected_code| switch (actual) { | |
| 343 | .Unknown => |actual_code| expected_code == actual_code, | |
| 344 | else => false, | |
| 345 | }, | |
| 346 | } else switch (actual) { | |
| 347 | .Exited => true, | |
| 348 | else => false, | |
| 349 | }; | |
| 350 | } | |
| 351 | ||
| 307 | 352 | pub fn runCommand( |
| 308 | 353 | argv: []const []const u8, |
| 309 | 354 | builder: *std.Build, |
| 310 | expected_exit_code: ?u8, | |
| 355 | expected_term: ?std.ChildProcess.Term, | |
| 311 | 356 | stdout_action: StdIoAction, |
| 312 | 357 | stderr_action: StdIoAction, |
| 313 | 358 | stdin_behavior: std.ChildProcess.StdIo, |
| ... | ... | @@ -369,32 +414,14 @@ pub fn runCommand( |
| 369 | 414 | return err; |
| 370 | 415 | }; |
| 371 | 416 | |
| 372 | switch (term) { | |
| 373 | .Exited => |code| blk: { | |
| 374 | const expected_code = expected_exit_code orelse break :blk; | |
| 375 | ||
| 376 | if (code != expected_code) { | |
| 377 | if (builder.prominent_compile_errors) { | |
| 378 | std.debug.print("Run step exited with error code {} (expected {})\n", .{ | |
| 379 | code, | |
| 380 | expected_code, | |
| 381 | }); | |
| 382 | } else { | |
| 383 | std.debug.print("The following command exited with error code {} (expected {}):\n", .{ | |
| 384 | code, | |
| 385 | expected_code, | |
| 386 | }); | |
| 387 | printCmd(cwd, argv); | |
| 388 | } | |
| 389 | ||
| 390 | return error.UnexpectedExitCode; | |
| 391 | } | |
| 392 | }, | |
| 393 | else => { | |
| 394 | std.debug.print("The following command terminated unexpectedly:\n", .{}); | |
| 417 | if (!termMatches(expected_term, term)) { | |
| 418 | if (builder.prominent_compile_errors) { | |
| 419 | std.debug.print("Run step {} (expected {})\n", .{ fmtTerm(term), fmtTerm(expected_term) }); | |
| 420 | } else { | |
| 421 | std.debug.print("The following command {} (expected {}):\n", .{ fmtTerm(term), fmtTerm(expected_term) }); | |
| 395 | 422 | printCmd(cwd, argv); |
| 396 | return error.UncleanExit; | |
| 397 | }, | |
| 423 | } | |
| 424 | return error.UnexpectedExit; | |
| 398 | 425 | } |
| 399 | 426 | |
| 400 | 427 | switch (stderr_action) { |
lib/std/os.zig+18| ... | ... | @@ -7056,3 +7056,21 @@ pub fn timerfd_gettime(fd: i32) TimerFdGetError!linux.itimerspec { |
| 7056 | 7056 | else => |err| return unexpectedErrno(err), |
| 7057 | 7057 | }; |
| 7058 | 7058 | } |
| 7059 | ||
| 7060 | pub const have_sigpipe_support = @hasDecl(@This(), "SIG") and @hasDecl(SIG, "PIPE"); | |
| 7061 | ||
| 7062 | fn noopSigHandler(_: c_int) callconv(.C) void {} | |
| 7063 | ||
| 7064 | pub fn maybeIgnoreSigpipe() void { | |
| 7065 | if (have_sigpipe_support and !std.options.keep_sigpipe) { | |
| 7066 | const act = Sigaction{ | |
| 7067 | // We set handler to a noop function instead of SIG.IGN so we don't leak our | |
| 7068 | // signal disposition to a child process | |
| 7069 | .handler = .{ .handler = noopSigHandler }, | |
| 7070 | .mask = empty_sigset, | |
| 7071 | .flags = 0, | |
| 7072 | }; | |
| 7073 | sigaction(SIG.PIPE, &act, null) catch |err| | |
| 7074 | std.debug.panic("failed to install noop SIGPIPE handler with '{s}'", .{@errorName(err)}); | |
| 7075 | } | |
| 7076 | } |
lib/std/start.zig+1| ... | ... | @@ -496,6 +496,7 @@ fn callMainWithArgs(argc: usize, argv: [*][*:0]u8, envp: [][*:0]u8) u8 { |
| 496 | 496 | std.os.environ = envp; |
| 497 | 497 | |
| 498 | 498 | std.debug.maybeEnableSegfaultHandler(); |
| 499 | std.os.maybeIgnoreSigpipe(); | |
| 499 | 500 | |
| 500 | 501 | return initEventLoopAndCallMain(); |
| 501 | 502 | } |
lib/std/std.zig+16| ... | ... | @@ -167,6 +167,22 @@ pub const options = struct { |
| 167 | 167 | options_override.crypto_always_getrandom |
| 168 | 168 | else |
| 169 | 169 | false; |
| 170 | ||
| 171 | /// By default Zig disables SIGPIPE by setting a "no-op" handler for it. Set this option | |
| 172 | /// to `true` to prevent that. | |
| 173 | /// | |
| 174 | /// Note that we use a "no-op" handler instead of SIG_IGN because it will not be inherited by | |
| 175 | /// any child process. | |
| 176 | /// | |
| 177 | /// SIGPIPE is triggered when a process attempts to write to a broken pipe. By default, SIGPIPE | |
| 178 | /// will terminate the process instead of exiting. It doesn't trigger the panic handler so in many | |
| 179 | /// cases it's unclear why the process was terminated. By capturing SIGPIPE instead, functions that | |
| 180 | /// write to broken pipes will return the EPIPE error (error.BrokenPipe) and the program can handle | |
| 181 | /// it like any other error. | |
| 182 | pub const keep_sigpipe: bool = if (@hasDecl(options_override, "keep_sigpipe")) | |
| 183 | options_override.keep_sigpipe | |
| 184 | else | |
| 185 | false; | |
| 170 | 186 | }; |
| 171 | 187 | |
| 172 | 188 | // This forces the start.zig file to be imported, and the comptime logic inside that |
test/link/macho/dead_strip_dylibs/build.zig+1-1| ... | ... | @@ -29,7 +29,7 @@ pub fn build(b: *std.Build) void { |
| 29 | 29 | exe.dead_strip_dylibs = true; |
| 30 | 30 | |
| 31 | 31 | const run_cmd = exe.run(); |
| 32 | run_cmd.expected_exit_code = @bitCast(u8, @as(i8, -2)); // should fail | |
| 32 | run_cmd.expected_term = .{ .Exited = @bitCast(u8, @as(i8, -2)) }; // should fail | |
| 33 | 33 | test_step.dependOn(&run_cmd.step); |
| 34 | 34 | } |
| 35 | 35 | } |
test/src/compare_output.zig+1-1| ... | ... | @@ -168,7 +168,7 @@ pub const CompareOutputContext = struct { |
| 168 | 168 | run.addArgs(case.cli_args); |
| 169 | 169 | run.stderr_action = .ignore; |
| 170 | 170 | run.stdout_action = .ignore; |
| 171 | run.expected_exit_code = 126; | |
| 171 | run.expected_term = .{ .Exited = 126 }; | |
| 172 | 172 | |
| 173 | 173 | self.step.dependOn(&run.step); |
| 174 | 174 | }, |
test/standalone.zig+3| ... | ... | @@ -84,6 +84,9 @@ pub fn addCases(cases: *tests.StandaloneContext) void { |
| 84 | 84 | cases.addBuildFile("test/standalone/pie/build.zig", .{}); |
| 85 | 85 | } |
| 86 | 86 | cases.addBuildFile("test/standalone/issue_12706/build.zig", .{}); |
| 87 | if (std.os.have_sigpipe_support) { | |
| 88 | cases.addBuildFile("test/standalone/sigpipe/build.zig", .{}); | |
| 89 | } | |
| 87 | 90 | |
| 88 | 91 | // Ensure the development tools are buildable. Alphabetically sorted. |
| 89 | 92 | // No need to build `tools/spirv/grammar.zig`. |
test/standalone/sigpipe/breakpipe.zig created+21| ... | ... | @@ -0,0 +1,21 @@ |
| 1 | const std = @import("std"); | |
| 2 | const build_options = @import("build_options"); | |
| 3 | ||
| 4 | pub const std_options = if (build_options.keep_sigpipe) struct { | |
| 5 | pub const keep_sigpipe = true; | |
| 6 | } else struct { | |
| 7 | // intentionally not setting keep_sigpipe to ensure the default behavior is equivalent to false | |
| 8 | }; | |
| 9 | ||
| 10 | pub fn main() !void { | |
| 11 | const pipe = try std.os.pipe(); | |
| 12 | std.os.close(pipe[0]); | |
| 13 | _ = std.os.write(pipe[1], "a") catch |err| switch (err) { | |
| 14 | error.BrokenPipe => { | |
| 15 | try std.io.getStdOut().writer().writeAll("BrokenPipe\n"); | |
| 16 | std.os.exit(123); | |
| 17 | }, | |
| 18 | else => |e| return e, | |
| 19 | }; | |
| 20 | unreachable; | |
| 21 | } |
test/standalone/sigpipe/build.zig created+35| ... | ... | @@ -0,0 +1,35 @@ |
| 1 | const std = @import("std"); | |
| 2 | const os = std.os; | |
| 3 | ||
| 4 | pub fn build(b: *std.build.Builder) !void { | |
| 5 | const test_step = b.step("test", "Run the tests"); | |
| 6 | ||
| 7 | // This test runs "breakpipe" as a child process and that process | |
| 8 | // depends on inheriting a SIGPIPE disposition of "default". | |
| 9 | { | |
| 10 | const act = os.Sigaction{ | |
| 11 | .handler = .{ .handler = os.SIG.DFL }, | |
| 12 | .mask = os.empty_sigset, | |
| 13 | .flags = 0, | |
| 14 | }; | |
| 15 | try os.sigaction(os.SIG.PIPE, &act, null); | |
| 16 | } | |
| 17 | ||
| 18 | for ([_]bool{ false, true }) |keep_sigpipe| { | |
| 19 | const options = b.addOptions(); | |
| 20 | options.addOption(bool, "keep_sigpipe", keep_sigpipe); | |
| 21 | const exe = b.addExecutable(.{ | |
| 22 | .name = "breakpipe", | |
| 23 | .root_source_file = .{ .path = "breakpipe.zig" }, | |
| 24 | }); | |
| 25 | exe.addOptions("build_options", options); | |
| 26 | const run = exe.run(); | |
| 27 | if (keep_sigpipe) { | |
| 28 | run.expected_term = .{ .Signal = std.os.SIG.PIPE }; | |
| 29 | } else { | |
| 30 | run.stdout_action = .{ .expect_exact = "BrokenPipe\n" }; | |
| 31 | run.expected_term = .{ .Exited = 123 }; | |
| 32 | } | |
| 33 | test_step.dependOn(&run.step); | |
| 34 | } | |
| 35 | } |