@@ -276,6 +276,64 @@ def list_processes() -> List[Tuple[int, str]]:
276276 continue
277277 return processes
278278
279+ def list_windows_processes () -> List [dict ]:
280+ """列出 Windows 进程信息"""
281+ if platform .system () != 'Windows' :
282+ return []
283+
284+ script = (
285+ "Get-CimInstance Win32_Process | "
286+ "Select-Object ProcessId,ParentProcessId,Name,ExecutablePath,CommandLine | "
287+ "ConvertTo-Json -Compress"
288+ )
289+ try :
290+ result = subprocess .run (
291+ ['powershell' , '-NoProfile' , '-Command' , script ],
292+ capture_output = True ,
293+ text = True ,
294+ check = False ,
295+ )
296+ except Exception :
297+ return []
298+
299+ if result .returncode != 0 :
300+ return []
301+
302+ raw = result .stdout .strip ()
303+ if not raw :
304+ return []
305+
306+ try :
307+ data = json .loads (raw )
308+ except Exception :
309+ return []
310+
311+ if isinstance (data , dict ):
312+ data = [data ]
313+ if not isinstance (data , list ):
314+ return []
315+
316+ processes = []
317+ for item in data :
318+ if not isinstance (item , dict ):
319+ continue
320+ try :
321+ pid = int (item .get ('ProcessId' ) or 0 )
322+ ppid = int (item .get ('ParentProcessId' ) or 0 )
323+ except Exception :
324+ continue
325+ if pid <= 0 :
326+ continue
327+
328+ processes .append ({
329+ 'pid' : pid ,
330+ 'ppid' : ppid ,
331+ 'name' : str (item .get ('Name' ) or '' ),
332+ 'exe_path' : str (item .get ('ExecutablePath' ) or '' ),
333+ 'command_line' : str (item .get ('CommandLine' ) or '' ),
334+ })
335+ return processes
336+
279337def list_process_tree () -> Dict [int , Tuple [int , str ]]:
280338 """列出当前用户进程树"""
281339 try :
@@ -319,6 +377,23 @@ def get_process_cwd(pid: int) -> str:
319377
320378def detect_codex_desktop_instances () -> List [dict ]:
321379 """检测运行中的 Codex Desktop 主进程"""
380+ if platform .system () == 'Windows' :
381+ instances = []
382+ for proc in list_windows_processes ():
383+ exe_path = (proc .get ('exe_path' ) or '' ).replace ('/' , '\\ ' )
384+ command = proc .get ('command_line' , '' )
385+ if not exe_path .lower ().endswith ('\\ app\\ codex.exe' ):
386+ continue
387+ if '\\ resources\\ codex.exe' in exe_path .lower ():
388+ continue
389+ if '--type=' in command :
390+ continue
391+ instances .append ({
392+ 'pid' : proc ['pid' ],
393+ 'app_path' : exe_path ,
394+ })
395+ return instances
396+
322397 instances = []
323398 for pid , command in list_processes ():
324399 if '/Contents/MacOS/Codex' not in command :
@@ -338,6 +413,9 @@ def detect_codex_desktop_instances() -> List[dict]:
338413
339414def detect_codex_cli_instances () -> List [dict ]:
340415 """检测运行中的 codex CLI 进程"""
416+ if platform .system () == 'Windows' :
417+ return []
418+
341419 tree = list_process_tree ()
342420 instances = []
343421 for pid , (ppid , command ) in tree .items ():
@@ -382,6 +460,27 @@ def escape_applescript_string(value: str) -> str:
382460 """转义 AppleScript 字符串"""
383461 return value .replace ('\\ ' , '\\ \\ ' ).replace ('"' , '\\ "' )
384462
463+ def escape_powershell_string (value : str ) -> str :
464+ """转义 PowerShell 单引号字符串"""
465+ return value .replace ("'" , "''" )
466+
467+ def collect_windows_restart_targets (desktop_instances : List [dict ]) -> List [str ]:
468+ """收集 Windows 下需要关闭的 Codex 可执行文件路径"""
469+ targets = []
470+ seen = set ()
471+ for item in desktop_instances :
472+ app_path = str (item .get ('app_path' ) or '' )
473+ if not app_path :
474+ continue
475+
476+ for candidate in [app_path , str (Path (app_path ).parent / 'resources' / 'codex.exe' )]:
477+ normalized = candidate .lower ()
478+ if normalized in seen :
479+ continue
480+ seen .add (normalized )
481+ targets .append (candidate )
482+ return targets
483+
385484def build_restart_script (
386485 script_path : Path ,
387486 desktop_instances : List [dict ],
@@ -408,10 +507,51 @@ def build_restart_script(
408507 lines .append (f"rm -f { shlex .quote (str (script_path ))} " )
409508 return '\n ' .join (lines ) + '\n '
410509
510+ def build_windows_restart_script (script_path : Path , desktop_instances : List [dict ]) -> str :
511+ """构建 Windows PowerShell 重启脚本"""
512+ desktop_pids = [str (int (item ['pid' ])) for item in desktop_instances if item .get ('pid' )]
513+ restart_paths = []
514+ seen_paths = set ()
515+ for item in desktop_instances :
516+ app_path = str (item .get ('app_path' ) or '' )
517+ if not app_path :
518+ continue
519+ normalized = app_path .lower ()
520+ if normalized in seen_paths :
521+ continue
522+ seen_paths .add (normalized )
523+ restart_paths .append (app_path )
524+
525+ target_paths = collect_windows_restart_targets (desktop_instances )
526+ escaped_target_paths = ', ' .join (
527+ f"'{ escape_powershell_string (path )} '" for path in target_paths
528+ ) or "''"
529+ escaped_restart_paths = ', ' .join (
530+ f"'{ escape_powershell_string (path )} '" for path in restart_paths
531+ ) or "''"
532+ escaped_script_path = escape_powershell_string (str (script_path ))
533+
534+ lines = [
535+ "$ErrorActionPreference = 'SilentlyContinue'" ,
536+ "Start-Sleep -Seconds 1" ,
537+ f"$desktopPids = @({ ', ' .join (desktop_pids )} )" if desktop_pids else "$desktopPids = @()" ,
538+ f"$targetPaths = @({ escaped_target_paths } )" ,
539+ f"$restartPaths = @({ escaped_restart_paths } )" ,
540+ "foreach ($pid in $desktopPids) { Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue }" ,
541+ "$lookup = @{}" ,
542+ "foreach ($path in $targetPaths) { if ($path) { $lookup[$path.ToLowerInvariant()] = $true } }" ,
543+ "Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -and $lookup.ContainsKey($_.ExecutablePath.ToLowerInvariant()) } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }" ,
544+ "Start-Sleep -Seconds 1" ,
545+ "foreach ($path in $restartPaths) { if ($path -and (Test-Path -LiteralPath $path)) { Start-Process -FilePath $path | Out-Null } }" ,
546+ f"Remove-Item -LiteralPath '{ escaped_script_path } ' -Force -ErrorAction SilentlyContinue" ,
547+ ]
548+ return '\r \n ' .join (lines ) + '\r \n '
549+
411550def schedule_codex_restart () -> Tuple [bool , bool , str ]:
412551 """安排后台重启运行中的 Codex Desktop 和 CLI"""
413- if platform .system () != 'Darwin' :
414- return False , False , '自动重启目前仅支持 macOS'
552+ current_platform = platform .system ()
553+ if current_platform not in {'Darwin' , 'Windows' }:
554+ return False , False , '自动重启目前仅支持 macOS 和 Windows'
415555
416556 desktop_instances = detect_codex_desktop_instances ()
417557 cli_instances = detect_codex_cli_instances ()
@@ -420,12 +560,19 @@ def schedule_codex_restart() -> Tuple[bool, bool, str]:
420560
421561 runtime_dir = get_switcher_dir () / 'runtime'
422562 runtime_dir .mkdir (parents = True , exist_ok = True )
423- script_path = runtime_dir / f"restart_codex_{ int (time .time ())} .sh"
424- script_path .write_text (
425- build_restart_script (script_path , desktop_instances , cli_instances ),
426- encoding = 'utf-8' ,
427- )
428- script_path .chmod (0o700 )
563+ if current_platform == 'Windows' :
564+ script_path = runtime_dir / f"restart_codex_{ int (time .time ())} .ps1"
565+ script_path .write_text (
566+ build_windows_restart_script (script_path , desktop_instances ),
567+ encoding = 'utf-8' ,
568+ )
569+ else :
570+ script_path = runtime_dir / f"restart_codex_{ int (time .time ())} .sh"
571+ script_path .write_text (
572+ build_restart_script (script_path , desktop_instances , cli_instances ),
573+ encoding = 'utf-8' ,
574+ )
575+ script_path .chmod (0o700 )
429576
430577 dry_run = os .environ .get (RESTART_DRY_RUN_ENV ) == '1'
431578 if dry_run :
@@ -435,12 +582,20 @@ def schedule_codex_restart() -> Tuple[bool, bool, str]:
435582 )
436583 return True , True , message
437584
438- subprocess .Popen (
439- ['/bin/zsh' , str (script_path )],
440- stdout = subprocess .DEVNULL ,
441- stderr = subprocess .DEVNULL ,
442- start_new_session = True ,
443- )
585+ if current_platform == 'Windows' :
586+ subprocess .Popen (
587+ ['powershell' , '-NoProfile' , '-ExecutionPolicy' , 'Bypass' , '-File' , str (script_path )],
588+ stdout = subprocess .DEVNULL ,
589+ stderr = subprocess .DEVNULL ,
590+ creationflags = getattr (subprocess , 'CREATE_NO_WINDOW' , 0 ),
591+ )
592+ else :
593+ subprocess .Popen (
594+ ['/bin/zsh' , str (script_path )],
595+ stdout = subprocess .DEVNULL ,
596+ stderr = subprocess .DEVNULL ,
597+ start_new_session = True ,
598+ )
444599 message = (
445600 f"已安排关闭 Codex 客户端及相关 CLI 进程,并重启 { len (desktop_instances )} 个 Codex 客户端"
446601 )
0 commit comments