mbproxy: initial commit through Phase 9 (TxId multiplexing)
Adds the mbproxy service end-to-end. Phases 00-08 implement the production-ready single-listener / 1:1-backend transparent Modbus TCP proxy with bidirectional BCD rewriting for the ~54-PLC DL205/DL260 fleet. Phase 9 replaces the connection layer with a single backend socket per PLC plus MBAP TxId rewriting, lifting the H2-ECOM100's 4-concurrent-client cap as an operational ceiling. Phase 9 additions of note: - PlcMultiplexer + UpstreamPipe + TxIdAllocator + CorrelationMap - InFlightRequest with IReadOnlyList<InterestedParty> (load-bearing for Phase 10 read coalescing — do not collapse to a single field) - Per-request watchdog: surfaces Modbus exception 0x0B to upstream on BackendRequestTimeoutMs, defending against lost responses, dead-PLC paths, and pymodbus 3.13.0's concurrent-multiplexed- request bug (its ServerRequestHandler.last_pdu state race) - Status DTO + HTML gain inFlight / maxInFlight / txIdWraps / disconnectCascades / queueDepth (Tier 1.6 in docs/kpi.md) Tests: 263 unit + 38 E2E. Multiplexer correctness under truly concurrent backend traffic is proved against a stub backend in PlcMultiplexerTests; MultiplexerE2ETests paces requests so pymodbus 3.13's single-PDU framer stays in known-good mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
#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'..."
|
||||
New-EventLog -Source 'mbproxy' -LogName '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/"
|
||||
@@ -0,0 +1,155 @@
|
||||
// mbproxy configuration template — copy to %ProgramData%\mbproxy\appsettings.json
|
||||
// and edit before starting the service.
|
||||
//
|
||||
// The .NET configuration loader accepts // and /* */ comments in JSON files
|
||||
// (JSONC semantics) when using the default Host.CreateApplicationBuilder path.
|
||||
//
|
||||
// IMPORTANT: This file is overwritten on each install ONLY if no appsettings.json
|
||||
// already exists at the destination. An existing file is always preserved.
|
||||
{
|
||||
"Mbproxy": {
|
||||
|
||||
// ── Global BCD tag list ─────────────────────────────────────────────────────────────
|
||||
// These tags apply to EVERY PLC by default.
|
||||
// Each entry: Address (Modbus PDU address, decimal), Width (16 or 32 bits).
|
||||
//
|
||||
// Width 16 — one register holds 4 BCD digits (0–9999).
|
||||
// Wire value 0x1234 decodes to decimal 1234.
|
||||
//
|
||||
// Width 32 — a CDAB-ordered register pair (Address = low word, Address+1 = high word).
|
||||
// Decoded decimal = high * 10000 + low (DirectLOGIC CDAB word order).
|
||||
//
|
||||
// Per-PLC overrides (see Plcs[].BcdTags below):
|
||||
// Add — appends extra tags beyond what Global defines, or overrides a
|
||||
// Global entry's Width when the same Address appears in both.
|
||||
// Remove — removes specific addresses from the effective set for that PLC.
|
||||
// Effective set = (Global ∪ Add) − Remove, resolved per PDU.
|
||||
"BcdTags": {
|
||||
"Global": [
|
||||
// V2000 (octal) = decimal address 1024. 16-bit BCD counter.
|
||||
{ "Address": 1024, "Width": 16 },
|
||||
|
||||
// V2040 (octal) = decimal address 1056. 32-bit BCD total at 1056/1057.
|
||||
{ "Address": 1056, "Width": 32 },
|
||||
|
||||
// V2100 (octal) = decimal address 1088. 16-bit BCD setpoint.
|
||||
{ "Address": 1088, "Width": 16 }
|
||||
]
|
||||
},
|
||||
|
||||
// ── PLC list ────────────────────────────────────────────────────────────────────────
|
||||
// Each entry maps one upstream proxy port → one backend PLC.
|
||||
// Upstream clients connect to ListenPort; the proxy forwards to Host:Port.
|
||||
//
|
||||
// IMPORTANT: H2-ECOM100 modules accept at most 4 simultaneous TCP connections.
|
||||
// With the 1:1 upstream↔backend model, a fifth upstream client to the same proxy
|
||||
// port will cause a backend connect failure and an immediate upstream disconnect.
|
||||
"Plcs": [
|
||||
{
|
||||
"Name": "Line1-Mixer", // Human-readable name (shown on status page and in logs)
|
||||
"ListenPort": 5020, // Port the proxy listens on (upstream clients connect here)
|
||||
"Host": "10.0.1.1", // PLC IP address or hostname
|
||||
"Port": 502, // PLC Modbus TCP port (almost always 502)
|
||||
"BcdTags": {
|
||||
// Additional 32-bit tag specific to this PLC only.
|
||||
"Add": [
|
||||
{ "Address": 1200, "Width": 32 }
|
||||
],
|
||||
// Remove address 1056 from the Global list for this PLC
|
||||
// (this mixer doesn't use the 32-bit BCD total).
|
||||
"Remove": [ 1056 ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "Line1-Conveyor",
|
||||
"ListenPort": 5021,
|
||||
"Host": "10.0.1.2",
|
||||
"Port": 502
|
||||
// No BcdTags override — uses the Global set as-is.
|
||||
}
|
||||
// Add one entry per PLC. Ports must be unique per host. Typical fleet: 54 PLCs.
|
||||
],
|
||||
|
||||
// ── Admin port ──────────────────────────────────────────────────────────────────────
|
||||
// Read-only HTTP status page.
|
||||
// GET / → self-contained HTML (auto-refreshes every 5 s)
|
||||
// GET /status.json → same data as JSON for monitoring scrapers
|
||||
//
|
||||
// Authentication is assumed at the network layer (trusted internal segment).
|
||||
// Set to 0 to disable the admin endpoint.
|
||||
"AdminPort": 8080,
|
||||
|
||||
// ── Connection timeouts ─────────────────────────────────────────────────────────────
|
||||
"Connection": {
|
||||
// Max time (ms) to wait for a TCP connect to the PLC backend.
|
||||
// Each Polly retry attempt gets its own copy of this timeout.
|
||||
"BackendConnectTimeoutMs": 3000,
|
||||
|
||||
// Max time (ms) to wait for the PLC to respond to a forwarded PDU.
|
||||
// Non-idempotent FC06/FC16 writes are one-shot — the upstream client
|
||||
// is disconnected immediately on timeout (no retry).
|
||||
"BackendRequestTimeoutMs": 3000,
|
||||
|
||||
// Max time (ms) to wait for in-flight PDUs to complete during graceful shutdown
|
||||
// (sc.exe stop / Windows Service stop signal). After this deadline the coordinator
|
||||
// cancels remaining work and proceeds. Keep at or below the SCM wait-hint (30 s).
|
||||
"GracefulShutdownTimeoutMs": 10000
|
||||
},
|
||||
|
||||
// ── Resilience policies ─────────────────────────────────────────────────────────────
|
||||
"Resilience": {
|
||||
|
||||
// Polly retry policy for backend TCP connect attempts.
|
||||
// MaxAttempts: total connect tries (including the first).
|
||||
// BackoffMs: delay between each attempt (must have MaxAttempts−1 entries).
|
||||
"BackendConnect": {
|
||||
"MaxAttempts": 3,
|
||||
"BackoffMs": [ 100, 500, 2000 ]
|
||||
},
|
||||
|
||||
// Polly recovery policy for listener bind failures.
|
||||
// If a PLC's listen port can't be bound (in-use, bad IP, transient OS error),
|
||||
// the supervisor retries according to this schedule.
|
||||
// InitialBackoffMs: backoff per step (first N retries).
|
||||
// SteadyStateMs: backoff for all subsequent retries (runs indefinitely).
|
||||
"ListenerRecovery": {
|
||||
"InitialBackoffMs": [ 1000, 2000, 5000, 15000, 30000 ],
|
||||
"SteadyStateMs": 30000
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ── Serilog ─────────────────────────────────────────────────────────────────────────────
|
||||
// Structured log output. Default: Information level, rolling-file under ProgramData.
|
||||
// The EventLogBridge writes Error+ events to the Windows Application Event Log
|
||||
// automatically when the service runs under the SCM (not under dotnet run).
|
||||
"Serilog": {
|
||||
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
|
||||
"MinimumLevel": {
|
||||
"Default": "Information",
|
||||
"Override": {
|
||||
"Microsoft": "Warning",
|
||||
"System": "Warning"
|
||||
}
|
||||
},
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "Console",
|
||||
"Args": {
|
||||
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "File",
|
||||
"Args": {
|
||||
// Rolling log: one file per day, kept for 30 days.
|
||||
// Survives uninstall — logs are archived to %ProgramData%\mbproxy.archived-<ts>\.
|
||||
"path": "C:\\ProgramData\\mbproxy\\logs\\mbproxy-.log",
|
||||
"rollingInterval": "Day",
|
||||
"retainedFileCountLimit": 30,
|
||||
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
#Requires -RunAsAdministrator
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Removes the mbproxy Windows Service and its installed files.
|
||||
|
||||
.DESCRIPTION
|
||||
Stops the service, deletes the service registration, removes the binary
|
||||
install directory, and (unless -KeepConfig is specified) removes the data
|
||||
directory. Log files are always preserved: they are moved to a timestamped
|
||||
archive directory so post-uninstall diagnostics remain accessible.
|
||||
|
||||
.PARAMETER ServiceName
|
||||
Windows Service name to uninstall.
|
||||
Default: mbproxy
|
||||
|
||||
.PARAMETER InstallPath
|
||||
Directory that was used as the install target.
|
||||
Default: C:\Program Files\Mbproxy
|
||||
|
||||
.PARAMETER KeepConfig
|
||||
If specified, leaves %ProgramData%\mbproxy\appsettings.json in place.
|
||||
Logs are always preserved regardless of this flag.
|
||||
|
||||
.EXAMPLE
|
||||
.\uninstall.ps1
|
||||
|
||||
.EXAMPLE
|
||||
.\uninstall.ps1 -KeepConfig
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ServiceName = 'mbproxy',
|
||||
[string]$InstallPath = 'C:\Program Files\Mbproxy',
|
||||
[switch]$KeepConfig
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
Write-Host "Uninstalling mbproxy service..." -ForegroundColor Cyan
|
||||
Write-Host " ServiceName : $ServiceName"
|
||||
Write-Host " InstallPath : $InstallPath"
|
||||
Write-Host " KeepConfig : $KeepConfig"
|
||||
|
||||
# ── 1. Stop the service ───────────────────────────────────────────────────────────────────
|
||||
|
||||
$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
|
||||
if ($svc) {
|
||||
if ($svc.Status -eq 'Running') {
|
||||
Write-Host "Stopping 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)
|
||||
|
||||
$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
|
||||
if ($svc -and $svc.Status -ne 'Stopped') {
|
||||
Write-Warning "Service did not stop within 30 s — attempting force delete."
|
||||
}
|
||||
}
|
||||
|
||||
# ── 2. Delete the service ─────────────────────────────────────────────────────────────
|
||||
Write-Host "Deleting service registration '$ServiceName'..."
|
||||
sc.exe delete $ServiceName | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warning "sc.exe delete returned $LASTEXITCODE — the service entry may already be gone."
|
||||
}
|
||||
} else {
|
||||
Write-Host "Service '$ServiceName' not found — skipping stop/delete."
|
||||
}
|
||||
|
||||
# ── 3. Archive log files ─────────────────────────────────────────────────────────────────
|
||||
# Logs are ALWAYS archived (never deleted) so post-uninstall crash diagnostics survive.
|
||||
|
||||
$dataDir = Join-Path $env:ProgramData 'mbproxy'
|
||||
$logDir = Join-Path $dataDir 'logs'
|
||||
|
||||
if (Test-Path $logDir) {
|
||||
$timestamp = [DateTime]::UtcNow.ToString('yyyyMMddTHHmmssZ')
|
||||
$archiveName = "mbproxy.archived-$timestamp"
|
||||
$archiveRoot = Join-Path $env:ProgramData $archiveName
|
||||
$archiveLogs = Join-Path $archiveRoot 'logs'
|
||||
|
||||
Write-Host "Archiving logs to '$archiveLogs'..."
|
||||
New-Item -ItemType Directory -Force $archiveLogs | Out-Null
|
||||
Get-ChildItem -Path $logDir | ForEach-Object {
|
||||
Move-Item -Path $_.FullName -Destination $archiveLogs -Force
|
||||
}
|
||||
Write-Host " Logs archived to: $archiveLogs" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# ── 4. Remove data directory ─────────────────────────────────────────────────────────────
|
||||
|
||||
if (Test-Path $dataDir) {
|
||||
if ($KeepConfig) {
|
||||
# Remove everything except appsettings.json; then remove the now-empty log dir.
|
||||
Write-Host "Keeping config at '$dataDir\appsettings.json' (-KeepConfig specified)."
|
||||
$logDirPath = Join-Path $dataDir 'logs'
|
||||
if (Test-Path $logDirPath) {
|
||||
Remove-Item -Recurse -Force $logDirPath -ErrorAction SilentlyContinue
|
||||
}
|
||||
} else {
|
||||
Write-Host "Removing data directory '$dataDir'..."
|
||||
Remove-Item -Recurse -Force $dataDir -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# ── 5. Remove binary install directory ───────────────────────────────────────────────────
|
||||
|
||||
if (Test-Path $InstallPath) {
|
||||
Write-Host "Removing install directory '$InstallPath'..."
|
||||
Remove-Item -Recurse -Force $InstallPath -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
Write-Host "Install directory '$InstallPath' not found — skipping."
|
||||
}
|
||||
|
||||
# ── 6. Remove Windows Event Log source ───────────────────────────────────────────────────
|
||||
|
||||
if ([System.Diagnostics.EventLog]::SourceExists('mbproxy')) {
|
||||
Write-Host "Removing Windows Event Log source 'mbproxy'..."
|
||||
try {
|
||||
Remove-EventLog -Source 'mbproxy'
|
||||
} catch {
|
||||
Write-Warning "Could not remove Event Log source: $_"
|
||||
}
|
||||
} else {
|
||||
Write-Host "Windows Event Log source 'mbproxy' not registered — skipping."
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Uninstall complete." -ForegroundColor Green
|
||||
|
||||
if (Test-Path (Join-Path $env:ProgramData 'mbproxy.archived-*')) {
|
||||
Write-Host "Archived logs can be found under: $env:ProgramData\mbproxy.archived-*" -ForegroundColor Yellow
|
||||
}
|
||||
Reference in New Issue
Block a user