Initial commit to new repository
This commit is contained in:
228
src/AxCopilot/Handlers/UninstallHandler.cs
Normal file
228
src/AxCopilot/Handlers/UninstallHandler.cs
Normal file
@@ -0,0 +1,228 @@
|
||||
using Microsoft.Win32;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 앱 제거 핸들러. "uninstall" 프리픽스로 사용합니다.
|
||||
/// 예: uninstall → 설치된 앱 전체 목록
|
||||
/// uninstall chrome → "chrome" 포함 앱 필터
|
||||
/// Enter 시 해당 앱의 제거 프로그램 실행.
|
||||
/// </summary>
|
||||
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<InstalledApp> Apps)? _cache;
|
||||
private static readonly TimeSpan CacheTtl = TimeSpan.FromSeconds(30);
|
||||
|
||||
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var q = query.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
{
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(
|
||||
[
|
||||
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<IEnumerable<LauncherItem>>(
|
||||
[
|
||||
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<IEnumerable<LauncherItem>>(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<string>();
|
||||
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<InstalledApp> GetInstalledApps()
|
||||
{
|
||||
if (_cache.HasValue && (DateTime.Now - _cache.Value.At) < CacheTtl)
|
||||
return _cache.Value.Apps;
|
||||
|
||||
var result = new List<InstalledApp>();
|
||||
var seen = new HashSet<string>(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; }
|
||||
}
|
||||
Reference in New Issue
Block a user