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/<version>/. 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.
This commit is contained in:
yetone committed 2026-10-07 01:37:31 +08:00
1 parent de2c84ace7
commit 05683637d5
4 files changed
+140 -4

No files matched your search

+3 -1
View File
@@ -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/<version>/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/<provider>.json` — model lists fetched from vendors
+1 -1
View File
@@ -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 {
+123
View File
@@ -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)
}
}
+13 -2
View File
@@ -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
}