Files
opcda-diagnozer/CHANGELOG.md
T
2026-10-08 11:01:03 +02:00

163 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
All notable changes to the OPC DA Diagnozer are documented here. The format
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions
bump per [SemVer](https://semver.org/). 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 `EventKind`s), 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.