stabilizaiton work - projects rework for maintenanceability and ui livening
This commit is contained in:
6
src/__Libraries/StellaOps.Interop/IToolPathResolver.cs
Normal file
6
src/__Libraries/StellaOps.Interop/IToolPathResolver.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace StellaOps.Interop;
|
||||
|
||||
public interface IToolPathResolver
|
||||
{
|
||||
string? ResolveToolPath(string tool);
|
||||
}
|
||||
14
src/__Libraries/StellaOps.Interop/IToolProcessRunner.cs
Normal file
14
src/__Libraries/StellaOps.Interop/IToolProcessRunner.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Interop;
|
||||
|
||||
public interface IToolProcessRunner
|
||||
{
|
||||
Task<ToolResult> RunAsync(
|
||||
string tool,
|
||||
string toolPath,
|
||||
string args,
|
||||
string workingDirectory,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
@@ -10,3 +10,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| AUDIT-0091-A | TODO | Pending approval (revalidated 2026-01-08). |
|
||||
| AUDIT-TESTGAP-CORELIB-INTEROP-0001 | DONE | Added interop ToolManager unit tests + test wiring (2026-01-13). |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
| REMED-08 | DONE | Split ToolManager; added resolver/runner interfaces + unit stubs. |
|
||||
|
||||
14
src/__Libraries/StellaOps.Interop/ToolExecutionException.cs
Normal file
14
src/__Libraries/StellaOps.Interop/ToolExecutionException.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace StellaOps.Interop;
|
||||
|
||||
public sealed class ToolExecutionException : Exception
|
||||
{
|
||||
public ToolExecutionException(string message, ToolResult result)
|
||||
: base(message)
|
||||
{
|
||||
Result = result;
|
||||
}
|
||||
|
||||
public ToolResult Result { get; }
|
||||
}
|
||||
7
src/__Libraries/StellaOps.Interop/ToolManager.Path.cs
Normal file
7
src/__Libraries/StellaOps.Interop/ToolManager.Path.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace StellaOps.Interop;
|
||||
|
||||
public sealed partial class ToolManager
|
||||
{
|
||||
public static string? FindOnPath(string tool)
|
||||
=> ToolPathResolver.FindOnPath(tool);
|
||||
}
|
||||
@@ -1,18 +1,26 @@
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Interop;
|
||||
|
||||
public sealed class ToolManager
|
||||
public sealed partial class ToolManager
|
||||
{
|
||||
private readonly string _workDir;
|
||||
private readonly IReadOnlyDictionary<string, string> _toolPaths;
|
||||
private readonly IToolPathResolver _pathResolver;
|
||||
private readonly IToolProcessRunner _processRunner;
|
||||
|
||||
public ToolManager(string workDir, IReadOnlyDictionary<string, string>? toolPaths = null)
|
||||
public ToolManager(
|
||||
string workDir,
|
||||
IReadOnlyDictionary<string, string>? toolPaths = null,
|
||||
IToolPathResolver? pathResolver = null,
|
||||
IToolProcessRunner? processRunner = null)
|
||||
{
|
||||
_workDir = workDir;
|
||||
_toolPaths = toolPaths ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var resolvedPaths = toolPaths ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
_pathResolver = pathResolver ?? new ToolPathResolver(resolvedPaths);
|
||||
_processRunner = processRunner ?? new ToolProcessRunner();
|
||||
}
|
||||
|
||||
public async Task VerifyToolAsync(string tool, string args, CancellationToken ct = default)
|
||||
@@ -28,125 +36,16 @@ public sealed class ToolManager
|
||||
|
||||
public async Task<ToolResult> RunAsync(string tool, string args, CancellationToken ct = default)
|
||||
{
|
||||
var toolPath = ResolveToolPath(tool);
|
||||
var toolPath = _pathResolver.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);
|
||||
}
|
||||
return await _processRunner.RunAsync(tool, toolPath, args, _workDir, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public bool IsToolAvailable(string tool) => ResolveToolPath(tool) is not null;
|
||||
public bool IsToolAvailable(string tool) => _pathResolver.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; }
|
||||
public string? ResolveToolPath(string tool) => _pathResolver.ResolveToolPath(tool);
|
||||
}
|
||||
|
||||
64
src/__Libraries/StellaOps.Interop/ToolPathResolver.cs
Normal file
64
src/__Libraries/StellaOps.Interop/ToolPathResolver.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace StellaOps.Interop;
|
||||
|
||||
internal sealed class ToolPathResolver : IToolPathResolver
|
||||
{
|
||||
private readonly IReadOnlyDictionary<string, string> _toolPaths;
|
||||
|
||||
public ToolPathResolver(IReadOnlyDictionary<string, string> toolPaths)
|
||||
{
|
||||
_toolPaths = toolPaths ?? throw new ArgumentNullException(nameof(toolPaths));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
54
src/__Libraries/StellaOps.Interop/ToolProcessRunner.cs
Normal file
54
src/__Libraries/StellaOps.Interop/ToolProcessRunner.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Interop;
|
||||
|
||||
internal sealed class ToolProcessRunner : IToolProcessRunner
|
||||
{
|
||||
public async Task<ToolResult> RunAsync(
|
||||
string tool,
|
||||
string toolPath,
|
||||
string args,
|
||||
string workingDirectory,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = toolPath,
|
||||
Arguments = args,
|
||||
WorkingDirectory = workingDirectory,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/__Libraries/StellaOps.Interop/ToolResult.cs
Normal file
18
src/__Libraries/StellaOps.Interop/ToolResult.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace StellaOps.Interop;
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user