Skip to content

[Bug]: Desktop picks the next free port before checking home ownership, so an SSH client's managed server turns startup into a permanent lockout with unactionable advice #34266

Closed

Description

@ksajan

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/desktop

Steps to reproduce

Host A is a macOS desktop with the default home ~/.t3. Host B is another machine with a T3 Code SSH environment pointing at A.

  1. On A, quit T3 Code desktop. Nothing owns ~/.t3/userdata any more.
  2. On B, its SSH environment launches its managed server through ~/.t3/ssh-launch/<key>/run-t3.sh. With no live runtime record, the script picks a free port and runs t3 serve --host 127.0.0.1 --port 3773 --base-dir ~/.t3, which acquires ~/.t3/userdata/server-owner.sqlite and serves on 3773.
  3. On A, open T3 Code desktop with the default Local environment.
  4. Startup scans for a port. 3773 is in use, so it selects 3774 (startPort: 3773, port: 3774) and spawns its backend child there.
  5. The child exits 78 with ServerAlreadyRunningError. The failure is the ownership lock, not the port. Exit 78 routes to onStateDirOwned, the "This T3 home is unavailable" box is shown, and the app quits.
  6. Follow the dialog. t3 status --base-dir ~/.t3 reports Service: not installed, so there is nothing for t3 update or t3 service uninstall to act on. The real owner is the SSH-launched server, which the dialog never mentions.
  7. Try stopping the owner by hand. B's supervisor relaunched it in 2 s, and again in 9 s, after SIGTERM to the server each time, so there is no window in which to open the app.

The only escape found was quitting T3 Code on B (or disabling that SSH environment), then opening the desktop.

Expected behavior

The desktop should decide whether it may start from home ownership, not from port availability. Before spawning it can read ~/.t3/userdata/server-runtime.json (pid, port, version) and the server-owner.sqlite lock, which packages/ssh/src/tunnel.ts already does on the SSH side, and then:

  • if a live owner already serves this home, adopt it (or ask the user) rather than starting a second server, and
  • if it must refuse, name the actual owner and how it was started (managed server launched over SSH from <peer>, pid and version), instead of advising t3 service uninstall for a service that is not installed.

At minimum it should not spend a doomed spawn on a home it cannot possibly own.

Actual behavior

resolveDesktopBackendPort treats "the default port is busy" as "use the next free port". That check says nothing about whether the home is owned, so the desktop spawns a second server for the same ~/.t3/userdata on 3774. That server correctly refuses to start (ServerAlreadyRunningError, exit 78), the app quits, and the dialog blames a background service that does not exist.

The asymmetry is the clearest signal that this is a gap rather than a design choice: in the reverse direction the SSH launcher handles it correctly. Once the desktop owns the home, ~/.t3/ssh-launch/<key>/managed flips from managed to external and the remote client adopts port 3773 instead of launching a rival server. The desktop has no equivalent adoption or ownership preflight anywhere: grep -rn "serverRuntimeState\|server-runtime.json\|server-owner" apps/desktop/src returns no matches.

Impact

Blocks work completely. The app quits with no in-app recovery path, and the advertised remedy is a no-op in this configuration.

Version or commit

  • Desktop: 0.1.9-nightly.20261111.4501 (auto-updated to 0.1.9-nightly.20261111.4515 during triage; the failure reproduced on 4501)
  • SSH-launched server, pinned by the remote client: 0.1.7-nightly.20261104.4151
  • t3 CLI: v0.1.9-nightly.20261111.4501

Environment

  • Host A: macOS 27.2.1 (26B131), arm64, desktop app, Local environment enabled, no background service installed, home ~/.t3 (400 MB statev2.sqlite).
  • Host B: T3 Code desktop with an SSH environment to A, over the LAN (A 192.168.50.71, B 192.168.50.115).
  • ~/.t3/ssh-launch/c0a8a54905bb042c/ existed before the incident (created weeks earlier) and pinned progressively older servers over time: 0.1.3-nightly.20261027.3748, 0.1.5-nightly.20261029.3877, 0.1.7-nightly.20261104.4151.

Logs or stack traces

~/.t3/userdata/logs/server-child.log (two consecutive startup attempts):

{"annotations":{"component":"desktop-backend-child","runId":"efc82f9c56be","instanceId":"primary","phase":"START","details":"pid=86944 port=3774 cwd=/Users/skumar"}}
ERROR (#2): ServerAlreadyRunningError: A T3 Code server already owns /Users/skumar/.t3/userdata. Stop that server, or use a separate T3 home and pair with it, then retry. No server was stopped. server-owner.sqlite is the ownership lock; do not delete it while a server is running.
{"annotations":{"component":"desktop-backend-child","runId":"efc82f9c56be","phase":"END","details":"pid=86944 code=78"}}
{"annotations":{"component":"desktop-backend-child","runId":"dd7b847d4e89","phase":"START","details":"pid=87403 port=3774 cwd=/Users/skumar"}}

~/.t3/userdata/logs/desktop.trace.ndjson, the port decision that makes it Doomed:

{"attributes":{"scope":"desktop","runId":"efc82f9c56be","component":"desktop-bootstrap","port":3774,"startPort":3773,"effect.logLevel":"INFO"}}

The owner holding the lock, and that it is not a service:

$ lsof ~/.t3/userdata/server-owner.sqlite
COMMAND   PID   USER   FD   TYPE ... NAME
t3      98563 skumar   13u   REG ... /Users/skumar/.t3/userdata/server-owner.sqlite

$ cat ~/.t3/userdata/server-runtime.json
{"version":1,"pid":98563,"host":"127.0.0.1","port":3773,...,"serverVersion":"0.1.7-nightly.20261104.4151"}

$ t3 status --base-dir ~/.t3
This machine
  Service        not installed
  T3 Connect     off
  Local network  off

SSH-launch state while it held the home, and after the desktop won it:

# during the lockout
pid=93146  port=3773  managed=managed

# after quitting the app on B and opening the desktop: the SSH side adopts it
pid=  port=3773  managed=external

Relaunch timing after kill -TERM on the SSH-launched server (no window to start the app):

t=1s free
t=9s SERVER_UP pid=98563     # first attempt
... (killed again) ...
t=1s free
t=4s SERVER_UP pid=4699      # second attempt

Code pointers

  • apps/desktop/src/app/DesktopApp.ts — resolveDesktopBackendPort scans 3773 to 65535 using net.canListenOnHost only, and the call site logs port/startPort.
  • apps/desktop/src/backend/DesktopBackendManager.ts (~line 834) — const stateDirOwned = exitCode === SERVER_EXIT_CODE_STATE_DIR_OWNED;
  • apps/desktop/src/backend/DesktopBackendPool.ts (~line 180) — the dialog text shown for onStateDirOwned.
  • apps/server/src/serverOwnership.ts — ServerAlreadyRunningError and the lock; the comment on SERVER_EXIT_CODE_STATE_DIR_OWNED says the distinct code exists so supervisors stop restarting, which is why the child dies quietly and the port scan never learns why.
  • packages/ssh/src/tunnel.ts — the launch script already reads server-runtime.json, adopts a live owner, and flips managed to external.

Related issues

Workaround

Quit T3 Code on B (or disable that SSH environment) so B stops relaunching, then open the desktop. Verified on this host: the desktop took the lock on 3773 as 0.1.9-nightly.20261111.4515, and B's launcher then adopted it as external with no further launches.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions