using System.Diagnostics;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using Avalonia.Layout;
using Avalonia.Media;
using ClawdDotNet.App;
using ClawdDotNet.App.Services;
using ClawdDotNet.App.Settings;
using ClawdDotNet.Desktop.Services;
using ClawdDotNet.Desktop.ViewModels;
using ClawdDotNet.Desktop.Views;
using ClawdDotNet.Web;
using ClawdDotNet.Core.Instances;
using ClawdDotNet.Core.Storage;
using Microsoft.Extensions.Logging;
namespace ClawdDotNet.Desktop;
public partial class App : Application
{
///
/// Schalter der Befehlszeile, mit dem sich ClawdDotNet auf eine bestimmte Instanz
/// starten laesst. Gesetzt wird er nur von der Anwendung selbst, wenn der
/// Instanzdialog einen Wechsel verlangt — deshalb ist er nirgends dokumentiert.
///
private const string InstanceArgument = CommandLine.InstanceArgument;
private AppHost? _host;
/// Die Weboberflaeche, falls sie eingeschaltet ist. Sonst null.
private InstanceHttpHost? _web;
/// Vorgewaehlte Instanz aus der Befehlszeile; wird genau einmal verbraucht.
private string? _preselectedInstancePath;
///
/// Vom Instanzmanager gestartet (--managed).
///
/// Dann gehoert der Instanzwechsel dorthin: Der Manager haelt die Fenster aller
/// Instanzen im Hintergrund und holt das gewuenschte nach vorn (E-M2). Ein Fenster,
/// das sich selbst mit einer anderen Instanz neu startet, naehme genau die Instanz
/// mit, aus der heraus jemand wechselt.
///
private bool _managed;
// #576: Muessen leben, solange die Anwendung laeuft - eine eingesammelte
// Registrierung meldet sich ab.
private System.Runtime.InteropServices.PosixSignalRegistration? _sigterm;
private System.Runtime.InteropServices.PosixSignalRegistration? _sigint;
///
/// SIGTERM und SIGINT beenden auch mit Fenster geordnet (#576).
///
/// Kopflos war das seit HeadlessRun verdrahtet, mit Fenster nicht: Unter Linux
/// reagierte die Instanz auf SIGTERM gar nicht und wurde nach Ablauf jeder Frist hart
/// abgeschossen - beim Abmelden, bei systemctl stop, beim Herunterfahren. Derselbe Weg
/// wie die Bitte des Instanzmanagers: das Fenster ohne Rueckfrage schliessen, das
/// geordnete Ende laeuft von dort.
///
private void RegisterSignals(IClassicDesktopStyleApplicationLifetime desktop)
{
void Beenden(System.Runtime.InteropServices.PosixSignalContext context)
{
// Abfangen, nicht durchlassen: Sonst raeumte die Laufzeit den Prozess ab,
// waehrend das geordnete Ende noch laeuft.
context.Cancel = true;
var signal = context.Signal.ToString();
Dispatcher.UIThread.Post(() =>
{
_host?.LoggerFactory.CreateLogger("ClawdDotNet.Shutdown")
.LogInformation("Signal {Signal} empfangen - die Instanz endet geordnet.", signal);
if (_host is null)
desktop.Shutdown();
else if (desktop.MainWindow is MainWindow fenster)
fenster.BeendenOhneRueckfrage();
else
_ = BeendeGeordnetAsync(desktop, signal);
});
}
try
{
_sigterm = System.Runtime.InteropServices.PosixSignalRegistration.Create(
System.Runtime.InteropServices.PosixSignal.SIGTERM, Beenden);
_sigint = System.Runtime.InteropServices.PosixSignalRegistration.Create(
System.Runtime.InteropServices.PosixSignal.SIGINT, Beenden);
}
catch (PlatformNotSupportedException)
{
// Ohne Signale bleibt es beim Schliessen ueber Fenster und Manager.
}
}
public override void Initialize() => AvaloniaXamlLoader.Load(this);
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
// Erst beenden, wenn wir es sagen: Zwischen Instanzauswahl und Hauptfenster
// ist kurz gar kein Fenster offen. Mit OnLastWindowClose würde die Anwendung
// in genau dieser Lücke aussteigen.
desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown;
_preselectedInstancePath = CommandLine.ReadInstancePath(desktop.Args);
_managed = CommandLine.IsManaged(desktop.Args);
ApplySavedThemeEarly();
ApplySavedLanguageEarly();
desktop.ShutdownRequested += async (_, _) =>
{
// Reihenfolge: erst die Weboberflaeche, dann der Aufbau. Andersherum
// bediente Kestrel noch Anfragen gegen eine bereits geschlossene
// Datenbank.
await ShutdownWebAsync();
if (_host is not null) await _host.DisposeAsync();
};
RegisterSignals(desktop);
// Nicht abwarten: OnFrameworkInitializationCompleted muss zurückkehren,
// damit die Nachrichtenschleife anläuft — sonst gäbe es keinen Faden, auf
// dem die Fenster des Startvorgangs überhaupt erscheinen könnten.
_ = StartAsync(desktop);
}
base.OnFrameworkInitializationCompleted();
}
///
/// Setzt das gespeicherte Farbschema, bevor das erste Fenster erscheint.
///
/// Das Ansichtsmodell des Hauptfensters tut dasselbe — aber erst, wenn der
/// Aufbau steht. Dazwischen liegt die Instanzauswahl, und die waere sonst bei einem
/// Benutzer im Dunkelmodus hell und der Rest der Anwendung dunkel.
///
/// Ein zweites Laden der Einstellungen ist unbedenklich: Load liest nur und
/// schreibt allenfalls beim allerersten Start die Vorgaben fest.
///
private static void ApplySavedThemeEarly()
{
try
{
var settings = new SettingsManager();
settings.Load();
ThemeManager.Apply(ThemeManager.IsDarkName(settings.AppSettings.Theme));
}
catch (Exception)
{
// Ohne lesbare Einstellungen bleibt es beim hellen Schema.
}
}
///
/// Setzt die gespeicherte Sprache, bevor das erste Fenster erscheint.
///
private static void ApplySavedLanguageEarly()
{
try
{
var settings = new SettingsManager();
settings.Load();
LocalizationManager.Instance.SetLanguage(settings.AppSettings.Language);
}
catch (Exception)
{
LocalizationManager.Instance.SetLanguage(LocalizationManager.DefaultLanguage);
}
}
private async Task StartAsync(IClassicDesktopStyleApplicationLifetime desktop)
{
var result = await AppHost.StartAsync(new AppHost.Callbacks
{
SelectInstance = SelectInstanceAsync,
License = new AvaloniaLicensePrompt(),
IsManaged = _managed,
TelegramLogin = prompt => AskAsync("Telegram Verifizierung", prompt),
Telegram2FA = () => AskAsync("Telegram 2FA", "Bitte 2FA-Passwort eingeben:")
});
if (result.Error is { } error)
{
await new AvaloniaLicensePrompt().ShowErrorAsync("ClawdDotNet – Fehler", error);
desktop.Shutdown(1);
return;
}
if (result.Host is null)
{
// Abbruch durch den Benutzer oder fehlende Lizenz — beides ist bereits
// erklärt worden, hier kommt keine weitere Meldung hinterher.
desktop.Shutdown();
return;
}
_host = result.Host;
LocalizationManager.Instance.SetLanguage(_host.Settings.AppSettings.Language);
HookErrorReporting(_host);
HookLicenseWatch(_host, desktop);
// Nach dem Aufbau und vor dem Fenster: Die HTTP-Flaeche braucht den fertigen
// AppHost, und ein belegter Port darf das Fenster nicht aufhalten — TryStart
// gibt in jedem Fehlerfall null zurueck und schreibt den Grund ins Protokoll.
//
// Sie ist mehr als die Weboberflaeche: Immer dabei ist der Socket im
// Instanzverzeichnis, ueber den der Instanzmanager die Instanz erreicht (#528).
_web = InstanceHttpHost.TryStart(_host, _host.LoggerFactory);
var mainViewModel = new MainWindowViewModel(_host)
{
// Wer beaufsichtigt wird, wechselt nicht selbst. Der geprüfte Befund des
// Aufbaus, nicht der Schalter der Befehlszeile (#535): Wer --managed ohne
// gueltige Unterschrift setzt, ist eine eigenstaendige Instanz und darf
// wechseln wie jede andere.
CanSwitchInstance = !_host.IsManaged
};
// Der Wechsel gehoert hierher und nicht ins Fenster: Er braucht den Aufbau und
// die Lebensdauer der Anwendung, und beides kennt nur diese Klasse.
mainViewModel.RequestInstanceManagerDialog += () => _ = SwitchInstanceAsync(desktop);
desktop.MainWindow = new MainWindow { DataContext = mainViewModel };
// Der Manager bittet ueber den Socket, dieses Fenster nach vorn zu holen (E-M2).
if (_web is not null)
{
_web.Control.FocusRequested += () => BringToFront(desktop);
// Und er bittet, zu enden. Ohne diese Zeile lief die Bitte ins Leere: Der
// Endpunkt nahm sie an, antwortete mit 202 - und niemand hoerte zu. Der
// Manager wartete seine Frist ab und schoss den Prozess dann jedes Mal ab.
// Kopflos war das verdrahtet (HeadlessRun), mit Fenster nicht; und mit
// Fenster ist die Vorgabe (ManagerSettings.InstanceWindows).
_web.Control.ShutdownRequested += () => Dispatcher.UIThread.Post(() =>
{
if (desktop.MainWindow is MainWindow fenster)
fenster.BeendenOhneRueckfrage();
else
_ = BeendeGeordnetAsync(desktop, "Instanzmanager");
});
}
desktop.MainWindow.Show();
desktop.MainWindow.Closed += (_, _) => _ = BeendeGeordnetAsync(desktop, "Fenster");
await OfferUpdateAsync(_host, desktop);
}
///
/// Meldet ungefangene Ausnahmen an den Fehler-Stream des Deploymentcenters.
///
/// Erst hier verdrahtet, nicht in Main: Vor dem Aufbau gibt es weder
/// Einstellungen noch Token, und ohne die wäre der Meldeweg ohnehin der Leerlauf.
/// Die Kehrseite ist bewusst in Kauf genommen — ein Absturz während des
/// Starts erreicht das Deploymentcenter nicht, steht aber im Protokoll.
///
private static void HookErrorReporting(AppHost host)
{
AppDomain.CurrentDomain.UnhandledException += (_, args) =>
{
if (args.ExceptionObject is Exception ex)
{
// Der Prozess endet gleich: kurze Frist, dann weiterlaufen lassen.
host.Errors.ReportAsync(ex, fatal: args.IsTerminating)
.Wait(TimeSpan.FromSeconds(3));
}
};
TaskScheduler.UnobservedTaskException += (_, args) =>
{
_ = host.Errors.ReportAsync(args.Exception, fatal: false);
// Ohne Observe reißt eine unbeobachtete Ausnahme in manchen Konfigurationen
// den Prozess mit — und das wäre eine Nebenwirkung des Meldens.
args.SetObserved();
};
Dispatcher.UIThread.UnhandledException += (_, args) =>
{
_ = host.Errors.ReportAsync(args.Exception, fatal: false);
// Ein Fehler in einem Ereignisbehandler soll die Oberfläche nicht beenden.
args.Handled = true;
};
}
/// Ein Widerruf beendet die Anwendung, ohne Beenden-Rückfrage.
private static void HookLicenseWatch(AppHost host, IClassicDesktopStyleApplicationLifetime desktop)
{
if (host.LicenseWatch is null) return;
host.LicenseWatch.Revoked += async message =>
{
await new AvaloniaLicensePrompt().ShowErrorAsync("ClawdDotNet – Lizenz", message);
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(2));
};
}
///
/// Fragt, ob ein verfügbares Update jetzt eingespielt werden soll.
///
/// Bewusst eine Frage und kein Selbstlauf: Wann aktualisiert wird, entscheidet
/// der Benutzer — eine Anwendung, die sich beim Start selbst beendet, um sich zu
/// erneuern, ist genau dann im Weg, wenn man sie braucht.
///
private async Task OfferUpdateAsync(AppHost host, IClassicDesktopStyleApplicationLifetime desktop)
{
if (host.Deploymentcenter is null) return;
// Die Prüfung läuft nebenher; kurz Zeit geben, dann aufgeben.
for (var waited = 0; host.Deploymentcenter.Update is null && waited < 10; waited++)
await Task.Delay(TimeSpan.FromSeconds(1));
if (host.Deploymentcenter.Update is not { IsAvailable: true } update) return;
var notes = string.IsNullOrWhiteSpace(update.ReleaseNotes) ? "" : $"\n\n{update.ReleaseNotes}";
var install = await ConfirmAsync(
update.IsCritical ? "ClawdDotNet – Wichtiges Update" : "ClawdDotNet – Update",
$"Version {update.LatestVersion} ist verfügbar.{notes}\n\n"
+ "Zum Einspielen wird ClawdDotNet beendet und danach neu gestartet.",
confirmText: "Jetzt installieren",
cancelText: "Später");
if (!install) return;
await StartUpdateAsync(host, desktop, update.LatestVersion);
}
///
/// Die Reihenfolge ist der ganze Punkt dieser Methode.
///
///
/// - Dem Watchdog sagen, dass gleich ein Update kommt — sonst sieht das
/// Verschwinden der Instanz aus wie ein Ausfall.
/// - Geordnet herunterfahren: Datenbank, Scanner, Telegram, Abmeldung.
/// - Erst dann den Agenten starten. Er bekommt unsere Prozesskennung und wartet,
/// bis wir weg sind, statt über gesperrte Dateien zu kopieren.
/// - Selbst beenden.
///
///
/// Die Verlockung wäre, LaunchUpdateAgent das Beenden zu überlassen —
/// das tut es aber über Environment.Exit und übergeht damit Schritt 2
/// vollständig.
///
private async Task StartUpdateAsync(
AppHost host, IClassicDesktopStyleApplicationLifetime desktop, string version)
{
host.Deploymentcenter!.AnnounceUpdate(version);
var settings = host.Settings.AppSettings;
// Vor dem Aufbau: Kestrel darf keine Anfrage mehr gegen eine geschlossene
// Datenbank bedienen — und schon gar nicht, waehrend der update-agent
// gleich die Dateien darunter austauscht.
await ShutdownWebAsync();
await host.DisposeAsync();
var problem = host.Deploymentcenter.StartUpdate(settings, AppHost.AppVersion);
if (problem is not null)
{
await new AvaloniaLicensePrompt().ShowErrorAsync("ClawdDotNet – Update", problem);
// Der Aufbau ist bereits abgebaut — weiterarbeiten wäre nicht ehrlich.
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(1));
return;
}
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown());
}
/// Ja/Nein-Rückfrage. Avalonia bringt kein MessageBox mit.
private static async Task ConfirmAsync(
string title, string message, string confirmText, string cancelText)
=> await Dispatcher.UIThread.InvokeAsync(async () =>
{
var completion = new TaskCompletionSource();
var confirm = new Button { Content = confirmText, IsDefault = true };
var cancel = new Button { Content = cancelText, IsCancel = true };
var window = new Window
{
Title = title,
Width = 480,
SizeToContent = SizeToContent.Height,
CanResize = false,
WindowStartupLocation = WindowStartupLocation.CenterScreen,
Content = new StackPanel
{
Margin = new Thickness(20),
Spacing = 16,
Children =
{
new TextBlock { Text = message, TextWrapping = Avalonia.Media.TextWrapping.Wrap },
new StackPanel
{
Orientation = Avalonia.Layout.Orientation.Horizontal,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right,
Spacing = 8,
Children = { cancel, confirm }
}
}
}
};
var answer = false;
confirm.Click += (_, _) => { answer = true; window.Close(); };
cancel.Click += (_, _) => window.Close();
// Schließt der Benutzer das Fenster, gilt das als „Später".
window.Closed += (_, _) => completion.TrySetResult(answer);
window.Show();
return await completion.Task;
});
///
/// Holt das Fenster dieser Instanz nach vorn — auf Bitte des Instanzmanagers.
///
/// Die Instanz macht das selbst, statt dass jemand von aussen an ihrem
/// Fenster zieht. Der Manager kennt kein Fenstersystem und soll keines kennen; er
/// bittet über den Socket, und wer ein Fenster hat, holt es nach vorn. Kopflos läuft
/// die Bitte ins Leere, und das ist die richtige Antwort.
///
/// Das letzte Wort hat der Fenstermanager. Windows lässt einen Prozess
/// im Hintergrund den Vordergrund nicht einfach an sich reissen — Activate
/// blinkt dann nur in der Leiste. Der kurze Ausflug über Topmost bringt es in
/// den meisten Fällen trotzdem nach vorn; unter Linux entscheidet der Fenstermanager,
/// und manche sind da streng. Mehr als bitten kann eine Anwendung nicht, und mehr
/// sollte sie auch nicht tun.
///
private static void BringToFront(IClassicDesktopStyleApplicationLifetime desktop)
=> Dispatcher.UIThread.Post(() =>
{
if (desktop.MainWindow is not { } fenster) return;
if (fenster.WindowState == WindowState.Minimized)
fenster.WindowState = WindowState.Normal;
fenster.Show();
var vorher = fenster.Topmost;
fenster.Topmost = true;
fenster.Topmost = vorher;
fenster.Activate();
});
///
/// Instanzwechsel aus der laufenden Anwendung.
///
/// Ein Wechsel im laufenden Prozess ist nicht vorgesehen und waere auch keine
/// kleine Sache: Engine, Scanner, Telegram-Anbindung und Datenbank haengen alle an
/// einer Instanz. Statt das umzubauen startet sich die Anwendung mit
/// --instance neu — derselbe Weg, den auch das Update geht.
///
/// Reihenfolge wie dort: erst geordnet herunterfahren, dann den neuen Prozess
/// starten. Andersherum liefen kurz zwei Prozesse auf derselben Datenbank.
///
private enum StartModeChoice
{
LaunchManager,
Standalone,
Cancel
}
private enum StandaloneSwitchChoice
{
LaunchManager,
SwitchStandalone,
Cancel
}
///
/// Instanzwechsel aus der laufenden Anwendung.
///
private async Task SwitchInstanceAsync(IClassicDesktopStyleApplicationLifetime desktop)
{
if (_host is null) return;
if (_host.IsManaged)
{
await new AvaloniaLicensePrompt().ShowInfoAsync(
"ClawdDotNet – Instanzwechsel",
"Diese Instanz wird vom Instanzmanager geführt. Gewechselt wird dort — "
+ "er holt das Fenster der gewünschten Instanz nach vorn, statt eine "
+ "laufende zu beenden.");
return;
}
// Im Standalone-Modus den Benutzer fragen:
var switchChoice = await AskStandaloneSwitchModeAsync(desktop.MainWindow);
if (switchChoice == StandaloneSwitchChoice.LaunchManager)
{
LaunchInstanceManagerAndOpenBrowser();
return;
}
if (switchChoice == StandaloneSwitchChoice.Cancel)
{
return;
}
var viewModel = new InstancePickerViewModel(_host.Directories, _host.InstancePath);
var window = new InstancePickerWindow { DataContext = viewModel };
// Schliesst der Benutzer das Fenster, gilt das als Abbruch.
window.Closed += (_, _) => viewModel.CancelCommand.Execute(null);
// Umgekehrt: Sobald eine Wahl feststeht, schliesst sich das Fenster von selbst.
_ = viewModel.Result.ContinueWith(
_ => Dispatcher.UIThread.Post(window.Close), TaskScheduler.Default);
if (desktop.MainWindow is { } owner) await window.ShowDialog(owner);
else window.Show();
var path = await viewModel.Result;
if (path is null) return;
if (string.Equals(path, _host.InstancePath, StringComparison.OrdinalIgnoreCase)) return;
var executable = Environment.ProcessPath;
if (executable is null)
{
await new AvaloniaLicensePrompt().ShowErrorAsync(
"ClawdDotNet – Instanzwechsel",
"Der eigene Programmpfad ist nicht ermittelbar; ein Neustart ist von hier "
+ "aus nicht möglich. Bitte ClawdDotNet von Hand neu starten.");
return;
}
// Neue Instanz mit UseShellExecute = true starten (keine geerbten Datei- oder Socket-Handles)
try
{
Process.Start(new ProcessStartInfo(executable)
{
ArgumentList = { InstanceArgument, path },
UseShellExecute = true
});
}
catch (Exception ex)
{
await new AvaloniaLicensePrompt().ShowErrorAsync(
"ClawdDotNet – Instanzwechsel",
$"Neustart fehlgeschlagen: {ex.Message}");
return;
}
// Hauptfenster sofort verbergen, damit es nicht eingefroren sichtbar bleibt
if (desktop.MainWindow is { } mainWin)
{
mainWin.Hide();
}
await ShutdownWebAsync();
var hostToDispose = _host;
_host = null;
if (hostToDispose is not null)
{
await hostToDispose.DisposeAsync();
}
// Sauber beenden
await Dispatcher.UIThread.InvokeAsync(() =>
{
desktop.Shutdown(0);
Environment.Exit(0);
});
}
private static void LaunchInstanceManagerAndOpenBrowser()
{
var exePath = FindInstanceManagerExecutable();
if (exePath is null)
{
_ = new AvaloniaLicensePrompt().ShowErrorAsync(
"ClawdDotNet – Instanzmanager",
"Die Programmdatei des Instanzmanagers (ClawdDotNet.Instanzmanager.exe) wurde nicht gefunden.");
return;
}
try
{
Process.Start(new ProcessStartInfo(exePath)
{
UseShellExecute = true
});
SystemShell.OpenUrl($"http://localhost:{ManagerPort()}/#/instanzen");
}
catch (Exception ex)
{
_ = new AvaloniaLicensePrompt().ShowErrorAsync(
"ClawdDotNet – Instanzmanager",
$"Konnte Instanzmanager nicht starten: {ex.Message}");
}
}
///
/// Sucht die Programmdatei des Instanzmanagers.
///
/// Die Pfade werden aus Segmenten gebaut, nicht als Zeichenkette. Hier
/// stand @"..\..\..\..\ClawdDotNet.ManagerApp\bin\Debug\net10.0" — unter Linux,
/// dem einzigen Zielbetrieb, ist das kein Pfad, sondern ein einziger Dateiname mit
/// Rückstrichen darin, und die Suche ging dort nie auf.
///
/// Die Entwicklungspfade bleiben, weil im Betrieb beide Programme
/// nebeneinanderliegen und der erste Versuch trifft; sie kosten dort nur zwei
/// vergebliche File.Exists.
///
private static string? FindInstanceManagerExecutable()
{
var exeName = OperatingSystem.IsWindows()
? "ClawdDotNet.Instanzmanager.exe"
: "ClawdDotNet.Instanzmanager";
var nebenan = Path.Combine(AppContext.BaseDirectory, exeName);
if (File.Exists(nebenan)) return nebenan;
string[][] entwicklungspfade =
[
["..", "..", "..", "..", "ClawdDotNet.ManagerApp", "bin", "Debug", "net10.0"],
["..", "..", "..", "..", "..", "src", "ClawdDotNet.ManagerApp", "bin", "Debug", "net10.0"]
];
foreach (var segmente in entwicklungspfade)
{
var pfad = Path.GetFullPath(Path.Combine(
[AppContext.BaseDirectory, .. segmente, exeName]));
if (File.Exists(pfad)) return pfad;
}
return null;
}
///
/// Der Port, auf dem der Instanzmanager seine Oberfläche anbietet.
///
/// Aus manager.json gelesen und nicht fest eingetragen: Wer den Port
/// dort ändert, bekäme sonst einen Browser auf eine Adresse, an der nichts
/// antwortet. Diese Anwendung kennt den Typ hinter der Datei nicht — sie liest das
/// eine Feld, das sie braucht, und nimmt bei jedem Zweifel die Vorgabe.
///
private static int ManagerPort()
{
const int vorgabe = 7000;
try
{
var datei = Path.Combine(AppPaths.ConfigDirectory, "manager.json");
if (!File.Exists(datei)) return vorgabe;
using var doc = System.Text.Json.JsonDocument.Parse(File.ReadAllText(datei));
return doc.RootElement.TryGetProperty("webUiPort", out var port)
&& port.ValueKind == System.Text.Json.JsonValueKind.Number
&& port.TryGetInt32(out var wert)
&& wert is > 0 and < 65536
? wert
: vorgabe;
}
catch (Exception)
{
// Eine unlesbare Managerdatei ist kein Grund, den Browser gar nicht zu oeffnen.
return vorgabe;
}
}
private static async Task AskStandaloneSwitchModeAsync(Window? owner)
=> await Dispatcher.UIThread.InvokeAsync(async () =>
{
var completion = new TaskCompletionSource();
var l = LocalizationManager.Instance;
var managerBtn = new Button
{
Content = l["desktop.startMode.managerBtn"],
Classes = { "primary" }
};
var switchBtn = new Button
{
Content = l["desktop.standaloneSwitch.switchAnyway"]
};
var cancelBtn = new Button
{
Content = l["common.cancel"],
IsCancel = true
};
var window = new Window
{
Title = l["desktop.standaloneSwitch.title"],
Width = 540,
SizeToContent = SizeToContent.Height,
CanResize = false,
WindowStartupLocation = WindowStartupLocation.CenterScreen,
Icon = owner?.Icon,
Content = new StackPanel
{
Margin = new Thickness(20),
Spacing = 16,
Children =
{
new TextBlock
{
Text = l["desktop.standaloneSwitch.warning"],
Classes = { "h1" }
},
new TextBlock
{
Text = l["desktop.standaloneSwitch.text"],
TextWrapping = TextWrapping.Wrap
},
new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 10,
HorizontalAlignment = HorizontalAlignment.Right,
Children = { cancelBtn, switchBtn, managerBtn }
}
}
}
};
managerBtn.Click += (_, _) =>
{
completion.TrySetResult(StandaloneSwitchChoice.LaunchManager);
window.Close();
};
switchBtn.Click += (_, _) =>
{
completion.TrySetResult(StandaloneSwitchChoice.SwitchStandalone);
window.Close();
};
cancelBtn.Click += (_, _) =>
{
completion.TrySetResult(StandaloneSwitchChoice.Cancel);
window.Close();
};
window.Closed += (_, _) => completion.TrySetResult(StandaloneSwitchChoice.Cancel);
if (owner is not null)
await window.ShowDialog(owner);
else
window.Show();
return await completion.Task;
});
private static async Task AskStartModeAsync()
=> await Dispatcher.UIThread.InvokeAsync(async () =>
{
var completion = new TaskCompletionSource();
var l = LocalizationManager.Instance;
var managerBtn = new Button
{
Content = l["desktop.startMode.managerBtn"],
Classes = { "primary" }
};
var standaloneBtn = new Button
{
Content = l["desktop.startMode.standaloneBtn"]
};
var cancelBtn = new Button
{
Content = l["common.cancel"],
IsCancel = true
};
var window = new Window
{
Title = l["desktop.startMode.title"],
Width = 540,
SizeToContent = SizeToContent.Height,
CanResize = false,
WindowStartupLocation = WindowStartupLocation.CenterScreen,
Content = new StackPanel
{
Margin = new Thickness(22),
Spacing = 16,
Children =
{
new TextBlock
{
Text = l["desktop.startMode.question"],
Classes = { "h1" }
},
new TextBlock
{
Text = l["desktop.startMode.explanation"],
TextWrapping = TextWrapping.Wrap
},
new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 10,
HorizontalAlignment = HorizontalAlignment.Right,
Children = { cancelBtn, standaloneBtn, managerBtn }
}
}
}
};
managerBtn.Click += (_, _) =>
{
completion.TrySetResult(StartModeChoice.LaunchManager);
window.Close();
};
standaloneBtn.Click += (_, _) =>
{
completion.TrySetResult(StartModeChoice.Standalone);
window.Close();
};
cancelBtn.Click += (_, _) =>
{
completion.TrySetResult(StartModeChoice.Cancel);
window.Close();
};
window.Closed += (_, _) => completion.TrySetResult(StartModeChoice.Cancel);
window.Show();
return await completion.Task;
});
private int _beendetSich;
///
/// Das geordnete Ende dieser Instanz — der eine Weg, egal wer darum gebeten hat.
///
/// Erst auslaufen lassen, dann abschneiden. Laufende Agenten und
/// beanspruchte Aufgaben bekommen ihre Frist
/// (), bevor Weboberfläche und Aufbau fallen. Ohne
/// das endete eine Aufgabe mitten in ProcessAsync und bliebe bis zum Ablauf
/// ihres Lease auf running stehen.
///
/// Und nicht im Rückruf von desktop.ShutdownRequested. Avalonia
/// wartet dort nicht auf async-Behandlungen; was länger dauert als ein
/// Wimpernschlag, läuft gegen einen Prozess, der schon weg ist. Deshalb erst
/// aufräumen und dann Shutdown rufen — der Rückruf dort bleibt als
/// zweites Schloss stehen und tut dank der Sperren beim zweiten Mal nichts.
///
private async Task BeendeGeordnetAsync(
IClassicDesktopStyleApplicationLifetime desktop, string grund)
{
if (Interlocked.Exchange(ref _beendetSich, 1) != 0) return;
var host = _host;
if (host is not null)
{
host.LoggerFactory.CreateLogger("ClawdDotNet.Shutdown")
.LogInformation("Beendigung angefordert ({Grund}).", grund);
try { await host.Shutdown.BeginAsync(); }
catch (Exception) { /* Beim Beenden zaehlt, dass alle Schritte drankommen */ }
}
await ShutdownWebAsync();
if (host is not null) await host.DisposeAsync();
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown());
}
///
/// Faehrt die Weboberflaeche herunter, falls sie laeuft, und merkt sich, dass sie
/// weg ist. Mehrfach aufrufbar: Update, Instanzwechsel und das gewoehnliche
/// Beenden nehmen denselben Weg, und der letzte von ihnen soll nicht ins Leere
/// greifen.
///
private async Task ShutdownWebAsync()
{
if (_web is null) return;
var web = _web;
_web = null;
await web.DisposeAsync();
}
///
/// Instanzauswahl beim Start.
///
private async Task SelectInstanceAsync(InstanceDirectoryManager directories)
{
var preselected = _preselectedInstancePath;
_preselectedInstancePath = null;
if (preselected is not null && Directory.Exists(preselected))
return preselected;
// Hier steht bewusst der Schalter und nicht der geprüfte Befund: Diese Methode
// läuft mitten im Aufbau, bevor es einen AppHost gibt, der geprüft haben könnte.
// Sie entscheidet auch nur, ob ein Dialog erscheint — wer --managed ohne gültige
// Unterschrift setzt, bekommt ihn nicht und wird gleich darauf trotzdem nach
// seiner Lizenz gefragt (#535).
if (!_managed)
{
var choice = await AskStartModeAsync();
if (choice == StartModeChoice.LaunchManager)
{
LaunchInstanceManagerAndOpenBrowser();
return null;
}
if (choice == StartModeChoice.Cancel)
{
return null;
}
}
return await ShowInstancePickerAsync(directories);
}
private static async Task ShowInstancePickerAsync(InstanceDirectoryManager directories)
=> await Dispatcher.UIThread.InvokeAsync(async () =>
{
var viewModel = new InstancePickerViewModel(directories);
var window = new InstancePickerWindow { DataContext = viewModel };
// Schließt der Benutzer das Fenster, gilt das als Abbruch — sonst wartete
// der Start für immer auf eine Auswahl, die nie kommt.
window.Closed += (_, _) => viewModel.CancelCommand.Execute(null);
window.Show();
var path = await viewModel.Result;
window.Close();
return path;
});
///
/// Einzeilige Abfrage — ersetzt Microsoft.VisualBasic.Interaction.InputBox,
/// das die Telegram-Anmeldung an Windows band.
///
private static async Task AskAsync(string title, string prompt)
=> await Dispatcher.UIThread.InvokeAsync(async () =>
{
var completion = new TaskCompletionSource();
var input = new TextBox();
var ok = new Button { Content = LocalizationManager.Instance["common.ok"], IsDefault = true };
var window = new Window
{
Title = title,
Width = 420,
SizeToContent = SizeToContent.Height,
CanResize = false,
WindowStartupLocation = WindowStartupLocation.CenterScreen,
Content = new StackPanel
{
Margin = new Thickness(20),
Spacing = 12,
Children =
{
new TextBlock { Text = prompt, TextWrapping = Avalonia.Media.TextWrapping.Wrap },
input,
ok
}
}
};
ok.Click += (_, _) => window.Close();
window.Closed += (_, _) => completion.TrySetResult(input.Text?.Trim() ?? "");
window.Show();
input.Focus();
return await completion.Task;
});
}