Files
gpt-register-oss/dev_services.ps1
2026-04-05 10:23:02 +08:00

554 lines
13 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
param(
[string]$Action = "",
[string]$Service = "",
[switch]$Background
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$ScriptPath = $MyInvocation.MyCommand.Path
$ProjectRoot = Split-Path -Parent $ScriptPath
$RuntimeDir = Join-Path $ProjectRoot "logs/dev-services"
$PidDir = Join-Path $RuntimeDir "pids"
$Services = @("backend", "frontend")
function Ensure-RuntimeDirectories {
New-Item -ItemType Directory -Force -Path $RuntimeDir | Out-Null
New-Item -ItemType Directory -Force -Path $PidDir | Out-Null
}
function Show-Usage {
@"
:
.\dev_services.ps1 fg Ctrl+C
.\dev_services.ps1 bg
.\dev_services.ps1 stop
.\dev_services.ps1 restart
.\dev_services.ps1 status
:
- : logs/dev-services/
- PID : logs/dev-services/pids/
"@
}
function Get-ServiceTitle {
param([Parameter(Mandatory = $true)][string]$Name)
switch ($Name) {
"backend" { return "backend" }
"frontend" { return "frontend" }
default { return $Name }
}
}
function Get-ServiceLogFile {
param([Parameter(Mandatory = $true)][string]$Name)
Join-Path $RuntimeDir ("{0}.log" -f $Name)
}
function Get-ServicePidFile {
param([Parameter(Mandatory = $true)][string]$Name)
Join-Path $PidDir ("{0}.pid" -f $Name)
}
function Resolve-CommandPath {
param([Parameter(Mandatory = $true)][string[]]$Candidates)
foreach ($candidate in $Candidates) {
if ([System.IO.Path]::IsPathRooted($candidate) -and (Test-Path -LiteralPath $candidate)) {
return $candidate
}
$command = Get-Command -Name $candidate -ErrorAction SilentlyContinue
if ($null -ne $command) {
return $command.Source
}
}
return $null
}
function Get-HostPowerShell {
$currentPath = (Get-Process -Id $PID).Path
if ([string]::IsNullOrWhiteSpace($currentPath)) {
$currentPath = Resolve-CommandPath @("powershell.exe", "pwsh.exe")
}
if ([string]::IsNullOrWhiteSpace($currentPath)) {
throw "找不到当前 PowerShell 可执行文件。"
}
$fileName = [System.IO.Path]::GetFileName($currentPath)
if ($fileName -ieq "pwsh.exe") {
return [pscustomobject]@{
Executable = $currentPath
Arguments = @("-NoProfile", "-File")
}
}
return [pscustomobject]@{
Executable = $currentPath
Arguments = @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File")
}
}
function Get-ServiceDefinition {
param([Parameter(Mandatory = $true)][string]$Name)
$frontendRoot = Join-Path $ProjectRoot "frontend"
switch ($Name) {
"backend" {
$pythonExe = Join-Path $ProjectRoot ".venv\Scripts\python.exe"
return [pscustomobject]@{
Name = $Name
Executable = $pythonExe
Arguments = @("api_server.py")
WorkingDirectory = $ProjectRoot
}
}
"frontend" {
$viteCmd = Join-Path $frontendRoot "node_modules\.bin\vite.cmd"
if (Test-Path -LiteralPath $viteCmd) {
return [pscustomobject]@{
Name = $Name
Executable = $viteCmd
Arguments = @()
WorkingDirectory = $frontendRoot
}
}
$pnpmExe = Resolve-CommandPath @("pnpm.cmd", "pnpm.exe", "pnpm")
if ([string]::IsNullOrWhiteSpace($pnpmExe)) {
throw "缺少前端启动命令: pnpm"
}
return [pscustomobject]@{
Name = $Name
Executable = $pnpmExe
Arguments = @("run", "dev")
WorkingDirectory = $frontendRoot
}
}
default {
throw ("未知服务: {0}" -f $Name)
}
}
}
function Format-ServiceCommand {
param([Parameter(Mandatory = $true)]$Definition)
$parts = @($Definition.Executable) + @($Definition.Arguments)
($parts | ForEach-Object {
if ($_ -match "\s") {
'"{0}"' -f $_
}
else {
$_
}
}) -join " "
}
function Require-Dependencies {
$null = Get-HostPowerShell
$backendPython = Join-Path $ProjectRoot ".venv\Scripts\python.exe"
if (-not (Test-Path -LiteralPath $backendPython)) {
throw ("缺少 Python 解释器: {0}" -f $backendPython)
}
$frontendRoot = Join-Path $ProjectRoot "frontend"
if (-not (Test-Path -LiteralPath $frontendRoot)) {
throw ("缺少前端目录: {0}" -f $frontendRoot)
}
$null = Get-ServiceDefinition "frontend"
}
function Test-PidRunning {
param([Parameter(Mandatory = $true)][int]$ProcessId)
$null -ne (Get-Process -Id $ProcessId -ErrorAction SilentlyContinue)
}
function Get-ServicePid {
param([Parameter(Mandatory = $true)][string]$Name)
$pidFile = Get-ServicePidFile $Name
if (-not (Test-Path -LiteralPath $pidFile)) {
return $null
}
$rawPid = (Get-Content -LiteralPath $pidFile -Raw).Trim()
if ([string]::IsNullOrWhiteSpace($rawPid)) {
return $null
}
try {
return [int]$rawPid
}
catch {
return $null
}
}
function Clear-StalePid {
param([Parameter(Mandatory = $true)][string]$Name)
$pidFile = Get-ServicePidFile $Name
if (-not (Test-Path -LiteralPath $pidFile)) {
return
}
$servicePid = Get-ServicePid $Name
if ($null -eq $servicePid -or -not (Test-PidRunning $servicePid)) {
Remove-Item -LiteralPath $pidFile -Force -ErrorAction SilentlyContinue
}
}
function Test-ServiceRunning {
param([Parameter(Mandatory = $true)][string]$Name)
$servicePid = Get-ServicePid $Name
if ($null -eq $servicePid) {
return $false
}
Test-PidRunning $servicePid
}
function Stop-ProcessTree {
param([Parameter(Mandatory = $true)][int]$ProcessId)
if (-not (Test-PidRunning $ProcessId)) {
return
}
& taskkill /PID $ProcessId /T /F | Out-Null
}
function Stop-ServiceProcess {
param([Parameter(Mandatory = $true)][string]$Name)
Clear-StalePid $Name
$servicePid = Get-ServicePid $Name
if ($null -eq $servicePid) {
return
}
if (-not (Test-PidRunning $servicePid)) {
Remove-Item -LiteralPath (Get-ServicePidFile $Name) -Force -ErrorAction SilentlyContinue
return
}
Write-Host ("停止 {0,-12} pid={1}" -f (Get-ServiceTitle $Name), $servicePid)
Stop-ProcessTree $servicePid
$deadline = (Get-Date).AddSeconds(10)
while ((Get-Date) -lt $deadline) {
if (-not (Test-PidRunning $servicePid)) {
break
}
Start-Sleep -Milliseconds 250
}
Remove-Item -LiteralPath (Get-ServicePidFile $Name) -Force -ErrorAction SilentlyContinue
}
function Ensure-NoManagedServicesRunning {
$busy = $false
foreach ($service in $Services) {
Clear-StalePid $service
if (Test-ServiceRunning $service) {
$servicePid = Get-ServicePid $service
Write-Error ("{0,-12} 已在运行 pid={1},请先执行 .\dev_services.ps1 stop" -f (Get-ServiceTitle $service), $servicePid)
$busy = $true
}
}
if ($busy) {
throw "已有托管服务正在运行。"
}
}
function Write-LogHeader {
param(
[Parameter(Mandatory = $true)][string]$Name,
[Parameter(Mandatory = $true)]$Definition
)
$logFile = Get-ServiceLogFile $Name
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Add-Content -Path $logFile -Value "" -Encoding UTF8
Add-Content -Path $logFile -Value ("[{0}] starting {1}" -f $timestamp, (Get-ServiceTitle $Name)) -Encoding UTF8
Add-Content -Path $logFile -Value ("[{0}] command: {1}" -f $timestamp, (Format-ServiceCommand $Definition)) -Encoding UTF8
}
function Convert-CommandOutput {
param($Value)
if ($null -eq $Value) {
return $null
}
if ($Value -is [System.Management.Automation.ErrorRecord]) {
return $Value.ToString()
}
return [string]$Value
}
function Run-ServiceProcess {
param(
[Parameter(Mandatory = $true)][string]$Name,
[switch]$BackgroundMode
)
Ensure-RuntimeDirectories
$definition = Get-ServiceDefinition $Name
$logFile = Get-ServiceLogFile $Name
if (-not $BackgroundMode) {
Set-Content -Path $logFile -Value $null -Encoding UTF8
}
Write-LogHeader -Name $Name -Definition $definition
Set-Location -LiteralPath $definition.WorkingDirectory
try {
& $definition.Executable @($definition.Arguments) 2>&1 |
ForEach-Object {
$line = Convert-CommandOutput $_
if ($null -eq $line) {
return
}
Add-Content -Path $logFile -Value $line -Encoding UTF8
if (-not $BackgroundMode) {
Write-Host ("[{0}] {1}" -f (Get-ServiceTitle $Name), $line)
}
}
if ($null -ne $LASTEXITCODE) {
exit ([int]$LASTEXITCODE)
}
exit 0
}
catch {
$message = $_.Exception.Message
Add-Content -Path $logFile -Value $message -Encoding UTF8
if (-not $BackgroundMode) {
Write-Host ("[{0}] {1}" -f (Get-ServiceTitle $Name), $message)
}
exit 1
}
}
function Start-ServiceBackground {
param([Parameter(Mandatory = $true)][string]$Name)
$hostPowerShell = Get-HostPowerShell
$logFile = Get-ServiceLogFile $Name
Set-Content -Path $logFile -Value $null -Encoding UTF8
$arguments = @($hostPowerShell.Arguments + @($ScriptPath, "__runservice", $Name, "-Background"))
$process = Start-Process -FilePath $hostPowerShell.Executable -ArgumentList $arguments -WorkingDirectory $ProjectRoot -WindowStyle Hidden -PassThru
Set-Content -Path (Get-ServicePidFile $Name) -Value $process.Id -Encoding ASCII
Start-Sleep -Seconds 1
if (Test-PidRunning $process.Id) {
Write-Host ("启动 {0,-12} 成功 pid={1} log={2}" -f (Get-ServiceTitle $Name), $process.Id, $logFile)
return
}
Write-Error ("启动 {0} 失败,最近日志:" -f (Get-ServiceTitle $Name))
if (Test-Path -LiteralPath $logFile) {
Get-Content -Path $logFile -Tail 20 | ForEach-Object { Write-Host $_ }
}
Remove-Item -LiteralPath (Get-ServicePidFile $Name) -Force -ErrorAction SilentlyContinue
throw ("启动 {0} 失败。" -f (Get-ServiceTitle $Name))
}
function Start-Background {
Ensure-RuntimeDirectories
Require-Dependencies
Ensure-NoManagedServicesRunning
$started = @()
try {
foreach ($service in $Services) {
Start-ServiceBackground $service
$started += $service
}
}
catch {
foreach ($startedService in $started) {
Stop-ServiceProcess $startedService
}
throw
}
Write-Host ""
Write-Host "后台服务已启动。"
Write-Host "停止命令: .\dev_services.ps1 stop"
Write-Host "状态命令: .\dev_services.ps1 status"
}
function Show-Status {
Ensure-RuntimeDirectories
foreach ($service in $Services) {
Clear-StalePid $service
$title = Get-ServiceTitle $service
$logFile = Get-ServiceLogFile $service
if (Test-ServiceRunning $service) {
$servicePid = Get-ServicePid $service
Write-Host ("{0,-12} running pid={1,-8} log={2}" -f $title, $servicePid, $logFile)
}
else {
Write-Host ("{0,-12} stopped pid={1,-8} log={2}" -f $title, "-", $logFile)
}
}
}
function Stop-Background {
Ensure-RuntimeDirectories
foreach ($service in $Services) {
Stop-ServiceProcess $service
}
}
function Stop-ForegroundProcesses {
param([Parameter(Mandatory = $true)][object[]]$ManagedProcesses)
if ($ManagedProcesses.Count -eq 0) {
return
}
Write-Host ""
Write-Host "正在关闭前台服务..."
foreach ($managed in $ManagedProcesses) {
$process = $managed.Process
if ($null -eq $process) {
continue
}
$process.Refresh()
if ($process.HasExited) {
continue
}
Stop-ProcessTree $process.Id
}
}
function Start-ForegroundService {
param([Parameter(Mandatory = $true)][string]$Name)
$hostPowerShell = Get-HostPowerShell
$arguments = @($hostPowerShell.Arguments + @($ScriptPath, "__runservice", $Name))
Write-Host ("启动 {0,-12} 前台模式" -f (Get-ServiceTitle $Name))
$process = Start-Process -FilePath $hostPowerShell.Executable -ArgumentList $arguments -WorkingDirectory $ProjectRoot -NoNewWindow -PassThru
[pscustomobject]@{
Service = $Name
Process = $process
}
}
function Start-Foreground {
Ensure-RuntimeDirectories
Require-Dependencies
Ensure-NoManagedServicesRunning
$managedProcesses = @()
try {
foreach ($service in $Services) {
$managedProcesses += Start-ForegroundService $service
}
Write-Host ""
Write-Host "两个服务已进入前台托管模式。按 Ctrl+C 可一键关闭。"
while ($true) {
foreach ($managed in $managedProcesses) {
$process = $managed.Process
$process.Refresh()
if (-not $process.HasExited) {
continue
}
$exitCode = $process.ExitCode
Write-Host ""
Write-Host ("{0} 已退出,退出码={1},其余服务也会一并关闭。" -f (Get-ServiceTitle $managed.Service), $exitCode)
return $exitCode
}
Start-Sleep -Seconds 1
}
}
finally {
Stop-ForegroundProcesses $managedProcesses
}
}
Ensure-RuntimeDirectories
switch ($Action) {
"__runservice" {
Run-ServiceProcess -Name $Service -BackgroundMode:$Background
break
}
"fg" {
exit (Start-Foreground)
}
"bg" {
Start-Background
break
}
"stop" {
Stop-Background
break
}
"restart" {
Stop-Background
Start-Background
break
}
"status" {
Show-Status
break
}
"help" {
Show-Usage
break
}
"" {
Show-Usage
break
}
default {
Write-Error ("未知命令: {0}" -f $Action)
Write-Host ""
Show-Usage
exit 1
}
}