Queue/Task(s)/Gate in deutschem Fliesstext auf Warteschlange/Aufgabe(n)/ Limit-Stopp vereinheitlicht, columnTask-Keys auf AUFGABE angeglichen, Kill-Tonalitaet (closeSession/killSessionTip) ehrlich-destruktiv gemacht. RepoImportModalViewModel.CreateButtonText und alle Installer-Check-Messages (Git/GitIdentity/WriteAccess/ClaudeCli/ClaudeVersion/ClaudeAuth/ PermissionModeAuto/Ports) ueber Locale-Keys geroutet statt hartem Englisch, mit dem bestehenden TrExtension.Localizer-Fallback-Muster.
78 lines
2.6 KiB
C#
78 lines
2.6 KiB
C#
using System.Net;
|
|
using System.Net.Sockets;
|
|
using ClaudeDo.Installer.Checks.Interfaces;
|
|
using ClaudeDo.Installer.Core;
|
|
using ClaudeDo.Installer.Localization;
|
|
|
|
namespace ClaudeDo.Installer.Checks;
|
|
|
|
/// <summary>Both ports are configurable in the installer settings, so a conflict is only a warning.</summary>
|
|
public sealed class PortCheck : IEnvironmentCheck
|
|
{
|
|
public const string CheckId = "ports";
|
|
|
|
private readonly IPortOwnerResolver _portOwnerResolver;
|
|
|
|
public PortCheck(IPortOwnerResolver portOwnerResolver) => _portOwnerResolver = portOwnerResolver;
|
|
|
|
public string Id => CheckId;
|
|
public CheckSeverity Severity => CheckSeverity.Warning;
|
|
|
|
public async Task<CheckResult> RunAsync(InstallContext ctx, CancellationToken ct)
|
|
{
|
|
var loc = TrExtension.Localizer;
|
|
var ports = new (string Label, int Port)[]
|
|
{
|
|
("SignalR", ctx.SignalRPort),
|
|
("External MCP", ctx.ExternalMcpPort),
|
|
};
|
|
|
|
var blocked = new List<string>();
|
|
var ownWorker = new List<string>();
|
|
|
|
foreach (var (label, port) in ports)
|
|
{
|
|
if (IsFree(port)) continue;
|
|
|
|
var owner = await _portOwnerResolver.FindOwningProcessNameAsync(port, ct);
|
|
if (owner is not null && owner.Contains("ClaudeDo.Worker", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
ownWorker.Add($"{label} ({port})");
|
|
continue;
|
|
}
|
|
|
|
blocked.Add(owner is null
|
|
? loc?.Get("checks.ports.inUse", label, port) ?? $"{label} port {port} is in use."
|
|
: loc?.Get("checks.ports.inUseBy", label, port, owner) ?? $"{label} port {port} is in use by '{owner}'.");
|
|
}
|
|
|
|
if (blocked.Count > 0)
|
|
{
|
|
return CheckResult.Fail(Id, Severity, "checks.ports.title", string.Join(" ", blocked), "checks.ports.hint");
|
|
}
|
|
|
|
if (ownWorker.Count > 0)
|
|
{
|
|
var ownWorkerList = string.Join(", ", ownWorker);
|
|
return CheckResult.Ok(Id, Severity, "checks.ports.title",
|
|
loc?.Get("checks.ports.inUseByOwnWorker", ownWorkerList) ?? $"In use by the running ClaudeDo Worker: {ownWorkerList}.");
|
|
}
|
|
|
|
return CheckResult.Ok(Id, Severity, "checks.ports.title", loc?["checks.ports.available"] ?? "Ports are available.");
|
|
}
|
|
|
|
private static bool IsFree(int port)
|
|
{
|
|
try
|
|
{
|
|
using var listener = new TcpListener(IPAddress.Loopback, port);
|
|
listener.Start();
|
|
return true;
|
|
}
|
|
catch (SocketException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|