Laura Moloney
Reader Laura Moloney tells how she figured out the answer to a particularly vexing problem, and gives some kudos to SearchVB.com and the rest of the VB community in the process.
I started working with Visual Basic about 6 months ago when I had to pick up an application that was written by someone else and was not meeting the functional requirements of the user. So I had an opportunity to learn as I went, relying heavily on the Internet and VB sites like SearchVB that offered assistance where needed (many of my favorites were recommended by SearchVB). My next foray was to build a back-end job process logging application where a user would initiate several back-end tasks via a VB application, for example run awk to format data, use Oracle's SQL Loader to load the data to temporary Oracle tables, and then run Oracle PL/SQL code to validate and post the data to the production databases.
Throughout this process I needed to ensure that start/stop times and error messages from each step were captured in a central database table. This required me to build a console application that would manage each step of the process. Again this is where the Internet and the VB community as a whole played a significant part in helping me build the application.
One area where I could not find an answer from the VB community was in capturing the system responses to commands run in the console window. For example if I were executing an AWK process and the process could not find a file, the system would return a message 'file not found' to the console window. I needed to be able to capture that message. After many failed attempts, I was able to find the resolution to the problem.
The procedure below opens a console window via the EXECCMD function, runs an AWK process, waits for its completion, and then reads the console screen buffer twice - the first time to get the console buffer contents as a whole, and the next time to get a line at a time. I hope others find this useful!
------------------------------------------------------
FORM LOAD EVENT:
Public Sub Form_Load()
On Error GoTo errHandler
Dim commandText As String
Dim dTaskID As Double
Dim fsuccess As Integer
Dim csScreenBuffer As CONSOLE_SCREEN_BUFFER_INFO
Dim ConsoleBoundary As SMALL_RECT
Dim XPos As Integer, YPos As Integer
commandText = "x:packagesawkawk -f c:visual~1consol~1post.awk"
If AllocConsole() Then
hConsoleOut = GetStdHandle(STD_OUTPUT_HANDLE)
If hConsoleOut = 0 Then Err.Raise Number:=502,
Description:="Couldn't allocate STDOUT"
Else
Err.Raise Number:=502, Description:="Couldn't allocate console"
End If
dTaskID = ExecCmd(commandText) 'start AWK processing
fsuccess = GetConsoleScreenBufferInfo(hConsoleOut, csScreenBuffer)
'non-zero result is a success
ConsoleBoundary = csScreenBuffer.srWindow
Dim readLength As Integer
'to read console window messages as one blob of text
Dim fullMessage As String * 400
Dim fsuccess2 As Integer
readLength = 400
XPos = 0
YPos = 0
fsuccess2 = ReadConsoleOutputCharacter(hConsoleOut, fullMessage,
readLength, XPos + YPos, vbNull) 'non-zero result is a success
MsgBox fullMessage
'To read console window messages one line at a time using the
'ypos axis I was unable to figure out how to pass in the x and y
'coordinates (x,y). All formats I used did not work. Instead I found
that
'the screen buffer length is 64K, so using the ypos as a variable I
'basically multiply the row number I want with 64k to get 'the starting
'point of the row. This works because the screen buffer 'can be
accessed as
'one contiguous string.
Dim lineMessage As String * 80
readLength = 80
XPos = 0
YPos = 0
For YPos = 0 To ConsoleBoundary.Bottom
def = ReadConsoleOutputCharacter(hConsoleOut, lineMessage,
readLength, YPos * 65536, vbNull)
If Trim(lineMessage) <> "" Then MsgBox Trim(lineMessage)
Next YPos
CloseHandle hConsoleOut
FreeConsole
Exit Sub
errHandler:
MsgBox Err.Number & " " & Err.Source & " " & Err.Description
CloseHandle hConsoleOut
FreeConsole
End Sub
MODULE.BAS:
'Functions that support the application opening the command
'window where the subsequent execcmd will execute from,
'as well as close and free the command window.
'Also functions that support reading the console screen buffer
Public hConsoleIn As Long
Public hConsoleOut As Long
Public Const NORMAL_PRIORITY_CLASS = &H20&
Public Const INFINITE = -1&
Public Const STD_OUTPUT_HANDLE = -11&
Public Const STD_INPUT_HANDLE = -10&
Public Type STARTUPINFO
cb As Long
lpReserved As String
lpDesktop As String
lpTitle As String
dwX As Long
dwY As Long
dwXSize As Long
dwYSize As Long
dwXCountChars As Long
dwYCountChars As Long
dwFillAttribute As Long
dwFlags As Long
wShowWindow As Integer
cbReserved2 As Integer
lpReserved2 As Long
hStdInput As Long
hStdOutput As Long
hStdError As Long
End Type
Public Type PROCESS_INFORMATION
hProcess As Long
hThread As Long
dwProcessID As Long
dwThreadID As Long
End Type
Public Type COORD
x As Integer
y As Integer
End Type
Public Type SMALL_RECT
Left As Integer
Top As Integer
Right As Integer
Bottom As Integer
End Type
Public Type CONSOLE_SCREEN_BUFFER_INFO
dwSize As COORD
dwCursorPosition As COORD
wAttributes As Integer
srWindow As SMALL_RECT
dwMaximumWindowSize As COORD
End Type
Declare Function AllocConsole Lib "kernel32" () As Long
Declare Function FreeConsole Lib "kernel32" () As Long
Declare Function GetStdHandle Lib "kernel32" (ByVal _
nStdHandle As Long) As Long
Declare Function GetLastError Lib "kernel32" ()
Declare Function GetConsoleScreenBufferInfo Lib "kernel32" _
(ByVal hConsoleOutput As Long, _
lpConsoleScreenBufferInfo As CONSOLE_SCREEN_BUFFER_INFO) As Long
Declare Function SetConsoleCursorPosition Lib "kernel32" _
(ByVal hConsoleOutput As Long, ByVal CursorPosition As Long) As Long
Declare Function WaitForSingleObject Lib "kernel32" (ByVal _
hHandle As Long, ByVal dwMilliseconds As Long) As Long
Declare Function CreateProcessA Lib "kernel32" (ByVal _
lpApplicationName As String, ByVal lpCommandLine As String, ByVal _
lpProcessAttributes As Long, ByVal lpThreadAttributes As Long, _
ByVal bInheritHandles As Long, ByVal dwCreationFlags As Long, _
ByVal lpEnvironment As Long, ByVal lpCurrentDirectory As String, _
lpStartupInfo As STARTUPINFO, lpProcessInformation As _
PROCESS_INFORMATION) As Long
Declare Function CloseHandle Lib "kernel32" _
(ByVal hObject As Long) As Long
Declare Function GetExitCodeProcess Lib "kernel32" _
(ByVal hProcess As Long, lpExitCode As Long) As Long
Declare Function ReadConsoleOutputCharacter Lib "kernel32" Alias
"ReadConsoleOutputCharacterA" _
(ByVal hConsoleOutput As Long, ByVal lpCharacter As String, ByVal _
nLength As Long, ByVal dwReadCoord As Long, _
lpNumberOfCharsRead As Long) As Long
Public Function ExecCmd(cmdline$)
Dim proc As PROCESS_INFORMATION
Dim start As STARTUPINFO
' Initialize the STARTUPINFO structure:
start.cb = Len(start)
' Start the shelled application:
ret& = CreateProcessA(vbNullString, cmdline$, 0&, 0&, 1&, _
NORMAL_PRIORITY_CLASS, 0&, vbNullString, start, proc)
' Wait for the shelled application to finish:
ret& = WaitForSingleObject(proc.hProcess, INFINITE)
Call GetExitCodeProcess(proc.hProcess, ret&)
Call CloseHandle(proc.hThread)
Call CloseHandle(proc.hProcess)
ExecCmd = ret&
End FunctionThis was first published in December 2000