app: avoid System Events for ChatGPT/Codex detection (#18601)

This commit is contained in:
Eva H 2026-09-22 16:28:58 -04:00 • committed by GitHub
parent 5a0ff3116d
commit ac3439083d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 91 additions and 6 deletions

View file

@ -2160,12 +2160,7 @@ func defaultCodexAppIsRunning() bool {
case "windows":
return len(codexAppMatchingProcessIDs()) > 0
case "darwin":
out, err := exec.Command("osascript", "-e", `tell application "System Events" to exists process "ChatGPT"`).Output()
if err == nil && strings.TrimSpace(string(out)) == "true" {
return true
}
out, err = exec.Command("osascript", "-e", `tell application "System Events" to exists process "Codex"`).Output()
if err == nil && strings.TrimSpace(string(out)) == "true" {
if err := exec.Command("pgrep", "-a", "-x", "ChatGPT|Codex").Run(); err == nil {
return true
}
return len(codexAppMatchingProcessIDs()) > 0

View file

@ -67,6 +67,22 @@ func withCodexAppProcessHooks(t *testing.T, isRunning func() bool, quit func() e
})
}
func stubCodexAppProcessCommands(t *testing.T, commands map[string]string) {
t.Helper()
if runtime.GOOS == "windows" {
t.Skip("test commands require a POSIX shell")
}
binDir := t.TempDir()
for name, script := range commands {
if err := os.WriteFile(filepath.Join(binDir, name), []byte("#!/bin/sh\n"+script), 0o755); err != nil {
t.Fatal(err)
}
}
// Never fall back to the real osascript or another system command.
t.Setenv("PATH", binDir)
}
func TestCodexAppIntegration(t *testing.T) {
c := &CodexApp{}
@ -3422,6 +3438,80 @@ func TestCodexAppRunRejectsExtraArgs(t *testing.T) {
}
}
func TestDefaultCodexAppIsRunningProcesses(t *testing.T) {
otherPID := os.Getpid() + 1
for _, tt := range []struct {
name string
goos string
output string
exitCode string
nameMatchExit string
want bool
}{
{name: "no processes", goos: "darwin"},
{name: "apps stopped or absent", goos: "darwin", output: "1 /sbin/launchd\n100 /Applications/Ollama.app/Contents/MacOS/Ollama\n"},
{name: "ChatGPT main", goos: "darwin", output: fmt.Sprintf("%d /Applications/ChatGPT.app/Contents/MacOS/ChatGPT\n", otherPID), want: true},
{name: "Codex main", goos: "darwin", output: fmt.Sprintf("%d /Applications/Codex.app/Contents/MacOS/Codex\n", otherPID), want: true},
{name: "renamed ChatGPT bundle", goos: "darwin", output: fmt.Sprintf("%d /Applications/ChatGPT Preview.app/Contents/MacOS/ChatGPT\n", otherPID), nameMatchExit: "0", want: true},
{name: "renamed Codex bundle", goos: "darwin", output: fmt.Sprintf("%d /Applications/Codex Preview.app/Contents/MacOS/Codex\n", otherPID), nameMatchExit: "0", want: true},
{name: "Codex installed as ChatGPT", goos: "darwin", output: fmt.Sprintf("%d /Applications/ChatGPT.app/Contents/MacOS/Codex\n", otherPID), nameMatchExit: "0", want: true},
{name: "ChatGPT app server", goos: "darwin", output: fmt.Sprintf("%d /Applications/ChatGPT.app/Contents/Resources/codex app-server --analytics-default-enabled\n", otherPID), want: true},
{name: "Codex app server", goos: "darwin", output: fmt.Sprintf("%d /Applications/Codex.app/Contents/Resources/codex app-server --analytics-default-enabled\n", otherPID), want: true},
{name: "ChatGPT helper only", goos: "darwin", output: "105 /Applications/ChatGPT.app/Contents/Frameworks/ChatGPT Helper.app/Contents/MacOS/ChatGPT Helper\n"},
{name: "Codex helpers only", goos: "darwin", output: "106 /Applications/Codex.app/Contents/Frameworks/Codex Helper.app/Contents/MacOS/Codex Helper\n107 /Applications/Codex.app/Contents/Frameworks/Electron Framework.framework/Helpers/chrome_crashpad_handler\n"},
{name: "current process", goos: "darwin", output: fmt.Sprintf("%d /Applications/ChatGPT.app/Contents/MacOS/ChatGPT\n", os.Getpid())},
{name: "malformed process list", goos: "darwin", output: "\n108\ninvalid /Applications/Codex.app/Contents/MacOS/Codex\n"},
{name: "failed process lookup", goos: "darwin", output: fmt.Sprintf("%d /Applications/ChatGPT.app/Contents/MacOS/ChatGPT\n", otherPID), exitCode: "1"},
{name: "name match with failed process lookup", goos: "darwin", exitCode: "1", nameMatchExit: "0", want: true},
{name: "failed name lookup falls back to process list", goos: "darwin", output: fmt.Sprintf("%d /Applications/ChatGPT.app/Contents/MacOS/ChatGPT\n", otherPID), nameMatchExit: "3", want: true},
{name: "Windows stopped", goos: "windows"},
{name: "Windows running", goos: "windows", output: fmt.Sprintf("%d\r\n", otherPID), want: true},
{name: "Windows current process", goos: "windows", output: fmt.Sprintf("%d\r\n", os.Getpid())},
{name: "unsupported platform", goos: "linux", output: fmt.Sprintf("%d /Applications/Codex.app/Contents/MacOS/Codex\n", otherPID)},
} {
t.Run(tt.name, func(t *testing.T) {
withCodexAppPlatform(t, tt.goos)
command := "ps"
if tt.goos == "windows" {
command = "powershell.exe"
}
commands := map[string]string{
command: "printf '%s' \"$OLLAMA_TEST_CODEX_APP_PROCESSES\"\nexit \"${OLLAMA_TEST_CODEX_APP_EXIT_CODE:-0}\"\n",
}
if tt.goos == "darwin" {
// Exact names exclude helpers; -a includes the app if it launched Ollama.
commands["pgrep"] = "[ \"$*\" = '-a -x ChatGPT|Codex' ] || exit 2\nexit \"${OLLAMA_TEST_CODEX_APP_NAME_EXIT_CODE:-1}\"\n"
}
stubCodexAppProcessCommands(t, commands)
t.Setenv("OLLAMA_TEST_CODEX_APP_PROCESSES", tt.output)
t.Setenv("OLLAMA_TEST_CODEX_APP_EXIT_CODE", tt.exitCode)
t.Setenv("OLLAMA_TEST_CODEX_APP_NAME_EXIT_CODE", tt.nameMatchExit)
if got := defaultCodexAppIsRunning(); got != tt.want {
t.Fatalf("defaultCodexAppIsRunning() = %v, want %v", got, tt.want)
}
})
}
}
func TestDefaultCodexAppIsRunningDoesNotCallAppleScript(t *testing.T) {
withCodexAppPlatform(t, "darwin")
marker := filepath.Join(t.TempDir(), "osascript-called")
t.Setenv("OLLAMA_TEST_CODEX_APP_OSASCRIPT_MARKER", marker)
stubCodexAppProcessCommands(t, map[string]string{
"ps": "exit 0\n",
// Simulate slow System Events without contacting the real application.
"osascript": "printf called > \"$OLLAMA_TEST_CODEX_APP_OSASCRIPT_MARKER\"\nexec /bin/sleep 1\n",
})
if defaultCodexAppIsRunning() {
t.Fatal("reported a running app with no matching processes")
}
if _, err := os.Stat(marker); !os.IsNotExist(err) {
t.Fatalf("running-state detection invoked osascript: %v", err)
}
}
func TestCodexAppProcessMatchesMainAndAppServer(t *testing.T) {
for _, command := range []string{
"/Applications/ChatGPT.app/Contents/MacOS/ChatGPT",