Merge remote-tracking branch 'origin/sub_laptop_2' into sub_laptop_3
This commit is contained in:
@@ -4,15 +4,28 @@
|
||||
(`build:design-calc`). 부름은 `common_util_node_bundle.run_bundle_json` 한 길 · 계산 없음.
|
||||
`NODE_IDS` = 등록부에서 실행기가 `node:` 인 줄(같은지 시험 `test_472_3_calc_runner.py`).
|
||||
입력 꼴 = 입구 머리(`detail` · `route_points` · `context` · `excavation`).
|
||||
`run_calc_served` = 계산 자리 「서버」 창구(511-A · `B06_Section_Router_Calc`) —
|
||||
늘 떠 있는 Node 하나(`--serve` · 한 줄 요청 · 한 줄 답) · 죽음 · 묶음 다시 지음 = 다시 띄움 ·
|
||||
시험 길 `TRIAL_IDS` 도 받음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
import json
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_node_bundle import run_bundle_json
|
||||
from common_util.common_util_node_bundle import (
|
||||
build_bundle,
|
||||
bundle_stale,
|
||||
node_env,
|
||||
run_bundle_json,
|
||||
)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BUNDLE = ROOT / "config" / "design_calc_node" / "design_calc_node.js"
|
||||
@@ -33,6 +46,20 @@ NODE_IDS = frozenset(
|
||||
"stations.carry",
|
||||
}
|
||||
)
|
||||
# 시험 길(`design_calc_node_trial.ts` `TRIAL` 표와 같음) — 조작 중 브라우저 함수를 서버가 같은 TS 로
|
||||
TRIAL_IDS = frozenset(
|
||||
{
|
||||
"masshaul.series",
|
||||
"stations.carry_keyed",
|
||||
"route.handle",
|
||||
"route.station_fit",
|
||||
"stations.swap",
|
||||
"profile.tools",
|
||||
"cross.fit",
|
||||
"footprint.plan",
|
||||
}
|
||||
)
|
||||
SERVE_TIMEOUT_S = 60
|
||||
|
||||
|
||||
def run_calc(ids: Sequence[str], inputs: dict[str, Any]) -> dict[str, Any] | None:
|
||||
@@ -42,3 +69,71 @@ def run_calc(ids: Sequence[str], inputs: dict[str, Any]) -> dict[str, Any] | Non
|
||||
raise ValueError(f"Node 실행기 아닌 id: {', '.join(unknown)}")
|
||||
output = run_bundle_json(BUNDLE, NPM_SCRIPT, {"run": list(ids), "inputs": inputs})
|
||||
return output.get("results") if isinstance(output, dict) else None
|
||||
|
||||
|
||||
class _Served:
|
||||
"""늘 떠 있는 Node 하나 — 한 번에 한 요청(잠금) · 답 없음 · 죽음 = 끄고 다음 부름에 다시."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.lock = threading.Lock()
|
||||
self.proc: subprocess.Popen[str] | None = None
|
||||
self.stamp = 0
|
||||
|
||||
def _start(self) -> subprocess.Popen[str]:
|
||||
self.stop()
|
||||
self.proc = subprocess.Popen( # noqa: S603 — 고정 실행 파일 · 입력은 표준 입력 한 줄
|
||||
["node", str(BUNDLE), "--serve"],
|
||||
cwd=str(ROOT),
|
||||
env=node_env(),
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
self.stamp = BUNDLE.stat().st_mtime_ns
|
||||
return self.proc
|
||||
|
||||
def stop(self) -> None:
|
||||
if self.proc and self.proc.poll() is None:
|
||||
self.proc.kill()
|
||||
self.proc = None
|
||||
|
||||
def call(self, payload: dict[str, Any]) -> dict[str, Any] | None:
|
||||
line = json.dumps(payload, default=float) + "\n"
|
||||
with self.lock:
|
||||
if bundle_stale(BUNDLE) and not build_bundle(NPM_SCRIPT):
|
||||
return None
|
||||
for _ in range(2): # 죽은 프로세스에 쓴 첫 부름 = 다시 띄워 한 번 더
|
||||
proc = self.proc
|
||||
if not proc or proc.poll() is not None or BUNDLE.stat().st_mtime_ns != self.stamp:
|
||||
proc = self._start()
|
||||
timer = threading.Timer(SERVE_TIMEOUT_S, proc.kill)
|
||||
timer.start()
|
||||
try:
|
||||
proc.stdin.write(line) # type: ignore[union-attr]
|
||||
proc.stdin.flush() # type: ignore[union-attr]
|
||||
answer = proc.stdout.readline() # type: ignore[union-attr]
|
||||
except OSError:
|
||||
answer = ""
|
||||
finally:
|
||||
timer.cancel()
|
||||
if answer:
|
||||
return json.loads(answer)
|
||||
self.stop()
|
||||
return None
|
||||
|
||||
|
||||
_SERVED = _Served()
|
||||
atexit.register(_SERVED.stop)
|
||||
|
||||
|
||||
def run_calc_served(ids: Sequence[str], inputs: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""창구 길 — `{results, ms(Node 셈), wall_ms(파이썬 ↔ Node)}` · `{error}` · 못 돌림 None."""
|
||||
unknown = [item for item in ids if item not in NODE_IDS | TRIAL_IDS]
|
||||
if unknown:
|
||||
raise ValueError(f"서버 계산 id 아님: {', '.join(unknown)}")
|
||||
t0 = time.perf_counter()
|
||||
answer = _SERVED.call({"run": list(ids), "inputs": inputs})
|
||||
if answer is not None:
|
||||
answer["wall_ms"] = round((time.perf_counter() - t0) * 1000, 2)
|
||||
return answer
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""계산 자리 판(PLAN 504-2 · 요구 309-2) — 시스템 관리자 「계산 자리」 창이 읽고 쓰는 판 하나.
|
||||
|
||||
판 = `resources/master_template/calc_sites.json` `{version, sites: {묶음: "both" | "server"}}`.
|
||||
both = 조작 중 브라우저 셈 + [저장] 서버 정본(지금 등록부 자리) · server = 조작 중에도 서버 길.
|
||||
both = 조작 중 브라우저 셈 + [저장] 서버 정본(지금 등록부 자리) · server = 조작 중에도 서버 길
|
||||
(G1 · D3 = 504-2 · ③ 토량 · ④ 이월 · ⑧ 고정 브라우저(시험용 · fixed 는 정본 칸 표시) = 511-A 창구).
|
||||
판이 없거나 깨지면 기본값(전부 both = 등록부 그대로). [저장] · [확정] 서버 정본 길은 판을 안 읽음.
|
||||
묶음 8 줄 = 등록부(`design_calc_registry*.ts`) # 전부 · 같은지 시험 `test_309_2_calc_sites.py`.
|
||||
"""
|
||||
@@ -24,11 +25,11 @@ GROUPS: tuple[dict[str, Any], ...] = (
|
||||
{
|
||||
"key": "quantity",
|
||||
"nos": ["E1", "F7", "F8", "C4"],
|
||||
"switchable": False,
|
||||
"fixed": "both",
|
||||
"why": "later",
|
||||
"switchable": True,
|
||||
"fixed": None,
|
||||
"why": "quantity",
|
||||
},
|
||||
{"key": "carry", "nos": ["R9", "F10"], "switchable": False, "fixed": "both", "why": "later"},
|
||||
{"key": "carry", "nos": ["R9", "F10"], "switchable": True, "fixed": None, "why": "carry"},
|
||||
{
|
||||
"key": "stages",
|
||||
"nos": ["R1", "R3", "R5", "R6", "R7", "G2", "G4", "P1", "P2", "P3", "P4", "P5", "P6"]
|
||||
@@ -54,12 +55,14 @@ GROUPS: tuple[dict[str, Any], ...] = (
|
||||
{
|
||||
"key": "fixed_browser",
|
||||
"nos": ["R2", "R4", "R8", "P7", "C7", "D4"],
|
||||
"switchable": False,
|
||||
"switchable": True,
|
||||
"fixed": "browser",
|
||||
"why": "browser",
|
||||
},
|
||||
)
|
||||
SWITCHABLE = frozenset(g["key"] for g in GROUPS if g["switchable"])
|
||||
# 판 줄 차례 = 묶음 차례(파일 늘 같은 꼴)
|
||||
ORDER = tuple(g["key"] for g in GROUPS if g["switchable"])
|
||||
SWITCHABLE = frozenset(ORDER)
|
||||
LOCKED = frozenset(g["key"] for g in GROUPS if not g["switchable"])
|
||||
|
||||
|
||||
@@ -71,7 +74,7 @@ def read_sites() -> dict[str, Any]:
|
||||
raw = {}
|
||||
stored = raw.get("sites") if isinstance(raw, dict) else None
|
||||
stored = stored if isinstance(stored, dict) else {}
|
||||
sites = {key: stored.get(key) if stored.get(key) in VALUES else "both" for key in SWITCHABLE}
|
||||
sites = {k: stored.get(k) if stored.get(k) in VALUES else "both" for k in ORDER}
|
||||
version = raw.get("version") if isinstance(raw, dict) else None
|
||||
return {"version": version if isinstance(version, int) else 0, "sites": sites}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user