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>
170 lines
6.9 KiB
PowerShell
170 lines
6.9 KiB
PowerShell
#Requires -Version 7
|
|
<#
|
|
.SYNOPSIS
|
|
Provision a Python venv and launch the pymodbus DL205 simulator.
|
|
|
|
.DESCRIPTION
|
|
Idempotent: re-runs skip venv provisioning when tests/sim/.venv is fully provisioned.
|
|
Spawns 'pymodbus.simulator' with the DL205/DL260 register profile on a configurable
|
|
port; the server process stays attached so Ctrl-C (or parent exit) kills it cleanly.
|
|
|
|
pymodbus version pin: 3.13.0
|
|
(Matches the profile comment in dl205.json. Record the version here AND in
|
|
tests/sim/README.md so it is never lost across re-provisioning.)
|
|
|
|
API note: pymodbus 3.13.0 uses 'pymodbus.simulator' (not the legacy 'pymodbus.server
|
|
run' command). The Modbus TCP port is set in the JSON config; this script writes a
|
|
temp config that overrides the port so the free-port-picker pattern works.
|
|
aiohttp is required by the pymodbus simulator HTTP console and is installed alongside
|
|
pymodbus.
|
|
|
|
.PARAMETER Profile
|
|
Path to the pymodbus JSON profile. Defaults to dl205.json in this script's
|
|
directory (i.e. the checked-in DL205 quirk profile).
|
|
|
|
.PARAMETER Port
|
|
TCP port for the Modbus server to listen on. Defaults to 5020.
|
|
|
|
.EXIT CODES
|
|
0 Clean exit (Ctrl-C or natural termination).
|
|
1 Python not found, or venv provisioning failed.
|
|
2 pymodbus.simulator launch failed.
|
|
3 Profile file not found.
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[string]$Profile = (Join-Path $PSScriptRoot 'dl205.json'),
|
|
[int]$Port = 5020
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
# ── 1. Resolve and validate the profile path ─────────────────────────────────
|
|
$ProfileResolved = (Resolve-Path -Path $Profile -ErrorAction SilentlyContinue)?.Path
|
|
if (-not $ProfileResolved) {
|
|
Write-Error "Profile not found: $Profile"
|
|
exit 3
|
|
}
|
|
|
|
# ── 2. Locate Python ─────────────────────────────────────────────────────────
|
|
# Windows: 'python' (standard PATH install), then the 'py' launcher.
|
|
# Linux/macOS: 'python3' (the canonical name), then 'python'.
|
|
# The candidate order is platform-specific so Windows never matches the Microsoft
|
|
# Store 'python3' stub.
|
|
$pythonExe = $null
|
|
$pythonCandidates = $IsWindows ? @('python', 'py') : @('python3', 'python')
|
|
foreach ($candidate in $pythonCandidates) {
|
|
try {
|
|
$ver = & $candidate --version 2>&1
|
|
if ($LASTEXITCODE -eq 0) {
|
|
$pythonExe = $candidate
|
|
Write-Host "[sim] Python found via '$candidate': $ver"
|
|
break
|
|
}
|
|
} catch {
|
|
# not on PATH — continue
|
|
}
|
|
}
|
|
if (-not $pythonExe) {
|
|
Write-Error @"
|
|
Python 3.10+ is required to run the DL205 simulator but was not found on PATH.
|
|
Install Python from https://www.python.org/downloads/ and ensure it is on your PATH,
|
|
or use the Windows Store launcher ('py').
|
|
"@
|
|
exit 1
|
|
}
|
|
|
|
# ── 3. Provision the venv (idempotent) ───────────────────────────────────────
|
|
# pymodbus version pin: 3.13.0
|
|
# Update this constant AND tests/sim/README.md together if you re-pin.
|
|
$PYMODBUS_VERSION = '3.13.0'
|
|
|
|
$venvDir = Join-Path $PSScriptRoot '.venv'
|
|
# venv executable layout differs by OS: Windows puts them in Scripts\ with a .exe
|
|
# extension; Linux/macOS put them in bin/ with no extension.
|
|
$venvBin = $IsWindows ? 'Scripts' : 'bin'
|
|
$exeExt = $IsWindows ? '.exe' : ''
|
|
$venvPython = Join-Path $venvDir $venvBin "python$exeExt"
|
|
$pipExe = Join-Path $venvDir $venvBin "pip$exeExt"
|
|
$simulatorExe = Join-Path $venvDir $venvBin "pymodbus.simulator$exeExt" # sentinel for complete install
|
|
|
|
# Provisioning is idempotent: we only skip it when pymodbus.simulator.exe exists.
|
|
# Checking only the .venv directory is not enough — a previous run killed mid-install
|
|
# leaves the directory but without pymodbus installed.
|
|
$needsProvision = (-not (Test-Path $simulatorExe))
|
|
|
|
if ($needsProvision) {
|
|
if (-not (Test-Path $venvDir)) {
|
|
Write-Host "[sim] Creating venv at $venvDir ..."
|
|
& $pythonExe -m venv $venvDir
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Error "Failed to create Python venv (exit $LASTEXITCODE)."
|
|
exit 1
|
|
}
|
|
} else {
|
|
Write-Host "[sim] Venv exists but pymodbus is not fully installed — installing now."
|
|
}
|
|
|
|
# pymodbus 3.13.0 does not provide a [server] extra; the simulator module is
|
|
# included in the base package. aiohttp is required by the simulator's HTTP
|
|
# console and is not a declared dependency of pymodbus, so we install it
|
|
# explicitly here.
|
|
Write-Host "[sim] Installing pymodbus==$PYMODBUS_VERSION + aiohttp ..."
|
|
& $pipExe install "pymodbus==$PYMODBUS_VERSION" aiohttp
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Error "Failed to install pymodbus / aiohttp (exit $LASTEXITCODE). Check network or proxy settings."
|
|
exit 1
|
|
}
|
|
|
|
Write-Host "[sim] Venv provisioned."
|
|
} else {
|
|
Write-Host "[sim] Venv and pymodbus already provisioned — skipping."
|
|
}
|
|
|
|
# ── 4. Prepare a port-specific config file ───────────────────────────────────
|
|
# pymodbus.simulator 3.13.0 reads the Modbus TCP port from the JSON config, not
|
|
# from a command-line --port flag. To allow the fixture's free-port-picker pattern,
|
|
# we write a temp config that is a copy of the base profile but with srv.port
|
|
# overridden to $Port.
|
|
$tempConfig = [System.IO.Path]::GetTempFileName() + '.json'
|
|
try {
|
|
$json = Get-Content -Raw $ProfileResolved | ConvertFrom-Json -Depth 20
|
|
$json.server_list.srv.port = $Port
|
|
$json | ConvertTo-Json -Depth 20 | Set-Content -Encoding UTF8 $tempConfig
|
|
Write-Host "[sim] Wrote temp config with port=$Port to: $tempConfig"
|
|
}
|
|
catch {
|
|
Write-Error "Failed to prepare port-specific config: $_"
|
|
exit 2
|
|
}
|
|
|
|
# ── 5. Launch pymodbus simulator ─────────────────────────────────────────────
|
|
# pymodbus 3.13.0 API: pymodbus.simulator --json_file <path> --modbus_server <key>
|
|
# --modbus_device <key>
|
|
# We don't pass --http_port because we don't need the REST API in tests.
|
|
# The process is kept alive in the foreground; Ctrl-C (or parent-exit Kill) stops it.
|
|
Write-Host "[sim] Starting pymodbus DL205 simulator on Modbus TCP port $Port ..."
|
|
|
|
try {
|
|
& $simulatorExe `
|
|
--json_file $tempConfig `
|
|
--modbus_server srv `
|
|
--modbus_device dev
|
|
$exitCode = $LASTEXITCODE
|
|
} catch {
|
|
Write-Error "Failed to launch pymodbus.simulator: $_"
|
|
Remove-Item -Force $tempConfig -ErrorAction SilentlyContinue
|
|
exit 2
|
|
} finally {
|
|
Remove-Item -Force $tempConfig -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
# A non-zero exit from pymodbus is unexpected (0 = clean shutdown).
|
|
if ($exitCode -ne 0) {
|
|
Write-Error "pymodbus.simulator exited with code $exitCode."
|
|
exit 2
|
|
}
|
|
|
|
exit 0
|