xyc
4 天以前 cdccc9840978adb989f173b8d4763b7b5677f9b5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
<#
  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-<version>.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).'
}