#!/usr/bin/env python3 """Batch detect target_path via SSH (find crmeb directory under /www/wwwroot). Targets servers with「未设路径」(empty or ``/www/wwwroot`` placeholder) by default. Usage: python scripts/batch_detect_target_path.py --unset-only python scripts/batch_detect_target_path.py --ids 245,246 python scripts/batch_detect_target_path.py --name 温胜夜 --dry-run """ from __future__ import annotations import argparse import asyncio import os import sys from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) def _load_dotenv() -> None: env_path = ROOT / ".env" if not env_path.is_file(): return for line in env_path.read_text(encoding="utf-8", errors="replace").splitlines(): line = line.strip() if not line or line.startswith("#") or "=" not in line: continue key, _, value = line.partition("=") key, value = key.strip(), value.strip().strip("\"'") if key.startswith("NEXUS_") and key[6:] not in os.environ: os.environ[key[6:]] = value elif key and key not in os.environ: os.environ[key] = value async def _fetch_servers( session: Any, *, ids: list[int] | None, name_like: str | None, unset_only: bool, limit: int, ) -> list[Any]: from sqlalchemy import or_, select from server.domain.models import Server from server.infrastructure.database.server_repo import _target_path_unset_filter q = select(Server).order_by(Server.id) if ids: q = q.where(Server.id.in_(ids)) if name_like: q = q.where(or_(Server.name.like(f"%{name_like}%"), Server.domain.like(f"%{name_like}%"))) if unset_only: q = q.where(_target_path_unset_filter(True)) result = await session.execute(q) servers = list(result.scalars().all()) if limit > 0: servers = servers[:limit] return servers async def _detect_one_server(server: Any) -> dict[str, Any]: from server.application.server_batch_common import detect_and_save_target_path from server.application.services.server_batch_service import result_item_dict sid = server.id label = server.name or str(sid) try: outcome = await detect_and_save_target_path(server) if outcome.get("success"): return result_item_dict( server_id=sid, server_name=label, success=True, stdout=str(outcome.get("stdout") or ""), ) return result_item_dict( server_id=sid, server_name=label, success=False, error=str(outcome.get("error") or "探测失败"), ) except Exception as e: return result_item_dict(server_id=sid, server_name=label, success=False, error=str(e)[:300]) async def run_detect(args: argparse.Namespace) -> list[dict[str, Any]]: from server.config import settings from server.infrastructure.database.session import AsyncSessionLocal, init_db from server.infrastructure.redis.client import close_redis, init_redis await init_db() await init_redis(settings.REDIS_URL) ids: list[int] | None = None if args.ids: ids = [int(x.strip()) for x in args.ids.split(",") if x.strip()] async with AsyncSessionLocal() as session: servers = await _fetch_servers( session, ids=ids, name_like=args.name, unset_only=args.unset_only, limit=args.limit, ) if not servers: print("No servers matched.", file=sys.stderr) await close_redis() return [] if args.dry_run: for s in servers: print(f"[DRY] #{s.id} {s.name} target_path={s.target_path!r}") print(f"\nWould detect: {len(servers)} servers") await close_redis() return [{"server_id": s.id, "dry_run": True} for s in servers] sem = asyncio.Semaphore(max(1, getattr(args, "concurrency", 8))) async def _one(server: Any) -> dict[str, Any]: async with sem: return await _detect_one_server(server) results = await asyncio.gather(*[_one(s) for s in servers]) await close_redis() ok = sum(1 for r in results if r.get("success")) fail = len(results) - ok print(f"\nDetect: {ok} ok / {fail} fail / {len(results)} total") for r in results: status = "OK" if r.get("success") else "FAIL" print( f"[{status}] #{r.get('server_id')} {r.get('server_name')}: " f"{r.get('stdout') or r.get('error') or ''}" ) return list(results) def main() -> int: _load_dotenv() parser = argparse.ArgumentParser(description="Batch detect server target_path") parser.add_argument("--ids", type=str, help="Comma-separated server ids") parser.add_argument("--name", type=str, help="Filter name/domain LIKE") parser.add_argument("--unset-only", action="store_true", help="Only「未设路径」servers") parser.add_argument("--limit", type=int, default=0) parser.add_argument("--concurrency", type=int, default=8) parser.add_argument("--dry-run", action="store_true") args = parser.parse_args() if not args.ids and not args.name and not args.unset_only: parser.error("Specify --ids, --name, or --unset-only") results = asyncio.run(run_detect(args)) if args.dry_run: return 0 if results else 2 return 0 if results and all(r.get("success") for r in results) else 1 if __name__ == "__main__": raise SystemExit(main())