source: Common/ProcessExecutor.cs

Last change on this file was 15, checked in by chronos, 6 months ago
  • Modified: Updated files.
File size: 10.4 KB
Line 
1using System;
2using System.Text;
3using System.Threading;
4using System.Diagnostics;
5using System.IO;
6using Microsoft.Win32.SafeHandles;
7
8namespace Common
9{
10 public enum ProcessExecutorResult
11 {
12 None,
13 Finished,
14 Killed,
15 OutputOverflow,
16 TimedOut,
17 Unexpected
18 }
19
20 public class ProcessExecutor : IDisposable
21 {
22 private readonly StringBuilder error;
23 private readonly StringBuilder output;
24 private bool disposedValue;
25 private AutoResetEvent outputOverflowWaitHandle;
26 private readonly object internalLock;
27 private AsyncStreamReaderFast asyncStreamReaderOutput;
28 private AsyncStreamReaderFast asyncStreamReaderError;
29 public ManualResetEvent KillerWaitHandle;
30 public ProcessStartInfo StartInfo { get; }
31 public Process Process { get; private set; }
32 public int TimeOut { get; set; }
33 public int MaxOutputCount { get; set; }
34 public delegate void OutputChangedHandler(ProcessExecutor processExecutor);
35 public event OutputChangedHandler OutputChanged;
36 public event OutputChangedHandler ErrorChanged;
37 public bool UseAsyncStreamReaderFast;
38 public ProcessExecutorResult Result;
39
40 public string Error
41 {
42 get
43 {
44 lock (internalLock)
45 {
46 return error.ToString();
47 }
48 }
49 set
50 {
51 lock (internalLock)
52 {
53 error.Length = 0;
54 error.Append(value);
55 }
56 }
57 }
58
59 public string Output
60 {
61 get
62 {
63 lock (internalLock)
64 {
65 return output.ToString();
66 }
67 }
68 set
69 {
70 lock (internalLock)
71 {
72 output.Length = 0;
73 output.Append(value);
74 }
75 }
76 }
77
78 public ProcessExecutor()
79 {
80 UseAsyncStreamReaderFast = true;
81 internalLock = new object();
82 Process = new Process();
83 StartInfo = new ProcessStartInfo
84 {
85 CreateNoWindow = true,
86 RedirectStandardOutput = true,
87 RedirectStandardError = true,
88 UseShellExecute = false,
89 };
90 Process.StartInfo = StartInfo;
91
92 output = new StringBuilder();
93 error = new StringBuilder();
94 TimeOut = -1;
95 MaxOutputCount = -1;
96 KillerWaitHandle = null;
97 outputOverflowWaitHandle = new AutoResetEvent(false);
98 }
99
100 public void Execute()
101 {
102 Result = ProcessExecutorResult.None;
103
104 if (!File.Exists(Process.StartInfo.FileName))
105 {
106 throw new Exception("Executable file " + Process.StartInfo.FileName + " not found.");
107 }
108
109 if (!UseAsyncStreamReaderFast)
110 {
111 Process.OutputDataReceived += OutputDataReceived;
112 Process.ErrorDataReceived += ErrorDataReceived;
113 }
114
115 Process.Start();
116
117 if (UseAsyncStreamReaderFast)
118 {
119 asyncStreamReaderOutput = new AsyncStreamReaderFast(Process, Process.StandardOutput.BaseStream,
120 OutputDataReceived, Process.StandardOutput.CurrentEncoding);
121 asyncStreamReaderOutput.BeginRead();
122 asyncStreamReaderError = new AsyncStreamReaderFast(Process, Process.StandardError.BaseStream,
123 ErrorDataReceived, Process.StandardOutput.CurrentEncoding);
124 asyncStreamReaderError.BeginRead();
125 }
126 else
127 {
128 Process.BeginOutputReadLine();
129 Process.BeginErrorReadLine();
130 }
131
132 // From the Process.WaitForExit(int) documentation on the MSDN:
133 // "When standard output has been redirected to asynchronous event handlers,
134 // it is possible that output processing will not have completed when this method returns.
135 // To ensure that asynchronous event handling has been completed, call the WaitForExit()
136 // overload that takes no parameter after receiving a true from this overload."
137 const int waitFinished = 0;
138 const int waitKilled = 1;
139 const int waitOutputOverflow = 2;
140 int wait;
141 using (SafeWaitHandle processHandle = new SafeWaitHandle(Process.Handle, false))
142 using (ManualResetEvent processFinishedEvent = new ManualResetEvent(false))
143 {
144 processFinishedEvent.SafeWaitHandle = processHandle;
145 WaitHandle[] waitHandles = { processFinishedEvent, KillerWaitHandle, outputOverflowWaitHandle };
146 if (TimeOut == -1)
147 {
148 wait = WaitHandle.WaitAny(waitHandles);
149 }
150 else
151 {
152 wait = WaitHandle.WaitAny(waitHandles, TimeOut * 1000);
153 }
154 }
155
156 if (wait == waitFinished)
157 {
158 // see above
159 if (UseAsyncStreamReaderFast)
160 {
161 asyncStreamReaderOutput.WaitUtilEOF();
162 asyncStreamReaderError.WaitUtilEOF();
163 }
164 else
165 {
166 Process.WaitForExit();
167 }
168 }
169 else // Timed out or scheduled for execution.
170 {
171 try
172 {
173 Process.Kill();
174 }
175 catch (System.ComponentModel.Win32Exception ex)
176 {
177 const int errorAccessDenied = 5;
178 if (ex.NativeErrorCode != errorAccessDenied)
179 throw;
180 }
181 catch (InvalidOperationException)
182 {
183 // Process has already exited.
184 }
185 }
186
187 switch (wait)
188 {
189 case WaitHandle.WaitTimeout:
190 Result = ProcessExecutorResult.TimedOut;
191 break;
192 case waitKilled:
193 Result = ProcessExecutorResult.Killed;
194 break;
195 case waitOutputOverflow:
196 Result = ProcessExecutorResult.OutputOverflow;
197 break;
198 case waitFinished:
199 Result = ProcessExecutorResult.Finished;
200 break;
201 default:
202 Result = ProcessExecutorResult.Unexpected;
203 break;
204 }
205 }
206
207 private void OutputDataReceived(string data)
208 {
209 if (string.IsNullOrEmpty(data)) return;
210 lock (internalLock)
211 {
212 if (MaxOutputCount != -1)
213 {
214 if (output.Length + data.Length <= MaxOutputCount)
215 {
216 output.Append(data);
217 }
218 else
219 {
220 if (output.Length <= MaxOutputCount)
221 {
222 output.Append(data.Substring(0, MaxOutputCount - output.Length));
223 }
224 else
225 {
226 outputOverflowWaitHandle.Set();
227 }
228 }
229 }
230 else
231 {
232 output.Append(data);
233 }
234 }
235
236 OutputChanged?.Invoke(this);
237 }
238
239 private void ErrorDataReceived(string data)
240 {
241 if (string.IsNullOrEmpty(data)) return;
242 lock (internalLock)
243 {
244 if (MaxOutputCount != -1)
245 {
246 if (error.Length + data.Length <= MaxOutputCount)
247 {
248 error.Append(data);
249 }
250 else
251 {
252 if (error.Length <= MaxOutputCount)
253 {
254 error.Append(data.Substring(0, MaxOutputCount - error.Length));
255 }
256 else
257 {
258 outputOverflowWaitHandle.Set();
259 }
260 }
261 }
262 else
263 {
264 error.Append(data);
265 }
266 }
267
268 ErrorChanged?.Invoke(this);
269 }
270
271 private void OutputDataReceived(object sender, DataReceivedEventArgs e)
272 {
273 OutputDataReceived(e.Data + "\n");
274 }
275
276 private void ErrorDataReceived(object sender, DataReceivedEventArgs e)
277 {
278 ErrorDataReceived(e.Data + "\n");
279 }
280
281 protected virtual void Dispose(bool disposing)
282 {
283 if (!disposedValue)
284 {
285 if (disposing)
286 {
287 lock (internalLock)
288 {
289 // Explicitly clear StringBuilder instances to avoid Out of memory exception
290 output.Clear();
291 output.Capacity = 0;
292 error.Clear();
293 error.Capacity = 0;
294 }
295
296 if (Process != null)
297 {
298 Process.Dispose();
299 Process = null;
300 }
301
302 if (outputOverflowWaitHandle != null)
303 {
304 outputOverflowWaitHandle.Close();
305 outputOverflowWaitHandle = null;
306 }
307 }
308
309 disposedValue = true;
310 }
311 }
312
313 public void Dispose()
314 {
315 Dispose(true);
316 GC.SuppressFinalize(this);
317 }
318
319 ~ProcessExecutor() => Dispose(false);
320 }
321}
Note: See TracBrowser for help on using the repository browser.