tgstation-server 6.0.1
The /tg/station 13 server suite
Loading...
Searching...
No Matches
ProcessExecutor.cs
Go to the documentation of this file.
1using System;
2using System.Diagnostics;
3using System.IO;
4using System.Text;
5using System.Threading;
6using System.Threading.Channels;
7using System.Threading.Tasks;
8
9using Microsoft.Extensions.Logging;
10
12
14{
17 {
21 static readonly ReaderWriterLockSlim ExclusiveProcessLaunchLock = new();
22
27
32
36 readonly ILogger<ProcessExecutor> logger;
37
41 readonly ILoggerFactory loggerFactory;
42
47 public static void WithProcessLaunchExclusivity(Action action)
48 {
49 ExclusiveProcessLaunchLock.EnterWriteLock();
50 try
51 {
52 action();
53 }
54 finally
55 {
56 ExclusiveProcessLaunchLock.ExitWriteLock();
57 }
58 }
59
70 ILogger<ProcessExecutor> logger,
71 ILoggerFactory loggerFactory)
72 {
73 this.processFeatures = processFeatures ?? throw new ArgumentNullException(nameof(processFeatures));
74 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
75 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
76 this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
77 }
78
80 public IProcess? GetProcess(int id)
81 {
82 logger.LogDebug("Attaching to process {pid}...", id);
83 global::System.Diagnostics.Process handle;
84 try
85 {
86 handle = global::System.Diagnostics.Process.GetProcessById(id);
87 }
88 catch (Exception e)
89 {
90 logger.LogDebug(e, "Unable to get process {pid}!", id);
91 return null;
92 }
93
94 return CreateFromExistingHandle(handle);
95 }
96
99 {
100 logger.LogTrace("Getting current process...");
101 var handle = global::System.Diagnostics.Process.GetCurrentProcess();
102 return CreateFromExistingHandle(handle);
103 }
104
107 string fileName,
108 string workingDirectory,
109 string arguments,
110 string? fileRedirect,
111 bool readStandardHandles,
112 bool noShellExecute)
113 {
114 ArgumentNullException.ThrowIfNull(fileName);
115 ArgumentNullException.ThrowIfNull(workingDirectory);
116 ArgumentNullException.ThrowIfNull(arguments);
117
118 if (noShellExecute)
119 logger.LogDebug(
120 "Launching process in {workingDirectory}: {exe} {arguments}",
121 workingDirectory,
122 fileName,
123 arguments);
124 else
125 logger.LogDebug(
126 "Shell launching process in {workingDirectory}: {exe} {arguments}",
127 workingDirectory,
128 fileName,
129 arguments);
130
131 var handle = new global::System.Diagnostics.Process();
132 try
133 {
134 handle.StartInfo.FileName = fileName;
135 handle.StartInfo.Arguments = arguments;
136 handle.StartInfo.WorkingDirectory = workingDirectory;
137
138 handle.StartInfo.UseShellExecute = !noShellExecute;
139
140 Task<string?>? readTask = null;
141 CancellationTokenSource? disposeCts = null;
142 try
143 {
144 TaskCompletionSource<int>? processStartTcs = null;
145 if (readStandardHandles)
146 {
147 processStartTcs = new TaskCompletionSource<int>();
148 disposeCts = new CancellationTokenSource();
149 readTask = ConsumeReaders(handle, processStartTcs.Task, fileRedirect, disposeCts.Token);
150 }
151
152 int pid;
153 try
154 {
155 ExclusiveProcessLaunchLock.EnterReadLock();
156 try
157 {
158 handle.Start();
159 }
160 finally
161 {
162 ExclusiveProcessLaunchLock.ExitReadLock();
163 }
164
165 pid = handle.Id;
166 processStartTcs?.SetResult(pid);
167 }
168 catch (Exception ex)
169 {
170 processStartTcs?.SetException(ex);
171 throw;
172 }
173
174 var process = new Process(
176 handle,
177 disposeCts,
178 readTask,
179 loggerFactory.CreateLogger<Process>(),
180 false);
181
182 return process;
183 }
184 catch
185 {
186 disposeCts?.Dispose();
187 throw;
188 }
189 }
190 catch
191 {
192 handle.Dispose();
193 throw;
194 }
195 }
196
198 public IProcess? GetProcessByName(string name)
199 {
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)
204 if (handle == null)
205 handle = proc;
206 else
207 {
208 logger.LogTrace("Disposing extra found PID: {pid}...", proc.Id);
209 proc.Dispose();
210 }
211
212 if (handle == null)
213 return null;
214
215 return CreateFromExistingHandle(handle);
216 }
217
226 async Task<string?> ConsumeReaders(global::System.Diagnostics.Process handle, Task<int> startupAndPid, string? fileRedirect, CancellationToken cancellationToken)
227 {
228 handle.StartInfo.RedirectStandardOutput = true;
229 handle.StartInfo.RedirectStandardError = true;
230
231 bool writingToFile;
232 await using var fileStream = (writingToFile = fileRedirect != null) ? ioManager.CreateAsyncSequentialWriteStream(fileRedirect!) : null;
233 await using var fileWriter = fileStream != null ? new StreamWriter(fileStream) : null;
234
235 var stringBuilder = fileStream == null ? new StringBuilder() : null;
236
237 var dataChannel = Channel.CreateUnbounded<string>(
238 new UnboundedChannelOptions
239 {
240 AllowSynchronousContinuations = !writingToFile,
241 SingleReader = true,
242 SingleWriter = false,
243 });
244
245 var handlesOpen = 2;
246 async void DataReceivedHandler(object sender, DataReceivedEventArgs eventArgs)
247 {
248 var line = eventArgs.Data;
249 if (line == null)
250 {
251 var handlesRemaining = Interlocked.Decrement(ref handlesOpen);
252 if (handlesRemaining == 0)
253 dataChannel.Writer.Complete();
254
255 return;
256 }
257
258 try
259 {
260 await dataChannel.Writer.WriteAsync(line, cancellationToken);
261 }
262 catch (OperationCanceledException ex)
263 {
264 logger.LogWarning(ex, "Handle channel write interrupted!");
265 }
266 }
267
268 handle.OutputDataReceived += DataReceivedHandler;
269 handle.ErrorDataReceived += DataReceivedHandler;
270
271 async ValueTask OutputWriter()
272 {
273 var enumerable = dataChannel.Reader.ReadAllAsync(cancellationToken);
274 if (writingToFile)
275 {
276 var enumerator = enumerable.GetAsyncEnumerator(cancellationToken);
277 var nextEnumeration = enumerator.MoveNextAsync();
278 while (await nextEnumeration)
279 {
280 var text = enumerator.Current;
281 nextEnumeration = enumerator.MoveNextAsync();
282 await fileWriter!.WriteLineAsync(text.AsMemory(), cancellationToken);
283
284 if (!nextEnumeration.IsCompleted)
285 await fileWriter.FlushAsync(cancellationToken);
286 }
287 }
288 else
289 await foreach (var text in enumerable)
290 stringBuilder!.AppendLine(text);
291 }
292
293 var pid = await startupAndPid;
294 logger.LogTrace("Starting read for PID {pid}...", pid);
295
296 using (cancellationToken.Register(() => dataChannel.Writer.TryComplete()))
297 {
298 handle.BeginOutputReadLine();
299 using (cancellationToken.Register(handle.CancelOutputRead))
300 {
301 handle.BeginErrorReadLine();
302 using (cancellationToken.Register(handle.CancelErrorRead))
303 {
304 try
305 {
306 await OutputWriter();
307
308 logger.LogTrace("Finished read for PID {pid}", pid);
309 }
310 catch (OperationCanceledException ex)
311 {
312 logger.LogWarning(ex, "PID {pid} stream reading interrupted!", pid);
313 if (writingToFile)
314 await fileWriter!.WriteLineAsync("-- Process detached, log truncated. This is likely due a to TGS restart --");
315 }
316 }
317 }
318 }
319
320 return stringBuilder?.ToString();
321 }
322
328 Process CreateFromExistingHandle(global::System.Diagnostics.Process handle)
329 {
330 try
331 {
332 var pid = handle.Id;
333 return new Process(
335 handle,
336 null,
337 null,
338 loggerFactory.CreateLogger<Process>(),
339 true);
340 }
341 catch
342 {
343 handle.Dispose();
344 throw;
345 }
346 }
347 }
348}
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).
int Id
The IProcess' ID.
Definition: Process.cs:17
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