Core/PortManager.ps1
|
# Core\PortManager.ps1 # Centralized port allocation and process tracking for Posh apps $script:PortConfig = @{ RangeStart = 49152 RangeEnd = 49251 Registry = @{} # [int]Port -> @{ App; PID; Started } } # Load persisted registry on module import $script:_portRegistryPath = Join-Path $script:PoshDE.AppDataPath "port-registry.json" if (Test-Path $script:_portRegistryPath) { try { $saved = Get-Content $script:_portRegistryPath -Raw | ConvertFrom-Json foreach ($prop in $saved.PSObject.Properties) { $script:PortConfig.Registry[[int]$prop.Name] = @{ App = $prop.Value.App PID = $prop.Value.PID Started = $prop.Value.Started } } } catch { } } # ── Internal helpers ───────────────────────────────────────────────────────── function Save-PortRegistry { $toSave = @{} foreach ($key in $script:PortConfig.Registry.Keys) { $toSave[[string]$key] = $script:PortConfig.Registry[$key] } $toSave | ConvertTo-Json -Depth 3 | Set-Content $script:_portRegistryPath -Encoding UTF8 } function Test-PortInUse { param([int]$Port) return ($null -ne (Get-NetTCPConnection -LocalPort $Port -ErrorAction SilentlyContinue)) } function Get-ProcessByPort { param([int]$Port) $conn = Get-NetTCPConnection -LocalPort $Port -ErrorAction SilentlyContinue | Select-Object -First 1 if ($conn) { return Get-Process -Id $conn.OwningProcess -ErrorAction SilentlyContinue } return $null } # ── Public functions ───────────────────────────────────────────────────────── function Register-PoshPort { <# .SYNOPSIS Allocate a port from the PoshDE pool for a Posh application. .DESCRIPTION Returns a port number from the ephemeral range (49152-49251). If the app already has a registered port and the process is still alive, that port is returned. Otherwise a new one is allocated. .PARAMETER App Application name (e.g. "PoshConsole"). .EXAMPLE $port = Register-PoshPort -App "PoshConsole" #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$App ) # Reuse existing registration if process is alive foreach ($port in $script:PortConfig.Registry.Keys) { $entry = $script:PortConfig.Registry[$port] if ($entry.App -eq $App) { if ($entry.PID -gt 0) { $proc = Get-Process -Id $entry.PID -ErrorAction SilentlyContinue if ($proc -and -not $proc.HasExited) { Write-Warning "$App already has port $port (PID $($entry.PID))" return $port } } # Dead process — reclaim the slot $script:PortConfig.Registry[$port].PID = 0 $script:PortConfig.Registry[$port].Started = (Get-Date).ToString('o') Save-PortRegistry Write-Verbose "Reclaimed port $port for $App" return $port } } # Find a free port for ($port = $script:PortConfig.RangeStart; $port -le $script:PortConfig.RangeEnd; $port++) { if ($script:PortConfig.Registry.ContainsKey($port)) { continue } if (Test-PortInUse -Port $port) { continue } $script:PortConfig.Registry[$port] = @{ App = $App PID = 0 Started = (Get-Date).ToString('o') } Save-PortRegistry Write-Verbose "Assigned port $port to $App" return $port } Write-Error "No available ports in range $($script:PortConfig.RangeStart)-$($script:PortConfig.RangeEnd)" return $null } function Update-PoshPortPID { <# .SYNOPSIS Associate a process ID with a registered port. .DESCRIPTION Call this after spawning the WebView2 subprocess to record its PID. .EXAMPLE Update-PoshPortPID -Port 49152 -PID $proc.Id #> [CmdletBinding()] param( [Parameter(Mandatory)][int]$Port, [Parameter(Mandatory)][int]$PID ) if ($script:PortConfig.Registry.ContainsKey($Port)) { $script:PortConfig.Registry[$Port].PID = $PID Save-PortRegistry Write-Verbose "Registered PID $PID on port $Port" } else { Write-Warning "Port $Port is not in the registry" } } function Unregister-PoshPort { <# .SYNOPSIS Release a port and optionally kill its associated process. .PARAMETER Port Port number to release. .PARAMETER App App name to release (alternative to -Port). .PARAMETER Kill Kill the associated process before releasing. .EXAMPLE Unregister-PoshPort -App "PoshConsole" -Kill #> [CmdletBinding()] param( [Parameter(ParameterSetName = 'ByPort')][int]$Port, [Parameter(ParameterSetName = 'ByApp')][string]$App, [switch]$Kill ) if ($App) { $Port = 0 foreach ($p in $script:PortConfig.Registry.Keys) { if ($script:PortConfig.Registry[$p].App -eq $App) { $Port = $p; break } } if ($Port -eq 0) { Write-Warning "App '$App' not found in port registry" return } } if (-not $script:PortConfig.Registry.ContainsKey($Port)) { Write-Warning "Port $Port is not registered" return } $entry = $script:PortConfig.Registry[$Port] $appName = $entry.App $pid = $entry.PID if ($Kill -and $pid -gt 0) { $proc = Get-Process -Id $pid -ErrorAction SilentlyContinue if ($proc -and -not $proc.HasExited) { $proc.Kill() $proc.WaitForExit(3000) } # Also kill anything else that grabbed the port $portProc = Get-ProcessByPort -Port $Port if ($portProc -and $portProc.Id -ne $pid) { $portProc.Kill() $portProc.WaitForExit(3000) } } $script:PortConfig.Registry.Remove($Port) Save-PortRegistry Write-Host " Released port $Port ($appName)" -ForegroundColor Yellow } function Get-PoshPortRegistry { <# .SYNOPSIS Show all registered ports with live process status. .EXAMPLE Get-PoshPortRegistry #> [CmdletBinding()] param() if ($script:PortConfig.Registry.Count -eq 0) { Write-Host " No ports registered." -ForegroundColor DarkGray return } foreach ($port in ($script:PortConfig.Registry.Keys | Sort-Object)) { $entry = $script:PortConfig.Registry[$port] $status = 'Unknown' $statusColor = 'DarkGray' if ($entry.PID -eq 0) { $status = 'Starting' $statusColor = 'Yellow' } else { $proc = Get-Process -Id $entry.PID -ErrorAction SilentlyContinue if ($proc -and -not $proc.HasExited) { $status = 'Running' $statusColor = 'Green' } elseif (Test-PortInUse -Port $port) { $status = 'Orphaned' $statusColor = 'Red' } else { $status = 'Dead' $statusColor = 'Red' } } $started = try { ([datetime]$entry.Started).ToString('h:mm:ss tt') } catch { 'Unknown' } [PSCustomObject]@{ Port = $port App = $entry.App PID = if ($entry.PID -gt 0) { $entry.PID } else { '-' } Status = $status Started = $started } } } function Clear-PoshOrphanedPorts { <# .SYNOPSIS Remove registry entries whose processes are no longer running. .PARAMETER Force Also kill any unregistered process found using a port in the PoshDE range. .EXAMPLE Clear-PoshOrphanedPorts #> [CmdletBinding()] param([switch]$Force) Write-Host "" Write-Host " Cleaning orphaned ports..." -ForegroundColor Cyan Write-Host "" $toRemove = @() $killed = 0 foreach ($port in @($script:PortConfig.Registry.Keys)) { $entry = $script:PortConfig.Registry[$port] if ($entry.PID -gt 0) { $proc = Get-Process -Id $entry.PID -ErrorAction SilentlyContinue if (-not $proc -or $proc.HasExited) { Write-Host " [!] Port $port ($($entry.App)) — PID $($entry.PID) is dead" -ForegroundColor Yellow $toRemove += $port $portProc = Get-ProcessByPort -Port $port if ($portProc) { Write-Host " Killing orphan $($portProc.Id) ($($portProc.ProcessName))" -ForegroundColor Red $portProc.Kill(); $portProc.WaitForExit(3000) $killed++ } } } } foreach ($port in $toRemove) { $script:PortConfig.Registry.Remove($port) } if ($Force) { for ($port = $script:PortConfig.RangeStart; $port -le $script:PortConfig.RangeEnd; $port++) { if (-not $script:PortConfig.Registry.ContainsKey($port)) { $proc = Get-ProcessByPort -Port $port if ($proc) { Write-Host " [!] Port $port — unregistered process $($proc.Id) ($($proc.ProcessName))" -ForegroundColor Yellow $proc.Kill(); $proc.WaitForExit(3000) $killed++ } } } } if ($toRemove.Count -gt 0 -or $killed -gt 0) { Save-PortRegistry Write-Host "" Write-Host " Removed: $($toRemove.Count) entries Killed: $killed processes" -ForegroundColor Green } else { Write-Host " No orphaned ports found." -ForegroundColor Green } Write-Host "" } function Stop-PoshApp { <# .SYNOPSIS Stop a named Posh application and release its port. .EXAMPLE Stop-PoshApp -Name PoshConsole #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Name ) Unregister-PoshPort -App $Name -Kill } function Stop-AllPoshApps { <# .SYNOPSIS Stop every running Posh application. #> [CmdletBinding()] param() Write-Host "" Write-Host " Stopping all Posh apps..." -ForegroundColor Cyan foreach ($port in @($script:PortConfig.Registry.Keys)) { Unregister-PoshPort -Port $port -Kill } Write-Host " Done." -ForegroundColor Green Write-Host "" } # ── Startup orphan notification ────────────────────────────────────────────── $_orphans = 0 foreach ($port in @($script:PortConfig.Registry.Keys)) { $e = $script:PortConfig.Registry[$port] if ($e.PID -gt 0) { $p = Get-Process -Id $e.PID -ErrorAction SilentlyContinue if (-not $p -or $p.HasExited) { $_orphans++ } } } if ($_orphans -gt 0) { Write-Host " [!] $($_orphans) orphaned port(s) detected — run " -NoNewline -ForegroundColor Yellow Write-Host "Clear-PoshOrphanedPorts" -ForegroundColor Cyan } Write-Verbose "PortManager loaded" |