186 lines
7.4 KiB
C#
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 }],
|
|
};
|
|
}
|
|
}
|