refactor(daemon): document lock ownership and Locked suffix
- commit
- 4d933cbd7df333db09041d99fc5865422f35590b
- author
- brice · · 2026-10-08 06:13 UTC
- committer
- brice · · 2026-10-08 08:12 UTC
- parent
- 0bb8a8e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
11 files · +44 -7
M internal/usecase/daemon/bar_refresh.go +1 -0
| @@ -316,6 +316,7 @@ func (d *Daemon) refreshBarScriptsIfDue(sess *session, now time.Time, force bool | |||
| 316 | 316 | return true | |
| 317 | 317 | } | |
| 318 | 318 | ||
| 319 | + | // scheduleBarScriptRefreshLocked requires d.barScripts.mu, not d.mu. | |
| 319 | 320 | func (d *Daemon) scheduleBarScriptRefreshLocked(sess *session, delay time.Duration) { | |
| 320 | 321 | if d.barScripts.pending[sess.id] { | |
| 321 | 322 | return | |
| @@ -316,6 +316,7 @@ func (d *Daemon) refreshBarScriptsIfDue(sess *session, now time.Time, force bool | |||||
| 316 | return true | 316 | return true | ||
| 317 | } | 317 | } | ||
| 318 | 318 | ||||
| 319 | + | // scheduleBarScriptRefreshLocked requires d.barScripts.mu, not d.mu. | |||
| 319 | func (d *Daemon) scheduleBarScriptRefreshLocked(sess *session, delay time.Duration) { | 320 | func (d *Daemon) scheduleBarScriptRefreshLocked(sess *session, delay time.Duration) { | ||
| 320 | if d.barScripts.pending[sess.id] { | 321 | if d.barScripts.pending[sess.id] { | ||
| 321 | return | 322 | return | ||
M internal/usecase/daemon/client.go +19 -0
| @@ -32,6 +32,25 @@ | |||
| 32 | 32 | // | |
| 33 | 33 | // paletteHistory.mu guards the daemon-wide recent-command list. It is a leaf: | |
| 34 | 34 | // it is only held to copy or rewrite that slice, never while calling out. | |
| 35 | + | // | |
| 36 | + | // purgeAllMu serializes whole KillAll purges (purgeAllSessions). It is the | |
| 37 | + | // outermost daemon-level lock on that path: the owner takes d.mu (briefly, to | |
| 38 | + | // open and close the purge admission gate) and later per-session teardown locks | |
| 39 | + | // while holding it. No path acquires purgeAllMu while holding d.mu, | |
| 40 | + | // moveLifecycleMu, a session lock, or a pane lock. It is not an admission gate; | |
| 41 | + | // admission is the d.mu-guarded purgeAdmission* state. | |
| 42 | + | // | |
| 43 | + | // attnMu guards only the attention animation frame counter (animFrame). It is | |
| 44 | + | // a leaf: attentionFrame, advanceAttentionFrame, and setAttentionFrame take it | |
| 45 | + | // for the read or write and call nothing while holding it. | |
| 46 | + | // | |
| 47 | + | // Naming rule for the Locked suffix: on a Daemon method (or a free function | |
| 48 | + | // taking a Daemon-owned structure) a ...Locked suffix means the caller holds | |
| 49 | + | // d.mu. On a method of any other type, it means the caller holds that | |
| 50 | + | // receiver's own mutex (session.mu, tab.mu, pane.mu, and so on). Whenever a | |
| 51 | + | // ...Locked function instead requires a different lock (for example | |
| 52 | + | // overlayRuntime.copyMu, snapshotWorker.mu, moveLifecycleMu, or | |
| 53 | + | // barScriptState.mu), its doc comment names that lock explicitly. | |
| 35 | 54 | package daemon | |
| 36 | 55 | ||
| 37 | 56 | import ( | |
| @@ -32,6 +32,25 @@ | |||||
| 32 | // | 32 | // | ||
| 33 | // paletteHistory.mu guards the daemon-wide recent-command list. It is a leaf: | 33 | // paletteHistory.mu guards the daemon-wide recent-command list. It is a leaf: | ||
| 34 | // it is only held to copy or rewrite that slice, never while calling out. | 34 | // it is only held to copy or rewrite that slice, never while calling out. | ||
| 35 | + | // | |||
| 36 | + | // purgeAllMu serializes whole KillAll purges (purgeAllSessions). It is the | |||
| 37 | + | // outermost daemon-level lock on that path: the owner takes d.mu (briefly, to | |||
| 38 | + | // open and close the purge admission gate) and later per-session teardown locks | |||
| 39 | + | // while holding it. No path acquires purgeAllMu while holding d.mu, | |||
| 40 | + | // moveLifecycleMu, a session lock, or a pane lock. It is not an admission gate; | |||
| 41 | + | // admission is the d.mu-guarded purgeAdmission* state. | |||
| 42 | + | // | |||
| 43 | + | // attnMu guards only the attention animation frame counter (animFrame). It is | |||
| 44 | + | // a leaf: attentionFrame, advanceAttentionFrame, and setAttentionFrame take it | |||
| 45 | + | // for the read or write and call nothing while holding it. | |||
| 46 | + | // | |||
| 47 | + | // Naming rule for the Locked suffix: on a Daemon method (or a free function | |||
| 48 | + | // taking a Daemon-owned structure) a ...Locked suffix means the caller holds | |||
| 49 | + | // d.mu. On a method of any other type, it means the caller holds that | |||
| 50 | + | // receiver's own mutex (session.mu, tab.mu, pane.mu, and so on). Whenever a | |||
| 51 | + | // ...Locked function instead requires a different lock (for example | |||
| 52 | + | // overlayRuntime.copyMu, snapshotWorker.mu, moveLifecycleMu, or | |||
| 53 | + | // barScriptState.mu), its doc comment names that lock explicitly. | |||
| 35 | package daemon | 54 | package daemon | ||
| 36 | 55 | ||||
| 37 | import ( | 56 | import ( | ||
M internal/usecase/daemon/copy_scroll_animation.go +1 -0
| @@ -66,6 +66,7 @@ func (d *Daemon) smoothCopyWheel(sess *session, ac *attachedClient, delta int) { | |||
| 66 | 66 | } | |
| 67 | 67 | } | |
| 68 | 68 | ||
| 69 | + | // advanceCopyScrollLocked requires ac.overlays.copyMu, not d.mu. | |
| 69 | 70 | func (d *Daemon) advanceCopyScrollLocked(sess *session, ac *attachedClient) (bool, bool) { | |
| 70 | 71 | rt := ac.overlays | |
| 71 | 72 | motion := &rt.copyScroll | |
| @@ -66,6 +66,7 @@ func (d *Daemon) smoothCopyWheel(sess *session, ac *attachedClient, delta int) { | |||||
| 66 | } | 66 | } | ||
| 67 | } | 67 | } | ||
| 68 | 68 | ||||
| 69 | + | // advanceCopyScrollLocked requires ac.overlays.copyMu, not d.mu. | |||
| 69 | func (d *Daemon) advanceCopyScrollLocked(sess *session, ac *attachedClient) (bool, bool) { | 70 | func (d *Daemon) advanceCopyScrollLocked(sess *session, ac *attachedClient) (bool, bool) { | ||
| 70 | rt := ac.overlays | 71 | rt := ac.overlays | ||
| 71 | motion := &rt.copyScroll | 72 | motion := &rt.copyScroll | ||
M internal/usecase/daemon/copymode.go +9 -0
| @@ -22,6 +22,7 @@ const copyDoubleClickInterval = 500 * time.Millisecond | |||
| 22 | 22 | ||
| 23 | 23 | func sameCopyPos(a, b scopy.Pos) bool { return a.Row == b.Row && a.Col == b.Col } | |
| 24 | 24 | ||
| 25 | + | // isCopyDoubleClickLocked requires rt.copyMu, not d.mu. | |
| 25 | 26 | func (d *Daemon) isCopyDoubleClickLocked(rt *overlayRuntime, pane *pane, pos scopy.Pos, now time.Time) bool { | |
| 26 | 27 | candidate := rt.copyClick | |
| 27 | 28 | return candidate.valid && !candidate.dragged && candidate.pane == pane && | |
| @@ -342,6 +343,8 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||
| 342 | 343 | d.invalidateRender(sess, ac, true, "copymode.go") | |
| 343 | 344 | } | |
| 344 | 345 | } | |
| 346 | + | ||
| 347 | + | // routeCopySearchInputLocked requires rt.copyMu, not d.mu. | |
| 345 | 348 | func (d *Daemon) routeCopySearchInputLocked(rt *overlayRuntime, data []byte) (changed, closeSearch, accepted bool) { | |
| 346 | 349 | routeOverlayBytes(data, &rt.copySearchPending, overlayEvents{rune: func(r rune) { rt.copySearch.Insert(r); changed = true }, backspace: func() { rt.copySearch.Backspace(); changed = true }, enter: func() { | |
| 347 | 350 | if _, ok := rt.copySearch.Selected(); ok { | |
| @@ -358,6 +361,8 @@ func (d *Daemon) routeCopySearchInputLocked(rt *overlayRuntime, data []byte) (ch | |||
| 358 | 361 | } | |
| 359 | 362 | return | |
| 360 | 363 | } | |
| 364 | + | ||
| 365 | + | // previewCopySearchSelectionLocked requires rt.copyMu, not d.mu. | |
| 361 | 366 | func (d *Daemon) previewCopySearchSelectionLocked(rt *overlayRuntime) { | |
| 362 | 367 | if rt == nil || rt.copyMode == nil || rt.copySearch == nil { | |
| 363 | 368 | return | |
| @@ -366,6 +371,8 @@ func (d *Daemon) previewCopySearchSelectionLocked(rt *overlayRuntime) { | |||
| 366 | 371 | rt.copyMode.SetPosition(scopy.Pos{Row: match.Row, Col: match.Start}) | |
| 367 | 372 | } | |
| 368 | 373 | } | |
| 374 | + | ||
| 375 | + | // retainCopyESCLocked requires ac.overlays.copyMu, not d.mu. | |
| 369 | 376 | func (d *Daemon) retainCopyESCLocked(ac *attachedClient) { | |
| 370 | 377 | rt := ac.overlays | |
| 371 | 378 | mode := rt.copyMode | |
| @@ -390,6 +397,8 @@ func (d *Daemon) retainCopyESCLocked(ac *attachedClient) { | |||
| 390 | 397 | } | |
| 391 | 398 | }) | |
| 392 | 399 | } | |
| 400 | + | ||
| 401 | + | // stopCopyPendingTimerLocked requires ac.overlays.copyMu, not d.mu. | |
| 393 | 402 | func (d *Daemon) stopCopyPendingTimerLocked(ac *attachedClient) { ac.overlays.copyESC.stop() } | |
| 394 | 403 | func routeCopyEscape(m *scopy.Mode, data []byte) (int, bool) { | |
| 395 | 404 | if len(data) >= 3 && (data[1] == '[' || data[1] == 'O') { | |
| @@ -22,6 +22,7 @@ const copyDoubleClickInterval = 500 * time.Millisecond | |||||
| 22 | 22 | ||||
| 23 | func sameCopyPos(a, b scopy.Pos) bool { return a.Row == b.Row && a.Col == b.Col } | 23 | func sameCopyPos(a, b scopy.Pos) bool { return a.Row == b.Row && a.Col == b.Col } | ||
| 24 | 24 | ||||
| 25 | + | // isCopyDoubleClickLocked requires rt.copyMu, not d.mu. | |||
| 25 | func (d *Daemon) isCopyDoubleClickLocked(rt *overlayRuntime, pane *pane, pos scopy.Pos, now time.Time) bool { | 26 | func (d *Daemon) isCopyDoubleClickLocked(rt *overlayRuntime, pane *pane, pos scopy.Pos, now time.Time) bool { | ||
| 26 | candidate := rt.copyClick | 27 | candidate := rt.copyClick | ||
| 27 | return candidate.valid && !candidate.dragged && candidate.pane == pane && | 28 | return candidate.valid && !candidate.dragged && candidate.pane == pane && | ||
| @@ -342,6 +343,8 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||||
| 342 | d.invalidateRender(sess, ac, true, "copymode.go") | 343 | d.invalidateRender(sess, ac, true, "copymode.go") | ||
| 343 | } | 344 | } | ||
| 344 | } | 345 | } | ||
| 346 | + | ||||
| 347 | + | // routeCopySearchInputLocked requires rt.copyMu, not d.mu. | |||
| 345 | func (d *Daemon) routeCopySearchInputLocked(rt *overlayRuntime, data []byte) (changed, closeSearch, accepted bool) { | 348 | func (d *Daemon) routeCopySearchInputLocked(rt *overlayRuntime, data []byte) (changed, closeSearch, accepted bool) { | ||
| 346 | routeOverlayBytes(data, &rt.copySearchPending, overlayEvents{rune: func(r rune) { rt.copySearch.Insert(r); changed = true }, backspace: func() { rt.copySearch.Backspace(); changed = true }, enter: func() { | 349 | routeOverlayBytes(data, &rt.copySearchPending, overlayEvents{rune: func(r rune) { rt.copySearch.Insert(r); changed = true }, backspace: func() { rt.copySearch.Backspace(); changed = true }, enter: func() { | ||
| 347 | if _, ok := rt.copySearch.Selected(); ok { | 350 | if _, ok := rt.copySearch.Selected(); ok { | ||
| @@ -358,6 +361,8 @@ func (d *Daemon) routeCopySearchInputLocked(rt *overlayRuntime, data []byte) (ch | |||||
| 358 | } | 361 | } | ||
| 359 | return | 362 | return | ||
| 360 | } | 363 | } | ||
| 364 | + | ||||
| 365 | + | // previewCopySearchSelectionLocked requires rt.copyMu, not d.mu. | |||
| 361 | func (d *Daemon) previewCopySearchSelectionLocked(rt *overlayRuntime) { | 366 | func (d *Daemon) previewCopySearchSelectionLocked(rt *overlayRuntime) { | ||
| 362 | if rt == nil || rt.copyMode == nil || rt.copySearch == nil { | 367 | if rt == nil || rt.copyMode == nil || rt.copySearch == nil { | ||
| 363 | return | 368 | return | ||
| @@ -366,6 +371,8 @@ func (d *Daemon) previewCopySearchSelectionLocked(rt *overlayRuntime) { | |||||
| 366 | rt.copyMode.SetPosition(scopy.Pos{Row: match.Row, Col: match.Start}) | 371 | rt.copyMode.SetPosition(scopy.Pos{Row: match.Row, Col: match.Start}) | ||
| 367 | } | 372 | } | ||
| 368 | } | 373 | } | ||
| 374 | + | ||||
| 375 | + | // retainCopyESCLocked requires ac.overlays.copyMu, not d.mu. | |||
| 369 | func (d *Daemon) retainCopyESCLocked(ac *attachedClient) { | 376 | func (d *Daemon) retainCopyESCLocked(ac *attachedClient) { | ||
| 370 | rt := ac.overlays | 377 | rt := ac.overlays | ||
| 371 | mode := rt.copyMode | 378 | mode := rt.copyMode | ||
| @@ -390,6 +397,8 @@ func (d *Daemon) retainCopyESCLocked(ac *attachedClient) { | |||||
| 390 | } | 397 | } | ||
| 391 | }) | 398 | }) | ||
| 392 | } | 399 | } | ||
| 400 | + | ||||
| 401 | + | // stopCopyPendingTimerLocked requires ac.overlays.copyMu, not d.mu. | |||
| 393 | func (d *Daemon) stopCopyPendingTimerLocked(ac *attachedClient) { ac.overlays.copyESC.stop() } | 402 | func (d *Daemon) stopCopyPendingTimerLocked(ac *attachedClient) { ac.overlays.copyESC.stop() } | ||
| 394 | func routeCopyEscape(m *scopy.Mode, data []byte) (int, bool) { | 403 | func routeCopyEscape(m *scopy.Mode, data []byte) (int, bool) { | ||
| 395 | if len(data) >= 3 && (data[1] == '[' || data[1] == 'O') { | 404 | if len(data) >= 3 && (data[1] == '[' || data[1] == 'O') { | ||
M internal/usecase/daemon/floating_spare.go +2 -2
| @@ -100,8 +100,8 @@ func (d *Daemon) ensureFloatingSpare(sess *session, tb *tab) { | |||
| 100 | 100 | } | |
| 101 | 101 | d.reapInstalledFloating(spare) | |
| 102 | 102 | } | |
| 103 | - | d.sessWg.Add(1) | |
| 104 | - | go d.readPanePTY(p) | |
| 103 | + | reader := p | |
| 104 | + | d.sessWg.Go(func() { d.readPanePTY(reader) }) | |
| 105 | 105 | p = nil | |
| 106 | 106 | } | |
| 107 | 107 | sess.floatingLaunchMu.Unlock() | |
| @@ -100,8 +100,8 @@ func (d *Daemon) ensureFloatingSpare(sess *session, tb *tab) { | |||||
| 100 | } | 100 | } | ||
| 101 | d.reapInstalledFloating(spare) | 101 | d.reapInstalledFloating(spare) | ||
| 102 | } | 102 | } | ||
| 103 | - | d.sessWg.Add(1) | 103 | + | reader := p |
| 104 | - | go d.readPanePTY(p) | 104 | + | d.sessWg.Go(func() { d.readPanePTY(reader) }) |
| 105 | p = nil | 105 | p = nil | ||
| 106 | } | 106 | } | ||
| 107 | sess.floatingLaunchMu.Unlock() | 107 | sess.floatingLaunchMu.Unlock() | ||
M internal/usecase/daemon/move_lifecycle.go +2 -0
| @@ -179,6 +179,7 @@ func (d *Daemon) releaseMoveAdmissionLocked() { | |||
| 179 | 179 | d.mu.Unlock() | |
| 180 | 180 | } | |
| 181 | 181 | ||
| 182 | + | // moveLifecycleChangeLocked requires moveLifecycleMu, not d.mu. | |
| 182 | 183 | func (d *Daemon) moveLifecycleChangeLocked() chan struct{} { | |
| 183 | 184 | if d.moveLifecycleChanged == nil { | |
| 184 | 185 | d.moveLifecycleChanged = make(chan struct{}) | |
| @@ -186,6 +187,7 @@ func (d *Daemon) moveLifecycleChangeLocked() chan struct{} { | |||
| 186 | 187 | return d.moveLifecycleChanged | |
| 187 | 188 | } | |
| 188 | 189 | ||
| 190 | + | // signalMoveLifecycleChangedLocked requires moveLifecycleMu, not d.mu. | |
| 189 | 191 | func (d *Daemon) signalMoveLifecycleChangedLocked() { | |
| 190 | 192 | close(d.moveLifecycleChangeLocked()) | |
| 191 | 193 | d.moveLifecycleChanged = make(chan struct{}) | |
| @@ -179,6 +179,7 @@ func (d *Daemon) releaseMoveAdmissionLocked() { | |||||
| 179 | d.mu.Unlock() | 179 | d.mu.Unlock() | ||
| 180 | } | 180 | } | ||
| 181 | 181 | ||||
| 182 | + | // moveLifecycleChangeLocked requires moveLifecycleMu, not d.mu. | |||
| 182 | func (d *Daemon) moveLifecycleChangeLocked() chan struct{} { | 183 | func (d *Daemon) moveLifecycleChangeLocked() chan struct{} { | ||
| 183 | if d.moveLifecycleChanged == nil { | 184 | if d.moveLifecycleChanged == nil { | ||
| 184 | d.moveLifecycleChanged = make(chan struct{}) | 185 | d.moveLifecycleChanged = make(chan struct{}) | ||
| @@ -186,6 +187,7 @@ func (d *Daemon) moveLifecycleChangeLocked() chan struct{} { | |||||
| 186 | return d.moveLifecycleChanged | 187 | return d.moveLifecycleChanged | ||
| 187 | } | 188 | } | ||
| 188 | 189 | ||||
| 190 | + | // signalMoveLifecycleChangedLocked requires moveLifecycleMu, not d.mu. | |||
| 189 | func (d *Daemon) signalMoveLifecycleChangedLocked() { | 191 | func (d *Daemon) signalMoveLifecycleChangedLocked() { | ||
| 190 | close(d.moveLifecycleChangeLocked()) | 192 | close(d.moveLifecycleChangeLocked()) | ||
| 191 | d.moveLifecycleChanged = make(chan struct{}) | 193 | d.moveLifecycleChanged = make(chan struct{}) | ||
M internal/usecase/daemon/palette.go +1 -0
| @@ -786,6 +786,7 @@ func paletteArgs(query string, cmd command.Command) []string { | |||
| 786 | 786 | return action.Args | |
| 787 | 787 | } | |
| 788 | 788 | ||
| 789 | + | // clearPaletteLocked requires ac.overlays.paletteMu. | |
| 789 | 790 | func (ac *attachedClient) clearPaletteLocked() { | |
| 790 | 791 | ac.overlays.paletteGeneration++ | |
| 791 | 792 | ac.overlays.palette = nil | |
| @@ -786,6 +786,7 @@ func paletteArgs(query string, cmd command.Command) []string { | |||||
| 786 | return action.Args | 786 | return action.Args | ||
| 787 | } | 787 | } | ||
| 788 | 788 | ||||
| 789 | + | // clearPaletteLocked requires ac.overlays.paletteMu. | |||
| 789 | func (ac *attachedClient) clearPaletteLocked() { | 790 | func (ac *attachedClient) clearPaletteLocked() { | ||
| 790 | ac.overlays.paletteGeneration++ | 791 | ac.overlays.paletteGeneration++ | ||
| 791 | ac.overlays.palette = nil | 792 | ac.overlays.palette = nil | ||
M internal/usecase/daemon/render.go +5 -3
| @@ -56,9 +56,12 @@ func (d *Daemon) paneRenderable(sess *session, tb *tab, p *pane) bool { | |||
| 56 | 56 | return false | |
| 57 | 57 | } | |
| 58 | 58 | ||
| 59 | - | // ptyReader is retained as a compatibility entry point for focused tests. The | |
| 60 | - | // production goroutine calls readPanePTY, whose closure owns only the pane. | |
| 59 | + | // ptyReader is retained as a compatibility entry point for focused tests, which | |
| 60 | + | // register the goroutine with sessWg.Add before calling it. The production | |
| 61 | + | // goroutine is launched with sessWg.Go and calls readPanePTY, whose closure owns | |
| 62 | + | // only the pane. | |
| 61 | 63 | func (d *Daemon) ptyReader(sess *session, tb *tab, p *pane) { | |
| 64 | + | defer d.sessWg.Done() | |
| 62 | 65 | if p != nil && p.ownerSnapshot() == nil && sess != nil && tb != nil { | |
| 63 | 66 | publishPaneOwner(p, sess, tb, 0) | |
| 64 | 67 | } | |
| @@ -66,7 +69,6 @@ func (d *Daemon) ptyReader(sess *session, tb *tab, p *pane) { | |||
| 66 | 69 | } | |
| 67 | 70 | ||
| 68 | 71 | func (d *Daemon) readPanePTY(p *pane) { | |
| 69 | - | defer d.sessWg.Done() | |
| 70 | 72 | if p == nil { | |
| 71 | 73 | return | |
| 72 | 74 | } | |
| @@ -56,9 +56,12 @@ func (d *Daemon) paneRenderable(sess *session, tb *tab, p *pane) bool { | |||||
| 56 | return false | 56 | return false | ||
| 57 | } | 57 | } | ||
| 58 | 58 | ||||
| 59 | - | // ptyReader is retained as a compatibility entry point for focused tests. The | 59 | + | // ptyReader is retained as a compatibility entry point for focused tests, which |
| 60 | - | // production goroutine calls readPanePTY, whose closure owns only the pane. | 60 | + | // register the goroutine with sessWg.Add before calling it. The production |
| 61 | + | // goroutine is launched with sessWg.Go and calls readPanePTY, whose closure owns | |||
| 62 | + | // only the pane. | |||
| 61 | func (d *Daemon) ptyReader(sess *session, tb *tab, p *pane) { | 63 | func (d *Daemon) ptyReader(sess *session, tb *tab, p *pane) { | ||
| 64 | + | defer d.sessWg.Done() | |||
| 62 | if p != nil && p.ownerSnapshot() == nil && sess != nil && tb != nil { | 65 | if p != nil && p.ownerSnapshot() == nil && sess != nil && tb != nil { | ||
| 63 | publishPaneOwner(p, sess, tb, 0) | 66 | publishPaneOwner(p, sess, tb, 0) | ||
| 64 | } | 67 | } | ||
| @@ -66,7 +69,6 @@ func (d *Daemon) ptyReader(sess *session, tb *tab, p *pane) { | |||||
| 66 | } | 69 | } | ||
| 67 | 70 | ||||
| 68 | func (d *Daemon) readPanePTY(p *pane) { | 71 | func (d *Daemon) readPanePTY(p *pane) { | ||
| 69 | - | defer d.sessWg.Done() | |||
| 70 | if p == nil { | 72 | if p == nil { | ||
| 71 | return | 73 | return | ||
| 72 | } | 74 | } | ||
M internal/usecase/daemon/resize_mode.go +1 -0
| @@ -146,6 +146,7 @@ func (d *Daemon) handleResizeInput(ac *attachedClient, data []byte) { | |||
| 146 | 146 | } | |
| 147 | 147 | } | |
| 148 | 148 | ||
| 149 | + | // retainResizeESCLocked requires ac.overlays.resizeMu, not d.mu. | |
| 149 | 150 | func (d *Daemon) retainResizeESCLocked(ac *attachedClient) { | |
| 150 | 151 | rt := ac.overlays | |
| 151 | 152 | rt.resizePending = append(rt.resizePending[:0], keys.ESC) | |
| @@ -146,6 +146,7 @@ func (d *Daemon) handleResizeInput(ac *attachedClient, data []byte) { | |||||
| 146 | } | 146 | } | ||
| 147 | } | 147 | } | ||
| 148 | 148 | ||||
| 149 | + | // retainResizeESCLocked requires ac.overlays.resizeMu, not d.mu. | |||
| 149 | func (d *Daemon) retainResizeESCLocked(ac *attachedClient) { | 150 | func (d *Daemon) retainResizeESCLocked(ac *attachedClient) { | ||
| 150 | rt := ac.overlays | 151 | rt := ac.overlays | ||
| 151 | rt.resizePending = append(rt.resizePending[:0], keys.ESC) | 152 | rt.resizePending = append(rt.resizePending[:0], keys.ESC) | ||
M internal/usecase/daemon/session.go +1 -2
| @@ -901,8 +901,7 @@ func (d *Daemon) startPaneGoroutines(sess *session, tb *tab, p *pane) { | |||
| 901 | 901 | return | |
| 902 | 902 | } | |
| 903 | 903 | // Scheduler ownership was removed; this launch creates exactly one reader. | |
| 904 | - | d.sessWg.Add(1) | |
| 905 | - | go d.readPanePTY(p) | |
| 904 | + | d.sessWg.Go(func() { d.readPanePTY(p) }) | |
| 906 | 905 | } | |
| 907 | 906 | ||
| 908 | 907 | // detachIfCurrent publishes terminal attachment invalidation through the attachment | |
| @@ -901,8 +901,7 @@ func (d *Daemon) startPaneGoroutines(sess *session, tb *tab, p *pane) { | |||||
| 901 | return | 901 | return | ||
| 902 | } | 902 | } | ||
| 903 | // Scheduler ownership was removed; this launch creates exactly one reader. | 903 | // Scheduler ownership was removed; this launch creates exactly one reader. | ||
| 904 | - | d.sessWg.Add(1) | 904 | + | d.sessWg.Go(func() { d.readPanePTY(p) }) |
| 905 | - | go d.readPanePTY(p) | |||
| 906 | } | 905 | } | ||
| 907 | 906 | ||||
| 908 | // detachIfCurrent publishes terminal attachment invalidation through the attachment | 907 | // detachIfCurrent publishes terminal attachment invalidation through the attachment | ||
M internal/usecase/daemon/theme_resolve.go +2 -0
| @@ -15,6 +15,8 @@ func (d *Daemon) applyHostTheme(sess *session, ac *attachedClient, t theme.Theme | |||
| 15 | 15 | return d.applyHostThemeLocked(sess, ac, t, clearUnknownScheme) | |
| 16 | 16 | } | |
| 17 | 17 | ||
| 18 | + | // applyHostThemeLocked requires sess.themeMu (and ac.sendMu when ac is non-nil), | |
| 19 | + | // not d.mu. | |
| 18 | 20 | func (d *Daemon) applyHostThemeLocked(sess *session, ac *attachedClient, t theme.Theme, clearUnknownScheme bool) bool { | |
| 19 | 21 | // Resolve while applying, never while composing. The theme mutex only | |
| 20 | 22 | // publishes the completed value and is not held across session/tab/pane | |
| @@ -15,6 +15,8 @@ func (d *Daemon) applyHostTheme(sess *session, ac *attachedClient, t theme.Theme | |||||
| 15 | return d.applyHostThemeLocked(sess, ac, t, clearUnknownScheme) | 15 | return d.applyHostThemeLocked(sess, ac, t, clearUnknownScheme) | ||
| 16 | } | 16 | } | ||
| 17 | 17 | ||||
| 18 | + | // applyHostThemeLocked requires sess.themeMu (and ac.sendMu when ac is non-nil), | |||
| 19 | + | // not d.mu. | |||
| 18 | func (d *Daemon) applyHostThemeLocked(sess *session, ac *attachedClient, t theme.Theme, clearUnknownScheme bool) bool { | 20 | func (d *Daemon) applyHostThemeLocked(sess *session, ac *attachedClient, t theme.Theme, clearUnknownScheme bool) bool { | ||
| 19 | // Resolve while applying, never while composing. The theme mutex only | 21 | // Resolve while applying, never while composing. The theme mutex only | ||
| 20 | // publishes the completed value and is not held across session/tab/pane | 22 | // publishes the completed value and is not held across session/tab/pane | ||