|
@@ -1,7 +1,7 @@
|
|
|
[CmdletBinding()]
|
|
[CmdletBinding()]
|
|
|
param(
|
|
param(
|
|
|
[Parameter(Position = 0)]
|
|
[Parameter(Position = 0)]
|
|
|
- [ValidateSet('menu', 'start', 'restart', 'stop', 'status')]
|
|
|
|
|
|
|
+ [ValidateSet('menu', 'start', 'restart', 'stop', 'status', 'publish', 'publish-data')]
|
|
|
[string]$Action = 'menu',
|
|
[string]$Action = 'menu',
|
|
|
|
|
|
|
|
[ValidateSet('split', 'wails')]
|
|
[ValidateSet('split', 'wails')]
|
|
@@ -21,6 +21,7 @@ $FrontendStdout = Join-Path $RunDir 'frontend.stdout.log'
|
|
|
$FrontendStderr = Join-Path $RunDir 'frontend.stderr.log'
|
|
$FrontendStderr = Join-Path $RunDir 'frontend.stderr.log'
|
|
|
$WailsStdout = Join-Path $RunDir 'wails.stdout.log'
|
|
$WailsStdout = Join-Path $RunDir 'wails.stdout.log'
|
|
|
$WailsStderr = Join-Path $RunDir 'wails.stderr.log'
|
|
$WailsStderr = Join-Path $RunDir 'wails.stderr.log'
|
|
|
|
|
+$PublishScript = Join-Path $PSScriptRoot 'publish-release.ps1'
|
|
|
|
|
|
|
|
function Ensure-RunDirectory {
|
|
function Ensure-RunDirectory {
|
|
|
if (-not (Test-Path -LiteralPath $RunDir)) {
|
|
if (-not (Test-Path -LiteralPath $RunDir)) {
|
|
@@ -28,6 +29,24 @@ function Ensure-RunDirectory {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+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 {
|
|
function Read-State {
|
|
|
if (-not (Test-Path -LiteralPath $StatePath)) { return $null }
|
|
if (-not (Test-Path -LiteralPath $StatePath)) { return $null }
|
|
|
try {
|
|
try {
|
|
@@ -55,22 +74,78 @@ function Remove-State {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function Stop-ProcessTree([int]$ProcessId) {
|
|
function Stop-ProcessTree([int]$ProcessId) {
|
|
|
- if ($ProcessId -le 0) { return }
|
|
|
|
|
|
|
+ if ($ProcessId -le 0) { return $true }
|
|
|
$process = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue
|
|
$process = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue
|
|
|
- if ($null -eq $process) { return }
|
|
|
|
|
|
|
+ if ($null -eq $process) { return $true }
|
|
|
Write-Host "Stopping $($process.ProcessName) (PID $ProcessId)"
|
|
Write-Host "Stopping $($process.ProcessName) (PID $ProcessId)"
|
|
|
& taskkill.exe /PID $ProcessId /T /F 2>$null | Out-Null
|
|
& 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 {
|
|
function Stop-ManagedProcesses {
|
|
|
$state = Read-State
|
|
$state = Read-State
|
|
|
if ($null -eq $state) {
|
|
if ($null -eq $state) {
|
|
|
- Write-Host 'No managed project state found.'
|
|
|
|
|
|
|
+ Write-Host 'No managed project state found; checking for a stale project backend on port 8888.'
|
|
|
|
|
+ Stop-StaleBackendProcess
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
foreach ($item in @($state.processes)) {
|
|
foreach ($item in @($state.processes)) {
|
|
|
- Stop-ProcessTree ([int]$item.pid)
|
|
|
|
|
|
|
+ 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
|
|
Remove-State
|
|
|
Write-Host 'Project processes stopped.'
|
|
Write-Host 'Project processes stopped.'
|
|
|
}
|
|
}
|
|
@@ -96,6 +171,18 @@ function Start-SplitMode {
|
|
|
Assert-Command 'go'
|
|
Assert-Command 'go'
|
|
|
Assert-Command 'npm'
|
|
Assert-Command 'npm'
|
|
|
Ensure-RunDirectory
|
|
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...'
|
|
Write-Host 'Building backend...'
|
|
|
Push-Location $Root
|
|
Push-Location $Root
|
|
@@ -108,6 +195,12 @@ function Start-SplitMode {
|
|
|
|
|
|
|
|
$backend = Start-Process -FilePath $BackendExe -WorkingDirectory $Root `
|
|
$backend = Start-Process -FilePath $BackendExe -WorkingDirectory $Root `
|
|
|
-RedirectStandardOutput $BackendStdout -RedirectStandardError $BackendStderr -PassThru
|
|
-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') `
|
|
$frontend = Start-Process -FilePath 'npm.cmd' -ArgumentList @('run', 'dev') `
|
|
|
-WorkingDirectory (Join-Path $Root 'frontend') `
|
|
-WorkingDirectory (Join-Path $Root 'frontend') `
|
|
|
-RedirectStandardOutput $FrontendStdout -RedirectStandardError $FrontendStderr -PassThru
|
|
-RedirectStandardOutput $FrontendStdout -RedirectStandardError $FrontendStderr -PassThru
|
|
@@ -115,11 +208,6 @@ function Start-SplitMode {
|
|
|
[ordered]@{ name = 'backend'; pid = $backend.Id; port = 8888 },
|
|
[ordered]@{ name = 'backend'; pid = $backend.Id; port = 8888 },
|
|
|
[ordered]@{ name = 'frontend'; pid = $frontend.Id; port = 5173 }
|
|
[ordered]@{ name = 'frontend'; pid = $frontend.Id; port = 5173 }
|
|
|
)
|
|
)
|
|
|
-
|
|
|
|
|
- if (-not (Wait-Http 'http://127.0.0.1:8888/health')) {
|
|
|
|
|
- Stop-ManagedProcesses
|
|
|
|
|
- throw "Backend did not become ready. See $BackendStdout and $BackendStderr"
|
|
|
|
|
- }
|
|
|
|
|
Write-Host 'Backend ready: http://127.0.0.1:8888'
|
|
Write-Host 'Backend ready: http://127.0.0.1:8888'
|
|
|
Write-Host 'Frontend: http://localhost:5173'
|
|
Write-Host 'Frontend: http://localhost:5173'
|
|
|
}
|
|
}
|
|
@@ -153,6 +241,19 @@ function Show-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 {
|
|
function Show-Menu {
|
|
|
while ($true) {
|
|
while ($true) {
|
|
|
Clear-Host
|
|
Clear-Host
|
|
@@ -163,6 +264,8 @@ function Show-Menu {
|
|
|
Write-Host '2. Stop project'
|
|
Write-Host '2. Stop project'
|
|
|
Write-Host '3. Restart project'
|
|
Write-Host '3. Restart project'
|
|
|
Write-Host '4. Show status'
|
|
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 '0. Exit'
|
|
|
Write-Host ''
|
|
Write-Host ''
|
|
|
|
|
|
|
@@ -182,6 +285,10 @@ function Show-Menu {
|
|
|
if ($Mode -eq 'wails') { Start-WailsMode } else { Start-SplitMode }
|
|
if ($Mode -eq 'wails') { Start-WailsMode } else { Start-SplitMode }
|
|
|
} elseif ($choice -eq '4') {
|
|
} elseif ($choice -eq '4') {
|
|
|
Show-Status
|
|
Show-Status
|
|
|
|
|
+ } elseif ($choice -eq '5') {
|
|
|
|
|
+ Publish-Release
|
|
|
|
|
+ } elseif ($choice -eq '6') {
|
|
|
|
|
+ Publish-Release $true
|
|
|
} elseif ($choice -eq '0') {
|
|
} elseif ($choice -eq '0') {
|
|
|
return
|
|
return
|
|
|
} else {
|
|
} else {
|
|
@@ -216,4 +323,10 @@ switch ($Action) {
|
|
|
}
|
|
}
|
|
|
if ($Mode -eq 'wails') { Start-WailsMode } else { Start-SplitMode }
|
|
if ($Mode -eq 'wails') { Start-WailsMode } else { Start-SplitMode }
|
|
|
}
|
|
}
|
|
|
|
|
+ 'publish' {
|
|
|
|
|
+ Publish-Release
|
|
|
|
|
+ }
|
|
|
|
|
+ 'publish-data' {
|
|
|
|
|
+ Publish-Release $true
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|