Make the service build, run, and install on Linux as a first-class target while keeping the Windows Service + Event Log behaviour intact. - Build: drop the hardcoded win-x64 RID — single-file publish now works for any RID. publish.ps1 gains -Rid; new publish.sh for Linux hosts. - Diagnostics: DiagnosticSinkSelector picks the Error+ sink per host — Windows Event Log under the SCM, local syslog under systemd (Serilog.Sinks.SyslogMessages), none for interactive runs. The EventLog truncation helper is extracted so it is testable cross-OS. - Host: Program.cs registers AddSystemd() alongside AddWindowsService(). - Config: a RID-conditioned appsettings template ships Windows or Unix paths; both templates are schema-validated by a test. - Install: systemd unit (Type=exec) plus install.sh / uninstall.sh. Also fixes two cross-platform bugs found while testing: install.ps1 and uninstall.ps1 used New-EventLog / Remove-EventLog (absent in PowerShell 7), and the E2E sim launcher hardcoded Windows venv paths. - Docs updated across README, CLAUDE.md, and docs/ for dual-platform. 413 tests pass on Windows; 374 (all non-simulator) on Linux. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
29 KiB
mbproxy Multiplatform Implementation Plan
Created: 2026-05-15
Status: All six phases implemented. 413 tests green on Windows; Windows Service and
Linux systemd install E2E both green. Two findings (pymodbus-sim-on-Linux, AddSystemd()
notify) logged as orthogonal follow-ups. Working tree only — nothing committed.
Working artifact — not part of the docs/ source-of-truth tree (per ../DOCS-GUIDE.md).
Delete or archive once the work lands.
Progress log
-
2026-05-15 — Phase 1 done, Gate 1 green. RID removed from
csproj(single-file settings now gated on'$(RuntimeIdentifier)' != '');publish.ps1gained-Rid;publish.shadded.dotnet build -c Debug0 warnings;dotnet test398 passed / 0 failed (baseline 325 → 398, the Keepalive feature added tests);win-x64→Mbproxy.exe100.1 MB,linux-x64→MbproxyELF 97.2 MB. ELF launch-smoked on10.100.0.35: full startup, listeners bound,mbproxy.startup.ready+ admin endpoint up, no errors. Box prep done (.NET SDK 10.0.300, shellcheck 0.10.0 installed). -
2026-05-15 — Phases 2 + 3 code done (combined integrator pass). Packages added:
Microsoft.Extensions.Hosting.Systemd10.0.8,Serilog.Sinks.SyslogMessages4.1.0 (the maintained IonxSolutions package — the bareSerilog.Sinks.SyslogID is a near-abandoned 0.2.0 package; same approved intent). NewDiagnosticSinkenum +DiagnosticSinkSelector(pure); newSyslogBridge;EventLogBridgetruncation extracted to a non-annotatedEventLogMessagetype (testable cross-OS).AddMbproxySerilognow selects the sink internally;Program.cscallsAddSystemd()+AddWindowsService(). 13 new tests. 411 passed / 0 failed on Windows; on10.100.0.35372 passed / 39 skipped / 0 failed — all 39 skips are simulator-backed E2E (see finding below), every host/diagnostic/smoke test green on Linux. -
2026-05-15 — Two cross-platform bugs found and fixed in install tooling. (1)
tests/sim/run-dl205-sim.ps1was Windows-only — hardcoded venv pathsScripts\*.exe; now branchesScripts/.exevsbin/`` on$IsWindowsand addspython3to the interpreter candidates. (2)install.ps1/uninstall.ps1usedNew-EventLog/Remove-EventLog, which exist only in Windows PowerShell 5.1 — they fail under PowerShell 7+. Switched to the .NET API ([EventLog]::CreateEventSource/DeleteEventSource), symmetric with theSourceExistscalls already in those scripts. -
2026-05-15 — Windows Service E2E green (local, admin). Republished
win-x64;install.ps1 -Startinstalls + starts the service; verified Running/Automatic,status.jsonserved, listeners bound,mbproxy.startup.readylogged, Event Log source registered,WindowsServiceLifetimewrote "Service started successfully" (proves the process runs under the SCM).uninstall.ps1stopped/deleted the service, archived logs, removed the Event Log source. Box left clean. (A forcedEventLogBridgeError+ write was not pursued —Emitis unchanged code, covered byEventLogMessageTests; sink selection is covered byDiagnosticSinkSelectorTests.) -
2026-05-15 — Linux systemd E2E done. The
linux-x64ELF runs under a real systemd unit on10.100.0.35: starts, binds listeners, serves the admin endpoint, andsystemctl stop→ graceful SIGTERM drain (mbproxy.shutdown.completein the journal).Type=notifydoes not work (see Findings) → Phase 5 will shipType=exec. Box prep this session:dotnet-sdk-10.0,shellcheck,python3-venv, pwsh 7.6.1 (dotnet global tool), pymodbus 3.13.0 venv. -
2026-05-15 — Phases 4–6 done. Phase 4: new
install/mbproxy.linux.config.template.json(Unix log path/var/log/mbproxy, systemd-oriented comments);csprojlinks the platform-correct template into the publishedappsettings.jsonby RID (win-*/RID-less → Windows, else Unix) — verified by publishing both RIDs;MbproxyOptionsBindingTestsextended to load + schema-validate both templates (now 413 tests on Windows). Phase 5:install/mbproxy.service(Type=exec, hardened,mbproxyservice account),install/install.sh,install/uninstall.sh—shellcheckclean; install→active→status.jsonserved→uninstall→clean E2E passed on10.100.0.35. Phase 6:README.md,mbproxy/CLAUDE.md,../CLAUDE.md,docs/Operations/Configuration.md,docs/Reference/LogEvents.md,docs/Operations/Troubleshooting.md,docs/Architecture/Overview.md,docs/Features/HotReload.mdupdated for the dual-platform reality.
Findings
- Linux full run: 374 passed / 37 failed / 0 skipped. With the simulator
launcher fixed and pymodbus provisioned, the simulator-backed E2E tests now
run on Linux (0 skipped) but 37 fail with
IOException: Broken pipe(SocketException) when the NModbus client writes through the proxy. The failures are broad across all simulator-backed E2E (cache, forwarding, rewriter, supervision). Not a Phases 1–3 regression: the multiplatform work touches only build config, diagnostic sinks, and host registration — none of the Modbus proxy data path. The same 37 tests pass on Windows (411/411), and every non-E2E test — including all 13 new diagnostic tests — passes on Linux. Root cause isolated: theSimulatorSmokeTests— which connect directly to the pymodbus simulator with no proxy in the path — also fail (TCP connect error). So the fault is the pymodbus 3.13.0 simulator itself on this box, not mbproxy's proxy code. Likely pymodbus 3.13.0 vs Python 3.13.5 (both very new), or the box's Docker-host networking. Treated as a separate investigation (pymodbus-simulator-on-Linux), entirely orthogonal to the multiplatform service work — see the session report. - The
run-dl205-sim.ps1idempotency check keys onTest-Path $venvDironly; a venv left structurally broken by a killed run (nobin/) is not detected and re-created. Pre-existing latent gap, not platform-specific — noted, not fixed (out of scope; a clean run is unaffected). AddSystemd()does not deliversd_notify(READY=1)here → Phase 5 usesType=exec. mbproxy runs correctly under systemd (starts, binds, serves, and SIGTERM → graceful drain all work — verified in the journal), but aType=notifyunit never receivesREADY=1and times out. Isolated step by step:SystemdHelpers.IsSystemdService()correctly returnsTrueunder systemd; a minimalHost.CreateApplicationBuilder()+AddSystemd()host reproduces the failure; both asystemd-runtransient unit and a realType=notifyunit file fail identically. So it is not an mbproxy bug — it is aHostApplicationBuilder+Microsoft.Extensions.Hosting.Systemd10.0.8 (minimal-hosting) issue. Resolution: the Phase 5 unit usesType=exec— mbproxy is a leaf service that nothing orders against, so the readiness signal is unnecessary;Type=exec+ the generic host's built-in POSIXSIGTERMhandling (independent ofSystemdLifetime) gives a fully working unit withRestart=on-failure.AddSystemd()stays inProgram.cs(correct, documented, forward-compatible, harmless). Root-causing the .NET notify gap is logged as a separate follow-up.
A plan to make mbproxy run on Linux (and incidentally macOS) as a first-class target while keeping the Windows Service + Event Log behavior intact and adding systemd + journald/syslog equivalents.
The hosting model (Host.CreateApplicationBuilder + IHostedService + Kestrel)
is already portable, so the work is narrow: generalize the build, abstract one
diagnostic sink, add one package + one call, and add Linux tooling/docs.
0. Test Environments
Both platforms can be exercised fully — no environment is simulated or deferred.
0.1 Windows (the dev box — local)
The dev box runs with administrator rights, so every Windows gate runs locally with no separate test machine:
install.ps1(requires elevation) installs the real Windows Service.- The Event Log source
mbproxycan be registered andEventLogBridgewrites verified against the Application log. - Install → start → stop → uninstall is a full local round-trip.
Windows Service E2E mutates machine state (a registered service + Event Log source). It is integrator-only and the integrator always runs
uninstall.ps1to leave the box clean after each gate.
0.2 Linux
Host: dohertj2@10.100.0.35 — Debian 13 (trixie), amd64, kernel 6.12,
hostname DOCKER. systemd 257.
- Access: passwordless SSH from the Windows dev box; passwordless
sudo(verified 2026-05-15). - Reachable on
10.100.0.35(also10.50.0.35,10.200.0.35). - One-time prep (run once before Wave 1 gates):
ssh dohertj2@10.100.0.35 'sudo apt-get update && \ sudo apt-get install -y dotnet-sdk-10.0 shellcheck'dotnet-sdk-10.0candidate is10.0.203— matches thenet10.0target. - Docker is installed on the box (the user is in the
dockergroup). Use ephemeral Debian containers to isolate per-subagent E2E runs so parallel Wave-4 agents don't collide on the host's systemd / ports (see section 3, rule 8).
How the integrator uses the box per gate:
- Push the integration branch (or
rsyncthe worktree) to the box, then rundotnet build/dotnet test/dotnet publish -r linux-x64over SSH. - Run the actual
linux-x64ELF binary, the systemd unit, andshellcheckhere — Windows can cross-publish alinux-x64binary but cannot run or service-host it.
The box is a shared mutable resource. Host-level mutations (apt installs,
systemctlon the real host, privileged-port binds) are integrator-only and run serially between waves. Subagents that need Linux E2E use throwaway Docker containers, never the host's init system directly.
1. Scope
In scope
- Linux (
linux-x64) as a supported runtime target alongsidewin-x64. - systemd integration (
Type=notify, sd_notify readiness, SIGTERM drain). - A Linux-appropriate error-event diagnostic sink (syslog, severity-mapped).
- RID-agnostic build + dual-RID publish tooling.
- Linux install tooling (systemd unit + shell scripts).
- Docs/README/CLAUDE.md updates.
Out of scope (state explicitly in docs)
- macOS
launchdintegration — mbproxy will run on macOS as a console process but ships no service-manager integration. - ARM RIDs (
linux-arm64) — the build will not forbid them, but they are untested. - Container/Docker packaging — separate future effort.
Locked design decisions
- Reference
Microsoft.Extensions.Hosting.WindowsServicesandMicrosoft.Extensions.Hosting.Systemdunconditionally; both packages are portable and both helpers self-detect their host. No conditional<PackageReference>. - All Windows API calls (
System.Diagnostics.EventLog) stay behindOperatingSystem.IsWindows()+[SupportedOSPlatform("windows")]; CA1416 (already enforced viaTreatWarningsAsErrors) is the safety net. - Diagnostic sink selection happens once, at the composition root
(
AddMbproxySerilog). No OS branching anywhere else. - Prefer new files over editing shared files, to keep parallel work conflict-free.
- Linux error-event sink:
Serilog.Sinks.Syslog(decided 2026-05-15). Error+ events get RFC5424 severity mapping on Linux, mirroring the Windows Event Log behavior where Error+ is surfaced distinctly.DiagnosticSinkSelectorreturnsEventLog | Syslog | None.
2. Phase Breakdown
Each phase lists its owned file set (the parallel-safety contract), changes, tests, and a gate that must be green before the next phase starts.
Phase 1 — Build & publish generalization (foundation)
Objective: Remove the hardcoded RID so the project builds/publishes for any runtime; keep the Windows output byte-identical.
Owned files
src/Mbproxy/Mbproxy.csprojinstall/publish.ps1install/publish.sh(new)
Changes
Mbproxy.csproj: delete<RuntimeIdentifier>win-x64</RuntimeIdentifier>from the ReleasePropertyGroup; keepPublishSingleFile/SelfContained/IncludeNativeLibrariesForSelfExtract. RID becomes a publish-time-rargument.publish.ps1: add a-Ridparameter (defaultwin-x64), keep the two-flavor logic.publish.sh: Linux counterpart producinglinux-x64self-contained + framework-dependent builds.- (The RID-conditioned
appsettings.jsoncontent item is Phase 4; in Phase 1 just confirm the build works without a baked RID.)
Tests
- No xunit tests (build-config change). Gate is publish success on both RIDs.
Gate 1
dotnet build -c Debuggreen;dotnet testfull suite green (unchanged count).dotnet publish -c Release -r win-x64produces a single-fileMbproxy.exe(same size class as before).dotnet publish -c Release -r linux-x64produces a single-fileMbproxyELF binary. Cross-published from the Windows dev box; the ELF is then copied to10.100.0.35and confirmed to launch (./Mbproxy --version-class smoke).- Zero new analyzer warnings.
Phase 2 — Diagnostic sink abstraction
Objective: Make error-event delivery a platform-selected sink. Windows
keeps EventLogBridge; Linux gets a syslog sink.
Owned files
src/Mbproxy/Diagnostics/DiagnosticSinkSelector.cs(new — pure selection logic)src/Mbproxy/Diagnostics/SyslogBridge.cs(new)src/Mbproxy/Diagnostics/EventLogBridge.cs(minor: extract the 32 KB truncation helper into a testable static method)src/Mbproxy/HostingExtensions.cs(onlyAddMbproxySerilog)src/Mbproxy/Mbproxy.csproj(addSerilog.Sinks.Syslogpackage)- New test files (see below)
HostingExtensions.csandMbproxy.csprojare also touched by Phase 3. Phases 2 and 3 must not run in parallel (see section 3). They are sequential.
Changes
DiagnosticSinkSelector— a pure function taking(bool isWindows, bool isWindowsService, bool isSystemd)and returning an enum (EventLog | Syslog | None). No I/O, fully unit-testable.SyslogBridge: SerilogILogEventSinkwrappingSerilog.Sinks.Syslog, active for Error+ only, mirroringEventLogBridge's contract (silent no-op if syslog unavailable).AddMbproxySerilog: replace theaddEventLogBridgebool parameter with aDiagnosticSinkSelectorresult; wire the chosen sink. Keep theOperatingSystem.IsWindows()guard aroundEventLogBridge.- Extract
EventLogBridge's message-truncation intointernal static string TruncateToEventLogLimit(string)so it can be tested OS-independently.
Tests (tests/Mbproxy.Tests/Diagnostics/)
DiagnosticSinkSelectorTests— table-driven: Windows+service→EventLog; Windows console→None; Linux+systemd→Syslog; Linux console→None; macOS→None.EventLogBridgeTests—[Trait("Category","Unit")], Windows-guarded facts: source-missing → silent no-op; truncation helper caps at 32 KB and appends...(this fact runs on all OSes since the helper is pure).SyslogBridgeTests— Error+ filter; no-throw when transport unavailable.
Gate 2
- Full test suite green on Windows (local); full suite green on Linux —
integrator runs
dotnet testover SSH on10.100.0.35. EventLogBridgeemits to the Application log — verified locally via a real Windows Service install (install.ps1, admin rights available), thenuninstall.ps1to clean up.- CA1416: zero warnings.
Phase 3 — Service host integration (systemd)
Objective: Register both init-system integrations; the host correctly reports readiness to whichever launched it.
Owned files
src/Mbproxy/Program.cssrc/Mbproxy/HostingExtensions.cs(call-site update only)src/Mbproxy/Mbproxy.csproj(addMicrosoft.Extensions.Hosting.Systemd)
Changes
csproj: add<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" />(pin to the 10.0.x line matching the existing Windows-services package).Program.cs: callbuilder.Services.AddSystemd();alongsideAddWindowsService();. ComputeisSystemdviaSystemdHelpers.IsSystemdService()and feedDiagnosticSinkSelectortogether withisWindowsService.- Confirm SIGTERM → host shutdown → existing
Connection.GracefulShutdownTimeoutMsdrain path works (it does — POSIX signal handling is built into the generic host; just verify).
Tests (tests/Mbproxy.Tests/HostSmokeTests.cs — extend existing file)
HostSmoke_RegistersBothServiceIntegrations_StartsAndStops— builds the host with bothAddWindowsService+AddSystemd, asserts no throw, assertsmbproxy.startup.readystill logged.- Existing two smoke tests must remain green.
Gate 3
- Full suite green on Windows (local) and Linux (
10.100.0.35via SSH). - Windows Service E2E, run locally with admin rights:
install.ps1→ service starts, logsmbproxy.startup.ready+ writes to Event Log,Stop-Servicedrains cleanly,uninstall.ps1removes it. No regression in Windows behavior is the hard requirement of this gate. - Linux systemd E2E on
10.100.0.35— done. Thelinux-x64binary runs under a real systemd unit: it starts, binds listeners, serves the admin endpoint, andsystemctl stop(SIGTERM) drains gracefully (mbproxy.shutdown.completein the journal).Type=notifywas found not to deliverREADY=1(Findings) → the Phase 5 unit usesType=exec, under which the service is fully functional.
Phase 4 — Config & filesystem portability
Objective: No Windows-only paths in the shipped/installed config.
Owned files
install/mbproxy.config.template.json(Windows — keepC:\ProgramData\...path)install/mbproxy.linux.config.template.json(new —/var/log/mbproxy/..., Linux syslogUsingentry)src/Mbproxy/Mbproxy.csproj(condition the linkedappsettings.jsoncontent item by$(RuntimeIdentifier))
Touches
csproj. Must run after Phase 3's csproj edit is merged (sequential w.r.t. csproj), but is otherwise independent of Phase 5/6.
Changes
- New Linux template: log path
/var/log/mbproxy/mbproxy-.log; SerilogUsingarray includes the syslog sink; comment header points at/etc/mbproxy/appsettings.json. csproj: link the win template forwin-*RIDs and the linux template forlinux-*RIDs into the publishedappsettings.json(RID-conditioned<Content>items).
Tests (tests/Mbproxy.Tests/Options/)
- Extend
MbproxyOptionsBindingTests: load each shipped template through the config binder +MbproxyOptionsValidator; assert both bind and validate cleanly. Catches a malformed Linux template at build time.
Gate 4
- Both templates bind + validate (new test green).
dotnet publish -r linux-x64ships the Linux template asappsettings.json;-r win-x64ships the Windows one. Verify by inspecting publish output.
Phase 5 — Linux install tooling
Objective: Parity with install.ps1 for systemd hosts.
Owned files (all new, fully disjoint from all other phases)
install/mbproxy.service— systemd unit,Type=exec(notType=notify— see Findings:AddSystemd()does not deliverREADY=1for the minimal hosting model),Restart=on-failure,User=mbproxy,ExecStartpointing at the installed binary; setsDOTNET_BUNDLE_EXTRACT_BASE_DIR.install/install.sh— createsmbproxyservice account, lays down binary +/etc/mbproxy/appsettings.json(preserve-if-exists, matchinginstall.ps1semantics), creates/var/log/mbproxy, installs +systemctl enable --now.install/uninstall.sh—systemctl disable --now, archives logs (mirror the.archived-<ts>convention), removes unit.
Tests
- Not xunit. Gate =
shellcheckclean + a dry-run inside a throwaway Debian container on10.100.0.35.
Gate 5
shellcheck install/*.shclean — run on10.100.0.35(shellcheck installed in the one-time prep).- End-to-end on
10.100.0.35, inside a throwaway Debian container:install.sh→ service active → proxy answers Modbus on a configured port →uninstall.sh→ service gone, logs archived. Container isolation keeps thembproxyservice account / unit off the real host.
Phase 6 — Documentation
Objective: Docs reflect dual-platform reality; doctrine in DOCS-GUIDE.md
respected.
Owned files
README.md— rewrite "Hard constraints / prerequisites" (drop "No Linux or Docker support"); add Linux install path; document both publish flavors × both RIDs.docs/Operations/Configuration.md— both config templates, log-path differences, syslog vs Event Log.docs/Operations/Troubleshooting.md—journalctlguidance alongside Event Viewer.docs/Architecture/Overview.md— note dual init-system hosting (only if it shifts a headline bullet).docs/Reference/LogEvents.md— note Error+ events route to Event Log (Windows) / syslog (Linux).mbproxy/CLAUDE.md— correct the implied Windows-only framing.wwtools/CLAUDE.md— broaden the mbproxy index row if the task→tool mapping changed.
Tests
- Markdown link-check across touched files.
Gate 6
- All internal doc links resolve.
- README "Hard constraints" no longer contradicts the shipped tooling.
3. Parallel Subagent Execution Plan
Dependency graph
Phase 1 (build) ──> Phase 2 (diagnostics) ──> Phase 3 (host) ──┬─> Phase 4 (config)
├─> Phase 5 (install)
└─> Phase 6 (docs)
Phases 2 and 3 are strictly sequential: Phase 3 calls the new
AddMbproxySerilog signature Phase 2 defines, and both edit
HostingExtensions.cs + csproj. Phases 4, 5, 6 are mutually independent
and parallelizable once Phase 3 is merged.
Wave plan
| Wave | Phases | Agents | Mode |
|---|---|---|---|
| W1 | Phase 1 | 1 agent | Single — touches csproj |
| W2 | Phase 2 | 1 agent | Single — touches csproj + HostingExtensions |
| W3 | Phase 3 | 1 agent | Single — touches csproj + HostingExtensions + Program.cs |
| W4 | 4, 5, 6 | 3 agents (parallel) | Parallel — disjoint file sets |
Phase 4 touches
csprojbut no other W4 phase does, so within W4 the file sets are still disjoint. Safe.
File-ownership matrix (the parallel-safety contract)
| File | P1 | P2 | P3 | P4 | P5 | P6 |
|---|---|---|---|---|---|---|
Mbproxy.csproj |
x | x | x | x | ||
HostingExtensions.cs |
x | x | ||||
Program.cs |
x | |||||
Diagnostics/* (new + EventLogBridge) |
x | |||||
install/publish.* |
x | |||||
install/*.config.template.json |
x | |||||
install/install.sh, uninstall.sh, .service |
x | |||||
tests/** |
x | x | x | |||
| docs / READMEs / CLAUDE.md | x |
No column in W4 (P4/P5/P6) shares a row. Confirmed conflict-free.
Subagent rules (enforce in every dispatch prompt)
- One git worktree per subagent — dispatch each
Agentcall withisolation: "worktree". Physical isolation means even a stray edit can't corrupt a sibling's tree. - Owned-file contract — each subagent is told its exact owned file set from the matrix and instructed to edit nothing outside it. A subagent that discovers it needs an out-of-set file must stop and report, not edit.
- No intra-wave API coupling — subagents in the same wave may only depend on public APIs from already-merged prior waves, never on a sibling's in-progress work. (This is why P2→P3 are separate waves, not parallel.)
- Tests ship with code — the subagent that writes a phase's code also
writes that phase's tests and runs
dotnet testgreen in its own worktree before reporting done. No separate "test agent." - Integrator merges in declared order — the main agent merges each worktree, runs the full build + test suite, and only then declares the phase gate met. A failed gate blocks the next wave.
- High-contention files are single-agent-only —
csproj,HostingExtensions.cs,Program.cs,CLAUDE.mdare never edited by two agents in the same wave (the matrix guarantees this). - Prefer new files —
DiagnosticSinkSelector.cs,SyslogBridge.cs,mbproxy.linux.config.template.json, the shell scripts, the unit file are all new — new files can't merge-conflict, maximizing safe parallelism. - Shared test hosts are integrator-only for mutations — subagents may run
dotnet build/dotnet test(read-mostly) but must not install a Windows Service, register an Event Log source, orsystemctlagainst the real10.100.0.35host. Service-level E2E is the integrator's job at gate time; if a subagent needs Linux E2E it spins an ephemeral Docker container on the box (named per-agent,--rm) so parallel agents never collide on ports, the init system, or service accounts.
Merge protocol per wave
for each wave:
dispatch agent(s) with isolation: worktree + owned-file list
on completion:
integrator: merge worktree(s) in matrix order
integrator: dotnet build -c Debug (must be green)
integrator: dotnet test (green, count >= prior)
integrator: dotnet publish -r win-x64 AND -r linux-x64 (must succeed)
integrator: verify phase-specific gate checklist
gate green? -> next wave. gate red? -> fix in a single-agent pass, re-gate.
4. Cross-Cutting Test Strategy
- Existing baseline (325 = 282 unit + 43 E2E) must never regress. Every gate re-runs the full suite.
- New tests target pure logic —
DiagnosticSinkSelectoris a pure function precisely so platform-selection is testable without being a service. Highest- value new test. - OS-conditional tests use
[Trait]+ a runtimeOperatingSystem.IsWindows()skip so the suite is green on both Windows and Linux. - Both platforms are exercised every gate, no simulation. Windows runs
locally (admin rights → real Windows Service install). Linux runs on
dohertj2@10.100.0.35(Debian 13, systemd 257) — the integrator drivesdotnet build/dotnet test/ publish / systemd E2E over SSH. - CI (if/when a pipeline exists): add a
linux-x64build+test leg, ideally pointed at the same box or an equivalent image. Until then the integrator's per-gate SSH run on10.100.0.35is the Linux leg. - CA1416 platform analyzer is treated as a test —
TreatWarningsAsErrorsalready fails the build if a Windows API escapes its guard.
5. Risk Register
| Risk | Phase | Mitigation |
|---|---|---|
| Windows Service behavior regresses unnoticed | P3 | Gate 3 mandates a real Windows Service install/start/stop smoke check |
Serilog.Sinks.Syslog version drift |
P2 | Pin the version; SyslogBridge is isolated behind DiagnosticSinkSelector |
| Linux publish ships Windows config path | P4 | RID-conditioned <Content> item + MbproxyOptionsBindingTests on both templates |
| Self-extracting single-file temp-dir perms | P1/P5 | Document + set DOTNET_BUNDLE_EXTRACT_BASE_DIR in the systemd unit |
Two agents racing csproj |
all | Matrix forbids it — csproj edited only in single-agent waves W1–W3 + lone P4 |
| Hidden Windows path elsewhere in code | all | Grep sweep for C:\\, ProgramData, \\\\ before Gate 6 |
Parallel Wave-4 agents collide on the shared 10.100.0.35 host |
W4 | Rule 8 — service-level E2E is integrator-only and serial; subagent E2E uses per-agent --rm Docker containers |
| Windows Service E2E leaves stale service/Event Log source | P2/P3 | Integrator always runs uninstall.ps1 after each Windows gate |
6. Deliverable Summary
- 3 modified source files (
csproj,HostingExtensions.cs,Program.cs)- 3 new (
DiagnosticSinkSelector.cs,SyslogBridge.cs, and the truncation-helper extraction inEventLogBridge.cs).
- 3 new (
- 2 new packages (
Microsoft.Extensions.Hosting.Systemd,Serilog.Sinks.Syslog). - 6 new install/tooling files (
publish.sh, Linux config template,mbproxy.service,install.sh,uninstall.sh). - ~6–8 new tests across 3 new/extended test files; baseline 325 preserved.
- 7 doc files updated.
- 4 waves, max 3 concurrent subagents, conflict-free by construction.