| New file |
| | |
| | | <# |
| | | Install / update / remove the TrafficAudit backend as a Windows service. |
| | | |
| | | The service is a WinSW (Windows Service Wrapper) host: trafficAudit.exe reads |
| | | trafficAudit.xml and runs "java -jar traffic-audit-server.jar". |
| | | Because it is a real Windows service it starts at boot with no user logged on, |
| | | is listed in services.msc / Get-Service, and is restarted by the Service |
| | | Control Manager when the java process dies. |
| | | |
| | | Run from an ELEVATED PowerShell in the backend folder |
| | | (the folder that contains the jar, application.yml, docs, web and trafficAudit.exe): |
| | | |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File install-service.ps1 |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File install-service.ps1 -Status |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File install-service.ps1 -Restart |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File install-service.ps1 -Remove |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File install-service.ps1 -NoStart |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File install-service.ps1 -JavaPath 'D:\jdk8\bin\java.exe' |
| | | |
| | | What it does: |
| | | 1. locates java.exe (JAVA_HOME -> PATH -> JavaSoft registry -> Program Files) |
| | | 2. writes that absolute path into trafficAudit.xml (<executable>) |
| | | 3. installs or reinstalls the service and starts it (unless -NoStart) |
| | | |
| | | Notes: |
| | | - Re-running is idempotent: an existing service is stopped and reinstalled. |
| | | - Logs: logs\trafficAudit.out.log / logs\trafficAudit.err.log (10 MB x 10 roll). |
| | | - Do NOT enable the scheduled-task autostart (install-autostart.ps1) at the |
| | | same time: two hosts would race for the same jar and port 8090. |
| | | - Keep this file ASCII-only; Windows PowerShell 5.1 reads a BOM-less .ps1 as ANSI. |
| | | #> |
| | | param( |
| | | [switch]$Remove, |
| | | [switch]$Status, |
| | | [switch]$Restart, |
| | | [switch]$NoStart, |
| | | [string]$JavaPath |
| | | ) |
| | | |
| | | $ErrorActionPreference = 'Stop' |
| | | |
| | | $root = Split-Path -Parent $MyInvocation.MyCommand.Path |
| | | $wrapper = Join-Path $root 'trafficAudit.exe' |
| | | $config = Join-Path $root 'trafficAudit.xml' |
| | | $appConfig = Join-Path $root 'application.yml' |
| | | $serviceId = 'TrafficAuditBackend' |
| | | |
| | | # Any terminating error below lands here instead of dumping a raw stack trace |
| | | # at the operator. The line number is kept so a copy of this window is enough |
| | | # to diagnose a report. |
| | | trap { |
| | | Write-Host '' |
| | | Write-Host '==============================================================' |
| | | Write-Host ' ERROR: the installer stopped before finishing.' |
| | | Write-Host (' ' + $_.Exception.Message) |
| | | Write-Host (' at line ' + $_.InvocationInfo.ScriptLineNumber + ' of install-service.ps1') |
| | | Write-Host ' The service may not be installed. Send this window text to the' |
| | | Write-Host ' developer if the reason is not obvious.' |
| | | Write-Host '==============================================================' |
| | | exit 1 |
| | | } |
| | | |
| | | function Assert-Admin { |
| | | $identity = [Security.Principal.WindowsIdentity]::GetCurrent() |
| | | $principal = New-Object Security.Principal.WindowsPrincipal($identity) |
| | | if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { |
| | | throw 'This script must run from an elevated (Run as Administrator) PowerShell.' |
| | | } |
| | | } |
| | | |
| | | function Resolve-JavaPath { |
| | | param([string]$Explicit) |
| | | |
| | | if ($Explicit) { |
| | | if (-not (Test-Path $Explicit)) { throw "JavaPath does not exist: $Explicit" } |
| | | return (Resolve-Path $Explicit).Path |
| | | } |
| | | |
| | | if ($env:JAVA_HOME) { |
| | | $candidate = Join-Path $env:JAVA_HOME 'bin\java.exe' |
| | | if (Test-Path $candidate) { return (Resolve-Path $candidate).Path } |
| | | } |
| | | |
| | | $onPath = Get-Command java.exe -ErrorAction SilentlyContinue |
| | | if ($onPath -and $onPath.Source) { return $onPath.Source } |
| | | |
| | | # A SYSTEM service inherits the machine PATH, which often lacks the JDK. |
| | | # Fall back to the install locations used by Oracle / OpenJDK installers. |
| | | foreach ($reg in @('HKLM:\SOFTWARE\JavaSoft\JDK', 'HKLM:\SOFTWARE\JavaSoft\Java Development Kit')) { |
| | | if (-not (Test-Path $reg)) { continue } |
| | | foreach ($ver in (Get-ChildItem $reg -ErrorAction SilentlyContinue | Sort-Object Name -Descending)) { |
| | | $home = (Get-ItemProperty -Path $ver.PSPath -Name JavaHome -ErrorAction SilentlyContinue).JavaHome |
| | | if ($home) { |
| | | $candidate = Join-Path $home 'bin\java.exe' |
| | | if (Test-Path $candidate) { return (Resolve-Path $candidate).Path } |
| | | } |
| | | } |
| | | } |
| | | |
| | | $roots = @($env:ProgramFiles) |
| | | if (${env:ProgramFiles(x86)}) { $roots += ${env:ProgramFiles(x86)} } |
| | | foreach ($base in $roots) { |
| | | foreach ($vendor in @('Java', 'Eclipse Adoptium', 'AdoptOpenJDK', 'Microsoft')) { |
| | | $hit = Get-ChildItem -Path (Join-Path $base ($vendor + '\*\bin\java.exe')) -ErrorAction SilentlyContinue | |
| | | Sort-Object FullName -Descending | Select-Object -First 1 |
| | | if ($hit) { return $hit.FullName } |
| | | } |
| | | } |
| | | |
| | | throw 'java.exe not found. Install a JDK 8 and re-run, or pass -JavaPath "D:\path\to\bin\java.exe".' |
| | | } |
| | | |
| | | function Set-WrapperJavaPath { |
| | | param([string]$JavaExe) |
| | | |
| | | if (-not (Test-Path $config)) { throw "Missing config file: $config" } |
| | | |
| | | # Edit through the DOM: a regex would also hit the executable mention inside |
| | | # the header comment and would mangle the whole document. |
| | | $doc = New-Object System.Xml.XmlDocument |
| | | $doc.PreserveWhitespace = $true |
| | | $doc.Load($config) |
| | | |
| | | $node = $doc.SelectSingleNode('/service/executable') |
| | | if (-not $node) { throw "No /service/executable element found in $config" } |
| | | |
| | | if ($node.InnerText -eq $JavaExe) { |
| | | Write-Host "Config already points at $JavaExe" |
| | | return |
| | | } |
| | | |
| | | $node.InnerText = $JavaExe |
| | | |
| | | $settings = New-Object System.Xml.XmlWriterSettings |
| | | $settings.Encoding = New-Object System.Text.UTF8Encoding $false |
| | | $settings.Indent = $true |
| | | $settings.IndentChars = ' ' |
| | | $writer = [System.Xml.XmlWriter]::Create($config, $settings) |
| | | try { $doc.Save($writer) } finally { $writer.Close() } |
| | | Write-Host "Updated $config -> $JavaExe" |
| | | } |
| | | |
| | | function Get-ServiceState { |
| | | Get-Service -Name $serviceId -ErrorAction SilentlyContinue |
| | | } |
| | | |
| | | function Test-LocalPort { |
| | | param([int]$Port, [string]$TargetHost = '127.0.0.1') |
| | | |
| | | # Probe TCP instead of HTTP: Invoke-WebRequest does not exist on Windows |
| | | # PowerShell 2 (Windows 7), and this works on every PowerShell version. |
| | | $client = New-Object System.Net.Sockets.TcpClient |
| | | try { |
| | | $async = $client.BeginConnect($TargetHost, $Port, $null, $null) |
| | | if ($async.AsyncWaitHandle.WaitOne(1000, $false) -and $client.Connected) { return $true } |
| | | return $false |
| | | } catch { |
| | | return $false |
| | | } finally { |
| | | $client.Close() |
| | | } |
| | | } |
| | | |
| | | function Test-AppConfigPassword { |
| | | param([string]$Path) |
| | | |
| | | # Returns $true when the datasource password looks usable. Prints a WARN and |
| | | # returns $false for the states that break startup with |
| | | # "Access denied ... (using password: NO)": missing file / empty value / placeholder. |
| | | if (-not (Test-Path -LiteralPath $Path)) { |
| | | Write-Host 'WARN: application.yml is missing next to the jar.' |
| | | Write-Host ' The built-in password placeholder resolves to an empty string and startup will fail with' |
| | | Write-Host " Java SQLException: Access denied for user 'root'@'localhost' (using password: NO)." |
| | | return $false |
| | | } |
| | | |
| | | $line = Select-String -LiteralPath $Path -Pattern '^\s*password\s*:' | Select-Object -First 1 |
| | | if (-not $line) { |
| | | Write-Host "WARN: no 'password:' entry found in application.yml; check the datasource block." |
| | | return $false |
| | | } |
| | | |
| | | # Trim() takes a single char, so strip whitespace, then one quote kind at a |
| | | # time. Passing a 2-char string here throws |
| | | # "cannot convert value ... to type System.Char" and aborts the script. |
| | | $value = $line.Line -replace '^\s*password\s*:\s*', '' |
| | | $value = $value.Trim() |
| | | $value = $value.Trim("'") |
| | | $value = $value.Trim('"') |
| | | |
| | | if ($value -eq '') { |
| | | Write-Host "WARN: spring.datasource.password is empty in application.yml; startup may fail with '(using password: NO)'." |
| | | return $false |
| | | } |
| | | if ($value -match '[^\x00-\x7F]') { |
| | | Write-Host 'WARN: the datasource password contains non-ASCII characters.' |
| | | Write-Host ' If it is still the Chinese placeholder text, replace it with the real MySQL password first.' |
| | | return $false |
| | | } |
| | | return $true |
| | | } |
| | | |
| | | # ---------- read-only status: no elevation needed ---------- |
| | | if ($Status) { |
| | | $service = Get-ServiceState |
| | | if (-not $service) { |
| | | Write-Host "Service '$serviceId' is NOT installed." |
| | | exit 1 |
| | | } |
| | | $service | Select-Object Name, DisplayName, Status, StartType | Format-List |
| | | # Get-CimInstance needs PowerShell 3+; fall back to WMI for Windows 7 / PowerShell 2. |
| | | $detail = $null |
| | | try { |
| | | $detail = Get-CimInstance Win32_Service -Filter "Name='$serviceId'" -ErrorAction Stop |
| | | } catch { |
| | | $detail = Get-WmiObject Win32_Service -Filter "Name='$serviceId'" -ErrorAction SilentlyContinue |
| | | } |
| | | if ($detail) { $detail | Select-Object ProcessId, StartMode, DelayedAutoStart, PathName | Format-List } |
| | | if (Test-Path $wrapper) { & $wrapper status } |
| | | exit 0 |
| | | } |
| | | |
| | | if ($Restart) { |
| | | Assert-Admin |
| | | if (-not (Get-ServiceState)) { throw "Service '$serviceId' is not installed; run this script without -Restart first." } |
| | | Write-Host "Restarting service '$serviceId'..." |
| | | Restart-Service -Name $serviceId -Force |
| | | Start-Sleep -Seconds 3 |
| | | Get-ServiceState | Select-Object Name, Status | Format-List |
| | | exit 0 |
| | | } |
| | | |
| | | if ($Remove) { |
| | | Assert-Admin |
| | | if (-not (Test-Path $wrapper)) { throw "Missing wrapper executable: $wrapper" } |
| | | if (Get-ServiceState) { |
| | | Write-Host "Stopping and removing service '$serviceId'..." |
| | | & $wrapper stop | Out-Null |
| | | Start-Sleep -Seconds 3 |
| | | & $wrapper uninstall | Out-Null |
| | | Write-Host "Removed service '$serviceId'. Files in $root were left untouched." |
| | | } else { |
| | | Write-Host "Service '$serviceId' is not installed; nothing to remove." |
| | | } |
| | | exit 0 |
| | | } |
| | | |
| | | # ---------- install / reinstall ---------- |
| | | Assert-Admin |
| | | |
| | | if (-not (Test-Path $wrapper)) { |
| | | throw "Missing wrapper executable: $wrapper`nRebuild the deploy package with pack-deploy.ps1 (it provides WinSW), or copy WinSW-x64.exe here renamed to trafficAudit.exe." |
| | | } |
| | | $passwordOk = Test-AppConfigPassword -Path $appConfig |
| | | |
| | | $java = Resolve-JavaPath -Explicit $JavaPath |
| | | Set-WrapperJavaPath -JavaExe $java |
| | | |
| | | $existing = Get-ServiceState |
| | | if ($existing) { |
| | | Write-Host "Service '$serviceId' already exists (status: $($existing.Status)); reinstalling..." |
| | | if ($existing.Status -ne 'Stopped') { |
| | | & $wrapper stop | Out-Null |
| | | Start-Sleep -Seconds 3 |
| | | } |
| | | & $wrapper uninstall | Out-Null |
| | | Start-Sleep -Seconds 2 |
| | | } |
| | | |
| | | Write-Host "Installing service '$serviceId'..." |
| | | & $wrapper install |
| | | if ($LASTEXITCODE -ne 0) { throw "WinSW install failed with exit code $LASTEXITCODE" } |
| | | |
| | | if (-not $NoStart) { |
| | | Write-Host "Starting service '$serviceId' (auto-start at boot is enabled)..." |
| | | & $wrapper start |
| | | Start-Sleep -Seconds 5 |
| | | $service = Get-ServiceState |
| | | $service | Select-Object Name, DisplayName, Status, StartType | Format-List |
| | | |
| | | # Read the port from application.yml so the check matches the real config. |
| | | $port = 8090 |
| | | if (Test-Path $appConfig) { |
| | | $portLine = Select-String -LiteralPath $appConfig -Pattern '^\s*port\s*:' | Select-Object -First 1 |
| | | if ($portLine -and $portLine.Line -match '(\d{2,5})') { $port = [int]$matches[1] } |
| | | } |
| | | |
| | | $ready = $false |
| | | for ($i = 1; $i -le 30; $i++) { |
| | | if (Test-LocalPort -Port $port) { $ready = $true; break } |
| | | Start-Sleep -Seconds 2 |
| | | $state = Get-ServiceState |
| | | if ($state -and $state.Status -ne 'Running') { break } |
| | | } |
| | | |
| | | $logDir = Join-Path $root 'logs' |
| | | Write-Host '' |
| | | if ($ready) { |
| | | Write-Host "OK : the backend is listening on port $port." |
| | | Write-Host " On this server : http://127.0.0.1:$port" |
| | | Write-Host " For other people : http://<server-ip>:$port" |
| | | } else { |
| | | Write-Host "WARN: nothing is listening on port $port yet - startup is still running, or it failed." |
| | | Write-Host " Fix application.yml / MySQL, then run: .\install-service.ps1 -Restart" |
| | | foreach ($name in @('trafficAudit.err.log', 'trafficAudit.out.log')) { |
| | | $logFile = Join-Path $logDir $name |
| | | if (Test-Path -LiteralPath $logFile) { |
| | | Write-Host '' |
| | | Write-Host "---- last 15 lines of $name ----" |
| | | Get-Content -LiteralPath $logFile -Tail 15 -ErrorAction SilentlyContinue | ForEach-Object { Write-Host " $_" } |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (-not $passwordOk) { |
| | | Write-Host '' |
| | | Write-Host '==============================================================' |
| | | Write-Host ' IMPORTANT: backend\application.yml still looks unset.' |
| | | Write-Host ' Put the real MySQL url / username / password in it, then run' |
| | | Write-Host ' .\install-service.ps1 -Restart' |
| | | Write-Host '==============================================================' |
| | | } |
| | | |
| | | Write-Host '' |
| | | Write-Host "Logs : $logDir\trafficAudit.out.log / trafficAudit.err.log" |
| | | Write-Host "Manage: .\install-service.ps1 -Status | -Restart | -Remove" |
| | | Write-Host " Stop-Service $serviceId | Start-Service $serviceId" |
| | | Write-Host '' |
| | | Write-Host 'This service starts automatically on every boot (Automatic + delayed start).' |
| | | } |