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 }