using System.Net;
using System.Net.Sockets;
using ClaudeDo.Installer.Checks.Interfaces;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Localization;
namespace ClaudeDo.Installer.Checks;
/// Both ports are configurable in the installer settings, so a conflict is only a warning.
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 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();
var ownWorker = new List();
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;
}
}
}