diff --git a/Borealis.ps1 b/Borealis.ps1 index e287193..cd81507 100644 --- a/Borealis.ps1 +++ b/Borealis.ps1 @@ -1,19 +1,4 @@ -#////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: /Launch-Borealis.ps1 - -<# - Borealis.ps1 - ---------------------- - This script deploys the Borealis Workflow Automation Tool with three modules: - - Server (Web Dashboard) - - Agent (Client / Data Collector) - - Desktop App (Electron) - - It begins by presenting a menu to the user. Based on the selection, - the corresponding module is launched or deployed. - - Usage: - Set-ExecutionPolicy Unrestricted -Scope Process; .\Borealis.ps1 -#> +#////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: /Borealis.ps1 [CmdletBinding()] param( @@ -62,20 +47,6 @@ if ($Server) { $host.UI.RawUI.WindowTitle = "Borealis" Clear-Host -# ASCII Art Banner -@' - -███████████ ████ ███ -░░███░░░░░███ ░░███ ░░░ - ░███ ░███ ██████ ████████ ██████ ██████ ░███ ████ █████ - ░██████████ ███░░███░░███░░███ ███░░███ ░░░░░███ ░███ ░░███ ███░░ - ░███░░░░░███░███ ░███ ░███ ░░░ ░███████ ███████ ░███ ░███ ░░█████ - ░███ ░███░███ ░███ ░███ ░███░░░ ███░░███ ░███ ░███ ░░░░███ - ███████████ ░░██████ █████ ░░██████ ░░████████ █████ █████ ██████ -░░░░░░░░░░░ ░░░░░░ ░░░░░ ░░░░░░ ░░░░░░░░ ░░░░░ ░░░░░ ░░░░░░ -'@ | Write-Host -ForegroundColor DarkCyan -Write-Host "Automation Platform" -ForegroundColor DarkGray -Write-Host " " ## Note: Heavy dependency downloads are deferred until selecting Server (option 1) # ---------------------- ASCII Art Terminal Required Changes ---------------------- # Set the .NET Console output encoding to UTF8 @@ -132,66 +103,8 @@ function Remove-BorealisServicesAndTasks { function Repair-BorealisAgent { $logName = 'Repair.log' Write-AgentLog -FileName $logName -Message "=== Repair start ===" - $agentDir = Join-Path $scriptDir 'Agent' - $venvPython = Join-Path $agentDir 'Scripts\python.exe' - $deployScript = Join-Path $agentDir 'Borealis\agent_deployment.py' - - # Aggressive cleanup first Remove-BorealisServicesAndTasks -LogName $logName - Start-Sleep -Seconds 1 - - # Ensure venv and files exist by reusing the install block - Write-AgentLog -FileName $logName -Message "Ensuring agent venv and files" - $venvFolder = 'Agent' - $agentSourcePath = 'Data\Agent\borealis-agent.py' - $agentRequirements = 'Data\Agent\agent-requirements.txt' - $agentDestinationFolder = "$venvFolder\Borealis" - $venvPythonPath = Join-Path $scriptDir $venvFolder | Join-Path -ChildPath 'Scripts\python.exe' - - if (-not (Test-Path "$venvFolder\Scripts\Activate")) { - $pythonForVenv = $pythonExe - if (-not (Test-Path $pythonForVenv)) { - $pyCmd = Get-Command py -ErrorAction SilentlyContinue - $pythonCmd = Get-Command python -ErrorAction SilentlyContinue - if ($pyCmd) { $pythonForVenv = $pyCmd.Source } - elseif ($pythonCmd) { $pythonForVenv = $pythonCmd.Source } - else { throw "Python not found. Please run Server setup (option 1) first." } - } - Write-AgentLog -FileName $logName -Message "Creating venv using: $pythonForVenv" - & $pythonForVenv -m venv $venvFolder | Out-Null - } - - if (Test-Path $agentSourcePath) { - Write-AgentLog -FileName $logName -Message "Refreshing Agent/Borealis files" - Remove-Item $agentDestinationFolder -Recurse -Force -ErrorAction SilentlyContinue - New-Item -Path $agentDestinationFolder -ItemType Directory -Force | Out-Null - Copy-Item 'Data\Agent\borealis-agent.py' $agentDestinationFolder -Recurse - Copy-Item 'Data\Agent\agent_info.py' $agentDestinationFolder -Recurse - Copy-Item 'Data\Agent\agent_roles.py' $agentDestinationFolder -Recurse - Copy-Item 'Data\Agent\Python_API_Endpoints' $agentDestinationFolder -Recurse - Copy-Item 'Data\Agent\windows_script_service.py' $agentDestinationFolder -Force - Copy-Item 'Data\Agent\agent_deployment.py' $agentDestinationFolder -Force - Copy-Item 'Data\Agent\tray_launcher.py' $agentDestinationFolder -Force - if (Test-Path 'Data\Agent\Borealis.ico') { Copy-Item 'Data\Agent\Borealis.ico' $agentDestinationFolder -Force } - } - - . "$venvFolder\Scripts\Activate" - if (Test-Path $agentRequirements) { - Write-AgentLog -FileName $logName -Message "Installing agent requirements" - & $venvPythonPath -m pip install --disable-pip-version-check -q -r $agentRequirements | Out-Null - } - - # Install service via deployment helper (sets PythonHome/PythonPath) - Write-AgentLog -FileName $logName -Message "Running agent_deployment.py ensure-all" - $deployScript = Join-Path $agentDestinationFolder 'agent_deployment.py' - & $venvPythonPath -W ignore::SyntaxWarning $deployScript ensure-all *>&1 | Tee-Object -FilePath (Join-Path (Ensure-AgentLogDir) $logName) -Append | Out-Null - - # Start the service explicitly - Write-AgentLog -FileName $logName -Message "Starting service BorealisAgent" - try { sc.exe start BorealisAgent 2>$null | Out-Null } catch {} - Start-Sleep -Seconds 2 - Write-AgentLog -FileName $logName -Message "Query service state" - sc.exe query BorealisAgent *>> (Join-Path (Ensure-AgentLogDir) $logName) + InstallOrUpdate-BorealisAgent Write-AgentLog -FileName $logName -Message "=== Repair end ===" } @@ -411,328 +324,121 @@ function Install_Agent_Dependencies { } } -#<# ---------------------- Agent Service Helper Functions (Windows) [Deprecated: superseded by Data/Agent/agent_deployment.py] ---------------------- -function Ensure-BorealisAgent-Service { +function Ensure-AgentTasks { + # Registers SYSTEM Supervisor + 5-min Watchdog via UAC-elevated stub param( - [string]$VenvPython, - [string]$ServiceScript + [string]$ScriptRoot ) - if (-not (Test-IsWindows)) { - Write-Host "Agent service setup skipped (non-Windows)." -ForegroundColor DarkGray - return - } + $supName = 'Borealis Agent - Supervisor' + $py = Join-Path $ScriptRoot 'Agent\Scripts\python.exe' + $supScript= Join-Path $ScriptRoot 'Data\Agent\agent_supervisor.py' + $wdName = 'Borealis Agent - Watchdog' - $serviceName = 'BorealisAgent' - $svc = Get-CimInstance -ClassName Win32_Service -Filter "Name='$serviceName'" -ErrorAction SilentlyContinue - $needsInstall = $false - if (-not $svc) { $needsInstall = $true } - else { - if ($svc.StartMode -notin @('Auto','Automatic')) { $needsInstall = $true } - if ($svc.StartName -ne 'LocalSystem') { $needsInstall = $true } - # Verify the service points to the current project venv (folder may have moved) - try { - $venvRoot = Split-Path $VenvPython -Parent - $pathName = $svc.PathName - if ($pathName -and $venvRoot) { - if ($pathName -notlike ("*" + $venvRoot + "*")) { $needsInstall = $true } + $taskStub = @" +$ErrorActionPreference='Continue' +$sup = '$supName' +$py = "$py" +$supScr = "$supScript" +$wd = '$wdName' + +try { Unregister-ScheduledTask -TaskName $sup -Confirm:$false -ErrorAction SilentlyContinue } catch {} +$a = New-ScheduledTaskAction -Execute $py -Argument ('-W ignore::SyntaxWarning "' + $supScr + '"') +$t = New-ScheduledTaskTrigger -AtStartup +$s = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -Hidden -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) +$p = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest +Register-ScheduledTask -TaskName $sup -Action $a -Trigger $t -Settings $s -Principal $p -Force | Out-Null + +$wdScript = @" +$ErrorActionPreference='SilentlyContinue' +if (-not (Get-ScheduledTask -TaskName "$sup" -ErrorAction SilentlyContinue)) { exit } +$st = (Get-ScheduledTask -TaskName "$sup").State +if ($st -eq 'Disabled') { Enable-ScheduledTask -TaskName "$sup" | Out-Null } +if ($st -ne 'Running') { Start-ScheduledTask -TaskName "$sup" | Out-Null } +"@ +$wdFile = Join-Path $env:ProgramData 'Borealis\watchdog.ps1' +New-Item -ItemType Directory -Force -Path (Split-Path $wdFile -Parent) | Out-Null +Set-Content -Path $wdFile -Value $wdScript -Encoding UTF8 +try { Unregister-ScheduledTask -TaskName $wd -Confirm:$false -ErrorAction SilentlyContinue } catch {} +$wa = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument ('-NoProfile -ExecutionPolicy Bypass -File "' + $wdFile + '"') +$wt = New-ScheduledTaskTrigger -Once -At ([datetime]::Now.AddMinutes(1)) -RepetitionInterval (New-TimeSpan -Minutes 5) -RepetitionDuration (New-TimeSpan -Days 365) +$ws = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -Hidden +Register-ScheduledTask -TaskName $wd -Action $wa -Trigger $wt -Settings $ws -Principal $p -Force | Out-Null +Start-ScheduledTask -TaskName $sup | Out-Null +@" + $tmpElev = New-TemporaryFile + Set-Content -Path $tmpElev.FullName -Value $taskStub -Encoding UTF8 + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = 'powershell.exe' + $psi.Verb = 'runas' + $psi.ArgumentList = @('-NoProfile','-ExecutionPolicy','Bypass','-File', $tmpElev.FullName) + $psi.UseShellExecute = $true + try { $proc = [System.Diagnostics.Process]::Start($psi); $proc.WaitForExit() } catch {} + Remove-Item $tmpElev.FullName -Force -ErrorAction SilentlyContinue +"@ +} +function InstallOrUpdate-BorealisAgent { + Write-Host "Ensuring Agent Dependencies Exist..." -ForegroundColor DarkCyan + Install_Shared_Dependencies + Install_Agent_Dependencies + if (-not (Test-Path $pythonExe)) { + Write-Host "`r$($symbols.Fail) Bundled Python not found at '$pythonExe'." -ForegroundColor Red + exit 1 + } + $env:PATH = '{0};{1}' -f (Split-Path $pythonExe), $env:PATH + Write-Host "Deploying Borealis Agent..." -ForegroundColor Blue + + $venvFolder = "Agent" + $agentSourcePath = "Data\Agent\borealis-agent.py" + $agentRequirements = "Data\Agent\agent-requirements.txt" + $agentDestinationFolder = "$venvFolder\Borealis" + $agentDestinationFile = "$venvFolder\Borealis\borealis-agent.py" + $venvPython = Join-Path $scriptDir $venvFolder | Join-Path -ChildPath 'Scripts\python.exe' + + Run-Step "Create Virtual Python Environment" { + if (-not (Test-Path "$venvFolder\Scripts\Activate")) { + $pythonForVenv = $pythonExe + if (-not (Test-Path $pythonForVenv)) { + $pyCmd = Get-Command py -ErrorAction SilentlyContinue + $pythonCmd = Get-Command python -ErrorAction SilentlyContinue + if ($pyCmd) { $pythonForVenv = $pyCmd.Source } + elseif ($pythonCmd) { $pythonForVenv = $pythonCmd.Source } + else { + Write-Host "Python not found. Install Python or run Server setup (option 1)." -ForegroundColor Red + exit 1 + } } - } catch {} + & $pythonForVenv -m venv $venvFolder + } + if (Test-Path $agentSourcePath) { + Remove-Item $agentDestinationFolder -Recurse -Force -ErrorAction SilentlyContinue + New-Item -Path $agentDestinationFolder -ItemType Directory -Force | Out-Null + Copy-Item "Data\Agent\borealis-agent.py" $agentDestinationFolder -Recurse + Copy-Item "Data\Agent\agent_info.py" $agentDestinationFolder -Recurse + Copy-Item "Data\Agent\agent_roles.py" $agentDestinationFolder -Recurse + Copy-Item "Data\Agent\Python_API_Endpoints" $agentDestinationFolder -Recurse + Copy-Item "Data\Agent\agent_deployment.py" $agentDestinationFolder -Force + Copy-Item "Data\Agent\tray_launcher.py" $agentDestinationFolder -Force + if (Test-Path "Data\Agent\Borealis.ico") { Copy-Item "Data\Agent\Borealis.ico" $agentDestinationFolder -Force } + } + . "$venvFolder\Scripts\Activate" } - if ($needsInstall) { - if (-not (Test-IsAdmin)) { - Write-Host "Admin rights required to install/update the agent service. Prompting UAC..." -ForegroundColor Yellow - $tmp = New-TemporaryFile - $logDir = Join-Path $scriptDir 'Logs' - if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } - $log = Join-Path $logDir 'Borealis_AgentService_Install.log' - $vEsc = $VenvPython.Replace('"','""') - $sEsc = $ServiceScript.Replace('"','""') - $content = @" -$ErrorActionPreference = 'Continue' -$venv = "$vEsc" -$srv = "$sEsc" -$logPath = "$log" -try { - try { New-Item -ItemType Directory -Force -Path (Split-Path $logPath -Parent) | Out-Null } catch {} - # Ensure pywin32 postinstall is applied in this venv (required for services) - try { - & $venv -m pywin32_postinstall -install *>&1 | Tee-Object -FilePath $logPath -Append - } catch { } - "[INFO] Installing service via: $venv $srv --startup auto install" | Out-File -FilePath $logPath -Encoding UTF8 - & $venv $srv --startup auto install *>&1 | Tee-Object -FilePath $logPath -Append - try { sc.exe config BorealisAgent obj= LocalSystem | Tee-Object -FilePath $logPath -Append } catch {} - $code = $LASTEXITCODE - if ($code -eq $null) { $code = 0 } - "[INFO] Exit code: $code" | Tee-Object -FilePath $logPath -Append | Out-Host -} catch { - "[ERROR] $_" | Tee-Object -FilePath $logPath -Append | Out-Host - $code = 1 -} finally { - if ($code -ne 0 -or $env:BOREALIS_DEBUG_UAC -eq '1') { - Read-Host 'Press Enter to close this elevated window...' - } - exit $code -} -"@ - # pass key vars via env so the elevated scope can see them - $psi = New-Object System.Diagnostics.ProcessStartInfo - $psi.FileName = 'powershell.exe' - $psi.Verb = 'runas' - $psi.Arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$($tmp.FullName)`"" - $psi.UseShellExecute = $true - $psi.WindowStyle = 'Normal' - Set-Content -Path $tmp.FullName -Value $content -Force -Encoding UTF8 - $proc = [System.Diagnostics.Process]::Start($psi) - $proc.WaitForExit() - Remove-Item $tmp.FullName -Force -ErrorAction SilentlyContinue - if (Test-Path $log) { - Write-Host "--- Elevated install log: $log ---" -ForegroundColor DarkCyan - Get-Content $log | Write-Host - } - } else { - try { & $VenvPython $ServiceScript remove } catch {} - & $VenvPython $ServiceScript --startup auto install - try { sc.exe config BorealisAgent obj= LocalSystem } catch {} - } - # Refresh service object - $svc = Get-CimInstance -ClassName Win32_Service -Filter "Name='$serviceName'" -ErrorAction SilentlyContinue - if (-not $svc) { - Write-Host "Failed to install Borealis Agent service." -ForegroundColor Red - return + Run-Step "Install Python Dependencies" { + if (Test-Path $agentRequirements) { + & $venvPython -m pip install --disable-pip-version-check -q -r $agentRequirements | Out-Null } } - if ($svc.State -ne 'Running') { - if (-not (Test-IsAdmin)) { - Write-Host "Admin rights required to start the agent service. Prompting UAC..." -ForegroundColor Yellow - $tmp2 = New-TemporaryFile - $log2 = Join-Path $scriptDir 'Logs\Borealis_AgentService_Start.log' - $content2 = @" -$ErrorActionPreference = 'Continue' -try { - Start-Service -Name '$serviceName' - "[INFO] Started Borealis Agent service." | Out-File -FilePath "$log2" -Encoding UTF8 - $code = 0 -} catch { - "[ERROR] $_" | Out-File -FilePath "$log2" -Encoding UTF8 - $code = 1 -} finally { - if ($code -ne 0 -or $env:BOREALIS_DEBUG_UAC -eq '1') { Read-Host 'Press Enter to close this elevated window...' } - exit $code -} -"@ - Set-Content -Path $tmp2.FullName -Value $content2 -Force -Encoding UTF8 - $psi2 = New-Object System.Diagnostics.ProcessStartInfo - $psi2.FileName = 'powershell.exe' - $psi2.Verb = 'runas' - $psi2.Arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$($tmp2.FullName)`"" - $psi2.UseShellExecute = $true - $psi2.WindowStyle = 'Normal' - $proc2 = [System.Diagnostics.Process]::Start($psi2) - $proc2.WaitForExit() - Remove-Item $tmp2.FullName -Force -ErrorAction SilentlyContinue - if (Test-Path $log2) { - Write-Host "--- Elevated start log: $log2 ---" -ForegroundColor DarkCyan - Get-Content $log2 | Write-Host - } - } else { - Start-Service -Name $serviceName - } - } + Write-Host "`nConfiguring Borealis Agent (tasks)..." -ForegroundColor Blue + Write-Host "====================================================================================" + Ensure-AgentTasks -ScriptRoot $scriptDir - Write-Host "Borealis Agent service is installed and running as LocalSystem (Automatic)." -ForegroundColor Green + # Ensure per-user logon task for helper + $deployScript = Join-Path (Join-Path $scriptDir 'Agent\Borealis') 'agent_deployment.py' + try { & (Join-Path $scriptDir 'Agent\Scripts\python.exe') -W ignore::SyntaxWarning $deployScript task-ensure | Out-Null } catch {} } -# Ensure Script Agent (LocalSystem Windows Service) -function Ensure-BorealisScriptAgent-Service { - param( - [string]$VenvPython, - [string]$ServiceScript - ) - if (-not (Test-IsWindows)) { return } - $serviceName = 'BorealisAgent' - $svc = Get-CimInstance -ClassName Win32_Service -Filter "Name='$serviceName'" -ErrorAction SilentlyContinue - $needsInstall = $false - if (-not $svc) { $needsInstall = $true } - else { - if ($svc.StartMode -notin @('Auto','Automatic')) { $needsInstall = $true } - if ($svc.StartName -ne 'LocalSystem') { $needsInstall = $true } - try { - $venvRoot = Split-Path $VenvPython -Parent - if ($svc.PathName -and $venvRoot -and ($svc.PathName -notlike ("*" + $venvRoot + "*"))) { $needsInstall = $true } - } catch {} - } - - $logs = Join-Path $scriptDir 'Logs' - $tempDir = Join-Path $scriptDir 'Temp'; if (-not (Test-Path $tempDir)) { New-Item -ItemType Directory -Path $tempDir -Force | Out-Null } - if (-not (Test-Path $logs)) { New-Item -ItemType Directory -Path $logs -Force | Out-Null } - $preflight = Join-Path $logs 'Borealis_ScriptAgent_Preflight.log' - "$(Get-Date -Format s) Preflight: Ensure-BorealisScriptAgent-Service (needsInstall=$needsInstall)" | Out-File -FilePath $preflight -Append -Encoding UTF8 - - if ($needsInstall) { - if (-not (Test-IsAdmin)) { - Write-Host "Admin rights required to install/update the script agent service. Prompting UAC..." -ForegroundColor Yellow - $log = Join-Path $logs 'Borealis_ScriptAgent_Install.log' - $vEsc = $VenvPython.Replace('"','""') - $sEsc = $ServiceScript.Replace('"','""') - $content = @" -$ErrorActionPreference = 'Continue' -$venv = "$vEsc" -$srv = "$sEsc" -$logPath = "$log" -try { - try { New-Item -ItemType Directory -Force -Path (Split-Path $logPath -Parent) | Out-Null } catch {} - & $venv -m pywin32_postinstall -install *>&1 | Tee-Object -FilePath $logPath -Append - try { & $venv $srv remove *>&1 | Tee-Object -FilePath $logPath -Append } catch {} - & $venv $srv --startup auto install *>&1 | Tee-Object -FilePath $logPath -Append - sc.exe config $serviceName obj= LocalSystem | Tee-Object -FilePath $logPath -Append - $code = $LASTEXITCODE; if ($code -eq $null) { $code = 0 } -} catch { - "[ERROR] $_" | Tee-Object -FilePath $logPath -Append | Out-Host - $code = 1 -} finally { - if ($code -ne 0 -or $env:BOREALIS_DEBUG_UAC -eq '1') { Read-Host 'Press Enter to close this elevated window...' } - exit $code -} -"@ - $elevateScript = Join-Path $tempDir 'Elevated-Install-ScriptAgent.ps1' - Set-Content -Path $elevateScript -Value $content -Force -Encoding UTF8 - $psi = New-Object System.Diagnostics.ProcessStartInfo - $psi.FileName = 'powershell.exe' - $psi.Verb = 'runas' - $psi.Arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$elevateScript`"" - $psi.UseShellExecute = $true - $psi.WindowStyle = 'Normal' - $psi.WorkingDirectory = $scriptDir - $proc = [System.Diagnostics.Process]::Start($psi) - $proc.WaitForExit() - if ($proc.ExitCode -eq 0) { Remove-Item $elevateScript -Force -ErrorAction SilentlyContinue } else { Write-Host "Keeping stub for troubleshooting: $elevateScript" -ForegroundColor Yellow } - if (Test-Path $log) { Write-Host "--- Elevated install log: $log ---" -ForegroundColor DarkCyan; Get-Content $log | Write-Host } - else { Write-Host "Install log not found at: $log" -ForegroundColor Yellow } - if ($proc.ExitCode -ne 0) { Write-Host "Elevated install returned code $($proc.ExitCode)" -ForegroundColor Red; return } - } else { - try { & $VenvPython $ServiceScript remove } catch {} - & $VenvPython $ServiceScript --startup auto install - try { sc.exe config $serviceName obj= LocalSystem } catch {} - } - } - - $svc = Get-CimInstance -ClassName Win32_Service -Filter "Name='$serviceName'" -ErrorAction SilentlyContinue - if (-not $svc) { Write-Host "ScriptAgent service not found after install." -ForegroundColor Red; return $false } - if ($svc.State -ne 'Running') { - if (-not (Test-IsAdmin)) { - Write-Host "Admin rights required to start the script agent service. Prompting UAC..." -ForegroundColor Yellow - $log2 = Join-Path $logs 'Borealis_ScriptAgent_Start.log' - $content2 = @" -$ErrorActionPreference = 'Continue' -try { Start-Service -Name '$serviceName'; "[INFO] Started." | Out-File -FilePath "$log2" -Encoding UTF8; $code = 0 } catch { "[ERROR] $_" | Out-File -FilePath "$log2" -Encoding UTF8; $code = 1 } finally { if ($code -ne 0 -or $env:BOREALIS_DEBUG_UAC -eq '1') { Read-Host 'Press Enter to close this elevated window...' }; exit $code } -"@ - $elevateStart = Join-Path $tempDir 'Elevated-Start-ScriptAgent.ps1' - Set-Content -Path $elevateStart -Value $content2 -Force -Encoding UTF8 - $psi2 = New-Object System.Diagnostics.ProcessStartInfo - $psi2.FileName = 'powershell.exe' - $psi2.Verb = 'runas' - $psi2.Arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$elevateStart`"" - $psi2.UseShellExecute = $true - $psi2.WindowStyle = 'Normal' - $psi2.WorkingDirectory = $scriptDir - $proc2 = [System.Diagnostics.Process]::Start($psi2) - $proc2.WaitForExit() - if ($proc2.ExitCode -eq 0) { Remove-Item $elevateStart -Force -ErrorAction SilentlyContinue } else { Write-Host "Keeping stub for troubleshooting: $elevateStart" -ForegroundColor Yellow } - if (Test-Path $log2) { Write-Host "--- Elevated start log: $log2 ---" -ForegroundColor DarkCyan; Get-Content $log2 | Write-Host } - else { Write-Host "Start log not found at: $log2" -ForegroundColor Yellow } - if ($proc2.ExitCode -ne 0) { Write-Host "Elevated start returned code $($proc2.ExitCode)" -ForegroundColor Red; return } - } else { - Start-Service -Name $serviceName - } - Start-Sleep -Seconds 2 - $svc = Get-CimInstance -ClassName Win32_Service -Filter "Name='$serviceName'" -ErrorAction SilentlyContinue - if ($svc.State -ne 'Running') { Write-Host "ScriptAgent service failed to start." -ForegroundColor Red; return $false } - } - return $true -} - -# Ensure Collector Agent (per-user scheduled task) -function Ensure-BorealisCollector-Task { - param( - [string]$VenvPython, - [string]$CollectorScript - ) - $logs = Join-Path $scriptDir 'Logs' - if (-not (Test-Path $logs)) { New-Item -ItemType Directory -Path $logs -Force | Out-Null } - "$(Get-Date -Format s) Preflight: Ensure-BorealisCollector-Task" | Out-File -FilePath (Join-Path $logs 'Borealis_Collector_Preflight.log') -Append -Encoding UTF8 - - $tempDir = Join-Path $scriptDir 'Temp' - if (-not (Test-Path $tempDir)) { New-Item -ItemType Directory -Path $tempDir -Force | Out-Null } - $logC = Join-Path $logs 'Borealis_CollectorTask_Install.log' - - $taskName = 'BorealisCollectorAgent' - $taskExists = $false - try { - $null = schtasks.exe /Query /TN $taskName 2>$null - if ($LASTEXITCODE -eq 0) { $taskExists = $true } - } catch {} - - $quoted = '"' + $VenvPython + '" -W ignore::SyntaxWarning "' + $CollectorScript + '"' - - if ($taskExists) { - # Replace to ensure it points to current project - # Attempt delete in current user context first - try { - schtasks.exe /Delete /TN $taskName /F 2>&1 | Tee-Object -FilePath $logC -Append | Out-Host - } catch {} - if ($LASTEXITCODE -ne 0) { - # If deletion failed (likely created under elevated/Admin previously), delete via elevated stub - $stubDel = Join-Path $tempDir 'Elevated-Delete-CollectorTask.ps1' - $tnEsc = $taskName.Replace('"','""') - $lgEsc = $logC.Replace('"','""') - $contentDel = @" -$ErrorActionPreference = 'Continue' -try { - schtasks.exe /Delete /TN "$tnEsc" /F *>&1 | Tee-Object -FilePath "$lgEsc" -Append - $code = $LASTEXITCODE; if ($code -eq $null) { $code = 0 } -} catch { "[ERROR] $_" | Tee-Object -FilePath "$lgEsc" -Append; $code = 1 } finally { - if ($code -ne 0 -or $env:BOREALIS_DEBUG_UAC -eq '1') { Read-Host 'Press Enter to close this elevated window...' } - exit $code -} -"@ - Set-Content -Path $stubDel -Value $contentDel -Force -Encoding UTF8 - $psiD = New-Object System.Diagnostics.ProcessStartInfo - $psiD.FileName = 'powershell.exe' - $psiD.Verb = 'runas' - $psiD.Arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$stubDel`"" - $psiD.UseShellExecute = $true - $psiD.WindowStyle = 'Normal' - try { $psiD.WorkingDirectory = $scriptDir; $pd = [System.Diagnostics.Process]::Start($psiD); $pd.WaitForExit() } catch {} - Remove-Item $stubDel -Force -ErrorAction SilentlyContinue - } - # Re-evaluate existence; if still present, abort - try { $null = schtasks.exe /Query /TN $taskName 2>$null } catch {} - if ($LASTEXITCODE -eq 0) { - Write-Host "Failed to remove existing Collector task (permissions)." -ForegroundColor Red - if (Test-Path $logC) { Write-Host "--- Collector task log: $logC ---" -ForegroundColor DarkCyan; Get-Content $logC | Write-Host } - return $false - } - } - - # Create per-user task in the current (non-elevated) user context - # Do NOT elevate creation to avoid creating task under Administrator by mistake - schtasks.exe /Create /SC ONLOGON /TN $taskName /TR $quoted /F /RL LIMITED 2>&1 | Tee-Object -FilePath $logC -Append | Out-Host - if ($LASTEXITCODE -ne 0) { - Write-Host "Failed to create Collector task." -ForegroundColor Red - if (Test-Path $logC) { Write-Host "--- Collector task log: $logC ---" -ForegroundColor DarkCyan; Get-Content $logC | Write-Host } - return $false - } - # If currently logged in, start now - try { schtasks.exe /Run /TN $taskName 2>&1 | Tee-Object -FilePath $logC -Append | Out-Host } catch {} - # Validate task exists - try { $null = schtasks.exe /Query /TN $taskName 2>$null } catch {} - if ($LASTEXITCODE -ne 0) { Write-Host "Collector task missing after create." -ForegroundColor Red; return $false } - return $true -} -#> -# ---------------------- Common Initialization & Visuals ---------------------- +# ---------------------- Main ---------------------- Clear-Host # ASCII Art Banner @@ -749,9 +455,6 @@ Clear-Host '@ | Write-Host -ForegroundColor DarkCyan Write-Host "Automation Platform" -ForegroundColor DarkGray -## Defer PATH updates and tool presence checks until after a selection is made - -# ---------------------- Menu Prompt & User Input ---------------------- if (-not $choice) { Write-Host " " Write-Host "Please choose which function you want to launch:" @@ -769,48 +472,35 @@ if (-not $choice) { Write-Host "" -ForegroundColor DarkCyan $choice = Read-Host } -switch ($choice) { +switch ($choice) { "1" { $host.UI.RawUI.WindowTitle = "Borealis Server" Write-Host "Ensuring Server Dependencies Exist..." -ForegroundColor DarkCyan - - # ---------------------- Ensure users.json is Present ---------------------- + Run-Step "First-Run: Generating users.json" { $usersJsonPath = Join-Path $scriptDir "users.json" if (-not (Test-Path $usersJsonPath)) { - $defaultUsers = @{ users = @(@{ username = "admin"; password = "e6c83b282aeb2e022844595721cc00bbda47cb24537c1779f9bb84f04039e1676e6ba8573e588da1052510e3aa0a32a9e55879ae22b0c2d62136fc0a3e85f8bb" }) } | - ConvertTo-Json -Depth 4 - $utf8NoBom = New-Object System.Text.UTF8Encoding($false) - [System.IO.File]::WriteAllText($usersJsonPath, $defaultUsers, $utf8NoBom) + $defaultUsers = @{ users = @(@{ username = "admin"; password = "e6c83b282aeb2e022844595721cc00bbda47cb24537c1779f9bb84f04039e1676e6ba8573e588da1052510e3aa0a32a9e55879ae22b0c2d62136fc0a3e85f8bb" }) } | ConvertTo-Json -Depth 4 + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($usersJsonPath, $defaultUsers, $utf8NoBom) } } - + Install_Shared_Dependencies Install_Server_Dependencies - # After ensuring dependencies, verify key tools and update PATH foreach ($tool in @($pythonExe, $nodeExe, $npmCmd, $npxCmd)) { - if (-not (Test-Path $tool)) { - Write-Host "`r$($symbols.Fail) Bundled executable not found at '$tool'." -ForegroundColor Red - exit 1 - } + if (-not (Test-Path $tool)) { Write-Host "`r$($symbols.Fail) Bundled executable not found at '$tool'." -ForegroundColor Red; exit 1 } } $env:PATH = '{0};{1};{2}' -f (Split-Path $pythonExe), (Split-Path $nodeExe), $env:PATH if (-not $modeChoice) { Write-Host " " Write-Host "Configure Borealis Server Mode:" -ForegroundColor DarkYellow - Write-Host " 1) Build & Launch > " -NoNewLine -ForegroundColor DarkGray - Write-Host "Production Flask Server @ " -NoNewLine - Write-Host "http://localhost:5000" -ForegroundColor DarkCyan - Write-Host " 2) [Skip Build] & Immediately Launch > " -NoNewLine -ForegroundColor DarkGray - Write-Host "Production Flask Server @ " -NoNewLine - Write-Host "http://localhost:5000" -ForegroundColor DarkCyan - Write-Host " 3) Launch > " -NoNewLine -ForegroundColor DarkGray - Write-Host "[Hotload-Ready] " -NoNewLine -ForegroundColor Green - Write-Host "Vite Dev Server @ " -NoNewLine - Write-Host "http://localhost:5173" -ForegroundColor DarkCyan + Write-Host " 1) Build & Launch > Production Flask Server @ http://localhost:5000" -ForegroundColor DarkCyan + Write-Host " 2) [Skip Build] & Immediately Launch > Production Flask Server @ http://localhost:5000" -ForegroundColor DarkCyan + Write-Host " 3) Launch > [Hotload-Ready] Vite Dev Server @ http://localhost:5173" -ForegroundColor DarkCyan $modeChoice = Read-Host "Enter choice [1/2/3]" } @@ -822,16 +512,12 @@ switch ($choice) { & (Join-Path $scriptDir "Server\Scripts\python.exe") (Join-Path $scriptDir "Server\Borealis\server.py") Pop-Location } - Exit 0 - } - "3" { $borealis_operation_mode = "developer" } - default { - Write-Host "Invalid mode choice: $modeChoice" -ForegroundColor Red - Exit 1 + break } + "3" { $borealis_operation_mode = "developer" } + default { Write-Host "Invalid mode choice: $modeChoice" -ForegroundColor Red; break } } - # ───── Now run your deploy logic ───── Write-Host "Deploying Borealis Server in '$borealis_operation_mode' mode" -ForegroundColor Blue $venvFolder = "Server" @@ -841,9 +527,7 @@ switch ($choice) { $webUIDestination = "$venvFolder\web-interface" $venvPython = Join-Path $venvFolder 'Scripts\python.exe' - # Create Virtual Environment & Copy Server Assets Run-Step "Create Borealis Virtual Python Environment" { - # Leverage Bundled Python Dependency to Construct Virtual Python Environment if (-not (Test-Path "$venvFolder\Scripts\Activate")) { & $pythonExe -m venv $venvFolder | Out-Null } if (Test-Path $dataSource) { Remove-Item $dataDestination -Recurse -Force -ErrorAction SilentlyContinue @@ -855,14 +539,10 @@ switch ($choice) { . "$venvFolder\Scripts\Activate" } - # Install Python Dependencies Run-Step "Install Python Dependencies into Virtual Python Environment" { - if (Test-Path "$dataSource\Server\server-requirements.txt") { - & $venvPython -m pip install --disable-pip-version-check -q -r "$dataSource\Server\server-requirements.txt" | Out-Null - } + if (Test-Path "$dataSource\Server\server-requirements.txt") { & $venvPython -m pip install --disable-pip-version-check -q -r "$dataSource\Server\server-requirements.txt" | Out-Null } } - # Copy Vite WebUI Assets Run-Step "Copy Borealis WebUI Files into: $webUIDestination" { if (Test-Path $webUIDestination) { Remove-Item "$webUIDestination\public\*" -Recurse -Force -ErrorAction SilentlyContinue @@ -873,7 +553,6 @@ switch ($choice) { Copy-Item "$customUIPath\*" $webUIDestination -Recurse -Force } - # NPM Install for WebUI Run-Step "Vite Web Frontend: Install NPM Packages" { Push-Location $webUIDestination $env:npm_config_loglevel = "silent" @@ -881,7 +560,6 @@ switch ($choice) { Pop-Location } - # Vite Operation Mode Control (build vs dev) Run-Step "Vite Web Frontend: Start ($borealis_operation_mode)" { Push-Location $webUIDestination if ($borealis_operation_mode -eq "developer") { $viteSubCommand = "dev" } else { $viteSubCommand = "build" } @@ -889,16 +567,13 @@ switch ($choice) { Pop-Location } - # Launch Flask Server Run-Step "Borealis: Launch Flask Server" { Push-Location (Join-Path $scriptDir "Server") $py = Join-Path $scriptDir "Server\Scripts\python.exe" $server_py = Join-Path $scriptDir "Server\Borealis\server.py" - Write-Host "`nLaunching Borealis..." -ForegroundColor Green Write-Host "====================================================================================" Write-Host "$($symbols.Running) Python Flask API Server Started..." - & $py $server_py Pop-Location } @@ -917,16 +592,10 @@ switch ($choice) { if (-not $agentSubChoice) { $agentSubChoice = Read-Host "Select an option" } switch ($agentSubChoice) { - '2' { - Repair-BorealisAgent - break - } - '3' { - Remove-BorealisAgent - break - } + '1' { InstallOrUpdate-BorealisAgent; break } + '2' { Repair-BorealisAgent; break } + '3' { Remove-BorealisAgent; break } '4' { - # Manually launch helper for the current session (optional) $venvPythonw = Join-Path $scriptDir 'Agent\Scripts\pythonw.exe' $helper = Join-Path $scriptDir 'Agent\Borealis\borealis-agent.py' if (-not (Test-Path $venvPythonw)) { Write-Host "pythonw.exe not found under Agent\Scripts" -ForegroundColor Yellow } @@ -937,115 +606,33 @@ switch ($choice) { } break } - '5' { break } - Default { - # 1) Install/Update Agent (original behavior) - Write-Host "Ensuring Agent Dependencies Exist..." -ForegroundColor DarkCyan - Install_Shared_Dependencies - Install_Agent_Dependencies - if (-not (Test-Path $pythonExe)) { - Write-Host "`r$($symbols.Fail) Bundled Python not found at '$pythonExe'." -ForegroundColor Red - exit 1 - } - $env:PATH = '{0};{1}' -f (Split-Path $pythonExe), $env:PATH - Write-Host "Deploying Borealis Agent..." -ForegroundColor Blue - - $venvFolder = "Agent" - $agentSourcePath = "Data\Agent\borealis-agent.py" - $agentRequirements = "Data\Agent\agent-requirements.txt" - $agentDestinationFolder = "$venvFolder\Borealis" - $agentDestinationFile = "$venvFolder\Borealis\borealis-agent.py" - $venvPython = Join-Path $scriptDir $venvFolder | Join-Path -ChildPath 'Scripts\python.exe' - - Run-Step "Create Virtual Python Environment" { - if (-not (Test-Path "$venvFolder\Scripts\Activate")) { - $pythonForVenv = $pythonExe - if (-not (Test-Path $pythonForVenv)) { - $pyCmd = Get-Command py -ErrorAction SilentlyContinue - $pythonCmd = Get-Command python -ErrorAction SilentlyContinue - if ($pyCmd) { $pythonForVenv = $pyCmd.Source } - elseif ($pythonCmd) { $pythonForVenv = $pythonCmd.Source } - else { - Write-Host "Python not found. Install Python or run Server setup (option 1)." -ForegroundColor Red - exit 1 - } - } - & $pythonForVenv -m venv $venvFolder - } - if (Test-Path $agentSourcePath) { - Remove-Item $agentDestinationFolder -Recurse -Force -ErrorAction SilentlyContinue - New-Item -Path $agentDestinationFolder -ItemType Directory -Force | Out-Null - Copy-Item "Data\Agent\borealis-agent.py" $agentDestinationFolder -Recurse - Copy-Item "Data\Agent\agent_info.py" $agentDestinationFolder -Recurse - Copy-Item "Data\Agent\agent_roles.py" $agentDestinationFolder -Recurse - Copy-Item "Data\Agent\Python_API_Endpoints" $agentDestinationFolder -Recurse - Copy-Item "Data\Agent\windows_script_service.py" $agentDestinationFolder -Force - Copy-Item "Data\Agent\agent_deployment.py" $agentDestinationFolder -Force - Copy-Item "Data\Agent\tray_launcher.py" $agentDestinationFolder -Force - if (Test-Path "Data\Agent\Borealis.ico") { Copy-Item "Data\Agent\Borealis.ico" $agentDestinationFolder -Force } - } - . "$venvFolder\Scripts\Activate" - } - - Run-Step "Install Python Dependencies" { - if (Test-Path $agentRequirements) { - & $venvPython -m pip install --disable-pip-version-check -q -r $agentRequirements | Out-Null - } - } - - Write-Host "`nConfiguring Borealis Agent (service)..." -ForegroundColor Blue - Write-Host "====================================================================================" - $deployScript = Join-Path $agentDestinationFolder 'agent_deployment.py' - & $venvPython -W ignore::SyntaxWarning $deployScript ensure-all - if ($LASTEXITCODE -ne 0) { - Write-Host "Agent setup FAILED." -ForegroundColor Red - Write-Host " - See logs under: $(Join-Path $scriptDir 'Logs')" -ForegroundColor Red - exit 1 - } else { - Write-Host "Agent setup complete. Service ensured." -ForegroundColor DarkGreen - } - } + default { break } } } "3" { $host.UI.RawUI.WindowTitle = "Borealis Electron" - # Desktop App Deployment (Electron) Clear-Host Write-Host "Deploying Borealis Desktop App..." -ForegroundColor Cyan Write-Host "====================================================================================" - + $electronSource = "Data\Electron" $electronDestination = "ElectronApp" $scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent - # 1) Prepare ElectronApp folder Run-Step "Prepare ElectronApp folder" { - if (Test-Path $electronDestination) { - Remove-Item $electronDestination -Recurse -Force - } + if (Test-Path $electronDestination) { Remove-Item $electronDestination -Recurse -Force } New-Item -Path $electronDestination -ItemType Directory | Out-Null - - # Copy deployed Flask server $deployedServer = Join-Path $scriptDir 'Server\Borealis' - if (-not (Test-Path $deployedServer)) { - throw "Server\Borealis not found - please run choice 1 first." - } + if (-not (Test-Path $deployedServer)) { throw "Server\Borealis not found - please run choice 1 first." } Copy-Item $deployedServer "$electronDestination\Server" -Recurse - - # Copy Electron scaffold files Copy-Item "$electronSource\package.json" "$electronDestination" -Force Copy-Item "$electronSource\main.js" "$electronDestination" -Force - - # Copy built WebUI into renderer $staticBuild = Join-Path $scriptDir 'Server\web-interface\build' - if (-not (Test-Path $staticBuild)) { - throw "WebUI build not found - run choice 1 to build WebUI first." - } + if (-not (Test-Path $staticBuild)) { throw "WebUI build not found - run choice 1 to build WebUI first." } Copy-Item "$staticBuild\*" "$electronDestination\renderer" -Recurse -Force } - # 2) Install Electron & Builder Run-Step "ElectronApp: Install Node dependencies" { Push-Location $electronDestination $env:NODE_ENV = '' @@ -1054,14 +641,12 @@ switch ($choice) { Pop-Location } - # 3) Package desktop app Run-Step "ElectronApp: Package with electron-builder" { Push-Location $electronDestination & $npmCmd run dist Pop-Location } - # 4) Launch in dev mode Run-Step "ElectronApp: Launch in dev mode" { Push-Location $electronDestination & $npmCmd run dev @@ -1071,43 +656,29 @@ switch ($choice) { "4" { $host.UI.RawUI.WindowTitle = "Borealis Packager" - # Prompt the User for Which System to Package using Pyinstaller Write-Host "Choose which module to package into a self-contained EXE file:" -ForegroundColor DarkYellow Write-Host " 1) Server" -ForegroundColor DarkGray Write-Host " 2) Agent" -ForegroundColor DarkGray $exePackageChoice = Read-Host "Enter choice [1/2]" - switch ($exePackageChoice) { - "1" { + "1" { $serverScriptDir = Join-Path -Path $PSScriptRoot -ChildPath "Data\Server" Set-Location -Path $serverScriptDir - & (Join-Path -Path $serverScriptDir -ChildPath "Package-Borealis-Server.ps1") + & (Join-Path -Path $serverScriptDir -ChildPath "Package-Borealis-Server.ps1") } - - "2" { + "2" { $agentScriptDir = Join-Path -Path $PSScriptRoot -ChildPath "Data\Agent" Set-Location -Path $agentScriptDir - & (Join-Path -Path $agentScriptDir -ChildPath "Package_Borealis-Agent.ps1") + & (Join-Path -Path $agentScriptDir -ChildPath "Package_Borealis-Agent.ps1") } - - default { - Write-Host "Invalid Choice. Exiting..." -ForegroundColor Red - exit 1 - } - } - } - - default { - Write-Host "Invalid selection. Exiting..." -ForegroundColor Red - exit 1 + default { Write-Host "Invalid Choice. Exiting..." -ForegroundColor Red; exit 1 } + } } "5" { $host.UI.RawUI.WindowTitle = "Borealis Updater" Write-Host " " Write-Host "Updating Borealis..." -ForegroundColor Green - - # Prepare paths $updateZip = Join-Path $scriptDir "Update_Staging\main.zip" $updateDir = Join-Path $scriptDir "Update_Staging\Borealis-main" $preservePath = Join-Path $scriptDir "Data\Server\Python_API_Endpoints\Tesseract-OCR" @@ -1115,62 +686,41 @@ switch ($choice) { Run-Step "Updating: Move Tesseract-OCR Folder Somewhere Safe to Restore Later" { if (Test-Path $preservePath) { - # Ensure staging folder exists $stagingPath = Join-Path $scriptDir "Update_Staging" - if (-not (Test-Path $stagingPath)) { - New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null - } - + if (-not (Test-Path $stagingPath)) { New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null } Move-Item -Path $preservePath -Destination $preserveBackupPath -Force } } Run-Step "Updating: Clean Up Folders to Prepare for Update" { Remove-Item -Recurse -Force -ErrorAction SilentlyContinue ` - (Join-Path $scriptDir "Data"), ` - (Join-Path $scriptDir "Server\web-interface\src"), ` - (Join-Path $scriptDir "Server\web-interface\build"), ` - (Join-Path $scriptDir "Server\web-interface\public"), ` - (Join-Path $scriptDir "Server\Borealis") + (Join-Path $scriptDir "Data"), ` + (Join-Path $scriptDir "Server\web-interface\src"), ` + (Join-Path $scriptDir "Server\web-interface\build"), ` + (Join-Path $scriptDir "Server\web-interface\public"), ` + (Join-Path $scriptDir "Server\Borealis") } Run-Step "Updating: Create Update Staging Folder" { $stagingPath = Join-Path $scriptDir "Update_Staging" - if (-not (Test-Path $stagingPath)) { - New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null - } - + if (-not (Test-Path $stagingPath)) { New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null } $updateZip = Join-Path $stagingPath "main.zip" $updateDir = Join-Path $stagingPath "Borealis-main" } - Run-Step "Updating: Download Update from https://github.com/bunny-lab-io/Borealis/archive/refs/heads/main.zip" { - Invoke-WebRequest -Uri "https://github.com/bunny-lab-io/Borealis/archive/refs/heads/main.zip" -OutFile $updateZip - } - - Run-Step "Updating: Extract Update Files" { - Expand-Archive -Path $updateZip -DestinationPath (Join-Path $scriptDir "Update_Staging") -Force - } - - Run-Step "Updating: Copy Update Files into Production Borealis Root Folder" { - Copy-Item "$updateDir\*" $scriptDir -Recurse -Force - } + Run-Step "Updating: Download Update" { Invoke-WebRequest -Uri "https://github.com/bunny-lab-io/Borealis/archive/refs/heads/main.zip" -OutFile $updateZip } + Run-Step "Updating: Extract Update Files" { Expand-Archive -Path $updateZip -DestinationPath (Join-Path $scriptDir "Update_Staging") -Force } + Run-Step "Updating: Copy Update Files into Production Borealis Root Folder" { Copy-Item "$updateDir\*" $scriptDir -Recurse -Force } Run-Step "Updating: Restore Tesseract-OCR Folder" { $restorePath = Join-Path $scriptDir "Data\Server\Python_API_Endpoints" if (Test-Path $preserveBackupPath) { - # Ensure destination path exists - if (-not (Test-Path $restorePath)) { - New-Item -ItemType Directory -Force -Path $restorePath | Out-Null - } - + if (-not (Test-Path $restorePath)) { New-Item -ItemType Directory -Force -Path $restorePath | Out-Null } Move-Item -Path $preserveBackupPath -Destination $restorePath -Force } } - Run-Step "Updating: Clean Up Update Staging Folder" { - Remove-Item -Recurse -Force -ErrorAction SilentlyContinue (Join-Path $scriptDir "Update_Staging") - } + Run-Step "Updating: Clean Up Update Staging Folder" { Remove-Item -Recurse -Force -ErrorAction SilentlyContinue (Join-Path $scriptDir "Update_Staging") } Write-Host "`nUpdate Complete! Please Re-Launch the Borealis Script." -ForegroundColor Green Read-Host "Press any key to re-launch Borealis..." @@ -1182,12 +732,11 @@ switch ($choice) { $host.UI.RawUI.WindowTitle = "AutoHotKey Automation Testing" Write-Host " " Write-Host "Lauching AutoHotKey Testing Script..." -ForegroundColor Blue - $venvFolder = "Macro_Testing" - $scriptSourcePath = "Data\Experimental\Macros\Macro_Script.py" - $scriptRequirements = "Data\Experimental\Macros\macro-requirements.txt" - $scriptDestinationFolder = "$venvFolder\Borealis" - $scriptDestinationFile = "$venvFolder\Borealis\Macro_Script.py" + $scriptSourcePath = "Data\Experimental\Macros\Macro_Script.py" + $scriptRequirements = "Data\Experimental\Macros\macro-requirements.txt" + $scriptDestinationFolder= "$venvFolder\Borealis" + $scriptDestinationFile = "$venvFolder\Borealis\Macro_Script.py" $venvPython = Join-Path $scriptDir $venvFolder | Join-Path -ChildPath 'Scripts\python.exe' Run-Step "Create Virtual Python Environment" { @@ -1198,10 +747,7 @@ switch ($choice) { $pythonCmd = Get-Command python -ErrorAction SilentlyContinue if ($pyCmd) { $pythonForVenv = $pyCmd.Source } elseif ($pythonCmd) { $pythonForVenv = $pythonCmd.Source } - else { - Write-Host "Python not found. Install Python or run Server setup (option 1)." -ForegroundColor Red - exit 1 - } + else { Write-Host "Python not found. Install Python or run Server setup (option 1)." -ForegroundColor Red; exit 1 } } & $pythonForVenv -m venv $venvFolder } @@ -1214,14 +760,11 @@ switch ($choice) { . "$venvFolder\Scripts\Activate" } - Run-Step "Install Python Dependencies" { - if (Test-Path $scriptRequirements) { - & $venvPython -m pip install --disable-pip-version-check -q -r $scriptRequirements | Out-Null - } - } - + Run-Step "Install Python Dependencies" { if (Test-Path $scriptRequirements) { & $venvPython -m pip install --disable-pip-version-check -q -r $scriptRequirements | Out-Null } } Write-Host "`nLaunching Macro Testing Script..." -ForegroundColor Blue Write-Host "====================================================================================" & $venvPython -W ignore::SyntaxWarning $scriptDestinationFile } + + default { Write-Host "Invalid selection. Exiting..." -ForegroundColor Red; exit 1 } } diff --git a/Data/Agent/agent_deployment.py b/Data/Agent/agent_deployment.py index 4e02377..15d9e8e 100644 --- a/Data/Agent/agent_deployment.py +++ b/Data/Agent/agent_deployment.py @@ -1,7 +1,6 @@ import os import sys import subprocess -import shlex import ctypes from datetime import datetime @@ -11,12 +10,6 @@ def _now(): def project_paths(): - r"""Derive important paths relative to the venv python (sys.executable). - - Layout assumed at runtime: - /Agent/Scripts/python.exe == sys.executable - /Agent/Borealis/*.py == deployed agent files - """ venv_scripts = os.path.dirname(os.path.abspath(sys.executable)) venv_root = os.path.abspath(os.path.join(venv_scripts, os.pardir)) project_root = os.path.abspath(os.path.join(venv_root, os.pardir)) @@ -31,8 +24,6 @@ def project_paths(): "borealis_dir": borealis_dir, "logs_dir": logs_dir, "temp_dir": temp_dir, - "service_script": os.path.join(borealis_dir, "windows_script_service.py"), - # Use tray launcher for the scheduled task "agent_script": os.path.join(borealis_dir, "tray_launcher.py"), } @@ -53,43 +44,21 @@ def log_write(paths, name, text): def is_admin(): try: - return ctypes.windll.shell32.IsUserAnAdmin() != 0 # type: ignore[attr-defined] + return ctypes.windll.shell32.IsUserAnAdmin() != 0 except Exception: return False -def run(cmd, cwd=None, capture=False): - if isinstance(cmd, str): - shell = True - args = cmd - else: - shell = False - args = cmd - return subprocess.run( - args, - cwd=cwd, - shell=shell, - text=True, - capture_output=capture, - check=False, - ) +def run(cmd, capture=False): + return subprocess.run(cmd, text=True, capture_output=capture, check=False) def run_elevated_powershell(paths, ps_content, log_name): - """Run a short PowerShell script elevated and wait for completion. - Writes combined output to Logs/log_name. - """ ensure_dirs(paths) log_path = os.path.join(paths["logs_dir"], log_name) stub_path = os.path.join(paths["temp_dir"], f"elevate_{os.getpid()}_{log_name.replace('.', '_')}.ps1") with open(stub_path, "w", encoding="utf-8") as f: f.write(ps_content) - - # Build powershell command - ps = "powershell.exe" - args = f"-NoProfile -ExecutionPolicy Bypass -File \"{stub_path}\"" - - # ShellExecute to run as admin SEE_MASK_NOCLOSEPROCESS = 0x00000040 class SHELLEXECUTEINFO(ctypes.Structure): _fields_ = [ @@ -109,31 +78,21 @@ def run_elevated_powershell(paths, ps_content, log_name): ("hIcon", ctypes.c_void_p), ("hProcess", ctypes.c_void_p), ] - sei = SHELLEXECUTEINFO() sei.cbSize = ctypes.sizeof(SHELLEXECUTEINFO) sei.fMask = SEE_MASK_NOCLOSEPROCESS sei.hwnd = None sei.lpVerb = "runas" - sei.lpFile = ps - sei.lpParameters = args + sei.lpFile = "powershell.exe" + sei.lpParameters = f"-NoProfile -ExecutionPolicy Bypass -File \"{stub_path}\"" sei.lpDirectory = paths["project_root"] sei.nShow = 1 if not ctypes.windll.shell32.ShellExecuteExW(ctypes.byref(sei)): log_write(paths, log_name, "[ERROR] UAC elevation failed (ShellExecuteExW)") - try: - os.remove(stub_path) - except Exception: - pass return 1 - # Wait for elevated process - ctypes.windll.kernel32.WaitForSingleObject(sei.hProcess, 0xFFFFFFFF) - # Capture output from stub if it appended - try: - with open(log_path, "a", encoding="utf-8") as f: - f.write("") - except Exception: - pass + hproc = sei.hProcess + if hproc: + ctypes.windll.kernel32.WaitForSingleObject(hproc, 0xFFFFFFFF) try: os.remove(stub_path) except Exception: @@ -141,262 +100,64 @@ def run_elevated_powershell(paths, ps_content, log_name): return 0 -def current_service_path(service_name): - """Return the configured BinaryPathName for a service (or None).""" - try: - r = run(["sc.exe", "qc", service_name], capture=True) - if r.returncode != 0: - return None - for line in (r.stdout or "").splitlines(): - if "BINARY_PATH_NAME" in line: - # Example: "BINARY_PATH_NAME : C:\\...\\python.exe C:\\...\\windows_script_service.py" - parts = line.split(":", 1) - if len(parts) == 2: - return parts[1].strip() - except Exception: - pass - return None - - -def ensure_script_service(paths): - service_name = "BorealisAgent" - log_name = "Borealis_ScriptService_Install.log" - ensure_dirs(paths) - log_write(paths, log_name, "[INFO] Ensuring script execution service...") - - # Decide if install/update needed - need_install = False - bin_path = current_service_path(service_name) - expected_root = paths["venv_root"].lower() - if not bin_path: - need_install = True - else: - if expected_root not in bin_path.lower(): - need_install = True - - if not is_admin(): - # Relaunch elevated to perform service installation/update - venv = paths["venv_python"].replace("\"", "\"\"") - srv = paths["service_script"].replace("\"", "\"\"") - log = os.path.join(paths["logs_dir"], log_name).replace("\"", "\"\"") - venv_dir = os.path.dirname(paths["venv_python"]).replace("\"", "\"\"") - postinstall = os.path.join(venv_dir, "pywin32_postinstall.py").replace("\"", "\"\"") - py_home = paths["venv_root"].replace("\"", "\"\"") - content = f""" -$ErrorActionPreference = 'Continue' -$venv = "{venv}" -$srv = "{srv}" -$log = "{log}" -$post = "{postinstall}" -$pyhome = "{py_home}" -try {{ - try {{ New-Item -ItemType Directory -Force -Path (Split-Path $log -Parent) | Out-Null }} catch {{}} - # Remove legacy service names if present - try {{ sc.exe stop BorealisScriptAgent 2>$null | Out-Null }} catch {{}} - try {{ sc.exe delete BorealisScriptAgent 2>$null | Out-Null }} catch {{}} - try {{ sc.exe stop BorealisScriptService 2>$null | Out-Null }} catch {{}} - try {{ sc.exe delete BorealisScriptService 2>$null | Out-Null }} catch {{}} - if (Test-Path $post) {{ & $venv $post -install *>> "$log" }} else {{ & $venv -m pywin32_postinstall -install *>> "$log" }} - try {{ & $venv $srv remove *>> "$log" }} catch {{}} - & $venv $srv --startup auto install *>> "$log" - # Ensure registry points to correct module and PY path - reg add "HKLM\SYSTEM\CurrentControlSet\Services\{service_name}\PythonClass" /ve /t REG_SZ /d "windows_script_service.BorealisAgentService" /f | Out-Null - reg add "HKLM\SYSTEM\CurrentControlSet\Services\{service_name}\PythonPath" /ve /t REG_SZ /d "{paths['borealis_dir']}" /f | Out-Null - reg add "HKLM\SYSTEM\CurrentControlSet\Services\{service_name}\PythonHome" /ve /t REG_SZ /d "$pyhome" /f | Out-Null - sc.exe config {service_name} obj= LocalSystem start= auto | Out-File -FilePath "$log" -Append -Encoding UTF8 - sc.exe start {service_name} | Out-File -FilePath "$log" -Append -Encoding UTF8 - "[INFO] Completed service ensure." | Out-File -FilePath "$log" -Append -Encoding UTF8 -}} catch {{ - "[ERROR] $_" | Out-File -FilePath "$log" -Append -Encoding UTF8 - exit 1 -}} -""" - rc = run_elevated_powershell(paths, content, log_name) - return rc == 0 - - # Admin path: perform directly - try: - # Ensure pywin32 service hooks present in this venv - post_py = os.path.join(os.path.dirname(paths["venv_python"]), "pywin32_postinstall.py") - if os.path.isfile(post_py): - run([paths["venv_python"], post_py, "-install"]) # ignore rc - else: - run([paths["venv_python"], "-m", "pywin32_postinstall", "-install"]) # ignore rc - except Exception: - pass - try: - # Remove legacy service if it exists - run(["sc.exe", "stop", "BorealisScriptAgent"]) # ignore rc - run(["sc.exe", "delete", "BorealisScriptAgent"]) # ignore rc - run(["sc.exe", "stop", "BorealisScriptService"]) # ignore rc - run(["sc.exe", "delete", "BorealisScriptService"]) # ignore rc - if need_install: - run([paths["venv_python"], paths["service_script"], "remove"]) # ignore rc - r1 = run([paths["venv_python"], paths["service_script"], "--startup", "auto", "install"], capture=True) - log_write(paths, log_name, f"[INFO] install rc={r1.returncode} out={r1.stdout}\nerr={r1.stderr}") - # fix registry for module import and runtime resolution - # PythonHome: base interpreter home (from pyvenv.cfg 'home') so pythonservice can load pythonXY.dll - # PythonPath: add Borealis dir and venv site-packages including pywin32 dirs - try: - cfg = os.path.join(paths["venv_root"], "pyvenv.cfg") - base_home = None - if os.path.isfile(cfg): - with open(cfg, "r", encoding="utf-8", errors="ignore") as f: - for line in f: - if line.strip().lower().startswith("home ="): - base_home = line.split("=",1)[1].strip() - break - if not base_home: - # fallback to parent of venv Scripts - base_home = os.path.dirname(os.path.dirname(paths["venv_python"])) - except Exception: - base_home = os.path.dirname(os.path.dirname(paths["venv_python"])) - - site = os.path.join(paths["venv_root"], "Lib", "site-packages") - pypath = ";".join([ - paths["borealis_dir"], - site, - os.path.join(site, "win32"), - os.path.join(site, "win32", "lib"), - os.path.join(site, "pywin32_system32"), - ]) - - run(["reg", "add", fr"HKLM\\SYSTEM\\CurrentControlSet\\Services\\{service_name}\\PythonClass", "/ve", "/t", "REG_SZ", "/d", "windows_script_service.BorealisAgentService", "/f"]) # noqa - run(["reg", "add", fr"HKLM\\SYSTEM\\CurrentControlSet\\Services\\{service_name}\\PythonPath", "/ve", "/t", "REG_SZ", "/d", pypath, "/f"]) # noqa - run(["reg", "add", fr"HKLM\\SYSTEM\\CurrentControlSet\\Services\\{service_name}\\PythonHome", "/ve", "/t", "REG_SZ", "/d", base_home, "/f"]) # noqa - run(["sc.exe", "config", service_name, "obj=", "LocalSystem"]) # ensure LocalSystem - run(["sc.exe", "start", service_name]) - # quick validate - qc = run(["sc.exe", "query", service_name], capture=True) - ok = (qc.returncode == 0) - log_write(paths, log_name, f"[INFO] ensure complete (ok={ok})") - return ok - except Exception as e: - log_write(paths, log_name, f"[ERROR] ensure (admin) failed: {e}") - return False - - def ensure_user_logon_task(paths): - """Ensure the per-user scheduled task that launches the agent in the user's session. - - Name: "Borealis Agent" - Trigger: On logon (current user) - Action: -W ignore::SyntaxWarning + """Ensure per-user scheduled task that launches the helper at logon. + Task name: "Borealis Agent" """ - ensure_dirs(paths) - log_name = "Borealis_CollectorTask_Install.log" task_name = "Borealis Agent" - # Use pythonw.exe to avoid opening a console window in the user's session pyw = paths.get("venv_pythonw") or paths["venv_python"] - cmd = f"\"{pyw}\" -W ignore::SyntaxWarning \"{paths['agent_script']}\"" - - # If task exists, try remove (non-elevated first) + cmd = f'"{pyw}" -W ignore::SyntaxWarning "{paths["agent_script"]}"' + # Try create non-elevated q = run(["schtasks.exe", "/Query", "/TN", task_name]) if q.returncode == 0: - d = run(["schtasks.exe", "/Delete", "/TN", task_name, "/F"], capture=True) + d = run(["schtasks.exe", "/Delete", "/TN", task_name, "/F"]) if d.returncode != 0: - # Attempt elevated deletion (task might have been created under admin previously) - ps = f""" -$ErrorActionPreference = 'SilentlyContinue' -try {{ schtasks.exe /Delete /TN "{task_name}" /F | Out-Null }} catch {{}} -""" - run_elevated_powershell(paths, ps, log_name) - - c = run(["schtasks.exe", "/Create", "/SC", "ONLOGON", "/TN", task_name, "/TR", cmd, "/F", "/RL", "LIMITED"], capture=True) - log_write(paths, log_name, f"[INFO] create rc={c.returncode} out={c.stdout} err={c.stderr}") - if c.returncode != 0: - # Fallback: elevate and register task for the current user SID - user = os.environ.get("USERNAME", "") - domain = os.environ.get("USERDOMAIN", os.environ.get("COMPUTERNAME", "")) - py = (paths.get("venv_pythonw") or paths["venv_python"]).replace("\"", "\"\"") - ag = paths["agent_script"].replace("\"", "\"\"") - content = f""" -$ErrorActionPreference = 'Continue' + pass + c = run(["schtasks.exe", "/Create", "/SC", "ONLOGON", "/TN", task_name, "/TR", cmd, "/F", "/RL", "LIMITED"]) + if c.returncode == 0: + run(["schtasks.exe", "/Run", "/TN", task_name]) + return True + # Elevated fallback using ScheduledTasks cmdlets for better reliability + ps = f""" +$ErrorActionPreference='Continue' $task = "{task_name}" -$py = "{py}" -$arg = "-W ignore::SyntaxWarning {ag}" -$cmd = '"' + $py + '" ' + $arg -$user = "{domain}\{user}" -try {{ - # Resolve user SID - $sid = (New-Object System.Security.Principal.NTAccount($user)).Translate([System.Security.Principal.SecurityIdentifier]).Value -}} catch {{ $sid = $null }} -try {{ - # Delete any existing task (any scope) - try {{ schtasks.exe /Delete /TN $task /F | Out-Null }} catch {{}} - $action = New-ScheduledTaskAction -Execute $py -Argument $arg - $trigger = New-ScheduledTaskTrigger -AtLogOn - $settings = New-ScheduledTaskSettingsSet -Hidden - if ($sid) {{ - $principal = New-ScheduledTaskPrincipal -UserId $sid -LogonType Interactive -RunLevel Limited - Register-ScheduledTask -TaskName $task -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force - }} else {{ - # Fallback: bind by username (use Interactive to avoid password) - $principal = New-ScheduledTaskPrincipal -UserId "{domain}\{user}" -LogonType Interactive -RunLevel Limited - Register-ScheduledTask -TaskName $task -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force - }} -}} catch {{ - "[ERROR] Task register failed: $_" | Out-File -FilePath "{os.path.join(paths['logs_dir'], log_name)}" -Append -Encoding UTF8 - exit 1 -}} +$py = "{pyw}" +$arg = "-W ignore::SyntaxWarning {paths['agent_script']}" +try {{ Unregister-ScheduledTask -TaskName $task -Confirm:$false -ErrorAction SilentlyContinue }} catch {{}} +$action = New-ScheduledTaskAction -Execute $py -Argument $arg +$trigger= New-ScheduledTaskTrigger -AtLogOn +$settings = New-ScheduledTaskSettingsSet -Hidden +Register-ScheduledTask -TaskName $task -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null +Start-ScheduledTask -TaskName $task | Out-Null """ - rc = run_elevated_powershell(paths, content, log_name) - if rc != 0: - return False - else: - # Created via schtasks; set Hidden=true via elevated PowerShell (schtasks lacks a /HIDDEN switch) - ps_hide = f""" -$ErrorActionPreference = 'SilentlyContinue' -try {{ - $settings = New-ScheduledTaskSettingsSet -Hidden - Set-ScheduledTask -TaskName "{task_name}" -Settings $settings | Out-Null -}} catch {{}} -""" - run_elevated_powershell(paths, ps_hide, log_name) - # Start immediately (if a session is active) - run(["schtasks.exe", "/Run", "/TN", task_name]) - return True + rc = run_elevated_powershell(paths, ps, "Borealis_CollectorTask_Install.log") + return rc == 0 def ensure_all(): paths = project_paths() ensure_dirs(paths) - ok_svc = ensure_script_service(paths) - # Service now launches per-session helper; scheduled task is not required. - return 0 if ok_svc else 1 + ok = ensure_user_logon_task(paths) + return 0 if ok else 1 def main(argv): - # Simple CLI if len(argv) <= 1: - print("Usage: agent_deployment.py [ensure-all|service-install|service-remove|task-ensure|task-remove]") + print("Usage: agent_deployment.py [ensure-all|task-ensure|task-remove]") return 2 cmd = argv[1].lower() paths = project_paths() ensure_dirs(paths) - if cmd == "ensure-all": return ensure_all() - if cmd == "service-install": - return 0 if ensure_script_service(paths) else 1 - if cmd == "service-remove": - name = "BorealisAgent" - if not is_admin(): - ps = f"try {{ sc.exe stop {name} }} catch {{}}; try {{ sc.exe delete {name} }} catch {{}}" - return run_elevated_powershell(paths, ps, "Borealis_ScriptService_Remove.log") - run(["sc.exe", "stop", name]) - r = run(["sc.exe", "delete", name]) - return r.returncode if cmd == "task-ensure": return 0 if ensure_user_logon_task(paths) else 1 if cmd == "task-remove": - tn = "Borealis Agent" - r = run(["schtasks.exe", "/Delete", "/TN", tn, "/F"]) - return r.returncode - + return run(["schtasks.exe", "/Delete", "/TN", "Borealis Agent", "/F"]).returncode print(f"Unknown command: {cmd}") return 2 if __name__ == "__main__": sys.exit(main(sys.argv)) + diff --git a/Data/Agent/agent_supervisor.py b/Data/Agent/agent_supervisor.py new file mode 100644 index 0000000..1b717bf --- /dev/null +++ b/Data/Agent/agent_supervisor.py @@ -0,0 +1,169 @@ +import os +import sys +import time +import subprocess +import threading +import datetime + +# Optional pywin32 imports for per-session launching +try: + import win32ts + import win32con + import win32process + import win32security + import win32profile + import win32api + import pywintypes +except Exception: + win32ts = None + + +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) +AGENT_DIR = os.path.join(ROOT, 'Agent') +BOREALIS_DIR = os.path.join(AGENT_DIR, 'Borealis') +LOG_DIR = os.path.join(ROOT, 'Logs', 'Agent') +os.makedirs(LOG_DIR, exist_ok=True) +LOG_FILE = os.path.join(LOG_DIR, 'Supervisor.log') + + +def log(msg: str): + try: + ts = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + with open(LOG_FILE, 'a', encoding='utf-8') as f: + f.write(f"[{ts}] {msg}\n") + except Exception: + pass + + +def venv_python(): + try: + exe_dir = os.path.join(AGENT_DIR, 'Scripts') + py = os.path.join(exe_dir, 'python.exe') + if os.path.isfile(py): + return py + except Exception: + pass + return sys.executable + + +def venv_pythonw(): + try: + exe_dir = os.path.join(AGENT_DIR, 'Scripts') + pyw = os.path.join(exe_dir, 'pythonw.exe') + if os.path.isfile(pyw): + return pyw + except Exception: + pass + return venv_python() + + +def ensure_script_agent(): + """Ensure LocalSystem script_agent.py is running; restart if not.""" + try: + # best-effort: avoid duplicate spawns + import psutil # type: ignore + for p in psutil.process_iter(['name', 'cmdline']): + try: + cl = (p.info.get('cmdline') or []) + if any('script_agent.py' in (part or '') for part in cl): + return + except Exception: + pass + except Exception: + pass + + py = venv_python() + script = os.path.join(ROOT, 'Data', 'Agent', 'script_agent.py') + try: + subprocess.Popen([py, '-W', 'ignore::SyntaxWarning', script], creationflags=(0x08000000 if os.name == 'nt' else 0)) + log('Launched script_agent.py') + except Exception as e: + log(f'Failed to launch script_agent.py: {e}') + + +def _enable_privileges(): + try: + hProc = win32api.GetCurrentProcess() + hTok = win32security.OpenProcessToken(hProc, win32con.TOKEN_ADJUST_PRIVILEGES | win32con.TOKEN_QUERY) + for name in [ + win32security.SE_ASSIGNPRIMARYTOKEN_NAME, + win32security.SE_INCREASE_QUOTA_NAME, + win32security.SE_TCB_NAME, + win32security.SE_BACKUP_NAME, + win32security.SE_RESTORE_NAME, + ]: + try: + luid = win32security.LookupPrivilegeValue(None, name) + win32security.AdjustTokenPrivileges(hTok, False, [(luid, win32con.SE_PRIVILEGE_ENABLED)]) + except Exception: + pass + except Exception: + pass + + +def active_sessions(): + ids = [] + try: + if win32ts is None: + return ids + for s in win32ts.WTSEnumerateSessions(None, 1, 0): + sid, _, state = s + if state == win32ts.WTSActive: + ids.append(sid) + except Exception: + pass + return ids + + +def launch_helper_in_session(session_id): + try: + if win32ts is None: + return False + _enable_privileges() + hUser = win32ts.WTSQueryUserToken(session_id) + primary = win32security.DuplicateTokenEx( + hUser, + win32con.MAXIMUM_ALLOWED, + win32security.SECURITY_ATTRIBUTES(), + win32security.SecurityImpersonation, + win32con.TOKEN_PRIMARY, + ) + env = win32profile.CreateEnvironmentBlock(primary, True) + si = win32process.STARTUPINFO() + si.lpDesktop = 'winsta0\\default' + cmd = f'"{venv_pythonw()}" -W ignore::SyntaxWarning "{os.path.join(BOREALIS_DIR, "borealis-agent.py")}"' + flags = getattr(win32con, 'CREATE_UNICODE_ENVIRONMENT', 0x00000400) + win32process.CreateProcessAsUser(primary, None, cmd, None, None, False, flags, env, BOREALIS_DIR, si) + log(f'Started user helper in session {session_id}') + return True + except Exception as e: + log(f'Failed to start helper in session {session_id}: {e}') + return False + + +def manage_user_helpers_loop(): + known = set() + while True: + try: + cur = set(active_sessions()) + for sid in cur: + if sid not in known: + launch_helper_in_session(sid) + known = cur + except Exception: + pass + time.sleep(3) + + +def main(): + log('Supervisor starting') + t = threading.Thread(target=manage_user_helpers_loop, daemon=True) + t.start() + while True: + ensure_script_agent() + time.sleep(5) + + +if __name__ == '__main__': + main() + diff --git a/Data/Agent/borealis-agent.py b/Data/Agent/borealis-agent.py index 8a53df2..583006b 100644 --- a/Data/Agent/borealis-agent.py +++ b/Data/Agent/borealis-agent.py @@ -998,18 +998,22 @@ async def on_quick_job_run(payload): content = payload.get('script_content') or '' # Only handle non-SYSTEM runs here; SYSTEM runs are handled by the LocalSystem service agent if run_mode == 'system': - # Optionally, could emit a status indicating delegation; for now, just ignore to avoid overlap + # Ignore: handled by SYSTEM supervisor/agent return if script_type != 'powershell': await sio.emit('quick_job_result', { 'job_id': job_id, 'status': 'Failed', 'stdout': '', 'stderr': f"Unsupported type: {script_type}" }) return - path = _write_temp_script(content, '.ps1') rc = 0; out = ''; err = '' if run_mode == 'admin': # Admin credentialed runs are disabled in current design - rc, out, err = -1, '', 'Admin credentialed runs are disabled; use SYSTEM (service) or Current User.' + rc, out, err = -1, '', 'Admin credentialed runs are disabled; use SYSTEM or Current User.' else: - rc, out, err = await _run_powershell_local(path) + # Prefer ephemeral scheduled task in current user context + rc, out, err = await _run_powershell_via_user_task(content) + if rc == -999: + # Fallback to direct execution + path = _write_temp_script(content, '.ps1') + rc, out, err = await _run_powershell_local(path) status = 'Success' if rc == 0 else 'Failed' await sio.emit('quick_job_result', { 'job_id': job_id, @@ -1053,6 +1057,60 @@ async def idle_task(): print(f"[FATAL] Idle task crashed: {e}") traceback.print_exc() +async def _run_powershell_via_user_task(content: str): + ps = None + if IS_WINDOWS: + ps = os.path.expandvars(r"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe") + if not os.path.isfile(ps): + ps = "powershell.exe" + else: + return -999, '', 'Windows only' + try: + temp_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'Temp') + temp_dir = os.path.abspath(temp_dir) + os.makedirs(temp_dir, exist_ok=True) + fd, path = tempfile.mkstemp(prefix='usr_task_', suffix='.ps1', dir=temp_dir, text=True) + with os.fdopen(fd, 'w', encoding='utf-8', newline='\n') as f: + f.write(content or '') + out_path = os.path.join(temp_dir, f'out_{uuid.uuid4().hex}.txt') + name = f"Borealis Agent - Task - {uuid.uuid4().hex} @ CurrentUser" + task_ps = f""" +$ErrorActionPreference='Continue' +$task = "{name}" +$ps = "{ps}" +$scr = "{path}" +$out = "{out_path}" +try {{ Unregister-ScheduledTask -TaskName $task -Confirm:$false -ErrorAction SilentlyContinue }} catch {{}} +$action = New-ScheduledTaskAction -Execute $ps -Argument ('-NoProfile -ExecutionPolicy Bypass -File "' + $scr + '" *> "' + $out + '"') +$settings = New-ScheduledTaskSettingsSet -DeleteExpiredTaskAfter (New-TimeSpan -Minutes 5) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries +$principal= New-ScheduledTaskPrincipal -UserId ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) -LogonType Interactive -RunLevel Limited +Register-ScheduledTask -TaskName $task -Action $action -Settings $settings -Principal $principal -Force | Out-Null +Start-ScheduledTask -TaskName $task | Out-Null +Start-Sleep -Seconds 2 +Get-ScheduledTask -TaskName $task | Out-Null +""" + proc = await asyncio.create_subprocess_exec(ps, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', task_ps, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + await proc.communicate() + if proc.returncode != 0: + return -999, '', 'failed to create user task' + # Wait for output + deadline = time.time() + 60 + out_data = '' + while time.time() < deadline: + try: + if os.path.isfile(out_path) and os.path.getsize(out_path) > 0: + with open(out_path, 'r', encoding='utf-8', errors='replace') as f: + out_data = f.read() + break + except Exception: + pass + await asyncio.sleep(1) + cleanup = f"try {{ Unregister-ScheduledTask -TaskName '{name}' -Confirm:$false }} catch {{}}" + await asyncio.create_subprocess_exec(ps, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', cleanup) + return 0, out_data or '', '' + except Exception as e: + return -999, '', str(e) + # ---------------- Dummy Qt Widget to Prevent Exit ---------------- class PersistentWindow(QtWidgets.QWidget): def __init__(self): diff --git a/Data/Agent/script_agent.py b/Data/Agent/script_agent.py index 5f5d0c5..8cc0a92 100644 --- a/Data/Agent/script_agent.py +++ b/Data/Agent/script_agent.py @@ -10,6 +10,8 @@ import tempfile import socketio import platform import time +import uuid +import tempfile def get_project_root(): @@ -89,7 +91,11 @@ async def main(): 'stderr': f"Unsupported type: {script_type}" }) return - rc, out, err = run_powershell_script_content(content) + # Preferred: run via ephemeral scheduled task under SYSTEM for isolation + rc, out, err = run_powershell_via_system_task(content) + if rc == -999: + # Fallback to direct execution if task creation not available + rc, out, err = run_powershell_script_content(content) status = 'Success' if rc == 0 else 'Failed' await sio.emit('quick_job_result', { 'job_id': job_id, @@ -144,5 +150,61 @@ async def main(): await asyncio.sleep(5) +def run_powershell_via_system_task(content: str): + """Create an ephemeral scheduled task under SYSTEM to run the script. + Returns (rc, stdout, stderr). If the environment lacks PowerShell ScheduledTasks module, returns (-999, '', 'unavailable'). + """ + ps_exe = os.path.expandvars(r"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe") + if not os.path.isfile(ps_exe): + ps_exe = 'powershell.exe' + try: + os.makedirs(os.path.join(get_project_root(), 'Temp'), exist_ok=True) + # Write the target script + script_fd, script_path = tempfile.mkstemp(prefix='sys_task_', suffix='.ps1', dir=os.path.join(get_project_root(), 'Temp'), text=True) + with os.fdopen(script_fd, 'w', encoding='utf-8', newline='\n') as f: + f.write(content or '') + # Output capture path + out_path = os.path.join(get_project_root(), 'Temp', f'out_{uuid.uuid4().hex}.txt') + task_name = f"Borealis Agent - Task - {uuid.uuid4().hex} @ SYSTEM" + # Build PS to create/run task with DeleteExpiredTaskAfter + task_ps = f""" +$ErrorActionPreference='Continue' +$task = "{task_name}" +$ps = "{ps_exe}" +$scr = "{script_path}" +$out = "{out_path}" +try {{ Unregister-ScheduledTask -TaskName $task -Confirm:$false -ErrorAction SilentlyContinue }} catch {{}} +$action = New-ScheduledTaskAction -Execute $ps -Argument ('-NoProfile -ExecutionPolicy Bypass -File "' + $scr + '" *> "' + $out + '"') +$settings = New-ScheduledTaskSettingsSet -DeleteExpiredTaskAfter (New-TimeSpan -Minutes 5) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries +$principal= New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest +Register-ScheduledTask -TaskName $task -Action $action -Settings $settings -Principal $principal -Force | Out-Null +Start-ScheduledTask -TaskName $task | Out-Null +Start-Sleep -Seconds 2 +Get-ScheduledTask -TaskName $task | Out-Null +""" + # Run task creation + proc = subprocess.run([ps_exe, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', task_ps], capture_output=True, text=True) + if proc.returncode != 0: + return -999, '', (proc.stderr or proc.stdout or 'scheduled task creation failed') + # Wait up to 60s for output to be written + deadline = time.time() + 60 + out_data = '' + while time.time() < deadline: + try: + if os.path.isfile(out_path) and os.path.getsize(out_path) > 0: + with open(out_path, 'r', encoding='utf-8', errors='replace') as f: + out_data = f.read() + break + except Exception: + pass + time.sleep(1) + # Cleanup task (best-effort) + cleanup_ps = f"try {{ Unregister-ScheduledTask -TaskName '{task_name}' -Confirm:$false }} catch {{}}" + subprocess.run([ps_exe, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', cleanup_ps], capture_output=True, text=True) + return 0, out_data or '', '' + except Exception as e: + return -999, '', str(e) + + if __name__ == '__main__': asyncio.run(main()) diff --git a/Data/Agent/tray_launcher.py b/Data/Agent/tray_launcher.py index 069647c..438e0e7 100644 --- a/Data/Agent/tray_launcher.py +++ b/Data/Agent/tray_launcher.py @@ -37,14 +37,12 @@ class TrayApp(QtWidgets.QSystemTrayIcon): self.action_show_console = self.menu.addAction('Switch to Foreground Mode') self.action_hide_console = self.menu.addAction('Switch to Background Mode') self.action_restart = self.menu.addAction('Restart Agent') - self.action_restart_service = self.menu.addAction('Restart Borealis Agent Service') self.menu.addSeparator() self.action_quit = self.menu.addAction('Quit Agent and Tray') self.action_show_console.triggered.connect(self.switch_to_console) self.action_hide_console.triggered.connect(self.switch_to_background) self.action_restart.triggered.connect(self.restart_agent) - self.action_restart_service.triggered.connect(self.restart_script_service) self.action_quit.triggered.connect(self.quit_all) self.setContextMenu(self.menu) @@ -101,35 +99,7 @@ class TrayApp(QtWidgets.QSystemTrayIcon): # Restart using current mode self._start_agent(console=self.console_mode) - def restart_script_service(self): - # Try direct stop/start; if fails (likely due to permissions), attempt elevation via PowerShell - service_name = 'BorealisAgent' - try: - # Stop service - subprocess.run(["sc.exe", "stop", service_name], check=False, capture_output=True) - # Start service - subprocess.run(["sc.exe", "start", service_name], check=False, capture_output=True) - return - except Exception: - pass - - # Fallback: elevate via PowerShell - try: - script = ( - f"$ErrorActionPreference='Continue'; " - f"try {{ Stop-Service -Name '{service_name}' -Force -ErrorAction SilentlyContinue }} catch {{}}; " - f"Start-Sleep -Seconds 1; " - f"try {{ Start-Service -Name '{service_name}' }} catch {{}};" - ) - # Start-Process PowerShell -Verb RunAs to elevate - ps_cmd = [ - 'powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass', - '-Command', - "Start-Process PowerShell -Verb RunAs -ArgumentList '-NoProfile -ExecutionPolicy Bypass -Command \"" + script.replace("\"", "\\\"") + "\"'" - ] - subprocess.Popen(ps_cmd) - except Exception: - pass + # Service controls removed in task-centric architecture def quit_all(self): self._stop_agent() diff --git a/Data/Agent/windows_script_service.py b/Data/Agent/windows_script_service.py deleted file mode 100644 index f044a9e..0000000 --- a/Data/Agent/windows_script_service.py +++ /dev/null @@ -1,274 +0,0 @@ -import win32serviceutil -import win32service -import win32event -import servicemanager -import subprocess -import os -import sys -import datetime -import threading - -# Session/process helpers for per-user helper launch -try: - import win32ts - import win32con - import win32process - import win32security - import win32profile - import win32api -except Exception: - win32ts = None - - -class BorealisAgentService(win32serviceutil.ServiceFramework): - _svc_name_ = "BorealisAgent" - _svc_display_name_ = "Borealis Agent" - _svc_description_ = "Background agent for data collection and remote script execution." - - def __init__(self, args): - win32serviceutil.ServiceFramework.__init__(self, args) - self.hWaitStop = win32event.CreateEvent(None, 0, 0, None) - self.proc = None - self.user_helpers = {} - self.helpers_thread = None - try: - self._log("Service initialized") - except Exception: - pass - - def SvcStop(self): - self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) - try: - self._log("Stop requested") - except Exception: - pass - try: - if self.proc and self.proc.poll() is None: - try: - self.proc.terminate() - except Exception: - pass - except Exception: - pass - # Stop user helpers - try: - for sid, h in list(self.user_helpers.items()): - try: - hp = h.get('hProcess') - if hp: - win32api.TerminateProcess(hp, 0) - except Exception: - pass - except Exception: - pass - win32event.SetEvent(self.hWaitStop) - - def SvcDoRun(self): - try: - servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, - servicemanager.PYS_SERVICE_STARTED, - (self._svc_name_, '')) - except Exception: - pass - try: - self._log("SvcDoRun entered") - except Exception: - pass - # Mark the service as running once initialized - try: - self.ReportServiceStatus(win32service.SERVICE_RUNNING) - except Exception: - pass - self.main() - - def main(self): - script_dir = os.path.dirname(os.path.abspath(__file__)) - agent_py = os.path.join(script_dir, 'script_agent.py') - - def _venv_python(): - try: - exe_dir = os.path.dirname(sys.executable) - py = os.path.join(exe_dir, 'python.exe') - if os.path.isfile(py): - return py - except Exception: - pass - return sys.executable - - def _start_script_agent(): - python = _venv_python() - try: - self._log(f"Launching script_agent via {python}") - return subprocess.Popen( - [python, '-W', 'ignore::SyntaxWarning', agent_py], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - creationflags=0x08000000 if os.name == 'nt' else 0 - ) - except Exception as e: - try: - self._log(f"Failed to start script_agent: {e}") - except Exception: - pass - return None - - # Launch the system script agent (and keep it alive) - self.proc = _start_script_agent() - - # Start per-user helper manager in a background thread - if win32ts is not None: - try: - self.helpers_thread = threading.Thread(target=self._manage_user_helpers_loop, daemon=True) - self.helpers_thread.start() - except Exception: - try: - self._log("Failed to start user helper manager thread") - except Exception: - pass - - # Monitor stop event and child process; restart child if it exits - while True: - rc = win32event.WaitForSingleObject(self.hWaitStop, 2000) - if rc == win32event.WAIT_OBJECT_0: - break - try: - if not self.proc or (self.proc and self.proc.poll() is not None): - # child exited; attempt restart - self._log("script_agent exited; attempting restart") - self.proc = _start_script_agent() - except Exception: - pass - - def _log(self, msg: str): - try: - root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) - logs = os.path.join(root, 'Logs') - os.makedirs(logs, exist_ok=True) - p = os.path.join(logs, 'AgentService_Runtime.log') - ts = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') - with open(p, 'a', encoding='utf-8') as f: - f.write(f"{ts} {msg}\n") - except Exception: - pass - - # ---------------------- Per-Session User Helper Management ---------------------- - def _enable_privileges(self): - try: - hProc = win32api.GetCurrentProcess() - hTok = win32security.OpenProcessToken( - hProc, - win32con.TOKEN_ADJUST_PRIVILEGES | win32con.TOKEN_QUERY, - ) - for name in [ - win32security.SE_ASSIGNPRIMARYTOKEN_NAME, - win32security.SE_INCREASE_QUOTA_NAME, - win32security.SE_TCB_NAME, - win32security.SE_BACKUP_NAME, - win32security.SE_RESTORE_NAME, - ]: - try: - luid = win32security.LookupPrivilegeValue(None, name) - win32security.AdjustTokenPrivileges( - hTok, False, [(luid, win32con.SE_PRIVILEGE_ENABLED)] - ) - except Exception: - pass - except Exception: - pass - - def _active_session_ids(self): - ids = [] - try: - sessions = win32ts.WTSEnumerateSessions(None, 1, 0) - for s in sessions: - # tuple: (SessionId, WinStationName, State) - sess_id, _, state = s - if state == win32ts.WTSActive: - ids.append(sess_id) - except Exception: - pass - return ids - - def _launch_helper_in_session(self, session_id): - try: - self._enable_privileges() - # User token for session - hUser = win32ts.WTSQueryUserToken(session_id) - # Duplicate to primary - primary = win32security.DuplicateTokenEx( - hUser, - win32con.MAXIMUM_ALLOWED, - win32security.SECURITY_ATTRIBUTES(), - win32security.SecurityImpersonation, - win32con.TOKEN_PRIMARY, - ) - env = win32profile.CreateEnvironmentBlock(primary, True) - startup = win32process.STARTUPINFO() - startup.lpDesktop = "winsta0\\default" - - # Compute pythonw + helper script - venv_dir = os.path.dirname(sys.executable) # e.g., Agent - pyw = os.path.join(venv_dir, 'pythonw.exe') - agent_dir = os.path.dirname(os.path.abspath(__file__)) - helper = os.path.join(agent_dir, 'borealis-agent.py') - cmd = f'"{pyw}" -W ignore::SyntaxWarning "{helper}"' - - flags = getattr(win32con, 'CREATE_NEW_PROCESS_GROUP', 0) - flags |= getattr(win32con, 'CREATE_UNICODE_ENVIRONMENT', 0x00000400) - proc_tuple = win32process.CreateProcessAsUser( - primary, - None, - cmd, - None, - None, - False, - flags, - env, - agent_dir, - startup, - ) - # proc_tuple: (hProcess, hThread, dwProcessId, dwThreadId) - self.user_helpers[session_id] = { - 'hProcess': proc_tuple[0], - 'hThread': proc_tuple[1], - 'pid': proc_tuple[2], - 'started': datetime.datetime.now(), - } - self._log(f"Started user helper in session {session_id}") - return True - except Exception as e: - try: - self._log(f"Failed to start helper in session {session_id}: {e}") - except Exception: - pass - return False - - def _manage_user_helpers_loop(self): - # Periodically ensure one user helper per active session - while True: - rc = win32event.WaitForSingleObject(self.hWaitStop, 2000) - if rc == win32event.WAIT_OBJECT_0: - break - try: - active = set(self._active_session_ids()) - # Start missing - for sid in active: - if sid not in self.user_helpers: - self._launch_helper_in_session(sid) - # Cleanup ended - for sid in list(self.user_helpers.keys()): - if sid not in active: - info = self.user_helpers.pop(sid, None) - try: - hp = info and info.get('hProcess') - if hp: - win32api.TerminateProcess(hp, 0) - except Exception: - pass - self._log(f"Cleaned helper for session {sid}") - except Exception: - pass - - -if __name__ == '__main__': - win32serviceutil.HandleCommandLine(BorealisAgentService) diff --git a/Data/Server/WebUI/src/Scheduling/Quick_Job.jsx b/Data/Server/WebUI/src/Scheduling/Quick_Job.jsx index d683cae..ae4e50c 100644 --- a/Data/Server/WebUI/src/Scheduling/Quick_Job.jsx +++ b/Data/Server/WebUI/src/Scheduling/Quick_Job.jsx @@ -185,7 +185,7 @@ export default function QuickJob({ open, onClose, hostnames = [] }) { label={Run as currently logged-in user} /> - Unchecked = run as SYSTEM (requires agent service) + Unchecked = Run-As BUILTIN\SYSTEM {error && (