mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(desktop): the status icon ends up on the fold the tracker actually holds
Two panes changing at once take their folds under the lock and report outside it, so the two reports could reach the icon in the opposite order. The one that lost left the icon describing a state the tracker had already moved past, and nothing corrected it: the fold reports only what changed, so a wrong paint latched until the next real change. A pane running a fifteen-minute turn is fifteen minutes of a tray that says nothing is running. Painting is now serialised and describes the fold as it stands when it paints, so a report that lost the race repaints the truth instead of overwriting it. The test fails on the old code with the reported symptom exactly: the icon shows Working:0 while the tracker holds Working:1. Reported in #9448.
This commit is contained in:
1 parent
4d320f7cd7
commit
db030ad097
2 files changed
+64
-9
No files matched your search
@@ -48,13 +48,16 @@ func (s State) Mood() Mood {
|
||||
func (s State) Busy() bool { return s.Working > 0 || s.Attention > 0 || s.Jobs > 0 }
|
||||
|
||||
// Tracker folds what it is shown. Panes emit from their own goroutines, so it
|
||||
// is safe for concurrent use, and it never calls back while holding its lock.
|
||||
// is safe for concurrent use, and it never calls back while holding the fold's
|
||||
// lock — a callback is free to ask it anything except to publish.
|
||||
type Tracker struct {
|
||||
mu sync.Mutex
|
||||
panes map[string]*pane
|
||||
jobs int
|
||||
last State
|
||||
changed func(State)
|
||||
// paint serialises reports. A callback must not publish from inside itself.
|
||||
paint sync.Mutex
|
||||
}
|
||||
|
||||
type pane struct {
|
||||
@@ -79,11 +82,8 @@ func (t *Tracker) SetOnChange(changed func(State)) {
|
||||
}
|
||||
t.mu.Lock()
|
||||
t.changed = changed
|
||||
state := t.stateLocked()
|
||||
t.mu.Unlock()
|
||||
if changed != nil {
|
||||
changed(state)
|
||||
}
|
||||
t.report(changed)
|
||||
}
|
||||
|
||||
// Watch returns inner wrapped so this pane's events reach the fold on the way
|
||||
@@ -151,8 +151,8 @@ func (t *Tracker) stateLocked() State {
|
||||
return out
|
||||
}
|
||||
|
||||
// publish reports a change once, outside the lock: a callback that paints an
|
||||
// icon has no business running inside the fold every pane emits into.
|
||||
// publish reports a change once, outside the fold's lock: a callback that
|
||||
// paints an icon has no business running inside the fold every pane emits into.
|
||||
func (t *Tracker) publish() {
|
||||
t.mu.Lock()
|
||||
next := t.stateLocked()
|
||||
@@ -160,9 +160,27 @@ func (t *Tracker) publish() {
|
||||
t.last = next
|
||||
report := t.changed
|
||||
t.mu.Unlock()
|
||||
if changed && report != nil {
|
||||
report(next)
|
||||
if !changed {
|
||||
return
|
||||
}
|
||||
t.report(report)
|
||||
}
|
||||
|
||||
// report paints, serialised and always describing the fold as it stands now.
|
||||
// Two panes changing at once take their folds under the lock and report outside
|
||||
// it, so the reports can arrive in the opposite order; the one that lost would
|
||||
// leave the icon on a state the tracker has already moved past, and nothing
|
||||
// would correct it, because the fold reports only what changed.
|
||||
func (t *Tracker) report(changed func(State)) {
|
||||
if changed == nil {
|
||||
return
|
||||
}
|
||||
t.paint.Lock()
|
||||
defer t.paint.Unlock()
|
||||
t.mu.Lock()
|
||||
now := t.stateLocked()
|
||||
t.mu.Unlock()
|
||||
changed(now)
|
||||
}
|
||||
|
||||
type paneSink struct {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package traystate
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
@@ -125,3 +126,39 @@ func TestConcurrentPanesFoldSafely(t *testing.T) {
|
||||
t.Fatalf("state = %+v, want eight quiet panes", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The icon has to end up describing the fold the tracker holds. Folds are taken
|
||||
// under the lock and painted outside it, so two panes changing at once can
|
||||
// reach the callback in the opposite order, and the loser strands the icon on a
|
||||
// state already moved past — with nothing to correct it, since the fold reports
|
||||
// only what changed.
|
||||
func TestTheLastPaintDescribesTheCurrentFold(t *testing.T) {
|
||||
for attempt := range 200 {
|
||||
var mu sync.Mutex
|
||||
var last State
|
||||
tracker := New(func(s State) {
|
||||
// The real callback paints through a platform call: not fast, and
|
||||
// that is what opens the window between fold and paint.
|
||||
runtime.Gosched()
|
||||
mu.Lock()
|
||||
last = s
|
||||
mu.Unlock()
|
||||
})
|
||||
a := tracker.Watch("a", event.Discard)
|
||||
b := tracker.Watch("b", event.Discard)
|
||||
emit(a, event.TurnStarted)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() { defer wg.Done(); emit(a, event.TurnDone) }()
|
||||
go func() { defer wg.Done(); emit(b, event.TurnStarted) }()
|
||||
wg.Wait()
|
||||
|
||||
mu.Lock()
|
||||
painted := last
|
||||
mu.Unlock()
|
||||
if got := tracker.State(); painted != got {
|
||||
t.Fatalf("attempt %d: icon shows %+v, tracker holds %+v", attempt, painted, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user