Skip to content

fix(dispatch): stop PTY teardown from killing one-click CLI installs - #2153

Merged
bobleer merged 1 commit into
GCWing:mainfrom
bobleer:bob/one-click-deploy-failure-a34586
Aug 7, 2026
Merged

fix(dispatch): stop PTY teardown from killing one-click CLI installs#2153
bobleer merged 1 commit into
GCWing:mainfrom
bobleer:bob/one-click-deploy-failure-a34586

Conversation

@bobleer

@bobleer bobleer commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

One-click deploy to an SSH target failed with "未能完整部署 BitFun" even though the release had already been downloaded and verified on the target.

The installer launched its driver over a PTY exec channel. The driver only spawns a nohup body and exits — about a millisecond later — and sshd tears the PTY down the moment it does. That teardown races the body it just spawned: a body still inside bash's startup has not reached its own trap finish EXIT yet, so losing the race kills it silently.

Nothing survives to explain it:

  • the body writes no log and no .exit file,
  • the driver's cleanup trap removes the .preparing marker,
  • the next poll reaps the now-stale .pid via its liveness check.

install_cli_poll reads an all-empty state, maps it to Failed, and the dialog treats the very first poll as fatal.

Fix: launch over a plain exec channel. Without a controlling terminal there is no hangup to race, and the installer needs no TTY semantics anyway — it never uses sudo.

Type and Areas

Type: bug fix (+ two diagnostics fixes that kept it invisible)

Areas: Rust core (remote_ssh/dispatch), web UI

Motivation / Impact

The failure is probabilistic, which is why the same target sometimes installs fine and sometimes does not. When it loses the race the user gets a generic, unactionable error and a target left with a verified archive and no CLI.

Two diagnostics gaps are fixed alongside it, because together they made the root cause unrecoverable from the logs:

  • logger.ts — WebKit (what Tauri embeds on macOS) builds Error.stack from frames only, with no message line. The logger preferred stack over the message, so the warning reached app.log as Failed to prepare SSH dispatch target {...}, @tauri://localhost/assets/ChatPane-*.js:96:14836 — a location with no reason. This affects every call site that logs an Error.
  • DispatchInstallDialog.tsx — the install-poll failure threw a fixed string and discarded poll.output, so even a populated remote installer log never reached the error.

No user-facing string changes.

Verification

Diagnosed and verified against a real Ubuntu 24.04 aarch64 target over SSH, using a standalone russh 0.45 program that replicates stage_and_launch_installer exactly (PTY, background drain, first poll at +52 ms — the timing taken from the failing app.log).

Detachment matrix, launching a nohup'd background script and letting the channel close:

launch result
PTY + nohup + & killed before writing a single line
PTY + setsid + nohup + & killed before writing a single line
non-PTY + nohup + & survives, runs to completion

Measured: the driver runs start→exit in ~1 ms, and a hangup reaches the remote process in ~3 ms — comfortably inside the window before the body installs its traps.

End-to-end with the real staged installer over a non-PTY channel, polling on the app's schedule:

poll 0: running=1 preparing=0 size=75  marker=0 exit_recorded=0
poll 1: running=0 preparing=0 size=169 marker=1 exit_recorded=1 exit_code=0

Target ends on bitfun 0.2.16, install directory clean, archive consumed as designed.

Automated checks:

cargo test -p bitfun-services-integrations --features remote-ssh-concrete   # 126 passed, 1 ignored
cargo clippy -p bitfun-services-integrations --features remote-ssh-concrete # no new warnings
pnpm --filter web-ui exec vitest run src/shared/utils/logger.test.ts src/features/dispatch/DispatchInstallDialog.test.tsx  # 18 passed
pnpm --filter web-ui exec tsc --noEmit                                      # clean
pnpm --filter web-ui exec eslint <changed files>                            # clean

Two regression tests added for the logger's error formatting (stack-without-message, and no duplication when the stack already carries it).

Reviewer Notes

  • The account-sync / daemon-provisioning half of one-click deploy (provision_account_daemon) runs through execute_command_with_options and has no PTY, so it never had this hazard.
  • open_exec_channel passes tty: false to workspace_command, which only matters for Docker-container targets — and ensure_plain_ssh_target already rejects those before this point, so the change is a no-op for every path that reaches it.
  • The remote installer scripts themselves are unchanged and were confirmed correct: launched with a channel that stays open, they install successfully.
  • AI-assisted; fully tested — root cause reproduced and the fix verified end-to-end against a live SSH target.

The SSH CLI installer launched its driver over a PTY exec channel. The
driver only spawns a nohup body and exits, which it does about a
millisecond later, and sshd tears the PTY down the moment it does. That
teardown races the body: a body still inside bash's startup has not
reached its own exit trap yet, so losing the race kills it silently.

Nothing survives to explain it. The body writes no log and no exit file,
the driver's cleanup trap removes the `.preparing` marker, and the next
poll reaps the now-stale `.pid`. The controller reads an empty state,
maps it to Failed on its very first poll, and the user gets "could not
fully deploy BitFun" for an install that had already downloaded and
verified the release.

Launch over a plain exec channel instead. Without a controlling terminal
there is no hangup to race, and the installer needs no TTY semantics
anyway since it never uses sudo.

Two diagnostics gaps kept this invisible and are fixed alongside it:

- WebKit, which Tauri embeds on macOS, builds `Error.stack` from frames
  only. The logger preferred `stack` over the message, so the warning
  reached the log file as a bare source location with no reason.
- The install poll's failure discarded the installer's own output, so
  even a populated remote log never reached the error.

Verified against a real Ubuntu aarch64 target. Launching a detached
process from a PTY channel is killed before it writes a line; over a
plain channel it survives and the install completes (running=1 on the
first poll, then marker=1 exit_code=0).
@bobleer
bobleer merged commit 9dff7e0 into GCWing:main Aug 7, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant