"""Runner patch 2 (2026-10-12): separate start stall from long jobs; fix window-title decoding.""" from pathlib import Path APP = Path(r"C:\Users\User\Desktop\바이브코딩\pier_T\Code\교각모델링앱") def patch(rel, reps): p = APP / rel s = p.read_text(encoding="utf-8") for a, b in reps: assert s.count(a) == 1, (rel, a[:60]) s = s.replace(a, b) p.write_text(s, encoding="utf-8") patch("runner.py", [ ("""SPEC = json.load(open(r"%(spec)s", encoding="utf-8")) doc = Rhino.RhinoDoc.ActiveDoc""", """SPEC = json.load(open(r"%(spec)s", encoding="utf-8")) open(SPEC["started"], "w").write(time.strftime("%%H:%%M:%%S")) doc = Rhino.RhinoDoc.ActiveDoc"""), ("""class RhinoJobError(RuntimeError): pass """, """class RhinoJobError(RuntimeError): pass class RhinoStartError(RhinoJobError): \"\"\"Rhino did not start the job script in time (retryable).\"\"\" """), (""" out = subprocess.run(["powershell", "-NoProfile", "-Command", f"(Get-Process -Id {pid}).MainWindowTitle"], capture_output=True, text=True, timeout=20) return out.stdout.strip()""", """ out = subprocess.run(["powershell", "-NoProfile", "-Command", f"(Get-Process -Id {pid}).MainWindowTitle"], capture_output=True, timeout=20) return out.stdout.decode("mbcs", errors="replace").strip() # console code page (cp949 here)"""), ("""def _launch_and_wait(job, timeout, exit_grace): \"\"\"Start Rhino on job.py; return when result.json exists. A Rhino that does not exit within `exit_grace` seconds after writing the result is killed (2026-10-12: Rhino sometimes stalled on exit). Raises RhinoJobError if no result appears within `timeout` (Rhino killed; its window title is reported).\"\"\" rp = job / "result.json\"""", """def _launch_and_wait(job, start_timeout, run_timeout, exit_grace): \"\"\"Start Rhino on job.py; return when result.json exists. start_timeout: seconds until job.py writes its `started` marker (2026-10-12: a launch once stalled 23 min); exceeded -> Rhino killed, RhinoStartError (retryable). run_timeout: seconds from start to result (long jobs such as a full fingerprint take ~12 min); exceeded -> Rhino killed, RhinoJobError. A Rhino that does not exit within `exit_grace` seconds after writing the result is killed (the result is already on disk). The main window title is reported on every kill to help find the cause (e.g. a modal dialog).\"\"\" rp, sp = job / "result.json", job / "started" sp.unlink(missing_ok=True)"""), (""" t0 = time.time() while not rp.exists(): if proc.poll() is not None: raise RhinoJobError(f"Rhino exited (code {proc.returncode}) without a result: {job}") if time.time() - t0 > timeout: title = _window_title(proc.pid) proc.kill() raise RhinoJobError(f"Rhino gave no result within {timeout}s (window: '{title}'): {job}") time.sleep(0.5)""", """ t0, t_start = time.time(), None while not rp.exists(): if proc.poll() is not None: raise RhinoJobError(f"Rhino exited (code {proc.returncode}) without a result: {job}") if t_start is None and sp.exists(): t_start = time.time() if t_start is None and time.time() - t0 > start_timeout: title = _window_title(proc.pid) proc.kill() raise RhinoStartError(f"Rhino did not start the job within {start_timeout}s (window: '{title}'): {job}") if t_start is not None and time.time() - t_start > run_timeout: title = _window_title(proc.pid) proc.kill() raise RhinoJobError(f"Rhino job ran over {run_timeout}s (window: '{title}'): {job}") time.sleep(0.5)"""), ("""def run_job(steps, save=None, units_mm_tolerance=None, timeout=600, open_path=None, retries=1, exit_grace=30): \"\"\"Run `steps` in a fresh Rhino; return the result dict (raises RhinoJobError on failure). open_path: run the steps on that .3dm opened headless (reload test) instead of the empty active doc. timeout: seconds to wait for the result; a stalled launch is killed and retried `retries` times. \"\"\"""", """def run_job(steps, save=None, units_mm_tolerance=None, open_path=None, start_timeout=600, run_timeout=3600, retries=1, exit_grace=30): \"\"\"Run `steps` in a fresh Rhino; return the result dict (raises RhinoJobError on failure). open_path: run the steps on that .3dm opened headless (reload test) instead of the empty active doc. A launch that does not start the script within start_timeout is killed and retried `retries` times. \"\"\""""), (""" "result": str(job / "result.json")}""", """ "result": str(job / "result.json"), "started": str(job / "started")}"""), (""" exit_note = _launch_and_wait(job, timeout, exit_grace) break except RhinoJobError as e:""", """ exit_note = _launch_and_wait(job, start_timeout, run_timeout, exit_grace) break except RhinoStartError as e:"""), ]) patch("tools/test_runner_stall.py", [ (""" run_job([{"name": "noop", "script": step}], timeout=3, retries=1)""", """ run_job([{"name": "noop", "script": step}], start_timeout=3, retries=1)"""), (""" n = str(e).count("gave no result within 3s")""", """ n = str(e).count("did not start the job within 3s")"""), ("""1. timeout=3 s with 1 retry:""", """1. start_timeout=3 s with 1 retry:"""), ]) print("patched")