diff --git a/scripts/dev-start.ps1 b/scripts/dev-start.ps1 index 454ddf6..7e954c0 100644 --- a/scripts/dev-start.ps1 +++ b/scripts/dev-start.ps1 @@ -14,11 +14,77 @@ $ErrorActionPreference = 'Stop' $root = Split-Path -Parent $PSScriptRoot $stateDir = Join-Path $PSScriptRoot '.dev' $stateFile = Join-Path $stateDir 'state.json' +$rootPattern = [regex]::Escape($root) $backendLog = Join-Path $stateDir 'backend.log' +$backendErrLog = Join-Path $stateDir 'backend.err.log' $frontendLog = Join-Path $stateDir 'frontend.log' +$frontendErrLog = Join-Path $stateDir 'frontend.err.log' New-Item -ItemType Directory -Force -Path $stateDir | Out-Null +function Invoke-Compose { + param( + [Parameter(Mandatory=$true)][string[]]$Args + ) + + $podmanCompose = Get-Command podman-compose -ErrorAction SilentlyContinue + if ($podmanCompose) { + & podman-compose @Args + return $LASTEXITCODE + } + + $podman = Get-Command podman -ErrorAction SilentlyContinue + if ($podman) { + & podman compose @Args + return $LASTEXITCODE + } + + throw 'Neither podman-compose nor podman was found in PATH.' +} + +function Get-CommandLine { + param( + [Parameter(Mandatory=$true)][int]$ProcessId + ) + + try { + $p = Get-CimInstance Win32_Process -Filter "ProcessId=$ProcessId" -ErrorAction Stop + return $p.CommandLine + } catch { + return $null + } +} + +function Get-ListeningProcessIdsOnPort { + param( + [Parameter(Mandatory=$true)][int]$Port + ) + + $connections = netstat -ano 2>$null | Select-String ":$Port\s+.*LISTENING" + $pids = @() + foreach ($conn in $connections) { + $parts = ($conn.Line -replace '\s+', ' ').Trim().Split(' ') + $listenerPid = 0 + [int]::TryParse($parts[-1], [ref]$listenerPid) | Out-Null + if ($listenerPid -gt 0) { $pids += $listenerPid } + } + return ($pids | Select-Object -Unique) +} + +function Test-LikwidProcess { + param( + [Parameter(Mandatory=$true)][int]$ProcessId + ) + + $cmd = Get-CommandLine -ProcessId $ProcessId + if (-not $cmd) { return $false } + if ($cmd -match $rootPattern) { return $true } + if ($cmd -match 'npm run dev') { return $true } + if ($cmd -match 'astro') { return $true } + if ($cmd -match 'cargo') { return $true } + return $false +} + # Environment if (-not $env:POSTGRES_USER) { $env:POSTGRES_USER = 'likwid' } if (-not $env:POSTGRES_PASSWORD) { $env:POSTGRES_PASSWORD = 'likwid' } @@ -29,22 +95,54 @@ $env:DATABASE_URL = "postgres://$($env:POSTGRES_USER):$($env:POSTGRES_PASSWORD)@ # Check if already running if (Test-Path $stateFile) { $state = Get-Content -Raw $stateFile | ConvertFrom-Json -ErrorAction SilentlyContinue + $backendAlive = $false + $frontendAlive = $false if ($state -and $state.backendPid) { $p = Get-Process -Id $state.backendPid -ErrorAction SilentlyContinue - if ($p) { - Write-Host "Already running (backend PID $($state.backendPid)). Run dev-stop.ps1 first." - exit 0 - } + if ($p) { $backendAlive = $true } } + if ($state -and $state.frontendPid) { + $p = Get-Process -Id $state.frontendPid -ErrorAction SilentlyContinue + if ($p) { $frontendAlive = $true } + } + + if ($backendAlive -or $frontendAlive) { + Write-Host "Already running. Run dev-stop.ps1 first." + exit 0 + } + + Remove-Item -Force $stateFile -ErrorAction SilentlyContinue +} + +# Guard against duplicate starts / port conflicts +foreach ($port in @(3000, 4321)) { + $pids = Get-ListeningProcessIdsOnPort -Port $port + if (-not $pids -or $pids.Count -eq 0) { continue } + + $likwidPid = $pids | Where-Object { Test-LikwidProcess -ProcessId $_ } | Select-Object -First 1 + if ($likwidPid) { + Write-Host "Already running (detected Likwid process PID $likwidPid listening on port $port). Run dev-stop.ps1 first." + exit 0 + } + + $pidList = ($pids -join ', ') + throw "Port $port is already in use by PID(s): $pidList. Stop the process or change ports before starting Likwid." } # Start Podman machine if needed try { $machines = podman machine list --format json 2>$null | ConvertFrom-Json + if (-not $machines) { + throw 'No Podman machines found.' + } $running = $machines | Where-Object { $_.Running -eq $true } if (-not $running) { + $name = ($machines | Select-Object -First 1).Name + if (-not $name) { + throw 'No Podman machine name found.' + } Write-Host "Starting Podman machine..." - podman machine start ($machines | Select-Object -First 1).Name 2>$null + podman machine start $name 2>$null } } catch { Write-Host "Podman machine check failed (may already be running)" @@ -56,8 +154,7 @@ $composeFile = Join-Path $root 'compose/dev.yml' $composeExitCode = 0 try { - podman-compose -f $composeFile up -d 2>$null - $composeExitCode = $LASTEXITCODE + $composeExitCode = Invoke-Compose -Args @('-f', $composeFile, 'up', '-d') } catch { $composeExitCode = $LASTEXITCODE } @@ -70,22 +167,32 @@ if ($composeExitCode -ne 0) { $containerExistsExitCode = $LASTEXITCODE } if ($containerExistsExitCode -ne 0) { - throw "Failed to start PostgreSQL via podman-compose (exit code: $composeExitCode)." + throw "Failed to start PostgreSQL via Podman Compose (exit code: $composeExitCode)." } - Write-Host "Warning: podman-compose returned exit code $composeExitCode, but likwid-postgres exists. Continuing." + Write-Host "Warning: Podman Compose returned exit code $composeExitCode, but likwid-postgres exists. Continuing." } # Wait for PostgreSQL to be ready $maxWait = 30 for ($i = 0; $i -lt $maxWait; $i++) { - $pg = netstat -ano 2>$null | Select-String ':5432.*LISTENING' - if ($pg) { break } + podman exec likwid-postgres pg_isready -U $env:POSTGRES_USER -d $env:POSTGRES_DB 2>$null | Out-Null + if ($LASTEXITCODE -eq 0) { break } Start-Sleep -Seconds 1 } +podman exec likwid-postgres pg_isready -U $env:POSTGRES_USER -d $env:POSTGRES_DB 2>$null | Out-Null +if ($LASTEXITCODE -ne 0) { + Write-Host "PostgreSQL did not become ready." + podman logs --tail 60 likwid-postgres 2>$null + throw "PostgreSQL startup timed out." +} + Write-Host "Running database migrations..." Push-Location (Join-Path $root 'backend') try { + if (-not (Get-Command sqlx -ErrorAction SilentlyContinue)) { + throw "sqlx CLI not found in PATH. Install with: cargo install sqlx-cli --no-default-features --features postgres" + } sqlx migrate run --ignore-missing if ($LASTEXITCODE -ne 0) { throw "Failed to run database migrations (sqlx migrate run)." @@ -97,18 +204,20 @@ try { # Start backend Write-Host "Starting backend..." $backend = Start-Process -FilePath 'cmd.exe' ` - -ArgumentList '/c', 'cargo', 'run', '2>&1' ` + -ArgumentList '/c', 'cargo', 'run' ` -WorkingDirectory (Join-Path $root 'backend') ` -PassThru -WindowStyle Hidden ` - -RedirectStandardOutput $backendLog + -RedirectStandardOutput $backendLog ` + -RedirectStandardError $backendErrLog # Start frontend Write-Host "Starting frontend..." $frontend = Start-Process -FilePath 'cmd.exe' ` - -ArgumentList '/c', "set PUBLIC_API_BASE=http://127.0.0.1:3000&& npm run dev", '2>&1' ` + -ArgumentList '/c', "set PUBLIC_API_BASE=http://127.0.0.1:3000&& npm run dev" ` -WorkingDirectory (Join-Path $root 'frontend') ` -PassThru -WindowStyle Hidden ` - -RedirectStandardOutput $frontendLog + -RedirectStandardOutput $frontendLog ` + -RedirectStandardError $frontendErrLog # Save state @{ diff --git a/scripts/dev-stop.ps1 b/scripts/dev-stop.ps1 index ab79528..26e1a66 100644 --- a/scripts/dev-stop.ps1 +++ b/scripts/dev-stop.ps1 @@ -1,71 +1,119 @@ -<# -.SYNOPSIS - Stops the Likwid development environment. -.DESCRIPTION - Gracefully stops backend, frontend, and PostgreSQL container. -#> -[CmdletBinding()] -param() - -$ErrorActionPreference = 'Continue' - -$root = Split-Path -Parent $PSScriptRoot -$stateDir = Join-Path $PSScriptRoot '.dev' -$stateFile = Join-Path $stateDir 'state.json' - -function Stop-ProcessSafely([int]$ProcessId, [string]$Name) { - if (-not $ProcessId) { return $false } - - $proc = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue - if (-not $proc) { return $false } - - Write-Host "Stopping $Name (PID $ProcessId)..." - Stop-Process -Id $ProcessId -ErrorAction SilentlyContinue - Start-Sleep -Milliseconds 500 - - # Force kill if still running - $proc = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue - if ($proc) { - Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue - } - return $true -} - -function Stop-ProcessOnPort([int]$Port) { - $connections = netstat -ano 2>$null | Select-String ":$Port.*LISTENING" - foreach ($conn in $connections) { - $parts = ($conn.Line -replace '\s+', ' ').Trim().Split(' ') - $procId = [int]$parts[-1] - if ($procId -gt 0) { - Stop-ProcessSafely -ProcessId $procId -Name "process on port $Port" | Out-Null - } - } -} - -Write-Host "=== Stopping Likwid Dev Environment ===" - -# Stop from saved state -if (Test-Path $stateFile) { - $state = Get-Content -Raw $stateFile | ConvertFrom-Json -ErrorAction SilentlyContinue - if ($state) { - Stop-ProcessSafely -ProcessId $state.frontendPid -Name 'Frontend' | Out-Null - Stop-ProcessSafely -ProcessId $state.backendPid -Name 'Backend' | Out-Null - } -} - -# Cleanup any orphaned processes on the ports -Stop-ProcessOnPort -Port 4321 -Stop-ProcessOnPort -Port 3000 - -# Stop PostgreSQL container -Write-Host "Stopping PostgreSQL container..." -$composeFile = Join-Path $root 'compose/dev.yml' -podman-compose -f $composeFile down 2>$null - -# Clean up state file -if (Test-Path $stateFile) { - Remove-Item -Force $stateFile -} - -Write-Host "" -Write-Host "All services stopped." +<# +.SYNOPSIS + Stops the Likwid development environment. +.DESCRIPTION + Gracefully stops backend, frontend, and PostgreSQL container. +#> +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Continue' + +$root = Split-Path -Parent $PSScriptRoot +$stateDir = Join-Path $PSScriptRoot '.dev' +$stateFile = Join-Path $stateDir 'state.json' +$rootPattern = [regex]::Escape($root) + +function Invoke-Compose { + param( + [Parameter(Mandatory=$true)][string[]]$Args + ) + + $podmanCompose = Get-Command podman-compose -ErrorAction SilentlyContinue + if ($podmanCompose) { + & podman-compose @Args + return $LASTEXITCODE + } + + $podman = Get-Command podman -ErrorAction SilentlyContinue + if ($podman) { + & podman compose @Args + return $LASTEXITCODE + } + + throw 'Neither podman-compose nor podman was found in PATH.' +} + +function Get-CommandLine { + param( + [Parameter(Mandatory=$true)][int]$ProcessId + ) + + try { + $p = Get-CimInstance Win32_Process -Filter "ProcessId=$ProcessId" -ErrorAction Stop + return $p.CommandLine + } catch { + return $null + } +} + +function Stop-ProcessSafely([int]$ProcessId, [string]$Name) { + if (-not $ProcessId) { return $false } + + $proc = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue + if (-not $proc) { return $false } + + Write-Host "Stopping $Name (PID $ProcessId)..." + Stop-Process -Id $ProcessId -ErrorAction SilentlyContinue + Start-Sleep -Milliseconds 500 + + # Force kill if still running + $proc = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue + if ($proc) { + Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue + } + return $true +} + +function Stop-ProcessOnPort([int]$Port) { + $connections = netstat -ano 2>$null | Select-String ":$Port.*LISTENING" + foreach ($conn in $connections) { + $parts = ($conn.Line -replace '\s+', ' ').Trim().Split(' ') + $procId = [int]$parts[-1] + if ($procId -gt 0) { + $cmd = Get-CommandLine -ProcessId $procId + $looksLikeLikwid = $false + if ($cmd) { + if ($cmd -match $rootPattern) { $looksLikeLikwid = $true } + if ($cmd -match 'npm run dev') { $looksLikeLikwid = $true } + if ($cmd -match 'astro') { $looksLikeLikwid = $true } + if ($cmd -match 'cargo') { $looksLikeLikwid = $true } + } + + if ($looksLikeLikwid) { + Stop-ProcessSafely -ProcessId $procId -Name "process on port $Port" | Out-Null + } + } + } +} + +Write-Host "=== Stopping Likwid Dev Environment ===" + +# Stop from saved state +if (Test-Path $stateFile) { + $state = Get-Content -Raw $stateFile | ConvertFrom-Json -ErrorAction SilentlyContinue + if ($state) { + Stop-ProcessSafely -ProcessId $state.frontendPid -Name 'Frontend' | Out-Null + Stop-ProcessSafely -ProcessId $state.backendPid -Name 'Backend' | Out-Null + } +} + +# Cleanup any orphaned processes on the ports +Stop-ProcessOnPort -Port 4321 +Stop-ProcessOnPort -Port 3000 + +# Stop PostgreSQL container +Write-Host "Stopping PostgreSQL container..." +$composeFile = Join-Path $root 'compose/dev.yml' +try { + Invoke-Compose -Args @('-f', $composeFile, 'down') | Out-Null +} catch { +} + +# Clean up state file +if (Test-Path $stateFile) { + Remove-Item -Force $stateFile +} + +Write-Host "" +Write-Host "All services stopped."