Phase 0 of expert feedback (#1~#11): infrastructure + design + 1차 fixes
Implementations (즉시 동작): - #1 crash logging: harness/crash_logger.py (sys.excepthook + threading + faulthandler, 회전 파일 logs/scanvas.log). main 진입점 통합. - #2 smooth curves (1차): gate_3d_builder ogee profile를 arc-length parametric CubicSpline로 4× densify (8pt→32pt, 36→132 cells, 60 FPS 안전). - #3 TIN colormap: matplotlib "terrain"의 파란색 범위 제거 → 짙은갈색→황토→ 모래→능선 LinearSegmentedColormap. 9 사이트 교체. 회귀 테스트 추가. - #5 uv: pyproject.toml + UV_GUIDE.md. base/[py313]/[dev]/[build] extras + hatchling. - #6,#7,#8 dev cycle infra: .pre-commit-config.yaml (ruff+secrets+위생), .gitea/workflows/ci.yml (Py3.11+3.13 matrix), tests/test_regressions.py (18 회귀 테스트, iter=1~7 fix 박제), CONTRIBUTING.md (Red→Green 알고리즘). Design docs (다음 세션 마이그레이션 청사진): - #4 UI/UX 전면 수정: UI_REDESIGN_PLAN.md (12 popup→1 inspector, vtkTkRenderWidget embedding 게이트, 4 phase × 7 sessions). - #10 Core/Plugin: ARCHITECTURE_PLAN.md (Core 14 / Plugin 7 구조물 + 2 렌더 + 1 QA, STRUCTURE_REGISTRY 확장, manifest 기반 디스커버리). - #11 perf hotspots: PERFORMANCE_BASELINE.md (19 핫스팟, P1: 타일 직렬DL 5~30s, 캡처 직렬 4.5~15s, numpy 벡터화 가능 Python loops, 텍스처 4회 반복read). Behavior preservation: ruff 0 errors, pytest 17 passed/1 skipped(bpy), import 33/33 OK on Py3.13.13. Item #2 P2/P3 곡선, #4 UI 마이그레이션, #10 Phase 1 추출, #11 P1 최적화는 차기 세션. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
136
harness/crash_logger.py
Normal file
136
harness/crash_logger.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""S-CANVAS 크래시/예외 로깅 — sys.excepthook + faulthandler + 회전 파일 핸들러.
|
||||
|
||||
사용:
|
||||
from harness.crash_logger import install_crash_handlers
|
||||
install_crash_handlers() # main() 진입점에서 한 번 호출
|
||||
|
||||
동작:
|
||||
- 미처리 예외: traceback을 logs/scanvas.log + logs/crash_<ts>.txt에 저장
|
||||
- C-level 크래시(segfault 등): faulthandler가 stderr + logs/faulthandler.log로 trace
|
||||
- 메인 thread 외 thread 예외도 캡처 (threading.excepthook)
|
||||
- 기존 stdout/stderr 출력은 유지 (사용자 인지 가능)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as _dt
|
||||
import faulthandler
|
||||
import logging
|
||||
import logging.handlers
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
# 상수
|
||||
_LOG_DIR = Path(__file__).resolve().parent.parent / "logs"
|
||||
_MAIN_LOG = _LOG_DIR / "scanvas.log"
|
||||
_FAULT_LOG = _LOG_DIR / "faulthandler.log"
|
||||
_MAX_BYTES = 5 * 1024 * 1024 # 5MB per file
|
||||
_BACKUP_COUNT = 5 # rotate up to scanvas.log.5
|
||||
|
||||
_installed = False
|
||||
_crash_logger: logging.Logger | None = None
|
||||
|
||||
|
||||
def _ensure_log_dir() -> None:
|
||||
_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def _build_logger() -> logging.Logger:
|
||||
logger = logging.getLogger("scanvas.crash")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# 회전 파일 핸들러 (중복 install 방지: 핸들러 이미 있으면 재사용)
|
||||
if not any(isinstance(h, logging.handlers.RotatingFileHandler)
|
||||
for h in logger.handlers):
|
||||
handler = logging.handlers.RotatingFileHandler(
|
||||
_MAIN_LOG, maxBytes=_MAX_BYTES, backupCount=_BACKUP_COUNT,
|
||||
encoding="utf-8",
|
||||
)
|
||||
fmt = logging.Formatter(
|
||||
"%(asctime)s | %(levelname)s | %(threadName)s | %(message)s",
|
||||
datefmt="%Y-%m-%dT%H:%M:%S",
|
||||
)
|
||||
handler.setFormatter(fmt)
|
||||
logger.addHandler(handler)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
def _dump_crash_artifact(prefix: str, body: str) -> Path:
|
||||
"""크래시별 별도 파일에도 전체 traceback 보관 (회전 로그와 별개)."""
|
||||
ts = _dt.datetime.now().strftime("%Y%m%dT%H%M%S")
|
||||
path = _LOG_DIR / f"{prefix}_{ts}.txt"
|
||||
path.write_text(body, encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def _excepthook(exc_type, exc_value, exc_tb):
|
||||
"""sys.excepthook — 메인 thread 미처리 예외."""
|
||||
if exc_type is KeyboardInterrupt:
|
||||
# Ctrl+C는 정상 흐름으로 처리 (기본 동작 유지)
|
||||
sys.__excepthook__(exc_type, exc_value, exc_tb)
|
||||
return
|
||||
body = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
|
||||
if _crash_logger:
|
||||
_crash_logger.critical("UNCAUGHT EXCEPTION (main thread):\n%s", body)
|
||||
artifact = _dump_crash_artifact("crash", body)
|
||||
# 기존 동작 유지 — stderr에도 출력
|
||||
sys.__excepthook__(exc_type, exc_value, exc_tb)
|
||||
print(f"\n[crash_logger] 크래시 덤프 → {artifact}", file=sys.stderr)
|
||||
|
||||
|
||||
def _thread_excepthook(args: threading.ExceptHookArgs) -> None:
|
||||
"""threading.excepthook — 워커 thread 미처리 예외 (Py3.8+)."""
|
||||
if args.exc_type is SystemExit:
|
||||
return
|
||||
body = "".join(traceback.format_exception(
|
||||
args.exc_type, args.exc_value, args.exc_traceback))
|
||||
thread_name = args.thread.name if args.thread else "<unknown>"
|
||||
if _crash_logger:
|
||||
_crash_logger.error(
|
||||
"UNCAUGHT EXCEPTION (thread %s):\n%s", thread_name, body)
|
||||
_dump_crash_artifact(f"crash_thread_{thread_name}", body)
|
||||
|
||||
|
||||
def install_crash_handlers(also_install_faulthandler: bool = True) -> Path:
|
||||
"""크래시 핸들러를 한 번만 install. 재호출은 no-op.
|
||||
|
||||
Returns:
|
||||
로그 디렉토리 경로 (사용자에게 보여줄 수 있음).
|
||||
"""
|
||||
global _installed, _crash_logger # noqa: PLW0603 (singleton install pattern)
|
||||
if _installed:
|
||||
return _LOG_DIR
|
||||
|
||||
_ensure_log_dir()
|
||||
_crash_logger = _build_logger()
|
||||
|
||||
# Python 예외 훅
|
||||
sys.excepthook = _excepthook
|
||||
threading.excepthook = _thread_excepthook
|
||||
|
||||
# C-level 크래시 (segfault 등) — 별도 파일에 기록
|
||||
if also_install_faulthandler:
|
||||
# 기존 stderr 출력은 그대로 남기고, 추가로 파일에도.
|
||||
# NOTE: faulthandler는 long-lived file handle이 필요 (크래시 시점에 쓰기 위해)
|
||||
# — context manager로 닫으면 안 됨.
|
||||
try:
|
||||
fh = open(_FAULT_LOG, "a", encoding="utf-8") # noqa: SIM115 (faulthandler needs long-lived fh)
|
||||
faulthandler.enable(file=fh, all_threads=True)
|
||||
except OSError as e:
|
||||
# 파일 핸들 못 열면 stderr만으로 폴백
|
||||
faulthandler.enable(all_threads=True)
|
||||
_crash_logger.warning("faulthandler 파일 모드 실패 (%s) — stderr만 사용", e)
|
||||
|
||||
_installed = True
|
||||
_crash_logger.info("crash_logger 설치 완료 (logs=%s)", _LOG_DIR)
|
||||
return _LOG_DIR
|
||||
|
||||
|
||||
def get_logger() -> logging.Logger:
|
||||
"""app.log() 같은 일반 로깅도 같은 회전 파일에 쓰고 싶을 때 사용."""
|
||||
if _crash_logger is None:
|
||||
# auto-install (설치 안 한 경우 안전한 폴백)
|
||||
install_crash_handlers()
|
||||
return _crash_logger # type: ignore[return-value]
|
||||
Reference in New Issue
Block a user