152 lines
4.2 KiB
C#
152 lines
4.2 KiB
C#
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
|
|
namespace StellaOps.Interop;
|
|
|
|
public sealed class ToolManager
|
|
{
|
|
private readonly string _workDir;
|
|
private readonly IReadOnlyDictionary<string, string> _toolPaths;
|
|
|
|
public ToolManager(string workDir, IReadOnlyDictionary<string, string>? toolPaths = null)
|
|
{
|
|
_workDir = workDir;
|
|
_toolPaths = toolPaths ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
public async Task VerifyToolAsync(string tool, string args, CancellationToken ct = default)
|
|
{
|
|
var result = await RunAsync(tool, args, ct).ConfigureAwait(false);
|
|
if (!result.Success)
|
|
{
|
|
throw new ToolExecutionException(
|
|
$"Tool '{tool}' not available or failed to run.",
|
|
result);
|
|
}
|
|
}
|
|
|
|
public async Task<ToolResult> RunAsync(string tool, string args, CancellationToken ct = default)
|
|
{
|
|
var toolPath = ResolveToolPath(tool);
|
|
if (toolPath is null)
|
|
{
|
|
return ToolResult.Failed($"Tool not found: {tool}");
|
|
}
|
|
|
|
var startInfo = new ProcessStartInfo
|
|
{
|
|
FileName = toolPath,
|
|
Arguments = args,
|
|
WorkingDirectory = _workDir,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
};
|
|
|
|
try
|
|
{
|
|
using var process = Process.Start(startInfo);
|
|
if (process is null)
|
|
{
|
|
return ToolResult.Failed($"Failed to start tool: {tool}");
|
|
}
|
|
|
|
var stdOutTask = process.StandardOutput.ReadToEndAsync(ct);
|
|
var stdErrTask = process.StandardError.ReadToEndAsync(ct);
|
|
|
|
await process.WaitForExitAsync(ct).ConfigureAwait(false);
|
|
|
|
var stdout = await stdOutTask.ConfigureAwait(false);
|
|
var stderr = await stdErrTask.ConfigureAwait(false);
|
|
|
|
return process.ExitCode == 0
|
|
? ToolResult.Ok(stdout, stderr, process.ExitCode)
|
|
: ToolResult.Failed(stderr, stdout, process.ExitCode);
|
|
}
|
|
catch (Exception ex) when (ex is InvalidOperationException or Win32Exception)
|
|
{
|
|
return ToolResult.Failed(ex.Message);
|
|
}
|
|
}
|
|
|
|
public bool IsToolAvailable(string tool) => ResolveToolPath(tool) is not null;
|
|
|
|
public string? ResolveToolPath(string tool)
|
|
{
|
|
if (_toolPaths.TryGetValue(tool, out var configured) && File.Exists(configured))
|
|
{
|
|
return configured;
|
|
}
|
|
|
|
return FindOnPath(tool);
|
|
}
|
|
|
|
public static string? FindOnPath(string tool)
|
|
{
|
|
if (File.Exists(tool))
|
|
{
|
|
return Path.GetFullPath(tool);
|
|
}
|
|
|
|
var path = Environment.GetEnvironmentVariable("PATH");
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
foreach (var dir in path.Split(Path.PathSeparator))
|
|
{
|
|
if (string.IsNullOrWhiteSpace(dir))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var candidate = Path.Combine(dir, tool);
|
|
if (File.Exists(candidate))
|
|
{
|
|
return candidate;
|
|
}
|
|
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
var exeCandidate = candidate + ".exe";
|
|
if (File.Exists(exeCandidate))
|
|
{
|
|
return exeCandidate;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public sealed record ToolResult(
|
|
bool Success,
|
|
int ExitCode,
|
|
string StdOut,
|
|
string StdErr,
|
|
string? Error)
|
|
{
|
|
public static ToolResult Ok(string stdout, string stderr, int exitCode)
|
|
=> new(true, exitCode, stdout, stderr, null);
|
|
|
|
public static ToolResult Failed(string error)
|
|
=> new(false, -1, string.Empty, string.Empty, error);
|
|
|
|
public static ToolResult Failed(string error, string stdout, int exitCode)
|
|
=> new(false, exitCode, stdout, error, error);
|
|
}
|
|
|
|
public sealed class ToolExecutionException : Exception
|
|
{
|
|
public ToolExecutionException(string message, ToolResult result)
|
|
: base(message)
|
|
{
|
|
Result = result;
|
|
}
|
|
|
|
public ToolResult Result { get; }
|
|
}
|