190 lines
6.0 KiB
Python
190 lines
6.0 KiB
Python
"""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,
|
|
}
|