$ErrorActionPreference = 'Stop' param( [switch]$NoBrowser ) $appRoot = Split-Path -Parent $MyInvocation.MyCommand.Path $productName = 'SSH Manager' $baseUrl = 'http://127.0.0.1:48080' $healthUrl = "$baseUrl/api/auth/health" $localRoot = Join-Path $env:LOCALAPPDATA 'SSHManager' $dataDir = Join-Path $localRoot 'data' $runtimeDir = Join-Path $localRoot 'runtime' $logsDir = Join-Path $localRoot 'logs' $stateFile = Join-Path $runtimeDir 'launcher-state.json' $pidFile = Join-Path $runtimeDir 'app.pid' $logFile = Join-Path $logsDir 'backend.log' $jarPath = Join-Path $appRoot 'ssh-manager.jar' $bundledJavaw = Join-Path $appRoot 'jre\bin\javaw.exe' $bundledJava = Join-Path $appRoot 'jre\bin\java.exe' function Ensure-Directory([string]$path) { if (-not (Test-Path $path)) { New-Item -ItemType Directory -Path $path | Out-Null } } function Test-Healthy { try { $response = Invoke-RestMethod -Uri $healthUrl -Method Get -TimeoutSec 2 return $response.app -eq 'ssh-manager' -and $response.status -eq 'ok' } catch { return $false } } function Get-JavaCommand { if (Test-Path $bundledJavaw) { return $bundledJavaw } if (Test-Path $bundledJava) { return $bundledJava } $javaw = Get-Command javaw.exe -ErrorAction SilentlyContinue if ($javaw) { return $javaw.Source } $java = Get-Command java.exe -ErrorAction SilentlyContinue if ($java) { return $java.Source } throw '未找到可用的 Java 运行时,请重新安装带内置 JRE 的版本。' } function New-State { $jwtBytes = New-Object byte[] 48 [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($jwtBytes) $encBytes = New-Object byte[] 32 [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($encBytes) $state = @{ jwtSecret = [Convert]::ToBase64String($jwtBytes) encryptionKey = [Convert]::ToBase64String($encBytes) createdAt = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } $state | ConvertTo-Json | Set-Content -Path $stateFile -Encoding ASCII return $state } function Get-State { if (-not (Test-Path $stateFile)) { return New-State } $state = Get-Content -Path $stateFile -Raw | ConvertFrom-Json if (-not $state.jwtSecret -or -not $state.encryptionKey) { return New-State } return $state } function Clear-StalePid { if (-not (Test-Path $pidFile)) { return } $rawPid = Get-Content -Path $pidFile -Raw $pidInfo = $null try { $pidInfo = $rawPid | ConvertFrom-Json } catch { $legacyPid = 0 if ([int]::TryParse($rawPid, [ref]$legacyPid)) { $pidInfo = @{ pid = $legacyPid startedAt = 0 } } } if (-not $pidInfo) { Remove-Item -Path $pidFile -Force -ErrorAction SilentlyContinue return } $process = Get-Process -Id ([int]$pidInfo.pid) -ErrorAction SilentlyContinue if (-not $process) { Remove-Item -Path $pidFile -Force -ErrorAction SilentlyContinue return } if ($pidInfo.startedAt -and $process.StartTime) { $processStartedAt = [DateTimeOffset]$process.StartTime if ($processStartedAt.ToUnixTimeMilliseconds() -ne [int64]$pidInfo.startedAt) { Remove-Item -Path $pidFile -Force -ErrorAction SilentlyContinue } } } Ensure-Directory $localRoot Ensure-Directory $dataDir Ensure-Directory $runtimeDir Ensure-Directory $logsDir if (-not (Test-Path $jarPath)) { throw "未找到程序包:$jarPath" } Clear-StalePid if (Test-Healthy) { if (-not $NoBrowser) { Start-Process $baseUrl | Out-Null } exit 0 } $state = Get-State $javaCommand = Get-JavaCommand $arguments = @( '-Dfile.encoding=UTF-8' '-Dserver.address=127.0.0.1' "-Dlogging.file.name=$logFile" "-DDATA_DIR=$dataDir" "-Dsshmanager.jwt-secret=$($state.jwtSecret)" "-Dsshmanager.encryption-key=$($state.encryptionKey)" '-jar' $jarPath ) $process = Start-Process -FilePath $javaCommand -ArgumentList $arguments -WorkingDirectory $appRoot -PassThru $pidInfo = @{ pid = $process.Id startedAt = ([DateTimeOffset]$process.StartTime).ToUnixTimeMilliseconds() } $pidInfo | ConvertTo-Json | Set-Content -Path $pidFile -Encoding ASCII for ($i = 0; $i -lt 45; $i++) { Start-Sleep -Seconds 1 if (Test-Healthy) { if (-not $NoBrowser) { Start-Process $baseUrl | Out-Null } exit 0 } $process.Refresh() if ($process.HasExited) { break } } if (-not $process.HasExited) { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue } throw "SSH Manager 启动失败,请检查日志:$logFile"