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:
Joseph Doherty
2026-05-15 09:41:59 -04:00
parent 0868613890
commit b330faff03
29 changed files with 1805 additions and 106 deletions
@@ -0,0 +1,60 @@
namespace Mbproxy.Diagnostics;
/// <summary>
/// The platform diagnostic sink to wire for <c>Error</c>+ events — picked once,
/// at the composition root, by <see cref="DiagnosticSinkSelector"/>.
/// </summary>
internal enum DiagnosticSink
{
/// <summary>
/// No platform diagnostic sink — console (and rolling-file) sinks only. Used
/// for interactive / dev runs on every OS.
/// </summary>
None,
/// <summary>
/// Windows Application Event Log, via <see cref="EventLogBridge"/>. Selected
/// only when the process is hosted as a Windows Service.
/// </summary>
EventLog,
/// <summary>
/// Local syslog, via <see cref="SyslogBridge"/>. Selected only when the
/// process is hosted as a systemd service on Linux.
/// </summary>
Syslog,
}
/// <summary>
/// Pure platform-selection logic for the <c>Error</c>+ diagnostic sink. Holds no
/// I/O and no host APIs so it is unit-testable for every OS / host combination;
/// the host detection itself happens in <see cref="HostingExtensions.AddMbproxySerilog"/>.
/// </summary>
internal static class DiagnosticSinkSelector
{
/// <summary>
/// Picks the diagnostic sink for the current host:
/// <list type="bullet">
/// <item>Windows hosted as a Windows Service → <see cref="DiagnosticSink.EventLog"/>.</item>
/// <item>Linux hosted as a systemd service → <see cref="DiagnosticSink.Syslog"/>.</item>
/// <item>Everything else — interactive / dev runs, macOS, launches not owned
/// by an init system → <see cref="DiagnosticSink.None"/>.</item>
/// </list>
/// The managed-service gate mirrors the original <see cref="EventLogBridge"/>
/// contract: a diagnostic sink is wired only when an init system actually owns
/// the process, so dev / console runs never need an Event Log source registered
/// or a syslog socket reachable.
/// </summary>
/// <param name="isWindows">Running on Windows.</param>
/// <param name="isWindowsService">Hosted by the Windows Service Control Manager.</param>
/// <param name="isSystemd">Hosted by systemd.</param>
public static DiagnosticSink Select(bool isWindows, bool isWindowsService, bool isSystemd)
{
// Windows takes precedence: isSystemd is meaningless there, and on
// non-Windows isWindowsService is always false.
if (isWindows)
return isWindowsService ? DiagnosticSink.EventLog : DiagnosticSink.None;
return isSystemd ? DiagnosticSink.Syslog : DiagnosticSink.None;
}
}