using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; using AxCopilot.Models; namespace AxCopilot.Services.Agent; public static class AgentHookRunner { private const int MaxEnvValueLength = 4096; public static async Task> RunAsync(IReadOnlyList hooks, string toolName, string timing, string? toolInput = null, string? toolOutput = null, bool success = true, string? workFolder = null, int timeoutMs = 10000, CancellationToken ct = default(CancellationToken)) { List results = new List(); if (hooks == null || hooks.Count == 0) { return results; } foreach (AgentHookEntry hook in hooks) { if (hook.Enabled && string.Equals(hook.Timing, timing, StringComparison.OrdinalIgnoreCase) && (!(hook.ToolName != "*") || string.Equals(hook.ToolName, toolName, StringComparison.OrdinalIgnoreCase))) { results.Add(await ExecuteHookAsync(hook, toolName, timing, toolInput, toolOutput, success, workFolder, timeoutMs, ct)); } } return results; } private static async Task ExecuteHookAsync(AgentHookEntry hook, string toolName, string timing, string? toolInput, string? toolOutput, bool success, string? workFolder, int timeoutMs, CancellationToken ct) { try { if (string.IsNullOrWhiteSpace(hook.ScriptPath)) { return new HookExecutionResult(hook.Name, Success: false, "스크립트 경로가 비어 있습니다."); } string scriptPath = Environment.ExpandEnvironmentVariables(hook.ScriptPath); if (!File.Exists(scriptPath)) { return new HookExecutionResult(hook.Name, Success: false, "스크립트를 찾을 수 없습니다: " + scriptPath); } string ext = Path.GetExtension(scriptPath).ToLowerInvariant(); string fileName; string arguments; switch (ext) { case ".ps1": fileName = "powershell.exe"; arguments = "-NoProfile -ExecutionPolicy Bypass -File \"" + scriptPath + "\""; break; case ".bat": case ".cmd": fileName = "cmd.exe"; arguments = "/c \"" + scriptPath + "\""; break; default: return new HookExecutionResult(hook.Name, Success: false, "지원하지 않는 스크립트 확장자: " + ext + " (.bat/.cmd/.ps1만 허용)"); } if (!string.IsNullOrWhiteSpace(hook.Arguments)) { arguments = arguments + " " + hook.Arguments; } ProcessStartInfo psi = new ProcessStartInfo { FileName = fileName, Arguments = arguments, WorkingDirectory = (workFolder ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)), UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true }; psi.EnvironmentVariables["AX_TOOL_NAME"] = toolName; psi.EnvironmentVariables["AX_TOOL_TIMING"] = timing; psi.EnvironmentVariables["AX_TOOL_INPUT"] = Truncate(toolInput, 4096); psi.EnvironmentVariables["AX_WORK_FOLDER"] = workFolder ?? ""; if (string.Equals(timing, "post", StringComparison.OrdinalIgnoreCase)) { psi.EnvironmentVariables["AX_TOOL_OUTPUT"] = Truncate(toolOutput, 4096); psi.EnvironmentVariables["AX_TOOL_SUCCESS"] = (success ? "true" : "false"); } using Process process = new Process { StartInfo = psi }; StringBuilder stdOut = new StringBuilder(); StringBuilder stdErr = new StringBuilder(); process.OutputDataReceived += delegate(object _, DataReceivedEventArgs e) { if (e.Data != null) { stdOut.AppendLine(e.Data); } }; process.ErrorDataReceived += delegate(object _, DataReceivedEventArgs e) { if (e.Data != null) { stdErr.AppendLine(e.Data); } }; process.Start(); process.BeginOutputReadLine(); process.BeginErrorReadLine(); using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct); cts.CancelAfter(timeoutMs); try { await process.WaitForExitAsync(cts.Token); } catch (OperationCanceledException) { try { process.Kill(entireProcessTree: true); } catch { } return new HookExecutionResult(hook.Name, Success: false, $"타임아웃 ({timeoutMs}ms 초과)"); } int exitCode = process.ExitCode; string output = stdOut.ToString().TrimEnd(); string error = stdErr.ToString().TrimEnd(); if (exitCode != 0) { return new HookExecutionResult(hook.Name, Success: false, $"종료 코드 {exitCode}: {(string.IsNullOrEmpty(error) ? output : error)}"); } return new HookExecutionResult(hook.Name, Success: true, string.IsNullOrEmpty(output) ? "(정상 완료)" : output); } catch (Exception ex2) { Exception ex3 = ex2; return new HookExecutionResult(hook.Name, Success: false, "훅 실행 예외: " + ex3.Message); } } private static string Truncate(string? value, int maxLen) { return string.IsNullOrEmpty(value) ? "" : ((value.Length <= maxLen) ? value : value.Substring(0, maxLen)); } }