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 explanation file
|
||||||
.specstory/.what-is-this.md
|
.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">
|
RequestedThemeVariant="Default">
|
||||||
<Application.Styles>
|
<Application.Styles>
|
||||||
<FluentTheme />
|
<FluentTheme />
|
||||||
</Application.Styles>
|
|
||||||
<Application.Resources>
|
|
||||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
||||||
</Application.Resources>
|
</Application.Styles>
|
||||||
</Application>
|
</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.php",
|
||||||
"https://www.sheepit-renderfarm.com/media/applet/client-latest.jar"
|
"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")]
|
[JsonPropertyName("gpu")]
|
||||||
public string Gpu { get; set; } = string.Empty;
|
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
|
public class FlamencoConfig
|
||||||
{
|
{
|
||||||
[JsonPropertyName("workerPath")]
|
|
||||||
public string WorkerPath { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[JsonPropertyName("networkDrives")]
|
[JsonPropertyName("networkDrives")]
|
||||||
public List<string> NetworkDrives { get; set; } = new();
|
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(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)]
|
||||||
[string]$WorkerName,
|
[string]$WorkerName,
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)]
|
||||||
[string]$WorkerType,
|
[string]$WorkerType,
|
||||||
|
|
||||||
|
[Parameter(ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)]
|
||||||
[string]$DataRoot = (Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'UnifiedWorkers'),
|
[string]$DataRoot = (Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'UnifiedWorkers'),
|
||||||
|
|
||||||
|
[Parameter(ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)]
|
||||||
[switch]$CommandOnly,
|
[switch]$CommandOnly,
|
||||||
|
|
||||||
|
[Parameter(ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)]
|
||||||
[string]$Command
|
[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 {
|
function Get-WorkerPaths {
|
||||||
param([string]$Root, [string]$Type, [string]$Name)
|
param([string]$Root, [string]$Type, [string]$Name)
|
||||||
@@ -28,7 +62,17 @@ function Get-WorkerPaths {
|
|||||||
$paths = Get-WorkerPaths -Root $DataRoot -Type $WorkerType -Name $WorkerName
|
$paths = Get-WorkerPaths -Root $DataRoot -Type $WorkerType -Name $WorkerName
|
||||||
|
|
||||||
if (-not (Test-Path $paths.Metadata)) {
|
if (-not (Test-Path $paths.Metadata)) {
|
||||||
Write-Host "No worker metadata found for $WorkerName ($WorkerType)." -ForegroundColor Red
|
Write-Host ""
|
||||||
|
Write-Host "ERROR: No worker metadata found for $WorkerName ($WorkerType)." -ForegroundColor Red
|
||||||
|
Write-Host "Metadata path: $($paths.Metadata)" -ForegroundColor Gray
|
||||||
|
Write-Host "Data root: $DataRoot" -ForegroundColor Gray
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Press any key to exit..."
|
||||||
|
try {
|
||||||
|
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
||||||
|
} catch {
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
}
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,16 +117,58 @@ if ($CommandOnly) {
|
|||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
Write-Host "Attaching to $WorkerName ($WorkerType) logs." -ForegroundColor Cyan
|
Write-Host "Attaching to $WorkerName ($WorkerType) logs." -ForegroundColor Cyan
|
||||||
Write-Host "Type commands and press Enter. Type 'detach' to exit session." -ForegroundColor Yellow
|
Write-Host "Log file: $($paths.Log)" -ForegroundColor Gray
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
[Console]::Out.Flush()
|
||||||
|
[Console]::Error.WriteLine("Attach session initialized")
|
||||||
|
|
||||||
|
# Show initial log content if available
|
||||||
|
if (Test-Path $paths.Log) {
|
||||||
|
$initialLines = Get-Content -Path $paths.Log -Tail 50 -ErrorAction SilentlyContinue
|
||||||
|
if ($initialLines) {
|
||||||
|
Write-Host "--- Recent log output ---" -ForegroundColor DarkGray
|
||||||
|
$initialLines | ForEach-Object { Write-Host $_ }
|
||||||
|
Write-Host "--- End of initial output ---" -ForegroundColor DarkGray
|
||||||
|
} else {
|
||||||
|
Write-Host "Log file exists but is empty. Waiting for worker output..." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "Log file does not exist yet. Waiting for worker to start..." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Type commands and press Enter. Type 'detach' to exit session." -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Use Register-ObjectEvent for file system watcher, or simpler: just tail in background and poll
|
||||||
$logJob = Start-Job -ScriptBlock {
|
$logJob = Start-Job -ScriptBlock {
|
||||||
param($LogPath)
|
param($LogPath)
|
||||||
Get-Content -Path $LogPath -Tail 50 -Wait
|
if (Test-Path $LogPath) {
|
||||||
|
Get-Content -Path $LogPath -Tail 0 -Wait -ErrorAction SilentlyContinue
|
||||||
|
} else {
|
||||||
|
while (-not (Test-Path $LogPath)) {
|
||||||
|
Start-Sleep -Milliseconds 500
|
||||||
|
}
|
||||||
|
Get-Content -Path $LogPath -Tail 0 -Wait -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
} -ArgumentList $paths.Log
|
} -ArgumentList $paths.Log
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while ($true) {
|
while ($true) {
|
||||||
|
# Check for new log output from background job (non-blocking)
|
||||||
|
$logOutput = Receive-Job -Job $logJob -ErrorAction SilentlyContinue
|
||||||
|
if ($logOutput) {
|
||||||
|
$logOutput | ForEach-Object { Write-Host $_ }
|
||||||
|
[Console]::Out.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use a timeout on Read-Host to allow periodic log checking
|
||||||
|
# Since Read-Host doesn't support timeout, we'll just check logs before each Read-Host
|
||||||
|
# This means logs will be checked whenever user presses Enter
|
||||||
|
|
||||||
$input = Read-Host "> "
|
$input = Read-Host "> "
|
||||||
if ($null -eq $input) {
|
if ($null -eq $input) {
|
||||||
continue
|
continue
|
||||||
@@ -104,10 +190,14 @@ try {
|
|||||||
Send-WorkerCommand -Value $input
|
Send-WorkerCommand -Value $input
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch {
|
||||||
|
Write-Host "Error in attach session: $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
Write-Host $_.ScriptStackTrace -ForegroundColor DarkRed
|
||||||
|
[Console]::Error.WriteLine("Attach error: $($_.Exception.Message)")
|
||||||
|
}
|
||||||
finally {
|
finally {
|
||||||
if ($logJob) {
|
if ($logJob) {
|
||||||
Stop-Job -Job $logJob -ErrorAction SilentlyContinue
|
Stop-Job -Job $logJob -ErrorAction SilentlyContinue
|
||||||
Receive-Job -Job $logJob -ErrorAction SilentlyContinue | Out-Null
|
|
||||||
Remove-Job -Job $logJob -ErrorAction SilentlyContinue
|
Remove-Job -Job $logJob -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -224,8 +224,24 @@ function Get-PendingCommands {
|
|||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
try {
|
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
|
# 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
|
$resolvedPayloadBase64 = Resolve-PayloadBase64
|
||||||
$PayloadBase64 = $resolvedPayloadBase64
|
$PayloadBase64 = $resolvedPayloadBase64
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using UnifiedFarmLauncher.Models;
|
using UnifiedFarmLauncher.Models;
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ namespace UnifiedFarmLauncher.Services
|
|||||||
await _controllerService.DeployAttachHelperAsync(worker);
|
await _controllerService.DeployAttachHelperAsync(worker);
|
||||||
|
|
||||||
var remoteBasePath = await _sshService.GetWorkerBasePathAsync(worker);
|
var remoteBasePath = await _sshService.GetWorkerBasePathAsync(worker);
|
||||||
var remoteHelper = $"{remoteBasePath.Replace("\\", "/")}/attach-helper.ps1";
|
var remoteHelper = Path.Combine(remoteBasePath, "attach-helper.ps1");
|
||||||
|
|
||||||
var paramsBlock = $"-WorkerName \"{worker.Name}\" -WorkerType \"{workerType}\"";
|
var paramsBlock = $"-WorkerName \"{worker.Name}\" -WorkerType \"{workerType}\"";
|
||||||
if (commandOnly)
|
if (commandOnly)
|
||||||
@@ -32,7 +33,11 @@ namespace UnifiedFarmLauncher.Services
|
|||||||
paramsBlock += $" -Command \"{command}\"";
|
paramsBlock += $" -Command \"{command}\"";
|
||||||
}
|
}
|
||||||
|
|
||||||
var remoteCmd = $"powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -File \"{remoteHelper}\" {paramsBlock}";
|
// Use Windows path format (backslashes) and ensure it's properly quoted
|
||||||
|
// Add -NoExit to keep window open and ensure output is visible
|
||||||
|
// 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);
|
_sshService.StartInteractiveSsh(worker, remoteCmd);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,12 +197,15 @@ namespace UnifiedFarmLauncher.Services
|
|||||||
var sshArgs = BuildSshArgs(parts, interactive);
|
var sshArgs = BuildSshArgs(parts, interactive);
|
||||||
sshArgs.Add(command);
|
sshArgs.Add(command);
|
||||||
|
|
||||||
|
var argsString = string.Join(" ", sshArgs.Select(arg => $"\"{arg.Replace("\"", "\\\"")}\""));
|
||||||
|
|
||||||
var process = new Process
|
var process = new Process
|
||||||
{
|
{
|
||||||
StartInfo = new ProcessStartInfo
|
StartInfo = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = GetSshExecutable(),
|
FileName = GetSshExecutable(),
|
||||||
Arguments = string.Join(" ", sshArgs.Select(arg => $"\"{arg.Replace("\"", "\\\"")}\"")),
|
WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
Arguments = argsString,
|
||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
RedirectStandardOutput = true,
|
RedirectStandardOutput = true,
|
||||||
RedirectStandardError = true,
|
RedirectStandardError = true,
|
||||||
@@ -230,6 +233,72 @@ namespace UnifiedFarmLauncher.Services
|
|||||||
return output.ToString();
|
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)
|
public async Task<string> GetWorkerBasePathAsync(WorkerConfig worker)
|
||||||
{
|
{
|
||||||
if (_workerBasePathCache.TryGetValue(worker.Name, out var cached))
|
if (_workerBasePathCache.TryGetValue(worker.Name, out var cached))
|
||||||
@@ -288,12 +357,15 @@ namespace UnifiedFarmLauncher.Services
|
|||||||
var sshArgs = BuildSshArgs(parts, true);
|
var sshArgs = BuildSshArgs(parts, true);
|
||||||
sshArgs.Add(command);
|
sshArgs.Add(command);
|
||||||
|
|
||||||
|
var argsString = string.Join(" ", sshArgs.Select(arg => $"\"{arg.Replace("\"", "\\\"")}\""));
|
||||||
|
|
||||||
var process = new Process
|
var process = new Process
|
||||||
{
|
{
|
||||||
StartInfo = new ProcessStartInfo
|
StartInfo = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = GetSshExecutable(),
|
FileName = GetSshExecutable(),
|
||||||
Arguments = string.Join(" ", sshArgs.Select(arg => $"\"{arg.Replace("\"", "\\\"")}\"")),
|
WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
Arguments = argsString,
|
||||||
UseShellExecute = true,
|
UseShellExecute = true,
|
||||||
CreateNoWindow = false
|
CreateNoWindow = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using UnifiedFarmLauncher.Models;
|
using UnifiedFarmLauncher.Models;
|
||||||
|
|
||||||
@@ -65,7 +68,7 @@ $controllerPath = Join-Path $dataRoot 'controller.ps1'
|
|||||||
[IO.File]::WriteAllBytes($controllerPath, [Convert]::FromBase64String('{controllerBase64}'))
|
[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)
|
public async Task DeployAttachHelperAsync(WorkerConfig worker)
|
||||||
@@ -79,7 +82,7 @@ $attachPath = Join-Path $dataRoot 'attach-helper.ps1'
|
|||||||
[IO.File]::WriteAllBytes($attachPath, [Convert]::FromBase64String('{helperBase64}'))
|
[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)
|
public string GenerateSheepItPayload(WorkerConfig worker)
|
||||||
@@ -89,9 +92,10 @@ $attachPath = Join-Path $dataRoot 'attach-helper.ps1'
|
|||||||
|
|
||||||
var config = _configService.Load();
|
var config = _configService.Load();
|
||||||
var sheepIt = worker.WorkerTypes.SheepIt;
|
var sheepIt = worker.WorkerTypes.SheepIt;
|
||||||
var safeKey = sheepIt.RenderKey.Replace("'", "''");
|
var globalSettings = config.GlobalSettings;
|
||||||
var safeUser = sheepIt.Username.Replace("'", "''");
|
var safeKey = globalSettings.SheepItRenderKey.Replace("'", "''");
|
||||||
var urls = config.GlobalSettings.SheepItJarUrls;
|
var safeUser = globalSettings.SheepItUsername.Replace("'", "''");
|
||||||
|
var urls = globalSettings.SheepItJarUrls;
|
||||||
var urlLiteral = "@(" + string.Join(", ", Array.ConvertAll(urls.ToArray(), url => $"'{url}'")) + ")";
|
var urlLiteral = "@(" + string.Join(", ", Array.ConvertAll(urls.ToArray(), url => $"'{url}'")) + ")";
|
||||||
|
|
||||||
return $@"
|
return $@"
|
||||||
@@ -170,7 +174,10 @@ catch {{
|
|||||||
if (worker.WorkerTypes.Flamenco == null)
|
if (worker.WorkerTypes.Flamenco == null)
|
||||||
throw new InvalidOperationException("Worker does not have Flamenco configuration");
|
throw new InvalidOperationException("Worker does not have Flamenco configuration");
|
||||||
|
|
||||||
|
var config = _configService.Load();
|
||||||
var flamenco = worker.WorkerTypes.Flamenco;
|
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 drives = string.Join(", ", Array.ConvertAll(flamenco.NetworkDrives.ToArray(), d => $"'{d}'"));
|
||||||
var paths = string.Join(", ", Array.ConvertAll(flamenco.NetworkPaths.ToArray(), p => $"'{p.Replace("\\", "\\\\")}'"));
|
var paths = string.Join(", ", Array.ConvertAll(flamenco.NetworkPaths.ToArray(), p => $"'{p.Replace("\\", "\\\\")}'"));
|
||||||
|
|
||||||
@@ -199,7 +206,7 @@ try {{
|
|||||||
|
|
||||||
# Start worker
|
# Start worker
|
||||||
Write-Host ""Starting Flamenco worker..."" -ForegroundColor Cyan
|
Write-Host ""Starting Flamenco worker..."" -ForegroundColor Cyan
|
||||||
Set-Location '{flamenco.WorkerPath}'
|
Set-Location '{workerPath}'
|
||||||
if (Test-Path 'flamenco-worker.exe') {{
|
if (Test-Path 'flamenco-worker.exe') {{
|
||||||
Write-Host ""Running flamenco-worker.exe..."" -ForegroundColor Green
|
Write-Host ""Running flamenco-worker.exe..."" -ForegroundColor Green
|
||||||
$workerProcess = Start-Process -FilePath '.\flamenco-worker.exe' -NoNewWindow -PassThru -Wait
|
$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
|
Write-Host ""Flamenco worker process has terminated with exit code: $exitCode"" -ForegroundColor Yellow
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}} else {{
|
}} else {{
|
||||||
Write-Host ""Error: flamenco-worker.exe not found in {flamenco.WorkerPath}"" -ForegroundColor Red
|
Write-Host ""Error: flamenco-worker.exe not found in {workerPath}"" -ForegroundColor Red
|
||||||
[Console]::Error.WriteLine(""Error: flamenco-worker.exe not found in {flamenco.WorkerPath}"")
|
[Console]::Error.WriteLine(""Error: flamenco-worker.exe not found in {workerPath}"")
|
||||||
exit 1
|
exit 1
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
@@ -245,9 +252,12 @@ catch {{
|
|||||||
var payloadBase64 = Convert.ToBase64String(Encoding.Unicode.GetBytes(payloadScript));
|
var payloadBase64 = Convert.ToBase64String(Encoding.Unicode.GetBytes(payloadScript));
|
||||||
var remoteBasePath = await _sshService.GetWorkerBasePathAsync(worker);
|
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 = $@"
|
var ensureScript = $@"
|
||||||
$ProgressPreference = 'SilentlyContinue'
|
$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
|
$workerName = $params.WorkerName
|
||||||
$workerType = $params.WorkerType
|
$workerType = $params.WorkerType
|
||||||
$payloadBase64 = $params.PayloadBase64
|
$payloadBase64 = $params.PayloadBase64
|
||||||
@@ -318,12 +328,367 @@ if ($shouldStart) {{
|
|||||||
'-PayloadBase64Path',""$payloadBase64Path""
|
'-PayloadBase64Path',""$payloadBase64Path""
|
||||||
)
|
)
|
||||||
|
|
||||||
Start-Process -FilePath $psExe -ArgumentList $controllerArgs -WindowStyle Hidden | Out-Null
|
Write-Host ""Attempting to start controller with: $psExe"" -ForegroundColor Cyan
|
||||||
Write-Host ""Worker $workerName started under controller."" -ForegroundColor Green
|
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)
|
public async Task StopWorkerAsync(WorkerConfig worker, string workerType)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Avalonia.Desktop">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
@@ -13,13 +13,14 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Avalonia" Version="11.0.0" />
|
<PackageReference Include="Avalonia" Version="11.3.9" />
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0" />
|
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.9" />
|
||||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.0" />
|
<PackageReference Include="Avalonia.Desktop" Version="11.3.9" />
|
||||||
<PackageReference Include="Avalonia.Theme.Fluent" Version="11.0.0" />
|
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.9" />
|
||||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0" />
|
<PackageReference Include="Avalonia.ReactiveUI" Version="11.3.9" />
|
||||||
<PackageReference Include="System.Text.Json" Version="8.0.0" />
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.9" />
|
||||||
<PackageReference Include="MsBox.Avalonia" Version="2.0.6" />
|
<PackageReference Include="MessageBox.Avalonia" Version="3.2.0" />
|
||||||
|
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<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 WorkerConfig? _selectedWorker;
|
||||||
private string _statusText = "Ready";
|
private string _statusText = "Ready";
|
||||||
private string _selectedWorkerType = "All";
|
private string _selectedWorkerType = "All";
|
||||||
|
private string _operationMode = "sheepit"; // "sheepit" or "flamenco"
|
||||||
|
|
||||||
public MainWindowViewModel()
|
public MainWindowViewModel()
|
||||||
{
|
{
|
||||||
@@ -52,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()
|
public void LoadWorkers()
|
||||||
{
|
{
|
||||||
|
_configService.Reload();
|
||||||
var config = _configService.Load();
|
var config = _configService.Load();
|
||||||
Workers.Clear();
|
Workers.Clear();
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ namespace UnifiedFarmLauncher.ViewModels
|
|||||||
private int _sshPort = 22;
|
private int _sshPort = 22;
|
||||||
private string _sshArgs = string.Empty;
|
private string _sshArgs = string.Empty;
|
||||||
private string _sheepItGpu = "OPTIX_0";
|
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 _hasSheepIt;
|
||||||
private bool _hasFlamenco;
|
private bool _hasFlamenco;
|
||||||
|
|
||||||
@@ -93,24 +90,6 @@ namespace UnifiedFarmLauncher.ViewModels
|
|||||||
set => SetAndRaise(ref _sheepItGpu, value);
|
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> NetworkDrives { get; }
|
||||||
public ObservableCollection<string> NetworkPaths { get; }
|
public ObservableCollection<string> NetworkPaths { get; }
|
||||||
|
|
||||||
@@ -126,15 +105,14 @@ namespace UnifiedFarmLauncher.ViewModels
|
|||||||
if (worker.WorkerTypes.SheepIt != null)
|
if (worker.WorkerTypes.SheepIt != null)
|
||||||
{
|
{
|
||||||
HasSheepIt = true;
|
HasSheepIt = true;
|
||||||
SheepItGpu = worker.WorkerTypes.SheepIt.Gpu;
|
SheepItGpu = string.IsNullOrEmpty(worker.WorkerTypes.SheepIt.Gpu)
|
||||||
SheepItUsername = worker.WorkerTypes.SheepIt.Username;
|
? "OPTIX_0"
|
||||||
SheepItRenderKey = worker.WorkerTypes.SheepIt.RenderKey;
|
: worker.WorkerTypes.SheepIt.Gpu;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (worker.WorkerTypes.Flamenco != null)
|
if (worker.WorkerTypes.Flamenco != null)
|
||||||
{
|
{
|
||||||
HasFlamenco = true;
|
HasFlamenco = true;
|
||||||
FlamencoWorkerPath = worker.WorkerTypes.Flamenco.WorkerPath;
|
|
||||||
NetworkDrives.Clear();
|
NetworkDrives.Clear();
|
||||||
foreach (var drive in worker.WorkerTypes.Flamenco.NetworkDrives)
|
foreach (var drive in worker.WorkerTypes.Flamenco.NetworkDrives)
|
||||||
{
|
{
|
||||||
@@ -168,9 +146,7 @@ namespace UnifiedFarmLauncher.ViewModels
|
|||||||
{
|
{
|
||||||
worker.WorkerTypes.SheepIt = new SheepItConfig
|
worker.WorkerTypes.SheepIt = new SheepItConfig
|
||||||
{
|
{
|
||||||
Gpu = SheepItGpu,
|
Gpu = SheepItGpu
|
||||||
Username = SheepItUsername,
|
|
||||||
RenderKey = SheepItRenderKey
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +154,6 @@ namespace UnifiedFarmLauncher.ViewModels
|
|||||||
{
|
{
|
||||||
worker.WorkerTypes.Flamenco = new FlamencoConfig
|
worker.WorkerTypes.Flamenco = new FlamencoConfig
|
||||||
{
|
{
|
||||||
WorkerPath = FlamencoWorkerPath,
|
|
||||||
NetworkDrives = NetworkDrives.ToList(),
|
NetworkDrives = NetworkDrives.ToList(),
|
||||||
NetworkPaths = NetworkPaths.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,12 +1,20 @@
|
|||||||
<Window xmlns="https://github.com/avaloniaui"
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
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:Class="UnifiedFarmLauncher.Views.MainWindow"
|
||||||
|
x:DataType="vm:MainWindowViewModel"
|
||||||
Title="Unified Farm Launcher"
|
Title="Unified Farm Launcher"
|
||||||
Width="1000" Height="700"
|
Width="1000" Height="700"
|
||||||
MinWidth="800" MinHeight="600">
|
MinWidth="800" MinHeight="600">
|
||||||
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
||||||
<!-- Toolbar -->
|
<!-- Toolbar -->
|
||||||
<StackPanel Orientation="Horizontal" Margin="5" Grid.Row="0">
|
<Grid Grid.Row="0" Margin="5">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Orientation="Horizontal" Grid.Column="0">
|
||||||
<Button Name="AddWorkerButton" Content="Add Worker" Margin="5" Width="120"/>
|
<Button Name="AddWorkerButton" Content="Add Worker" Margin="5" Width="120"/>
|
||||||
<Button Name="EditWorkerButton" Content="Edit Worker" Margin="5" Width="120"/>
|
<Button Name="EditWorkerButton" Content="Edit Worker" Margin="5" Width="120"/>
|
||||||
<Button Name="DeleteWorkerButton" Content="Delete Worker" Margin="5" Width="120"/>
|
<Button Name="DeleteWorkerButton" Content="Delete Worker" Margin="5" Width="120"/>
|
||||||
@@ -14,7 +22,23 @@
|
|||||||
<Button Name="StartWorkerButton" Content="Start" Margin="5" Width="80"/>
|
<Button Name="StartWorkerButton" Content="Start" Margin="5" Width="80"/>
|
||||||
<Button Name="StopWorkerButton" Content="Stop" Margin="5" Width="80"/>
|
<Button Name="StopWorkerButton" Content="Stop" Margin="5" Width="80"/>
|
||||||
<Button Name="AttachWorkerButton" Content="Attach" 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>
|
</StackPanel>
|
||||||
|
<!-- Operation Mode Toggle Button -->
|
||||||
|
<Button Name="OperationModeToggleButton" Grid.Column="1"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="5"
|
||||||
|
Padding="10,5"
|
||||||
|
MinWidth="120">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||||
|
<TextBlock Text="{Binding OperationModeIcon}" FontSize="16" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="{Binding OperationModeDisplayName}"
|
||||||
|
VerticalAlignment="Center" FontWeight="Bold"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<!-- Worker Type Filter -->
|
<!-- Worker Type Filter -->
|
||||||
<TabControl Name="WorkerTypeTabs" Grid.Row="1" Margin="5,0">
|
<TabControl Name="WorkerTypeTabs" Grid.Row="1" Margin="5,0">
|
||||||
@@ -34,7 +58,8 @@
|
|||||||
AutoGenerateColumns="False"
|
AutoGenerateColumns="False"
|
||||||
IsReadOnly="True"
|
IsReadOnly="True"
|
||||||
SelectionMode="Single"
|
SelectionMode="Single"
|
||||||
GridLinesVisibility="All">
|
GridLinesVisibility="All"
|
||||||
|
ItemsSource="{Binding Workers}">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="150"/>
|
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="150"/>
|
||||||
<DataGridCheckBoxColumn Header="Enabled" Binding="{Binding Enabled}" Width="80"/>
|
<DataGridCheckBoxColumn Header="Enabled" Binding="{Binding Enabled}" Width="80"/>
|
||||||
@@ -45,9 +70,9 @@
|
|||||||
</DataGrid>
|
</DataGrid>
|
||||||
|
|
||||||
<!-- Status Bar -->
|
<!-- Status Bar -->
|
||||||
<StatusBar Grid.Row="3">
|
<Border Grid.Row="3" Background="{DynamicResource ThemeControlLowBackgroundBrush}" BorderBrush="{DynamicResource ThemeBorderLowBrush}" BorderThickness="0,1,0,0">
|
||||||
<TextBlock Name="StatusText" Text="{Binding StatusText}" Margin="5"/>
|
<TextBlock Name="StatusText" Text="{Binding StatusText}" Margin="5" VerticalAlignment="Center"/>
|
||||||
</StatusBar>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|
||||||
|
|||||||
@@ -33,14 +33,25 @@ namespace UnifiedFarmLauncher.Views
|
|||||||
|
|
||||||
private void SetupEventHandlers()
|
private void SetupEventHandlers()
|
||||||
{
|
{
|
||||||
AddWorkerButton.Click += AddWorkerButton_Click;
|
this.FindControl<Button>("AddWorkerButton")!.Click += AddWorkerButton_Click;
|
||||||
EditWorkerButton.Click += EditWorkerButton_Click;
|
this.FindControl<Button>("EditWorkerButton")!.Click += EditWorkerButton_Click;
|
||||||
DeleteWorkerButton.Click += DeleteWorkerButton_Click;
|
this.FindControl<Button>("DeleteWorkerButton")!.Click += DeleteWorkerButton_Click;
|
||||||
StartWorkerButton.Click += StartWorkerButton_Click;
|
this.FindControl<Button>("StartWorkerButton")!.Click += StartWorkerButton_Click;
|
||||||
StopWorkerButton.Click += StopWorkerButton_Click;
|
this.FindControl<Button>("StopWorkerButton")!.Click += StopWorkerButton_Click;
|
||||||
AttachWorkerButton.Click += AttachWorkerButton_Click;
|
this.FindControl<Button>("AttachWorkerButton")!.Click += AttachWorkerButton_Click;
|
||||||
WorkerTypeTabs.SelectionChanged += WorkerTypeTabs_SelectionChanged;
|
this.FindControl<Button>("SettingsButton")!.Click += SettingsButton_Click;
|
||||||
WorkersGrid.SelectionChanged += WorkersGrid_SelectionChanged;
|
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)
|
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)
|
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);
|
var dialog = new WorkerEditWindow(worker);
|
||||||
if (await dialog.ShowDialogAsync(this))
|
if (await dialog.ShowDialogAsync(this))
|
||||||
@@ -66,11 +77,11 @@ namespace UnifiedFarmLauncher.Views
|
|||||||
|
|
||||||
private async void DeleteWorkerButton_Click(object? sender, RoutedEventArgs e)
|
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",
|
var box = MessageBoxManager.GetMessageBoxStandard("Delete Worker",
|
||||||
$"Are you sure you want to delete worker '{worker.Name}'?",
|
$"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();
|
var result = await box.ShowAsync();
|
||||||
|
|
||||||
if (result == ButtonResult.Yes)
|
if (result == ButtonResult.Yes)
|
||||||
@@ -83,96 +94,110 @@ namespace UnifiedFarmLauncher.Views
|
|||||||
|
|
||||||
private async void StartWorkerButton_Click(object? sender, RoutedEventArgs e)
|
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
|
try
|
||||||
{
|
{
|
||||||
string? workerType = null;
|
var vm = (MainWindowViewModel)DataContext!;
|
||||||
if (worker.WorkerTypes.SheepIt != null)
|
string? workerType = vm.OperationMode;
|
||||||
workerType = "sheepit";
|
|
||||||
else if (worker.WorkerTypes.Flamenco != null)
|
|
||||||
workerType = "flamenco";
|
|
||||||
|
|
||||||
if (workerType == null)
|
// Verify the worker supports the selected operation mode
|
||||||
|
if (workerType == "sheepit" && worker.WorkerTypes.SheepIt == null)
|
||||||
{
|
{
|
||||||
var box = MessageBoxManager.GetMessageBoxStandard("Error",
|
var box = MessageBoxManager.GetMessageBoxStandard("Error",
|
||||||
"Worker has no configured worker type.",
|
$"Worker '{worker.Name}' does not have SheepIt configured.",
|
||||||
ButtonEnum.Ok, Icon.Error);
|
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();
|
await box.ShowAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _controllerService.StartWorkerAsync(worker, workerType);
|
await _controllerService.StartWorkerAsync(worker, workerType);
|
||||||
var successBox = MessageBoxManager.GetMessageBoxStandard("Start Worker",
|
var successBox = MessageBoxManager.GetMessageBoxStandard("Start Worker",
|
||||||
$"Worker '{worker.Name}' started successfully.",
|
$"Worker '{worker.Name}' ({vm.OperationModeDisplayName}) started successfully.",
|
||||||
ButtonEnum.Ok, Icon.Success);
|
ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Success);
|
||||||
await successBox.ShowAsync();
|
await successBox.ShowAsync();
|
||||||
((MainWindowViewModel)DataContext!).RefreshWorkers();
|
vm.RefreshWorkers();
|
||||||
}
|
}
|
||||||
catch (System.Exception ex)
|
catch (System.Exception ex)
|
||||||
{
|
{
|
||||||
var errorBox = MessageBoxManager.GetMessageBoxStandard("Error",
|
var errorDialog = new ErrorDialog($"Failed to start worker: {ex.Message}");
|
||||||
$"Failed to start worker: {ex.Message}",
|
await errorDialog.ShowDialog(this);
|
||||||
ButtonEnum.Ok, Icon.Error);
|
|
||||||
await errorBox.ShowAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void StopWorkerButton_Click(object? sender, RoutedEventArgs e)
|
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
|
try
|
||||||
{
|
{
|
||||||
string? workerType = null;
|
var vm = (MainWindowViewModel)DataContext!;
|
||||||
if (worker.WorkerTypes.SheepIt != null)
|
string workerType = vm.OperationMode;
|
||||||
workerType = "sheepit";
|
|
||||||
else if (worker.WorkerTypes.Flamenco != null)
|
|
||||||
workerType = "flamenco";
|
|
||||||
|
|
||||||
if (workerType == null)
|
// Verify the worker supports the selected operation mode
|
||||||
|
if (workerType == "sheepit" && worker.WorkerTypes.SheepIt == null)
|
||||||
{
|
{
|
||||||
var box = MessageBoxManager.GetMessageBoxStandard("Error",
|
var box = MessageBoxManager.GetMessageBoxStandard("Error",
|
||||||
"Worker has no configured worker type.",
|
$"Worker '{worker.Name}' does not have SheepIt configured.",
|
||||||
ButtonEnum.Ok, Icon.Error);
|
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();
|
await box.ShowAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _controllerService.StopWorkerAsync(worker, workerType);
|
await _controllerService.StopWorkerAsync(worker, workerType);
|
||||||
var successBox = MessageBoxManager.GetMessageBoxStandard("Stop Worker",
|
var successBox = MessageBoxManager.GetMessageBoxStandard("Stop Worker",
|
||||||
$"Stop command sent to worker '{worker.Name}'.",
|
$"Stop command sent to worker '{worker.Name}' ({vm.OperationModeDisplayName}).",
|
||||||
ButtonEnum.Ok, Icon.Info);
|
ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Info);
|
||||||
await successBox.ShowAsync();
|
await successBox.ShowAsync();
|
||||||
}
|
}
|
||||||
catch (System.Exception ex)
|
catch (System.Exception ex)
|
||||||
{
|
{
|
||||||
var errorBox = MessageBoxManager.GetMessageBoxStandard("Error",
|
var errorDialog = new ErrorDialog($"Failed to stop worker: {ex.Message}");
|
||||||
$"Failed to stop worker: {ex.Message}",
|
await errorDialog.ShowDialog(this);
|
||||||
ButtonEnum.Ok, Icon.Error);
|
|
||||||
await errorBox.ShowAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void AttachWorkerButton_Click(object? sender, RoutedEventArgs e)
|
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
|
try
|
||||||
{
|
{
|
||||||
string? workerType = null;
|
var vm = (MainWindowViewModel)DataContext!;
|
||||||
if (worker.WorkerTypes.SheepIt != null)
|
string workerType = vm.OperationMode;
|
||||||
workerType = "sheepit";
|
|
||||||
else if (worker.WorkerTypes.Flamenco != null)
|
|
||||||
workerType = "flamenco";
|
|
||||||
|
|
||||||
if (workerType == null)
|
// Verify the worker supports the selected operation mode
|
||||||
|
if (workerType == "sheepit" && worker.WorkerTypes.SheepIt == null)
|
||||||
{
|
{
|
||||||
var box = MessageBoxManager.GetMessageBoxStandard("Error",
|
var box = MessageBoxManager.GetMessageBoxStandard("Error",
|
||||||
"Worker has no configured worker type.",
|
$"Worker '{worker.Name}' does not have SheepIt configured.",
|
||||||
ButtonEnum.Ok, Icon.Error);
|
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();
|
await box.ShowAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -181,17 +206,15 @@ namespace UnifiedFarmLauncher.Views
|
|||||||
}
|
}
|
||||||
catch (System.Exception ex)
|
catch (System.Exception ex)
|
||||||
{
|
{
|
||||||
var errorBox = MessageBoxManager.GetMessageBoxStandard("Error",
|
var errorDialog = new ErrorDialog($"Failed to attach to worker: {ex.Message}");
|
||||||
$"Failed to attach to worker: {ex.Message}",
|
await errorDialog.ShowDialog(this);
|
||||||
ButtonEnum.Ok, Icon.Error);
|
|
||||||
await errorBox.ShowAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WorkerTypeTabs_SelectionChanged(object? sender, SelectionChangedEventArgs e)
|
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";
|
var type = tab.Header?.ToString() ?? "All";
|
||||||
if (type == "All Workers") type = "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)
|
private void WorkersGrid_SelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (DataContext is MainWindowViewModel vm)
|
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"
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:UnifiedFarmLauncher.ViewModels"
|
||||||
x:Class="UnifiedFarmLauncher.Views.WorkerEditWindow"
|
x:Class="UnifiedFarmLauncher.Views.WorkerEditWindow"
|
||||||
|
x:DataType="vm:WorkerEditViewModel"
|
||||||
Title="Edit Worker"
|
Title="Edit Worker"
|
||||||
Width="600" Height="700"
|
Width="600" Height="700"
|
||||||
MinWidth="500" MinHeight="600">
|
MinWidth="500" MinHeight="600">
|
||||||
@@ -34,17 +36,17 @@
|
|||||||
<TextBlock Text="GPU:" IsVisible="{Binding HasSheepIt}"/>
|
<TextBlock Text="GPU:" IsVisible="{Binding HasSheepIt}"/>
|
||||||
<ComboBox Name="GpuComboBox" IsVisible="{Binding HasSheepIt}" SelectedItem="{Binding SheepItGpu}">
|
<ComboBox Name="GpuComboBox" IsVisible="{Binding HasSheepIt}" SelectedItem="{Binding SheepItGpu}">
|
||||||
<ComboBox.Items>
|
<ComboBox.Items>
|
||||||
<ComboBoxItem Content="OPTIX_0"/>
|
<x:String>OPTIX_0</x:String>
|
||||||
<ComboBoxItem Content="CUDA_0"/>
|
<x:String>CUDA_0</x:String>
|
||||||
<ComboBoxItem Content="OPENCL_0"/>
|
<x:String>OPENCL_0</x:String>
|
||||||
</ComboBox.Items>
|
</ComboBox.Items>
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
|
|
||||||
<TextBlock Text="Username:" IsVisible="{Binding HasSheepIt}" Margin="0,10,0,0"/>
|
<TextBlock Text="Note: Username and Render Key are configured in Global Settings."
|
||||||
<TextBox Name="SheepItUsernameTextBox" Text="{Binding SheepItUsername}" IsVisible="{Binding HasSheepIt}"/>
|
IsVisible="{Binding HasSheepIt}"
|
||||||
|
Margin="0,10,0,0"
|
||||||
<TextBlock Text="Render Key:" IsVisible="{Binding HasSheepIt}" Margin="0,10,0,0"/>
|
Foreground="Gray"
|
||||||
<TextBox Name="SheepItRenderKeyTextBox" Text="{Binding SheepItRenderKey}" IsVisible="{Binding HasSheepIt}" PasswordChar="*"/>
|
TextWrapping="Wrap"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
@@ -53,13 +55,13 @@
|
|||||||
<StackPanel Margin="10" Spacing="10">
|
<StackPanel Margin="10" Spacing="10">
|
||||||
<CheckBox Name="HasFlamencoCheckBox" Content="Enable Flamenco Worker" IsChecked="{Binding HasFlamenco}" Margin="0,0,0,10"/>
|
<CheckBox Name="HasFlamencoCheckBox" Content="Enable Flamenco Worker" IsChecked="{Binding HasFlamenco}" Margin="0,0,0,10"/>
|
||||||
|
|
||||||
<TextBlock Text="Worker Path:" IsVisible="{Binding HasFlamenco}"/>
|
<TextBlock Text="Note: Worker Path is configured in Global Settings."
|
||||||
<Grid IsVisible="{Binding HasFlamenco}" ColumnDefinitions="*,Auto">
|
IsVisible="{Binding HasFlamenco}"
|
||||||
<TextBox Name="FlamencoPathTextBox" Grid.Column="0" Text="{Binding FlamencoWorkerPath}" Margin="0,0,5,0"/>
|
Margin="0,0,0,10"
|
||||||
<Button Name="BrowseFlamencoPathButton" Grid.Column="1" Content="Browse..." Width="80"/>
|
Foreground="Gray"
|
||||||
</Grid>
|
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">
|
<Grid IsVisible="{Binding HasFlamenco}" RowDefinitions="*,Auto" Margin="0,5,0,0">
|
||||||
<ListBox Name="NetworkDrivesListBox" Grid.Row="0" ItemsSource="{Binding NetworkDrives}" MaxHeight="100"/>
|
<ListBox Name="NetworkDrivesListBox" Grid.Row="0" ItemsSource="{Binding NetworkDrives}" MaxHeight="100"/>
|
||||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,5,0,0">
|
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,5,0,0">
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ namespace UnifiedFarmLauncher.Views
|
|||||||
private readonly WorkerEditViewModel _viewModel;
|
private readonly WorkerEditViewModel _viewModel;
|
||||||
private bool _result;
|
private bool _result;
|
||||||
|
|
||||||
public WorkerEditWindow(WorkerConfig? worker = null)
|
public WorkerEditWindow() : this(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public WorkerEditWindow(WorkerConfig? worker)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
var configService = new ConfigService();
|
var configService = new ConfigService();
|
||||||
@@ -33,13 +37,12 @@ namespace UnifiedFarmLauncher.Views
|
|||||||
|
|
||||||
private void SetupEventHandlers()
|
private void SetupEventHandlers()
|
||||||
{
|
{
|
||||||
OkButton.Click += OkButton_Click;
|
this.FindControl<Button>("OkButton")!.Click += OkButton_Click;
|
||||||
CancelButton.Click += CancelButton_Click;
|
this.FindControl<Button>("CancelButton")!.Click += CancelButton_Click;
|
||||||
BrowseFlamencoPathButton.Click += BrowseFlamencoPathButton_Click;
|
this.FindControl<Button>("AddDriveButton")!.Click += AddDriveButton_Click;
|
||||||
AddDriveButton.Click += AddDriveButton_Click;
|
this.FindControl<Button>("RemoveDriveButton")!.Click += RemoveDriveButton_Click;
|
||||||
RemoveDriveButton.Click += RemoveDriveButton_Click;
|
this.FindControl<Button>("AddPathButton")!.Click += AddPathButton_Click;
|
||||||
AddPathButton.Click += AddPathButton_Click;
|
this.FindControl<Button>("RemovePathButton")!.Click += RemovePathButton_Click;
|
||||||
RemovePathButton.Click += RemovePathButton_Click;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OkButton_Click(object? sender, RoutedEventArgs e)
|
private async void OkButton_Click(object? sender, RoutedEventArgs e)
|
||||||
@@ -48,7 +51,7 @@ namespace UnifiedFarmLauncher.Views
|
|||||||
{
|
{
|
||||||
var box = MessageBoxManager.GetMessageBoxStandard("Error",
|
var box = MessageBoxManager.GetMessageBoxStandard("Error",
|
||||||
"Worker name is required.",
|
"Worker name is required.",
|
||||||
ButtonEnum.Ok, Icon.Error);
|
ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Error);
|
||||||
await box.ShowAsync();
|
await box.ShowAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -63,7 +66,7 @@ namespace UnifiedFarmLauncher.Views
|
|||||||
{
|
{
|
||||||
var errorBox = MessageBoxManager.GetMessageBoxStandard("Error",
|
var errorBox = MessageBoxManager.GetMessageBoxStandard("Error",
|
||||||
$"Failed to save worker: {ex.Message}",
|
$"Failed to save worker: {ex.Message}",
|
||||||
ButtonEnum.Ok, Icon.Error);
|
ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Error);
|
||||||
await errorBox.ShowAsync();
|
await errorBox.ShowAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,24 +77,7 @@ namespace UnifiedFarmLauncher.Views
|
|||||||
Close();
|
Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void BrowseFlamencoPathButton_Click(object? sender, RoutedEventArgs e)
|
private void AddDriveButton_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)
|
|
||||||
{
|
{
|
||||||
// Simplified: use a simple input box
|
// Simplified: use a simple input box
|
||||||
// In a full implementation, you'd use a proper input dialog
|
// 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)
|
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);
|
_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
|
// Simplified: use a simple input box
|
||||||
// In a full implementation, you'd use a proper input dialog
|
// 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)
|
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);
|
_viewModel.NetworkPaths.Remove(path);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user