feat(tools): 빌드판 포트 지정 · 개발판 자리에 빌드판 (PLAN 570-1 · 요구 398)
- 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>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
"""570-1 빌드판 자리 — 기본 = 개발 포트 − 1000 · --replace-dev = 개발 포트 · 빌드 실패면 안 끔."""
|
||||
|
||||
import importlib.util
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
SRC = Path(__file__).resolve().parents[2] / "tools" / "preview_up.py"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pu(monkeypatch, tmp_path):
|
||||
spec = importlib.util.spec_from_file_location("preview_up_570", SRC)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
calls: list = []
|
||||
monkeypatch.setattr(mod, "TMP", str(tmp_path))
|
||||
monkeypatch.setattr(mod, "PID_FILE", str(tmp_path / "preview.pid"))
|
||||
monkeypatch.setattr(mod, "LOG", str(tmp_path / "preview.log"))
|
||||
monkeypatch.setattr(mod, "owners_ports", lambda: (8001, 5174))
|
||||
monkeypatch.setattr(mod, "stop", lambda: calls.append("stop"))
|
||||
monkeypatch.setattr(mod, "free_port", lambda port: calls.append(("free", port)))
|
||||
monkeypatch.setattr(mod, "run", lambda cmd, env: None)
|
||||
monkeypatch.setattr(mod.time, "sleep", lambda s: None)
|
||||
monkeypatch.setattr(mod.urllib.request, "urlopen", lambda url, timeout: calls.append(url))
|
||||
|
||||
class Proc:
|
||||
pid = 1
|
||||
|
||||
monkeypatch.setattr(mod, "spawn", lambda cmd, env: calls.append(("spawn", cmd)) or Proc())
|
||||
monkeypatch.delenv("AISLO_VITE_PORT", raising=False)
|
||||
monkeypatch.delenv("AISLO_API_PORT", raising=False)
|
||||
mod.calls = calls
|
||||
return mod
|
||||
|
||||
|
||||
def spawned_port(calls) -> str:
|
||||
cmd = next(c[1] for c in calls if isinstance(c, tuple) and c[0] == "spawn")
|
||||
return cmd[cmd.index("--port") + 1]
|
||||
|
||||
|
||||
def test_default_dev_minus_1000(pu, monkeypatch):
|
||||
monkeypatch.setattr(pu.sys, "argv", ["preview_up.py"])
|
||||
assert pu.main() == 0
|
||||
assert spawned_port(pu.calls) == "4174"
|
||||
assert not any(isinstance(c, tuple) and c[0] == "free" for c in pu.calls)
|
||||
|
||||
|
||||
def test_port_arg(pu, monkeypatch):
|
||||
monkeypatch.setattr(pu.sys, "argv", ["preview_up.py", "--port", "4999"])
|
||||
assert pu.main() == 0
|
||||
assert spawned_port(pu.calls) == "4999"
|
||||
|
||||
|
||||
def test_replace_dev_takes_dev_port_after_build(pu, monkeypatch):
|
||||
monkeypatch.setattr(pu.sys, "argv", ["preview_up.py", "--replace-dev"])
|
||||
assert pu.main() == 0
|
||||
assert spawned_port(pu.calls) == "5174"
|
||||
assert pu.calls.index(("free", 5174)) < next(
|
||||
i for i, c in enumerate(pu.calls) if isinstance(c, tuple) and c[0] == "spawn"
|
||||
)
|
||||
|
||||
|
||||
def test_replace_dev_build_fail_kills_nothing(pu, monkeypatch):
|
||||
def boom(cmd, env):
|
||||
raise subprocess.CalledProcessError(1, cmd)
|
||||
|
||||
monkeypatch.setattr(pu, "run", boom)
|
||||
monkeypatch.setattr(pu.sys, "argv", ["preview_up.py", "--replace-dev"])
|
||||
assert pu.main() == 1
|
||||
assert pu.calls == []
|
||||
+59
-13
@@ -3,6 +3,10 @@
|
||||
|
||||
실행: ./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 · 폰)에서 첫 화면이 늦다 —
|
||||
빌드판은 묶은 파일 몇 개만 보낸다.
|
||||
@@ -54,6 +58,36 @@ def stop() -> None:
|
||||
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")
|
||||
@@ -61,7 +95,9 @@ def run(cmd: list[str], env: dict[str, str]) -> None:
|
||||
|
||||
def main() -> int:
|
||||
os.makedirs(TMP, exist_ok=True)
|
||||
stop()
|
||||
replace = "--replace-dev" in sys.argv
|
||||
if not replace:
|
||||
stop()
|
||||
if "--stop" in sys.argv:
|
||||
print("PREVIEW=stopped")
|
||||
return 0
|
||||
@@ -69,21 +105,31 @@ def main() -> int:
|
||||
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 = vite - 1000
|
||||
port = int(arg("--port") or (vite if replace else vite - 1000))
|
||||
env = {**os.environ, "AISLO_API_PORT": str(api), "FRONTEND_DEV_PORT": str(vite)}
|
||||
|
||||
run(["node", VITE, "build", "--configLoader", "runner"], env)
|
||||
run(["npm", "run", "build:b07-cad"], env)
|
||||
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)
|
||||
|
||||
log = open(LOG, "ab") # noqa: SIM115 — 자식 프로세스가 물려받는다
|
||||
flags = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
process = subprocess.Popen(
|
||||
["node", VITE, "preview", "--configLoader", "runner"],
|
||||
cwd=ROOT,
|
||||
env=env,
|
||||
stdout=log,
|
||||
stderr=subprocess.STDOUT,
|
||||
creationflags=flags,
|
||||
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))
|
||||
|
||||
Reference in New Issue
Block a user