a73x

0a3c567f

feat: attach auto-start — muxd proxy and local mux ensure a daemon

a73x   2026-08-10 17:32

Commit message
feat: attach auto-start — muxd proxy and local mux ensure a daemon

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

build.zig
Old New
@@ -182,6 +182,7 @@ pub fn build(b: *std.Build) void {
182 mux_mod.addImport("client", client_mod); 182 mux_mod.addImport("client", client_mod);
183 mux_mod.addImport("build_options", version_opts.createModule()); 183 mux_mod.addImport("build_options", version_opts.createModule());
184 mux_mod.addImport("xdg", xdg_mod); 184 mux_mod.addImport("xdg", xdg_mod);
185 mux_mod.addImport("spawn", spawn_mod);
185 186
186 // No imports that teach it anything, deliberately: the proxy is a byte 187 // No imports that teach it anything, deliberately: the proxy is a byte
187 // pump that knows nothing about the protocol it carries. `testtmp` is 188 // pump that knows nothing about the protocol it carries. `testtmp` is
src/main.zig
Old New
@@ -244,7 +244,35 @@ pub fn main() !u8 {
244 .dump => return dump(alloc, sock_path, o.vt), 244 .dump => return dump(alloc, sock_path, o.vt),
245 .stats => return stats(alloc, sock_path), 245 .stats => return stats(alloc, sock_path),
246 .stop => return stopCmd(alloc, sock_path), 246 .stop => return stopCmd(alloc, sock_path),
247 .proxy => return proxy.run(sock_path), 247 .proxy => {
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
250 // for, never appear because someone attached. Same helper,
251 // deadline, and silence contract as `muxd start`.
252 var exe_buf: [std.fs.max_path_bytes]u8 = undefined;
253 const exe = std.fs.selfExePath(&exe_buf) catch {
254 std.debug.print("muxd proxy: cannot find own binary via /proc/self/exe\n", .{});
255 return 1;
256 };
257 const sock_z = try alloc.dupeZ(u8, sock_path);
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);
275 },
248 } 276 }
249 } 277 }
250 278
@@ -440,7 +468,7 @@ fn stopCmd(alloc: std.mem.Allocator, sock_path: []const u8) !u8 {
440 const asked = if (proto.writeFrame(stream.handle, .stop_req, "")) |_| true else |_| false; 468 const asked = if (proto.writeFrame(stream.handle, .stop_req, "")) |_| true else |_| false;
441 stream.close(); 469 stream.close();
442 470
443 // Probe-first, deadline-second — ensureDaemon's shape (spawn.zig:134), 471 // Probe-first, deadline-second — ensureDaemon's poll shape (spawn.zig),
444 // so the final window before the deadline is still probed and the 472 // so the final window before the deadline is still probed and the
445 // failure line is never printed about an interval nobody checked. 473 // failure line is never printed about an interval nobody checked.
446 // spawn.probe is the connect-refusal test: a live listener's backlog 474 // spawn.probe is the connect-refusal test: a live listener's backlog
src/mux_main.zig
Old New
@@ -6,6 +6,7 @@ const std = @import("std");
6 const client = @import("client"); 6 const client = @import("client");
7 const build_options = @import("build_options"); 7 const build_options = @import("build_options");
8 const xdg = @import("xdg"); 8 const xdg = @import("xdg");
9 const spawn = @import("spawn");
9 10
10 const usage = 11 const usage =
11 \\usage: mux [HOST | --sock PATH | --via CMD | quic://HOST[:PORT]] 12 \\usage: mux [HOST | --sock PATH | --via CMD | quic://HOST[:PORT]]
@@ -186,6 +187,41 @@ pub fn main() !u8 {
186 else 187 else
187 try std.fmt.allocPrint(alloc, "/tmp/muxd-{d}.sock", .{std.os.linux.getuid()}); 188 try std.fmt.allocPrint(alloc, "/tmp/muxd-{d}.sock", .{std.os.linux.getuid()});
188 defer alloc.free(sock_path); 189 defer alloc.free(sock_path);
190
191 // Attach auto-start (M13): give the attach a daemon to land on.
192 // Unix-socket transport only — quic:// has nothing local to
193 // spawn, and --via's auto-starter is the remote proxy. Bare
194 // `run`: a listener must be asked for, never be a side effect.
195 const muxd_path = try spawn.findInPath(
196 alloc,
197 std.posix.getenv("PATH") orelse "",
198 "muxd",
199 );
200 defer if (muxd_path) |p| alloc.free(p);
201 if (muxd_path) |exe| {
202 const sock_z = try alloc.dupeZ(u8, sock_path);
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)) {
219 // No muxd anywhere AND nothing serving: only now is the
220 // missing binary the user's problem, and both facts fit in
221 // one honest line. A live daemon needs no binary on PATH.
222 std.debug.print("mux: no daemon on {s} and no muxd in PATH to start one\n", .{sock_path});
223 return 1;
224 }
189 return client.attach(alloc, sock_path, null, null); 225 return client.attach(alloc, sock_path, null, null);
190 }, 226 },
191 } 227 }