优化直播控制台:引入进程监控轻量轮询,并拆分 Live UI 组件
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
158
web2.py
158
web2.py
@@ -29,7 +29,8 @@ from web2_youtube_oauth import (
|
||||
oauth_status as youtube_oauth_status_snapshot,
|
||||
)
|
||||
from web2_client_overview import build_client_overview
|
||||
from web2_process_runtime import summarize_process_activity
|
||||
from web2_process_runtime import process_family, summarize_process_activity
|
||||
from web2_douyin_source_display import extract_douyin_source_meta
|
||||
from web2_config_governance import (
|
||||
list_config_backups,
|
||||
restore_config_backup,
|
||||
@@ -67,6 +68,7 @@ from web2_relay_pro import (
|
||||
watch_url_for_relay_process,
|
||||
write_url_config_for_channel,
|
||||
)
|
||||
from web2_youtube_preflight import youtube_start_preflight_message
|
||||
from web2_youtube_ini import parse_youtube_ini, serialize_youtube_ini, validate_youtube_keys
|
||||
from web2_host_caps import gpu_status_payload, machine_summary_payload
|
||||
from web2_camera import camera_config_ready, list_capture_devices, load_camera_config, save_camera_config
|
||||
@@ -555,7 +557,12 @@ def _ffmpeg_pids_for_process(info: dict, process_name: str) -> list[int]:
|
||||
if "ffmpeg" not in name:
|
||||
continue
|
||||
flat = " ".join(str(x) for x in (proc.info.get("cmdline") or [])).lower()
|
||||
if needle in flat and int(proc.info["pid"]) not in seen:
|
||||
env_name = ""
|
||||
try:
|
||||
env_name = str(proc.environ().get("LIVE_PM2_PROCESS_NAME") or "").strip().lower()
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||
env_name = ""
|
||||
if env_name == needle or (needle in flat and int(proc.info["pid"]) not in seen):
|
||||
pids.append(int(proc.info["pid"]))
|
||||
seen.add(int(proc.info["pid"]))
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess, ValueError):
|
||||
@@ -649,26 +656,48 @@ async def _youtube_guard_rows(targets: list[str]) -> tuple[list[dict], dict[str,
|
||||
return rows, config_by_process
|
||||
|
||||
|
||||
async def _status_payload_for_process(process: str) -> dict:
|
||||
async def _status_payload_for_process(process: str, *, light: bool = False) -> dict:
|
||||
info = await control_get_process_info(process)
|
||||
recent_log = await read_log_path(info["out_path"], MAX_LOG_LINES)
|
||||
recent_error = await read_log_path(info["err_path"], MAX_LOG_LINES)
|
||||
if info["process_status"] == "not_found":
|
||||
ps = str(info.get("process_status") or "")
|
||||
if ps == "not_found":
|
||||
recent_log = "进程未运行或未由本机管理器启动。\n"
|
||||
recent_error = ""
|
||||
ffmpeg_pids = _ffmpeg_pids_for_process(info, process)
|
||||
out_age = _log_age_seconds(info.get("out_path"))
|
||||
err_age = _log_age_seconds(info.get("err_path"))
|
||||
ages = [x for x in (out_age, err_age) if x is not None]
|
||||
log_age_seconds = min(ages) if ages else None
|
||||
ffmpeg_pids: list[int] = []
|
||||
log_age_seconds = None
|
||||
elif ps in {"stopped", "stopping"}:
|
||||
recent_log = ""
|
||||
recent_error = ""
|
||||
ffmpeg_pids = []
|
||||
log_age_seconds = None
|
||||
else:
|
||||
online = ps in {"online", "running", "launching"}
|
||||
read_logs = online or not light
|
||||
if light and process_family(process) == "youtube":
|
||||
log_lines = 80
|
||||
err_lines = 24
|
||||
else:
|
||||
log_lines = MAX_LOG_LINES
|
||||
err_lines = MAX_LOG_LINES
|
||||
if read_logs:
|
||||
recent_log = await read_log_path(info["out_path"], log_lines)
|
||||
recent_error = await read_log_path(info["err_path"], err_lines)
|
||||
else:
|
||||
recent_log = ""
|
||||
recent_error = ""
|
||||
ffmpeg_pids = _ffmpeg_pids_for_process(info, process) if online else []
|
||||
out_age = _log_age_seconds(info.get("out_path"))
|
||||
err_age = _log_age_seconds(info.get("err_path"))
|
||||
ages = [x for x in (out_age, err_age) if x is not None]
|
||||
log_age_seconds = min(ages) if ages else None
|
||||
activity = summarize_process_activity(
|
||||
process,
|
||||
str(info["process_status"]),
|
||||
ps,
|
||||
recent_log,
|
||||
recent_error,
|
||||
log_age_seconds=log_age_seconds,
|
||||
ffmpeg_pids=ffmpeg_pids,
|
||||
)
|
||||
source_meta = _youtube_source_meta(process, recent_log)
|
||||
return {
|
||||
"process": process,
|
||||
"process_status": info["process_status"],
|
||||
@@ -685,9 +714,17 @@ async def _status_payload_for_process(process: str) -> dict:
|
||||
"ffmpeg_speed": activity.get("ffmpeg_speed"),
|
||||
"ffmpeg_progress_stalled": activity.get("ffmpeg_progress_stalled"),
|
||||
"ffmpeg_speed_low": activity.get("ffmpeg_speed_low"),
|
||||
**source_meta,
|
||||
}
|
||||
|
||||
|
||||
def _youtube_source_meta(process: str, recent_log: str = "") -> dict:
|
||||
sc = script_for_pm2(process)
|
||||
if not sc or not is_youtube_script(sc):
|
||||
return {}
|
||||
return extract_douyin_source_meta(_url_lines_for_process(process), recent_log)
|
||||
|
||||
|
||||
# ---------------- 配置路由(youtube.ini,仅 youtube) ----------------
|
||||
@app.get("/get_config")
|
||||
async def get_config(process: str = Query(..., description="进程名")):
|
||||
@@ -984,6 +1021,10 @@ async def start(request: Request, process: str = Query(..., description="进程
|
||||
cam_msg = _stream_worker_ready_message(process)
|
||||
if cam_msg:
|
||||
return JSONResponse({"output": cam_msg}, status_code=400)
|
||||
if is_youtube_script(sc):
|
||||
preflight_msg = await youtube_start_preflight_message()
|
||||
if preflight_msg:
|
||||
return JSONResponse({"output": preflight_msg}, status_code=400)
|
||||
output = await control_start(process)
|
||||
return JSONResponse({"output": output})
|
||||
|
||||
@@ -1045,12 +1086,20 @@ async def restart(request: Request, process: str = Query(..., description="进
|
||||
cam_msg = _stream_worker_ready_message(process)
|
||||
if cam_msg:
|
||||
return JSONResponse({"output": f"{stop_output}\n{cam_msg}"}, status_code=400)
|
||||
sc = script_for_pm2(process)
|
||||
if is_youtube_script(sc):
|
||||
preflight_msg = await youtube_start_preflight_message()
|
||||
if preflight_msg:
|
||||
return JSONResponse({"output": f"{stop_output}\n{preflight_msg}"}, status_code=400)
|
||||
start_output = await control_start(process)
|
||||
return JSONResponse({"output": f"{stop_output}\n{start_output}"})
|
||||
|
||||
|
||||
@app.get("/status")
|
||||
async def status(process: str = Query(..., description="进程名")):
|
||||
async def status(
|
||||
process: str = Query(..., description="进程名"),
|
||||
light: bool = Query(False, description="轻量模式:减少日志读取,适合轮询"),
|
||||
):
|
||||
if not process_valid(process):
|
||||
return JSONResponse({
|
||||
"raw_status": "",
|
||||
@@ -1059,9 +1108,11 @@ async def status(process: str = Query(..., description="进程名")):
|
||||
"recent_error": ""
|
||||
})
|
||||
|
||||
full_status_raw = await control_full_status()
|
||||
full_status = strip_ansi_codes(full_status_raw)
|
||||
payload = await _status_payload_for_process(process)
|
||||
full_status = ""
|
||||
if not light:
|
||||
full_status_raw = await control_full_status()
|
||||
full_status = strip_ansi_codes(full_status_raw)
|
||||
payload = await _status_payload_for_process(process, light=light)
|
||||
ps = str(payload["process_status"])
|
||||
|
||||
prev = _PROCESS_STATUS_CACHE.get(process)
|
||||
@@ -1277,8 +1328,50 @@ async def config_restore(request: Request):
|
||||
return {"message": msg}
|
||||
|
||||
|
||||
def _process_monitor_family_matches(entry: dict, family: str | None) -> bool:
|
||||
target = (family or "").strip().lower()
|
||||
if not target or target == "all":
|
||||
return True
|
||||
script = str(entry.get("script") or "")
|
||||
pm2 = str(entry.get("pm2") or "")
|
||||
if target == "youtube":
|
||||
return is_youtube_script(script) or pm2.startswith("youtube2__")
|
||||
if target == "tiktok":
|
||||
return is_tiktok_script(script)
|
||||
return True
|
||||
|
||||
|
||||
async def _process_monitor_snapshot(entry: dict, *, light: bool = True) -> dict:
|
||||
pm = str(entry.get("pm2") or "").strip()
|
||||
snap = await _status_payload_for_process(pm, light=light)
|
||||
return {
|
||||
"pm2": pm,
|
||||
"script": str(entry.get("script") or ""),
|
||||
"label": str(entry.get("label") or pm),
|
||||
"process_status": snap.get("process_status", ""),
|
||||
"business_status": snap.get("business_status", ""),
|
||||
"business_note": snap.get("business_note", ""),
|
||||
"is_pushing": bool(snap.get("is_pushing")),
|
||||
"ffmpeg_active": snap.get("ffmpeg_active"),
|
||||
"ffmpeg_speed": snap.get("ffmpeg_speed"),
|
||||
"ffmpeg_frame": snap.get("ffmpeg_frame"),
|
||||
"ffmpeg_time_seconds": snap.get("ffmpeg_time_seconds"),
|
||||
"ffmpeg_progress_stalled": snap.get("ffmpeg_progress_stalled"),
|
||||
"ffmpeg_speed_low": snap.get("ffmpeg_speed_low"),
|
||||
"source_display": snap.get("source_display", ""),
|
||||
"source_title": snap.get("source_title", ""),
|
||||
"source_room_id": snap.get("source_room_id", ""),
|
||||
"source_url": snap.get("source_url", ""),
|
||||
"source_ambiguous": snap.get("source_ambiguous", False),
|
||||
"source_current": snap.get("source_current", False),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/process_monitor")
|
||||
async def process_monitor():
|
||||
async def process_monitor(
|
||||
family: str | None = Query(None, description="按进程族过滤:youtube/tiktok"),
|
||||
light: bool = Query(False, description="轻量快照:含 business_status / is_pushing"),
|
||||
):
|
||||
ensure_default_relay_config(CONFIG_DIR)
|
||||
entries = [
|
||||
{"pm2": p["pm2"], "script": p["script"], "label": p["label"]}
|
||||
@@ -1297,7 +1390,36 @@ async def process_monitor():
|
||||
"label": f"YouTube {ch.get('name', cid)}",
|
||||
}
|
||||
)
|
||||
return {"entries": entries}
|
||||
family_value = (family or "").strip().lower() or None
|
||||
if family_value:
|
||||
entries = [e for e in entries if _process_monitor_family_matches(e, family_value)]
|
||||
if not light and not family_value:
|
||||
return {"entries": entries}
|
||||
results = await asyncio.gather(
|
||||
*(_process_monitor_snapshot(e, light=bool(light)) for e in entries),
|
||||
return_exceptions=True,
|
||||
)
|
||||
snapshots: list[dict] = []
|
||||
for entry, result in zip(entries, results):
|
||||
if isinstance(result, Exception):
|
||||
snapshots.append(
|
||||
{
|
||||
"pm2": entry["pm2"],
|
||||
"script": entry.get("script", ""),
|
||||
"label": entry.get("label", entry["pm2"]),
|
||||
"process_status": "error",
|
||||
"business_status": "error",
|
||||
"business_note": "Failed to inspect process state",
|
||||
"is_pushing": False,
|
||||
}
|
||||
)
|
||||
else:
|
||||
snapshots.append(result)
|
||||
return {
|
||||
"entries": snapshots,
|
||||
"family": family_value or "all",
|
||||
"light": bool(light),
|
||||
}
|
||||
|
||||
|
||||
# ---------------- 首页 ----------------
|
||||
|
||||
Reference in New Issue
Block a user