fix: plugin --help output truncation from stdio teardown race#1580
Open
googleknight wants to merge 1 commit into
Open
fix: plugin --help output truncation from stdio teardown race#1580googleknight wants to merge 1 commit into
googleknight wants to merge 1 commit into
Conversation
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reviewers
cc @stripe/developer-products
Summary
Fixes #1563 —
stripe <plugin> --help(e.g.stripe apps --help) sometimes prints only the first line of help text and exits successfully, silently dropping the rest of the Cobra output. Reliably reproducible on a cold machine (CI, fresh clone); rarely seen locally because re-runs benefit from the OS page cache.Root Cause
When the CLI invokes a plugin via HashiCorp's
go-plugin, the plugin runs as a separate process with its stdout forwarded to the CLI over a gRPC stream by a fire-and-forget goroutine insidego-plugin.The bug is in that goroutine's lifecycle.
pkg/plugins/plugin.gopassedos.Stdout/os.Stderrdirectly togo-plugin(SyncStdout/SyncStderr). When the plugin's command finished,pkg/cmd/plugin_cmds.gocalledplugins.CleanupAllClients(), which killed the plugin process and tore down the gRPC connection — but nothing waited for the forwarding goroutine to finish flushing. The CLI'smainreturned, Go terminated the goroutine mid-flush, and any bytes still in flight were dropped.Cold-cache runs are slower at every step (process spawn, gRPC handshake, each stdio chunk), so more bytes remain in flight at teardown — which is why the bug surfaces there and not on warm runs.
Fix
Own the drain locally so it can be awaited explicitly.
Changes to
pkg/plugins/plugin.go→Plugin.Run:io.Pipefor stdout and another for stderr. Pass the pipe writers togo-pluginasSyncStdout/SyncStderrinstead ofos.Stdout/os.Stderr.os.Stdout/os.Stderr.Runexit:defer client.Kill()first (graceful plugin shutdown), then close the pipe writers, thenWaitGroup.Wait()for the drain goroutines to finish.io.Pipeis unbuffered — every byte thego-pluginforwarder writes is consumed by the local drain goroutine immediately. By the timeRunreturns, every byte received from the plugin has been written toos.Stdout/os.Stderr.Managed: truewas also flipped toManaged: falseso the client lifecycle is owned locally (Kill()is now called explicitly). The existingplugins.CleanupAllClients()call inplugin_cmds.gobecomes a safe no-op since the client is no longer registered as managed.Alternatives Considered
CleanupAllClients()Managed: false+ manualKill()inrunPluginCmdKill()doesn't wait for the stdio goroutine eitherbytes.Buffer, flush after command--helpbut breaks real-time output for long-running plugin commandsgo-pluginto join the stdio goroutineTest Plan
go vet ./...go build ./...bin/stripe --helpprints full top-level helpgo test -race ./...golangci-lint run ./pkg/plugins/...Remaining Caveat
There is still a sub-microsecond window between
client.Kill()returning and thego-pluginforwarder goroutine exiting. If the pipe closes while the forwarder is mid-write of a chunk, that chunk is lost.The original bug had a millisecond-scale window that dropped entire help screens. This fix shrinks it by roughly 1000×, which eliminates the user-visible behavior. Fully closing the residual race requires the upstream
go-pluginchange noted above.