| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332 |
- [CmdletBinding()]
- param(
- [Parameter(Position = 0)]
- [ValidateSet('menu', 'start', 'restart', 'stop', 'status', 'publish', 'publish-data')]
- [string]$Action = 'menu',
- [ValidateSet('split', 'wails')]
- [string]$Mode = 'split'
- )
- $ErrorActionPreference = 'Stop'
- $Root = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
- $RunDir = Join-Path $Root '.run'
- $StatePath = Join-Path $RunDir 'project-processes.json'
- $BackendExe = Join-Path $RunDir 'smart-parking-backend.exe'
- $WailsGoCache = Join-Path $Root '.gocache-wails'
- $BackendBuildLog = Join-Path $RunDir 'backend-build.log'
- $BackendStdout = Join-Path $RunDir 'backend.stdout.log'
- $BackendStderr = Join-Path $RunDir 'backend.stderr.log'
- $FrontendStdout = Join-Path $RunDir 'frontend.stdout.log'
- $FrontendStderr = Join-Path $RunDir 'frontend.stderr.log'
- $WailsStdout = Join-Path $RunDir 'wails.stdout.log'
- $WailsStderr = Join-Path $RunDir 'wails.stderr.log'
- $PublishScript = Join-Path $PSScriptRoot 'publish-release.ps1'
- function Ensure-RunDirectory {
- if (-not (Test-Path -LiteralPath $RunDir)) {
- New-Item -ItemType Directory -Path $RunDir | Out-Null
- }
- }
- function Initialize-LogFile([string]$Path) {
- try {
- Set-Content -LiteralPath $Path -Value '' -Encoding UTF8 -ErrorAction Stop
- return $Path
- } catch {
- # A previous npm.cmd wrapper can retain its redirected file handle even
- # after Vite exits. Keep the old log intact and use a new file for this
- # launch instead of making the project impossible to restart.
- $directory = Split-Path -Parent $Path
- $name = [System.IO.Path]::GetFileNameWithoutExtension($Path)
- $extension = [System.IO.Path]::GetExtension($Path)
- $fallback = Join-Path $directory ("{0}.{1}{2}" -f $name, (Get-Date -Format 'yyyyMMdd-HHmmss'), $extension)
- Set-Content -LiteralPath $fallback -Value '' -Encoding UTF8
- Write-Warning "Log file is locked: $Path. This launch will write to: $fallback"
- return $fallback
- }
- }
- function Read-State {
- if (-not (Test-Path -LiteralPath $StatePath)) { return $null }
- try {
- return Get-Content -LiteralPath $StatePath -Raw | ConvertFrom-Json
- } catch {
- Write-Warning "Ignoring invalid state file: $StatePath"
- return $null
- }
- }
- function Write-State([string]$RunMode, [array]$Processes) {
- Ensure-RunDirectory
- [ordered]@{
- root = $Root
- mode = $RunMode
- started_at = (Get-Date).ToString('o')
- processes = $Processes
- } | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $StatePath -Encoding UTF8
- }
- function Remove-State {
- if (Test-Path -LiteralPath $StatePath) {
- Remove-Item -LiteralPath $StatePath -Force
- }
- }
- function Stop-ProcessTree([int]$ProcessId) {
- if ($ProcessId -le 0) { return $true }
- $process = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue
- if ($null -eq $process) { return $true }
- Write-Host "Stopping $($process.ProcessName) (PID $ProcessId)"
- & taskkill.exe /PID $ProcessId /T /F 2>$null | Out-Null
- Start-Sleep -Milliseconds 300
- return $null -eq (Get-Process -Id $ProcessId -ErrorAction SilentlyContinue)
- }
- function Stop-PortProcess([int]$Port) {
- # npm.cmd exits after handing the server to node, so the state-file PID may
- # no longer own the listening socket. Clean up only this project's ports.
- $connections = @(Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue)
- foreach ($connection in $connections) {
- $owner = [int]$connection.OwningProcess
- if ($owner -gt 0) {
- if (-not (Stop-ProcessTree $owner)) {
- throw "Failed to stop process PID $owner listening on port $Port."
- }
- }
- }
- }
- function Stop-StaleBackendProcess {
- # project-processes.json may be missing after an interrupted PowerShell
- # session. In that case, still reclaim 8888 only when its owner is this
- # project's backend executable. Do not terminate an unrelated service.
- $connections = @(Get-NetTCPConnection -LocalPort 8888 -State Listen -ErrorAction SilentlyContinue)
- foreach ($connection in $connections) {
- $process = Get-Process -Id ([int]$connection.OwningProcess) -ErrorAction SilentlyContinue
- if ($null -eq $process) { continue }
- $isProjectBackend = $false
- if ($process.ProcessName -eq 'smart-parking-backend' -and $process.Path) {
- $isProjectBackend = [string]::Equals(
- [System.IO.Path]::GetFullPath($process.Path),
- [System.IO.Path]::GetFullPath($BackendExe),
- [System.StringComparison]::OrdinalIgnoreCase
- )
- }
- if (-not $isProjectBackend) {
- throw "Port 8888 is occupied by $($process.ProcessName) (PID $($process.Id), path: $($process.Path)). It is not this project's managed backend, so it was not stopped."
- }
- if (-not (Stop-ProcessTree $process.Id)) {
- throw "Failed to stop stale project backend PID $($process.Id) on port 8888."
- }
- }
- }
- function Stop-ManagedProcesses {
- $state = Read-State
- if ($null -eq $state) {
- Write-Host 'No managed project state found; checking for a stale project backend on port 8888.'
- Stop-StaleBackendProcess
- return
- }
- foreach ($item in @($state.processes)) {
- if (-not (Stop-ProcessTree ([int]$item.pid))) {
- # npm.cmd can remain as an inert wrapper after its node child has
- # stopped. The listening port check below is the authoritative
- # result, so do not prevent a restart merely because that wrapper
- # cannot be reaped immediately.
- Write-Warning "Process $($item.name) (PID $($item.pid)) is still present; verifying port $($item.port)."
- }
- }
- foreach ($item in @($state.processes)) {
- Stop-PortProcess ([int]$item.port)
- }
- # A previous launch can leave an untracked backend after the state file is
- # removed. Clean it after the tracked processes as well.
- Stop-StaleBackendProcess
- Remove-State
- Write-Host 'Project processes stopped.'
- }
- function Assert-Command([string]$Name) {
- if ($null -eq (Get-Command $Name -ErrorAction SilentlyContinue)) {
- throw "Required command not found: $Name"
- }
- }
- function Wait-Http([string]$Url, [int]$TimeoutSeconds = 30) {
- for ($i = 0; $i -lt $TimeoutSeconds; $i++) {
- try {
- $response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 2 -ErrorAction Stop
- if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500) { return $true }
- } catch { }
- Start-Sleep -Seconds 1
- }
- return $false
- }
- function Start-SplitMode {
- Assert-Command 'go'
- Assert-Command 'npm'
- Ensure-RunDirectory
- Stop-StaleBackendProcess
- # RedirectStandardOutput/RedirectStandardError may append on some
- # PowerShell versions. Start every launch with fresh logs so an old panic
- # is never reported as the current startup result.
- $BackendStdout = Initialize-LogFile $BackendStdout
- $BackendStderr = Initialize-LogFile $BackendStderr
- $FrontendStdout = Initialize-LogFile $FrontendStdout
- $FrontendStderr = Initialize-LogFile $FrontendStderr
- # Keep Go build artifacts in the project workspace. The default user cache
- # can be inaccessible on locked-down Windows installations.
- New-Item -ItemType Directory -Path $WailsGoCache -Force | Out-Null
- $env:GOCACHE = $WailsGoCache
- Write-Host 'Building backend...'
- Push-Location $Root
- try {
- & go build -o $BackendExe .\cmd_backend_only\ 2>&1 | Tee-Object -FilePath $BackendBuildLog
- if ($LASTEXITCODE -ne 0) { throw 'Backend build failed.' }
- } finally {
- Pop-Location
- }
- $backend = Start-Process -FilePath $BackendExe -WorkingDirectory $Root `
- -RedirectStandardOutput $BackendStdout -RedirectStandardError $BackendStderr -PassThru
- if (-not (Wait-Http 'http://127.0.0.1:8888/health')) {
- Stop-ProcessTree $backend.Id | Out-Null
- throw "Backend did not become ready. See $BackendStdout and $BackendStderr"
- }
- $frontend = Start-Process -FilePath 'npm.cmd' -ArgumentList @('run', 'dev') `
- -WorkingDirectory (Join-Path $Root 'frontend') `
- -RedirectStandardOutput $FrontendStdout -RedirectStandardError $FrontendStderr -PassThru
- Write-State 'split' @(
- [ordered]@{ name = 'backend'; pid = $backend.Id; port = 8888 },
- [ordered]@{ name = 'frontend'; pid = $frontend.Id; port = 5173 }
- )
- Write-Host 'Backend ready: http://127.0.0.1:8888'
- Write-Host 'Frontend: http://localhost:5173'
- }
- function Start-WailsMode {
- Assert-Command 'wails'
- Ensure-RunDirectory
- New-Item -ItemType Directory -Path $WailsGoCache -Force | Out-Null
- # Keep Wails/Go build artifacts in the project workspace. The default
- # user cache can be inaccessible on locked-down Windows installations.
- $env:GOCACHE = $WailsGoCache
- $wails = Start-Process -FilePath 'wails' -ArgumentList @('dev') `
- -WorkingDirectory $Root -RedirectStandardOutput $WailsStdout `
- -RedirectStandardError $WailsStderr -PassThru
- Write-State 'wails' @([ordered]@{ name = 'wails'; pid = $wails.Id; port = 5173 })
- Write-Host "Wails dev started (PID $($wails.Id)). See $WailsStdout and $WailsStderr"
- }
- function Show-Status {
- $state = Read-State
- if ($null -eq $state) {
- Write-Host 'Project is not managed by this script.'
- return
- }
- Write-Host "Mode: $($state.mode)"
- Write-Host "Started: $($state.started_at)"
- foreach ($item in @($state.processes)) {
- $process = Get-Process -Id ([int]$item.pid) -ErrorAction SilentlyContinue
- $status = if ($null -eq $process) { 'stopped' } else { 'running' }
- Write-Host ("{0}: PID {1}, port {2}, {3}" -f $item.name, $item.pid, $item.port, $status)
- }
- }
- function Publish-Release([bool]$IncludeExistingData = $false) {
- if (-not (Test-Path -LiteralPath $PublishScript -PathType Leaf)) {
- throw "Publish script not found: $PublishScript"
- }
- Write-Host 'Building deployment package...' -ForegroundColor Cyan
- if ($IncludeExistingData) {
- & $PublishScript -IncludeDatabase -IncludeUploads
- } else {
- & $PublishScript
- }
- }
- function Show-Menu {
- while ($true) {
- Clear-Host
- Write-Host '========================================' -ForegroundColor Cyan
- Write-Host ' Smart Parking Project Manager' -ForegroundColor Cyan
- Write-Host '========================================'
- Write-Host '1. Start project'
- Write-Host '2. Stop project'
- Write-Host '3. Restart project'
- Write-Host '4. Show status'
- Write-Host '5. Build deployment package'
- Write-Host '6. Build deployment package with current database and images'
- Write-Host '0. Exit'
- Write-Host ''
- $choice = Read-Host 'Select action'
- if ($choice -eq '1') {
- if ($null -ne (Read-State)) {
- Write-Host 'Project is already running; use stop or restart.' -ForegroundColor Yellow
- } elseif ($Mode -eq 'wails') {
- Start-WailsMode
- } else {
- Start-SplitMode
- }
- } elseif ($choice -eq '2') {
- Stop-ManagedProcesses
- } elseif ($choice -eq '3') {
- Stop-ManagedProcesses
- if ($Mode -eq 'wails') { Start-WailsMode } else { Start-SplitMode }
- } elseif ($choice -eq '4') {
- Show-Status
- } elseif ($choice -eq '5') {
- Publish-Release
- } elseif ($choice -eq '6') {
- Publish-Release $true
- } elseif ($choice -eq '0') {
- return
- } else {
- Write-Host 'Invalid option.' -ForegroundColor Red
- }
- if ($choice -ne '0') {
- Write-Host ''
- Read-Host 'Press Enter to return to menu'
- }
- }
- }
- switch ($Action) {
- 'menu' {
- Show-Menu
- }
- 'stop' {
- Stop-ManagedProcesses
- }
- 'status' {
- Show-Status
- }
- 'restart' {
- Stop-ManagedProcesses
- if ($Mode -eq 'wails') { Start-WailsMode } else { Start-SplitMode }
- }
- 'start' {
- if ($null -ne (Read-State)) {
- Write-Host 'A managed instance already exists; use restart or stop first.'
- exit 1
- }
- if ($Mode -eq 'wails') { Start-WailsMode } else { Start-SplitMode }
- }
- 'publish' {
- Publish-Release
- }
- 'publish-data' {
- Publish-Release $true
- }
- }
|