Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions libs/cmdio/io.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,14 @@ func (c *cmdIO) acquireTeaProgram(p *tea.Program) {
defer c.teaMu.Unlock()

// Wait for existing program to finish
if c.teaDone != nil {
<-c.teaDone
// Receive with teaMu released: releaseTeaProgram locks teaMu to close
// teaDone, so waiting while holding it would deadlock. Loop because another
// acquirer may register a new program before the lock is reacquired.
for c.teaDone != nil {
done := c.teaDone
c.teaMu.Unlock()
<-done
c.teaMu.Lock()
}

// Register new program
Expand Down
36 changes: 36 additions & 0 deletions libs/cmdio/io_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package cmdio

import (
"testing"
"time"
)

func TestAcquireTeaProgramWaitsForRelease(t *testing.T) {
c := &cmdIO{}
// acquireTeaProgram only stores the pointer, so a nil program suffices.
c.acquireTeaProgram(nil)

acquired := make(chan struct{})
go func() {
c.acquireTeaProgram(nil)
close(acquired)
}()

select {
case <-acquired:
t.Fatal("second acquireTeaProgram returned while the first program was still active")
case <-time.After(50 * time.Millisecond):
}

// Release on a goroutine so a regressed deadlock fails the timeout below
// instead of hanging the test.
go c.releaseTeaProgram()

select {
case <-acquired:
case <-time.After(5 * time.Second):
t.Fatal("second acquireTeaProgram did not return after the first program was released")
}

c.releaseTeaProgram()
}
Loading