Bricktop
Bricktop

Reputation: 153

How to avoid multiple instances of a script while allowing to start it up again without changing title

I have this script that should only run in a single instance. my method for making sure this doesn't happen is

tasklist /fi "imagename eq cmd.exe" /v | find "lootbot" && echo already running && pause && exit /b
title lootbot

and later does a graceful exit so the script can be restarted...

:action8
cd /d "%~dp0"
title command prompt
echo type launch to return to lootbot
exit /b

the problem is that whenever the script crashes because of a bug, it won't allow me to start it back up without first manually re-setting the title to something else.

not a big problem, granted, but I'm building this for other people to use so I'm looking for an easy-to-use solutions here. I'm thinking if there is a way to determine if the current command prompt is titled lootbot then this problem could be automatically avoided. this is the whole script just in case.

@echo off
tasklist /fi "imagename eq cmd.exe" /v | find "lootbot" && echo lootbot already running, press a key to exit && pause && exit /b
title lootbot
:: enable save files location (persistant memory)
set "saves=%appdata%\lootbot"
if not exist "%saves%" mkdir "%saves%"
:: create/load saves, echo first
if exist "%saves%\echoes\launch" echo on
if not exist "%saves%\loopdelay" echo 300>"%saves%\loopdelay"
if not exist "%saves%\promptdelay" echo 60>"%saves%\promptdelay"
set /p loopdelay=<"%saves%\loopdelay" & set /p promptdelay=<"%saves%\promptdelay"
:: advanced prep
chcp 65001 >nul
set "path=%path%;%~dp0;c:\program files\filebot"
wmic process where name="cmd.exe" call setpriority "idle" >nul
:: additional scripts (all-in-one launcher)
start vpn.cmd

:: add "going legit" to vpn or somewhere (meaning close torrents and vpn, reactivate vpn if torrents appear, or close torrents)
:: add skippable delay here to auto-avoid possible mishandling ones being checked in qbittorrent
cls
:menu
@colorx -c 08
@if exist "%saves%\echoes\launch" echo on
@cd /d "%~dp0"
@set "msg=lootbot ready (%time:~0,2%:%time:~3,2%) wait %loopdelay%s or enter command"
@set "msg=%msg:( =(%" & rem this removes the blank space from early hours
@choice /t %loopdelay% /c rcplsjdq /n /d r /m "%msg% [R]un [C]lear [P]ause [L]ist [S]etup [J]obs [D]onate [Q]uit: "
:: add selective choices here, based on what scripts are available. might need those scripts to be names or placed different to autodetect
@goto action%errorlevel%
@goto menu

:action1
:routine
:: run now/default infinite loop
call report %~n0 import
call report %~n0 direct
:: idea: add small amount of random files to scan?
goto menu

:action2
:clear
cls
goto menu

:action3
pause
goto menu

:action4
:playlist
call report %~n0 player
goto menu

:action5
:settings
call report %~n0 option
goto menu

:action6
:caring
call report %~n0 caring
goto menu

:action7
:donate
call report %~n0 donate
goto menu

:action8
:exit
:: quit back to command prompt
cd /d "%~dp0"
title command prompt
echo use command launch to return to lootbot, bye!
exit /b

Upvotes: 1

Views: 1462

Answers (1)

dbenham
dbenham

Reputation: 130849

This is easily done by strategically using redirection with CALL to establish a lock file. Only one instance can have write access to the lock file. Attempts by a second instance will fail. The first instance will release the lock as soon as it quits, regardless how it terminates.

I use multiple stages of redirection to hide unwanted error messages, yet the main routine has normal stdout and stderr. I add %* to pass the original arguments to the main. The only "odd" situation in main is %0 is :main instead of the executing script. But %~f0 can be used to get the full path to the executing script.

test.bat

@echo off
9>&2 2>nul (call :lockAndRestoreStdErr %* 8>"%~f0.lock") && (
  del "%~f0.lock"
) || (
  echo Only one instance allowed - "%~f0" is already running >&2
)
exit /b

:lockAndRestoreStdErr
call :main %* 2>&9
exit /b 0

:main
echo        %%0 = %0
echo    "%%~f0" = "%~f0"
echo Arguments = %*
pause
exit /b

Example Instance 1 output

C:\test>test arg1 arg2
       %0 = :main
   "%~f0" = "C:\test\test.bat"
Arguments = arg1 arg2
Press any key to continue . . .

Example Instance 2 output wile instance 1 is still running

C:\test>test arg1 arg2
Only one instance allowed - "C:\test\test.bat" is already running

C:\test>

Instance 1 will terminate once a key is pressed, and then instance 2 can run the script.

EDIT

Actually you don't need a separate lock file. You can use the batch script itself as the lock file. Just remember to use >> instead of >, else you will wipe out the script!

@echo off
9>&2 2>nul (call :lockAndRestoreStdErr %* 8>>"%~f0") || (
  echo Only one instance allowed - "%~f0" is already running >&2
)
exit /b

:lockAndRestoreStdErr
call :main %* 2>&9
exit /b 0

:main
echo        %%0 = %0
echo    "%%~f0" = "%~f0"
echo Arguments = %*
pause
exit /b

If users do not have write access to the script, then neither of the above will work. In that case you should put the lock file in the %temp% folder.

@echo off
9>&2 2>nul (call :lockAndRestoreStdErr %* 8>"%temp%\%~nx0.lock") && (
  del "%temp%\%~nx0.lock"
) || (
  echo Only one instance allowed - "%~f0" is already running >&2
)
exit /b

:lockAndRestoreStdErr
call :main %* 2>&9
exit /b 0

:main
echo        %%0 = %0
echo    "%%~f0" = "%~f0"
echo Arguments = %*
pause
exit /b

EDIT - Answer to question in comment

To put the above technique into your script, simply remove your tasklist line, and replace it with my technique, such that the top of your script looks like:

@echo off
9>&2 2>nul (call :lockAndRestoreStdErr %* 8>>"%~f0") || (
  echo Only one instance allowed - "%~f0" is already running >&2
  pause
)
exit /b

:lockAndRestoreStdErr
call :main %* 2>&9
exit /b 0

:main
title lootbot
:: enable save files location (persistant memory)
etc...

Upvotes: 5

Related Questions