147 lines
4.9 KiB
C#
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));
|
|
}
|
|
}
|