tgstation-server 6.19.2
The /tg/station 13 server suite
Loading...
Searching...
No Matches
ProcessExecutor.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Diagnostics;
4using System.IO;
5using System.Linq;
6using System.Text;
7using System.Threading;
8using System.Threading.Channels;
9using System.Threading.Tasks;
10
11using Microsoft.Extensions.Logging;
12
14
16{
19 {
23 static readonly ReaderWriterLockSlim ExclusiveProcessLaunchLock = new();
24
29
34
38 readonly ILogger<ProcessExecutor> logger;
39
43 readonly ILoggerFactory loggerFactory;
44
49 public static void WithProcessLaunchExclusivity(Action action)
50 {
51 ExclusiveProcessLaunchLock.EnterWriteLock();
52 try
53 {
54 action();
55 }
56 finally
57 {
58 ExclusiveProcessLaunchLock.ExitWriteLock();
59 }
60 }
61
72 ILogger<ProcessExecutor> logger,
73 ILoggerFactory loggerFactory)
74 {
75 this.processFeatures = processFeatures ?? throw new ArgumentNullException(nameof(processFeatures));
76 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
77 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
78 this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
79 }
80
82 public IProcess? GetProcess(int id)
83 {
84 logger.LogDebug("Attaching to process {pid}...", id);
85 global::System.Diagnostics.Process handle;
86 try
87 {
88 handle = global::System.Diagnostics.Process.GetProcessById(id);
89 }
90 catch (Exception e)
91 {
92 logger.LogDebug(e, "Unable to get process {pid}!", id);
93 return null;
94 }
95
96 return CreateFromExistingHandle(handle);
97 }
98
101 {
102 logger.LogTrace("Getting current process...");
103 var handle = global::System.Diagnostics.Process.GetCurrentProcess();
104 return CreateFromExistingHandle(handle);
105 }
106
108 public async ValueTask<IProcess> LaunchProcess(
109 string fileName,
110 string workingDirectory,
111 string arguments,
112 CancellationToken cancellationToken,
113 IReadOnlyDictionary<string, string>? environment,
114 string? fileRedirect,
115 bool readStandardHandles,
116 bool noShellExecute,
117 bool doNotLogArguments)
118 {
119 ArgumentNullException.ThrowIfNull(fileName);
120 ArgumentNullException.ThrowIfNull(workingDirectory);
121 ArgumentNullException.ThrowIfNull(arguments);
122
123 var enviromentLogLines = environment == null
124 ? String.Empty
125 : String.Concat(environment.Select(kvp => $"{Environment.NewLine}\t- {kvp.Key}={kvp.Value}"));
126 if (noShellExecute)
127 logger.LogDebug(
128 "Launching process in {workingDirectory}: {exe} {arguments}{environment}",
129 workingDirectory,
130 fileName,
131 doNotLogArguments ? "<ARGUMENTS HIDDEN>" : arguments,
132 enviromentLogLines);
133 else
134 logger.LogDebug(
135 "Shell launching process in {workingDirectory}: {exe} {arguments}{environment}",
136 workingDirectory,
137 fileName,
138 doNotLogArguments ? "<ARGUMENTS HIDDEN>" : arguments,
139 enviromentLogLines);
140
141 var handle = new global::System.Diagnostics.Process();
142 try
143 {
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!);
149
150 handle.StartInfo.WorkingDirectory = workingDirectory;
151
152 handle.StartInfo.UseShellExecute = !noShellExecute;
153
154 Task<string?>? readTask = null;
155 CancellationTokenSource? disposeCts = null;
156 try
157 {
158 TaskCompletionSource<int>? processStartTcs = null;
159 if (readStandardHandles)
160 {
161 processStartTcs = new TaskCompletionSource<int>();
162 disposeCts = new CancellationTokenSource();
163 readTask = ConsumeReaders(handle, processStartTcs.Task, fileRedirect, disposeCts.Token);
164 }
165
166 int pid;
167 try
168 {
169 ExclusiveProcessLaunchLock.EnterReadLock();
170 try
171 {
172 handle.Start();
173 }
174 finally
175 {
176 ExclusiveProcessLaunchLock.ExitReadLock();
177 }
178
179 try
180 {
181 pid = await processFeatures.HandleProcessStart(handle, cancellationToken);
182 }
183 catch
184 {
185 handle.Kill();
186 throw;
187 }
188
189 processStartTcs?.SetResult(pid);
190 }
191 catch (Exception ex)
192 {
193 processStartTcs?.SetException(ex);
194 throw;
195 }
196
197 var process = new Process(
199 handle,
200 disposeCts,
201 readTask,
202 loggerFactory.CreateLogger<Process>(),
203 false);
204
205 return process;
206 }
207 catch
208 {
209 disposeCts?.Dispose();
210 throw;
211 }
212 }
213 catch
214 {
215 handle.Dispose();
216 throw;
217 }
218 }
219
221 public IProcess? GetProcessByName(string name)
222 {
223 logger.LogTrace("GetProcessByName: {processName}...", name ?? throw new ArgumentNullException(nameof(name)));
224 var procs = global::System.Diagnostics.Process.GetProcessesByName(name);
225 global::System.Diagnostics.Process? handle = null;
226 foreach (var proc in procs)
227 if (handle == null)
228 handle = proc;
229 else
230 {
231 logger.LogTrace("Disposing extra found PID: {pid}...", proc.Id);
232 proc.Dispose();
233 }
234
235 if (handle == null)
236 return null;
237
238 return CreateFromExistingHandle(handle);
239 }
240
249 async Task<string?> ConsumeReaders(global::System.Diagnostics.Process handle, Task<int> startupAndPid, string? fileRedirect, CancellationToken cancellationToken)
250 {
251 handle.StartInfo.RedirectStandardOutput = true;
252 handle.StartInfo.RedirectStandardError = true;
253
254 bool writingToFile;
255 await using var fileStream = (writingToFile = fileRedirect != null) ? ioManager.CreateAsyncSequentialWriteStream(fileRedirect!) : null;
256 await using var fileWriter = fileStream != null ? new StreamWriter(fileStream) : null;
257
258 var stringBuilder = fileStream == null ? new StringBuilder() : null;
259
260 var dataChannel = Channel.CreateUnbounded<string>(
261 new UnboundedChannelOptions
262 {
263 AllowSynchronousContinuations = !writingToFile,
264 SingleReader = true,
265 SingleWriter = false,
266 });
267
268 var handlesOpen = 2;
269 async void DataReceivedHandler(object sender, DataReceivedEventArgs eventArgs)
270 {
271 var line = eventArgs.Data;
272 if (line == null)
273 {
274 var handlesRemaining = Interlocked.Decrement(ref handlesOpen);
275 if (handlesRemaining == 0)
276 dataChannel.Writer.Complete();
277
278 return;
279 }
280
281 try
282 {
283 await dataChannel.Writer.WriteAsync(line, cancellationToken);
284 }
285 catch (OperationCanceledException ex)
286 {
287 logger.LogWarning(ex, "Handle channel write interrupted!");
288 }
289 }
290
291 handle.OutputDataReceived += DataReceivedHandler;
292 handle.ErrorDataReceived += DataReceivedHandler;
293
294 async ValueTask OutputWriter()
295 {
296 var enumerable = dataChannel.Reader.ReadAllAsync(cancellationToken);
297 if (writingToFile)
298 {
299 var enumerator = enumerable.GetAsyncEnumerator(cancellationToken);
300 var nextEnumeration = enumerator.MoveNextAsync();
301 while (await nextEnumeration)
302 {
303 var text = enumerator.Current;
304 nextEnumeration = enumerator.MoveNextAsync();
305 await fileWriter!.WriteLineAsync(text.AsMemory(), cancellationToken);
306
307 if (!nextEnumeration.IsCompleted)
308 await fileWriter.FlushAsync(cancellationToken);
309 }
310 }
311 else
312 await foreach (var text in enumerable)
313 stringBuilder!.AppendLine(text);
314 }
315
316 var pid = await startupAndPid;
317 logger.LogTrace("Starting read for PID {pid}...", pid);
318
319 using (cancellationToken.Register(() => dataChannel.Writer.TryComplete()))
320 {
321 handle.BeginOutputReadLine();
322 using (cancellationToken.Register(handle.CancelOutputRead))
323 {
324 handle.BeginErrorReadLine();
325 using (cancellationToken.Register(handle.CancelErrorRead))
326 {
327 try
328 {
329 await OutputWriter();
330
331 logger.LogTrace("Finished read for PID {pid}", pid);
332 }
333 catch (OperationCanceledException ex)
334 {
335 logger.LogWarning(ex, "PID {pid} stream reading interrupted!", pid);
336 if (writingToFile)
337 await fileWriter!.WriteLineAsync("-- Process detached, log truncated. This is likely due a to TGS restart --");
338 }
339 }
340 }
341 }
342
343 return stringBuilder?.ToString();
344 }
345
351 Process CreateFromExistingHandle(global::System.Diagnostics.Process handle)
352 {
353 try
354 {
355 var pid = handle.Id;
356 return new Process(
358 handle,
359 null,
360 null,
361 loggerFactory.CreateLogger<Process>(),
362 true);
363 }
364 catch
365 {
366 handle.Dispose();
367 throw;
368 }
369 }
370 }
371}
async ValueTask< IProcess > LaunchProcess(string fileName, string workingDirectory, string arguments, CancellationToken cancellationToken, IReadOnlyDictionary< string, string >? environment, string? fileRedirect, bool readStandardHandles, bool noShellExecute, bool doNotLogArguments)
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.
Definition IIOManager.cs:14
Stream 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.
Definition IProcess.cs:11