launch: codex app restarts (#16155)

This commit is contained in:
Parth Sareen
2026-05-14 12:08:13 -07:00
committed by GitHub
parent 3ad2fa3fb5
commit c28ddc0a7b
2 changed files with 222 additions and 5 deletions

View File

@@ -39,11 +39,16 @@ var (
codexAppOpenPath = defaultCodexAppOpenAppPath
codexAppOpenStart = defaultCodexAppOpenStartAppID
codexAppQuitApp = defaultCodexAppQuitApp
codexAppForceQuit = defaultCodexAppForceQuitApp
codexAppHasWindow = defaultCodexAppHasOpenWindow
codexAppIsRunning = defaultCodexAppIsRunning
codexAppRunPath = defaultCodexAppRunningAppPath
codexAppStartID = defaultCodexAppStartAppID
codexAppCanOpenID = defaultCodexAppCanOpenBundleID
codexAppSleep = time.Sleep
codexAppExitTimeout = 5 * time.Second
codexAppForceExitTimeout = 5 * time.Second
)
// CodexApp configures the desktop Codex app with one launch-selected default
@@ -367,7 +372,7 @@ type codexAppModelMetadata struct {
func codexAppDefaultModelMetadata() codexAppModelMetadata {
return codexAppModelMetadata{
contextWindow: 272_000,
contextWindow: 128_000,
inputModalities: []string{"text"},
}
}
@@ -567,8 +572,21 @@ func codexAppLaunchOrRestart(prompt string) error {
if err := codexAppQuitApp(); err != nil {
return fmt.Errorf("quit Codex: %w", err)
}
if err := waitForCodexAppExit(30 * time.Second); err != nil {
return err
gracefulErr := waitForCodexAppGracefulExit(codexAppExitTimeout)
if gracefulErr != nil && !codexAppForceQuitSupported() {
return gracefulErr
}
if codexAppForceQuitSupported() && codexAppIsRunning() {
if forceErr := codexAppForceQuit(); forceErr != nil {
return fmt.Errorf("force stop Codex: %w", forceErr)
}
if err := waitForCodexAppExit(codexAppForceExitTimeout); err != nil {
return err
}
} else if gracefulErr != nil {
if codexAppIsRunning() {
return gracefulErr
}
}
if restartAppID != "" {
return codexAppOpenStart(restartAppID)
@@ -579,10 +597,29 @@ func codexAppLaunchOrRestart(prompt string) error {
return codexAppOpenApp()
}
func codexAppForceQuitSupported() bool {
return codexAppGOOS == "darwin" || codexAppGOOS == "windows"
}
func waitForCodexAppGracefulExit(timeout time.Duration) error {
return waitForCodexAppCondition(timeout, func() bool {
if codexAppGOOS == "windows" {
return !codexAppHasWindow()
}
return !codexAppIsRunning()
})
}
func waitForCodexAppExit(timeout time.Duration) error {
return waitForCodexAppCondition(timeout, func() bool {
return !codexAppIsRunning()
})
}
func waitForCodexAppCondition(timeout time.Duration, done func() bool) error {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if !codexAppIsRunning() {
if done() {
return nil
}
codexAppSleep(200 * time.Millisecond)
@@ -650,6 +687,46 @@ func defaultCodexAppQuitApp() error {
return scriptErr
}
func defaultCodexAppForceQuitApp() error {
if !codexAppForceQuitSupported() {
return nil
}
pids := codexAppMatchingProcessIDs()
if len(pids) == 0 {
return nil
}
pidArgs := make([]string, 0, len(pids))
for _, pid := range pids {
pidArgs = append(pidArgs, strconv.Itoa(pid))
}
switch codexAppGOOS {
case "windows":
script := "Stop-Process -Id " + strings.Join(pidArgs, ",") + " -Force -ErrorAction SilentlyContinue"
return runCodexAppForceQuitCommand(exec.Command("powershell.exe", "-NoProfile", "-Command", script))
case "darwin":
return runCodexAppForceQuitCommand(exec.Command("kill", append([]string{"-TERM"}, pidArgs...)...))
default:
return nil
}
}
func runCodexAppForceQuitCommand(cmd *exec.Cmd) error {
err := cmd.Run()
if err != nil && !codexAppIsRunning() {
return nil
}
return err
}
func defaultCodexAppHasOpenWindow() bool {
if codexAppGOOS != "windows" {
return codexAppIsRunning()
}
script := `(Get-Process Codex -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | Select-Object -First 1).Id`
out, err := exec.Command("powershell.exe", "-NoProfile", "-Command", script).Output()
return err == nil && strings.TrimSpace(string(out)) != ""
}
func defaultCodexAppIsRunning() bool {
switch codexAppGOOS {
case "windows":

View File

@@ -30,10 +30,15 @@ func withCodexAppProcessHooks(t *testing.T, isRunning func() bool, quit func() e
oldOpen := codexAppOpenApp
oldOpenPath := codexAppOpenPath
oldOpenStart := codexAppOpenStart
oldForceQuit := codexAppForceQuit
oldHasWindow := codexAppHasWindow
oldRunPath := codexAppRunPath
oldStartID := codexAppStartID
oldCanOpenID := codexAppCanOpenID
oldExitTimeout := codexAppExitTimeout
oldForceExitTimeout := codexAppForceExitTimeout
codexAppIsRunning = isRunning
codexAppHasWindow = isRunning
codexAppQuitApp = quit
codexAppOpenApp = open
t.Cleanup(func() {
@@ -42,9 +47,13 @@ func withCodexAppProcessHooks(t *testing.T, isRunning func() bool, quit func() e
codexAppOpenApp = oldOpen
codexAppOpenPath = oldOpenPath
codexAppOpenStart = oldOpenStart
codexAppForceQuit = oldForceQuit
codexAppHasWindow = oldHasWindow
codexAppRunPath = oldRunPath
codexAppStartID = oldStartID
codexAppCanOpenID = oldCanOpenID
codexAppExitTimeout = oldExitTimeout
codexAppForceExitTimeout = oldForceExitTimeout
})
}
@@ -585,7 +594,7 @@ func TestCodexAppConfigurePopulatesCatalogFromTagsAndShow(t *testing.T) {
if !ok || len(levels) != 0 {
t.Fatalf("supported_reasoning_levels for %q = %v, want empty list", slug, model["supported_reasoning_levels"])
}
wantContext := float64(272000)
wantContext := float64(128000)
wantModalities := []string{"text"}
wantShowCalls := 0
if slug == "gemma4" {
@@ -1165,6 +1174,64 @@ func TestCodexAppRunWaitsForGracefulExitBeforeReopening(t *testing.T) {
}
}
func TestCodexAppRunForceStopsMacAfterGracefulTimeout(t *testing.T) {
withCodexAppPlatform(t, "darwin")
restoreConfirm := withLaunchConfirmPolicy(launchConfirmPolicy{yes: true})
defer restoreConfirm()
running := true
calls := make([]string, 0)
withCodexAppProcessHooks(t,
func() bool { return running },
func() error {
calls = append(calls, "quit")
return nil
},
func() error {
calls = append(calls, "open")
return nil
},
)
codexAppExitTimeout = 0
codexAppForceQuit = func() error {
calls = append(calls, "force")
running = false
return nil
}
if err := (&CodexApp{}).Run("qwen3.5", nil); err != nil {
t.Fatalf("Run returned error: %v", err)
}
want := []string{"quit", "force", "open"}
if strings.Join(calls, ",") != strings.Join(want, ",") {
t.Fatalf("calls = %v, want %v", calls, want)
}
}
func TestCodexAppRunReturnsMacForceStopError(t *testing.T) {
withCodexAppPlatform(t, "darwin")
restoreConfirm := withLaunchConfirmPolicy(launchConfirmPolicy{yes: true})
defer restoreConfirm()
withCodexAppProcessHooks(t,
func() bool { return true },
func() error { return nil },
func() error {
t.Fatal("app should not reopen when force stop fails")
return nil
},
)
codexAppExitTimeout = 0
codexAppForceQuit = func() error {
return fmt.Errorf("operation not permitted")
}
err := (&CodexApp{}).Run("qwen3.5", nil)
if err == nil || !strings.Contains(err.Error(), "force stop Codex") || !strings.Contains(err.Error(), "operation not permitted") {
t.Fatalf("Run error = %v, want force stop failure", err)
}
}
func TestCodexAppRunOpensOnWindowsWhenNotRunning(t *testing.T) {
withCodexAppPlatform(t, "windows")
@@ -1234,6 +1301,79 @@ func TestCodexAppRunRestartsWindowsStartAppID(t *testing.T) {
}
}
func TestCodexAppRunForceStopsWindowsBackgroundProcessesBeforeReopening(t *testing.T) {
withCodexAppPlatform(t, "windows")
restoreConfirm := withLaunchConfirmPolicy(launchConfirmPolicy{yes: true})
defer restoreConfirm()
windowOpen := true
running := true
calls := make([]string, 0)
withCodexAppProcessHooks(t,
func() bool { return running },
func() error {
calls = append(calls, "quit")
windowOpen = false
return nil
},
func() error {
t.Fatal("open app fallback should not be used")
return nil
},
)
codexAppHasWindow = func() bool { return windowOpen }
codexAppForceQuit = func() error {
calls = append(calls, "force")
running = false
return nil
}
codexAppStartID = func() string { return "OpenAI.Codex_2p2nqsd0c76g0!App" }
codexAppOpenStart = func(appID string) error {
calls = append(calls, "open:"+appID)
return nil
}
if err := (&CodexApp{}).Run("qwen3.5", nil); err != nil {
t.Fatalf("Run returned error: %v", err)
}
want := []string{"quit", "force", "open:OpenAI.Codex_2p2nqsd0c76g0!App"}
if strings.Join(calls, ",") != strings.Join(want, ",") {
t.Fatalf("calls = %v, want %v", calls, want)
}
}
func TestCodexAppRunReturnsWindowsForceStopError(t *testing.T) {
withCodexAppPlatform(t, "windows")
restoreConfirm := withLaunchConfirmPolicy(launchConfirmPolicy{yes: true})
defer restoreConfirm()
windowOpen := true
withCodexAppProcessHooks(t,
func() bool { return true },
func() error {
windowOpen = false
return nil
},
func() error {
t.Fatal("open app fallback should not be used")
return nil
},
)
codexAppHasWindow = func() bool { return windowOpen }
codexAppForceQuit = func() error {
return fmt.Errorf("access denied")
}
codexAppOpenStart = func(string) error {
t.Fatal("app should not reopen when force stop fails")
return nil
}
err := (&CodexApp{}).Run("qwen3.5", nil)
if err == nil || !strings.Contains(err.Error(), "force stop Codex") || !strings.Contains(err.Error(), "access denied") {
t.Fatalf("Run error = %v, want force stop failure", err)
}
}
func TestCodexAppRunRejectsExtraArgs(t *testing.T) {
withCodexAppPlatform(t, "darwin")
err := (&CodexApp{}).Run("qwen3.5", []string{"--foo"})