Fix tray health detection and harden backend service startup
All checks were successful
CI / test (push) Successful in 7s

This commit is contained in:
Space-Banane
2026-05-28 13:44:31 +02:00
parent 114ddd80d6
commit 880bfb1c70
4 changed files with 209 additions and 44 deletions

View File

@@ -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 `

View File

@@ -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)
} }

View File

@@ -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)
{ {

View File

@@ -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