230 lines
8.8 KiB
C#
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;
|
|
}
|