using Microsoft.Win32; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// 앱 제거 핸들러. "uninstall" 프리픽스로 사용합니다. /// 예: uninstall → 설치된 앱 전체 목록 /// uninstall chrome → "chrome" 포함 앱 필터 /// Enter 시 해당 앱의 제거 프로그램 실행. /// public class UninstallHandler : IActionHandler { public string? Prefix => "uninstall"; public PluginMetadata Metadata => new( "Uninstall", "앱 제거 — uninstall 뒤에 앱 이름 입력", "1.0", "AX"); // 레지스트리 언인스톨 키 경로 private static readonly string[] _regPaths = { @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall", }; // 캐시: 30초 private static (DateTime At, List Apps)? _cache; private static readonly TimeSpan CacheTtl = TimeSpan.FromSeconds(30); public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); if (string.IsNullOrWhiteSpace(q)) { return Task.FromResult>( [ new LauncherItem( "제거할 앱 이름을 입력하세요", "예: uninstall chrome · uninstall office", null, null, Symbol: Symbols.Uninstall) ]); } var apps = GetInstalledApps(); var filtered = apps .Where(a => a.Name.Contains(q, StringComparison.OrdinalIgnoreCase) || (a.Publisher?.Contains(q, StringComparison.OrdinalIgnoreCase) ?? false)) .OrderBy(a => a.Name) .Take(12) .ToList(); if (!filtered.Any()) { return Task.FromResult>( [ new LauncherItem( "검색 결과 없음", $"'{q}'에 해당하는 설치된 앱이 없습니다", null, null, Symbol: Symbols.Info) ]); } var items = filtered.Select(a => new LauncherItem( a.Name, BuildSubtitle(a), null, a, Symbol: Symbols.Uninstall)).ToList(); return Task.FromResult>(items); } public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { if (item.Data is not InstalledApp app) return Task.CompletedTask; if (string.IsNullOrWhiteSpace(app.UninstallString)) { LogService.Warn($"제거 문자열 없음: {app.Name}"); return Task.CompletedTask; } try { // UninstallString이 "msiexec /x {GUID}" 또는 경로+args 형태 var uninstall = app.UninstallString.Trim(); if (uninstall.StartsWith("msiexec", StringComparison.OrdinalIgnoreCase)) { // msiexec 직접 실행 var args = uninstall.Substring("msiexec".Length).Trim(); System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo( "msiexec.exe", args) { UseShellExecute = true }); } else { // 따옴표로 둘러싸인 경로 + 나머지 인수 분리 string exe, arguments; if (uninstall.StartsWith('"')) { var close = uninstall.IndexOf('"', 1); exe = close > 0 ? uninstall[1..close] : uninstall; arguments = close > 0 ? uninstall[(close + 1)..].Trim() : ""; } else { var spaceIdx = uninstall.IndexOf(' '); if (spaceIdx > 0) { exe = uninstall[..spaceIdx]; arguments = uninstall[(spaceIdx + 1)..]; } else { exe = uninstall; arguments = ""; } } var psi = new System.Diagnostics.ProcessStartInfo(exe, arguments) { UseShellExecute = true }; System.Diagnostics.Process.Start(psi); } } catch (Exception ex) { LogService.Warn($"제거 실행 실패 [{app.Name}]: {ex.Message}"); } return Task.CompletedTask; } // ─── 내부 ───────────────────────────────────────────────────────────────── private static string BuildSubtitle(InstalledApp a) { var parts = new List(); if (!string.IsNullOrWhiteSpace(a.Publisher)) parts.Add(a.Publisher!); if (!string.IsNullOrWhiteSpace(a.Version)) parts.Add($"v{a.Version}"); if (!string.IsNullOrWhiteSpace(a.InstallDate)) parts.Add(a.InstallDate!); parts.Add("Enter로 제거"); return string.Join(" · ", parts); } private static List GetInstalledApps() { if (_cache.HasValue && (DateTime.Now - _cache.Value.At) < CacheTtl) return _cache.Value.Apps; var result = new List(); var seen = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var root in new[] { Registry.LocalMachine, Registry.CurrentUser }) { foreach (var regPath in _regPaths) { try { using var key = root.OpenSubKey(regPath); if (key == null) continue; foreach (var subName in key.GetSubKeyNames()) { try { using var sub = key.OpenSubKey(subName); if (sub == null) continue; var name = sub.GetValue("DisplayName") as string; var uninstall = sub.GetValue("UninstallString") as string; // 이름·제거 문자열 없으면 스킵, 시스템 업데이트/패치 스킵 if (string.IsNullOrWhiteSpace(name)) continue; if (string.IsNullOrWhiteSpace(uninstall)) continue; if (name.StartsWith("KB", StringComparison.OrdinalIgnoreCase) && name.Length < 12) continue; if (!seen.Add(name)) continue; var systemComponent = sub.GetValue("SystemComponent"); if (systemComponent is int sc && sc == 1) continue; result.Add(new InstalledApp { Name = name, UninstallString = uninstall, Publisher = sub.GetValue("Publisher") as string, Version = sub.GetValue("DisplayVersion") as string, InstallDate = FormatDate(sub.GetValue("InstallDate") as string), }); } catch (Exception) { /* 개별 키 읽기 실패 무시 */ } } } catch (Exception) { /* 레지스트리 접근 실패 무시 */ } } } result.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); _cache = (DateTime.Now, result); return result; } private static string? FormatDate(string? raw) { // InstallDate 형식: "20230615" → "2023-06-15" if (raw?.Length == 8 && int.TryParse(raw, out _)) { return $"{raw[..4]}-{raw[4..6]}-{raw[6..8]}"; } return null; } } internal sealed class InstalledApp { public string Name { get; init; } = ""; public string? UninstallString { get; init; } public string? Publisher { get; init; } public string? Version { get; init; } public string? InstallDate { get; init; } }