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") + } +}