Skip to content

Commit 29bd48b

Browse files
fix(cache): rehash PATH per command lookup for the serve daemon)
1 parent c94c27b commit 29bd48b

2 files changed

Lines changed: 29 additions & 5 deletions

File tree

src/cache/command.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package cache
33
import (
44
"hash/fnv"
55
"os"
6-
"sync"
76

87
"github.com/jandedobbeleer/oh-my-posh/src/maps"
98
)
@@ -14,15 +13,19 @@ const commandPathKeyPrefix = "command_path_"
1413

1514
// pathEnvHash returns an FNV-1a hash of the environment that determines how
1615
// exec.LookPath resolves a command: PATH, plus PATHEXT on Windows (unset and
17-
// therefore a no-op elsewhere). It's computed once per process -- the
18-
// environment can't change under us in a short-lived prompt render.
19-
var pathEnvHash = sync.OnceValue(func() uint64 {
16+
// therefore a no-op elsewhere). Computed on every call, NOT memoized: the
17+
// serve daemon lives across prompts and applies each request's env overlay
18+
// (PATH included) in-process, so a per-process hash would keep matching
19+
// entries persisted under an earlier PATH and pin e.g. `python` to a
20+
// previous virtual environment's interpreter for the daemon's lifetime -
21+
// the old binary still exists, so the os.Stat revalidation never catches it.
22+
func pathEnvHash() uint64 {
2023
h := fnv.New64a()
2124
_, _ = h.Write([]byte(os.Getenv("PATH")))
2225
_, _ = h.Write([]byte{0})
2326
_, _ = h.Write([]byte(os.Getenv("PATHEXT")))
2427
return h.Sum64()
25-
})
28+
}
2629

2730
// Command lookup TTLs for the session-persisted layer (L2). The in-memory
2831
// layer (L1, Commands below) is unbounded for the lifetime of the process,

src/cache/command_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,27 @@ func TestGetPersistedCommandPathStalePathHashIsAMiss(t *testing.T) {
4848
assert.False(t, ok, "entry persisted under a different PATH must be treated as a miss")
4949
}
5050

51+
func TestGetPersistedCommandPathInProcessPathChangeIsAMiss(t *testing.T) {
52+
session = Session.new()
53+
54+
// The serve daemon applies each render request's env overlay (PATH
55+
// included) in-process, so the hash must track the CURRENT environment:
56+
// an entry persisted before a venv activation changed PATH must not be
57+
// served afterwards, even within the same process.
58+
t.Setenv("PATH", "/project-a/.venv/bin")
59+
PersistCommandPath("python", "/project-a/.venv/bin/python", true)
60+
61+
path, found, ok := GetPersistedCommandPath("python")
62+
assert.True(t, ok)
63+
assert.True(t, found)
64+
assert.Equal(t, "/project-a/.venv/bin/python", path)
65+
66+
t.Setenv("PATH", "/project-b/.venv/bin")
67+
68+
_, _, ok = GetPersistedCommandPath("python")
69+
assert.False(t, ok, "entry persisted under a previous PATH must be a miss after PATH changes in-process")
70+
}
71+
5172
func TestCommandPathKeyIsPrefixed(t *testing.T) {
5273
assert.Equal(t, "command_path_git", commandPathKey("git"))
5374
}

0 commit comments

Comments
 (0)