110 string workingDirectory,
112 CancellationToken cancellationToken,
113 IReadOnlyDictionary<string, string>? environment,
114 string? fileRedirect,
115 bool readStandardHandles,
117 bool doNotLogArguments)
119 ArgumentNullException.ThrowIfNull(fileName);
120 ArgumentNullException.ThrowIfNull(workingDirectory);
121 ArgumentNullException.ThrowIfNull(arguments);
123 var enviromentLogLines = environment ==
null
125 : String.Concat(environment.Select(kvp => $
"{Environment.NewLine}\t- {kvp.Key}={kvp.Value}"));
128 "Launching process in {workingDirectory}: {exe} {arguments}{environment}",
131 doNotLogArguments ?
"<ARGUMENTS HIDDEN>" : arguments,
135 "Shell launching process in {workingDirectory}: {exe} {arguments}{environment}",
138 doNotLogArguments ?
"<ARGUMENTS HIDDEN>" : arguments,
141 var handle =
new global::System.Diagnostics.Process();
144 handle.StartInfo.FileName = fileName;
145 handle.StartInfo.Arguments = arguments;
146 if (environment !=
null)
147 foreach (var kvp
in environment)
148 handle.StartInfo.Environment.Add(kvp!);
150 handle.StartInfo.WorkingDirectory = workingDirectory;
152 handle.StartInfo.UseShellExecute = !noShellExecute;
154 Task<string?>? readTask =
null;
155 CancellationTokenSource? disposeCts =
null;
158 TaskCompletionSource<int>? processStartTcs =
null;
159 if (readStandardHandles)
161 processStartTcs =
new TaskCompletionSource<int>();
162 disposeCts =
new CancellationTokenSource();
163 readTask =
ConsumeReaders(handle, processStartTcs.Task, fileRedirect, disposeCts.Token);
189 processStartTcs?.SetResult(pid);
193 processStartTcs?.SetException(ex);
209 disposeCts?.Dispose();
249 async Task<string?>
ConsumeReaders(global::System.Diagnostics.Process handle, Task<int> startupAndPid,
string? fileRedirect, CancellationToken cancellationToken)
251 handle.StartInfo.RedirectStandardOutput =
true;
252 handle.StartInfo.RedirectStandardError =
true;
256 await
using var fileWriter = fileStream !=
null ?
new StreamWriter(fileStream) :
null;
258 var stringBuilder = fileStream ==
null ?
new StringBuilder() :
null;
260 var dataChannel = Channel.CreateUnbounded<
string>(
261 new UnboundedChannelOptions
263 AllowSynchronousContinuations = !writingToFile,
265 SingleWriter =
false,
269 async
void DataReceivedHandler(
object sender, DataReceivedEventArgs eventArgs)
271 var line = eventArgs.Data;
274 var handlesRemaining = Interlocked.Decrement(ref handlesOpen);
275 if (handlesRemaining == 0)
276 dataChannel.Writer.Complete();
283 await dataChannel.Writer.WriteAsync(line, cancellationToken);
285 catch (OperationCanceledException ex)
287 logger.LogWarning(ex,
"Handle channel write interrupted!");
291 handle.OutputDataReceived += DataReceivedHandler;
292 handle.ErrorDataReceived += DataReceivedHandler;
294 async ValueTask OutputWriter()
296 var enumerable = dataChannel.Reader.ReadAllAsync(cancellationToken);
299 var enumerator = enumerable.GetAsyncEnumerator(cancellationToken);
300 var nextEnumeration = enumerator.MoveNextAsync();
301 while (await nextEnumeration)
303 var text = enumerator.Current;
304 nextEnumeration = enumerator.MoveNextAsync();
305 await fileWriter!.WriteLineAsync(text.AsMemory(), cancellationToken);
307 if (!nextEnumeration.IsCompleted)
308 await fileWriter.FlushAsync(cancellationToken);
312 await
foreach (var text
in enumerable)
313 stringBuilder!.AppendLine(text);
316 var pid = await startupAndPid;
317 logger.LogTrace(
"Starting read for PID {pid}...", pid);
319 using (cancellationToken.Register(() => dataChannel.Writer.TryComplete()))
321 handle.BeginOutputReadLine();
322 using (cancellationToken.Register(handle.CancelOutputRead))
324 handle.BeginErrorReadLine();
325 using (cancellationToken.Register(handle.CancelErrorRead))
329 await OutputWriter();
331 logger.LogTrace(
"Finished read for PID {pid}", pid);
333 catch (OperationCanceledException ex)
335 logger.LogWarning(ex,
"PID {pid} stream reading interrupted!", pid);
337 await fileWriter!.WriteLineAsync(
"-- Process detached, log truncated. This is likely due a to TGS restart --");
343 return stringBuilder?.ToString();