mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-09-11 04:18:42 -06:00
Continued Work on Remote Script Execution
This commit is contained in:
747
Borealis.ps1
747
Borealis.ps1
@@ -1,19 +1,4 @@
|
|||||||
#////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Launch-Borealis.ps1
|
#////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/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
|
|
||||||
#>
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
@@ -62,20 +47,6 @@ if ($Server) {
|
|||||||
$host.UI.RawUI.WindowTitle = "Borealis"
|
$host.UI.RawUI.WindowTitle = "Borealis"
|
||||||
Clear-Host
|
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)
|
## Note: Heavy dependency downloads are deferred until selecting Server (option 1)
|
||||||
# ---------------------- ASCII Art Terminal Required Changes ----------------------
|
# ---------------------- ASCII Art Terminal Required Changes ----------------------
|
||||||
# Set the .NET Console output encoding to UTF8
|
# Set the .NET Console output encoding to UTF8
|
||||||
@@ -132,66 +103,8 @@ function Remove-BorealisServicesAndTasks {
|
|||||||
function Repair-BorealisAgent {
|
function Repair-BorealisAgent {
|
||||||
$logName = 'Repair.log'
|
$logName = 'Repair.log'
|
||||||
Write-AgentLog -FileName $logName -Message "=== Repair start ==="
|
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
|
Remove-BorealisServicesAndTasks -LogName $logName
|
||||||
Start-Sleep -Seconds 1
|
InstallOrUpdate-BorealisAgent
|
||||||
|
|
||||||
# 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)
|
|
||||||
Write-AgentLog -FileName $logName -Message "=== Repair end ==="
|
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-AgentTasks {
|
||||||
function Ensure-BorealisAgent-Service {
|
# Registers SYSTEM Supervisor + 5-min Watchdog via UAC-elevated stub
|
||||||
param(
|
param(
|
||||||
[string]$VenvPython,
|
[string]$ScriptRoot
|
||||||
[string]$ServiceScript
|
|
||||||
)
|
)
|
||||||
if (-not (Test-IsWindows)) {
|
$supName = 'Borealis Agent - Supervisor'
|
||||||
Write-Host "Agent service setup skipped (non-Windows)." -ForegroundColor DarkGray
|
$py = Join-Path $ScriptRoot 'Agent\Scripts\python.exe'
|
||||||
return
|
$supScript= Join-Path $ScriptRoot 'Data\Agent\agent_supervisor.py'
|
||||||
}
|
$wdName = 'Borealis Agent - Watchdog'
|
||||||
|
|
||||||
$serviceName = 'BorealisAgent'
|
$taskStub = @"
|
||||||
$svc = Get-CimInstance -ClassName Win32_Service -Filter "Name='$serviceName'" -ErrorAction SilentlyContinue
|
$ErrorActionPreference='Continue'
|
||||||
$needsInstall = $false
|
$sup = '$supName'
|
||||||
if (-not $svc) { $needsInstall = $true }
|
$py = "$py"
|
||||||
else {
|
$supScr = "$supScript"
|
||||||
if ($svc.StartMode -notin @('Auto','Automatic')) { $needsInstall = $true }
|
$wd = '$wdName'
|
||||||
if ($svc.StartName -ne 'LocalSystem') { $needsInstall = $true }
|
|
||||||
# Verify the service points to the current project venv (folder may have moved)
|
try { Unregister-ScheduledTask -TaskName $sup -Confirm:$false -ErrorAction SilentlyContinue } catch {}
|
||||||
try {
|
$a = New-ScheduledTaskAction -Execute $py -Argument ('-W ignore::SyntaxWarning "' + $supScr + '"')
|
||||||
$venvRoot = Split-Path $VenvPython -Parent
|
$t = New-ScheduledTaskTrigger -AtStartup
|
||||||
$pathName = $svc.PathName
|
$s = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -Hidden -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1)
|
||||||
if ($pathName -and $venvRoot) {
|
$p = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
|
||||||
if ($pathName -notlike ("*" + $venvRoot + "*")) { $needsInstall = $true }
|
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) {
|
Run-Step "Install Python Dependencies" {
|
||||||
if (-not (Test-IsAdmin)) {
|
if (Test-Path $agentRequirements) {
|
||||||
Write-Host "Admin rights required to install/update the agent service. Prompting UAC..." -ForegroundColor Yellow
|
& $venvPython -m pip install --disable-pip-version-check -q -r $agentRequirements | Out-Null
|
||||||
$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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($svc.State -ne 'Running') {
|
Write-Host "`nConfiguring Borealis Agent (tasks)..." -ForegroundColor Blue
|
||||||
if (-not (Test-IsAdmin)) {
|
Write-Host "===================================================================================="
|
||||||
Write-Host "Admin rights required to start the agent service. Prompting UAC..." -ForegroundColor Yellow
|
Ensure-AgentTasks -ScriptRoot $scriptDir
|
||||||
$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 "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)
|
# ---------------------- Main ----------------------
|
||||||
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 ----------------------
|
|
||||||
Clear-Host
|
Clear-Host
|
||||||
|
|
||||||
# ASCII Art Banner
|
# ASCII Art Banner
|
||||||
@@ -749,9 +455,6 @@ Clear-Host
|
|||||||
'@ | Write-Host -ForegroundColor DarkCyan
|
'@ | Write-Host -ForegroundColor DarkCyan
|
||||||
Write-Host "Automation Platform" -ForegroundColor DarkGray
|
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) {
|
if (-not $choice) {
|
||||||
Write-Host " "
|
Write-Host " "
|
||||||
Write-Host "Please choose which function you want to launch:"
|
Write-Host "Please choose which function you want to launch:"
|
||||||
@@ -769,48 +472,35 @@ if (-not $choice) {
|
|||||||
Write-Host "<ENTER>" -ForegroundColor DarkCyan
|
Write-Host "<ENTER>" -ForegroundColor DarkCyan
|
||||||
$choice = Read-Host
|
$choice = Read-Host
|
||||||
}
|
}
|
||||||
switch ($choice) {
|
|
||||||
|
|
||||||
|
switch ($choice) {
|
||||||
"1" {
|
"1" {
|
||||||
$host.UI.RawUI.WindowTitle = "Borealis Server"
|
$host.UI.RawUI.WindowTitle = "Borealis Server"
|
||||||
Write-Host "Ensuring Server Dependencies Exist..." -ForegroundColor DarkCyan
|
Write-Host "Ensuring Server Dependencies Exist..." -ForegroundColor DarkCyan
|
||||||
|
|
||||||
# ---------------------- Ensure users.json is Present ----------------------
|
|
||||||
Run-Step "First-Run: Generating users.json" {
|
Run-Step "First-Run: Generating users.json" {
|
||||||
$usersJsonPath = Join-Path $scriptDir "users.json"
|
$usersJsonPath = Join-Path $scriptDir "users.json"
|
||||||
if (-not (Test-Path $usersJsonPath)) {
|
if (-not (Test-Path $usersJsonPath)) {
|
||||||
$defaultUsers = @{ users = @(@{ username = "admin"; password = "e6c83b282aeb2e022844595721cc00bbda47cb24537c1779f9bb84f04039e1676e6ba8573e588da1052510e3aa0a32a9e55879ae22b0c2d62136fc0a3e85f8bb" }) } |
|
$defaultUsers = @{ users = @(@{ username = "admin"; password = "e6c83b282aeb2e022844595721cc00bbda47cb24537c1779f9bb84f04039e1676e6ba8573e588da1052510e3aa0a32a9e55879ae22b0c2d62136fc0a3e85f8bb" }) } | ConvertTo-Json -Depth 4
|
||||||
ConvertTo-Json -Depth 4
|
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
[System.IO.File]::WriteAllText($usersJsonPath, $defaultUsers, $utf8NoBom)
|
||||||
[System.IO.File]::WriteAllText($usersJsonPath, $defaultUsers, $utf8NoBom)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Install_Shared_Dependencies
|
Install_Shared_Dependencies
|
||||||
Install_Server_Dependencies
|
Install_Server_Dependencies
|
||||||
|
|
||||||
# After ensuring dependencies, verify key tools and update PATH
|
|
||||||
foreach ($tool in @($pythonExe, $nodeExe, $npmCmd, $npxCmd)) {
|
foreach ($tool in @($pythonExe, $nodeExe, $npmCmd, $npxCmd)) {
|
||||||
if (-not (Test-Path $tool)) {
|
if (-not (Test-Path $tool)) { Write-Host "`r$($symbols.Fail) Bundled executable not found at '$tool'." -ForegroundColor Red; exit 1 }
|
||||||
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
|
$env:PATH = '{0};{1};{2}' -f (Split-Path $pythonExe), (Split-Path $nodeExe), $env:PATH
|
||||||
|
|
||||||
if (-not $modeChoice) {
|
if (-not $modeChoice) {
|
||||||
Write-Host " "
|
Write-Host " "
|
||||||
Write-Host "Configure Borealis Server Mode:" -ForegroundColor DarkYellow
|
Write-Host "Configure Borealis Server Mode:" -ForegroundColor DarkYellow
|
||||||
Write-Host " 1) Build & Launch > " -NoNewLine -ForegroundColor DarkGray
|
Write-Host " 1) Build & Launch > Production Flask Server @ http://localhost:5000" -ForegroundColor DarkCyan
|
||||||
Write-Host "Production Flask Server @ " -NoNewLine
|
Write-Host " 2) [Skip Build] & Immediately Launch > Production Flask Server @ http://localhost:5000" -ForegroundColor DarkCyan
|
||||||
Write-Host "http://localhost:5000" -ForegroundColor DarkCyan
|
Write-Host " 3) Launch > [Hotload-Ready] Vite Dev Server @ http://localhost:5173" -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
|
|
||||||
$modeChoice = Read-Host "Enter choice [1/2/3]"
|
$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")
|
& (Join-Path $scriptDir "Server\Scripts\python.exe") (Join-Path $scriptDir "Server\Borealis\server.py")
|
||||||
Pop-Location
|
Pop-Location
|
||||||
}
|
}
|
||||||
Exit 0
|
break
|
||||||
}
|
|
||||||
"3" { $borealis_operation_mode = "developer" }
|
|
||||||
default {
|
|
||||||
Write-Host "Invalid mode choice: $modeChoice" -ForegroundColor Red
|
|
||||||
Exit 1
|
|
||||||
}
|
}
|
||||||
|
"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
|
Write-Host "Deploying Borealis Server in '$borealis_operation_mode' mode" -ForegroundColor Blue
|
||||||
|
|
||||||
$venvFolder = "Server"
|
$venvFolder = "Server"
|
||||||
@@ -841,9 +527,7 @@ switch ($choice) {
|
|||||||
$webUIDestination = "$venvFolder\web-interface"
|
$webUIDestination = "$venvFolder\web-interface"
|
||||||
$venvPython = Join-Path $venvFolder 'Scripts\python.exe'
|
$venvPython = Join-Path $venvFolder 'Scripts\python.exe'
|
||||||
|
|
||||||
# Create Virtual Environment & Copy Server Assets
|
|
||||||
Run-Step "Create Borealis Virtual Python Environment" {
|
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 (-not (Test-Path "$venvFolder\Scripts\Activate")) { & $pythonExe -m venv $venvFolder | Out-Null }
|
||||||
if (Test-Path $dataSource) {
|
if (Test-Path $dataSource) {
|
||||||
Remove-Item $dataDestination -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item $dataDestination -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
@@ -855,14 +539,10 @@ switch ($choice) {
|
|||||||
. "$venvFolder\Scripts\Activate"
|
. "$venvFolder\Scripts\Activate"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Install Python Dependencies
|
|
||||||
Run-Step "Install Python Dependencies into Virtual Python Environment" {
|
Run-Step "Install Python Dependencies into Virtual Python Environment" {
|
||||||
if (Test-Path "$dataSource\Server\server-requirements.txt") {
|
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 }
|
||||||
& $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" {
|
Run-Step "Copy Borealis WebUI Files into: $webUIDestination" {
|
||||||
if (Test-Path $webUIDestination) {
|
if (Test-Path $webUIDestination) {
|
||||||
Remove-Item "$webUIDestination\public\*" -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item "$webUIDestination\public\*" -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
@@ -873,7 +553,6 @@ switch ($choice) {
|
|||||||
Copy-Item "$customUIPath\*" $webUIDestination -Recurse -Force
|
Copy-Item "$customUIPath\*" $webUIDestination -Recurse -Force
|
||||||
}
|
}
|
||||||
|
|
||||||
# NPM Install for WebUI
|
|
||||||
Run-Step "Vite Web Frontend: Install NPM Packages" {
|
Run-Step "Vite Web Frontend: Install NPM Packages" {
|
||||||
Push-Location $webUIDestination
|
Push-Location $webUIDestination
|
||||||
$env:npm_config_loglevel = "silent"
|
$env:npm_config_loglevel = "silent"
|
||||||
@@ -881,7 +560,6 @@ switch ($choice) {
|
|||||||
Pop-Location
|
Pop-Location
|
||||||
}
|
}
|
||||||
|
|
||||||
# Vite Operation Mode Control (build vs dev)
|
|
||||||
Run-Step "Vite Web Frontend: Start ($borealis_operation_mode)" {
|
Run-Step "Vite Web Frontend: Start ($borealis_operation_mode)" {
|
||||||
Push-Location $webUIDestination
|
Push-Location $webUIDestination
|
||||||
if ($borealis_operation_mode -eq "developer") { $viteSubCommand = "dev" } else { $viteSubCommand = "build" }
|
if ($borealis_operation_mode -eq "developer") { $viteSubCommand = "dev" } else { $viteSubCommand = "build" }
|
||||||
@@ -889,16 +567,13 @@ switch ($choice) {
|
|||||||
Pop-Location
|
Pop-Location
|
||||||
}
|
}
|
||||||
|
|
||||||
# Launch Flask Server
|
|
||||||
Run-Step "Borealis: Launch Flask Server" {
|
Run-Step "Borealis: Launch Flask Server" {
|
||||||
Push-Location (Join-Path $scriptDir "Server")
|
Push-Location (Join-Path $scriptDir "Server")
|
||||||
$py = Join-Path $scriptDir "Server\Scripts\python.exe"
|
$py = Join-Path $scriptDir "Server\Scripts\python.exe"
|
||||||
$server_py = Join-Path $scriptDir "Server\Borealis\server.py"
|
$server_py = Join-Path $scriptDir "Server\Borealis\server.py"
|
||||||
|
|
||||||
Write-Host "`nLaunching Borealis..." -ForegroundColor Green
|
Write-Host "`nLaunching Borealis..." -ForegroundColor Green
|
||||||
Write-Host "===================================================================================="
|
Write-Host "===================================================================================="
|
||||||
Write-Host "$($symbols.Running) Python Flask API Server Started..."
|
Write-Host "$($symbols.Running) Python Flask API Server Started..."
|
||||||
|
|
||||||
& $py $server_py
|
& $py $server_py
|
||||||
Pop-Location
|
Pop-Location
|
||||||
}
|
}
|
||||||
@@ -917,16 +592,10 @@ switch ($choice) {
|
|||||||
if (-not $agentSubChoice) { $agentSubChoice = Read-Host "Select an option" }
|
if (-not $agentSubChoice) { $agentSubChoice = Read-Host "Select an option" }
|
||||||
|
|
||||||
switch ($agentSubChoice) {
|
switch ($agentSubChoice) {
|
||||||
'2' {
|
'1' { InstallOrUpdate-BorealisAgent; break }
|
||||||
Repair-BorealisAgent
|
'2' { Repair-BorealisAgent; break }
|
||||||
break
|
'3' { Remove-BorealisAgent; break }
|
||||||
}
|
|
||||||
'3' {
|
|
||||||
Remove-BorealisAgent
|
|
||||||
break
|
|
||||||
}
|
|
||||||
'4' {
|
'4' {
|
||||||
# Manually launch helper for the current session (optional)
|
|
||||||
$venvPythonw = Join-Path $scriptDir 'Agent\Scripts\pythonw.exe'
|
$venvPythonw = Join-Path $scriptDir 'Agent\Scripts\pythonw.exe'
|
||||||
$helper = Join-Path $scriptDir 'Agent\Borealis\borealis-agent.py'
|
$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 }
|
if (-not (Test-Path $venvPythonw)) { Write-Host "pythonw.exe not found under Agent\Scripts" -ForegroundColor Yellow }
|
||||||
@@ -937,80 +606,12 @@ switch ($choice) {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
'5' { break }
|
default { 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"3" {
|
"3" {
|
||||||
$host.UI.RawUI.WindowTitle = "Borealis Electron"
|
$host.UI.RawUI.WindowTitle = "Borealis Electron"
|
||||||
# Desktop App Deployment (Electron)
|
|
||||||
Clear-Host
|
Clear-Host
|
||||||
Write-Host "Deploying Borealis Desktop App..." -ForegroundColor Cyan
|
Write-Host "Deploying Borealis Desktop App..." -ForegroundColor Cyan
|
||||||
Write-Host "===================================================================================="
|
Write-Host "===================================================================================="
|
||||||
@@ -1019,33 +620,19 @@ switch ($choice) {
|
|||||||
$electronDestination = "ElectronApp"
|
$electronDestination = "ElectronApp"
|
||||||
$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
|
$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
|
||||||
|
|
||||||
# 1) Prepare ElectronApp folder
|
|
||||||
Run-Step "Prepare ElectronApp folder" {
|
Run-Step "Prepare ElectronApp folder" {
|
||||||
if (Test-Path $electronDestination) {
|
if (Test-Path $electronDestination) { Remove-Item $electronDestination -Recurse -Force }
|
||||||
Remove-Item $electronDestination -Recurse -Force
|
|
||||||
}
|
|
||||||
New-Item -Path $electronDestination -ItemType Directory | Out-Null
|
New-Item -Path $electronDestination -ItemType Directory | Out-Null
|
||||||
|
|
||||||
# Copy deployed Flask server
|
|
||||||
$deployedServer = Join-Path $scriptDir 'Server\Borealis'
|
$deployedServer = Join-Path $scriptDir 'Server\Borealis'
|
||||||
if (-not (Test-Path $deployedServer)) {
|
if (-not (Test-Path $deployedServer)) { throw "Server\Borealis not found - please run choice 1 first." }
|
||||||
throw "Server\Borealis not found - please run choice 1 first."
|
|
||||||
}
|
|
||||||
Copy-Item $deployedServer "$electronDestination\Server" -Recurse
|
Copy-Item $deployedServer "$electronDestination\Server" -Recurse
|
||||||
|
|
||||||
# Copy Electron scaffold files
|
|
||||||
Copy-Item "$electronSource\package.json" "$electronDestination" -Force
|
Copy-Item "$electronSource\package.json" "$electronDestination" -Force
|
||||||
Copy-Item "$electronSource\main.js" "$electronDestination" -Force
|
Copy-Item "$electronSource\main.js" "$electronDestination" -Force
|
||||||
|
|
||||||
# Copy built WebUI into renderer
|
|
||||||
$staticBuild = Join-Path $scriptDir 'Server\web-interface\build'
|
$staticBuild = Join-Path $scriptDir 'Server\web-interface\build'
|
||||||
if (-not (Test-Path $staticBuild)) {
|
if (-not (Test-Path $staticBuild)) { throw "WebUI build not found - run choice 1 to build WebUI first." }
|
||||||
throw "WebUI build not found - run choice 1 to build WebUI first."
|
|
||||||
}
|
|
||||||
Copy-Item "$staticBuild\*" "$electronDestination\renderer" -Recurse -Force
|
Copy-Item "$staticBuild\*" "$electronDestination\renderer" -Recurse -Force
|
||||||
}
|
}
|
||||||
|
|
||||||
# 2) Install Electron & Builder
|
|
||||||
Run-Step "ElectronApp: Install Node dependencies" {
|
Run-Step "ElectronApp: Install Node dependencies" {
|
||||||
Push-Location $electronDestination
|
Push-Location $electronDestination
|
||||||
$env:NODE_ENV = ''
|
$env:NODE_ENV = ''
|
||||||
@@ -1054,14 +641,12 @@ switch ($choice) {
|
|||||||
Pop-Location
|
Pop-Location
|
||||||
}
|
}
|
||||||
|
|
||||||
# 3) Package desktop app
|
|
||||||
Run-Step "ElectronApp: Package with electron-builder" {
|
Run-Step "ElectronApp: Package with electron-builder" {
|
||||||
Push-Location $electronDestination
|
Push-Location $electronDestination
|
||||||
& $npmCmd run dist
|
& $npmCmd run dist
|
||||||
Pop-Location
|
Pop-Location
|
||||||
}
|
}
|
||||||
|
|
||||||
# 4) Launch in dev mode
|
|
||||||
Run-Step "ElectronApp: Launch in dev mode" {
|
Run-Step "ElectronApp: Launch in dev mode" {
|
||||||
Push-Location $electronDestination
|
Push-Location $electronDestination
|
||||||
& $npmCmd run dev
|
& $npmCmd run dev
|
||||||
@@ -1071,43 +656,29 @@ switch ($choice) {
|
|||||||
|
|
||||||
"4" {
|
"4" {
|
||||||
$host.UI.RawUI.WindowTitle = "Borealis Packager"
|
$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 "Choose which module to package into a self-contained EXE file:" -ForegroundColor DarkYellow
|
||||||
Write-Host " 1) Server" -ForegroundColor DarkGray
|
Write-Host " 1) Server" -ForegroundColor DarkGray
|
||||||
Write-Host " 2) Agent" -ForegroundColor DarkGray
|
Write-Host " 2) Agent" -ForegroundColor DarkGray
|
||||||
$exePackageChoice = Read-Host "Enter choice [1/2]"
|
$exePackageChoice = Read-Host "Enter choice [1/2]"
|
||||||
|
|
||||||
switch ($exePackageChoice) {
|
switch ($exePackageChoice) {
|
||||||
"1" {
|
"1" {
|
||||||
$serverScriptDir = Join-Path -Path $PSScriptRoot -ChildPath "Data\Server"
|
$serverScriptDir = Join-Path -Path $PSScriptRoot -ChildPath "Data\Server"
|
||||||
Set-Location -Path $serverScriptDir
|
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"
|
$agentScriptDir = Join-Path -Path $PSScriptRoot -ChildPath "Data\Agent"
|
||||||
Set-Location -Path $agentScriptDir
|
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 Choice. Exiting..." -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
default {
|
|
||||||
Write-Host "Invalid selection. Exiting..." -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
"5" {
|
"5" {
|
||||||
$host.UI.RawUI.WindowTitle = "Borealis Updater"
|
$host.UI.RawUI.WindowTitle = "Borealis Updater"
|
||||||
Write-Host " "
|
Write-Host " "
|
||||||
Write-Host "Updating Borealis..." -ForegroundColor Green
|
Write-Host "Updating Borealis..." -ForegroundColor Green
|
||||||
|
|
||||||
# Prepare paths
|
|
||||||
$updateZip = Join-Path $scriptDir "Update_Staging\main.zip"
|
$updateZip = Join-Path $scriptDir "Update_Staging\main.zip"
|
||||||
$updateDir = Join-Path $scriptDir "Update_Staging\Borealis-main"
|
$updateDir = Join-Path $scriptDir "Update_Staging\Borealis-main"
|
||||||
$preservePath = Join-Path $scriptDir "Data\Server\Python_API_Endpoints\Tesseract-OCR"
|
$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" {
|
Run-Step "Updating: Move Tesseract-OCR Folder Somewhere Safe to Restore Later" {
|
||||||
if (Test-Path $preservePath) {
|
if (Test-Path $preservePath) {
|
||||||
# Ensure staging folder exists
|
|
||||||
$stagingPath = Join-Path $scriptDir "Update_Staging"
|
$stagingPath = Join-Path $scriptDir "Update_Staging"
|
||||||
if (-not (Test-Path $stagingPath)) {
|
if (-not (Test-Path $stagingPath)) { New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null }
|
||||||
New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
Move-Item -Path $preservePath -Destination $preserveBackupPath -Force
|
Move-Item -Path $preservePath -Destination $preserveBackupPath -Force
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Run-Step "Updating: Clean Up Folders to Prepare for Update" {
|
Run-Step "Updating: Clean Up Folders to Prepare for Update" {
|
||||||
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue `
|
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue `
|
||||||
(Join-Path $scriptDir "Data"), `
|
(Join-Path $scriptDir "Data"), `
|
||||||
(Join-Path $scriptDir "Server\web-interface\src"), `
|
(Join-Path $scriptDir "Server\web-interface\src"), `
|
||||||
(Join-Path $scriptDir "Server\web-interface\build"), `
|
(Join-Path $scriptDir "Server\web-interface\build"), `
|
||||||
(Join-Path $scriptDir "Server\web-interface\public"), `
|
(Join-Path $scriptDir "Server\web-interface\public"), `
|
||||||
(Join-Path $scriptDir "Server\Borealis")
|
(Join-Path $scriptDir "Server\Borealis")
|
||||||
}
|
}
|
||||||
|
|
||||||
Run-Step "Updating: Create Update Staging Folder" {
|
Run-Step "Updating: Create Update Staging Folder" {
|
||||||
$stagingPath = Join-Path $scriptDir "Update_Staging"
|
$stagingPath = Join-Path $scriptDir "Update_Staging"
|
||||||
if (-not (Test-Path $stagingPath)) {
|
if (-not (Test-Path $stagingPath)) { New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null }
|
||||||
New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
$updateZip = Join-Path $stagingPath "main.zip"
|
$updateZip = Join-Path $stagingPath "main.zip"
|
||||||
$updateDir = Join-Path $stagingPath "Borealis-main"
|
$updateDir = Join-Path $stagingPath "Borealis-main"
|
||||||
}
|
}
|
||||||
|
|
||||||
Run-Step "Updating: Download Update from https://github.com/bunny-lab-io/Borealis/archive/refs/heads/main.zip" {
|
Run-Step "Updating: Download Update" { Invoke-WebRequest -Uri "https://github.com/bunny-lab-io/Borealis/archive/refs/heads/main.zip" -OutFile $updateZip }
|
||||||
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: 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" {
|
Run-Step "Updating: Restore Tesseract-OCR Folder" {
|
||||||
$restorePath = Join-Path $scriptDir "Data\Server\Python_API_Endpoints"
|
$restorePath = Join-Path $scriptDir "Data\Server\Python_API_Endpoints"
|
||||||
if (Test-Path $preserveBackupPath) {
|
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
|
Move-Item -Path $preserveBackupPath -Destination $restorePath -Force
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Run-Step "Updating: Clean Up Update Staging Folder" {
|
Run-Step "Updating: Clean Up Update Staging Folder" { Remove-Item -Recurse -Force -ErrorAction SilentlyContinue (Join-Path $scriptDir "Update_Staging") }
|
||||||
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue (Join-Path $scriptDir "Update_Staging")
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "`nUpdate Complete! Please Re-Launch the Borealis Script." -ForegroundColor Green
|
Write-Host "`nUpdate Complete! Please Re-Launch the Borealis Script." -ForegroundColor Green
|
||||||
Read-Host "Press any key to re-launch Borealis..."
|
Read-Host "Press any key to re-launch Borealis..."
|
||||||
@@ -1182,12 +732,11 @@ switch ($choice) {
|
|||||||
$host.UI.RawUI.WindowTitle = "AutoHotKey Automation Testing"
|
$host.UI.RawUI.WindowTitle = "AutoHotKey Automation Testing"
|
||||||
Write-Host " "
|
Write-Host " "
|
||||||
Write-Host "Lauching AutoHotKey Testing Script..." -ForegroundColor Blue
|
Write-Host "Lauching AutoHotKey Testing Script..." -ForegroundColor Blue
|
||||||
|
|
||||||
$venvFolder = "Macro_Testing"
|
$venvFolder = "Macro_Testing"
|
||||||
$scriptSourcePath = "Data\Experimental\Macros\Macro_Script.py"
|
$scriptSourcePath = "Data\Experimental\Macros\Macro_Script.py"
|
||||||
$scriptRequirements = "Data\Experimental\Macros\macro-requirements.txt"
|
$scriptRequirements = "Data\Experimental\Macros\macro-requirements.txt"
|
||||||
$scriptDestinationFolder = "$venvFolder\Borealis"
|
$scriptDestinationFolder= "$venvFolder\Borealis"
|
||||||
$scriptDestinationFile = "$venvFolder\Borealis\Macro_Script.py"
|
$scriptDestinationFile = "$venvFolder\Borealis\Macro_Script.py"
|
||||||
$venvPython = Join-Path $scriptDir $venvFolder | Join-Path -ChildPath 'Scripts\python.exe'
|
$venvPython = Join-Path $scriptDir $venvFolder | Join-Path -ChildPath 'Scripts\python.exe'
|
||||||
|
|
||||||
Run-Step "Create Virtual Python Environment" {
|
Run-Step "Create Virtual Python Environment" {
|
||||||
@@ -1198,10 +747,7 @@ switch ($choice) {
|
|||||||
$pythonCmd = Get-Command python -ErrorAction SilentlyContinue
|
$pythonCmd = Get-Command python -ErrorAction SilentlyContinue
|
||||||
if ($pyCmd) { $pythonForVenv = $pyCmd.Source }
|
if ($pyCmd) { $pythonForVenv = $pyCmd.Source }
|
||||||
elseif ($pythonCmd) { $pythonForVenv = $pythonCmd.Source }
|
elseif ($pythonCmd) { $pythonForVenv = $pythonCmd.Source }
|
||||||
else {
|
else { Write-Host "Python not found. Install Python or run Server setup (option 1)." -ForegroundColor Red; exit 1 }
|
||||||
Write-Host "Python not found. Install Python or run Server setup (option 1)." -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
& $pythonForVenv -m venv $venvFolder
|
& $pythonForVenv -m venv $venvFolder
|
||||||
}
|
}
|
||||||
@@ -1214,14 +760,11 @@ switch ($choice) {
|
|||||||
. "$venvFolder\Scripts\Activate"
|
. "$venvFolder\Scripts\Activate"
|
||||||
}
|
}
|
||||||
|
|
||||||
Run-Step "Install Python Dependencies" {
|
Run-Step "Install Python Dependencies" { if (Test-Path $scriptRequirements) { & $venvPython -m pip install --disable-pip-version-check -q -r $scriptRequirements | Out-Null } }
|
||||||
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 "`nLaunching Macro Testing Script..." -ForegroundColor Blue
|
||||||
Write-Host "===================================================================================="
|
Write-Host "===================================================================================="
|
||||||
& $venvPython -W ignore::SyntaxWarning $scriptDestinationFile
|
& $venvPython -W ignore::SyntaxWarning $scriptDestinationFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default { Write-Host "Invalid selection. Exiting..." -ForegroundColor Red; exit 1 }
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
import shlex
|
|
||||||
import ctypes
|
import ctypes
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@@ -11,12 +10,6 @@ def _now():
|
|||||||
|
|
||||||
|
|
||||||
def project_paths():
|
def project_paths():
|
||||||
r"""Derive important paths relative to the venv python (sys.executable).
|
|
||||||
|
|
||||||
Layout assumed at runtime:
|
|
||||||
<ProjectRoot>/Agent/Scripts/python.exe == sys.executable
|
|
||||||
<ProjectRoot>/Agent/Borealis/*.py == deployed agent files
|
|
||||||
"""
|
|
||||||
venv_scripts = os.path.dirname(os.path.abspath(sys.executable))
|
venv_scripts = os.path.dirname(os.path.abspath(sys.executable))
|
||||||
venv_root = os.path.abspath(os.path.join(venv_scripts, os.pardir))
|
venv_root = os.path.abspath(os.path.join(venv_scripts, os.pardir))
|
||||||
project_root = os.path.abspath(os.path.join(venv_root, 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,
|
"borealis_dir": borealis_dir,
|
||||||
"logs_dir": logs_dir,
|
"logs_dir": logs_dir,
|
||||||
"temp_dir": temp_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"),
|
"agent_script": os.path.join(borealis_dir, "tray_launcher.py"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,43 +44,21 @@ def log_write(paths, name, text):
|
|||||||
|
|
||||||
def is_admin():
|
def is_admin():
|
||||||
try:
|
try:
|
||||||
return ctypes.windll.shell32.IsUserAnAdmin() != 0 # type: ignore[attr-defined]
|
return ctypes.windll.shell32.IsUserAnAdmin() != 0
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def run(cmd, cwd=None, capture=False):
|
def run(cmd, capture=False):
|
||||||
if isinstance(cmd, str):
|
return subprocess.run(cmd, text=True, capture_output=capture, check=False)
|
||||||
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_elevated_powershell(paths, ps_content, log_name):
|
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)
|
ensure_dirs(paths)
|
||||||
log_path = os.path.join(paths["logs_dir"], log_name)
|
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")
|
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:
|
with open(stub_path, "w", encoding="utf-8") as f:
|
||||||
f.write(ps_content)
|
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
|
SEE_MASK_NOCLOSEPROCESS = 0x00000040
|
||||||
class SHELLEXECUTEINFO(ctypes.Structure):
|
class SHELLEXECUTEINFO(ctypes.Structure):
|
||||||
_fields_ = [
|
_fields_ = [
|
||||||
@@ -109,31 +78,21 @@ def run_elevated_powershell(paths, ps_content, log_name):
|
|||||||
("hIcon", ctypes.c_void_p),
|
("hIcon", ctypes.c_void_p),
|
||||||
("hProcess", ctypes.c_void_p),
|
("hProcess", ctypes.c_void_p),
|
||||||
]
|
]
|
||||||
|
|
||||||
sei = SHELLEXECUTEINFO()
|
sei = SHELLEXECUTEINFO()
|
||||||
sei.cbSize = ctypes.sizeof(SHELLEXECUTEINFO)
|
sei.cbSize = ctypes.sizeof(SHELLEXECUTEINFO)
|
||||||
sei.fMask = SEE_MASK_NOCLOSEPROCESS
|
sei.fMask = SEE_MASK_NOCLOSEPROCESS
|
||||||
sei.hwnd = None
|
sei.hwnd = None
|
||||||
sei.lpVerb = "runas"
|
sei.lpVerb = "runas"
|
||||||
sei.lpFile = ps
|
sei.lpFile = "powershell.exe"
|
||||||
sei.lpParameters = args
|
sei.lpParameters = f"-NoProfile -ExecutionPolicy Bypass -File \"{stub_path}\""
|
||||||
sei.lpDirectory = paths["project_root"]
|
sei.lpDirectory = paths["project_root"]
|
||||||
sei.nShow = 1
|
sei.nShow = 1
|
||||||
if not ctypes.windll.shell32.ShellExecuteExW(ctypes.byref(sei)):
|
if not ctypes.windll.shell32.ShellExecuteExW(ctypes.byref(sei)):
|
||||||
log_write(paths, log_name, "[ERROR] UAC elevation failed (ShellExecuteExW)")
|
log_write(paths, log_name, "[ERROR] UAC elevation failed (ShellExecuteExW)")
|
||||||
try:
|
|
||||||
os.remove(stub_path)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return 1
|
return 1
|
||||||
# Wait for elevated process
|
hproc = sei.hProcess
|
||||||
ctypes.windll.kernel32.WaitForSingleObject(sei.hProcess, 0xFFFFFFFF)
|
if hproc:
|
||||||
# Capture output from stub if it appended
|
ctypes.windll.kernel32.WaitForSingleObject(hproc, 0xFFFFFFFF)
|
||||||
try:
|
|
||||||
with open(log_path, "a", encoding="utf-8") as f:
|
|
||||||
f.write("")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
try:
|
||||||
os.remove(stub_path)
|
os.remove(stub_path)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -141,262 +100,64 @@ def run_elevated_powershell(paths, ps_content, log_name):
|
|||||||
return 0
|
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):
|
def ensure_user_logon_task(paths):
|
||||||
"""Ensure the per-user scheduled task that launches the agent in the user's session.
|
"""Ensure per-user scheduled task that launches the helper at logon.
|
||||||
|
Task name: "Borealis Agent"
|
||||||
Name: "Borealis Agent"
|
|
||||||
Trigger: On logon (current user)
|
|
||||||
Action: <venv_python> -W ignore::SyntaxWarning <agent_script>
|
|
||||||
"""
|
"""
|
||||||
ensure_dirs(paths)
|
|
||||||
log_name = "Borealis_CollectorTask_Install.log"
|
|
||||||
task_name = "Borealis Agent"
|
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"]
|
pyw = paths.get("venv_pythonw") or paths["venv_python"]
|
||||||
cmd = f"\"{pyw}\" -W ignore::SyntaxWarning \"{paths['agent_script']}\""
|
cmd = f'"{pyw}" -W ignore::SyntaxWarning "{paths["agent_script"]}"'
|
||||||
|
# Try create non-elevated
|
||||||
# If task exists, try remove (non-elevated first)
|
|
||||||
q = run(["schtasks.exe", "/Query", "/TN", task_name])
|
q = run(["schtasks.exe", "/Query", "/TN", task_name])
|
||||||
if q.returncode == 0:
|
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:
|
if d.returncode != 0:
|
||||||
# Attempt elevated deletion (task might have been created under admin previously)
|
pass
|
||||||
ps = f"""
|
c = run(["schtasks.exe", "/Create", "/SC", "ONLOGON", "/TN", task_name, "/TR", cmd, "/F", "/RL", "LIMITED"])
|
||||||
$ErrorActionPreference = 'SilentlyContinue'
|
if c.returncode == 0:
|
||||||
try {{ schtasks.exe /Delete /TN "{task_name}" /F | Out-Null }} catch {{}}
|
run(["schtasks.exe", "/Run", "/TN", task_name])
|
||||||
"""
|
return True
|
||||||
run_elevated_powershell(paths, ps, log_name)
|
# Elevated fallback using ScheduledTasks cmdlets for better reliability
|
||||||
|
ps = f"""
|
||||||
c = run(["schtasks.exe", "/Create", "/SC", "ONLOGON", "/TN", task_name, "/TR", cmd, "/F", "/RL", "LIMITED"], capture=True)
|
$ErrorActionPreference='Continue'
|
||||||
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'
|
|
||||||
$task = "{task_name}"
|
$task = "{task_name}"
|
||||||
$py = "{py}"
|
$py = "{pyw}"
|
||||||
$arg = "-W ignore::SyntaxWarning {ag}"
|
$arg = "-W ignore::SyntaxWarning {paths['agent_script']}"
|
||||||
$cmd = '"' + $py + '" ' + $arg
|
try {{ Unregister-ScheduledTask -TaskName $task -Confirm:$false -ErrorAction SilentlyContinue }} catch {{}}
|
||||||
$user = "{domain}\{user}"
|
$action = New-ScheduledTaskAction -Execute $py -Argument $arg
|
||||||
try {{
|
$trigger= New-ScheduledTaskTrigger -AtLogOn
|
||||||
# Resolve user SID
|
$settings = New-ScheduledTaskSettingsSet -Hidden
|
||||||
$sid = (New-Object System.Security.Principal.NTAccount($user)).Translate([System.Security.Principal.SecurityIdentifier]).Value
|
Register-ScheduledTask -TaskName $task -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null
|
||||||
}} catch {{ $sid = $null }}
|
Start-ScheduledTask -TaskName $task | Out-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
|
|
||||||
}}
|
|
||||||
"""
|
"""
|
||||||
rc = run_elevated_powershell(paths, content, log_name)
|
rc = run_elevated_powershell(paths, ps, "Borealis_CollectorTask_Install.log")
|
||||||
if rc != 0:
|
return 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
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_all():
|
def ensure_all():
|
||||||
paths = project_paths()
|
paths = project_paths()
|
||||||
ensure_dirs(paths)
|
ensure_dirs(paths)
|
||||||
ok_svc = ensure_script_service(paths)
|
ok = ensure_user_logon_task(paths)
|
||||||
# Service now launches per-session helper; scheduled task is not required.
|
return 0 if ok else 1
|
||||||
return 0 if ok_svc else 1
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv):
|
def main(argv):
|
||||||
# Simple CLI
|
|
||||||
if len(argv) <= 1:
|
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
|
return 2
|
||||||
cmd = argv[1].lower()
|
cmd = argv[1].lower()
|
||||||
paths = project_paths()
|
paths = project_paths()
|
||||||
ensure_dirs(paths)
|
ensure_dirs(paths)
|
||||||
|
|
||||||
if cmd == "ensure-all":
|
if cmd == "ensure-all":
|
||||||
return 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":
|
if cmd == "task-ensure":
|
||||||
return 0 if ensure_user_logon_task(paths) else 1
|
return 0 if ensure_user_logon_task(paths) else 1
|
||||||
if cmd == "task-remove":
|
if cmd == "task-remove":
|
||||||
tn = "Borealis Agent"
|
return run(["schtasks.exe", "/Delete", "/TN", "Borealis Agent", "/F"]).returncode
|
||||||
r = run(["schtasks.exe", "/Delete", "/TN", tn, "/F"])
|
|
||||||
return r.returncode
|
|
||||||
|
|
||||||
print(f"Unknown command: {cmd}")
|
print(f"Unknown command: {cmd}")
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(main(sys.argv))
|
sys.exit(main(sys.argv))
|
||||||
|
|
||||||
|
169
Data/Agent/agent_supervisor.py
Normal file
169
Data/Agent/agent_supervisor.py
Normal file
@@ -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()
|
||||||
|
|
@@ -998,18 +998,22 @@ async def on_quick_job_run(payload):
|
|||||||
content = payload.get('script_content') or ''
|
content = payload.get('script_content') or ''
|
||||||
# Only handle non-SYSTEM runs here; SYSTEM runs are handled by the LocalSystem service agent
|
# Only handle non-SYSTEM runs here; SYSTEM runs are handled by the LocalSystem service agent
|
||||||
if run_mode == 'system':
|
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
|
return
|
||||||
if script_type != 'powershell':
|
if script_type != 'powershell':
|
||||||
await sio.emit('quick_job_result', { 'job_id': job_id, 'status': 'Failed', 'stdout': '', 'stderr': f"Unsupported type: {script_type}" })
|
await sio.emit('quick_job_result', { 'job_id': job_id, 'status': 'Failed', 'stdout': '', 'stderr': f"Unsupported type: {script_type}" })
|
||||||
return
|
return
|
||||||
path = _write_temp_script(content, '.ps1')
|
|
||||||
rc = 0; out = ''; err = ''
|
rc = 0; out = ''; err = ''
|
||||||
if run_mode == 'admin':
|
if run_mode == 'admin':
|
||||||
# Admin credentialed runs are disabled in current design
|
# 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:
|
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'
|
status = 'Success' if rc == 0 else 'Failed'
|
||||||
await sio.emit('quick_job_result', {
|
await sio.emit('quick_job_result', {
|
||||||
'job_id': job_id,
|
'job_id': job_id,
|
||||||
@@ -1053,6 +1057,60 @@ async def idle_task():
|
|||||||
print(f"[FATAL] Idle task crashed: {e}")
|
print(f"[FATAL] Idle task crashed: {e}")
|
||||||
traceback.print_exc()
|
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 ----------------
|
# ---------------- Dummy Qt Widget to Prevent Exit ----------------
|
||||||
class PersistentWindow(QtWidgets.QWidget):
|
class PersistentWindow(QtWidgets.QWidget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@@ -10,6 +10,8 @@ import tempfile
|
|||||||
import socketio
|
import socketio
|
||||||
import platform
|
import platform
|
||||||
import time
|
import time
|
||||||
|
import uuid
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
def get_project_root():
|
def get_project_root():
|
||||||
@@ -89,7 +91,11 @@ async def main():
|
|||||||
'stderr': f"Unsupported type: {script_type}"
|
'stderr': f"Unsupported type: {script_type}"
|
||||||
})
|
})
|
||||||
return
|
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'
|
status = 'Success' if rc == 0 else 'Failed'
|
||||||
await sio.emit('quick_job_result', {
|
await sio.emit('quick_job_result', {
|
||||||
'job_id': job_id,
|
'job_id': job_id,
|
||||||
@@ -144,5 +150,61 @@ async def main():
|
|||||||
await asyncio.sleep(5)
|
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__':
|
if __name__ == '__main__':
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
@@ -37,14 +37,12 @@ class TrayApp(QtWidgets.QSystemTrayIcon):
|
|||||||
self.action_show_console = self.menu.addAction('Switch to Foreground Mode')
|
self.action_show_console = self.menu.addAction('Switch to Foreground Mode')
|
||||||
self.action_hide_console = self.menu.addAction('Switch to Background Mode')
|
self.action_hide_console = self.menu.addAction('Switch to Background Mode')
|
||||||
self.action_restart = self.menu.addAction('Restart Agent')
|
self.action_restart = self.menu.addAction('Restart Agent')
|
||||||
self.action_restart_service = self.menu.addAction('Restart Borealis Agent Service')
|
|
||||||
self.menu.addSeparator()
|
self.menu.addSeparator()
|
||||||
self.action_quit = self.menu.addAction('Quit Agent and Tray')
|
self.action_quit = self.menu.addAction('Quit Agent and Tray')
|
||||||
|
|
||||||
self.action_show_console.triggered.connect(self.switch_to_console)
|
self.action_show_console.triggered.connect(self.switch_to_console)
|
||||||
self.action_hide_console.triggered.connect(self.switch_to_background)
|
self.action_hide_console.triggered.connect(self.switch_to_background)
|
||||||
self.action_restart.triggered.connect(self.restart_agent)
|
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.action_quit.triggered.connect(self.quit_all)
|
||||||
self.setContextMenu(self.menu)
|
self.setContextMenu(self.menu)
|
||||||
|
|
||||||
@@ -101,35 +99,7 @@ class TrayApp(QtWidgets.QSystemTrayIcon):
|
|||||||
# Restart using current mode
|
# Restart using current mode
|
||||||
self._start_agent(console=self.console_mode)
|
self._start_agent(console=self.console_mode)
|
||||||
|
|
||||||
def restart_script_service(self):
|
# Service controls removed in task-centric architecture
|
||||||
# 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
|
|
||||||
|
|
||||||
def quit_all(self):
|
def quit_all(self):
|
||||||
self._stop_agent()
|
self._stop_agent()
|
||||||
|
@@ -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)
|
|
@@ -185,7 +185,7 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
|
|||||||
label={<Typography variant="body2">Run as currently logged-in user</Typography>}
|
label={<Typography variant="body2">Run as currently logged-in user</Typography>}
|
||||||
/>
|
/>
|
||||||
<Typography variant="caption" sx={{ color: "#888" }}>
|
<Typography variant="caption" sx={{ color: "#888" }}>
|
||||||
Unchecked = run as SYSTEM (requires agent service)
|
Unchecked = Run-As BUILTIN\SYSTEM
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
{error && (
|
{error && (
|
||||||
|
Reference in New Issue
Block a user