2using System.Collections.Generic;
3using System.Diagnostics;
8using System.Threading.Channels;
9using System.Threading.Tasks;
11using Microsoft.Extensions.Logging;
38 readonly ILogger<ProcessExecutor>
logger;
72 ILogger<ProcessExecutor>
logger,
77 this.logger =
logger ??
throw new ArgumentNullException(nameof(
logger));
84 logger.LogDebug(
"Attaching to process {pid}...",
id);
85 global::System.Diagnostics.Process handle;
88 handle = global::System.Diagnostics.Process.GetProcessById(
id);
92 logger.LogDebug(e,
"Unable to get process {pid}!",
id);
102 logger.LogTrace(
"Getting current process...");
103 var handle = global::System.Diagnostics.Process.GetCurrentProcess();
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();
222 logger.LogTrace(
"GetProcessByName: {processName}...", name ??
throw new ArgumentNullException(nameof(name)));
223 var procs = global::System.Diagnostics.Process.GetProcessesByName(name);
224 global::System.Diagnostics.Process? handle =
null;
225 foreach (var proc
in procs)
230 logger.LogTrace(
"Disposing extra found PID: {pid}...", proc.Id);
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();
async ValueTask< IProcess > LaunchProcess(string fileName, string workingDirectory, string arguments, CancellationToken cancellationToken, IReadOnlyDictionary< string, string >? environment, string? fileRedirect, bool readStandardHandles, bool noShellExecute)
Launch a IProcess. A ValueTask<TResult> resulting in the new IProcess.
async Task< string?> ConsumeReaders(global::System.Diagnostics.Process handle, Task< int > startupAndPid, string? fileRedirect, CancellationToken cancellationToken)
Consume the stdout/stderr streams into a Task.
Process CreateFromExistingHandle(global::System.Diagnostics.Process handle)
Create a IProcess given an existing handle .
readonly IProcessFeatures processFeatures
The IProcessFeatures for the ProcessExecutor.
readonly IIOManager ioManager
The IIOManager for the ProcessExecutor.
static void WithProcessLaunchExclusivity(Action action)
Runs a given action making sure to not launch any processes while its running.
IProcess? GetProcess(int id)
Get a IProcess by id . The IProcess represented by id on success, null on failure.
IProcess? GetProcessByName(string name)
Get a IProcess with a given name . The IProcess represented by name on success, null on failure.
readonly ILogger< ProcessExecutor > logger
The ILogger for the ProcessExecutor.
IProcess GetCurrentProcess()
Get a IProcess representing the running executable. The current IProcess.
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the ProcessExecutor.
ProcessExecutor(IProcessFeatures processFeatures, IIOManager ioManager, ILogger< ProcessExecutor > logger, ILoggerFactory loggerFactory)
Initializes a new instance of the ProcessExecutor class.
static readonly ReaderWriterLockSlim ExclusiveProcessLaunchLock
ReaderWriterLockSlim for WithProcessLaunchExclusivity(Action).
Interface for using filesystems.
FileStream CreateAsyncSequentialWriteStream(string path)
Creates an asynchronous FileStream for sequential writing.
Abstraction for suspending and resuming processes.
ValueTask< int > HandleProcessStart(global::System.Diagnostics.Process process, CancellationToken cancellationToken)
Run events on starting a process.
Abstraction over a global::System.Diagnostics.Process.