a73x

d6452d03

refactor: spawn.ensureForAttach owns the attach shape; auto-start appends, never truncates

a73x   2026-08-10 17:53

Commit message
refactor: spawn.ensureForAttach owns the attach shape; auto-start appends, never truncates

The two attach call sites were sixteen identical lines modulo two
strings, and the 2s deadline was written three times unnamed across two
binaries. ensureForAttach owns the shape and spawn.start_deadline_ms
names the number; `muxd start` stays spelled out, because it forwards
the user's flags and reports already-running where an attach is silent.

The log open now takes a LogSpec. Auto-start passes truncate=false —
O_APPEND, not merely "do not truncate", since a non-appending fd would
overwrite the log from the front. Truncation is reserved for `muxd
start`, the one caller whose user asked for a (re)start; without that
split, one ssh attach could zero a live interactive daemon's log.

The up-line's leading space now appears only on a tty, where dots
precede it. e2e's non-tty pin moves from ' up (' to '^up (' to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

src/main.zig
Old New
@@ -246,31 +246,15 @@ pub fn main() !u8 {
246 .stop => return stopCmd(alloc, sock_path), 246 .stop => return stopCmd(alloc, sock_path),
247 .proxy => { 247 .proxy => {
248 // Attach auto-start (M13): the user asked for a session, not a 248 // Attach auto-start (M13): the user asked for a session, not a
249 // daemon. Bare `run` on purpose — a QUIC listener must be asked 249 // daemon. Same helper and deadline as `muxd start`. Unlike it, a
250 // for, never appear because someone attached. Same helper, 250 // daemon that was already there is silent: the user asked for a
251 // deadline, and silence contract as `muxd start`. 251 // session and is about to get one.
252 var exe_buf: [std.fs.max_path_bytes]u8 = undefined; 252 var exe_buf: [std.fs.max_path_bytes]u8 = undefined;
253 const exe = std.fs.selfExePath(&exe_buf) catch { 253 const exe = std.fs.selfExePath(&exe_buf) catch {
254 std.debug.print("muxd proxy: cannot find own binary via /proc/self/exe\n", .{}); 254 std.debug.print("muxd proxy: cannot find own binary via /proc/self/exe\n", .{});
255 return 1; 255 return 1;
256 }; 256 };
257 const sock_z = try alloc.dupeZ(u8, sock_path); 257 if (!try spawn.ensureForAttach(alloc, exe, sock_path, "muxd proxy")) return 1;
258 defer alloc.free(sock_z);
259 const run_args = [_][:0]const u8{ "--sock", sock_z };
260 const progress: spawn.Progress = .{
261 .fd = std.posix.STDERR_FILENO,
262 .prefix = "muxd proxy",
263 .tty = std.posix.isatty(std.posix.STDERR_FILENO),
264 };
265 _ = spawn.ensureDaemon(alloc, exe, &run_args, sock_path, progress, 2000, null) catch |err| switch (err) {
266 // The failure line, with the log path, was already printed
267 // by Progress — a second line would say the same thing worse.
268 error.NeverAnswered => return 1,
269 error.BinaryNotFound, error.SpawnFailed => {
270 std.debug.print("muxd proxy: could not spawn {s}: {s}\n", .{ exe, @errorName(err) });
271 return 1;
272 },
273 };
274 return proxy.run(sock_path); 258 return proxy.run(sock_path);
275 }, 259 },
276 } 260 }
@@ -532,8 +516,13 @@ fn startCmd(alloc: std.mem.Allocator, sock_path: []const u8, forwarded: []const
532 .prefix = "muxd", 516 .prefix = "muxd",
533 .tty = std.posix.isatty(std.posix.STDERR_FILENO), 517 .tty = std.posix.isatty(std.posix.STDERR_FILENO),
534 }; 518 };
535 // null log path: the xdg default is the whole point for a real daemon. 519 // Default log path: the xdg one is the whole point for a real daemon.
536 const r = spawn.ensureDaemon(alloc, exe, forwarded, sock_path, progress, 2000, null) catch |err| switch (err) { 520 // Truncating, and this is the only caller that does: `start` is the one
521 // verb whose user asked for a (re)start, so the log they go on to read
522 // must be about the daemon they just started.
523 const r = spawn.ensureDaemon(alloc, exe, forwarded, sock_path, progress, spawn.start_deadline_ms, .{
524 .truncate = true,
525 }) catch |err| switch (err) {
537 // The failure line, with the log path, was already printed by 526 // The failure line, with the log path, was already printed by
538 // Progress — a second line here would say the same thing worse. 527 // Progress — a second line here would say the same thing worse.
539 error.NeverAnswered => return 1, 528 error.NeverAnswered => return 1,
src/mux_main.zig
Old New
@@ -190,8 +190,7 @@ pub fn main() !u8 {
190 190
191 // Attach auto-start (M13): give the attach a daemon to land on. 191 // Attach auto-start (M13): give the attach a daemon to land on.
192 // Unix-socket transport only — quic:// has nothing local to 192 // Unix-socket transport only — quic:// has nothing local to
193 // spawn, and --via's auto-starter is the remote proxy. Bare 193 // spawn, and --via's auto-starter is the remote proxy.
194 // `run`: a listener must be asked for, never be a side effect.
195 const muxd_path = try spawn.findInPath( 194 const muxd_path = try spawn.findInPath(
196 alloc, 195 alloc,
197 std.posix.getenv("PATH") orelse "", 196 std.posix.getenv("PATH") orelse "",
@@ -199,22 +198,7 @@ pub fn main() !u8 {
199 ); 198 );
200 defer if (muxd_path) |p| alloc.free(p); 199 defer if (muxd_path) |p| alloc.free(p);
201 if (muxd_path) |exe| { 200 if (muxd_path) |exe| {
202 const sock_z = try alloc.dupeZ(u8, sock_path); 201 if (!try spawn.ensureForAttach(alloc, exe, sock_path, "mux")) return 1;
203 defer alloc.free(sock_z);
204 const run_args = [_][:0]const u8{ "--sock", sock_z };
205 const progress: spawn.Progress = .{
206 .fd = std.posix.STDERR_FILENO,
207 .prefix = "mux",
208 .tty = std.posix.isatty(std.posix.STDERR_FILENO),
209 };
210 _ = spawn.ensureDaemon(alloc, exe, &run_args, sock_path, progress, 2000, null) catch |err| switch (err) {
211 // ensureDaemon's own failure line named the log.
212 error.NeverAnswered => return 1,
213 error.BinaryNotFound, error.SpawnFailed => {
214 std.debug.print("mux: could not spawn {s}: {s}\n", .{ exe, @errorName(err) });
215 return 1;
216 },
217 };
218 } else if (!spawn.probe(sock_path)) { 202 } else if (!spawn.probe(sock_path)) {
219 // No muxd anywhere AND nothing serving: only now is the 203 // No muxd anywhere AND nothing serving: only now is the
220 // missing binary the user's problem, and both facts fit in 204 // missing binary the user's problem, and both facts fit in
src/spawn.zig
Old New
@@ -1,21 +1,48 @@
1 //! Get a daemon onto a socket path: probe, spawn detached, poll until it 1 //! Get a daemon onto a socket path: probe, spawn detached, poll until it
2 //! answers. `muxd start` is the explicit caller today; attach auto-start 2 //! answers. Three callers: `muxd start`, which is explicit and spelled
3 //! (banked) becomes a second call site, not a rewrite. 3 //! out, and the two attach auto-starts (`muxd proxy` and `mux`), which
4 //! share `ensureForAttach` below. The M10 prediction held — auto-start
5 //! arrived as call sites, not as a rewrite.
4 const std = @import("std"); 6 const std = @import("std");
5 const xdg = @import("xdg"); 7 const xdg = @import("xdg");
6 8
7 pub const EnsureError = error{ BinaryNotFound, SpawnFailed, NeverAnswered }; 9 pub const EnsureError = error{ BinaryNotFound, SpawnFailed, NeverAnswered };
8 pub const Ensured = enum { already_running, started }; 10 pub const Ensured = enum { already_running, started };
9 11
12 /// How long a spawn gets to answer, for every caller. One number because a
13 /// user who waited two seconds for `muxd start` must not wait a different
14 /// two seconds for an attach that starts the same daemon the same way: the
15 /// deadline describes how long muxd takes to bind, which is not a fact
16 /// about which verb asked for it.
17 pub const start_deadline_ms: u32 = 2000;
18
19 /// Where a spawned daemon's stdout+stderr go, and what becomes of whatever
20 /// is already there. `truncate` has no default on purpose — the callers
21 /// want opposite answers, so each one has to say which it means.
22 pub const LogSpec = struct {
23 /// null means the xdg default, which is what every real caller wants.
24 /// It is settable at all so tests can point it somewhere disposable:
25 /// the default path belongs to whatever daemon is running on the
26 /// machine, and writing over it from a unit test would punch a hole in
27 /// a live daemon's log.
28 path: ?[]const u8 = null,
29 /// True only for `muxd start`, the one caller whose user asked for a
30 /// (re)start and is owed a log about the daemon they just started
31 /// rather than a stale one. Auto-start appends instead: an attach is
32 /// not a restart, and one `mux` over ssh must not zero the log of an
33 /// interactive daemon that is still writing into it.
34 truncate: bool,
35 };
36
10 /// Test hook: the pid of the most recent spawn. Tests use it to reap the 37 /// Test hook: the pid of the most recent spawn. Tests use it to reap the
11 /// deliberately-orphaned stub; muxd start reads it for the up-line. Not 38 /// deliberately-orphaned stub; muxd start reads it for the up-line. Not
12 /// synchronized — single-threaded callers only, which both callers are. 39 /// synchronized — single-threaded callers only, which all three are.
13 pub var last_spawned_pid: std.posix.pid_t = 0; 40 pub var last_spawned_pid: std.posix.pid_t = 0;
14 41
15 /// All stderr output belongs to this struct: the caller decides the prefix 42 /// All of ensureDaemon's stderr output belongs to this struct: the caller
16 /// ("muxd" today, "mux" when auto-start lands) and whether dots animate. 43 /// decides the prefix ("muxd", "muxd proxy", "mux") and whether dots
17 /// Silence on the already-running path is part of the contract — any 44 /// animate. Silence on the already-running path is part of the contract —
18 /// output at all means something unusual happened. 45 /// any output at all means something unusual happened.
19 pub const Progress = struct { 46 pub const Progress = struct {
20 fd: std.posix.fd_t, 47 fd: std.posix.fd_t,
21 prefix: []const u8, 48 prefix: []const u8,
@@ -33,8 +60,9 @@ pub const Progress = struct {
33 }; 60 };
34 61
35 /// Probe `sock_path`; if nothing answers, exec `exe_path run <run_args...>` 62 /// Probe `sock_path`; if nothing answers, exec `exe_path run <run_args...>`
36 /// detached (setsid, stdin /dev/null, stdout+stderr truncating the xdg log) 63 /// detached (setsid, stdin /dev/null, stdout+stderr to the log `log_spec`
37 /// and poll every 50ms until the socket accepts or `deadline_ms` passes. 64 /// names) and poll every 50ms until the socket accepts or `deadline_ms`
65 /// passes.
38 /// 66 ///
39 /// On `NeverAnswered` the spawned pid is deliberately NOT killed: a daemon 67 /// On `NeverAnswered` the spawned pid is deliberately NOT killed: a daemon
40 /// that comes up at 2.5s should be there for the retry, not murdered for 68 /// that comes up at 2.5s should be there for the retry, not murdered for
@@ -43,11 +71,6 @@ pub const Progress = struct {
43 /// Two racers both spawning is handled by the daemon itself: the loser 71 /// Two racers both spawning is handled by the daemon itself: the loser
44 /// exits on DaemonAlreadyRunning (server.zig claimSockPath) and the 72 /// exits on DaemonAlreadyRunning (server.zig claimSockPath) and the
45 /// loser's poll connects to the winner. 73 /// loser's poll connects to the winner.
46 /// `log_path` names where the child's stdout+stderr go; null means the xdg
47 /// default, which is what every real caller wants. It is a parameter at all
48 /// so tests can point it somewhere disposable: the default path belongs to
49 /// whatever daemon is running on the machine, and truncating it from a unit
50 /// test would punch a hole in a live daemon's log.
51 pub fn ensureDaemon( 74 pub fn ensureDaemon(
52 alloc: std.mem.Allocator, 75 alloc: std.mem.Allocator,
53 exe_path: []const u8, 76 exe_path: []const u8,
@@ -55,7 +78,7 @@ pub fn ensureDaemon(
55 sock_path: []const u8, 78 sock_path: []const u8,
56 progress: Progress, 79 progress: Progress,
57 deadline_ms: u32, 80 deadline_ms: u32,
58 log_path_opt: ?[]const u8, 81 log_spec: LogSpec,
59 ) EnsureError!Ensured { 82 ) EnsureError!Ensured {
60 if (probe(sock_path)) return .already_running; 83 if (probe(sock_path)) return .already_running;
61 84
@@ -63,15 +86,26 @@ pub fn ensureDaemon(
63 86
64 // Owned either way, so one `free` covers both: a caller-supplied path is 87 // Owned either way, so one `free` covers both: a caller-supplied path is
65 // duped rather than borrowed, because the xdg branch must allocate. 88 // duped rather than borrowed, because the xdg branch must allocate.
66 const log_path = if (log_path_opt) |p| 89 const log_path = if (log_spec.path) |p|
67 alloc.dupe(u8, p) catch return error.SpawnFailed 90 alloc.dupe(u8, p) catch return error.SpawnFailed
68 else 91 else
69 xdg.logPath(alloc) catch return error.SpawnFailed; 92 xdg.logPath(alloc) catch return error.SpawnFailed;
70 defer alloc.free(log_path); 93 defer alloc.free(log_path);
71 if (std.fs.path.dirname(log_path)) |dir| 94 if (std.fs.path.dirname(log_path)) |dir|
72 std.fs.cwd().makePath(dir) catch return error.SpawnFailed; 95 std.fs.cwd().makePath(dir) catch return error.SpawnFailed;
73 const log = std.fs.cwd().createFile(log_path, .{ .truncate = true, .mode = 0o600 }) catch 96 // Opened by hand rather than through createFile because the non-
74 return error.SpawnFailed; 97 // truncating case needs O_APPEND specifically, which createFile cannot
98 // ask for. "Do not truncate" alone would be worse than truncating: the
99 // child's fd would start at offset zero and overwrite the log from the
100 // front, and two daemons sharing the path would overwrite each other.
101 // The kernel's atomic seek-to-end is the whole of what makes appending
102 // safe for a file someone else may be writing.
103 const log: std.fs.File = .{ .handle = std.posix.open(log_path, .{
104 .ACCMODE = .WRONLY,
105 .CREAT = true,
106 .TRUNC = log_spec.truncate,
107 .APPEND = !log_spec.truncate,
108 }, 0o600) catch return error.SpawnFailed };
75 defer log.close(); 109 defer log.close();
76 const devnull = std.fs.cwd().openFile("/dev/null", .{}) catch return error.SpawnFailed; 110 const devnull = std.fs.cwd().openFile("/dev/null", .{}) catch return error.SpawnFailed;
77 defer devnull.close(); 111 defer devnull.close();
@@ -134,7 +168,12 @@ pub fn ensureDaemon(
134 while (true) { 168 while (true) {
135 if (probe(sock_path)) { 169 if (probe(sock_path)) {
136 const secs = @as(f64, @floatFromInt(std.time.milliTimestamp() - t0)) / 1000.0; 170 const secs = @as(f64, @floatFromInt(std.time.milliTimestamp() - t0)) / 1000.0;
137 progress.emitFmt(" up ({d:.1}s) pid={d}\n", .{ secs, pid }); 171 // The leading space exists to follow the dots on a tty. There
172 // are no dots on a non-tty — the ssh proxy's case, and now the
173 // common one — where it would only be a stray space at the
174 // start of a scripted line.
175 if (progress.tty) progress.emit(" ");
176 progress.emitFmt("up ({d:.1}s) pid={d}\n", .{ secs, pid });
138 return .started; 177 return .started;
139 } 178 }
140 const now = std.time.milliTimestamp(); 179 const now = std.time.milliTimestamp();
@@ -144,9 +183,9 @@ pub fn ensureDaemon(
144 // and it would only put a blank line into scripted output. 183 // and it would only put a blank line into scripted output.
145 if (progress.tty) progress.emit("\n"); 184 if (progress.tty) progress.emit("\n");
146 // "daemon", not "muxd", in the body: the prefix is the program 185 // "daemon", not "muxd", in the body: the prefix is the program
147 // speaking, and this same string serves the mux-side caller 186 // speaking, and the same string serves the mux-side callers —
148 // when auto-start lands — `mux: daemon did not answer` reads 187 // `mux: daemon did not answer` reads correctly, `mux: muxd did
149 // correctly, `mux: muxd did not answer` would not. 188 // not answer` would not.
150 progress.emitFmt( 189 progress.emitFmt(
151 "{s}: daemon did not answer within {d}s \u{2014} log: {s}\n", 190 "{s}: daemon did not answer within {d}s \u{2014} log: {s}\n",
152 .{ progress.prefix, deadline_ms / 1000, log_path }, 191 .{ progress.prefix, deadline_ms / 1000, log_path },
@@ -176,6 +215,60 @@ pub fn probe(sock_path: []const u8) bool {
176 return true; 215 return true;
177 } 216 }
178 217
218 /// The attach shape, owned in one place: both auto-start call sites (`muxd
219 /// proxy` and `mux`) want exactly this — a BARE `run --sock <path>`, the
220 /// shared deadline, stderr progress under the caller's own prefix, and a
221 /// log that is appended to rather than truncated.
222 ///
223 /// True means there is a daemon to attach to, started or already up. False
224 /// means give up: the reason is already on stderr, so a caller's `return 1`
225 /// needs no message of its own.
226 ///
227 /// `muxd start` deliberately does NOT go through here. It forwards the
228 /// user's own flags rather than a fixed pair, it truncates the log, and it
229 /// REPORTS already-running where this stays silent — because only one of
230 /// the two was asked for. Someone who typed `muxd start` asked about a
231 /// daemon and is owed a verdict on one; someone who typed `mux` asked for a
232 /// session and is about to get it.
233 pub fn ensureForAttach(
234 alloc: std.mem.Allocator,
235 exe: []const u8,
236 sock_path: []const u8,
237 prefix: []const u8,
238 ) error{OutOfMemory}!bool {
239 const sock_z = try alloc.dupeZ(u8, sock_path);
240 defer alloc.free(sock_z);
241 // Bare, and the reason is the whole of why this is not `start`: a QUIC
242 // listener must be asked for, never appear because someone attached.
243 const run_args = [_][:0]const u8{ "--sock", sock_z };
244 const progress: Progress = .{
245 .fd = std.posix.STDERR_FILENO,
246 .prefix = prefix,
247 .tty = std.posix.isatty(std.posix.STDERR_FILENO),
248 };
249 _ = ensureDaemon(
250 alloc,
251 exe,
252 &run_args,
253 sock_path,
254 progress,
255 start_deadline_ms,
256 .{ .truncate = false },
257 ) catch |err| switch (err) {
258 // The failure line, with the log path, was already printed by
259 // Progress — a second line would say the same thing worse.
260 error.NeverAnswered => return false,
261 error.BinaryNotFound, error.SpawnFailed => {
262 // std.debug.print rather than progress.emitFmt: an exe path can
263 // run to max_path_bytes, and emitFmt's fixed buffer would drop
264 // the whole line rather than shorten it.
265 std.debug.print("{s}: could not spawn {s}: {s}\n", .{ prefix, exe, @errorName(err) });
266 return false;
267 },
268 };
269 return true;
270 }
271
179 /// Walk a colon-separated `path_env` for an executable `name`; the first 272 /// Walk a colon-separated `path_env` for an executable `name`; the first
180 /// hit wins, execvp's own rule. Caller owns the returned path. Takes the 273 /// hit wins, execvp's own rule. Caller owns the returned path. Takes the
181 /// PATH string rather than reading the environment so tests stay 274 /// PATH string rather than reading the environment so tests stay
@@ -242,7 +335,7 @@ test "ensureDaemon: an answering socket is already_running, nothing spawned" {
242 sock, 335 sock,
243 progress, 336 progress,
244 200, 337 200,
245 null, 338 .{ .truncate = true },
246 ); 339 );
247 try std.testing.expectEqual(Ensured.already_running, r); 340 try std.testing.expectEqual(Ensured.already_running, r);
248 341
@@ -263,7 +356,7 @@ test "ensureDaemon: missing binary is BinaryNotFound before any fork" {
263 sock, 356 sock,
264 silentProgress(), 357 silentProgress(),
265 200, 358 200,
266 null, 359 .{ .truncate = true },
267 )); 360 ));
268 } 361 }
269 362
@@ -293,7 +386,7 @@ test "ensureDaemon: a child that dies young is reported, not panicked on" {
293 sock, 386 sock,
294 silentProgress(), 387 silentProgress(),
295 300, 388 300,
296 log, 389 .{ .path = log, .truncate = true },
297 )); 390 ));
298 // The log is still there to be named by the failure line: a child that 391 // The log is still there to be named by the failure line: a child that
299 // died is exactly when an operator goes looking for it. 392 // died is exactly when an operator goes looking for it.
@@ -332,7 +425,7 @@ test "ensureDaemon: a binary that never binds is NeverAnswered, pid left alive"
332 sock, 425 sock,
333 silentProgress(), 426 silentProgress(),
334 300, 427 300,
335 log, 428 .{ .path = log, .truncate = true },
336 )); 429 ));
337 // The log is created before the fork, so it exists even when the child 430 // The log is created before the fork, so it exists even when the child
338 // never writes to it — that is what makes it the place to look when a 431 // never writes to it — that is what makes it the place to look when a
@@ -358,6 +451,68 @@ test "ensureDaemon: a binary that never binds is NeverAnswered, pid left alive"
358 _ = std.posix.waitpid(last_spawned_pid, 0); 451 _ = std.posix.waitpid(last_spawned_pid, 0);
359 } 452 }
360 453
454 test "ensureDaemon: an attach appends to the log, `muxd start` truncates it" {
455 var tmp = try testtmp.TmpDir.make();
456 defer tmp.cleanup();
457 var pbuf: [128]u8 = undefined;
458 var sbuf: [128]u8 = undefined;
459 var lbuf: [128]u8 = undefined;
460 const stub = try std.fmt.bufPrint(&pbuf, "{s}/dies.sh", .{tmp.path()});
461 const sock = try std.fmt.bufPrint(&sbuf, "{s}/dead.sock", .{tmp.path()});
462 const log = try std.fmt.bufPrint(&lbuf, "{s}/logs/muxd.log", .{tmp.path()});
463
464 // Exits at once and binds nothing, so both spawns below end in
465 // NeverAnswered — which is beside the point. The log is opened BEFORE
466 // the fork, so what happened to the bytes already in it is settled
467 // whether the daemon ever comes up or not, and a stub that writes
468 // nothing keeps the two file contents readable as pure evidence of the
469 // open mode.
470 try tmp.dir.writeFile(.{ .sub_path = "dies.sh", .data = "#!/bin/sh\nexit 3\n" });
471 const f = try tmp.dir.openFile("dies.sh", .{});
472 try f.chmod(0o755);
473 f.close();
474
475 const seed = "a live daemon was writing here\n";
476 try std.fs.cwd().makePath(std.fs.path.dirname(log).?);
477 try std.fs.cwd().writeFile(.{ .sub_path = log, .data = seed });
478
479 // The attach shape must leave it alone. Without this, one `mux` over
480 // ssh — no XDG_RUNTIME_DIR, so a /tmp socket nobody happens to be
481 // serving — zeroes the log of the interactive daemon still writing to
482 // it, and the operator reads a hole where the crash was.
483 try std.testing.expectError(error.NeverAnswered, ensureDaemon(
484 std.testing.allocator,
485 stub,
486 &.{},
487 sock,
488 silentProgress(),
489 100,
490 .{ .path = log, .truncate = false },
491 ));
492 var rbuf: [256]u8 = undefined;
493 // Equality, not "contains": appending must add at the END, so a mode
494 // that wrote over the front and happened to be shorter still fails.
495 try std.testing.expectEqualStrings(seed, try std.fs.cwd().readFile(log, &rbuf));
496
497 // `muxd start` still truncates, and that half is asserted here rather
498 // than assumed: the two verbs differ only in this flag, so a change that
499 // made everything append would otherwise pass unnoticed until an
500 // operator read a restarted daemon's log and found the old one's.
501 try std.testing.expectError(error.NeverAnswered, ensureDaemon(
502 std.testing.allocator,
503 stub,
504 &.{},
505 sock,
506 silentProgress(),
507 100,
508 .{ .path = log, .truncate = true },
509 ));
510 try std.testing.expectEqual(
511 @as(usize, 0),
512 (try std.fs.cwd().readFile(log, &rbuf)).len,
513 );
514 }
515
361 test "findInPath: first executable hit wins; non-executables are skipped" { 516 test "findInPath: first executable hit wins; non-executables are skipped" {
362 const alloc = std.testing.allocator; 517 const alloc = std.testing.allocator;
363 var tmp = try testtmp.TmpDir.make(); 518 var tmp = try testtmp.TmpDir.make();
test/e2e.sh
Old New
@@ -1084,7 +1084,7 @@ rm -f "$OUT.q" "$OUT.qc" "$OUT.qr" "$OUT.qa" "$OUT.qk" "$QKEY" "$QKEY.bad" "$QKE
1084 "$MUXD" start --sock "$SOCK8" 2> "$OUT.start" 1084 "$MUXD" start --sock "$SOCK8" 2> "$OUT.start"
1085 grep -q '^muxd: starting' "$OUT.start" || { 1085 grep -q '^muxd: starting' "$OUT.start" || {
1086 echo "e2e FAIL: start printed no starting line"; cat "$OUT.start"; exit 1; } 1086 echo "e2e FAIL: start printed no starting line"; cat "$OUT.start"; exit 1; }
1087 grep -q ' up (' "$OUT.start" || { 1087 grep -q '^up (' "$OUT.start" || {
1088 echo "e2e FAIL: start printed no up line"; cat "$OUT.start"; exit 1; } 1088 echo "e2e FAIL: start printed no up line"; cat "$OUT.start"; exit 1; }
1089 # Known gap, accepted: between the spawn above and this capture the daemon 1089 # Known gap, accepted: between the spawn above and this capture the daemon
1090 # is running with no pid the trap can reach, so a failure in the two 1090 # is running with no pid the trap can reach, so a failure in the two