Skip to content

fix: render on a live context so the encoders are not killed at Start - #791

Open
blackswanalpha wants to merge 1 commit into
charmbracelet:mainfrom
blackswanalpha:fix/render-on-live-context
Open

blackswanalpha wants to merge 1 commit into
charmbracelet:mainfrom
blackswanalpha:fix/render-on-live-context

Conversation

@blackswanalpha

Copy link
Copy Markdown

Fixes #787.

What happens

v0.12.0 runs a tape to completion, prints Creating out.gif..., prints the publish hint, exits 0, and writes no file. The gap between the two printed lines is a few hundred milliseconds, which is the tell: the encode never runs.

Why

Evaluate cancels the recording context in teardown, then hands that same context to Render:

ctx, cancel := context.WithCancel(ctx)   // shadows the parent
ch := v.Record(ctx)

teardown := func() {
    cancel()    // stops the frame recorder, correctly
    <-ch
}
...
teardown()
if err := v.Render(ctx); err != nil {    // the context is already done

makeMedia builds each encoder with exec.CommandContext, and a command started on a context that is already done is killed at Start before it writes anything. Every ffmpeg invocation dies immediately.

The failure is silent because Render logs the output and drops the error:

out, err := cmd.CombinedOutput()
if err != nil {
    log.Println(string(out))   // logged, never returned
}
return nil

CombinedOutput on a killed command returns empty, so the log prints a blank line and Evaluate sees nil. That is the whole visible symptom: two blank lines, exit 0.

Why v0.11.0 is unaffected

The context was threaded through this path in v0.12.0. In v0.11.0 the signatures are func (vhs *VHS) Render() error and func makeMedia(opts VideoOptions, targetFile string) *exec.Cmd — the encoders used plain exec.Command and nothing could cancel them. The cancellation in teardown is not new; carrying it into the encoders is.

The fix

Two changes, both small:

  1. evaluator.go — the recorder gets its own context. teardown still cancels it to stop frame capture, and Render runs on the parent, which is still live.
  2. vhs.go — Render returns the encode error instead of logging and returning nil, so a failure surfaces as a non-zero exit rather than a silent empty run.

The second change is not strictly needed to make rendering work, but it is why this took so long to pin down from the outside, and it keeps the next encode failure from looking like success.

Verification

  • TestRenderReportsEncodeFailure is added. It fails on main (Render returns nil) and passes with the fix.
  • Reproduced the original bug on a three-line tape: Output min.gif + Type "echo hello" produced no file on main, and produces a valid 600x200, 63-frame GIF with the fix.
  • Ran a real 34-second, four-command tape end to end: 1452x554 GIF, 604 KB.
  • gofmt, go vet and go test -short ./... are clean.

Nothing in the suite currently renders a tape and asserts the file exists — browser_e2e_test.go covers startBrowser only and is opt-in behind VHS_TEST_BROWSER=1 — which is why this reached a release. The added test locks the error path; catching the context bug itself would need an end-to-end render in CI, which is a larger change than this PR should carry.

Environment

os      Ubuntu 24.04.5 LTS, x86_64
go      1.26.7
ttyd    1.7.7-40e79c7
ffmpeg  6.1.1-3ubuntu5
chrome  Google Chrome 152.0.7977.64

Evaluate cancelled the recording context in teardown and then handed that
same context to Render. Since the encoders are built with
exec.CommandContext, every ffmpeg invocation was killed at Start and wrote
nothing, while Render logged the empty output and returned nil. VHS printed
"Creating out.gif...", exited 0, and produced no file.

Give the recorder its own context and render on the parent, and return the
encode error instead of swallowing it.

Fixes charmbracelet#787
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.

v0.12.0 exits 0 and writes no output file on ubuntu-24.04 runners; v0.11.0 records the same tape

1 participant