Skip to content

Commit 216e0f3

Browse files
authored
Merge pull request #22548 from mlugg/fix-broken-pipe
Wait for reported spawn success or failure before trying to write to the stdio pipe in the build runner. Hopefully fixes `error.BrokenPipe` failures
2 parents 8bcb578 + 048e85f commit 216e0f3

File tree

6 files changed

+79
-95
lines changed

6 files changed

+79
-95
lines changed

lib/std/Build.zig

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -979,7 +979,7 @@ pub const TestOptions = struct {
979979
/// Deprecated; use `.filters = &.{filter}` instead of `.filter = filter`.
980980
filter: ?[]const u8 = null,
981981
filters: []const []const u8 = &.{},
982-
test_runner: ?LazyPath = null,
982+
test_runner: ?Step.Compile.TestRunner = null,
983983
use_llvm: ?bool = null,
984984
use_lld: ?bool = null,
985985
zig_lib_dir: ?LazyPath = null,
@@ -1136,9 +1136,8 @@ pub fn addRunArtifact(b: *Build, exe: *Step.Compile) *Step.Run {
11361136
run_step.addArtifactArg(exe);
11371137
}
11381138

1139-
if (exe.test_server_mode) {
1140-
run_step.enableTestRunnerMode();
1141-
}
1139+
const test_server_mode = if (exe.test_runner) |r| r.mode == .server else true;
1140+
if (test_server_mode) run_step.enableTestRunnerMode();
11421141
} else {
11431142
run_step.addArtifactArg(exe);
11441143
}

lib/std/Build/Step/Compile.zig

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,7 @@ global_base: ?u64 = null,
5656
zig_lib_dir: ?LazyPath,
5757
exec_cmd_args: ?[]const ?[]const u8,
5858
filters: []const []const u8,
59-
test_runner: ?LazyPath,
60-
test_server_mode: bool,
59+
test_runner: ?TestRunner,
6160
wasi_exec_model: ?std.builtin.WasiExecModel = null,
6261

6362
installed_headers: ArrayList(HeaderInstallation),
@@ -268,7 +267,7 @@ pub const Options = struct {
268267
version: ?std.SemanticVersion = null,
269268
max_rss: usize = 0,
270269
filters: []const []const u8 = &.{},
271-
test_runner: ?LazyPath = null,
270+
test_runner: ?TestRunner = null,
272271
use_llvm: ?bool = null,
273272
use_lld: ?bool = null,
274273
zig_lib_dir: ?LazyPath = null,
@@ -347,6 +346,14 @@ pub const HeaderInstallation = union(enum) {
347346
}
348347
};
349348

349+
pub const TestRunner = struct {
350+
path: LazyPath,
351+
/// Test runners can either be "simple", running tests when spawned and terminating when the
352+
/// tests are complete, or they can use `std.zig.Server` over stdio to interact more closely
353+
/// with the build system.
354+
mode: enum { simple, server },
355+
};
356+
350357
pub fn create(owner: *std.Build, options: Options) *Compile {
351358
const name = owner.dupe(options.name);
352359
if (mem.indexOf(u8, name, "/") != null or mem.indexOf(u8, name, "\\") != null) {
@@ -411,8 +418,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
411418
.zig_lib_dir = null,
412419
.exec_cmd_args = null,
413420
.filters = options.filters,
414-
.test_runner = null,
415-
.test_server_mode = options.test_runner == null,
421+
.test_runner = null, // set below
416422
.rdynamic = false,
417423
.installed_path = null,
418424
.force_undefined_symbols = StringHashMap(void).init(owner.allocator),
@@ -438,9 +444,12 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
438444
lp.addStepDependencies(&compile.step);
439445
}
440446

441-
if (options.test_runner) |lp| {
442-
compile.test_runner = lp.dupe(compile.step.owner);
443-
lp.addStepDependencies(&compile.step);
447+
if (options.test_runner) |runner| {
448+
compile.test_runner = .{
449+
.path = runner.path.dupe(compile.step.owner),
450+
.mode = runner.mode,
451+
};
452+
runner.path.addStepDependencies(&compile.step);
444453
}
445454

446455
// Only the PE/COFF format has a Resource Table which is where the manifest
@@ -1399,7 +1408,7 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {
13991408

14001409
if (compile.test_runner) |test_runner| {
14011410
try zig_args.append("--test-runner");
1402-
try zig_args.append(test_runner.getPath2(b, step));
1411+
try zig_args.append(test_runner.path.getPath2(b, step));
14031412
}
14041413

14051414
for (b.debug_log_scopes) |log_scope| {

lib/std/Build/Step/Run.zig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,6 +1354,9 @@ fn spawnChildAndCollect(
13541354
_ = child.kill() catch {};
13551355
}
13561356

1357+
// We need to report `error.InvalidExe` *now* if applicable.
1358+
try child.waitForSpawn();
1359+
13571360
var timer = try std.time.Timer.start();
13581361

13591362
const result = if (run.stdio == .zig_test)

lib/std/process/Child.zig

Lines changed: 47 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ cwd: ?[]const u8,
7373
/// Once that is done, `cwd` will be deprecated in favor of this field.
7474
cwd_dir: ?fs.Dir = null,
7575

76-
err_pipe: ?if (native_os == .windows) void else [2]posix.fd_t,
76+
err_pipe: if (native_os == .windows) void else ?posix.fd_t,
7777

7878
expand_arg0: Arg0Expand,
7979

@@ -211,7 +211,7 @@ pub fn init(argv: []const []const u8, allocator: mem.Allocator) ChildProcess {
211211
.argv = argv,
212212
.id = undefined,
213213
.thread_handle = undefined,
214-
.err_pipe = null,
214+
.err_pipe = if (native_os == .windows) {} else null,
215215
.term = null,
216216
.env_map = null,
217217
.cwd = null,
@@ -293,17 +293,49 @@ pub fn killPosix(self: *ChildProcess) !Term {
293293
error.ProcessNotFound => return error.AlreadyTerminated,
294294
else => return err,
295295
};
296-
self.waitUnwrapped();
296+
self.waitUnwrappedPosix();
297297
return self.term.?;
298298
}
299299

300300
pub const WaitError = SpawnError || std.os.windows.GetProcessMemoryInfoError;
301301

302+
/// On some targets, `spawn` may not report all spawn errors, such as `error.InvalidExe`.
303+
/// This function will block until any spawn errors can be reported, and return them.
304+
pub fn waitForSpawn(self: *ChildProcess) SpawnError!void {
305+
if (native_os == .windows) return; // `spawn` reports everything
306+
if (self.term) |term| {
307+
_ = term catch |spawn_err| return spawn_err;
308+
return;
309+
}
310+
311+
const err_pipe = self.err_pipe orelse return;
312+
self.err_pipe = null;
313+
314+
// Wait for the child to report any errors in or before `execvpe`.
315+
if (readIntFd(err_pipe)) |child_err_int| {
316+
posix.close(err_pipe);
317+
const child_err: SpawnError = @errorCast(@errorFromInt(child_err_int));
318+
self.term = child_err;
319+
return child_err;
320+
} else |_| {
321+
// Write end closed by CLOEXEC at the time of the `execvpe` call, indicating success!
322+
posix.close(err_pipe);
323+
}
324+
}
325+
302326
/// Blocks until child process terminates and then cleans up all resources.
303327
pub fn wait(self: *ChildProcess) WaitError!Term {
304-
const term = if (native_os == .windows) try self.waitWindows() else self.waitPosix();
328+
try self.waitForSpawn(); // report spawn errors
329+
if (self.term) |term| {
330+
self.cleanupStreams();
331+
return term;
332+
}
333+
switch (native_os) {
334+
.windows => try self.waitUnwrappedWindows(),
335+
else => self.waitUnwrappedPosix(),
336+
}
305337
self.id = undefined;
306-
return term;
338+
return self.term.?;
307339
}
308340

309341
pub const RunResult = struct {
@@ -405,26 +437,6 @@ pub fn run(args: struct {
405437
};
406438
}
407439

408-
fn waitWindows(self: *ChildProcess) WaitError!Term {
409-
if (self.term) |term| {
410-
self.cleanupStreams();
411-
return term;
412-
}
413-
414-
try self.waitUnwrappedWindows();
415-
return self.term.?;
416-
}
417-
418-
fn waitPosix(self: *ChildProcess) SpawnError!Term {
419-
if (self.term) |term| {
420-
self.cleanupStreams();
421-
return term;
422-
}
423-
424-
self.waitUnwrapped();
425-
return self.term.?;
426-
}
427-
428440
fn waitUnwrappedWindows(self: *ChildProcess) WaitError!void {
429441
const result = windows.WaitForSingleObjectEx(self.id, windows.INFINITE, false);
430442

@@ -447,7 +459,7 @@ fn waitUnwrappedWindows(self: *ChildProcess) WaitError!void {
447459
return result;
448460
}
449461

450-
fn waitUnwrapped(self: *ChildProcess) void {
462+
fn waitUnwrappedPosix(self: *ChildProcess) void {
451463
const res: posix.WaitPidResult = res: {
452464
if (self.request_resource_usage_statistics) {
453465
switch (native_os) {
@@ -469,7 +481,7 @@ fn waitUnwrapped(self: *ChildProcess) void {
469481
}
470482

471483
fn handleWaitResult(self: *ChildProcess, status: u32) void {
472-
self.term = self.cleanupAfterWait(status);
484+
self.term = statusToTerm(status);
473485
}
474486

475487
fn cleanupStreams(self: *ChildProcess) void {
@@ -487,46 +499,6 @@ fn cleanupStreams(self: *ChildProcess) void {
487499
}
488500
}
489501

490-
fn cleanupAfterWait(self: *ChildProcess, status: u32) !Term {
491-
if (self.err_pipe) |err_pipe| {
492-
defer destroyPipe(err_pipe);
493-
494-
if (native_os == .linux) {
495-
var fd = [1]posix.pollfd{posix.pollfd{
496-
.fd = err_pipe[0],
497-
.events = posix.POLL.IN,
498-
.revents = undefined,
499-
}};
500-
501-
// Check if the eventfd buffer stores a non-zero value by polling
502-
// it, that's the error code returned by the child process.
503-
_ = posix.poll(&fd, 0) catch unreachable;
504-
505-
// According to eventfd(2) the descriptor is readable if the counter
506-
// has a value greater than 0
507-
if ((fd[0].revents & posix.POLL.IN) != 0) {
508-
const err_int = try readIntFd(err_pipe[0]);
509-
return @as(SpawnError, @errorCast(@errorFromInt(err_int)));
510-
}
511-
} else {
512-
// Write maxInt(ErrInt) to the write end of the err_pipe. This is after
513-
// waitpid, so this write is guaranteed to be after the child
514-
// pid potentially wrote an error. This way we can do a blocking
515-
// read on the error pipe and either get maxInt(ErrInt) (no error) or
516-
// an error code.
517-
try writeIntFd(err_pipe[1], maxInt(ErrInt));
518-
const err_int = try readIntFd(err_pipe[0]);
519-
// Here we potentially return the fork child's error from the parent
520-
// pid.
521-
if (err_int != maxInt(ErrInt)) {
522-
return @as(SpawnError, @errorCast(@errorFromInt(err_int)));
523-
}
524-
}
525-
}
526-
527-
return statusToTerm(status);
528-
}
529-
530502
fn statusToTerm(status: u32) Term {
531503
return if (posix.W.IFEXITED(status))
532504
Term{ .Exited = posix.W.EXITSTATUS(status) }
@@ -636,18 +608,9 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void {
636608
}
637609
};
638610

639-
// This pipe is used to communicate errors between the time of fork
640-
// and execve from the child process to the parent process.
641-
const err_pipe = blk: {
642-
if (native_os == .linux) {
643-
const fd = try posix.eventfd(0, linux.EFD.CLOEXEC);
644-
// There's no distinction between the readable and the writeable
645-
// end with eventfd
646-
break :blk [2]posix.fd_t{ fd, fd };
647-
} else {
648-
break :blk try posix.pipe2(.{ .CLOEXEC = true });
649-
}
650-
};
611+
// This pipe communicates to the parent errors in the child between `fork` and `execvpe`.
612+
// It is closed by the child (via CLOEXEC) without writing if `execvpe` succeeds.
613+
const err_pipe: [2]posix.fd_t = try posix.pipe2(.{ .CLOEXEC = true });
651614
errdefer destroyPipe(err_pipe);
652615

653616
const pid_result = try posix.fork();
@@ -687,6 +650,11 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void {
687650
}
688651

689652
// we are the parent
653+
errdefer comptime unreachable; // The child is forked; we must not error from now on
654+
655+
posix.close(err_pipe[1]); // make sure only the child holds the write end open
656+
self.err_pipe = err_pipe[0];
657+
690658
const pid: i32 = @intCast(pid_result);
691659
if (self.stdin_behavior == .Pipe) {
692660
self.stdin = .{ .handle = stdin_pipe[1] };
@@ -705,7 +673,6 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void {
705673
}
706674

707675
self.id = pid;
708-
self.err_pipe = err_pipe;
709676
self.term = null;
710677

711678
if (self.stdin_behavior == .Pipe) {

test/standalone/test_runner_module_imports/build.zig

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ pub fn build(b: *std.Build) void {
1313

1414
const t = b.addTest(.{
1515
.root_module = test_mod,
16-
.test_runner = b.path("test_runner/main.zig"),
16+
.test_runner = .{
17+
.path = b.path("test_runner/main.zig"),
18+
.mode = .simple,
19+
},
1720
});
1821

1922
const test_step = b.step("test", "Run unit tests");

test/standalone/test_runner_path/build.zig

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ pub fn build(b: *std.Build) void {
88
.target = b.graph.host,
99
.root_source_file = b.path("test.zig"),
1010
}) });
11-
test_exe.test_runner = b.path("test_runner.zig");
11+
test_exe.test_runner = .{
12+
.path = b.path("test_runner.zig"),
13+
.mode = .simple,
14+
};
1215

1316
const test_run = b.addRunArtifact(test_exe);
1417
test_step.dependOn(&test_run.step);

0 commit comments

Comments
 (0)