Files
Genarrative/scripts/dev-rust-stack.ps1

314 lines
9.3 KiB
PowerShell

[CmdletBinding()]
param(
[Alias("h")]
[switch]$Help,
[string]$ApiHost = "127.0.0.1",
[int]$ApiPort = 8082,
[string]$WebHost = "0.0.0.0",
[int]$WebPort = 3000,
[string]$SpacetimeHost = "127.0.0.1",
[int]$SpacetimePort = 3101,
[string]$SpacetimeRootDir = "",
[string]$Database = "genarrative-dev",
[string]$Log = "info,tower_http=info",
[int]$SpacetimeStartupTimeoutSeconds = 60,
[switch]$SkipSpacetime,
[switch]$SkipPublish,
[switch]$ClearDatabase
)
$ErrorActionPreference = "Stop"
function Write-Usage {
@(
'Usage:',
' npm run dev:rust',
' .\scripts\dev-rust-stack.ps1 -ApiPort 8090 -SpacetimePort 3110',
' .\scripts\dev-rust-stack.ps1 -SkipSpacetime -SkipPublish',
' .\scripts\dev-rust-stack.ps1 -ClearDatabase',
'',
'Notes:',
' 1. Start SpacetimeDB standalone, Rust api-server, and Vite web together.',
' 2. Publish server-rs/crates/spacetime-module by default, without clearing data.',
' 3. Only -ClearDatabase appends spacetime publish --clear-database.',
' 4. Web listens on 0.0.0.0:3000 by default; API listens on 127.0.0.1:8082.'
) -join [Environment]::NewLine
}
function Quote-ProcessArgument {
param([string]$Value)
if ($null -eq $Value) {
return '""'
}
if ($Value -notmatch '[\s"]') {
return $Value
}
return '"' + $Value.Replace('"', '\"') + '"'
}
function Join-ProcessArguments {
param([string[]]$Arguments)
return (($Arguments | ForEach-Object { Quote-ProcessArgument $_ }) -join " ")
}
function Resolve-ClientHost {
param([string]$HostName)
if ($HostName -eq "0.0.0.0" -or $HostName -eq "::") {
return "127.0.0.1"
}
return $HostName
}
function Start-StackProcess {
param(
[string]$Name,
[string]$FilePath,
[string[]]$Arguments,
[string]$WorkingDirectory,
[hashtable]$Environment
)
$argumentLine = Join-ProcessArguments -Arguments $Arguments
Write-Host "[dev:rust] start ${Name}: $FilePath $argumentLine"
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
$startInfo.FileName = $FilePath
$startInfo.Arguments = $argumentLine
$startInfo.WorkingDirectory = $WorkingDirectory
$startInfo.UseShellExecute = $false
$startInfo.RedirectStandardOutput = $false
$startInfo.RedirectStandardError = $false
$startInfo.RedirectStandardInput = $false
foreach ($entry in $Environment.GetEnumerator()) {
$startInfo.EnvironmentVariables[$entry.Key] = [string]$entry.Value
}
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $startInfo
if (-not $process.Start()) {
throw "Failed to start process: $Name"
}
return [PSCustomObject]@{
Name = $Name
Process = $process
}
}
function Stop-StackProcesses {
param([System.Collections.Generic.List[object]]$Processes)
for ($index = $Processes.Count - 1; $index -ge 0; $index--) {
$item = $Processes[$index]
$process = $item.Process
if ($null -ne $process -and -not $process.HasExited) {
Write-Host "[dev:rust] stop $($item.Name) (pid=$($process.Id))"
$taskkillCommand = Get-Command taskkill.exe -ErrorAction SilentlyContinue
if ($null -ne $taskkillCommand) {
& $taskkillCommand.Source /PID $process.Id /T /F *> $null
}
else {
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
}
}
}
}
function Wait-ForSpacetimeServer {
param(
[string]$CommandPath,
[string]$Server,
[int]$TimeoutSeconds,
$ProcessItem
)
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ((Get-Date) -lt $deadline) {
if ($null -ne $ProcessItem -and $ProcessItem.Process.HasExited) {
throw "SpacetimeDB exited before readiness, exit code: $($ProcessItem.Process.ExitCode)"
}
& $CommandPath server ping $Server *> $null
if ($LASTEXITCODE -eq 0) {
return
}
Start-Sleep -Milliseconds 500
}
throw "Timed out waiting for SpacetimeDB readiness: $Server"
}
if ($Help) {
Write-Usage
exit 0
}
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$repoRoot = Split-Path -Parent $scriptDir
$serverRsDir = Join-Path $repoRoot "server-rs"
$manifestPath = Join-Path $serverRsDir "Cargo.toml"
$modulePath = Join-Path $serverRsDir "crates\spacetime-module"
$viteCliPath = Join-Path $repoRoot "scripts\vite-cli.mjs"
if ([string]::IsNullOrWhiteSpace($SpacetimeRootDir)) {
$SpacetimeRootDir = Join-Path $serverRsDir ".spacetimedb\local"
}
if (-not (Test-Path $manifestPath)) {
throw "Missing server-rs/Cargo.toml, cannot start Rust local stack."
}
if (-not (Test-Path (Join-Path $modulePath "Cargo.toml"))) {
throw "Missing server-rs/crates/spacetime-module/Cargo.toml, cannot publish SpacetimeDB module."
}
if (-not (Test-Path $viteCliPath)) {
throw "Missing scripts/vite-cli.mjs, cannot start web frontend."
}
$cargoCommand = Get-Command cargo -ErrorAction SilentlyContinue
$nodeCommand = Get-Command node -ErrorAction SilentlyContinue
$spacetimeCommand = Get-Command spacetime -ErrorAction SilentlyContinue
if ($null -eq $cargoCommand) {
throw "Missing cargo. Install Rust toolchain first."
}
if ($null -eq $nodeCommand) {
throw "Missing node. Install Node.js or use the project bundled runtime first."
}
if (-not $SkipSpacetime -or -not $SkipPublish) {
if ($null -eq $spacetimeCommand) {
throw "Missing spacetime CLI. Install guide: https://spacetimedb.com/install"
}
}
$spacetimeServer = "http://$SpacetimeHost`:$SpacetimePort"
$apiTargetHost = Resolve-ClientHost -HostName $ApiHost
$rustServerTarget = "http://$apiTargetHost`:$ApiPort"
$stackProcesses = New-Object System.Collections.Generic.List[object]
$exitCode = 0
Write-Host "[dev:rust] repo: $repoRoot"
Write-Host "[dev:rust] web: http://127.0.0.1:$WebPort"
Write-Host "[dev:rust] rust api: $rustServerTarget"
Write-Host "[dev:rust] spacetime: $spacetimeServer"
Write-Host "[dev:rust] database: $Database"
try {
$spacetimeProcessItem = $null
if (-not $SkipSpacetime) {
New-Item -ItemType Directory -Force -Path $SpacetimeRootDir | Out-Null
$spacetimeProcessItem = Start-StackProcess `
-Name "spacetimedb" `
-FilePath $spacetimeCommand.Source `
-Arguments @(
"--root-dir", $SpacetimeRootDir,
"start",
"--edition", "standalone",
"--listen-addr", "$SpacetimeHost`:$SpacetimePort"
) `
-WorkingDirectory $serverRsDir `
-Environment @{}
$stackProcesses.Add($spacetimeProcessItem)
}
if (-not $SkipPublish) {
Write-Host "[dev:rust] wait for SpacetimeDB readiness"
Wait-ForSpacetimeServer `
-CommandPath $spacetimeCommand.Source `
-Server $spacetimeServer `
-TimeoutSeconds $SpacetimeStartupTimeoutSeconds `
-ProcessItem $spacetimeProcessItem
$publishArgs = @(
"publish",
$Database,
"--server", $spacetimeServer,
"--module-path", $modulePath
)
if ($ClearDatabase) {
$publishArgs += "--clear-database"
}
$publishArgs += "--yes"
Write-Host "[dev:rust] publish SpacetimeDB module: $Database"
& $spacetimeCommand.Source @publishArgs
if ($LASTEXITCODE -ne 0) {
throw "spacetime publish failed, exit code: $LASTEXITCODE"
}
}
$apiEnvironment = @{
GENARRATIVE_API_HOST = $ApiHost
GENARRATIVE_API_PORT = "$ApiPort"
GENARRATIVE_API_LOG = $Log
GENARRATIVE_SPACETIME_SERVER_URL = $spacetimeServer
GENARRATIVE_SPACETIME_DATABASE = $Database
}
$apiProcessItem = Start-StackProcess `
-Name "api-server" `
-FilePath $cargoCommand.Source `
-Arguments @("run", "-p", "api-server", "--manifest-path", $manifestPath) `
-WorkingDirectory $repoRoot `
-Environment $apiEnvironment
$stackProcesses.Add($apiProcessItem)
$webEnvironment = @{
GENARRATIVE_BACKEND_STACK = "rust"
RUST_SERVER_TARGET = $rustServerTarget
GENARRATIVE_RUNTIME_SERVER_TARGET = $rustServerTarget
VITE_DEV_HOST = $WebHost
}
$webProcessItem = Start-StackProcess `
-Name "vite" `
-FilePath $nodeCommand.Source `
-Arguments @($viteCliPath, "--port=$WebPort", "--host=$WebHost") `
-WorkingDirectory $repoRoot `
-Environment $webEnvironment
$stackProcesses.Add($webProcessItem)
Write-Host "[dev:rust] local Rust stack is running. Press Ctrl+C to stop all child processes."
while ($true) {
foreach ($item in $stackProcesses) {
if ($item.Process.HasExited) {
$exitCode = $item.Process.ExitCode
Write-Host "[dev:rust] $($item.Name) exited, code: $exitCode"
throw "Child process exited, shutting down Rust local stack."
}
}
Start-Sleep -Seconds 1
}
}
catch {
if ($exitCode -eq 0) {
$exitCode = 1
}
Write-Host "[dev:rust] $($_.Exception.Message)"
}
finally {
Stop-StackProcesses -Processes $stackProcesses
}
exit $exitCode