510 lines
22 KiB
C#
510 lines
22 KiB
C#
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);
|
||
}
|