#Requires -Version 5.1
|
<#
|
.SYNOPSIS
|
JNPF 微服务批量启动脚本(Windows / PowerShell 版,对齐 start-all.sh)
|
|
.DESCRIPTION
|
启动除 scheduletask 之外的所有服务(example 已于 2026-08-11 移除)
|
|
用法:
|
.\start-all.ps1 前台启动全部服务(Ctrl+C 统一停止)
|
.\start-all.ps1 --build 先编译再前台启动
|
.\start-all.ps1 stop 停止全部已启动的服务
|
.\start-all.ps1 restart 停止 → 编译 → 前台启动
|
.\start-all.ps1 status 查看各服务运行状态
|
|
.\start-all.ps1 jnpf-dms 只启动 jnpf-dms
|
.\start-all.ps1 restart jnpf-lims 只重启 jnpf-lims(仅编译该模块)
|
.\start-all.ps1 stop jnpf-lims 只停 jnpf-lims
|
.\start-all.ps1 restart jnpf-lims jnpf-platform 支持多服务
|
|
也可双击 start-all.bat,或在 cmd 中: start-all.bat [参数...]
|
#>
|
|
Set-StrictMode -Version Latest
|
$ErrorActionPreference = 'Stop'
|
|
# ────────────────────────────────────────────────
|
# 路径与常量
|
# ────────────────────────────────────────────────
|
$Script:BaseDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
$Script:ServicePidDir = Join-Path $Script:BaseDir '.pids'
|
$Script:LogDir = Join-Path $Script:BaseDir 'log'
|
$Script:Version = '6.1.0-RELEASE'
|
|
$Script:JvmAddOpens = @(
|
'--add-opens=java.base/java.lang=ALL-UNNAMED'
|
'--add-opens=java.base/java.lang.reflect=ALL-UNNAMED'
|
'--add-opens=java.base/java.lang.invoke=ALL-UNNAMED'
|
'--add-opens=java.base/java.math=ALL-UNNAMED'
|
'--add-opens=java.base/sun.net.util=ALL-UNNAMED'
|
'--add-opens=java.base/java.io=ALL-UNNAMED'
|
'--add-opens=java.base/java.net=ALL-UNNAMED'
|
'--add-opens=java.base/java.nio=ALL-UNNAMED'
|
'--add-opens=java.base/java.security=ALL-UNNAMED'
|
'--add-opens=java.base/java.text=ALL-UNNAMED'
|
'--add-opens=java.base/java.time=ALL-UNNAMED'
|
'--add-opens=java.base/java.util=ALL-UNNAMED'
|
'--add-opens=java.base/jdk.internal.module=ALL-UNNAMED'
|
'--add-opens=java.base/sun.security.util=ALL-UNNAMED'
|
)
|
|
# 服务列表: Name, Jar, Port
|
$Script:Services = @(
|
[PSCustomObject]@{ Name = 'jnpf-gateway'; Jar = "jnpf-gateway/target/jnpf-gateway-$($Script:Version).jar"; Port = 30000 }
|
[PSCustomObject]@{ Name = 'jnpf-platform'; Jar = "jnpf-platform/jnpf-platform-server/target/jnpf-platform-$($Script:Version).jar"; Port = 30002 }
|
[PSCustomObject]@{ Name = 'jnpf-biz-common'; Jar = "jnpf-biz-common/jnpf-biz-common-server/target/jnpf-biz-common-$($Script:Version).jar"; Port = 30015 }
|
[PSCustomObject]@{ Name = 'jnpf-eln'; Jar = "jnpf-eln/jnpf-eln-server/target/jnpf-eln-$($Script:Version).jar"; Port = 30013 }
|
[PSCustomObject]@{ Name = 'jnpf-lims'; Jar = "jnpf-lims/jnpf-lims-server/target/jnpf-lims-$($Script:Version).jar"; Port = 30019 }
|
[PSCustomObject]@{ Name = 'jnpf-dms'; Jar = "jnpf-dms/jnpf-dms-server/target/jnpf-dms-$($Script:Version).jar"; Port = 30016 }
|
)
|
|
$Script:TargetServices = @()
|
$Script:StartedServices = @()
|
$Script:StartedServiceCount = 0
|
|
# ── 颜色输出 ──
|
function Write-ColorLine {
|
param(
|
[string]$Text,
|
[ConsoleColor]$Color = [ConsoleColor]::Gray,
|
[switch]$NoNewline
|
)
|
$prev = [Console]::ForegroundColor
|
[Console]::ForegroundColor = $Color
|
if ($NoNewline) {
|
Write-Host $Text -NoNewline
|
} else {
|
Write-Host $Text
|
}
|
[Console]::ForegroundColor = $prev
|
}
|
|
function Write-Ok { param([string]$Text) Write-ColorLine $Text Green }
|
function Write-Warn { param([string]$Text) Write-ColorLine $Text Yellow }
|
function Write-Err { param([string]$Text) Write-ColorLine $Text Red }
|
|
# ────────────────────────────────────────────────
|
# .env 加载(不覆盖启动前已存在的环境变量)
|
# ────────────────────────────────────────────────
|
function Import-DotEnv {
|
param([string]$Path)
|
|
if (-not (Test-Path -LiteralPath $Path)) { return }
|
|
$preset = @{}
|
foreach ($key in [Environment]::GetEnvironmentVariables('Process').Keys) {
|
$preset[$key.ToString()] = $true
|
}
|
|
$loaded = 0
|
foreach ($rawLine in Get-Content -LiteralPath $Path -Encoding UTF8) {
|
$line = $rawLine.TrimEnd("`r")
|
if ($line -match '^\s*$' -or $line -match '^\s*#') { continue }
|
if ($line -notmatch '^([A-Za-z_][A-Za-z0-9_]*)=(.*)$') { continue }
|
|
$k = $Matches[1]
|
$v = $Matches[2]
|
if ($preset.ContainsKey($k)) { continue }
|
|
# 跳过空值:避免 JNPF_REDIS_PASSWORD= 这类行覆盖 yaml 默认值,或触发无意义的 AUTH
|
if ($null -eq $v -or $v -eq '') { continue }
|
|
if (-not [Environment]::GetEnvironmentVariable($k, 'Process')) {
|
$loaded++
|
}
|
[Environment]::SetEnvironmentVariable($k, $v, 'Process')
|
}
|
|
Write-Host "已从 .env 加载 $loaded 个环境变量"
|
}
|
|
function Initialize-Environment {
|
Import-DotEnv (Join-Path $Script:BaseDir '.env')
|
|
if (-not $env:ONLYOFFICE_FILE_FETCH_BASE_URL) {
|
$env:ONLYOFFICE_FILE_FETCH_BASE_URL = 'http://host.docker.internal:30000'
|
}
|
if (-not $env:ONLYOFFICE_CALLBACK_BASE_URL) {
|
$env:ONLYOFFICE_CALLBACK_BASE_URL = 'http://host.docker.internal:30000'
|
}
|
if (-not $env:ONLYOFFICE_DS_INTERNAL_URL) {
|
$env:ONLYOFFICE_DS_INTERNAL_URL = 'http://localhost:20000'
|
}
|
|
if ($env:CUSTOMER_DB_HOST -eq 'cx-postgres') {
|
$env:CUSTOMER_DB_HOST = '127.0.0.1'
|
$env:CUSTOMER_DB_PORT = '5433'
|
Write-Host '本机档:已翻译 PG 坐标 cx-postgres:5432 → 127.0.0.1:5433(宿主发布端口)'
|
}
|
|
if (-not $env:JNPF_REDIS_HOST) {
|
try {
|
[void][System.Net.Dns]::GetHostEntry('cx-redis')
|
} catch {
|
$env:JNPF_REDIS_HOST = '127.0.0.1'
|
Write-Warn 'cx-redis 无法解析,已回退 JNPF_REDIS_HOST=127.0.0.1'
|
Write-Host ' 请启动 Docker Redis: docker compose --profile local-redis up -d cx-redis'
|
Write-Host ' 并在 hosts 追加: 127.0.0.1 cx-redis(或保持 127.0.0.1:6379 直连)'
|
}
|
}
|
|
if (-not $env:JNPF_REDIS_PASSWORD) {
|
$env:JNPF_REDIS_PASSWORD = 'jnpf_redis_2026'
|
Write-Host 'Redis 密码:使用默认 jnpf_redis_2026(与 .env.example / cx-redis 容器一致)'
|
}
|
|
Test-WindowsHostsHint
|
|
$dbHost = if ($env:CUSTOMER_DB_HOST) { $env:CUSTOMER_DB_HOST } else { '<未设,将回退 cx-postgres>' }
|
$dbPort = if ($env:CUSTOMER_DB_PORT) { $env:CUSTOMER_DB_PORT } else { '<未设,将回退 5432>' }
|
Write-Host "客户坐标:${dbHost}:${dbPort}"
|
|
if (-not $env:JNPF_LOG_PROVIDER_URL) {
|
$env:JNPF_LOG_PROVIDER_URL = 'dubbo://127.0.0.1:20880'
|
}
|
|
if (-not $env:JNPF_SERVICE_MAX_HEAP) { $env:JNPF_SERVICE_MAX_HEAP = '384m' }
|
if (-not $env:JNPF_PLATFORM_MAX_HEAP) { $env:JNPF_PLATFORM_MAX_HEAP = '1536m' }
|
if (-not $env:SERVICE_READY_TIMEOUT) { $env:SERVICE_READY_TIMEOUT = '360' }
|
if (-not $env:LOAD_GATE) { $env:LOAD_GATE = '85' }
|
|
if ($env:JNPF_SERVICE_MAX_HEAP -notmatch '^[0-9]+[mMgG]$') {
|
throw 'JNPF_SERVICE_MAX_HEAP 必须是类似 384m 或 1g 的内存大小'
|
}
|
if ($env:JNPF_PLATFORM_MAX_HEAP -notmatch '^[0-9]+[mMgG]$') {
|
throw 'JNPF_PLATFORM_MAX_HEAP 必须是类似 1536m 或 2g 的内存大小'
|
}
|
|
New-Item -ItemType Directory -Force -Path $Script:ServicePidDir | Out-Null
|
Write-Host "JVM 单服务最大堆内存:$($env:JNPF_SERVICE_MAX_HEAP)(jnpf-platform 单独用 $($env:JNPF_PLATFORM_MAX_HEAP))"
|
|
$Script:JavaExe = Resolve-JavaExecutable
|
Test-JavaVersion -JavaExe $Script:JavaExe
|
Write-Host "Java 可执行文件:$($Script:JavaExe)"
|
}
|
|
function Test-WindowsHostsHint {
|
$aliases = @('cx-infra', 'cx-flow-engine')
|
$missing = @()
|
|
foreach ($alias in $aliases) {
|
try {
|
[void][System.Net.Dns]::GetHostEntry($alias)
|
} catch {
|
$missing += $alias
|
}
|
}
|
|
if ($missing.Count -gt 0) {
|
Write-Warn ("以下容器别名无法解析:{0}" -f ($missing -join ', '))
|
Write-Host ' 建议在 C:\Windows\System32\drivers\etc\hosts 追加一行:'
|
Write-Host ' 127.0.0.1 cx-infra cx-redis cx-flow-engine cx-postgres'
|
}
|
}
|
|
function Resolve-JavaExecutable {
|
if ($env:JAVA_HOME) {
|
$candidate = Join-Path $env:JAVA_HOME 'bin\java.exe'
|
if (Test-Path -LiteralPath $candidate) {
|
return (Resolve-Path -LiteralPath $candidate).Path
|
}
|
}
|
|
$fromPath = (Get-Command java -ErrorAction SilentlyContinue).Source
|
if ($fromPath) {
|
return (Resolve-Path -LiteralPath $fromPath).Path
|
}
|
|
throw '找不到 java.exe,请设置 JAVA_HOME 或将 Java 17+ 加入 PATH'
|
}
|
|
function Test-JavaVersion {
|
param([string]$JavaExe)
|
|
$oldEap = $ErrorActionPreference
|
$ErrorActionPreference = 'Continue'
|
try {
|
$versionLines = & $JavaExe -version 2>&1 | ForEach-Object { $_.ToString() }
|
} finally {
|
$ErrorActionPreference = $oldEap
|
}
|
|
$versionLine = $versionLines | Select-Object -First 1
|
if ($versionLine -notmatch 'version "([^"]+)"') {
|
throw "无法解析 Java 版本: $versionLine"
|
}
|
|
$versionText = $Matches[1]
|
$major = if ($versionText -match '^1\.(\d+)') {
|
[int]$Matches[1]
|
} elseif ($versionText -match '^(\d+)') {
|
[int]$Matches[1]
|
} else {
|
throw "无法解析 Java 主版本号: $versionText"
|
}
|
|
if ($major -lt 17) {
|
throw "需要 Java 17+ 才能使用 --add-opens(当前 $versionText)。请设置 JAVA_HOME 指向 Java 21。"
|
}
|
|
Write-Host "Java 版本:$versionText"
|
}
|
|
function Get-DefaultJvmOpts {
|
return @($Script:JvmAddOpens + @(
|
"-Xmx$($env:JNPF_SERVICE_MAX_HEAP)"
|
'-Dfile.encoding=utf8'
|
))
|
}
|
|
function Get-PlatformJvmOpts {
|
return @($Script:JvmAddOpens + @(
|
"-Xmx$($env:JNPF_PLATFORM_MAX_HEAP)"
|
'-Dfile.encoding=utf8'
|
))
|
}
|
|
function Start-JavaServiceProcess {
|
param(
|
[string[]]$JvmOpts,
|
[string]$JarPath,
|
[string]$LogFile
|
)
|
|
$errLogFile = "$LogFile.err"
|
Set-Content -LiteralPath $LogFile -Value '' -Encoding UTF8
|
if (Test-Path -LiteralPath $errLogFile) {
|
Remove-Item -LiteralPath $errLogFile -Force
|
}
|
|
$argList = @($JvmOpts) + @('-jar', $JarPath)
|
|
# Start-Process 在部分 PowerShell 7/Windows 环境会因 PATH 与 Path
|
# 大小写重复而构造环境字典失败。通过 ProcessStartInfo 启动 cmd,继承
|
# 当前进程(已加载 .env)的环境变量,
|
# 由 cmd 将 Java 输出重定向到日志文件,避免 PowerShell 事件回调的
|
# 生命周期问题,同时保留对 --add-opens 等参数的可靠传递。
|
$startInfo = [System.Diagnostics.ProcessStartInfo]::new()
|
$startInfo.FileName = $env:ComSpec
|
$startInfo.WorkingDirectory = $Script:BaseDir
|
$startInfo.UseShellExecute = $false
|
$startInfo.CreateNoWindow = $true
|
$javaArgs = (($argList | ForEach-Object {
|
'"' + ([string]$_).Replace('"', '\\"') + '"'
|
}) -join ' ')
|
$command = '"' + $Script:JavaExe + '" ' + $javaArgs + ' > "' + $LogFile + '" 2> "' + $errLogFile + '"'
|
$startInfo.Arguments = '/d /c "' + $command + '"'
|
|
$process = [System.Diagnostics.Process]::new()
|
$process.StartInfo = $startInfo
|
[void]$process.Start()
|
return $process
|
}
|
|
function Merge-ServiceErrLog {
|
param([string]$LogFile)
|
|
$errLogFile = "$LogFile.err"
|
if (-not (Test-Path -LiteralPath $errLogFile)) { return }
|
|
$errContent = Get-Content -LiteralPath $errLogFile -Raw -ErrorAction SilentlyContinue
|
if ($errContent) {
|
Add-Content -LiteralPath $LogFile -Value $errContent -Encoding UTF8
|
}
|
}
|
|
function Show-LogTail {
|
param(
|
[string]$LogFile,
|
[int]$Lines = 10
|
)
|
|
Merge-ServiceErrLog -LogFile $LogFile
|
|
if (Test-Path -LiteralPath $LogFile) {
|
Write-Warn " --- 日志末尾 ($LogFile) ---"
|
Get-Content -LiteralPath $LogFile -Tail $Lines -ErrorAction SilentlyContinue |
|
ForEach-Object { Write-Host " $_" }
|
}
|
}
|
|
# ────────────────────────────────────────────────
|
# 服务名解析
|
# ────────────────────────────────────────────────
|
function Resolve-TargetServices {
|
param([string[]]$Names)
|
|
if ($Names.Count -eq 0) {
|
$Script:TargetServices = @($Script:Services)
|
return
|
}
|
|
$resolved = @()
|
foreach ($req in $Names) {
|
$matched = $Script:Services | Where-Object { $_.Name -eq $req } | Select-Object -First 1
|
if (-not $matched) {
|
$available = ($Script:Services | ForEach-Object { $_.Name }) -join ' '
|
Write-Err "未知服务名: $req"
|
Write-Host "可用服务:$available"
|
exit 1
|
}
|
$resolved += $matched
|
}
|
$Script:TargetServices = $resolved
|
}
|
|
function Test-TargetContains {
|
param([string]$Name)
|
return [bool]($Script:TargetServices | Where-Object { $_.Name -eq $Name })
|
}
|
|
# ────────────────────────────────────────────────
|
# 端口 / 进程工具
|
# ────────────────────────────────────────────────
|
function Test-PortListening {
|
param([int]$Port)
|
|
try {
|
$client = New-Object System.Net.Sockets.TcpClient
|
$connect = $client.BeginConnect('127.0.0.1', $Port, $null, $null)
|
$waited = $connect.AsyncWaitHandle.WaitOne(300)
|
if ($waited -and $client.Connected) {
|
$client.Close()
|
return $true
|
}
|
$client.Close()
|
} catch { }
|
return $false
|
}
|
|
function Get-PortListenerPid {
|
param([int]$Port)
|
|
try {
|
$conn = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue |
|
Select-Object -First 1
|
if ($conn) { return [int]$conn.OwningProcess }
|
} catch { }
|
return $null
|
}
|
|
function Get-PortOccupancy {
|
param([int]$Port)
|
|
if (-not (Test-PortListening $Port)) {
|
return @{ Listening = $false; Pid = $null }
|
}
|
|
return @{
|
Listening = $true
|
Pid = (Get-PortListenerPid $Port)
|
}
|
}
|
|
function Get-PidFilePath {
|
param([string]$Name)
|
return Join-Path $Script:ServicePidDir "$Name.pid"
|
}
|
|
function Test-ProcessAlive {
|
param([int]$ProcId)
|
if ($ProcId -le 0) { return $false }
|
return [bool](Get-Process -Id $ProcId -ErrorAction SilentlyContinue)
|
}
|
|
function Wait-ForPort {
|
param(
|
[string]$Name,
|
[int]$Port,
|
[int]$Timeout = 60
|
)
|
|
$startTs = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
$servicePidFile = Get-PidFilePath $Name
|
|
for ($elapsed = 0; $elapsed -lt $Timeout; $elapsed += 2) {
|
if (Test-PortListening $Port) {
|
$cost = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() - $startTs
|
Write-Ok " [就绪] $Name (端口 $Port, ${cost}s)"
|
return $true
|
}
|
|
if (Test-Path -LiteralPath $servicePidFile) {
|
$childProcId = [int](Get-Content -LiteralPath $servicePidFile -Raw).Trim()
|
if (-not (Test-ProcessAlive $childProcId)) {
|
Merge-ServiceErrLog -LogFile (Join-Path $Script:LogDir "$Name/startup.log")
|
Write-Err " [失败] $Name (端口 $Port) — 进程已退出,查看 $Script:LogDir/$Name/startup.log"
|
Show-LogTail -LogFile (Join-Path $Script:LogDir "$Name/startup.log")
|
return $false
|
}
|
}
|
|
Start-Sleep -Seconds 2
|
}
|
|
Write-Warn " [警告] $Name (端口 $Port) 在 ${Timeout}s 内未就绪"
|
return $false
|
}
|
|
# ────────────────────────────────────────────────
|
# 启动 / 停止 / 状态
|
# ────────────────────────────────────────────────
|
function Start-JnpfService {
|
param(
|
[PSCustomObject]$Service
|
)
|
|
$name = $Service.Name
|
$jar = $Service.Jar
|
$port = $Service.Port
|
$jarPath = Join-Path $Script:BaseDir $jar
|
$servicePidFile = Get-PidFilePath $name
|
$logFile = Join-Path $Script:LogDir "$name/startup.log"
|
|
if (Test-Path -LiteralPath $servicePidFile) {
|
$existingProcId = [int](Get-Content -LiteralPath $servicePidFile -Raw).Trim()
|
if (Test-ProcessAlive $existingProcId) {
|
Write-Warn " [跳过] $name (端口 $port) — 已在运行 (PID $existingProcId)"
|
return $true
|
}
|
Remove-Item -LiteralPath $servicePidFile -Force
|
}
|
|
$occupancy = Get-PortOccupancy $port
|
if ($occupancy.Listening) {
|
$extra = if ($occupancy.Pid) { " (PID $($occupancy.Pid))" } else { '' }
|
Write-Err " [失败] $name — 端口 $port 已被未登记进程占用$extra"
|
return $false
|
}
|
|
if (-not (Test-Path -LiteralPath $jarPath)) {
|
Write-Err " [失败] $name — JAR 不存在: $jar"
|
return $false
|
}
|
|
$logDir = Split-Path -Parent $logFile
|
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
|
|
$dubboPort = if ($name -eq 'jnpf-platform') { '20880' } else { '-1' }
|
$env:DUBBO_PROTOCOL_PORT = $dubboPort
|
|
$jvmOpts = if ($name -eq 'jnpf-platform') { Get-PlatformJvmOpts } else { Get-DefaultJvmOpts }
|
$proc = Start-JavaServiceProcess -JvmOpts $jvmOpts -JarPath $jarPath -LogFile $logFile
|
|
Start-Sleep -Milliseconds 500
|
if ($proc.HasExited) {
|
Merge-ServiceErrLog -LogFile $logFile
|
Write-Err " [失败] $name — Java 进程立即退出,查看 $logFile"
|
Show-LogTail -LogFile $logFile
|
return $false
|
}
|
|
Set-Content -LiteralPath $servicePidFile -Value $proc.Id -NoNewline -Encoding ASCII
|
$Script:StartedServices += $Service
|
$Script:StartedServiceCount++
|
|
Write-Ok " [启动] $name (端口 $port, PID $($proc.Id)) — 日志: $logFile"
|
return $true
|
}
|
|
function Wait-ServicesReady {
|
param(
|
[int]$Timeout,
|
[PSCustomObject[]]$Entries
|
)
|
|
$names = @($Entries | ForEach-Object { $_.Name })
|
$ports = @($Entries | ForEach-Object { $_.Port })
|
$statuses = @($Entries | ForEach-Object { 'pending' })
|
|
$total = $Entries.Count
|
$startTs = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
$doneCnt = 0
|
|
while ((([DateTimeOffset]::UtcNow.ToUnixTimeSeconds() - $startTs) -lt $Timeout) -and ($doneCnt -lt $total)) {
|
for ($i = 0; $i -lt $total; $i++) {
|
if ($statuses[$i] -ne 'pending') { continue }
|
|
$name = $names[$i]
|
$port = $ports[$i]
|
$servicePidFile = Get-PidFilePath $name
|
$childProcId = 0
|
if (Test-Path -LiteralPath $servicePidFile) {
|
$childProcId = [int](Get-Content -LiteralPath $servicePidFile -Raw).Trim()
|
}
|
|
if ($childProcId -le 0 -or -not (Test-ProcessAlive $childProcId)) {
|
$statuses[$i] = 'failed'
|
$doneCnt++
|
$logFile = Join-Path $Script:LogDir "$name/startup.log"
|
Merge-ServiceErrLog -LogFile $logFile
|
Write-Err " [失败] $name (端口 $port) — 进程已退出,查看 $logFile"
|
Show-LogTail -LogFile $logFile
|
} elseif (Test-PortListening $port) {
|
$statuses[$i] = 'ready'
|
$doneCnt++
|
$cost = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() - $startTs
|
Write-Ok " [就绪] $name (端口 $port, PID $childProcId, ${cost}s) [$doneCnt/$total]"
|
}
|
}
|
|
if ($doneCnt -lt $total) {
|
Start-Sleep -Seconds 2
|
}
|
}
|
|
$readyCnt = 0
|
$failedCnt = 0
|
$timeoutCnt = 0
|
for ($i = 0; $i -lt $total; $i++) {
|
switch ($statuses[$i]) {
|
'ready' { $readyCnt++ }
|
'failed' { $failedCnt++ }
|
'pending' {
|
$timeoutCnt++
|
Write-Warn " [超时] $($names[$i]) (端口 $($ports[$i])) — ${Timeout}s 内未监听端口"
|
}
|
}
|
}
|
|
Write-Host ''
|
Write-Host "── 启动结果: " -NoNewline
|
Write-ColorLine "就绪 $readyCnt" Green -NoNewline
|
Write-Host ' ' -NoNewline
|
Write-ColorLine "失败 $failedCnt" Red -NoNewline
|
Write-Host ' ' -NoNewline
|
Write-ColorLine "超时 $timeoutCnt" Yellow -NoNewline
|
Write-Host " / 合计 $total ──"
|
|
return ($failedCnt -eq 0 -and $timeoutCnt -eq 0)
|
}
|
|
function Stop-StartedServices {
|
foreach ($svc in $Script:StartedServices) {
|
Stop-JnpfService -Service $svc
|
}
|
$Script:StartedServices = @()
|
$Script:StartedServiceCount = 0
|
}
|
|
function Stop-JnpfService {
|
param([PSCustomObject]$Service)
|
|
$name = $Service.Name
|
$port = $Service.Port
|
$servicePidFile = Get-PidFilePath $name
|
|
if (-not (Test-Path -LiteralPath $servicePidFile)) {
|
$occupancy = Get-PortOccupancy $port
|
if ($occupancy.Listening) {
|
$extra = if ($occupancy.Pid) { " (PID $($occupancy.Pid))" } else { '' }
|
Write-Warn " [跳过] $name — 端口 $port 被未登记进程占用$extra,未自动停止"
|
} else {
|
Write-Warn " [跳过] $name — 无 PID 文件"
|
}
|
return
|
}
|
|
$childProcId = [int](Get-Content -LiteralPath $servicePidFile -Raw).Trim()
|
if (Test-ProcessAlive $childProcId) {
|
try {
|
Stop-Process -Id $childProcId -Force -ErrorAction Stop
|
Write-Ok " [停止] $name (端口 $port, PID $childProcId)"
|
} catch {
|
Write-Warn " [警告] $name — 停止 PID $childProcId 失败: $($_.Exception.Message)"
|
}
|
} else {
|
Write-Warn " [跳过] $name — 进程已不存在 (PID $childProcId)"
|
}
|
|
Remove-Item -LiteralPath $servicePidFile -Force -ErrorAction SilentlyContinue
|
}
|
|
function Show-JnpfStatus {
|
param([PSCustomObject]$Service)
|
|
$name = $Service.Name
|
$port = $Service.Port
|
$servicePidFile = Get-PidFilePath $name
|
|
if (Test-Path -LiteralPath $servicePidFile) {
|
$childProcId = [int](Get-Content -LiteralPath $servicePidFile -Raw).Trim()
|
if (Test-ProcessAlive $childProcId) {
|
if (Test-PortListening $port) {
|
Write-Ok " [运行中] $name 端口=$port PID=$childProcId"
|
} else {
|
Write-Warn " [启动中] $name 端口=$port PID=$childProcId"
|
}
|
return
|
}
|
Remove-Item -LiteralPath $servicePidFile -Force -ErrorAction SilentlyContinue
|
}
|
|
$occupancy = Get-PortOccupancy $port
|
if ($occupancy.Listening) {
|
$extra = if ($occupancy.Pid) { " PID=$($occupancy.Pid)" } else { '' }
|
Write-Warn " [未登记] $name 端口=$port$extra"
|
} else {
|
Write-Err " [未运行] $name 端口=$port"
|
}
|
}
|
|
function Invoke-StartupFailed {
|
Write-Host ''
|
Write-Err '启动未完成,清理本次已启动的服务。'
|
Stop-StartedServices
|
exit 1
|
}
|
|
function Wait-Foreground {
|
if ($Script:StartedServiceCount -eq 0) {
|
Write-Host ''
|
Write-Warn '本次没有新启动的服务,脚本结束。'
|
return
|
}
|
|
Write-Host ''
|
Write-Ok '所有服务以前台模式运行,按 Ctrl+C 统一停止。'
|
Write-Host ''
|
|
$handler = {
|
param($sender, $e)
|
$e.Cancel = $true
|
Write-Host ''
|
Write-Host '── 收到中断信号,停止本次启动的服务 ──'
|
Stop-StartedServices
|
Write-Host '========================================'
|
[Environment]::Exit(0)
|
}
|
[Console]::add_CancelKeyPress($handler)
|
|
try {
|
while ($true) {
|
$anyAlive = $false
|
foreach ($svc in $Script:StartedServices) {
|
$servicePidFile = Get-PidFilePath $svc.Name
|
if (Test-Path -LiteralPath $servicePidFile) {
|
$childProcId = [int](Get-Content -LiteralPath $servicePidFile -Raw).Trim()
|
if (Test-ProcessAlive $childProcId) {
|
$anyAlive = $true
|
break
|
}
|
}
|
}
|
if (-not $anyAlive) {
|
Write-Warn '所有子进程均已退出,脚本结束。'
|
break
|
}
|
Start-Sleep -Seconds 5
|
}
|
} finally {
|
[Console]::remove_CancelKeyPress($handler)
|
}
|
}
|
|
# ────────────────────────────────────────────────
|
# 编译
|
# ────────────────────────────────────────────────
|
function Build-ServiceEntries {
|
param([PSCustomObject[]]$Entries)
|
|
$modules = @($Entries | ForEach-Object {
|
$targetDir = Split-Path -Parent $_.Jar
|
Split-Path -Parent $targetDir
|
})
|
$pl = $modules -join ','
|
Write-Host "── 编译项目(仅 $pl)──"
|
& mvn -f (Join-Path $Script:BaseDir 'pom.xml') -pl $pl -am clean package -DskipTests -q
|
if ($LASTEXITCODE -ne 0) {
|
Write-Err '编译失败,中止。'
|
exit 1
|
}
|
Write-Ok '编译完成'
|
}
|
|
function Invoke-Build {
|
if ($Script:TargetServices.Count -eq $Script:Services.Count) {
|
Write-Host '── 编译项目(全量)──'
|
& mvn -f (Join-Path $Script:BaseDir 'pom.xml') clean package -DskipTests -q
|
if ($LASTEXITCODE -ne 0) {
|
Write-Err '编译失败,中止。'
|
exit 1
|
}
|
Write-Ok '编译完成'
|
} else {
|
Build-ServiceEntries -Entries $Script:TargetServices
|
}
|
}
|
|
function Ensure-TargetJars {
|
$missing = @($Script:TargetServices | Where-Object {
|
-not (Test-Path -LiteralPath (Join-Path $Script:BaseDir $_.Jar))
|
})
|
|
foreach ($svc in $missing) {
|
Write-Warn " [缺 JAR] $($svc.Name) — 将自动编译"
|
}
|
|
if ($missing.Count -gt 0) {
|
Build-ServiceEntries -Entries $missing
|
}
|
}
|
|
# ────────────────────────────────────────────────
|
# 分波启动(Windows 用 CPU % 作负载闸门,LOAD_GATE 默认 85)
|
# ────────────────────────────────────────────────
|
function Get-LoadNow {
|
try {
|
$sample = Get-Counter '\Processor(_Total)\% Processor Time' -ErrorAction Stop
|
return [int][math]::Round($sample.CounterSamples[0].CookedValue)
|
} catch {
|
return 0
|
}
|
}
|
|
function Wait-LoadGate {
|
$gate = [int]$env:LOAD_GATE
|
while ($true) {
|
$load = Get-LoadNow
|
if ($load -lt $gate) { return }
|
Write-Host " CPU 负载 ${load}% ≥ 闸门 ${gate}%,等 20s 再放下一波…(可 LOAD_GATE=N 调整)"
|
Start-Sleep -Seconds 20
|
}
|
}
|
|
function Get-ServiceWave {
|
param([string]$Name)
|
switch ($Name) {
|
'jnpf-platform' { return 1 }
|
'jnpf-biz-common' { return 2 }
|
{ $_ -in 'jnpf-lims', 'jnpf-dms' } { return 3 }
|
default { return 4 }
|
}
|
}
|
|
function Start-InWaves {
|
param([PSCustomObject[]]$BizServices)
|
|
$timeout = [int]$env:SERVICE_READY_TIMEOUT
|
|
foreach ($w in 1..4) {
|
$waveEntries = @($BizServices | Where-Object { (Get-ServiceWave $_.Name) -eq $w })
|
if ($waveEntries.Count -eq 0) { continue }
|
|
$waveNames = ($waveEntries | ForEach-Object { $_.Name }) -join ' '
|
Write-Host ''
|
Write-Host "── 波次 $w/4: $waveNames ──"
|
Wait-LoadGate
|
|
foreach ($entry in $waveEntries) {
|
if (-not (Start-JnpfService -Service $entry)) {
|
return $false
|
}
|
}
|
|
if (-not (Wait-ServicesReady -Timeout $timeout -Entries $waveEntries)) {
|
return $false
|
}
|
}
|
|
return $true
|
}
|
|
function Invoke-DoStart {
|
Write-Host '========================================'
|
Write-Host " 启动 JNPF 微服务 (本次 $($Script:TargetServices.Count) 个 / 共 $($Script:Services.Count) 个)"
|
Write-Host '========================================'
|
|
$datasource = Join-Path $Script:BaseDir 'config/shared/datasource.yaml'
|
if (-not (Test-Path -LiteralPath $datasource)) {
|
Write-Err '缺 config/shared/datasource.yaml(补丁 #8 后配置在仓库内),中止启动。'
|
exit 1
|
}
|
|
$timeout = [int]$env:SERVICE_READY_TIMEOUT
|
$bizServices = @()
|
|
if (Test-TargetContains 'jnpf-gateway') {
|
Write-Host '── 阶段 1: 启动网关 ──'
|
foreach ($entry in $Script:TargetServices) {
|
if ($entry.Name -eq 'jnpf-gateway') {
|
if (-not (Start-JnpfService -Service $entry)) { Invoke-StartupFailed }
|
if (-not (Wait-ForPort -Name 'jnpf-gateway' -Port 30000 -Timeout $timeout)) { Invoke-StartupFailed }
|
} else {
|
$bizServices += $entry
|
}
|
}
|
} else {
|
$bizServices = @($Script:TargetServices)
|
}
|
|
if ($bizServices.Count -gt 0) {
|
if (($Script:TargetServices.Count -lt $Script:Services.Count) -and ($bizServices.Count -le 4)) {
|
Write-Host ''
|
Write-Host '── 阶段 2: 启动业务服务 ──'
|
foreach ($entry in $bizServices) {
|
if (-not (Start-JnpfService -Service $entry)) { Invoke-StartupFailed }
|
}
|
Write-Host ''
|
Write-Host "── 阶段 3: 等待业务服务就绪 (最多 ${timeout}s) ──"
|
if (-not (Wait-ServicesReady -Timeout $timeout -Entries $bizServices)) { Invoke-StartupFailed }
|
} else {
|
Write-Host ''
|
Write-Host "── 阶段 2: 分波启动业务服务(CPU 负载闸门 $($env:LOAD_GATE)%)──"
|
if (-not (Start-InWaves -BizServices $bizServices)) { Invoke-StartupFailed }
|
}
|
}
|
|
Write-Host '========================================'
|
Write-Host " 日志目录: $Script:LogDir/<服务名>/startup.log"
|
Write-Host " 跨终端停止: .\start-all.ps1 stop 跨终端查看状态: .\start-all.ps1 status"
|
Write-Host '========================================'
|
|
Wait-Foreground
|
}
|
|
function Show-Help {
|
$scriptName = if ($PSCommandPath) { Split-Path -Leaf $PSCommandPath } else { 'start-all.ps1' }
|
@"
|
|
JNPF 微服务批量启动脚本(Windows)
|
|
用法: .\$scriptName [命令] [选项] [服务名...]
|
|
命令:
|
start 启动服务(默认,前台运行,Ctrl+C 统一停止)
|
stop 停止服务(用于其他终端)
|
restart 停止 → 编译 → 启动(前台)
|
status 查看运行状态
|
help 显示此帮助信息
|
|
选项:
|
--build, -b 启动前先编译项目
|
|
服务名(可选,省略 = 全部):
|
jnpf-gateway jnpf-platform jnpf-biz-common jnpf-eln jnpf-lims jnpf-dms
|
|
注:jnpf-biz-common(30015) 含审计能力。
|
只起 jnpf-lims 而不起它,审计事件会投递失败并落 spool。
|
|
环境变量:
|
仓库根 .env 启动时自动加载全部 KEY=VALUE(可被命令行同名变量覆盖)
|
SERVICE_READY_TIMEOUT 等待业务服务全部就绪的最长秒数(默认 360)
|
JNPF_SERVICE_MAX_HEAP 单服务最大堆(默认 384m)
|
JNPF_PLATFORM_MAX_HEAP platform 最大堆(默认 1536m)
|
LOAD_GATE Windows 下为 CPU 负载闸门 %(默认 85;bash 版为 loadavg)
|
|
示例:
|
.\$scriptName 启动全部(缺失的 JAR 自动编译)
|
.\$scriptName --build 编译后启动全部
|
.\$scriptName restart 全量重启(自动全量编译)
|
.\$scriptName stop 停止全部
|
.\$scriptName status 查看全部状态
|
|
.\$scriptName jnpf-dms 只启动 dms
|
.\$scriptName restart jnpf-dms 只重启 dms(仅编译该模块)
|
.\$scriptName jnpf-lims 只启动 lims
|
.\$scriptName restart jnpf-lims 只重启 lims
|
.\$scriptName stop jnpf-lims 只停 lims
|
.\$scriptName status jnpf-lims 只看 lims 状态
|
.\$scriptName restart jnpf-lims jnpf-platform 多服务一起重启
|
|
"@ | Write-Host
|
}
|
|
# ────────────────────────────────────────────────
|
# 参数解析
|
# ────────────────────────────────────────────────
|
function Invoke-Main {
|
param([string[]]$CliArgs = @())
|
|
Initialize-Environment
|
|
$build = $false
|
$action = ''
|
$targetNames = @()
|
|
$i = 0
|
while ($i -lt $CliArgs.Count) {
|
$arg = $CliArgs[$i]
|
switch -Regex ($arg) {
|
'^(--build|-b)$' { $build = $true; $i++; continue }
|
'^(help|--help|-h)$' { Show-Help; return }
|
'^(start|stop|restart|status)$' {
|
if ($action) { $targetNames += $arg } else { $action = $arg }
|
$i++; continue
|
}
|
default {
|
$targetNames += $arg
|
$i++; continue
|
}
|
}
|
}
|
|
if (-not $action) { $action = 'start' }
|
Resolve-TargetServices -Names $targetNames
|
|
switch ($action) {
|
'restart' {
|
Write-Host '========================================'
|
Write-Host " 重启 JNPF 微服务 (stop → build → start) (本次 $($Script:TargetServices.Count) 个)"
|
Write-Host '========================================'
|
foreach ($entry in $Script:TargetServices) {
|
Stop-JnpfService -Service $entry
|
}
|
Invoke-Build
|
Invoke-DoStart
|
}
|
'start' {
|
if ($build) {
|
Invoke-Build
|
} else {
|
Ensure-TargetJars
|
}
|
Invoke-DoStart
|
}
|
'stop' {
|
Write-Host '========================================'
|
Write-Host " 停止 JNPF 微服务 (本次 $($Script:TargetServices.Count) 个)"
|
Write-Host '========================================'
|
foreach ($entry in $Script:TargetServices) {
|
Stop-JnpfService -Service $entry
|
}
|
Write-Host '========================================'
|
}
|
'status' {
|
Write-Host '========================================'
|
Write-Host " JNPF 微服务状态 (本次 $($Script:TargetServices.Count) 个)"
|
Write-Host '========================================'
|
foreach ($entry in $Script:TargetServices) {
|
Show-JnpfStatus -Service $entry
|
}
|
Write-Host '========================================'
|
}
|
default {
|
Write-Err "未知命令: $action"
|
Write-Host ''
|
Show-Help
|
exit 1
|
}
|
}
|
}
|
|
Invoke-Main -CliArgs @($args)
|