tgstation-server  4.4.0
The /tg/station 13 server suite
ProcessExecutor.cs
Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Text;
4 using System.Threading.Tasks;
5 
6 namespace Tgstation.Server.Host.System
7 {
10  {
15 
19  readonly ILogger<ProcessExecutor> logger;
20 
24  readonly ILoggerFactory loggerFactory;
25 
31  static Task<int> AttachExitHandler(global::System.Diagnostics.Process handle)
32  {
33  handle.EnableRaisingEvents = true;
34  var tcs = new TaskCompletionSource<int>();
35  handle.Exited += (a, b) =>
36  {
37  int exitCode;
38  try
39  {
40  exitCode = handle.ExitCode;
41  }
42  catch (InvalidOperationException)
43  {
44  return;
45  }
46 
47  // Try because this can be invoked twice for weird reasons
48  tcs.TrySetResult(exitCode);
49  };
50 
51  return tcs.Task;
52  }
53 
61  IProcessFeatures processFeatures,
62  ILogger<ProcessExecutor> logger,
63  ILoggerFactory loggerFactory)
64  {
65  this.processFeatures = processFeatures ?? throw new ArgumentNullException(nameof(processFeatures));
66  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
67  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
68  }
69 
71  public IProcess GetProcess(int id)
72  {
73  logger.LogDebug("Attaching to process {0}...", id);
74  global::System.Diagnostics.Process handle;
75  try
76  {
77  handle = global::System.Diagnostics.Process.GetProcessById(id);
78  }
79  catch(Exception e)
80  {
81  logger.LogDebug("Unable to get process {0}! Exception: {1}", id, e);
82  return null;
83  }
84 
85  return CreateFromExistingHandle(handle);
86  }
87 
90  {
91  logger.LogTrace("Getting current process...");
92  var handle = global::System.Diagnostics.Process.GetCurrentProcess();
93  return CreateFromExistingHandle(handle);
94  }
95 
98  string fileName,
99  string workingDirectory,
100  string arguments,
101  bool readOutput,
102  bool readError,
103  bool noShellExecute)
104  {
105  if (fileName == null)
106  throw new ArgumentNullException(nameof(fileName));
107  if (workingDirectory == null)
108  throw new ArgumentNullException(nameof(workingDirectory));
109  if (arguments == null)
110  throw new ArgumentNullException(nameof(arguments));
111 
112  if (!noShellExecute && (readOutput || readError))
113  throw new InvalidOperationException("Requesting output/error reading requires noShellExecute to be true!");
114 
115  logger.LogDebug(
116  "{0}aunching process in {1}: {2} {3}",
117  noShellExecute ? "L" : "Shell l",
118  workingDirectory,
119  fileName,
120  arguments);
121  var handle = new global::System.Diagnostics.Process();
122  try
123  {
124  handle.StartInfo.FileName = fileName;
125  handle.StartInfo.Arguments = arguments;
126  handle.StartInfo.WorkingDirectory = workingDirectory;
127 
128  handle.StartInfo.UseShellExecute = !noShellExecute;
129 
130  StringBuilder outputStringBuilder = null, errorStringBuilder = null, combinedStringBuilder = null;
131 
132  TaskCompletionSource<object> outputReadTcs = null;
133  TaskCompletionSource<object> errorReadTcs = null;
134  if (readOutput || readError)
135  {
136  combinedStringBuilder = new StringBuilder();
137  if (readOutput)
138  {
139  outputStringBuilder = new StringBuilder();
140  handle.StartInfo.RedirectStandardOutput = true;
141  outputReadTcs = new TaskCompletionSource<object>();
142  handle.OutputDataReceived += (sender, e) =>
143  {
144  if (e.Data == null)
145  {
146  outputReadTcs.SetResult(null);
147  return;
148  }
149 
150  combinedStringBuilder.Append(Environment.NewLine);
151  combinedStringBuilder.Append(e.Data);
152  outputStringBuilder.Append(Environment.NewLine);
153  outputStringBuilder.Append(e.Data);
154  };
155  }
156 
157  if (readError)
158  {
159  errorStringBuilder = new StringBuilder();
160  handle.StartInfo.RedirectStandardError = true;
161  errorReadTcs = new TaskCompletionSource<object>();
162  handle.ErrorDataReceived += (sender, e) =>
163  {
164  if (e.Data == null)
165  {
166  errorReadTcs.SetResult(null);
167  return;
168  }
169 
170  combinedStringBuilder.Append(Environment.NewLine);
171  combinedStringBuilder.Append(e.Data);
172  errorStringBuilder.Append(Environment.NewLine);
173  errorStringBuilder.Append(e.Data);
174  };
175  }
176  }
177 
178  var lifetimeTask = AttachExitHandler(handle);
179 
180  handle.Start();
181 
182  static async Task<int> AddToLifetimeTask(Task<int> originalTask, TaskCompletionSource<object> tcs)
183  {
184  var exitCode = await originalTask.ConfigureAwait(false);
185  await tcs.Task.ConfigureAwait(false);
186  return exitCode;
187  }
188 
189  try
190  {
191  if (readOutput)
192  {
193  handle.BeginOutputReadLine();
194  lifetimeTask = AddToLifetimeTask(lifetimeTask, outputReadTcs);
195  }
196  }
197  catch (InvalidOperationException) { }
198  try
199  {
200  if (readError)
201  {
202  handle.BeginErrorReadLine();
203  lifetimeTask = AddToLifetimeTask(lifetimeTask, errorReadTcs);
204  }
205  }
206  catch (InvalidOperationException) { }
207 
208  return new Process(
209  processFeatures,
210  handle,
211  lifetimeTask,
212  outputStringBuilder,
213  errorStringBuilder,
214  combinedStringBuilder,
215  loggerFactory.CreateLogger<Process>(), false);
216  }
217  catch
218  {
219  handle.Dispose();
220  throw;
221  }
222  }
223 
225  public IProcess GetProcessByName(string name)
226  {
227  logger.LogTrace("GetProcessByName: {0}...", name ?? throw new ArgumentNullException(nameof(name)));
228  var procs = global::System.Diagnostics.Process.GetProcessesByName(name);
229  global::System.Diagnostics.Process handle = null;
230  foreach (var proc in procs)
231  if (handle == null)
232  handle = proc;
233  else
234  {
235  logger.LogTrace("Disposing extra found PID: {0}", proc.Id);
236  proc.Dispose();
237  }
238 
239  if (handle == null)
240  return null;
241 
242  return CreateFromExistingHandle(handle);
243  }
244 
250  private IProcess CreateFromExistingHandle(global::System.Diagnostics.Process handle)
251  {
252  try
253  {
254  return new Process(
255  processFeatures,
256  handle,
257  AttachExitHandler(handle),
258  null,
259  null,
260  null,
261  loggerFactory.CreateLogger<Process>(),
262  true);
263  }
264  catch
265  {
266  handle.Dispose();
267  throw;
268  }
269  }
270  }
271 }
static Task< int > AttachExitHandler(global::System.Diagnostics.Process handle)
Create a Task<TResult> resulting in the exit code of a given handle
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the ProcessExecutor
readonly ILogger< ProcessExecutor > logger
The ILogger for the ProcessExecutor
IProcess GetProcessByName(string name)
Get a IProcess with a given name .
ProcessExecutor(IProcessFeatures processFeatures, ILogger< ProcessExecutor > logger, ILoggerFactory loggerFactory)
Construct a ProcessExecutor
IProcess CreateFromExistingHandle(global::System.Diagnostics.Process handle)
Create a IProcess given an existing handle .
IProcess LaunchProcess(string fileName, string workingDirectory, string arguments, bool readOutput, bool readError, bool noShellExecute)
Launch a IProcess
Abstraction over a global::System.Diagnostics.Process
Definition: IProcess.cs:9
readonly IProcessFeatures processFeatures
The IProcessFeatures for the ProcessExecutor.
Abstraction for suspending and resuming processes.
IProcess GetCurrentProcess()
Get a IProcess representing the running executable.
IProcess GetProcess(int id)
Get a IProcess by id .