tgstation-server 6.14.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
Process.cs
Go to the documentation of this file.
1using System;
2using System.Diagnostics;
3using System.Threading;
4using System.Threading.Tasks;
5
6using Microsoft.Extensions.Logging;
7using Microsoft.Win32.SafeHandles;
8
10
12{
14 sealed class Process : IProcess
15 {
17 public int Id { get; }
18
20 public DateTimeOffset? LaunchTime
21 {
22 get
23 {
24 try
25 {
26 return handle.StartTime;
27 }
28 catch (Exception ex)
29 {
30 logger.LogWarning(ex, "Failed to get PID {pid}'s memory usage!", Id);
31 return null;
32 }
33 }
34 }
35
37 public Task Startup { get; }
38
40 public Task<int?> Lifetime { get; }
41
43 public long? MemoryUsage
44 {
45 get
46 {
47 try
48 {
49 return handle.PrivateMemorySize64;
50 }
51 catch (Exception ex)
52 {
53 logger.LogWarning(ex, "Failed to get PID {pid}'s memory usage!", Id);
54 return null;
55 }
56 }
57 }
58
63
67 readonly ILogger<Process> logger;
68
72 readonly global::System.Diagnostics.Process handle;
73
77 readonly CancellationTokenSource cancellationTokenSource;
78
83 readonly SafeProcessHandle safeHandle;
84
88 readonly Task<string?>? readTask;
89
93 readonly object processTimeMeasureLock;
94
99
104
109
114
124 public Process(
126 global::System.Diagnostics.Process handle,
127 CancellationTokenSource? readerCts,
128 Task<string?>? readTask,
129 ILogger<Process> logger,
130 bool preExisting)
131 {
132 this.handle = handle ?? throw new ArgumentNullException(nameof(handle));
133
134 // Do this fast because the runtime will bitch if we try to access it after it ends
135 safeHandle = handle.SafeHandle;
136 Id = handle.Id;
137
138 cancellationTokenSource = readerCts ?? new CancellationTokenSource();
139
140 this.processFeatures = processFeatures ?? throw new ArgumentNullException(nameof(processFeatures));
141
142 this.readTask = readTask;
143
144 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
145
146 processTimeMeasureLock = new object();
147
149
150 if (preExisting)
151 {
152 Startup = Task.CompletedTask;
153 return;
154 }
155
156 Startup = Task.Factory.StartNew(
157 () =>
158 {
159 try
160 {
161 handle.WaitForInputIdle();
162 }
163 catch (Exception ex)
164 {
165 logger.LogTrace(ex, "WaitForInputIdle() failed, this is normal.");
166 }
167 },
168 CancellationToken.None, // DCT: None available
170 TaskScheduler.Current);
171
173
174 logger.LogTrace("Created process ID: {pid}", Id);
175 }
176
178 public async ValueTask DisposeAsync()
179 {
180 if (Interlocked.Exchange(ref disposed, 1) != 0)
181 return;
182
183 logger.LogTrace("Disposing PID {pid}...", Id);
185 cancellationTokenSource.Dispose();
186 if (readTask != null)
187 await readTask;
188
189 await Lifetime;
190
191 safeHandle.Dispose();
192 handle.Dispose();
193 }
194
196 public Task<string?> GetCombinedOutput(CancellationToken cancellationToken)
197 {
198 if (readTask == null)
199 throw new InvalidOperationException("Output/Error stream reading was not enabled!");
200
201 return readTask.WaitAsync(cancellationToken);
202 }
203
205 public void Terminate()
206 {
208 if (handle.HasExited)
209 {
210 logger.LogTrace("PID {pid} already exited", Id);
211 return;
212 }
213
214 try
215 {
216 logger.LogTrace("Terminating PID {pid}...", Id);
217 handle.Kill();
218 if (!handle.WaitForExit(5000))
219 logger.LogWarning("WaitForExit() on PID {pid} timed out!", Id);
220 }
221 catch (Exception e)
222 {
223 logger.LogDebug(e, "PID {pid} termination exception!", Id);
224 }
225 }
226
228 public void AdjustPriority(bool higher)
229 {
231 var targetPriority = higher ? ProcessPriorityClass.AboveNormal : ProcessPriorityClass.BelowNormal;
232 try
233 {
234 handle.PriorityClass = targetPriority;
235 logger.LogTrace("Set PID {pid} to {targetPriority} priority", Id, targetPriority);
236 }
237 catch (Exception ex)
238 {
239 logger.LogWarning(ex, "Unable to set priority for PID {id} to {targetPriority}!", Id, targetPriority);
240 }
241 }
242
244 public void SuspendProcess()
245 {
247 try
248 {
250 logger.LogTrace("Suspended PID {pid}", Id);
251 }
252 catch (Exception e)
253 {
254 logger.LogError(e, "Failed to suspend PID {pid}!", Id);
255 throw;
256 }
257 }
258
260 public void ResumeProcess()
261 {
263 try
264 {
266 logger.LogTrace("Resumed PID {pid}", Id);
267 }
268 catch (Exception e)
269 {
270 logger.LogError(e, "Failed to resume PID {pid}!", Id);
271 throw;
272 }
273 }
274
276 public string GetExecutingUsername()
277 {
280 logger.LogTrace("PID {pid} Username: {username}", Id, result);
281 return result;
282 }
283
285 public ValueTask CreateDump(string outputFile, bool minidump, CancellationToken cancellationToken)
286 {
287 ArgumentNullException.ThrowIfNull(outputFile);
289
290 logger.LogTrace("Dumping PID {pid} to {dumpFilePath}...", Id, outputFile);
291 return processFeatures.CreateDump(handle, outputFile, minidump, cancellationToken);
292 }
293
296 {
298 {
299 try
300 {
301 var now = DateTimeOffset.UtcNow;
302 var newTime = handle.TotalProcessorTime;
303
304 var timeDelta = now - lastProcessorMeasureTime;
305 var newUsage = newTime - lastProcessorUsageTime;
306
308 lastProcessorUsageTime = newTime;
309
310 if (timeDelta != TimeSpan.Zero)
311 lastProcessorUsageEstimation = newUsage / (Environment.ProcessorCount * timeDelta);
312 }
313 catch (Exception ex)
314 {
315 logger.LogWarning(ex, "Error measuring processor time delta!");
316 }
317
319 }
320 }
321
326 async Task<int?> WrapLifetimeTask()
327 {
328 bool hasExited;
329 try
330 {
331 await handle.WaitForExitAsync(cancellationTokenSource.Token);
332 hasExited = true;
333 }
334 catch (OperationCanceledException ex)
335 {
336 logger.LogTrace(ex, "Process lifetime task cancelled!");
337 hasExited = handle.HasExited;
338 }
339
340 if (!hasExited)
341 return null;
342
343 var exitCode = handle.ExitCode;
344 logger.LogTrace("PID {pid} exited with code {exitCode}", Id, exitCode);
345 return exitCode;
346 }
347
351 void CheckDisposed() => ObjectDisposedException.ThrowIf(disposed != 0, this);
352 }
353}
IIOManager that resolves paths to Environment.CurrentDirectory.
const TaskCreationOptions BlockingTaskCreationOptions
The TaskCreationOptions used to spawn Tasks for potentially long running, blocking operations.
readonly object processTimeMeasureLock
lock object for measuring processor time usage.
Definition Process.cs:93
Process(IProcessFeatures processFeatures, global::System.Diagnostics.Process handle, CancellationTokenSource? readerCts, Task< string?>? readTask, ILogger< Process > logger, bool preExisting)
Initializes a new instance of the Process class.
Definition Process.cs:124
DateTimeOffset lastProcessorMeasureTime
The last time MeasureProcessorTimeDelta was called.
Definition Process.cs:98
void CheckDisposed()
Throws an ObjectDisposedException if a method of the Process was called after DisposeAsync.
DateTimeOffset? LaunchTime
When the process was started.
Definition Process.cs:21
readonly global::System.Diagnostics.Process handle
The global::System.Diagnostics.Process object.
Definition Process.cs:72
Task< int?> Lifetime
The Task<TResult> resulting in the exit code of the process or null if the process was detached.
Definition Process.cs:40
Task< string?> GetCombinedOutput(CancellationToken cancellationToken)
Get the stderr and stdout output of the IProcess.A Task<TResult> resulting in the stderr and stdout o...
Definition Process.cs:196
void AdjustPriority(bool higher)
Set's the owned global::System.Diagnostics.Process.PriorityClass to a non-normal value.
Definition Process.cs:228
ValueTask CreateDump(string outputFile, bool minidump, CancellationToken cancellationToken)
Create a dump file of the process.A ValueTask representing the running operation.
Definition Process.cs:285
double MeasureProcessorTimeDelta()
Gets the estimated CPU usage fraction of the process based on the last time this was called....
Definition Process.cs:295
double lastProcessorUsageEstimation
The last valid return value of MeasureProcessorTimeDelta.
Definition Process.cs:108
TimeSpan lastProcessorUsageTime
The last value of global::System.Diagnostics.Process.TotalProcessorTime.
Definition Process.cs:103
void Terminate()
Asycnhronously terminates the process.To ensure the IProcess has ended, use the IProcessBase....
Definition Process.cs:205
readonly ILogger< Process > logger
The ILogger for the Process.
Definition Process.cs:67
void SuspendProcess()
Suspends the process.
Definition Process.cs:244
readonly CancellationTokenSource cancellationTokenSource
The CancellationTokenSource used to shutdown the readTask and Lifetime.
Definition Process.cs:77
string GetExecutingUsername()
Get the name of the account executing the IProcess.The name of the account executing the IProcess.
Definition Process.cs:276
void ResumeProcess()
Resumes the process.
Definition Process.cs:260
readonly SafeProcessHandle safeHandle
The global::System.Diagnostics.Process.SafeHandle.
Definition Process.cs:83
readonly IProcessFeatures processFeatures
The IProcessFeatures for the Process.
Definition Process.cs:62
Task Startup
The Task representing the time until the IProcess becomes "idle".
Definition Process.cs:37
long? MemoryUsage
Gets the process' memory usage in bytes.
Definition Process.cs:44
readonly? Task< string?> readTask
The Task<TResult> resulting in the process' standard output/error text.
Definition Process.cs:88
int disposed
If the Process was disposed.
Definition Process.cs:113
async Task< int?> WrapLifetimeTask()
Attaches a log message to the process' exit event.
Definition Process.cs:326
Abstraction for suspending and resuming processes.
string GetExecutingUsername(global::System.Diagnostics.Process process)
Get the name of the user executing a given process .
ValueTask CreateDump(global::System.Diagnostics.Process process, string outputFile, bool minidump, CancellationToken cancellationToken)
Create a dump file for a given process .
void SuspendProcess(global::System.Diagnostics.Process process)
Suspend a given process .
void ResumeProcess(global::System.Diagnostics.Process process)
Resume a given suspended global::System.Diagnostics.Process.
Abstraction over a global::System.Diagnostics.Process.
Definition IProcess.cs:11