Commit Graph

26 Commits

Author SHA1 Message Date
Joseph Doherty 081fa693af fix(s7): access-denied is an answer; review round-1 minors (R07:H4 follow-through)
I-1: a PUT/GET-disabled CPU (every read denied) stayed Faulted with
LastSuccessfulRead frozen, so the comms-loss watchdog reconnected and
re-subscribed it every 90 s forever. Both access-denied arms now also record
an answer: Faulted is kept (worst state wins), the stamp advances, and the
sticky write fault still clears only on a real success. Pinned end to end in
Runtime.Tests: the real S7Driver under the real DriverInstanceActor watchdog
is not reconnected across 2x the threshold; the control (every read timing
out) is.

Minors:
- m-1: injectable pass clock (UtcNow); "LastSuccessfulRead advanced" is now
  asserted strictly, including the write-refusal case.
- m-2: the editor uses S7TagShapeRules.IsByteAnchored instead of its own list.
- m-3: a release that races an init publish also cancels the probe loop the
  publish started after the release took its snapshot.
- m-4: a retained poll failing during a lifecycle operation writes no health.
- m-5: a multi-var chunk that fell back memoises its members refused alone.
- m-7: access-denied needs S7.Net's exact wording and no transport fault in
  the chain.
- m-8: sync teardown clears the write-side Degraded owner; a sync Dispose
  racing a running shutdown is tested.

Refs #742
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 15:57:53 -04:00
Joseph Doherty 1f7ab80b81 fix(s7): a refused span falls back per member and is not regrouped (R07:P2)
A span's block is the union of its members' ranges, so one member that runs
past the DB's real end (or over-declares a STRING) made the CPU refuse the
whole block, and every neighbour within it was Bad on every poll. The
planner's stated invariant (a grouped read cannot fault where per-tag reads
would succeed) was false; the multi-var path already fell back, spans did not.

- A refused multi-member span hands its members back; they are read one at a
  time for that pass, bypassing the multi-var pass.
- _noGroup remembers the members refused on their own (or every member when
  none was) and keeps them out of later plans, so the rest group again. It is
  cleared by every initialize publish. Non-refusal span failures are
  unchanged (every member stamped).
- The once-per-tag refusal Warning memo (R07:H4) is now also cleared by the
  publish, and shutdown resets the write-side Degraded owner, as documented.
- Live gate: MW10 + MD254 (2 bytes past the 256-byte M area) merged into one
  refused block; MW10 keeps reading 4097 on every pass.

Refs #742
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 15:30:12 -04:00
Joseph Doherty 1d83cde6bc perf(s7): fail the rest of a pass fast once the handle is dead (R07:P1)
No read or write loop checked _plcDead between items: on a frozen-but-
established peer the first op blew its deadline and marked the handle dead,
and every remaining span / tag / write still went to the wire and could wait
its own deadline under _gate (a 20-span pass ~100 s), with Degraded and the
comms-loss watchdog's clock deferred until the pass ended.

- The span loop, the multi-var chunk loop, the per-tag loop and the write
  loop stamp BadCommunicationError without I/O once _plcDead is set.
- A multi-var chunk that dies on a transport loss (socket / IO / timeout /
  ConnectionError) marks the handle dead instead of falling back to one
  doomed per-tag read per member. The framing class is deliberately excluded
  there: snap7/S7.Net report a multi-var with one refused item as
  ReadData(WrongNumberReceivedBytes) over a healthy stream.
- Live gate via an in-test freezable TCP proxy (counts request frames, not
  seconds). Mutation shows S7.Net 0.20 already self-closes on a cancelled op,
  so the gate pins the end-to-end property; the driver guard itself is proven
  by the unit tests (an op that ignores its token keeps the socket open).

Refs #742
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 15:26:14 -04:00
Joseph Doherty a1f20e78f9 fix(s7): a CPU refusal is per-tag Bad and counts as answered (R07:H4)
A deterministic PLC refusal (object does not exist, address out of range,
type inconsistent...) degraded driver health, and an all-refused driver (a
wrong DB number) sat Degraded with LastSuccessfulRead frozen, so the 90 s
comms-loss watchdog reconnected it forever against a reachable PLC.

- IsRefusal / IsAccessDenied classify over the whole exception chain. Live
  probe on the snap7 fixture: S7.Net's READ paths raise the item error as a
  BARE Exception ("Received error from PLC: ..."), its write paths as
  PlcException(WriteData). The old PlcException-only match missed the read
  shape entirely, so a real read access-denied was BadCommunicationError +
  Degraded instead of the Faulted PUT/GET alert; the Test Connect probe had
  the same blind spot.
- A refusal (read or write) is BadDeviceFailure with a once-per-tag Warning,
  no health write and no reopen; it counts as answered, so it advances
  LastSuccessfulRead and restores Healthy after a comms fault. An undecodable
  buffer (S7ValueRangeException) also counts as answered.
- A write batch the CPU answered without a failure advances
  LastSuccessfulRead and retires a Degraded the write side itself published.
- Tests that used ReadData as their generic "device fault" now use SendData
  so their Degraded leg is still exercised.

Refs #742
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 15:20:51 -04:00
Joseph Doherty 87a7414df7 fix(s7): refuse unservable tag shapes per tag at every gate (R07:H3, R07:U4)
The init-time tag guards THREW: one mis-authored tag (DB1.DBZ0, DB1.DBW0 typed
Float64, T5 typed Int32, a writable Bool array) faulted the whole driver and the
actor retried the same init every 10 s forever, darkening every tag on the PLC.
Neither the editor nor the deploy gate checked any of it.

- S7AddressParser moves to S7.Contracts (same namespace) and a new
  S7TagShapeRules there is the single authority on servable shapes.
- S7TagDefinitionFactory.FromTagConfig applies it, so the AdminUI editor, the
  deploy gate (Inspect, with the specific reason) and the driver init refuse
  the same tags. Init now skips a refused tag (ISkippedTagReporter) instead of
  throwing; the throwing RejectUnsupportedTagConfigs is gone.
- New rules on top of the old guards: a narrow type at the wrong access width
  (every read failed) and a Bool array not starting at bit 0 (U4: DB1.DBX0.3
  silently read bit 0 of bytes 0..n).
- AdminUI test fixtures that used addresses the driver could never serve
  (DB1.DBS0, a browse "SomeAddress", Int32/Float32 on DBW) now use real ones.

Refs #742
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 15:04:19 -04:00
Joseph Doherty cd50db136e docs: clarify S7 counter evidence and fixture limits 2026-10-13 11:34:59 -04:00
Joseph Doherty dac0747bb4 docs(s7): fixture DB1 size 2048 references
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 16:57:24 -04:00
Joseph Doherty bbea38483f test(s7): codec live tests use the documented byte-anchored DBB form (#632)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 16:55:10 -04:00
Joseph Doherty 2aae3c094f test(s7): live python-snap7 coverage for the #632/#638 codecs, TSAP override and PDU size (#632)
Extends the s7_1500 profile (DB1 2048 bytes, 'hex' seed type) with distinct
non-zero seeds for STRING/WSTRING/CHAR/WCHAR/DTL/DATE/TOD/TIME/S5TIME plus
undecodable buffers; S7_1500CodecTests covers reads, BadOutOfRange paths,
write round-trips (S7_SIM_ALLOW_WRITES=1), TSAP keys and negotiated PDU size.
Skips when S7_SIM_ENDPOINT is unreachable.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 16:53:02 -04:00
Joseph Doherty e77bca385a fix(galaxy,s7): R2 review minors — subscribe-once deadline, probe vs backoff, write-fault ownership, null array elements, write opt-in
- Galaxy: deadline bounds the wait, not the subscribe RPC; BadTimeout for all tags and late handles unsubscribed; doc comment relocated
- S7: probe connect runs on the connect deadline so a timeout opens the backoff window
- S7: a successful write only retires a Faulted the write side owns
- S7: null array elements rejected (BadTypeMismatch); multi-PDU array writes documented non-atomic
- S7 integration: array write test gated on S7_SIM_ALLOW_WRITES=1

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 07:31:22 -04:00
Joseph Doherty 82372f1672 fix(s7): connect backoff, worst-state write health, Volatile health, array writes (#602, #610)
STAB-21 ConnectionBackoff in EnsureConnectedAsync; STAB-22 write-pass accumulator with a
write-side Faulted that a clean read does not erase; _health on the Volatile idiom;
S7-016 contiguous array write (Bool arrays stay refused).

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 06:59:29 -04:00
Joseph Doherty f797df5187 docs(s7-fixture): recapture the TM/CT addressing probe against the shipped profile; note CountingPlcFactory thread-safety 2026-08-17 05:47:43 -04:00
Joseph Doherty fd202d81ce test(s7): live-pin the Timer/Counter multi-var exclusion against a TM/CT-seeded s7_1500 fixture 2026-08-17 05:39:58 -04:00
Joseph Doherty 57c66416d8 test(s7): live-gate span reads + multi-var leftovers against the s7_1500 fixture 2026-08-17 04:24:39 -04:00
Joseph Doherty a4c61989b2 test(v3): migrate 6 driver IntegrationTests to RawTags API
Replace pre-declared options.Tags (typed <Driver>TagDefinition lists) with
options.RawTags (IReadOnlyList<RawTagEntry>) across all 6 Docker-gated driver
integration suites so they COMPILE against the v3 driver API.

Each project gets a local <Driver>RawTags helper mirroring the unit-test
project's helper: it serialises a typed def back to its TagConfig blob via the
driver's <Driver>TagDefinitionFactory.ToTagConfig and packages it as a
RawTagEntry (RawPath = def.Name; DeviceName = def.DeviceHostAddress for the
multi-device drivers AbCip/AbLegacy/TwinCAT/FOCAS). Test intent (addresses,
data types, device routing, assertions) is unchanged — only the
tag-authoring/config-delivery shape was migrated.

Docker-gated: these still skip at runtime without fixtures; COMPILE is the gate.
2026-07-15 20:59:42 -04:00
Joseph Doherty a1b22f979d test(r2-01): env-gated live connect-timeout outage test — the STAB-14 outage class the bounce test cannot produce (task 11) 2026-07-13 10:06:08 -04:00
Joseph Doherty dfc02af3f0 Merge branch 'fix/archreview-crit3-s7-reconnect' 2026-07-09 06:07:33 -04:00
Joseph Doherty f0082af5b9 build(analyzers): wire OTOPCUA0001 tree-wide + triage the surfaced hits (arch-review 07/C-1)
Inject the custom UnwrappedCapabilityCallAnalyzer as an OutputItemType=Analyzer
ProjectReference from Directory.Build.props (excluding the analyzer + its test
project) so OTOPCUA0001 runs on every src/ and tests/ compilation — it previously
enforced its CapabilityInvoker-wrapping rule against nothing but its own 31 unit
tests (the 'built-but-never-wired' failure mode).

Triage of the ~280 surfaced hits, three categories:

1. RESILIENCE-DISPATCH-GAP (7 sites, DriverInstanceActor x6 + GenericDriverNodeManager
   x1): a REAL, previously-untracked gap the analyzer caught on first wiring — the
   Phase 6.1 CapabilityInvoker resilience pipeline (retry/breaker/bulkhead/telemetry)
   is constructed ONLY in tests and was never wired into the production dispatch
   layer. Scoped per-site #pragma with a greppable RESILIENCE-DISPATCH-GAP marker
   explicitly noting these are tracked-but-not-intentional, pending the dispatch-wiring
   remediation (filed as a follow-up). Keeps the analyzer live everywhere else in
   those projects so a NEW unwrapped call still fails the build.

2. Driver-INTERNAL self-calls (3 sites, AbCipAlarmProjection x2 + S7Driver x1):
   a driver's own poll/ack path calling its own capability method. The invoker wraps
   the driver from the dispatch layer OUTWARD; a driver re-wrapping its own internal
   calls would double-wrap. Genuinely intentional — scoped #pragma with that rationale.

3. Wire-level test suites + manual-testing CLIs (12 projects): invoke drivers directly
   by design — the analyzer's own documented intentional case. Project-level NoWarn
   with a comment.

Verified: full solution build green, 0 OTOPCUA0001 hits; analyzer's 31 tests pass;
negative control — dropping one dispatch-gap pragma re-fires OTOPCUA0001 and fails
the Runtime build, proving the analyzer is genuinely live tree-wide, not disabled.
2026-07-08 17:51:19 -04:00
Joseph Doherty 25c0c6f68f fix(s7): add lazy reconnect path (archreview STAB-1 / Critical 3)
The S7 Plc was opened once in InitializeAsync and never re-opened: a transient
PLC reboot / network blip permanently killed the driver until redeploy. Introduce
an IS7Plc / IS7PlcFactory seam (mirrors TwinCAT's ITwinCATClientFactory) and a lazy
EnsureConnectedAsync that disposes a dead handle and re-opens a fresh connection on
the next data call. Reads/writes mark the handle dead on a connection-fatal fault
(socket drop / ErrorCode.ConnectionError) but NOT on a data-address error; the probe
loop routes through EnsureConnectedAsync as a backstop.

The seam also closes the S7 TEST-1 gap — the reconnect state machine is now unit-
testable without a live PLC (S7.Net.Plc is sealed with no in-process fake).

Verification:
- 4 deterministic unit guards (fatal→reopen, data-error→no-reopen, raw socket→reopen,
  reopen-fail→degrade-then-recover); full S7 unit suite 234/234 green.
- LIVE end-to-end proof against real python-snap7 (S7_1500ReconnectTests, double-gated
  on sim reachability + S7_RECONNECT_BOUNCE_CMD): bounced the container, driver observed
  the outage and resumed Good reads with NO redeploy. Baseline smoke 3/3 still green.
2026-07-08 17:04:46 -04:00
Joseph Doherty 57355405a6 chore(security): drop dead audit suppressions; patch OpenTelemetry + Tmds.DBus CVEs
All five suppressed advisories are now resolved at baseline/resolved versions,
so every NuGetAuditSuppress is removed repo-wide:
- System.Security.Cryptography.Xml (GHSA-37gx-xxp4-5rgx / GHSA-w3x6-4m5h-cxqf)
  -> fixed by the .NET 10 baseline (10.0.6)
- OPCFoundation Opc.Ua.Core (GHSA-h958-fxgg-g7w3) -> fixed at resolved 1.5.378.106

Two were still live and are now patched via direct security pins:
- OpenTelemetry.Api 1.9.0 -> 1.15.3 (GHSA-g94r-2vxg-569j) pinned in Cluster;
  Runtime/ControlPlane/AdminUI + tests inherit via project reference
- Tmds.DBus.Protocol 0.20.0 -> 0.21.3 (GHSA-xrw6-gwf8-vvr9) pinned in Client.UI

Also correct the Historian sidecar runtime comments (x86 -> x64, matching the
csproj PlatformTarget). Solution audit: 0 vulnerable packages; full build clean.
2026-06-12 09:03:42 -04:00
Joseph Doherty 9f62f2c242 refactor(driver-s7): extract S7DriverOptions to .Contracts with parallel CpuType enum
Introduces Driver.S7.Contracts (dependency-free POCO project) and moves
S7DriverOptions / S7ProbeOptions / S7TagDefinition / S7DataType into it.
Adds S7CpuType enum mirroring S7.Net.CpuType exactly (7 values with
explicit integer codes). Runtime S7CpuTypeMap bridges S7CpuType →
S7.Net.CpuType at the single Plc construction site in S7Driver.InitializeAsync.
S7DriverFactoryExtensions and S7CommandBase updated to use S7CpuType; test
files updated to match (S7_1500Profile, S7DriverScaffoldTests). AdminUI can
now reference Driver.S7.Contracts without pulling in S7netplus.
2026-05-28 09:08:27 -04:00
Joseph Doherty 64e3fbe035 docs: backfill XML documentation across 756 files
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public
members surfaced by commentchecker — resolves 5,847 of 5,869 issues
(99.6%) across three /fixdocs passes.
2026-05-28 08:10:17 -04:00
Joseph Doherty 2b811477d1 chore(build): introduce central package management for v2
Adds Directory.Packages.props (ManagePackageVersionsCentrally) and
Directory.Build.props (net10.0/nullable/implicit usings/LangVersion latest).
Strips Version attributes from every csproj PackageReference and consolidates
versions into the central file.

Side fixes (necessary to keep the build green on .NET SDK 10.0.105 on macOS):

- Microsoft.CodeAnalysis.CSharp{,.Workspaces}: 5.3.0 -> 5.0.0. The 5.3.0
  analyzer DLL references compiler 5.3.0.0 and the local SDK ships compiler
  5.0.0.0, producing CS9057 on every project that loaded the Analyzers
  output. Master itself was broken on this machine pre-change.
- Server + Server.Tests pin OPCFoundation.NetStandard.Opc.Ua.{Configuration,
  Client} to 1.5.374.126 via VersionOverride, matching Opc.Ua.Server's
  pin. Mixing 1.5.378.106 Opc.Ua.Core transitively with 1.5.374.126
  Opc.Ua.Server breaks CustomNodeManager2 override signatures
  (CS0115 on LoadPredefinedNodes/Browse/HistoryRead*) and CS7069 in
  the tests. The pin disappears when the legacy Server project is
  deleted in Task 56.
- Client.UI + Client.UI.Tests: NuGetAuditSuppress for
  GHSA-xrw6-gwf8-vvr9 (Tmds.DBus.Protocol 0.20.0 reaches both projects
  transitively from Avalonia.Desktop on Linux/macOS only).

Deviation from the plan: TreatWarningsAsErrors=true is NOT set in
Directory.Build.props because the pre-v2 Admin/Server test projects carry
~240 xUnit1051 analyzer warnings that would fail the build. New v2 projects
opt in via their own csproj; the global flag can return once the legacy
projects are deleted in Task 56.
2026-05-26 03:40:24 -04:00
Joseph Doherty 49644fc7fd test(fixtures): migrate integration-test fixture defaults to 10.100.0.35
CLAUDE.md "Docker Workflow" claims (per the 2026-04-28 migration note)
that all fixture-class default endpoints were rewritten to target the
shared Docker host at 10.100.0.35. Audit during today's e2e run showed
the claim was incomplete — five fixture classes still defaulted to
localhost / 127.0.0.1, causing every fixture-touching integration test
to skip with "endpoint unreachable" on a fresh box that hadn't set
the override env vars.

Files corrected:

  - tests/.../Modbus.IntegrationTests/ModbusSimulatorFixture.cs
      DefaultEndpoint: localhost:5020 → 10.100.0.35:5020
  - tests/.../S7.IntegrationTests/Snap7ServerFixture.cs
      DefaultEndpoint: localhost:1102 → 10.100.0.35:1102
  - tests/.../OpcUaClient.IntegrationTests/OpcPlcFixture.cs
      DefaultEndpoint: opc.tcp://localhost:50000 → opc.tcp://10.100.0.35:50000
  - tests/.../AbCip.IntegrationTests/AbServerFixture.cs
      Host default + ResolveHost fallback: 127.0.0.1 → 10.100.0.35
  - tests/.../AbLegacy.IntegrationTests/AbLegacyServerFixture.cs
      Host default + ResolveEndpoint fallback: 127.0.0.1 → 10.100.0.35

XML doc comments referencing the old localhost defaults were updated in
the same pass so the class-summary documentation matches the actual
default. The override-via-env-var mechanism (MODBUS_SIM_ENDPOINT,
AB_SERVER_ENDPOINT, AB_LEGACY_ENDPOINT, S7_SIM_ENDPOINT,
OPCUA_SIM_ENDPOINT) is unchanged — pointing at a real PLC or a
locally-running container still works exactly as before.

Verification:
  - Solution-wide dotnet build: 0 errors.
  - S7.IntegrationTests: 3/3 pass without env-var override.
  - OpcUaClient.IntegrationTests: 3/3 pass without env-var override.
  - Modbus.IntegrationTests: 4/38 (same as the env-var-override run —
    the 2 failures + 32 skips are pre-existing fixture-profile
    mismatches unrelated to this fix).
  - AbCip.IntegrationTests / AbLegacy.IntegrationTests: same results
    as the env-var-override run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 19:32:23 -04:00
Joseph Doherty 969b0847a1 docs: update path references for module-folder reorganization
Rewrite src/ and tests/ project paths in docs, CLAUDE.md, README.md, and
test-fixture READMEs to the new module-folder layout (Core/Server/Drivers/
Client/Tooling). References to retired v1 projects (Galaxy.Host/Proxy/Shared,
the legacy monolithic test projects) are left untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 02:10:29 -04:00
Joseph Doherty a25593a9c6 chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the
Rider Solution Explorer mirrors the module structure. Folders: Core, Server,
Drivers (with a nested Driver CLIs subfolder), Client, Tooling.

- Move every project folder on disk with git mv (history preserved as renames).
- Recompute relative paths in 57 .csproj files: cross-category ProjectReferences,
  the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external
  mxaccessgw refs in Driver.Galaxy and its test project.
- Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders.
- Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL,
  integration, install).

Build green (0 errors); unit tests pass. Docs left for a separate pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 01:55:28 -04:00