agent, provider: a CLI whose --version fails isn't run again until it changes, and an npm CLI's version is read from its package (#864) — macOS put up 「已阻止恶意软件:未打开 "codex"」 each time magpie's window opened: the codex magpie finds was asked its version every ten minutes (the model list's client version) and again with the Agents page (a failure retried after ten minutes), and macOS stops a program XProtect flags with that alert on every start. proc.Version is the one way both ask now: an npm CLI (…/node_modules/@openai/codex/bin/codex.js) has its package.json's version read and isn't run at all; any other is run with --version, and a run that ran and said nothing, or couldn't start, isn't made again until the binary's path, size or mtime changes (a reinstall); one cut off by the timeout is. A CLI that answered is asked as before. TestCodexVersionNotRerun (a codex that exits 137 run once over three ten-minute refreshes; an npm codex's version read, not run) fails on the old code (run 3 times); TestVersionRunsOnce. TestUpdateCLI and the agent and provider tests pass. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but for TestClientGoneRecordedAsCanceled, which passes alone (-count=3).

This commit is contained in:
yetone committed 2026-10-05 13:24:01 +08:00
1 parent a77883ec91
commit 4183f516bf
5 files changed
+235 -16

No files matched your search

+4 -11
View File
@@ -403,17 +403,10 @@ func installedVersion(bin string) string {
})
}
// runVersion runs bin --version with nothing on its stdin; a var so tests
// can fake it.
var runVersion = func(bin string) string {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cmd := proc.ProbeContext(ctx, bin, "--version")
cmd.Stdin = nil // /dev/null: one that would ask something gets nothing
cmd.Env = append(os.Environ(), "NO_COLOR=1")
out, _ := cmd.CombinedOutput()
return string(out)
}
// runVersion is what bin says its version is (proc.Version: read from its
// npm package, else bin --version, not run again after it failed); a var so
// tests can fake it.
var runVersion = proc.Version
// latestVersion is the newest version where u would update from.
func latestVersion(u *updater) string {
+97
View File
@@ -0,0 +1,97 @@
package proc
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
// Version is what the CLI at bin says its version is: the version in the
// package.json of the npm package it is a file of, read rather than run,
// else what bin --version prints (stdout and stderr, ANSI and all), "" when
// it says nothing. A run that fails other than by taking too long isn't
// made again until the binary changes (#864): macOS stops a program XProtect flags with
// a "contains malware" alert on each start, and a CLI asked its version
// every few minutes put one up every time magpie's window opened. An npm
// CLI isn't run at all: its package says the version its --version would.
func Version(bin string) string {
real, err := filepath.EvalSymlinks(bin)
if err != nil {
real = bin
}
st, err := os.Stat(real)
if err != nil {
return ""
}
if v := npmVersion(real); v != "" {
return v
}
stamp := fmt.Sprint(real, st.Size(), st.ModTime().UnixNano())
versionsFailed.Lock()
failed := versionsFailed.m[bin] == stamp
versionsFailed.Unlock()
if failed {
return ""
}
out, err := runVersion(bin)
// a run cut off by the timeout (a busy machine, a slow first start) is
// made again; one that ran and said nothing, or couldn't start, is not.
// One that answered is asked again as its caller likes: what it says
// may come from a file beside it that an update changes.
if out == "" && !errors.Is(err, context.DeadlineExceeded) {
versionsFailed.Lock()
versionsFailed.m[bin] = stamp
versionsFailed.Unlock()
}
return out
}
// versionsFailed is, for each CLI whose run failed, the binary it was.
var versionsFailed = struct {
sync.Mutex
m map[string]string
}{m: map[string]string{}}
// runVersion runs bin --version with nothing on its stdin; a var so tests
// can count the runs.
var runVersion = func(bin string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cmd := ProbeContext(ctx, bin, "--version")
cmd.Stdin = nil // /dev/null: one that would ask something gets nothing
cmd.Env = append(os.Environ(), "NO_COLOR=1")
out, err := cmd.CombinedOutput()
if ctx.Err() != nil {
return "", ctx.Err()
}
// one that says its version and exits non-zero has still said it
return strings.TrimSpace(string(out)), err
}
// npmVersion is the version of the npm package the file at real is in
// (…/node_modules/@openai/codex/bin/codex.js is in @openai/codex), "" when
// it is in none.
func npmVersion(real string) string {
for d := filepath.Dir(real); ; d = filepath.Dir(d) {
up := filepath.Dir(d)
if up == d {
return ""
}
if filepath.Base(up) == "node_modules" || strings.HasPrefix(filepath.Base(up), "@") && filepath.Base(filepath.Dir(up)) == "node_modules" {
var p struct {
Version string `json:"version"`
}
b, err := os.ReadFile(filepath.Join(d, "package.json"))
if err != nil || json.Unmarshal(b, &p) != nil {
return ""
}
return p.Version
}
}
}
+79
View File
@@ -0,0 +1,79 @@
package proc
import (
"context"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)
// A CLI is asked its version once per binary: one that fails to run (macOS
// stopping it as malware, #864) isn't started again until it changes, one
// cut off by the timeout is, and an npm CLI's is read from its package
// without running it.
func TestVersionRunsOnce(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("shell scripts")
}
dir := t.TempDir()
runs := filepath.Join(dir, "runs")
bin := filepath.Join(dir, "codex")
write := func(body string) {
if err := os.WriteFile(bin, []byte("#!/bin/sh\necho x >> '"+runs+"'\n"+body), 0o755); err != nil {
t.Fatal(err)
}
}
count := func() int {
b, _ := os.ReadFile(runs)
return strings.Count(string(b), "x")
}
write("exit 9\n")
for range 3 {
if v := Version(bin); v != "" {
t.Fatalf("a failing CLI said %q", v)
}
}
if n := count(); n != 1 {
t.Fatalf("a failing CLI was run %d times", n)
}
// changed (reinstalled), it is asked again
write("echo codex-cli 0.161.1\n")
os.Chtimes(bin, time.Now().Add(time.Minute), time.Now().Add(time.Minute))
if v := Version(bin); v != "codex-cli 0.161.1" {
t.Fatalf("got %q", v)
}
if n := count(); n != 2 {
t.Fatalf("run %d times", n)
}
// a timeout is asked again
other := filepath.Join(dir, "slow")
os.WriteFile(other, []byte("#!/bin/sh\n"), 0o755)
old := runVersion
t.Cleanup(func() { runVersion = old })
asked := 0
runVersion = func(string) (string, error) { asked++; return "", context.DeadlineExceeded }
Version(other)
Version(other)
if asked != 2 {
t.Fatalf("a timed-out CLI was asked %d times", asked)
}
// an npm CLI: the package says it, nothing is run
asked = 0
pkg := filepath.Join(dir, "lib", "node_modules", "@openai", "codex")
os.MkdirAll(filepath.Join(pkg, "bin"), 0o755)
os.WriteFile(filepath.Join(pkg, "package.json"), []byte(`{"name":"@openai/codex","version":"0.162.0"}`), 0o644)
os.WriteFile(filepath.Join(pkg, "bin", "codex.js"), []byte("#!/usr/bin/env node\n"), 0o755)
link := filepath.Join(dir, "bin", "codex")
os.MkdirAll(filepath.Dir(link), 0o755)
if err := os.Symlink(filepath.Join(pkg, "bin", "codex.js"), link); err != nil {
t.Fatal(err)
}
if v := Version(link); v != "0.162.0" || asked != 0 {
t.Fatalf("npm CLI: %q, run %d times", v, asked)
}
}
+7 -5
View File
@@ -89,17 +89,19 @@ func codexVersion() string {
newer(c.ClientVersion)
}
if exe := codexExecutable(); exe != "" {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
if out, err := proc.ProbeContext(ctx, exe, "--version").Output(); err == nil {
newer(string(out)) // "codex-cli 0.155.1"
}
cancel()
// "codex-cli 0.155.1"; read from its npm package, and not run
// again once it failed (#864: macOS's malware alert each time)
newer(codexCLIVersion(exe))
}
codexVersionCache.v, codexVersionCache.at = v, time.Now()
}
return newerVersion(codexVersionCache.v, codexSeen.get())
}
// codexCLIVersion is what the codex CLI at exe says its version is; a var
// so tests can fake it.
var codexCLIVersion = proc.Version
// newerVersion is the later of two versions, a when b isn't one.
func newerVersion(a, b string) string {
if b = claudeSemverRE.FindString(b); b != "" && (a == "" || compareClaudeVersion(b, a) > 0) {
+48
View File
@@ -178,3 +178,51 @@ func TestCodexModelsMaxContext(t *testing.T) {
t.Fatalf("%+v", ms)
}
}
// A codex CLI that fails to run — macOS stopping it as malware, with an
// alert each time (#864) — isn't run again every ten minutes, and an npm
// install's version is read from its package, not run.
func TestCodexVersionNotRerun(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("shell scripts")
}
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
t.Setenv("CODEX_HOME", "")
dir := t.TempDir()
runs := filepath.Join(dir, "runs")
exe := filepath.Join(dir, "codex")
if err := os.WriteFile(exe, []byte("#!/bin/sh\necho x >> '"+runs+"'\nexit 137\n"), 0o755); err != nil {
t.Fatal(err)
}
old := codexExecutable
t.Cleanup(func() { codexExecutable = old; codexVersionCache.at = time.Time{}; codexSeen = seenVersion{} })
codexSeen = seenVersion{}
codexExecutable = func() string { return exe }
for range 3 { // ten minutes apart
codexVersionCache.at = time.Time{}
if got := codexVersion(); got != codexClientVersion {
t.Fatalf("got %s", got)
}
}
if b, _ := os.ReadFile(runs); strings.Count(string(b), "x") != 1 {
t.Fatalf("the failing codex was run %d times", strings.Count(string(b), "x"))
}
pkg := filepath.Join(dir, "node_modules", "@openai", "codex")
os.MkdirAll(filepath.Join(pkg, "bin"), 0o755)
os.WriteFile(filepath.Join(pkg, "package.json"), []byte(`{"version":"0.199.0"}`), 0o644)
os.WriteFile(filepath.Join(pkg, "bin", "codex.js"), []byte("#!/bin/sh\necho x >> '"+runs+"'\necho codex-cli 0.199.0\n"), 0o755)
link := filepath.Join(dir, "bin", "codex")
os.MkdirAll(filepath.Dir(link), 0o755)
os.Symlink(filepath.Join(pkg, "bin", "codex.js"), link)
codexExecutable = func() string { return link }
codexVersionCache.at = time.Time{}
if got := codexVersion(); got != "0.199.0" {
t.Fatalf("npm codex: %s", got)
}
if b, _ := os.ReadFile(runs); strings.Count(string(b), "x") != 1 {
t.Fatal("the npm codex was run")
}
}