Files
opcda-diagnozer/docs/IMPLEMENTATION_PLAN.md
T
2026-10-07 10:38:21 +02:00

211 lines
11 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.
# OPC DA Diagnozer — Implementation Plan
Companion to [`docs/SPECIFICATION.md`](SPECIFICATION.md). Targets **v1.0** as the first
production-deployable release.
---
## 1. Milestones
| Milestone | Content | Exit condition |
|-----------|---------|----------------|
| **v0.1 — Foreground proof** | CLI reads configured tags once per second in a console, prints events | Successfully reads from a real (or simulation) OPC DA server |
| **v0.2 — Detection engine** | All detectors (D1–D15), event model, JSONL logging | Unit tests green; chaos scenarios produce correct events |
| **v0.5 — Windows service** | pywin32 service, watchdog/hang recovery, Event Log, CLI install/run | Runs unattended through reboot + server-kill test |
| **v1.0 — Production** | Packaging, runbook, 24 h soak, optional webhook | Acceptance criteria AC1–AC10 (spec §13) met |
## 2. Project layout
```
opcda-diagnozer/
├── README.md
├── pyproject.toml
├── config.example.yaml
├── docs/
│ ├── SPECIFICATION.md
│ └── IMPLEMENTATION_PLAN.md
├── src/opcda_diagnozer/
│ ├── __init__.py
│ ├── __main__.py # python -m opcda_diagnozer
│ ├── main.py # CLI: run / install / uninstall / check-config / probe / list-devices / list-tags
│ ├── config.py # pydantic-free typed config (dataclasses) + YAML load + validation
│ ├── events.py # DiagEvent, Severity, EventKind
│ ├── quality.py # OPC DA quality decode tables
│ ├── opc/
│ │ ├── base.py # OpcClient + Browser protocols, ReadResult, OpcError(hresult)
│ │ ├── discovery.py # list-servers: Automation GetOPCServers / registry scan
│ │ ├── browse.py # list-devices/list-tags: address-space walk (branches/leaves)
│ │ ├── automation.py # OPC DA Automation Wrapper adapter (OPCAutomation.dll)
│ │ └── fake.py # in-process fake client + fake address space (tests, demo)
│ ├── monitor/
│ │ ├── state.py # connection state machine + backoff
│ │ ├── poller.py # COM thread: connect → poll loop → feed detectors
│ │ └── detectors.py # D1–D15 rule implementations
│ ├── report/
│ │ ├── base.py # Reporter protocol
│ │ ├── jsonl.py # rotating JSONL file
│ │ ├── eventlog.py # Windows Event Log (WARN+)
│ │ └── http.py # optional webhook
│ └── service.py # pywin32 ServiceFramework wrapper
└── tests/
├── test_quality.py
├── test_detectors.py
├── test_state.py
├── test_config.py
└── test_poller_fake.py # end-to-end against opc.fake, no COM needed
```
## 3. Phase 0 — Feasibility & environment (≈0.5–1 day)
**Goal:** validate the COM access strategy against reality before building anything.
Tasks:
1. Identify target OPC DA server: ProgID, host, DA version, bitness (32/64-bit EXE?).
2. Choose wrapper backend:
- **A. OPC DA Automation Wrapper** (`OPCAutomation.dll`) — ubiquitous in industry,
ships with many SCADA installs; likely default.
- **B. OpenOPC / Graybox wrapper** (`GBDaCtl.dll`) — fallback.
- Note: both are typically **32-bit in-proc DLLs** → client Python must be 32-bit
(see spec §11). Decide interpreter bitness here.
3. Stand up a **test server** (Matrikon OPC Simulation Server, KepServerEX demo, or the
actual control-system server in a maintenance window) and successfully read one tag
from Python (`win32com.client.Dispatch`).
4. Verify DCOM configuration for the service account (`dcomcnfg`: Launch/Activation →
Remote Activation; Access → Local+Remote). Record required firewall ports
(TCP 135 + RPC dynamic range).
5. Record baseline behavior: normal read RTT, quality values, what error the wrapper
surfaces when the server is stopped (feeds D1 implementation).
**Exit:** a throwaway script reads ≥ 3 tags from the remote server with measured RTT.
## 4. Phase 1 — Core engine (≈2–3 days)
1. `config.py` — YAML config (see `config.example.yaml` sketch in README), dataclass
model, strict validation, `check-config` messages that explain exactly what is wrong.
2. `quality.py` — decode tables (spec §7), `decode_quality(raw: int) -> QualityInfo`.
3. `opc/base.py` — client abstraction:
```python
class OpcClient(Protocol):
def connect(self) -> None: ...
def read(self, tags: Sequence[str]) -> list[ReadResult]: ...
def close(self) -> None: ...
```
`ReadResult`: `tag, value, quality_raw, device_time, error` — plus per-call RTT
measured by the poller. COM errors are normalized to `OpcError` carrying the raw
`HRESULT` + decoded name.
4. `opc/automation.py` — Automation Wrapper adapter (single group, static items,
`SyncRead`); every COM exception mapped to `OpcError` with `win32com` scode.
5. `monitor/state.py` — state machine (spec §10.2), backoff, reconnect accounting.
6. `monitor/detectors.py` — D1–D15 as small pure functions/classes over
`PollOutcome` (list[ReadResult] + rtt_ms + error), using an injected clock for
testability. Rolling window (deque, size `window_polls`) for p95/streak logic.
7. `monitor/poller.py` — the single COM thread; watchdog logic for hang detection
(spec §10.4): a second thread timestamps poll starts/ends; on `hard_hang_s` the COM
thread is abandoned and rebuilt.
8. `events.py` — event dataclass + JSON serialization.
9. `opc/fake.py` — scriptable fake client (sequences of outcomes: good reads, errors,
hangs, quality flips) → powers the entire unit-test suite without COM/hardware.
**Exit:** `opcda-diagnozer run` (foreground console) produces correct events against the
fake client; unit tests for all detectors.
## 5. Phase 2 — Reporters (≈1 day)
1. `report/jsonl.py` — rotation (size + backups), atomic-ish append, header line with
config digest for provenance.
2. `report/eventlog.py` — pywin32 `win32evtlog` usage; map severities; source
registration via `install` step.
3. `report/http.py` — optional webhook with timeout + simple retry/drop on failure
(never blocks the poll loop; queue with bound).
4. Heartbeat assembly (D15): uptime, reconnects, RTT percentiles, tags-bad counts.
**Exit:** events visible in file + Event Log; heartbeat cadence verified.
## 6. Phase 3 — Windows service (≈1 day)
1. `service.py` — `win32serviceutil.ServiceFramework`; service host thread spawns the
COM worker; clean `stop` handling (`SetServiceStatus STOP_PENDING`, join with
timeout, abandon hung COM thread on shutdown).
2. CLI verbs: `install /uninstall /start /stop` (wraps `sc.exe` semantics),
`run` (foreground), `check-config`, `probe` (connect once, print one read),
`list-items` (browse validation tool).
3. SCM recovery config (`sc failure ... restart`), auto-start, service account
documentation.
4. Config/log default dir: `%ProgramData%\OPCDA-Diagnozer\`.
**Exit:** survives reboot; survives OPC-server kill (produces D1/D2 events, reconnects);
`probe` and `check-config` give usable diagnostics to an ops engineer.
## 7. Phase 4 — Integration & chaos testing (≈1–2 days)
Scenario matrix against a simulation server (and, when available, the real server):
| Scenario | How to inject | Expected events |
|----------|---------------|-----------------|
| Server process killed | `taskkill` | D1 (HRESULT), D2 with outage duration |
| Server service stopped/restarted | `net stop/start` | D1, D2 |
| Firewall drop (silent) | block outbound RPC ports / pull NIC | D7 hang, then D1 |
| Latency injection | Clumsy / `netsh` delay | D4 spikes, D5 streak, D6 trend |
| Device comm failure | simulation server "communication disturbance" items | D8 with `BAD_COMM_FAILURE`, D10 persistence |
| Quality oscillation | toggling sim items | D11 flapping |
| Stale/frozen tag | simulation server static item marked `expect_change` | D12 / D13 |
| Mass bad quality | all sim items to bad | D14 |
| Config errors | bad ProgID, unknown tag | fast fail + actionable message; `OPC_E_INVALIDITEMID` surfaced at startup |
| Soak | 24 h healthy run | no leaks, heartbeats every 5 min, zero false positives |
**Exit:** each row verified manually against the event log; AC2/AC3/AC4/AC5 confirmed.
## 8. Phase 5 — Packaging & docs (≈0.5–1 day)
- `pyproject.toml` wheel installable with pip into the chosen (32-bit) Python;
document the on-host install procedure (PyInstaller packaging for pywin32 services is
fragile — deferred).
- `README.md` finalized (install, config, DCOM checklist, HRESULT table).
- Ops runbook: how to read events, what each kind means (link to spec §12 matrix),
how to reconfigure thresholds, where evidence lives.
- Version 1.0.0 tag.
## 9. Testing strategy
- **Unit** (no COM): detectors against scripted `PollOutcome` sequences with a fake
clock; quality decode; config validation; backoff/state transitions; JSONL rotation.
- **Integration:** `test_poller_fake.py` runs the full poller+reporters against
`opc.fake` in-process — the whole event pipeline without Windows COM.
- **Manual/chaos:** Phase 4 matrix (necessarily on Windows against a real/simulated
OPC DA server — cannot be automated in CI without the DCOM environment).
- CI: unit + fake-integration tests run on any OS (COM adapters are import-guarded);
chaos tests are a documented manual procedure.
## 10. Risk register
| Risk | Impact | Mitigation |
|------|--------|------------|
| Wrapper DLL bitness mismatch (32-bit only) | Cannot load wrapper | Phase 0 decision; use 32-bit Python; keep client abstraction so backend can be swapped |
| Automation Wrapper not present on diag host | No COM access | Phase 0 check; ship/distribute wrapper install in runbook (vendor redistribution rules apply) |
| DCOM hardening (KB5004442) blocks activation | Connect fails with `E_ACCESSDENIED`/activation errors | Runbook covers `dcomcnfg` steps; events surface raw HRESULT |
| COM calls hang indefinitely | Service appears alive but blind | Explicit watchdog + thread recycling (spec §10.4) |
| COM apartment violations (cross-thread calls) | Random `RPC_E_WRONG_THREAD` crashes | Single dedicated COM thread; all COM handles stay on it |
| Our client perturbs the system | Skewed diagnosis | One group, static small tag list, 1 s default poll, read-only |
| pywin32 service + COM quirks (GIL, STA init) | Service instability | Phase 3 spike; foreground `run` mode always available as fallback |
| Tag list too large / wrong item IDs | Noise, misleading RTT | `probe` + `check-config` validate up-front; per-poll error accounting |
| SCADA/host clock skew | Misleading staleness events | Local receive time always recorded; `max_age_s` configurable |
## 11. Effort estimate
| Phase | Estimate |
|-------|----------|
| 0 Feasibility | 0.5–1 d |
| 1 Core engine | 2–3 d |
| 2 Reporters | 1 d |
| 3 Service | 1 d |
| 4 Chaos testing | 1–2 d |
| 5 Packaging/docs | 0.5–1 d |
| **Total v1.0** | **≈ 6–9 days** |
## 12. Definition of done (v1.0)
- All Phase 4 scenarios pass; AC1–AC10 (spec §13) verified.
- 24 h soak clean.
- README + runbook complete enough for an ops engineer to deploy unaided.
- Code reviewed; no write path to OPC anywhere in the codebase.
- Config example covers every configurable threshold.