Version: 0.9.79.dev.260506
后端: 1. 本地后端启动体系收口到 `backend/scripts`,移除 `cmd/all` 聚合入口,并将仓库根兼容启动语义收敛为 `StartAPI` 别名;新增 dev-up / dev-down / services-up / services-down / dev-status / dev-logs / service-restart 脚本,统一托管多服务进程、日志、PID 与基础设施启动。 2. 课表服务超时口径统一放宽到 5 分钟,覆盖 gateway / client / rpc server / config example,避免课表导入与图片识别在长耗时场景下被内层提前截断。 3. `today` 课表查询修正为读取真实当前日期,不再使用硬编码测试日期;同时剔除旧缓存与返回结果里的 `empty` 占位事件,后端只返回真实日程,空档改由前端时间轴自行补齐。 前端: 4. 首页路由切回改为复用 `DashboardView` 实例,补 `keep-alive`、`onActivated` 与双帧缩放重算,修复从侧栏返回首页时首帧布局放大与重复加载闪动问题。 5. 首页加载态与今日时间线口径收口:移除额外 800ms `pageLoading` 人为延迟,task / schedule 改为分开驱动;时间线忽略 `empty` 事件,并统一空档文案为“无课”。 6. 收敛助手页与首页若干进场/弹性动画,降低结果卡片、微调弹窗、思考区与面板切换时的抖动感。 仓库: 7. README 补充后端本地快速启动说明,`.gitignore` 忽略 `backend/.dev` 脚本运行态产物。
This commit is contained in:
900
backend/scripts/dev-common.ps1
Normal file
900
backend/scripts/dev-common.ps1
Normal file
@@ -0,0 +1,900 @@
|
||||
$BackendRoot = Split-Path -Parent $PSScriptRoot
|
||||
$RepoRoot = Split-Path -Parent $BackendRoot
|
||||
$ComposeFile = Join-Path $RepoRoot "docker-compose.yml"
|
||||
$StateRoot = Join-Path $BackendRoot ".dev"
|
||||
$PidRoot = Join-Path $StateRoot "pids"
|
||||
$LogRoot = Join-Path $StateRoot "logs"
|
||||
$BinRoot = Join-Path $StateRoot "bin"
|
||||
$DockerConfigRoot = Join-Path $StateRoot "docker-config"
|
||||
$GoCacheRoot = Join-Path $StateRoot "gocache"
|
||||
|
||||
function Initialize-DevState {
|
||||
foreach ($path in @($StateRoot, $PidRoot, $LogRoot, $BinRoot, $DockerConfigRoot, $GoCacheRoot)) {
|
||||
if (-not (Test-Path -LiteralPath $path)) {
|
||||
New-Item -ItemType Directory -Path $path -Force | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# 1. 当前桌面环境里可能同时注入 Path / PATH 两个大小写不同的环境变量。
|
||||
# 2. Windows PowerShell 的 Env: 提供器与 Start-Process 在这种情况下会报“重复键”错误。
|
||||
# 3. 这里统一只保留一个进程级 Path,避免启动脚本因为环境脏数据直接失败。
|
||||
$effectivePath = ""
|
||||
try {
|
||||
$pathValue = [System.Environment]::GetEnvironmentVariable("Path", "Process")
|
||||
if ($null -ne $pathValue) {
|
||||
$effectivePath = [string]$pathValue
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$effectivePath = ""
|
||||
}
|
||||
[System.Environment]::SetEnvironmentVariable("PATH", $null, "Process")
|
||||
[System.Environment]::SetEnvironmentVariable("Path", $null, "Process")
|
||||
[System.Environment]::SetEnvironmentVariable("Path", $effectivePath, "Process")
|
||||
|
||||
$env:DOCKER_CONFIG = $DockerConfigRoot
|
||||
$env:GOCACHE = $GoCacheRoot
|
||||
}
|
||||
|
||||
function Assert-ToolExists {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
||||
throw "Command not found: $Name"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-ExternalCommand {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FilePath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$Arguments,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$WorkingDirectory
|
||||
)
|
||||
|
||||
Push-Location $WorkingDirectory
|
||||
try {
|
||||
& $FilePath @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Command failed: $FilePath $($Arguments -join ' ')"
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Get-InfrastructureDefinitions {
|
||||
return @(
|
||||
[pscustomobject]@{
|
||||
Name = "mysql"
|
||||
Container = "smartflow-mysql"
|
||||
TimeoutSec = 120
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = "redis"
|
||||
Container = "smartflow-redis"
|
||||
TimeoutSec = 90
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = "kafka"
|
||||
Container = "smartflow-kafka"
|
||||
TimeoutSec = 180
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = "etcd"
|
||||
Container = "smartflow-etcd"
|
||||
TimeoutSec = 120
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = "minio"
|
||||
Container = "smartflow-minio"
|
||||
TimeoutSec = 120
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = "milvus-standalone"
|
||||
Container = "smartflow-milvus"
|
||||
TimeoutSec = 240
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function Get-InfrastructureComposeServices {
|
||||
return @(
|
||||
"mysql",
|
||||
"redis",
|
||||
"kafka",
|
||||
"etcd",
|
||||
"minio",
|
||||
"milvus-standalone",
|
||||
"kafka-init"
|
||||
)
|
||||
}
|
||||
|
||||
function Get-BackendServiceDefinitions {
|
||||
return @(
|
||||
[pscustomobject]@{
|
||||
Name = "userauth"
|
||||
Package = "./cmd/userauth"
|
||||
BinaryPath = (Join-Path $BinRoot "userauth.exe")
|
||||
Port = 9081
|
||||
ProbeType = "tcp"
|
||||
ProbeTarget = $null
|
||||
StartTimeoutSec = 60
|
||||
Dependencies = @()
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = "task"
|
||||
Package = "./cmd/task"
|
||||
BinaryPath = (Join-Path $BinRoot "task.exe")
|
||||
Port = 9085
|
||||
ProbeType = "tcp"
|
||||
ProbeTarget = $null
|
||||
StartTimeoutSec = 90
|
||||
Dependencies = @()
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = "schedule"
|
||||
Package = "./cmd/schedule"
|
||||
BinaryPath = (Join-Path $BinRoot "schedule.exe")
|
||||
Port = 9084
|
||||
ProbeType = "tcp"
|
||||
ProbeTarget = $null
|
||||
StartTimeoutSec = 90
|
||||
Dependencies = @()
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = "task-class"
|
||||
Package = "./cmd/task-class"
|
||||
BinaryPath = (Join-Path $BinRoot "task-class.exe")
|
||||
Port = 9086
|
||||
ProbeType = "tcp"
|
||||
ProbeTarget = $null
|
||||
StartTimeoutSec = 90
|
||||
Dependencies = @()
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = "course"
|
||||
Package = "./cmd/course"
|
||||
BinaryPath = (Join-Path $BinRoot "course.exe")
|
||||
Port = 9087
|
||||
ProbeType = "tcp"
|
||||
ProbeTarget = $null
|
||||
StartTimeoutSec = 120
|
||||
Dependencies = @()
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = "tokenstore"
|
||||
Package = "./cmd/tokenstore"
|
||||
BinaryPath = (Join-Path $BinRoot "tokenstore.exe")
|
||||
Port = 9095
|
||||
ProbeType = "tcp"
|
||||
ProbeTarget = $null
|
||||
StartTimeoutSec = 90
|
||||
Dependencies = @()
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = "notification"
|
||||
Package = "./cmd/notification"
|
||||
BinaryPath = (Join-Path $BinRoot "notification.exe")
|
||||
Port = 9082
|
||||
ProbeType = "tcp"
|
||||
ProbeTarget = $null
|
||||
StartTimeoutSec = 90
|
||||
Dependencies = @()
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = "memory"
|
||||
Package = "./cmd/memory"
|
||||
BinaryPath = (Join-Path $BinRoot "memory.exe")
|
||||
Port = 9088
|
||||
ProbeType = "tcp"
|
||||
ProbeTarget = $null
|
||||
StartTimeoutSec = 150
|
||||
Dependencies = @()
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = "taskclassforum"
|
||||
Package = "./cmd/taskclassforum"
|
||||
BinaryPath = (Join-Path $BinRoot "taskclassforum.exe")
|
||||
Port = 9090
|
||||
ProbeType = "tcp"
|
||||
ProbeTarget = $null
|
||||
StartTimeoutSec = 90
|
||||
Dependencies = @("task-class")
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = "active-scheduler"
|
||||
Package = "./cmd/active-scheduler"
|
||||
BinaryPath = (Join-Path $BinRoot "active-scheduler.exe")
|
||||
Port = 9083
|
||||
ProbeType = "tcp"
|
||||
ProbeTarget = $null
|
||||
StartTimeoutSec = 120
|
||||
Dependencies = @("task", "schedule")
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = "agent"
|
||||
Package = "./cmd/agent"
|
||||
BinaryPath = (Join-Path $BinRoot "agent.exe")
|
||||
Port = 9089
|
||||
ProbeType = "tcp"
|
||||
ProbeTarget = $null
|
||||
StartTimeoutSec = 180
|
||||
Dependencies = @("task", "schedule", "task-class", "memory")
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = "api"
|
||||
Package = "./cmd/api"
|
||||
BinaryPath = (Join-Path $BinRoot "api.exe")
|
||||
Port = 8080
|
||||
ProbeType = "http"
|
||||
ProbeTarget = "http://127.0.0.1:8080/api/v1/health"
|
||||
StartTimeoutSec = 90
|
||||
Dependencies = @(
|
||||
"userauth",
|
||||
"task",
|
||||
"schedule",
|
||||
"task-class",
|
||||
"course",
|
||||
"tokenstore",
|
||||
"notification",
|
||||
"memory",
|
||||
"taskclassforum",
|
||||
"active-scheduler",
|
||||
"agent"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function Get-BackendServiceDefinition {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
foreach ($service in (Get-BackendServiceDefinitions)) {
|
||||
if ($service.Name -eq $Name) {
|
||||
return $service
|
||||
}
|
||||
}
|
||||
|
||||
throw "Service definition not found: $Name"
|
||||
}
|
||||
|
||||
function Get-ServicePidFilePath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Service
|
||||
)
|
||||
|
||||
return (Join-Path $PidRoot "$($Service.Name).json")
|
||||
}
|
||||
|
||||
function Get-ServiceStdoutLogPath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Service
|
||||
)
|
||||
|
||||
return (Join-Path $LogRoot "$($Service.Name).log")
|
||||
}
|
||||
|
||||
function Get-ServiceStderrLogPath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Service
|
||||
)
|
||||
|
||||
return (Join-Path $LogRoot "$($Service.Name).err.log")
|
||||
}
|
||||
|
||||
function New-ServiceLogPaths {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Service
|
||||
)
|
||||
|
||||
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
return [pscustomobject]@{
|
||||
Stdout = (Join-Path $LogRoot "$($Service.Name)-$stamp.log")
|
||||
Stderr = (Join-Path $LogRoot "$($Service.Name)-$stamp.err.log")
|
||||
}
|
||||
}
|
||||
|
||||
function Read-ServicePidInfo {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Service
|
||||
)
|
||||
|
||||
$path = Get-ServicePidFilePath -Service $Service
|
||||
if (-not (Test-Path -LiteralPath $path)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return (Get-Content -LiteralPath $path -Encoding UTF8 | ConvertFrom-Json)
|
||||
}
|
||||
|
||||
function Get-LatestServiceLogPath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Service,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet("stdout", "stderr")]
|
||||
[string]$Stream
|
||||
)
|
||||
|
||||
$pidInfo = Read-ServicePidInfo -Service $Service
|
||||
if ($null -ne $pidInfo) {
|
||||
$managedPath = if ($Stream -eq "stdout") { [string]$pidInfo.StdoutLogPath } else { [string]$pidInfo.StderrLogPath }
|
||||
if (-not [string]::IsNullOrWhiteSpace($managedPath) -and (Test-Path -LiteralPath $managedPath)) {
|
||||
return $managedPath
|
||||
}
|
||||
}
|
||||
|
||||
if ($Stream -eq "stdout") {
|
||||
$candidates = @(Get-ChildItem -LiteralPath $LogRoot -Filter "$($Service.Name)-*.log" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -notlike "*.err.log" } |
|
||||
Sort-Object LastWriteTime -Descending)
|
||||
}
|
||||
else {
|
||||
$candidates = @(Get-ChildItem -LiteralPath $LogRoot -Filter "$($Service.Name)-*.err.log" -ErrorAction SilentlyContinue |
|
||||
Sort-Object LastWriteTime -Descending)
|
||||
}
|
||||
|
||||
if ($null -eq $candidates -or $candidates.Count -eq 0) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return $candidates[0].FullName
|
||||
}
|
||||
|
||||
function Write-ServicePidInfo {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Service,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Diagnostics.Process]$Process,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$StdoutLogPath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$StderrLogPath
|
||||
)
|
||||
|
||||
$payload = [pscustomobject]@{
|
||||
Name = $Service.Name
|
||||
Pid = [int]$Process.Id
|
||||
Port = [int]$Service.Port
|
||||
StartedAt = (Get-Date).ToString("o")
|
||||
BinaryPath = $Service.BinaryPath
|
||||
StdoutLogPath = $StdoutLogPath
|
||||
StderrLogPath = $StderrLogPath
|
||||
}
|
||||
|
||||
$path = Get-ServicePidFilePath -Service $Service
|
||||
$payload | ConvertTo-Json | Set-Content -LiteralPath $path -Encoding UTF8
|
||||
}
|
||||
|
||||
function Remove-ServicePidInfo {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Service
|
||||
)
|
||||
|
||||
$path = Get-ServicePidFilePath -Service $Service
|
||||
if (Test-Path -LiteralPath $path) {
|
||||
Remove-Item -LiteralPath $path -Force
|
||||
}
|
||||
}
|
||||
|
||||
function Test-ProcessAlive {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[int]$ProcessId
|
||||
)
|
||||
|
||||
return ($null -ne (Get-Process -Id $ProcessId -ErrorAction SilentlyContinue))
|
||||
}
|
||||
|
||||
function Get-ListeningProcessId {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[int]$Port
|
||||
)
|
||||
|
||||
try {
|
||||
$connection = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction Stop |
|
||||
Sort-Object -Property OwningProcess |
|
||||
Select-Object -First 1
|
||||
if ($null -ne $connection) {
|
||||
return [int]$connection.OwningProcess
|
||||
}
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
$pattern = "^\s*TCP\s+\S+:$Port\s+\S+\s+LISTENING\s+(\d+)\s*$"
|
||||
foreach ($line in (netstat -ano -p tcp)) {
|
||||
if ($line -match $pattern) {
|
||||
return [int]$matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Test-TcpPort {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[int]$Port,
|
||||
|
||||
[string]$HostName = "127.0.0.1",
|
||||
|
||||
[int]$TimeoutMs = 500
|
||||
)
|
||||
|
||||
$client = New-Object System.Net.Sockets.TcpClient
|
||||
try {
|
||||
$asyncResult = $client.BeginConnect($HostName, $Port, $null, $null)
|
||||
if (-not $asyncResult.AsyncWaitHandle.WaitOne($TimeoutMs, $false)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$client.EndConnect($asyncResult)
|
||||
return $true
|
||||
}
|
||||
catch {
|
||||
return $false
|
||||
}
|
||||
finally {
|
||||
$client.Close()
|
||||
}
|
||||
}
|
||||
|
||||
function Test-HttpEndpoint {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Url,
|
||||
|
||||
[int]$TimeoutSec = 2
|
||||
)
|
||||
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri $Url -Method Get -TimeoutSec $TimeoutSec -UseBasicParsing
|
||||
return ($response.StatusCode -ge 200 -and $response.StatusCode -lt 300)
|
||||
}
|
||||
catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Test-ServiceHealthy {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Service
|
||||
)
|
||||
|
||||
switch ($Service.ProbeType) {
|
||||
"http" {
|
||||
return (Test-HttpEndpoint -Url $Service.ProbeTarget)
|
||||
}
|
||||
default {
|
||||
return (Test-TcpPort -Port $Service.Port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Wait-ServiceReady {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Service,
|
||||
|
||||
[int]$TimeoutSec,
|
||||
|
||||
[int]$ProcessId
|
||||
)
|
||||
|
||||
$effectiveTimeout = $TimeoutSec
|
||||
if ($effectiveTimeout -le 0) {
|
||||
$effectiveTimeout = [int]$Service.StartTimeoutSec
|
||||
}
|
||||
|
||||
$deadline = (Get-Date).AddSeconds($effectiveTimeout)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
if ($ProcessId -gt 0 -and -not (Test-ProcessAlive -ProcessId $ProcessId)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
if (Test-ServiceHealthy -Service $Service) {
|
||||
return $true
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
function Get-ServiceStatus {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Service
|
||||
)
|
||||
|
||||
$pidInfo = Read-ServicePidInfo -Service $Service
|
||||
$managedPid = $null
|
||||
$hasStalePid = $false
|
||||
$stdoutLogPath = Get-ServiceStdoutLogPath -Service $Service
|
||||
$stderrLogPath = Get-ServiceStderrLogPath -Service $Service
|
||||
if ($null -ne $pidInfo) {
|
||||
$managedPid = [int]$pidInfo.Pid
|
||||
if (-not [string]::IsNullOrWhiteSpace($pidInfo.StdoutLogPath)) {
|
||||
$stdoutLogPath = [string]$pidInfo.StdoutLogPath
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($pidInfo.StderrLogPath)) {
|
||||
$stderrLogPath = [string]$pidInfo.StderrLogPath
|
||||
}
|
||||
if (-not (Test-ProcessAlive -ProcessId $managedPid)) {
|
||||
$hasStalePid = $true
|
||||
$managedPid = $null
|
||||
}
|
||||
}
|
||||
|
||||
$healthy = Test-ServiceHealthy -Service $Service
|
||||
$listenerPid = Get-ListeningProcessId -Port $Service.Port
|
||||
|
||||
if ($null -ne $managedPid) {
|
||||
if ($healthy) {
|
||||
return [pscustomobject]@{
|
||||
State = "managed"
|
||||
Summary = "managed-running"
|
||||
Pid = $managedPid
|
||||
Port = $Service.Port
|
||||
HasStalePid = $false
|
||||
StdoutLog = $stdoutLogPath
|
||||
StderrLog = $stderrLogPath
|
||||
}
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
State = "managed_unhealthy"
|
||||
Summary = "managed-not-ready"
|
||||
Pid = $managedPid
|
||||
Port = $Service.Port
|
||||
HasStalePid = $false
|
||||
StdoutLog = $stdoutLogPath
|
||||
StderrLog = $stderrLogPath
|
||||
}
|
||||
}
|
||||
|
||||
if ($healthy) {
|
||||
return [pscustomobject]@{
|
||||
State = "external"
|
||||
Summary = "external-running"
|
||||
Pid = $listenerPid
|
||||
Port = $Service.Port
|
||||
HasStalePid = $hasStalePid
|
||||
StdoutLog = $null
|
||||
StderrLog = $null
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -ne $listenerPid) {
|
||||
return [pscustomobject]@{
|
||||
State = "conflict"
|
||||
Summary = "port-conflict"
|
||||
Pid = $listenerPid
|
||||
Port = $Service.Port
|
||||
HasStalePid = $hasStalePid
|
||||
StdoutLog = $null
|
||||
StderrLog = $null
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasStalePid) {
|
||||
return [pscustomobject]@{
|
||||
State = "stale"
|
||||
Summary = "stale-pid-file"
|
||||
Pid = $null
|
||||
Port = $Service.Port
|
||||
HasStalePid = $true
|
||||
StdoutLog = $stdoutLogPath
|
||||
StderrLog = $stderrLogPath
|
||||
}
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
State = "stopped"
|
||||
Summary = "stopped"
|
||||
Pid = $null
|
||||
Port = $Service.Port
|
||||
HasStalePid = $false
|
||||
StdoutLog = $stdoutLogPath
|
||||
StderrLog = $stderrLogPath
|
||||
}
|
||||
}
|
||||
|
||||
function Build-ServiceBinary {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Service
|
||||
)
|
||||
|
||||
Initialize-DevState
|
||||
Invoke-ExternalCommand -FilePath "go" -Arguments @(
|
||||
"build",
|
||||
"-o",
|
||||
$Service.BinaryPath,
|
||||
$Service.Package
|
||||
) -WorkingDirectory $BackendRoot
|
||||
}
|
||||
|
||||
function Start-ServiceProcess {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Service
|
||||
)
|
||||
|
||||
$logPaths = New-ServiceLogPaths -Service $Service
|
||||
$stdoutLog = $logPaths.Stdout
|
||||
$stderrLog = $logPaths.Stderr
|
||||
|
||||
$process = Start-Process -FilePath $Service.BinaryPath `
|
||||
-WorkingDirectory $BackendRoot `
|
||||
-WindowStyle Hidden `
|
||||
-RedirectStandardOutput $stdoutLog `
|
||||
-RedirectStandardError $stderrLog `
|
||||
-PassThru
|
||||
|
||||
Write-ServicePidInfo -Service $Service -Process $process -StdoutLogPath $stdoutLog -StderrLogPath $stderrLog
|
||||
if (-not (Wait-ServiceReady -Service $Service -TimeoutSec $Service.StartTimeoutSec -ProcessId $process.Id)) {
|
||||
if (Test-ProcessAlive -ProcessId $process.Id) {
|
||||
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
Remove-ServicePidInfo -Service $Service
|
||||
throw "Service start failed: $($Service.Name). Check logs: $stdoutLog and $stderrLog"
|
||||
}
|
||||
|
||||
return $process
|
||||
}
|
||||
|
||||
function Stop-ServiceProcess {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Service
|
||||
)
|
||||
|
||||
$pidInfo = Read-ServicePidInfo -Service $Service
|
||||
if ($null -eq $pidInfo) {
|
||||
return [pscustomobject]@{
|
||||
Name = $Service.Name
|
||||
Action = "skip"
|
||||
Message = "no managed process"
|
||||
}
|
||||
}
|
||||
|
||||
$managedProcessId = [int]$pidInfo.Pid
|
||||
if (Test-ProcessAlive -ProcessId $managedProcessId) {
|
||||
try {
|
||||
Stop-Process -Id $managedProcessId -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
Stop-Process -Id $managedProcessId -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
$deadline = (Get-Date).AddSeconds(15)
|
||||
while ((Get-Date) -lt $deadline -and (Test-ProcessAlive -ProcessId $managedProcessId)) {
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
|
||||
if (Test-ProcessAlive -ProcessId $managedProcessId) {
|
||||
Stop-Process -Id $managedProcessId -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
Remove-ServicePidInfo -Service $Service
|
||||
return [pscustomobject]@{
|
||||
Name = $Service.Name
|
||||
Action = "stopped"
|
||||
Message = "managed process stopped"
|
||||
}
|
||||
}
|
||||
|
||||
function Ensure-ServiceStarted {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Service
|
||||
)
|
||||
|
||||
$status = Get-ServiceStatus -Service $Service
|
||||
if ($status.HasStalePid) {
|
||||
Remove-ServicePidInfo -Service $Service
|
||||
$status = Get-ServiceStatus -Service $Service
|
||||
}
|
||||
|
||||
switch ($status.State) {
|
||||
"managed" {
|
||||
return [pscustomobject]@{
|
||||
Name = $Service.Name
|
||||
Port = $Service.Port
|
||||
Action = "skip"
|
||||
Detail = "managed and healthy"
|
||||
}
|
||||
}
|
||||
"external" {
|
||||
return [pscustomobject]@{
|
||||
Name = $Service.Name
|
||||
Port = $Service.Port
|
||||
Action = "skip"
|
||||
Detail = "external process already healthy"
|
||||
}
|
||||
}
|
||||
"managed_unhealthy" {
|
||||
if (Wait-ServiceReady -Service $Service -TimeoutSec 15 -ProcessId $status.Pid) {
|
||||
return [pscustomobject]@{
|
||||
Name = $Service.Name
|
||||
Port = $Service.Port
|
||||
Action = "skip"
|
||||
Detail = "existing managed process became healthy"
|
||||
}
|
||||
}
|
||||
|
||||
throw "Service $($Service.Name) already has a managed process, but it is still not healthy after waiting. Check log: $($status.StdoutLog)"
|
||||
}
|
||||
"conflict" {
|
||||
throw "Service $($Service.Name) port $($Service.Port) is occupied by process $($status.Pid), but the health check failed. Resolve the conflict first."
|
||||
}
|
||||
}
|
||||
|
||||
Assert-ServiceDependenciesReady -Service $Service
|
||||
Build-ServiceBinary -Service $Service
|
||||
$process = Start-ServiceProcess -Service $Service
|
||||
return [pscustomobject]@{
|
||||
Name = $Service.Name
|
||||
Port = $Service.Port
|
||||
Action = "start"
|
||||
Detail = "started, PID=$($process.Id)"
|
||||
}
|
||||
}
|
||||
|
||||
function Restart-BackendService {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Service
|
||||
)
|
||||
|
||||
$status = Get-ServiceStatus -Service $Service
|
||||
if ($status.HasStalePid) {
|
||||
Remove-ServicePidInfo -Service $Service
|
||||
$status = Get-ServiceStatus -Service $Service
|
||||
}
|
||||
|
||||
switch ($status.State) {
|
||||
"external" {
|
||||
throw "Service $($Service.Name) is managed by an external process. Refuse to restart it automatically."
|
||||
}
|
||||
"conflict" {
|
||||
throw "Service $($Service.Name) port $($Service.Port) is occupied by process $($status.Pid), but the health check failed. Resolve the conflict first."
|
||||
}
|
||||
"managed" {
|
||||
Stop-ServiceProcess -Service $Service | Out-Null
|
||||
}
|
||||
"managed_unhealthy" {
|
||||
Stop-ServiceProcess -Service $Service | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
Assert-ServiceDependenciesReady -Service $Service
|
||||
Build-ServiceBinary -Service $Service
|
||||
$process = Start-ServiceProcess -Service $Service
|
||||
return [pscustomobject]@{
|
||||
Name = $Service.Name
|
||||
Port = $Service.Port
|
||||
Action = "restart"
|
||||
Detail = "restarted, PID=$($process.Id)"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ContainerStatus {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ContainerName
|
||||
)
|
||||
|
||||
$status = cmd.exe /d /c "docker inspect --format ""{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}"" $ContainerName 2>nul"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
return "unavailable"
|
||||
}
|
||||
|
||||
return (($status | Select-Object -First 1).Trim())
|
||||
}
|
||||
|
||||
function Wait-ContainerStatus {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$ContainerDefinition
|
||||
)
|
||||
|
||||
$deadline = (Get-Date).AddSeconds([int]$ContainerDefinition.TimeoutSec)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
$status = Get-ContainerStatus -ContainerName $ContainerDefinition.Container
|
||||
if ($status -eq "healthy") {
|
||||
return
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
|
||||
throw "Container did not become healthy in time: $($ContainerDefinition.Name)"
|
||||
}
|
||||
|
||||
function Start-BackendInfrastructure {
|
||||
Assert-ToolExists -Name "docker"
|
||||
if (-not (Test-Path -LiteralPath $ComposeFile)) {
|
||||
throw "docker-compose.yml not found: $ComposeFile"
|
||||
}
|
||||
|
||||
$composeServices = Get-InfrastructureComposeServices
|
||||
Invoke-ExternalCommand -FilePath "docker" -Arguments (@(
|
||||
"compose",
|
||||
"-f",
|
||||
$ComposeFile,
|
||||
"up",
|
||||
"-d"
|
||||
) + $composeServices) -WorkingDirectory $RepoRoot
|
||||
|
||||
foreach ($definition in (Get-InfrastructureDefinitions)) {
|
||||
Wait-ContainerStatus -ContainerDefinition $definition
|
||||
}
|
||||
}
|
||||
|
||||
function Stop-BackendInfrastructure {
|
||||
Assert-ToolExists -Name "docker"
|
||||
$composeServices = Get-InfrastructureComposeServices
|
||||
Invoke-ExternalCommand -FilePath "docker" -Arguments (@(
|
||||
"compose",
|
||||
"-f",
|
||||
$ComposeFile,
|
||||
"stop"
|
||||
) + $composeServices) -WorkingDirectory $RepoRoot
|
||||
}
|
||||
|
||||
function Get-InfrastructureStatus {
|
||||
$rows = @()
|
||||
foreach ($definition in (Get-InfrastructureDefinitions)) {
|
||||
$rows += [pscustomobject]@{
|
||||
Name = $definition.Name
|
||||
Container = $definition.Container
|
||||
Status = (Get-ContainerStatus -ContainerName $definition.Container)
|
||||
}
|
||||
}
|
||||
return $rows
|
||||
}
|
||||
|
||||
function Assert-ServiceDependenciesReady {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Service
|
||||
)
|
||||
|
||||
foreach ($dependencyName in $Service.Dependencies) {
|
||||
$dependency = Get-BackendServiceDefinition -Name $dependencyName
|
||||
$status = Get-ServiceStatus -Service $dependency
|
||||
if ($status.State -notin @("managed", "external")) {
|
||||
throw "Dependency not ready for service $($Service.Name): $dependencyName (current state: $($status.Summary))"
|
||||
}
|
||||
}
|
||||
}
|
||||
29
backend/scripts/dev-down.ps1
Normal file
29
backend/scripts/dev-down.ps1
Normal file
@@ -0,0 +1,29 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$StopInfra
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
. "$PSScriptRoot\dev-common.ps1"
|
||||
|
||||
Initialize-DevState
|
||||
|
||||
$services = @(Get-BackendServiceDefinitions)
|
||||
[array]::Reverse($services)
|
||||
|
||||
$results = @()
|
||||
foreach ($service in $services) {
|
||||
Write-Host "==> Stop service: $($service.Name)"
|
||||
$results += Stop-ServiceProcess -Service $service
|
||||
}
|
||||
|
||||
if ($StopInfra) {
|
||||
Write-Host "==> Stop infrastructure containers"
|
||||
Stop-BackendInfrastructure
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Backend stop summary:"
|
||||
$results | Format-Table -AutoSize
|
||||
67
backend/scripts/dev-logs.ps1
Normal file
67
backend/scripts/dev-logs.ps1
Normal file
@@ -0,0 +1,67 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Service,
|
||||
|
||||
[ValidateSet("stdout", "stderr", "both")]
|
||||
[string]$Stream = "stdout",
|
||||
|
||||
[int]$Tail = 80,
|
||||
|
||||
[switch]$Follow
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
. "$PSScriptRoot\dev-common.ps1"
|
||||
|
||||
Initialize-DevState
|
||||
|
||||
if ($Tail -le 0) {
|
||||
throw "Tail must be greater than 0"
|
||||
}
|
||||
|
||||
$serviceDef = Get-BackendServiceDefinition -Name $Service
|
||||
$streams = @(
|
||||
if ($Stream -eq "both") {
|
||||
"stdout"
|
||||
"stderr"
|
||||
}
|
||||
else {
|
||||
$Stream
|
||||
}
|
||||
)
|
||||
|
||||
if ($Follow -and $streams.Count -gt 1) {
|
||||
throw "Follow mode only supports a single stream. Use -Stream stdout or -Stream stderr."
|
||||
}
|
||||
|
||||
$paths = @()
|
||||
foreach ($selectedStream in $streams) {
|
||||
$path = Get-LatestServiceLogPath -Service $serviceDef -Stream $selectedStream
|
||||
if ([string]::IsNullOrWhiteSpace($path)) {
|
||||
throw "No log file found for service $Service stream $selectedStream"
|
||||
}
|
||||
|
||||
$paths += [pscustomobject]@{
|
||||
Stream = $selectedStream
|
||||
Path = $path
|
||||
}
|
||||
}
|
||||
$paths = @($paths)
|
||||
|
||||
for ($index = 0; $index -lt $paths.Count; $index++) {
|
||||
$entry = $paths[$index]
|
||||
Write-Host "==> $Service [$($entry.Stream)]"
|
||||
Write-Host "Path: $($entry.Path)"
|
||||
if ($Follow) {
|
||||
Get-Content -LiteralPath $entry.Path -Encoding UTF8 -Tail $Tail -Wait
|
||||
return
|
||||
}
|
||||
|
||||
Get-Content -LiteralPath $entry.Path -Encoding UTF8 -Tail $Tail
|
||||
if ($paths.Count -gt 1 -and $index -lt ($paths.Count - 1)) {
|
||||
Write-Host ""
|
||||
}
|
||||
}
|
||||
32
backend/scripts/dev-status.ps1
Normal file
32
backend/scripts/dev-status.ps1
Normal file
@@ -0,0 +1,32 @@
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
. "$PSScriptRoot\dev-common.ps1"
|
||||
|
||||
Initialize-DevState
|
||||
|
||||
$serviceRows = foreach ($service in (Get-BackendServiceDefinitions)) {
|
||||
$status = Get-ServiceStatus -Service $service
|
||||
[pscustomobject]@{
|
||||
Name = $service.Name
|
||||
Port = $service.Port
|
||||
Status = $status.Summary
|
||||
PID = $(if ($null -ne $status.Pid) { $status.Pid } else { "-" })
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Backend service status:"
|
||||
$serviceRows | Format-Table -AutoSize
|
||||
|
||||
if (Get-Command docker -ErrorAction SilentlyContinue) {
|
||||
Write-Host ""
|
||||
Write-Host "Infrastructure status:"
|
||||
Get-InfrastructureStatus | Format-Table -AutoSize
|
||||
}
|
||||
else {
|
||||
Write-Host ""
|
||||
Write-Host "Infrastructure status: docker command not found"
|
||||
}
|
||||
27
backend/scripts/dev-up.ps1
Normal file
27
backend/scripts/dev-up.ps1
Normal file
@@ -0,0 +1,27 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$SkipInfra
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
. "$PSScriptRoot\dev-common.ps1"
|
||||
|
||||
Initialize-DevState
|
||||
Assert-ToolExists -Name "go"
|
||||
|
||||
if (-not $SkipInfra) {
|
||||
Write-Host "==> Start infrastructure and wait for health checks"
|
||||
Start-BackendInfrastructure
|
||||
}
|
||||
|
||||
$results = @()
|
||||
:serviceLoop foreach ($service in (Get-BackendServiceDefinitions)) {
|
||||
Write-Host "==> Process service: $($service.Name)"
|
||||
$results += Ensure-ServiceStarted -Service $service
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Backend start summary:"
|
||||
$results | Format-Table -AutoSize
|
||||
22
backend/scripts/service-restart.ps1
Normal file
22
backend/scripts/service-restart.ps1
Normal file
@@ -0,0 +1,22 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Service
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
. "$PSScriptRoot\dev-common.ps1"
|
||||
|
||||
Initialize-DevState
|
||||
Assert-ToolExists -Name "go"
|
||||
|
||||
$serviceDef = Get-BackendServiceDefinition -Name $Service
|
||||
|
||||
Write-Host "==> Restart service: $($serviceDef.Name)"
|
||||
$result = Restart-BackendService -Service $serviceDef
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Service restart summary:"
|
||||
@($result) | Format-Table -AutoSize
|
||||
7
backend/scripts/services-down.ps1
Normal file
7
backend/scripts/services-down.ps1
Normal file
@@ -0,0 +1,7 @@
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
& (Join-Path $PSScriptRoot "dev-down.ps1")
|
||||
7
backend/scripts/services-up.ps1
Normal file
7
backend/scripts/services-up.ps1
Normal file
@@ -0,0 +1,7 @@
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
& (Join-Path $PSScriptRoot "dev-up.ps1") -SkipInfra
|
||||
Reference in New Issue
Block a user