Files
AX-Copilot-Codex/src/AxCopilot/Handlers/PortHandler.cs

230 lines
8.8 KiB
C#

using System.Diagnostics;
using System.Net.NetworkInformation;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 포트/프로세스 점검 핸들러. "port " 프리픽스로 사용합니다.
/// 예: port → 활성 TCP 연결 목록
/// port 8080 → 8080 포트를 점유 중인 프로세스 상세
/// port chrome → chrome이 사용하는 포트 목록
/// </summary>
public class PortHandler : IActionHandler
{
public string? Prefix => "port";
public PluginMetadata Metadata => new(
"PortChecker",
"포트 & 프로세스 점검 — port 뒤에 포트번호 또는 프로세스명",
"1.0",
"AX");
// 프로세스 이름 캐시 (PID → 이름), 5초 유효
private static readonly Dictionary<int, string> _procCache = new();
// netstat 결과 캐시 (포트 → PID), 5초 유효 — netstat 단일 실행으로 N+1 해결
private static readonly Dictionary<int, int> _pidMap = new();
private static DateTime _cacheExpiry = DateTime.MinValue;
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
RefreshProcessCache();
TcpConnectionInformation[] tcpConns;
try
{
var props = IPGlobalProperties.GetIPGlobalProperties();
tcpConns = props.GetActiveTcpConnections();
}
catch (Exception ex)
{
LogService.Warn($"포트 조회 실패: {ex.Message}");
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem("네트워크 정보를 가져올 수 없습니다", ex.Message, null, null, Symbol: Symbols.Warning)
]);
}
var q = query.Trim();
// ─── 빈 쿼리: 활성 연결 상위 목록 ────────────────────────────────────
if (string.IsNullOrWhiteSpace(q))
{
var items = tcpConns
.Where(c => c.State == TcpState.Established || c.State == TcpState.Listen)
.OrderBy(c => c.LocalEndPoint.Port)
.Take(20)
.Select(c =>
{
var procName = GetProcessNameForPort(c.LocalEndPoint.Port);
var state = c.State == TcpState.Listen ? "LISTEN" : "ESTABLISHED";
return new LauncherItem(
$":{c.LocalEndPoint.Port} → {c.RemoteEndPoint}",
$"{state} · {procName} · Enter로 포트번호 복사",
null,
c.LocalEndPoint.Port.ToString(),
Symbol: Symbols.Network);
})
.ToList<LauncherItem>();
if (!items.Any())
items.Add(new LauncherItem("활성 연결 없음", "TCP 연결이 감지되지 않았습니다", null, null, Symbol: Symbols.Network));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// ─── 숫자: 포트 번호 검색 ─────────────────────────────────────────────
if (int.TryParse(q, out var portNum))
{
var matches = tcpConns
.Where(c => c.LocalEndPoint.Port == portNum || c.RemoteEndPoint.Port == portNum)
.ToList();
if (!matches.Any())
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
$"포트 {portNum} — 사용 중 아님",
"해당 포트를 사용하는 TCP 연결이 없습니다",
null,
portNum.ToString(),
Symbol: Symbols.Info)
]);
}
var result = matches.Select(c =>
{
var procName = GetProcessNameForPort(c.LocalEndPoint.Port);
var pid = GetPidForPort(c.LocalEndPoint.Port);
return new LauncherItem(
$":{c.LocalEndPoint.Port} ←→ {c.RemoteEndPoint}",
$"{c.State} · {procName} (PID {pid}) · Enter로 PID 복사",
null,
pid > 0 ? pid.ToString() : portNum.ToString(),
Symbol: Symbols.Network);
}).ToList<LauncherItem>();
return Task.FromResult<IEnumerable<LauncherItem>>(result);
}
// ─── 문자열: 프로세스명 검색 ──────────────────────────────────────────
var procLower = q.ToLowerInvariant();
var procPorts = tcpConns
.Where(c =>
{
var name = GetProcessNameForPort(c.LocalEndPoint.Port).ToLowerInvariant();
return name.Contains(procLower);
})
.Take(15)
.Select(c =>
{
var procName = GetProcessNameForPort(c.LocalEndPoint.Port);
return new LauncherItem(
$"{procName} : {c.LocalEndPoint.Port} → {c.RemoteEndPoint}",
$"{c.State} · Enter로 포트번호 복사",
null,
c.LocalEndPoint.Port.ToString(),
Symbol: Symbols.Network);
})
.ToList<LauncherItem>();
if (!procPorts.Any())
procPorts.Add(new LauncherItem(
$"'{q}' — 연결 없음",
"해당 프로세스의 TCP 연결이 없습니다",
null, null, Symbol: Symbols.Warning));
return Task.FromResult<IEnumerable<LauncherItem>>(procPorts);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string text)
{
try { System.Windows.Clipboard.SetText(text); } catch { }
}
return Task.CompletedTask;
}
// ─── 프로세스/PID 캐시 헬퍼 ──────────────────────────────────────────────────
/// <summary>
/// 프로세스 목록 + netstat PID 맵을 한 번에 갱신 (5초 캐시).
/// GetItemsAsync 진입 시 한 번만 호출하여 N+1 netstat 실행 방지.
/// </summary>
private static void RefreshProcessCache()
{
if (DateTime.Now < _cacheExpiry) return;
_procCache.Clear();
_pidMap.Clear();
// ① 프로세스 목록 (PID → 이름)
try
{
foreach (var p in Process.GetProcesses())
{
try { _procCache[p.Id] = p.ProcessName; }
catch { }
}
}
catch (Exception ex)
{
LogService.Warn($"프로세스 목록 갱신 실패: {ex.Message}");
}
// ② netstat -ano 단 1회 실행 → 포트→PID 전체 맵 구축
try
{
var psi = new ProcessStartInfo("netstat", "-ano")
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var proc = Process.Start(psi);
if (proc != null)
{
var output = proc.StandardOutput.ReadToEnd();
proc.WaitForExit(2000);
foreach (var line in output.Split('\n'))
{
var parts = line.Trim().Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries);
// 형식: Proto Local Address Foreign Address State PID
if (parts.Length < 5) continue;
if (!int.TryParse(parts[^1], out var pid)) continue;
// Local Address에서 포트 추출 (예: 0.0.0.0:8080 또는 [::]:8080)
var localAddr = parts[1];
var colonIdx = localAddr.LastIndexOf(':');
if (colonIdx >= 0 && int.TryParse(localAddr[(colonIdx + 1)..], out var port))
_pidMap.TryAdd(port, pid);
}
}
}
catch (Exception ex)
{
LogService.Warn($"netstat 실행 실패: {ex.Message}");
}
_cacheExpiry = DateTime.Now.AddSeconds(5);
}
private static string GetProcessNameForPort(int port)
{
var pid = GetPidForPort(port);
return pid > 0 && _procCache.TryGetValue(pid, out var name) ? name : "알 수 없음";
}
/// <summary>
/// 캐시된 pidMap에서 즉시 반환 — netstat를 추가로 실행하지 않음.
/// </summary>
private static int GetPidForPort(int port)
=> _pidMap.TryGetValue(port, out var pid) ? pid : -1;
}