253 lines
9.3 KiB
PowerShell
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 }
|
|
}
|