Wait until several processes have ended

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Message
Author
dbenham
Expert
Posts: 2461
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: Wait until several processes have ended

#16 Post by dbenham » 27 Jul 2012 23:37

Edit - I tweaked the code a bit, and documented another advantage of this technique

I developed a new method to detect when processes have ended while answering this Stack Overflow question That question deals with capturing output of multiple console apps run in parallel and printing the output in the master window. I use the redirected output as a lock file. When the lock is released, I know the program has ended. I've documented in detail how it works at the link.

Well it works just as well with a non-console app. :D I simply execute the program through CMD and redirect an unused file handle to create the lock file. There are 3 things I really like about this technique:

1) The called application does not require any modification. It is totally oblivious that the master script is waiting for it to finish.

2) I can run multiple instances of the master script, and each copy is able to keep track of which processes it owns because each master instance gets a unique lock file name.

3) It works properly even if one of the started applications crashes or is killed. The lock is released the moment the application dies.

4) Each process can have its own callback method to perform cleanup that is called once the process ends.

I tested the following script on both XP and Vista. The script launches calc.exe and notepad.exe and does not continue until both are terminated. I use WMIC to incorporate a timestamp into the lock filename. But WMIC is not always available on XP, so I fallback to just using a random number if WMIC is unavailable. It is unlikely to get a name collision, but it could happen with just a random number.

Code: Select all

@echo off
setlocal

::Get a unique base lock name for this particular instantiation.
::Incorporate a timestamp from WMIC if possible, but don't fail if
::WMIC not available. Also incorporate a random number.
  set "lock="
  for /f "skip=1 delims=-+ " %%T in ('2^>nul wmic os get localdatetime') do (
    set "lock=%%T"
    goto :break
  )
  :break
  set "lock=%temp%\startLock%lock%_%random%_"

:: Start 2 applications asynchronously, and establish callback routines.
:: I've set up 2 different callback methods for accomplishing the same thing.
  start /b "" cmd /c 9^>"%lock%1" calc.exe
  set "cleanup1=echo calc.exe ended"
  set "endFlag1="

  start /b "" cmd /c 9^>"%lock%2" notepad.exe
  set "cleanup2=:cleanup notepad.exe"
  set "endFlag2="

  set /a "startCount=2, endCount=0"

:loopUntilDone - poll the status of the lock files to detect when finished.
::               Call the callback method as each application finishes.
  >nul 2>nul ping -n 2 ::1
  for /l %%N in (1 1 %startCount%) do (
    if not defined endFlag%%N if exist "%lock%%%N" (
      if defined cleanup%%N call %%cleanup%%N%%
      set /a "endCount+=1, endFlag%%N=1"
    ) 9>"%lock%%%N"
  ) 2>nul
  if %endCount% neq %startcount% goto :loopUntilDone

:: We have reached the end!
  2>nul del "%lock%*"
  echo That's all folks!
  exit /b


:cleanup
  echo %1 ended
  exit /b


Dave Benham
Last edited by dbenham on 28 Jul 2012 06:45, edited 2 times in total.

Ed Dyreen
Expert
Posts: 1569
Joined: 16 May 2011 08:21
Location: Flanders(Belgium)
Contact:

Re: Wait until several processes have ended

#17 Post by Ed Dyreen » 28 Jul 2012 00:17

'
Remember this related discussion Howto avoid parallel I/O conflicts, setSerial ?
I wasn't too happy about the fact that the stream would be in use but you solve that in an elegant manner.


Thanks for sharing :D +

dbenham
Expert
Posts: 2461
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: Wait until several processes have ended

#18 Post by dbenham » 28 Jul 2012 22:51

I've encapsalated much of the code into generic functions that should make it easier to launch a series of parallel processes and wait for all of them to finish.

I've also developed a demo that shows how the functions can be used. It launches a combination of a non-console GUI process, an interactive console process in a new window, and non-interactive console processes with output displayed in the main window.

The big news is I show how to establish dependencies between parallel processes. The example has the dependent process waiting for a single parent process to finish before launching. But the entire master batch script waits for all processes to finish. There is no reason why a master cannot launch another master, and then have a dependent process wait for the child master to finish :)

The demo is totally silly - it launches useless commands just to show how the functions work. But a real world script would typically be launching complex scripts.

Code: Select all

@echo off
setlocal enableDelayedExpansion

:: Initialize parallel processing
  call :launchInitialize

:: Start multiple parallel asynchronous processes, with a callback cleanup
:: method for each one that gets executed when the process terminates.
:: Both the process command and the cleanup method MUST be stored in
:: environment variables whose names are passed to the launch routines.
:: The use of variables enables the commands to be complex with spaces,
:: pipes, redirection, and quotes as needed.

  :: Demonstrate launching a non-console process that has a dependent console
  :: process that gets launched once this one terminates.
  set "cmd=calc.exe"
  set "clean=:cleanupLaunch calc.exe "help shift""
  call :launchIgnoreOutput cmd clean

  :: Demonstrate launching a complex console command that uses a pipe.
  :: Show the output in the main console window.
  set "cmd=(systeminfo|findstr /b OS)"
  set "clean=:cleanupShowOutput systeminfo
  call :launchShowOutput cmd clean

  :: Demonstrate launching a complex console command that generates an error
  :: message. Show both stdout and stderr in the main console window.
  :: The complex command uses both concatenated commands and redirection.
  set "cmd=(dir :DoesNotExist & ping /n 5 ::1 2>nul 1>nul)"
  set "clean=:cleanupShowOutput dir"
  call :launchShowOutput cmd clean

  :: Demonstrate launching an interactive console command that has a
  :: dependent console process that gets launched once this one terminates.
  :: This silly example just asks the user to press a key to continue.
  set "cmd=pause"
  set "clean=:cleanupLaunch pause "ping /n 5 ::1""
  call :launchConsole cmd clean

:: Wait for the launched processes to terminate
  call :launchWait
::set launchClean

:: We have reached the end!
  echo(
  echo ========================================================================
  echo That's all folks!
exit /b


:: *****************************************************************************
:: The following callback cleanup routines should be customized to meet your
:: needs. Their purpose is to peform cleanup operations that must be executed
:: once a launched process terminates. The routines below print output to the
:: master screen and launch dependent processes. But there really is no limit
:: to the complexity of what you might do. You may create as many cleanup
:: routines as are needed.

:cleanupLaunch  endingApp  launchApp
  echo(
  echo ------------------------------------------------------------------------
  ::use FOR to gain access to the :launchWait FOR variables
  for %%. in (1) do echo Process %%N (%~1) has ended
  echo Now launching: %~2
  set cmd=%~2
  set "cleanup=:cleanupShowOutput %2
  call :launchShowOutput cmd cleanup
  exit /b

:cleanupShowOutput  endingApp
  echo(
  echo ------------------------------------------------------------------------
  ::use FOR to gain access to the :launchWait FOR variables
  for %%. in (1) do (
    echo process %%N: (%~1^) results:
    echo(
    type "%launchLock%%%N"
  )
  exit /b

:: *****************************************************************************
:: The following routines are static generic routines to manage the process
:: of launching multiple processes in parallel and waiting for them to finish.
:: Each launched process can have a callback method specified that gets executed
:: when the process terminates.
::
:: Each of the 3 routines that actually launch a process takes one required
:: argument and one optional argument.
::
::   cmdVar - The name of a variable that contains a string specifying the
::            command to be executed. It can be arbitrarilly complex. However,
::            it cannot call a label within the master script.
::
::   [cleanupVar] - The name of a variable that contains a string specifying a
::                  batch file, :label, or command that gets called once
::                  the launched process terminates. The called method can
::                  include arguments of arbitrary complexity.
::
:: Because variables are used instead of passing string literals, the command
:: strings can include spaces, quotes, redirection, pipes, etc. as needed.

:launchInitialize
  :: Get a unique base lock name for this particular instantiation.
  :: Incorporate a timestamp from WMIC if possible, but don't fail if
  :: WMIC not available. Also incorporate a random number.
    set "launchLock="
    for /f "skip=1 delims=-+ " %%T in ('2^>nul wmic os get localdatetime') do (
      set "launchLock=%%T"
      goto :break
    )
    :break
    set "launchLock=%temp%\launchLock%launchLock%_%random%_"
  :: Initialize the counters
    set /a "launchStartCount=0, launchEndCount=0"
  :: Clear any existing end flags
    for /f "delims==" %%A in ('2^>nul set launchEndFlag') do set "%%A="
exit /b

:launchConsole  cmdVar  [cleanupVar]
::  This launches a console application, batch file, script, or command in a
::  new console window. Both stdout and stderr are left alone so the user can
::  see the output and interact with the console as needed. An unused file
::  handle is used for the lock file.
  set /a launchStartCount+=1
  start "" cmd /c 9^>"%launchLock%%launchStartCount%" !%~1!
  set "launchCleanup%launchStartCount%=!%~2!"
exit /b

:launchShowOutput  cmdVar  [cleanupVar]
::  This should be a console app for which you want to see the outut
::  displayed on the master screen, but you want to make sure the output
::  is not interleaved with the output of other processes. The process must
::  not require any user interaction. Both stdout and stderr are captured
::  in the lock file, so it is safe to run the  process within the master
::  console using the /B option. The cleanup callback method should TYPE
::  the output (the lock file) to the screen.
  set /a launchStartCount+=1
  start /b "" cmd /c 1^>"%launchLock%%launchStartCount%" 2^>^&1 !%~1!
  set "launchCleanup%launchStartCount%=!%~2!"
exit /b

:launchIgnoreOutput  cmdVar  [cleanupVar]
::  This could launch a non-console application, perhaps with a GUI, that may
::  or may not require user interaction. Or it could launch a non-interactive
::  console application, batch file, script, or command for which you don't
::  need to see the output on the screen. Both stdout and stderr are redirected
::  to nul, so it is safe to run the process within the master console using
::  the /B option. An unused file handle is used for the lock file.
  set /a launchStartCount+=1
  start /b "" cmd /c 9^>"%launchLock%%launchStartCount%" 1^>nul 2^>nul !%~1!
  set "launchCleanup%launchStartCount%=!%~2!"
exit /b

:launchWait - Wait for all launched processes to finish before returning.
::            Poll each lock file status in a loop to detect when finished.
::            Incorporate a ~1 sec delay so the CPU is not inundated.
::            Use append mode to test the lock so output is not clobbered!
::            Call the callback method as each application finishes.
::            Delete all lock files once all processes have finished.
  >nul 2>nul ping -n 2 ::1
  for /l %%N in (1 1 %launchStartCount%) do (
    if not defined launchEndFlag%%N if exist "%launchLock%%%N" (
      if defined launchCleanup%%N call %%launchCleanup%%N%%
      set /a "launchEndCount+=1, launchEndFlag%%N=1"
    ) 9>>"%launchLock%%%N"
  ) 2>nul
  if %launchEndCount% neq %launchStartCount% goto :launchWait
  2>nul del "%launchLock%*"
exit /b


Here is what the output might look like (the order of events will vary depending on when the interactive processes are closed).

Code: Select all

------------------------------------------------------------------------
Process 4 (pause) has ended
Now launching: ping /n 5 ::1

------------------------------------------------------------------------
process 3: (dir) results:

 Volume in drive C is OS
 Volume Serial Number is EE2C-5A11

 Directory of C:\Users\Public\utils

File Not Found

------------------------------------------------------------------------
Process 1 (calc.exe) has ended
Now launching: help shift

------------------------------------------------------------------------
process 5: (ping /n 5 ::1) results:


Pinging ::1 from ::1 with 32 bytes of data:
Reply from ::1: time<1ms
Reply from ::1: time<1ms
Reply from ::1: time<1ms
Reply from ::1: time<1ms
Reply from ::1: time<1ms

Ping statistics for ::1:
    Packets: Sent = 5, Received = 5, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 0ms, Maximum = 0ms, Average = 0ms

------------------------------------------------------------------------
process 6: (help shift) results:

Changes the position of replaceable parameters in a batch file.

SHIFT [/n]

If Command Extensions are enabled the SHIFT command supports
the /n switch which tells the command to start shifting at the
nth argument, where n may be between zero and eight.  For example:

    SHIFT /2

would shift %3 to %2, %4 to %3, etc. and leave %0 and %1 unaffected.

------------------------------------------------------------------------
process 2: (systeminfo) results:

OS Name:                   Microsoftr Windows VistaT Home Premium
OS Version:                6.0.6002 Service Pack 2 Build 6002
OS Manufacturer:           Microsoft Corporation
OS Configuration:          Standalone Workstation
OS Build Type:             Multiprocessor Free

========================================================================
That's all folks



Dave Benham

Post Reply