#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" LINUXCNC_ROOT="${LINUXCNC_ROOT:-$ROOT_DIR/../linuxcnc}" DB_DEMO_DIR="$LINUXCNC_ROOT/configs/sim/axis/db_demo" DB_PROGRAM="$DB_DEMO_DIR/db_nonran.py" RUN_DIR="$ROOT_DIR/build/native/tool-db-runtime" DB_SAVEFILE="$RUN_DIR/db_nonran_probe" DB_STDOUT="$RUN_DIR/db_program.stdout.log" DB_STDERR="$RUN_DIR/db_program.stderr.log" required_commands=(python3 linuxcnc milltask halcmd) missing_requirements=() command_path() { command -v "$1" 2>/dev/null || true } csv_join() { local IFS=, printf '%s' "$*" } print_kv() { printf '%s=%s\n' "$1" "$2" } runtime_requirements=() for command_name in "${required_commands[@]}"; do path="$(command_path "$command_name")" if [[ -n "$path" ]]; then runtime_requirements+=("$command_name:1") print_kv "tool_db_runtime_${command_name}_path" "$path" else runtime_requirements+=("$command_name:0") missing_requirements+=("$command_name") fi done source_ready=1 for rel in \ configs/sim/axis/db_demo/db_nonran.ini \ configs/sim/axis/db_demo/db.py \ lib/python/tooldb.py \ lib/python/linuxcnc.so; do if [[ ! -e "$LINUXCNC_ROOT/$rel" ]]; then source_ready=0 missing_requirements+=("$rel") print_kv "tool_db_missing_source_${rel//[^A-Za-z0-9]/_}" "$LINUXCNC_ROOT/$rel" fi done if [[ ! -x "$DB_PROGRAM" ]]; then source_ready=0 missing_requirements+=("configs/sim/axis/db_demo/db_nonran.py:executable") fi runtime_ready=0 if [[ "${#missing_requirements[@]}" -eq 0 ]]; then runtime_ready=1 fi print_kv tool_db_runtime_requirements "$(csv_join "${runtime_requirements[@]}")" if [[ "${#missing_requirements[@]}" -eq 0 ]]; then print_kv tool_db_missing_requirements "-" else print_kv tool_db_missing_requirements "$(csv_join "${missing_requirements[@]}")" fi print_kv tool_db_source_proof_ready "$source_ready" print_kv tool_db_runtime_ready "$runtime_ready" print_kv tool_db_execution_enabled 0 print_kv tool_db_promotion_allowed 0 if [[ "$source_ready" != "1" ]]; then print_kv tool_db_runtime_probe_status "blocked_missing_source" exit 0 fi if [[ "$runtime_ready" != "1" ]]; then print_kv tool_db_runtime_probe_status "skipped_missing_host_runtime" exit 0 fi if [[ "${ENABLE_TOOL_DB_RUNTIME_PROBE:-0}" != "1" ]]; then print_kv tool_db_runtime_probe_status "ready_disabled_by_default" print_kv tool_db_runtime_probe_note "set_ENABLE_TOOL_DB_RUNTIME_PROBE_1_to_run_db_protocol_probe" exit 0 fi mkdir -p "$RUN_DIR" rm -f "$DB_SAVEFILE" "$DB_STDOUT" "$DB_STDERR" print_kv tool_db_runtime_probe_program "$DB_PROGRAM" print_kv tool_db_runtime_probe_db_savefile "$DB_SAVEFILE" print_kv tool_db_runtime_probe_stdout "$DB_STDOUT" print_kv tool_db_runtime_probe_stderr "$DB_STDERR" if python3 - "$DB_PROGRAM" "$DB_DEMO_DIR" "$LINUXCNC_ROOT/lib/python" "$DB_SAVEFILE" "$DB_STDOUT" "$DB_STDERR" <<'PY' import os import select import subprocess import sys import time db_program, db_demo_dir, python_path, db_savefile, stdout_log, stderr_log = sys.argv[1:] env = os.environ.copy() env["PYTHONPATH"] = python_path + (os.pathsep + env["PYTHONPATH"] if env.get("PYTHONPATH") else "") stdout_file = open(stdout_log, "w", encoding="utf-8") stderr_file = open(stderr_log, "w", encoding="utf-8") proc = subprocess.Popen( [db_program, "--period_minutes=999", db_savefile], cwd=db_demo_dir, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=stderr_file, text=True, bufsize=1, ) def emit(name, value): print(f"{name}={value}") def read_line(timeout=5.0): deadline = time.monotonic() + timeout while time.monotonic() < deadline: ready, _, _ = select.select([proc.stdout], [], [], 0.1) if ready: line = proc.stdout.readline() if line == "": raise RuntimeError("db_program_stdout_closed") line = line.rstrip("\n") stdout_file.write(line + "\n") stdout_file.flush() return line if proc.poll() is not None: raise RuntimeError(f"db_program_exited_{proc.returncode}") raise RuntimeError("timeout_waiting_for_db_program_reply") def send(command): proc.stdin.write(command + "\n") proc.stdin.flush() def read_until_fini(command, timeout=5.0): send(command) lines = [] deadline = time.monotonic() + timeout while time.monotonic() < deadline: line = read_line(max(0.1, deadline - time.monotonic())) lines.append(line) if line.startswith("FINI"): return lines raise RuntimeError(f"timeout_waiting_for_fini_{command.split()[0]}") def require(condition, message): if not condition: raise RuntimeError(message) ok = False try: version = read_line() emit("tool_db_protocol_version", version) require(version == "v2.1", f"version_drift:{version}") get_all = read_until_fini("g") tool_lines = [line for line in get_all if line.startswith("T")] emit("tool_db_get_all_count", len(tool_lines)) require(len(tool_lines) == 10, "get_all_tool_count_drift") for toolno in range(10, 20): expected = f"T{toolno} " require(any(line.startswith(expected) for line in tool_lines), f"missing_tool_{toolno}") require(any(line.startswith(expected) and f"P{toolno + 100}" in line for line in tool_lines), f"pocket_drift_{toolno}") emit("tool_db_get_all_fini", int(get_all[-1].startswith("FINI"))) put_reply = read_until_fini("p t11 p111 d0.33 z0.11") emit("tool_db_put_tool_update_fini", int(put_reply[-1].startswith("FINI"))) get_after_put = read_until_fini("g") t11_rows = [line for line in get_after_put if line.startswith("T11 ")] require(t11_rows and "D0.33" in t11_rows[0] and "Z0.11" in t11_rows[0], "put_update_state_drift") emit("tool_db_put_tool_update_state_ok", 1) load_reply = read_until_fini("l t14 p0") emit("tool_db_load_spindle_fini", int(load_reply[-1].startswith("FINI"))) t14_loaded = read_until_fini("t 14") require(any(line.startswith("T14 ") and "P0" in line for line in t14_loaded), "load_spindle_state_drift") emit("tool_db_load_spindle_state_ok", 1) unload_reply = read_until_fini("u t0 p0") emit("tool_db_unload_spindle_fini", int(unload_reply[-1].startswith("FINI"))) t14_unloaded = read_until_fini("t 14") require(any(line.startswith("T14 ") and "P114" in line for line in t14_unloaded), "unload_spindle_state_drift") emit("tool_db_unload_spindle_state_ok", 1) require(os.path.exists(db_savefile), "db_savefile_missing") with open(db_savefile, "r", encoding="utf-8") as handle: saved = handle.read() require("T11 P111 D0.33 Z0.11" in saved, "db_savefile_put_update_missing") require("T14 P114" in saved, "db_savefile_unload_state_missing") emit("tool_db_persistence_state_ok", 1) ok = True finally: try: if proc.stdin: proc.stdin.close() except Exception: pass if proc.poll() is None: proc.terminate() try: proc.wait(timeout=2) except subprocess.TimeoutExpired: proc.kill() proc.wait(timeout=2) stderr_file.close() stdout_file.close() emit("tool_db_runtime_protocol_probe_ok", int(ok)) sys.exit(0 if ok else 1) PY then print_kv tool_db_runtime_probe_status "runtime_protocol_probe_passed" print_kv tool_db_runtime_probe_note "linuxcnc_owned_DB_PROGRAM_protocol_probe_passed_without_promotion" exit 0 fi print_kv tool_db_runtime_probe_status "runtime_protocol_probe_failed" print_kv tool_db_runtime_probe_note "linuxcnc_owned_DB_PROGRAM_protocol_probe_failed" exit 1