using System.IO; using System.Text; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// L12-2: Windows hosts 파일 관리 핸들러. "hosts" 프리픽스로 사용합니다. /// /// 예: hosts → 현재 hosts 파일 항목 목록 /// hosts search dev → "dev" 포함 항목 필터 /// hosts open → hosts 파일을 메모장으로 열기 /// hosts copy → 전체 hosts 내용 클립보드 복사 /// Enter → 해당 항목을 클립보드에 복사. /// public class HostsHandler : IActionHandler { public string? Prefix => "hosts"; public PluginMetadata Metadata => new( "Hosts", "Windows hosts 파일 뷰어 — 항목 조회 · 검색 · 복사", "1.0", "AX"); private static readonly string HostsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), @"drivers\etc\hosts"); public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); var items = new List(); var entries = ReadHostsEntries(); if (string.IsNullOrWhiteSpace(q)) { var activeCount = entries.Count(e => !e.IsComment); var commentCount = entries.Count(e => e.IsComment && e.IsDisabledEntry); items.Add(new LauncherItem( $"hosts 파일 활성 {activeCount}개" + (commentCount > 0 ? $" · 비활성 {commentCount}개" : ""), HostsPath, null, ("copy_path", HostsPath), Symbol: "\uE8D2")); items.Add(new LauncherItem("파일 열기 (메모장)", HostsPath, null, ("open", HostsPath), Symbol: "\uE8A5")); items.Add(new LauncherItem("전체 내용 복사", $"{entries.Count}줄", null, ("copy_all", ""), Symbol: "\uE8A5")); // 유효 항목 목록 foreach (var e in entries.Where(e => !e.IsComment).Take(20)) items.Add(MakeEntryItem(e)); // 비활성 항목 (주석 처리된 IP 항목) var disabled = entries.Where(e => e.IsDisabledEntry).Take(5).ToList(); if (disabled.Count > 0) { items.Add(new LauncherItem($"── 비활성 항목 {disabled.Count}개 ──", "", null, null, Symbol: "\uE8D2")); foreach (var e in disabled) items.Add(MakeEntryItem(e)); } return Task.FromResult>(items); } var parts = q.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); var sub = parts[0].ToLowerInvariant(); switch (sub) { case "open": items.Add(new LauncherItem("메모장으로 열기", HostsPath, null, ("open", HostsPath), Symbol: "\uE8A5")); break; case "copy": case "copy_all": { var content = ReadHostsRaw(); items.Add(new LauncherItem("전체 내용 복사", $"{content.Split('\n').Length}줄 · Enter 복사", null, ("copy", content), Symbol: "\uE8A5")); break; } case "search": case "find": { var keyword = parts.Length > 1 ? parts[1].ToLowerInvariant() : ""; if (string.IsNullOrWhiteSpace(keyword)) { items.Add(new LauncherItem("검색어 입력", "예: hosts search dev", null, null, Symbol: "\uE783")); break; } var filtered = entries.Where(e => e.Hostname.Contains(keyword, StringComparison.OrdinalIgnoreCase) || e.IpAddress.Contains(keyword, StringComparison.OrdinalIgnoreCase)).ToList(); if (filtered.Count == 0) items.Add(new LauncherItem("결과 없음", $"'{keyword}' 항목 없음", null, null, Symbol: "\uE946")); else foreach (var e in filtered.Take(20)) items.Add(MakeEntryItem(e)); break; } default: { // 검색어로 처리 var keyword = q.ToLowerInvariant(); var filtered = entries.Where(e => e.Hostname.Contains(keyword, StringComparison.OrdinalIgnoreCase) || e.IpAddress.Contains(keyword, StringComparison.OrdinalIgnoreCase)).ToList(); if (filtered.Count == 0) items.Add(new LauncherItem("결과 없음", $"'{q}' 항목 없음", null, null, Symbol: "\uE946")); else foreach (var e in filtered.Take(20)) items.Add(MakeEntryItem(e)); break; } } return Task.FromResult>(items); } public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { switch (item.Data) { case ("copy", string text): try { System.Windows.Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text)); NotificationService.Notify("Hosts", "클립보드에 복사했습니다."); } catch { /* 비핵심 */ } break; case ("copy_path", string path): try { System.Windows.Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(path)); NotificationService.Notify("Hosts", "경로가 복사되었습니다."); } catch { /* 비핵심 */ } break; case ("copy_all", _): try { var content = ReadHostsRaw(); System.Windows.Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(content)); NotificationService.Notify("Hosts", $"hosts 파일 내용 복사됨 ({content.Split('\n').Length}줄)"); } catch { /* 비핵심 */ } break; case ("open", string path): try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = "notepad.exe", Arguments = $"\"{path}\"", UseShellExecute = false, }); } catch (Exception ex) { NotificationService.Notify("Hosts", $"열기 실패: {ex.Message}"); } break; } return Task.CompletedTask; } // ── hosts 파일 파서 ─────────────────────────────────────────────────────── private record HostsEntry(string IpAddress, string Hostname, string RawLine, bool IsComment, bool IsDisabledEntry); private static List ReadHostsEntries() { var result = new List(); string[] lines; try { lines = File.ReadAllLines(HostsPath, Encoding.UTF8); } catch { return result; } foreach (var rawLine in lines) { var line = rawLine.Trim(); if (string.IsNullOrWhiteSpace(line)) continue; // 순수 주석 (# 으로 시작, IP 없음) if (line.StartsWith('#')) { // 비활성 IP 항목인지 확인 (예: "# 127.0.0.1 example.com") var inner = line[1..].Trim(); if (TryParseIpEntry(inner, out var disIp, out var disHost)) { result.Add(new HostsEntry(disIp, disHost, rawLine, true, true)); } // 순수 주석은 목록에서 제외 continue; } // IP 항목 (인라인 주석 포함 가능) var withoutComment = line.Contains('#') ? line[..line.IndexOf('#')].Trim() : line; if (TryParseIpEntry(withoutComment, out var ip, out var host)) { result.Add(new HostsEntry(ip, host, rawLine, false, false)); } } return result; } private static bool TryParseIpEntry(string line, out string ip, out string host) { ip = host = ""; var parts = line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length < 2) return false; // 첫 토큰이 IP 주소 형태인지 간단히 확인 if (!System.Net.IPAddress.TryParse(parts[0], out _)) return false; ip = parts[0]; host = parts[1]; return true; } private static string ReadHostsRaw() { try { return File.ReadAllText(HostsPath, Encoding.UTF8); } catch { return "(hosts 파일을 읽을 수 없습니다)"; } } private static LauncherItem MakeEntryItem(HostsEntry e) { var prefix = e.IsComment ? "# " : ""; var icon = e.IsComment ? "\uE946" : "\uE8D2"; var subtitle = e.IsComment ? "비활성 항목" : e.IpAddress; return new LauncherItem( $"{prefix}{e.Hostname}", subtitle, null, ("copy", $"{e.IpAddress}\t{e.Hostname}"), Symbol: icon); } }