mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-02 08:28:18 +00:00
Pass 1 (correctness sweep): - All native invocations (mysql, mysqld init, winget, curl downloads, cargo run, seed curl) now go through Invoke-Native, so a caller capturing the scripts' output can never turn ordinary stderr (warnings, progress) into a terminating NativeCommandError under Stop preference - Provisioning DDL feeds through a temp file + `source` and prints the mysql output when it fails (with the MYSQL_ROOT_PASSWORD hint) - Get-MySqlCount distinguishes "table empty" (0) from "query failed" (-1); bootstrap and doctor no longer misread a failed COUNT as unseeded - diesel_cli that is present but broken (moved libmysql.dll) is detected via an exit-code probe and reinstalled with --force - Shared Add-BuildToolsToSession maps winget install dirs + LIBCLANG_PATH in bootstrap, doctor, and run_backend_dev (cargo run may recompile); the tools check maps PATH before probing so re-runs skip winget entirely - my.ini values quoted (clone paths with spaces); service-start timeouts point at the .devstack log files Pass 2 (live retest matrix, all green on this machine): - Full re-run with output captured in-PowerShell: exit 0, no NativeCommandError - Wiped-state rebuild (mysql-data/my.ini/redis/secrets deleted): re-init, re-provision, 281 migrations replayed, seeds + secrets regenerated, backend booted, demo user recreated via /v1/create_account, doctor 0 fail / 0 warn - Failure paths: seed without backend and doctor without services both die cleanly with actionable messages and exit 1; stop_dev_services frees both ports gracefully
220 lines
8.8 KiB
PowerShell
220 lines
8.8 KiB
PowerShell
# Shared helpers for the Windows dev-stack bootstrap scripts.
|
|
#
|
|
# Dot-source from a sibling script:
|
|
# . (Join-Path $PSScriptRoot "common.ps1")
|
|
#
|
|
# Design: the stack is PORTABLE - MySQL and Redis live under the gitignored
|
|
# .devstack/ directory and run as plain user processes. No admin rights, no
|
|
# Windows services, no system-wide installs (build tools excepted).
|
|
# See _docs/dev_setup_local_stack.md.
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
$script:RootDir = (Resolve-Path (Join-Path $PSScriptRoot "..\..\..")).Path
|
|
|
|
# Canonical local-dev database identity - must match _docs/dev_setup_server.md,
|
|
# the repo-root .env DATABASE_URL, and the server's MYSQL_URL default.
|
|
$script:DevMySqlDb = if ($env:DEV_MYSQL_DB) { $env:DEV_MYSQL_DB } else { "storyteller" }
|
|
$script:DevMySqlUser = if ($env:DEV_MYSQL_USER) { $env:DEV_MYSQL_USER } else { "storyteller" }
|
|
$script:DevMySqlPassword = if ($env:DEV_MYSQL_PASSWORD) { $env:DEV_MYSQL_PASSWORD } else { "password" }
|
|
|
|
$script:DevBackendUrl = if ($env:DEV_BACKEND_URL) { $env:DEV_BACKEND_URL } else { "http://localhost:12345" }
|
|
|
|
# Default demo account (see seed_demo_user.ps1). "demo"/"admin"/"test"/"dev"
|
|
# are on the backend's reserved-usernames list - don't use those.
|
|
$script:DemoUsername = if ($env:DEMO_USERNAME) { $env:DEMO_USERNAME } else { "localdev1" }
|
|
$script:DemoPassword = if ($env:DEMO_PASSWORD) { $env:DEMO_PASSWORD } else { "localdev1pass" }
|
|
$script:DemoEmail = if ($env:DEMO_EMAIL) { $env:DEMO_EMAIL } else { "localdev1@example.com" }
|
|
|
|
# --- Portable service pins -------------------------------------------------
|
|
# MySQL 8.4 LTS (the 8.x series is required; 9.x is unsupported by tooling).
|
|
# cdn.mysql.com hosts only some patch versions; 8.4.11 verified available.
|
|
$script:MySqlVersion = "8.4.11"
|
|
$script:MySqlZipUrl = "https://cdn.mysql.com/Downloads/MySQL-8.4/mysql-$MySqlVersion-winx64.zip"
|
|
|
|
# Redis for Windows (tporadowski build). Old (5.0) but stable and sufficient
|
|
# for the backend's usage (keepalive/TTL/rate-limiter commands). Swap to
|
|
# Memurai or Microsoft Garnet by pointing REDIS_ZIP_URL elsewhere.
|
|
$script:RedisZipUrl = if ($env:REDIS_ZIP_URL) { $env:REDIS_ZIP_URL } else {
|
|
"https://github.com/tporadowski/redis/releases/download/v5.0.14.1/Redis-x64-5.0.14.1.zip"
|
|
}
|
|
|
|
$script:DevStackDir = Join-Path $RootDir ".devstack"
|
|
$script:DownloadsDir = Join-Path $DevStackDir "downloads"
|
|
$script:MySqlBaseDir = Join-Path $DevStackDir "mysql\mysql-$MySqlVersion-winx64"
|
|
$script:MySqlDataDir = Join-Path $DevStackDir "mysql-data"
|
|
$script:MySqlIniPath = Join-Path $DevStackDir "my.ini"
|
|
$script:RedisDir = Join-Path $DevStackDir "redis"
|
|
$script:LogsDir = Join-Path $DevStackDir "logs"
|
|
$script:PidsDir = Join-Path $DevStackDir "pids"
|
|
|
|
$script:SecretsEnvFile = Join-Path $RootDir "crates\service\web\storyteller_web\config\storyteller-web.development-secrets.env"
|
|
|
|
function Write-Log([string]$Message) { Write-Host "[bootstrap] $Message" }
|
|
function Write-Warn2([string]$Message) { Write-Host "[bootstrap] WARNING: $Message" -ForegroundColor Yellow }
|
|
function Die([string]$Message) { Write-Host "[bootstrap] ERROR: $Message" -ForegroundColor Red; exit 1 }
|
|
function Step([string]$Title) { Write-Host ""; Write-Host "=== $Title ===" -ForegroundColor Cyan }
|
|
|
|
function Confirm-Or-Skip([string]$Prompt) {
|
|
if ($script:AssumeYes) { return $true }
|
|
$reply = Read-Host "$Prompt [y/N]"
|
|
return ($reply -match '^[Yy]')
|
|
}
|
|
|
|
function Test-TcpPort([int]$Port) {
|
|
$client = New-Object System.Net.Sockets.TcpClient
|
|
try {
|
|
$async = $client.BeginConnect("127.0.0.1", $Port, $null, $null)
|
|
if (-not $async.AsyncWaitHandle.WaitOne(1500)) { return $false }
|
|
$client.EndConnect($async)
|
|
return $true
|
|
} catch {
|
|
return $false
|
|
} finally {
|
|
$client.Close()
|
|
}
|
|
}
|
|
|
|
# Resolve a client/server exe: prefer the portable install, fall back to PATH.
|
|
function Get-MySqlTool([string]$Name) {
|
|
$portable = Join-Path $MySqlBaseDir "bin\$Name.exe"
|
|
if (Test-Path $portable) { return $portable }
|
|
$cmd = Get-Command $Name -ErrorAction SilentlyContinue
|
|
if ($cmd) { return $cmd.Source }
|
|
return $null
|
|
}
|
|
|
|
function Get-RedisTool([string]$Name) {
|
|
$portable = Join-Path $RedisDir "$Name.exe"
|
|
if (Test-Path $portable) { return $portable }
|
|
$cmd = Get-Command $Name -ErrorAction SilentlyContinue
|
|
if ($cmd) { return $cmd.Source }
|
|
return $null
|
|
}
|
|
|
|
# Run SQL as the app user against the dev database. Returns output lines
|
|
# (stdout and stderr merged via Invoke-Native, so a caller capturing our
|
|
# streams can never turn a mysql warning into a terminating error).
|
|
# Password goes via MYSQL_PWD so it stays off the command line.
|
|
# -Quiet discards output (for probes that are expected to fail, e.g.
|
|
# Test-MySqlApp before provisioning). Check $LASTEXITCODE for success.
|
|
function Invoke-MySqlApp([string]$Sql, [string]$SourceFile, [switch]$Quiet) {
|
|
$mysql = Get-MySqlTool "mysql"
|
|
if (-not $mysql) { Die "mysql client not found (run bootstrap_dev_stack.ps1 first)." }
|
|
$prev = $env:MYSQL_PWD
|
|
$env:MYSQL_PWD = $DevMySqlPassword
|
|
try {
|
|
if ($SourceFile) {
|
|
$forward = $SourceFile -replace '\\', '/'
|
|
$out = Invoke-Native "`"$mysql`" -u $DevMySqlUser -h 127.0.0.1 -D $DevMySqlDb -e `"source $forward`""
|
|
} else {
|
|
$out = Invoke-Native "`"$mysql`" -u $DevMySqlUser -h 127.0.0.1 -D $DevMySqlDb -N -e `"$Sql`""
|
|
}
|
|
if (-not $Quiet) { return $out }
|
|
} finally {
|
|
$env:MYSQL_PWD = $prev
|
|
}
|
|
}
|
|
|
|
# Run a query expected to return a single non-negative integer (e.g. COUNT(*)).
|
|
# Returns -1 if the query failed or produced no numeric line, so callers can
|
|
# distinguish "table is empty" (0) from "query failed" (-1).
|
|
function Get-MySqlCount([string]$Sql) {
|
|
$out = @(Invoke-MySqlApp -Sql $Sql)
|
|
if ($LASTEXITCODE -ne 0) { return -1 }
|
|
$line = $out | Where-Object { "$_" -match '^\d+$' } | Select-Object -First 1
|
|
if ($null -eq $line) { return -1 }
|
|
return [int]"$line"
|
|
}
|
|
|
|
function Test-MySqlApp {
|
|
try {
|
|
Invoke-MySqlApp -Sql "SELECT 1" -Quiet
|
|
return ($LASTEXITCODE -eq 0)
|
|
} catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Test-Redis {
|
|
$cli = Get-RedisTool "redis-cli"
|
|
if (-not $cli) { return $false }
|
|
try {
|
|
$pong = Invoke-Native "`"$cli`" -h 127.0.0.1 ping"
|
|
return ("$pong" -match "PONG")
|
|
} catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Test-Backend {
|
|
try {
|
|
$r = Invoke-WebRequest -Uri "$DevBackendUrl/_status" -UseBasicParsing -TimeoutSec 3
|
|
return ($r.StatusCode -eq 200)
|
|
} catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
# Start a detached background process, remembering its PID for stop_dev_services.
|
|
function Start-DevProcess([string]$Name, [string]$Exe, [string[]]$Arguments) {
|
|
New-Item -ItemType Directory -Force $PidsDir | Out-Null
|
|
New-Item -ItemType Directory -Force $LogsDir | Out-Null
|
|
$out = Join-Path $LogsDir "$Name.out.log"
|
|
$err = Join-Path $LogsDir "$Name.err.log"
|
|
$proc = Start-Process -FilePath $Exe -ArgumentList $Arguments -WindowStyle Hidden `
|
|
-RedirectStandardOutput $out -RedirectStandardError $err -PassThru
|
|
Set-Content -Path (Join-Path $PidsDir "$Name.pid") -Value $proc.Id -Encoding ascii
|
|
return $proc
|
|
}
|
|
|
|
function Wait-ForCondition([scriptblock]$Condition, [int]$TimeoutSeconds, [string]$What) {
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
while ((Get-Date) -lt $deadline) {
|
|
if (& $Condition) { return $true }
|
|
Start-Sleep -Seconds 1
|
|
}
|
|
Die "Timed out after ${TimeoutSeconds}s waiting for: $What"
|
|
}
|
|
|
|
# Run a native command line via cmd so its stderr is merged OUTSIDE PowerShell.
|
|
# PS 5.1 wraps captured native stderr lines in ErrorRecords; combined with our
|
|
# $ErrorActionPreference = "Stop", a harmless warning (e.g. from cargo) would
|
|
# otherwise abort the script whenever a caller captures/redirects our output.
|
|
# Quote embedded paths: Invoke-Native "`"$exe`" --flag value"
|
|
function Invoke-Native([string]$CommandLine) {
|
|
cmd /s /c "$CommandLine 2>&1"
|
|
}
|
|
|
|
# Make freshly-installed tools visible in this session without a new shell.
|
|
function Add-SessionPath([string[]]$Dirs) {
|
|
foreach ($d in $Dirs) {
|
|
if ((Test-Path $d) -and (($env:Path -split ';') -notcontains $d)) {
|
|
$env:Path = "$d;$env:Path"
|
|
}
|
|
}
|
|
}
|
|
|
|
# The portable MySQL's client DLL + import lib, needed to build AND run
|
|
# diesel-cli with the mysql feature.
|
|
function Add-MySqlClientToSession {
|
|
Add-SessionPath @((Join-Path $MySqlBaseDir "bin"), (Join-Path $MySqlBaseDir "lib"))
|
|
}
|
|
|
|
# Native build tools (cmake/perl/nasm/llvm) land in these dirs when installed
|
|
# via winget; a shell opened before the install won't have them on PATH yet.
|
|
# bindgen (used by boring2 / aws-lc-sys) additionally wants LIBCLANG_PATH.
|
|
function Add-BuildToolsToSession {
|
|
Add-SessionPath @(
|
|
"$env:ProgramFiles\CMake\bin",
|
|
"C:\Strawberry\perl\bin",
|
|
"$env:ProgramFiles\NASM",
|
|
"$env:LOCALAPPDATA\bin\NASM",
|
|
"$env:ProgramFiles\LLVM\bin"
|
|
)
|
|
if (-not $env:LIBCLANG_PATH) {
|
|
$clang = Get-Command clang -ErrorAction SilentlyContinue
|
|
if ($clang) { $env:LIBCLANG_PATH = Split-Path $clang.Source }
|
|
}
|
|
}
|