diff --git a/emrg/client/daemon_manager.py b/emrg/client/daemon_manager.py index 7dcfa642..4e6709fe 100644 --- a/emrg/client/daemon_manager.py +++ b/emrg/client/daemon_manager.py @@ -80,7 +80,22 @@ async def start_daemon() -> subprocess.Popen: if is_running(): logger.info("emrgd started (pid=%d)", proc.pid) return proc - raise RuntimeError("emrgd failed to start within timeout") + # R124: 超时后读取 emrgd.log 尾部打印真实失败原因(rant 2026-08-05T15:54:28 关联: + # config.toml 解析错误时 CLI 只显示 'failed to start within timeout',吞掉真实报错) + tail = _read_log_tail(Path.home() / ".emrg" / "emrgd.log", lines=15) + detail = f"\n emrgd.log 尾部:\n{tail}" if tail else "" + raise RuntimeError(f"emrgd failed to start within timeout{detail}") + + +def _read_log_tail(path: Path, lines: int = 15) -> str: + """Return the last `lines` of a log file (empty string on any error).""" + try: + if not path.exists(): + return "" + data = path.read_text(encoding="utf-8", errors="replace") + return "\n".join(data.rstrip().splitlines()[-lines:]) + except OSError: + return "" async def check_and_restart_if_stale() -> None: diff --git a/tests/test_daemon_manager.py b/tests/test_daemon_manager.py index cfc625d3..10eec195 100644 --- a/tests/test_daemon_manager.py +++ b/tests/test_daemon_manager.py @@ -287,3 +287,26 @@ def test_close_calls_ws_close(self): conn = self._conn() asyncio.run(conn.close()) assert conn._ws.closed is True + +class TestReadLogTail: + """_read_log_tail — daemon start-timeout diagnostics (R124).""" + + def test_tail_last_lines(self, tmp_path): + p = tmp_path / "emrgd.log" + p.write_text("\n".join(f"line{i}" for i in range(1, 31)), encoding="utf-8") + out = daemon_manager._read_log_tail(p, lines=5) + assert out == "line26\nline27\nline28\nline29\nline30" + + def test_missing_file_returns_empty(self, tmp_path): + assert daemon_manager._read_log_tail(tmp_path / "nope.log") == "" + + def test_shorter_than_lines_returns_all(self, tmp_path): + p = tmp_path / "emrgd.log" + p.write_text("a\nb", encoding="utf-8") + assert daemon_manager._read_log_tail(p, lines=10) == "a\nb" + + def test_invalid_utf8_replaced(self, tmp_path): + p = tmp_path / "emrgd.log" + p.write_bytes(b"ok\n\xff\xfebad\nend") + out = daemon_manager._read_log_tail(p, lines=5) + assert "end" in out and "\ufffd" in out