It’s all made out of pipes
|So these things called pipes exist, and they’re used by every executable made in the last 40 years to send information in and out of themselves.
In a former life, one of my slightly more intelligible bosses used to create things called ‘egg diagrams’, which was essentially a circle (the egg), which represented whatever project we were trying to create specifications for, with maybe 4 or 5 different arrows going into or out of the egg denoting how people and other systems exchange data with the egg/circle/very reasonably priced software project.
It probably helps if it’s on a whiteboard.
These were normally extremely high level, so you’d have one arrow for ‘users’, one for ‘some kind of database’, one for ‘authentication server’, one for ‘rotating knives‘, that sort of thing.
So the egg diagram for a process as far as I want to get into things today would look like this:
Which is surprisingly difficult to get right for some languages.
VB, for one. It’s got a builtin command ‘Shell’, which allows you to kick off a process, but that’s about it. If you want to write to the process’s standard input or read its standard output or standard error handles/streams/file descriptors, then you need to use cmd’s redirect facilities (“<", ">“, “|”) to temporary files and then read the files afterwards, which is a bit suboptimal, and doesn’t work in some use-cases. Here’s a module I wrote up in VBA6 which gives you the ability to write to stdin and read stderr/stdout from a Windows process. It doesn’t use .NET.
I’m providing it here in two flavours, one as a module (for simple redirection of stdin/stdout/stderr), and one as a class, which provides an event-driven interface for more complex interactions and adds features like timeouts.
This is a screenshot of an MSAccess form containing the class-based process object, text boxes containing the output streams, and a standard ‘file copy’ animation which gives the user some feedback whilst the process is executing. (Update: I’ve included a list of resource IDs and system AVIs in this blog post)
The source code for this form and the supporting modules are at the bottom of this blog post.
The Module
The module should be easy enough; just cut and paste the source code into your Access/Excel/Word/Powerpoint/other-VBA-enabled-program, and then you can call other executables using the fncRedirectProcess function. It returns a RedirectProcessResult object.
Dim rpr As RedirectProcessResult rpr = new RedirectProcessResult ' You wouldn't normally call 'cmd' here; just call whatever program you want to run directly. ' (I'm just using a command-line program that everyone should have already installed, ' i.e. the Windows command shell) Debug.Print "Calling cmd process..." Set rpr = fncRedirectProcess("cmd", "", _ "echo hello" & vbCrLf & _ "set" & vbCrLf & _ "this-program-doesnt-exist" & vbCrlf & _ "exit 1" & vbCrLf, _ True, True) ' boolShowWindow, boolSeparateStdoutStderr Debug.Print "Process complete" Debug.Print "rpr.strCommandLine=" & rpr.strCommandLine Debug.Print "rpr.lngErrNumber=" & rpr.lngErrNumber Debug.Print "rpr.strErrDescription=" & rpr.strErrDescription Debug.Print "rpr.lngErrExitCode=" & rpr.lngExitCode Debug.Print "rpr.strStdOut=" & rpr.strStdOut Debug.Print "rpr.strStdErr=" & rpr.strStdErr |
which should give you output that looks similar to the following (notice the environment block).
Calling cmd process... Process complete rpr.strCommandLine="cmd" rpr.lngErrNumber=0 rpr.strErrDescription= rpr.lngErrExitCode=1 rpr.strStdOut=Microsoft Windows XP [Version 5.1.2600] (C) Copyright 1985-2001 Microsoft Corp. C:\transfer\2013-10-03-cvs>echo hello hello C:\transfer\2013-10-03-cvs>set COMSPEC=C:\WINDOWS\system32\cmd.exe PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS PROMPT=$P$G SystemRoot=C:\WINDOWS C:\transfer\2013-10-03-cvs>this-program-doesnt-exist C:\transfer\2013-10-03-cvs>exit rpr.strStdErr='this-program-doesnt-exist' is not recognized as an internal or external command, operable program or batch file. |
The parameters to the function, in the time-honoured Microsoft™ formatting, terminology and dry sense of clinical accuracy before they started throwing Clippy, ribbons, tiles and other user-derived-opti-sensed-vacu-context internet-based help systems into the mix, are:
See AlsoExampleSpecifics
Runs an executable program and returns a RedirectProcessResult user-defined type containing the program’s output and exit codes.
fncRedirectProcess(strApplicationName, strArguments [ ,strStdIn [ ,boolShowWindow [ ,boolSeparateStdoutStderr ]]])
The fncRedirectProcess function syntax has these named arguments:
Part | Description |
---|---|
strApplicationName | Required; String. Name of the program to execute.
Standard CreateProcess rules apply when searching for the application: If the file name does not contain an extension, .exe is appended. Therefore, if the file name extension is .com, this parameter must include the .com extension. If the file name ends in a period (.) with no extension, or if the file name contains a path, .exe is not appended. If the file name does not contain a directory path, the system searches for the executable file in the following sequence:
Note that this function does not search the per-application path specified by the App Paths registry key. |
strArguments | Required; String. Any arguments or command-line switches |
strStdIn | Optional; String. A string containing the data to be written to the standard input handle of the process. If strStdIn is omitted, the standard input is the empty string (“”). |
boolShowWindow | Optional; Boolean. If True, will display the process window as the process is running. If boolShowWindow is omitted, the window is not shown. |
boolSeparateStdoutStderr | Optional; Boolean. If True, the function will separate the standard output and standard error streams in the strStdOut and strStdErr elements of the returned RedirectProcessResult object, otherwise both streams will be interleaved in the strStdOut element.
If boolSeparateStdoutStderr is omitted, the streams are not separated. |
The fncRedirectProcess function creates a Windows child process and redirects its standard input, output and error handles.
The process will run synchronously. This means the function will not return until the child process has terminated (or an internal error has occurred).
If the process was created and terminated successfully, the value of the lngErrNumber element will be zero.
If lngErrNumber is zero, the result of the child process can be determined by inspecting the value of the lngExitCode element. Any data written by the process to its standard output and error handles will be contained in the strStdOut and strStdErr elements, respectively.
Even if the process window is displayed (due to the boolShowWindow being set to True) normal output may not be visible in the window, since it is redirected by this function.
The fncRedirectProcess function will return a RedirectProcessResult user-defined type, containing the following elements:
Part | Description |
---|---|
strCommandLine | String. The complete command line which was passed to the Windows CreateProcess function.
The command line is composed of the concatenation of the strApplicationName enclosed in double quotes, a space, and the value of the strArguments. |
strStdOut | String. The standard output generated by the process. |
strStdErr | String. The standard error generated by the process. |
lngExitCode | Long. The exit code of the process.
In Windows, a non-zero exit code conventionally denotes an error condition within the program. See the documentation for the specific process for more information. |
lngErrNumber | Long. If this value is non-zero, an error occurred launching or handling the process in the fncRedirectProcess function itself. If this occurs, you can refer to strErrDescription for a text description of the error. |
strErrDescription | String. If the value of lngErrNumber is non-zero, this element will contain a text description of the error that occurred. |
The Class
The clsProcess class is slightly more complex but has a few more features. It provides a number of events (Tick, StdOutAvailable, StdErrAvailable), which are triggered by the class as the process executes.
In it’s simplest form, it’s similar to fncRedirectProcess, except instead of returning a RedirectProcessResult, you retrieve information from the class itself.
Dim clsProcess As clsProcess p = new clsProcess ' You wouldn't normally call 'cmd' here; just call whatever program you want to run directly. ' (I'm just using a command-line program that everyone should have already installed, ' i.e. the Windows command shell) p.ApplicationName = "cmd" p.Arguments = "" p.StdIn = "echo hello" & vbCrLf & _ "set" & vbCrLf & _ "this-program-doesnt-exist" & vbCrlf & _ "exit 1" & vbCrLf Debug.Print "Calling cmd process..." p.Execute Debug.Print "Process complete" Debug.Print "p.ApplicationName=" & p.ApplicationName Debug.Print "p.Arguments=" & p.Arguments Debug.Print "p.ErrNumber=" & p.errNumber Debug.Print "p.ErrDescription=" & p.errDescription Debug.Print "p.ErrExitCode=" & p.ExitCode Debug.Print "p.StdOut=" & p.StdOut Debug.Print "p.StdErr=" & p.StdErr |
which gives similiar output to the fncRedirectProcess() example above.
If you prefer, you could use the event interface, which allows you to read stdout/stderr as the process runs, allowing you to update progress bars or provide other feedback to the user, or to send/receive data to interactive commands (similar to the unix ‘expect‘ command), or who knows, get your computer to do two things at the same time.
The frmProcess screenshot at the top of this blog entry is an example of what is possible using the clsProcess class.
Seeing as I don’t have a fleet of technical writers whose job it is document this thing in 12 different languages, I’ve only written up some documentation for the main class itself, and a couple of the properties. I may update this at a later time.
See AlsoExamplePropertiesMethodsEvents
A clsProcess object represents an executable program, its invocation, output and exit codes.
You can create, execute and evaluate the result of processes using the clsProcess object.
In order to use the object, you should:
- Create a new clsProcess object.
- Set any Properties required prior to the invocation of the process:
- Assign a value to the ApplicationName property containing the filename of the process to be created.
- Optionally, assign a value to the Arguments property if any command-line arguments are to be supplied to the application.
- If any input is to be supplied to the standard input of the process at startup, assign a value to the StdIn property.
- If the process window is to be shown, assign the value True to the ShowWindow property.
- If the standard output and standard error streams should be separated, assign the value True to the SeparateStdoutStderr property.
- If the process is to be automatically terminated after a period of inactivity, assign a value to the TimeoutMillis property.
- Call the Execute method.
- If the calling application needs to process output as it is created, handle the StdoutAvailable event.
- If the calling application needs to process error output as it is created, handle the StderrAvailable event.
- If the calling application needs to perform other periodic tasks whilst the process is executing, handle the Tick event.
The delay between tick counts can be set using the TickWaitMillis property.
The current tick count can be read from the TickCount property. The tick count when the data was last received on the standard output or standard input handles can be read from the LastOutputTickCount property.
- If the calling application needs to terminate the process, call the Terminate method during any of the event handlers above.
- Once the process has completed, inspect the ErrNumber, ErrDescription, ExitCode, StdOut, StdErr and Terminated properties.
It is an error to set the value of the SeparateStdoutStderr, ShowWindow, Arguments, StdIn or TickWaitMillis properties whilst the Execute method is running.
It is an error to get the value of the ExitCode, ErrNumber, ErrDescription properties whilst the Execute method is running.
You may reuse clsProcess objects for multiple executions of processes. Properties can be modified between invocations.
You can determine whether a process exited normally or was terminated (either by a timeout or by the Terminate method) by checking the Terminated property.
If the clsProcess object goes out of scope or is otherwise destroyed (e.g. by setting it to Null), then any running Windows process referenced by that clsProcess object is terminated.
If an internal error occurs, it is possible for the Execute method to return without properly terminating the Windows process, or closing all handles to that process. The values of the ErrNumber and ErrDescription properties may be used to determine the cause of the internal error.
See AlsoExampleApplies ToSpecifics
Execute a clsProcess.
expression.Execute | |
expression | Required. An expression that returns one of the objects in the Applies To list. |
See the class documentation.
See the blog post.
A StdoutAvailable event occurs when a clsProcess is executing a Windows process during a call to Execute, and the child process writes data to the standard output handle (or to the standard error handle if the clsProcess has the SeparateStdoutStderr property set to False).
Private Sub object_StdoutAvailable(strText) | |
object | The name of a clsProcess. |
strText | String. The data sent to standard output since the last StdoutAvailable event. |
Only the output received since the last StdoutAvailable event is returned (or since the Execute method if no previous event has occurred). The complete (cumulative) data written to the standard output handle can be read from the StdOut property if required.
StdoutAvailable events will not occur less than TickWaitMillis milliseconds apart (by default, 100 milliseconds). See the section marked ‘Timing’ below.
Additional input can be sent to the process using the SendInput method.
To perform activity whilst no output is received, use the Tick event.
The child Windows process can be signalled for termination by calling the Terminate method within the StdoutAvailable event handler.
Responsiveness may be improved by reducing TickWaitMillis.
CPU usage may be reduced by increasing TickWaitMillis.
Buffering of output by the child process may increase the time between when data is written to standard output, and when this event occurs.
The TickWaitMillis property does not include time spent inside any clsProcess event handlers.
The TickWaitMillis property can be set before calling Execute, or modified during the StdoutAvailable event handler.
The current tick count can be retrieved by reading the TickCount property.
The last tick count in which any output was written to standard output or standard error can be retrieved by reading the LastOutputTickCount property.
See AlsoApplies ToExample
You can use the TimeoutMillis property of a clsProcess object to specify or determine the amount of idle time in milliseconds that can elapse before a process will be terminated.
expression.TimeoutMillis | |
expression | Required. An expression that returns one of the objects in the Applies To list. |
This property setting contains a Long value representing a duration in milliseconds. If no data is received on either the standard output or standard error handles for at least this duration, then the process will be forcibly terminated.
This property is read/write.
Foribly terminating a process will interrupt any processing it may be performing. This may, for example, leave application disk file structures in an inconsistent state, or cause other data to be corrupted.
If no timeout is to be applied or an existing timeout is to be cleared/cancelled, use a negative or zero value for TimeoutMillis.
The TimeoutMillis property does not include time spent inside any clsProcess event handlers.
Timeouts will only have a resolution as precise as the current value of the TickWaitMillis property (e.g. a TimeoutMillis value of 50 and a TickWaitMillis of 100 will time out in 100 milliseconds, not 50 milliseconds).
It is recommended that the TickWaitMillis property not be modified during a timeout. If this property is modified during a timeout, then the actual duration of the timeout is not specified.
The Tick event handler can also cause a process to be terminated by calling the Terminate method.
The presence of a Tick event handler will not, in and of itself, cancel any timeouts on a clsProcess object, although it may cancel a timeout by setting the TimeoutMillis property to zero.
If a process is terminated by exceeding its timeout, then the value of the Terminated property will be True
Here’s the code already.
And here’s an MSAccess .mdb file which contains both the module and class, plus some tests, and the example form shown earlier.
Update 27/10/2013: Added link to a page of system animations.
THANK YOU!!
I have been looking like crazy for something like this.
I knew it was possible but I couldn’t seem to find it.
I had to close the hStdinWrite socket otherwise my processes were hanging.
thanks for the code!
‘ first first stdin block; TODO: may exceed buffer size ?
If (WriteFile(hStdinWrite, ByVal strStdIn, Len(strStdIn), lngResult, ByVal 0&) = 0) Then
subSetError 16, “WriteFile failed”
Exit Sub
End If
Debug.Print “wrote ” & lngResult & ” bytes to stdin; buflen was ” & Len(strStdIn)
lngStdInBufferPos = lngResult + 1
If (CloseHandle(hStdinWrite) = 0) Then
subSetError 16, “CloseHandle of hStdinWrite failed”
Exit Sub
End If