plugin: magpie plugin update in a terminal restarts the app's plugin host (#952, mxzs123) — the running magpie noticed another magpie's plugin change only by plugins.json, which an update leaves as it was (the spec stays, bun installs a newer version), so the app's host went on answering with the old code (0.1.17 after the update to 0.1.18, still 403) until it was killed. The stamp checkList compares now also covers what is installed: Dir()'s package.json and bun's lockfile; and Restart takes the stamp as it is, so this magpie's own install isn't taken for another's and restarted twice. In a sandbox (serve on 3476 under its own HOME, the Factory plugin at 0.1.17, unpinned spec; a request, then magpie plugin update in another process, then a request): on 2d85a086 the host process stayed the same (58657) with 0.1.18 installed; with this change it was replaced (60004 → 60583) and the old one stopped. TestTerminalUpdateRestartsTheHost is new and fails without the change; go vet and go test -race -tags nogui ./internal/plugin/ ./internal/provider/ under a temp HOME pass.

This commit is contained in:
yetone committed 2026-10-06 08:47:59 +08:00
1 parent 2d85a08635
commit 2bd49cc545
3 files changed
+87 -6

No files matched your search

+5
View File
@@ -536,6 +536,11 @@ func changed() {
// waiting for its browser) finishes them first, while the calls made
// from now on go to the new one: a plugin updating never cuts a reply.
func Restart() {
// the next host loads the plugins as they are now: what this magpie
// installed is not another magpie's change (checkList)
listSeen.Lock()
listSeen.stamp, listSeen.set = listStamp(), true
listSeen.Unlock()
hostMu.Lock()
h := current
current = nil
+13 -6
View File
@@ -224,17 +224,24 @@ var listSeen struct {
set bool
}
// listStamp is plugins.json as it is now, and what is installed: bun's
// package.json and lockfile in Dir(), which `magpie plugin update` changes
// without touching plugins.json (#952: a terminal's update to 0.1.18 left
// the app's host answering with 0.1.17 until it was killed).
func listStamp() string {
fi, err := os.Stat(listPath())
if err != nil {
return ""
var b strings.Builder
for _, p := range []string{listPath(), filepath.Join(Dir(), "package.json"), filepath.Join(Dir(), "bun.lock"), filepath.Join(Dir(), "bun.lockb")} {
if fi, err := os.Stat(p); err == nil {
fmt.Fprint(&b, fi.ModTime().UnixNano(), " ", fi.Size())
}
b.WriteString(";")
}
return fmt.Sprint(fi.ModTime().UnixNano(), fi.Size())
return b.String()
}
// hostStale is set when another magpie changed the plugins (magpie plugin
// add, remove or move in a terminal while the app runs): the host running
// has the old ones loaded.
// add, remove, update or move in a terminal while the app runs): the host
// running has the old ones loaded.
var hostStale atomic.Bool
// checkList notices plugins.json changed by another magpie: the host is
+69
View File
@@ -0,0 +1,69 @@
package plugin
import (
"os"
"path/filepath"
"testing"
"time"
)
// TestTerminalUpdateRestartsTheHost (#952): `magpie plugin update` in a
// terminal installs a newer version without changing plugins.json, so the
// app running beside it went on with the host it had (0.1.17's code after
// the update to 0.1.18) until that was killed. What bun installs in Dir()
// counts as a change another magpie made; this magpie's own restart does
// not.
func TestTerminalUpdateRestartsTheHost(t *testing.T) {
storeSandbox(t)
writeList(t, `{"plugins":[{"spec":"@magpie-community/opencode-factory-auth"}]}`)
if err := os.MkdirAll(Dir(), 0o755); err != nil {
t.Fatal(err)
}
pkg := filepath.Join(Dir(), "package.json")
lock := filepath.Join(Dir(), "bun.lock")
write := func(p, body string, at time.Time) {
t.Helper()
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
if err := os.Chtimes(p, at, at); err != nil {
t.Fatal(err)
}
}
then := time.Now().Add(-time.Hour)
write(pkg, `{"dependencies":{"@magpie-community/opencode-factory-auth":"0.1.17"}}`, then)
write(lock, `"@magpie-community/opencode-factory-auth@0.1.17"`, then)
prevStale := hostStale.Swap(false)
t.Cleanup(func() {
if prevStale {
hostStale.Store(true)
}
})
checkList() // the app has seen the plugins as they are
hostStale.Store(false)
checkList()
if hostStale.Load() {
t.Fatal("nothing changed, and the host is stale")
}
// the terminal's update: bun rewrites both, plugins.json is as it was
list, _ := os.ReadFile(listPath())
write(pkg, `{"dependencies":{"@magpie-community/opencode-factory-auth":"0.1.18"}}`, then.Add(time.Minute))
write(lock, `"@magpie-community/opencode-factory-auth@0.1.18"`, then.Add(time.Minute))
if now, _ := os.ReadFile(listPath()); string(now) != string(list) {
t.Fatal("plugins.json changed")
}
checkList()
if !hostStale.Swap(false) {
t.Fatal("the plugins installed changed, and the running host is not stale: it goes on with the old version")
}
// this magpie's own install restarts the host itself: no second restart
write(lock, `"@magpie-community/opencode-factory-auth@0.1.19"`, then.Add(2*time.Minute))
Restart()
checkList()
if hostStale.Load() {
t.Fatal("this magpie's own restart was taken for another magpie's change")
}
}