110 string workingDirectory,
112 CancellationToken cancellationToken,
113 IReadOnlyDictionary<string, string>? environment,
114 string? fileRedirect,
115 bool readStandardHandles,
118 ArgumentNullException.ThrowIfNull(fileName);
119 ArgumentNullException.ThrowIfNull(workingDirectory);
120 ArgumentNullException.ThrowIfNull(arguments);
122 var enviromentLogLines = environment ==
null
124 : String.Concat(environment.Select(kvp => $
"{Environment.NewLine}\t- {kvp.Key}={kvp.Value}"));
127 "Launching process in {workingDirectory}: {exe} {arguments}{environment}",
134 "Shell launching process in {workingDirectory}: {exe} {arguments}{environment}",
140 var handle =
new global::System.Diagnostics.Process();
143 handle.StartInfo.FileName = fileName;
144 handle.StartInfo.Arguments = arguments;
145 if (environment !=
null)
146 foreach (var kvp
in environment)
147 handle.StartInfo.Environment.Add(kvp!);
149 handle.StartInfo.WorkingDirectory = workingDirectory;
151 handle.StartInfo.UseShellExecute = !noShellExecute;
153 Task<string?>? readTask =
null;
154 CancellationTokenSource? disposeCts =
null;
157 TaskCompletionSource<int>? processStartTcs =
null;
158 if (readStandardHandles)
160 processStartTcs =
new TaskCompletionSource<int>();
161 disposeCts =
new CancellationTokenSource();
162 readTask =
ConsumeReaders(handle, processStartTcs.Task, fileRedirect, disposeCts.Token);
188 processStartTcs?.SetResult(pid);
192 processStartTcs?.SetException(ex);
208 disposeCts?.Dispose();
248 async Task<string?>
ConsumeReaders(global::System.Diagnostics.Process handle, Task<int> startupAndPid,
string? fileRedirect, CancellationToken cancellationToken)
250 handle.StartInfo.RedirectStandardOutput =
true;
251 handle.StartInfo.RedirectStandardError =
true;
255 await
using var fileWriter = fileStream !=
null ?
new StreamWriter(fileStream) :
null;
257 var stringBuilder = fileStream ==
null ?
new StringBuilder() :
null;
259 var dataChannel = Channel.CreateUnbounded<
string>(
260 new UnboundedChannelOptions
262 AllowSynchronousContinuations = !writingToFile,
264 SingleWriter =
false,
268 async
void DataReceivedHandler(
object sender, DataReceivedEventArgs eventArgs)
270 var line = eventArgs.Data;
273 var handlesRemaining = Interlocked.Decrement(ref handlesOpen);
274 if (handlesRemaining == 0)
275 dataChannel.Writer.Complete();
282 await dataChannel.Writer.WriteAsync(line, cancellationToken);
284 catch (OperationCanceledException ex)
286 logger.LogWarning(ex,
"Handle channel write interrupted!");
290 handle.OutputDataReceived += DataReceivedHandler;
291 handle.ErrorDataReceived += DataReceivedHandler;
293 async ValueTask OutputWriter()
295 var enumerable = dataChannel.Reader.ReadAllAsync(cancellationToken);
298 var enumerator = enumerable.GetAsyncEnumerator(cancellationToken);
299 var nextEnumeration = enumerator.MoveNextAsync();
300 while (await nextEnumeration)
302 var text = enumerator.Current;
303 nextEnumeration = enumerator.MoveNextAsync();
304 await fileWriter!.WriteLineAsync(text.AsMemory(), cancellationToken);
306 if (!nextEnumeration.IsCompleted)
307 await fileWriter.FlushAsync(cancellationToken);
311 await
foreach (var text
in enumerable)
312 stringBuilder!.AppendLine(text);
315 var pid = await startupAndPid;
316 logger.LogTrace(
"Starting read for PID {pid}...", pid);
318 using (cancellationToken.Register(() => dataChannel.Writer.TryComplete()))
320 handle.BeginOutputReadLine();
321 using (cancellationToken.Register(handle.CancelOutputRead))
323 handle.BeginErrorReadLine();
324 using (cancellationToken.Register(handle.CancelErrorRead))
328 await OutputWriter();
330 logger.LogTrace(
"Finished read for PID {pid}", pid);
332 catch (OperationCanceledException ex)
334 logger.LogWarning(ex,
"PID {pid} stream reading interrupted!", pid);
336 await fileWriter!.WriteLineAsync(
"-- Process detached, log truncated. This is likely due a to TGS restart --");
342 return stringBuilder?.ToString();