Files
2026-10-08 11:01:03 +02:00

9.4 KiB
Raw Permalink Blame History

Changelog

All notable changes to the OPC DA Diagnozer are documented here. The format follows Keep a Changelog; versions bump per SemVer. Dates are YYYY-MM-DD.

[Unreleased]

Added — Phase 3 Windows service

  • Service host (service.py): pywin32 ServiceFramework subclass runs the monitoring loop on its own monitor thread; clean SCM stop semantics — STOP_PENDING reported immediately, 30 s join-with-timeout, a hung COM poll is abandoned (thread never joined, daemon; spec §10.4); service start/stop recorded in the Windows Event Log (PYS_SERVICE_STARTED/ STOPPED). Verified in-process via the pywin32 debug host against the fake client: start → poll → stop in 0.1 s.
  • CLI verbs install / uninstall / start / stop / status:
    • install registers the exe itself as the service binary (no pythonservice.exe, no Python install on the diag host) with auto-start, validates the config before touching the SCM, records the config path in the service registry Parameters (startup never depends on the service CWD = System32), configures SCM crash recovery (restart on first+second failure, 5 s delay, 24 h counter reset — the ChangeServiceConfig2 failure-actions dict shape verified against pywin32's SWIG parser), and registers the Windows Event Log source with its message file (completing the eventlog reporter's deferred step).
    • Errors map to actionable one-liners: elevation needed, service not installed, service not running, marked-for-delete — plus the raw Windows error code and net helpmsg pointer for anything else.
    • status prints the SCM state without needing admin.
  • SCM dispatch: when the SCM starts the exe (fixed service argv shape + CWD = System32, exactly what install records in the ImagePath), main hands the process to servicemanager.Initialize + PrepareToHostSingle + StartServiceCtrlDispatcher; any other invocation stays a CLI verb.
  • PyInstaller build script: hidden imports added for the service host's lazily-imported SCM modules (win32service, win32serviceutil, servicemanager, win32evtlogutil).

Added — Phase 2 reporters

  • Rotating JSONL evidence log: one JSON event per line, config-digest header for provenance, size rotation with backups retained.
  • Plain-text anomaly log (spec §9.1): anomalies-YYYYMMDD.log, one line per detected anomaly in timestamp SEVERITY KIND message order with clear-text statuses; recoveries included so the file reads as an incident narrative.
  • Windows Event Log sink: severity-mapped (INFO/WARN/CRITICAL → Information/Warning/Error), WARN+ by default, delivery verified live.
  • Optional HTTP webhook (report.http): bounded queue + daemon sender thread, per-event POST with bearer auth; failures dropped and counted — never blocks the poll loop.
  • Reporter fan-out dispatcher: every sink isolated (one failing sink never kills the others or polling); the run verb wires all configured sinks.

Fixed

  • Windows Event Log reporter initially lost every event silently: win32evtlog.ReportEvent takes positional arguments only (keyword calls raise TypeError, swallowed by the sink's own isolation) — now uses a RegisterEventSource handle with positional calls (live-verified).
  • HTTP webhook in injected-post mode raised into the poll loop on a failing POST; both modes now count-and-drop.

Added — Phase 1 core engine (the continuous monitoring loop)

  • run verb: the foreground monitoring loop — coordinator + dedicated COM worker thread (the client is born on the worker; COM is per-thread), 1-second default polling of configured tags and device indicators, heartbeat (D15) every heartbeat_s, clean Ctrl+C stop.
  • Detection engine, all pure and fake-client tested: latency spike / sustained streak / rolling p95 (D4–D6), per-tag quality transitions, persistence and flapping (D8–D11), stale timestamp & frozen value (D12–D13), mass-quality degradation with latch (D14), device-link indicators (D16, edge-level DOWN/UP against configurable ok_values).
  • Hang watchdog (spec §10.4): polls exceeding timeout_ms raise the D7 soft-hang event and keep the thread; silence beyond hard_hang_s abandons the stuck COM thread (never joined — dead RPC channel) and rebuilds.
  • Connection state machine with exponential backoff (1 s → 60 s, reset on success) and reconnect accounting (D1/D2 with outage duration, D3 flapping).
  • Fake-client scenario engine: scripted polls and whole-call failures power the entire poller/detector test suite without COM.
  • Clear-text status rendering (status_text): BAD / COMM FAILURE (0x18) everywhere events are shown.

Added — server discovery & browsing

  • list-servers verb: enumerates OPC DA servers locally or on a DCOM host via the Automation Wrapper (GetOPCServers), with a no-COM registry-scan fallback (--source registry|automation|auto), JSON output, and DA-version metadata.
  • Browsing of the connected server's address space, unified in the list-items [all|branches|tags] verb: all prints the full structure tree (branches with /, tags with [ItemID: ...]), branches the structure-only flat list, tags the flat ItemID list for config tags:. --branch (dotted path or repeated segments), --depth N (1 default, 0 = whole subtree), --json. Replaces the earlier split list-devices/list-tags.
  • browse.max_depth config key (default 32) as the address-space walk guard, validated by check-config; cycle-safe tree walk in opc/browse.py.
  • SpaceNode tree data model (branch/leaf) replacing the earlier device-centric DeviceInfo.

Added — reading & CLI

  • read verb: one-shot read of explicit tags (or the config's tags) with a table or --json, --source device|cache, and a non-zero exit when any tag doesn't read GOOD — scriptable as a health check.
  • probe verb: connect once and read every configured tag once.
  • --server / --host overrides on every connecting verb; with --server no config file is needed at all.
  • Frozen-exe default config path: config.yaml next to the exe (a Windows service starts with CWD = System32), not the working directory.

Added — monitoring design (D16) & config

  • devices: config section with DeviceEntry(name, tag, ok_values): D16 device-link indicators. The normal (connected) reading is configurable per indicator (ok_values, default [true]) — inverted fault flags [false, 0] and string states ["RUN"] are first-class. Strict validation: duplicate indicators and indicator/tag-list overlap are rejected; check-config prints the indicators.
  • Detection rules D16 (DEVICE_LINK_DOWN/DEVICE_LINK_UP) and the plain-text anomaly log reporter (report/textlog.py task) specified in the docs (SPECIFICATION §6.1, §9.1; IMPLEMENTATION_PLAN Phases 1–2) — implementation pending, see TODO.md.
  • Clear-text status rule (spec §7): every reporter writes NAME (0xNN) — e.g. BAD / communication failure (0x18).

Added — packaging

  • scripts/build_exe.ps1: PyInstaller build of the 32-bit console exe with hidden imports for the deferred COM modules (incl. win32timezone — see Fixed), embedded version metadata, a PE-bitness check, and smoke tests including a frozen-read canary against a live server.
  • --version and this CHANGELOG.md / TODO.md.

Fixed

  • COM lifecycle: all COM references (browsers, groups, items) are released inside the apartment before CoUninitialize; retained exception tracebacks are cleared in as_com_error — both caused "Win32 exception occurred releasing IUnknown" noise and hard crashes at exit.
  • Frozen exe: every tag read failed with SystemError: ... InvokeTypes ... exception set because pywin32 lazily imports win32timezone during VARIANT-DATE conversion and PyInstaller never bundles it — now a hidden import, with the build canary guarding it.
  • Reads: per-tag failures now carry the underlying exception's own type and text (_per_tag_error) instead of a bare "failed"; a degenerate device timestamp (never-updated alias, DST math) can no longer fail a read — the timestamp is metadata and is dropped to None.
  • Matrikon browse quirk: servers may return an empty ItemID for special leaves (alias roots); the adapter falls back to the bare browse name.
  • HRESULT labels: OPC_E_UNKNOWNITEMID (0xC0040007) and OPC_E_INVALIDITEMID (0xC0040008) were swapped; now corrected everywhere.

Verified

  • All CLI verbs live-tested against Matrikon OPC Simulation Server (Matrikon.OPC.Simulation.1): discovery, browse (all kinds/depths), reads (incl. alias tags after server-side alias repair), config-driven max_depth, and the frozen exe (onedir + onefile) including the read canary.
  • Test suite: 108 tests, all passing (pure logic + fake-client driven, runs on any OS; COM paths are live-smoke-tested on dev boxes only).

[0.1.0] - 2026-10-07

Added

  • Initial project skeleton: typed YAML config with strict actionable validation (check-config), OPC DA quality decode tables (spec §7), event model skeletons (DiagEvent, D1–D15 EventKinds), client/protocol abstractions, and the fake in-process client powering cross-OS tests.
  • Phase 0 decisions recorded: 32-bit interpreter mandatory (Automation Wrapper is a 32-bit in-proc DLL), ruamel.yaml over PyYAML (no 32-bit cp314 wheels), COM imports import-guarded for cross-OS test runs.