Dev infra: Windows-first bootstrap - portable MySQL/Redis stack, no WSL required

Adds script/bootstrap/windows/*.ps1 (PowerShell 5.1-compatible) as the primary
bootstrap path, validated end-to-end on a real Windows 10 machine:

- Portable stack under gitignored .devstack/: MySQL 8.4.11 (official
  cdn.mysql.com zip, --initialize-insecure, bound to 127.0.0.1) and Redis
  5.0.14.1 (tporadowski build, REDIS_ZIP_URL-overridable) run as plain user
  processes - no admin, no services, no WSL, no Docker
- bootstrap_dev_stack.ps1: winget-installs missing build tools (Strawberry
  Perl, NASM, LLVM; wreq's BoringSSL + aws-lc-sys need them), compiles
  diesel_cli against the portable MySQL's own client lib
  (MYSQLCLIENT_LIB_DIR), runs all 281 migrations, seeds roles/badges
  idempotently, generates the dummy secrets env (plus TEMP_DIR for Windows),
  builds storyteller-web, installs frontend deps
- start/stop_dev_services.ps1, run_backend_dev.ps1, seed_demo_user.ps1,
  dev_stack_doctor.ps1 mirror the bash variants
- Invoke-Native (cmd /c ... 2>&1) wraps long-running native calls so PS 5.1
  cannot turn harmless stderr warnings into terminating NativeCommandErrors
  when output is captured; my.ini uses forward slashes (MySQL escape rules)

Verified on this machine: 281/281 migrations applied, backend built (3m05s)
and booted on :12345 with dummy secrets, POST /v1/create_account +
/v1/login round-trip succeeded, webapp dev server served on :4201, doctor
reports 0 failures. The bash scripts remain the Linux/WSL2/CI path.
This commit is contained in:
Tanish Manku
2026-07-28 21:23:06 -07:00
parent 537409eb3e
commit 345cbe766a
10 changed files with 1034 additions and 6 deletions
+3
View File
@@ -104,3 +104,6 @@ windows/sqlite/sqlite3.lib
crates/desktop/src-tauri/*.dll
windows_dlls/
# Portable local dev stack (script/bootstrap/windows) - MySQL/Redis binaries, data, logs
.devstack/
+35 -3
View File
@@ -5,7 +5,19 @@ How to run the full ArtCraft dev stack **entirely locally** — the
to production servers. Scope: webapp-first; the Tauri desktop app is a later
phase.
The automated path is:
The automated path on **Windows** (primary platform, native — no WSL, no
Docker, no admin rights for the stack itself):
```powershell
.\script\bootstrap\windows\bootstrap_dev_stack.ps1 # one-time setup, idempotent, re-run anytime
.\script\bootstrap\windows\run_backend_dev.ps1 # terminal 1: API on http://localhost:12345
.\script\bootstrap\windows\seed_demo_user.ps1 # terminal 2: create demo login (localdev1 / localdev1pass)
cd frontend; npx nx dev artcraft-webapp # terminal 2: webapp on http://localhost:4201
.\script\bootstrap\windows\dev_stack_doctor.ps1 # health check, any time
.\script\bootstrap\windows\stop_dev_services.ps1 # stop MySQL/Redis when done
```
The equivalent on **Linux** (Ubuntu 22.04+ — native, WSL2, or CI):
```bash
./script/bootstrap/bootstrap_dev_stack.sh # one-time setup, idempotent, re-run anytime
@@ -22,10 +34,30 @@ black box, and corrects some stale claims in older docs.
| Platform | Status |
|---------------------|---------------------------------------------------------------------------|
| Ubuntu 22.04+ | Supported by the bootstrap scripts (native, WSL2, or CI runner/container) |
| Windows | Use WSL2: `wsl --install -d Ubuntu-22.04`, then run the scripts inside it. Prefer cloning inside the WSL filesystem (`~/...`), not `/mnt/c` — Rust/npm builds through `/mnt` are much slower |
| Windows 10/11 | Primary. `script/bootstrap/windows/*.ps1` run a **portable stack**: MySQL 8.4 (official zip) and Redis live under the gitignored `.devstack/` and run as plain user processes — no admin, no services, no WSL, no Docker. Build tools (cmake, perl, nasm, llvm) are offered via winget |
| Ubuntu 22.04+ | Supported by `script/bootstrap/*.sh` (native, WSL2, or CI runner/container); services via apt |
| macOS | Not covered by the scripts; follow `_docs/dev_setup_server.md` manually |
### Windows specifics
- Everything stateful lives in `.devstack/` (gitignored): `mysql/` (extracted
zip), `mysql-data/`, `redis/`, `logs/`, `pids/`, `downloads/` (cached zips),
`my.ini`. Delete the directory to fully reset; re-run bootstrap to rebuild.
- Pinned services: MySQL **8.4.11** from `cdn.mysql.com` (the 8.x series is
required; only some patch versions exist on the CDN) and Redis **5.0.14.1**
(tporadowski build — old but sufficient for the backend's keepalive/TTL/
rate-limit usage; override `REDIS_ZIP_URL` to use Memurai/Garnet instead).
- The portable MySQL root user has **no password** (`--initialize-insecure`) —
local dev only, bound to 127.0.0.1.
- diesel-cli is compiled against the portable MySQL's own client library
(`MYSQLCLIENT_LIB_DIR`/`MYSQLCLIENT_VERSION`); `libmysql.dll` must be on
`PATH` to *run* it — the scripts handle both.
- Compiling the backend needs cmake, Strawberry Perl, NASM, and LLVM/libclang
(the `wreq` HTTP client builds BoringSSL, and `aws-lc-sys`/`ring` need
NASM/clang on MSVC). The bootstrap checks and offers winget installs.
- The generated secrets file additionally sets `TEMP_DIR` (the upload temp-dir
helper defaults to `/tmp`, which doesn't exist on Windows).
## Requirements matrix
| Requirement | Version / detail | Needed for |
+4 -3
View File
@@ -2,8 +2,9 @@
# Bootstrap the full local dev stack (backend + frontend) on a fresh clone.
#
# Target platform: Ubuntu 22.04+ — native, WSL2, or a CI runner/container.
# (macOS users: follow _docs/dev_setup_server.md manually; Windows users: run
# this inside WSL2. See _docs/dev_setup_local_stack.md.)
# (Windows users: the native PowerShell variant in script/bootstrap/windows/
# is the primary path — no WSL needed. macOS users: follow
# _docs/dev_setup_server.md manually. See _docs/dev_setup_local_stack.md.)
#
# Idempotent: safe to re-run at any time; each step detects work already done.
#
@@ -94,7 +95,7 @@ ensure_supported_platform() {
die "This script targets Ubuntu/WSL2. On macOS, follow _docs/dev_setup_server.md."
;;
*)
die "Unsupported platform '$(uname -s)'. Run inside WSL2 Ubuntu (wsl --install -d Ubuntu-22.04)."
die "Unsupported platform '$(uname -s)'. On Windows use script/bootstrap/windows/bootstrap_dev_stack.ps1 (native), or WSL2 Ubuntu for this script."
;;
esac
@@ -0,0 +1,447 @@
# Bootstrap the full local dev stack (backend + frontend) on native Windows.
#
# Everything service-shaped is PORTABLE and lives under the gitignored
# .devstack\ directory: MySQL 8.4 (official zip) and Redis run as plain user
# processes - no admin rights, no Windows services, no WSL, no Docker.
# Build tools (cmake, perl, nasm, llvm) are checked and offered via winget.
#
# Idempotent: safe to re-run at any time; each step detects work already done.
#
# Usage (from anywhere; paths are self-resolving):
# .\script\bootstrap\windows\bootstrap_dev_stack.ps1 [options]
#
# Options:
# -Yes Non-interactive: assume "yes" for prompts (CI mode)
# -SkipTools Don't check/install build tools via winget
# -SkipRustBuild Don't build the storyteller-web binary
# -SkipFrontend Don't run the frontend npm install
#
# After it succeeds:
# .\script\bootstrap\windows\run_backend_dev.ps1 # API on :12345
# .\script\bootstrap\windows\seed_demo_user.ps1 # demo login
# cd frontend; npx nx dev artcraft-webapp # webapp on :4201
param(
[switch]$Yes,
[switch]$SkipTools,
[switch]$SkipRustBuild,
[switch]$SkipFrontend
)
. (Join-Path $PSScriptRoot "common.ps1")
. (Join-Path $PSScriptRoot "start_dev_services.ps1")
$script:AssumeYes = [bool]$Yes
# Tools needed to compile the Rust workspace on Windows (wreq's BoringSSL build
# needs cmake + nasm + perl + libclang; see the workspace Cargo.toml notes).
# Each entry: name, exe to probe, winget id, dirs the installer typically uses
# (appended to this session's PATH after install).
$BuildTools = @(
@{ Name = "CMake"; Exe = "cmake"; WingetId = "Kitware.CMake"; PathDirs = @("$env:ProgramFiles\CMake\bin") },
@{ Name = "Strawberry Perl"; Exe = "perl"; WingetId = "StrawberryPerl.StrawberryPerl"; PathDirs = @("C:\Strawberry\perl\bin") },
@{ Name = "NASM"; Exe = "nasm"; WingetId = "NASM.NASM"; PathDirs = @("$env:ProgramFiles\NASM", "$env:LOCALAPPDATA\bin\NASM") },
@{ Name = "LLVM (libclang)"; Exe = "clang"; WingetId = "LLVM.LLVM"; PathDirs = @("$env:ProgramFiles\LLVM\bin") }
)
function Main {
Step "Preflight"
Ensure-CoreTools
if (-not $SkipTools) { Ensure-BuildTools } else { Write-Log "Build-tool check skipped (-SkipTools)." }
Step "Portable MySQL $MySqlVersion"
Install-PortableMySql
Start-DevMySql
Provision-MySqlDatabase
Step "Portable Redis"
Install-PortableRedis
Start-DevRedis
Step "diesel_cli (migration runner)"
Ensure-DieselCli
Step "Database migrations (_database/sql/migrations)"
Run-Migrations
Step "Seed data (system roles + badges)"
Seed-RolesAndBadges
Step "Development secrets file"
Write-SecretsEnvIfMissing
Step "Backend build (storyteller-web)"
Build-Backend
Step "Frontend install (frontend/)"
Setup-Frontend
Print-NextSteps
}
function Ensure-CoreTools {
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) {
$cargoBin = Join-Path $env:USERPROFILE ".cargo\bin"
Add-SessionPath @($cargoBin)
}
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) {
Die "Rust is not installed. Install via winget (winget install Rustlang.Rustup) or https://rustup.rs, then re-run."
}
Write-Log "rust: $(rustc --version)"
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
Die "Node.js is not installed. Install Node 20+ (winget install OpenJS.NodeJS.LTS), then re-run."
}
$nodeMajor = [int]((node --version) -replace '^v(\d+).*', '$1')
if ($nodeMajor -lt 20) {
Die "Node.js 20+ is required (found $(node --version)) - Nx 21 / Vite 6 requirement."
}
Write-Log "node: $(node --version)"
}
function Ensure-BuildTools {
$missing = @($BuildTools | Where-Object { -not (Get-Command $_.Exe -ErrorAction SilentlyContinue) })
if ($missing.Count -eq 0) {
Write-Log "All native build tools present (cmake, perl, nasm, clang)."
return
}
$names = ($missing | ForEach-Object { $_.Name }) -join ", "
Write-Log "Missing build tools: $names"
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
Die "winget is unavailable; install the tools manually: $names"
}
if (-not (Confirm-Or-Skip "Install them via winget? (installers may raise a UAC prompt)")) {
Die "Cannot build the backend without: $names (or re-run with -SkipTools if you manage them yourself)."
}
foreach ($tool in $missing) {
Write-Log "winget install $($tool.WingetId)..."
winget install --id $tool.WingetId -e --accept-source-agreements --accept-package-agreements --disable-interactivity
if ($LASTEXITCODE -ne 0) {
Write-Warn2 "winget exited with $LASTEXITCODE for $($tool.Name); checking whether the tool landed anyway..."
}
Add-SessionPath $tool.PathDirs
if (-not (Get-Command $tool.Exe -ErrorAction SilentlyContinue)) {
Die "$($tool.Name) still not found after install. Open a new terminal (PATH refresh) and re-run, or install manually."
}
Write-Log "$($tool.Name) OK."
}
}
function Install-PortableMySql {
if (Test-Path (Join-Path $MySqlBaseDir "bin\mysqld.exe")) {
Write-Log "Portable MySQL already extracted."
} elseif ((Test-TcpPort 3306)) {
# Something else already serves 3306 (e.g. a system-wide MySQL install).
# Use it rather than fighting over the port; provisioning below needs root
# credentials via MYSQL_ROOT_PASSWORD if that server has a root password.
Write-Warn2 "Port 3306 is already in use by an existing MySQL server - using it instead of the portable install."
if (-not (Get-Command mysql -ErrorAction SilentlyContinue)) {
Die "A server owns port 3306 but no 'mysql' client is on PATH. Add the client or stop that server."
}
return
} else {
New-Item -ItemType Directory -Force $DownloadsDir | Out-Null
$zipPath = Join-Path $DownloadsDir "mysql-$MySqlVersion-winx64.zip"
if (-not (Test-Path $zipPath)) {
Write-Log "Downloading MySQL $MySqlVersion (~250 MB, official cdn.mysql.com)..."
Download-File $MySqlZipUrl $zipPath
}
Write-Log "Extracting..."
Expand-Archive -Path $zipPath -DestinationPath (Join-Path $DevStackDir "mysql") -Force
if (-not (Test-Path (Join-Path $MySqlBaseDir "bin\mysqld.exe"))) {
Die "Extraction finished but $MySqlBaseDir\bin\mysqld.exe is missing."
}
}
if (-not (Test-Path $MySqlIniPath)) {
# Minimal config; utf8mb4 collation comes from each migration's DDL.
# MySQL's ini parser escape-processes backslashes - use forward slashes.
$baseForward = $MySqlBaseDir -replace '\\', '/'
$dataForward = $MySqlDataDir -replace '\\', '/'
$ini = @"
[mysqld]
basedir=$baseForward
datadir=$dataForward
port=3306
bind-address=127.0.0.1
"@
New-Item -ItemType Directory -Force $DevStackDir | Out-Null
Set-Content -Path $MySqlIniPath -Value $ini -Encoding ascii
}
if (-not (Test-Path (Join-Path $MySqlDataDir "mysql"))) {
Write-Log "Initializing MySQL data directory (root without password, local dev only)..."
$mysqld = Join-Path $MySqlBaseDir "bin\mysqld.exe"
Invoke-Native "`"$mysqld`" --defaults-file=`"$MySqlIniPath`" --initialize-insecure --console"
if ($LASTEXITCODE -ne 0) { Die "mysqld --initialize-insecure failed (exit $LASTEXITCODE). See output above." }
}
}
function Provision-MySqlDatabase {
if (Test-MySqlApp) {
Write-Log "Database '$DevMySqlDb' and user '$DevMySqlUser' already provisioned."
return
}
$mysql = Get-MySqlTool "mysql"
# Portable install: root has no password. External server: honor MYSQL_ROOT_PASSWORD.
$prev = $env:MYSQL_PWD
if ($env:MYSQL_ROOT_PASSWORD) { $env:MYSQL_PWD = $env:MYSQL_ROOT_PASSWORD } else { $env:MYSQL_PWD = "" }
try {
# Same DDL as _docs/dev_setup_server.md, made idempotent. The extra
# '127.0.0.1' account covers hosts where reverse-DNS of 127.0.0.1 doesn't
# resolve to 'localhost'.
$ddl = @"
CREATE DATABASE IF NOT EXISTS $DevMySqlDb;
CREATE USER IF NOT EXISTS '$DevMySqlUser'@'localhost' IDENTIFIED BY '$DevMySqlPassword';
CREATE USER IF NOT EXISTS '$DevMySqlUser'@'127.0.0.1' IDENTIFIED BY '$DevMySqlPassword';
GRANT ALL PRIVILEGES ON $DevMySqlDb.* TO '$DevMySqlUser'@'localhost';
GRANT ALL PRIVILEGES ON $DevMySqlDb.* TO '$DevMySqlUser'@'127.0.0.1';
FLUSH PRIVILEGES;
"@
$ddl | & $mysql -u root -h 127.0.0.1
if ($LASTEXITCODE -ne 0) {
Die "Provisioning failed. For an external MySQL server, set MYSQL_ROOT_PASSWORD and re-run."
}
} finally {
$env:MYSQL_PWD = $prev
}
if (-not (Test-MySqlApp)) { Die "Provisioning ran but '$DevMySqlUser' still cannot reach '$DevMySqlDb'." }
Write-Log "Provisioned."
}
function Install-PortableRedis {
if (Test-Path (Join-Path $RedisDir "redis-server.exe")) {
Write-Log "Portable Redis already extracted."
return
}
if (Test-TcpPort 6379) {
Write-Warn2 "Port 6379 is already served (existing Redis/Memurai?) - using it."
return
}
New-Item -ItemType Directory -Force $DownloadsDir | Out-Null
$zipPath = Join-Path $DownloadsDir "redis-win-x64.zip"
if (-not (Test-Path $zipPath)) {
Write-Log "Downloading Redis for Windows (~12 MB)..."
Download-File $RedisZipUrl $zipPath
}
Write-Log "Extracting..."
Expand-Archive -Path $zipPath -DestinationPath $RedisDir -Force
if (-not (Test-Path (Join-Path $RedisDir "redis-server.exe"))) {
Die "Extraction finished but $RedisDir\redis-server.exe is missing."
}
}
function Ensure-DieselCli {
Add-MySqlClientToSession
if (Get-Command diesel -ErrorAction SilentlyContinue) {
Write-Log "Found $(diesel --version)."
return
}
# diesel_cli's mysql feature links against libmysqlclient. The portable MySQL
# ships its own client lib - point mysqlclient-sys at it instead of requiring
# vcpkg. sqlite is bundled so no system sqlite is needed.
if (-not (Test-Path (Join-Path $MySqlBaseDir "lib\libmysql.lib"))) {
Die "MySQL client lib not found under $MySqlBaseDir\lib (needed to compile diesel_cli)."
}
Write-Log "Installing diesel_cli (compiles from source; a few minutes)..."
$env:MYSQLCLIENT_LIB_DIR = Join-Path $MySqlBaseDir "lib"
$env:MYSQLCLIENT_VERSION = $MySqlVersion
Invoke-Native "cargo install diesel_cli --no-default-features --features mysql,sqlite-bundled"
if ($LASTEXITCODE -ne 0) { Die "cargo install diesel_cli failed (exit $LASTEXITCODE)." }
Write-Log "Installed $(diesel --version)."
}
function Run-Migrations {
# diesel-cli reads DATABASE_URL (also provided by the repo-root .env, which
# diesel auto-loads from the cwd). diesel.exe needs libmysql.dll at runtime -
# Add-MySqlClientToSession put the portable lib dirs on PATH.
if (-not $env:DATABASE_URL) {
$env:DATABASE_URL = "mysql://${DevMySqlUser}:${DevMySqlPassword}@localhost/${DevMySqlDb}"
}
Push-Location $RootDir
try {
# NB: "Encountered unknown type for Mysql: enum" warnings are harmless.
Invoke-Native "diesel migration run"
if ($LASTEXITCODE -ne 0) { Die "diesel migration run failed (exit $LASTEXITCODE)." }
} finally {
Pop-Location
}
Write-Log "Migrations up to date."
}
function Seed-RolesAndBadges {
# Same data as _database/sql/seed/bootstrap_inserts_roles_etc.sh, but guarded
# so re-runs don't hit duplicate-key errors. The 'user' role is mandatory:
# account creation hardcodes user_role_slug='user'.
$roleCount = [int](Invoke-MySqlApp -Sql "SELECT COUNT(*) FROM user_roles" | Select-Object -First 1)
if ($roleCount -gt 0) {
Write-Log "user_roles already seeded ($roleCount rows)."
} else {
Invoke-MySqlApp -SourceFile (Join-Path $RootDir "_database\sql\seed\sql\system_roles.sql")
if ($LASTEXITCODE -ne 0) { Die "Seeding system_roles.sql failed." }
Write-Log "Inserted system roles (user, mod, admin)."
}
$badgeCount = [int](Invoke-MySqlApp -Sql "SELECT COUNT(*) FROM badges" | Select-Object -First 1)
if ($badgeCount -gt 0) {
Write-Log "badges already seeded ($badgeCount rows)."
} else {
Invoke-MySqlApp -SourceFile (Join-Path $RootDir "_database\sql\seed\sql\user_badges.sql")
if ($LASTEXITCODE -ne 0) { Die "Seeding user_badges.sql failed." }
Write-Log "Inserted badges."
}
}
function Write-SecretsEnvIfMissing {
if (Test-Path $SecretsEnvFile) {
Write-Log "Already exists: $SecretsEnvFile (leaving it untouched)."
return
}
# Every var below is read with get_env_string_required at server startup but
# is NOT supplied by the checked-in config files. None is contacted at boot -
# placeholders are enough to run the server. Providers are only contacted by
# job worker binaries, which local dev does not run.
#
# NOTE: config loading is FIRST-VALUE-WINS (dotenv never overrides a set
# key), so values here cannot override storyteller-web.development.env.
#
# TEMP_DIR: the upload temp-dir helper defaults to /tmp, which doesn't exist
# on Windows - point it at the user temp dir (forward slashes work fine).
$tempDirForward = $env:TEMP -replace '\\', '/'
$content = @"
# Local-development secrets for storyteller-web. Generated by
# script/bootstrap/windows/bootstrap_dev_stack.ps1 - gitignored; edit freely.
#
# Placeholders below satisfy required-at-boot checks without enabling any
# external integration. Replace individual values with real dev credentials
# only if you need that specific integration locally.
# Windows: scoped upload temp dirs (code default is /tmp)
TEMP_DIR=$tempDirForward
# R2/S3 object storage (clients are constructed offline)
ACCESS_KEY=dummy-local-dev
SECRET_KEY=dummy-local-dev
REGION_NAME=us-east-1
W2L_PRIVATE_DOWNLOAD_BUCKET_NAME=dummy-local-dev-private
W2L_PUBLIC_DOWNLOAD_BUCKET_NAME=dummy-local-dev-public
# Email (Resend)
RESEND_API_KEY=dummy-local-dev
# Generation providers (only job workers ever contact these; workers are not
# run in local dev, so enqueued jobs simply stay pending)
FAL_API_KEY=dummy-local-dev
GMICLOUD_API_KEY=dummy-local-dev
GROK_API_KEY=dummy-local-dev
BEEBLE_API_KEY=dummy-local-dev
OPENAI_API_KEY=dummy-local-dev
WORLDLABS_API_KEY=dummy-local-dev
SEEDANCE2PRO_COOKIES=dummy-local-dev
SEEDANCE2PRO_WHITELIST_COOKIES=dummy-local-dev
SEEDANCE2PRO_BYTEPLUS_ULTRA_COOKIES=dummy-local-dev
# Stripe account ids (test-mode secret keys are already checked into
# storyteller-web.development.env; only the account ids are missing there)
STRIPE_FAKEYOU_ACCOUNT_ID=acct_dummylocaldev
STRIPE_ARTCRAFT_ACCOUNT_ID=acct_dummylocaldev
STRIPE_ARTCRAFT_SECRET_KEY=sk_test_dummylocaldev
STRIPE_ARTCRAFT_SECRET_WEBHOOK_KEY=whsec_dummylocaldev
"@
Set-Content -Path $SecretsEnvFile -Value $content -Encoding ascii
Write-Log "Wrote $SecretsEnvFile"
}
function Build-Backend {
if ($SkipRustBuild) {
Write-Log "Skipped (-SkipRustBuild)."
return
}
# Freshly-installed tools live in dirs this shell may not have on PATH yet;
# bindgen (boring2/aws-lc-sys) additionally wants LIBCLANG_PATH.
foreach ($tool in $BuildTools) { Add-SessionPath $tool.PathDirs }
if (-not $env:LIBCLANG_PATH) {
$clang = Get-Command clang -ErrorAction SilentlyContinue
if ($clang) { $env:LIBCLANG_PATH = Split-Path $clang.Source }
}
Push-Location $RootDir
try {
# SQLX_OFFLINE only affects compile-time query verification (checked-in
# .sqlx/ cache instead of a live DB); it has no runtime effect.
$env:SQLX_OFFLINE = "true"
Write-Log "Building (first build takes several minutes)..."
Invoke-Native "cargo build -p storyteller-web --bin storyteller-web"
if ($LASTEXITCODE -ne 0) { Die "cargo build failed (exit $LASTEXITCODE)." }
} finally {
Pop-Location
}
Write-Log "Built target\debug\storyteller-web.exe."
}
function Setup-Frontend {
if ($SkipFrontend) {
Write-Log "Skipped (-SkipFrontend)."
return
}
$frontendPath = Join-Path $RootDir "frontend"
# Mirror script/common/frontend_preflight.sh: the workspace uses npm; stale
# pnpm artifacts break installs with ENOTEMPTY.
if ((Test-Path (Join-Path $frontendPath "pnpm-lock.yaml")) -or
(Test-Path (Join-Path $frontendPath "pnpm-workspace.yaml")) -or
(Test-Path (Join-Path $frontendPath "node_modules\.pnpm"))) {
Die "Stale pnpm artifacts detected in frontend\. Clean up (see frontend/README.md): remove node_modules, pnpm-lock.yaml, pnpm-workspace.yaml, then re-run."
}
Push-Location $frontendPath
try {
Invoke-Native "npm install"
if ($LASTEXITCODE -ne 0) {
Write-Warn2 "npm install failed; wiping node_modules + .nx and retrying once..."
Remove-Item -Recurse -Force -Confirm:$false node_modules, .nx -ErrorAction SilentlyContinue
Invoke-Native "npm install"
if ($LASTEXITCODE -ne 0) { Die "npm install failed again. See frontend/README.md troubleshooting." }
}
} finally {
Pop-Location
}
Write-Log "Frontend dependencies installed."
}
function Download-File([string]$Url, [string]$Destination) {
# curl.exe ships with Windows 10+ and handles redirects/resume better than
# Invoke-WebRequest (which is also painfully slow on large files in PS 5.1).
curl.exe -fL --retry 3 -o "$Destination.partial" $Url
if ($LASTEXITCODE -ne 0) {
Remove-Item "$Destination.partial" -Force -Confirm:$false -ErrorAction SilentlyContinue
Die "Download failed: $Url"
}
Move-Item "$Destination.partial" $Destination -Force
}
function Print-NextSteps {
Step "Done"
Write-Host @"
The dev stack is bootstrapped. Next steps:
1. Start the backend (binds $DevBackendUrl):
.\script\bootstrap\windows\run_backend_dev.ps1
2. In another terminal, create the demo login (idempotent):
.\script\bootstrap\windows\seed_demo_user.ps1
Credentials: $DemoUsername / $DemoPassword
3. Start the webapp (binds http://localhost:4201; in dev it talks to
$DevBackendUrl automatically):
cd frontend; npx nx dev artcraft-webapp
Health checks at any time:
.\script\bootstrap\windows\dev_stack_doctor.ps1
Services (MySQL/Redis) start on demand and persist; stop them with:
.\script\bootstrap\windows\stop_dev_services.ps1
"@
}
Main
+190
View File
@@ -0,0 +1,190 @@
# 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 stdout lines.
# Password goes via MYSQL_PWD so it stays off the command line.
# -Quiet merges stderr into the discarded output (for probes that are
# expected to fail, e.g. Test-MySqlApp before provisioning).
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 '\\', '/'
& $mysql -u $DevMySqlUser -h 127.0.0.1 -D $DevMySqlDb -e "source $forward"
} elseif ($Quiet) {
& $mysql -u $DevMySqlUser -h 127.0.0.1 -D $DevMySqlDb -N -e $Sql 2>&1 | Out-Null
} else {
& $mysql -u $DevMySqlUser -h 127.0.0.1 -D $DevMySqlDb -N -e $Sql
}
} finally {
$env:MYSQL_PWD = $prev
}
}
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 = & $cli -h 127.0.0.1 ping 2>$null
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"))
}
@@ -0,0 +1,160 @@
# Diagnose the local dev stack on Windows: checks every prerequisite the
# bootstrap sets up and reports PASS / WARN / FAIL per item.
#
# FAIL = the stack cannot work until fixed (exit code 1).
# WARN = optional or run-time-only (e.g. backend not currently running).
param()
. (Join-Path $PSScriptRoot "common.ps1")
$ErrorActionPreference = "Continue"
$script:Failures = 0
$script:Warnings = 0
function Pass([string]$Message) { Write-Host " PASS $Message" }
function Fail([string]$Message) { Write-Host " FAIL $Message" -ForegroundColor Red; $script:Failures++ }
function WarnCheck([string]$Message) { Write-Host " WARN $Message" -ForegroundColor Yellow; $script:Warnings++ }
Write-Host "Dev stack doctor ($RootDir)"
Write-Host ""
Write-Host "--- Toolchains ---"
Add-SessionPath @((Join-Path $env:USERPROFILE ".cargo\bin"))
Add-MySqlClientToSession
# Freshly-installed build tools may not be on this session's PATH yet.
Add-SessionPath @("$env:ProgramFiles\CMake\bin", "C:\Strawberry\perl\bin", "$env:ProgramFiles\NASM", "$env:ProgramFiles\LLVM\bin")
if (Get-Command cargo -ErrorAction SilentlyContinue) {
Pass "rust: $(rustc --version)"
} else {
Fail "rust: cargo not on PATH (winget install Rustlang.Rustup)"
}
if (Get-Command diesel -ErrorAction SilentlyContinue) {
try {
$dv = diesel --version 2>$null
if ($LASTEXITCODE -eq 0) { Pass "diesel_cli: $dv" }
else { Fail "diesel_cli: present but broken (libmysql.dll missing from PATH? re-run bootstrap)" }
} catch { Fail "diesel_cli: present but broken (libmysql.dll missing from PATH?)" }
} else {
Fail "diesel_cli: not installed (bootstrap installs it against the portable MySQL client lib)"
}
if (Get-Command node -ErrorAction SilentlyContinue) {
$nodeMajor = [int]((node --version) -replace '^v(\d+).*', '$1')
if ($nodeMajor -ge 20) { Pass "node: $(node --version) (>= 20 required by Nx 21 / Vite 6)" }
else { Fail "node: $(node --version) is too old - Node 20+ required" }
} else {
Fail "node: not installed (Node 20+ required for the frontend)"
}
foreach ($tool in @("cmake", "perl", "nasm", "clang")) {
if (Get-Command $tool -ErrorAction SilentlyContinue) {
Pass "${tool}: found"
} else {
WarnCheck "${tool}: not on PATH (needed to COMPILE the backend; bootstrap offers winget installs)"
}
}
Write-Host ""
Write-Host "--- Services (portable, under .devstack\) ---"
if (Test-Path (Join-Path $MySqlBaseDir "bin\mysqld.exe")) {
Pass "mysql: portable install present ($MySqlVersion)"
} elseif (Test-TcpPort 3306) {
WarnCheck "mysql: no portable install, but something serves port 3306 (external server assumed)"
} else {
Fail "mysql: not installed (run bootstrap_dev_stack.ps1)"
}
if (Test-TcpPort 3306) {
if (Test-MySqlApp) {
Pass "mysql: reachable as '$DevMySqlUser' on database '$DevMySqlDb'"
$migrationDirs = (Get-ChildItem -Directory (Join-Path $RootDir "_database\sql\migrations")).Count
$applied = 0
try { $applied = [int](Invoke-MySqlApp -Sql "SELECT COUNT(*) FROM __diesel_schema_migrations" | Select-Object -First 1) } catch {}
if (($applied -ge $migrationDirs) -and ($applied -gt 0)) {
Pass "migrations: $applied applied ($migrationDirs in _database\sql\migrations)"
} elseif ($applied -gt 0) {
Fail "migrations: only $applied/$migrationDirs applied - run 'diesel migration run'"
} else {
Fail "migrations: none applied - run 'diesel migration run' (or re-run bootstrap)"
}
$roleCount = 0
try { $roleCount = [int](Invoke-MySqlApp -Sql "SELECT COUNT(*) FROM user_roles" | Select-Object -First 1) } catch {}
if ($roleCount -ge 3) {
Pass "seed: user_roles has $roleCount rows (user/mod/admin present)"
} else {
Fail "seed: user_roles has $roleCount rows - account creation needs the 'user' role (re-run bootstrap)"
}
$badgeCount = 0
try { $badgeCount = [int](Invoke-MySqlApp -Sql "SELECT COUNT(*) FROM badges" | Select-Object -First 1) } catch {}
if ($badgeCount -gt 0) { Pass "seed: badges has $badgeCount rows" }
else { WarnCheck "seed: badges table is empty (cosmetic; re-run bootstrap to fill)" }
$demoCount = 0
try { $demoCount = [int](Invoke-MySqlApp -Sql "SELECT COUNT(*) FROM users WHERE username='$DemoUsername'" | Select-Object -First 1) } catch {}
if ($demoCount -gt 0) { Pass "demo user: '$DemoUsername' exists" }
else { WarnCheck "demo user: '$DemoUsername' not created yet (seed_demo_user.ps1, backend must be running)" }
} else {
Fail "mysql: port 3306 is up but '$DevMySqlUser' cannot access '$DevMySqlDb' (re-run bootstrap provisioning)"
}
} else {
Fail "mysql: nothing listening on 3306 (start_dev_services.ps1)"
}
if (Test-Redis) {
Pass "redis: PONG"
} elseif (Test-TcpPort 6379) {
WarnCheck "redis: port 6379 open but redis-cli not found to verify"
} else {
Fail "redis: not reachable on 6379 (the backend's r2d2 pool connects eagerly at boot; start_dev_services.ps1)"
}
Write-Host ""
Write-Host "--- Backend ---"
if (Test-Path $SecretsEnvFile) {
Pass "secrets env: storyteller-web.development-secrets.env exists"
} else {
Fail "secrets env: missing - the server aborts at boot on ~19 required vars (re-run bootstrap)"
}
if ((Test-Path (Join-Path $RootDir "target\debug\storyteller-web.exe")) -or
(Test-Path (Join-Path $RootDir "target\release\storyteller-web.exe"))) {
Pass "binary: storyteller-web.exe is built"
} else {
WarnCheck "binary: storyteller-web not built yet (bootstrap builds it; cargo run builds on demand)"
}
if (Test-Backend) {
Pass "server: $DevBackendUrl/_status responds"
} else {
WarnCheck "server: not running (run_backend_dev.ps1)"
}
Write-Host ""
Write-Host "--- Frontend ---"
if (Test-Path (Join-Path $RootDir "frontend\node_modules")) {
Pass "frontend: node_modules present"
} else {
WarnCheck "frontend: node_modules missing (cd frontend; npm install)"
}
if ((Test-Path (Join-Path $RootDir "frontend\pnpm-lock.yaml")) -or
(Test-Path (Join-Path $RootDir "frontend\node_modules\.pnpm"))) {
Fail "frontend: stale pnpm artifacts detected (see frontend/README.md - the workspace uses npm)"
} else {
Pass "frontend: no stale pnpm artifacts"
}
Write-Host ""
Write-Host "Summary: $Failures failure(s), $Warnings warning(s)."
if ($Failures -gt 0) {
Write-Host "Fix failures with: .\script\bootstrap\windows\bootstrap_dev_stack.ps1 (safe to re-run)"
exit 1
}
@@ -0,0 +1,31 @@
# Run the storyteller-web backend for local development (binds 0.0.0.0:12345).
#
# Requires a bootstrapped environment (bootstrap_dev_stack.ps1). Starts the
# portable MySQL/Redis if they aren't running. Always runs from the repo root:
# the server's config search path (crates/service/web/storyteller_web/config)
# and its includes/ defaults are cwd-relative.
param()
. (Join-Path $PSScriptRoot "common.ps1")
. (Join-Path $PSScriptRoot "start_dev_services.ps1")
Start-DevMySql
Start-DevRedis
if (-not (Test-MySqlApp)) { Die "MySQL is up but '$DevMySqlUser' cannot reach '$DevMySqlDb'. Run bootstrap_dev_stack.ps1." }
if (-not (Test-Path $SecretsEnvFile)) { Die "Missing $SecretsEnvFile. Run bootstrap_dev_stack.ps1." }
Add-SessionPath @((Join-Path $env:USERPROFILE ".cargo\bin"))
Push-Location $RootDir
try {
# SERVER_ENVIRONMENT defaults to Development when unset; dev config files are
# picked up from the search path automatically. SQLX_OFFLINE only affects
# compile-time query checking, never runtime. TEMP_DIR belt-and-suspenders in
# case an older generated secrets file predates the Windows TEMP_DIR line.
$env:SQLX_OFFLINE = "true"
if (-not $env:TEMP_DIR) { $env:TEMP_DIR = $env:TEMP -replace '\\', '/' }
Write-Log "Starting storyteller-web on $DevBackendUrl (Ctrl-C to stop)..."
Invoke-Native "cargo run -p storyteller-web --bin storyteller-web"
} finally {
Pop-Location
}
@@ -0,0 +1,71 @@
# Create (or verify) the local demo user via the running backend, then verify
# that login works. Idempotent: an already-existing user is a success.
#
# Requires the backend to be up (run_backend_dev.ps1) - waits up to
# -WaitSeconds (default 60) for it.
#
# The account is created through POST /v1/create_account rather than raw SQL so
# it exercises the real signup path (bcrypt hash, session, firehose row) and
# stays correct if the users schema evolves.
#
# Override credentials via DEMO_USERNAME / DEMO_PASSWORD / DEMO_EMAIL env vars.
# Username rules: 3-16 chars of [A-Za-z0-9_-], not on the reserved list
# (includes/binary_includes/usernames/reserved_usernames.txt); password >= 6.
param(
[int]$WaitSeconds = 60
)
. (Join-Path $PSScriptRoot "common.ps1")
Write-Log "Waiting for backend at $DevBackendUrl (up to ${WaitSeconds}s)..."
Wait-ForCondition { Test-Backend } $WaitSeconds "backend /_status to respond (start it with run_backend_dev.ps1)" | Out-Null
Write-Log "Backend is up."
# curl.exe + ConvertFrom-Json: Invoke-RestMethod in PS 5.1 throws on non-2xx
# and hides the response body, which we need for the already-exists case.
function Invoke-JsonPost([string]$Url, [hashtable]$Body) {
$json = $Body | ConvertTo-Json -Compress
$tmp = Join-Path $env:TEMP "bootstrap_post_body.json"
Set-Content -Path $tmp -Value $json -Encoding ascii
$response = curl.exe -sS -X POST $Url -H "Content-Type: application/json" --data "@$tmp"
Remove-Item $tmp -Force -Confirm:$false -ErrorAction SilentlyContinue
if ($LASTEXITCODE -ne 0) { Die "Request to $Url failed (curl exit $LASTEXITCODE)." }
try {
return $response | ConvertFrom-Json
} catch {
Die "Non-JSON response from ${Url}: $response"
}
}
Write-Log "Creating account '$DemoUsername'..."
$create = Invoke-JsonPost "$DevBackendUrl/v1/create_account" @{
username = $DemoUsername
password = $DemoPassword
password_confirmation = $DemoPassword
email_address = $DemoEmail
}
if ($create.success -eq $true) {
Write-Log "Account created."
} else {
$errorCode = "$($create.error_code)$($create.error_code_str)"
if ($errorCode -match "(?i)username_?taken|email_?taken") {
Write-Log "Account already exists ($errorCode) - continuing."
} else {
Die "Account creation failed: $($create | ConvertTo-Json -Compress)"
}
}
Write-Log "Verifying login..."
$login = Invoke-JsonPost "$DevBackendUrl/v1/login" @{
username_or_email = $DemoUsername
password = $DemoPassword
}
if ($login.success -ne $true) { Die "Login verification failed: $($login | ConvertTo-Json -Compress)" }
if (-not $login.signed_session) { Die "Login succeeded but returned no signed_session." }
Write-Log "Demo user is ready."
Write-Log " Username: $DemoUsername"
Write-Log " Password: $DemoPassword"
Write-Log "Log in at the webapp /login page (http://localhost:4201/login once 'nx dev artcraft-webapp' is running)."
@@ -0,0 +1,52 @@
# Start (or verify) the portable MySQL and Redis processes under .devstack/.
# Idempotent: already-running services are left alone. Safe to call from other
# scripts via dot-sourcing (defines Start-DevMySql / Start-DevRedis) or run
# directly.
param()
. (Join-Path $PSScriptRoot "common.ps1")
function Start-DevMySql {
if (Test-TcpPort 3306) {
Write-Log "MySQL already listening on 3306."
return
}
$mysqld = Join-Path $MySqlBaseDir "bin\mysqld.exe"
if (-not (Test-Path $mysqld)) {
Die "Portable MySQL not installed at $MySqlBaseDir. Run bootstrap_dev_stack.ps1 first."
}
if (-not (Test-Path (Join-Path $MySqlDataDir "mysql"))) {
Die "MySQL data directory not initialized at $MySqlDataDir. Run bootstrap_dev_stack.ps1 first."
}
Write-Log "Starting mysqld (portable, data in .devstack\mysql-data)..."
Start-DevProcess -Name "mysqld" -Exe $mysqld -Arguments @("--defaults-file=`"$MySqlIniPath`"") | Out-Null
Wait-ForCondition { Test-TcpPort 3306 } 60 "MySQL to listen on 3306" | Out-Null
Write-Log "MySQL is up."
}
function Start-DevRedis {
if (Test-TcpPort 6379) {
Write-Log "Redis already listening on 6379."
return
}
$redis = Join-Path $RedisDir "redis-server.exe"
if (-not (Test-Path $redis)) {
Die "Portable Redis not installed at $RedisDir. Run bootstrap_dev_stack.ps1 first."
}
Write-Log "Starting redis-server (portable)..."
# Keep the working dir (dump.rdb etc.) inside .devstack. NB: Start-Process
# rejects empty-string ArgumentList elements, so pass `--save ""` as one
# token to disable snapshotting.
Start-DevProcess -Name "redis" -Exe $redis -Arguments @(
"--port", "6379",
"--save", "`"`"",
"--dir", "`"$RedisDir`""
) | Out-Null
Wait-ForCondition { Test-TcpPort 6379 } 30 "Redis to listen on 6379" | Out-Null
Write-Log "Redis is up."
}
if ($MyInvocation.InvocationName -ne '.') {
Start-DevMySql
Start-DevRedis
}
@@ -0,0 +1,41 @@
# Stop the portable MySQL and Redis processes started by start_dev_services.ps1.
param()
. (Join-Path $PSScriptRoot "common.ps1")
# Graceful MySQL shutdown (flushes InnoDB) - root has no password in the
# portable install (initialized with --initialize-insecure).
if (Test-TcpPort 3306) {
$mysqladmin = Get-MySqlTool "mysqladmin"
if ($mysqladmin) {
Write-Log "Shutting down MySQL..."
try { & $mysqladmin -u root -h 127.0.0.1 shutdown 2>$null } catch {}
$deadline = (Get-Date).AddSeconds(30)
while ((Test-TcpPort 3306) -and ((Get-Date) -lt $deadline)) { Start-Sleep -Seconds 1 }
}
}
if (Test-TcpPort 6379) {
$cli = Get-RedisTool "redis-cli"
if ($cli) {
Write-Log "Shutting down Redis..."
try { & $cli -h 127.0.0.1 shutdown nosave 2>$null } catch {}
}
}
# Last resort: kill anything left via recorded PIDs.
if (Test-Path $PidsDir) {
foreach ($pidFile in Get-ChildItem $PidsDir -Filter "*.pid" -ErrorAction SilentlyContinue) {
$procId = Get-Content $pidFile.FullName -ErrorAction SilentlyContinue
if ($procId) {
$proc = Get-Process -Id $procId -ErrorAction SilentlyContinue
if ($proc -and -not $proc.HasExited) {
Write-Log "Force-stopping $($pidFile.BaseName) (pid $procId)..."
try { Stop-Process -Id $procId -Force -Confirm:$false -ErrorAction Stop } catch {}
}
}
Remove-Item $pidFile.FullName -Force -Confirm:$false -ErrorAction SilentlyContinue
}
}
Write-Log "Dev services stopped."