using System.Runtime.InteropServices; using ClawdDotNet.App; using ClawdDotNet.App.Services; using ClawdDotNet.Licensing; using ClawdDotNet.Web; using ClawdDotNet.Web.Endpoints; using Microsoft.Extensions.Logging; namespace ClawdDotNet.Desktop; /// /// An instance without a window (#528, stage 2). /// /// What for, if every instance keeps its window anyway (E-M2). Because the /// instance process thereby proves that it does not need one. A server without a /// screen session, a systemd service, a test run in the build — all three need exactly /// that, and every accidental dependency on the user interface shows up here immediately, /// instead of only on the target machine. /// /// What is not asked here is not asked at all. A service without a /// session that opens an input window at startup hangs forever without anyone /// ever getting to see the question. The instance therefore comes from the argument, the license /// via , and the Telegram login stays /// off — Callbacks.TelegramLogin is null, and that is the answer here, not an /// omission. /// /// Under Windows you see little of it. The executable is a /// WinExe and has no console; the messages below then go nowhere. What /// matters is in the instance log anyway. The target platform is Linux, and there it is /// an ordinary console application. /// internal static class HeadlessRun { private const int ExitOk = 0; private const int ExitNoInstance = 2; private const int ExitStartFailed = 3; private const int ExitNoEndpoint = 4; public static async Task RunAsync(string[] args) { var instancePath = CommandLine.ReadInstancePath(args); if (string.IsNullOrWhiteSpace(instancePath) || !Directory.Exists(instancePath)) { await Console.Error.WriteLineAsync( $"Ohne Fenster gibt es keinen Auswahldialog. Erwartet wird " + $"{CommandLine.InstanceArgument} " + (instancePath is null ? "." : $" — '{instancePath}' gibt es nicht.")); return ExitNoInstance; } var (host, error) = await AppHost.StartAsync(new AppHost.Callbacks { SelectInstance = _ => Task.FromResult(instancePath), // Does not ask, but reports what to do. In managed mode (E-M3) // the local check is dropped; the manager checks centrally. License = new NonInteractiveLicensePrompt(Console.Error.WriteLine), IsManaged = CommandLine.IsManaged(args) }); if (host is null) { // No error text means abort with an announcement - the license prompt has already written its // message itself. if (error is not null) await Console.Error.WriteLineAsync(error); return ExitStartFailed; } await using (host) { var logger = host.LoggerFactory.CreateLogger("ClawdDotNet.Headless"); var web = InstanceHttpHost.TryStart(host, host.LoggerFactory); if (web is null) { // Without a socket a headless instance cannot be addressed - so nothing // you should let run. With a window that would be different: there the // user interface remains as access. logger.LogError("Kein Zugang: Die HTTP-Fläche ist nicht hochgekommen."); return ExitNoEndpoint; } await using (web) { Console.WriteLine(web.Endpoint); logger.LogInformation( "Instanz {Name} läuft ohne Fenster auf {Endpunkt}", host.Instance.InstanceName, web.Endpoint); await WaitForShutdownAsync(logger, web.Control); logger.LogInformation("Instanz {Name} wird beendet.", host.Instance.InstanceName); } } return ExitOk; } /// /// Waits until someone says stop. /// /// Four ways, because four senders come into question: the request of the instance manager /// via the socket (the normal case), SIGTERM from systemd, SIGINT from the /// terminal, and as the last opportunity. /// /// The signal is caught, not let through /// (context.Cancel = true). Otherwise the runtime would tear down the process while /// the orderly shutdown is still running — and exactly then the write buffer /// of the log is flushed. An end without these lines is the state from #529. /// private static async Task WaitForShutdownAsync(ILogger logger, InstanceControl control) { var conclusion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); void Shutdown(string reason) { if (conclusion.TrySetResult()) logger.LogInformation("Beendigung angefordert ({Grund}).", reason); } // The instance manager, via the socket. The usual way, as soon as it exists: // it asks instead of shooting - an instance process holds an open database // and running agents, and both want to be brought to an end. control.ShutdownRequested += () => Shutdown("Instanzmanager"); using var sigterm = PosixSignalRegistration.Create(PosixSignal.SIGTERM, context => { context.Cancel = true; Shutdown("SIGTERM"); }); using var sigint = PosixSignalRegistration.Create(PosixSignal.SIGINT, context => { context.Cancel = true; Shutdown("SIGINT"); }); AppDomain.CurrentDomain.ProcessExit += (_, _) => Shutdown("ProcessExit"); await conclusion.Task; } }