Batch file multitasking issues sharing variables with parent - windows

I'm trying to mass encode video files in x265 and wanted to come up with a batch file that could automate the process. In order to expedite things, I found that having 3 instances of ffmpeg called with 2 threads resulted in ideal encoding times, however I've tried all day yesterday to come up with a way to get a batch file that will call 3 instances and then call new ones when they complete. So far, this is where I am:
PARENT BATCH
#echo off
Setlocal EnableDelayedExpansion
SET /A COUNT=0
for %%a in (*.mkv) do (
CALL :CHECK
SET /A COUNT+=1
START CALL "child.bat" "%%a"
)
EXIT /B 0
:CHECK
IF !COUNT! EQU 3 (
TIMEOUT /T 5
GOTO :CHECK
)
EXIT /B 0
CHILD BATCH
ffmpeg <COMMAND LINE ARGS>
SET /A COUNT-=1
EXIT /B 0
I have two problems. 1) The COUNT variable isn't being updated in the parent process and it never spawns new instances when the child processes finish. 2) The child process doesn't cleanly exit. It leaves a separate cmd.exe window open with a DOS prompt.
Any ideas?
Edit: Replaced nested GOTO to prevent FOR loop breakage
Workaround below
Setlocal EnableDelayedExpansion
SET /A COUNT=0
for %%a in (*.mkv) do (
IF !COUNT! EQU 3 CALL :CHECK
SET /A COUNT+=1
START CALL "child.bat" "%%a"
)
EXIT /B 0
:CHECK
IF EXIST DONE.TXT (
SET /A COUNT-=1
DEL DONE.TXT
EXIT /B 0
) ELSE (
TIMEOUT /T 5
GOTO :CHECK
)
EXIT /B 0
CHILD BATCH
ffmpeg <COMMAND LINE ARGS>
:DONE
IF EXIST DONE.TXT (
TIMEOUT /T 1
GOTO :DONE
)
echo 1 >> DONE.TXT
EXIT 0

Regarding your stated problems:
1) A child process cannot modify environment variables in the parent process. You will need a different mechanism to detect when the child has terminated. Also, as Squashman states in his comment, a GOTO within a loop will break (terminate) the loop, which is why no new child processes are launched after the first 3.
2) Your child window does not terminate because you use EXIT /B. Use EXIT instead and the window will close.
You have a long way to go before you have a working solution ;-)
Perhaps the biggest hurdle is detecting when a child process terminates.
I know of 3 strategies:
1) Use TASKLIST coupled with FIND /C to count the number of ffmpeg processes that are currently running. This is perhaps the simplest solution, but it cannot differentiate between processes that your script launches vs processes that may have been launched by some other mechanism.
2) Use a file as a signal. Create an empty file for each process, and then when the process finishes, have it delete the file. Your main script can monitor which processes are active by looking for the files. This is also simple, but it does not behave well if one of your processes crashes before it can delete the file. That leaves your system in an unhealthy state.
3) My favorite is to use lock files. Each child process locks a file via redirection, and when the process terminates (crash, normal exit, it doesn't matter how), then the lock is released. The main process can attempt to lock the same files. It knows the child has terminated if the lock is successful, else the child is still running. This strategy is the most complicated, and it uses arcane syntax, but I find it highly effective.
I have already implemented a solution at Parallel execution of shell processes that uses option 3). Below is an adaptation/simplification of that code for your situation.
I launch each child process in the parent window using START /B, and I redirect all output to the lock file. When finished, I type the output file so you can see what happened. I also list the start and stop times for each child process.
You just need to adjust the 3 top environment variables to suit your needs. The remainder should be good to go. However, the code as written will fail if any file names contain the ! character. This limitation can be removed with a bit more work.
There is extensive documentation within the script. The %= COMMENT =%syntax is one way of safely embedding comments within a loop without using REM.
#echo off
setlocal enableDelayedExpansion
:: Define the command that will be run to obtain the list of files to process
set listCmd=dir /b /a-d *.mkv
:: Define the command to run for each file, where "%%F" is an iterated file name from the list
:: something like YOUR_COMMAND -i "%%F"
set runCmd=ffmpeg [** command arguments go here **]
:: Define the maximum number of parallel processes to run.
set "maxProc=3"
::---------------------------------------------------------------------------------
:: The remainder of the code should remain constant
::
:: 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%\lock%lock%_%random%_"
:: Initialize the counters
set /a "startCount=0, endCount=0"
:: Clear any existing end flags
for /l %%N in (1 1 %maxProc%) do set "endProc%%N="
:: Launch the commands in a loop
set launch=1
for /f "tokens=* delims=:" %%F in ('%listCmd%') do (
if !startCount! lss %maxProc% (
set /a "startCount+=1, nextProc=startCount"
) else (
call :wait
)
set cmd!nextProc!=%runCmd%
echo -------------------------------------------------------------------------------
echo !time! - proc!nextProc!: starting %runCmd%
2>nul del %lock%!nextProc!
%= Redirect the lock handle to the lock file. The CMD process will =%
%= maintain an exclusive lock on the lock file until the process ends. =%
start /b "" cmd /c >"%lock%!nextProc!" 2^>^&1 %runCmd%
)
set "launch="
:wait
:: Wait for procs to finish in a loop
:: If still launching then return as soon as a proc ends
:: else wait for all procs to finish
:: redirect stderr to null to suppress any error message if redirection
:: within the loop fails.
for /l %%N in (1 1 %startCount%) do 2>nul (
%= Redirect an unused file handle to the lock file. If the process is =%
%= still running then redirection will fail and the IF body will not run =%
if not defined endProc%%N if exist "%lock%%%N" 9>>"%lock%%%N" (
%= Made it inside the IF body so the process must have finished =%
echo ===============================================================================
echo !time! - proc%%N: finished !cmd%%N!
type "%lock%%%N"
if defined launch (
set nextProc=%%N
exit /b
)
set /a "endCount+=1, endProc%%N=1"
)
)
if %endCount% lss %startCount% (
timeout /t 1 /nobreak >nul
goto :wait
)
2>nul del %lock%*
echo ===============================================================================
echo Thats all folks

Easy solution with a random window title (based on user2956477 idea):
set windowtitle=Worker_%random%%random%%random%
start /min cmd /c "title %windowtitle% & dosomething.bat file1"
start /min cmd /c "title %windowtitle% & dosomething.bat file2"
start /min cmd /c "title %windowtitle% & dosomethingelse.exe"
timeout 3 >nul
:LOOP
tasklist /V /FI "imagename eq cmd.exe" /FI "windowtitle eq %windowtitle%*" | findstr %windowtitle% >nul
if "%errorlevel%"=="0" timeout 1 >nul && goto LOOP

Related

Multiple brotli processes at the same time

I currently have to compress several thousand files (~40-80MB each) with brotli and get them ready for an s3 bucket.
From what i've researched so far, brotli can't multithread the compression so, brotli.exe uses ~10% of the cpu. How can I iterate through the files in a folder and spawn multiple (brotli).exe files to work at the same time (8-10 processes should fill the cpu)?
windows/powershell/vbs, I can try any suggestions
At the moment, I'm running this batch
for /R %%f in (*.) do (
"brotli" -Z "--output=E:\output\brotli\%%~nf" "%%f"
)
#ECHO OFF
SETLOCAL
:: set limit to #jobs
SET /a limit=8
:: establish a subdirectory in %temp%
SET "control=%temp%\brotlicontrol"
MD "%control%" 2>NUL
:: Dummy for testing
for %%f IN (fred anna george bill betty carl celia daphne john kelly ian zoe brian
tracey susan colin jane selina valerie david stephen) DO (
rem for /R %%f in (*.) do (
CALL :wait
START /min "brotli %%~nf" q75403766_2 "%%f"
)
GOTO :EOF
:wait
SET /a running=0
FOR /f %%y IN ('DIR /a-d /b "%control%\*.flg" 2^>nul ^|FIND /c ".flg" ') DO SET /a running=%%y
IF %running% geq %limit% timeout /t 1 >nul&GOTO wait
GOTO :eof
Here's a main batch which starts a subsidiary batch
#echo off
setlocal
ECHO.>"%control%\%~n1.flg"
REM "brotli" -Z "--output=E:\output\brotli\%~n1" %1
:: Dummy - variable timeout 5-20 seconds
SET /a exectime=(%RANDOM% %% 16) + 5
timeout /t %exectime% >nul
del "%control%\%~n1.flg"
EXIT
I had %%f iterate through a list of names for testing. All you need to do is to remove that test code and use your original code which I remmed out to process your list of files.
The process calls the :wait routine, which counts the .flg files in the temporary directory, and sets running to that value.
If the number running is greater than or equal to (geq) the limit established in the initialisation, wait 1 second and try again, otherwise the :wait routine terminates and the subsidiary batch q75403766_2 is started /min minimised and with the name brotli nameoffile. It's important that the first quoted parameter to start exists as it's used as the title of the started process. You could use "" if you want (for no title) but you should not omit this title string.
The sub-process started (q75403766_2) first creates a .flg file with the name of the file being processed in the control directory, then runs the brotli job (remmed out again) - I added a few lines to create a variable timeout to simulate the brotli process-time - and deletes the control file and exits.
The carets before the redirectors in the for loops tell cmd that the redirection is to be applied to the command being executed, not the for. 2>nul (+caret) says "redirect error messages (file not found) to nowhere (ie. discard them)".

Batch File to execute all .exe in a folder with count

I have quite a few exe files, I want to run them with a single batch file. As far as I understand, these two codes work for me;
for %%a in ("\*.exe") do start "" "%%\~fa"
for %%i in (\*.exe) do start "" /b "%%i"
But that cmd screen closes when all files are run. What I want is this: That cmd screen will not close when the process is finished and will show me the result (counting if possible), a code that can count how many of these .exe files work and how many fail.
So for example;
87 files blocked
13 files could not be blocked
Something like this? Is this possible?
Maybe you can get an inspiration from this batch below. It works through program exit code. It spawns all executable, wait for their completion, then count how much failed / succeeded.
It should also work with well-designed GUI program, not only command-line based ones.
It's a rough/basic answer, you may need to refine it according to your exact needs.
#echo off
setlocal enableextensions enabledelayedexpansion
REM Use marker files for getting results.
set OK_EXT=.SUCCESS
set FAIL_EXT=.FAILURE
REM Purge all possible marker files.
del /q *!OK_EXT! *!FAIL_EXT! > NUL 2>&1
set /a count=0
REM Parse all executables
for %%E in (*.exe) do (
echo Launching: %%~nxE
REM Create two marker files for each executable.
echo.>%%~nE!OK_EXT!
echo.>%%~nE!FAIL_EXT!
REM Start the executable, delete the WRONG marker.
REM I would have prefered to use "touch" to create the good one instead, but not standard on Windows.
start %comspec% /C "%%~nxE && ( del /q %%~nE!FAIL_EXT! ) || ( del /q %%~nE!OK_EXT! )"
set /a count +=1
)
REM Now, "count" contains the number of executables launched.
echo All processes launched.
echo.
:loop
echo Waiting for results...
set /a curr=0
REM Simply count the number of marker files. Must be equal to "count" when everything is finished.
for /F "usebackq tokens=*" %%C in (`dir /b *!OK_EXT! *!FAIL_EXT!`) do (
set /A curr+=1
)
if !curr! GTR !count! (
set /a curr-=!count!
echo Still !curr! processes running...
timeout /t 2
goto :loop
)
echo All results found.
echo.
echo Parsing results...
set /a ok_exe=0
set ok_exe_list=
set /a fail_exe=0
set fail_exe_list=
REM Parse all marker files.
for /F "usebackq tokens=*" %%C in (`dir /b *!OK_EXT! *!FAIL_EXT!`) do (
REM And set counters + list according to the marker file type (OK or FAILED).
if /I "%%~xC"=="!OK_EXT!" (
set /A ok_exe+=1
set ok_exe_list=!ok_exe_list! %%~nC
) else (
set /A fail_exe+=1
set fail_exe_list=!fail_exe_list! %%~nC
)
)
REM Simple display.
echo Programs without error: !ok_exe!/!count!
echo !ok_exe_list!
echo.
echo Programs with error: !fail_exe!/!count!
echo !fail_exe_list!
echo.
goto :eof

Checking for File and not Running command till file doesn't exist in batch

I'm feeling a bit constrained by batch's limitations and unsure the best way to code some logic. I have a hybrid Batch/VBScript for printing and decided the best way to confirm the VBScript had finished was by utilizing an external file as a placeholder.
The logic is like this:
Batch Script --> Create tmpfile --> Run VBScript
VBScript --> Print Job --> Delete File
That part works, but in my "call script" I have an array of files that I use a FOR LOOP to iterate over and print, but I can't figure out how to code the logic WHILE FILE EXISTS DON'T RUN NEXT PRINT JOB. I have figured out how to "loop" until the file doesn't exist, but this uses GoTo statements and they don't work inside FOR LOOPS. I tried using "EXIT /B" which I thought just exits a function, but it just closed the main script all together.
Here is my "call script" which needs some help~
::This is a placeholder file to check if VBScript is running
SET File="%TEMP%\jobrunning.txt"
del /F /Q %File%
::Print Each File in Array
for /l %%n in (0,1,%arrsize%) do (
SET FILEPATH="FILEPATH=!file[%%n]!
GoTo file_exists
)
::Is Job Still Running?
:file_exists
IF EXIST %File% (
GoTo :file_exists
) ELSE (
ECHO %PFUNC% %FILEPATH%
%PFUNC% %FILEPATH%
EXIT /B
)
The issue was using GoTo w/ EXIT /B. GoTo just jumps to a LINELABEL, and EXIT /B exits PROCESS IE: The Script. CALL opens a SUBPROCESS and EXIT /B returns to the main PROCESS allow further code execution. (It's the same as calling an external script and closing it).
This is what I ended up using to test and just deleted the file manually after each iteration of the array.
#ECHO OFF
setlocal enabledelayedexpansion
::This is a placeholder file to check if VBScript is running
SET File="%TEMP%\jobrunning"
del /F /Q %File%
::Set arrsize -1
set arrsize=4
set arrayline[0]=1A
set arrayline[1]=2A
set arrayline[2]=3A
set arrayline[3]=4A
set arrayline[4]=5A
::read it using a FOR /L statement
for /l %%n in (0,1,%arrsize%) do (
copy /y NUL %TEMP%\jobrunning.txt >NUL
TIMEOUT 2
CALL :file_exists
echo !arrayline[%%n]!
)
PAUSE
GoTo :EOF
:Is Job Still Running?
:file_exists
IF EXIST %File% (
GoTo :file_exists
) ELSE (
EXIT /B
)

Windows batch file looping Wait animation during processing command

I have created the following batch file that makes Wait "| / -- \" animation. I want to use it during processing mysql restore database command mysql -u %DBuser% -p %DB% < "%thefile%" where thefile is the sql dump file path
#Echo OFF
setlocal EnableDelayedExpansion
for /f %%a in ('copy /Z "%~f0" nul') do set "CR=%%a"
SET p=-1
set num=1
set "st[1]=| "
set "st[2]=/ "
set "st[3]=--"
set "st[4]=\ "
if /i %p% lss 0 (
set p=2
call :LOOP
call :DoSomeThing
)
:LOOP
if /i %num% lss 4 (
set /a num=num+1
) else (
set num=1
)
<nul set /P "=Wait !st[%num%]!!CR!"
TIMEOUT /T 1 >NUL
GOTO :LOOP
:DoSomeThing
TIMEOUT /T 10 >NUL
echo Doing...
Here, :DoSomeThing is for testing purposes and It should be replaced or include the mysql command. I get the problem that :LOOP works for ever and there is no call to :DoSomeThing
I tried to call :DoSomeThing before call :LOOP but the LOOP started after DoSomeThing is finished so it becomes useless! Is there any way to make the DoSomeThing or the MySQL command works in the background while the animation wait loop works too?
EDIT: Some explanations added
In order to fulfill your request it is necessary to execute two threads simultanously, so one thread execute the mysql command and the other thread execute the looping wait animation. Both threads can be synchronized using a flag file that is created before mysql execution starts and is deleted after it ends, so the wait animation loops until the flag file is deleted.
The way to create a new thread is via start command, that may run a second Batch file that execute the mysql command and delete the flag file. However, in order to keep all the code in the same place, the start command may run the same Batch file (represented by "%~F0" in the code below). The key that allows this trick to work is a special parameter that indicate if the Batch file was re-executed from inside itself, so in this case the code just goto the section that execute the mysql command and delete the flag file.
#Echo OFF
rem If the Batch file was re-executed with the special parameter (second thread)
rem go to the section that execute the mysql command
if "%~1" equ ":DoSomething" goto %1
setlocal EnableDelayedExpansion
for /f %%a in ('copy /Z "%~f0" nul') do set "CR=%%a"
set num=0
set "st[0]=| "
set "st[1]=/ "
set "st[2]=--"
set "st[3]=\ "
rem Do here anything you want before the mysql command...
echo Doing something for 10 seconds...
rem Create the flag file
echo X > DoingSomething
rem Re-start this Batch file with the special parameter
start "" /B "%~F0" :DoSomething
rem Simultaneously execute the waiting animation
call :LOOP
rem Do here anything you want after the mysql command...
rem ... and terminate
goto :EOF
:LOOP
set /a num=(num+1) %% 4
<nul set /P "=Wait !st[%num%]!!CR!"
TIMEOUT /T 1 >NUL
IF EXIST DoingSomething GOTO :LOOP
echo Ending loop
goto :EOF
:DoSomeThing
rem Place here the mysql command
TIMEOUT /T 10 >NUL
rem Delete the flag file
del DoingSomething
rem And terminate the second thread
goto :EOF

Limiting the number of spawned processes in batch script

I have a situation very similar to the one described in this question (but in batch, not shell). I made a simple batch script to iterate through the lines of a tile and download data from a server using a python script (the process itself is more complicated than just a simple download, it has to authenticate with an API and fetch several URLs).
The first version was as follows:
for /F "tokens=*" %%A in (client_name_list.txt) do python download_metadata.py "%%A"
The way it is it waits until each iteration is done to move on, so I updated it to the following:
for /F "tokens=*" %%A in (client_name_list.txt) do start cmd /C python download_metadata.py "%%A"
The second versions does what I want to but, as the file client_name_list.txt is about 30,000 lines long, a lot of command prompts start spawning and the computers freezes within seconds.
How do I limit the number of running instances of CMD (to, for example 10) and make the script wait until there is a "free CMD slot" to go the next line?
Adapted from my answer to "Parallel execution of shell processes". Follow the link to get an explanation.
#echo off
setlocal enableDelayedExpansion
:: Display the output of each process if the /O option is used
:: else ignore the output of each process
if /i "%~1" equ "/O" (
set "lockHandle=1"
set "showOutput=1"
) else (
set "lockHandle=1^>nul 9"
set "showOutput="
)
:: Define the maximum number of parallel processes to run.
set "maxProc=10"
:: 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%\lock%lock%_%random%_"
:: Initialize the counters
set /a "startCount=0, endCount=0"
:: Clear any existing end flags
for /l %%N in (1 1 %maxProc%) do set "endProc%%N="
:: Launch the commands in a loop
set launch=1
for /f "delims=" %%A in (client_name_list.txt) do (
if !startCount! lss %maxProc% (
set /a "startCount+=1, nextProc=startCount"
) else (
call :wait
)
set cmd!nextProc!=%%A
if defined showOutput echo -------------------------------------------------------------------------------
echo !time! - proc!nextProc!: starting %%A
2>nul del %lock%!nextProc!
%= Redirect the lock handle to the lock file. The CMD process will =%
%= maintain an exclusive lock on the lock file until the process ends. =%
start /b "" cmd /c %lockHandle%^>"%lock%!nextProc!" 2^>^&1 python download_metadata.py "%%A"
)
set "launch="
:wait
:: Wait for procs to finish in a loop
:: If still launching then return as soon as a proc ends
:: else wait for all procs to finish
:: redirect stderr to null to suppress any error message if redirection
:: within the loop fails.
for /l %%N in (1 1 %startCount%) do 2>nul (
%= Redirect an unused file handle to the lock file. If the process is =%
%= still running then redirection will fail and the IF body will not run =%
if not defined endProc%%N if exist "%lock%%%N" 9>>"%lock%%%N" (
%= Made it inside the IF body so the process must have finished =%
if defined showOutput echo ===============================================================================
echo !time! - proc%%N: finished !cmd%%N!
if defined showOutput type "%lock%%%N"
if defined launch (
set nextProc=%%N
exit /b
)
set /a "endCount+=1, endProc%%N=1"
)
)
if %endCount% lss %startCount% (
1>nul 2>nul ping /n 2 ::1
goto :wait
)
2>nul del %lock%*
if defined showOutput echo ===============================================================================
echo Done
In each iteration of your for loop you can count the number of CMD task open. If the value is lesser than the limit you start a new task else you wait until one slot is free.
#echo off
set $Limit=11
setlocal enabledelayedexpansion
for /F "tokens=*" %%A in (client_name_list.txt) do (call:wait %%A)
exit/b
:wait
set "$cmd="
for /f %%a in ('tasklist ^| findstr /i "cmd"') do set /a $cmd+=1
if !$cmd! lss %$Limit% (
start cmd /C python download_metadata.py "%1"
goto:eof)
ping localhost -n 2 >nul
goto:wait

Resources