This commit is contained in:
Jonas Bengtsson
2026-10-08 07:26:04 +02:00
parent a1af8d68c9
commit 406fdc61ac
13 changed files with 1750 additions and 89 deletions
+62 -2
View File
@@ -11,6 +11,7 @@ from __future__ import annotations
import argparse
import sys
import threading
from collections.abc import Sequence
from pathlib import Path
@@ -35,7 +36,6 @@ def _default_config_path() -> str:
return DEFAULT_CONFIG
_NOT_IMPLEMENTED: dict[str, str] = {
"run": "Phase 1 (core engine)",
"install": "Phase 3 (Windows service)",
"uninstall": "Phase 3 (Windows service)",
"start": "Phase 3 (Windows service)",
@@ -81,8 +81,12 @@ def _build_parser() -> argparse.ArgumentParser:
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
sub = parser.add_subparsers(dest="verb", metavar="verb", required=True)
p = sub.add_parser("run", help="run in the foreground (console)")
p = sub.add_parser(
"run",
help="run the monitoring loop in the foreground (Ctrl+C stops)",
)
_add_config_option(p)
_add_server_overrides(p)
p = sub.add_parser(
"read",
@@ -550,11 +554,67 @@ def _cmd_probe(
return _cmd_read(config_path, server, host, (), source, as_json=False)
def _cmd_run(config_path: str, server: str | None, host: str | None) -> int:
"""Foreground monitoring loop: poll → detect → print (Ctrl+C stops)."""
import signal
from opcda_diagnozer.monitor.poller import Poller
from opcda_diagnozer.opc.automation import AutomationOpcClient
try:
config = load_config(config_path)
except ConfigError as exc:
print(f"run: FAILED\n {exc}", file=sys.stderr)
return 2
# --server/--host override opc.* for this run (same as the other verbs)
if server is not None or host is not None:
from dataclasses import replace
from opcda_diagnozer.config import OpcConfig
opc = replace(
config.opc,
server=server or config.opc.server,
host=config.opc.host if host is None else host,
)
config = replace(config, opc=opc)
def client_factory() -> AutomationOpcClient:
# Built on the worker thread: CoInitialize is per-thread (spec §10.3).
return AutomationOpcClient(
server=config.opc.server,
host=config.opc.host,
connect_timeout_s=config.opc.connect_timeout_s,
)
def reporter(event) -> None:
# Console = the human view (spec §9): clear text, status NAME (0xNN)
line = f"{event.time_utc.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]} UTC {event.severity.value:<8} {event.kind.value}"
extras = " ".join(f"{k}={v}" for k, v in event.detail.items())
print(f"{line} {extras}" if extras else line, flush=True)
poller = Poller(client_factory, config, reporter)
stop_requested = threading.Event()
def _on_ctrl_c(signum, frame) -> None: # noqa: ANN001, ARG001
if stop_requested.is_set():
raise KeyboardInterrupt # second Ctrl+C aborts immediately
stop_requested.set()
poller.request_stop()
print("\nrun: stop requested - finishing current poll ...", file=sys.stderr, flush=True)
signal.signal(signal.SIGINT, _on_ctrl_c)
poller.run_forever()
return 0
def main(argv: Sequence[str] | None = None) -> int:
"""CLI entry point (console script + ``python -m opcda_diagnozer``)."""
args = _build_parser().parse_args(argv)
if args.verb == "check-config":
return _cmd_check_config(args.config)
if args.verb == "run":
return _cmd_run(args.config, args.server, args.host)
if args.verb == "list-servers":
return _cmd_list_servers(args.host, args.source, args.json)
if args.verb == "list-items":