param( [string]$ServiceName = "ScreenJobBackend" ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $controlScript = Join-Path $scriptDir "tray_service_control.ps1" $logsDir = Join-Path $scriptDir "screenjob_runs\service" $defaultHost = "127.0.0.1" $defaultPort = "8787" function Read-EnvConfig { param([string]$EnvFilePath) $result = @{} if (-not (Test-Path -LiteralPath $EnvFilePath)) { return $result } foreach ($line in Get-Content -Path $EnvFilePath) { $trimmed = $line.Trim() if ($trimmed.Length -eq 0 -or $trimmed.StartsWith("#")) { continue } $parts = $trimmed.Split("=", 2) if ($parts.Count -eq 2) { $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 } function Get-ServiceStatusSafe { param([string]$Name) try { $svc = Get-Service -Name $Name -ErrorAction Stop return $svc.Status.ToString() } catch { return "NotInstalled" } } function Invoke-ServiceActionElevated { param( [Parameter(Mandatory = $true)][string]$Action, [Parameter(Mandatory = $true)][string]$Name ) if (-not (Test-Path -LiteralPath $controlScript)) { [System.Windows.Forms.MessageBox]::Show( "Missing control script: $controlScript", "ScreenJob Tray", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error ) | Out-Null return } $argList = @( "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "`"$controlScript`"", "-Action", $Action, "-ServiceName", $Name ) try { Start-Process -FilePath "powershell.exe" -ArgumentList $argList -Verb RunAs -WindowStyle Hidden | Out-Null } catch { # User canceled UAC prompt or launch failed. } } function Get-DashboardUrl { $envFile = Join-Path $scriptDir ".env" $envVars = Read-EnvConfig -EnvFilePath $envFile $dashboardHost = $defaultHost $dashboardPort = $defaultPort if ($envVars.ContainsKey("SCREENJOB_HOST") -and -not [string]::IsNullOrWhiteSpace($envVars["SCREENJOB_HOST"])) { $dashboardHost = $envVars["SCREENJOB_HOST"] } if ($envVars.ContainsKey("SCREENJOB_PORT") -and -not [string]::IsNullOrWhiteSpace($envVars["SCREENJOB_PORT"])) { $dashboardPort = $envVars["SCREENJOB_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 { param( [System.Windows.Forms.NotifyIcon]$NotifyIcon, [System.Windows.Forms.ToolStripMenuItem]$StatusItem, [string]$Name ) $status = Get-ServiceStatusSafe -Name $Name $isBackendReachable = Get-BackendReachability $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" { $NotifyIcon.Icon = [System.Drawing.SystemIcons]::Information } "Stopped" { $NotifyIcon.Icon = [System.Drawing.SystemIcons]::Warning } default { $NotifyIcon.Icon = [System.Drawing.SystemIcons]::Error } } $tooltip = "ScreenJob Backend: $displayStatus" if ($tooltip.Length -gt 63) { $tooltip = $tooltip.Substring(0, 63) } $NotifyIcon.Text = $tooltip } $appContext = New-Object System.Windows.Forms.ApplicationContext $notifyIcon = New-Object System.Windows.Forms.NotifyIcon $notifyIcon.Visible = $false $menu = New-Object System.Windows.Forms.ContextMenuStrip $statusItem = New-Object System.Windows.Forms.ToolStripMenuItem "Status: Unknown" $statusItem.Enabled = $false $refreshItem = New-Object System.Windows.Forms.ToolStripMenuItem "Refresh Status" $refreshItem.Add_Click({ Update-TrayState -NotifyIcon $notifyIcon -StatusItem $statusItem -Name $ServiceName }) $startItem = New-Object System.Windows.Forms.ToolStripMenuItem "Start Service (Admin)" $startItem.Add_Click({ Invoke-ServiceActionElevated -Action "start" -Name $ServiceName }) $stopItem = New-Object System.Windows.Forms.ToolStripMenuItem "Stop Service (Admin)" $stopItem.Add_Click({ Invoke-ServiceActionElevated -Action "stop" -Name $ServiceName }) $restartItem = New-Object System.Windows.Forms.ToolStripMenuItem "Restart Service (Admin)" $restartItem.Add_Click({ Invoke-ServiceActionElevated -Action "restart" -Name $ServiceName }) $dashboardItem = New-Object System.Windows.Forms.ToolStripMenuItem "Open Dashboard" $dashboardItem.Add_Click({ $url = Get-DashboardUrl Start-Process $url | Out-Null }) $logsItem = New-Object System.Windows.Forms.ToolStripMenuItem "Open Service Logs" $logsItem.Add_Click({ if (-not (Test-Path -LiteralPath $logsDir)) { New-Item -ItemType Directory -Path $logsDir -Force | Out-Null } Start-Process explorer.exe $logsDir | Out-Null }) $openFolderItem = New-Object System.Windows.Forms.ToolStripMenuItem "Open Project Folder" $openFolderItem.Add_Click({ Start-Process explorer.exe $scriptDir | Out-Null }) $exitItem = New-Object System.Windows.Forms.ToolStripMenuItem "Exit Tray" $exitItem.Add_Click({ $refreshTimer.Stop() $notifyIcon.Visible = $false $notifyIcon.Dispose() $menu.Dispose() $appContext.ExitThread() }) [void]$menu.Items.Add($statusItem) [void]$menu.Items.Add($refreshItem) [void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) [void]$menu.Items.Add($startItem) [void]$menu.Items.Add($stopItem) [void]$menu.Items.Add($restartItem) [void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) [void]$menu.Items.Add($dashboardItem) [void]$menu.Items.Add($logsItem) [void]$menu.Items.Add($openFolderItem) [void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) [void]$menu.Items.Add($exitItem) $notifyIcon.ContextMenuStrip = $menu $notifyIcon.Visible = $true $notifyIcon.Add_DoubleClick({ $url = Get-DashboardUrl Start-Process $url | Out-Null }) $refreshTimer = New-Object System.Windows.Forms.Timer $refreshTimer.Interval = 5000 $refreshTimer.Add_Tick({ Update-TrayState -NotifyIcon $notifyIcon -StatusItem $statusItem -Name $ServiceName }) Update-TrayState -NotifyIcon $notifyIcon -StatusItem $statusItem -Name $ServiceName $refreshTimer.Start() [System.Windows.Forms.Application]::Run($appContext)