Compare commits
14 Commits
610cdb62a8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34c80a00cd | ||
|
|
188d74c072 | ||
|
|
84a518a9c9 | ||
|
|
3833311f11 | ||
|
|
553758c378 | ||
|
|
0460865ebc | ||
|
|
4628fafc79 | ||
|
|
38105c2cfb | ||
|
|
17f685c91a | ||
|
|
202ad49f48 | ||
|
|
03b3ae58f4 | ||
|
|
9436ab7f62 | ||
|
|
06d2c3934b | ||
|
|
d0ac16e53a |
401
.gitignore
vendored
401
.gitignore
vendored
@@ -1,2 +1,403 @@
|
||||
# SpecStory explanation file
|
||||
.specstory/.what-is-this.md
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio cache/options directory
|
||||
.vs/
|
||||
.vscode/
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
*.snupkg
|
||||
**/packages/*
|
||||
!**/packages/build/
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages Directory
|
||||
## TODO: If you have NuGet Package Restore enabled, uncomment the next line
|
||||
#packages/
|
||||
|
||||
# Windows Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Windows Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
@@ -0,0 +1,645 @@
|
||||
<!-- Generated by SpecStory, Markdown v2.1.0 -->
|
||||
|
||||
<!-- cursor Session 2a506942-5bcd-4a99-9e1e-a3cf55d5af92 (2025-12-17 22:32Z) -->
|
||||
|
||||
# Readme for sheepit worker incompatibility fix (2025-12-17 22:32Z)
|
||||
|
||||
_**User (2025-12-17 22:32Z)**_
|
||||
|
||||
context:@.specstory/history/2025-11-21_16-43Z-fixing-sheepit-worker-incompatibility.md
|
||||
|
||||
add a readme.
|
||||
|
||||
---
|
||||
|
||||
_**Agent (model default, mode Agent)**_
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reviewing the context file and codebase to draft the README.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||
<details><summary>Tool use: **read_file** • Read file: .specstory/history/2025-11-21_16-43Z-fixing-sheepit-worker-incompatibility.md</summary>
|
||||
|
||||
</details>
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||
<details><summary>Tool use: **read_file** • Read file: UnifiedFarmLauncher.csproj</summary>
|
||||
|
||||
</details>
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||
<details><summary>Tool use: **read_file** • Read file: Program.cs</summary>
|
||||
|
||||
</details>
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||
<details><summary>Tool use: **read_file** • Read file: ViewModels/MainWindowViewModel.cs</summary>
|
||||
|
||||
</details>
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reading a portion of the context file and reviewing additional files:
|
||||
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||
<details><summary>Tool use: **read_file** • Read file: .specstory/history/2025-11-21_16-43Z-fixing-sheepit-worker-incompatibility.md</summary>
|
||||
|
||||
</details>
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||
<details><summary>Tool use: **read_file** • Read file: Models/ConfigRoot.cs</summary>
|
||||
|
||||
</details>
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||
<details><summary>Tool use: **read_file** • Read file: Services/WorkerControllerService.cs</summary>
|
||||
|
||||
</details>
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||
<details><summary>Tool use: **read_file** • Read file: Views/MainWindow.axaml</summary>
|
||||
|
||||
</details>
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
Checking a few more files to understand the project structure:
|
||||
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||
<details><summary>Tool use: **read_file** • Read file: Models/WorkerConfig.cs</summary>
|
||||
|
||||
</details>
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||
<details><summary>Tool use: **read_file** • Read file: Services/ConfigService.cs</summary>
|
||||
|
||||
</details>
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="search" data-tool-name="codebase_search">
|
||||
<details>
|
||||
<summary>Tool use: **codebase_search** • Searched codebase "What is the purpose of this application and what does it do?" • **8** results</summary>
|
||||
|
||||
| File | Lines |
|
||||
|------|-------|
|
||||
| `App.axaml.cs` | L30 |
|
||||
| `Views/MainWindow.axaml` | L32 |
|
||||
| `Program.cs` | L19 |
|
||||
| `Services/ConfigService.cs` | L62 |
|
||||
| `app.manifest` | L12 |
|
||||
| `ViewModels/MainWindowViewModel.cs` | L60 |
|
||||
| `UnifiedFarmLauncher.csproj` | L30 |
|
||||
| `Views/MainWindow.axaml.cs` | L46 |
|
||||
|
||||
</details>
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
Creating the README:
|
||||
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="write" data-tool-name="write">
|
||||
|
||||
<details><summary>Tool use: **write** • Edit file: README.md</summary>
|
||||
|
||||
**Chunk 1**
|
||||
Lines added: 162, lines removed: 1
|
||||
|
||||
```diff
|
||||
@@ -1,1 +1,162 @@
|
||||
-
|
||||
+ # Unified Farm Launcher
|
||||
+
|
||||
+ A desktop application for managing remote render farm workers across multiple systems. Supports both **SheepIt** and **Flamenco** worker types with a unified interface for starting, stopping, and monitoring workers via SSH.
|
||||
+
|
||||
+ ## Features
|
||||
+
|
||||
+ - **Multi-Worker Support**: Manage multiple remote workers from a single interface
|
||||
+ - **Worker Types**:
|
||||
+ - **SheepIt**: Distributed rendering for Blender projects
|
||||
+ - **Flamenco**: Blender Foundation's render farm manager
|
||||
+ - **Remote Management**: Control workers on remote machines via SSH
|
||||
+ - **Worker Lifecycle**: Start, stop, and attach to worker processes
|
||||
+ - **Filtering**: Filter workers by type (All, SheepIt, Flamenco)
|
||||
+ - **Configuration Management**: Add, edit, and delete worker configurations
|
||||
+ - **Real-time Logging**: Attach to worker logs for real-time monitoring
|
||||
+
|
||||
+ ## Requirements
|
||||
+
|
||||
+ - **.NET 8.0** or later
|
||||
+ - **Windows** (PowerShell required on remote machines)
|
||||
+ - **SSH access** to remote worker machines
|
||||
+ - **Java** (Temurin 21 recommended) on remote machines for SheepIt workers
|
||||
+ - **Flamenco worker executable** on remote machines for Flamenco workers
|
||||
+
|
||||
+ ## Building
|
||||
+
|
||||
+ ```bash
|
||||
+ dotnet build
|
||||
+ ```
|
||||
+
|
||||
+ ## Running
|
||||
+
|
||||
+ ```bash
|
||||
+ dotnet run
|
||||
+ ```
|
||||
+
|
||||
+ Or build and run the executable from the output directory.
|
||||
+
|
||||
+ ## Configuration
|
||||
+
|
||||
+ Configuration is stored in `%LocalAppData%\UnifiedFarmLauncher\workers.json`. The application will create this file automatically on first run.
|
||||
+
|
||||
+ ### Configuration Structure
|
||||
+
|
||||
+ ```json
|
||||
+ {
|
||||
+ "workers": [
|
||||
+ {
|
||||
+ "id": 1,
|
||||
+ "name": "worker-name",
|
||||
+ "enabled": true,
|
||||
+ "ssh": {
|
||||
+ "host": "192.168.1.100",
|
||||
+ "port": 22,
|
||||
+ "username": "user",
|
||||
+ "password": "password"
|
||||
+ },
|
||||
+ "workerTypes": {
|
||||
+ "sheepIt": {
|
||||
+ "username": "sheepit-username",
|
||||
+ "renderKey": "render-key",
|
||||
+ "gpu": "CPU"
|
||||
+ },
|
||||
+ "flamenco": {
|
||||
+ "workerPath": "C:\\path\\to\\flamenco-worker",
|
||||
+ "networkDrives": ["Z:", "Y:"],
|
||||
+ "networkPaths": ["\\\\server\\share"]
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "globalSettings": {
|
||||
+ "sheepItJarUrls": [
|
||||
+ "https://sheepit-renderfarm.com/media/applet/client-latest.php"
|
||||
+ ]
|
||||
+ }
|
||||
+ }
|
||||
+ ```
|
||||
+
|
||||
+ ## Usage
|
||||
+
|
||||
+ ### Adding a Worker
|
||||
+
|
||||
+ 1. Click **Add Worker** button
|
||||
+ 2. Fill in the worker details:
|
||||
+ - **Name**: Unique identifier for the worker
|
||||
+ - **SSH Configuration**: Host, port, username, and password
|
||||
+ - **Worker Type Configuration**: Configure either SheepIt or Flamenco (or both)
|
||||
+ 3. Click **Save**
|
||||
+
|
||||
+ ### Starting a Worker
|
||||
+
|
||||
+ 1. Select a worker from the list
|
||||
+ 2. Click **Start** button
|
||||
+ 3. The application will:
|
||||
+ - Deploy the controller script to the remote machine
|
||||
+ - Start the worker process
|
||||
+ - Monitor the worker status
|
||||
+
|
||||
+ ### Stopping a Worker
|
||||
+
|
||||
+ 1. Select a running worker
|
||||
+ 2. Click **Stop** button
|
||||
+ 3. The worker process will be gracefully terminated
|
||||
+
|
||||
+ ### Attaching to Worker Logs
|
||||
+
|
||||
+ 1. Select a worker
|
||||
+ 2. Click **Attach** button
|
||||
+ 3. A terminal session will open showing real-time worker logs
|
||||
+ 4. Type `detach` to exit the session
|
||||
+
|
||||
+ ### Filtering Workers
|
||||
+
|
||||
+ Use the tabs at the top to filter workers:
|
||||
+ - **All Workers**: Shows all configured workers
|
||||
+ - **SheepIt**: Shows only SheepIt workers
|
||||
+ - **Flamenco**: Shows only Flamenco workers
|
||||
+
|
||||
+ ## Architecture
|
||||
+
|
||||
+ ### Project Structure
|
||||
+
|
||||
+ ```
|
||||
+ UFL/
|
||||
+ ├── Models/ # Data models (ConfigRoot, WorkerConfig, etc.)
|
||||
+ ├── Services/ # Business logic services
|
||||
+ │ ├── ConfigService.cs # Configuration management
|
||||
+ │ ├── SshService.cs # SSH communication
|
||||
+ │ ├── WorkerControllerService.cs # Worker lifecycle management
|
||||
+ │ └── AttachService.cs # Log attachment service
|
||||
+ ├── ViewModels/ # MVVM view models
|
||||
+ ├── Views/ # Avalonia UI views
|
||||
+ └── Scripts/ # PowerShell scripts (embedded resources)
|
||||
+ ├── remote_worker_controller.ps1
|
||||
+ └── remote_worker_attach.ps1
|
||||
+ ```
|
||||
+
|
||||
+ ### Remote Worker Architecture
|
||||
+
|
||||
+ Workers run on remote machines under a controller process:
|
||||
+
|
||||
+ 1. **Controller Script**: Deployed to `%LocalAppData%\UnifiedWorkers\controller.ps1`
|
||||
+ 2. **Worker Data**: Stored in `%LocalAppData%\UnifiedWorkers\{workerType}\{workerName}\`
|
||||
+ - `logs\worker.log`: Worker output logs
|
||||
+ - `state\worker-info.json`: Worker metadata and status
|
||||
+ - `state\commands.txt`: Command queue for worker control
|
||||
+ - `state\payload.ps1`: Worker execution script
|
||||
+
|
||||
+ 3. **Worker Process**: The controller manages the worker process lifecycle, handles restarts, and monitors status.
|
||||
+
|
||||
+ ## Technologies
|
||||
+
|
||||
+ - **Avalonia UI**: Cross-platform .NET UI framework
|
||||
+ - **ReactiveUI**: MVVM framework
|
||||
+ - **System.Text.Json**: JSON serialization
|
||||
+ - **PowerShell**: Remote script execution
|
||||
+
|
||||
+ ## License
|
||||
+
|
||||
+ [Specify your license here]
|
||||
+
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
Created a README for the Unified Farm Launcher. It includes:
|
||||
|
||||
- **Overview**: Description of the application and its purpose
|
||||
- **Features**: List of capabilities
|
||||
- **Requirements**: Prerequisites for running the application
|
||||
- **Building & Running**: Instructions for building and running
|
||||
- **Configuration**: Structure and location of the config file
|
||||
- **Usage**: Step-by-step instructions for common tasks
|
||||
- **Architecture**: Project structure and remote worker architecture
|
||||
- **Technologies**: Technologies used
|
||||
|
||||
The README provides an overview of the application and how to use it. If you want any sections expanded or additional details, say what to add.
|
||||
|
||||
---
|
||||
|
||||
_**User (2025-12-17 22:32Z)**_
|
||||
|
||||
add a changelog.
|
||||
|
||||
---
|
||||
|
||||
_**Agent (model default, mode Agent)**_
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reviewing the history file to identify changes for the changelog.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||
<details><summary>Tool use: **read_file** • Read file: .specstory/history/2025-11-21_16-43Z-fixing-sheepit-worker-incompatibility.md</summary>
|
||||
|
||||
</details>
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="generic" data-tool-name="glob_file_search">
|
||||
<details>
|
||||
<summary>Tool use: **glob_file_search** • Searched codebase "*.md" • **20** results in **1** directory</summary>
|
||||
|
||||
Directory: **c:\Users\Nathan\Desktop\UFL\.specstory** (20 files)
|
||||
|
||||
| File |
|
||||
|------|
|
||||
| `history\2025-11-21_16-43Z-fixing-sheepit-worker-incompatibility.md` |
|
||||
| `history\2025-11-21_01-14Z-workers-failing-and-ssh-connection-issues.md` |
|
||||
| `history\2025-11-19_17-03Z-why-is-git-frozen.md` |
|
||||
| `history\2025-11-06_17-28Z-update-path-structure-for-renders.md` |
|
||||
| `history\2025-11-03_19-27Z-create-sister-script-for-sheepit.md` |
|
||||
| `history\2025-08-13_18-59Z-remove-seq-from-jobname-format.md` |
|
||||
| `.what-is-this.md` |
|
||||
| `history\2025-07-15_18-30Z-configure-launch-standard-worker-for-flamenco-3-7.md` |
|
||||
| `history\2025-06-03_00-04Z-change-retry-function-to-infinite-retries.md` |
|
||||
| `history\2025-05-23_23-12Z-replace-remotecmd-with-remotesimplified.md` |
|
||||
| `history\2025-05-14_15-49Z-cmd-worker-error-handling-and-retry-logic.md` |
|
||||
| `history\2025-05-13_23-13Z-bypassing-password-prompts-in-script.md` |
|
||||
| `history\2025-05-12_16-04Z-technical-scripting-and-troubleshooting-discussion.md` |
|
||||
| `history\2025-05-07_22-05Z-script-modification-for-masterbox.md` |
|
||||
| `history\2025-05-06_21-34Z-identifying-mistakes-and-seeking-guidance.md` |
|
||||
| `history\2025-05-06_20-07Z-switching-to-optix-gpu-rendering.md` |
|
||||
| `history\2025-05-05_16-17Z-flamenco-job-status-troubleshooting.md` |
|
||||
| `history\2025-04-25_19-30Z-i9-13ks-setup-troubleshooting.md` |
|
||||
| `history\2025-04-23_20-48Z-network-drive-mapping-and-file-copying-script.md` |
|
||||
| `history\2025-04-04_17-25Z-creating-worker2-scripts-for-flamenco.md` |
|
||||
|
||||
</details>
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="search" data-tool-name="codebase_search">
|
||||
<details>
|
||||
<summary>Tool use: **codebase_search** • Searched codebase "What are the main features and changes that have been implemented in this project?" • **8** results</summary>
|
||||
|
||||
| File | Lines |
|
||||
|------|-------|
|
||||
| `Views/MainWindow.axaml.cs` | L46 |
|
||||
| `Views/MainWindow.axaml` | L32 |
|
||||
| `ViewModels/MainWindowViewModel.cs` | L60 |
|
||||
| `UnifiedFarmLauncher.csproj` | L30 |
|
||||
| `ViewModels/ViewModelBase.cs` | L26 |
|
||||
| `Services/ConfigService.cs` | L62 |
|
||||
| `App.axaml.cs` | L30 |
|
||||
| `Services/ConfigService.cs` | L59-121 |
|
||||
|
||||
</details>
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reviewing a few more history files to understand the project's evolution:
|
||||
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||
<details><summary>Tool use: **read_file** • Read file: .specstory/history/2025-11-21_01-14Z-workers-failing-and-ssh-connection-issues.md</summary>
|
||||
|
||||
</details>
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||
<details><summary>Tool use: **read_file** • Read file: .specstory/history/2025-11-03_19-27Z-create-sister-script-for-sheepit.md</summary>
|
||||
|
||||
</details>
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||
<details><summary>Tool use: **read_file** • Read file: .specstory/history/2025-07-15_18-30Z-configure-launch-standard-worker-for-flamenco-3-7.md</summary>
|
||||
|
||||
</details>
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||
<details><summary>Tool use: **read_file** • Read file: .specstory/.what-is-this.md</summary>
|
||||
|
||||
</details>
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
Creating the changelog based on the project history and codebase:
|
||||
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="write" data-tool-name="write">
|
||||
|
||||
<details><summary>Tool use: **write** • Edit file: CHANGELOG.md</summary>
|
||||
|
||||
**Chunk 1**
|
||||
Lines added: 121, lines removed: 1
|
||||
|
||||
```diff
|
||||
@@ -1,1 +1,121 @@
|
||||
-
|
||||
+ # Changelog
|
||||
+
|
||||
+ All notable changes to the Unified Farm Launcher project will be documented in this file.
|
||||
+
|
||||
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
+
|
||||
+ ## [Unreleased]
|
||||
+
|
||||
+ ### Added
|
||||
+ - README documentation
|
||||
+ - CHANGELOG file
|
||||
+
|
||||
+ ## [1.0.0] - 2025-11-21
|
||||
+
|
||||
+ ### Added
|
||||
+ - **Avalonia Desktop Application**: Complete migration from PowerShell scripts to a cross-platform desktop application
|
||||
+ - **Worker Management UI**: Graphical interface for managing multiple remote workers
|
||||
+ - **Multi-Worker Type Support**: Unified interface supporting both SheepIt and Flamenco worker types
|
||||
+ - **Worker Configuration Management**: Add, edit, and delete worker configurations through the UI
|
||||
+ - **Worker Filtering**: Filter workers by type (All, SheepIt, Flamenco) using tab controls
|
||||
+ - **Remote Worker Controller**: PowerShell-based controller script deployed to remote machines for process management
|
||||
+ - **Worker Lifecycle Management**: Start, stop, and monitor worker processes remotely
|
||||
+ - **Log Attachment Service**: Real-time log viewing by attaching to remote worker processes
|
||||
+ - **SSH Service**: Secure remote communication with worker machines
|
||||
+ - **Configuration Persistence**: JSON-based configuration stored in `%LocalAppData%\UnifiedFarmLauncher\workers.json`
|
||||
+ - **Worker Status Tracking**: Metadata tracking for worker state, PIDs, restart counts, and exit codes
|
||||
+ - **Automatic Restart Logic**: Configurable restart attempts with delay for failed workers
|
||||
+ - **Command Queue System**: File-based command queue for remote worker control
|
||||
+ - **Embedded PowerShell Scripts**: Controller and attach helper scripts embedded as resources
|
||||
+
|
||||
+ ### Changed
|
||||
+ - **Architecture**: Migrated from standalone PowerShell scripts to structured C# application with MVVM pattern
|
||||
+ - **Payload Handling**: Switched from command-line argument passing to file-based payload storage to avoid Windows command-line length limits
|
||||
+ - **Worker Process Management**: Implemented controller-based architecture for better process lifecycle management
|
||||
+ - **Logging System**: Enhanced logging with timestamped, prefixed log entries (CTRL, OUT, ERR)
|
||||
+ - **SheepIt Integration**: Updated to use JAR file downloads with fallback URLs instead of executable-based approach
|
||||
+
|
||||
+ ### Fixed
|
||||
+ - **Command-Line Length Limits**: Resolved issues with large payloads exceeding Windows command-line argument limits by using file-based payload storage
|
||||
+ - **Worker Process Startup**: Fixed issues with worker processes not starting properly on remote machines
|
||||
+ - **SSH Connection Failures**: Improved SSH connection handling and error reporting
|
||||
+ - **Log Output Capture**: Fixed missing worker output in logs by properly redirecting stdout/stderr
|
||||
+ - **Controller Deployment**: Ensured controller script is properly deployed and executed on remote machines
|
||||
+ - **Process Monitoring**: Improved worker process status tracking and metadata updates
|
||||
+
|
||||
+ ### Technical Details
|
||||
+ - **Framework**: .NET 8.0 with Avalonia UI 11.0.0
|
||||
+ - **Architecture**: MVVM pattern with ReactiveUI
|
||||
+ - **Remote Execution**: PowerShell scripts executed via SSH
|
||||
+ - **Data Storage**: JSON configuration with file-based state management on remote machines
|
||||
+ - **Logging**: File-based logging with real-time streaming support
|
||||
+
|
||||
+ ## [Pre-1.0.0] - Historical Development
|
||||
+
|
||||
+ ### 2025-11-21
|
||||
+ - Fixed SheepIt worker incompatibility issues
|
||||
+ - Resolved command-line argument size limitations
|
||||
+ - Improved payload resolution and file-based storage
|
||||
+
|
||||
+ ### 2025-11-21
|
||||
+ - Fixed SSH connection issues and worker failures
|
||||
+ - Improved error handling for remote command execution
|
||||
+
|
||||
+ ### 2025-11-06
|
||||
+ - Updated path structure for renders
|
||||
+
|
||||
+ ### 2025-11-03
|
||||
+ - Created unified SheepIt launcher script
|
||||
+ - Added support for SheepIt worker type alongside Flamenco
|
||||
+
|
||||
+ ### 2025-08-13
|
||||
+ - Removed sequence numbers from job name format
|
||||
+
|
||||
+ ### 2025-07-15
|
||||
+ - Configured launch standard worker for Flamenco 3.7
|
||||
+ - Updated worker paths to use `F:\software\Flamenco 3.7`
|
||||
+
|
||||
+ ### 2025-06-03
|
||||
+ - Changed retry function to infinite retries
|
||||
+
|
||||
+ ### 2025-05-23
|
||||
+ - Replaced RemoteCmd with RemoteSimplified
|
||||
+
|
||||
+ ### 2025-05-14
|
||||
+ - Added CMD worker error handling and retry logic
|
||||
+
|
||||
+ ### 2025-05-13
|
||||
+ - Implemented password prompt bypassing in scripts
|
||||
+
|
||||
+ ### 2025-05-12
|
||||
+ - Technical scripting and troubleshooting improvements
|
||||
+
|
||||
+ ### 2025-05-07
|
||||
+ - Script modifications for masterbox system
|
||||
+
|
||||
+ ### 2025-05-06
|
||||
+ - Switched to OptiX GPU rendering
|
||||
+ - Identified and fixed various mistakes
|
||||
+
|
||||
+ ### 2025-05-05
|
||||
+ - Flamenco job status troubleshooting
|
||||
+
|
||||
+ ### 2025-04-25
|
||||
+ - i9-13ks setup troubleshooting
|
||||
+
|
||||
+ ### 2025-04-23
|
||||
+ - Network drive mapping and file copying script improvements
|
||||
+
|
||||
+ ### 2025-04-04
|
||||
+ - Created worker2 scripts for Flamenco
|
||||
+
|
||||
+ ---
|
||||
+
|
||||
+ ## Notes
|
||||
+
|
||||
+ - This project evolved from PowerShell-based scripts to a full desktop application
|
||||
+ - Early development focused on Flamenco worker management
|
||||
+ - SheepIt support was added later as a second worker type
|
||||
+ - The migration to Avalonia UI provided a unified interface for managing both worker types
|
||||
+
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
Created a CHANGELOG.md that includes:
|
||||
|
||||
- **Current Version (1.0.0)**: Major features and changes from the migration to the Avalonia desktop application
|
||||
- **Historical Development Timeline**: Chronological entries based on the project history files
|
||||
- **Technical Details**: Framework versions, architecture patterns, and implementation details
|
||||
- **Project Evolution Notes**: Summary of the transition from PowerShell scripts to a desktop application
|
||||
|
||||
The changelog follows the [Keep a Changelog](https://keepachangelog.com/) format with sections for Added, Changed, and Fixed. It documents the project's evolution from early Flamenco worker scripts through the addition of SheepIt support to the current unified desktop application.
|
||||
|
||||
The changelog is ready to use and can be updated as new features and fixes are added.
|
||||
|
||||
---
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
60502
.specstory/history/2025-12-17_23-08Z-build-status-query.md
Normal file
60502
.specstory/history/2025-12-17_23-08Z-build-status-query.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,9 +4,7 @@
|
||||
RequestedThemeVariant="Default">
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
</Application.Styles>
|
||||
<Application.Resources>
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
||||
</Application.Resources>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
|
||||
|
||||
121
CHANGELOG.md
Normal file
121
CHANGELOG.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to the Unified Farm Launcher project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- README documentation
|
||||
- CHANGELOG file
|
||||
|
||||
## [0.0.1] - 2025-11-21
|
||||
|
||||
### Added
|
||||
- **Avalonia Desktop Application**: Complete migration from PowerShell scripts to a cross-platform desktop application
|
||||
- **Worker Management UI**: Graphical interface for managing multiple remote workers
|
||||
- **Multi-Worker Type Support**: Unified interface supporting both SheepIt and Flamenco worker types
|
||||
- **Worker Configuration Management**: Add, edit, and delete worker configurations through the UI
|
||||
- **Worker Filtering**: Filter workers by type (All, SheepIt, Flamenco) using tab controls
|
||||
- **Remote Worker Controller**: PowerShell-based controller script deployed to remote machines for process management
|
||||
- **Worker Lifecycle Management**: Start, stop, and monitor worker processes remotely
|
||||
- **Log Attachment Service**: Real-time log viewing by attaching to remote worker processes
|
||||
- **SSH Service**: Secure remote communication with worker machines
|
||||
- **Configuration Persistence**: JSON-based configuration stored in `%LocalAppData%\UnifiedFarmLauncher\workers.json`
|
||||
- **Worker Status Tracking**: Metadata tracking for worker state, PIDs, restart counts, and exit codes
|
||||
- **Automatic Restart Logic**: Configurable restart attempts with delay for failed workers
|
||||
- **Command Queue System**: File-based command queue for remote worker control
|
||||
- **Embedded PowerShell Scripts**: Controller and attach helper scripts embedded as resources
|
||||
|
||||
### Changed
|
||||
- **Architecture**: Migrated from standalone PowerShell scripts to structured C# application with MVVM pattern
|
||||
- **Payload Handling**: Switched from command-line argument passing to file-based payload storage to avoid Windows command-line length limits
|
||||
- **Worker Process Management**: Implemented controller-based architecture for better process lifecycle management
|
||||
- **Logging System**: Enhanced logging with timestamped, prefixed log entries (CTRL, OUT, ERR)
|
||||
- **SheepIt Integration**: Updated to use JAR file downloads with fallback URLs instead of executable-based approach
|
||||
|
||||
### Fixed
|
||||
- **Command-Line Length Limits**: Resolved issues with large payloads exceeding Windows command-line argument limits by using file-based payload storage
|
||||
- **Worker Process Startup**: Fixed issues with worker processes not starting properly on remote machines
|
||||
- **SSH Connection Failures**: Improved SSH connection handling and error reporting
|
||||
- **Log Output Capture**: Fixed missing worker output in logs by properly redirecting stdout/stderr
|
||||
- **Controller Deployment**: Ensured controller script is properly deployed and executed on remote machines
|
||||
- **Process Monitoring**: Improved worker process status tracking and metadata updates
|
||||
|
||||
### Technical Details
|
||||
- **Framework**: .NET 8.0 with Avalonia UI 11.0.0
|
||||
- **Architecture**: MVVM pattern with ReactiveUI
|
||||
- **Remote Execution**: PowerShell scripts executed via SSH
|
||||
- **Data Storage**: JSON configuration with file-based state management on remote machines
|
||||
- **Logging**: File-based logging with real-time streaming support
|
||||
|
||||
## [Pre-1.0.0] - Historical Development
|
||||
|
||||
### 2025-11-21
|
||||
- Fixed SheepIt worker incompatibility issues
|
||||
- Resolved command-line argument size limitations
|
||||
- Improved payload resolution and file-based storage
|
||||
|
||||
### 2025-11-21
|
||||
- Fixed SSH connection issues and worker failures
|
||||
- Improved error handling for remote command execution
|
||||
|
||||
### 2025-11-06
|
||||
- Updated path structure for renders
|
||||
|
||||
### 2025-11-03
|
||||
- Created unified SheepIt launcher script
|
||||
- Added support for SheepIt worker type alongside Flamenco
|
||||
|
||||
### 2025-08-13
|
||||
- Removed sequence numbers from job name format
|
||||
|
||||
### 2025-07-15
|
||||
- Configured launch standard worker for Flamenco 3.7
|
||||
- Updated worker paths to use `F:\software\Flamenco 3.7`
|
||||
|
||||
### 2025-06-03
|
||||
- Changed retry function to infinite retries
|
||||
|
||||
### 2025-05-23
|
||||
- Replaced RemoteCmd with RemoteSimplified
|
||||
|
||||
### 2025-05-14
|
||||
- Added CMD worker error handling and retry logic
|
||||
|
||||
### 2025-05-13
|
||||
- Implemented password prompt bypassing in scripts
|
||||
|
||||
### 2025-05-12
|
||||
- Technical scripting and troubleshooting improvements
|
||||
|
||||
### 2025-05-07
|
||||
- Script modifications for masterbox system
|
||||
|
||||
### 2025-05-06
|
||||
- Switched to OptiX GPU rendering
|
||||
- Identified and fixed various mistakes
|
||||
|
||||
### 2025-05-05
|
||||
- Flamenco job status troubleshooting
|
||||
|
||||
### 2025-04-25
|
||||
- i9-13ks setup troubleshooting
|
||||
|
||||
### 2025-04-23
|
||||
- Network drive mapping and file copying script improvements
|
||||
|
||||
### 2025-04-04
|
||||
- Created worker2 scripts for Flamenco
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- This project evolved from PowerShell-based scripts to a full desktop application
|
||||
- Early development focused on Flamenco worker management
|
||||
- SheepIt support was added later as a second worker type
|
||||
- The migration to Avalonia UI provided a unified interface for managing both worker types
|
||||
|
||||
@@ -11,6 +11,15 @@ namespace UnifiedFarmLauncher.Models
|
||||
"https://www.sheepit-renderfarm.com/media/applet/client-latest.php",
|
||||
"https://www.sheepit-renderfarm.com/media/applet/client-latest.jar"
|
||||
};
|
||||
|
||||
[JsonPropertyName("sheepItUsername")]
|
||||
public string SheepItUsername { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("sheepItRenderKey")]
|
||||
public string SheepItRenderKey { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("flamencoWorkerPath")]
|
||||
public string FlamencoWorkerPath { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,19 +8,10 @@ namespace UnifiedFarmLauncher.Models
|
||||
{
|
||||
[JsonPropertyName("gpu")]
|
||||
public string Gpu { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("username")]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("renderKey")]
|
||||
public string RenderKey { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class FlamencoConfig
|
||||
{
|
||||
[JsonPropertyName("workerPath")]
|
||||
public string WorkerPath { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("networkDrives")]
|
||||
public List<string> NetworkDrives { get; set; } = new();
|
||||
|
||||
|
||||
162
README.md
Normal file
162
README.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# Unified Farm Launcher
|
||||
|
||||
A desktop application for managing remote render farm workers across multiple systems. Supports both **SheepIt** and **Flamenco** worker types with a unified interface for starting, stopping, and monitoring workers via SSH.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-Worker Support**: Manage multiple remote workers from a single interface
|
||||
- **Worker Types**:
|
||||
- **[SheepIt](https://www.sheepit-renderfarm.com/)**: Distributed rendering for Blender projects
|
||||
- **[Flamenco](https://flamenco.blender.org/)**: Blender Foundation's render farm manager
|
||||
- **Remote Management**: Control workers on remote machines via SSH
|
||||
- **Worker Lifecycle**: Start, stop, and attach to worker processes
|
||||
- **Filtering**: Filter workers by type (All, SheepIt, Flamenco)
|
||||
- **Configuration Management**: Add, edit, and delete worker configurations
|
||||
- **Real-time Logging**: Attach to worker logs for real-time monitoring
|
||||
|
||||
## Requirements
|
||||
|
||||
- **.NET 8.0** or later
|
||||
- **Windows** (PowerShell required on remote machines)
|
||||
- **SSH access** to remote worker machines
|
||||
- **Java** (Temurin 21 recommended) on remote machines for SheepIt workers
|
||||
- **Flamenco worker executable** on remote machines for Flamenco workers
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
dotnet build
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
Or build and run the executable from the output directory.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is stored in `%LocalAppData%\UnifiedFarmLauncher\workers.json`. The application will create this file automatically on first run.
|
||||
|
||||
### Configuration Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"workers": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "worker-name",
|
||||
"enabled": true,
|
||||
"ssh": {
|
||||
"host": "192.168.1.100",
|
||||
"port": 22,
|
||||
"username": "user",
|
||||
"password": "password"
|
||||
},
|
||||
"workerTypes": {
|
||||
"sheepIt": {
|
||||
"username": "sheepit-username",
|
||||
"renderKey": "render-key",
|
||||
"gpu": "CPU"
|
||||
},
|
||||
"flamenco": {
|
||||
"workerPath": "C:\\path\\to\\flamenco-worker",
|
||||
"networkDrives": ["Z:", "Y:"],
|
||||
"networkPaths": ["\\\\server\\share"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalSettings": {
|
||||
"sheepItJarUrls": [
|
||||
"https://sheepit-renderfarm.com/media/applet/client-latest.php"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Adding a Worker
|
||||
|
||||
1. Click **Add Worker** button
|
||||
2. Fill in the worker details:
|
||||
- **Name**: Unique identifier for the worker
|
||||
- **SSH Configuration**: Host, port, username, and password
|
||||
- **Worker Type Configuration**: Configure either SheepIt or Flamenco (or both)
|
||||
3. Click **Save**
|
||||
|
||||
### Starting a Worker
|
||||
|
||||
1. Select a worker from the list
|
||||
2. Click **Start** button
|
||||
3. The application will:
|
||||
- Deploy the controller script to the remote machine
|
||||
- Start the worker process
|
||||
- Monitor the worker status
|
||||
|
||||
### Stopping a Worker
|
||||
|
||||
1. Select a running worker
|
||||
2. Click **Stop** button
|
||||
3. The worker process will be gracefully terminated
|
||||
|
||||
### Attaching to Worker Logs
|
||||
|
||||
1. Select a worker
|
||||
2. Click **Attach** button
|
||||
3. A terminal session will open showing real-time worker logs
|
||||
4. Type `detach` to exit the session
|
||||
|
||||
### Filtering Workers
|
||||
|
||||
Use the tabs at the top to filter workers:
|
||||
- **All Workers**: Shows all configured workers
|
||||
- **SheepIt**: Shows only SheepIt workers
|
||||
- **Flamenco**: Shows only Flamenco workers
|
||||
|
||||
## Architecture
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
UFL/
|
||||
├── Models/ # Data models (ConfigRoot, WorkerConfig, etc.)
|
||||
├── Services/ # Business logic services
|
||||
│ ├── ConfigService.cs # Configuration management
|
||||
│ ├── SshService.cs # SSH communication
|
||||
│ ├── WorkerControllerService.cs # Worker lifecycle management
|
||||
│ └── AttachService.cs # Log attachment service
|
||||
├── ViewModels/ # MVVM view models
|
||||
├── Views/ # Avalonia UI views
|
||||
└── Scripts/ # PowerShell scripts (embedded resources)
|
||||
├── remote_worker_controller.ps1
|
||||
└── remote_worker_attach.ps1
|
||||
```
|
||||
|
||||
### Remote Worker Architecture
|
||||
|
||||
Workers run on remote machines under a controller process:
|
||||
|
||||
1. **Controller Script**: Deployed to `%LocalAppData%\UnifiedWorkers\controller.ps1`
|
||||
2. **Worker Data**: Stored in `%LocalAppData%\UnifiedWorkers\{workerType}\{workerName}\`
|
||||
- `logs\worker.log`: Worker output logs
|
||||
- `state\worker-info.json`: Worker metadata and status
|
||||
- `state\commands.txt`: Command queue for worker control
|
||||
- `state\payload.ps1`: Worker execution script
|
||||
|
||||
3. **Worker Process**: The controller manages the worker process lifecycle, handles restarts, and monitors status.
|
||||
|
||||
## Technologies
|
||||
|
||||
- **Avalonia UI**: Cross-platform .NET UI framework
|
||||
- **ReactiveUI**: MVVM framework
|
||||
- **System.Text.Json**: JSON serialization
|
||||
- **PowerShell**: Remote script execution
|
||||
|
||||
## License
|
||||
|
||||
[Specify your license here]
|
||||
|
||||
@@ -1,18 +1,52 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[Parameter(Mandatory = $true, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)]
|
||||
[string]$WorkerName,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[Parameter(Mandatory = $true, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)]
|
||||
[string]$WorkerType,
|
||||
|
||||
[string]$DataRoot = (Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'UnifiedWorkers'),
|
||||
[Parameter(ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)]
|
||||
[string]$DataRoot = (Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'UnifiedWorkers'),
|
||||
|
||||
[Parameter(ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)]
|
||||
[switch]$CommandOnly,
|
||||
|
||||
[Parameter(ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)]
|
||||
[string]$Command
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
# Explicitly clear any pipeline input to prevent binding errors
|
||||
$null = $input
|
||||
|
||||
# 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 {
|
||||
param([string]$Root, [string]$Type, [string]$Name)
|
||||
@@ -28,7 +62,17 @@ function Get-WorkerPaths {
|
||||
$paths = Get-WorkerPaths -Root $DataRoot -Type $WorkerType -Name $WorkerName
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -73,16 +117,58 @@ if ($CommandOnly) {
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -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 {
|
||||
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
|
||||
|
||||
try {
|
||||
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 "> "
|
||||
if ($null -eq $input) {
|
||||
continue
|
||||
@@ -104,10 +190,14 @@ try {
|
||||
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 {
|
||||
if ($logJob) {
|
||||
Stop-Job -Job $logJob -ErrorAction SilentlyContinue
|
||||
Receive-Job -Job $logJob -ErrorAction SilentlyContinue | Out-Null
|
||||
Remove-Job -Job $logJob -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
|
||||
@@ -224,8 +224,24 @@ function Get-PendingCommands {
|
||||
# endregion
|
||||
|
||||
try {
|
||||
# Check if metadata already exists with a ControllerPid (set by ensureScript)
|
||||
$existingControllerPid = $null
|
||||
if (Test-Path $metaPath) {
|
||||
try {
|
||||
$existing = Get-Content $metaPath -Raw | ConvertFrom-Json
|
||||
if ($existing.ControllerPid) {
|
||||
$existingControllerPid = $existing.ControllerPid
|
||||
}
|
||||
} catch {
|
||||
# Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
# Use existing ControllerPid if available, otherwise use current PID
|
||||
$controllerPidToUse = if ($existingControllerPid) { $existingControllerPid } else { $PID }
|
||||
|
||||
# record initial state before launching worker
|
||||
Write-Metadata -Status 'initializing' -WorkerPid $null -ControllerPid $PID -Restarts 0
|
||||
Write-Metadata -Status 'initializing' -WorkerPid $null -ControllerPid $controllerPidToUse -Restarts 0
|
||||
|
||||
$resolvedPayloadBase64 = Resolve-PayloadBase64
|
||||
$PayloadBase64 = $resolvedPayloadBase64
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using UnifiedFarmLauncher.Models;
|
||||
|
||||
@@ -20,7 +21,7 @@ namespace UnifiedFarmLauncher.Services
|
||||
await _controllerService.DeployAttachHelperAsync(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}\"";
|
||||
if (commandOnly)
|
||||
@@ -32,7 +33,11 @@ namespace UnifiedFarmLauncher.Services
|
||||
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
|
||||
// Use -Command with & to properly invoke the script and avoid pipeline input issues
|
||||
var escapedHelper = remoteHelper.Replace("'", "''");
|
||||
var remoteCmd = $"powershell.exe -NoLogo -NoProfile -NoExit -ExecutionPolicy Bypass -Command \"& '{escapedHelper}' {paramsBlock}\"";
|
||||
|
||||
_sshService.StartInteractiveSsh(worker, remoteCmd);
|
||||
}
|
||||
|
||||
@@ -197,12 +197,15 @@ namespace UnifiedFarmLauncher.Services
|
||||
var sshArgs = BuildSshArgs(parts, interactive);
|
||||
sshArgs.Add(command);
|
||||
|
||||
var argsString = string.Join(" ", sshArgs.Select(arg => $"\"{arg.Replace("\"", "\\\"")}\""));
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = GetSshExecutable(),
|
||||
Arguments = string.Join(" ", sshArgs.Select(arg => $"\"{arg.Replace("\"", "\\\"")}\"")),
|
||||
WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
Arguments = argsString,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
@@ -230,6 +233,72 @@ namespace UnifiedFarmLauncher.Services
|
||||
return output.ToString();
|
||||
}
|
||||
|
||||
public async Task<string> ExecuteRemoteScriptAsync(WorkerConfig worker, string script, bool interactive = false)
|
||||
{
|
||||
var parts = ParseConnectionParts(worker.Ssh.Args, worker.Ssh.Host);
|
||||
var sshArgs = BuildSshArgs(parts, interactive);
|
||||
// Add PowerShell command as a single quoted argument for remote execution
|
||||
sshArgs.Add("powershell -NoLogo -NoProfile -NonInteractive -OutputFormat Text -ExecutionPolicy Bypass -Command -");
|
||||
|
||||
var argsString = string.Join(" ", sshArgs.Select(arg => $"\"{arg.Replace("\"", "\\\"")}\""));
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = GetSshExecutable(),
|
||||
WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
Arguments = argsString,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = !interactive
|
||||
}
|
||||
};
|
||||
|
||||
var output = new StringBuilder();
|
||||
var error = new StringBuilder();
|
||||
|
||||
process.OutputDataReceived += (s, e) => { if (e.Data != null) output.AppendLine(e.Data); };
|
||||
process.ErrorDataReceived += (s, e) => { if (e.Data != null) error.AppendLine(e.Data); };
|
||||
|
||||
process.Start();
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
// Write script to stdin
|
||||
await process.StandardInput.WriteAsync(script);
|
||||
process.StandardInput.Close();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
if (process.ExitCode != 0 && !interactive)
|
||||
{
|
||||
var errorText = error.ToString();
|
||||
var combinedOutput = output.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(errorText))
|
||||
{
|
||||
combinedOutput = string.IsNullOrWhiteSpace(combinedOutput)
|
||||
? errorText
|
||||
: $"{combinedOutput}\n\n=== STDERR ===\n{errorText}";
|
||||
}
|
||||
throw new InvalidOperationException($"SSH script execution failed with exit code {process.ExitCode}: {combinedOutput}");
|
||||
}
|
||||
|
||||
// Include stderr in output if present (for diagnostics)
|
||||
var result = output.ToString();
|
||||
var stderrText = error.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(stderrText) && !interactive)
|
||||
{
|
||||
result = string.IsNullOrWhiteSpace(result)
|
||||
? stderrText
|
||||
: $"{result}\n\n=== STDERR ===\n{stderrText}";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<string> GetWorkerBasePathAsync(WorkerConfig worker)
|
||||
{
|
||||
if (_workerBasePathCache.TryGetValue(worker.Name, out var cached))
|
||||
@@ -288,12 +357,15 @@ namespace UnifiedFarmLauncher.Services
|
||||
var sshArgs = BuildSshArgs(parts, true);
|
||||
sshArgs.Add(command);
|
||||
|
||||
var argsString = string.Join(" ", sshArgs.Select(arg => $"\"{arg.Replace("\"", "\\\"")}\""));
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = GetSshExecutable(),
|
||||
Arguments = string.Join(" ", sshArgs.Select(arg => $"\"{arg.Replace("\"", "\\\"")}\"")),
|
||||
WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
Arguments = argsString,
|
||||
UseShellExecute = true,
|
||||
CreateNoWindow = false
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnifiedFarmLauncher.Models;
|
||||
|
||||
@@ -65,7 +68,7 @@ $controllerPath = Join-Path $dataRoot 'controller.ps1'
|
||||
[IO.File]::WriteAllBytes($controllerPath, [Convert]::FromBase64String('{controllerBase64}'))
|
||||
";
|
||||
|
||||
await _sshService.ExecuteRemoteCommandAsync(worker, $"powershell -NoLogo -NoProfile -NonInteractive -OutputFormat Text -ExecutionPolicy Bypass -EncodedCommand {Convert.ToBase64String(Encoding.Unicode.GetBytes(script))}");
|
||||
await _sshService.ExecuteRemoteScriptAsync(worker, script);
|
||||
}
|
||||
|
||||
public async Task DeployAttachHelperAsync(WorkerConfig worker)
|
||||
@@ -79,7 +82,7 @@ $attachPath = Join-Path $dataRoot 'attach-helper.ps1'
|
||||
[IO.File]::WriteAllBytes($attachPath, [Convert]::FromBase64String('{helperBase64}'))
|
||||
";
|
||||
|
||||
await _sshService.ExecuteRemoteCommandAsync(worker, $"powershell -NoLogo -NoProfile -NonInteractive -OutputFormat Text -ExecutionPolicy Bypass -EncodedCommand {Convert.ToBase64String(Encoding.Unicode.GetBytes(script))}");
|
||||
await _sshService.ExecuteRemoteScriptAsync(worker, script);
|
||||
}
|
||||
|
||||
public string GenerateSheepItPayload(WorkerConfig worker)
|
||||
@@ -89,9 +92,10 @@ $attachPath = Join-Path $dataRoot 'attach-helper.ps1'
|
||||
|
||||
var config = _configService.Load();
|
||||
var sheepIt = worker.WorkerTypes.SheepIt;
|
||||
var safeKey = sheepIt.RenderKey.Replace("'", "''");
|
||||
var safeUser = sheepIt.Username.Replace("'", "''");
|
||||
var urls = config.GlobalSettings.SheepItJarUrls;
|
||||
var globalSettings = config.GlobalSettings;
|
||||
var safeKey = globalSettings.SheepItRenderKey.Replace("'", "''");
|
||||
var safeUser = globalSettings.SheepItUsername.Replace("'", "''");
|
||||
var urls = globalSettings.SheepItJarUrls;
|
||||
var urlLiteral = "@(" + string.Join(", ", Array.ConvertAll(urls.ToArray(), url => $"'{url}'")) + ")";
|
||||
|
||||
return $@"
|
||||
@@ -170,7 +174,10 @@ catch {{
|
||||
if (worker.WorkerTypes.Flamenco == null)
|
||||
throw new InvalidOperationException("Worker does not have Flamenco configuration");
|
||||
|
||||
var config = _configService.Load();
|
||||
var flamenco = worker.WorkerTypes.Flamenco;
|
||||
var globalSettings = config.GlobalSettings;
|
||||
var workerPath = globalSettings.FlamencoWorkerPath.Replace("'", "''");
|
||||
var drives = string.Join(", ", Array.ConvertAll(flamenco.NetworkDrives.ToArray(), d => $"'{d}'"));
|
||||
var paths = string.Join(", ", Array.ConvertAll(flamenco.NetworkPaths.ToArray(), p => $"'{p.Replace("\\", "\\\\")}'"));
|
||||
|
||||
@@ -199,7 +206,7 @@ try {{
|
||||
|
||||
# Start worker
|
||||
Write-Host ""Starting Flamenco worker..."" -ForegroundColor Cyan
|
||||
Set-Location '{flamenco.WorkerPath}'
|
||||
Set-Location '{workerPath}'
|
||||
if (Test-Path 'flamenco-worker.exe') {{
|
||||
Write-Host ""Running flamenco-worker.exe..."" -ForegroundColor Green
|
||||
$workerProcess = Start-Process -FilePath '.\flamenco-worker.exe' -NoNewWindow -PassThru -Wait
|
||||
@@ -207,8 +214,8 @@ try {{
|
||||
Write-Host ""Flamenco worker process has terminated with exit code: $exitCode"" -ForegroundColor Yellow
|
||||
exit $exitCode
|
||||
}} else {{
|
||||
Write-Host ""Error: flamenco-worker.exe not found in {flamenco.WorkerPath}"" -ForegroundColor Red
|
||||
[Console]::Error.WriteLine(""Error: flamenco-worker.exe not found in {flamenco.WorkerPath}"")
|
||||
Write-Host ""Error: flamenco-worker.exe not found in {workerPath}"" -ForegroundColor Red
|
||||
[Console]::Error.WriteLine(""Error: flamenco-worker.exe not found in {workerPath}"")
|
||||
exit 1
|
||||
}}
|
||||
}}
|
||||
@@ -245,9 +252,12 @@ catch {{
|
||||
var payloadBase64 = Convert.ToBase64String(Encoding.Unicode.GetBytes(payloadScript));
|
||||
var remoteBasePath = await _sshService.GetWorkerBasePathAsync(worker);
|
||||
|
||||
var jsonParams = $@"{{""WorkerName"":""{worker.Name}"",""WorkerType"":""{workerType}"",""PayloadBase64"":""{payloadBase64}""}}";
|
||||
var jsonParamsBase64 = Convert.ToBase64String(Encoding.Unicode.GetBytes(jsonParams));
|
||||
|
||||
var ensureScript = $@"
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
$params = ConvertFrom-Json ([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('{Convert.ToBase64String(Encoding.Unicode.GetBytes($@"{{""WorkerName"":""{worker.Name}"",""WorkerType"":""{workerType}"",""PayloadBase64"":""{payloadBase64}""}}"@))}')))
|
||||
$params = ConvertFrom-Json ([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('{jsonParamsBase64}')))
|
||||
$workerName = $params.WorkerName
|
||||
$workerType = $params.WorkerType
|
||||
$payloadBase64 = $params.PayloadBase64
|
||||
@@ -318,12 +328,367 @@ if ($shouldStart) {{
|
||||
'-PayloadBase64Path',""$payloadBase64Path""
|
||||
)
|
||||
|
||||
Start-Process -FilePath $psExe -ArgumentList $controllerArgs -WindowStyle Hidden | Out-Null
|
||||
Write-Host ""Worker $workerName started under controller."" -ForegroundColor Green
|
||||
Write-Host ""Attempting to start controller with: $psExe"" -ForegroundColor Cyan
|
||||
Write-Host ""Controller path: $controllerPath"" -ForegroundColor Cyan
|
||||
Write-Host ""Arguments: $($controllerArgs -join ' ')"" -ForegroundColor Cyan
|
||||
|
||||
try {{
|
||||
$controllerProcess = Start-Process -FilePath $psExe -ArgumentList $controllerArgs -WindowStyle Hidden -PassThru -ErrorAction Stop
|
||||
if ($controllerProcess) {{
|
||||
$controllerPid = $controllerProcess.Id
|
||||
Write-Host ""Controller process started with PID: $controllerPid"" -ForegroundColor Green
|
||||
|
||||
# Give it a moment to initialize
|
||||
Start-Sleep -Milliseconds 500
|
||||
|
||||
# Verify process is still running
|
||||
$stillRunning = Get-Process -Id $controllerPid -ErrorAction SilentlyContinue
|
||||
if (-not $stillRunning) {{
|
||||
throw ""Controller process (PID: $controllerPid) exited immediately after start""
|
||||
}}
|
||||
|
||||
# Update metadata with controller PID
|
||||
$updatedMeta = [pscustomobject]@{{
|
||||
WorkerName = $workerName
|
||||
WorkerType = $workerType
|
||||
Status = 'launching'
|
||||
ControllerPid = $controllerPid
|
||||
WorkerPid = $null
|
||||
Restarts = 0
|
||||
LastExitCode = $null
|
||||
LogPath = $logPath
|
||||
CommandPath = $commandPath
|
||||
PayloadPath = $payloadPath
|
||||
UpdatedAtUtc = (Get-Date).ToUniversalTime()
|
||||
}} | ConvertTo-Json -Depth 5
|
||||
$updatedMeta | Set-Content -Path $metaPath -Encoding UTF8
|
||||
|
||||
# Ensure file is flushed to disk before controller reads it
|
||||
Start-Sleep -Milliseconds 200
|
||||
|
||||
Write-Host ""Worker $workerName started under controller (PID: $controllerPid)."" -ForegroundColor Green
|
||||
}} else {{
|
||||
throw ""Start-Process returned null - controller failed to start""
|
||||
}}
|
||||
}} catch {{
|
||||
$errorMsg = $_.Exception.Message
|
||||
$errorDetails = $_.Exception.ToString()
|
||||
Write-Host ""Failed to start controller: $errorMsg"" -ForegroundColor Red
|
||||
Write-Host ""Error details: $errorDetails"" -ForegroundColor Red
|
||||
[Console]::Error.WriteLine(""Controller startup error: $errorMsg"")
|
||||
[Console]::Error.WriteLine(""Full error: $errorDetails"")
|
||||
|
||||
# Update metadata with error
|
||||
$errorMeta = [pscustomobject]@{{
|
||||
WorkerName = $workerName
|
||||
WorkerType = $workerType
|
||||
Status = 'error'
|
||||
ControllerPid = $null
|
||||
WorkerPid = $null
|
||||
Restarts = 0
|
||||
LastExitCode = 1
|
||||
LogPath = $logPath
|
||||
CommandPath = $commandPath
|
||||
PayloadPath = $payloadPath
|
||||
UpdatedAtUtc = (Get-Date).ToUniversalTime()
|
||||
ErrorMessage = ""Controller startup failed: $errorMsg""
|
||||
}} | ConvertTo-Json -Depth 5
|
||||
$errorMeta | Set-Content -Path $metaPath -Encoding UTF8
|
||||
throw
|
||||
}}
|
||||
}}
|
||||
";
|
||||
|
||||
await _sshService.ExecuteRemoteCommandAsync(worker, $"powershell -NoLogo -NoProfile -NonInteractive -OutputFormat Text -ExecutionPolicy Bypass -EncodedCommand {Convert.ToBase64String(Encoding.Unicode.GetBytes(ensureScript))}");
|
||||
// Pipe script through stdin to avoid command line length limits
|
||||
string? ensureScriptOutput = null;
|
||||
try
|
||||
{
|
||||
ensureScriptOutput = await _sshService.ExecuteRemoteScriptAsync(worker, ensureScript);
|
||||
// Log any output from ensureScript for debugging
|
||||
if (!string.IsNullOrWhiteSpace(ensureScriptOutput))
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"EnsureScript output: {ensureScriptOutput}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If ensureScript fails, include that in diagnostics
|
||||
System.Diagnostics.Debug.WriteLine($"EnsureScript execution error: {ex.Message}");
|
||||
// Don't throw here - let the status check below handle it with full diagnostics
|
||||
}
|
||||
|
||||
// Wait a moment for the controller to start and update metadata
|
||||
// Give extra time for the metadata write to complete
|
||||
await Task.Delay(3000);
|
||||
|
||||
// Verify the worker actually started by checking metadata
|
||||
// Try up to 3 times with delays to account for slower startup
|
||||
WorkerStatus? status = null;
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
status = await GetWorkerStatusAsync(worker, workerType);
|
||||
if (status != null && (status.Status == "running" || status.Status == "launching"))
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (i < 2) // Don't delay on last attempt
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
|
||||
if (status == null || (status.Status != "running" && status.Status != "launching"))
|
||||
{
|
||||
// Gather comprehensive diagnostic information
|
||||
var diagnostics = await GatherDiagnosticsAsync(worker, workerType);
|
||||
var errorMessage = status?.ErrorMessage ?? "Worker failed to start. Check logs for details.";
|
||||
var statusText = status?.Status ?? "unknown";
|
||||
|
||||
// Include ensureScript output if available
|
||||
var ensureOutputInfo = string.IsNullOrWhiteSpace(ensureScriptOutput)
|
||||
? ""
|
||||
: $"\n\n=== EnsureScript Output ===\n{ensureScriptOutput}";
|
||||
|
||||
throw new InvalidOperationException($"Worker did not start successfully. Status: {statusText}. {errorMessage}\n\n{diagnostics}{ensureOutputInfo}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<WorkerStatus?> GetWorkerStatusAsync(WorkerConfig worker, string workerType)
|
||||
{
|
||||
var remoteBasePath = await _sshService.GetWorkerBasePathAsync(worker);
|
||||
var instanceRoot = Path.Combine(remoteBasePath, workerType, worker.Name);
|
||||
var metaPath = Path.Combine(instanceRoot, "state", "worker-info.json");
|
||||
|
||||
var script = $@"
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
$dataRoot = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'UnifiedWorkers'
|
||||
$instanceRoot = Join-Path (Join-Path $dataRoot '{workerType}') '{worker.Name}'
|
||||
$metaPath = Join-Path $instanceRoot 'state\worker-info.json'
|
||||
if (Test-Path $metaPath) {{
|
||||
try {{
|
||||
$meta = Get-Content $metaPath -Raw | ConvertFrom-Json
|
||||
$meta | ConvertTo-Json -Depth 5 -Compress
|
||||
}} catch {{
|
||||
Write-Error ""Error reading metadata: $($_.Exception.Message)""
|
||||
}}
|
||||
}} else {{
|
||||
Write-Error ""Metadata file not found at $metaPath""
|
||||
}}
|
||||
";
|
||||
|
||||
try
|
||||
{
|
||||
var output = await _sshService.ExecuteRemoteScriptAsync(worker, script);
|
||||
if (string.IsNullOrWhiteSpace(output))
|
||||
return null;
|
||||
|
||||
var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
// Look for JSON line - it might be on a single line or multiple lines
|
||||
var jsonLines = lines.Where(line => line.Trim().StartsWith("{")).ToList();
|
||||
|
||||
string? jsonText = null;
|
||||
if (jsonLines.Count > 0)
|
||||
{
|
||||
// Use the first JSON line found
|
||||
jsonText = jsonLines[0].Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try to find JSON that might span multiple lines
|
||||
var fullText = string.Join("", lines);
|
||||
var startIdx = fullText.IndexOf('{');
|
||||
if (startIdx >= 0)
|
||||
{
|
||||
var endIdx = fullText.LastIndexOf('}');
|
||||
if (endIdx > startIdx)
|
||||
{
|
||||
jsonText = fullText.Substring(startIdx, endIdx - startIdx + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(jsonText))
|
||||
return null;
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
return JsonSerializer.Deserialize<WorkerStatus>(jsonText, options);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the exception for debugging but return null
|
||||
System.Diagnostics.Debug.WriteLine($"Error parsing worker status: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> GatherDiagnosticsAsync(WorkerConfig worker, string workerType)
|
||||
{
|
||||
var diagnostics = new StringBuilder();
|
||||
|
||||
var script = $@"
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$dataRoot = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'UnifiedWorkers'
|
||||
$instanceRoot = Join-Path (Join-Path $dataRoot '{workerType}') '{worker.Name}'
|
||||
$metaPath = Join-Path $instanceRoot 'state\worker-info.json'
|
||||
$logPath = Join-Path $instanceRoot 'logs\worker.log'
|
||||
$controllerPath = Join-Path $dataRoot 'controller.ps1'
|
||||
|
||||
Write-Host ""=== Diagnostics ==="" -ForegroundColor Cyan
|
||||
Write-Host ""Metadata file exists: $((Test-Path $metaPath))""
|
||||
Write-Host ""Log file exists: $((Test-Path $logPath))""
|
||||
Write-Host ""Controller script exists: $((Test-Path $controllerPath))""
|
||||
|
||||
# Check controller script content
|
||||
if (Test-Path $controllerPath) {{
|
||||
$controllerSize = (Get-Item $controllerPath).Length
|
||||
Write-Host ""Controller script size: $controllerSize bytes""
|
||||
$firstLine = Get-Content $controllerPath -First 1 -ErrorAction SilentlyContinue
|
||||
if ($firstLine) {{
|
||||
Write-Host ""Controller script first line: $firstLine""
|
||||
}}
|
||||
|
||||
# Try to test if controller script can be parsed/executed
|
||||
Write-Host ""`n=== Testing Controller Script ==="" -ForegroundColor Cyan
|
||||
try {{
|
||||
$testResult = powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command ""& {{ Get-Command -Name '$controllerPath' -ErrorAction Stop }}"" 2>&1
|
||||
Write-Host ""Controller script syntax check: Passed""
|
||||
}} catch {{
|
||||
Write-Host ""Controller script syntax check failed: $($_.Exception.Message)"" -ForegroundColor Red
|
||||
}}
|
||||
|
||||
# Check if we can manually invoke the controller with test parameters
|
||||
$testParams = @('-WorkerName', 'TEST', '-WorkerType', 'sheepit', '-PayloadBase64Path', 'C:\temp\test.b64')
|
||||
Write-Host ""Attempting to test controller invocation...""
|
||||
try {{
|
||||
$testProcess = Start-Process -FilePath powershell -ArgumentList @('-NoLogo', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ""$controllerPath"", '-WorkerName', 'TEST') -PassThru -WindowStyle Hidden -ErrorAction Stop
|
||||
Start-Sleep -Milliseconds 200
|
||||
if ($testProcess.HasExited) {{
|
||||
Write-Host ""Test controller process exited with code: $($testProcess.ExitCode)"" -ForegroundColor Yellow
|
||||
}} else {{
|
||||
Write-Host ""Test controller process started successfully (PID: $($testProcess.Id))"" -ForegroundColor Green
|
||||
Stop-Process -Id $testProcess.Id -Force -ErrorAction SilentlyContinue
|
||||
}}
|
||||
}} catch {{
|
||||
Write-Host ""Test controller invocation failed: $($_.Exception.Message)"" -ForegroundColor Red
|
||||
Write-Host ""Full error: $($_.Exception.ToString())"" -ForegroundColor Red
|
||||
}}
|
||||
}}
|
||||
|
||||
if (Test-Path $metaPath) {{
|
||||
try {{
|
||||
$meta = Get-Content $metaPath -Raw | ConvertFrom-Json
|
||||
Write-Host ""Metadata Status: $($meta.Status)""
|
||||
Write-Host ""Controller PID: $($meta.ControllerPid)""
|
||||
Write-Host ""Worker PID: $($meta.WorkerPid)""
|
||||
if ($meta.ErrorMessage) {{
|
||||
Write-Host ""Error Message: $($meta.ErrorMessage)"" -ForegroundColor Red
|
||||
}}
|
||||
}} catch {{
|
||||
Write-Host ""Error reading metadata: $($_.Exception.Message)"" -ForegroundColor Red
|
||||
Write-Host ""Metadata file content (first 500 chars):"" -ForegroundColor Yellow
|
||||
$metaContent = Get-Content $metaPath -Raw -ErrorAction SilentlyContinue
|
||||
if ($metaContent) {{
|
||||
Write-Host $metaContent.Substring(0, [Math]::Min(500, $metaContent.Length))
|
||||
}}
|
||||
}}
|
||||
}} else {{
|
||||
Write-Host ""Metadata file not found - controller may not have started"" -ForegroundColor Yellow
|
||||
}}
|
||||
|
||||
# Check if controller process is running
|
||||
$controllerRunning = $false
|
||||
$controllerProcessInfo = ""N/A""
|
||||
if (Test-Path $metaPath) {{
|
||||
try {{
|
||||
$meta = Get-Content $metaPath -Raw | ConvertFrom-Json
|
||||
if ($meta.ControllerPid) {{
|
||||
$proc = Get-Process -Id $meta.ControllerPid -ErrorAction SilentlyContinue
|
||||
$controllerRunning = ($null -ne $proc)
|
||||
if ($controllerRunning) {{
|
||||
$controllerProcessInfo = ""Running (PID: $($meta.ControllerPid))""
|
||||
}} else {{
|
||||
$controllerProcessInfo = ""Not running (PID: $($meta.ControllerPid) - process not found)""
|
||||
}}
|
||||
}} else {{
|
||||
$controllerProcessInfo = ""No controller PID in metadata""
|
||||
}}
|
||||
}} catch {{
|
||||
$controllerProcessInfo = ""Error checking: $($_.Exception.Message)""
|
||||
}}
|
||||
}} else {{
|
||||
$controllerProcessInfo = ""Metadata file not found""
|
||||
}}
|
||||
Write-Host ""Controller process: $controllerProcessInfo""
|
||||
|
||||
Write-Host ""`n=== Recent Log Entries (last 30 lines) ==="" -ForegroundColor Cyan
|
||||
if (Test-Path $logPath) {{
|
||||
try {{
|
||||
$logFileInfo = Get-Item $logPath -ErrorAction Stop
|
||||
$fileSize = $logFileInfo.Length
|
||||
Write-Host ""Log file size: $fileSize bytes"" -ForegroundColor Gray
|
||||
|
||||
if ($fileSize -eq 0) {{
|
||||
Write-Host ""Log file is empty (0 bytes) - controller may not have started writing yet"" -ForegroundColor Yellow
|
||||
}} else {{
|
||||
# Try Get-Content first
|
||||
$logContent = Get-Content $logPath -ErrorAction SilentlyContinue
|
||||
if ($null -ne $logContent -and $logContent.Count -gt 0) {{
|
||||
$logLines = if ($logContent.Count -gt 30) {{ $logContent[-30..-1] }} else {{ $logContent }}
|
||||
Write-Host ""Found $($logLines.Count) log lines (showing last $([Math]::Min(30, $logLines.Count)))""
|
||||
foreach ($line in $logLines) {{
|
||||
Write-Host $line
|
||||
}}
|
||||
}} else {{
|
||||
Write-Host ""Get-Content returned no lines, attempting to read raw bytes..."" -ForegroundColor Yellow
|
||||
try {{
|
||||
$rawBytes = [IO.File]::ReadAllBytes($logPath)
|
||||
Write-Host ""Read $($rawBytes.Length) bytes from log file""
|
||||
if ($rawBytes.Length -gt 0) {{
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($rawBytes)
|
||||
$lines = $text -split ""`r?`n""
|
||||
Write-Host ""Parsed into $($lines.Count) lines""
|
||||
$displayLines = if ($lines.Count -gt 30) {{ $lines[-30..-1] }} else {{ $lines }}
|
||||
foreach ($line in $displayLines) {{
|
||||
if ($line) {{ Write-Host $line }}
|
||||
}}
|
||||
}} else {{
|
||||
Write-Host ""Log file is empty"" -ForegroundColor Yellow
|
||||
}}
|
||||
}} catch {{
|
||||
Write-Host ""Could not read log file bytes: $($_.Exception.Message)"" -ForegroundColor Red
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}} catch {{
|
||||
Write-Host ""Error reading log: $($_.Exception.Message)"" -ForegroundColor Red
|
||||
Write-Host ""Stack trace: $($_.ScriptStackTrace)"" -ForegroundColor DarkRed
|
||||
}}
|
||||
}} else {{
|
||||
Write-Host ""Log file does not exist yet at: $logPath"" -ForegroundColor Yellow
|
||||
}}
|
||||
";
|
||||
|
||||
try
|
||||
{
|
||||
var output = await _sshService.ExecuteRemoteScriptAsync(worker, script);
|
||||
return output.Trim();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"Unable to gather diagnostics: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private class WorkerStatus
|
||||
{
|
||||
public string? Status { get; set; }
|
||||
public int? WorkerPid { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public async Task StopWorkerAsync(WorkerConfig worker, string workerType)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Avalonia.Desktop">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
@@ -13,13 +13,14 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.Theme.Fluent" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.0" />
|
||||
<PackageReference Include="MsBox.Avalonia" Version="2.0.6" />
|
||||
<PackageReference Include="Avalonia" Version="11.3.9" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.9" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.9" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.9" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.3.9" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.9" />
|
||||
<PackageReference Include="MessageBox.Avalonia" Version="3.2.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
61
ViewModels/GlobalSettingsViewModel.cs
Normal file
61
ViewModels/GlobalSettingsViewModel.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using UnifiedFarmLauncher.Models;
|
||||
using UnifiedFarmLauncher.Services;
|
||||
|
||||
namespace UnifiedFarmLauncher.ViewModels
|
||||
{
|
||||
public class GlobalSettingsViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ConfigService _configService;
|
||||
private string _sheepItUsername = string.Empty;
|
||||
private string _sheepItRenderKey = string.Empty;
|
||||
private bool _isRenderKeyVisible = false;
|
||||
private string _flamencoWorkerPath = string.Empty;
|
||||
|
||||
public GlobalSettingsViewModel(ConfigService configService)
|
||||
{
|
||||
_configService = configService;
|
||||
LoadSettings();
|
||||
}
|
||||
|
||||
public string SheepItUsername
|
||||
{
|
||||
get => _sheepItUsername;
|
||||
set => SetAndRaise(ref _sheepItUsername, value);
|
||||
}
|
||||
|
||||
public string SheepItRenderKey
|
||||
{
|
||||
get => _sheepItRenderKey;
|
||||
set => SetAndRaise(ref _sheepItRenderKey, value);
|
||||
}
|
||||
|
||||
public bool IsRenderKeyVisible
|
||||
{
|
||||
get => _isRenderKeyVisible;
|
||||
set => SetAndRaise(ref _isRenderKeyVisible, value);
|
||||
}
|
||||
|
||||
public string FlamencoWorkerPath
|
||||
{
|
||||
get => _flamencoWorkerPath;
|
||||
set => SetAndRaise(ref _flamencoWorkerPath, value);
|
||||
}
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
var config = _configService.Load();
|
||||
SheepItUsername = config.GlobalSettings.SheepItUsername;
|
||||
SheepItRenderKey = config.GlobalSettings.SheepItRenderKey;
|
||||
FlamencoWorkerPath = config.GlobalSettings.FlamencoWorkerPath;
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
var config = _configService.Load();
|
||||
config.GlobalSettings.SheepItUsername = SheepItUsername;
|
||||
config.GlobalSettings.SheepItRenderKey = SheepItRenderKey;
|
||||
config.GlobalSettings.FlamencoWorkerPath = FlamencoWorkerPath;
|
||||
_configService.Save(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ namespace UnifiedFarmLauncher.ViewModels
|
||||
private WorkerConfig? _selectedWorker;
|
||||
private string _statusText = "Ready";
|
||||
private string _selectedWorkerType = "All";
|
||||
private string _operationMode = "sheepit"; // "sheepit" or "flamenco"
|
||||
|
||||
public MainWindowViewModel()
|
||||
{
|
||||
@@ -52,8 +53,27 @@ 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()
|
||||
{
|
||||
_configService.Reload();
|
||||
var config = _configService.Load();
|
||||
Workers.Clear();
|
||||
|
||||
|
||||
@@ -16,9 +16,6 @@ namespace UnifiedFarmLauncher.ViewModels
|
||||
private int _sshPort = 22;
|
||||
private string _sshArgs = string.Empty;
|
||||
private string _sheepItGpu = "OPTIX_0";
|
||||
private string _sheepItUsername = string.Empty;
|
||||
private string _sheepItRenderKey = string.Empty;
|
||||
private string _flamencoWorkerPath = string.Empty;
|
||||
private bool _hasSheepIt;
|
||||
private bool _hasFlamenco;
|
||||
|
||||
@@ -93,24 +90,6 @@ namespace UnifiedFarmLauncher.ViewModels
|
||||
set => SetAndRaise(ref _sheepItGpu, value);
|
||||
}
|
||||
|
||||
public string SheepItUsername
|
||||
{
|
||||
get => _sheepItUsername;
|
||||
set => SetAndRaise(ref _sheepItUsername, value);
|
||||
}
|
||||
|
||||
public string SheepItRenderKey
|
||||
{
|
||||
get => _sheepItRenderKey;
|
||||
set => SetAndRaise(ref _sheepItRenderKey, value);
|
||||
}
|
||||
|
||||
public string FlamencoWorkerPath
|
||||
{
|
||||
get => _flamencoWorkerPath;
|
||||
set => SetAndRaise(ref _flamencoWorkerPath, value);
|
||||
}
|
||||
|
||||
public ObservableCollection<string> NetworkDrives { get; }
|
||||
public ObservableCollection<string> NetworkPaths { get; }
|
||||
|
||||
@@ -126,15 +105,14 @@ namespace UnifiedFarmLauncher.ViewModels
|
||||
if (worker.WorkerTypes.SheepIt != null)
|
||||
{
|
||||
HasSheepIt = true;
|
||||
SheepItGpu = worker.WorkerTypes.SheepIt.Gpu;
|
||||
SheepItUsername = worker.WorkerTypes.SheepIt.Username;
|
||||
SheepItRenderKey = worker.WorkerTypes.SheepIt.RenderKey;
|
||||
SheepItGpu = string.IsNullOrEmpty(worker.WorkerTypes.SheepIt.Gpu)
|
||||
? "OPTIX_0"
|
||||
: worker.WorkerTypes.SheepIt.Gpu;
|
||||
}
|
||||
|
||||
if (worker.WorkerTypes.Flamenco != null)
|
||||
{
|
||||
HasFlamenco = true;
|
||||
FlamencoWorkerPath = worker.WorkerTypes.Flamenco.WorkerPath;
|
||||
NetworkDrives.Clear();
|
||||
foreach (var drive in worker.WorkerTypes.Flamenco.NetworkDrives)
|
||||
{
|
||||
@@ -168,9 +146,7 @@ namespace UnifiedFarmLauncher.ViewModels
|
||||
{
|
||||
worker.WorkerTypes.SheepIt = new SheepItConfig
|
||||
{
|
||||
Gpu = SheepItGpu,
|
||||
Username = SheepItUsername,
|
||||
RenderKey = SheepItRenderKey
|
||||
Gpu = SheepItGpu
|
||||
};
|
||||
}
|
||||
|
||||
@@ -178,7 +154,6 @@ namespace UnifiedFarmLauncher.ViewModels
|
||||
{
|
||||
worker.WorkerTypes.Flamenco = new FlamencoConfig
|
||||
{
|
||||
WorkerPath = FlamencoWorkerPath,
|
||||
NetworkDrives = NetworkDrives.ToList(),
|
||||
NetworkPaths = NetworkPaths.ToList()
|
||||
};
|
||||
|
||||
37
Views/ErrorDialog.axaml
Normal file
37
Views/ErrorDialog.axaml
Normal file
@@ -0,0 +1,37 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="UnifiedFarmLauncher.Views.ErrorDialog"
|
||||
Title="Error"
|
||||
Width="700"
|
||||
Height="500"
|
||||
MinWidth="500"
|
||||
MinHeight="300"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
<Grid RowDefinitions="Auto,*,Auto" Margin="15">
|
||||
<!-- Header with icon and title -->
|
||||
<StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,15">
|
||||
<TextBlock Text="❌" FontSize="24" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||
<TextBlock Text="Error" FontSize="18" FontWeight="Bold" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Scrollable error content -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<Border>
|
||||
<TextBlock x:Name="ErrorTextBlock"
|
||||
TextWrapping="Wrap"
|
||||
FontFamily="Consolas, Courier New, monospace"
|
||||
FontSize="12"/>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- OK Button -->
|
||||
<Button Grid.Row="2"
|
||||
Content="OK"
|
||||
HorizontalAlignment="Right"
|
||||
Width="80"
|
||||
Margin="0,15,0,0"
|
||||
Click="OkButton_Click"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
23
Views/ErrorDialog.axaml.cs
Normal file
23
Views/ErrorDialog.axaml.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace UnifiedFarmLauncher.Views
|
||||
{
|
||||
public partial class ErrorDialog : Window
|
||||
{
|
||||
public ErrorDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public ErrorDialog(string errorMessage) : this()
|
||||
{
|
||||
this.FindControl<TextBlock>("ErrorTextBlock")!.Text = errorMessage;
|
||||
}
|
||||
|
||||
private void OkButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
55
Views/GlobalSettingsWindow.axaml
Normal file
55
Views/GlobalSettingsWindow.axaml
Normal file
@@ -0,0 +1,55 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:UnifiedFarmLauncher.ViewModels"
|
||||
x:Class="UnifiedFarmLauncher.Views.GlobalSettingsWindow"
|
||||
x:DataType="vm:GlobalSettingsViewModel"
|
||||
Title="Global Settings"
|
||||
Width="500" Height="350"
|
||||
MinWidth="400" MinHeight="300">
|
||||
<Grid RowDefinitions="Auto,*,Auto" Margin="10">
|
||||
<!-- Settings Content -->
|
||||
<TabControl Grid.Row="1" Margin="0,10">
|
||||
<TabItem Header="SheepIt">
|
||||
<StackPanel Margin="10" Spacing="10">
|
||||
<TextBlock Text="Username:"/>
|
||||
<TextBox Name="SheepItUsernameTextBox" Text="{Binding SheepItUsername}"/>
|
||||
|
||||
<TextBlock Text="Render Key:" Margin="0,10,0,0"/>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBox Name="SheepItRenderKeyTextBox"
|
||||
Grid.Column="0"
|
||||
Text="{Binding SheepItRenderKey}"
|
||||
PasswordChar="*"
|
||||
Margin="0,0,5,0"/>
|
||||
<Button Name="ToggleRenderKeyButton"
|
||||
Grid.Column="1"
|
||||
Content="👁"
|
||||
Width="40"
|
||||
ToolTip.Tip="Show/Hide Render Key"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
<TabItem Header="Flamenco">
|
||||
<StackPanel Margin="10" Spacing="10">
|
||||
<TextBlock Text="Worker Path:"/>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBox Name="FlamencoWorkerPathTextBox"
|
||||
Grid.Column="0"
|
||||
Text="{Binding FlamencoWorkerPath}"
|
||||
Margin="0,0,5,0"/>
|
||||
<Button Name="BrowseFlamencoPathButton"
|
||||
Grid.Column="1"
|
||||
Content="Browse..."
|
||||
Width="80"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
|
||||
<!-- Buttons -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row="2" Spacing="10" Margin="0,10,0,0">
|
||||
<Button Name="OkButton" Content="OK" Width="80" IsDefault="True"/>
|
||||
<Button Name="CancelButton" Content="Cancel" Width="80" IsCancel="True"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
96
Views/GlobalSettingsWindow.axaml.cs
Normal file
96
Views/GlobalSettingsWindow.axaml.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using UnifiedFarmLauncher.Services;
|
||||
using UnifiedFarmLauncher.ViewModels;
|
||||
|
||||
namespace UnifiedFarmLauncher.Views
|
||||
{
|
||||
public partial class GlobalSettingsWindow : Window
|
||||
{
|
||||
private readonly GlobalSettingsViewModel _viewModel;
|
||||
private bool _result;
|
||||
|
||||
public GlobalSettingsWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
var configService = new ConfigService();
|
||||
_viewModel = new GlobalSettingsViewModel(configService);
|
||||
DataContext = _viewModel;
|
||||
SetupEventHandlers();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
Avalonia.Markup.Xaml.AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private void SetupEventHandlers()
|
||||
{
|
||||
this.FindControl<Button>("OkButton")!.Click += OkButton_Click;
|
||||
this.FindControl<Button>("CancelButton")!.Click += CancelButton_Click;
|
||||
this.FindControl<Button>("ToggleRenderKeyButton")!.Click += ToggleRenderKeyButton_Click;
|
||||
this.FindControl<Button>("BrowseFlamencoPathButton")!.Click += BrowseFlamencoPathButton_Click;
|
||||
}
|
||||
|
||||
private void OkButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_viewModel.Save();
|
||||
_result = true;
|
||||
Close();
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
// Could show error message here
|
||||
System.Diagnostics.Debug.WriteLine($"Failed to save global settings: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_result = false;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void ToggleRenderKeyButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var textBox = this.FindControl<Avalonia.Controls.TextBox>("SheepItRenderKeyTextBox");
|
||||
if (textBox != null)
|
||||
{
|
||||
_viewModel.IsRenderKeyVisible = !_viewModel.IsRenderKeyVisible;
|
||||
textBox.PasswordChar = _viewModel.IsRenderKeyVisible ? '\0' : '*';
|
||||
var button = sender as Button;
|
||||
if (button != null)
|
||||
{
|
||||
button.Content = _viewModel.IsRenderKeyVisible ? "🙈" : "👁";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void BrowseFlamencoPathButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel?.StorageProvider.CanPickFolder == true)
|
||||
{
|
||||
var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = "Select Flamenco Worker Path"
|
||||
});
|
||||
|
||||
if (folders.Count > 0 && folders[0].TryGetLocalPath() is { } localPath)
|
||||
{
|
||||
_viewModel.FlamencoWorkerPath = localPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ShowDialogAsync(Window parent)
|
||||
{
|
||||
await base.ShowDialog(parent);
|
||||
return _result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,44 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:UnifiedFarmLauncher.ViewModels"
|
||||
xmlns:models="using:UnifiedFarmLauncher.Models"
|
||||
x:Class="UnifiedFarmLauncher.Views.MainWindow"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
Title="Unified Farm Launcher"
|
||||
Width="1000" Height="700"
|
||||
MinWidth="800" MinHeight="600">
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
||||
<!-- Toolbar -->
|
||||
<StackPanel Orientation="Horizontal" Margin="5" Grid.Row="0">
|
||||
<Button Name="AddWorkerButton" Content="Add 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"/>
|
||||
<Separator Margin="10,0"/>
|
||||
<Button Name="StartWorkerButton" Content="Start" Margin="5" Width="80"/>
|
||||
<Button Name="StopWorkerButton" Content="Stop" Margin="5" Width="80"/>
|
||||
<Button Name="AttachWorkerButton" Content="Attach" Margin="5" Width="80"/>
|
||||
</StackPanel>
|
||||
<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="EditWorkerButton" Content="Edit Worker" Margin="5" Width="120"/>
|
||||
<Button Name="DeleteWorkerButton" Content="Delete Worker" Margin="5" Width="120"/>
|
||||
<Separator Margin="10,0"/>
|
||||
<Button Name="StartWorkerButton" Content="Start" Margin="5" Width="80"/>
|
||||
<Button Name="StopWorkerButton" Content="Stop" Margin="5" Width="80"/>
|
||||
<Button Name="AttachWorkerButton" Content="Attach" Margin="5" Width="80"/>
|
||||
<Separator Margin="10,0"/>
|
||||
<Button Name="SettingsButton" Content="Settings" Margin="5" Width="80"/>
|
||||
</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 -->
|
||||
<TabControl Name="WorkerTypeTabs" Grid.Row="1" Margin="5,0">
|
||||
@@ -34,7 +58,8 @@
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
SelectionMode="Single"
|
||||
GridLinesVisibility="All">
|
||||
GridLinesVisibility="All"
|
||||
ItemsSource="{Binding Workers}">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="150"/>
|
||||
<DataGridCheckBoxColumn Header="Enabled" Binding="{Binding Enabled}" Width="80"/>
|
||||
@@ -45,9 +70,9 @@
|
||||
</DataGrid>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<StatusBar Grid.Row="3">
|
||||
<TextBlock Name="StatusText" Text="{Binding StatusText}" Margin="5"/>
|
||||
</StatusBar>
|
||||
<Border Grid.Row="3" Background="{DynamicResource ThemeControlLowBackgroundBrush}" BorderBrush="{DynamicResource ThemeBorderLowBrush}" BorderThickness="0,1,0,0">
|
||||
<TextBlock Name="StatusText" Text="{Binding StatusText}" Margin="5" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
|
||||
@@ -33,14 +33,25 @@ namespace UnifiedFarmLauncher.Views
|
||||
|
||||
private void SetupEventHandlers()
|
||||
{
|
||||
AddWorkerButton.Click += AddWorkerButton_Click;
|
||||
EditWorkerButton.Click += EditWorkerButton_Click;
|
||||
DeleteWorkerButton.Click += DeleteWorkerButton_Click;
|
||||
StartWorkerButton.Click += StartWorkerButton_Click;
|
||||
StopWorkerButton.Click += StopWorkerButton_Click;
|
||||
AttachWorkerButton.Click += AttachWorkerButton_Click;
|
||||
WorkerTypeTabs.SelectionChanged += WorkerTypeTabs_SelectionChanged;
|
||||
WorkersGrid.SelectionChanged += WorkersGrid_SelectionChanged;
|
||||
this.FindControl<Button>("AddWorkerButton")!.Click += AddWorkerButton_Click;
|
||||
this.FindControl<Button>("EditWorkerButton")!.Click += EditWorkerButton_Click;
|
||||
this.FindControl<Button>("DeleteWorkerButton")!.Click += DeleteWorkerButton_Click;
|
||||
this.FindControl<Button>("StartWorkerButton")!.Click += StartWorkerButton_Click;
|
||||
this.FindControl<Button>("StopWorkerButton")!.Click += StopWorkerButton_Click;
|
||||
this.FindControl<Button>("AttachWorkerButton")!.Click += AttachWorkerButton_Click;
|
||||
this.FindControl<Button>("SettingsButton")!.Click += SettingsButton_Click;
|
||||
this.FindControl<Button>("OperationModeToggleButton")!.Click += OperationModeToggleButton_Click;
|
||||
this.FindControl<TabControl>("WorkerTypeTabs")!.SelectionChanged += WorkerTypeTabs_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)
|
||||
@@ -54,7 +65,7 @@ namespace UnifiedFarmLauncher.Views
|
||||
|
||||
private async void EditWorkerButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (WorkersGrid.SelectedItem is WorkerConfig worker)
|
||||
if (this.FindControl<DataGrid>("WorkersGrid")?.SelectedItem is WorkerConfig worker)
|
||||
{
|
||||
var dialog = new WorkerEditWindow(worker);
|
||||
if (await dialog.ShowDialogAsync(this))
|
||||
@@ -66,11 +77,11 @@ namespace UnifiedFarmLauncher.Views
|
||||
|
||||
private async void DeleteWorkerButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (WorkersGrid.SelectedItem is WorkerConfig worker)
|
||||
if (this.FindControl<DataGrid>("WorkersGrid")?.SelectedItem is WorkerConfig worker)
|
||||
{
|
||||
var box = MessageBoxManager.GetMessageBoxStandard("Delete Worker",
|
||||
$"Are you sure you want to delete worker '{worker.Name}'?",
|
||||
ButtonEnum.YesNo, Icon.Warning);
|
||||
ButtonEnum.YesNo, MsBox.Avalonia.Enums.Icon.Warning);
|
||||
var result = await box.ShowAsync();
|
||||
|
||||
if (result == ButtonResult.Yes)
|
||||
@@ -83,96 +94,110 @@ namespace UnifiedFarmLauncher.Views
|
||||
|
||||
private async void StartWorkerButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (WorkersGrid.SelectedItem is WorkerConfig worker)
|
||||
if (this.FindControl<DataGrid>("WorkersGrid")?.SelectedItem is WorkerConfig worker)
|
||||
{
|
||||
try
|
||||
{
|
||||
string? workerType = null;
|
||||
if (worker.WorkerTypes.SheepIt != null)
|
||||
workerType = "sheepit";
|
||||
else if (worker.WorkerTypes.Flamenco != null)
|
||||
workerType = "flamenco";
|
||||
var vm = (MainWindowViewModel)DataContext!;
|
||||
string? workerType = vm.OperationMode;
|
||||
|
||||
if (workerType == null)
|
||||
// Verify the worker supports the selected operation mode
|
||||
if (workerType == "sheepit" && worker.WorkerTypes.SheepIt == null)
|
||||
{
|
||||
var box = MessageBoxManager.GetMessageBoxStandard("Error",
|
||||
"Worker has no configured worker type.",
|
||||
ButtonEnum.Ok, Icon.Error);
|
||||
$"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);
|
||||
await box.ShowAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await _controllerService.StartWorkerAsync(worker, workerType);
|
||||
var successBox = MessageBoxManager.GetMessageBoxStandard("Start Worker",
|
||||
$"Worker '{worker.Name}' started successfully.",
|
||||
ButtonEnum.Ok, Icon.Success);
|
||||
$"Worker '{worker.Name}' ({vm.OperationModeDisplayName}) started successfully.",
|
||||
ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Success);
|
||||
await successBox.ShowAsync();
|
||||
((MainWindowViewModel)DataContext!).RefreshWorkers();
|
||||
vm.RefreshWorkers();
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
var errorBox = MessageBoxManager.GetMessageBoxStandard("Error",
|
||||
$"Failed to start worker: {ex.Message}",
|
||||
ButtonEnum.Ok, Icon.Error);
|
||||
await errorBox.ShowAsync();
|
||||
var errorDialog = new ErrorDialog($"Failed to start worker: {ex.Message}");
|
||||
await errorDialog.ShowDialog(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void StopWorkerButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (WorkersGrid.SelectedItem is WorkerConfig worker)
|
||||
if (this.FindControl<DataGrid>("WorkersGrid")?.SelectedItem is WorkerConfig worker)
|
||||
{
|
||||
try
|
||||
{
|
||||
string? workerType = null;
|
||||
if (worker.WorkerTypes.SheepIt != null)
|
||||
workerType = "sheepit";
|
||||
else if (worker.WorkerTypes.Flamenco != null)
|
||||
workerType = "flamenco";
|
||||
var vm = (MainWindowViewModel)DataContext!;
|
||||
string workerType = vm.OperationMode;
|
||||
|
||||
if (workerType == null)
|
||||
// Verify the worker supports the selected operation mode
|
||||
if (workerType == "sheepit" && worker.WorkerTypes.SheepIt == null)
|
||||
{
|
||||
var box = MessageBoxManager.GetMessageBoxStandard("Error",
|
||||
"Worker has no configured worker type.",
|
||||
ButtonEnum.Ok, Icon.Error);
|
||||
$"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);
|
||||
await box.ShowAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await _controllerService.StopWorkerAsync(worker, workerType);
|
||||
var successBox = MessageBoxManager.GetMessageBoxStandard("Stop Worker",
|
||||
$"Stop command sent to worker '{worker.Name}'.",
|
||||
ButtonEnum.Ok, Icon.Info);
|
||||
$"Stop command sent to worker '{worker.Name}' ({vm.OperationModeDisplayName}).",
|
||||
ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Info);
|
||||
await successBox.ShowAsync();
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
var errorBox = MessageBoxManager.GetMessageBoxStandard("Error",
|
||||
$"Failed to stop worker: {ex.Message}",
|
||||
ButtonEnum.Ok, Icon.Error);
|
||||
await errorBox.ShowAsync();
|
||||
var errorDialog = new ErrorDialog($"Failed to stop worker: {ex.Message}");
|
||||
await errorDialog.ShowDialog(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void AttachWorkerButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (WorkersGrid.SelectedItem is WorkerConfig worker)
|
||||
if (this.FindControl<DataGrid>("WorkersGrid")?.SelectedItem is WorkerConfig worker)
|
||||
{
|
||||
try
|
||||
{
|
||||
string? workerType = null;
|
||||
if (worker.WorkerTypes.SheepIt != null)
|
||||
workerType = "sheepit";
|
||||
else if (worker.WorkerTypes.Flamenco != null)
|
||||
workerType = "flamenco";
|
||||
var vm = (MainWindowViewModel)DataContext!;
|
||||
string workerType = vm.OperationMode;
|
||||
|
||||
if (workerType == null)
|
||||
// Verify the worker supports the selected operation mode
|
||||
if (workerType == "sheepit" && worker.WorkerTypes.SheepIt == null)
|
||||
{
|
||||
var box = MessageBoxManager.GetMessageBoxStandard("Error",
|
||||
"Worker has no configured worker type.",
|
||||
ButtonEnum.Ok, Icon.Error);
|
||||
$"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);
|
||||
await box.ShowAsync();
|
||||
return;
|
||||
}
|
||||
@@ -181,17 +206,15 @@ namespace UnifiedFarmLauncher.Views
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
var errorBox = MessageBoxManager.GetMessageBoxStandard("Error",
|
||||
$"Failed to attach to worker: {ex.Message}",
|
||||
ButtonEnum.Ok, Icon.Error);
|
||||
await errorBox.ShowAsync();
|
||||
var errorDialog = new ErrorDialog($"Failed to attach to worker: {ex.Message}");
|
||||
await errorDialog.ShowDialog(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WorkerTypeTabs_SelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (WorkerTypeTabs.SelectedItem is TabItem tab)
|
||||
if (this.FindControl<TabControl>("WorkerTypeTabs")?.SelectedItem is TabItem tab)
|
||||
{
|
||||
var type = tab.Header?.ToString() ?? "All";
|
||||
if (type == "All Workers") type = "All";
|
||||
@@ -199,11 +222,17 @@ namespace UnifiedFarmLauncher.Views
|
||||
}
|
||||
}
|
||||
|
||||
private async void SettingsButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var dialog = new GlobalSettingsWindow();
|
||||
await dialog.ShowDialogAsync(this);
|
||||
}
|
||||
|
||||
private void WorkersGrid_SelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (DataContext is MainWindowViewModel vm)
|
||||
{
|
||||
vm.SelectedWorker = WorkersGrid.SelectedItem as WorkerConfig;
|
||||
vm.SelectedWorker = this.FindControl<DataGrid>("WorkersGrid")?.SelectedItem as WorkerConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:UnifiedFarmLauncher.ViewModels"
|
||||
x:Class="UnifiedFarmLauncher.Views.WorkerEditWindow"
|
||||
x:DataType="vm:WorkerEditViewModel"
|
||||
Title="Edit Worker"
|
||||
Width="600" Height="700"
|
||||
MinWidth="500" MinHeight="600">
|
||||
@@ -34,17 +36,17 @@
|
||||
<TextBlock Text="GPU:" IsVisible="{Binding HasSheepIt}"/>
|
||||
<ComboBox Name="GpuComboBox" IsVisible="{Binding HasSheepIt}" SelectedItem="{Binding SheepItGpu}">
|
||||
<ComboBox.Items>
|
||||
<ComboBoxItem Content="OPTIX_0"/>
|
||||
<ComboBoxItem Content="CUDA_0"/>
|
||||
<ComboBoxItem Content="OPENCL_0"/>
|
||||
<x:String>OPTIX_0</x:String>
|
||||
<x:String>CUDA_0</x:String>
|
||||
<x:String>OPENCL_0</x:String>
|
||||
</ComboBox.Items>
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Text="Username:" IsVisible="{Binding HasSheepIt}" Margin="0,10,0,0"/>
|
||||
<TextBox Name="SheepItUsernameTextBox" Text="{Binding SheepItUsername}" IsVisible="{Binding HasSheepIt}"/>
|
||||
|
||||
<TextBlock Text="Render Key:" IsVisible="{Binding HasSheepIt}" Margin="0,10,0,0"/>
|
||||
<TextBox Name="SheepItRenderKeyTextBox" Text="{Binding SheepItRenderKey}" IsVisible="{Binding HasSheepIt}" PasswordChar="*"/>
|
||||
<TextBlock Text="Note: Username and Render Key are configured in Global Settings."
|
||||
IsVisible="{Binding HasSheepIt}"
|
||||
Margin="0,10,0,0"
|
||||
Foreground="Gray"
|
||||
TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
|
||||
@@ -53,13 +55,13 @@
|
||||
<StackPanel Margin="10" Spacing="10">
|
||||
<CheckBox Name="HasFlamencoCheckBox" Content="Enable Flamenco Worker" IsChecked="{Binding HasFlamenco}" Margin="0,0,0,10"/>
|
||||
|
||||
<TextBlock Text="Worker Path:" IsVisible="{Binding HasFlamenco}"/>
|
||||
<Grid IsVisible="{Binding HasFlamenco}" ColumnDefinitions="*,Auto">
|
||||
<TextBox Name="FlamencoPathTextBox" Grid.Column="0" Text="{Binding FlamencoWorkerPath}" Margin="0,0,5,0"/>
|
||||
<Button Name="BrowseFlamencoPathButton" Grid.Column="1" Content="Browse..." Width="80"/>
|
||||
</Grid>
|
||||
<TextBlock Text="Note: Worker Path is configured in Global Settings."
|
||||
IsVisible="{Binding HasFlamenco}"
|
||||
Margin="0,0,0,10"
|
||||
Foreground="Gray"
|
||||
TextWrapping="Wrap"/>
|
||||
|
||||
<TextBlock Text="Network Drives:" IsVisible="{Binding HasFlamenco}" Margin="0,10,0,0"/>
|
||||
<TextBlock Text="Network Drives:" IsVisible="{Binding HasFlamenco}"/>
|
||||
<Grid IsVisible="{Binding HasFlamenco}" RowDefinitions="*,Auto" Margin="0,5,0,0">
|
||||
<ListBox Name="NetworkDrivesListBox" Grid.Row="0" ItemsSource="{Binding NetworkDrives}" MaxHeight="100"/>
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,5,0,0">
|
||||
|
||||
@@ -17,7 +17,11 @@ namespace UnifiedFarmLauncher.Views
|
||||
private readonly WorkerEditViewModel _viewModel;
|
||||
private bool _result;
|
||||
|
||||
public WorkerEditWindow(WorkerConfig? worker = null)
|
||||
public WorkerEditWindow() : this(null)
|
||||
{
|
||||
}
|
||||
|
||||
public WorkerEditWindow(WorkerConfig? worker)
|
||||
{
|
||||
InitializeComponent();
|
||||
var configService = new ConfigService();
|
||||
@@ -33,13 +37,12 @@ namespace UnifiedFarmLauncher.Views
|
||||
|
||||
private void SetupEventHandlers()
|
||||
{
|
||||
OkButton.Click += OkButton_Click;
|
||||
CancelButton.Click += CancelButton_Click;
|
||||
BrowseFlamencoPathButton.Click += BrowseFlamencoPathButton_Click;
|
||||
AddDriveButton.Click += AddDriveButton_Click;
|
||||
RemoveDriveButton.Click += RemoveDriveButton_Click;
|
||||
AddPathButton.Click += AddPathButton_Click;
|
||||
RemovePathButton.Click += RemovePathButton_Click;
|
||||
this.FindControl<Button>("OkButton")!.Click += OkButton_Click;
|
||||
this.FindControl<Button>("CancelButton")!.Click += CancelButton_Click;
|
||||
this.FindControl<Button>("AddDriveButton")!.Click += AddDriveButton_Click;
|
||||
this.FindControl<Button>("RemoveDriveButton")!.Click += RemoveDriveButton_Click;
|
||||
this.FindControl<Button>("AddPathButton")!.Click += AddPathButton_Click;
|
||||
this.FindControl<Button>("RemovePathButton")!.Click += RemovePathButton_Click;
|
||||
}
|
||||
|
||||
private async void OkButton_Click(object? sender, RoutedEventArgs e)
|
||||
@@ -48,7 +51,7 @@ namespace UnifiedFarmLauncher.Views
|
||||
{
|
||||
var box = MessageBoxManager.GetMessageBoxStandard("Error",
|
||||
"Worker name is required.",
|
||||
ButtonEnum.Ok, Icon.Error);
|
||||
ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Error);
|
||||
await box.ShowAsync();
|
||||
return;
|
||||
}
|
||||
@@ -63,7 +66,7 @@ namespace UnifiedFarmLauncher.Views
|
||||
{
|
||||
var errorBox = MessageBoxManager.GetMessageBoxStandard("Error",
|
||||
$"Failed to save worker: {ex.Message}",
|
||||
ButtonEnum.Ok, Icon.Error);
|
||||
ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Error);
|
||||
await errorBox.ShowAsync();
|
||||
}
|
||||
}
|
||||
@@ -74,24 +77,7 @@ namespace UnifiedFarmLauncher.Views
|
||||
Close();
|
||||
}
|
||||
|
||||
private async void BrowseFlamencoPathButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel?.StorageProvider.CanPickFolder == true)
|
||||
{
|
||||
var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = "Select Flamenco Worker Path"
|
||||
});
|
||||
|
||||
if (folders.Count > 0 && folders[0].TryGetLocalPath() is { } localPath)
|
||||
{
|
||||
_viewModel.FlamencoWorkerPath = localPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void AddDriveButton_Click(object? sender, RoutedEventArgs e)
|
||||
private void AddDriveButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
// Simplified: use a simple input box
|
||||
// In a full implementation, you'd use a proper input dialog
|
||||
@@ -100,13 +86,13 @@ namespace UnifiedFarmLauncher.Views
|
||||
|
||||
private void RemoveDriveButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (NetworkDrivesListBox.SelectedItem is string drive)
|
||||
if (this.FindControl<ListBox>("NetworkDrivesListBox")?.SelectedItem is string drive)
|
||||
{
|
||||
_viewModel.NetworkDrives.Remove(drive);
|
||||
}
|
||||
}
|
||||
|
||||
private async void AddPathButton_Click(object? sender, RoutedEventArgs e)
|
||||
private void AddPathButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
// Simplified: use a simple input box
|
||||
// In a full implementation, you'd use a proper input dialog
|
||||
@@ -115,7 +101,7 @@ namespace UnifiedFarmLauncher.Views
|
||||
|
||||
private void RemovePathButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (NetworkPathsListBox.SelectedItem is string path)
|
||||
if (this.FindControl<ListBox>("NetworkPathsListBox")?.SelectedItem is string path)
|
||||
{
|
||||
_viewModel.NetworkPaths.Remove(path);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user