using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using AxCopilot.Core; using AxCopilot.Models; namespace AxCopilot.Services; public class IndexService : IDisposable { private readonly SettingsService _settings; private List _index = new List(); private readonly List _watchers = new List(); private Timer? _debounceTimer; private readonly object _timerLock = new object(); private const int DebounceMs = 3000; private readonly SemaphoreSlim _rebuildLock = new SemaphoreSlim(1, 1); public IReadOnlyList Entries => _index; public TimeSpan LastIndexDuration { get; private set; } public int LastIndexCount { get; private set; } public event EventHandler? IndexRebuilt; public IndexService(SettingsService settings) { _settings = settings; } public async Task BuildAsync(CancellationToken ct = default(CancellationToken)) { await _rebuildLock.WaitAsync(ct); Stopwatch sw = Stopwatch.StartNew(); try { List entries = new List(); IEnumerable paths = _settings.Settings.IndexPaths.Select((string p) => Environment.ExpandEnvironmentVariables(p)); HashSet allowedExts = new HashSet(_settings.Settings.IndexExtensions.Select((string e) => e.ToLowerInvariant().StartsWith(".") ? e.ToLowerInvariant() : ("." + e.ToLowerInvariant())), StringComparer.OrdinalIgnoreCase); string indexSpeed = _settings.Settings.IndexSpeed ?? "normal"; foreach (string dir in paths) { if (Directory.Exists(dir)) { await ScanDirectoryAsync(dir, entries, allowedExts, indexSpeed, ct); } } foreach (AliasEntry alias in _settings.Settings.Aliases) { List list = entries; IndexEntry indexEntry = new IndexEntry { Name = alias.Key, DisplayName = (alias.Description ?? alias.Key), Path = alias.Target, AliasType = alias.Type }; IndexEntry indexEntry2 = indexEntry; string type = alias.Type; if (1 == 0) { } string text = type; IndexEntryType type2 = ((!(text == "app")) ? ((!(text == "folder")) ? IndexEntryType.Alias : IndexEntryType.Folder) : IndexEntryType.App); if (1 == 0) { } indexEntry2.Type = type2; indexEntry.Score = 100; list.Add(indexEntry); } RegisterBuiltInApps(entries); ComputeAllSearchCaches(entries); _index = entries; sw.Stop(); LastIndexDuration = sw.Elapsed; LastIndexCount = entries.Count; LogService.Info($"인덱싱 완료: {entries.Count}개 항목 ({sw.Elapsed.TotalSeconds:F1}초)"); this.IndexRebuilt?.Invoke(this, EventArgs.Empty); } finally { _rebuildLock.Release(); } } public void StartWatchers() { StopWatchers(); foreach (string indexPath in _settings.Settings.IndexPaths) { string text = Environment.ExpandEnvironmentVariables(indexPath); if (Directory.Exists(text)) { try { FileSystemWatcher fileSystemWatcher = new FileSystemWatcher(text) { Filter = "*.*", IncludeSubdirectories = true, NotifyFilter = (NotifyFilters.FileName | NotifyFilters.DirectoryName), EnableRaisingEvents = true }; fileSystemWatcher.Created += OnWatcherEvent; fileSystemWatcher.Deleted += OnWatcherEvent; fileSystemWatcher.Renamed += OnWatcherRenamed; _watchers.Add(fileSystemWatcher); } catch (Exception ex) { LogService.Warn("FileSystemWatcher 생성 실패: " + text + " - " + ex.Message); } } } LogService.Info($"파일 감시 시작: {_watchers.Count}개 경로"); } private void StopWatchers() { foreach (FileSystemWatcher watcher in _watchers) { watcher.EnableRaisingEvents = false; watcher.Dispose(); } _watchers.Clear(); } private void OnWatcherEvent(object sender, FileSystemEventArgs e) { ScheduleRebuild(e.FullPath); } private void OnWatcherRenamed(object sender, RenamedEventArgs e) { ScheduleRebuild(e.FullPath); } private void ScheduleRebuild(string triggerPath) { LogService.Info($"파일 변경 감지: {triggerPath} — {3000}ms 후 재빌드 예약"); lock (_timerLock) { _debounceTimer?.Dispose(); _debounceTimer = new Timer(delegate { BuildAsync(); }, null, 3000, -1); } } public void Dispose() { _debounceTimer?.Dispose(); StopWatchers(); } private static void RegisterBuiltInApps(List entries) { (string, string[], string)[] array = new(string, string[], string)[43] { ("메모장 (Notepad)", new string[4] { "메모장", "notepad", "note", "txt" }, "C:\\Windows\\notepad.exe"), ("계산기 (Calculator)", new string[3] { "계산기", "calc", "calculator" }, "calculator:"), ("캡처 도구 (Snipping Tool)", new string[5] { "캡처", "캡처도구", "snippingtool", "snip", "스크린샷" }, "C:\\Windows\\System32\\SnippingTool.exe"), ("그림판 (Paint)", new string[3] { "그림판", "mspaint", "paint" }, "C:\\Windows\\System32\\mspaint.exe"), ("파일 탐색기 (Explorer)", new string[3] { "탐색기", "explorer", "파일탐색기" }, "C:\\Windows\\explorer.exe"), ("명령 프롬프트 (CMD)", new string[4] { "cmd", "명령프롬프트", "커맨드", "터미널" }, "C:\\Windows\\System32\\cmd.exe"), ("PowerShell", new string[2] { "powershell", "파워쉘" }, "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"), ("제어판 (Control Panel)", new string[3] { "제어판", "control", "controlpanel" }, "C:\\Windows\\System32\\control.exe"), ("작업 관리자 (Task Manager)", new string[3] { "작업관리자", "taskmgr", "taskmanager" }, "C:\\Windows\\System32\\Taskmgr.exe"), ("원격 데스크톱 (Remote Desktop)", new string[4] { "원격", "mstsc", "rdp", "원격데스크톱" }, "C:\\Windows\\System32\\mstsc.exe"), ("레지스트리 편집기", new string[2] { "regedit", "레지스트리" }, "C:\\Windows\\regedit.exe"), ("장치 관리자", new string[3] { "장치관리자", "devmgmt", "devicemanager" }, "C:\\Windows\\System32\\devmgmt.msc"), ("Microsoft Edge", new string[3] { "edge", "엣지", "브라우저" }, "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"), ("Microsoft Excel", new string[6] { "엑셀", "excel", "msexcel", "xlsx", "xls", "csv" }, FindOfficeApp("EXCEL.EXE")), ("Microsoft PowerPoint", new string[5] { "파워포인트", "powerpoint", "mspowerpoint", "pptx", "ppt" }, FindOfficeApp("POWERPNT.EXE")), ("Microsoft Word", new string[5] { "워드", "word", "msword", "docx", "doc" }, FindOfficeApp("WINWORD.EXE")), ("Microsoft Outlook", new string[3] { "아웃룩", "outlook", "메일" }, FindOfficeApp("OUTLOOK.EXE")), ("Microsoft Teams", new string[2] { "팀즈", "teams" }, FindTeams()), ("Microsoft OneNote", new string[2] { "원노트", "onenote" }, FindOfficeApp("ONENOTE.EXE")), ("Microsoft Access", new string[3] { "액세스", "access", "msaccess" }, FindOfficeApp("MSACCESS.EXE")), ("Visual Studio Code", new string[4] { "vscode", "비주얼스튜디오코드", "코드에디터", "code" }, FindInPath("code.cmd") ?? FindInLocalAppData("Programs\\Microsoft VS Code\\Code.exe")), ("Visual Studio", new string[5] { "비주얼스튜디오", "devenv", "vs2022", "vs2019", "visualstudio" }, FindInProgramFiles("Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\devenv.exe") ?? FindInProgramFiles("Microsoft Visual Studio\\2022\\Professional\\Common7\\IDE\\devenv.exe") ?? FindInProgramFiles("Microsoft Visual Studio\\2022\\Enterprise\\Common7\\IDE\\devenv.exe")), ("Windows Terminal", new string[4] { "윈도우터미널", "wt", "windowsterminal", "터미널" }, FindInLocalAppData("Microsoft\\WindowsApps\\wt.exe")), ("OneDrive", new string[2] { "원드라이브", "onedrive" }, FindInLocalAppData("Microsoft\\OneDrive\\OneDrive.exe")), ("Google Chrome", new string[4] { "크롬", "구글크롬", "chrome", "google" }, FindInProgramFiles("Google\\Chrome\\Application\\chrome.exe") ?? FindInLocalAppData("Google\\Chrome\\Application\\chrome.exe")), ("Mozilla Firefox", new string[3] { "파이어폭스", "불여우", "firefox" }, FindInProgramFiles("Mozilla Firefox\\firefox.exe")), ("Naver Whale", new string[3] { "웨일", "네이버웨일", "whale" }, FindInLocalAppData("Naver\\Naver Whale\\Application\\whale.exe")), ("KakaoTalk", new string[4] { "카카오톡", "카톡", "kakaotalk", "kakao" }, FindInLocalAppData("Kakao\\KakaoTalk\\KakaoTalk.exe")), ("KakaoWork", new string[3] { "카카오워크", "카워크", "kakaowork" }, FindInLocalAppData("Kakao\\KakaoWork\\KakaoWork.exe")), ("Zoom", new string[2] { "줌", "zoom" }, FindInRoaming("Zoom\\bin\\Zoom.exe") ?? FindInLocalAppData("Zoom\\bin\\Zoom.exe")), ("Slack", new string[2] { "슬랙", "slack" }, FindInLocalAppData("slack\\slack.exe")), ("Figma", new string[2] { "피그마", "figma" }, FindInLocalAppData("Figma\\Figma.exe")), ("Notepad++", new string[3] { "노트패드++", "npp", "notepad++" }, FindInProgramFiles("Notepad++\\notepad++.exe")), ("7-Zip", new string[4] { "7zip", "7집", "세븐집", "7z" }, FindInProgramFiles("7-Zip\\7zFM.exe")), ("Bandizip", new string[2] { "반디집", "bandizip" }, FindInProgramFiles("Bandizip\\Bandizip.exe") ?? FindInLocalAppData("Bandizip\\Bandizip.exe")), ("ALZip", new string[3] { "알집", "alzip", "이스트소프트" }, FindInProgramFiles("ESTsoft\\ALZip\\ALZip.exe")), ("PotPlayer", new string[3] { "팟플레이어", "팟플", "potplayer" }, FindInProgramFiles("DAUM\\PotPlayer\\PotPlayerMini64.exe") ?? FindInProgramFiles("DAUM\\PotPlayer\\PotPlayerMini.exe")), ("GOM Player", new string[4] { "곰플레이어", "곰플", "gomplayer", "gom" }, FindInProgramFiles("GRETECH\\GomPlayer\\GOM.EXE")), ("Adobe Photoshop", new string[4] { "포토샵", "포샵", "photoshop", "ps" }, FindInProgramFiles("Adobe\\Adobe Photoshop 2025\\Photoshop.exe") ?? FindInProgramFiles("Adobe\\Adobe Photoshop 2024\\Photoshop.exe") ?? FindInProgramFiles("Adobe\\Adobe Photoshop 2023\\Photoshop.exe")), ("Adobe Acrobat", new string[3] { "아크로뱃", "acrobat", "아도비pdf" }, FindInProgramFiles("Adobe\\Acrobat DC\\Acrobat\\Acrobat.exe")), ("한컴 한글", new string[5] { "한글", "한컴한글", "hwp", "hangul", "아래아한글" }, FindInProgramFiles("HNC\\Hwp\\Hwp.exe") ?? FindInProgramFiles("HNC\\Office NEO\\HOffice NEO\\Bin\\Hwp.exe") ?? FindInProgramFiles("Hnc\\Hwp80\\Hwp.exe")), ("한컴 한셀", new string[3] { "한셀", "hcell", "한컴스프레드" }, FindInProgramFiles("HNC\\Hwp\\Hcell.exe") ?? FindInProgramFiles("HNC\\Office NEO\\HOffice NEO\\Bin\\Hcell.exe")), ("한컴 한쇼", new string[3] { "한쇼", "hshow", "한컴프레젠테이션" }, FindInProgramFiles("HNC\\Hwp\\Show.exe") ?? FindInProgramFiles("HNC\\Office NEO\\HOffice NEO\\Bin\\Show.exe")) }; Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); Dictionary dictionary2 = new Dictionary(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < entries.Count; i++) { dictionary.TryAdd(entries[i].Name, i); dictionary2.TryAdd(entries[i].Path, i); } (string, string[], string)[] array2 = array; for (int j = 0; j < array2.Length; j++) { var (displayName, array3, text) = array2[j]; if (string.IsNullOrEmpty(text) || (!text.Contains(":") && !File.Exists(text) && !text.EndsWith(".msc"))) { continue; } if (dictionary2.TryGetValue(text, out var value)) { entries[value].DisplayName = displayName; if (entries[value].Score < 95) { entries[value].Score = 95; } } bool flag = dictionary2.ContainsKey(text); string[] array4 = array3; foreach (string text2 in array4) { if (dictionary.ContainsKey(text2)) { if (dictionary.TryGetValue(text2, out var value2) && entries[value2].Score < 95) { entries[value2].Score = 95; } continue; } bool flag2 = text2.Any((char c) => c >= '가' && c <= '힣'); if (!flag || flag2) { entries.Add(new IndexEntry { Name = text2, DisplayName = displayName, Path = text, Type = IndexEntryType.App, Score = 95 }); int value3 = (dictionary[text2] = entries.Count - 1); if (!flag) { dictionary2[text] = value3; flag = true; } } else { dictionary[text2] = dictionary2[text]; } } } } private static string? FindOfficeApp(string exeName) { string[] array = new string[4] { "C:\\Program Files\\Microsoft Office\\root\\Office16", "C:\\Program Files (x86)\\Microsoft Office\\root\\Office16", "C:\\Program Files\\Microsoft Office\\Office16", "C:\\Program Files (x86)\\Microsoft Office\\Office16" }; string[] array2 = array; foreach (string path in array2) { string text = Path.Combine(path, exeName); if (File.Exists(text)) { return text; } } return null; } private static string? FindTeams() { string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); string text = Path.Combine(folderPath, "Microsoft\\Teams\\current\\Teams.exe"); if (File.Exists(text)) { return text; } string text2 = Path.Combine(folderPath, "Microsoft\\WindowsApps\\ms-teams.exe"); return File.Exists(text2) ? text2 : null; } private static string? FindInPath(string fileName) { string text = Environment.GetEnvironmentVariable("PATH") ?? ""; string[] array = text.Split(';'); foreach (string text2 in array) { if (!string.IsNullOrWhiteSpace(text2)) { string text3 = Path.Combine(text2.Trim(), fileName); if (File.Exists(text3)) { return text3; } } } return null; } private static string? FindInLocalAppData(string relativePath) { string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); string text = Path.Combine(folderPath, relativePath); return File.Exists(text) ? text : null; } private static string? FindInProgramFiles(string relativePath) { string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); string folderPath2 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); string text = Path.Combine(folderPath, relativePath); if (File.Exists(text)) { return text; } string text2 = Path.Combine(folderPath2, relativePath); return File.Exists(text2) ? text2 : null; } private static string? FindInRoaming(string relativePath) { string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); string text = Path.Combine(folderPath, relativePath); return File.Exists(text) ? text : null; } private static void ComputeAllSearchCaches(List entries) { foreach (IndexEntry entry in entries) { ComputeSearchCache(entry); } } private static void ComputeSearchCache(IndexEntry entry) { entry.NameLower = entry.Name.ToLowerInvariant(); entry.NameJamo = FuzzyEngine.DecomposeToJamo(entry.NameLower); StringBuilder stringBuilder = new StringBuilder(entry.NameLower.Length); string nameLower = entry.NameLower; foreach (char hangul in nameLower) { char chosung = FuzzyEngine.GetChosung(hangul); if (chosung != 0) { stringBuilder.Append(chosung); } } entry.NameChosung = stringBuilder.ToString(); } private static (int batchSize, int delayMs) GetThrottle(string speed) { if (1 == 0) { } (int, int) result = ((speed == "fast") ? (500, 0) : ((!(speed == "slow")) ? (150, 5) : (50, 15))); if (1 == 0) { } return result; } private static async Task ScanDirectoryAsync(string dir, List entries, HashSet allowedExts, string indexSpeed, CancellationToken ct) { var (batchSize, delayMs) = GetThrottle(indexSpeed); await Task.Run(async delegate { try { int count = 0; foreach (string file in Directory.EnumerateFiles(dir, "*.*", SearchOption.AllDirectories)) { ct.ThrowIfCancellationRequested(); string ext = Path.GetExtension(file).ToLowerInvariant(); if (allowedExts.Count <= 0 || allowedExts.Contains(ext)) { string name = Path.GetFileNameWithoutExtension(file); if (!string.IsNullOrEmpty(name)) { if (1 == 0) { } IndexEntryType indexEntryType; switch (ext) { case ".exe": indexEntryType = IndexEntryType.App; break; case ".lnk": case ".url": indexEntryType = IndexEntryType.File; break; default: indexEntryType = IndexEntryType.File; break; } if (1 == 0) { } IndexEntryType type = indexEntryType; List list = entries; IndexEntry indexEntry = new IndexEntry { Name = name }; IndexEntry indexEntry2 = indexEntry; bool flag; switch (ext) { case ".exe": case ".lnk": case ".url": flag = true; break; default: flag = false; break; } indexEntry2.DisplayName = (flag ? name : (name + ext)); indexEntry.Path = file; indexEntry.Type = type; list.Add(indexEntry); int num2; if (delayMs > 0) { int num = count + 1; count = num; num2 = ((num % batchSize == 0) ? 1 : 0); } else { num2 = 0; } if (num2 != 0) { await Task.Delay(delayMs, ct); } } } } foreach (string subDir in Directory.EnumerateDirectories(dir, "*", SearchOption.TopDirectoryOnly)) { ct.ThrowIfCancellationRequested(); string name2 = Path.GetFileName(subDir); if (!name2.StartsWith(".")) { entries.Add(new IndexEntry { Name = name2, DisplayName = name2, Path = subDir, Type = IndexEntryType.Folder }); } } } catch (UnauthorizedAccessException ex) { UnauthorizedAccessException ex2 = ex; LogService.Warn("폴더 접근 불가 (건너뜀): " + dir + " - " + ex2.Message); } }, ct); } }