From b1f93b90c5cf73207a4c260dc15abe8d2ad1d9bc Mon Sep 17 00:00:00 2001 From: Neil Date: Thu, 10 Sep 2026 20:42:45 -0700 Subject: [PATCH] fix(ssh): record the superseded-relay pass the Windows arm abandons `sweepSupersededRelayEndpoints` returned `[]` for every Windows remote host and for every failed listing without writing a line. Both returns are indistinguishable from "this host had no orphans", which is the one thing this sweep exists not to be: its own header says it makes the orphan population "visible and deliberate rather than silent". The Windows population is real. `relayEndpointForHost` hashes the version directory into the pipe name, so an app update strands the incumbent exactly as it does on POSIX, and with `--grace-time 0` that relay keeps its PTYs and agents forever. Measured on a Windows 11 host (awin): the NPFS root lists 262 named pipes from an unprivileged shell, and the count of `orca-relay-*` names goes 0 -> 1 the moment a relay binds, so the endpoints are enumerable; the repo already enumerates them for GC via `relayLivenessProbeCommand`'s `.windows-active-pipe-*` marker scan. Reclaiming them is not this change. `probeRelayEndpointIncumbent` answers `unverifiable` for every Windows path, so nothing here could be classified, let alone reaped, and nothing about the kill path moves. What changes is that an abandoned pass now leaves a trace. --- .../ssh-relay-superseded-endpoints.test.ts | 22 ++++++++++++++++++- .../ssh/ssh-relay-superseded-endpoints.ts | 20 ++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/main/ssh/ssh-relay-superseded-endpoints.test.ts b/src/main/ssh/ssh-relay-superseded-endpoints.test.ts index 9168f4688bd..a18c610b55f 100644 --- a/src/main/ssh/ssh-relay-superseded-endpoints.test.ts +++ b/src/main/ssh/ssh-relay-superseded-endpoints.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest' const execCommand = vi.fn() vi.mock('./ssh-relay-deploy-helpers', () => ({ @@ -43,6 +43,13 @@ function issuedCommands(): string[] { return execCommand.mock.calls.map((call) => String(call[1])) } +/** The `beforeEach` spy is reinstalled, not reset, so its calls survive the previous test. */ +function warnSpy(): MockInstance { + const spy = vi.spyOn(console, 'warn') + spy.mockClear() + return spy +} + beforeEach(() => { execCommand.mockReset() vi.spyOn(console, 'warn').mockImplementation(() => {}) @@ -157,8 +164,21 @@ describe('sweepSupersededRelayEndpoints', () => { await expect(sweepSupersededRelayEndpoints(CONN, HOST, SWEEP)).resolves.toEqual([]) }) + it('records the abandoned pass when the listing fails, so it reads apart from an empty host', async () => { + const warn = warnSpy() + execCommand.mockRejectedValueOnce(new Error('exec failed')) + await sweepSupersededRelayEndpoints(CONN, HOST, SWEEP) + expect(warn.mock.calls.flat().join('\n')).toContain('no pass ran: exec failed') + }) + it('does not run against Windows hosts, whose endpoints are named pipes', async () => { + const warn = warnSpy() await expect(sweepSupersededRelayEndpoints(CONN, WINDOWS_HOST, SWEEP)).resolves.toEqual([]) expect(execCommand).not.toHaveBeenCalled() + // The skip has to leave a trace: a Windows orphan is never listed and never reclaimed, and + // an empty return is otherwise indistinguishable from a host that had nothing to sweep. + const logged = warn.mock.calls.flat().join('\n') + expect(logged).toContain('Superseded relay sweep did not run') + expect(logged).toContain(CURRENT_DIR) }) }) diff --git a/src/main/ssh/ssh-relay-superseded-endpoints.ts b/src/main/ssh/ssh-relay-superseded-endpoints.ts index 4b1ad5637ef..415a74278eb 100644 --- a/src/main/ssh/ssh-relay-superseded-endpoints.ts +++ b/src/main/ssh/ssh-relay-superseded-endpoints.ts @@ -118,6 +118,17 @@ export async function sweepSupersededRelayEndpoints( options: SupersededRelaySweepOptions ): Promise { if (isWindowsRemoteHost(hostPlatform)) { + // No pass runs here: a Windows endpoint is a named pipe with no inode to stat, so the + // `$HOME` glob cannot see it, and `probeRelayEndpointIncumbent` answers `unverifiable` for + // every Windows path anyway — nothing on this host could be classified, let alone reaped. + // The population is real all the same (`relayEndpointForHost` hashes the version dir into + // the pipe name, so an update strands the incumbent exactly as it does on POSIX), and with + // `--grace-time 0` it keeps its PTYs forever. Returning silently was the whole bug: this + // sweep exists to make that population visible, and on Windows it made it invisible. + console.warn( + `[ssh-relay] Superseded relay sweep did not run (Windows named-pipe endpoints are not enumerated); ` + + `orphans from earlier builds are neither listed nor reclaimed: current=${options.currentRelayDir}` + ) return [] } let listing: string @@ -126,7 +137,14 @@ export async function sweepSupersededRelayEndpoints( wrapCommand: true, signal: options.signal }) - } catch { + } catch (err) { + // Same reason the Windows arm logs: an abandoned pass and an empty host are the same return + // value, and only the log tells them apart. + console.warn( + `[ssh-relay] Superseded relay listing failed; no pass ran: ${ + err instanceof Error ? err.message : String(err) + }` + ) return [] } const sockPaths = listing