journal/Journal.DevTool/Core/Debug/DebugProfileRunner.cs

186 lines
7.4 KiB
C#

using Sdt.Config;
using Sdt.Runner;
namespace Sdt.Core.Debug;
public sealed class DebugProfileRunner(
IToolProbe toolProbe,
IPrereqInstaller installer) : IDebugProfileRunner
{
private readonly IToolProbe _toolProbe = toolProbe;
private readonly IPrereqInstaller _installer = installer;
public async Task<DebugRunResult> RunAsync(
DebugProfileDefinition profile,
DevToolConfig config,
string projectRoot,
bool verbose,
Func<string, InstallPlan, Task<bool>> confirmInstallAsync,
Action<string, bool> onOutput,
Action<RunEvent>? onEvent = null,
CancellationToken cancellationToken = default)
{
var probes = new List<ProbeResult>();
var output = new List<string>();
onEvent?.Invoke(new RunEvent(
Category: "debug",
Type: RunEventType.DebugStarted,
Message: $"Debug profile '{profile.Id}' started."));
var requires = profile.Requires.Count > 0
? profile.Requires
: InferRequirements(profile);
foreach (var req in requires)
{
var probe = await _toolProbe.ProbeAsync(req.Tool, projectRoot, config, cancellationToken).ConfigureAwait(false);
probes.Add(probe);
if (probe.IsAvailable)
continue;
if (!string.IsNullOrWhiteSpace(probe.Details))
{
var line = $"Probe detail [{req.Tool}]: {probe.Details}";
output.Add("OUT: " + line);
if (verbose)
onOutput(line, false);
}
if (req.InstallPolicy == InstallPolicy.Never)
{
onEvent?.Invoke(new RunEvent(
Category: "debug",
Type: RunEventType.DebugCompleted,
Message: $"Missing prerequisite '{req.Tool}'.",
Tool: req.Tool,
Success: false));
return new DebugRunResult(
Success: false,
StopReason: ExecutionStopReason.MissingPrereq,
Message: $"Missing prerequisite '{req.Tool}' for debug profile '{profile.Label}'.",
Profile: profile,
RunResult: null,
OutputLines: output,
Probes: probes);
}
var installPlan = await _installer.GetInstallPlanAsync(req.Tool, projectRoot, config, cancellationToken).ConfigureAwait(false);
if (!installPlan.Supported || installPlan.Commands.Count == 0)
{
onEvent?.Invoke(new RunEvent(
Category: "debug",
Type: RunEventType.DebugCompleted,
Message: $"No installer plan available for '{req.Tool}'.",
Tool: req.Tool,
Success: false));
return new DebugRunResult(
Success: false,
StopReason: ExecutionStopReason.MissingPrereq,
Message: $"Missing prerequisite '{req.Tool}' and no installer plan is available.",
Profile: profile,
RunResult: null,
OutputLines: output,
Probes: probes);
}
var approved = req.InstallPolicy == InstallPolicy.Auto
? true
: await confirmInstallAsync(req.Tool, installPlan).ConfigureAwait(false);
if (!approved)
{
onEvent?.Invoke(new RunEvent(
Category: "debug",
Type: RunEventType.InstallDeclined,
Message: $"Install declined for '{req.Tool}'.",
Tool: req.Tool,
Success: false));
return new DebugRunResult(
Success: false,
StopReason: ExecutionStopReason.UserDeclined,
Message: $"Install declined for missing prerequisite '{req.Tool}'.",
Profile: profile,
RunResult: null,
OutputLines: output,
Probes: probes);
}
foreach (var cmd in installPlan.Commands)
{
var installResult = await _installer.RunInstallAsync(cmd, projectRoot, onOutput, cancellationToken).ConfigureAwait(false);
if (!installResult.Success)
{
onEvent?.Invoke(new RunEvent(
Category: "debug",
Type: RunEventType.DebugCompleted,
Message: $"Install failed for '{req.Tool}'.",
Tool: req.Tool,
Success: false,
ExitCode: installResult.ExitCode));
return new DebugRunResult(
Success: false,
StopReason: ExecutionStopReason.InstallFailed,
Message: $"Failed to install prerequisite '{req.Tool}'.",
Profile: profile,
RunResult: installResult,
OutputLines: output,
Probes: probes);
}
}
}
var cwd = Path.GetFullPath(Path.Combine(projectRoot, profile.WorkingDir));
var mergedEnv = profile.Env.Count > 0 ? profile.Env : null;
onEvent?.Invoke(new RunEvent(
Category: "debug",
Type: RunEventType.DebugCommandStarted,
Message: $"{profile.Command} {string.Join(" ", profile.Args)}"));
var run = await ProcessRunner.RunAsync(
profile.Command,
profile.Args,
cwd,
(line, isErr) =>
{
output.Add((isErr ? "ERR: " : "OUT: ") + line);
if (verbose)
onOutput(line, isErr);
},
mergedEnv,
cancellationToken).ConfigureAwait(false);
onEvent?.Invoke(new RunEvent(
Category: "debug",
Type: RunEventType.DebugCommandCompleted,
Message: $"Debug command exited {run.ExitCode}.",
Success: run.Success,
ExitCode: run.ExitCode));
onEvent?.Invoke(new RunEvent(
Category: "debug",
Type: RunEventType.DebugCompleted,
Message: run.Success ? "Debug run completed." : "Debug run failed.",
Success: run.Success,
ExitCode: run.ExitCode));
return new DebugRunResult(
Success: run.Success,
StopReason: run.Success ? null : ExecutionStopReason.CommandFailed,
Message: run.Success
? $"Debug profile '{profile.Label}' completed."
: $"Debug profile '{profile.Label}' exited with code {run.ExitCode}.",
Profile: profile,
RunResult: run,
OutputLines: output,
Probes: probes);
}
private static List<ToolRequirement> InferRequirements(DebugProfileDefinition profile)
{
return profile.Type.ToLowerInvariant() switch
{
"dotnet" => [new ToolRequirement { Tool = "dotnet" }],
"node" => [new ToolRequirement { Tool = "node" }, new ToolRequirement { Tool = "npm" }],
"python" => [new ToolRequirement { Tool = "python" }],
_ => string.IsNullOrWhiteSpace(profile.Command)
? []
: [new ToolRequirement { Tool = profile.Command }],
};
}
}