Files
DDSRem 2de976f6b3 Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
2026-10-17 19:26:29 +08:00

121 lines
4.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Python 内核的部署环境继承、父进程死亡通知和整组进程终止。
"""
import asyncio
import os
import signal
import subprocess
import sys
from typing import Any
def child_environment(values: dict[str, str]) -> dict[str, str]:
"""
管理员内核继承宿主部署配置和连接凭据,保留独立导入路径与 UTF-8 输出。
"""
# 内核与管理员 shell 同属宿主权限;过滤配置或密码会使脚本静默连接默认数据库。
environment = values.copy()
environment.pop('PYTHONPATH', None)
environment.update(PYTHONDONTWRITEBYTECODE='1', PYTHONIOENCODING='utf-8', PYTHONUTF8='1')
return environment
class ParentWatch:
"""
POSIX 只继承死亡管道读端;宿主关闭写端或意外退出均让内核结束。
"""
def __init__(self) -> None:
"""
Windows 用真实进程对象 handle,避免 PID 重用误认父进程。
"""
self.read_fd: int | None = None
self.write_fd: int | None = None
self.handle: Any = None
self.close_handle: Any = None
def prepare(self, environment: dict[str, str]) -> dict[str, Any]:
"""
返回可直接传给 subprocess 的继承选项,凭据不通过 argv。
"""
if os.name != 'nt':
self.read_fd, self.write_fd = os.pipe()
environment['MOVIEPILOT_KERNEL_PARENT_DEATH_FD'] = str(self.read_fd)
return {'pass_fds': (self.read_fd,), 'start_new_session': True}
return self._windows(environment)
def _windows(self, environment: dict[str, str]) -> dict[str, Any]:
"""
通过 SYNCHRONIZE handle 建立 Windows 父进程死亡看护。
"""
# 仅 Windows 分支需要进程句柄类型,延迟导入以保持其他平台独立。
import ctypes # pylint: disable=import-outside-toplevel
# 与 ctypes 一同按平台加载句柄参数类型。
from ctypes import wintypes # pylint: disable=import-outside-toplevel
kernel32 = getattr(ctypes, 'WinDLL')('kernel32', use_last_error=True)
kernel32.GetCurrentProcessId.restype = wintypes.DWORD
kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
kernel32.OpenProcess.restype = wintypes.HANDLE
kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
self.handle = kernel32.OpenProcess(0x00100000, True, kernel32.GetCurrentProcessId())
if not self.handle:
raise OSError('无法建立 Python 内核父进程看护')
self.close_handle = kernel32.CloseHandle
environment['MOVIEPILOT_KERNEL_PARENT_PROCESS_HANDLE'] = str(int(self.handle))
startup = getattr(subprocess, 'STARTUPINFO')()
startup.lpAttributeList = {'handle_list': [int(self.handle)]}
return {'startupinfo': startup, 'creationflags': getattr(subprocess, 'CREATE_NO_WINDOW')}
def spawned(self) -> None:
"""
子进程启动后宿主不再持有继承端,否则死亡通知无法可靠触发。
"""
if self.read_fd is not None:
os.close(self.read_fd)
self.read_fd = None
if self.handle is not None:
self.close_handle(self.handle)
self.handle = None
def close(self) -> None:
"""
同步撤销父存活信号,重复清理不误关其他新建描述符。
"""
self.spawned()
if self.write_fd is not None:
os.close(self.write_fd)
self.write_fd = None
async def kill_tree(process: asyncio.subprocess.Process) -> None:
"""
取消与超时都终止整个进程组,不能只杀 Python 而遗留其命令。
"""
if os.name == 'nt':
if process.returncode is not None:
return
killer = await asyncio.create_subprocess_exec('taskkill', '/PID', str(process.pid), '/T', '/F',
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
await asyncio.wait_for(killer.wait(), 5)
else:
for attempt in range(2):
try:
os.killpg(process.pid, signal.SIGKILL)
except ProcessLookupError:
break
except PermissionError as error:
if sys.platform != 'darwin' or attempt:
raise
# macOS 对只剩未回收僵尸的组返回 EPERM。先让 asyncio 回收根进程,
# 再发一次组信号,不能因根已退出就漏掉仍存活的后代或吞掉真实权限错误。
try:
await asyncio.wait_for(process.wait(), 5)
except TimeoutError:
raise error from None
else:
break
await asyncio.wait_for(process.wait(), 5)