Files
m2dev-server/deploy/systemd/bin/metin-core-backtrace.in
2026-04-14 17:01:50 +02:00

198 lines
5.7 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import os
import re
import shutil
import subprocess
from pathlib import Path
RUNTIME_ROOT = Path("{{RUNTIME_ROOT}}")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Generate a backtrace for a Metin runtime core file")
parser.add_argument("--core", help="Core file path. Defaults to the newest core under the runtime tree.")
parser.add_argument("--exe", help="Executable path override. If omitted, infer it from the core path.")
return parser.parse_args()
def ensure_root() -> None:
if os.geteuid() != 0:
raise SystemExit("Run as root.")
def run(command: list[str], check: bool = False) -> subprocess.CompletedProcess[str]:
return subprocess.run(command, check=check, capture_output=True, text=True)
def iter_core_files() -> list[Path]:
return sorted(
(path for path in RUNTIME_ROOT.glob("channels/**/core*") if path.is_file()),
key=lambda path: path.stat().st_mtime,
reverse=True,
)
def resolve_core_path(core_arg: str | None) -> Path:
if core_arg:
candidate = Path(core_arg)
if not candidate.is_absolute():
runtime_relative = RUNTIME_ROOT / core_arg
if runtime_relative.exists():
candidate = runtime_relative
candidate = candidate.resolve()
if not candidate.is_file():
raise SystemExit(f"Core file not found: {candidate}")
return candidate
cores = iter_core_files()
if not cores:
raise SystemExit(f"No core files found under {RUNTIME_ROOT}")
return cores[0]
def infer_execfn_from_file_output(core_path: Path) -> Path | None:
completed = run(["file", str(core_path)])
if completed.returncode != 0:
return None
match = re.search(r"execfn: '([^']+)'", completed.stdout)
if not match:
return None
candidate = Path(match.group(1))
if candidate.is_file():
return candidate
return None
def infer_executable(core_path: Path, exe_arg: str | None) -> Path:
if exe_arg:
exe_path = Path(exe_arg).resolve()
if not exe_path.is_file():
raise SystemExit(f"Executable not found: {exe_path}")
return exe_path
parent_name = core_path.parent.name
grandparent_name = core_path.parent.parent.name if core_path.parent.parent else ""
candidates: list[Path] = []
if parent_name == "db":
candidates.append(core_path.parent / "db")
elif parent_name == "auth":
candidates.append(core_path.parent / "game_auth")
elif parent_name.startswith("core") and grandparent_name.startswith("channel"):
candidates.append(core_path.parent / f"{grandparent_name}_{parent_name}")
execfn_candidate = infer_execfn_from_file_output(core_path)
if execfn_candidate:
candidates.append(execfn_candidate)
for candidate in candidates:
if candidate.is_file():
return candidate.resolve()
raise SystemExit(f"Could not infer executable for core file: {core_path}")
def preferred_debugger() -> str | None:
for tool in ("gdb", "lldb"):
if shutil.which(tool):
return tool
return None
def format_section(title: str, body: str) -> str:
return f"== {title} ==\n{body.rstrip()}\n"
def render_file_info(path: Path) -> str:
completed = run(["file", str(path)])
body = completed.stdout or completed.stderr or "<no output>"
return format_section(f"file {path}", body)
def render_readelf_notes(core_path: Path) -> str:
if not shutil.which("readelf"):
return ""
completed = run(["readelf", "-n", str(core_path)])
body = completed.stdout or completed.stderr or "<no output>"
return format_section(f"readelf -n {core_path}", body)
def render_debugger_backtrace(debugger: str, exe_path: Path, core_path: Path) -> str:
if debugger == "gdb":
command = [
"gdb",
"-batch",
"-ex",
"set pagination off",
"-ex",
"thread apply all bt full",
str(exe_path),
str(core_path),
]
elif debugger == "lldb":
command = [
"lldb",
"--batch",
"-o",
"thread backtrace all",
"-c",
str(core_path),
str(exe_path),
]
else:
raise SystemExit(f"Unsupported debugger: {debugger}")
completed = run(command)
output = completed.stdout or completed.stderr or "<no output>"
return format_section("backtrace", f"$ {' '.join(command)}\n\n{output}")
def main() -> int:
args = parse_args()
ensure_root()
core_path = resolve_core_path(args.core)
exe_path = infer_executable(core_path, args.exe)
debugger = preferred_debugger()
sections = [
format_section(
"summary",
"\n".join(
[
f"core: {core_path}",
f"executable: {exe_path}",
f"debugger: {debugger or '<none>'}",
]
),
),
render_file_info(core_path),
render_file_info(exe_path),
]
readelf_section = render_readelf_notes(core_path)
if readelf_section:
sections.append(readelf_section)
if debugger:
sections.append(render_debugger_backtrace(debugger, exe_path, core_path))
else:
sections.append(
format_section(
"backtrace",
"No supported debugger found. Install gdb or lldb on the host to generate a stack trace.",
)
)
print("\n".join(section.rstrip() for section in sections if section).rstrip())
return 0
if __name__ == "__main__":
raise SystemExit(main())