Skip to content

Commit b25e620

Browse files
committed
fix(pin): resolve race conditions in spinner state management
1 parent 709e616 commit b25e620

File tree

1 file changed

+23
-13
lines changed

1 file changed

+23
-13
lines changed

pin.go

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import (
6464
"io"
6565
"os"
6666
"sync"
67+
"sync/atomic"
6768
"time"
6869
)
6970

@@ -245,7 +246,7 @@ type Pin struct {
245246
message string
246247
messageMu sync.RWMutex
247248
stopChan chan struct{}
248-
isRunning bool
249+
isRunning int32
249250
spinnerColor Color
250251
textColor Color
251252
doneSymbol rune
@@ -298,25 +299,25 @@ func New(message string, opts ...Option) *Pin {
298299
// Note: Canceling the returned function stops the spinner without printing
299300
// a final message. To print a final message, use the Stop() method.
300301
func (p *Pin) Start(ctx context.Context) context.CancelFunc {
301-
if p.isRunning {
302+
if p.IsRunning() {
302303
return func() {}
303304
}
304305

305306
if !isTerminal(p.out) {
306307
ctx, cancel := context.WithCancel(ctx)
307-
p.isRunning = true
308+
p.setRunning(true)
308309
p.messageMu.RLock()
309310
msg := p.message
310311
p.messageMu.RUnlock()
311312
_, _ = fmt.Fprintln(p.out, msg)
312313
go func() {
313314
<-ctx.Done()
314-
p.isRunning = false
315+
p.setRunning(false)
315316
}()
316317
return cancel
317318
}
318319

319-
p.isRunning = true
320+
p.setRunning(true)
320321

321322
ctx, cancel := context.WithCancel(ctx)
322323
ticker := time.NewTicker(100 * time.Millisecond)
@@ -329,7 +330,7 @@ func (p *Pin) Start(ctx context.Context) context.CancelFunc {
329330
case <-p.stopChan:
330331
return
331332
case <-ctx.Done():
332-
p.isRunning = false
333+
p.setRunning(false)
333334
_, _ = fmt.Fprint(p.out, "\r\033[K")
334335
return
335336
case <-ticker.C:
@@ -369,15 +370,15 @@ func (p *Pin) Start(ctx context.Context) context.CancelFunc {
369370

370371
// Stop halts the spinner animation and optionally displays a final message.
371372
func (p *Pin) Stop(message ...string) {
372-
if !p.isRunning {
373+
if !p.IsRunning() {
373374
return
374375
}
375376

376377
if p.handleNonTerminal(message...) {
377378
return
378379
}
379380

380-
p.isRunning = false
381+
p.setRunning(false)
381382
p.stopChan <- struct{}{}
382383
p.wg.Wait()
383384

@@ -391,15 +392,15 @@ func (p *Pin) Stop(message ...string) {
391392
// Fail halts the spinner animation and displays a failure message.
392393
// This method is similar to Stop but uses a distinct symbol and color scheme to indicate an error state.
393394
func (p *Pin) Fail(message ...string) {
394-
if !p.isRunning {
395+
if !p.IsRunning() {
395396
return
396397
}
397398

398399
if p.handleNonTerminal(message...) {
399400
return
400401
}
401402

402-
p.isRunning = false
403+
p.setRunning(false)
403404
p.stopChan <- struct{}{}
404405
p.wg.Wait()
405406

@@ -412,7 +413,7 @@ func (p *Pin) Fail(message ...string) {
412413

413414
// UpdateMessage changes the message shown next to the spinner.
414415
func (p *Pin) UpdateMessage(message string) {
415-
if !p.isRunning {
416+
if !p.IsRunning() {
416417
return
417418
}
418419

@@ -508,7 +509,7 @@ func (p *Pin) handleNonTerminal(message ...string) bool {
508509
if len(message) > 0 {
509510
_, _ = fmt.Fprintln(p.out, message[0])
510511
}
511-
p.isRunning = false
512+
p.setRunning(false)
512513
return true
513514
}
514515
return false
@@ -521,5 +522,14 @@ func (p *Pin) Message() string {
521522

522523
// IsRunning returns whether the spinner is active.
523524
func (p *Pin) IsRunning() bool {
524-
return p.isRunning
525+
return atomic.LoadInt32(&p.isRunning) == 1
526+
}
527+
528+
// setRunning sets the running state of the spinner.
529+
func (p *Pin) setRunning(running bool) {
530+
var val int32
531+
if running {
532+
val = 1
533+
}
534+
atomic.StoreInt32(&p.isRunning, val)
525535
}

0 commit comments

Comments
 (0)