using System.Text; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// L16-2: Docker 컨테이너·이미지 조회 핸들러. "docker" 프리픽스로 사용합니다. /// /// 예: docker → 실행 중 컨테이너 목록 /// docker all → 모든 컨테이너 (중지 포함) /// docker images → 로컬 이미지 목록 /// docker ps → 컨테이너 목록 (docker ps 동일) /// docker stop → 컨테이너 중지 /// docker start → 컨테이너 시작 /// docker logs → 컨테이너 로그 (터미널) /// docker shell → 컨테이너 shell 접속 /// Enter → 명령 실행 또는 컨테이너 ID 복사. /// public class DockerHandler : IActionHandler { public string? Prefix => "docker"; public PluginMetadata Metadata => new( "Docker", "Docker 컨테이너·이미지 조회 — 시작·중지·로그·쉘", "1.0", "AX"); public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); var items = new List(); if (!IsDockerAvailable()) { items.Add(new LauncherItem("Docker를 찾을 수 없습니다", "Docker Desktop이 설치되어 있는지 확인하세요", null, null, Symbol: "\uE756")); items.Add(new LauncherItem("Docker Desktop 설치", "https://www.docker.com/products/docker-desktop", null, ("open_url", "https://www.docker.com/products/docker-desktop"), Symbol: "\uE756")); return Task.FromResult>(items); } if (string.IsNullOrWhiteSpace(q)) { var containers = GetContainers(running: true); items.Add(new LauncherItem( $"실행 중 컨테이너 {containers.Count}개", "docker ps / docker all / docker images", null, null, Symbol: "\uE756")); if (containers.Count == 0) items.Add(new LauncherItem("실행 중인 컨테이너 없음", "docker all → 전체 목록", null, null, Symbol: "\uE946")); else foreach (var c in containers) items.Add(MakeContainerItem(c)); items.Add(new LauncherItem("docker images", "로컬 이미지 목록", null, ("sub", "images"), Symbol: "\uE756")); items.Add(new LauncherItem("docker all", "모든 컨테이너 목록", null, ("sub", "all"), Symbol: "\uE756")); return Task.FromResult>(items); } var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries); var sub = parts[0].ToLowerInvariant(); switch (sub) { case "all": case "ps": { var all = sub == "all"; var containers = GetContainers(running: !all); items.Add(new LauncherItem( $"{(all ? "전체" : "실행 중")} 컨테이너 {containers.Count}개", "", null, null, Symbol: "\uE756")); foreach (var c in containers) items.Add(MakeContainerItem(c)); if (containers.Count == 0) items.Add(new LauncherItem("컨테이너 없음", "", null, null, Symbol: "\uE946")); break; } case "images": case "image": case "img": { var images = GetImages(); items.Add(new LauncherItem($"로컬 이미지 {images.Count}개", "", null, null, Symbol: "\uE756")); foreach (var img in images) items.Add(MakeImageItem(img)); if (images.Count == 0) items.Add(new LauncherItem("이미지 없음", "docker pull <이름> 으로 받기", null, null, Symbol: "\uE946")); break; } case "stop": { var name = parts.Length > 1 ? parts[1] : ""; if (string.IsNullOrWhiteSpace(name)) { // 실행 중 컨테이너 목록 표시 → 클릭 시 stop var running = GetContainers(running: true); items.Add(new LauncherItem("중지할 컨테이너 선택", "Enter → 중지", null, null, Symbol: "\uE756")); foreach (var c in running) items.Add(new LauncherItem($"중지: {c.Name}", c.Image, null, ("stop", c.Id), Symbol: "\uE756")); } else { items.Add(new LauncherItem($"컨테이너 중지: {name}", $"docker stop {name} · Enter 실행", null, ("stop", name), Symbol: "\uE756")); } break; } case "start": { var name = parts.Length > 1 ? parts[1] : ""; if (string.IsNullOrWhiteSpace(name)) { var stopped = GetContainers(running: false, stopped: true); items.Add(new LauncherItem("시작할 컨테이너 선택", "Enter → 시작", null, null, Symbol: "\uE756")); foreach (var c in stopped) items.Add(new LauncherItem($"시작: {c.Name}", c.Image, null, ("start", c.Id), Symbol: "\uE756")); } else { items.Add(new LauncherItem($"컨테이너 시작: {name}", $"docker start {name} · Enter 실행", null, ("start", name), Symbol: "\uE756")); } break; } case "logs": case "log": { var name = parts.Length > 1 ? parts[1] : ""; if (string.IsNullOrWhiteSpace(name)) { var running = GetContainers(running: true); foreach (var c in running) items.Add(new LauncherItem($"로그: {c.Name}", "Enter → 터미널에서 로그 보기", null, ("logs", c.Name), Symbol: "\uE756")); } else { items.Add(new LauncherItem($"로그: {name}", $"docker logs -f {name}", null, ("logs", name), Symbol: "\uE756")); } break; } case "shell": case "exec": case "sh": { var name = parts.Length > 1 ? parts[1] : ""; if (string.IsNullOrWhiteSpace(name)) { var running = GetContainers(running: true); foreach (var c in running) items.Add(new LauncherItem($"쉘: {c.Name}", "Enter → 컨테이너 shell 접속", null, ("shell", c.Name), Symbol: "\uE756")); } else { items.Add(new LauncherItem($"쉘 접속: {name}", $"docker exec -it {name} sh", null, ("shell", name), Symbol: "\uE756")); } break; } default: { // 컨테이너 이름 검색 var all = GetContainers(running: false, stopped: true, all: true); var found = all.Where(c => c.Name.Contains(q, StringComparison.OrdinalIgnoreCase) || c.Image.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList(); if (found.Count > 0) foreach (var c in found) items.Add(MakeContainerItem(c)); else items.Add(new LauncherItem($"'{q}' 컨테이너 없음", "docker all → 전체 목록", null, null, Symbol: "\uE946")); break; } } return Task.FromResult>(items); } public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { switch (item.Data) { case ("stop", string id): RunDockerSilent($"stop {id}"); NotificationService.Notify("Docker", $"중지: {id}"); break; case ("start", string id): RunDockerSilent($"start {id}"); NotificationService.Notify("Docker", $"시작: {id}"); break; case ("logs", string name): RunInTerminal($"docker logs -f {name}"); break; case ("shell", string name): RunInTerminal($"docker exec -it {name} sh"); break; case ("copy", string text): try { System.Windows.Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text)); NotificationService.Notify("Docker", "복사됨"); } catch { /* 비핵심 */ } break; case ("open_url", string url): try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = url, UseShellExecute = true }); } catch { /* 비핵심 */ } break; } return Task.CompletedTask; } // ── Docker 조회 ────────────────────────────────────────────────────────── private record DockerContainer(string Id, string Name, string Image, string Status, string Ports); private record DockerImage(string Repository, string Tag, string Id, string Size, string Created); private static List GetContainers(bool running = true, bool stopped = false, bool all = false) { var result = new List(); try { var filter = all || (!running && stopped) ? "-a" : (running ? "" : "--filter status=exited"); var output = RunDockerOutput($"ps {filter} --format \"{{{{.ID}}}}\\t{{{{.Names}}}}\\t{{{{.Image}}}}\\t{{{{.Status}}}}\\t{{{{.Ports}}}}\""); foreach (var line in output.Split('\n')) { var trimmed = line.Trim(); if (string.IsNullOrWhiteSpace(trimmed)) continue; var cols = trimmed.Split('\t'); if (cols.Length < 4) continue; result.Add(new DockerContainer( Id: cols[0], Name: cols[1], Image: cols[2], Status: cols[3], Ports: cols.Length > 4 ? cols[4] : "")); } } catch { /* Docker 없음 */ } return result; } private static List GetImages() { var result = new List(); try { var output = RunDockerOutput("images --format \"{{.Repository}}\\t{{.Tag}}\\t{{.ID}}\\t{{.Size}}\\t{{.CreatedSince}}\""); foreach (var line in output.Split('\n')) { var trimmed = line.Trim(); if (string.IsNullOrWhiteSpace(trimmed)) continue; var cols = trimmed.Split('\t'); if (cols.Length < 4) continue; result.Add(new DockerImage( Repository: cols[0], Tag: cols.Length > 1 ? cols[1] : "latest", Id: cols.Length > 2 ? cols[2] : "", Size: cols.Length > 3 ? cols[3] : "", Created: cols.Length > 4 ? cols[4] : "")); } } catch { /* Docker 없음 */ } return result; } private static string RunDockerOutput(string args) { var psi = new System.Diagnostics.ProcessStartInfo { FileName = "docker", Arguments = args, UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true, StandardOutputEncoding = Encoding.UTF8, }; using var proc = System.Diagnostics.Process.Start(psi); if (proc == null) return ""; var output = proc.StandardOutput.ReadToEnd(); proc.WaitForExit(5000); return output; } private static void RunDockerSilent(string args) { try { var psi = new System.Diagnostics.ProcessStartInfo { FileName = "docker", Arguments = args, UseShellExecute = false, CreateNoWindow = true, }; using var proc = System.Diagnostics.Process.Start(psi); proc?.WaitForExit(10000); } catch { /* 비핵심 */ } } private static bool IsDockerAvailable() { try { var psi = new System.Diagnostics.ProcessStartInfo { FileName = "docker", Arguments = "version --format json", UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, }; using var proc = System.Diagnostics.Process.Start(psi); proc?.WaitForExit(3000); return proc?.ExitCode == 0; } catch { return false; } } private static LauncherItem MakeContainerItem(DockerContainer c) { var isRunning = c.Status.StartsWith("Up", StringComparison.OrdinalIgnoreCase); var icon = isRunning ? "\uE768" : "\uE71A"; var ports = string.IsNullOrWhiteSpace(c.Ports) ? "" : $" · {c.Ports}"; return new LauncherItem(c.Name, $"{c.Status}{ports} · {c.Image}", null, ("copy", c.Id), Symbol: icon); } private static LauncherItem MakeImageItem(DockerImage img) { var name = img.Tag == "" ? img.Repository : $"{img.Repository}:{img.Tag}"; return new LauncherItem(name, $"{img.Size} · {img.Created} · {img.Id[..Math.Min(12, img.Id.Length)]}", null, ("copy", name), Symbol: "\uE756"); } private static void RunInTerminal(string cmd) { try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = "cmd", Arguments = $"/K {cmd}", UseShellExecute = true, }); } catch { /* 비핵심 */ } } }