"""Dev-only: push local project sources to the USB peer (manual sync_peer.sh).""" from __future__ import annotations import os from pathlib import Path from typing import Any, Callable, Optional from .protocol import Session _ROOT = Path(__file__).resolve().parent.parent # Keep in sync with scripts/sync_peer.sh default set. SYNC_XFER = ( "juc500_xfer/gui_server.py", "juc500_xfer/usb_link.py", "juc500_xfer/inputshare.py", "juc500_xfer/protocol.py", "juc500_xfer/peer_sync.py", ) SYNC_STATIC = ( "juc500_xfer/static/app.js", "juc500_xfer/static/index.html", "juc500_xfer/static/styles.css", ) def _use_mac_peer_paths() -> bool: return os.environ.get("USE_MAC_PEER_PATHS", "").strip() in ("1", "true", "yes") def peer_dest_dirs() -> dict[str, str]: """Destination directories on the peer filesystem.""" if _use_mac_peer_paths(): base = "dev/juc500/juc500" return { "xfer": f"{base}/juc500_xfer", "static": f"{base}/juc500_xfer/static", } root = os.environ.get("JUC500_PEER_ROOT", "C:/dev/juc500").replace("\\", "/").rstrip("/") alt = os.environ.get("JUC500_PEER_ROOT_ALT", "C:/dev/juc500/juc500").replace("\\", "/").rstrip("/") return { "xfer": f"{root}/juc500_xfer", "static": f"{root}/juc500_xfer/static", "xfer_alt": f"{alt}/juc500_xfer", "static_alt": f"{alt}/juc500_xfer/static", } def _local_paths(rel_paths: tuple[str, ...]) -> list[Path]: out: list[Path] = [] for rel in rel_paths: p = (_ROOT / rel).resolve() if p.is_file(): out.append(p) return out def _send_batch( session: Session, dest_dir: str, files: list[Path], *, on_log: Callable[[str], None], on_progress: Optional[Callable[[int, int, str], None]] = None, ) -> int: if not files: return 0 dest = dest_dir.replace("\\", "/").rstrip("/") count = 0 for local in files: remote_name = f"{dest}/{local.name}" if dest else local.name size = local.stat().st_size if on_progress: on_progress(0, size, local.name) session.on_progress = ( (lambda done, total, name=local.name: on_progress(done, total, name)) if on_progress else None ) timeout = 180.0 if size > 32 * 1024 * 1024 else 120.0 session.send_file(local, remote_name=remote_name, accept_timeout=timeout) count += 1 on_log(f"상대 동기화: {local.name} → {dest}") return count def peer_ping(session: Session, *, timeout: float = 10.0) -> dict[str, Any]: try: result = session.rpc("ping", timeout=timeout) return result if isinstance(result, dict) else {} except Exception: return {} def local_build_ids() -> tuple[str, str]: try: from .inputshare import INPUTSHARE_BUILD inputshare = INPUTSHARE_BUILD except Exception: inputshare = "unknown" try: from .usb_link import XFER_BUILD xfer = XFER_BUILD except Exception: xfer = "unknown" return inputshare, xfer def peer_needs_restart(session: Session) -> bool: ping = peer_ping(session) local_inputshare, local_xfer = local_build_ids() peer_inputshare = str(ping.get("inputshare_build") or "unknown") peer_xfer = str(ping.get("xfer_build") or "unknown") if peer_xfer == "unknown": return True return peer_inputshare != local_inputshare or peer_xfer != local_xfer def request_peer_restart(session: Session, *, on_log: Callable[[str], None]) -> bool: try: session.rpc("restart_gui", timeout=12.0) on_log("상대편 GUI 재시작 예약됨 — 프로세스가 새 코드를 로드합니다") return True except Exception as exc: on_log(f"상대 재시작 RPC 실패: {exc} — 상대 PC에서 ./start.sh 수동 실행 필요") return False def push_sources_only( session: Session, *, on_log: Callable[[str], None], on_progress: Optional[Callable[[int, int, str], None]] = None, ) -> int: xfer_files = _local_paths(SYNC_XFER) static_files = _local_paths(SYNC_STATIC) dests = peer_dest_dirs() total = 0 for key in ("xfer", "static"): files = xfer_files if key == "xfer" else static_files dest = dests.get(key) if dest: total += _send_batch(session, dest, files, on_log=on_log, on_progress=on_progress) for key, alt_key in (("xfer", "xfer_alt"), ("static", "static_alt")): alt = dests.get(alt_key) if not alt or alt == dests.get(key): continue files = xfer_files if key == "xfer" else static_files total += _send_batch(session, alt, files, on_log=on_log, on_progress=on_progress) return total def sync_sources_to_peer( session: Session, *, on_log: Callable[[str], None], on_progress: Optional[Callable[[int, int, str], None]] = None, restart_if_stale: bool = False, ) -> dict[str, Any]: """Send default source bundle to peer. Optionally restart peer to load new code.""" on_log("상대편 소스 자동 동기화 시작…") total = push_sources_only(session, on_log=on_log, on_progress=on_progress) if total <= 0: return {"ok": False, "error": "동기화할 로컬 파일 없음", "count": 0} local_inputshare, local_xfer = local_build_ids() ping = peer_ping(session) peer_inputshare = str(ping.get("inputshare_build") or "unknown") peer_xfer = str(ping.get("xfer_build") or "unknown") restart = False if restart_if_stale and ( peer_xfer != local_xfer or peer_inputshare != local_inputshare ): on_log( f"상대 build 불일치 (xfer 로컬 {local_xfer} · 상대 {peer_xfer}) — 재시작 요청" ) restart = request_peer_restart(session, on_log=on_log) on_log(f"상대편 소스 동기화 완료 ({total}개 파일)") return { "ok": True, "count": total, "local_xfer_build": local_xfer, "peer_xfer_build": peer_xfer, "restart_requested": restart, }