Stufe 2 (#764): Huelle ClawdDotNet.Shell mit eingebettetem Chromium
Avalonia 11.3.17 + CefGlue.Next 152 in eigenem Ordner shell/. Startet den Manager (ManagerStarter, nach Core gezogen), holt ueber den lokalen Socket einen Einmal-Link und zeigt die WebUI angemeldet; Taskleistensymbol mit Zustand. ClawdDotNet ohne Schalter startet die Huelle, sonst den Browser. publish.py baut sie mit (--no-shell zum Weglassen), unter Linux mit den Chromium-Ressourcen neben libcef.so. --selftest fuer die Pruefung ohne Blick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
775c4dbc8e
commit
c9c56ea53b
@@ -1,6 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net.Sockets;
|
||||
using System.Text.Json;
|
||||
using ClawdDotNet.Core.Instances;
|
||||
using ClawdDotNet.Core.Storage;
|
||||
|
||||
namespace ClawdDotNet.Desktop;
|
||||
@@ -11,47 +10,29 @@ namespace ClawdDotNet.Desktop;
|
||||
///
|
||||
/// <para>Until #764 that opened the Avalonia window. The windows are gone; the web interface is
|
||||
/// the only one. What a person who starts the program wants is therefore: the instance manager
|
||||
/// running, and its web interface in front of them. Until the shell with its own window exists
|
||||
/// (#764, stage 2) the system browser shows it.</para>
|
||||
/// running, and its web interface in front of them — in the shell's window
|
||||
/// (<c>shell/ClawdDotNet.Shell</c>) where the package brings it, otherwise in the system
|
||||
/// browser.</para>
|
||||
///
|
||||
/// <para><b>Never a second manager.</b> If the manager answers, it is left alone. If it runs as
|
||||
/// a systemd service on this machine, the service is started — starting the program next to it
|
||||
/// would put two managers on the same instances. Only otherwise is the manager program started
|
||||
/// directly.</para>
|
||||
/// <para>The shell is a program of its own, in its own folder: it carries Avalonia 11.3 and
|
||||
/// Chromium, and the instance process — started once per instance by the manager — should load
|
||||
/// neither.</para>
|
||||
/// </summary>
|
||||
internal static class LauncherRun
|
||||
{
|
||||
private const int ExitOk = 0;
|
||||
private const int ExitNoManager = 6;
|
||||
|
||||
private const int DefaultPort = 7000;
|
||||
private const string ServiceUnit = "clawddotnet-manager.service";
|
||||
private static readonly TimeSpan StartWait = TimeSpan.FromSeconds(30);
|
||||
|
||||
public static async Task<int> RunAsync()
|
||||
{
|
||||
var port = ManagerPort();
|
||||
if (StartShell())
|
||||
return ExitOk;
|
||||
|
||||
if (!Answers(port))
|
||||
var (port, problem) = await ManagerStarter.EnsureRunningAsync(AppContext.BaseDirectory);
|
||||
if (problem is not null)
|
||||
{
|
||||
var problem = StartManager();
|
||||
if (problem is not null)
|
||||
{
|
||||
await Console.Error.WriteLineAsync(problem);
|
||||
return ExitNoManager;
|
||||
}
|
||||
|
||||
var until = DateTime.UtcNow + StartWait;
|
||||
while (!Answers(port) && DateTime.UtcNow < until)
|
||||
await Task.Delay(500);
|
||||
|
||||
if (!Answers(port))
|
||||
{
|
||||
await Console.Error.WriteLineAsync(
|
||||
$"The instance manager did not answer on port {port} within {StartWait.TotalSeconds:0} s. "
|
||||
+ "Its log is under Logs/Manager in the data directory.");
|
||||
return ExitNoManager;
|
||||
}
|
||||
await Console.Error.WriteLineAsync(problem);
|
||||
return ExitNoManager;
|
||||
}
|
||||
|
||||
var url = $"http://localhost:{port}/";
|
||||
@@ -60,121 +41,23 @@ internal static class LauncherRun
|
||||
return ExitOk;
|
||||
}
|
||||
|
||||
/// <returns>Why the manager could not be started, or <c>null</c>.</returns>
|
||||
private static string? StartManager()
|
||||
/// <summary>The shell starts the manager itself and signs its window in.</summary>
|
||||
private static bool StartShell()
|
||||
{
|
||||
if (OperatingSystem.IsLinux() && Systemctl("cat", ServiceUnit) == 0)
|
||||
{
|
||||
return Systemctl("start", ServiceUnit) == 0
|
||||
? null
|
||||
: $"systemctl --user start {ServiceUnit} failed; see journalctl --user -u {ServiceUnit}.";
|
||||
}
|
||||
|
||||
if (FindManagerExecutable() is not { } manager)
|
||||
{
|
||||
return "The instance manager (ClawdDotNet.Instanzmanager) was not found next to this program.";
|
||||
}
|
||||
var name = OperatingSystem.IsWindows() ? "ClawdDotNet.Shell.exe" : "ClawdDotNet.Shell";
|
||||
var shell = Path.Combine(AppContext.BaseDirectory, "shell", name);
|
||||
if (!File.Exists(shell))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
// On Windows with its own console window: that is where the manager shows the access
|
||||
// token on first start. Elsewhere a plain child that outlives us.
|
||||
Process.Start(new ProcessStartInfo(manager)
|
||||
{
|
||||
UseShellExecute = OperatingSystem.IsWindows(),
|
||||
WorkingDirectory = Path.GetDirectoryName(manager)!
|
||||
});
|
||||
return null;
|
||||
Process.Start(new ProcessStartInfo(shell) { UseShellExecute = false, WorkingDirectory = Path.GetDirectoryName(shell)! });
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"The instance manager could not be started: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private static bool Answers(int port)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = new TcpClient();
|
||||
return client.ConnectAsync("127.0.0.1", port).Wait(TimeSpan.FromSeconds(1)) && client.Connected;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Console.Error.WriteLine($"The shell could not be started ({ex.Message}); opening the browser instead.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static int Systemctl(string verb, string unit)
|
||||
{
|
||||
try
|
||||
{
|
||||
var start = new ProcessStartInfo("systemctl") { RedirectStandardOutput = true, RedirectStandardError = true };
|
||||
start.ArgumentList.Add("--user");
|
||||
start.ArgumentList.Add(verb);
|
||||
start.ArgumentList.Add(unit);
|
||||
using var p = Process.Start(start)!;
|
||||
p.StandardOutput.ReadToEnd();
|
||||
p.StandardError.ReadToEnd();
|
||||
p.WaitForExit(30_000);
|
||||
return p.HasExited ? p.ExitCode : -1;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return -1; // no systemd here
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Next to this program in an installation; the build folders in a development checkout.
|
||||
///
|
||||
/// <para>Built from segments, not as a string: a path with backslashes is one file name on
|
||||
/// Linux, and the search never found anything there (moved here from <c>App.axaml.cs</c>).</para>
|
||||
/// </summary>
|
||||
private static string? FindManagerExecutable()
|
||||
{
|
||||
var name = OperatingSystem.IsWindows() ? "ClawdDotNet.Instanzmanager.exe" : "ClawdDotNet.Instanzmanager";
|
||||
|
||||
var nextTo = Path.Combine(AppContext.BaseDirectory, name);
|
||||
if (File.Exists(nextTo)) return nextTo;
|
||||
|
||||
string[][] developmentPaths =
|
||||
[
|
||||
["..", "..", "..", "..", "ClawdDotNet.ManagerApp", "bin", "Debug", "net10.0"],
|
||||
["..", "..", "..", "..", "..", "src", "ClawdDotNet.ManagerApp", "bin", "Debug", "net10.0"]
|
||||
];
|
||||
|
||||
foreach (var segments in developmentPaths)
|
||||
{
|
||||
var path = Path.GetFullPath(Path.Combine([AppContext.BaseDirectory, .. segments, name]));
|
||||
if (File.Exists(path)) return path;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The port from <c>manager.json</c>; the default on any doubt. Read as one field, not as
|
||||
/// the manager's settings type, which this program does not know.
|
||||
/// </summary>
|
||||
private static int ManagerPort()
|
||||
{
|
||||
try
|
||||
{
|
||||
var file = Path.Combine(AppPaths.ConfigDirectory, "manager.json");
|
||||
if (!File.Exists(file)) return DefaultPort;
|
||||
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(file));
|
||||
return doc.RootElement.TryGetProperty("webUiPort", out var port)
|
||||
&& port.ValueKind == JsonValueKind.Number
|
||||
&& port.TryGetInt32(out var value)
|
||||
&& value is > 0 and < 65536
|
||||
? value
|
||||
: DefaultPort;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return DefaultPort;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user