s
This commit is contained in:
@@ -84,13 +84,17 @@ async def run_pm2(cmd: str, timeout: float = 15.0) -> str:
|
||||
|
||||
def _ffmpeg_cmdline_markers(process_name: str, project_root: Path | None) -> list[str]:
|
||||
safe = re.sub(r"[^a-zA-Z0-9_.-]", "_", process_name).strip()
|
||||
markers = {str(process_name or "").strip(), safe}
|
||||
markers: set[str] = set()
|
||||
if project_root is not None and safe:
|
||||
config_dir = project_root / "config"
|
||||
markers.add(f"youtube.{safe}.ini")
|
||||
markers.add(f"URL_config.{safe}.ini")
|
||||
markers.add(str(config_dir / f"youtube.{safe}.ini"))
|
||||
markers.add(str(config_dir / f"URL_config.{safe}.ini"))
|
||||
markers.update(
|
||||
{
|
||||
f"youtube.{safe}.ini",
|
||||
f"URL_config.{safe}.ini",
|
||||
str(config_dir / f"youtube.{safe}.ini"),
|
||||
str(config_dir / f"URL_config.{safe}.ini"),
|
||||
}
|
||||
)
|
||||
return [m for m in markers if m]
|
||||
|
||||
|
||||
@@ -116,6 +120,16 @@ def _read_proc_env_value(pid: int, key: str) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _ffmpeg_pid_matches_process(pid: int, process_name: str) -> bool:
|
||||
target = (process_name or "").strip()
|
||||
if not target:
|
||||
return False
|
||||
for key in ("LIVE_PM2_PROCESS_NAME", "PM2_SERVICE_NAME"):
|
||||
if _read_proc_env_value(pid, key) == target:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _proc_parent_and_group(pid: int) -> tuple[int | None, int | None]:
|
||||
try:
|
||||
raw = (Path("/proc") / str(pid) / "stat").read_text(encoding="utf-8", errors="ignore")
|
||||
@@ -178,7 +192,7 @@ def _linux_ffmpeg_pids_for_process_tree(root_pid: int, *, root_pgid: int | None
|
||||
|
||||
|
||||
def _linux_ffmpeg_pids_for_stack(process_name: str, project_root: Path | None) -> list[int]:
|
||||
"""仅匹配与本项目直播栈相关的 ffmpeg(命令行含 YouTube 推流、抖音拉流或项目路径),避免 killall 误伤整机其它 ffmpeg。"""
|
||||
"""仅匹配继承了目标进程环境变量的 ffmpeg,避免按关键词误杀其它线路。"""
|
||||
markers = _ffmpeg_cmdline_markers(process_name, project_root)
|
||||
seen: set[int] = set()
|
||||
proc_root = Path("/proc")
|
||||
@@ -198,6 +212,9 @@ def _linux_ffmpeg_pids_for_stack(process_name: str, project_root: Path | None) -
|
||||
continue
|
||||
if not raw or b"ffmpeg" not in raw.lower():
|
||||
continue
|
||||
if _ffmpeg_pid_matches_process(pid, process_name):
|
||||
seen.add(pid)
|
||||
continue
|
||||
cmd = raw.replace(b"\0", b" ").decode("utf-8", "replace")
|
||||
if any(m in cmd for m in markers):
|
||||
seen.add(pid)
|
||||
@@ -420,9 +437,64 @@ def resolve_pm2_bin() -> Optional[str]:
|
||||
|
||||
def preferred_python_interpreter(base_dir: Path) -> str:
|
||||
venv_python = base_dir / ".venv" / "bin" / "python"
|
||||
if venv_python.is_file():
|
||||
return str(venv_python)
|
||||
return sys.executable or "python3"
|
||||
candidates = [
|
||||
str(venv_python),
|
||||
shutil_which("python3"),
|
||||
shutil_which("python"),
|
||||
sys.executable,
|
||||
]
|
||||
for candidate in candidates:
|
||||
if not candidate:
|
||||
continue
|
||||
path = Path(candidate).expanduser()
|
||||
if path.is_file() and os.access(str(path), os.X_OK):
|
||||
return str(path)
|
||||
return "python3"
|
||||
|
||||
|
||||
def _resolved_pm2_item_path(item: dict[str, Any], *keys: str) -> Optional[Path]:
|
||||
env = item.get("pm2_env") if isinstance(item.get("pm2_env"), dict) else {}
|
||||
for key in keys:
|
||||
raw = env.get(key)
|
||||
if raw in (None, ""):
|
||||
raw = item.get(key)
|
||||
value = str(raw or "").strip()
|
||||
if value:
|
||||
return Path(value).expanduser()
|
||||
return None
|
||||
|
||||
|
||||
def _samefile_or_resolved_path(left: Path, right: Path) -> bool:
|
||||
try:
|
||||
return left.resolve() == right.resolve()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def pm2_process_entry_needs_recreate(base_dir: Path, process: str, script: str, item: dict[str, Any] | None) -> bool:
|
||||
if not item:
|
||||
return False
|
||||
|
||||
script_path = (base_dir / script).resolve()
|
||||
configured_script = _resolved_pm2_item_path(item, "pm_exec_path")
|
||||
if configured_script is None or not _samefile_or_resolved_path(configured_script, script_path):
|
||||
return True
|
||||
|
||||
configured_cwd = _resolved_pm2_item_path(item, "pm_cwd", "cwd")
|
||||
if configured_cwd is None or not _samefile_or_resolved_path(configured_cwd, base_dir.resolve()):
|
||||
return True
|
||||
|
||||
suffix = script_path.suffix.lower()
|
||||
if suffix == ".py":
|
||||
interpreter = _resolved_pm2_item_path(item, "exec_interpreter")
|
||||
if interpreter is None or not interpreter.is_file() or not os.access(str(interpreter), os.X_OK):
|
||||
return True
|
||||
elif suffix == ".sh":
|
||||
interpreter = _resolved_pm2_item_path(item, "exec_interpreter")
|
||||
if interpreter is not None and (not interpreter.is_file() or not os.access(str(interpreter), os.X_OK)):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def build_pm2_start_command(base_dir: Path, process: str, script: str) -> Optional[str]:
|
||||
@@ -946,12 +1018,33 @@ class WebProcessBackend:
|
||||
with contextlib.suppress(Exception):
|
||||
await run_pm2("save", timeout=20.0)
|
||||
|
||||
async def _pm2_start_fresh(self, process: str, script: str) -> str:
|
||||
await self.clear_process_logs(process)
|
||||
cmd = build_pm2_start_command(self.base_dir, process, script)
|
||||
if not cmd:
|
||||
return f"ERROR: script not found: {script}"
|
||||
code, text = await _shell_out(cmd, timeout=45.0)
|
||||
if code == 0:
|
||||
await self._set_pm2_desired_running(process, True)
|
||||
await self._pm2_save()
|
||||
self.invalidate_pm2_cache()
|
||||
return text
|
||||
|
||||
async def _pm2_delete_existing_process(self, process: str) -> None:
|
||||
with contextlib.suppress(Exception):
|
||||
await run_pm2_result(f"delete {process}", timeout=20.0)
|
||||
self.invalidate_pm2_cache()
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
async def start(self, process: str, script: str) -> str:
|
||||
await self.ensure_pm2_runtime()
|
||||
if self._use_pm2:
|
||||
registered = await self._pm2_registered_names()
|
||||
if process in registered:
|
||||
info = await self.parse_pm2_describe(process)
|
||||
item = (await self._pm2_jlist_index(force_refresh=True)).get(process)
|
||||
if pm2_process_entry_needs_recreate(self.base_dir, process, script, item):
|
||||
await self._pm2_delete_existing_process(process)
|
||||
item = None
|
||||
if item is not None:
|
||||
info = self._pm2_info_from_item(process, item)
|
||||
if str(info.get("process_status") or "").lower() not in ("online", "running"):
|
||||
await self.clear_process_logs(process)
|
||||
code, text = await run_pm2_result(f"start {process}")
|
||||
@@ -960,16 +1053,7 @@ class WebProcessBackend:
|
||||
await self._pm2_save()
|
||||
self.invalidate_pm2_cache()
|
||||
return text
|
||||
await self.clear_process_logs(process)
|
||||
cmd = build_pm2_start_command(self.base_dir, process, script)
|
||||
if not cmd:
|
||||
return f"ERROR: script not found: {script}"
|
||||
code, text = await _shell_out(cmd, timeout=45.0)
|
||||
if code == 0:
|
||||
await self._set_pm2_desired_running(process, True)
|
||||
await self._pm2_save()
|
||||
self.invalidate_pm2_cache()
|
||||
return text
|
||||
return await self._pm2_start_fresh(process, script)
|
||||
return await self._local.start(process, script)
|
||||
|
||||
async def stop(self, process: str) -> str:
|
||||
@@ -986,9 +1070,12 @@ class WebProcessBackend:
|
||||
async def restart(self, process: str, script: str) -> str:
|
||||
await self.ensure_pm2_runtime()
|
||||
if self._use_pm2:
|
||||
registered = await self._pm2_registered_names()
|
||||
if process not in registered:
|
||||
item = (await self._pm2_jlist_index(force_refresh=True)).get(process)
|
||||
if item is None:
|
||||
return await self.start(process, script)
|
||||
if pm2_process_entry_needs_recreate(self.base_dir, process, script, item):
|
||||
await self._pm2_delete_existing_process(process)
|
||||
return await self._pm2_start_fresh(process, script)
|
||||
await self.clear_process_logs(process)
|
||||
code, text = await run_pm2_result(f"restart {process}")
|
||||
if code == 0:
|
||||
|
||||
Reference in New Issue
Block a user