Files
AX-Copilot/src/AxCopilot/Handlers/SystemInfoHandler.cs

510 lines
22 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// 시스템 정보를 표시합니다. "info" 프리픽스로 사용합니다.
/// 예: info → 전체 시스템 정보 목록
/// info ip → IP 주소
/// info battery → 배터리 상태
/// info uptime → 시스템 가동 시간
/// info volume → 볼륨 수준
/// </summary>
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<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim().ToLowerInvariant();
var items = new List<LauncherItem>();
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<MEMORYSTATUSEX>() };
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<IEnumerable<LauncherItem>>(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<ResourceMonitorWindow>().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;
}
// ─── 액션 데이터 태그 ────────────────────────────────────────────────────
/// <summary>info 항목에 의미 있는 동작을 부여하기 위한 태그</summary>
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}초";
}
}
/// <summary>
/// * 단축키로 시스템 정보를 빠르게 조회합니다. SystemInfoHandler에 완전히 위임합니다.
/// </summary>
public class StarInfoHandler : IActionHandler
{
private readonly SystemInfoHandler _inner = new();
public string? Prefix => "*";
public PluginMetadata Metadata => new(
"StarInfo",
"시스템 정보 빠른 조회 — * 단축키 (info와 동일)",
"1.0",
"AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
=> _inner.GetItemsAsync(query, ct);
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
=> _inner.ExecuteAsync(item, ct);
}