- preview_up.py --port N · --replace-dev(빌드가 된 뒤에만 개발 포트 vite dev 를 끄고 그 자리에 빌드판) - 빌드 실패 = 아무것도 안 끔 · 옛 빌드판이 그 자리면 끄고 개발판으로 - 기본(개발 포트 − 1000) 그대로 · 지킴 시험 test_570_1_preview_up - (git 밖 .claude) dev_up.py --build · git-sync-all.ps1 -Global 데스크탑 메인 = WMI 로 dev_up --build · 5173 빌드판 확인 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
151 lines
5.9 KiB
Python
151 lines
5.9 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""빌드판 서버(555-2) — 앱 · CAD 를 빌드하고 `vite preview` 를 띄운다(폰 · 외부 접속용).
|
||
|
||
실행: ./venv/Scripts/python.exe tools/preview_up.py (빌드 + 띄움 · 수십 초)
|
||
./venv/Scripts/python.exe tools/preview_up.py --stop (끄기만)
|
||
--port N : 띄울 포트(기본 = 개발 포트 − 1000)
|
||
--replace-dev : 개발판 자리(개발 포트)에 띄움 — 빌드가 된 뒤에만 그 포트의 vite dev 를 끔
|
||
· 빌드 실패면 아무것도 안 끄고 1 로 끝남(개발판 그대로)
|
||
· `.claude/dev_up.py --build` 가 부름
|
||
|
||
- 개발 서버(vite)는 파일 수백 개를 하나씩 보내 느린 연결(Tailscale · 폰)에서 첫 화면이 늦다 —
|
||
빌드판은 묶은 파일 몇 개만 보낸다.
|
||
- 포트 = 개발 포트 − 1000(메인 5173 → 4173 · 서브 5181 → 4181) · 개발 포트 · API 포트는
|
||
환경변수(AISLO_VITE_PORT · AISLO_API_PORT) > `OWNERS.md` 이 폴더 줄 > 8000 · 5173.
|
||
- /api · /b07-cad 는 같은 폴더 백엔드로 넘김(`vite.config.ts` preview) — 백엔드는 따로 켜 둠.
|
||
- 빌드 산출물 = config/node_modules/.build · CAD dist(둘 다 git 밖) · 로그 tmp/preview.log.
|
||
- 다시 부르면 옛 것을 끄고 새로 빌드해 띄움(preview 는 띄울 때의 파일 목록을 기억).
|
||
"""
|
||
|
||
import os
|
||
import re
|
||
import subprocess
|
||
import sys
|
||
import time
|
||
import urllib.request
|
||
|
||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
TMP = os.path.join(ROOT, "tmp")
|
||
PID_FILE = os.path.join(TMP, "preview.pid")
|
||
LOG = os.path.join(TMP, "preview.log")
|
||
VITE = os.path.join(ROOT, "config", "node_modules", "vite", "bin", "vite.js")
|
||
|
||
|
||
def owners_ports() -> tuple[int, int] | None:
|
||
"""OWNERS.md 표에서 이 폴더 줄의 (API, vite) 포트 — 짝 = `.claude/dev_up.py`."""
|
||
try:
|
||
with open(os.path.join(ROOT, "OWNERS.md"), encoding="utf-8-sig") as owners:
|
||
lines = owners.read().splitlines()
|
||
except OSError:
|
||
return None
|
||
here = os.path.normcase(os.path.normpath(ROOT))
|
||
for line in lines:
|
||
cells = [cell.strip().strip("`") for cell in line.strip().strip("|").split("|")]
|
||
if len(cells) >= 5 and os.path.normcase(os.path.normpath(cells[2])) == here:
|
||
ports = re.findall(r"\d{4,5}", cells[4])
|
||
if len(ports) == 2:
|
||
return int(ports[0]), int(ports[1])
|
||
return None
|
||
|
||
|
||
def stop() -> None:
|
||
try:
|
||
with open(PID_FILE, encoding="utf-8") as f:
|
||
pid = int(f.read().strip())
|
||
except (OSError, ValueError):
|
||
return
|
||
subprocess.run(["taskkill", "/T", "/F", "/PID", str(pid)], capture_output=True, check=False)
|
||
os.remove(PID_FILE)
|
||
|
||
|
||
def free_port(port: int) -> None:
|
||
"""그 포트를 물고 있는 프로세스(개발판 vite)를 끈다 — 짝 = `_free_dev_port`(config)."""
|
||
out = subprocess.run(
|
||
["netstat", "-ano", "-p", "TCP"], capture_output=True, text=True, errors="replace"
|
||
).stdout
|
||
for line in out.splitlines():
|
||
cols = line.split()
|
||
if (
|
||
len(cols) >= 5
|
||
and cols[1].endswith(f":{port}")
|
||
and cols[3] == "LISTENING"
|
||
and cols[4].isdigit()
|
||
):
|
||
subprocess.run(
|
||
["taskkill", "/T", "/F", "/PID", cols[4]], capture_output=True, check=False
|
||
)
|
||
|
||
|
||
def arg(name: str) -> str | None:
|
||
return sys.argv[sys.argv.index(name) + 1] if name in sys.argv[:-1] else None
|
||
|
||
|
||
def spawn(cmd: list[str], env: dict[str, str]) -> subprocess.Popen:
|
||
log = open(LOG, "ab") # noqa: SIM115 — 자식 프로세스가 물려받는다
|
||
flags = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
|
||
return subprocess.Popen(
|
||
cmd, cwd=ROOT, env=env, stdout=log, stderr=subprocess.STDOUT, creationflags=flags
|
||
)
|
||
|
||
|
||
def run(cmd: list[str], env: dict[str, str]) -> None:
|
||
print("RUN", " ".join(cmd), flush=True)
|
||
subprocess.run(cmd, cwd=ROOT, env=env, check=True, shell=os.name == "nt" and cmd[0] == "npm")
|
||
|
||
|
||
def main() -> int:
|
||
os.makedirs(TMP, exist_ok=True)
|
||
replace = "--replace-dev" in sys.argv
|
||
if not replace:
|
||
stop()
|
||
if "--stop" in sys.argv:
|
||
print("PREVIEW=stopped")
|
||
return 0
|
||
|
||
api, vite = owners_ports() or (8000, 5173)
|
||
api = int(os.environ.get("AISLO_API_PORT") or api)
|
||
vite = int(os.environ.get("AISLO_VITE_PORT") or vite)
|
||
port = int(arg("--port") or (vite if replace else vite - 1000))
|
||
env = {**os.environ, "AISLO_API_PORT": str(api), "FRONTEND_DEV_PORT": str(vite)}
|
||
|
||
try:
|
||
run(["node", VITE, "build", "--configLoader", "runner"], env)
|
||
run(["npm", "run", "build:b07-cad"], env)
|
||
except subprocess.CalledProcessError as exc:
|
||
# 옛 빌드판이 자리에 있으면 파일 목록이 낡음 — 끄고 개발판으로
|
||
if replace and os.path.exists(PID_FILE):
|
||
stop()
|
||
free_port(port)
|
||
time.sleep(1)
|
||
spawn(["node", VITE, "--configLoader", "runner", "--host", "--port", str(port)], env)
|
||
print(f"PREVIEW=BUILD_FAIL ({exc.returncode}) — 옛 빌드판을 끄고 {port} 개발판")
|
||
else:
|
||
print(f"PREVIEW=BUILD_FAIL ({exc.returncode}) — 아무것도 안 끔")
|
||
return 1
|
||
if replace: # 빌드가 된 뒤에만 — 옛 빌드판 · 그 포트의 개발판을 끔
|
||
stop()
|
||
free_port(port)
|
||
time.sleep(1)
|
||
|
||
process = spawn(
|
||
["node", VITE, "preview", "--configLoader", "runner", "--port", str(port), "--strictPort"],
|
||
env,
|
||
)
|
||
with open(PID_FILE, "w", encoding="utf-8") as f:
|
||
f.write(str(process.pid))
|
||
|
||
url = f"http://127.0.0.1:{port}/"
|
||
for _ in range(60):
|
||
try:
|
||
urllib.request.urlopen(url, timeout=2)
|
||
print(f"PREVIEW=up {url} (API {api}) · 로그 {LOG}")
|
||
return 0
|
||
except OSError:
|
||
time.sleep(0.5)
|
||
print(f"PREVIEW=TIMEOUT — {LOG} 확인")
|
||
return 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|