Files
chorus/scripts/chorus-supervisor.ps1

253 lines
9.3 KiB
PowerShell

[CmdletBinding()]
param(
[Parameter(Position = 0)]
[ValidateSet("install", "run-portal", "run-admin", "run-admin-ui")]
[string]$Command = "install",
[string]$SupervisorRoot = "D:\supervisor",
[string]$ConfigFile
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$script:RepoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot ".."))
if ([string]::IsNullOrWhiteSpace($ConfigFile)) {
$ConfigFile = Join-Path $script:RepoRoot "config\local-services.yml"
}
$ConfigFile = [IO.Path]::GetFullPath($ConfigFile)
$SupervisorRoot = [IO.Path]::GetFullPath($SupervisorRoot)
$script:InstallRoot = Join-Path $SupervisorRoot "chorus"
$script:BinRoot = Join-Path $script:InstallRoot "bin"
$script:ProgramsRoot = Join-Path $SupervisorRoot "programs"
$script:LogsRoot = Join-Path $SupervisorRoot "logs"
$script:Resolver = Join-Path $script:BinRoot "chorus-local-config.exe"
function Write-Step {
param([string]$Message)
Write-Host "[chorus-supervisor] $Message"
}
function Get-GoCommand {
if (-not [string]::IsNullOrWhiteSpace($env:CHORUS_GO)) {
$candidate = Resolve-Path -LiteralPath $env:CHORUS_GO -ErrorAction SilentlyContinue
if ($null -eq $candidate -or -not (Test-Path -LiteralPath $candidate.Path -PathType Leaf)) {
throw "CHORUS_GO does not point to an executable file."
}
return $candidate.Path
}
$command = Get-Command go.exe -ErrorAction SilentlyContinue
if ($null -ne $command) {
return $command.Source
}
$portable = Join-Path (Split-Path $script:RepoRoot -Parent) "chorus-tools\go1.26.5\bin\go.exe"
if (Test-Path -LiteralPath $portable -PathType Leaf) {
return $portable
}
throw "Go was not found. Add go.exe to PATH or set CHORUS_GO."
}
function Get-ResolvedConfig {
if (-not (Test-Path -LiteralPath $script:Resolver -PathType Leaf)) {
throw "Supervisor config resolver is missing. Run scripts\install-supervisor.bat first."
}
$output = & $script:Resolver -config $ConfigFile
if ($LASTEXITCODE -ne 0) {
throw "Local services configuration validation failed."
}
try {
return ($output -join [Environment]::NewLine) | ConvertFrom-Json
}
catch {
throw "Local services configuration returned invalid JSON."
}
}
function ConvertTo-Endpoint {
param([string]$Address)
if ($Address -notmatch '^\[?(?<host>[^\]]+)\]?:(?<port>\d+)$') {
throw "Resolved service address is invalid."
}
return [pscustomobject]@{ Host = $Matches.host; Port = [int]$Matches.port }
}
function Invoke-CheckedBuild {
param([string]$Go, [string[]]$Arguments, [string]$Output, [string]$Name)
& $Go @Arguments
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $Output -PathType Leaf)) {
throw "Failed to build $Name."
}
}
function Quote-SupervisorArgument {
param([string]$Value)
if ($Value.Contains('"')) {
throw "Supervisor paths must not contain quotation marks."
}
return '"' + $Value + '"'
}
function ConvertTo-SupervisorPath {
param([string]$Value)
return $Value.Replace('\', '/')
}
function Install-ChorusSupervisor {
$supervisor = Join-Path $SupervisorRoot "supervisord.exe"
$supervisorConfig = Join-Path $SupervisorRoot "supervisord.conf"
if (-not (Test-Path -LiteralPath $supervisor -PathType Leaf)) {
throw "supervisord.exe was not found in $SupervisorRoot."
}
if (-not (Test-Path -LiteralPath $supervisorConfig -PathType Leaf)) {
throw "supervisord.conf was not found in $SupervisorRoot."
}
foreach ($directory in @($script:InstallRoot, $script:BinRoot, $script:ProgramsRoot, $script:LogsRoot)) {
New-Item -ItemType Directory -Path $directory -Force | Out-Null
}
$go = Get-GoCommand
$corepack = Get-Command corepack.cmd -ErrorAction SilentlyContinue
if ($null -eq $corepack) {
throw "Corepack was not found. Install the locked Node toolchain before installing Chorus."
}
$portal = Join-Path $script:BinRoot "chorus-portal.exe"
$admin = Join-Path $script:BinRoot "chorus-admin.exe"
Write-Step "Building Portal, Admin API, and configuration resolver..."
Push-Location $script:RepoRoot
try {
Invoke-CheckedBuild $go @("build", "-o", $portal, "./portal") $portal "Portal"
Invoke-CheckedBuild $go @("build", "-o", $script:Resolver, "./cmd/chorus-local-config") $script:Resolver "configuration resolver"
Invoke-CheckedBuild $go @("-C", "admin", "build", "-o", $admin, ".") $admin "Admin API"
}
finally {
Pop-Location
}
$pnpmProxy = Join-Path $script:BinRoot "pnpm.cmd"
$pnpmProxyBody = '@"' + $corepack.Source + '" pnpm %*' + [Environment]::NewLine
[IO.File]::WriteAllText($pnpmProxy, $pnpmProxyBody, [Text.ASCIIEncoding]::new())
$resolved = Get-ResolvedConfig
$portalEndpoint = ConvertTo-Endpoint $resolved.portal_address
$adminEndpoint = ConvertTo-Endpoint $resolved.admin_address
$adminUIEndpoint = ConvertTo-Endpoint $resolved.admin_ui_address
$runner = ConvertTo-SupervisorPath ([IO.Path]::GetFullPath($PSCommandPath))
$supervisorRootArgument = ConvertTo-SupervisorPath $SupervisorRoot
$configFileArgument = ConvertTo-SupervisorPath $ConfigFile
$repoRootArgument = ConvertTo-SupervisorPath $script:RepoRoot
$logsRootArgument = ConvertTo-SupervisorPath $script:LogsRoot
$commandPrefix = "pwsh.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File $(Quote-SupervisorArgument $runner)"
$commonArguments = "-SupervisorRoot $(Quote-SupervisorArgument $supervisorRootArgument) -ConfigFile $(Quote-SupervisorArgument $configFileArgument)"
$programConfig = @"
[program:chorus-user]
command=$commandPrefix run-portal $commonArguments
directory=$repoRootArgument
autostart=true
autorestart=true
startsecs=3
startretries=3
stopwaitsecs=20
stopasgroup=true
killasgroup=true
redirect_stderr=true
stdout_logfile=$logsRootArgument/chorus-user.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=5
[program:chorus-admin-api]
command=$commandPrefix run-admin $commonArguments
directory=$repoRootArgument/admin
autostart=true
autorestart=true
startsecs=3
startretries=3
stopwaitsecs=20
stopasgroup=true
killasgroup=true
redirect_stderr=true
stdout_logfile=$logsRootArgument/chorus-admin-api.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=5
[program:chorus-admin-ui]
command=$commandPrefix run-admin-ui $commonArguments
directory=$repoRootArgument/admin-ui
autostart=true
autorestart=true
startsecs=3
startretries=3
stopwaitsecs=20
stopasgroup=true
killasgroup=true
redirect_stderr=true
stdout_logfile=$logsRootArgument/chorus-admin-ui.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=5
"@
$programPath = Join-Path $script:ProgramsRoot "chorus.conf"
[IO.File]::WriteAllText($programPath, $programConfig, [Text.UTF8Encoding]::new($false))
$supervisorBody = [IO.File]::ReadAllText($supervisorConfig)
if ($supervisorBody -notmatch '(?m)^\s*\[include\]\s*$') {
$include = "`r`n[include]`r`nfiles=%(here)s/programs/*.conf`r`n"
[IO.File]::AppendAllText($supervisorConfig, $include, [Text.UTF8Encoding]::new($false))
}
elseif ($supervisorBody -notmatch '(?m)^\s*files\s*=.*programs[/\\]\*\.conf') {
throw "The existing [include] section does not load programs/*.conf; update it before installing Chorus."
}
Write-Step "Installed three programs:"
Write-Step "chorus-user -> $($resolved.portal_address)"
Write-Step "chorus-admin-api -> $($resolved.admin_address)"
Write-Step "chorus-admin-ui -> $($resolved.admin_ui_address)"
Write-Step "Reload Supervisor with: D:\supervisor\supervisord.exe ctl /c D:\supervisor\supervisord.conf reload"
}
function Run-Portal {
$resolved = Get-ResolvedConfig
. $resolved.portal_environment_file
if ([string]::IsNullOrWhiteSpace($env:CHORUS_DSN)) {
throw "CHORUS_DSN is required in the portal environment file."
}
$env:CHORUS_LISTEN_ADDRESS = $resolved.portal_address
$executable = Join-Path $script:BinRoot "chorus-portal.exe"
& $executable --config $resolved.portal_settings_file
exit $LASTEXITCODE
}
function Run-Admin {
$resolved = Get-ResolvedConfig
$executable = Join-Path $script:BinRoot "chorus-admin.exe"
& $executable server --config $resolved.admin_settings_file
exit $LASTEXITCODE
}
function Run-AdminUI {
$resolved = Get-ResolvedConfig
$endpoint = ConvertTo-Endpoint $resolved.admin_ui_address
$adminUIRoot = Join-Path $script:RepoRoot "admin-ui"
$entry = Join-Path $adminUIRoot "node_modules\@vue\cli-service\bin\vue-cli-service.js"
if (-not (Test-Path -LiteralPath $entry -PathType Leaf)) {
throw "Admin UI dependencies are missing."
}
$pnpmShim = Join-Path $script:BinRoot "pnpm.cmd"
if (-not (Test-Path -LiteralPath $pnpmShim -PathType Leaf)) {
throw "The installed pnpm proxy is missing. Run scripts\install-supervisor.bat again."
}
$pnpmDirectory = Split-Path -Parent $pnpmShim
if (($env:Path -split ';') -notcontains $pnpmDirectory) {
$env:Path = $pnpmDirectory + ';' + $env:Path
}
$env:VUE_APP_BASE_API = $resolved.admin_ui_api_base
& $pnpmShim --dir $adminUIRoot exec vue-cli-service serve --host $endpoint.Host --port $endpoint.Port
exit $LASTEXITCODE
}
switch ($Command) {
"install" { Install-ChorusSupervisor }
"run-portal" { Run-Portal }
"run-admin" { Run-Admin }
"run-admin-ui" { Run-AdminUI }
}