using System.Diagnostics; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; using AxCopilot.Views; namespace AxCopilot.Handlers; /// /// 시스템 정보를 표시합니다. "info" 프리픽스로 사용합니다. /// 예: info → 전체 시스템 정보 목록 /// info ip → IP 주소 /// info battery → 배터리 상태 /// info uptime → 시스템 가동 시간 /// info volume → 볼륨 수준 /// public class SystemInfoHandler : IActionHandler { public string? Prefix => "info"; public PluginMetadata Metadata => new( "SystemInfo", "시스템 정보 — IP, 배터리, 볼륨, 가동시간 등", "1.0", "AX"); // ─── P/Invoke ──────────────────────────────────────────────────────────── [DllImport("winmm.dll")] private static extern int waveOutGetVolume(IntPtr hwo, out uint dwVolume); [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] private struct MEMORYSTATUSEX { public uint dwLength; public uint dwMemoryLoad; // % 사용 중 public ulong ullTotalPhys; public ulong ullAvailPhys; public ulong ullTotalPageFile; public ulong ullAvailPageFile; public ulong ullTotalVirtual; public ulong ullAvailVirtual; public ulong ullAvailExtendedVirtual; } // CPU 카운터: 앱 시작 시 백그라운드에서 사전 초기화 (UI 스레드 차단 방지) private static volatile PerformanceCounter? _cpuCounter; private static float _cpuCached; private static DateTime _cpuUpdated = DateTime.MinValue; private static readonly object _cpuLock = new(); static SystemInfoHandler() { // 백그라운드에서 PerformanceCounter 초기화 (1~3초 소요, UI 스레드 차단 방지) _ = Task.Run(() => { try { var counter = new PerformanceCounter("Processor", "% Processor Time", "_Total"); counter.NextValue(); // 첫 호출은 항상 0 → 초기화만 _cpuCounter = counter; } catch (Exception) { /* PerformanceCounter 미지원 환경 */ } }); } private static float GetCpuUsage() { try { var counter = _cpuCounter; if (counter == null) return -1; // 아직 초기화 중 lock (_cpuLock) { if ((DateTime.Now - _cpuUpdated).TotalMilliseconds > 800) { _cpuCached = counter.NextValue(); _cpuUpdated = DateTime.Now; } return _cpuCached; } } catch (Exception) { return -1; } } public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim().ToLowerInvariant(); var items = new List(); try { // 어떤 카테고리를 보여줄지 결정 bool showAll = string.IsNullOrEmpty(q); bool showIp = showAll || q.Contains("ip") || q.Contains("네트워크") || q.Contains("network"); bool showBat = showAll || q.Contains("bat") || q.Contains("배터리") || q.Contains("battery"); bool showVol = showAll || q.Contains("vol") || q.Contains("볼륨") || q.Contains("volume"); bool showUp = showAll || q.Contains("up") || q.Contains("가동") || q.Contains("uptime"); bool showSys = showAll || q.Contains("sys") || q.Contains("시스템") || q.Contains("system") || q.Contains("host") || q.Contains("호스트") || q.Contains("os"); bool showUser = showAll || q.Contains("user") || q.Contains("사용자"); bool showCpu = showAll || q.Contains("cpu") || q.Contains("프로세서") || q.Contains("processor"); bool showRam = showAll || q.Contains("ram") || q.Contains("메모리") || q.Contains("memory"); bool showDisk = showAll || q.Contains("disk") || q.Contains("디스크") || q.Contains("storage") || q.Contains("저장"); bool showGfx = showAll || q.Contains("screen") || q.Contains("화면") || q.Contains("resolution") || q.Contains("display"); // ─── 호스트 / OS / 사용자 ──────────────────────────────────────── if (showSys) { items.Add(new LauncherItem( $"컴퓨터: {Environment.MachineName}", $"OS: {GetOsVersion()} · Enter로 시스템 정보 열기", null, new InfoAction("shell", "msinfo32"), Symbol: Symbols.Computer)); } if (showUser) { var user = $"{Environment.UserDomainName}\\{Environment.UserName}"; items.Add(new LauncherItem( $"사용자: {Environment.UserName}", $"{user} · Enter로 사용자 계정 설정 열기", null, new InfoAction("shell", "netplwiz"), Symbol: Symbols.Person)); } // ─── IP 주소 ──────────────────────────────────────────────────── if (showIp) { var localIp = GetLocalIpAddress(); if (localIp != null) { items.Add(new LauncherItem( $"로컬 IP: {localIp}", "LAN / Wi-Fi 주소 · Enter로 네트워크 설정 열기", null, new InfoAction("ms_settings", "ms-settings:network"), Symbol: Symbols.Network)); } var gateway = GetDefaultGateway(); if (gateway != null) { items.Add(new LauncherItem( $"게이트웨이: {gateway}", "기본 게이트웨이 · Enter로 네트워크 설정 열기", null, new InfoAction("ms_settings", "ms-settings:network"), Symbol: Symbols.Network)); } } // ─── 배터리 ──────────────────────────────────────────────────── if (showBat) { var batItem = GetBatteryItem(); if (batItem != null) items.Add(batItem); } // ─── 볼륨 ────────────────────────────────────────────────────── if (showVol) { var volItem = GetVolumeItem(); if (volItem != null) items.Add(volItem); } // ─── 가동 시간 ───────────────────────────────────────────────── if (showUp) { var uptime = TimeSpan.FromMilliseconds(Environment.TickCount64); var uptimeStr = FormatUptime(uptime); var bootTime = DateTime.Now - uptime; items.Add(MakeItem( $"가동 시간: {uptimeStr}", $"마지막 재시작: {bootTime:MM/dd HH:mm}", uptimeStr, Symbols.Clock)); } // ─── CPU 사용률 ──────────────────────────────────────────────── if (showCpu) { var cpu = GetCpuUsage(); var cpuTitle = cpu < 0 ? "CPU: 측정 중…" : $"CPU: {(int)cpu}%"; var cpuProc = GetProcessorName(); items.Add(new LauncherItem( cpuTitle, (string.IsNullOrEmpty(cpuProc) ? "전체 CPU 사용률" : cpuProc) + " · Enter로 리소스 모니터 열기", null, new InfoAction("resource_mon"), Symbol: Symbols.Processor)); } // ─── 메모리 (RAM) ────────────────────────────────────────────── if (showRam) { var mem = new MEMORYSTATUSEX { dwLength = (uint)Marshal.SizeOf() }; if (GlobalMemoryStatusEx(ref mem)) { var totalGb = mem.ullTotalPhys / 1024.0 / 1024 / 1024; var usedGb = (mem.ullTotalPhys - mem.ullAvailPhys) / 1024.0 / 1024 / 1024; var pct = mem.dwMemoryLoad; items.Add(new LauncherItem( $"RAM: {usedGb:F1} / {totalGb:F1} GB ({pct}%)", $"사용 중: {usedGb:F1} GB · 여유: {(mem.ullAvailPhys / 1024.0 / 1024 / 1024):F1} GB · Enter로 리소스 모니터 열기", null, new InfoAction("resource_mon"), Symbol: Symbols.Memory)); } } // ─── 디스크 ──────────────────────────────────────────────────── if (showDisk) { foreach (var drive in System.IO.DriveInfo.GetDrives() .Where(d => d.IsReady && d.DriveType == System.IO.DriveType.Fixed)) { var totalGb = drive.TotalSize / 1024.0 / 1024 / 1024; var freeGb = drive.AvailableFreeSpace / 1024.0 / 1024 / 1024; var usedGb = totalGb - freeGb; var pct = (int)(usedGb / totalGb * 100); var label = string.IsNullOrWhiteSpace(drive.VolumeLabel) ? drive.Name.TrimEnd('\\') : drive.VolumeLabel; items.Add(new LauncherItem( $"드라이브 {drive.Name.TrimEnd('\\')} ({label}) — {usedGb:F0} / {totalGb:F0} GB ({pct}%)", $"여유 공간: {freeGb:F1} GB · Enter로 탐색기 열기", null, new InfoAction("open_drive", drive.RootDirectory.FullName), Symbol: Symbols.Storage)); } } // ─── 화면 해상도 ─────────────────────────────────────────────── if (showGfx) { var screen = System.Windows.SystemParameters.PrimaryScreenWidth; var screenH = System.Windows.SystemParameters.PrimaryScreenHeight; items.Add(new LauncherItem( $"화면: {(int)screen} × {(int)screenH}", "기본 모니터 해상도 · Enter로 디스플레이 설정 열기", null, new InfoAction("ms_settings", "ms-settings:display"), Symbol: Symbols.Computer)); } } catch (Exception ex) { LogService.Warn($"시스템 정보 조회 오류: {ex.Message}"); items.Add(new LauncherItem("시스템 정보 조회 실패", ex.Message, null, null, Symbol: Symbols.Error)); } if (items.Count == 0) { items.Add(new LauncherItem( "시스템 정보 없음", "ip · battery · volume · uptime · system 키워드로 검색하세요", null, null, Symbol: Symbols.Info)); } return Task.FromResult>(items); } public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { if (item.Data is not InfoAction action) return Task.CompletedTask; switch (action.Type) { case "copy": // 클립보드에 복사 if (!string.IsNullOrWhiteSpace(action.Payload)) { try { Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(action.Payload)); LogService.Info($"클립보드 복사: {action.Payload}"); } catch (Exception ex) { LogService.Warn($"클립보드 복사 실패: {ex.Message}"); } } break; case "open_drive": // 탐색기로 드라이브 열기 if (!string.IsNullOrWhiteSpace(action.Payload)) { try { Process.Start(new ProcessStartInfo("explorer.exe", action.Payload) { UseShellExecute = true }); } catch (Exception ex) { LogService.Warn($"드라이브 열기 실패: {ex.Message}"); } } break; case "resource_mon": // CPU/RAM 실시간 모니터 창 열기 Application.Current.Dispatcher.Invoke(() => { var existing = Application.Current.Windows .OfType().FirstOrDefault(); if (existing != null) { existing.Activate(); return; } new ResourceMonitorWindow().Show(); }); break; case "shell": // 셸 명령 실행 (msinfo32, netplwiz 등) if (!string.IsNullOrWhiteSpace(action.Payload)) { try { Process.Start(new ProcessStartInfo(action.Payload) { UseShellExecute = true }); } catch (Exception ex) { LogService.Warn($"셸 명령 실행 실패: {ex.Message}"); } } break; case "ms_settings": // Windows 설정 URI (ms-settings:network, ms-settings:display 등) if (!string.IsNullOrWhiteSpace(action.Payload)) { try { Process.Start(new ProcessStartInfo(action.Payload) { UseShellExecute = true }); } catch (Exception ex) { LogService.Warn($"설정 열기 실패: {ex.Message}"); } } break; } return Task.CompletedTask; } // ─── 액션 데이터 태그 ──────────────────────────────────────────────────── /// info 항목에 의미 있는 동작을 부여하기 위한 태그 internal sealed record InfoAction(string Type, string? Payload = null); // Type: "copy" → 클립보드 복사 (Payload = 복사할 텍스트) // "open_drive" → 탐색기로 드라이브 열기 (Payload = 드라이브 루트) // "resource_mon" → CPU/RAM 실시간 모니터 창 열기 // "shell" → 셸 명령 실행 (Payload = 명령) // "ms_settings" → ms-settings: URI 열기 (Payload = URI) // ─── 헬퍼 ──────────────────────────────────────────────────────────────── private static LauncherItem MakeItem(string title, string subtitle, string? copyValue, string symbol) => new(title, subtitle, null, copyValue != null ? new InfoAction("copy", copyValue) : null, Symbol: symbol); private static string? GetLocalIpAddress() { try { foreach (var iface in NetworkInterface.GetAllNetworkInterfaces()) { if (iface.OperationalStatus != OperationalStatus.Up) continue; if (iface.NetworkInterfaceType is NetworkInterfaceType.Loopback or NetworkInterfaceType.Tunnel) continue; foreach (var addr in iface.GetIPProperties().UnicastAddresses) { if (addr.Address.AddressFamily == AddressFamily.InterNetwork) return addr.Address.ToString(); } } } catch (Exception) { /* 무시 */ } return null; } private static string? GetDefaultGateway() { try { foreach (var iface in NetworkInterface.GetAllNetworkInterfaces()) { if (iface.OperationalStatus != OperationalStatus.Up) continue; var gw = iface.GetIPProperties().GatewayAddresses .FirstOrDefault(g => g.Address.AddressFamily == AddressFamily.InterNetwork); if (gw != null) return gw.Address.ToString(); } } catch (Exception) { /* 무시 */ } return null; } private static LauncherItem? GetBatteryItem() { try { var status = System.Windows.Forms.SystemInformation.PowerStatus; var chargeLevel = status.BatteryLifePercent; if (chargeLevel < 0) // 배터리 없는 데스크톱 return new LauncherItem("배터리: 해당 없음", "데스크톱 PC 또는 항상 연결됨 · Enter로 전원 설정 열기", null, new InfoAction("ms_settings", "ms-settings:powersleep"), Symbol: Symbols.Battery); var pct = (int)(chargeLevel * 100); var charging = status.PowerLineStatus == System.Windows.Forms.PowerLineStatus.Online; var timeLeft = status.BatteryLifeRemaining >= 0 ? $" · 잔여: {FormatUptime(TimeSpan.FromSeconds(status.BatteryLifeRemaining))}" : ""; var symbol = charging ? Symbols.BatteryCharging : pct > 50 ? Symbols.Battery : Symbols.BatteryLow; return new LauncherItem( $"배터리: {pct}%{(charging ? " ⚡ 충전 중" : "")}", $"전원: {(charging ? "AC 연결됨" : "배터리 사용 중")}{timeLeft} · Enter로 전원 설정 열기", null, new InfoAction("ms_settings", "ms-settings:powersleep"), Symbol: symbol); } catch (Exception) { return null; } } private static LauncherItem? GetVolumeItem() { try { if (waveOutGetVolume(IntPtr.Zero, out uint vol) == 0) { // 왼쪽 채널 0~0xFFFF → 0~100 변환 var left = (int)((vol & 0xFFFF) / 655.35); var right = (int)(((vol >> 16) & 0xFFFF) / 655.35); var avg = (left + right) / 2; return new LauncherItem( $"볼륨: {avg}%", $"L: {left}% · R: {right}% · Enter로 사운드 설정 열기", null, new InfoAction("ms_settings", "ms-settings:sound"), Symbol: avg == 0 ? Symbols.VolumeMute : Symbols.VolumeUp); } } catch (Exception) { /* 무시 */ } return null; } private static string GetOsVersion() { try { // registry에서 실제 Windows 버전 읽기 using var key = Microsoft.Win32.Registry.LocalMachine .OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion"); if (key != null) { var name = key.GetValue("ProductName") as string ?? "Windows"; var build = key.GetValue("CurrentBuildNumber") as string ?? ""; var ubr = key.GetValue("UBR"); return ubr != null ? $"{name} (빌드 {build}.{ubr})" : $"{name} (빌드 {build})"; } } catch (Exception) { /* 무시 */ } return Environment.OSVersion.ToString(); } private static string GetProcessorName() { try { using var key = Microsoft.Win32.Registry.LocalMachine .OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0"); return key?.GetValue("ProcessorNameString") as string ?? ""; } catch (Exception) { return ""; } } private static string FormatUptime(TimeSpan t) { if (t.TotalDays >= 1) return $"{(int)t.TotalDays}일 {t.Hours}시간 {t.Minutes}분"; if (t.TotalHours >= 1) return $"{t.Hours}시간 {t.Minutes}분"; return $"{t.Minutes}분 {t.Seconds}초"; } } /// /// * 단축키로 시스템 정보를 빠르게 조회합니다. SystemInfoHandler에 완전히 위임합니다. /// public class StarInfoHandler : IActionHandler { private readonly SystemInfoHandler _inner = new(); public string? Prefix => "*"; public PluginMetadata Metadata => new( "StarInfo", "시스템 정보 빠른 조회 — * 단축키 (info와 동일)", "1.0", "AX"); public Task> GetItemsAsync(string query, CancellationToken ct) => _inner.GetItemsAsync(query, ct); public Task ExecuteAsync(LauncherItem item, CancellationToken ct) => _inner.ExecuteAsync(item, ct); }