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 OverloadsSub New
40     '         Delegate Function
41     
42     
43     '         Delegate Sub
44     
45     '             FunctionGetError, 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 StringImplements 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 StringString)) = 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 StringStringIn 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 BooleanAs 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 = FalseAs 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 IntegerAs Boolean
339             Return outputWaitHandle.WaitOne(timeout)
340         End Function
341
342         <MethodImpl(MethodImplOptions.AggressiveInlining)>
343         Public Function WaitError(timeout As IntegerAs 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 IntegerAs 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 = FalseAs 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 = FalseAs Integer Implements IIORedirectAbstract.Start
385             Return Start(waitForExit, NothingTrue)
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 StringAs 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 StringAs 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 StringImplements 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