function Show-Header { Clear-Host Write-Host "====================================" -ForegroundColor Cyan Write-Host " UNIFIED SHEEPIT LAUNCHER" -ForegroundColor Cyan Write-Host "====================================" -ForegroundColor Cyan Write-Host } $SheepItJarUrls = @( 'https://www.sheepit-renderfarm.com/media/applet/client-latest.php', 'https://www.sheepit-renderfarm.com/media/applet/client-latest.jar' ) $script:SheepItUsername = $null $script:SheepItRenderKey = $null function Initialize-SheepItCredentials { if (-not $script:SheepItUsername) { $script:SheepItUsername = "RaincloudTheDragon" } if (-not $script:SheepItRenderKey) { $script:SheepItRenderKey = "IfCOWBHFQpceG0601DmyrwOOJOAp2UJAQ0O0X0jF" } } $workers = @( @{ ID = 1; Name = "i9kf"; SSHArgs = "-t i9kf"; Enabled = $true }, @{ ID = 2; Name = "blender-boss"; SSHArgs = "-t blender-boss"; Enabled = $true }, @{ ID = 3; Name = "max"; SSHArgs = "-t max"; Enabled = $true }, @{ ID = 4; Name = "masterbox"; SSHArgs = "-t masterbox"; Enabled = $true }, @{ ID = 5; Name = "echo"; SSHArgs = "-t echo"; Enabled = $true }, @{ ID = 6; Name = "i9-13ks"; SSHArgs = "-t -p 22146 i9-13ks"; Enabled = $true } ) $script:ControllerScriptBase64 = $null $script:AttachHelperScriptBase64 = $null $script:WorkerBasePathCache = @{} function Remove-ClixmlNoise { param([object[]]$Lines) $noisePatterns = @( '^#<\s*CLIXML', '^\s*', '^\s*', '^\s*', '^\s*', '^\s*', '^\s*', '^\s*', '^\s*', '^\s*', '^\s*', '^\s*', '^\s*', '^\s*' ) $filtered = @() foreach ($entry in $Lines) { if ($null -eq $entry) { continue } $text = $entry.ToString() $isNoise = $false foreach ($pattern in $noisePatterns) { if ($text -match $pattern) { $isNoise = $true break } } if (-not $isNoise) { $filtered += $text } } return $filtered } function Write-FilteredSshOutput { param([object[]]$Lines) $clean = Remove-ClixmlNoise -Lines $Lines foreach ($line in $clean) { Write-Host $line } } function Build-SshArgsFromParts { param( [pscustomobject]$Parts, [switch]$Interactive ) $args = @('-o','ServerAliveInterval=60','-o','ServerAliveCountMax=30') if ($Interactive -and $Parts.RequestPty) { $args += '-t' } elseif (-not $Interactive) { $args += '-T' } $args += $Parts.Options if ($Parts.Port) { $args += '-p' $args += $Parts.Port } $args += $Parts.Host return $args } function Build-ScpArgsFromParts { param( [pscustomobject]$Parts ) $args = @('-o','ServerAliveInterval=60','-o','ServerAliveCountMax=30') $args += $Parts.Options if ($Parts.Port) { $args += '-P' $args += $Parts.Port } return $args } function Get-SshArgs { param( [object]$Worker, [switch]$Interactive ) $parts = Get-WorkerConnectionParts -RawArgs $Worker.SSHArgs -DefaultHost $Worker.Name return Build-SshArgsFromParts -Parts $parts -Interactive:$Interactive } function Get-WorkerBasePath { param( [object]$Worker, [pscustomobject]$ConnectionParts = $null ) if ($script:WorkerBasePathCache.ContainsKey($Worker.Name)) { return $script:WorkerBasePathCache[$Worker.Name] } if (-not $ConnectionParts) { $ConnectionParts = Get-WorkerConnectionParts -RawArgs $Worker.SSHArgs -DefaultHost $Worker.Name } $sshArgs = Build-SshArgsFromParts -Parts $ConnectionParts -Interactive:$false $scriptBlock = "`$ProgressPreference='SilentlyContinue'; [Environment]::GetFolderPath('LocalApplicationData')" $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($scriptBlock)) $remoteCmd = "powershell -NoLogo -NoProfile -NonInteractive -OutputFormat Text -ExecutionPolicy Bypass -EncodedCommand $encoded" $rawOutput = & ssh @sshArgs $remoteCmd 2>&1 $output = Remove-ClixmlNoise -Lines $rawOutput if ($LASTEXITCODE -ne 0) { throw "Unable to determine LocalAppData on $($Worker.Name)." } $base = ($output | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Last 1).Trim() if (-not $base) { throw "Unable to read LocalAppData path on $($Worker.Name)." } $final = Join-Path $base 'UnifiedWorkers' $script:WorkerBasePathCache[$Worker.Name] = $final return $final } function Get-FileBase64 { param([string]$Path) [Convert]::ToBase64String([IO.File]::ReadAllBytes($Path)) } function Get-ControllerScriptBase64 { if (-not $script:ControllerScriptBase64) { $controllerPath = Join-Path $PSScriptRoot 'remote_worker_controller.ps1' $script:ControllerScriptBase64 = Get-FileBase64 -Path $controllerPath } return $script:ControllerScriptBase64 } function Get-AttachHelperScriptBase64 { if (-not $script:AttachHelperScriptBase64) { $helperPath = Join-Path $PSScriptRoot 'remote_worker_attach.ps1' $script:AttachHelperScriptBase64 = Get-FileBase64 -Path $helperPath } return $script:AttachHelperScriptBase64 } function ConvertTo-Base64Unicode { param([string]$Content) [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($Content)) } function Get-WorkerSshArgs { param([string]$RawArgs) if ([string]::IsNullOrWhiteSpace($RawArgs)) { return @() } return $RawArgs -split '\s+' | Where-Object { $_.Trim().Length -gt 0 } } function Get-WorkerConnectionParts { param( [string]$RawArgs, [string]$DefaultHost ) $tokens = Get-WorkerSshArgs -RawArgs $RawArgs $options = New-Object System.Collections.Generic.List[string] $targetHost = $null $port = $null $requestPty = $false $optionsWithArgs = @('-i','-o','-c','-D','-E','-F','-I','-J','-L','-l','-m','-O','-Q','-R','-S','-W','-w') for ($i = 0; $i -lt $tokens.Count; $i++) { $token = $tokens[$i] if ($token -eq '-t' -or $token -eq '-tt') { $requestPty = $true continue } if ($token -eq '-p' -and ($i + 1) -lt $tokens.Count) { $port = $tokens[$i + 1] $i++ continue } if ($token.StartsWith('-')) { $options.Add($token) if ($optionsWithArgs -contains $token -and ($i + 1) -lt $tokens.Count) { $options.Add($tokens[$i + 1]) $i++ } continue } if (-not $targetHost) { $targetHost = $token continue } $options.Add($token) } if (-not $targetHost) { $targetHost = $DefaultHost } return [pscustomobject]@{ Host = $targetHost Options = $options.ToArray() Port = $port RequestPty = $requestPty } } function Invoke-RemotePowerShell { param( [object]$Worker, [string]$Script, [switch]$Interactive ) $parts = Get-WorkerConnectionParts -RawArgs $Worker.SSHArgs -DefaultHost $Worker.Name if (-not $parts.Host) { throw "Unable to determine SSH host for $($Worker.Name)" } $sshBaseArgs = Build-SshArgsFromParts -Parts $parts -Interactive:$Interactive $remoteBasePath = Get-WorkerBasePath -Worker $Worker -ConnectionParts $parts $localTemp = [System.IO.Path]::GetTempFileName() + '.ps1' Set-Content -Path $localTemp -Value $Script -Encoding UTF8 $remoteTmpDir = Join-Path $remoteBasePath 'tmp' $remoteScriptWin = Join-Path $remoteTmpDir ("script-{0}.ps1" -f ([guid]::NewGuid().ToString())) $remoteScriptScp = $remoteScriptWin -replace '\\','/' $remoteTarget = "{0}:{1}" -f $parts.Host, ('"'+$remoteScriptScp+'"') $ensureScript = "New-Item -ItemType Directory -Path '$remoteTmpDir' -Force | Out-Null" $ensureCmd = "powershell -NoLogo -NoProfile -NonInteractive -OutputFormat Text -ExecutionPolicy Bypass -EncodedCommand " + [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($ensureScript)) $ensureOutput = & ssh @sshBaseArgs $ensureCmd 2>&1 $ensureExit = $LASTEXITCODE Write-FilteredSshOutput -Lines $ensureOutput if ($ensureExit -ne 0) { Remove-Item $localTemp -ErrorAction SilentlyContinue return $ensureExit } $scpArgs = Build-ScpArgsFromParts -Parts $parts $scpArgs += $localTemp $scpArgs += $remoteTarget & scp @scpArgs $scpExit = $LASTEXITCODE Remove-Item $localTemp -ErrorAction SilentlyContinue if ($scpExit -ne 0) { return $scpExit } $execCmd = "powershell -NoLogo -NoProfile -NonInteractive -OutputFormat Text -ExecutionPolicy Bypass -File `"$remoteScriptWin`"" $execOutput = & ssh @sshBaseArgs $execCmd 2>&1 $execExit = $LASTEXITCODE Write-FilteredSshOutput -Lines $execOutput $cleanupScript = "Remove-Item -LiteralPath '$remoteScriptWin' -ErrorAction SilentlyContinue" $cleanupCmd = "powershell -NoLogo -NoProfile -NonInteractive -OutputFormat Text -ExecutionPolicy Bypass -EncodedCommand " + [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($cleanupScript)) $cleanupOutput = & ssh @sshBaseArgs $cleanupCmd 2>&1 Write-FilteredSshOutput -Lines $cleanupOutput return [int]$execExit } function Resolve-ExitCode { param($Value) if ($Value -is [System.Array]) { for ($i = $Value.Count - 1; $i -ge 0; $i--) { $candidate = Resolve-ExitCode -Value $Value[$i] if ($candidate -ne $null) { return $candidate } } return 0 } if ($Value -is [int]) { return $Value } $text = $Value if ($null -eq $text) { return 0 } $parsed = 0 if ([int]::TryParse($text.ToString(), [ref]$parsed)) { return $parsed } return 0 } function Ensure-ControllerDeployed { param([object]$Worker) $controllerBase64 = Get-ControllerScriptBase64 $script = @" `$ProgressPreference = 'SilentlyContinue' `$dataRoot = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'UnifiedWorkers' New-Item -ItemType Directory -Path `$dataRoot -Force | Out-Null `$controllerPath = Join-Path `$dataRoot 'controller.ps1' [IO.File]::WriteAllBytes(`$controllerPath, [Convert]::FromBase64String('$controllerBase64')) "@ $exit = Resolve-ExitCode (Invoke-RemotePowerShell -Worker $Worker -Script $script) if ($exit -ne 0) { throw "Controller deployment failed on $($Worker.Name) (exit $exit)." } } function Ensure-AttachHelperDeployed { param([object]$Worker) $helperBase64 = Get-AttachHelperScriptBase64 $script = @" `$ProgressPreference = 'SilentlyContinue' `$dataRoot = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'UnifiedWorkers' New-Item -ItemType Directory -Path `$dataRoot -Force | Out-Null `$attachPath = Join-Path `$dataRoot 'attach-helper.ps1' [IO.File]::WriteAllBytes(`$attachPath, [Convert]::FromBase64String('$helperBase64')) "@ $exit = Resolve-ExitCode (Invoke-RemotePowerShell -Worker $Worker -Script $script) if ($exit -ne 0) { throw "Attach helper deployment failed on $($Worker.Name) (exit $exit)." } } function Get-EnsureWorkerScript { param( [string]$WorkerName, [string]$WorkerType, [string]$PayloadBase64 ) $payload = @{ WorkerName = $WorkerName WorkerType = $WorkerType PayloadBase64 = $PayloadBase64 } | ConvertTo-Json -Compress $payloadBase64 = ConvertTo-Base64Unicode -Content $payload return @" `$ProgressPreference = 'SilentlyContinue' `$params = ConvertFrom-Json ([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('$payloadBase64'))) `$workerName = `$params.WorkerName `$workerType = `$params.WorkerType `$payloadBase64 = `$params.PayloadBase64 `$dataRoot = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'UnifiedWorkers' `$instanceRoot = Join-Path (Join-Path `$dataRoot `$workerType) `$workerName `$logsRoot = Join-Path `$instanceRoot 'logs' `$stateRoot = Join-Path `$instanceRoot 'state' New-Item -ItemType Directory -Path `$logsRoot -Force | Out-Null New-Item -ItemType Directory -Path `$stateRoot -Force | Out-Null `$logPath = Join-Path `$logsRoot 'worker.log' `$commandPath = Join-Path `$stateRoot 'commands.txt' if (-not (Test-Path `$logPath)) { New-Item -Path `$logPath -ItemType File -Force | Out-Null } if (-not (Test-Path `$commandPath)) { New-Item -Path `$commandPath -ItemType File -Force | Out-Null } `$metaPath = Join-Path `$instanceRoot 'state\worker-info.json' `$controllerPath = Join-Path `$dataRoot 'controller.ps1' if (-not (Test-Path `$controllerPath)) { throw "Controller missing at `$controllerPath" } `$shouldStart = `$true if (Test-Path `$metaPath) { try { `$meta = Get-Content `$metaPath -Raw | ConvertFrom-Json if (`$meta.Status -eq 'running' -and `$meta.WorkerPid) { if (Get-Process -Id `$meta.WorkerPid -ErrorAction SilentlyContinue) { Write-Host "Worker `$workerName already running (PID `$($meta.WorkerPid))." `$shouldStart = `$false } } } catch { Write-Host "Failed to read metadata. Controller will restart worker." -ForegroundColor Yellow } } if (`$shouldStart) { `$initialMeta = [pscustomobject]@{ WorkerName = `$workerName WorkerType = `$workerType Status = 'launching' ControllerPid = `$null WorkerPid = `$null Restarts = 0 LastExitCode = `$null LogPath = `$logPath CommandPath = `$commandPath PayloadPath = `$null UpdatedAtUtc = (Get-Date).ToUniversalTime() } | ConvertTo-Json -Depth 5 `$initialMeta | Set-Content -Path `$metaPath -Encoding UTF8 `$pwsh = Get-Command pwsh -ErrorAction SilentlyContinue if (`$pwsh) { `$psExe = `$pwsh.Source } else { `$psExe = (Get-Command powershell -ErrorAction Stop).Source } `$controllerArgs = @( '-NoLogo','-NoProfile','-ExecutionPolicy','Bypass', '-File',"`$controllerPath", '-WorkerName',"`$workerName", '-WorkerType',"`$workerType", '-PayloadBase64',"`$payloadBase64" ) Start-Process -FilePath `$psExe -ArgumentList `$controllerArgs -WindowStyle Hidden | Out-Null Write-Host "Worker `$workerName started under controller." -ForegroundColor Green } "@ } function Ensure-PersistentWorker { param( [object]$Worker, [string]$WorkerType, [string]$PayloadScript ) Ensure-ControllerDeployed -Worker $Worker $payloadBase64 = ConvertTo-Base64Unicode -Content $PayloadScript $ensureScript = Get-EnsureWorkerScript -WorkerName $Worker.Name -WorkerType $WorkerType -PayloadBase64 $payloadBase64 $exit = Resolve-ExitCode (Invoke-RemotePowerShell -Worker $Worker -Script $ensureScript) if ($exit -ne 0) { throw "Worker ensure script failed on $($Worker.Name) (exit $exit)." } } function Test-WorkerMetadataExists { param( [object]$Worker, [string]$WorkerType ) $payload = @{ WorkerName = $Worker.Name WorkerType = $WorkerType } | ConvertTo-Json -Compress $payloadBase64 = ConvertTo-Base64Unicode -Content $payload $script = @" `$ProgressPreference = 'SilentlyContinue' `$params = ConvertFrom-Json ([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('$payloadBase64'))) `$dataRoot = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'UnifiedWorkers' `$instanceRoot = Join-Path (Join-Path `$dataRoot `$params.WorkerType) `$params.WorkerName `$metaPath = Join-Path `$instanceRoot 'state\worker-info.json' if (Test-Path `$metaPath) { exit 0 } exit 1 "@ $result = Invoke-RemotePowerShell -Worker $Worker -Script $script return ($result -eq 0) } function Wait-WorkerMetadata { param( [object]$Worker, [string]$WorkerType, [int]$TimeoutSeconds = 30 ) $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds) while ([DateTime]::UtcNow -lt $deadline) { if (Test-WorkerMetadataExists -Worker $Worker -WorkerType $WorkerType) { return $true } Start-Sleep -Milliseconds 500 } return $false } function Invoke-WorkerAttach { param( [object]$Worker, [string]$WorkerType, [switch]$CommandOnly, [string]$Command ) Ensure-AttachHelperDeployed -Worker $Worker $paramsBlock = @("-WorkerName","$($Worker.Name)","-WorkerType","$WorkerType") if ($CommandOnly) { $paramsBlock += "-CommandOnly" } if ($Command) { $paramsBlock += "-Command" $paramsBlock += $Command } $parts = Get-WorkerConnectionParts -RawArgs $Worker.SSHArgs -DefaultHost $Worker.Name $sshArgs = Build-SshArgsFromParts -Parts $parts -Interactive:(!$CommandOnly) $remoteBase = Get-WorkerBasePath -Worker $Worker -ConnectionParts $parts $remoteHelper = Join-Path $remoteBase 'attach-helper.ps1' $quotedArgs = ($paramsBlock | ForEach-Object { '"' + ($_ -replace '"','""') + '"' }) -join ' ' $remoteCmd = "powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -File `"$remoteHelper`" $quotedArgs" & ssh @sshArgs $remoteCmd } function Get-RemoteSheepItCommand { param( [string]$RenderKey, [string]$Username ) $safeKey = $RenderKey -replace "'", "''" $safeUser = $Username -replace "'", "''" $urlLiteral = '@(' + (($SheepItJarUrls | ForEach-Object { "'$_'" }) -join ', ') + ')' @" `$ProgressPreference = 'SilentlyContinue' `$ErrorActionPreference = 'Stop' try { `$appData = [Environment]::GetFolderPath('ApplicationData') `$sheepDir = Join-Path `$appData 'sheepit' if (-not (Test-Path `$sheepDir)) { New-Item -Path `$sheepDir -ItemType Directory -Force | Out-Null } `$jarPath = Join-Path `$sheepDir 'sheepit-client.jar' `$urls = $urlLiteral `$headers = @{ 'User-Agent' = 'Mozilla/5.0' } if (Test-Path `$jarPath) { Write-Host "SheepIt client already present at `$jarPath. Skipping download." -ForegroundColor Green } else { `$downloaded = `$false foreach (`$url in `$urls) { Write-Host "Downloading SheepIt client from `$url..." -ForegroundColor Cyan try { Invoke-WebRequest -Uri `$url -OutFile `$jarPath -UseBasicParsing -Headers `$headers `$downloaded = `$true Write-Host "Download complete." -ForegroundColor Green break } catch { Write-Host ("Download failed from {0}: {1}" -f `$url, `$_.Exception.Message) -ForegroundColor Yellow } } if (-not `$downloaded) { throw 'Unable to download SheepIt client from any known URL.' } } Write-Host "Starting SheepIt client..." -ForegroundColor Cyan # Check and fix problematic environment variables that can cause boot class path errors `$envVarsFixed = `$false # Check JAVA_HOME - invalid values like '\' or empty can cause issues if (`$env:JAVA_HOME) { if (`$env:JAVA_HOME -eq '\' -or `$env:JAVA_HOME.Trim() -eq '' -or -not (Test-Path `$env:JAVA_HOME)) { Write-Host "Warning: Invalid JAVA_HOME detected ('`$env:JAVA_HOME'). Temporarily unsetting..." -ForegroundColor Yellow Remove-Item Env:\JAVA_HOME -ErrorAction SilentlyContinue `$envVarsFixed = `$true } } # Check JAVA_TOOL_OPTIONS - invalid values can cause boot class path errors if (`$env:JAVA_TOOL_OPTIONS) { if (`$env:JAVA_TOOL_OPTIONS -eq '\' -or `$env:JAVA_TOOL_OPTIONS.Trim() -eq '') { Write-Host "Warning: Invalid JAVA_TOOL_OPTIONS detected ('`$env:JAVA_TOOL_OPTIONS'). Temporarily unsetting..." -ForegroundColor Yellow Remove-Item Env:\JAVA_TOOL_OPTIONS -ErrorAction SilentlyContinue `$envVarsFixed = `$true } } if (`$envVarsFixed) { Write-Host "Environment variables fixed. Proceeding with Java launch..." -ForegroundColor Green } # Check Java version try { `$javaOutput = java -version 2>&1 `$javaVersion = `$javaOutput | Select-Object -First 1 Write-Host "Java version: `$javaVersion" -ForegroundColor Gray } catch { Write-Host "Warning: Could not determine Java version" -ForegroundColor Yellow } Set-Location `$sheepDir # Use -XX:+IgnoreUnrecognizedVMOptions to handle any incompatible JVM args # This flag helps with Java 9+ compatibility issues `$javaArgs = @('-XX:+IgnoreUnrecognizedVMOptions', '-jar', `$jarPath, '-ui', 'text', '--log-stdout', '--verbose', '-gpu', 'OPTIX_0', '-login', '${safeUser}', '-password', '${safeKey}') try { & java @javaArgs } catch { Write-Host ('Java execution error: {0}' -f `$_.Exception.Message) -ForegroundColor Red Write-Host "If the error persists, try reinstalling Java (Temurin 21 recommended)." -ForegroundColor Yellow throw } } catch { Write-Host ('Error: {0}' -f `$_.Exception.Message) -ForegroundColor Red Write-Host ('Stack trace: {0}' -f `$_.ScriptStackTrace) -ForegroundColor DarkRed } "@ } function Ensure-SheepItWorkerController { param([object]$Worker) Initialize-SheepItCredentials $payloadScript = Get-RemoteSheepItCommand -RenderKey $script:SheepItRenderKey -Username $script:SheepItUsername Ensure-PersistentWorker -Worker $Worker -WorkerType 'sheepit' -PayloadScript $payloadScript } function Start-SheepItWorker { param([object]$Worker) try { Write-Host "Ensuring SheepIt controller on $($Worker.Name)..." -ForegroundColor Cyan Ensure-SheepItWorkerController -Worker $Worker } catch { Write-Host "Failed to ensure controller on $($Worker.Name): $($_.Exception.Message)" -ForegroundColor Red return } if (-not (Wait-WorkerMetadata -Worker $Worker -WorkerType 'sheepit' -TimeoutSeconds 30)) { Write-Host "Worker metadata did not appear on $($Worker.Name). Check controller logs under %LocalAppData%\UnifiedWorkers." -ForegroundColor Red return } Write-Host "Controller ready. Attaching to SheepIt worker on $($Worker.Name)..." -ForegroundColor Cyan Invoke-WorkerAttach -Worker $Worker -WorkerType 'sheepit' } function Start-AllSheepIt { Initialize-SheepItCredentials foreach ($worker in $workers | Where-Object { $_.Enabled }) { Ensure-SheepItWorkerController -Worker $worker } Write-Host "All enabled SheepIt workers ensured running under controllers." -ForegroundColor Green Write-Host "Use the attach option to monitor any worker." -ForegroundColor Cyan Read-Host "Press Enter to continue" | Out-Null } function Send-SheepItCommandAll { param([string]$CommandText) foreach ($worker in $workers | Where-Object { $_.Enabled }) { Write-Host "[$($worker.Name)] Sending '$CommandText'..." -ForegroundColor Gray Invoke-WorkerAttach -Worker $worker -WorkerType 'sheepit' -CommandOnly -Command $CommandText } Write-Host "Command '$CommandText' dispatched to all enabled workers." -ForegroundColor Green Read-Host "Press Enter to continue" | Out-Null } function Select-SheepItWorker { while ($true) { Show-Header Write-Host "Select a system:" -ForegroundColor Magenta foreach ($worker in $workers) { $status = if ($worker.Enabled) { "ready" } else { "disabled" } $color = if ($worker.Enabled) { "Green" } else { "DarkYellow" } Write-Host ("{0}. {1} ({2})" -f $worker.ID, $worker.Name, $status) -ForegroundColor $color } Write-Host "B. Back" -ForegroundColor Yellow $selection = Read-Host "Choose system" if ($selection -match '^[Bb]$') { return } if ($selection -match '^\d+$') { $id = [int]$selection $worker = $workers | Where-Object { $_.ID -eq $id } if ($worker) { Start-SheepItWorker -Worker $worker Write-Host Read-Host "Press Enter to return to the main menu" | Out-Null return } Write-Host "Unknown selection." -ForegroundColor Red Start-Sleep -Seconds 1 } else { Write-Host "Invalid input." -ForegroundColor Red Start-Sleep -Seconds 1 } } } Initialize-SheepItCredentials while ($true) { Show-Header Write-Host "Main Menu:" -ForegroundColor Magenta Write-Host "1. Launch/Attach SheepIt on a single system" -ForegroundColor Yellow Write-Host "2. Ensure all ready systems are running" -ForegroundColor Yellow Write-Host "3. Pause all workers" -ForegroundColor Yellow Write-Host "4. Resume all workers" -ForegroundColor Yellow Write-Host "5. Quit all workers" -ForegroundColor Yellow Write-Host "6. Exit" -ForegroundColor Yellow $choice = Read-Host "Select option (1-3)" switch ($choice) { '1' { Select-SheepItWorker } '2' { Start-AllSheepIt } '3' { Send-SheepItCommandAll -CommandText 'pause' } '4' { Send-SheepItCommandAll -CommandText 'resume' } '5' { Send-SheepItCommandAll -CommandText 'quit' } '6' { break } default { Write-Host "Invalid selection." -ForegroundColor Red Start-Sleep -Seconds 1 } } } Write-Host "`nExiting SheepIt launcher." -ForegroundColor Cyan