1 | #Region "Microsoft.VisualBasic::78417a1280c39aa92aa5d39f87b1219e, Microsoft.VisualBasic.Core\CommandLine\CLI\IORedirect.vb" |
2 | |
3 | ' Author: |
4 | ' |
5 | ' asuka (amethyst.asuka@gcmodeller.org) |
6 | ' xie (genetics@smrucc.org) |
7 | ' xieguigang (xie.guigang@live.com) |
8 | ' |
9 | ' Copyright (c) 2018 GPL3 Licensed |
10 | ' |
11 | ' |
12 | ' GNU GENERAL PUBLIC LICENSE (GPL3) |
13 | ' |
14 | ' |
15 | ' This program is free software: you can redistribute it and/or modify |
16 | ' it under the terms of the GNU General Public License as published by |
17 | ' the Free Software Foundation, either version 3 of the License, or |
18 | ' (at your option) any later version. |
19 | ' |
20 | ' This program is distributed in the hope that it will be useful, |
21 | ' but WITHOUT ANY WARRANTY; without even the implied warranty of |
22 | ' MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
23 | ' GNU General Public License for more details. |
24 | ' |
25 | ' You should have received a copy of the GNU General Public License |
26 | ' along with this program. If not, see <http://www.gnu.org/licenses/>. |
27 | |
28 | |
29 | |
30 | ' /********************************************************************************/ |
31 | |
32 | ' Summaries: |
33 | |
34 | ' Class IORedirect |
35 | ' |
36 | ' Properties: Bin, CLIArguments, ExitCode, HasExited, PID |
37 | ' StandardOutput |
38 | ' |
39 | ' Constructor: (+1 Overloads) Sub New |
40 | ' Delegate Function |
41 | ' |
42 | ' |
43 | ' Delegate Sub |
44 | ' |
45 | ' Function: GetError, Read, ReadLine, Run, Shell |
46 | ' (+3 Overloads) Start, ToString, WaitError, waitForExit, WaitForExit |
47 | ' WaitOutput |
48 | ' |
49 | ' Sub: (+2 Overloads) Dispose, errorHandler, Kill, outputHandler, Write |
50 | ' (+2 Overloads) WriteLine |
51 | ' |
52 | ' |
53 | ' |
54 | ' |
55 | ' |
56 | ' /********************************************************************************/ |
57 | |
58 | #End Region |
59 | |
60 | Imports System.IO |
61 | Imports System.Runtime.CompilerServices |
62 | Imports System.Text |
63 | Imports System.Text.RegularExpressions |
64 | Imports System.Threading |
65 | Imports Microsoft.VisualBasic.Language |
66 | Imports Microsoft.VisualBasic.Parallel |
67 | Imports Microsoft.VisualBasic.Terminal.STDIO |
68 | Imports Microsoft.VisualBasic.Terminal.STDIO__ |
69 | |
70 | Namespace CommandLine |
71 | |
72 | ''' <summary> |
73 | ''' A communication fundation class type for the commandline program interop. |
74 | ''' (一个简单的用于从当前进程派生子进程的Wrapper对象,假若需要folk出来的子进程对象 |
75 | ''' 不需要终端交互功能,则更加推荐使用<see cref="IORedirectFile"/>对象来进行调用) |
76 | ''' </summary> |
77 | ''' <remarks></remarks> |
78 | Public Class IORedirect : Implements I_ConsoleDeviceHandle |
79 | Implements IDisposable, IIORedirectAbstract |
80 | |
81 | ''' <summary> |
82 | ''' 当前的这个进程实例是否处于运行的状态 |
83 | ''' </summary> |
84 | ''' <remarks></remarks> |
85 | Dim processIsRunning As Boolean = False |
86 | Dim outputWaitHandle As New AutoResetEvent(False) |
87 | Dim errorWaitHandle As New AutoResetEvent(False) |
88 | |
89 | ''' <summary> |
90 | ''' The target invoked process event has been exit with a specific return code. |
91 | ''' (目标派生子进程已经结束了运行并且返回了一个错误值) |
92 | ''' </summary> |
93 | ''' <param name="exitCode"></param> |
94 | ''' <param name="exitTime"></param> |
95 | ''' <remarks></remarks> |
96 | Public Event ProcessExit(exitCode As Integer, exitTime As String) Implements IIORedirectAbstract.ProcessExit |
97 | Public Event PrintOutput(output As String) |
98 | |
99 | ''' <summary> |
100 | ''' The process invoke interface of current I/O redirect operation. |
101 | ''' </summary> |
102 | ''' <remarks></remarks> |
103 | Dim WithEvents processInfo As Process |
104 | |
105 | ''' <summary> |
106 | ''' Gets the standard output for the target invoke process. |
107 | ''' </summary> |
108 | ''' <value></value> |
109 | ''' <returns></returns> |
110 | ''' <remarks></remarks> |
111 | Public ReadOnly Property StandardOutput As String Implements IIORedirectAbstract.StandardOutput |
112 | <MethodImpl(MethodImplOptions.AggressiveInlining)> |
113 | Get |
114 | Return output.ToString |
115 | End Get |
116 | End Property |
117 | |
118 | Public ReadOnly Property Bin As String Implements IIORedirectAbstract.Bin |
119 | <MethodImpl(MethodImplOptions.AggressiveInlining)> |
120 | Get |
121 | Return processInfo.StartInfo.FileName |
122 | End Get |
123 | End Property |
124 | |
125 | Public ReadOnly Property CLIArguments As String Implements IIORedirectAbstract.CLIArguments |
126 | <MethodImpl(MethodImplOptions.AggressiveInlining)> |
127 | Get |
128 | Return processInfo.StartInfo.Arguments |
129 | End Get |
130 | End Property |
131 | |
132 | Public ReadOnly Property ExitCode As Integer |
133 | Get |
134 | Return processInfo.ExitCode |
135 | End Get |
136 | End Property |
137 | |
138 | Public ReadOnly Property HasExited As Boolean |
139 | Get |
140 | Return processInfo.HasExited |
141 | End Get |
142 | End Property |
143 | |
144 | Public ReadOnly Property PID As Integer |
145 | Get |
146 | Return processInfo.Id |
147 | End Get |
148 | End Property |
149 | |
150 | Dim input As StreamWriter |
151 | Dim output As New StringBuilder(1024) |
152 | Dim [error] As New StringBuilder() |
153 | Dim IOredirect As Boolean |
154 | Dim displayOutput As Boolean = False |
155 | |
156 | ''' <summary> |
157 | ''' Creates a <see cref="System.Diagnostics.Process"/> wrapper for the CLI program operations. |
158 | ''' (在服务器上面可能会有一些线程方面的兼容性BUG的问题,不太清楚为什么会导致这样) |
159 | ''' </summary> |
160 | ''' <param name="Exe">The file path of the executable file.</param> |
161 | ''' <param name="args"> |
162 | ''' The CLI arguments for the folked program. |
163 | ''' |
164 | ''' (程序会自动将这个参数之中的换行符替换为空格.) |
165 | ''' </param> |
166 | ''' <param name="ENV">Set up the environment variable for the target invoked child process.</param> |
167 | ''' <param name="displayDebug"></param> |
168 | ''' <param name="displayStdOut">是否显示目标被调用的外部程序的标准输出</param> |
169 | ''' <remarks></remarks> |
170 | Public Sub New(exe$, Optional args$ = "", |
171 | Optional ENV As IEnumerable(Of KeyValuePair(Of String, String)) = Nothing, |
172 | Optional IOredirect As Boolean = True, |
173 | Optional displayStdOut As Boolean = True, |
174 | Optional displayDebug As Boolean = False) |
175 | |
176 | Dim program$ = exe.GetString(""""c) |
177 | Dim pInfo As New ProcessStartInfo(program, args.TrimNewLine.Trim) With { |
178 | .UseShellExecute = False |
179 | } |
180 | |
181 | If IOredirect Then '只是重定向输出设备流 |
182 | pInfo.RedirectStandardOutput = True |
183 | pInfo.RedirectStandardError = True |
184 | pInfo.RedirectStandardInput = True |
185 | End If |
186 | |
187 | pInfo.RedirectStandardInput = True |
188 | pInfo.ErrorDialog = False |
189 | pInfo.WindowStyle = ProcessWindowStyle.Hidden |
190 | pInfo.CreateNoWindow = True |
191 | |
192 | If Not ENV Is Nothing Then |
193 | For Each para As KeyValuePair(Of String, String) In ENV |
194 | Call pInfo.EnvironmentVariables.Add(para.Key, para.Value) |
195 | Next |
196 | End If |
197 | |
198 | Me.IOredirect = IOredirect |
199 | Me.displayOutput = displayStdOut |
200 | Me.processInfo = New Process With { |
201 | .EnableRaisingEvents = True, |
202 | .StartInfo = pInfo |
203 | } |
204 | |
205 | If displayDebug Then |
206 | Call $"""{exe}"" {args}".__DEBUG_ECHO |
207 | Call $"disp_STDOUT -> ${ "Yes!" Or "NO!".When(Not Me.displayOutput) }".__DEBUG_ECHO |
208 | End If |
209 | End Sub |
210 | |
211 | Public Delegate Function ProcessAyHandle(WaitForExit As Boolean, PushingData As String(), _DISP_DEBUG_INFO As Boolean) As Integer |
212 | |
213 | ''' <summary> |
214 | ''' A function pointer for process the events when the target invoked child process was terminated and exit. |
215 | ''' (当目标进程退出的时候所调用的过程) |
216 | ''' </summary> |
217 | ''' <param name="exitCode">The exit code for the target sub invoke process.进程的退出代码</param> |
218 | ''' <param name="exitTime">The exit time for the target sub invoke process.(进程的退出时间)</param> |
219 | ''' <remarks></remarks> |
220 | Public Delegate Sub ProcessExitCallback(exitCode As Integer, exitTime As String) |
221 | |
222 | Private Sub outputHandler(sender As Object, e As DataReceivedEventArgs) Handles processInfo.OutputDataReceived |
223 | If e.Data Is Nothing Then |
224 | Call outputWaitHandle.[Set]() |
225 | Else |
226 | Call output.AppendLine(e.Data) |
227 | End If |
228 | End Sub |
229 | |
230 | Private Sub errorHandler(sender As Object, e As DataReceivedEventArgs) Handles processInfo.ErrorDataReceived |
231 | If e.Data Is Nothing Then |
232 | Call errorWaitHandle.[Set]() |
233 | Else |
234 | Call [error].AppendLine(e.Data) |
235 | End If |
236 | End Sub |
237 | |
238 | Public Sub Kill() |
239 | Call processInfo.Kill() |
240 | End Sub |
241 | |
242 | ''' <summary> |
243 | ''' Gets a <see cref="String"/> used to read the error output of the application. |
244 | ''' </summary> |
245 | ''' <returns>A <see cref="String"/> text value that read from the std_error of <see cref="StreamReader"/> |
246 | ''' that can be used to read the standard error stream of the application.</returns> |
247 | ''' |
248 | <MethodImpl(MethodImplOptions.AggressiveInlining)> |
249 | Public Function GetError() As String |
250 | Return [error].ToString |
251 | End Function |
252 | |
253 | ''' <summary> |
254 | ''' Start the target process. If the target invoked process is currently on the running state, |
255 | ''' then this function will returns the -100 value as error code and print the warning |
256 | ''' information on the system console.(启动目标进程) |
257 | ''' </summary> |
258 | ''' <param name="WaitForExit"> |
259 | ''' Indicate that the program code wait for the target process exit or not? |
260 | ''' (参数指示应用程序代码是否等待目标进程的结束) |
261 | ''' </param> |
262 | ''' <returns> |
263 | ''' 当发生错误的时候会返回错误代码,当当前的进程任然处于运行的状态的时候,程序会返回-100错误代码并在终端之上打印出警告信息 |
264 | ''' </returns> |
265 | ''' <remarks></remarks> |
266 | Public Function Start(Optional waitForExit As Boolean = False, |
267 | Optional pushingData As String() = Nothing, |
268 | Optional displaDebug As Boolean = False) As Integer |
269 | |
270 | If processIsRunning Then |
271 | Dim msg As String = $"Target process ""{processInfo.StartInfo.FileName.ToFileURL}"" is currently in the running state, Operation abort!" |
272 | Call VBDebugger.Warning(msg) |
273 | Return -100 |
274 | End If |
275 | |
276 | If displaDebug Then |
277 | Dim Exe As String = FileIO.FileSystem.GetFileInfo(processInfo.StartInfo.FileName).FullName.Replace("\", "/") |
278 | Dim argvs As String = (" " & processInfo.StartInfo.Arguments).Trim |
279 | |
280 | Call Console.WriteLine(" --> system(""file:""{0}""{1})", Exe, argvs) |
281 | End If |
282 | |
283 | Try |
284 | Call processInfo.Start() |
285 | |
286 | If IOredirect Then |
287 | Call processInfo.BeginOutputReadLine() |
288 | Call processInfo.BeginErrorReadLine() |
289 | End If |
290 | Catch ex As Exception |
291 | Call printf("FATAL_ERROR::%s", ex.ToString) |
292 | Call Console.WriteLine(" Exe ==> " & processInfo.StartInfo.FileName) |
293 | Call Console.WriteLine("argvs ==> " & processInfo.StartInfo.Arguments) |
294 | |
295 | ' Return ex.HResul '4.5 |
296 | Return -1 |
297 | End Try |
298 | |
299 | processIsRunning = True |
300 | input = processInfo.StandardInput |
301 | |
302 | If Not pushingData.IsNullOrEmpty Then |
303 | For Each line As String In pushingData |
304 | Call input.WriteLine(line) |
305 | Call input.Flush() |
306 | |
307 | Call Console.WriteLine(" >>> " & line) |
308 | Next |
309 | End If |
310 | |
311 | If waitForExit Then |
312 | ' 请注意这个函数名称是和当前的这个函数参数是一样的 |
313 | ' 会需要me来区别引用 |
314 | Return Me.waitForExit |
315 | Else |
316 | Call RunTask(AddressOf Me.waitForExit) |
317 | Return 0 |
318 | End If |
319 | End Function |
320 | |
321 | Private Function waitForExit() As Integer |
322 | Dim exitCode% |
323 | |
324 | Call processInfo.WaitForExit() |
325 | |
326 | Try |
327 | ' process exit and raise events |
328 | exitCode = processInfo.ExitCode |
329 | RaiseEvent ProcessExit(exitCode, Process.ExitTime) |
330 | Catch ex As Exception |
331 | |
332 | End Try |
333 | |
334 | Return exitCode |
335 | End Function |
336 | |
337 | <MethodImpl(MethodImplOptions.AggressiveInlining)> |
338 | Public Function WaitOutput(timeout As Integer) As Boolean |
339 | Return outputWaitHandle.WaitOne(timeout) |
340 | End Function |
341 | |
342 | <MethodImpl(MethodImplOptions.AggressiveInlining)> |
343 | Public Function WaitError(timeout As Integer) As Boolean |
344 | Return errorWaitHandle.WaitOne(timeout) |
345 | End Function |
346 | |
347 | ''' <summary> |
348 | ''' With a given timeout in milliseconds unit |
349 | ''' </summary> |
350 | ''' <param name="timeout"></param> |
351 | ''' <returns></returns> |
352 | ''' |
353 | <MethodImpl(MethodImplOptions.AggressiveInlining)> |
354 | Public Function WaitForExit(timeout As Integer) As Boolean |
355 | Return processInfo.WaitForExit(timeout) |
356 | End Function |
357 | |
358 | ''' <summary> |
359 | ''' Start the target process.(启动目标进程) |
360 | ''' </summary> |
361 | ''' <returns>当发生错误的时候会返回错误代码</returns> |
362 | ''' <remarks></remarks> |
363 | Public Function Start(processExitCallback As ProcessExitCallback, |
364 | Optional pushData As String() = Nothing, |
365 | Optional displayDebug As Boolean = False) As Integer |
366 | |
367 | AddHandler ProcessExit, Sub(exitCode As Integer, exitTime As String) |
368 | Call processExitCallback(exitCode, exitTime) |
369 | End Sub |
370 | Return Start( |
371 | waitForExit:=False, |
372 | pushingData:=pushData, |
373 | displaDebug:=displayDebug |
374 | ) |
375 | End Function |
376 | |
377 | ''' <summary> |
378 | ''' Gets the value that the associated process specified when it terminated. |
379 | ''' </summary> |
380 | ''' <param name="WaitForExit"></param> |
381 | ''' <returns>The code that the associated process specified when it terminated.</returns> |
382 | ''' |
383 | <MethodImpl(MethodImplOptions.AggressiveInlining)> |
384 | Public Function Start(Optional waitForExit As Boolean = False) As Integer Implements IIORedirectAbstract.Start |
385 | Return Start(waitForExit, Nothing, True) |
386 | End Function |
387 | |
388 | ''' <summary> |
389 | ''' 线程会被阻塞在这里,直到外部应用程序执行完毕 |
390 | ''' </summary> |
391 | ''' <returns></returns> |
392 | Public Function Run() As Integer Implements IIORedirectAbstract.Run |
393 | Return Start(waitForExit:=True) |
394 | End Function |
395 | |
396 | Public Sub WriteLine(Optional s As String = "") Implements I_ConsoleDeviceHandle.WriteLine |
397 | If s.StringEmpty Then |
398 | Call input.WriteLine() |
399 | Else |
400 | Call input.WriteLine(s) |
401 | End If |
402 | |
403 | Call input.Flush() |
404 | End Sub |
405 | |
406 | <MethodImpl(MethodImplOptions.AggressiveInlining)> |
407 | Public Sub Write(buffer As Byte()) |
408 | Call input.BaseStream.Write(buffer, Scan0, buffer.Length) |
409 | End Sub |
410 | |
411 | <MethodImpl(MethodImplOptions.AggressiveInlining)> |
412 | Public Overrides Function ToString() As String |
413 | Return $"{Bin} {CLIArguments}" |
414 | End Function |
415 | |
416 | ''' <summary> |
417 | ''' 在进行隐士转换的时候,假若可执行文件的文件路径之中含有空格,则这个时候应该要特别的小心 |
418 | ''' </summary> |
419 | ''' <param name="CLI"></param> |
420 | ''' <returns></returns> |
421 | Public Shared Widening Operator CType(CLI As String) As IORedirect |
422 | Dim tokens As String() = Regex.Split(CLI, SPLIT_REGX_EXPRESSION) |
423 | Dim EXE As String = tokens.First |
424 | Dim args As String = Mid$(CLI, Len(EXE) + 1) |
425 | |
426 | Return New IORedirect(EXE, args) |
427 | End Operator |
428 | |
429 | Public Shared Function Shell(commandLine As String) As IORedirect |
430 | Return CType(commandLine, IORedirect) |
431 | End Function |
432 | |
433 | Private Function Read() As Integer Implements I_ConsoleDeviceHandle.Read |
434 | Return output.Length |
435 | End Function |
436 | |
437 | Private Function ReadLine() As String Implements I_ConsoleDeviceHandle.ReadLine |
438 | Return "" |
439 | End Function |
440 | |
441 | Public Sub WriteLine(s$, ParamArray args() As String) Implements I_ConsoleDeviceHandle.WriteLine |
442 | Call input.WriteLine(String.Format(s, args)) |
443 | Call input.Flush() |
444 | End Sub |
445 | |
446 | #Region "IDisposable Support" |
447 | Private disposedValue As Boolean ' To detect redundant calls |
448 | |
449 | ' IDisposable |
450 | Protected Overridable Sub Dispose(disposing As Boolean) |
451 | If Not Me.disposedValue Then |
452 | If disposing Then |
453 | ' TODO: dispose managed state (managed objects). |
454 | Call processInfo.Close() |
455 | Call processInfo.Dispose() |
456 | Call outputWaitHandle.Dispose() |
457 | Call errorWaitHandle.Dispose() |
458 | End If |
459 | |
460 | ' TODO: free unmanaged resources (unmanaged objects) and override Finalize() below. |
461 | ' TODO: set large fields to null. |
462 | End If |
463 | Me.disposedValue = True |
464 | End Sub |
465 | |
466 | ' TODO: override Finalize() only if Dispose( disposing As Boolean) above has code to free unmanaged resources. |
467 | 'Protected Overrides Sub Finalize() |
468 | ' ' Do not change this code. Put cleanup code in Dispose( disposing As Boolean) above. |
469 | ' Dispose(False) |
470 | ' MyBase.Finalize() |
471 | 'End Sub |
472 | |
473 | ' This code added by Visual Basic to correctly implement the disposable pattern. |
474 | Public Sub Dispose() Implements IDisposable.Dispose |
475 | ' Do not change this code. Put cleanup code in Dispose(disposing As Boolean) above. |
476 | Dispose(True) |
477 | GC.SuppressFinalize(Me) |
478 | End Sub |
479 | #End Region |
480 | End Class |
481 | End Namespace |