-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathProcessRunner.cs
More file actions
139 lines (125 loc) · 4.33 KB
/
ProcessRunner.cs
File metadata and controls
139 lines (125 loc) · 4.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// Process runner port: absorbs child-process execution, OS shell wrapping,
// output tail buffering, and the non-zero-exit → failed-step convention.
using System.Diagnostics;
using System.Runtime.InteropServices;
using Spectre.Console;
internal interface IProcessRunner
{
ProcRunResult Run(ProcSpec spec, RunContext ctx);
}
internal readonly record struct ProcSpec(
string FileName,
string Arguments,
string DisplayShell,
string DisplayCommandLine,
int TailLines = 50,
IReadOnlyList<string>? ArgList = null)
{
public static ProcSpec Exec(string file, string args, int tail = 50)
=> new(file, args, "exec",
string.IsNullOrEmpty(args) ? file : $"{file} {args}", tail);
public static ProcSpec ExecArgs(string file, IReadOnlyList<string> args, int tail = 50)
=> new(file, string.Empty, "exec",
args.Count == 0 ? file : $"{file} {string.Join(' ', args)}",
tail, args);
public static ProcSpec Shell(string commandLine, int tail = 50)
=> RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? new("cmd.exe", string.Empty, "cmd", commandLine, tail, new[] { "/S", "/C", commandLine })
: new("bash", string.Empty, "bash", commandLine, tail, new[] { "-c", commandLine });
}
internal sealed record ProcRunResult(
int ExitCode,
string[] StderrTail,
string[] StdoutTail,
string DisplayShell,
string DisplayCommandLine)
{
public bool Ok => ExitCode == 0;
}
internal sealed class RealProcessRunner : IProcessRunner
{
public ProcRunResult Run(ProcSpec spec, RunContext ctx)
{
var psi = new ProcessStartInfo(spec.FileName)
{
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = ctx.JsonMode,
};
if (spec.ArgList is { Count: > 0 } argList)
foreach (var a in argList) psi.ArgumentList.Add(a);
else
psi.Arguments = spec.Arguments;
using var p = new Process { StartInfo = psi };
var errBuffer = new Queue<string>();
var outBuffer = new Queue<string>();
var bufferLock = new object();
p.ErrorDataReceived += (_, e) =>
{
if (e.Data is null) return;
lock (bufferLock)
{
if (!ctx.JsonMode) Console.Error.WriteLine(e.Data);
errBuffer.Enqueue(e.Data);
while (errBuffer.Count > spec.TailLines) errBuffer.Dequeue();
}
};
if (ctx.JsonMode)
{
p.OutputDataReceived += (_, e) =>
{
if (e.Data is null) return;
lock (bufferLock)
{
outBuffer.Enqueue(e.Data);
while (outBuffer.Count > spec.TailLines) outBuffer.Dequeue();
}
};
}
try
{
p.Start();
}
catch (Exception ex)
{
if (!ctx.JsonMode) AnsiConsole.WriteException(ex);
return new ProcRunResult(-1, new[] { ex.Message }, Array.Empty<string>(),
spec.DisplayShell, spec.DisplayCommandLine);
}
p.BeginErrorReadLine();
if (ctx.JsonMode) p.BeginOutputReadLine();
p.WaitForExit();
lock (bufferLock)
return new ProcRunResult(p.ExitCode, errBuffer.ToArray(), outBuffer.ToArray(),
spec.DisplayShell, spec.DisplayCommandLine);
}
}
internal static class ProcRunnerExtensions
{
public static bool RunOrFail(
this IProcessRunner runner,
ProcSpec spec,
StepResult step,
RunContext ctx,
string errorCode,
string? errorMessage,
out ProcRunResult result)
{
result = runner.Run(spec, ctx);
if (result.Ok) return true;
step.Status = "failed";
step.ExitCode = 1;
step.Error = new StepError
{
Code = errorCode,
Message = errorMessage ?? $"{spec.DisplayCommandLine} exited with code {result.ExitCode}",
Detail = new Dictionary<string, object?>
{
["exitCode"] = result.ExitCode,
["stderrTail"] = result.StderrTail,
["stdoutTail"] = result.StdoutTail,
},
};
return false;
}
}