575 lines
28 KiB
Markdown
575 lines
28 KiB
Markdown
# Linux-Portierung — Analyse
|
||
|
||
Stand: 2026-08-06. Reine Bestandsaufnahme und Aufwandsschätzung, **kein** Umbau.
|
||
|
||
Frage: Was ist nötig, damit ClawdDotNet unter Linux läuft, und was kostet das?
|
||
|
||
---
|
||
|
||
## 0. Kurzfassung
|
||
|
||
Die gute Nachricht zuerst: **Der Kern ist bereits portabel.** Alle 16 Bibliotheks-
|
||
und beide Testprojekte zielen auf `net10.0` (nicht `net10.0-windows`), es gibt im
|
||
gesamten Repository **kein einziges `DllImport`, keinen Registry-Zugriff und keine
|
||
`System.Drawing`-Nutzung** in `src/`. Windows steckt an genau drei Stellen im Kern:
|
||
DPAPI-Verschlüsselung, Groß-/Kleinschreibung bei Pfadvergleichen und die
|
||
Zeitzonen-IDs.
|
||
|
||
Die schlechte Nachricht: Die gesamte Bedienoberfläche — rund **8.900 Zeilen** in
|
||
`frm_*.cs`, `UI/`, `Models/` und `Services/` — hängt an Windows Forms, an WebView2
|
||
und, am unangenehmsten, an vier `PropertyGrid`-Instanzen, die praktisch die
|
||
komplette Einstellungsverwaltung ausmachen. Dafür gibt es in Avalonia keine
|
||
Eins-zu-eins-Entsprechung.
|
||
|
||
**Empfehlung: den Umzug in zwei Schnitte teilen.** Ein kopfloser Host (ohne GUI)
|
||
auf Linux ist in etwa **12–18 Personentagen** erreichbar und liefert den
|
||
eigentlichen Nutzen — Agenten laufen auf einem Server, nicht auf einem
|
||
Windows-Desktop. Die Avalonia-Oberfläche ist ein davon unabhängiges Vorhaben
|
||
von **32–52 Personentagen**, das man danach in Ruhe angehen kann.
|
||
|
||
Gesamt für „alles auf Linux, mit GUI": **50–80 Personentage.**
|
||
|
||
---
|
||
|
||
## 1. Bestandsaufnahme
|
||
|
||
### 1.1 Was bereits portabel ist
|
||
|
||
| Bereich | Zeilen | Zielframework | Windows-Abhängigkeit |
|
||
|---|---:|---|---|
|
||
| `src/ClawdDotNet.Core` | 8.959 | `net10.0` | nur DPAPI (1 Datei) |
|
||
| 15 Tool-Projekte | 6.415 | `net10.0` | nur `.exe`-Pfade im SocialMediaManager |
|
||
| `tests/` (348 Tests, 39 Dateien) | 6.888 | `net10.0` | 3 Testfälle mit `C:\`-Pfaden |
|
||
|
||
Alle NuGet-Pakete laufen unter Linux: `Microsoft.Data.Sqlite` (bringt
|
||
`e_sqlite3` nativ für linux-x64/arm64 mit), `MySqlConnector`, `Npgsql`,
|
||
`Microsoft.Data.SqlClient`, `MongoDB.Driver`, `MailKit`, `FluentFTP`,
|
||
`Telegram.Bot`, `WTelegramClient`, `SharpCompress`, `Snappier`,
|
||
`Microsoft.Extensions.Logging`. Kein Paket muss ersetzt werden — mit zwei
|
||
Ausnahmen (siehe 2.1 und 2.3).
|
||
|
||
Auch die Dinge, bei denen man Ärger erwarten würde, sind sauber gelöst:
|
||
|
||
- [AtomicFile.cs:167](src/ClawdDotNet.Core/Storage/AtomicFile.cs:167) — `Commit`
|
||
prüft `File.Exists` und weicht auf `File.Move` aus. `File.Replace` verlangt
|
||
unter Unix ebenfalls eine vorhandene Zieldatei; der Fall ist also schon
|
||
abgedeckt. Die Wiederholschleife ist unter Linux überflüssig, aber harmlos.
|
||
- [TaskFrontmatter.cs:27](src/ClawdDotNet.Core/Tasks/TaskFrontmatter.cs:27) —
|
||
normalisiert `\r\n` und `\r` vor dem Zerlegen. Task-Dateien von einem
|
||
Windows-Rechner werden unter Linux korrekt gelesen.
|
||
- Textdateien werden durchgängig als **UTF-8 ohne BOM** geschrieben
|
||
(`AtomicFile`, `FileLogWriter`, `AgentEditorTool`). Kein `Encoding.Default`,
|
||
keine Codepage-Fallen.
|
||
- Zeitstempel gehen als `DateTime.UtcNow` in die Datenbank und werden mit
|
||
`DateTimeStyles.RoundtripKind` gelesen.
|
||
|
||
### 1.2 Was am Windows-Teil hängt
|
||
|
||
| Bereich | Zeilen | davon Designer |
|
||
|---|---:|---:|
|
||
| `frm_*.cs` (6 Formulare + Dialoge) | 4.946 | 1.865 |
|
||
| `UI/` (BackupPanel, WebViewBridge, EmbeddedUiManager) | 1.087 | 428 |
|
||
| `Models/` (PropertyGrid-ViewModels) | 1.257 | — |
|
||
| `Services/` (4 Dienste, an WinForms-Timer gekoppelt) | 1.501 | — |
|
||
| `Properties/` | 123 | — |
|
||
| **Summe** | **8.914** | **2.293** |
|
||
|
||
Dazu drei `.resx`-Dateien à ~272 KB (eingebettete Symbole/Bilder) und eine
|
||
`frm_main.en.resx` für die englische Lokalisierung über den
|
||
WinForms-Resx-Mechanismus.
|
||
|
||
Steuerelement-Inventar aus den Designer-Dateien: 24 `Label`, 19
|
||
`ToolStripButton`, 12 `TabPage`, 12 `Button`, 9 `TextBox`, 6 `DataGridView`, 6
|
||
`ToolStrip`, 5 `ComboBox`, **4 `PropertyGrid`**, 4 `TableLayoutPanel`, 4
|
||
`FlowLayoutPanel`, 3 `TabControl`, 3 `SplitContainer`, 1 `RichTextBox`, 1
|
||
`ListView`, 1 `NotifyIcon`, 1 `DateTimePicker`, 1 `NumericUpDown`.
|
||
|
||
Tabs in `frm_main`: Chat, Logs, Settings (mit Unter-Tabs App-Settings,
|
||
Instance-Settings), Agent Settings, Jobs/Services (mit Unter-Tabs Jobs,
|
||
Services, Job History), Info, Backup.
|
||
|
||
---
|
||
|
||
## 2. Die harten Brocken
|
||
|
||
### 2.1 WebView2 → kein Linux (Chat- und Übersichts-Ansicht)
|
||
|
||
`Microsoft.Web.WebView2` ist die einzige Windows-only-Paketabhängigkeit des
|
||
Hauptprojekts und trägt die zwei sichtbarsten Ansichten:
|
||
[frm_main.cs:235](frm_main.cs:235) und [frm_chat.cs:44](frm_chat.cs:44) laden
|
||
`overview.html` bzw. `chat.html` aus `EmbeddedUI/` über
|
||
`SetVirtualHostNameToFolderMapping` unter `https://ui.clwd.internal/`. Die
|
||
Kommunikation läuft über [WebViewBridge.cs](UI/WebViewBridge.cs) —
|
||
`WebMessageReceived` in die eine, `ExecuteScriptAsync` in die andere Richtung.
|
||
|
||
Drei Wege, jeder mit einem eigenen Preis:
|
||
|
||
| Variante | Was passiert | Aufwand | Risiko |
|
||
|---|---|---:|---|
|
||
| **A — Avalonia.WebView** | HTML/JS bleiben. Unter Linux rendert WebKitGTK, unter Windows weiterhin WebView2. Die Bridge wird auf die Abstraktion der Bibliothek umgeschrieben. | 4–6 PT | Bibliothek ist deutlich weniger reif als WebView2; WebKitGTK-Abhängigkeit muss auf dem Zielserver vorhanden sein; Verhalten unterscheidet sich je Plattform. |
|
||
| **B — nativ neu in Avalonia** | Chat als echte Avalonia-Ansicht mit `ItemsControl` und einem Markdown-Renderer. `EmbeddedUI/` entfällt. | 8–12 PT | Kein Fremdrisiko, aber Neuentwicklung. Am Ende deutlich wartbarer als HTML-in-Container. |
|
||
| **C — lokaler HTTP-Server + Systembrowser** | Die App liefert `EmbeddedUI/` über `http://localhost:port` aus, der Nutzer öffnet den Browser. | 3–4 PT | Bricht die Ein-Fenster-Anmutung. Passt aber ausgezeichnet zum kopflosen Betrieb — dort **ist** der Browser die Oberfläche. |
|
||
|
||
**Empfehlung:** C für den kopflosen Host (fällt dort ohnehin an), B für die
|
||
Desktop-Oberfläche. Variante A koppelt uns an eine Bibliothek, die weniger stabil
|
||
ist als alles andere im Projekt.
|
||
|
||
### 2.2 PropertyGrid → es gibt keinen Ersatz von der Stange
|
||
|
||
Vier `PropertyGrid`-Instanzen in [frm_main.Designer.cs](frm_main.Designer.cs)
|
||
bilden App-Settings, Instance-Settings, Agent-Settings und Tool-Settings ab. Sie
|
||
werden vollständig durch Attribute gesteuert — **246 `[Category]`,
|
||
`[DisplayName]`, `[Description]`-Angaben** verteilt auf vier Dateien:
|
||
|
||
- [Models/ToolSettingsViewModels.cs](Models/ToolSettingsViewModels.cs) — 108
|
||
- [Models/AgentSettingsViewModel.cs](Models/AgentSettingsViewModel.cs) — 54
|
||
- [Models/AppSettings.cs](Models/AppSettings.cs) — 51
|
||
- [Models/InstanceSettingsViewModel.cs](Models/InstanceSettingsViewModel.cs) — 33
|
||
|
||
Dazu kommen `[TypeConverter(typeof(ExpandableObjectConverter))]` für
|
||
verschachtelte Objekte, `[PasswordPropertyText(true)]` für Geheimnisse und ein
|
||
eigener [ModelTypeConverter](Models/ModelTypeConverter.cs), der das
|
||
Modell-Auswahlfeld dynamisch aus der OpenRouter-Modellliste füllt.
|
||
|
||
Avalonia hat kein `PropertyGrid`. Zwei Möglichkeiten:
|
||
|
||
1. **`Avalonia.PropertyGrid`** (Community, MIT). Versteht `Category`,
|
||
`DisplayName`, `Description`, `Browsable`, `ReadOnly` und
|
||
`ExpandableObjectConverter`. Die ViewModels und ihre Attribute könnten
|
||
weitgehend unverändert bleiben — das spart am meisten. Zu prüfen ist, ob der
|
||
dynamische `ModelTypeConverter` mit `GetStandardValues` unterstützt wird; das
|
||
ist der Punkt, an dem so etwas erfahrungsgemäß hakt. **Aufwand 6–8 PT**, plus
|
||
dauerhafte Abhängigkeit an ein Ein-Personen-Projekt.
|
||
2. **Von Hand gebaute Einstellungsformulare.** Mehr Arbeit, aber wir bekommen
|
||
eine Oberfläche, die man Nutzern zumuten kann — das `PropertyGrid` ist
|
||
ehrlicherweise eine Entwickleransicht. Passwörter, Verzeichnisauswahl,
|
||
Validierung und die Modell-Auswahl werden dabei richtig statt behelfsmäßig.
|
||
**Aufwand 10–14 PT.**
|
||
|
||
Das ist der größte Einzelposten der GUI-Portierung. Die Entscheidung kann und
|
||
sollte man verschieben, bis das Grundgerüst steht.
|
||
|
||
### 2.3 DPAPI → Geheimnisse liegen unter Linux im Klartext
|
||
|
||
[SecretProtector.cs:42](src/ClawdDotNet.Core/Security/SecretProtector.cs:42):
|
||
|
||
```csharp
|
||
if (!OperatingSystem.IsWindows())
|
||
return plainText;
|
||
```
|
||
|
||
Unter Linux verschlüsselt `Protect` **stillschweigend nicht**. OpenRouter-Key,
|
||
Datenbank-Verbindungszeichenfolgen mit Passwort, Mail-Zugangsdaten und das
|
||
Telegram-2FA-Passwort lägen im Klartext in `InstanceConfig.json` und
|
||
`AgentSettings.json` — genau der Zustand, den S7 behoben hat. Auf einem Server,
|
||
der per SSH erreichbar ist und gesichert wird, ist das schlechter als auf einem
|
||
Einzelplatz-Windows.
|
||
|
||
Dasselbe gilt für den Lizenz-Zustandsspeicher:
|
||
`LicenseLabrador/client-dotnet/.../StateStore.cs:92` schützt seine Datei ebenfalls
|
||
nur unter Windows per DPAPI.
|
||
|
||
Zu klären ist also ein plattformübergreifendes Verfahren. Realistisch:
|
||
|
||
- **AES-GCM mit Schlüssel aus einer Datei mit `0600`** neben der Konfiguration
|
||
(Linux) bzw. weiterhin DPAPI (Windows). Einfach, wirkt gegen versehentliche
|
||
Weitergabe und Backups, nicht gegen einen Angreifer mit demselben Benutzer —
|
||
dieselbe Schutzstufe wie DPAPI heute.
|
||
- Optional zusätzlich `libsecret`/Schlüsselbund, wenn eine Desktop-Sitzung da
|
||
ist. Auf einem Server gibt es die nicht, also braucht es den Dateiweg ohnehin.
|
||
|
||
Nebenwirkung, die man einplanen muss: **Konfigurationen sind nicht mehr zwischen
|
||
Betriebssystemen austauschbar.** Ein `enc:v1:`-Wert von Windows ist unter Linux
|
||
nicht lesbar und umgekehrt. `Unprotect` wirft dann korrekterweise eine
|
||
`SecretProtectionException` ([SecretProtector.cs:81](src/ClawdDotNet.Core/Security/SecretProtector.cs:81)) —
|
||
für den Umzug einer Instanz braucht es einen Migrationsweg (Präfix `enc:v2:`,
|
||
Werte neu eintragen oder ein Export/Import-Kommando).
|
||
|
||
**Aufwand 3–5 PT** inklusive Tests und Migration.
|
||
|
||
### 2.4 Zeitzonen → das ist die stillste Fehlerquelle
|
||
|
||
[TaskSchedule.cs:161](src/ClawdDotNet.Core/Tasks/TaskSchedule.cs:161):
|
||
|
||
```csharp
|
||
try { return TimeZoneInfo.FindSystemTimeZoneById(id); }
|
||
catch { return TimeZoneInfo.Utc; }
|
||
```
|
||
|
||
Und [SchedulerTaskMigration.cs:32](src/ClawdDotNet.Core/Tasks/SchedulerTaskMigration.cs:32)
|
||
schreibt `TimeZoneInfo.Local.Id` in die Task-Frontmatter. Auf dem
|
||
Entwicklungsrechner ergibt das `"W. Europe Standard Time"`, unter Linux
|
||
`"Europe/Berlin"`.
|
||
|
||
Task-Dateien sind Markdown im `SharedWorkspace` und wandern zwischen Rechnern.
|
||
Trifft eine Windows-ID auf ein System ohne die Umsetzungsdaten, greift das
|
||
`catch` — und der Task läuft ab sofort nach **UTC statt Ortszeit**, also im
|
||
Sommer zwei Stunden zu früh. Ohne Fehlermeldung, ohne Logeintrag. Ein Task, der
|
||
um 08:00 die Marktübersicht holen soll, läuft um 06:00.
|
||
|
||
.NET 6+ kann Windows-IDs unter Linux über ICU auflösen, aber nur wenn ICU
|
||
vorhanden ist. In einem schlanken Container (Alpine ohne `icu-libs`, distroless)
|
||
oder bei `InvariantGlobalization=true` ist es das nicht — dann schlägt jede
|
||
Auflösung fehl und alles fällt auf UTC.
|
||
|
||
Was zu tun ist:
|
||
|
||
- Beim Schreiben auf **IANA normalisieren**
|
||
(`TimeZoneInfo.TryConvertWindowsIdToIanaId`), beim Lesen beide Formen
|
||
akzeptieren.
|
||
- Das `catch` **nicht mehr still schlucken** — eine unbekannte Zeitzone muss
|
||
protokolliert werden, besser noch den Task als fehlerhaft markieren.
|
||
- Das Zielsystem muss `tzdata` haben. Für Container explizit installieren.
|
||
|
||
Verwandt: **82 Vorkommen von `DateTime.Now`/`UtcNow`**. Die meisten sind
|
||
unkritisch, zwei fallen auf:
|
||
[TaskboardService.cs:80](src/ClawdDotNet.Core/Tasks/TaskboardService.cs:80)
|
||
schreibt `DateTime.Now`-Zeitstempel in Task-Dateien, und
|
||
[LiveLogViewerService.cs:98](Services/LiveLogViewerService.cs:98) sucht die
|
||
Logdatei des Tages über `DateTime.Now`. Server laufen üblicherweise mit `TZ=UTC`
|
||
— dort wechselt die Logdatei dann um 02:00 Ortszeit statt um Mitternacht, und
|
||
Task-Zeitstempel bekommen eine andere Bedeutung als bisher. Kein Fehler, aber
|
||
eine Verhaltensänderung, die man kennen sollte.
|
||
|
||
**Aufwand 2–3 PT.**
|
||
|
||
### 2.5 Groß-/Kleinschreibung bei Pfaden → sicherheitsrelevant
|
||
|
||
Linux-Dateisysteme unterscheiden Groß- und Kleinschreibung, Windows nicht. An
|
||
vier Stellen wird das Gegenteil angenommen — und drei davon bewachen eine
|
||
Sandbox-Grenze:
|
||
|
||
- [WorkspacePath.cs:72](src/ClawdDotNet.Tools.FileRW/WorkspacePath.cs:72) —
|
||
`normalizedCandidate.StartsWith(normalizedRoot, OrdinalIgnoreCase)`. Das ist
|
||
die Prüfung, die Agenten daran hindert, aus ihrem Arbeitsverzeichnis
|
||
auszubrechen.
|
||
- [FileRWTool.cs:174](src/ClawdDotNet.Tools.FileRW/FileRWTool.cs:174) —
|
||
Abgleich gegen die Liste geschützter Pfade.
|
||
- [FtpTool.cs:155](src/ClawdDotNet.Tools.FTP/FtpTool.cs:155) — dieselbe
|
||
Einschließungsprüfung.
|
||
- [BackupService.cs:384](src/ClawdDotNet.Core/Backup/BackupService.cs:384).
|
||
|
||
Unter Linux sind `/home/x/Workspace` und `/home/x/workspace` **zwei
|
||
verschiedene Verzeichnisse**. Der Vergleich mit `OrdinalIgnoreCase` würde einen
|
||
Pfad im zweiten als „innerhalb" des ersten durchwinken. Genauso liefe die
|
||
Sperrliste in `FileRWTool` ins Leere, sobald jemand die Schreibweise ändert.
|
||
|
||
Nötig ist ein Vergleichsverfahren, das die Plattform berücksichtigt — ein
|
||
`PathComparer`, der unter Windows `OrdinalIgnoreCase` und unter Unix `Ordinal`
|
||
verwendet, konsequent an allen vier Stellen.
|
||
|
||
Ebenfalls betroffen, aber harmlos:
|
||
[AtomicFile.cs:35](src/ClawdDotNet.Core/Storage/AtomicFile.cs:35) schlüsselt
|
||
seine Sperren mit `fullPath.ToLowerInvariant()`. Unter Linux teilen sich damit
|
||
zwei verschiedene Dateien eine Sperre — das serialisiert zu viel, gefährdet aber
|
||
nichts.
|
||
|
||
**Aufwand 2–3 PT**, davon der größere Teil Tests.
|
||
|
||
### 2.6 Prozessaufrufe und `.exe`-Annahmen
|
||
|
||
- **`Process.Start("explorer.exe", …)`** — 4 Stellen
|
||
([frm_main.cs:1552](frm_main.cs:1552), [frm_main.cs:1557](frm_main.cs:1557),
|
||
[frm_main.cs:1562](frm_main.cs:1562), [BackupPanel.cs:338](UI/BackupPanel.cs:338)).
|
||
Ersatz: `Process.Start(new ProcessStartInfo(path) { UseShellExecute = true })`
|
||
bzw. `xdg-open`. Die Variante `explorer.exe /select,"…"` hat unter Linux kein
|
||
Gegenstück — dort öffnet man nur den Ordner.
|
||
- **`Microsoft.VisualBasic.Interaction.InputBox`** — 3 Stellen
|
||
([Program.cs:280](Program.cs:280), [Program.cs:291](Program.cs:291),
|
||
[frm_main.cs:658](frm_main.cs:658)), zwei davon für den interaktiven
|
||
Telegram-Login (Code und 2FA-Passwort). Braucht einen eigenen Dialog. Für den
|
||
kopflosen Betrieb ohnehin problematisch: **ein Login, der ein Eingabefenster
|
||
öffnet, blockiert einen Dienst.** Dort muss der Telegram-Login anders gelöst
|
||
werden (vorab per CLI, oder über die Weboberfläche).
|
||
- **`yt-dlp.exe` / `ffmpeg.exe`** —
|
||
[SocialMediaManagerTool.cs:782](src/ClawdDotNet.Tools.SocialMediaManager/SocialMediaManagerTool.cs:782)
|
||
und `:825`. Die PATH-Suche davor funktioniert unter Linux bereits; nur die
|
||
Ausweichpfade sind fest auf `.exe` verdrahtet und laufen dort ins Leere.
|
||
Kleine Änderung, aber sie fällt sonst erst zur Laufzeit auf.
|
||
|
||
**Aufwand zusammen 1–2 PT.**
|
||
|
||
### 2.7 WinForms-Timer in der Dienstschicht
|
||
|
||
`Services/` ist logisch kein UI-Code, hängt aber an
|
||
`System.Windows.Forms.Timer`:
|
||
|
||
- [BackupScheduler.cs:41](Services/BackupScheduler.cs:41)
|
||
- [LiveLogViewerService.cs:38](Services/LiveLogViewerService.cs:38) — schreibt
|
||
zusätzlich direkt in eine `RichTextBox`
|
||
- [OpenRouterStatusService.cs:43](Services/OpenRouterStatusService.cs:43)
|
||
- [frm_main.License.cs:41](frm_main.License.cs:41)
|
||
|
||
Der Backup-Zeitplan und die Lizenzprüfung gehören in den kopflosen Host und
|
||
müssen dafür auf `System.Threading.PeriodicTimer` umgestellt werden. Der
|
||
Log-Betrachter ist echte Oberfläche und wird ohnehin neu gebaut.
|
||
|
||
**Aufwand 2–3 PT.**
|
||
|
||
### 2.8 Lizenzierung — erledigt (Stand 2026-08-06)
|
||
|
||
> **Nachtrag.** LicenseLabrador und WatchDog sind im **Deploymentcenter**
|
||
> zusammengefasst, Hardware-ID v2 ist dort umgesetzt. Der Client
|
||
> (`Deploymentcenter.Client`, `netstandard2.0;net8.0`) läuft auf beiden
|
||
> Plattformen, die HW-ID ist container- und umbenennungsfest, der Ablageort
|
||
> löst sich auch ohne `HOME` auf, und der Zustandsspeicher ist mit AES-GCM
|
||
> plattformübergreifend verschlüsselt.
|
||
>
|
||
> **Damit ist die einzige potenziell blockierende Frage dieser Analyse
|
||
> beantwortet.** Details und offene Punkte der Anbindung:
|
||
> [Deploymentcenter-Anbindung-Review.md](Deploymentcenter-Anbindung-Review.md).
|
||
|
||
Es bleibt reine Anschlussarbeit in ClawdDotNet: Projektverweis umhängen,
|
||
[LicenseGate](Services/LicenseGate.cs) gegen die neue Ergebnisklasse schreiben
|
||
(`LicenseState` ist entfallen, `Status` ist jetzt eine Zeichenkette), `MessageBox`
|
||
durch das mitgelieferte `ILicensePrompt` ersetzen und die Lizenzprüfung aus
|
||
[Program.cs:112](Program.cs:112) fensterfrei machen.
|
||
|
||
**Aufwand 2–3 PT** (vorher 3–5).
|
||
|
||
---
|
||
|
||
## 3. Kleinere Punkte, die trotzdem beißen
|
||
|
||
### 3.1 Kultur- und Zahlenformatierung
|
||
|
||
Nur 16 Stellen im gesamten Projekt nennen eine Kultur explizit. Das heißt
|
||
umgekehrt: fast alles formatiert mit `CurrentCulture`. Auf dem
|
||
Entwicklungsrechner ist das `de-DE`, auf einem Server mit unbesetztem `LANG`
|
||
ist es `InvariantCulture`. Aus `1,25` wird `1.25`.
|
||
|
||
Wo das folgenlos bleibt:
|
||
- **JSON** — `System.Text.Json` schreibt Zahlen immer invariant. Alle
|
||
Konfigurationen, Zustandsdateien und API-Aufrufe sind sicher.
|
||
- **SQLite** — Werte gehen typisiert über Parameter, nicht als Text.
|
||
|
||
Wo hinzuschauen ist:
|
||
- Zeichenkettenverkettung in Logeinträgen und Prompts (`$"{cost:F4}"`). Wenn
|
||
eine Zahl mit deutschem Dezimalkomma in einen Prompt gerät, muss das Modell
|
||
raten.
|
||
- Anzeigewerte in der Oberfläche — dort ist Ortsformat gewünscht, aber es sollte
|
||
bewusst gesetzt sein, nicht zufällig.
|
||
|
||
**Empfehlung:** einmal alle Formatierungen durchgehen und trennen — invariant
|
||
für alles Maschinenlesbare, `CurrentCulture` nur für die Anzeige. Am besten mit
|
||
einem Analyzer (`CA1305`, `CA1304`, `CA1310`) als Warnung im Build, damit es so
|
||
bleibt.
|
||
|
||
**Aufwand 2–3 PT.**
|
||
|
||
### 3.2 Globalisierungsmodus festlegen
|
||
|
||
`InvariantGlobalization=true` macht das Publikat kleiner und ICU überflüssig —
|
||
kostet aber `TimeZoneInfo.FindSystemTimeZoneById` (siehe 2.4), kulturabhängige
|
||
Vergleiche und korrektes `ToLower()` für Umlaute. Für dieses Projekt mit
|
||
zeitzonenabhängiger Planung ist das **keine Option**; die Entscheidung sollte im
|
||
Projekt dokumentiert und ICU/tzdata als Voraussetzung festgehalten werden.
|
||
|
||
Nebenbemerkung: `COLLATE NOCASE` in
|
||
[SqliteMemoryRepository.cs:143](src/ClawdDotNet.Core/Memory/SqliteMemoryRepository.cs:143)
|
||
und [SqliteTaskRepository.cs:105](src/ClawdDotNet.Core/Tasks/SqliteTaskRepository.cs:105)
|
||
ist ASCII-beschränkt — `Ä` und `ä` gelten SQLite als verschieden. Das ist heute
|
||
schon so und ändert sich beim Umzug nicht, ist also kein Portierungsthema,
|
||
sondern eine bestehende Eigenheit.
|
||
|
||
### 3.3 Zeilenenden
|
||
|
||
1.230 Stellen verwenden `Environment.NewLine` oder `\r\n`. Für Logdateien ist
|
||
das egal. Bei **Task-Dateien** und Agenten-erzeugten Dateien im geteilten
|
||
Arbeitsverzeichnis führt es zu Rauschen: Eine Datei, die unter Windows
|
||
geschrieben und unter Linux angefasst wird, ändert komplett ihre Zeilenenden.
|
||
Wenn der Arbeitsbereich unter Git liegt oder synchronisiert wird, sieht jede
|
||
Änderung wie eine Vollumschreibung aus. Der Parser kommt damit klar (siehe 1.1)
|
||
— es ist eine Frage der Ordnung, kein Fehler. Empfehlung: für Task- und
|
||
Konfigurationsdateien fest `\n` schreiben.
|
||
|
||
### 3.4 Dateinamen
|
||
|
||
`Path.GetInvalidFileNameChars()` liefert unter Windows 41 Zeichen, unter Linux
|
||
genau zwei (`\0` und `/`). [FileLogWriter.cs:95](src/ClawdDotNet.Core/Logging/FileLogWriter.cs:95)
|
||
säubert Modulnamen damit — unter Linux entstehen also Dateinamen, die auf
|
||
Windows nicht mehr lesbar sind. Betrifft Sicherungen, die zwischen Systemen
|
||
wandern. Ebenso die Windows-Sonderfälle `CON`, `PRN`, `AUX` und Namen mit
|
||
abschließendem Punkt: unter Linux erlaubt, beim Rückspielen auf Windows nicht.
|
||
Für den Sicherungs-/Wiederherstellungsweg über Systemgrenzen hinweg relevant.
|
||
|
||
### 3.5 Ablageorte
|
||
|
||
[SettingsManager.cs:24](Services/SettingsManager.cs:24) legt `AppSettings.json`
|
||
neben die Programmdatei (`AppDomain.CurrentDomain.BaseDirectory`). Unter Windows
|
||
in einem Benutzerverzeichnis geht das; unter Linux liegt die Anwendung typisch
|
||
in `/opt/…` oder `/usr/lib/…` und ist für den Dienstbenutzer **nicht
|
||
schreibbar**. Dasselbe gilt für die Zielordner `tools/`, `Logs/` und
|
||
`Instances/`, die die Build-Ziele in `ClawdDotNet.csproj` neben der
|
||
Programmdatei anlegen.
|
||
|
||
Nötig ist eine Trennung von Programm und Daten nach XDG-Konvention:
|
||
`$XDG_CONFIG_HOME` bzw. `/etc/clawddotnet` für die Konfiguration,
|
||
`$XDG_DATA_HOME` bzw. `/var/lib/clawddotnet` für Instanzen und Datenbanken,
|
||
`/var/log/clawddotnet` für Logs. Dazu Dateirechte: Instanzverzeichnisse mit
|
||
Geheimnissen gehören auf `0700`, Konfigurationsdateien auf `0600` — unter
|
||
Windows regelt das die ACL des Benutzerprofils, unter Linux muss man es setzen.
|
||
|
||
**Aufwand 2–3 PT.**
|
||
|
||
### 3.6 Tests
|
||
|
||
Von 348 Tests sind fast alle portabel. Auffällig ist
|
||
[WorkspacePathTests.cs:49](tests/ClawdDotNet.Tools.Tests/FileRW/WorkspacePathTests.cs:49):
|
||
|
||
```csharp
|
||
[InlineData(@"C:\Windows\System32\config\SAM")]
|
||
[InlineData(@"\\server\share\evil.txt")]
|
||
[InlineData(@"C:\temp\datei.txt")]
|
||
```
|
||
|
||
Unter Linux liefert `Path.IsPathRooted(@"C:\temp\datei.txt")` **`false`** — das
|
||
ist ein gewöhnlicher relativer Dateiname mit Doppelpunkt und Backslashes darin.
|
||
Der Test prüft dort also etwas anderes als beabsichtigt. Und da er die
|
||
Sandbox-Grenze absichert, ist das keine Kleinigkeit: Er muss
|
||
betriebssystemabhängig aufgeteilt werden, mit einer eigenen Linux-Fassung
|
||
(`/etc/passwd`, `../../etc/passwd`, Symlinks). Symlinks sind überhaupt ein
|
||
Prüfpunkt, den es unter Windows so nicht gab — `Path.GetFullPath` löst sie
|
||
**nicht** auf, `File.ResolveLinkTarget` schon. Ein Agent könnte im
|
||
Arbeitsverzeichnis einen Symlink nach `/etc` anlegen und die Prüfung ginge
|
||
durch.
|
||
|
||
Ebenso in [YouTubeUrlTests.cs:118](tests/ClawdDotNet.Tools.Tests/SocialMedia/YouTubeUrlTests.cs:118)
|
||
(harmlos, nur Beispieldaten).
|
||
|
||
**Aufwand 2–4 PT**, inklusive Symlink-Absicherung in `WorkspacePath` selbst.
|
||
|
||
### 3.7 Bau und Auslieferung
|
||
|
||
[Deploy-Build.ps1](Deploy-Build.ps1) setzt PowerShell 5.1 voraus, verwendet
|
||
Backslash-Pfade und den festen Ausgabepfad `bin\Release\net10.0-windows`. Für
|
||
Linux braucht es entweder eine `pwsh`-taugliche Fassung oder — besser — einen
|
||
schlichten `dotnet publish -r linux-x64 --self-contained` mit einer
|
||
systemd-Unit-Datei. Dazu:
|
||
|
||
- systemd-Unit mit eigenem Dienstbenutzer, `Restart=on-failure`
|
||
- Prüfen, ob der bestehende Watchdog-Heartbeat
|
||
([Program.cs:367](Program.cs:367)) mit `systemd-notify` zusammenspielen soll
|
||
- optional `.deb` oder AppImage für den Desktop-Fall
|
||
|
||
**Aufwand 3–5 PT.**
|
||
|
||
---
|
||
|
||
## 4. Der empfohlene Schnitt
|
||
|
||
Der entscheidende Befund dieser Analyse: **Die Oberfläche ist nicht der Grund,
|
||
warum wir Linux wollen.** Der Grund ist, dass Agenten auf einem Server laufen
|
||
sollen. [Program.cs](Program.cs) baut bereits alles — Speicher, Engine,
|
||
Taskboard-Scanner, Watchdog, Lizenzprüfung — vollständig auf, **bevor**
|
||
`frm_main` überhaupt entsteht (Zeilen 36–385 gegen 388–401). Diese Trennung
|
||
existiert faktisch schon; sie muss nur formalisiert werden.
|
||
|
||
### Stufe 1 — Kern Linux-fest und kopfloser Host (12–18 PT)
|
||
|
||
| Schritt | PT |
|
||
|---|---:|
|
||
| Geheimnisse plattformübergreifend (2.3) | 3–5 |
|
||
| Zeitzonen normalisieren, Fehler nicht mehr schlucken (2.4) | 2–3 |
|
||
| Pfadvergleiche plattformabhängig + Symlink-Prüfung (2.5, 3.6) | 3–5 |
|
||
| Prozessaufrufe, `.exe`-Pfade (2.6) | 1–2 |
|
||
| Ablageorte und Dateirechte nach XDG (3.5) | 2–3 |
|
||
| `ClawdDotNet.Host` — Startlogik aus `Program.cs` herauslösen, `PeriodicTimer` statt WinForms-Timer, Telegram-Login ohne Dialog | 4–6 |
|
||
| Tests auf Linux grün, CI-Lauf für linux-x64 | 2–3 |
|
||
|
||
**Ergebnis:** Die Anwendung läuft als systemd-Dienst auf einem Linux-Server. Die
|
||
Windows-GUI bleibt unverändert bestehen und wird weiter benutzt. Das ist der
|
||
Punkt, an dem der Nutzen anfällt.
|
||
|
||
### Stufe 2 — Avalonia-Oberfläche (32–52 PT)
|
||
|
||
| Schritt | PT |
|
||
|---|---:|
|
||
| Grundgerüst: Avalonia-Projekt, DI, Dispatcher, Shell mit Tabs, MVVM-Schicht | 5–7 |
|
||
| Logs-Tab (`RichTextBox` → `SelectingItemsControl` mit Filterung) | 2–3 |
|
||
| Agent-Settings: Liste, Werkzeugauswahl, Aktionsschaltflächen | 5–8 |
|
||
| Einstellungs-Tabs — PropertyGrid-Ersatz (2.2) | 6–10 |
|
||
| Jobs / Services / Job History (4 `DataGridView`) | 4–6 |
|
||
| Backup-Panel | 3–4 |
|
||
| Instance-Manager und die fünf Dialoge | 4–6 |
|
||
| Chat-Ansicht (Variante B, siehe 2.1) | 8–12 |
|
||
| Info, Statusleiste, Werkzeugleisten, Menü, Lokalisierung de/en | 3–4 |
|
||
|
||
Die Spanne ist breit, weil zwei Entscheidungen noch offen sind (PropertyGrid-Ersatz
|
||
und Chat-Variante). Sind die getroffen, lässt sich das auf etwa ±15 % genau
|
||
angeben.
|
||
|
||
### Stufe 3 — Auslieferung und Härtung (6–10 PT)
|
||
|
||
Publish-Pipeline, systemd-Unit, Paketierung, Abnahme auf echter Hardware,
|
||
Dokumentation, Umzugsweg für bestehende Instanzen.
|
||
|
||
### Gesamt
|
||
|
||
| | PT | bei Vollzeit |
|
||
|---|---:|---|
|
||
| Stufe 1 | 12–18 | 2,5–3,5 Wochen |
|
||
| Stufe 2 | 32–52 | 6,5–10,5 Wochen |
|
||
| Stufe 3 | 6–10 | 1,5–2 Wochen |
|
||
| **Summe** | **50–80** | **10–16 Wochen** |
|
||
|
||
---
|
||
|
||
## 5. LiveCharts2
|
||
|
||
Zur Einordnung: **Das Projekt enthält heute keine einzige Diagrammdarstellung.**
|
||
Die Suche nach `Chart`, `Series` oder `Plot` findet nur JSON-Feldnamen der
|
||
Yahoo-Finance-Abfrage in
|
||
[DirectAPITool.cs:126](src/ClawdDotNet.Tools.DirectAPI/DirectAPITool.cs:126).
|
||
|
||
LiveCharts2 ist damit **keine Portierung, sondern neue Funktionalität** — sie
|
||
gehört zum Trading-Teil, nicht zum Linux-Umzug, und ist in den 50–80 PT oben
|
||
nicht enthalten. Wenn die Kursansichten kommen, sind dafür grob 5–10 PT
|
||
zusätzlich zu rechnen. Das passt zu dem, was in
|
||
[docs/Roadmap.md](docs/Roadmap.md) und der Notiz „Basis vor Trading härten"
|
||
festgehalten ist: Erst die Basis, dann die Handelsansichten.
|
||
|
||
Ein Punkt, der jetzt schon zählt: LiveCharts2 setzt auf SkiaSharp, genau wie
|
||
Avalonia. Das spricht zusätzlich dafür, die Diagramme erst **nach** der
|
||
Avalonia-Portierung zu bauen — sonst entstehen sie zweimal.
|
||
|
||
---
|
||
|
||
## 6. Was vor dem ersten Handgriff zu entscheiden ist
|
||
|
||
1. **Ist das Ziel Server oder Desktop?** Bei „Server" reicht Stufe 1, und Stufe 2
|
||
kann entfallen oder durch eine Weboberfläche ersetzt werden. Das ändert die
|
||
Schätzung um den Faktor drei.
|
||
2. **Chat-Ansicht: HTML behalten oder nativ neu bauen?** (2.1)
|
||
3. **PropertyGrid: Fremdbibliothek oder eigene Formulare?** (2.2)
|
||
4. ~~**Erlaubt LicenseLabrador den Wechsel der Hardware-ID?**~~ — **geklärt**,
|
||
siehe 2.8 und
|
||
[Deploymentcenter-Anbindung-Review.md](Deploymentcenter-Anbindung-Review.md).
|
||
5. **Bleibt Windows als Zielplattform bestehen?** Wenn ja, muss alles doppelt
|
||
getestet werden, und die Geheimnis-Verschlüsselung braucht beide Wege plus
|
||
Umzugspfad. Wenn nein, wird 2.3 deutlich einfacher.
|
||
|
||
Frage 1 und 5 beantworten sich vermutlich schnell; 2 und 3 kann man bis zum
|
||
Beginn von Stufe 2 offenlassen, ohne Stufe 1 zu blockieren. Damit liegt nichts
|
||
mehr außerhalb unserer Hand — **Stufe 1 kann beginnen.**
|
||
|
||
---
|
||
|
||
## Anhang — Vollständige Fundstellenliste
|
||
|
||
| Thema | Datei:Zeile |
|
||
|---|---|
|
||
| DPAPI | [SecretProtector.cs:42](src/ClawdDotNet.Core/Security/SecretProtector.cs:42), `:66`, `:90`, `:94` |
|
||
| DPAPI (Lizenz) | `LicenseLabrador/client-dotnet/.../StateStore.cs:43`, `:92` |
|
||
| Zeitzone | [TaskSchedule.cs:161](src/ClawdDotNet.Core/Tasks/TaskSchedule.cs:161), [SchedulerTaskMigration.cs:32](src/ClawdDotNet.Core/Tasks/SchedulerTaskMigration.cs:32) |
|
||
| Pfad-Groß-/Kleinschreibung | [WorkspacePath.cs:72](src/ClawdDotNet.Tools.FileRW/WorkspacePath.cs:72), [FileRWTool.cs:174](src/ClawdDotNet.Tools.FileRW/FileRWTool.cs:174), [FtpTool.cs:155](src/ClawdDotNet.Tools.FTP/FtpTool.cs:155), [BackupService.cs:384](src/ClawdDotNet.Core/Backup/BackupService.cs:384), [AtomicFile.cs:35](src/ClawdDotNet.Core/Storage/AtomicFile.cs:35) |
|
||
| `explorer.exe` | [frm_main.cs:1552](frm_main.cs:1552), `:1557`, `:1562`, [BackupPanel.cs:338](UI/BackupPanel.cs:338) |
|
||
| `VisualBasic.InputBox` | [Program.cs:280](Program.cs:280), `:291`, [frm_main.cs:658](frm_main.cs:658) |
|
||
| `.exe`-Werkzeugpfade | [SocialMediaManagerTool.cs:782](src/ClawdDotNet.Tools.SocialMediaManager/SocialMediaManagerTool.cs:782), `:825` |
|
||
| WinForms-Timer | [BackupScheduler.cs:41](Services/BackupScheduler.cs:41), [LiveLogViewerService.cs:38](Services/LiveLogViewerService.cs:38), [OpenRouterStatusService.cs:43](Services/OpenRouterStatusService.cs:43), [frm_main.License.cs:41](frm_main.License.cs:41) |
|
||
| WebView2 | [frm_main.cs:235](frm_main.cs:235), [frm_chat.cs:44](frm_chat.cs:44), [WebViewBridge.cs](UI/WebViewBridge.cs), [ClawdDotNet.csproj](ClawdDotNet.csproj) |
|
||
| PropertyGrid | [frm_main.Designer.cs](frm_main.Designer.cs) (4×), [Models/](Models/) (246 Attribute) |
|
||
| Datenablage | [SettingsManager.cs:24](Services/SettingsManager.cs:24), [ClawdDotNet.csproj](ClawdDotNet.csproj) (Build-Ziele) |
|
||
| Testdaten mit Windows-Pfaden | [WorkspacePathTests.cs:49](tests/ClawdDotNet.Tools.Tests/FileRW/WorkspacePathTests.cs:49) |
|
||
| Build-Skript | [Deploy-Build.ps1](Deploy-Build.ps1) |
|