From 1334c87bc62b8d51a1461859dfb7454a82daf683 Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Thu, 20 Aug 2026 21:32:03 -0700 Subject: [PATCH] fix: print why a service failed instead of naming a log file When cMCP Runtime fails to bind, the launchers wait the full 60 seconds and then say "See the *.log files in this demo folder." The reason is already in cmcp.log, and nobody looks there. The case that costs the most time is a cmcp on PATH that is present but broken, which is easy to end up with alongside an editable dev install. Both launcher branches, the one where the process exits early and the one where it never binds, now print the tail of the relevant log. Before: cMCP Runtime did not start listening on :8443 within 60s. See the *.log files in this demo folder. After: cMCP Runtime exited with code 1 before listening on :8443. Last 15 lines of cmcp.log: ... ImportError: cannot import name 'GovernancePolicy' from 'agent_os.mcp_gateway' Applied to the four demo launchers and the web console, which each carry their own copy of the helper by design. Verified against a real failure on both branches. The success path is an untouched early return. Not fixed here: _find_cmcp() takes the first cmcp on PATH without checking that it runs, so a broken install still shadows a working .venv. That wants a functional probe, and there is no cheap subcommand that exercises the import the way `start` does. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013EQx4N5BzTQbY8kvXUsdkY --- demo-01-cmcp-in-action/run.py | 27 +++++++++++++++++++++++---- demo-02-policy-swap/run.py | 27 +++++++++++++++++++++++---- demo-04-context-enforcement/run.py | 27 +++++++++++++++++++++++---- demo-05-compliance-domain/run.py | 27 +++++++++++++++++++++++---- web-console/run.py | 27 +++++++++++++++++++++++---- 5 files changed, 115 insertions(+), 20 deletions(-) diff --git a/demo-01-cmcp-in-action/run.py b/demo-01-cmcp-in-action/run.py index fb79276..85cebb7 100644 --- a/demo-01-cmcp-in-action/run.py +++ b/demo-01-cmcp-in-action/run.py @@ -36,6 +36,25 @@ def _find_cmcp() -> str: sys.exit("cmcp not found. Run: pip install cmcp-runtime") + +def _log_tail(what: str, lines: int = 15) -> str: + """Return the tail of the log for `what`, or "" if there is nothing to show. + + A service that fails to bind has already written the reason to its log. The + launcher used to point at the file and leave; printing the tail turns a 60 + second wait plus a scavenger hunt into the actual error. + """ + name = "cmcp.log" if "cMCP" in what or "cmcp" in what else "server.log" + path = pathlib.Path(__file__).parent.resolve() / name + try: + tail = path.read_text(encoding="utf-8", errors="replace").splitlines()[-lines:] + except OSError: + return "" + if not tail: + return "" + body = "\n".join(" " + line for line in tail) + return f"\n\nLast {len(tail)} lines of {name}:\n{body}" + def _wait_for_port(port: int, what: str, process=None, timeout: float = 60.0) -> None: """Block until something is listening on 127.0.0.1:port. @@ -46,15 +65,15 @@ def _wait_for_port(port: int, what: str, process=None, timeout: float = 60.0) -> deadline = time.monotonic() + timeout while time.monotonic() < deadline: if process is not None and process.poll() is not None: - sys.exit(f"{what} exited with code {process.returncode} before listening on :{port}. " - "See the *.log files in this demo folder.") + sys.exit(f"{what} exited with code {process.returncode} before listening on :{port}." + + _log_tail(what)) try: with socket.create_connection(("127.0.0.1", port), timeout=1): return except OSError: time.sleep(0.25) - sys.exit(f"{what} did not start listening on :{port} within {timeout:.0f}s. " - "See the *.log files in this demo folder.") + sys.exit(f"{what} did not start listening on :{port} within {timeout:.0f}s." + + _log_tail(what)) def _assert_port_free(port: int, what: str) -> None: diff --git a/demo-02-policy-swap/run.py b/demo-02-policy-swap/run.py index c4b807a..f2322db 100644 --- a/demo-02-policy-swap/run.py +++ b/demo-02-policy-swap/run.py @@ -42,6 +42,25 @@ def _find_cmcp() -> str: sys.exit("cmcp not found. Run: pip install cmcp-runtime") + +def _log_tail(what: str, lines: int = 15) -> str: + """Return the tail of the log for `what`, or "" if there is nothing to show. + + A service that fails to bind has already written the reason to its log. The + launcher used to point at the file and leave; printing the tail turns a 60 + second wait plus a scavenger hunt into the actual error. + """ + name = "cmcp.log" if "cMCP" in what or "cmcp" in what else "server.log" + path = pathlib.Path(__file__).parent.resolve() / name + try: + tail = path.read_text(encoding="utf-8", errors="replace").splitlines()[-lines:] + except OSError: + return "" + if not tail: + return "" + body = "\n".join(" " + line for line in tail) + return f"\n\nLast {len(tail)} lines of {name}:\n{body}" + def _wait_for_port(port: int, what: str, process=None, timeout: float = 60.0) -> None: """Block until something is listening on 127.0.0.1:port. @@ -52,15 +71,15 @@ def _wait_for_port(port: int, what: str, process=None, timeout: float = 60.0) -> deadline = time.monotonic() + timeout while time.monotonic() < deadline: if process is not None and process.poll() is not None: - sys.exit(f"{what} exited with code {process.returncode} before listening on :{port}. " - "See the *.log files in this demo folder.") + sys.exit(f"{what} exited with code {process.returncode} before listening on :{port}." + + _log_tail(what)) try: with socket.create_connection(("127.0.0.1", port), timeout=1): return except OSError: time.sleep(0.25) - sys.exit(f"{what} did not start listening on :{port} within {timeout:.0f}s. " - "See the *.log files in this demo folder.") + sys.exit(f"{what} did not start listening on :{port} within {timeout:.0f}s." + + _log_tail(what)) def _post(url: str, payload: dict, token: str) -> dict: diff --git a/demo-04-context-enforcement/run.py b/demo-04-context-enforcement/run.py index 949d462..e361830 100644 --- a/demo-04-context-enforcement/run.py +++ b/demo-04-context-enforcement/run.py @@ -36,6 +36,25 @@ def _find_cmcp() -> str: sys.exit("cmcp not found. Run: pip install cmcp-runtime") + +def _log_tail(what: str, lines: int = 15) -> str: + """Return the tail of the log for `what`, or "" if there is nothing to show. + + A service that fails to bind has already written the reason to its log. The + launcher used to point at the file and leave; printing the tail turns a 60 + second wait plus a scavenger hunt into the actual error. + """ + name = "cmcp.log" if "cMCP" in what or "cmcp" in what else "server.log" + path = pathlib.Path(__file__).parent.resolve() / name + try: + tail = path.read_text(encoding="utf-8", errors="replace").splitlines()[-lines:] + except OSError: + return "" + if not tail: + return "" + body = "\n".join(" " + line for line in tail) + return f"\n\nLast {len(tail)} lines of {name}:\n{body}" + def _wait_for_port(port: int, what: str, process=None, timeout: float = 60.0) -> None: """Block until something is listening on 127.0.0.1:port. @@ -46,15 +65,15 @@ def _wait_for_port(port: int, what: str, process=None, timeout: float = 60.0) -> deadline = time.monotonic() + timeout while time.monotonic() < deadline: if process is not None and process.poll() is not None: - sys.exit(f"{what} exited with code {process.returncode} before listening on :{port}. " - "See the *.log files in this demo folder.") + sys.exit(f"{what} exited with code {process.returncode} before listening on :{port}." + + _log_tail(what)) try: with socket.create_connection(("127.0.0.1", port), timeout=1): return except OSError: time.sleep(0.25) - sys.exit(f"{what} did not start listening on :{port} within {timeout:.0f}s. " - "See the *.log files in this demo folder.") + sys.exit(f"{what} did not start listening on :{port} within {timeout:.0f}s." + + _log_tail(what)) def _assert_port_free(port: int, what: str) -> None: diff --git a/demo-05-compliance-domain/run.py b/demo-05-compliance-domain/run.py index 03021c3..5b6fb5a 100644 --- a/demo-05-compliance-domain/run.py +++ b/demo-05-compliance-domain/run.py @@ -36,6 +36,25 @@ def _find_cmcp() -> str: sys.exit("cmcp not found. Run: pip install cmcp-runtime") + +def _log_tail(what: str, lines: int = 15) -> str: + """Return the tail of the log for `what`, or "" if there is nothing to show. + + A service that fails to bind has already written the reason to its log. The + launcher used to point at the file and leave; printing the tail turns a 60 + second wait plus a scavenger hunt into the actual error. + """ + name = "cmcp.log" if "cMCP" in what or "cmcp" in what else "server.log" + path = pathlib.Path(__file__).parent.resolve() / name + try: + tail = path.read_text(encoding="utf-8", errors="replace").splitlines()[-lines:] + except OSError: + return "" + if not tail: + return "" + body = "\n".join(" " + line for line in tail) + return f"\n\nLast {len(tail)} lines of {name}:\n{body}" + def _wait_for_port(port: int, what: str, process=None, timeout: float = 60.0) -> None: """Block until something is listening on 127.0.0.1:port. @@ -46,15 +65,15 @@ def _wait_for_port(port: int, what: str, process=None, timeout: float = 60.0) -> deadline = time.monotonic() + timeout while time.monotonic() < deadline: if process is not None and process.poll() is not None: - sys.exit(f"{what} exited with code {process.returncode} before listening on :{port}. " - "See the *.log files in this demo folder.") + sys.exit(f"{what} exited with code {process.returncode} before listening on :{port}." + + _log_tail(what)) try: with socket.create_connection(("127.0.0.1", port), timeout=1): return except OSError: time.sleep(0.25) - sys.exit(f"{what} did not start listening on :{port} within {timeout:.0f}s. " - "See the *.log files in this demo folder.") + sys.exit(f"{what} did not start listening on :{port} within {timeout:.0f}s." + + _log_tail(what)) def _assert_port_free(port: int, what: str) -> None: diff --git a/web-console/run.py b/web-console/run.py index d62bc97..5e4095b 100644 --- a/web-console/run.py +++ b/web-console/run.py @@ -47,6 +47,25 @@ def _find_cmcp() -> str: sys.exit("cmcp not found. Run: pip install cmcp-runtime httpx") +def _log_tail(what: str, lines: int = 15) -> str: + """Return the tail of the log for `what`, or "" if there is nothing to show. + + A service that fails to bind has already written the reason to its log. The + launcher used to point at the file and leave; printing the tail turns a 60 + second wait plus a scavenger hunt into the actual error. + """ + name = "cmcp.log" if "cMCP" in what or "cmcp" in what else "server.log" + path = pathlib.Path(__file__).parent.resolve() / name + try: + tail = path.read_text(encoding="utf-8", errors="replace").splitlines()[-lines:] + except OSError: + return "" + if not tail: + return "" + body = "\n".join(" " + line for line in tail) + return f"\n\nLast {len(tail)} lines of {name}:\n{body}" + + def _wait_for_port(port: int, what: str, process=None, timeout: float = 60.0) -> None: """Block until something is listening on 127.0.0.1:port. @@ -57,15 +76,15 @@ def _wait_for_port(port: int, what: str, process=None, timeout: float = 60.0) -> deadline = time.monotonic() + timeout while time.monotonic() < deadline: if process is not None and process.poll() is not None: - sys.exit(f"{what} exited with code {process.returncode} before listening on :{port}. " - "See server.log and cmcp.log in this folder.") + sys.exit(f"{what} exited with code {process.returncode} before listening on :{port}." + + _log_tail(what)) try: with socket.create_connection(("127.0.0.1", port), timeout=1): return except OSError: time.sleep(0.25) - sys.exit(f"{what} did not start listening on :{port} within {timeout:.0f}s. " - "See server.log and cmcp.log in this folder.") + sys.exit(f"{what} did not start listening on :{port} within {timeout:.0f}s." + + _log_tail(what)) def _ensure_submodule() -> None: