tgstation-server 5.12.7
The /tg/station 13 server suite
Loading...
Searching...
No Matches
ProcessExecutor.cs
Go to the documentation of this file.
1using System;
2using System.IO;
3using System.Text;
4using System.Threading;
5using System.Threading.Tasks;
6
7using Microsoft.Extensions.Logging;
8
11
13{
16 {
21
26
30 readonly ILogger<ProcessExecutor> logger;
31
35 readonly ILoggerFactory loggerFactory;
36
47 ILogger<ProcessExecutor> logger,
48 ILoggerFactory loggerFactory)
49 {
50 this.processFeatures = processFeatures ?? throw new ArgumentNullException(nameof(processFeatures));
51 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
52 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
53 this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
54 }
55
57 public IProcess GetProcess(int id)
58 {
59 logger.LogDebug("Attaching to process {pid}...", id);
60 global::System.Diagnostics.Process handle;
61 try
62 {
63 handle = global::System.Diagnostics.Process.GetProcessById(id);
64 }
65 catch (Exception e)
66 {
67 logger.LogDebug(e, "Unable to get process {pid}!", id);
68 return null;
69 }
70
71 return CreateFromExistingHandle(handle);
72 }
73
76 {
77 logger.LogTrace("Getting current process...");
78 var handle = global::System.Diagnostics.Process.GetCurrentProcess();
79 return CreateFromExistingHandle(handle);
80 }
81
83 public async Task<IProcess> LaunchProcess(
84 string fileName,
85 string workingDirectory,
86 string arguments,
87 string fileRedirect,
88 bool readStandardHandles,
89 bool noShellExecute)
90 {
91 ArgumentNullException.ThrowIfNull(fileName);
92 ArgumentNullException.ThrowIfNull(workingDirectory);
93 ArgumentNullException.ThrowIfNull(arguments);
94
95 if (!noShellExecute && readStandardHandles)
96 throw new InvalidOperationException("Requesting output/error reading requires noShellExecute to be true!");
97
98 logger.LogDebug(
99 "{launchType}aunching process in {workingDirectory}: {exe} {arguments}",
100 noShellExecute ? "L" : "Shell l",
101 workingDirectory,
102 fileName,
103 arguments);
104 var handle = new global::System.Diagnostics.Process();
105 try
106 {
107 handle.StartInfo.FileName = fileName;
108 handle.StartInfo.Arguments = arguments;
109 handle.StartInfo.WorkingDirectory = workingDirectory;
110
111 handle.StartInfo.UseShellExecute = !noShellExecute;
112
113 var processStartTcs = new TaskCompletionSource();
114 var lifetimeTaskTask = AttachExitHandlerBeforeLaunch(handle, processStartTcs.Task);
115
116 Task<string> readTask = null;
117 CancellationTokenSource disposeCts = null;
118 if (readStandardHandles)
119 {
120 handle.StartInfo.RedirectStandardOutput = true;
121 handle.StartInfo.RedirectStandardError = true;
122
123 disposeCts = new CancellationTokenSource();
124 readTask = ConsumeReaders(handle, processStartTcs.Task, fileRedirect, disposeCts.Token);
125 }
126
127 try
128 {
129 handle.Start();
130
131 processStartTcs.SetResult();
132 }
133 catch (Exception ex)
134 {
135 processStartTcs.SetException(ex);
136 throw;
137 }
138
139 var process = new Process(
141 handle,
142 disposeCts,
143 await lifetimeTaskTask, // won't block
144 readTask,
145 loggerFactory.CreateLogger<Process>(),
146 false);
147
148 return process;
149 }
150 catch
151 {
152 handle.Dispose();
153 throw;
154 }
155 }
156
158 public IProcess GetProcessByName(string name)
159 {
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)
164 if (handle == null)
165 handle = proc;
166 else
167 {
168 logger.LogTrace("Disposing extra found PID: {pid}...", proc.Id);
169 proc.Dispose();
170 }
171
172 if (handle == null)
173 return null;
174
175 return CreateFromExistingHandle(handle);
176 }
177
184 async Task<Task<int>> AttachExitHandlerBeforeLaunch(global::System.Diagnostics.Process handle, Task startupTask)
185 {
186 var id = -1;
187 var result = AttachExitHandler(handle, () => id);
188 await startupTask;
189 id = handle.Id;
190 return result;
191 }
192
199 Task<int> AttachExitHandler(global::System.Diagnostics.Process handle, Func<int> idProvider)
200 {
201 handle.EnableRaisingEvents = true;
202
203 var tcs = new TaskCompletionSource<int>();
204 void ExitHandler(object sender, EventArgs args)
205 {
206 var id = idProvider();
207 try
208 {
209 try
210 {
211 var exitCode = handle.ExitCode;
212
213 // Try because this can be invoked twice for weird reasons
214 if (tcs.TrySetResult(exitCode))
215 logger.LogTrace("PID {pid} termination event completed", id);
216 else
217 logger.LogTrace("Ignoring duplicate PID {pid} termination event", id);
218 }
219 catch (InvalidOperationException ex)
220 {
221 if (!tcs.Task.IsCompleted)
222 throw;
223
224 logger.LogTrace(ex, "Ignoring expected PID {pid} exit handler exception!", id);
225 }
226 }
227 catch (Exception ex)
228 {
229 logger.LogError(ex, "PID {pid} exit handler exception!", id);
230 }
231 }
232
233 handle.Exited += ExitHandler;
234
235 return tcs.Task;
236 }
237
246 async Task<string> ConsumeReaders(global::System.Diagnostics.Process handle, Task startTask, string fileRedirect, CancellationToken disposeToken)
247 {
248 await startTask;
249
250 var pid = handle.Id;
251 logger.LogTrace("Starting read for PID {pid}...", pid);
252
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()
258 {
259 if (outputOpen && outputReadTask == null)
260 outputReadTask = stdOutHandle.ReadLineAsync();
261
262 if (errorOpen && errorReadTask == null)
263 errorReadTask = stdErrHandle.ReadLineAsync();
264
265 var completedTask = await Task.WhenAny(outputReadTask ?? errorReadTask, errorReadTask ?? outputReadTask).WithToken(disposeToken);
266 var line = await completedTask;
267 if (completedTask == outputReadTask)
268 {
269 outputReadTask = null;
270 if (line == null)
271 outputOpen = false;
272 }
273 else
274 {
275 errorReadTask = null;
276 if (line == null)
277 errorOpen = false;
278 }
279
280 if (line == null && (errorOpen || outputOpen))
281 return await GetNextLine();
282
283 return line;
284 }
285
286 await using var fileStream = fileRedirect != null ? ioManager.CreateAsyncSequentialWriteStream(fileRedirect) : null;
287 await using var writer = fileStream != null ? new StreamWriter(fileStream) : null;
288
289 string text;
290 var stringBuilder = fileStream == null ? new StringBuilder() : null;
291 try
292 {
293 while ((text = await GetNextLine()) != null)
294 {
295 if (fileStream != null)
296 {
297 await writer.WriteLineAsync(text);
298 await writer.FlushAsync();
299 }
300 else
301 stringBuilder.AppendLine(text);
302 }
303
304 logger.LogTrace("Finished read for PID {pid}", pid);
305 }
306 catch (OperationCanceledException ex)
307 {
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 --");
311 }
312
313 return stringBuilder?.ToString();
314 }
315
321 IProcess CreateFromExistingHandle(global::System.Diagnostics.Process handle)
322 {
323 try
324 {
325 var pid = handle.Id;
326 return new Process(
328 handle,
329 null,
330 AttachExitHandler(handle, () => pid),
331 null,
332 loggerFactory.CreateLogger<Process>(),
333 true);
334 }
335 catch
336 {
337 handle.Dispose();
338 throw;
339 }
340 }
341 }
342}
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.
Definition: IIOManager.cs:13
FileStream CreateAsyncSequentialWriteStream(string path)
Creates an asynchronous FileStream for sequential writing.
Abstraction for suspending and resuming processes.
Abstraction over a global::System.Diagnostics.Process.
Definition: IProcess.cs:11