0a3c567f
feat: attach auto-start — muxd proxy and local mux ensure a daemon
a73x 2026-08-10 17:32
Commit message
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 | } |