Files
AX-Copilot-Codex/src/AxCopilot/Handlers/WslHandler.cs
lacvet 0336904258 AX Commander 비교본 런처 기능 대량 이식
변경 목적: Agent Compare 아래 비교본의 개발 문서와 런처 소스를 기준으로 현재 AX Commander에 빠져 있던 신규 런처 기능을 동일한 흐름으로 옮겨, 비교본 수준의 기능 폭을 현재 제품에 반영했습니다.

핵심 수정사항: 비교본의 신규 런처 핸들러 다수를 src/AxCopilot/Handlers로 이식하고 App.xaml.cs 등록 흐름에 연결했습니다. 빠른 링크, 파일 태그, 알림 센터, 포모도로, 파일 브라우저, 핫키 관리, OCR, 세션/스케줄/매크로, Git/정규식/네트워크/압축/해시/UUID/JWT/QR 등 AX Commander 기능을 추가했습니다.

핵심 수정사항: 신규 기능이 실제 동작하도록 AppSettings 확장, SchedulerService/FileTagService/NotificationCenterService/IconCacheService/UrlTemplateEngine/PomodoroService 추가, 배치 이름변경/세션/스케줄/매크로 편집 창 추가, NotificationService와 Symbols 보강, QR/OCR용 csproj 의존성과 Windows 타겟 프레임워크를 반영했습니다.

문서 반영: README.md와 docs/DEVELOPMENT.md에 비교본 기반 런처 기능 이식 이력과 검증 결과를 업데이트했습니다.

검증 결과: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 실행 기준 경고 0개, 오류 0개를 확인했습니다.
2026-04-05 00:59:45 +09:00

275 lines
9.9 KiB
C#

using System.Text;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// L15-1: WSL(Windows Subsystem for Linux) 관리 핸들러. "wsl" 프리픽스로 사용합니다.
///
/// 예: wsl → 설치된 distro 목록 + 상태
/// wsl ubuntu → Ubuntu 실행 (새 터미널)
/// wsl stop ubuntu → 특정 distro 종료
/// wsl stop all → 전체 WSL 종료
/// wsl default ubuntu → 기본 distro 변경
/// Enter → distro 실행 또는 명령 실행.
/// </summary>
public class WslHandler : IActionHandler
{
public string? Prefix => "wsl";
public PluginMetadata Metadata => new(
"WSL",
"WSL 관리 — distro 목록 · 실행 · 종료",
"1.0",
"AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List<LauncherItem>();
var distros = GetDistros();
if (string.IsNullOrWhiteSpace(q))
{
if (distros.Count == 0)
{
items.Add(new LauncherItem("WSL 없음",
"WSL이 설치되지 않았거나 distro가 없습니다", null, null, Symbol: "\uE756"));
items.Add(new LauncherItem("WSL 설치",
"Microsoft Store에서 Ubuntu 등 설치", null,
("open_url", "ms-windows-store://search/?query=linux"), Symbol: "\uE756"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
items.Add(new LauncherItem(
$"WSL distro {distros.Count}개",
"Enter → 실행 / wsl stop all → 전체 종료",
null, null, Symbol: "\uE756"));
foreach (var d in distros)
items.Add(MakeDistroItem(d));
items.Add(new LauncherItem("wsl stop all", "전체 WSL 종료 (wsl --shutdown)", null,
("shutdown", ""), Symbol: "\uE756"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var sub = parts[0].ToLowerInvariant();
switch (sub)
{
case "stop":
case "shutdown":
case "kill":
{
var target = parts.Length > 1 ? parts[1].ToLowerInvariant() : "all";
if (target == "all" || target == "--all")
{
items.Add(new LauncherItem("WSL 전체 종료", "wsl --shutdown · Enter 실행",
null, ("shutdown", ""), Symbol: "\uE756"));
}
else
{
var found = distros.FirstOrDefault(d =>
d.Name.Contains(target, StringComparison.OrdinalIgnoreCase));
if (found == null)
items.Add(new LauncherItem("없는 distro", $"'{target}'를 찾을 수 없습니다", null, null, Symbol: "\uE783"));
else
items.Add(new LauncherItem($"{found.Name} 종료", $"wsl --terminate {found.Name}",
null, ("terminate", found.Name), Symbol: "\uE756"));
}
break;
}
case "default":
case "set-default":
{
var target = parts.Length > 1 ? parts[1] : "";
if (string.IsNullOrWhiteSpace(target))
{
items.Add(new LauncherItem("distro 이름 입력", "예: wsl default Ubuntu", null, null, Symbol: "\uE783"));
break;
}
items.Add(new LauncherItem($"기본 distro: {target}",
$"wsl --set-default {target} · Enter 실행",
null, ("set_default", target), Symbol: "\uE756"));
break;
}
default:
{
// distro 이름 검색 → 실행
var found = distros.Where(d =>
d.Name.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList();
if (found.Count > 0)
foreach (var d in found)
items.Add(MakeDistroItem(d));
else
items.Add(new LauncherItem($"'{q}' distro 없음",
"wsl 입력으로 전체 목록 확인", null, null, Symbol: "\uE946"));
break;
}
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
switch (item.Data)
{
case ("launch", string distro):
RunWsl($"-d \"{distro}\"");
break;
case ("shutdown", _):
RunWslSilent("--shutdown");
NotificationService.Notify("WSL", "WSL 전체 종료 요청됨");
break;
case ("terminate", string distro):
RunWslSilent($"--terminate \"{distro}\"");
NotificationService.Notify("WSL", $"{distro} 종료됨");
break;
case ("set_default", string distro):
RunWslSilent($"--set-default \"{distro}\"");
NotificationService.Notify("WSL", $"기본 distro → {distro}");
break;
case ("open_url", string url):
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = url, UseShellExecute = true,
});
}
catch { /* 비핵심 */ }
break;
case ("copy", string text):
try
{
System.Windows.Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text));
NotificationService.Notify("WSL", "복사됨");
}
catch { /* 비핵심 */ }
break;
}
return Task.CompletedTask;
}
// ── WSL 조회 ─────────────────────────────────────────────────────────────
private record WslDistro(string Name, string State, string Version, bool IsDefault);
private static List<WslDistro> GetDistros()
{
var result = new List<WslDistro>();
try
{
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = "wsl",
Arguments = "--list --verbose",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.Unicode, // WSL outputs UTF-16
};
using var proc = System.Diagnostics.Process.Start(psi);
if (proc == null) return result;
var output = proc.StandardOutput.ReadToEnd();
proc.WaitForExit(3000);
foreach (var line in output.Split('\n').Skip(1)) // 첫 줄은 헤더
{
var trimmed = line.Trim().TrimEnd('\r');
if (string.IsNullOrWhiteSpace(trimmed)) continue;
var isDefault = trimmed.StartsWith('*');
trimmed = trimmed.TrimStart('*').Trim();
var parts = trimmed.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2) continue;
result.Add(new WslDistro(
Name: parts[0],
State: parts.Length > 1 ? parts[1] : "Unknown",
Version: parts.Length > 2 ? parts[2] : "?",
IsDefault: isDefault));
}
}
catch { /* WSL 없음 */ }
return result;
}
private static LauncherItem MakeDistroItem(WslDistro d)
{
var icon = d.State.Equals("Running", StringComparison.OrdinalIgnoreCase) ? "\uE768" : "\uE756";
var label = d.IsDefault ? $"★ {d.Name}" : d.Name;
var subtitle = $"{d.State} · WSL {d.Version}" + (d.IsDefault ? " (기본)" : "");
return new LauncherItem(label, subtitle, null, ("launch", d.Name), Symbol: icon);
}
private static void RunWsl(string args)
{
// 터미널에서 실행 (wt 또는 powershell 폴백)
var wtPath = FindExe("wt.exe");
if (wtPath != null)
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = wtPath,
Arguments = $"wsl {args}",
UseShellExecute = false,
});
}
else
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = "wsl",
Arguments = args,
UseShellExecute = true,
});
}
}
private static void RunWslSilent(string args)
{
try
{
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = "wsl",
Arguments = args,
UseShellExecute = false,
CreateNoWindow = true,
};
using var proc = System.Diagnostics.Process.Start(psi);
proc?.WaitForExit(5000);
}
catch { /* 비핵심 */ }
}
private static string? FindExe(string name)
{
var pathEnv = Environment.GetEnvironmentVariable("PATH") ?? "";
foreach (var dir in pathEnv.Split(';'))
{
var full = System.IO.Path.Combine(dir.Trim(), name);
if (System.IO.File.Exists(full)) return full;
}
return null;
}
}