refactor(daemon): drop test-only graphics namespace release and cwd override inspector
- commit
- db16c3170dc0cd799ead274af92c3280575ee886
- author
- brice · · 2026-10-08 08:59 UTC
- committer
- brice · · 2026-10-08 09:31 UTC
- parent
- 0f5321a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
13 files · +20 -209
M internal/usecase/daemon/bar_refresh_test.go +2 -2
| @@ -96,10 +96,10 @@ func TestBarScriptContextUsesFirstOrderedPaneAndClientCols(t *testing.T) { | |||
| 96 | 96 | active := sess.tabs[0].focusedPane() | |
| 97 | 97 | active.stableID = "pane-default" | |
| 98 | 98 | active.pty = newScriptPTY(nil) | |
| 99 | - | WithCwdReader(func(pid int) (string, error) { | |
| 99 | + | WithProcessInspector(&processInspectorFake{cwd: func(pid int) (string, error) { | |
| 100 | 100 | require.Equal(t, 4242, pid) | |
| 101 | 101 | return "/pane-repo", nil | |
| 102 | - | })(d) | |
| 102 | + | }})(d) | |
| 103 | 103 | ||
| 104 | 104 | require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), true)) | |
| 105 | 105 | waitBarRefreshIdle(t, d) | |
| @@ -96,10 +96,10 @@ func TestBarScriptContextUsesFirstOrderedPaneAndClientCols(t *testing.T) { | |||||
| 96 | active := sess.tabs[0].focusedPane() | 96 | active := sess.tabs[0].focusedPane() | ||
| 97 | active.stableID = "pane-default" | 97 | active.stableID = "pane-default" | ||
| 98 | active.pty = newScriptPTY(nil) | 98 | active.pty = newScriptPTY(nil) | ||
| 99 | - | WithCwdReader(func(pid int) (string, error) { | 99 | + | WithProcessInspector(&processInspectorFake{cwd: func(pid int) (string, error) { |
| 100 | require.Equal(t, 4242, pid) | 100 | require.Equal(t, 4242, pid) | ||
| 101 | return "/pane-repo", nil | 101 | return "/pane-repo", nil | ||
| 102 | - | })(d) | 102 | + | }})(d) |
| 103 | 103 | ||||
| 104 | require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), true)) | 104 | require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), true)) | ||
| 105 | waitBarRefreshIdle(t, d) | 105 | waitBarRefreshIdle(t, d) | ||
M internal/usecase/daemon/daemon.go +1 -12
| @@ -187,7 +187,7 @@ type Daemon struct { | |||
| 187 | 187 | restoreDone chan struct{} | |
| 188 | 188 | restoreOnce sync.Once | |
| 189 | 189 | // proc inspects pane processes (cwd, foreground command, argv). Nil means | |
| 190 | - | // no inspection is available; see WithProcessInspector and WithCwdReader. | |
| 190 | + | // no inspection is available; see WithProcessInspector. | |
| 191 | 191 | proc ports.ProcessInspector | |
| 192 | 192 | dirOrHome func(string) string | |
| 193 | 193 | // bindings is read by keys.Router through its own atomic pointer, so it is | |
| @@ -527,17 +527,6 @@ func WithSnapshotGarbageCollection() Option { | |||
| 527 | 527 | return func(d *Daemon) { d.snapshotGarbageCollection = true } | |
| 528 | 528 | } | |
| 529 | 529 | ||
| 530 | - | // WithCwdReader overrides the process cwd reader used for persistence tests. | |
| 531 | - | // Every other inspection keeps using the installed process inspector, or stays | |
| 532 | - | // unavailable when none is installed. | |
| 533 | - | func WithCwdReader(fn func(int) (string, error)) Option { | |
| 534 | - | return func(d *Daemon) { | |
| 535 | - | if fn != nil { | |
| 536 | - | d.proc = cwdOverrideInspector{base: unwrapCwdOverride(d.proc), cwd: fn} | |
| 537 | - | } | |
| 538 | - | } | |
| 539 | - | } | |
| 540 | - | ||
| 541 | 530 | // WithProcessInspector installs the platform process-inspection implementation. | |
| 542 | 531 | func WithProcessInspector(ins ports.ProcessInspector) Option { | |
| 543 | 532 | return func(d *Daemon) { | |
| @@ -187,7 +187,7 @@ type Daemon struct { | |||||
| 187 | restoreDone chan struct{} | 187 | restoreDone chan struct{} | ||
| 188 | restoreOnce sync.Once | 188 | restoreOnce sync.Once | ||
| 189 | // proc inspects pane processes (cwd, foreground command, argv). Nil means | 189 | // proc inspects pane processes (cwd, foreground command, argv). Nil means | ||
| 190 | - | // no inspection is available; see WithProcessInspector and WithCwdReader. | 190 | + | // no inspection is available; see WithProcessInspector. |
| 191 | proc ports.ProcessInspector | 191 | proc ports.ProcessInspector | ||
| 192 | dirOrHome func(string) string | 192 | dirOrHome func(string) string | ||
| 193 | // bindings is read by keys.Router through its own atomic pointer, so it is | 193 | // bindings is read by keys.Router through its own atomic pointer, so it is | ||
| @@ -527,17 +527,6 @@ func WithSnapshotGarbageCollection() Option { | |||||
| 527 | return func(d *Daemon) { d.snapshotGarbageCollection = true } | 527 | return func(d *Daemon) { d.snapshotGarbageCollection = true } | ||
| 528 | } | 528 | } | ||
| 529 | 529 | ||||
| 530 | - | // WithCwdReader overrides the process cwd reader used for persistence tests. | |||
| 531 | - | // Every other inspection keeps using the installed process inspector, or stays | |||
| 532 | - | // unavailable when none is installed. | |||
| 533 | - | func WithCwdReader(fn func(int) (string, error)) Option { | |||
| 534 | - | return func(d *Daemon) { | |||
| 535 | - | if fn != nil { | |||
| 536 | - | d.proc = cwdOverrideInspector{base: unwrapCwdOverride(d.proc), cwd: fn} | |||
| 537 | - | } | |||
| 538 | - | } | |||
| 539 | - | } | |||
| 540 | - | ||||
| 541 | // WithProcessInspector installs the platform process-inspection implementation. | 530 | // WithProcessInspector installs the platform process-inspection implementation. | ||
| 542 | func WithProcessInspector(ins ports.ProcessInspector) Option { | 531 | func WithProcessInspector(ins ports.ProcessInspector) Option { | ||
| 543 | return func(d *Daemon) { | 532 | return func(d *Daemon) { | ||
M internal/usecase/daemon/floating_spare_test.go +1 -1
| @@ -124,7 +124,7 @@ func TestFloatingSpareIsSharedPerSession(t *testing.T) { | |||
| 124 | 124 | d := newTestDaemon(t, factory, stubClock{}) | |
| 125 | 125 | d.ApplyConfig(domain.Config{Floating: cfg}) | |
| 126 | 126 | cwds := map[int]string{1: "/work", 2: tt.claimCwd} | |
| 127 | - | WithCwdReader(func(pid int) (string, error) { return cwds[pid], nil })(d) | |
| 127 | + | WithProcessInspector(&processInspectorFake{cwd: func(pid int) (string, error) { return cwds[pid], nil }})(d) | |
| 128 | 128 | d.dirOrHome = func(cwd string) string { return cwd } | |
| 129 | 129 | ||
| 130 | 130 | first := newTab(&pidPTY{pid: 1}, domain.Size{Cols: 120, Rows: 40}) | |
| @@ -124,7 +124,7 @@ func TestFloatingSpareIsSharedPerSession(t *testing.T) { | |||||
| 124 | d := newTestDaemon(t, factory, stubClock{}) | 124 | d := newTestDaemon(t, factory, stubClock{}) | ||
| 125 | d.ApplyConfig(domain.Config{Floating: cfg}) | 125 | d.ApplyConfig(domain.Config{Floating: cfg}) | ||
| 126 | cwds := map[int]string{1: "/work", 2: tt.claimCwd} | 126 | cwds := map[int]string{1: "/work", 2: tt.claimCwd} | ||
| 127 | - | WithCwdReader(func(pid int) (string, error) { return cwds[pid], nil })(d) | 127 | + | WithProcessInspector(&processInspectorFake{cwd: func(pid int) (string, error) { return cwds[pid], nil }})(d) |
| 128 | d.dirOrHome = func(cwd string) string { return cwd } | 128 | d.dirOrHome = func(cwd string) string { return cwd } | ||
| 129 | 129 | ||||
| 130 | first := newTab(&pidPTY{pid: 1}, domain.Size{Cols: 120, Rows: 40}) | 130 | first := newTab(&pidPTY{pid: 1}, domain.Size{Cols: 120, Rows: 40}) | ||
M internal/usecase/daemon/floating_test.go +1 -1
| @@ -627,7 +627,7 @@ func TestFloatingLaunchUsesLiveOrValidatedSessionCwd(t *testing.T) { | |||
| 627 | 627 | return cwd | |
| 628 | 628 | } | |
| 629 | 629 | if tt.liveCwd != "" { | |
| 630 | - | WithCwdReader(func(int) (string, error) { return tt.liveCwd, nil })(d) | |
| 630 | + | WithProcessInspector(&processInspectorFake{cwd: func(int) (string, error) { return tt.liveCwd, nil }})(d) | |
| 631 | 631 | } | |
| 632 | 632 | tb := newFloatingTestTab(t) | |
| 633 | 633 | sess := &session{sessionCore: sessionCore{name: "work"}, cwd: tt.sessionCwd, tabs: []*tab{tb}, ctx: t.Context()} | |
| @@ -627,7 +627,7 @@ func TestFloatingLaunchUsesLiveOrValidatedSessionCwd(t *testing.T) { | |||||
| 627 | return cwd | 627 | return cwd | ||
| 628 | } | 628 | } | ||
| 629 | if tt.liveCwd != "" { | 629 | if tt.liveCwd != "" { | ||
| 630 | - | WithCwdReader(func(int) (string, error) { return tt.liveCwd, nil })(d) | 630 | + | WithProcessInspector(&processInspectorFake{cwd: func(int) (string, error) { return tt.liveCwd, nil }})(d) |
| 631 | } | 631 | } | ||
| 632 | tb := newFloatingTestTab(t) | 632 | tb := newFloatingTestTab(t) | ||
| 633 | sess := &session{sessionCore: sessionCore{name: "work"}, cwd: tt.sessionCwd, tabs: []*tab{tb}, ctx: t.Context()} | 633 | sess := &session{sessionCore: sessionCore{name: "work"}, cwd: tt.sessionCwd, tabs: []*tab{tb}, ctx: t.Context()} | ||
M internal/usecase/daemon/graphics_namespace_pool.go +0 -14
| @@ -70,20 +70,6 @@ func (p *graphicsNamespacePool) reserveLease(key string) (uint64, uint64) { | |||
| 70 | 70 | return 0, 0 | |
| 71 | 71 | } | |
| 72 | 72 | ||
| 73 | - | // releaseBase is the legacy base-only release. A quarantined block is never | |
| 74 | - | // released by it. | |
| 75 | - | func (p *graphicsNamespacePool) releaseBase(base uint64) { | |
| 76 | - | if base == 0 || base%graphicsIDNamespaceSize != 1 { | |
| 77 | - | return | |
| 78 | - | } | |
| 79 | - | block := (base - 1) / graphicsIDNamespaceSize | |
| 80 | - | if _, quarantined := p.quarantines[block]; quarantined { | |
| 81 | - | return | |
| 82 | - | } | |
| 83 | - | delete(p.reserved, block) | |
| 84 | - | delete(p.fences, block) | |
| 85 | - | } | |
| 86 | - | ||
| 87 | 73 | func (p *graphicsNamespacePool) releaseLease(state *graphicsOutputState) { | |
| 88 | 74 | if state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 { | |
| 89 | 75 | return | |
| @@ -70,20 +70,6 @@ func (p *graphicsNamespacePool) reserveLease(key string) (uint64, uint64) { | |||||
| 70 | return 0, 0 | 70 | return 0, 0 | ||
| 71 | } | 71 | } | ||
| 72 | 72 | ||||
| 73 | - | // releaseBase is the legacy base-only release. A quarantined block is never | |||
| 74 | - | // released by it. | |||
| 75 | - | func (p *graphicsNamespacePool) releaseBase(base uint64) { | |||
| 76 | - | if base == 0 || base%graphicsIDNamespaceSize != 1 { | |||
| 77 | - | return | |||
| 78 | - | } | |||
| 79 | - | block := (base - 1) / graphicsIDNamespaceSize | |||
| 80 | - | if _, quarantined := p.quarantines[block]; quarantined { | |||
| 81 | - | return | |||
| 82 | - | } | |||
| 83 | - | delete(p.reserved, block) | |||
| 84 | - | delete(p.fences, block) | |||
| 85 | - | } | |||
| 86 | - | ||||
| 87 | func (p *graphicsNamespacePool) releaseLease(state *graphicsOutputState) { | 73 | func (p *graphicsNamespacePool) releaseLease(state *graphicsOutputState) { | ||
| 88 | if state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 { | 74 | if state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 { | ||
| 89 | return | 75 | return | ||
M internal/usecase/daemon/graphics_output.go +4 -27
| @@ -149,14 +149,10 @@ func graphicsNamespaceKey(sess *session, clientID [16]byte) string { | |||
| 149 | 149 | return fmt.Sprintf("session:%s:%x:%x", sess.id, sess.incarnation, clientID) | |
| 150 | 150 | } | |
| 151 | 151 | ||
| 152 | - | // reserveGraphicsNamespaceLocked chooses a deterministic preferred block and | |
| 153 | - | // linearly probes the bounded namespace table for a free block. Caller holds | |
| 154 | - | // d.mu; probing makes equal session/client identities collision-safe too. | |
| 155 | - | func (d *Daemon) reserveGraphicsNamespaceLocked(key string) uint64 { | |
| 156 | - | base, _ := d.reserveGraphicsNamespaceLeaseLocked(key) | |
| 157 | - | return base | |
| 158 | - | } | |
| 159 | - | ||
| 152 | + | // reserveGraphicsNamespaceLeaseLocked chooses a deterministic preferred block | |
| 153 | + | // and linearly probes the bounded namespace table for a free block, returning | |
| 154 | + | // its base and fence. Caller holds d.mu; probing makes equal session/client | |
| 155 | + | // identities collision-safe too. | |
| 160 | 156 | func (d *Daemon) reserveGraphicsNamespaceLeaseLocked(key string) (uint64, uint64) { | |
| 161 | 157 | if d == nil { | |
| 162 | 158 | return 0, 0 | |
| @@ -164,25 +160,6 @@ func (d *Daemon) reserveGraphicsNamespaceLeaseLocked(key string) (uint64, uint64 | |||
| 164 | 160 | return d.graphics.reserveLease(key) | |
| 165 | 161 | } | |
| 166 | 162 | ||
| 167 | - | func (d *Daemon) releaseGraphicsNamespace(base uint64) { | |
| 168 | - | if d == nil || base == 0 || base%graphicsIDNamespaceSize != 1 { | |
| 169 | - | return | |
| 170 | - | } | |
| 171 | - | d.mu.Lock() | |
| 172 | - | d.releaseGraphicsNamespaceLocked(base) | |
| 173 | - | d.mu.Unlock() | |
| 174 | - | } | |
| 175 | - | ||
| 176 | - | // releaseGraphicsNamespaceLocked is used by resume publication while d.mu is | |
| 177 | - | // already held. A quarantined block is intentionally not released by this | |
| 178 | - | // legacy base-only helper; only its fenced cleanup lifecycle may retire it. | |
| 179 | - | func (d *Daemon) releaseGraphicsNamespaceLocked(base uint64) { | |
| 180 | - | if d == nil { | |
| 181 | - | return | |
| 182 | - | } | |
| 183 | - | d.graphics.releaseBase(base) | |
| 184 | - | } | |
| 185 | - | ||
| 186 | 163 | func (d *Daemon) releaseGraphicsNamespaceLeaseLocked(state *graphicsOutputState) { | |
| 187 | 164 | if d == nil { | |
| 188 | 165 | return | |
| @@ -149,14 +149,10 @@ func graphicsNamespaceKey(sess *session, clientID [16]byte) string { | |||||
| 149 | return fmt.Sprintf("session:%s:%x:%x", sess.id, sess.incarnation, clientID) | 149 | return fmt.Sprintf("session:%s:%x:%x", sess.id, sess.incarnation, clientID) | ||
| 150 | } | 150 | } | ||
| 151 | 151 | ||||
| 152 | - | // reserveGraphicsNamespaceLocked chooses a deterministic preferred block and | 152 | + | // reserveGraphicsNamespaceLeaseLocked chooses a deterministic preferred block |
| 153 | - | // linearly probes the bounded namespace table for a free block. Caller holds | 153 | + | // and linearly probes the bounded namespace table for a free block, returning |
| 154 | - | // d.mu; probing makes equal session/client identities collision-safe too. | 154 | + | // its base and fence. Caller holds d.mu; probing makes equal session/client |
| 155 | - | func (d *Daemon) reserveGraphicsNamespaceLocked(key string) uint64 { | 155 | + | // identities collision-safe too. |
| 156 | - | base, _ := d.reserveGraphicsNamespaceLeaseLocked(key) | |||
| 157 | - | return base | |||
| 158 | - | } | |||
| 159 | - | ||||
| 160 | func (d *Daemon) reserveGraphicsNamespaceLeaseLocked(key string) (uint64, uint64) { | 156 | func (d *Daemon) reserveGraphicsNamespaceLeaseLocked(key string) (uint64, uint64) { | ||
| 161 | if d == nil { | 157 | if d == nil { | ||
| 162 | return 0, 0 | 158 | return 0, 0 | ||
| @@ -164,25 +160,6 @@ func (d *Daemon) reserveGraphicsNamespaceLeaseLocked(key string) (uint64, uint64 | |||||
| 164 | return d.graphics.reserveLease(key) | 160 | return d.graphics.reserveLease(key) | ||
| 165 | } | 161 | } | ||
| 166 | 162 | ||||
| 167 | - | func (d *Daemon) releaseGraphicsNamespace(base uint64) { | |||
| 168 | - | if d == nil || base == 0 || base%graphicsIDNamespaceSize != 1 { | |||
| 169 | - | return | |||
| 170 | - | } | |||
| 171 | - | d.mu.Lock() | |||
| 172 | - | d.releaseGraphicsNamespaceLocked(base) | |||
| 173 | - | d.mu.Unlock() | |||
| 174 | - | } | |||
| 175 | - | ||||
| 176 | - | // releaseGraphicsNamespaceLocked is used by resume publication while d.mu is | |||
| 177 | - | // already held. A quarantined block is intentionally not released by this | |||
| 178 | - | // legacy base-only helper; only its fenced cleanup lifecycle may retire it. | |||
| 179 | - | func (d *Daemon) releaseGraphicsNamespaceLocked(base uint64) { | |||
| 180 | - | if d == nil { | |||
| 181 | - | return | |||
| 182 | - | } | |||
| 183 | - | d.graphics.releaseBase(base) | |||
| 184 | - | } | |||
| 185 | - | ||||
| 186 | func (d *Daemon) releaseGraphicsNamespaceLeaseLocked(state *graphicsOutputState) { | 163 | func (d *Daemon) releaseGraphicsNamespaceLeaseLocked(state *graphicsOutputState) { | ||
| 187 | if d == nil { | 164 | if d == nil { | ||
| 188 | return | 165 | return | ||
M internal/usecase/daemon/graphics_output_test.go +5 -7
| @@ -353,16 +353,14 @@ func TestUnsupportedGraphicsWarningIsBoundedForLateSceneCreation(t *testing.T) { | |||
| 353 | 353 | func TestGraphicsNamespacesAreDeterministicAndCollisionSafe(t *testing.T) { | |
| 354 | 354 | d := &Daemon{} | |
| 355 | 355 | d.mu.Lock() | |
| 356 | - | first := d.reserveGraphicsNamespaceLocked("session:work:attachment") | |
| 357 | - | second := d.reserveGraphicsNamespaceLocked("session:work:attachment") | |
| 358 | - | d.mu.Unlock() | |
| 356 | + | defer d.mu.Unlock() | |
| 357 | + | first, firstFence := d.reserveGraphicsNamespaceLeaseLocked("session:work:attachment") | |
| 358 | + | second, _ := d.reserveGraphicsNamespaceLeaseLocked("session:work:attachment") | |
| 359 | 359 | require.NotZero(t, first) | |
| 360 | 360 | require.NotZero(t, second) | |
| 361 | 361 | require.NotEqual(t, first, second, "equal attachment/session keys still need a collision-safe namespace") | |
| 362 | - | d.releaseGraphicsNamespace(first) | |
| 363 | - | d.mu.Lock() | |
| 364 | - | reused := d.reserveGraphicsNamespaceLocked("session:work:attachment") | |
| 365 | - | d.mu.Unlock() | |
| 362 | + | d.releaseGraphicsNamespaceLeaseLocked(&graphicsOutputState{namespaceBase: first, namespaceFence: firstFence}) | |
| 363 | + | reused, _ := d.reserveGraphicsNamespaceLeaseLocked("session:work:attachment") | |
| 366 | 364 | require.Equal(t, first, reused, "released namespaces should deterministically return to their preferred block") | |
| 367 | 365 | } | |
| 368 | 366 | ||
| @@ -353,16 +353,14 @@ func TestUnsupportedGraphicsWarningIsBoundedForLateSceneCreation(t *testing.T) { | |||||
| 353 | func TestGraphicsNamespacesAreDeterministicAndCollisionSafe(t *testing.T) { | 353 | func TestGraphicsNamespacesAreDeterministicAndCollisionSafe(t *testing.T) { | ||
| 354 | d := &Daemon{} | 354 | d := &Daemon{} | ||
| 355 | d.mu.Lock() | 355 | d.mu.Lock() | ||
| 356 | - | first := d.reserveGraphicsNamespaceLocked("session:work:attachment") | 356 | + | defer d.mu.Unlock() |
| 357 | - | second := d.reserveGraphicsNamespaceLocked("session:work:attachment") | 357 | + | first, firstFence := d.reserveGraphicsNamespaceLeaseLocked("session:work:attachment") |
| 358 | - | d.mu.Unlock() | 358 | + | second, _ := d.reserveGraphicsNamespaceLeaseLocked("session:work:attachment") |
| 359 | require.NotZero(t, first) | 359 | require.NotZero(t, first) | ||
| 360 | require.NotZero(t, second) | 360 | require.NotZero(t, second) | ||
| 361 | require.NotEqual(t, first, second, "equal attachment/session keys still need a collision-safe namespace") | 361 | require.NotEqual(t, first, second, "equal attachment/session keys still need a collision-safe namespace") | ||
| 362 | - | d.releaseGraphicsNamespace(first) | 362 | + | d.releaseGraphicsNamespaceLeaseLocked(&graphicsOutputState{namespaceBase: first, namespaceFence: firstFence}) |
| 363 | - | d.mu.Lock() | 363 | + | reused, _ := d.reserveGraphicsNamespaceLeaseLocked("session:work:attachment") |
| 364 | - | reused := d.reserveGraphicsNamespaceLocked("session:work:attachment") | |||
| 365 | - | d.mu.Unlock() | |||
| 366 | require.Equal(t, first, reused, "released namespaces should deterministically return to their preferred block") | 364 | require.Equal(t, first, reused, "released namespaces should deterministically return to their preferred block") | ||
| 367 | } | 365 | } | ||
| 368 | 366 | ||||
M internal/usecase/daemon/pane_titles.go +1 -1
| @@ -187,7 +187,7 @@ func (d *Daemon) refreshPaneDisplayTitle(_ *session, p *pane, force bool) string | |||
| 187 | 187 | // distinct from the display fallback: an OSC title must then stand alone, | |
| 188 | 188 | // and future floating panes can supply their command fallback at format time. | |
| 189 | 189 | processName := "" | |
| 190 | - | if d.hasProcessDetails() && p.pty != nil { | |
| 190 | + | if d.proc != nil && p.pty != nil { | |
| 191 | 191 | if pgid, err := p.pty.ForegroundPgid(); err == nil && pgid > 0 { | |
| 192 | 192 | if comm, err := d.proc.Comm(pgid); err == nil && strings.TrimSpace(comm) != "" { | |
| 193 | 193 | processName = strings.TrimSpace(comm) | |
| @@ -187,7 +187,7 @@ func (d *Daemon) refreshPaneDisplayTitle(_ *session, p *pane, force bool) string | |||||
| 187 | // distinct from the display fallback: an OSC title must then stand alone, | 187 | // distinct from the display fallback: an OSC title must then stand alone, | ||
| 188 | // and future floating panes can supply their command fallback at format time. | 188 | // and future floating panes can supply their command fallback at format time. | ||
| 189 | processName := "" | 189 | processName := "" | ||
| 190 | - | if d.hasProcessDetails() && p.pty != nil { | 190 | + | if d.proc != nil && p.pty != nil { |
| 191 | if pgid, err := p.pty.ForegroundPgid(); err == nil && pgid > 0 { | 191 | if pgid, err := p.pty.ForegroundPgid(); err == nil && pgid > 0 { | ||
| 192 | if comm, err := d.proc.Comm(pgid); err == nil && strings.TrimSpace(comm) != "" { | 192 | if comm, err := d.proc.Comm(pgid); err == nil && strings.TrimSpace(comm) != "" { | ||
| 193 | processName = strings.TrimSpace(comm) | 193 | processName = strings.TrimSpace(comm) | ||
M internal/usecase/daemon/pane_titles_test.go +2 -0
| @@ -205,6 +205,8 @@ func TestRefreshPaneTitleLookupFailureKeepsProcessNameEmpty(t *testing.T) { | |||
| 205 | 205 | require.Equal(t, "sh", d.refreshPaneDisplayTitle(sess, p, true), "pointer refresh must retain its pane-owned fallback") | |
| 206 | 206 | } | |
| 207 | 207 | ||
| 208 | + | var errProcessInspectionUnavailable = errors.New("process inspection unavailable") | |
| 209 | + | ||
| 208 | 210 | // processInspectorFake is a hand-written ports.ProcessInspector whose | |
| 209 | 211 | // behavior is set per test through func fields; an unset func reports an | |
| 210 | 212 | // error, the same as an inspector that cannot read the process. | |
| @@ -205,6 +205,8 @@ func TestRefreshPaneTitleLookupFailureKeepsProcessNameEmpty(t *testing.T) { | |||||
| 205 | require.Equal(t, "sh", d.refreshPaneDisplayTitle(sess, p, true), "pointer refresh must retain its pane-owned fallback") | 205 | require.Equal(t, "sh", d.refreshPaneDisplayTitle(sess, p, true), "pointer refresh must retain its pane-owned fallback") | ||
| 206 | } | 206 | } | ||
| 207 | 207 | ||||
| 208 | + | var errProcessInspectionUnavailable = errors.New("process inspection unavailable") | |||
| 209 | + | ||||
| 208 | // processInspectorFake is a hand-written ports.ProcessInspector whose | 210 | // processInspectorFake is a hand-written ports.ProcessInspector whose | ||
| 209 | // behavior is set per test through func fields; an unset func reports an | 211 | // behavior is set per test through func fields; an unset func reports an | ||
| 210 | // error, the same as an inspector that cannot read the process. | 212 | // error, the same as an inspector that cannot read the process. | ||
D internal/usecase/daemon/process_inspector.go +0 -58
| @@ -1,58 +0,0 @@ | |||
| 1 | - | package daemon | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "errors" | |
| 5 | - | ||
| 6 | - | "github.com/bnema/vev/internal/ports" | |
| 7 | - | ) | |
| 8 | - | ||
| 9 | - | var errProcessInspectionUnavailable = errors.New("process inspection unavailable") | |
| 10 | - | ||
| 11 | - | // cwdOverrideInspector replaces only Cwd on top of an optional base inspector. | |
| 12 | - | // With a nil base, Comm, Argv, and GroupArgv report the inspection as | |
| 13 | - | // unavailable, matching a daemon that was given only a cwd reader. | |
| 14 | - | type cwdOverrideInspector struct { | |
| 15 | - | base ports.ProcessInspector | |
| 16 | - | cwd func(int) (string, error) | |
| 17 | - | } | |
| 18 | - | ||
| 19 | - | func (i cwdOverrideInspector) Cwd(pid int) (string, error) { return i.cwd(pid) } | |
| 20 | - | ||
| 21 | - | func (i cwdOverrideInspector) Comm(pid int) (string, error) { | |
| 22 | - | if i.base == nil { | |
| 23 | - | return "", errProcessInspectionUnavailable | |
| 24 | - | } | |
| 25 | - | return i.base.Comm(pid) | |
| 26 | - | } | |
| 27 | - | ||
| 28 | - | func (i cwdOverrideInspector) Argv(pid int) ([]string, error) { | |
| 29 | - | if i.base == nil { | |
| 30 | - | return nil, errProcessInspectionUnavailable | |
| 31 | - | } | |
| 32 | - | return i.base.Argv(pid) | |
| 33 | - | } | |
| 34 | - | ||
| 35 | - | func (i cwdOverrideInspector) GroupArgv(pgid, shellPid int) ([]string, error) { | |
| 36 | - | if i.base == nil { | |
| 37 | - | return nil, errProcessInspectionUnavailable | |
| 38 | - | } | |
| 39 | - | return i.base.GroupArgv(pgid, shellPid) | |
| 40 | - | } | |
| 41 | - | ||
| 42 | - | // unwrapCwdOverride returns the inspector beneath a previous cwd override so | |
| 43 | - | // repeated overrides do not stack. | |
| 44 | - | func unwrapCwdOverride(ins ports.ProcessInspector) ports.ProcessInspector { | |
| 45 | - | if o, ok := ins.(cwdOverrideInspector); ok { | |
| 46 | - | return o.base | |
| 47 | - | } | |
| 48 | - | return ins | |
| 49 | - | } | |
| 50 | - | ||
| 51 | - | // hasProcessDetails reports whether Comm, Argv, and GroupArgv are backed by a | |
| 52 | - | // real inspector rather than a cwd-only override. | |
| 53 | - | func (d *Daemon) hasProcessDetails() bool { | |
| 54 | - | if o, ok := d.proc.(cwdOverrideInspector); ok { | |
| 55 | - | return o.base != nil | |
| 56 | - | } | |
| 57 | - | return d.proc != nil | |
| 58 | - | } | |
| @@ -1,58 +0,0 @@ | |||||
| 1 | - | package daemon | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "errors" | |||
| 5 | - | ||||
| 6 | - | "github.com/bnema/vev/internal/ports" | |||
| 7 | - | ) | |||
| 8 | - | ||||
| 9 | - | var errProcessInspectionUnavailable = errors.New("process inspection unavailable") | |||
| 10 | - | ||||
| 11 | - | // cwdOverrideInspector replaces only Cwd on top of an optional base inspector. | |||
| 12 | - | // With a nil base, Comm, Argv, and GroupArgv report the inspection as | |||
| 13 | - | // unavailable, matching a daemon that was given only a cwd reader. | |||
| 14 | - | type cwdOverrideInspector struct { | |||
| 15 | - | base ports.ProcessInspector | |||
| 16 | - | cwd func(int) (string, error) | |||
| 17 | - | } | |||
| 18 | - | ||||
| 19 | - | func (i cwdOverrideInspector) Cwd(pid int) (string, error) { return i.cwd(pid) } | |||
| 20 | - | ||||
| 21 | - | func (i cwdOverrideInspector) Comm(pid int) (string, error) { | |||
| 22 | - | if i.base == nil { | |||
| 23 | - | return "", errProcessInspectionUnavailable | |||
| 24 | - | } | |||
| 25 | - | return i.base.Comm(pid) | |||
| 26 | - | } | |||
| 27 | - | ||||
| 28 | - | func (i cwdOverrideInspector) Argv(pid int) ([]string, error) { | |||
| 29 | - | if i.base == nil { | |||
| 30 | - | return nil, errProcessInspectionUnavailable | |||
| 31 | - | } | |||
| 32 | - | return i.base.Argv(pid) | |||
| 33 | - | } | |||
| 34 | - | ||||
| 35 | - | func (i cwdOverrideInspector) GroupArgv(pgid, shellPid int) ([]string, error) { | |||
| 36 | - | if i.base == nil { | |||
| 37 | - | return nil, errProcessInspectionUnavailable | |||
| 38 | - | } | |||
| 39 | - | return i.base.GroupArgv(pgid, shellPid) | |||
| 40 | - | } | |||
| 41 | - | ||||
| 42 | - | // unwrapCwdOverride returns the inspector beneath a previous cwd override so | |||
| 43 | - | // repeated overrides do not stack. | |||
| 44 | - | func unwrapCwdOverride(ins ports.ProcessInspector) ports.ProcessInspector { | |||
| 45 | - | if o, ok := ins.(cwdOverrideInspector); ok { | |||
| 46 | - | return o.base | |||
| 47 | - | } | |||
| 48 | - | return ins | |||
| 49 | - | } | |||
| 50 | - | ||||
| 51 | - | // hasProcessDetails reports whether Comm, Argv, and GroupArgv are backed by a | |||
| 52 | - | // real inspector rather than a cwd-only override. | |||
| 53 | - | func (d *Daemon) hasProcessDetails() bool { | |||
| 54 | - | if o, ok := d.proc.(cwdOverrideInspector); ok { | |||
| 55 | - | return o.base != nil | |||
| 56 | - | } | |||
| 57 | - | return d.proc != nil | |||
| 58 | - | } | |||
D internal/usecase/daemon/process_inspector_test.go +0 -83
| @@ -1,83 +0,0 @@ | |||
| 1 | - | package daemon | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "testing" | |
| 5 | - | ||
| 6 | - | "github.com/stretchr/testify/require" | |
| 7 | - | ) | |
| 8 | - | ||
| 9 | - | func TestProcessInspectorOptionsComposition(t *testing.T) { | |
| 10 | - | cwd := func(int) (string, error) { return "/override", nil } | |
| 11 | - | base := &processInspectorFake{ | |
| 12 | - | cwd: func(int) (string, error) { return "/base", nil }, | |
| 13 | - | comm: func(int) (string, error) { return "vim", nil }, | |
| 14 | - | } | |
| 15 | - | ||
| 16 | - | tests := []struct { | |
| 17 | - | name string | |
| 18 | - | opts []Option | |
| 19 | - | wantDetails bool | |
| 20 | - | wantCwd string | |
| 21 | - | wantComm string | |
| 22 | - | wantCommErr error | |
| 23 | - | }{ | |
| 24 | - | {name: "none", wantDetails: false}, | |
| 25 | - | { | |
| 26 | - | name: "cwd reader only", | |
| 27 | - | opts: []Option{WithCwdReader(cwd)}, | |
| 28 | - | wantDetails: false, | |
| 29 | - | wantCwd: "/override", | |
| 30 | - | wantCommErr: errProcessInspectionUnavailable, | |
| 31 | - | }, | |
| 32 | - | { | |
| 33 | - | name: "inspector only", | |
| 34 | - | opts: []Option{WithProcessInspector(base)}, | |
| 35 | - | wantDetails: true, | |
| 36 | - | wantCwd: "/base", | |
| 37 | - | wantComm: "vim", | |
| 38 | - | }, | |
| 39 | - | { | |
| 40 | - | name: "inspector then cwd reader keeps base details", | |
| 41 | - | opts: []Option{WithProcessInspector(base), WithCwdReader(cwd)}, | |
| 42 | - | wantDetails: true, | |
| 43 | - | wantCwd: "/override", | |
| 44 | - | wantComm: "vim", | |
| 45 | - | }, | |
| 46 | - | { | |
| 47 | - | name: "cwd reader then inspector replaces override", | |
| 48 | - | opts: []Option{WithCwdReader(cwd), WithProcessInspector(base)}, | |
| 49 | - | wantDetails: true, | |
| 50 | - | wantCwd: "/base", | |
| 51 | - | wantComm: "vim", | |
| 52 | - | }, | |
| 53 | - | { | |
| 54 | - | name: "repeated cwd readers do not stack", | |
| 55 | - | opts: []Option{WithProcessInspector(base), WithCwdReader(cwd), WithCwdReader(cwd)}, | |
| 56 | - | wantDetails: true, | |
| 57 | - | wantCwd: "/override", | |
| 58 | - | wantComm: "vim", | |
| 59 | - | }, | |
| 60 | - | } | |
| 61 | - | for _, tt := range tests { | |
| 62 | - | t.Run(tt.name, func(t *testing.T) { | |
| 63 | - | d := &Daemon{} | |
| 64 | - | for _, opt := range tt.opts { | |
| 65 | - | opt(d) | |
| 66 | - | } | |
| 67 | - | require.Equal(t, tt.wantDetails, d.hasProcessDetails()) | |
| 68 | - | if d.proc == nil { | |
| 69 | - | return | |
| 70 | - | } | |
| 71 | - | got, err := d.proc.Cwd(1) | |
| 72 | - | require.NoError(t, err) | |
| 73 | - | require.Equal(t, tt.wantCwd, got) | |
| 74 | - | comm, err := d.proc.Comm(1) | |
| 75 | - | require.ErrorIs(t, err, tt.wantCommErr) | |
| 76 | - | require.Equal(t, tt.wantComm, comm) | |
| 77 | - | if o, ok := d.proc.(cwdOverrideInspector); ok { | |
| 78 | - | _, nested := o.base.(cwdOverrideInspector) | |
| 79 | - | require.False(t, nested) | |
| 80 | - | } | |
| 81 | - | }) | |
| 82 | - | } | |
| 83 | - | } | |
| @@ -1,83 +0,0 @@ | |||||
| 1 | - | package daemon | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "testing" | |||
| 5 | - | ||||
| 6 | - | "github.com/stretchr/testify/require" | |||
| 7 | - | ) | |||
| 8 | - | ||||
| 9 | - | func TestProcessInspectorOptionsComposition(t *testing.T) { | |||
| 10 | - | cwd := func(int) (string, error) { return "/override", nil } | |||
| 11 | - | base := &processInspectorFake{ | |||
| 12 | - | cwd: func(int) (string, error) { return "/base", nil }, | |||
| 13 | - | comm: func(int) (string, error) { return "vim", nil }, | |||
| 14 | - | } | |||
| 15 | - | ||||
| 16 | - | tests := []struct { | |||
| 17 | - | name string | |||
| 18 | - | opts []Option | |||
| 19 | - | wantDetails bool | |||
| 20 | - | wantCwd string | |||
| 21 | - | wantComm string | |||
| 22 | - | wantCommErr error | |||
| 23 | - | }{ | |||
| 24 | - | {name: "none", wantDetails: false}, | |||
| 25 | - | { | |||
| 26 | - | name: "cwd reader only", | |||
| 27 | - | opts: []Option{WithCwdReader(cwd)}, | |||
| 28 | - | wantDetails: false, | |||
| 29 | - | wantCwd: "/override", | |||
| 30 | - | wantCommErr: errProcessInspectionUnavailable, | |||
| 31 | - | }, | |||
| 32 | - | { | |||
| 33 | - | name: "inspector only", | |||
| 34 | - | opts: []Option{WithProcessInspector(base)}, | |||
| 35 | - | wantDetails: true, | |||
| 36 | - | wantCwd: "/base", | |||
| 37 | - | wantComm: "vim", | |||
| 38 | - | }, | |||
| 39 | - | { | |||
| 40 | - | name: "inspector then cwd reader keeps base details", | |||
| 41 | - | opts: []Option{WithProcessInspector(base), WithCwdReader(cwd)}, | |||
| 42 | - | wantDetails: true, | |||
| 43 | - | wantCwd: "/override", | |||
| 44 | - | wantComm: "vim", | |||
| 45 | - | }, | |||
| 46 | - | { | |||
| 47 | - | name: "cwd reader then inspector replaces override", | |||
| 48 | - | opts: []Option{WithCwdReader(cwd), WithProcessInspector(base)}, | |||
| 49 | - | wantDetails: true, | |||
| 50 | - | wantCwd: "/base", | |||
| 51 | - | wantComm: "vim", | |||
| 52 | - | }, | |||
| 53 | - | { | |||
| 54 | - | name: "repeated cwd readers do not stack", | |||
| 55 | - | opts: []Option{WithProcessInspector(base), WithCwdReader(cwd), WithCwdReader(cwd)}, | |||
| 56 | - | wantDetails: true, | |||
| 57 | - | wantCwd: "/override", | |||
| 58 | - | wantComm: "vim", | |||
| 59 | - | }, | |||
| 60 | - | } | |||
| 61 | - | for _, tt := range tests { | |||
| 62 | - | t.Run(tt.name, func(t *testing.T) { | |||
| 63 | - | d := &Daemon{} | |||
| 64 | - | for _, opt := range tt.opts { | |||
| 65 | - | opt(d) | |||
| 66 | - | } | |||
| 67 | - | require.Equal(t, tt.wantDetails, d.hasProcessDetails()) | |||
| 68 | - | if d.proc == nil { | |||
| 69 | - | return | |||
| 70 | - | } | |||
| 71 | - | got, err := d.proc.Cwd(1) | |||
| 72 | - | require.NoError(t, err) | |||
| 73 | - | require.Equal(t, tt.wantCwd, got) | |||
| 74 | - | comm, err := d.proc.Comm(1) | |||
| 75 | - | require.ErrorIs(t, err, tt.wantCommErr) | |||
| 76 | - | require.Equal(t, tt.wantComm, comm) | |||
| 77 | - | if o, ok := d.proc.(cwdOverrideInspector); ok { | |||
| 78 | - | _, nested := o.base.(cwdOverrideInspector) | |||
| 79 | - | require.False(t, nested) | |||
| 80 | - | } | |||
| 81 | - | }) | |||
| 82 | - | } | |||
| 83 | - | } | |||
M internal/usecase/daemon/session_test.go +2 -2
| @@ -1336,7 +1336,7 @@ func TestRefreshSessionCwdTouchesOnlyOnChange(t *testing.T) { | |||
| 1336 | 1336 | d := newTestDaemon(t, newFactory(t, p), stubClock{}) | |
| 1337 | 1337 | WithStore(t, store)(d) | |
| 1338 | 1338 | cwd := "/tmp/work" | |
| 1339 | - | WithCwdReader(func(int) (string, error) { return cwd, nil })(d) | |
| 1339 | + | WithProcessInspector(&processInspectorFake{cwd: func(int) (string, error) { return cwd, nil }})(d) | |
| 1340 | 1340 | ||
| 1341 | 1341 | sess, err := createSessionForTest(d, "work", false, "/tmp/work", sz, terminalEnv{}, d.baseEnv) | |
| 1342 | 1342 | require.NoError(t, err) | |
| @@ -1529,7 +1529,7 @@ func TestNaturalExitStoppedButExplicitKillPurges(t *testing.T) { | |||
| 1529 | 1529 | store, state := newMockStore(t) | |
| 1530 | 1530 | d := newTestDaemon(t, newFactorySeq(t, p1, p2), stubClock{}) | |
| 1531 | 1531 | WithStore(t, store)(d) | |
| 1532 | - | WithCwdReader(func(int) (string, error) { return "/tmp/latest", nil })(d) | |
| 1532 | + | WithProcessInspector(&processInspectorFake{cwd: func(int) (string, error) { return "/tmp/latest", nil }})(d) | |
| 1533 | 1533 | ||
| 1534 | 1534 | natural, err := createSessionForTest(d, "natural", false, "/tmp/old", sz, terminalEnv{}, d.baseEnv) | |
| 1535 | 1535 | require.NoError(t, err) | |
| @@ -1336,7 +1336,7 @@ func TestRefreshSessionCwdTouchesOnlyOnChange(t *testing.T) { | |||||
| 1336 | d := newTestDaemon(t, newFactory(t, p), stubClock{}) | 1336 | d := newTestDaemon(t, newFactory(t, p), stubClock{}) | ||
| 1337 | WithStore(t, store)(d) | 1337 | WithStore(t, store)(d) | ||
| 1338 | cwd := "/tmp/work" | 1338 | cwd := "/tmp/work" | ||
| 1339 | - | WithCwdReader(func(int) (string, error) { return cwd, nil })(d) | 1339 | + | WithProcessInspector(&processInspectorFake{cwd: func(int) (string, error) { return cwd, nil }})(d) |
| 1340 | 1340 | ||||
| 1341 | sess, err := createSessionForTest(d, "work", false, "/tmp/work", sz, terminalEnv{}, d.baseEnv) | 1341 | sess, err := createSessionForTest(d, "work", false, "/tmp/work", sz, terminalEnv{}, d.baseEnv) | ||
| 1342 | require.NoError(t, err) | 1342 | require.NoError(t, err) | ||
| @@ -1529,7 +1529,7 @@ func TestNaturalExitStoppedButExplicitKillPurges(t *testing.T) { | |||||
| 1529 | store, state := newMockStore(t) | 1529 | store, state := newMockStore(t) | ||
| 1530 | d := newTestDaemon(t, newFactorySeq(t, p1, p2), stubClock{}) | 1530 | d := newTestDaemon(t, newFactorySeq(t, p1, p2), stubClock{}) | ||
| 1531 | WithStore(t, store)(d) | 1531 | WithStore(t, store)(d) | ||
| 1532 | - | WithCwdReader(func(int) (string, error) { return "/tmp/latest", nil })(d) | 1532 | + | WithProcessInspector(&processInspectorFake{cwd: func(int) (string, error) { return "/tmp/latest", nil }})(d) |
| 1533 | 1533 | ||||
| 1534 | natural, err := createSessionForTest(d, "natural", false, "/tmp/old", sz, terminalEnv{}, d.baseEnv) | 1534 | natural, err := createSessionForTest(d, "natural", false, "/tmp/old", sz, terminalEnv{}, d.baseEnv) | ||
| 1535 | require.NoError(t, err) | 1535 | require.NoError(t, err) | ||
M internal/usecase/daemon/snapshot_capture.go +1 -1
| @@ -82,7 +82,7 @@ func (d *Daemon) captureSession(sess *session) bool { | |||
| 82 | 82 | } | |
| 83 | 83 | ||
| 84 | 84 | func (d *Daemon) capturePaneProcess(pty interface{ ForegroundPgid() (int, error) }, shellPid int) *snapcodec.Process { | |
| 85 | - | if d == nil || pty == nil || shellPid <= 0 || !d.hasProcessDetails() { | |
| 85 | + | if d == nil || pty == nil || shellPid <= 0 || d.proc == nil { | |
| 86 | 86 | return nil | |
| 87 | 87 | } | |
| 88 | 88 | pgid, err := pty.ForegroundPgid() | |
| @@ -82,7 +82,7 @@ func (d *Daemon) captureSession(sess *session) bool { | |||||
| 82 | } | 82 | } | ||
| 83 | 83 | ||||
| 84 | func (d *Daemon) capturePaneProcess(pty interface{ ForegroundPgid() (int, error) }, shellPid int) *snapcodec.Process { | 84 | func (d *Daemon) capturePaneProcess(pty interface{ ForegroundPgid() (int, error) }, shellPid int) *snapcodec.Process { | ||
| 85 | - | if d == nil || pty == nil || shellPid <= 0 || !d.hasProcessDetails() { | 85 | + | if d == nil || pty == nil || shellPid <= 0 || d.proc == nil { |
| 86 | return nil | 86 | return nil | ||
| 87 | } | 87 | } | ||
| 88 | pgid, err := pty.ForegroundPgid() | 88 | pgid, err := pty.ForegroundPgid() | ||