e66b17fe5f
Resolves the 21 Major findings catalogued in
codereviews/2026-05-14/RemediationPlan.md (Wave 2). Tests: 370 pass / 0 fail
(baseline 363 + 7 new W2 regression tests).
Multiplexer / concurrency:
W2.1 ConfigReconciler.Attach now threads the live coalescingAccessor through
to add/restart-built supervisors so a hot-reload of
ReadCoalescing.{Enabled,MaxParties} propagates to PLCs added or
restarted via reload.
W2.2 PlcMultiplexer._disposed and UpstreamPipe._disposed are now volatile
for ARM/portability defense.
W2.3 ProxyWorker._supervisors / ConfigReconciler._supervisors switched from
Dictionary to ConcurrentDictionary; reconciler uses TryRemove. The
outer Apply is serialised by a semaphore but the inner Add/Remove/
Restart Task.WhenAll continuations run in parallel.
W2.4 Counter parity for cache miss + coalescing-saturation miss documented
inline (per-design contract; behavior unchanged).
W2.5 _disposeCts.Dispose() and _connectGate.Dispose() guarded against late
watchdog ticks.
W2.6 _connectGate disposed in DisposeAsync.
W2.7 Inline doc clarifying the post-rewriter FC byte read.
Cache / hot-reload:
W2.8 PlcListenerSupervisor.ReplaceContextAsync now calls Clear() to capture
the entry count, emits mbproxy.cache.flushed, then disposes the old
cache. Previously the event was defined but never emitted.
W2.9 Inline doc explaining the implicit "skip cache invalidation while
recovering" gating (no backend reader during recovery → no FC06/FC16
response → no invalidation).
W2.10 ReloadValidator now re-checks resolved per-tag CacheTtlMs against
Cache.AllowLongTtl after BcdTagMapBuilder folds the per-PLC default.
BCD rewriter:
W2.11 Duplicate addresses detected within Global itself and within the per-PLC
Add list itself, BEFORE the working dictionary collapses keys. Cross-list
collisions (Global vs Add) remain the documented width-override pattern.
Previously the DuplicateAddress error was unreachable dead code.
W2.12 OverlappingHighRegister reports each colliding pair exactly once
(canonicalised low/high pair tracked in a HashSet).
W2.13 FC16 32-bit write rejects clientLow > 9999 or clientHigh > 9999 BEFORE
the high*10000+low reconstruction. Without this guard, (high=9999,
low=9999) silently re-encoded as (high=9998, low=9999), losing 1 from
the high word.
W2.14 FC16 validates pdu.Length >= 6 + qty*2 upfront — no half-rewritten
requests when a malformed client claims more registers than it ships.
Supervisor:
W2.15 WaitForInitialBindAttemptAsync now backed by TaskCompletionSource
instead of 10ms busy-poll. Resolves race against fast Stopped→Bound→
Stopped transitions and hangs when the supervisor task throws.
W2.16 StartAsync refuses re-entry on a non-Stopped supervisor (was leaking
the previous _supervisorCts).
W2.17 New TransitionTo helper writes _state, _lastBindError, and (optionally)
_recoveryAttempts under one lock. Snapshot() reads under the same lock
so the status page never reports an inconsistent triple. Truncate
helper extracted (was copy-pasted across three sites).
W2.18 MbproxyOptionsValidator + ReloadValidator reject Connection.{Backend
ConnectTimeoutMs, BackendRequestTimeoutMs, GracefulShutdownTimeoutMs}
<= 0. Misconfigured 0 produces immediate CancelAfter(0) failures.
Hosting / diagnostics:
W2.20 ProxyWorker.StopAsync supervisor-stop deadline now reads from
IOptionsMonitor.CurrentValue.Connection.GracefulShutdownTimeoutMs
(was hard-coded 5s).
W2.21 src/Mbproxy/appsettings.json deleted; the published file is now a Link
to install/mbproxy.config.template.json so the binary ships with a
usable, fully-commented example config instead of an empty stub. Tests
strip the inherited file from their bin via an AfterTargets="Build"
Target so they don't pick up the template's example PLCs.
W2.22 invalidBcdWarnings (PlcPdusStatus) and codeOther (ExceptionCounts)
added to StatusDto, plumbed through StatusSnapshotBuilder, surfaced
in StatusHtmlRenderer table cells.
W2.23 EventLogBridge caches EventLog.SourceExists at construction so Emit
doesn't hit the registry on every Error+ log line.
New regression tests:
ReloadValidatorTests:
Validate_PerTagCacheTtl_Above60s_Without_AllowLongTtl_Fails
Validate_PerTagCacheTtl_Above60s_With_AllowLongTtl_Passes
Validate_ResolvedTtl_FromPerPlcDefault_AboveCap_Fails
Validate_ZeroBackendConnectTimeoutMs_Fails
Validate_NegativeGracefulShutdownTimeoutMs_Fails
BcdPduPipelineTests:
FC16_32Bit_ClientHighOrLowAbove9999_PassesThroughRaw_WithInvalidBcdWarning
FC16_TruncatedRegisterData_PassesThroughRaw_NoPartialRewrite
Reworked tests in BcdTagMapBuilderTests for the W2.11 contract (Global dup,
Add dup, Add-overrides-Global accepted as width override).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
66 lines
3.2 KiB
XML
66 lines
3.2 KiB
XML
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
|
|
|
<PropertyGroup>
|
|
<TargetFramework>net10.0</TargetFramework>
|
|
<OutputType>Exe</OutputType>
|
|
<Nullable>enable</Nullable>
|
|
<ImplicitUsings>enable</ImplicitUsings>
|
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
<RootNamespace>Mbproxy</RootNamespace>
|
|
<AssemblyName>Mbproxy</AssemblyName>
|
|
<!-- Phase 08: Assembly version. CI can override via /p:InformationalVersion=... -->
|
|
<InformationalVersion>1.0.0</InformationalVersion>
|
|
</PropertyGroup>
|
|
|
|
<!-- Phase 08: single-file self-contained publish (Release only; Debug stays normal for fast iteration).
|
|
NOTE: the resulting Mbproxy.exe is ~100 MB because the self-contained publish bundles the full
|
|
.NET 10 + ASP.NET Core runtime. This exceeds the original 50 MB target in the phase spec;
|
|
the runtime size is a fixed cost of self-contained deployment on .NET 10 with ASP.NET Core.
|
|
Operators who need a smaller footprint can use a framework-dependent publish
|
|
(dotnet publish -c Release -r win-x64 - -self-contained false /p:PublishSingleFile=true)
|
|
if the target machine has .NET 10 installed. -->
|
|
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
|
<PublishSingleFile>true</PublishSingleFile>
|
|
<SelfContained>true</SelfContained>
|
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
|
</PropertyGroup>
|
|
|
|
<ItemGroup>
|
|
<!-- ASP.NET Core for the Phase 07 Kestrel-hosted admin endpoint. -->
|
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
|
</ItemGroup>
|
|
|
|
<ItemGroup>
|
|
<!-- Microsoft.Extensions.Hosting is already included transitively via
|
|
Microsoft.AspNetCore.App — do not re-add it explicitly. -->
|
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.8" />
|
|
<PackageReference Include="Serilog.Extensions.Hosting" Version="10.0.0" />
|
|
<PackageReference Include="Serilog.Settings.Configuration" Version="10.0.0" />
|
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
|
|
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
|
<!-- Referenced now so phase 04/05 don't need to touch this csproj; usage is deferred -->
|
|
<PackageReference Include="Polly" Version="8.6.6" />
|
|
</ItemGroup>
|
|
|
|
<ItemGroup>
|
|
<!-- Allow test project to access internal types (HeartbeatWorker, HostingExtensions, etc.) -->
|
|
<InternalsVisibleTo Include="Mbproxy.Tests" />
|
|
</ItemGroup>
|
|
|
|
<ItemGroup>
|
|
<!-- Phase 12 (W2.21) — link the install template as the published appsettings.json
|
|
so the binary ships with a fully-commented, usable example config (one PLC, one
|
|
BCD tag, all sections present) instead of an empty stub. The .NET configuration
|
|
loader supports JSONC (comments) under the default Host.CreateApplicationBuilder
|
|
path, so the comments in the template are valid at runtime.
|
|
A fresh `dotnet run` from src/Mbproxy is no longer a no-op service. -->
|
|
<None Remove="appsettings.json" />
|
|
<Content Include="..\..\install\mbproxy.config.template.json"
|
|
Link="appsettings.json">
|
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
</Content>
|
|
</ItemGroup>
|
|
|
|
</Project>
|