Skip to content

Scope signal handling to doppler run - #564

Open
watsonian wants to merge 1 commit into
masterfrom
watsonian/fix-signal-handling
Open

watsonian wants to merge 1 commit into
masterfrom
watsonian/fix-signal-handling

Conversation

@watsonian

Copy link
Copy Markdown
Member

A user reported that they couldn't cancel doppler login with Ctrl-C while it was waiting for browser authorization. The command only ended when the five-minute timeout fired. Both times it happened, the CLI had just offered an update and the user had accepted it in the same invocation.

The cause was utils.RunCommand. Every time it started a subprocess, it registered a handler for all signals and never removed it. The update check runs before every command, and accepting the update runs the install script through RunCommand. After that, every Ctrl-C was caught and forwarded to an installer that had already exited, so the rest of the command, here doppler login, couldn't be interrupted. The winget checks on Windows also go through RunCommand, and they run on every command when a check is due (at most once every 24 hours) whether or not an update is offered. So Windows users installed via winget could hit this without accepting anything.

This PR removes signal handling from RunCommand and RunCommandString. The install script is the one helper that still needs it: if the user presses Ctrl-C mid-install, we want to report the failure and delete the temporary script rather than die and leave a file in their home directory. A new utils.ForwardSignals forwards SIGINT and SIGTERM to the script only while it runs.

doppler run was the only command that relied on the old behavior. It now owns its signal handling through runSignalHandler in pkg/cmd/run_signals.go. The handler starts listening just before the first child starts, so Ctrl-C while secrets are still loading keeps its default behavior. From then on, it forwards signals to whichever child is currently running, which matters after a --watch restart. --forward-signals and its TTY-dependent default behave exactly as before.

The one deliberate behavior change is for a termination signal that arrives while --watch is restarting the child. Previously that signal was swallowed, and doppler went on to start the new child anyway. Now doppler exits as soon as no child is running, without starting another one. It removes the secrets mount, then dies by the same signal it received, so the shell sees the usual 128 plus the signal number. SIGQUIT is the exception and exits with 131: re-raising it would make the Go runtime dump every goroutine.

Most of the diff is tests. pkg/cmd/run_signals_test.go builds the CLI, points doppler run at an httptest mock of the secrets download and watch endpoints, and sends real signals. A signal goes either to doppler alone or to its whole process group, which is what a terminal does on Ctrl-C. The child is a small shell script that logs each signal it receives. The suite pins today's behavior: forwarding with the flag on, off, and at its default under both a TTY and a pipe; both --command and argument forms; exit codes; mount cleanup; repeated Ctrl-C; and the window while secrets are loading. All of these tests pass against a binary built from master. The restart cases fail on master and pass here, as does TestSignalsNotInterceptedAfterWaitCommand in pkg/utils, which reproduces the original login bug. Setting DOPPLER_TEST_BINARY runs the suite against any prebuilt binary, which is how the comparison with master was done. The TTY cases use a Linux pseudo-terminal and skip elsewhere, and the whole suite is excluded on Windows.


Stack created with GitHub Stacks CLI • Give Feedback 💬

@watsonian
watsonian requested a review from a team as a code owner October 13, 2026 15:33
@watsonian
watsonian added this pull request to stack #566 October 13, 2026 15:34
`utils.RunCommand` registered a handler for every signal and never removed
it. When an update was installed from the version-check prompt, every
later Ctrl-C in the same process was swallowed, so `doppler login` could
only end at its 5 minute timeout. The winget checks did the same on Windows.

Helper commands no longer catch signals. The install script forwards
SIGINT/SIGTERM only while it runs, so an interrupted update still reports
the failure and removes its temp file.

`doppler run` now owns signal handling for its lifetime and forwards to
the current child. A termination signal during a --watch restart cancels
the restart: doppler exits by that signal once no child is running
instead of starting a new one. `--forward-signals` behaves as before.

Adds end-to-end signal tests that run the CLI against a mock API.
@watsonian
watsonian force-pushed the watsonian/fix-signal-handling branch from e5b84c0 to 2025ae0 Compare October 13, 2026 16:37

This branch has not been deployed

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant