Skip to content

Commit c7265d1

Browse files
committed
新增 Windows 切换后自动重启支持
- 新增,Windows 下切换账号后自动重启 Codex 客户端 - 改进,跨平台重启脚本调度逻辑 - 修复,Windows 环境下切换后无法自动重启的问题
1 parent b11e346 commit c7265d1

1 file changed

Lines changed: 169 additions & 14 deletions

File tree

‎codex_switcher.py‎

Lines changed: 169 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
279337
def list_process_tree() -> Dict[int, Tuple[int, str]]:
280338
"""列出当前用户进程树"""
281339
try:
@@ -319,6 +377,23 @@ def get_process_cwd(pid: int) -> str:
319377

320378
def 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

339414
def 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+
385484
def 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+
411550
def 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

Comments
 (0)