Fix tray health detection and harden backend service startup
All checks were successful
CI / test (push) Successful in 7s
All checks were successful
CI / test (push) Successful in 7s
This commit is contained in:
@@ -43,7 +43,38 @@ $publishDir = Join-Path $scriptDir "service_host\publish"
|
|||||||
$serviceExe = Join-Path $publishDir "ScreenJob.WindowsServiceHost.exe"
|
$serviceExe = Join-Path $publishDir "ScreenJob.WindowsServiceHost.exe"
|
||||||
$logDir = Join-Path $scriptDir "screenjob_runs\service"
|
$logDir = Join-Path $scriptDir "screenjob_runs\service"
|
||||||
|
|
||||||
|
$existingService = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
|
||||||
|
if ($null -ne $existingService) {
|
||||||
|
if (-not $ForceReinstall) {
|
||||||
|
throw "Service '$ServiceName' already exists. Re-run with -ForceReinstall to replace it."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($PSCmdlet.ShouldProcess($ServiceName, "Remove existing service")) {
|
||||||
|
if ($existingService.Status -ne "Stopped") {
|
||||||
|
Stop-Service -Name $ServiceName -Force -ErrorAction Stop
|
||||||
|
}
|
||||||
|
|
||||||
|
& sc.exe delete $ServiceName | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Failed to delete existing service '$ServiceName' (sc.exe exit code $LASTEXITCODE)."
|
||||||
|
}
|
||||||
|
|
||||||
|
$deadline = (Get-Date).AddSeconds(15)
|
||||||
|
while ((Get-Date) -lt $deadline) {
|
||||||
|
$stillThere = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
|
||||||
|
if ($null -eq $stillThere) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
Start-Sleep -Milliseconds 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($PSCmdlet.ShouldProcess($projectFile, "Publish Windows service host")) {
|
if ($PSCmdlet.ShouldProcess($projectFile, "Publish Windows service host")) {
|
||||||
|
if (Test-Path -LiteralPath $serviceExe) {
|
||||||
|
Remove-Item -LiteralPath $serviceExe -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
& $dotnetCmd.Source publish `
|
& $dotnetCmd.Source publish `
|
||||||
$projectFile `
|
$projectFile `
|
||||||
-c Release `
|
-c Release `
|
||||||
@@ -63,24 +94,6 @@ if (-not (Test-Path -LiteralPath $serviceExe)) {
|
|||||||
|
|
||||||
$binaryPath = "`"$serviceExe`" --backend-script `"$backendScript`" --working-dir `"$scriptDir`" --log-dir `"$logDir`""
|
$binaryPath = "`"$serviceExe`" --backend-script `"$backendScript`" --working-dir `"$scriptDir`" --log-dir `"$logDir`""
|
||||||
|
|
||||||
$existingService = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
|
|
||||||
if ($null -ne $existingService) {
|
|
||||||
if (-not $ForceReinstall) {
|
|
||||||
throw "Service '$ServiceName' already exists. Re-run with -ForceReinstall to replace it."
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($PSCmdlet.ShouldProcess($ServiceName, "Remove existing service")) {
|
|
||||||
if ($existingService.Status -ne "Stopped") {
|
|
||||||
Stop-Service -Name $ServiceName -Force -ErrorAction Stop
|
|
||||||
}
|
|
||||||
|
|
||||||
& sc.exe delete $ServiceName | Out-Null
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "Failed to delete existing service '$ServiceName' (sc.exe exit code $LASTEXITCODE)."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($PSCmdlet.ShouldProcess($ServiceName, "Create service")) {
|
if ($PSCmdlet.ShouldProcess($ServiceName, "Create service")) {
|
||||||
New-Service `
|
New-Service `
|
||||||
-Name $ServiceName `
|
-Name $ServiceName `
|
||||||
|
|||||||
@@ -28,7 +28,12 @@ function Read-EnvConfig {
|
|||||||
}
|
}
|
||||||
$parts = $trimmed.Split("=", 2)
|
$parts = $trimmed.Split("=", 2)
|
||||||
if ($parts.Count -eq 2) {
|
if ($parts.Count -eq 2) {
|
||||||
$result[$parts[0].Trim()] = $parts[1].Trim()
|
$key = $parts[0].Trim()
|
||||||
|
$value = $parts[1].Trim()
|
||||||
|
if (($value.StartsWith('"') -and $value.EndsWith('"')) -or ($value.StartsWith("'") -and $value.EndsWith("'"))) {
|
||||||
|
$value = $value.Substring(1, $value.Length - 2)
|
||||||
|
}
|
||||||
|
$result[$key] = $value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $result
|
return $result
|
||||||
@@ -79,17 +84,98 @@ function Get-DashboardUrl {
|
|||||||
$envFile = Join-Path $scriptDir ".env"
|
$envFile = Join-Path $scriptDir ".env"
|
||||||
$envVars = Read-EnvConfig -EnvFilePath $envFile
|
$envVars = Read-EnvConfig -EnvFilePath $envFile
|
||||||
|
|
||||||
$host = $defaultHost
|
$dashboardHost = $defaultHost
|
||||||
$port = $defaultPort
|
$dashboardPort = $defaultPort
|
||||||
|
|
||||||
if ($envVars.ContainsKey("SCREENJOB_HOST") -and -not [string]::IsNullOrWhiteSpace($envVars["SCREENJOB_HOST"])) {
|
if ($envVars.ContainsKey("SCREENJOB_HOST") -and -not [string]::IsNullOrWhiteSpace($envVars["SCREENJOB_HOST"])) {
|
||||||
$host = $envVars["SCREENJOB_HOST"]
|
$dashboardHost = $envVars["SCREENJOB_HOST"]
|
||||||
}
|
}
|
||||||
if ($envVars.ContainsKey("SCREENJOB_PORT") -and -not [string]::IsNullOrWhiteSpace($envVars["SCREENJOB_PORT"])) {
|
if ($envVars.ContainsKey("SCREENJOB_PORT") -and -not [string]::IsNullOrWhiteSpace($envVars["SCREENJOB_PORT"])) {
|
||||||
$port = $envVars["SCREENJOB_PORT"]
|
$dashboardPort = $envVars["SCREENJOB_PORT"]
|
||||||
}
|
}
|
||||||
|
|
||||||
return "http://{0}:{1}/" -f $host, $port
|
$connectHost = Resolve-ConnectHost -ConfiguredHost $dashboardHost
|
||||||
|
return "http://{0}:{1}/" -f $connectHost, $dashboardPort
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ConnectHost {
|
||||||
|
param([string]$ConfiguredHost)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ConfiguredHost)) {
|
||||||
|
return "127.0.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($ConfiguredHost.Trim().ToLowerInvariant()) {
|
||||||
|
"0.0.0.0" { return "127.0.0.1" }
|
||||||
|
"::" { return "127.0.0.1" }
|
||||||
|
"*" { return "127.0.0.1" }
|
||||||
|
default { return $ConfiguredHost }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-HealthCheckHosts {
|
||||||
|
param([string]$ConfiguredHost)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ConfiguredHost)) {
|
||||||
|
return @("127.0.0.1", "localhost")
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = $ConfiguredHost.Trim().ToLowerInvariant()
|
||||||
|
switch ($normalized) {
|
||||||
|
"0.0.0.0" { return @("127.0.0.1", "localhost", "::1") }
|
||||||
|
"::" { return @("127.0.0.1", "localhost", "::1") }
|
||||||
|
"*" { return @("127.0.0.1", "localhost", "::1") }
|
||||||
|
default { return @($ConfiguredHost) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-TcpEndpoint {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$HostName,
|
||||||
|
[Parameter(Mandatory = $true)][int]$Port,
|
||||||
|
[int]$TimeoutMs = 1200
|
||||||
|
)
|
||||||
|
|
||||||
|
$client = New-Object System.Net.Sockets.TcpClient
|
||||||
|
try {
|
||||||
|
$async = $client.BeginConnect($HostName, $Port, $null, $null)
|
||||||
|
$connected = $async.AsyncWaitHandle.WaitOne($TimeoutMs, $false)
|
||||||
|
if (-not $connected) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
$client.EndConnect($async) | Out-Null
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
return $false
|
||||||
|
} finally {
|
||||||
|
$client.Dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-BackendReachability {
|
||||||
|
$envFile = Join-Path $scriptDir ".env"
|
||||||
|
$envVars = Read-EnvConfig -EnvFilePath $envFile
|
||||||
|
$configuredHost = $defaultHost
|
||||||
|
$configuredPort = $defaultPort
|
||||||
|
|
||||||
|
if ($envVars.ContainsKey("SCREENJOB_HOST") -and -not [string]::IsNullOrWhiteSpace($envVars["SCREENJOB_HOST"])) {
|
||||||
|
$configuredHost = $envVars["SCREENJOB_HOST"]
|
||||||
|
}
|
||||||
|
if ($envVars.ContainsKey("SCREENJOB_PORT") -and -not [string]::IsNullOrWhiteSpace($envVars["SCREENJOB_PORT"])) {
|
||||||
|
$configuredPort = $envVars["SCREENJOB_PORT"]
|
||||||
|
}
|
||||||
|
|
||||||
|
$portNumber = 8787
|
||||||
|
[void][int]::TryParse([string]$configuredPort, [ref]$portNumber)
|
||||||
|
$hostsToTry = Get-HealthCheckHosts -ConfiguredHost $configuredHost
|
||||||
|
|
||||||
|
foreach ($candidateHost in $hostsToTry) {
|
||||||
|
if (Test-TcpEndpoint -HostName $candidateHost -Port $portNumber) {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $false
|
||||||
}
|
}
|
||||||
|
|
||||||
function Update-TrayState {
|
function Update-TrayState {
|
||||||
@@ -100,9 +186,20 @@ function Update-TrayState {
|
|||||||
)
|
)
|
||||||
|
|
||||||
$status = Get-ServiceStatusSafe -Name $Name
|
$status = Get-ServiceStatusSafe -Name $Name
|
||||||
$StatusItem.Text = "Status: $status"
|
$isBackendReachable = Get-BackendReachability
|
||||||
|
|
||||||
switch ($status) {
|
$displayStatus = $status
|
||||||
|
if ($status -eq "Running" -and -not $isBackendReachable) {
|
||||||
|
$displayStatus = "Running (Backend Down)"
|
||||||
|
} elseif ($status -eq "Stopped" -and $isBackendReachable) {
|
||||||
|
$displayStatus = "Stopped (Backend Up)"
|
||||||
|
} elseif ($status -eq "NotInstalled" -and $isBackendReachable) {
|
||||||
|
$displayStatus = "NotInstalled (Backend Up)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$StatusItem.Text = "Status: $displayStatus"
|
||||||
|
|
||||||
|
switch ($displayStatus) {
|
||||||
"Running" {
|
"Running" {
|
||||||
$NotifyIcon.Icon = [System.Drawing.SystemIcons]::Information
|
$NotifyIcon.Icon = [System.Drawing.SystemIcons]::Information
|
||||||
}
|
}
|
||||||
@@ -114,7 +211,7 @@ function Update-TrayState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$tooltip = "ScreenJob Backend: $status"
|
$tooltip = "ScreenJob Backend: $displayStatus"
|
||||||
if ($tooltip.Length -gt 63) {
|
if ($tooltip.Length -gt 63) {
|
||||||
$tooltip = $tooltip.Substring(0, 63)
|
$tooltip = $tooltip.Substring(0, 63)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,8 +63,12 @@ internal sealed class BackendProcessService : BackgroundService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _backendProcess.WaitForExitAsync(stoppingToken);
|
await _backendProcess.WaitForExitAsync(stoppingToken);
|
||||||
LogStdOut($"Backend process exited with code {_backendProcess.ExitCode}.");
|
var exitCode = _backendProcess.ExitCode;
|
||||||
_logger.LogWarning("Backend process exited with code {ExitCode}.", _backendProcess.ExitCode);
|
LogStdErr($"Backend process exited unexpectedly with code {exitCode}.");
|
||||||
|
_logger.LogError("Backend process exited unexpectedly with code {ExitCode}.", exitCode);
|
||||||
|
Environment.ExitCode = exitCode == 0 ? 1 : exitCode;
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Backend process ended unexpectedly. Service host exit code: {Environment.ExitCode}.");
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,25 +15,76 @@ function Test-EnvVarLine {
|
|||||||
return [bool](Select-String -Path $FilePath -Pattern ("^\s*" + [regex]::Escape($Name) + "=") -Quiet)
|
return [bool](Select-String -Path $FilePath -Pattern ("^\s*" + [regex]::Escape($Name) + "=") -Quiet)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Get-Command python -ErrorAction SilentlyContinue)) {
|
function Resolve-PythonExecutable {
|
||||||
$venvPython = Join-Path $scriptDir ".venv\Scripts\python.exe"
|
$venvPython = Join-Path $scriptDir ".venv\Scripts\python.exe"
|
||||||
if (-not (Test-Path -LiteralPath $venvPython)) {
|
if (Test-Path -LiteralPath $venvPython) {
|
||||||
throw "Python was not found in PATH and .venv\\Scripts\\python.exe is missing. Install Python 3.11+ or create .venv first."
|
return $venvPython
|
||||||
|
}
|
||||||
|
|
||||||
|
$pythonCmd = Get-Command python -ErrorAction SilentlyContinue
|
||||||
|
if ($null -ne $pythonCmd -and (Test-Path -LiteralPath $pythonCmd.Source)) {
|
||||||
|
return $pythonCmd.Source
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidatePyLaunchers = @()
|
||||||
|
$pyFromPath = Get-Command py -ErrorAction SilentlyContinue
|
||||||
|
if ($null -ne $pyFromPath -and (Test-Path -LiteralPath $pyFromPath.Source)) {
|
||||||
|
$candidatePyLaunchers += $pyFromPath.Source
|
||||||
|
}
|
||||||
|
$candidatePyLaunchers += "C:\Windows\py.exe"
|
||||||
|
|
||||||
|
if ($scriptDir -match "^[A-Za-z]:\\Users\\[^\\]+") {
|
||||||
|
$repoUserHome = $Matches[0]
|
||||||
|
$candidatePyLaunchers += (Join-Path $repoUserHome "AppData\Local\Programs\Python\Launcher\py.exe")
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($pyLauncher in ($candidatePyLaunchers | Select-Object -Unique)) {
|
||||||
|
if (-not (Test-Path -LiteralPath $pyLauncher)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$resolved = (& $pyLauncher -3 -c "import sys; print(sys.executable)" 2>$null | Select-Object -Last 1).Trim()
|
||||||
|
if ($resolved -and (Test-Path -LiteralPath $resolved)) {
|
||||||
|
return $resolved
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$pythonExe = $null
|
$candidatePythonPaths = @()
|
||||||
$venvPython = Join-Path $scriptDir ".venv\Scripts\python.exe"
|
if ($scriptDir -match "^[A-Za-z]:\\Users\\[^\\]+") {
|
||||||
if (Test-Path -LiteralPath $venvPython) {
|
$repoUserHome = $Matches[0]
|
||||||
$pythonExe = $venvPython
|
$pythonBase = Join-Path $repoUserHome "AppData\Local\Programs\Python"
|
||||||
} else {
|
if (Test-Path -LiteralPath $pythonBase) {
|
||||||
$pythonCmd = Get-Command python -ErrorAction SilentlyContinue
|
$candidatePythonPaths += (Get-ChildItem -LiteralPath $pythonBase -Directory -ErrorAction SilentlyContinue |
|
||||||
if ($null -eq $pythonCmd) {
|
Sort-Object Name -Descending |
|
||||||
throw "Python was not found in PATH. Install Python 3.11+ or create .venv first."
|
ForEach-Object { Join-Path $_.FullName "python.exe" })
|
||||||
}
|
}
|
||||||
$pythonExe = $pythonCmd.Source
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$candidatePythonPaths += @(
|
||||||
|
"C:\Python314\python.exe",
|
||||||
|
"C:\Python313\python.exe",
|
||||||
|
"C:\Python312\python.exe",
|
||||||
|
"C:\Python311\python.exe",
|
||||||
|
"C:\Program Files\Python314\python.exe",
|
||||||
|
"C:\Program Files\Python313\python.exe",
|
||||||
|
"C:\Program Files\Python312\python.exe",
|
||||||
|
"C:\Program Files\Python311\python.exe"
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($candidate in ($candidatePythonPaths | Select-Object -Unique)) {
|
||||||
|
if (Test-Path -LiteralPath $candidate) {
|
||||||
|
return $candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "Python was not found. Install Python 3.11+ system-wide, or create .venv in the repo root."
|
||||||
|
}
|
||||||
|
|
||||||
|
$pythonExe = Resolve-PythonExecutable
|
||||||
|
|
||||||
$envFile = Join-Path $scriptDir ".env"
|
$envFile = Join-Path $scriptDir ".env"
|
||||||
if (-not (Test-Path -LiteralPath $envFile)) {
|
if (-not (Test-Path -LiteralPath $envFile)) {
|
||||||
Write-Warning ".env was not found at $envFile. Server startup may fail if required vars are missing."
|
Write-Warning ".env was not found at $envFile. Server startup may fail if required vars are missing."
|
||||||
@@ -46,5 +97,5 @@ if (-not (Test-Path -LiteralPath $envFile)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Starting ScreenJob backend on configured host/port..." -ForegroundColor Cyan
|
Write-Host "Starting ScreenJob backend with Python: $pythonExe" -ForegroundColor Cyan
|
||||||
& $pythonExe main.py server
|
& $pythonExe main.py server
|
||||||
|
|||||||
Reference in New Issue
Block a user