| β¦ | β¦ | @@ -643,6 +645,9 @@ export const make = Effect.gen(function* () { | |
| 645 | 647 | ); | |
| 648 | + | // The updater quit skips the scope finalizers that normally reap SSH | |
| 649 | + | // forwards, so they would outlive this process. | |
| 650 | + | yield* sshEnvironment.closeLocalForwards; | |
π High updates/DesktopUpdates.ts:650
quitAndInstall can leave an SSH ssh -L forward orphaned, causing the relaunched app to fail when it reuses that local port. closeLocalForwards only closes the forwards present when it snapshots the map, so an IPC-driven ensureEnvironment that runs concurrently can register a new forward after the snapshot; because updater shutdown skips the manager finalizer, that entry is never killed. Block new SSH environment ensures once installation starts or make closeLocalForwards reject concurrent ensures.
π€ Copy this AI Prompt to have your agent fix this:
| 1 | In file @apps/desktop/src/updates/DesktopUpdates.ts around line 650: |
| 2 | |
| 3 | `quitAndInstall` can leave an SSH `ssh -L` forward orphaned, causing the relaunched app to fail when it reuses that local port. `closeLocalForwards` only closes the forwards present when it snapshots the map, so an IPC-driven `ensureEnvironment` that runs concurrently can register a new forward after the snapshot; because updater shutdown skips the manager finalizer, that entry is never killed. Block new SSH environment ensures once installation starts or make `closeLocalForwards` reject concurrent ensures. |
Fixed in 9025a799e1. Cleanup closes admission before collecting registered targets and active target-lock keys, then drains those same per-target locks. An admitted creator settles before cleanup closes its forward; queued and late ensures reject inside the lock. The updater awaits the drain before installer handoff. The controlled in-flight readiness regression passes, as do all 87 focused/associated cases. The headless service/OpenSSH proof also verifies the old local listener is closed at handoff while remote HTTP remains alive. Native packaged update/relaunch approval is still outstanding.
Verified: admission closes before target collection, in-flight and later ensures are rejected under the target lock, and the updater awaits the drain before handoff. This resolves the race.
| β¦ | β¦ | @@ -643,6 +645,9 @@ export const make = Effect.gen(function* () { | |
| 645 | 647 | ); | |
| 648 | + | // The updater quit skips the scope finalizers that normally reap SSH | |
| 649 | + | // forwards, so they would outlive this process. | |
| 650 | + | yield* sshEnvironment.closeLocalForwards; | |
π High updates/DesktopUpdates.ts:650
When quitAndInstall fails or emits an error, recovery kills every active SSH forward, so existing SSH-backed renderer sessions retain loopback URLs whose listeners no longer exist. recoverFailedInstall only restarts backend instances and never reruns ensureEnvironment; preserve or restore the forwards during recovery, or defer closeLocalForwards until the updater has committed to exit.
No longer relevant as of 9025a799e1c723e762d328da1ca5c1fc00497cd3
Addressed in 9025a799e1. Failure recovery reopens SSH admission. The existing renderer supervisor reconnect path calls driver.connect β resolver.prepare β ensureSshEnvironment and adopts its returned HTTP/WebSocket URLs, so recovery does not pin the old loopback port. The headless service/OpenSSH proof exercises the actual updater/desktop SSH services and real OpenSSH transport: typed/thrown/native errors and interruption reconnect successfully, including when the former port is occupied. Manager-owned remote finalizers also survive cleanup, so a later normal quit stops the remote once even without reconnect. Native installer adapters are stubbed in this proof; packaged platform verification and human lifecycle approval remain required.