From 05683637d506b161d52f623b47434f25ff104925 Mon Sep 17 00:00:00 2001 From: yetone Date: Wed, 7 Oct 2026 01:37:23 +0800 Subject: [PATCH] plugins: the Bun the plugin host runs on is kept as magpie-bun, so a proxy app's PROCESS-NAME rule can route the requests plugins (Cursor and the other moved subscriptions) make (#1048) magpie downloads its own Bun into ~/.cache/magpie/bun//. It is now written there as magpie-bun (magpie-bun.exe on Windows), and a Bun downloaded before is renamed in place the first time it is looked for, so nothing is downloaded again and no second copy takes disk. The host, bun add and bun update all run it, so all of their traffic carries the one name. A $MAGPIE_BUN the user set is run as it is. Verified: TestBunDownloadedAsMagpieBun, TestBunDownloadedBeforeTakesTheName and TestHostRunsAsMagpieBun fail without the change (the Bun is .../bun, the host runs .../bun). TestHostRunsAsMagpieBun starts the real host on a copy of Bun 1.4.2 and `ps -o comm=` names it magpie-bun on macOS. On the Windows box the three tests pass with the Windows build of Bun 1.4.2, a running bun.exe renames to magpie-bun.exe, and Get-Process/Win32_Process name it magpie-bun. Linux not run here (CI's ubuntu job runs the test with Bun on PATH). go test -tags nogui ./... green; -race -count=10 on the new tests green. --- docs/reference.md | 4 +- internal/plugin/bun.go | 2 +- internal/plugin/bunname_test.go | 123 ++++++++++++++++++++++++++++++++ internal/plugin/bunupdate.go | 15 +++- 4 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 internal/plugin/bunname_test.go diff --git a/docs/reference.md b/docs/reference.md index 4f5c137b..0905bd27 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -1674,7 +1674,9 @@ MAGPIE_GITHUB_MIRROR=https://gh.example magpie serve - `~/.config/magpie/stash.json` — values magpie replaced, restored on switch-back - `~/.config/magpie/plugins.json`, `plugins/` — the plugins added, and their packages - `~/.config/magpie/plugin-auth.json` — the plugins' sign-ins (0600) -- `~/.cache/magpie/bun/` — the Bun plugins run on +- `~/.cache/magpie/bun//magpie-bun` — the Bun plugins run on, named + so that a proxy app's PROCESS-NAME rule can match it (`magpie-bun.exe` on + Windows) - `~/.cache/magpie/models.json` — models.dev catalog (OpenCode's cache at `~/.cache/opencode/models.json` is used when present) - `~/.cache/magpie/models/.json` — model lists fetched from vendors diff --git a/internal/plugin/bun.go b/internal/plugin/bun.go index ca13eddc..77830269 100644 --- a/internal/plugin/bun.go +++ b/internal/plugin/bun.go @@ -100,7 +100,7 @@ func Bun(ctx context.Context) (string, error) { defer bunMu.Unlock() v := inUseLocked() exe := bunExeOf(v) - if _, err := os.Stat(exe); err == nil { + if haveBun(v) { return exe, nil } if err := downloadBun(ctx, v, exe); err != nil { diff --git a/internal/plugin/bunname_test.go b/internal/plugin/bunname_test.go new file mode 100644 index 00000000..b522931f --- /dev/null +++ b/internal/plugin/bunname_test.go @@ -0,0 +1,123 @@ +package plugin + +import ( + "context" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "testing" + "time" +) + +// The Bun magpie downloads is kept as magpie-bun, the name a proxy app's +// PROCESS-NAME rule sees (#1048). +func TestBunDownloadedAsMagpieBun(t *testing.T) { + bunHome(t, BunVersion, time.Now()) + asked := bunReleases(t, []byte("bun")) + exe, err := Bun(context.Background()) + if err != nil { + t.Fatal(err) + } + if filepath.Base(exe) != magpieBun() || asked.Load() != 1 { + t.Fatalf("Bun = %s after %d downloads, want one named %s", exe, asked.Load(), magpieBun()) + } + if _, err := os.Stat(filepath.Join(filepath.Dir(exe), bunExe())); !os.IsNotExist(err) { + t.Fatalf("a bun beside magpie-bun: %v", err) + } +} + +// A Bun downloaded before is renamed magpie-bun, not downloaded again. +func TestBunDownloadedBeforeTakesTheName(t *testing.T) { + bunHome(t, BunVersion, time.Now()) + asked := bunReleases(t, []byte("new download")) + old := filepath.Join(bunDirOf(BunVersion), bunExe()) + if err := os.MkdirAll(filepath.Dir(old), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(old, []byte("downloaded before"), 0o755); err != nil { + t.Fatal(err) + } + if !HasBun() { + t.Fatal("HasBun is false with a bun downloaded before") + } + exe, err := Bun(context.Background()) + if err != nil { + t.Fatal(err) + } + if b, _ := os.ReadFile(exe); filepath.Base(exe) != magpieBun() || string(b) != "downloaded before" || asked.Load() != 0 { + t.Fatalf("Bun = %s holding %q after %d downloads", exe, b, asked.Load()) + } + if _, err := os.Stat(old); !os.IsNotExist(err) { + t.Fatalf("the old bun is still there: %v", err) + } +} + +// The plugin host runs as magpie-bun: the process name the system reports +// for it is magpie-bun, not bun. +func TestHostRunsAsMagpieBun(t *testing.T) { + real, err := exec.LookPath("bun") + if err != nil { + t.Skip("no bun on PATH") + } + bunHome(t, BunVersion, time.Now()) + t.Cleanup(Settle) + // a real Bun as magpie downloaded it before: a copy (not a link, whose + // process would take the target's name) at the old name + old := filepath.Join(bunDirOf(BunVersion), bunExe()) + if err := os.MkdirAll(filepath.Dir(old), 0o755); err != nil { + t.Fatal(err) + } + copyExe(t, real, old) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + abs, _ := filepath.Abs("testdata/fake/index.js") + if _, err := Add(ctx, abs); err != nil { + t.Fatal(err) + } + if ps, err := Providers(ctx); err != nil || len(ps) != 1 { + t.Fatalf("Providers = %+v, %v", ps, err) + } + hostMu.Lock() + h := current + hostMu.Unlock() + if h == nil || h.cmd.Process == nil { + t.Fatal("no host running") + } + if got := filepath.Base(h.cmd.Path); got != magpieBun() { + t.Fatalf("the host runs %s", h.cmd.Path) + } + if runtime.GOOS == "windows" { + return + } + out, err := exec.Command("ps", "-o", "comm=", "-p", strconv.Itoa(h.cmd.Process.Pid)).Output() + if err != nil { + t.Fatal(err) + } + if got := filepath.Base(strings.TrimSpace(string(out))); got != "magpie-bun" { + t.Fatalf("ps says the host is %q", got) + } +} + +func copyExe(t *testing.T, from, to string) { + t.Helper() + in, err := os.Open(from) + if err != nil { + t.Fatal(err) + } + defer in.Close() + out, err := os.OpenFile(to, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755) + if err != nil { + t.Fatal(err) + } + if _, err := io.Copy(out, in); err != nil { + t.Fatal(err) + } + if err := out.Close(); err != nil { + t.Fatal(err) + } +} diff --git a/internal/plugin/bunupdate.go b/internal/plugin/bunupdate.go index e97e6230..62190b77 100644 --- a/internal/plugin/bunupdate.go +++ b/internal/plugin/bunupdate.go @@ -46,7 +46,12 @@ func bunRoot() string { return filepath.Join(filepath.Dir(catalog.CachePath()), func bunDirOf(v string) string { return filepath.Join(bunRoot(), v) } -func bunExeOf(v string) string { return filepath.Join(bunDirOf(v), bunExe()) } +// bunExeOf is where magpie keeps Bun v: named magpie-bun, so a proxy app's +// PROCESS-NAME rule can tell the requests plugins make (and their installs) +// from any other Bun's (#1048). +func bunExeOf(v string) string { return filepath.Join(bunDirOf(v), magpieBun()) } + +func magpieBun() string { return "magpie-" + bunExe() } func bunStatePath() string { return filepath.Join(bunRoot(), "state.json") } @@ -70,7 +75,13 @@ func writeBunState(s bunState) error { } func haveBun(v string) bool { - _, err := os.Stat(bunExeOf(v)) + exe := bunExeOf(v) + if _, err := os.Stat(exe); err == nil { + return true + } + // a Bun downloaded before it was named magpie-bun takes the name + _ = os.Rename(filepath.Join(bunDirOf(v), bunExe()), exe) + _, err := os.Stat(exe) return err == nil }