First, let me say that I'm not a Visual Basic 6 expert...
My need is to:
launch from a VB6 client code an exeternal .exe file
wait for the process to finish and - during its execution - read the messages coming from its standard output "on the fly" (so that I can print it on a text-filed widget or similars).
I'm wondering if it is even possible to do that in VB6...after a long search on the Internet I didn't come up with anything. Found a lot of examples of how to use the Shell function, but it seems to force me to read the stdout all at once when the process' execution is over, but I want to poll the process for "fresh" messages as they become available.
Any code snippets/suggestions/references are really appreciated.
Thanks in advance!
Use CreatePipe() to create an anonymous pipe that you can pass to CreateProcess().
You can then read from this pipe as required (either using polling or overlapped/async I/O.
This should give you enough info to find a good example.
You can always use Exec method of WshShell to do the job.
I prefer to use a home-grown API based solution cExec.cls much simpler than Bob Riemersma's user control (but not as versatile).
You can also create a batch file that has all the commands that you need to run, and then from VB6 call the batch file by executing
Shell "C:\YourPath\BatchFileName.bat > OutputFileName.txt" 'Overwrites OutputFilename.txt everytime
once you execute that, then open OutputFileName.txt and you will find all of the messages and output that was generated during the batch process. You can then read it in VB6 in a simple open "filename" for input as #1
You should also notice that if you use double GreaterThan symbols, the the output file will not be overwritten every time the batch runs. Instead, it will get appended with the new lines of output.
Shell "C:\YourPath\BatchFileName.bat >> OutputFileName.txt" 'This will append to OutputFileName.txt
Here is the function you want. The exercise of declaring the API (CreatePipe, CreateProcessA, CloseHandle, etc), the types (PROCESS_INFORMATION, STARTUPINFO, SECURITY_ATTRIBUTES) the constants (STARTF_USESTDHANDLES, STARF_USESHOWWINDOW, etc) are left to the reader.
Public Function ExecuteCommand(ByVal CommandLine As String, Optional bShowWindow As Boolean = False, Optional sCurrentDir As String) As String
Dim proc As PROCESS_INFORMATION 'Process info filled by CreateProcessA
Dim ret As Long 'long variable for get the return value of the
'API functions
Dim start As STARTUPINFO 'StartUp Info passed to the CreateProceeeA
'function
Dim sa As SECURITY_ATTRIBUTES 'Security Attributes passeed to the
'CreateProcessA function
Dim hReadPipe As Long 'Read Pipe handle created by CreatePipe
Dim hWritePipe As Long 'Write Pite handle created by CreatePipe
Dim lngBytesRead As Long 'Amount of byte read from the Read Pipe handle
Dim strBuff As String * 256 'String buffer reading the Pipe
'if the parameter is not empty update the CommandLine property
If Len(CommandLine) > 0 Then
mCommand = CommandLine
End If
'if the command line is empty then exit whit a error message
If Len(mCommand) = 0 Then
ApplicationEventLogError "Command Line empty in procedure ExecuteCommand of module modPipedOutput."
Exit Function
End If
'Create the Pipe
sa.nLength = Len(sa)
sa.bInheritHandle = 1&
sa.lpSecurityDescriptor = 0&
ret = CreatePipe(hReadPipe, hWritePipe, sa, 0)
If ret = 0 Then
'If an error occur during the Pipe creation exit
Debug.Print "CreatePipe failed. Error: " & Err.LastDllError & " (" & ReturnError(Err.LastDllError)
Exit Function
End If
'Launch the command line application
start.cb = Len(start)
start.dwFlags = STARTF_USESTDHANDLES Or STARTF_USESHOWWINDOW
'set the StdOutput and the StdError output to the same Write Pipe handle
start.hStdOutput = hWritePipe
start.hStdError = hWritePipe
' start.hStdInput = hInReadPipe
If bShowWindow Then
start.wShowWindow = SW_SHOWNORMAL
Else
start.wShowWindow = SW_HIDE
End If
'Execute the command
If Len(sCurrentDir) = 0 Then
ret& = CreateProcessA(0&, mCommand, sa, sa, 1&, _
NORMAL_PRIORITY_CLASS, 0&, vbNullString, start, proc)
Else
ret& = CreateProcessA(0&, mCommand, sa, sa, 1&, _
NORMAL_PRIORITY_CLASS, 0&, sCurrentDir, start, proc)
End If
If ret <> 1 Then
'if the command is not found ....
Debug.Print "File or command not found in procedure ExecuteCommand"
Exit Function
End If
'Now We can ... must close the hWritePipe
ret = CloseHandle(hWritePipe)
' ret = CloseHandle(hInReadPipe)
mOutputs = vbNullString
'Read the ReadPipe handle
Do
ret = ReadFile(hReadPipe, strBuff, 256, lngBytesRead, 0&)
mOutputs = mOutputs & Left$(strBuff, lngBytesRead)
'Send data to the object via ReceiveOutputs event
Loop While ret <> 0
'Close the opened handles
Call CloseHandle(proc.hProcess)
Call CloseHandle(proc.hThread)
Call CloseHandle(hReadPipe)
'Return the Outputs property with the entire DOS output
ExecuteCommand = mOutputs
End Function
Related
I want to execute a program from MS Access VBA Code. On click of a button, the program should execute.
I am using below code for this:
Dim myPath As String
myPath = "F:\MyExecutables\Prog_copy.exe"
iReturn = wsh.Run(myPath, windowStyle, waitOnReturn)
iReturn gives me 1 if the program executed successfully and 0 in case of any error or exception.
I want to get the exception details (at least the exception message) in VBA if the return value is 0.
How can do that.?
Note: I am using try catch blocks to handle different types of exceptions in my prg_Copy.exe.
Assuming your program outputs errors using StdErr, you can read errors using the following code:
Declare PtrSafe Sub SleepEx Lib "Kernel32.dll" (ByVal dwMilliseconds As Long, Optional ByVal bAlertable As Boolean = True)
Public Sub RunSomething()
Dim wsh As Object
Set wsh = CreateObject("WScript.Shell")
Dim myPath As String
myPath = "F:\MyExecutables\Prog_copy.exe"
Dim ex As Object
Set ex = wsh.Run("""" & myPath & """")
Do While ex.Status = 0 'WshRunning
SleepEx 50
Loop
If ex.Status = 2 'WshFailed
Debug.Print ex.StdErr.ReadAll 'Print any errors
End If
End Sub
Most console programs output errors to StdErr. If your program uses a different method of outputting errors, you need to use a different way to capture them.
Below is the sequence of Process calling;
Service calling Process A (this causes Process A to run under local system account).
Process A launching Process B under different user account (non admin user) using CreateProcessWithLogonW API (as logon credentials of different user is available).
Process B performing some activity which involves invoking another process.
Up to step 2 everything works fine. Process B launched under given user account but Process B is not able to perform a task like executing batch file or launching one more process using CreateProcess API.
Below is the code for invoking Process B;
Dim si As STARTUPINFO
Dim pi As PROCESS_INFORMATION
Dim wUser As String
Dim wDomain As String
Dim wPassword As String
Dim wCommandLine As String
Dim wCurrentDir As String
Dim wApplicaiotnName
Dim Result As Long
si.cb = Len(si)
si.lpDesktop = "WinSta0\Default"
Result = CreateProcessWithLogonW(wUser, wDomain, wPassword, _LOGON_WITH_PROFILE,
wApplicaiotnName, "", _CREATE_UNICODE_ENVIRONMENT, 0&, wCurrentDir, si, pi)
And below code used in Process B to execute batch file;
Dim proc As PROCESS_INFORMATION
Dim start As STARTUPINFO
Dim ret As Long
Dim lpId As Long
Dim llReturn As Long
Dim RetVal As Long
With start
.cb = Len(start)
.lpDesktop = "WinSta0\Default"
If Not IsMissing(WindowStyle) Then
.dwFlags = STARTF_USESHOWWINDOW
.wShowWindow = WindowStyle
End If
End With
ret& = CreateProcessA(0&, pathName, 0&, 0&, 1&, _
NORMAL_PRIORITY_CLASS, 0&, 0&, start, proc)
I have the following sub:
Public Sub ShellApp(URL As String)
Dim vResult As Long
vResult = ShellExecute(0, "open", URL, vbNullString, vbNullString, vbMinimizedFocus)
End If
End Sub
This is on a layer that cannot be changed due to several functionality needed on that sub.
Now, on our Main() sub for example, we check a list of added plugins saved in a text file beside the EXE, and call the above Sub in for loop with the path of the plugins to run them. So if I have 3 plugins as below in the text file:
C:\App1.EXE
C:\App2.EXE
C:\Users\AhmadMusa\AppData\Roaming\App3.exe
First two apps will run fine on all PCs (Static path), but third app will not work on any PC except mine which is not ok... Note that App3 always installed on AppData on any PC, so how to dynamically access it to run the app from any PC without adjustments on the sub.
What will be the path placed in the text file for third app so it can work on all PCs?
I tried (AppData\Roaming\App3.exe) but it does not work... I found on a thread (http://www.vbforums.com/showthread.php?529776-RESOLVED-Open-a-folder-in-AppData) that I can call something like (shell:AppData\Roaming\App3.exe) it did not work to run the App3.exe, but if I call (shell:AppData\Roaming) it will open the Roaming folder very well. But cannot run the EXE.
Any ideas ?
Thanks.
I believe that there is no way to solve the problem without altering the original procedure "ShellApp".
in case you change your mind, i think this post may come in help (with some tweekings)
Public Sub ShellApp(URL As String)
Dim vResult As Long
'vResult = ShellExecute(0, vbNullString, URL, vbNullString, vbNullString, vbNormalFocus)
vResult = ShellExecute(0, vbNullString, "cmd.exe", "/k """"" & URL & """""", vbNullString, vbNormalFocus)
End Sub
Private Sub Command1_Click()
ShellApp "%appdata%\PROGRAME.exe"
End Sub
this because only "cmd.exe" and ofcourse batch scripts are able to expend variables that are enclosed with percent character "%"
To close the console as soon as it starts change the parameter "/k" to "/c",
sub test()
dim tmp as string
tmp = environ("AppData ") & "\calc.exe"
call ShellExecute(..., tmp, ...)
end sub
fill the other arguments (...) the way you see it right
You need to expand the Environment variable (this is what the %...% does):
Debug.Print Environ("APPDATA") ' will print the expanded %appdata%
So, in your text file you should put:
%APPDATA%\App3.exe
How to expand the path? You can loop over the environment variables provided by the VB Environ function and do a string replace by yourself (the VB way) or you can profit from the ExpandEnvironmentStrings function (the Win32 API way).
Below a snippet using this second option:
Private Declare Function ExpandEnvironmentStrings Lib "kernel32.dll" _
Alias "ExpandEnvironmentStringsA" ( _
ByVal lpSrc As String, _
ByVal lpDst As String, _
ByVal nSize As Long) As Long
Public Function ExpandEnvironString(ByVal URL As String) As String
Dim buf As String, bufSize As Long
bufSize = ExpandEnvironmentStrings(URL, ByVal 0&, 0&)
buf = String(bufSize + 1, vbNullChar)
bufSize = ExpandEnvironmentStrings(URL, buf, Len(buf))
ExpandEnvironString = Left$(buf, InStr(1, buf, vbNullChar) - 1)
End Function
Before you call ShellApp(URL As String) you should expand the path:
URL = ExpandEnvironString(URL)
ShellExecute will receive the expanded path: C:\Users\AhmadMusa\AppData\Roaming\App3.exe
This is a non-breaking change, because if your initial default setting will be later changed to a custom fixed path, the ExpandEnvironmentStrings function will simply ignore it.
Example:
ExpandEnvironString("C:\App1.EXE ") will return C:\App1.EXE
More info:
you can get all your environment variables with following procedure:
Private Sub EnvironmentEntries()
Dim Entry As String, i As Long
i = 1
Do
Entry = Environ(i)
i = i + 1
If Entry = "" Then Exit Do
Debug.Print Entry
Loop
End Sub
... and check some additional info here:
Why are there directories called Local, LocalLow, and Roaming under \Users\?
I am trying to use api functions inside vb6 that will allow me to bring a program to the foreground if it is running. At which point I will use sendkeys to send key strokes to the program in question.
The kicker is that the only thing I know about the program is its path and .exe name. For instance, 'c:\anyfolder\anyprog.exe'.
I can find all kinds of info on how to do this if I know other things about the program but not if I only know the above (not even what the title bar says when it is in the foreground, which the program itself changes regularly).
Is there a way to do this?
So far, with Remy's help, I have this vb6 code where I try to convert C code from Taking a Snapshot and Viewing Processes to vb6. But it is not quite working, any ideas?
Private Sub FillLists_Click()
PathList.Clear
FileNameList.Clear
Dim p As Long
Dim m As Long
Dim ml As Long
Dim hProcessSnapshot As Long
Dim h As Long
Dim hl As Long
Dim uProcess As PROCESSENTRY32
Dim uModule As MODULEENTRY32
hProcessSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0&)
If hProcessSnapshot = 0 Then Exit Sub
uProcess.dwSize = Len(uProcess)
p = ProcessFirst(hProcessSnapshot, uProcess)
Do While p 'as long as p is not 0
h = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, uProcess.th32ProcessID)
hl = GetLastError()
uModule.dwSize = Len(uModule)
m = Module32First(h, uModule)
ml = GetLastError()
PathList.AddItem "h=" & h & " hl=" & hl & " m=" & m & " ml=" & ml & uModule.szModule
FileNameList.AddItem uProcess.szExeFile
Call CloseHandle(h)
p = ProcessNext(hProcessSnapshot, uProcess)
Loop
Call CloseHandle(hProcessSnapshot)
End Sub
AND the output from that:
So, the above did not work, likely because vb6 is 32bit and my computer is Win7 64 bit. I found this function on a Google search for 'vb6 QueryFullProcessImageName' from a Russian forum thread, couldn't read the comments but the code was golden!
Function GetProcessNameByPID(pid As Long) As String
Const PROCESS_QUERY_LIMITED_INFORMATION As Long = &H1000
Const PROCESS_QUERY_INFORMATION As Long = &H400
Const MAX_PATH As Long = 260
Dim hProc As Long
Dim Path As String
Dim lStr As Long
Dim inf(68) As Long
Dim IsVistaAndLater As Boolean
inf(0) = 276: GetVersionEx inf(0): IsVistaAndLater = inf(1) >= 6
If Not IsVistaAndLater Then Exit Function
hProc = OpenProcess(IIf(IsVistaAndLater, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_QUERY_INFORMATION), False, pid)
If hProc <> 0 Then 'INVALID_HANDLE_VALUE Then
lStr = MAX_PATH
Path = Space(lStr)
' minimum Windows Vista !!!!!!!
If QueryFullProcessImageName(hProc, 0, StrPtr(Path), lStr) Then
GetProcessNameByPID = Left$(Path, lStr)
End If
CloseHandle hProc
End If
End Function
Now my code is:
Private Sub FillLists_Click()
PathList.Clear
FileNameList.Clear
Dim p As Long
Dim hProcessSnapshot As Long
Dim uProcess As PROCESSENTRY32
hProcessSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0&)
If hProcessSnapshot = 0 Then Exit Sub
uProcess.dwSize = Len(uProcess)
p = ProcessFirst(hProcessSnapshot, uProcess)
Do While p 'as long as p is not 0
PathList.AddItem GetProcessNameByPID(uProcess.th32ProcessID)
FileNameList.AddItem uProcess.szExeFile
p = ProcessNext(hProcessSnapshot, uProcess)
Loop
Call CloseHandle(hProcessSnapshot)
End Sub
AND the output from that is (and the rest is just do a AppActivate uProcess.th32ProcessID when you find the one you want):
THANKS REMY!
EDIT: Be careful, it turns out that you can't bring another application to the foreground if the application that is causing another app to come to foreground in minimized. I also needed to enumerate only the apps in the Alt-Tab group of programs and use api's in a way that forced a window to the foreground, not AppActivate or the SetForeGroundWindow() by itself.
You would have to:
enumerate all running processes, looking at their full paths and filenames until you find the one you are interested in. Use EnumProcesses() or CreateToolhelp32Snapshot() for that. See Enumerating All Processes and Taking a Snapshot and Viewing Processes for examples. Once you find the desired filename, you will know its Process ID.
use EnumWindows() and GetWindowThreadProcessId() to enumerate all top-level windows looking for the one(s) that belong to the same Process ID. Then you can restore those window(s) as needed (if you have permission to do so, that is - see all of the restrictions mentioned in the documentation for SetForegroundWindow()). If a window is minimized, you can try sending it a WM_SYCOMMAND/SC_RESTORE message. But if the window is already non-minimized but just not focused, you might run into resistence trying to focus it programmably.
I am running following command from Command Prompt
dir > c:\log.txt 2>&1
The out is directed to c:\log.txt file successfully.
Then, running the same command using CreateProcessA as below and nothing happens
Public Function ExecCmd(cmdline$)
Dim proc As PROCESS_INFORMATION
Dim start As STARTUPINFO
Dim ret As Long
start.cb = Len(start)
start.dwFlags = 1
start.wShowWindow = 1
ret& = CreateProcessA(vbNullString, cmdline$, 0&, 0&, 1&, NORMAL_PRIORITY_CLASS, 0&, vbNullString, start, proc)
ret = WaitForSingleObject(proc.hProcess, INFINITE)
Call GetExitCodeProcess(proc.hProcess, ret&)
Call CloseHandle(proc.hThread)
Call CloseHandle(proc.hProcess)
ExecCmd = ret&
End Function
Here cmdline$ is passed as dir > c:\log.txt 2>&1
I have tried Batch file - How to redirect output from exe after it has terminated? and Display & Redirect Output
Please suggest what is wrong here
Why don't you use the shell function? Here is an example of how to redirect the output:
Option Explicit
Private Sub Form_Load()
ExecCmd "dir >c:\log.txt 2>&1"
End Sub
Private Sub ExecCmd(cmdline As String)
Shell "cmd /c " & cmdline, vbHide
End Sub