Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations derfloh on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

Wait for a shelled app to finish 3

Status
Not open for further replies.

guitardave78

Programmer
Joined
Sep 5, 2001
Messages
1,294
Location
GB
Is it possible to asynchronously shell and wait an app.
I want a timer running on a form whilst some command prompt stuff runs in the background. The only scripts i have found lock up the application so you cant redraw the timer.

}...the bane of my life!
 
This is the standard ShellAnd Wait routine;

'ShellAndWait defs
Public Declare Function OpenProcess Lib "kernel32" (ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, ByVal dwProcessId As Long) As Long
Public Declare Function WaitForInputIdle Lib "user32" (ByVal hProcess As Long, ByVal dwMilliseconds As Long) As Long
Public Declare Function WaitForSingleObject Lib "kernel32" (ByVal hHandle As Long, ByVal dwMilliseconds As Long) As Long
Public Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
Public Const INFINITE = &HFFFF, SYNCHRONIZE = &H100000

Sub ShellAndWait(sAppPath$, Optional iWindowStyle As VbAppWinStyle = vbMinimizedFocus, Optional lmsTimeOut As Long = INFINITE)

'ref Compuserve BasLan forum post #732415 from Tom Esh

Dim lPid As Long, hProc As Long, lTimeout As Long

lPid = Shell(sAppPath, iWindowStyle)
hProc = OpenProcess(SYNCHRONIZE, 0&, lPid)
If hProc <> 0& Then
WaitForInputIdle hProc, INFINITE
WaitForSingleObject hProc, lmsTimeOut
CloseHandle hProc
End If

End Sub

However if you really require the shelled to app to run asynchronously may be you should just be using VB's Shell function.
 
Yeah, trouble is that locks the form so no timer shows.
I found
Code:
Private Declare Function OpenProcess Lib "kernel32" _
    (ByVal dwDesiredAccess As Long, _
    ByVal bInheritHandle As Long, _
    ByVal dwProcessId As Long) As Long
Private Declare Function GetExitCodeProcess Lib _
    "kernel32" (ByVal hprocess As Long, _
   lpExitCode As Long) As Long
Private Declare Function CloseHandle Lib "kernel32" _
    (ByVal hObject As Long) As Long
Private Declare Sub Sleep Lib "kernel32" _
    (ByVal dwMilliseconds As Long)

Private Const STILL_ACTIVE = &H103
Private Const PROCESS_QUERY_INFORMATION = &H400

Private Sub Command1_Click()
   Dim ret&
   ret = ShellAndWait("Notepad", vbNormalFocus)
   MsgBox "Done"
End Sub

Function ShellAndWait(PathName As String, _
    Optional WS As VbAppWinStyle = vbMinimizedFocus) As Double
' Drop-in replacement for VB's Shell command- except it
' doesn't return until shelled app is done. J.LeVasseur
' <lvass...@tiac.net> Pieced together from a couple of
' other people's ideas, actually. Works for me ...
'-------------------------------------------------------
   Dim lhProcess     As Long
   Dim lExitcode     As Long
   Dim dProcessID    As Double
   '----------------------------
   On Error GoTo errShellAndWait
   dProcessID = Shell(PathName, WS)
   lhProcess = OpenProcess(PROCESS_QUERY_INFORMATION, False, dProcessID)
   Do
      Call Sleep(50): DoEvents
      Call GetExitCodeProcess(lhProcess, lExitcode)
   Loop While (lExitcode = STILL_ACTIVE)
   CloseHandle (lhProcess)
   ShellAndWait = dProcessID
Exit Function
errShellAndWait:
   If lhProcess <> 0 Then
      CloseHandle (lhProcess)
   End If
   ShellAndWait = dProcessID
End Function

The app runns for about 3 hours and my last shellwait async code had an overflow error after an hour.

}...the bane of my life!
 
This is Hypetia's code, can't find the original thread:
Code:
Private Declare Function OpenProcess Lib "kernel32.dll" (ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, ByVal dwProcessId As Long) As Long
Private Const PROCESS_QUERY_INFORMATION = &H400

Private Function IsProcessRunning(pid As Long) As Boolean
Dim hProcess As Long
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid)
CloseHandle hProcess
IsProcessRunning = hProcess
End Function
Basically, this code queries for a running process (in this case, shell.exe), and returns false if it doesn't find it. You can store the pid value and loop around a call to isprocessrunning.

HTH

Bob
 
What you need is my minor reworking of the classic ShellAndWait. This is the simplest variant, which uses VB's DoEvents to provide the illusion of asynchronicity, which will achieve the goal you describe (I have some increasingly complex versions that we don't need here). Here's the main routine, which can go in a module:
Code:
[blue]Option Explicit

Private Type PROCESS_INFORMATION
        hProcess As Long
        hThread As Long
        dwProcessId As Long
        dwThreadId As Long
End Type

Private 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

Private Const NORMAL_PRIORITY_CLASS = &H20
Private Const INFINITE = &HFFFF

Private Const WAIT_OBJECT_0 = 0

Private 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
Private Declare Function WaitForSingleObject Lib "kernel32" (ByVal hHandle As Long, ByVal dwMilliseconds As Long) As Long
Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
Private Declare Function WaitForInputIdle Lib "user32" (ByVal hProcess As Long, ByVal dwMilliseconds As Long) As Long

Public Sub ExecCmd(ByVal cmdline As String)
    Dim proc As PROCESS_INFORMATION
    Dim start As STARTUPINFO
    Dim ret As Long
    Dim hProc As Long

    ' Initialize the STARTUPINFO structure:
      start.cb = Len(start)
    
    ' Start the shelled application:
      hProc = CreateProcessA(vbNullString, cmdline, 0&, 0&, 1&, NORMAL_PRIORITY_CLASS, 0&, vbNullString, start, proc)

    If hProc <> 0& Then WaitForInputIdle hProc, INFINITE

    ' Wait for the shelled application to finish:
    ret = -1 ' can't leave as default because that is same value as WAIT_OBJECT_0
    Do Until ret = WAIT_OBJECT_0
        ret = WaitForSingleObject(proc.hProcess, 10)
        DoEvents
    Loop

    ret = CloseHandle(proc.hProcess)
End Sub[/blue]

Then, to illustrate it in use you'll need a form with a command button, a textbox and a timer:
Code:
[blue]Option Explicit

Private Sub Command1_Click()
    ExecCmd "c:\windows\notepad.exe"
    MsgBox "I only get here when launched program finishes" & vbCrLf & "thus this must be a ShellAndWait ..."
End Sub

Private Sub Form_Load()
    Timer1.Interval = 1000
    Timer1.Enabled = True
End Sub

Private Sub Timer1_Timer()
    Text1.Text = FormatDateTime(Now, vbLongTime)
End Sub[/blue]
 
Very nice, strongm. No doubt you'll be less than surprised to find that I have several questions which I believe to be of general "pedagogical" value:

1. I substituted the following code in your form's Command1 event handler, and added the IsProcessRunning code above:
Code:
Private Sub Command1_Click()
    Dim hProcess As Long
    hProcess = Shell("c:\windows\notepad.exe", vbNormalFocus)
    Do While IsProcessRunning(hProcess)
        DoEvents
    Loop
    MsgBox "I only get here when launched program finishes" & vbCrLf & "thus this must be a ShellAndWait ..."
End Sub
This appears on the face of it to behave similarly to yours, although when you run multiple instances, the msgboxes run in a different sequence. Why do you prefer the code you posted? (By the way, after reading up on WaitForInputIdle, it would seem that using it in "my" code would be an improvement, so that might be one reason.)

2. I'm not sure I understand the cb property in the STARTUPINFO type. I assume that the function plugs string values into the string properties, so does it take the starting length, initialize the memory to hold it, and then append bytes to the end as needed to accommodate the strings?

3. I was expecting that your code would fire the msgbox each time I closed an instance of notepad. However, if I open multiple instances, the msgboxes wait to fire until all of them are closed. It would seem that WaitForSingleObject has something to do with this, but I don't quite gather why. Can you explain this behavior?

4. Can you give an overview of circumstances wherein your "increasingly complex versions" would be necessary?

TIA

Bob
 
>This appears on the face of it to behave similarly to yours

Here's one difference: check performance of both versions in Task Manager ...

>Why do you prefer the code you posted?

I hadn't seen your post when I posted, so it wasn't posted as a preference to that at all. However, the code I posted is the basis of some extensions not (easily) possible with your solution. For example, we can pass parameters to the function to populate our STARTUPINFO variable, which means we can choose where to display our application, what size it's window should be, and all the various windows styles that Shell also supports.

>the cb property in the STARTUPINFO

Microsoft were smart enough to realise that a fair number of their structures might change in size over the years as the API developed (service packs adding features and new operating systems for example). So they provided a member of the structure that tells the OS how big the structure actually is (remember we only pass the pointer to the structure to the API, so there is no other way fore it to know how big the structure being pointed to actually is)

>append bytes to the end as needed to accommodate the strings

Variable length strings are held as pointers in UDTs

>However, if I open multiple instances, the msgboxes wait to fire until all of them are closed

Well, not really. This isn't production code as such, and as it stands isn't designed to be invoked multiple times - and is a great example of the dangers of DoEvents allowing reentrancy in a single-threaded app...

Change the Click_ event to
Code:
[blue]

Private Sub Command1_Click()
    Static ClickCount As Long
    ClickCount = ClickCount + 1
    ExecCmd "c:\windows\notepad.exe"
    MsgBox "Click " & ClickCount & vbCrLf & "I only get here when launched program finishes" & vbCrLf & "thus this must be a ShellAndWait ..."
    ClickCount = ClickCount - 1
End Sub[/blue]

Then try closing any opened notepads in different orders to get a better picture of what is actually happening.

>"increasingly complex versions"

Well, for example, I have a version that doesn't require DoEvents because I roll my own version that uses MsgWaitForMultipleObjects (which I've illustrated in this forum in the past, so a keyword search should find), which gives me the ability to pick and choose exactly how I want to respond to messages that appear in the queue, whether for the application or globally
 
>can't find the original thread:
Here it is... thread222-1010889

>check performance of both versions in Task Manager ...
Here I would like to clarify a couple of points.

1. The IsProcessRunning function was not originally written for use in a loop (see above-mentioned thread).

2. It is not the IsProcessRunning function which causes performance blow, but the call to the DoEvents function without the presence of a wait or timeout function.

If you put a [tt]Sleep 10[/tt] below the [tt]DoEvents[/tt] call in the loop, it will create the same relaxing effect as the 10ms timeout does in WaitForSingleObject call, reducing the CPU usage to normal levels.
 
>was not originally written for use in a loop

I know. My comments were directed towards Bob's implementation, rather than about IsProcessRunning itself.

>Sleep 10

Indeed

 
When all is said and done you're almost as well off to create a UserControl to do this.

The UserControl can have an embedded Timer control to drive the process of detecting when the child process completes. If you need to go further and pipe data to/from the child this same Timer can drive the Write/Read process. You can build the UserControl in such a way as to have an event model very similar to that of the Winsock control, and not only pipe I/O but retrieve the process exitcode, kill the process when desired, send Ctrl-C or Ctrl-Break events, etc.

You'll want to buffer (or discard) incoming data from the child process to avoid the pipes (StdOut and StdErr) blocking of course.

Using a Timer also avoids the penalty of Sleep() calls, allowing your program to "stay awake" more or go idle awaiting an incoming message as needed.

The downside of course is that UserControls aren't useful unless you have a form. Then again the majority of VB programs do have a form, if only a hidden one.

A simple control array allows multiple processes to be controlled.

Think of a UserControl as a class bound to a container, with clean hooks into the form's event processing mechanism. They don't have to be GUI elements or even visible at all at runtime.
 
Very interesting thread. strongm and Hypetia, thanks for your explanations.

Bob
 
Status
Not open for further replies.

Part and Inventory Search

Sponsor

Back
Top