From 2bd49cc5459c6d801ca90b4c86cb631116fbd2e2 Mon Sep 17 00:00:00 2001 From: yetone Date: Tue, 6 Oct 2026 08:47:59 +0800 Subject: [PATCH] =?UTF-8?q?plugin:=20`magpie=20plugin=20update`=20in=20a?= =?UTF-8?q?=20terminal=20restarts=20the=20app's=20plugin=20host=20(#952,?= =?UTF-8?q?=20mxzs123)=20=E2=80=94=20the=20running=20magpie=20noticed=20an?= =?UTF-8?q?other=20magpie's=20plugin=20change=20only=20by=20plugins.json,?= =?UTF-8?q?=20which=20an=20update=20leaves=20as=20it=20was=20(the=20spec?= =?UTF-8?q?=20stays,=20bun=20installs=20a=20newer=20version),=20so=20the?= =?UTF-8?q?=20app's=20host=20went=20on=20answering=20with=20the=20old=20co?= =?UTF-8?q?de=20(0.1.17=20after=20the=20update=20to=200.1.18,=20still=2040?= =?UTF-8?q?3)=20until=20it=20was=20killed.=20The=20stamp=20checkList=20com?= =?UTF-8?q?pares=20now=20also=20covers=20what=20is=20installed:=20Dir()'s?= =?UTF-8?q?=20package.json=20and=20bun's=20lockfile;=20and=20Restart=20tak?= =?UTF-8?q?es=20the=20stamp=20as=20it=20is,=20so=20this=20magpie's=20own?= =?UTF-8?q?=20install=20isn't=20taken=20for=20another's=20and=20restarted?= =?UTF-8?q?=20twice.=20In=20a=20sandbox=20(serve=20on=203476=20under=20its?= =?UTF-8?q?=20own=20HOME,=20the=20Factory=20plugin=20at=200.1.17,=20unpinn?= =?UTF-8?q?ed=20spec;=20a=20request,=20then=20`magpie=20plugin=20update`?= =?UTF-8?q?=20in=20another=20process,=20then=20a=20request):=20on=202d85a0?= =?UTF-8?q?86=20the=20host=20process=20stayed=20the=20same=20(58657)=20wit?= =?UTF-8?q?h=200.1.18=20installed;=20with=20this=20change=20it=20was=20rep?= =?UTF-8?q?laced=20(60004=20=E2=86=92=2060583)=20and=20the=20old=20one=20s?= =?UTF-8?q?topped.=20TestTerminalUpdateRestartsTheHost=20is=20new=20and=20?= =?UTF-8?q?fails=20without=20the=20change;=20go=20vet=20and=20go=20test=20?= =?UTF-8?q?-race=20-tags=20nogui=20./internal/plugin/=20./internal/provide?= =?UTF-8?q?r/=20under=20a=20temp=20HOME=20pass.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/plugin/host.go | 5 ++ internal/plugin/store.go | 19 ++++--- internal/plugin/update_restart_test.go | 69 ++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 internal/plugin/update_restart_test.go diff --git a/internal/plugin/host.go b/internal/plugin/host.go index 3b193a2e..49bd7f8f 100644 --- a/internal/plugin/host.go +++ b/internal/plugin/host.go @@ -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 diff --git a/internal/plugin/store.go b/internal/plugin/store.go index 53ee0bdc..ddf71762 100644 --- a/internal/plugin/store.go +++ b/internal/plugin/store.go @@ -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 diff --git a/internal/plugin/update_restart_test.go b/internal/plugin/update_restart_test.go new file mode 100644 index 00000000..4585be10 --- /dev/null +++ b/internal/plugin/update_restart_test.go @@ -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") + } +}