journal/Journal.DevTool/Core/ToolProbeService.cs

123 lines
4.7 KiB
C#

using System.Diagnostics;
using System.Text.Json;
using Sdt.Config;
namespace Sdt.Core;
public sealed class ToolProbeService : IToolProbe
{
public async Task<ProbeResult> ProbeAsync(
string tool,
string projectRoot,
DevToolConfig? config = null,
CancellationToken cancellationToken = default)
{
var direct = await ProbeDirectAsync(tool, config, cancellationToken).ConfigureAwait(false);
if (direct.IsAvailable)
return direct;
var scriptPath = ScriptLocator.FindHelperScript(projectRoot, "diag.py");
if (scriptPath is null)
return direct;
if (!(await ProbeDirectAsync("python", config, cancellationToken).ConfigureAwait(false)).IsAvailable)
return direct;
try
{
var psi = new ProcessStartInfo
{
FileName = PythonResolver.ResolveExecutable(),
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = projectRoot,
};
psi.ArgumentList.Add(scriptPath);
psi.ArgumentList.Add("probe");
psi.ArgumentList.Add("--tool");
psi.ArgumentList.Add(tool);
psi.ArgumentList.Add("--json");
using var process = new Process { StartInfo = psi };
process.Start();
var stdout = await process.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
var stderr = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
if (process.ExitCode != 0)
return new ProbeResult(tool, false, Details: stderr.Trim());
var parsed = JsonSerializer.Deserialize<DiagProbeJson>(stdout);
if (parsed is null)
return new ProbeResult(tool, false, Details: "diag.py returned invalid JSON");
return new ProbeResult(parsed.Tool ?? tool, parsed.Available, parsed.Version, parsed.Details);
}
catch (Exception ex)
{
return new ProbeResult(tool, false, Details: ex.Message);
}
}
private static async Task<ProbeResult> ProbeDirectAsync(string tool, DevToolConfig? config, CancellationToken cancellationToken)
{
var command = tool.ToLowerInvariant() switch
{
"python" => PythonResolver.ResolveExecutable(),
"dotnet" => "dotnet",
"node" => "node",
"npm" => "npm",
"cargo" => "cargo",
"tauri" => "tauri",
"git" => "git",
"docker" => "docker",
_ => tool,
};
var resolution = CommandResolver.ResolveWithTrace(command, config, tool);
command = resolution.Resolved;
var versionArg = command is "python" ? "--version" : "--version";
try
{
var psi = new ProcessStartInfo
{
FileName = command,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
psi.ArgumentList.Add(versionArg);
using var process = new Process { StartInfo = psi };
process.Start();
var stdout = await process.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
var stderr = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
if (process.ExitCode != 0)
{
var failDetails = string.IsNullOrWhiteSpace(stderr) ? stdout.Trim() : stderr.Trim();
var trace = $"{resolution.Source}: {resolution.Resolved}";
return new ProbeResult(tool, false, Details: string.IsNullOrWhiteSpace(failDetails) ? trace : $"{trace} | {failDetails}");
}
var version = string.IsNullOrWhiteSpace(stdout) ? stderr.Trim() : stdout.Trim();
return new ProbeResult(tool, true, Version: version, Details: $"{resolution.Source}: {resolution.Resolved}");
}
catch (Exception ex)
{
return new ProbeResult(tool, false, Details: $"{resolution.Source}: {resolution.Resolved} | {ex.Message}");
}
}
private sealed class DiagProbeJson
{
public string? Tool { get; init; }
public bool Available { get; init; }
public string? Version { get; init; }
public string? Details { get; init; }
}
}