5using System.Threading.Tasks;
7using Microsoft.Extensions.Logging;
30 readonly ILogger<ProcessExecutor>
logger;
47 ILogger<ProcessExecutor>
logger,
52 this.logger =
logger ??
throw new ArgumentNullException(nameof(
logger));
59 logger.LogDebug(
"Attaching to process {pid}...",
id);
60 global::System.Diagnostics.Process handle;
63 handle = global::System.Diagnostics.Process.GetProcessById(
id);
67 logger.LogDebug(e,
"Unable to get process {pid}!",
id);
77 logger.LogTrace(
"Getting current process...");
78 var handle = global::System.Diagnostics.Process.GetCurrentProcess();
85 string workingDirectory,
88 bool readStandardHandles,
91 ArgumentNullException.ThrowIfNull(fileName);
92 ArgumentNullException.ThrowIfNull(workingDirectory);
93 ArgumentNullException.ThrowIfNull(arguments);
95 if (!noShellExecute && readStandardHandles)
96 throw new InvalidOperationException(
"Requesting output/error reading requires noShellExecute to be true!");
99 "{launchType}aunching process in {workingDirectory}: {exe} {arguments}",
100 noShellExecute ?
"L" :
"Shell l",
104 var handle =
new global::System.Diagnostics.Process();
107 handle.StartInfo.FileName = fileName;
108 handle.StartInfo.Arguments = arguments;
109 handle.StartInfo.WorkingDirectory = workingDirectory;
111 handle.StartInfo.UseShellExecute = !noShellExecute;
113 var processStartTcs =
new TaskCompletionSource();
116 Task<string> readTask =
null;
117 CancellationTokenSource disposeCts =
null;
118 if (readStandardHandles)
120 handle.StartInfo.RedirectStandardOutput =
true;
121 handle.StartInfo.RedirectStandardError =
true;
123 disposeCts =
new CancellationTokenSource();
124 readTask =
ConsumeReaders(handle, processStartTcs.Task, fileRedirect, disposeCts.Token);
131 processStartTcs.SetResult();
135 processStartTcs.SetException(ex);
143 await lifetimeTaskTask,
160 logger.LogTrace(
"GetProcessByName: {processName}...", name ??
throw new ArgumentNullException(nameof(name)));
161 var procs = global::System.Diagnostics.Process.GetProcessesByName(name);
162 global::System.Diagnostics.Process handle =
null;
163 foreach (var proc
in procs)
168 logger.LogTrace(
"Disposing extra found PID: {pid}...", proc.Id);
201 handle.EnableRaisingEvents =
true;
203 var tcs =
new TaskCompletionSource<int>();
204 void ExitHandler(
object sender, EventArgs args)
206 var
id = idProvider();
211 var exitCode = handle.ExitCode;
214 if (tcs.TrySetResult(exitCode))
215 logger.LogTrace(
"PID {pid} termination event completed",
id);
217 logger.LogTrace(
"Ignoring duplicate PID {pid} termination event",
id);
219 catch (InvalidOperationException ex)
221 if (!tcs.Task.IsCompleted)
224 logger.LogTrace(ex,
"Ignoring expected PID {pid} exit handler exception!",
id);
229 logger.LogError(ex,
"PID {pid} exit handler exception!",
id);
233 handle.Exited += ExitHandler;
246 async Task<string>
ConsumeReaders(global::System.Diagnostics.Process handle, Task startTask,
string fileRedirect, CancellationToken disposeToken)
251 logger.LogTrace(
"Starting read for PID {pid}...", pid);
253 var stdOutHandle = handle.StandardOutput;
254 var stdErrHandle = handle.StandardError;
255 Task<string> outputReadTask =
null, errorReadTask =
null;
256 bool outputOpen =
true, errorOpen =
true;
257 async Task<string> GetNextLine()
259 if (outputOpen && outputReadTask ==
null)
260 outputReadTask = stdOutHandle.ReadLineAsync();
262 if (errorOpen && errorReadTask ==
null)
263 errorReadTask = stdErrHandle.ReadLineAsync();
265 var completedTask = await Task.WhenAny(outputReadTask ?? errorReadTask, errorReadTask ?? outputReadTask).WithToken(disposeToken);
266 var line = await completedTask;
267 if (completedTask == outputReadTask)
269 outputReadTask =
null;
275 errorReadTask =
null;
280 if (line ==
null && (errorOpen || outputOpen))
281 return await GetNextLine();
287 await
using var writer = fileStream !=
null ?
new StreamWriter(fileStream) :
null;
290 var stringBuilder = fileStream ==
null ?
new StringBuilder() :
null;
293 while ((text = await GetNextLine()) !=
null)
295 if (fileStream !=
null)
297 await writer.WriteLineAsync(text);
298 await writer.FlushAsync();
301 stringBuilder.AppendLine(text);
304 logger.LogTrace(
"Finished read for PID {pid}", pid);
306 catch (OperationCanceledException ex)
308 logger.LogWarning(ex,
"PID {pid} stream reading interrupted!", pid);
309 if (fileStream !=
null)
310 await writer.WriteLineAsync(
"-- Process detached, log truncated. This is likely due a to TGS restart --");
313 return stringBuilder?.ToString();
Task< int > AttachExitHandler(global::System.Diagnostics.Process handle, Func< int > idProvider)
Attach an asychronous exit handler to a given process handle .
IProcess GetProcessByName(string name)
Get a IProcess with a given name . The IProcess represented by name on success, null on failure.
readonly IProcessFeatures processFeatures
The IProcessFeatures for the ProcessExecutor.
async Task< string > ConsumeReaders(global::System.Diagnostics.Process handle, Task startTask, string fileRedirect, CancellationToken disposeToken)
Consume the stdout/stderr streams into a Task.
readonly IIOManager ioManager
The IIOManager for the ProcessExecutor.
async Task< Task< int > > AttachExitHandlerBeforeLaunch(global::System.Diagnostics.Process handle, Task startupTask)
Wrapper for AttachExitHandler(global::System.Diagnostics.Process, Func<int>) to safely provide the pr...
IProcess CreateFromExistingHandle(global::System.Diagnostics.Process handle)
Create a IProcess given an existing handle .
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.
async Task< IProcess > LaunchProcess(string fileName, string workingDirectory, string arguments, string fileRedirect, bool readStandardHandles, bool noShellExecute)
Launch a IProcess. A Task resulting in the new IProcess.
IProcess GetProcess(int id)
Get a IProcess by id . The IProcess represented by id on success, null on failure.
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.