using System.Diagnostics; using System.Net.NetworkInformation; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// 포트/프로세스 점검 핸들러. "port " 프리픽스로 사용합니다. /// 예: port → 활성 TCP 연결 목록 /// port 8080 → 8080 포트를 점유 중인 프로세스 상세 /// port chrome → chrome이 사용하는 포트 목록 /// public class PortHandler : IActionHandler { public string? Prefix => "port"; public PluginMetadata Metadata => new( "PortChecker", "포트 & 프로세스 점검 — port 뒤에 포트번호 또는 프로세스명", "1.0", "AX"); // 프로세스 이름 캐시 (PID → 이름), 5초 유효 private static readonly Dictionary _procCache = new(); // netstat 결과 캐시 (포트 → PID), 5초 유효 — netstat 단일 실행으로 N+1 해결 private static readonly Dictionary _pidMap = new(); private static DateTime _cacheExpiry = DateTime.MinValue; public Task> 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>( [ 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(); if (!items.Any()) items.Add(new LauncherItem("활성 연결 없음", "TCP 연결이 감지되지 않았습니다", null, null, Symbol: Symbols.Network)); return Task.FromResult>(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>( [ 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(); return Task.FromResult>(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(); if (!procPorts.Any()) procPorts.Add(new LauncherItem( $"'{q}' — 연결 없음", "해당 프로세스의 TCP 연결이 없습니다", null, null, Symbol: Symbols.Warning)); return Task.FromResult>(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 캐시 헬퍼 ────────────────────────────────────────────────── /// /// 프로세스 목록 + netstat PID 맵을 한 번에 갱신 (5초 캐시). /// GetItemsAsync 진입 시 한 번만 호출하여 N+1 netstat 실행 방지. /// 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 : "알 수 없음"; } /// /// 캐시된 pidMap에서 즉시 반환 — netstat를 추가로 실행하지 않음. /// private static int GetPidForPort(int port) => _pidMap.TryGetValue(port, out var pid) ? pid : -1; }