Files
smartmate/backend/scripts/dev-common.ps1
Losita 61db646805 Version: 0.9.80.dev.260506
后端:
1. LLM 独立服务与统一计费出口落地:新增 `cmd/llm`、`client/llm` 与 `services/llm/rpc`,补齐 BillingContext、CreditBalanceGuard、价格规则解析、stream usage 归集与 `credit.charge.requested` outbox 发布,active-scheduler / agent / course / memory / gateway fallback 全部改走 llm zrpc,不再各自本地初始化模型。
2. TokenStore 收口为 Credit 权威账本:新增 credit account / ledger / product / order / price-rule / reward-rule 能力与 Redis 快照缓存,扩展 tokenstore rpc/client 支撑余额快照、消耗看板、商品、订单、流水、价格规则和奖励规则,并接入 LLM charge 事件消费完成 Credit 扣费落账。
3. 计费旧链路下线与网关切口切换:`/token-store` 语义整体切到 `/credit-store`,agent chat 移除旧 TokenQuotaGuard,userauth 的 CheckTokenQuota / AdjustTokenUsage 改为废弃,聊天历史落库不再同步旧 token 额度账本,course 图片解析请求补 user_id 进入新计费口径。

前端:
4. 计划广场从 mock 数据切到真实接口:新增 forum api/types,首页支持真实列表、标签、搜索、防抖、点赞、导入和发布计划,详情页补齐帖子详情、评论树、回复和删除评论链路,同时补上“至少一个标签”的前后端约束与默认标签兜底。
5. 商店页切到 Credit 体系并重做展示:顶部改为余额 + Credit/Token 消耗看板,支持 24h/7d/30d/all 周期切换;套餐区展示原价与当前价;历史区改为当前用户 Credit 流水并支持查看更多,整体视觉和交互同步收口。

仓库:
6. 配置与本地启动体系补齐 llm / outbox 编排:`config.example.yaml` 增加 llm rpc 和统一 outbox service 配置,`dev-common.ps1` 把 llm 纳入多服务依赖并自动建 Kafka topic,`docker-compose.yml` 同步初始化 agent/task/memory/active-scheduler/notification/taskclass-forum/llm/token-store 全量 outbox topic。
2026-05-06 20:16:53 +08:00

971 lines
28 KiB
PowerShell
Raw Permalink 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.
$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-KafkaTopicDefinitions {
return @(
"smartflow.agent.outbox",
"smartflow.task.outbox",
"smartflow.memory.outbox",
"smartflow.active-scheduler.outbox",
"smartflow.notification.outbox",
"smartflow.taskclass-forum.outbox",
"smartflow.llm.outbox",
"smartflow.token-store.outbox"
)
}
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 = "llm"
Package = "./cmd/llm"
BinaryPath = (Join-Path $BinRoot "llm.exe")
Port = 9096
ProbeType = "tcp"
ProbeTarget = $null
StartTimeoutSec = 120
Dependencies = @()
},
[pscustomobject]@{
Name = "course"
Package = "./cmd/course"
BinaryPath = (Join-Path $BinRoot "course.exe")
Port = 9087
ProbeType = "tcp"
ProbeTarget = $null
StartTimeoutSec = 120
Dependencies = @("llm")
},
[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 = @("llm")
},
[pscustomobject]@{
Name = "taskclassforum"
Aliases = @("forum")
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", "llm")
},
[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", "llm")
},
[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",
"llm",
"tokenstore",
"notification",
"memory",
"taskclassforum",
"active-scheduler",
"agent"
)
}
)
}
function Get-BackendServiceDefinition {
param(
[Parameter(Mandatory = $true)]
[string]$Name
)
$serviceDefinitions = @(Get-BackendServiceDefinitions)
foreach ($service in $serviceDefinitions) {
$aliases = @()
if ($service.PSObject.Properties.Name -contains "Aliases" -and $null -ne $service.Aliases) {
$aliases = @($service.Aliases)
}
if ($service.Name -eq $Name -or $aliases -contains $Name) {
return $service
}
}
$availableNames = foreach ($service in $serviceDefinitions) {
$aliases = @()
if ($service.PSObject.Properties.Name -contains "Aliases" -and $null -ne $service.Aliases) {
$aliases = @($service.Aliases)
}
if ($aliases.Count -gt 0) {
"{0} ({1})" -f $service.Name, ($aliases -join ", ")
}
else {
$service.Name
}
}
throw "Service definition not found: $Name. Available names: $($availableNames -join '; ')"
}
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
}
Ensure-KafkaTopics
}
function Ensure-KafkaTopics {
Assert-ToolExists -Name "docker"
foreach ($topic in (Get-KafkaTopicDefinitions)) {
$arguments = @(
"exec",
"smartflow-kafka",
"/opt/kafka/bin/kafka-topics.sh",
"--bootstrap-server",
"localhost:9092",
"--create",
"--if-not-exists",
"--topic",
$topic,
"--partitions",
"3",
"--replication-factor",
"1"
)
Invoke-ExternalCommand -FilePath "docker" -Arguments $arguments -WorkingDirectory $RepoRoot
}
}
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))"
}
}
}