Files

147 lines
4.9 KiB
C#

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<List<HookExecutionResult>> RunAsync(IReadOnlyList<AgentHookEntry> 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<HookExecutionResult> results = new List<HookExecutionResult>();
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<HookExecutionResult> 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));
}
}