Nuke.Cola
Loading...
Searching...
No Matches
ToolEx.cs
1using System.Collections.Concurrent;
2using System.Diagnostics;
3using System.Text;
4using Nuke.Common;
5using Nuke.Common.Tooling;
6using Nuke.Common.Utilities;
7using Serilog;
8using Serilog.Events;
9
10namespace Nuke.Cola.Tooling;
11
12public delegate ToolEx? ToolExRetry(ToolEx previousTool, IProcess previousProcess, int attempt);
13
14/// <summary>
15/// Extended copy of Tool delegate of Nuke
16/// </summary>
17public delegate IReadOnlyCollection<Output>? ToolEx(
18 // Nuke Tool
19 ArgumentStringHandlerEx arguments = default,
20 string? workingDirectory = null,
21 IReadOnlyDictionary<string, string>? environmentVariables = null,
22 int? timeout = null,
23 bool? logOutput = null,
24 bool? logInvocation = null,
25 Action<OutputType, string>? logger = null,
26 Action<IProcess>? exitHandler = null,
27
28 // Extension
29 Action<StreamWriter>? input = null,
30 Encoding? standardOutputEncoding = null,
31 Encoding? standardInputEncoding = null,
32 ToolExRetry? retry = null
33);
34
35/// <summary>
36/// A record listing Tool and ToolEx delegate parameters and provides a way to meaningfully merge multiple together
37/// </summary>
38/// <param name="ToolArgs">Regular Tool delegate arguments</param>
39/// <param name="Input">Handle standard input stream after process creation</param>
40/// <param name="StandardOutputEncoding">Encoding for standard output. Default is UTF8 (with BOM)</param>
41/// <param name="StandardInputEncoding">Encoding for standard input. Default is UTF8 (without BOM)</param>
42/// <param name="Retry">Automatically make another attempt if the returned tool delegate is not null</param>
43public record class ToolExArguments(
44 ToolArguments ToolArgs,
45 Action<StreamWriter>? Input = null,
46 Encoding? StandardOutputEncoding = null,
47 Encoding? StandardInputEncoding = null,
48 ToolExRetry? Retry = null
49) {
50
51 /// <summary>
52 /// Merge two ToolEx argument records together.
53 /// </summary>
54 /// <remarks>
55 /// <list>
56 /// <item><term>Arguments </term><description> will be concatenated</description></item>
57 /// <item><term>Working directory </term><description> B overrides the one from A but not when B doesn't have one</description></item>
58 /// <item><term>Environmnent variables </term><description> will be merged</description></item>
59 /// <item><term>TimeOut </term><description> will be maxed</description></item>
60 /// <item><term>LogOutput </term><description> is OR-ed</description></item>
61 /// <item><term>LogInvocation </term><description> is OR-ed</description></item>
62 /// <item><term>Logger / ExitHandler </term><description> A + B is invoked</description></item>
63 /// <item><term>Input </term><description> A + B is invoked</description></item>
64 /// <item><term>Encodings </term><description> B overrides the one from A but not when B doesn't have one</description></item>
65 /// </list>
66 /// </remarks>
67 public static ToolExArguments operator | (ToolExArguments? a, ToolExArguments? b)
68 => new(
69 a?.ToolArgs | b?.ToolArgs,
70 a?.Input + b?.Input,
71 b?.StandardOutputEncoding ?? a?.StandardOutputEncoding,
72 b?.StandardInputEncoding ?? a?.StandardInputEncoding,
73 b?.Retry ?? a?.Retry
74 );
75
76 /// <summary>
77 /// Merge a ToolEx and a Tool argument record together.
78 /// </summary>
79 /// <remarks>
80 /// <list>
81 /// <item><term>Arguments </term><description> will be concatenated</description></item>
82 /// <item><term>Working directory </term><description> B overrides the one from A but not when B doesn't have one</description></item>
83 /// <item><term>Environmnent variables </term><description> will be merged</description></item>
84 /// <item><term>TimeOut </term><description> will be maxed</description></item>
85 /// <item><term>LogOutput </term><description> is OR-ed</description></item>
86 /// <item><term>LogInvocation </term><description> is OR-ed</description></item>
87 /// <item><term>Logger / ExitHandler </term><description> A + B is invoked</description></item>
88 /// <item><term>Input </term><description> Used from ToolEx arguments</description></item>
89 /// <item><term>Encodings </term><description> Used from ToolEx arguments</description></item>
90 /// </list>
91 /// </remarks>
92 public static ToolExArguments operator | (ToolExArguments? a, ToolArguments? b)
93 => new(
94 a?.ToolArgs | b,
95 a?.Input,
96 a?.StandardOutputEncoding,
97 a?.StandardInputEncoding,
98 a?.Retry
99 );
100
101 /// <summary>
102 /// Merge a Tool and a ToolEx argument record together.
103 /// </summary>
104 /// <remarks>
105 /// <list>
106 /// <item><term>Arguments </term><description> will be concatenated</description></item>
107 /// <item><term>Working directory </term><description> B overrides the one from A but not when B doesn't have one</description></item>
108 /// <item><term>Environmnent variables </term><description> will be merged</description></item>
109 /// <item><term>TimeOut </term><description> will be maxed</description></item>
110 /// <item><term>LogOutput </term><description> is OR-ed</description></item>
111 /// <item><term>LogInvocation </term><description> is OR-ed</description></item>
112 /// <item><term>Logger / ExitHandler </term><description> A + B is invoked</description></item>
113 /// <item><term>Input </term><description> Used from ToolEx arguments</description></item>
114 /// <item><term>Encodings </term><description> Used from ToolEx arguments</description></item>
115 /// </list>
116 /// </remarks>
117 public static ToolExArguments operator | (ToolArguments? a, ToolExArguments? b)
118 => new(
119 a | b?.ToolArgs,
120 b?.Input,
121 b?.StandardOutputEncoding,
122 b?.StandardInputEncoding,
123 b?.Retry
124 );
125}
126
127/// <summary>
128/// Propagated ToolEx delegate provider for launch parameter composition.
129/// </summary>
130/// <param name="Target"></param>
131/// <param name="PropagateArguments"></param>
132public record class PropagateToolExExecution(ToolEx Target, ToolExArguments? PropagateArguments = null)
133{
134 public IReadOnlyCollection<Output>? Execute(
135 // Nuke Tool
136 ArgumentStringHandlerEx arguments = default,
137 string? workingDirectory = null,
138 IReadOnlyDictionary<string, string>? environmentVariables = null,
139 int? timeout = null,
140 bool? logOutput = null,
141 bool? logInvocation = null,
142 Action<OutputType, string>? logger = null,
143 Action<IProcess>? exitHandler = null,
144
145 // Extension
146 Action<StreamWriter>? input = null,
147 Encoding? standardOutputEncoding = null,
148 Encoding? standardInputEncoding = null,
149 ToolExRetry? retry = null
150 ) => Target.ExecuteWith(
151 PropagateArguments | new ToolExArguments(
152 new(
153 arguments.ToStringAndClear(),
154 workingDirectory,
155 environmentVariables,
156 timeout,
157 logOutput,
158 logInvocation,
159 logger,
160 exitHandler
161 ),
162 input,
163 standardOutputEncoding,
164 standardInputEncoding,
165 retry
166 )
167 );
168}
169
170internal class ToolExExecutor
171{
172 private static readonly object _lock = new();
173
174 private readonly string _toolPath;
175
176 private int _attempt = 0;
177
178 public ToolExExecutor(string toolPath)
179 {
180 _toolPath = toolPath;
181 }
182
183 public IReadOnlyCollection<Output>? Execute(
184 // Nuke Tool
185 ArgumentStringHandlerEx arguments = default,
186 string? workingDirectory = null,
187 IReadOnlyDictionary<string, string>? environmentVariables = null,
188 int? timeout = null,
189 bool? logOutput = null,
190 bool? logInvocation = null,
191 Action<OutputType, string>? logger = null,
192 Action<IProcess>? exitHandler = null,
193
194 // Extension
195 Action<StreamWriter>? input = null,
196 Encoding? standardOutputEncoding = null,
197 Encoding? standardInputEncoding = null,
198 ToolExRetry? retry = null
199 )
200 {
201 workingDirectory ??= EnvironmentInfo.WorkingDirectory;
202 logInvocation ??= true;
203 logOutput ??= true;
204 logger ??= ProcessTasks.DefaultLogger;
205 standardOutputEncoding ??= Encoding.UTF8;
206 standardInputEncoding ??= new UTF8Encoding(false);
207 exitHandler ??= p => p.AssertZeroExitCode();
208
209 var outputFilter = arguments.GetFilter();
210
211 var toolPath = _toolPath;
212 var args = arguments.ToStringAndClear();
213
214 if (!Path.IsPathRooted(_toolPath) && !_toolPath.Contains(Path.DirectorySeparatorChar))
215 toolPath = ToolPathResolver.GetPathExecutable(_toolPath);
216
217 var toolPathOverride = GetToolPathOverride(toolPath);
218 if (!string.IsNullOrEmpty(toolPathOverride))
219 {
220 args = $"{toolPath.DoubleQuoteIfNeeded()} {args}".TrimEnd();
221 toolPath = toolPathOverride;
222 }
223
224 Assert.FileExists(toolPath);
225 Assert.DirectoryExists(workingDirectory);
226
227 var startInfo = new ProcessStartInfo
228 {
229 FileName = toolPath,
230 Arguments = args,
231 WorkingDirectory = workingDirectory,
232 RedirectStandardOutput = true,
233 RedirectStandardError = true,
234 RedirectStandardInput = input != null,
235 UseShellExecute = false,
236 StandardErrorEncoding = standardOutputEncoding,
237 StandardOutputEncoding = standardOutputEncoding,
238 };
239 if (input != null)
240 startInfo.StandardInputEncoding = standardInputEncoding;
241
242 if (environmentVariables != null)
243 {
244 startInfo.Environment.Clear();
245 foreach (var (key, value) in environmentVariables)
246 startInfo.Environment[key] = value;
247 }
248
249 if (logInvocation.Value)
250 LogInvocation(startInfo, outputFilter);
251
252 var process = Process.Start(startInfo);
253 if (process == null)
254 return null;
255
256 input?.Invoke(process.StandardInput);
257
258 var output = GetOutputCollection(process, logger, outputFilter);
259 var proc2 = new Process2(process, outputFilter, timeout, output);
260 proc2.AssertWaitForExit();
261
262 if (retry != null)
263 {
264 var previousTool = new ToolEx(Execute).With(
265 args,
266 workingDirectory,
267 environmentVariables,
268 timeout,
269 logOutput,
270 logInvocation,
271 logger,
272 exitHandler,
273 input,
274 standardOutputEncoding,
275 standardInputEncoding,
276 retry
277 );
278 var nextAttempt = retry(previousTool, proc2, ++_attempt);
279 if (nextAttempt != null)
280 {
281 return nextAttempt();
282 }
283 }
284 exitHandler.Invoke(proc2);
285 return proc2.Output;
286 }
287
288 private static string? GetToolPathOverride(string toolPath)
289 {
290 if (toolPath.EndsWithOrdinalIgnoreCase(".dll"))
291 {
292 return ToolPathResolver.TryGetEnvironmentExecutable("DOTNET_EXE") ??
293 ToolPathResolver.GetPathExecutable("dotnet");
294 }
295
296 if (EnvironmentInfo.IsUnix &&
297 toolPath.EndsWithOrdinalIgnoreCase(".exe") &&
298 !EnvironmentInfo.IsWsl)
299 return ToolPathResolver.GetPathExecutable("mono");
300
301 return null;
302 }
303
304 private static BlockingCollection<Output> GetOutputCollection(
305 Process process,
306 Action<OutputType, string>? logger,
307 Func<string, string> outputFilter)
308 {
309 var output = new BlockingCollection<Output>();
310
311 process.OutputDataReceived += (_, e) =>
312 {
313 if (e.Data == null)
314 return;
315
316 var filteredOutput = outputFilter(e.Data);
317 output.Add(new Output { Text = filteredOutput, Type = OutputType.Std });
318 logger?.Invoke(OutputType.Std, filteredOutput);
319 };
320 process.ErrorDataReceived += (_, e) =>
321 {
322 if (e.Data == null)
323 return;
324
325 var filteredOutput = outputFilter(e.Data);
326 output.Add(new Output { Text = filteredOutput, Type = OutputType.Err });
327 logger?.Invoke(OutputType.Err, filteredOutput);
328 };
329
330 process.BeginOutputReadLine();
331 process.BeginErrorReadLine();
332
333 return output;
334 }
335
336 private static void LogInvocation(ProcessStartInfo startInfo, Func<string, string> outputFilter)
337 {
338 lock (_lock)
339 {
340 Log.Information("> {ToolPath} {Arguments}", startInfo.FileName.DoubleQuoteIfNeeded(), outputFilter(startInfo.Arguments));
341 Log.Write(
342 startInfo.WorkingDirectory != EnvironmentInfo.WorkingDirectory
343 ? LogEventLevel.Information
344 : LogEventLevel.Verbose,
345 "@ {WorkingDirectory}",
346 startInfo.WorkingDirectory);
347 }
348 }
349}
Extension class for dealing with passing arguments from the user through nuke to a tool.
Definition Arguments.cs:15
record class PropagateToolExExecution(ToolEx Target, ToolExArguments? PropagateArguments=null)
Propagated ToolEx delegate provider for launch parameter composition.
Definition ToolEx.cs:132
delegate? IReadOnlyCollection< Output > ToolEx(ArgumentStringHandlerEx arguments=default, string? workingDirectory=null, IReadOnlyDictionary< string, string >? environmentVariables=null, int? timeout=null, bool? logOutput=null, bool? logInvocation=null, Action< OutputType, string >? logger=null, Action< IProcess >? exitHandler=null, Action< StreamWriter >? input=null, Encoding? standardOutputEncoding=null, Encoding? standardInputEncoding=null, ToolExRetry? retry=null)
Extended copy of Tool delegate of Nuke.
record class ToolArguments(string? Arguments=null, string? WorkingDirectory=null, IReadOnlyDictionary< string, string >? EnvironmentVariables=null, int? Timeout=null, bool? LogOutput=null, bool? LogInvocation=null, Action< OutputType, string >? Logger=null, Action< IProcess >? ExitHandler=null)
A record listing Tool delegate parameters and provides a way to meaningfully merge multiple together.
record class ToolExArguments(ToolArguments ToolArgs, Action< StreamWriter >? Input=null, Encoding? StandardOutputEncoding=null, Encoding? StandardInputEncoding=null, ToolExRetry? Retry=null)
A record listing Tool and ToolEx delegate parameters and provides a way to meaningfully merge multipl...
Definition ToolEx.cs:43