sheepit/flamenco toggle

This commit is contained in:
Nathan
2025-12-17 16:46:35 -07:00
parent 0460865ebc
commit 553758c378
7 changed files with 16425 additions and 44 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,34 @@ param(
[string]$Command [string]$Command
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Continue'
# Ensure we can see output immediately
try {
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
} catch {}
try {
$Host.UI.RawUI.BufferSize = New-Object System.Management.Automation.Host.Size(200, 9999)
} catch {}
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Worker Attach Session Starting" -ForegroundColor Cyan
Write-Host "Worker: $WorkerName" -ForegroundColor White
Write-Host "Type: $WorkerType" -ForegroundColor White
Write-Host "========================================" -ForegroundColor Cyan
[Console]::Out.Flush()
# Ensure output is visible immediately
try {
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
} catch {}
try {
$Host.UI.RawUI.BufferSize = New-Object System.Management.Automation.Host.Size(200, 9999)
} catch {}
Write-Host "Starting attach session..." -ForegroundColor Green
Write-Host "Worker: $WorkerName, Type: $WorkerType" -ForegroundColor Gray
[Console]::Out.Flush()
function Get-WorkerPaths { function Get-WorkerPaths {
param([string]$Root, [string]$Type, [string]$Name) param([string]$Root, [string]$Type, [string]$Name)
@@ -28,7 +55,17 @@ function Get-WorkerPaths {
$paths = Get-WorkerPaths -Root $DataRoot -Type $WorkerType -Name $WorkerName $paths = Get-WorkerPaths -Root $DataRoot -Type $WorkerType -Name $WorkerName
if (-not (Test-Path $paths.Metadata)) { if (-not (Test-Path $paths.Metadata)) {
Write-Host "No worker metadata found for $WorkerName ($WorkerType)." -ForegroundColor Red Write-Host ""
Write-Host "ERROR: No worker metadata found for $WorkerName ($WorkerType)." -ForegroundColor Red
Write-Host "Metadata path: $($paths.Metadata)" -ForegroundColor Gray
Write-Host "Data root: $DataRoot" -ForegroundColor Gray
Write-Host ""
Write-Host "Press any key to exit..."
try {
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
} catch {
Start-Sleep -Seconds 5
}
exit 1 exit 1
} }
@@ -73,16 +110,58 @@ if ($CommandOnly) {
exit 0 exit 0
} }
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Attaching to $WorkerName ($WorkerType) logs." -ForegroundColor Cyan Write-Host "Attaching to $WorkerName ($WorkerType) logs." -ForegroundColor Cyan
Write-Host "Type commands and press Enter. Type 'detach' to exit session." -ForegroundColor Yellow Write-Host "Log file: $($paths.Log)" -ForegroundColor Gray
Write-Host "========================================" -ForegroundColor Cyan
[Console]::Out.Flush()
[Console]::Error.WriteLine("Attach session initialized")
# Show initial log content if available
if (Test-Path $paths.Log) {
$initialLines = Get-Content -Path $paths.Log -Tail 50 -ErrorAction SilentlyContinue
if ($initialLines) {
Write-Host "--- Recent log output ---" -ForegroundColor DarkGray
$initialLines | ForEach-Object { Write-Host $_ }
Write-Host "--- End of initial output ---" -ForegroundColor DarkGray
} else {
Write-Host "Log file exists but is empty. Waiting for worker output..." -ForegroundColor Yellow
}
} else {
Write-Host "Log file does not exist yet. Waiting for worker to start..." -ForegroundColor Yellow
}
Write-Host ""
Write-Host "Type commands and press Enter. Type 'detach' to exit session." -ForegroundColor Yellow
Write-Host ""
# Use Register-ObjectEvent for file system watcher, or simpler: just tail in background and poll
$logJob = Start-Job -ScriptBlock { $logJob = Start-Job -ScriptBlock {
param($LogPath) param($LogPath)
Get-Content -Path $LogPath -Tail 50 -Wait if (Test-Path $LogPath) {
Get-Content -Path $LogPath -Tail 0 -Wait -ErrorAction SilentlyContinue
} else {
while (-not (Test-Path $LogPath)) {
Start-Sleep -Milliseconds 500
}
Get-Content -Path $LogPath -Tail 0 -Wait -ErrorAction SilentlyContinue
}
} -ArgumentList $paths.Log } -ArgumentList $paths.Log
try { try {
while ($true) { while ($true) {
# Check for new log output from background job (non-blocking)
$logOutput = Receive-Job -Job $logJob -ErrorAction SilentlyContinue
if ($logOutput) {
$logOutput | ForEach-Object { Write-Host $_ }
[Console]::Out.Flush()
}
# Use a timeout on Read-Host to allow periodic log checking
# Since Read-Host doesn't support timeout, we'll just check logs before each Read-Host
# This means logs will be checked whenever user presses Enter
$input = Read-Host "> " $input = Read-Host "> "
if ($null -eq $input) { if ($null -eq $input) {
continue continue
@@ -104,10 +183,14 @@ try {
Send-WorkerCommand -Value $input Send-WorkerCommand -Value $input
} }
} }
catch {
Write-Host "Error in attach session: $($_.Exception.Message)" -ForegroundColor Red
Write-Host $_.ScriptStackTrace -ForegroundColor DarkRed
[Console]::Error.WriteLine("Attach error: $($_.Exception.Message)")
}
finally { finally {
if ($logJob) { if ($logJob) {
Stop-Job -Job $logJob -ErrorAction SilentlyContinue Stop-Job -Job $logJob -ErrorAction SilentlyContinue
Receive-Job -Job $logJob -ErrorAction SilentlyContinue | Out-Null
Remove-Job -Job $logJob -ErrorAction SilentlyContinue Remove-Job -Job $logJob -ErrorAction SilentlyContinue
} }

View File

@@ -1,4 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using UnifiedFarmLauncher.Models; using UnifiedFarmLauncher.Models;
@@ -20,7 +21,7 @@ namespace UnifiedFarmLauncher.Services
await _controllerService.DeployAttachHelperAsync(worker); await _controllerService.DeployAttachHelperAsync(worker);
var remoteBasePath = await _sshService.GetWorkerBasePathAsync(worker); var remoteBasePath = await _sshService.GetWorkerBasePathAsync(worker);
var remoteHelper = $"{remoteBasePath.Replace("\\", "/")}/attach-helper.ps1"; var remoteHelper = Path.Combine(remoteBasePath, "attach-helper.ps1");
var paramsBlock = $"-WorkerName \"{worker.Name}\" -WorkerType \"{workerType}\""; var paramsBlock = $"-WorkerName \"{worker.Name}\" -WorkerType \"{workerType}\"";
if (commandOnly) if (commandOnly)
@@ -32,7 +33,9 @@ namespace UnifiedFarmLauncher.Services
paramsBlock += $" -Command \"{command}\""; paramsBlock += $" -Command \"{command}\"";
} }
var remoteCmd = $"powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -File \"{remoteHelper}\" {paramsBlock}"; // Use Windows path format (backslashes) and ensure it's properly quoted
// Add -NoExit to keep window open and ensure output is visible
var remoteCmd = $"powershell.exe -NoLogo -NoProfile -NoExit -ExecutionPolicy Bypass -File \"{remoteHelper}\" {paramsBlock}";
_sshService.StartInteractiveSsh(worker, remoteCmd); _sshService.StartInteractiveSsh(worker, remoteCmd);
} }

View File

@@ -339,12 +339,15 @@ namespace UnifiedFarmLauncher.Services
var sshArgs = BuildSshArgs(parts, true); var sshArgs = BuildSshArgs(parts, true);
sshArgs.Add(command); sshArgs.Add(command);
var argsString = string.Join(" ", sshArgs.Select(arg => $"\"{arg.Replace("\"", "\\\"")}\""));
var process = new Process var process = new Process
{ {
StartInfo = new ProcessStartInfo StartInfo = new ProcessStartInfo
{ {
FileName = GetSshExecutable(), FileName = GetSshExecutable(),
Arguments = string.Join(" ", sshArgs.Select(arg => $"\"{arg.Replace("\"", "\\\"")}\"")), WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
Arguments = argsString,
UseShellExecute = true, UseShellExecute = true,
CreateNoWindow = false CreateNoWindow = false
} }

View File

@@ -12,6 +12,7 @@ namespace UnifiedFarmLauncher.ViewModels
private WorkerConfig? _selectedWorker; private WorkerConfig? _selectedWorker;
private string _statusText = "Ready"; private string _statusText = "Ready";
private string _selectedWorkerType = "All"; private string _selectedWorkerType = "All";
private string _operationMode = "sheepit"; // "sheepit" or "flamenco"
public MainWindowViewModel() public MainWindowViewModel()
{ {
@@ -52,6 +53,24 @@ namespace UnifiedFarmLauncher.ViewModels
} }
} }
public string OperationMode
{
get => _operationMode;
set
{
if (SetAndRaise(ref _operationMode, value))
{
// Notify that dependent properties also changed
OnPropertyChanged(nameof(OperationModeDisplayName));
OnPropertyChanged(nameof(OperationModeIcon));
}
}
}
public string OperationModeDisplayName => OperationMode == "sheepit" ? "SheepIt" : "Flamenco";
public string OperationModeIcon => OperationMode == "sheepit" ? "🐑" : "🔥";
public void LoadWorkers() public void LoadWorkers()
{ {
_configService.Reload(); _configService.Reload();

View File

@@ -9,7 +9,12 @@
MinWidth="800" MinHeight="600"> MinWidth="800" MinHeight="600">
<Grid RowDefinitions="Auto,Auto,*,Auto"> <Grid RowDefinitions="Auto,Auto,*,Auto">
<!-- Toolbar --> <!-- Toolbar -->
<StackPanel Orientation="Horizontal" Margin="5" Grid.Row="0"> <Grid Grid.Row="0" Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Grid.Column="0">
<Button Name="AddWorkerButton" Content="Add Worker" Margin="5" Width="120"/> <Button Name="AddWorkerButton" Content="Add Worker" Margin="5" Width="120"/>
<Button Name="EditWorkerButton" Content="Edit Worker" Margin="5" Width="120"/> <Button Name="EditWorkerButton" Content="Edit Worker" Margin="5" Width="120"/>
<Button Name="DeleteWorkerButton" Content="Delete Worker" Margin="5" Width="120"/> <Button Name="DeleteWorkerButton" Content="Delete Worker" Margin="5" Width="120"/>
@@ -20,6 +25,20 @@
<Separator Margin="10,0"/> <Separator Margin="10,0"/>
<Button Name="SettingsButton" Content="Settings" Margin="5" Width="80"/> <Button Name="SettingsButton" Content="Settings" Margin="5" Width="80"/>
</StackPanel> </StackPanel>
<!-- Operation Mode Toggle Button -->
<Button Name="OperationModeToggleButton" Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Margin="5"
Padding="10,5"
MinWidth="120">
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock Text="{Binding OperationModeIcon}" FontSize="16" VerticalAlignment="Center"/>
<TextBlock Text="{Binding OperationModeDisplayName}"
VerticalAlignment="Center" FontWeight="Bold"/>
</StackPanel>
</Button>
</Grid>
<!-- Worker Type Filter --> <!-- Worker Type Filter -->
<TabControl Name="WorkerTypeTabs" Grid.Row="1" Margin="5,0"> <TabControl Name="WorkerTypeTabs" Grid.Row="1" Margin="5,0">

View File

@@ -40,8 +40,18 @@ namespace UnifiedFarmLauncher.Views
this.FindControl<Button>("StopWorkerButton")!.Click += StopWorkerButton_Click; this.FindControl<Button>("StopWorkerButton")!.Click += StopWorkerButton_Click;
this.FindControl<Button>("AttachWorkerButton")!.Click += AttachWorkerButton_Click; this.FindControl<Button>("AttachWorkerButton")!.Click += AttachWorkerButton_Click;
this.FindControl<Button>("SettingsButton")!.Click += SettingsButton_Click; this.FindControl<Button>("SettingsButton")!.Click += SettingsButton_Click;
this.FindControl<Button>("OperationModeToggleButton")!.Click += OperationModeToggleButton_Click;
this.FindControl<TabControl>("WorkerTypeTabs")!.SelectionChanged += WorkerTypeTabs_SelectionChanged; this.FindControl<TabControl>("WorkerTypeTabs")!.SelectionChanged += WorkerTypeTabs_SelectionChanged;
this.FindControl<DataGrid>("WorkersGrid")!.SelectionChanged += WorkersGrid_SelectionChanged; this.FindControl<DataGrid>("WorkersGrid")!.SelectionChanged += WorkersGrid_SelectionChanged;
}
private void OperationModeToggleButton_Click(object? sender, RoutedEventArgs e)
{
if (DataContext is MainWindowViewModel vm)
{
vm.OperationMode = vm.OperationMode == "sheepit" ? "flamenco" : "sheepit";
}
} }
private async void AddWorkerButton_Click(object? sender, RoutedEventArgs e) private async void AddWorkerButton_Click(object? sender, RoutedEventArgs e)
@@ -88,16 +98,22 @@ namespace UnifiedFarmLauncher.Views
{ {
try try
{ {
string? workerType = null; var vm = (MainWindowViewModel)DataContext!;
if (worker.WorkerTypes.SheepIt != null) string? workerType = vm.OperationMode;
workerType = "sheepit";
else if (worker.WorkerTypes.Flamenco != null)
workerType = "flamenco";
if (workerType == null) // Verify the worker supports the selected operation mode
if (workerType == "sheepit" && worker.WorkerTypes.SheepIt == null)
{ {
var box = MessageBoxManager.GetMessageBoxStandard("Error", var box = MessageBoxManager.GetMessageBoxStandard("Error",
"Worker has no configured worker type.", $"Worker '{worker.Name}' does not have SheepIt configured.",
ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Error);
await box.ShowAsync();
return;
}
if (workerType == "flamenco" && worker.WorkerTypes.Flamenco == null)
{
var box = MessageBoxManager.GetMessageBoxStandard("Error",
$"Worker '{worker.Name}' does not have Flamenco configured.",
ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Error); ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Error);
await box.ShowAsync(); await box.ShowAsync();
return; return;
@@ -105,10 +121,10 @@ namespace UnifiedFarmLauncher.Views
await _controllerService.StartWorkerAsync(worker, workerType); await _controllerService.StartWorkerAsync(worker, workerType);
var successBox = MessageBoxManager.GetMessageBoxStandard("Start Worker", var successBox = MessageBoxManager.GetMessageBoxStandard("Start Worker",
$"Worker '{worker.Name}' started successfully.", $"Worker '{worker.Name}' ({vm.OperationModeDisplayName}) started successfully.",
ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Success); ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Success);
await successBox.ShowAsync(); await successBox.ShowAsync();
((MainWindowViewModel)DataContext!).RefreshWorkers(); vm.RefreshWorkers();
} }
catch (System.Exception ex) catch (System.Exception ex)
{ {
@@ -126,16 +142,22 @@ namespace UnifiedFarmLauncher.Views
{ {
try try
{ {
string? workerType = null; var vm = (MainWindowViewModel)DataContext!;
if (worker.WorkerTypes.SheepIt != null) string workerType = vm.OperationMode;
workerType = "sheepit";
else if (worker.WorkerTypes.Flamenco != null)
workerType = "flamenco";
if (workerType == null) // Verify the worker supports the selected operation mode
if (workerType == "sheepit" && worker.WorkerTypes.SheepIt == null)
{ {
var box = MessageBoxManager.GetMessageBoxStandard("Error", var box = MessageBoxManager.GetMessageBoxStandard("Error",
"Worker has no configured worker type.", $"Worker '{worker.Name}' does not have SheepIt configured.",
ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Error);
await box.ShowAsync();
return;
}
if (workerType == "flamenco" && worker.WorkerTypes.Flamenco == null)
{
var box = MessageBoxManager.GetMessageBoxStandard("Error",
$"Worker '{worker.Name}' does not have Flamenco configured.",
ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Error); ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Error);
await box.ShowAsync(); await box.ShowAsync();
return; return;
@@ -143,7 +165,7 @@ namespace UnifiedFarmLauncher.Views
await _controllerService.StopWorkerAsync(worker, workerType); await _controllerService.StopWorkerAsync(worker, workerType);
var successBox = MessageBoxManager.GetMessageBoxStandard("Stop Worker", var successBox = MessageBoxManager.GetMessageBoxStandard("Stop Worker",
$"Stop command sent to worker '{worker.Name}'.", $"Stop command sent to worker '{worker.Name}' ({vm.OperationModeDisplayName}).",
ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Info); ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Info);
await successBox.ShowAsync(); await successBox.ShowAsync();
} }
@@ -163,16 +185,22 @@ namespace UnifiedFarmLauncher.Views
{ {
try try
{ {
string? workerType = null; var vm = (MainWindowViewModel)DataContext!;
if (worker.WorkerTypes.SheepIt != null) string workerType = vm.OperationMode;
workerType = "sheepit";
else if (worker.WorkerTypes.Flamenco != null)
workerType = "flamenco";
if (workerType == null) // Verify the worker supports the selected operation mode
if (workerType == "sheepit" && worker.WorkerTypes.SheepIt == null)
{ {
var box = MessageBoxManager.GetMessageBoxStandard("Error", var box = MessageBoxManager.GetMessageBoxStandard("Error",
"Worker has no configured worker type.", $"Worker '{worker.Name}' does not have SheepIt configured.",
ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Error);
await box.ShowAsync();
return;
}
if (workerType == "flamenco" && worker.WorkerTypes.Flamenco == null)
{
var box = MessageBoxManager.GetMessageBoxStandard("Error",
$"Worker '{worker.Name}' does not have Flamenco configured.",
ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Error); ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Error);
await box.ShowAsync(); await box.ShowAsync();
return; return;