e144f97c
refactor: a dial leaves ssh's last line AND the pid that said it
a73x 2026-08-30 10:58
Commit message
src/client/client.zig
| Old | New | ||
|---|---|---|---|
| @@ -463,15 +463,17 @@ pub const Transport = struct { | |||
| 463 | /// waits: stdin in the CLI, -1 (no abort channel) in a hub that | 463 | /// waits: stdin in the CLI, -1 (no abort channel) in a hub that |
| 464 | /// has no terminal — its stray fd 0 must never be read. | 464 | /// has no terminal — its stray fd 0 must never be read. |
| 465 | abort_fd: std.posix.fd_t, | 465 | abort_fd: std.posix.fd_t, |
| 466 | /// Where the handoff ssh's last line is left for the caller, so a | 466 | /// What the handoff leaves behind: ssh's last line, so a dial that |
| 467 | /// dial that failed can be reported in ssh's own words. Null is | 467 | /// failed is reported in ssh's own words, and the pid of the ssh |
| 468 | /// every caller that has nowhere to show it. | 468 | /// that said it, so a prompt that ssh raised can be answered — and |
| 469 | reason: ?*handoff.Reason, | 469 | /// a refusal remembered — against the one dial it belongs to. Null |
| 470 | /// is every caller that has nowhere to show either. | ||
| 471 | dial: ?*handoff.Dial, | ||
| 470 | ) !Transport { | 472 | ) !Transport { |
| 471 | switch (target) { | 473 | switch (target) { |
| 472 | // Delegated whole, because the handoff can end up producing | 474 | // Delegated whole, because the handoff can end up producing |
| 473 | // either of the two links below and owns the choice itself. | 475 | // either of the two links below and owns the choice itself. |
| 474 | .hand => |h| return openHandoff(alloc, h, carry, abort_fd, reason), | 476 | .hand => |h| return openHandoff(alloc, h, carry, abort_fd, dial), |
| 475 | .quic => |q| { | 477 | .quic => |q| { |
| 476 | const key = try quic.Key.load(q.key_path); | 478 | const key = try quic.Key.load(q.key_path); |
| 477 | const addr = try quic.parseAddr(alloc, q.host_port); | 479 | const addr = try quic.parseAddr(alloc, q.host_port); |
| @@ -503,7 +505,7 @@ pub const Transport = struct { | |||
| 503 | h: HandoffTarget, | 505 | h: HandoffTarget, |
| 504 | carry: ?*std.ArrayList(u8), | 506 | carry: ?*std.ArrayList(u8), |
| 505 | abort_fd: std.posix.fd_t, | 507 | abort_fd: std.posix.fd_t, |
| 506 | reason: ?*handoff.Reason, | 508 | dial: ?*handoff.Dial, |
| 507 | ) !Transport { | 509 | ) !Transport { |
| 508 | // The ORDER is `handoff.next`'s; this loop performs the step it is | 510 | // The ORDER is `handoff.next`'s; this loop performs the step it is |
| 509 | // handed and reports what came of it. | 511 | // handed and reports what came of it. |
| @@ -522,10 +524,11 @@ pub const Transport = struct { | |||
| 522 | // A caller with nowhere to show a reason still needs one kept: the | 524 | // A caller with nowhere to show a reason still needs one kept: the |
| 523 | // Transport carries it, and a failed dial's last line would | 525 | // Transport carries it, and a failed dial's last line would |
| 524 | // otherwise have to be read twice. | 526 | // otherwise have to be read twice. |
| 525 | var local_reason: handoff.Reason = .{}; | 527 | var local_dial: handoff.Dial = .{}; |
| 528 | const out = dial orelse &local_dial; | ||
| 526 | var errp: ErrPipe = .{ | 529 | var errp: ErrPipe = .{ |
| 527 | .fd = -1, | 530 | .fd = -1, |
| 528 | .reason = reason orelse &local_reason, | 531 | .reason = &out.reason, |
| 529 | .narrate = h.narrate, | 532 | .narrate = h.narrate, |
| 530 | }; | 533 | }; |
| 531 | errdefer errp.close(); | 534 | errdefer errp.close(); |
| @@ -586,6 +589,12 @@ pub const Transport = struct { | |||
| 586 | errp.fd = f.handle; | 589 | errp.fd = f.handle; |
| 587 | child.?.stderr = null; | 590 | child.?.stderr = null; |
| 588 | } | 591 | } |
| 592 | // Recorded HERE rather than on the way out, because the | ||
| 593 | // caller that needs it is the one this call is about to | ||
| 594 | // fail: a dial whose ssh asked for a password the user | ||
| 595 | // refused has no transport to read a pid off, and the | ||
| 596 | // refusal is keyed on exactly this number. | ||
| 597 | out.ssh_pid = child.?.id; | ||
| 589 | break :blk .ok; | 598 | break :blk .ok; |
| 590 | }, | 599 | }, |
| 591 | .read_announce => blk: { | 600 | .read_announce => blk: { |
| @@ -1341,9 +1350,9 @@ pub fn listSessions( | |||
| 1341 | out: *[proto.sessions_text_max]u8, | 1350 | out: *[proto.sessions_text_max]u8, |
| 1342 | budget_ms: i64, | 1351 | budget_ms: i64, |
| 1343 | answered: ?*std.meta.Tag(Link), | 1352 | answered: ?*std.meta.Tag(Link), |
| 1344 | /// Where ssh's last line is left when the dial fails: the picker row | 1353 | /// Where the dial's leavings go when it fails: the picker row quotes |
| 1345 | /// quotes it, so a box that is down says why instead of `unreachable`. | 1354 | /// ssh's line, so a box that is down says why instead of `unreachable`. |
| 1346 | reason: ?*handoff.Reason, | 1355 | dial: ?*handoff.Dial, |
| 1347 | ) ![]const u8 { | 1356 | ) ![]const u8 { |
| 1348 | // A fresh connection per poll: the observer idle deadline and the | 1357 | // A fresh connection per poll: the observer idle deadline and the |
| 1349 | // redial backoff stay the pump's problem, and a `--via` host — an ssh | 1358 | // redial backoff stay the pump's problem, and a `--via` host — an ssh |
| @@ -1359,7 +1368,7 @@ pub fn listSessions( | |||
| 1359 | .quic => .quic, | 1368 | .quic => .quic, |
| 1360 | .sock => .fd, | 1369 | .sock => .fd, |
| 1361 | }; | 1370 | }; |
| 1362 | var tr = Transport.open(alloc, target, null, -1, reason) catch |e| return oomOrTransport(e); | 1371 | var tr = Transport.open(alloc, target, null, -1, dial) catch |e| return oomOrTransport(e); |
| 1363 | defer tr.close(); | 1372 | defer tr.close(); |
| 1364 | // The handoff picks its own link, so only the success case knows it. | 1373 | // The handoff picks its own link, so only the success case knows it. |
| 1365 | if (answered) |a| a.* = tr.link; | 1374 | if (answered) |a| a.* = tr.link; |
| @@ -1502,7 +1511,7 @@ pub const SessionPoll = struct { | |||
| 1502 | // The poll's OWN reason, copied in under the lock below: this | 1511 | // The poll's OWN reason, copied in under the lock below: this |
| 1503 | // thread is the only writer, and a row must never read a | 1512 | // thread is the only writer, and a row must never read a |
| 1504 | // sentence being written into it. | 1513 | // sentence being written into it. |
| 1505 | var said: handoff.Reason = .{}; | 1514 | var said: handoff.Dial = .{}; |
| 1506 | const got = listSessions(std.heap.page_allocator, target, &out, 2000, &link, &said) catch null; | 1515 | const got = listSessions(std.heap.page_allocator, target, &out, 2000, &link, &said) catch null; |
| 1507 | if (got) |list| { | 1516 | if (got) |list| { |
| 1508 | self.list_mu.lock(); | 1517 | self.list_mu.lock(); |
| @@ -1515,7 +1524,7 @@ pub const SessionPoll = struct { | |||
| 1515 | self.reachable.store(true, .release); | 1524 | self.reachable.store(true, .release); |
| 1516 | } else { | 1525 | } else { |
| 1517 | self.list_mu.lock(); | 1526 | self.list_mu.lock(); |
| 1518 | self.reason = said; | 1527 | self.reason = said.reason; |
| 1519 | self.list_mu.unlock(); | 1528 | self.list_mu.unlock(); |
| 1520 | self.reachable.store(false, .release); | 1529 | self.reachable.store(false, .release); |
| 1521 | } | 1530 | } |
| @@ -2441,7 +2450,7 @@ test "openHandoff: the handoff ssh's stderr is a pipe, and only `narrate` relays | |||
| 2441 | \\exit 1 | 2450 | \\exit 1 |
| 2442 | , .{ .d = tmp.path() }); | 2451 | , .{ .d = tmp.path() }); |
| 2443 | 2452 | ||
| 2444 | var reason: handoff.Reason = .{}; | 2453 | var dial: handoff.Dial = .{}; |
| 2445 | var cap = try CapturedStderr.install(); | 2454 | var cap = try CapturedStderr.install(); |
| 2446 | const opened = Transport.open(alloc, .{ .hand = .{ | 2455 | const opened = Transport.open(alloc, .{ .hand = .{ |
| 2447 | .host = "fake", | 2456 | .host = "fake", |
| @@ -2451,7 +2460,7 @@ test "openHandoff: the handoff ssh's stderr is a pipe, and only `narrate` relays | |||
| 2451 | .deadline_ms = 200, | 2460 | .deadline_ms = 200, |
| 2452 | .asked = asked, | 2461 | .asked = asked, |
| 2453 | .narrate = narrate, | 2462 | .narrate = narrate, |
| 2454 | } }, &carry, std.posix.STDIN_FILENO, &reason); | 2463 | } }, &carry, std.posix.STDIN_FILENO, &dial); |
| 2455 | var relayed_buf: [256]u8 = undefined; | 2464 | var relayed_buf: [256]u8 = undefined; |
| 2456 | const relayed = try cap.take(&relayed_buf); | 2465 | const relayed = try cap.take(&relayed_buf); |
| 2457 | try expectNoSession("the stderr fixture's box announced a session it has no daemon for", opened); | 2466 | try expectNoSession("the stderr fixture's box announced a session it has no daemon for", opened); |
| @@ -2462,7 +2471,11 @@ test "openHandoff: the handoff ssh's stderr is a pipe, and only `narrate` relays | |||
| 2462 | // Kept in every case: the picker row is painted from a dial | 2471 | // Kept in every case: the picker row is painted from a dial |
| 2463 | // nobody narrated, which is the whole point of keeping it here | 2472 | // nobody narrated, which is the whole point of keeping it here |
| 2464 | // rather than letting the bytes fall out onto a screen. | 2473 | // rather than letting the bytes fall out onto a screen. |
| 2465 | try std.testing.expectEqualStrings("boom: no route", reason.slice()); | 2474 | try std.testing.expectEqualStrings("boom: no route", dial.reason.slice()); |
| 2475 | // A dial that spawned an ssh knows which one, whatever became of | ||
| 2476 | // it: the refusal a prompt earns is keyed on this pid, and the | ||
| 2477 | // dial it belongs to is the one that just failed. | ||
| 2478 | try std.testing.expect(dial.ssh_pid > 0); | ||
| 2466 | if (narrate) { | 2479 | if (narrate) { |
| 2467 | try std.testing.expectEqualStrings("boom: no route\n", relayed); | 2480 | try std.testing.expectEqualStrings("boom: no route\n", relayed); |
| 2468 | } else { | 2481 | } else { |
src/client/handoff.zig
| Old | New | ||
|---|---|---|---|
| @@ -152,6 +152,19 @@ pub const Reason = struct { | |||
| 152 | } | 152 | } |
| 153 | }; | 153 | }; |
| 154 | 154 | ||
| 155 | /// What one dial left behind for whoever has somewhere to show it. | ||
| 156 | /// | ||
| 157 | /// Two facts with one lifetime: a failed dial is REPORTED in ssh's own | ||
| 158 | /// words and ATTRIBUTED to the ssh that said them, and a caller that had | ||
| 159 | /// to thread two out-params would be a caller free to thread one. | ||
| 160 | pub const Dial = struct { | ||
| 161 | reason: Reason = .{}, | ||
| 162 | /// The coordination ssh this dial spawned, 0 when it spawned none. | ||
| 163 | /// Whoever answers that ssh's prompts is keyed on it: a prompt the | ||
| 164 | /// user refused belongs to one dial, not to the host forever. | ||
| 165 | ssh_pid: std.posix.pid_t = 0, | ||
| 166 | }; | ||
| 167 | |||
| 155 | pub const CacheError = error{ | 168 | pub const CacheError = error{ |
| 156 | CacheMissing, | 169 | CacheMissing, |
| 157 | CachePermissive, | 170 | CachePermissive, |
src/tui/wallview.zig
| Old | New | ||
|---|---|---|---|
| @@ -1384,10 +1384,10 @@ pub fn runAttach( | |||
| 1384 | // The one dial with a person waiting on it, so the one dial that keeps | 1384 | // The one dial with a person waiting on it, so the one dial that keeps |
| 1385 | // ssh's last line: the failure below is said in ssh's words when ssh | 1385 | // ssh's last line: the failure below is said in ssh's words when ssh |
| 1386 | // had any, and in mux's only when it did not. | 1386 | // had any, and in mux's only when it did not. |
| 1387 | var reason: handoff.Reason = .{}; | 1387 | var dial: handoff.Dial = .{}; |
| 1388 | var transport = client.Transport.open(alloc, target, &carry, std.posix.STDIN_FILENO, &reason) catch |err| { | 1388 | var transport = client.Transport.open(alloc, target, &carry, std.posix.STDIN_FILENO, &dial) catch |err| { |
| 1389 | var buf: [client.open_err_len]u8 = undefined; | 1389 | var buf: [client.open_err_len]u8 = undefined; |
| 1390 | const f = client.openFailure(&buf, target, err, reason.slice()); | 1390 | const f = client.openFailure(&buf, target, err, dial.reason.slice()); |
| 1391 | std.debug.print("{s}", .{f.msg}); | 1391 | std.debug.print("{s}", .{f.msg}); |
| 1392 | return f.exit; | 1392 | return f.exit; |
| 1393 | }; | 1393 | }; |