@@ -107,7 +107,7 @@ pub fn gracefully_stop_codex_processes(processes: &[RunningCodexProcess]) -> any
107107 return Ok ( ( ) ) ;
108108 }
109109
110- anyhow:: bail!( "Timed out waiting for Codex processes to close gracefully" ) ;
110+ anyhow:: bail!( format_graceful_shutdown_timeout ( processes) ) ;
111111}
112112
113113pub fn restart_codex_processes ( processes : & [ RunningCodexProcess ] ) -> anyhow:: Result < ( ) > {
@@ -157,40 +157,67 @@ fn collect_running_codex_processes_unix() -> anyhow::Result<Vec<RunningCodexProc
157157 let stdout = String :: from_utf8_lossy ( & output. stdout ) ;
158158
159159 for line in stdout. lines ( ) {
160- let line = line. trim ( ) ;
161- if line. is_empty ( ) {
162- continue ;
163- }
164-
165- if let Some ( ( pid_str, command) ) = line. split_once ( ' ' ) {
166- let command = command. trim ( ) ;
167- let executable = command. split_whitespace ( ) . next ( ) . unwrap_or ( "" ) ;
168- let is_codex = executable == "codex" || executable. ends_with ( "/codex" ) ;
169- let is_background = command. contains ( ".antigravity" )
170- || command. contains ( "openai.chatgpt" )
171- || command. contains ( ".vscode" ) ;
172- let is_switcher =
173- command. contains ( "codex-switcher" ) || command. contains ( "Codex Switcher" ) ;
174-
175- if is_codex && !is_switcher {
176- if let Ok ( pid) = pid_str. trim ( ) . parse :: < u32 > ( ) {
177- if pid != std:: process:: id ( )
178- && !processes. iter ( ) . any ( |p : & RunningCodexProcess | p. pid == pid)
179- {
180- processes. push ( RunningCodexProcess {
181- pid,
182- command : command. to_string ( ) ,
183- is_background,
184- } ) ;
185- }
186- }
160+ if let Some ( process) = parse_unix_process_line ( line) {
161+ if process. pid != std:: process:: id ( )
162+ && !processes. iter ( ) . any ( |p : & RunningCodexProcess | p. pid == process. pid )
163+ {
164+ processes. push ( process) ;
187165 }
188166 }
189167 }
190168
191169 Ok ( processes)
192170}
193171
172+ #[ cfg( unix) ]
173+ fn parse_unix_process_line ( line : & str ) -> Option < RunningCodexProcess > {
174+ let line = line. trim ( ) ;
175+ if line. is_empty ( ) {
176+ return None ;
177+ }
178+
179+ let ( pid_str, command) = line. split_once ( ' ' ) ?;
180+ let command = command. trim ( ) ;
181+ let executable = command. split_whitespace ( ) . next ( ) . unwrap_or ( "" ) ;
182+ let is_codex = executable == "codex" || executable. ends_with ( "/codex" ) ;
183+ let is_background =
184+ command. contains ( ".antigravity" ) || command. contains ( "openai.chatgpt" ) || command. contains ( ".vscode" ) ;
185+ let is_switcher = command. contains ( "codex-switcher" ) || command. contains ( "Codex Switcher" ) ;
186+ let is_codex_app_server =
187+ command. contains ( "/Codex.app/Contents/Resources/codex app-server" )
188+ || command. contains ( "/Applications/Codex.app/Contents/Resources/codex app-server" ) ;
189+
190+ if !is_codex || is_switcher || is_codex_app_server {
191+ return None ;
192+ }
193+
194+ let pid = pid_str. trim ( ) . parse :: < u32 > ( ) . ok ( ) ?;
195+ Some ( RunningCodexProcess {
196+ pid,
197+ command : command. to_string ( ) ,
198+ is_background,
199+ } )
200+ }
201+
202+ fn format_graceful_shutdown_timeout ( processes : & [ RunningCodexProcess ] ) -> String {
203+ let details = processes
204+ . iter ( )
205+ . map ( |process| format ! ( "pid {} ({})" , process. pid, summarize_command( & process. command) ) )
206+ . collect :: < Vec < _ > > ( )
207+ . join ( ", " ) ;
208+ format ! ( "Timed out waiting for Codex processes to close gracefully: {details}" )
209+ }
210+
211+ fn summarize_command ( command : & str ) -> String {
212+ const MAX_LEN : usize = 80 ;
213+ if command. chars ( ) . count ( ) <= MAX_LEN {
214+ return command. to_string ( ) ;
215+ }
216+
217+ let summary = command. chars ( ) . take ( MAX_LEN - 3 ) . collect :: < String > ( ) ;
218+ format ! ( "{summary}..." )
219+ }
220+
194221#[ cfg( windows) ]
195222fn collect_running_codex_processes_windows ( ) -> anyhow:: Result < Vec < RunningCodexProcess > > {
196223 let mut processes = Vec :: new ( ) ;
@@ -221,3 +248,42 @@ fn collect_running_codex_processes_windows() -> anyhow::Result<Vec<RunningCodexP
221248
222249 Ok ( processes)
223250}
251+
252+ #[ cfg( test) ]
253+ mod tests {
254+ use super :: * ;
255+
256+ #[ test]
257+ fn ignores_codex_app_server_processes ( ) {
258+ let line = "5989 /Applications/Codex.app/Contents/Resources/codex app-server --analytics-default-enabled" ;
259+
260+ let process = parse_unix_process_line ( line) ;
261+
262+ assert ! ( process. is_none( ) ) ;
263+ }
264+
265+ #[ test]
266+ fn timeout_error_lists_remaining_processes ( ) {
267+ let processes = vec ! [
268+ RunningCodexProcess {
269+ pid: 100 ,
270+ command: String :: from(
271+ "/opt/homebrew/lib/node_modules/@openai/codex/vendor/codex/codex resume 123" ,
272+ ) ,
273+ is_background: false ,
274+ } ,
275+ RunningCodexProcess {
276+ pid: 200 ,
277+ command: String :: from( "/Applications/Codex.app/Contents/Resources/codex app-server" ) ,
278+ is_background: true ,
279+ } ,
280+ ] ;
281+
282+ let message = format_graceful_shutdown_timeout ( & processes) ;
283+
284+ assert ! ( message. contains( "100" ) ) ;
285+ assert ! ( message. contains( "resume 123" ) ) ;
286+ assert ! ( message. contains( "200" ) ) ;
287+ assert ! ( message. contains( "app-server" ) ) ;
288+ }
289+ }
0 commit comments