b330faff03
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>
206 lines
8.8 KiB
PowerShell
206 lines
8.8 KiB
PowerShell
#Requires -RunAsAdministrator
|
|
<#
|
|
.SYNOPSIS
|
|
Installs the mbproxy service on a Windows host.
|
|
|
|
.DESCRIPTION
|
|
Copies the published binaries to InstallPath, registers the Windows Service,
|
|
sets failure-recovery actions, creates the data directory and log folder,
|
|
copies the config template if no config exists, and registers the Windows Event
|
|
Log source. Re-running this script on an already-installed service is safe
|
|
(idempotent): binaries are updated, service config is refreshed, and the service
|
|
is restarted if it was running.
|
|
|
|
.PARAMETER PublishOutput
|
|
Path to the directory produced by 'dotnet publish'. Must contain Mbproxy.exe.
|
|
|
|
.PARAMETER InstallPath
|
|
Destination directory for the service binaries.
|
|
Default: C:\Program Files\Mbproxy
|
|
|
|
.PARAMETER ServiceName
|
|
Windows Service name (used with sc.exe).
|
|
Default: mbproxy
|
|
|
|
.PARAMETER DisplayName
|
|
Display name shown in services.msc.
|
|
Default: Mbproxy — Modbus TCP BCD proxy
|
|
|
|
.PARAMETER Account
|
|
Service account (e.g. LocalSystem, NT AUTHORITY\LocalService, or a gMSA UPN).
|
|
Default: LocalSystem
|
|
|
|
.PARAMETER Start
|
|
If specified, starts the service immediately after install.
|
|
|
|
.EXAMPLE
|
|
.\install.ps1 -PublishOutput C:\build\publish -Start
|
|
|
|
.EXAMPLE
|
|
.\install.ps1 -PublishOutput \\fileserver\mbproxy\publish -ServiceName mbproxy-line2 -Start
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$PublishOutput,
|
|
|
|
[string]$InstallPath = 'C:\Program Files\Mbproxy',
|
|
[string]$ServiceName = 'mbproxy',
|
|
[string]$DisplayName = 'Mbproxy — Modbus TCP BCD proxy',
|
|
[string]$Account = 'LocalSystem',
|
|
[switch]$Start
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
# ── 0. Pre-flight checks ──────────────────────────────────────────────────────────────────
|
|
|
|
if (-not (Test-Path (Join-Path $PublishOutput 'Mbproxy.exe'))) {
|
|
Write-Error "Mbproxy.exe not found in '$PublishOutput'. Run 'dotnet publish' first."
|
|
}
|
|
|
|
Write-Host "Installing mbproxy service..." -ForegroundColor Cyan
|
|
Write-Host " PublishOutput : $PublishOutput"
|
|
Write-Host " InstallPath : $InstallPath"
|
|
Write-Host " ServiceName : $ServiceName"
|
|
Write-Host " Account : $Account"
|
|
|
|
# ── 1. Stop the service if it's running ─────────────────────────────────────────────────
|
|
|
|
$existingService = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
|
|
if ($existingService -and $existingService.Status -eq 'Running') {
|
|
Write-Host "Stopping running service '$ServiceName'..."
|
|
sc.exe stop $ServiceName | Out-Null
|
|
$deadline = [DateTime]::UtcNow.AddSeconds(30)
|
|
do {
|
|
Start-Sleep -Milliseconds 500
|
|
$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
|
|
} while ($svc -and $svc.Status -ne 'Stopped' -and [DateTime]::UtcNow -lt $deadline)
|
|
|
|
if ($svc -and $svc.Status -ne 'Stopped') {
|
|
Write-Warning "Service did not stop within 30 s — proceeding anyway."
|
|
}
|
|
}
|
|
|
|
# ── 2. Copy binaries ─────────────────────────────────────────────────────────────────────
|
|
|
|
if (-not (Test-Path $InstallPath)) {
|
|
Write-Host "Creating install directory '$InstallPath'..."
|
|
New-Item -ItemType Directory -Force $InstallPath | Out-Null
|
|
}
|
|
|
|
# Preserve any existing appsettings.json (operator may have customised it).
|
|
$destConfig = Join-Path $InstallPath 'appsettings.json'
|
|
$hasExistingConfig = Test-Path $destConfig
|
|
|
|
Write-Host "Copying binaries from '$PublishOutput' to '$InstallPath'..."
|
|
# Exclude appsettings.json if it already exists at the destination.
|
|
Get-ChildItem -Path $PublishOutput -File | ForEach-Object {
|
|
$dest = Join-Path $InstallPath $_.Name
|
|
if ($_.Name -eq 'appsettings.json' -and $hasExistingConfig) {
|
|
Write-Host " Preserving existing appsettings.json at '$destConfig'"
|
|
} else {
|
|
Copy-Item -Path $_.FullName -Destination $dest -Force
|
|
}
|
|
}
|
|
|
|
# ── 3. Register (or update) the Windows Service ──────────────────────────────────────────
|
|
|
|
$binPath = Join-Path $InstallPath 'Mbproxy.exe'
|
|
|
|
if ($existingService) {
|
|
Write-Host "Updating existing service '$ServiceName'..."
|
|
sc.exe config $ServiceName binPath= "`"$binPath`"" start= auto displayName= `"$DisplayName`" obj= $Account | Out-Null
|
|
} else {
|
|
Write-Host "Creating service '$ServiceName'..."
|
|
sc.exe create $ServiceName binPath= "`"$binPath`"" start= auto displayName= `"$DisplayName`" obj= $Account | Out-Null
|
|
}
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Error "sc.exe failed with exit code $LASTEXITCODE"
|
|
}
|
|
|
|
# ── 4. Set failure-recovery actions ──────────────────────────────────────────────────────
|
|
# Restart after 60 s on first and second failure; no action on third and subsequent.
|
|
Write-Host "Configuring failure-recovery actions..."
|
|
sc.exe failure $ServiceName reset= 86400 actions= restart/60000/restart/60000/""/0 | Out-Null
|
|
|
|
# ── 5. Create data directory and log folder ──────────────────────────────────────────────
|
|
|
|
$dataDir = Join-Path $env:ProgramData 'mbproxy'
|
|
$logDir = Join-Path $dataDir 'logs'
|
|
|
|
if (-not (Test-Path $logDir)) {
|
|
Write-Host "Creating log directory '$logDir'..."
|
|
New-Item -ItemType Directory -Force $logDir | Out-Null
|
|
}
|
|
|
|
# Grant the service account write access to the data directory.
|
|
# For LocalSystem this is redundant (it already has full access), but explicit ACLs
|
|
# are good practice when using a restricted MSA/gMSA account.
|
|
if ($Account -notin @('LocalSystem', 'NT AUTHORITY\LocalSystem')) {
|
|
Write-Host "Setting ACLs on '$dataDir' for account '$Account'..."
|
|
try {
|
|
icacls $logDir /grant "${Account}:(OI)(CI)M" /T /Q | Out-Null
|
|
} catch {
|
|
Write-Warning "Could not set ACLs on '$logDir': $_"
|
|
}
|
|
}
|
|
|
|
# ── 6. Copy config template if no config exists ──────────────────────────────────────────
|
|
|
|
$configDest = Join-Path $dataDir 'appsettings.json'
|
|
if (-not (Test-Path $configDest)) {
|
|
$templateSrc = Join-Path $PSScriptRoot 'mbproxy.config.template.json'
|
|
if (Test-Path $templateSrc) {
|
|
Write-Host "Copying config template to '$configDest'..."
|
|
Copy-Item -Path $templateSrc -Destination $configDest -Force
|
|
} else {
|
|
Write-Warning "Config template not found at '$templateSrc' — create appsettings.json manually."
|
|
}
|
|
}
|
|
|
|
# ── 7. Register Windows Event Log source ─────────────────────────────────────────────────
|
|
|
|
if (-not [System.Diagnostics.EventLog]::SourceExists('mbproxy')) {
|
|
Write-Host "Registering Windows Event Log source 'mbproxy'..."
|
|
# .NET API, not New-EventLog: the *-EventLog cmdlets exist only in Windows
|
|
# PowerShell 5.1, not PowerShell 7+. This call is symmetric with the
|
|
# SourceExists check above and works on every PowerShell edition.
|
|
[System.Diagnostics.EventLog]::CreateEventSource('mbproxy', 'Application')
|
|
} else {
|
|
Write-Host "Windows Event Log source 'mbproxy' already registered."
|
|
}
|
|
|
|
# ── 8. Optionally start the service ──────────────────────────────────────────────────────
|
|
|
|
if ($Start) {
|
|
Write-Host "Starting service '$ServiceName'..."
|
|
sc.exe start $ServiceName | Out-Null
|
|
|
|
$deadline = [DateTime]::UtcNow.AddSeconds(30)
|
|
do {
|
|
Start-Sleep -Milliseconds 500
|
|
$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
|
|
} while ($svc -and $svc.Status -ne 'Running' -and [DateTime]::UtcNow -lt $deadline)
|
|
|
|
$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
|
|
if ($svc -and $svc.Status -eq 'Running') {
|
|
Write-Host "Service '$ServiceName' is running." -ForegroundColor Green
|
|
} else {
|
|
Write-Warning "Service '$ServiceName' did not reach RUNNING state within 30 s. Check Event Log for errors."
|
|
}
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "Install complete." -ForegroundColor Green
|
|
Write-Host " Config : $configDest"
|
|
Write-Host " Logs : $logDir"
|
|
Write-Host " Binaries: $InstallPath"
|
|
Write-Host ""
|
|
Write-Host "Next steps:"
|
|
Write-Host " 1. Edit '$configDest' to configure your PLC list and BCD tags."
|
|
Write-Host " 2. Start the service: sc.exe start $ServiceName"
|
|
Write-Host " 3. Check status page: http://localhost:8080/"
|