2using System.Diagnostics;
6using System.Threading.Channels;
7using System.Threading.Tasks;
9using Microsoft.Extensions.Logging;
36 readonly ILogger<ProcessExecutor>
logger;
70 ILogger<ProcessExecutor>
logger,
75 this.logger =
logger ??
throw new ArgumentNullException(nameof(
logger));
82 logger.LogDebug(
"Attaching to process {pid}...",
id);
83 global::System.Diagnostics.Process handle;
86 handle = global::System.Diagnostics.Process.GetProcessById(
id);
90 logger.LogDebug(e,
"Unable to get process {pid}!",
id);
100 logger.LogTrace(
"Getting current process...");
101 var handle = global::System.Diagnostics.Process.GetCurrentProcess();
108 string workingDirectory,
110 string? fileRedirect,
111 bool readStandardHandles,
114 ArgumentNullException.ThrowIfNull(fileName);
115 ArgumentNullException.ThrowIfNull(workingDirectory);
116 ArgumentNullException.ThrowIfNull(arguments);
120 "Launching process in {workingDirectory}: {exe} {arguments}",
126 "Shell launching process in {workingDirectory}: {exe} {arguments}",
131 var handle =
new global::System.Diagnostics.Process();
134 handle.StartInfo.FileName = fileName;
135 handle.StartInfo.Arguments = arguments;
136 handle.StartInfo.WorkingDirectory = workingDirectory;
138 handle.StartInfo.UseShellExecute = !noShellExecute;
140 Task<string?>? readTask =
null;
141 CancellationTokenSource? disposeCts =
null;
144 TaskCompletionSource<int>? processStartTcs =
null;
145 if (readStandardHandles)
147 processStartTcs =
new TaskCompletionSource<int>();
148 disposeCts =
new CancellationTokenSource();
149 readTask =
ConsumeReaders(handle, processStartTcs.Task, fileRedirect, disposeCts.Token);
166 processStartTcs?.SetResult(pid);
170 processStartTcs?.SetException(ex);
186 disposeCts?.Dispose();
200 logger.LogTrace(
"GetProcessByName: {processName}...", name ??
throw new ArgumentNullException(nameof(name)));
201 var procs = global::System.Diagnostics.Process.GetProcessesByName(name);
202 global::System.Diagnostics.Process? handle =
null;
203 foreach (var proc
in procs)
208 logger.LogTrace(
"Disposing extra found PID: {pid}...", proc.Id);
226 async Task<string?>
ConsumeReaders(global::System.Diagnostics.Process handle, Task<int> startupAndPid,
string? fileRedirect, CancellationToken cancellationToken)
228 handle.StartInfo.RedirectStandardOutput =
true;
229 handle.StartInfo.RedirectStandardError =
true;
233 await
using var fileWriter = fileStream !=
null ?
new StreamWriter(fileStream) :
null;
235 var stringBuilder = fileStream ==
null ?
new StringBuilder() :
null;
237 var dataChannel = Channel.CreateUnbounded<
string>(
238 new UnboundedChannelOptions
240 AllowSynchronousContinuations = !writingToFile,
242 SingleWriter =
false,
246 async
void DataReceivedHandler(
object sender, DataReceivedEventArgs eventArgs)
248 var line = eventArgs.Data;
251 var handlesRemaining = Interlocked.Decrement(ref handlesOpen);
252 if (handlesRemaining == 0)
253 dataChannel.Writer.Complete();
260 await dataChannel.Writer.WriteAsync(line, cancellationToken);
262 catch (OperationCanceledException ex)
264 logger.LogWarning(ex,
"Handle channel write interrupted!");
268 handle.OutputDataReceived += DataReceivedHandler;
269 handle.ErrorDataReceived += DataReceivedHandler;
271 async ValueTask OutputWriter()
273 var enumerable = dataChannel.Reader.ReadAllAsync(cancellationToken);
276 var enumerator = enumerable.GetAsyncEnumerator(cancellationToken);
277 var nextEnumeration = enumerator.MoveNextAsync();
278 while (await nextEnumeration)
280 var text = enumerator.Current;
281 nextEnumeration = enumerator.MoveNextAsync();
282 await fileWriter!.WriteLineAsync(text.AsMemory(), cancellationToken);
284 if (!nextEnumeration.IsCompleted)
285 await fileWriter.FlushAsync(cancellationToken);
289 await
foreach (var text
in enumerable)
290 stringBuilder!.AppendLine(text);
293 var pid = await startupAndPid;
294 logger.LogTrace(
"Starting read for PID {pid}...", pid);
296 using (cancellationToken.Register(() => dataChannel.Writer.TryComplete()))
298 handle.BeginOutputReadLine();
299 using (cancellationToken.Register(handle.CancelOutputRead))
301 handle.BeginErrorReadLine();
302 using (cancellationToken.Register(handle.CancelErrorRead))
306 await OutputWriter();
308 logger.LogTrace(
"Finished read for PID {pid}", pid);
310 catch (OperationCanceledException ex)
312 logger.LogWarning(ex,
"PID {pid} stream reading interrupted!", pid);
314 await fileWriter!.WriteLineAsync(
"-- Process detached, log truncated. This is likely due a to TGS restart --");
320 return stringBuilder?.ToString();
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 LaunchProcess(string fileName, string workingDirectory, string arguments, string? fileRedirect, bool readStandardHandles, bool noShellExecute)
Launch a IProcess. The new IProcess.
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.
Abstraction over a global::System.Diagnostics.Process.