mbproxy: cross-platform support — Linux/systemd alongside Windows
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>
This commit is contained in:
+37
-20
@@ -1,14 +1,14 @@
|
||||
# mbproxy
|
||||
|
||||
A .NET 10 Windows Service that sits inline as a Modbus TCP proxy in front of a fleet of AutomationDirect DirectLOGIC DL205/DL260 controllers, rewriting BCD-encoded registers bidirectionally so upstream clients can read and write them as plain integers. The proxy also offers an opt-in per-tag response cache (default OFF) for FC03/FC04 reads with bounded operator-configured staleness — see [`docs/Architecture/ResponseCache.md`](docs/Architecture/ResponseCache.md) before enabling it.
|
||||
A .NET 10 background service — a **Windows Service** or a **Linux systemd unit** — that sits inline as a Modbus TCP proxy in front of a fleet of AutomationDirect DirectLOGIC DL205/DL260 controllers, rewriting BCD-encoded registers bidirectionally so upstream clients can read and write them as plain integers. The proxy also offers an opt-in per-tag response cache (default OFF) for FC03/FC04 reads with bounded operator-configured staleness — see [`docs/Architecture/ResponseCache.md`](docs/Architecture/ResponseCache.md) before enabling it.
|
||||
|
||||
> ⚠ **32-bit BCD wire format is "two base-10000 digits in CDAB", not standard CDAB binary Int32.** A 32-bit BCD tag at address `A` decodes as `decimal = high * 10_000 + low` where `low` is the register at `A` and `high` is the register at `A+1`. Each word independently must be 0–9999. Standard Modbus clients (NModbus, FluentModbus, Wonderware DAServer) that interpret CDAB as straight binary Int32 will silently corrupt any value > 9999 on writes and read garbage on reads. Configure your client to send/receive each register as a separate base-10000 BCD digit pair, not as a single binary Int32. Full details in [`docs/Features/BcdRewriting.md`](docs/Features/BcdRewriting.md).
|
||||
|
||||
## Hard constraints / prerequisites
|
||||
|
||||
- **Windows 10 / Server 2019 or later, 64-bit.** No Linux or Docker support — the service uses `Microsoft.Extensions.Hosting.WindowsServices` and the Windows Event Log.
|
||||
- **Windows (10 / Server 2019+) or Linux (any systemd distro), 64-bit.** Ships as a Windows Service (Application Event Log integration) or a systemd unit (syslog integration); builds single-file for `win-x64` and `linux-x64`. macOS is not a deployment target — it runs only as a foreground console process.
|
||||
- **Modbus TCP backends reachable** from the proxy host on port 502 (or the port configured per PLC). The H2-ECOM100 module caps simultaneous connections at **4 per PLC** — a fifth upstream client will fail to connect.
|
||||
- **Admin rights** to install the service (`install.ps1` requires elevation).
|
||||
- **Admin / root rights** to install the service (`install.ps1` requires elevation; `install.sh` requires root).
|
||||
- **No COM dependency** — this is a pure .NET 10 socket-level proxy (unlike the `.NET Framework 4.8 / x86` siblings in this repo).
|
||||
- **Python 3.10+** on the test machine to run the pymodbus-backed E2E simulator (not needed to run the service in production).
|
||||
|
||||
@@ -16,8 +16,8 @@ A .NET 10 Windows Service that sits inline as a Modbus TCP proxy in front of a f
|
||||
|
||||
```
|
||||
src/Mbproxy/ Main C# project (net10.0, Microsoft.NET.Sdk.Worker)
|
||||
tests/Mbproxy.Tests/ xUnit v3 test project (314 unit + 48 E2E tests)
|
||||
install/ PowerShell install/uninstall scripts and config template
|
||||
tests/Mbproxy.Tests/ xUnit v3 test project (unit + simulator-backed E2E tests)
|
||||
install/ Install/uninstall + publish scripts (PowerShell + shell), systemd unit, config templates
|
||||
docs/ Architecture, features, operations, reference, and testing docs
|
||||
```
|
||||
|
||||
@@ -40,6 +40,7 @@ The `docs/` tree is organized by topic. Start with [`Architecture/Overview.md`](
|
||||
- [`Architecture/ConnectionModel.md`](docs/Architecture/ConnectionModel.md) — Single backend connection per PLC, TxId multiplexing, request-timeout watchdog, disconnect cascade.
|
||||
- [`Architecture/ReadCoalescing.md`](docs/Architecture/ReadCoalescing.md) — In-flight FC03/FC04 deduplication via `InFlightByKeyMap`.
|
||||
- [`Architecture/ResponseCache.md`](docs/Architecture/ResponseCache.md) — Opt-in per-tag response cache with bounded operator-configured staleness.
|
||||
- [`Architecture/Keepalive.md`](docs/Architecture/Keepalive.md) — TCP `SO_KEEPALIVE` on every socket plus an idle-backend FC03 heartbeat.
|
||||
|
||||
### Features
|
||||
|
||||
@@ -54,7 +55,7 @@ The `docs/` tree is organized by topic. Start with [`Architecture/Overview.md`](
|
||||
|
||||
### Reference
|
||||
|
||||
- [`Reference/LogEvents.md`](docs/Reference/LogEvents.md) — Stable `mbproxy.*` event catalog (28 events across 7 categories).
|
||||
- [`Reference/LogEvents.md`](docs/Reference/LogEvents.md) — Stable `mbproxy.*` event catalog (31 events across 8 categories).
|
||||
|
||||
### Testing
|
||||
|
||||
@@ -68,20 +69,27 @@ The `docs/` tree is organized by topic. Start with [`Architecture/Overview.md`](
|
||||
dotnet build Mbproxy.slnx -c Debug
|
||||
```
|
||||
|
||||
**Publish (Release, single-file, win-x64):**
|
||||
**Publish (Release, single-file):**
|
||||
|
||||
```powershell
|
||||
.\install\publish.ps1 -Clean
|
||||
.\install\publish.ps1 -Clean # win-x64 (default)
|
||||
.\install\publish.ps1 -Rid linux-x64 -Clean # cross-publish for linux-x64
|
||||
```
|
||||
|
||||
Produces both flavours under `publish-out\`:
|
||||
On a Linux build host, use the shell counterpart:
|
||||
|
||||
| Flavour | Path | Size | Target prerequisite |
|
||||
```bash
|
||||
./install/publish.sh --clean # linux-x64 (default)
|
||||
```
|
||||
|
||||
Each run produces both flavours under `publish-out\`:
|
||||
|
||||
| Flavour | Path (win-x64) | Size | Target prerequisite |
|
||||
|---|---|---|---|
|
||||
| Self-contained | `publish-out\self-contained\Mbproxy.exe` | ~100 MB | None — bundles .NET 10 + ASP.NET Core runtime |
|
||||
| Framework-dependent | `publish-out\framework-dependent\Mbproxy.exe` | ~1.5 MB | .NET 10 + ASP.NET Core preinstalled |
|
||||
| Framework-dependent | `publish-out\framework-dependent\Mbproxy.exe` | ~1.6 MB | .NET 10 + ASP.NET Core preinstalled |
|
||||
|
||||
Pass `-OutputDir <path>` to publish elsewhere; omit `-Clean` to skip the wipe. The script wraps `dotnet publish src/Mbproxy/Mbproxy.csproj -c Release -r win-x64 [-p:SelfContained=false]` — run those directly if you only need one flavour.
|
||||
On `linux-x64` the binary is `Mbproxy` (no extension) and ships the Linux config template. Pass `-OutputDir`/`-o` to publish elsewhere; omit `-Clean`/`--clean` to skip the wipe. The scripts wrap `dotnet publish src/Mbproxy/Mbproxy.csproj -c Release -r <rid> [-p:SelfContained=false]` — run that directly if you only need one flavour.
|
||||
|
||||
**Run tests:**
|
||||
|
||||
@@ -102,21 +110,30 @@ Edit `src/Mbproxy/appsettings.json` to configure PLCs before running. The admin
|
||||
|
||||
## Install
|
||||
|
||||
The `install/` directory holds the publish, install, and uninstall scripts. Quick path:
|
||||
The `install/` directory holds the publish, install, and uninstall scripts for both platforms.
|
||||
|
||||
**Windows** — elevated PowerShell:
|
||||
|
||||
```powershell
|
||||
# 1. Publish (produces publish-out\self-contained\ and publish-out\framework-dependent\)
|
||||
.\install\publish.ps1 -Clean
|
||||
|
||||
# 2. Install (elevated PowerShell) — point at the flavour you want to deploy
|
||||
.\install\install.ps1 -PublishOutput .\publish-out\self-contained -Start
|
||||
|
||||
# 3. Edit the config that was placed at %ProgramData%\mbproxy\appsettings.json
|
||||
|
||||
# 4. Verify
|
||||
# Config is placed at %ProgramData%\mbproxy\appsettings.json — edit it, then:
|
||||
# Restart-Service mbproxy
|
||||
Invoke-WebRequest http://localhost:8080/ -UseBasicParsing
|
||||
```
|
||||
|
||||
**Linux** — root / `sudo` on a systemd host:
|
||||
|
||||
```bash
|
||||
./install/publish.sh --clean
|
||||
sudo ./install/install.sh --publish-dir ./publish-out/self-contained
|
||||
# Config is placed at /etc/mbproxy/appsettings.json — edit it, then:
|
||||
# sudo systemctl restart mbproxy
|
||||
curl http://localhost:8080/
|
||||
```
|
||||
|
||||
`uninstall.ps1` / `uninstall.sh` reverse the install; both archive log files rather than deleting them. The systemd unit runs mbproxy as `Type=exec` under a dedicated `mbproxy` service account.
|
||||
|
||||
## Maintenance
|
||||
|
||||
Documentation doctrine for this repo: [`../DOCS-GUIDE.md`](../DOCS-GUIDE.md).
|
||||
|
||||
Reference in New Issue
Block a user