2

Hello I am very new in this and I was able to run one instance of a batch file.

However, another file is being created called something.lock but the file is not been deleted by itself when I stop the batch or close it. The new file create is the one that helps to have one instance running. Can the new file ".lock " be deleted after I close the script with the "X" or because an user ended correctly with going to label end:

The code that I have is

:init
set "started="
2>nul (
 9>"%~f0.lock" (
  set "started=1"
  call :start
 )
)
@if defined started (
    del "%~f0.lock" 
) else (
    cls
    ECHO                         Only one instance is allowed
    timeout /NOBREAK /T 3 >nul
   cls
)
exit /b
:start
cd /d %~dp0
cls
:initial
pause >nul
kboga
  • 43
  • 6
  • If the user clicks the "X" or forces exit another way (such as Ctrl+C), the execution simply stops and you will not be able to do clean up. – Jason Faulkner Jan 02 '15 at 19:21

2 Answers2

3

You are misapplying the lock file. You are simply checking to see if the file exists, which means you must guarantee that the file is deleted upon batch termination.

There is a much better way, which you have only partially implemented. Only one process can have the file open for write access. You just need to determine if the file is already locked by another process.

Once the process with the exclusive lock terminates, the lock will be released. This is true no matter how the script terminates - even if it was the result of Ctrl-C or window closure. The file might not be deleted, but the next time the script runs, the file won't be locked, so the script will proceed nicely.

In the code below I save the current definition of stderr to an unused file handle before I redirect sterr to nul. Within the inner block I redirect stderr back to the saved definition. In this way I prevent the error message if the file is already locked, but the CALLed :start routine will still print out error messages normally.

@echo off
:init
8>&2 2>nul ( 2>&8 9>"%~f0.lock" call :start ) || (
  cls
  ECHO                         Only one instance is allowed
  timeout /NOBREAK /T 3 >nul
  cls
)
del "%~f0.lock" 2>nul
exit /b

:start
cd /d %~dp0
cls
del asdfasdfasdf
:initial
pause >nul
dbenham
  • 127,446
  • 28
  • 251
  • 390
  • Such witchcraft is not natural. Have you sold your soul for these powers? +1. – rojo Jan 05 '15 at 01:12
  • 2
    @rojo - LOL! It helps to have a [thourough understanding of redirection mechanics](http://stackoverflow.com/a/9880156/1012053) (especially read the update at the bottom) so you can safely do things like [swap stderr and stdout](http://stackoverflow.com/a/12274145/1012053). Exclusive locks allow things like a [shared log file for multiple processes](http://stackoverflow.com/a/9344547/1012053), [serialized execution of a program across machines](http://stackoverflow.com/a/18005562/1012053), and [controlled parallel processing of a queue of tasks](http://stackoverflow.com/a/11715437/1012053). – dbenham Jan 05 '15 at 17:47
  • 1
    @rojo - Another use for redirection is [interprocess communication between batch files](http://www.dostips.com/forum/viewtopic.php?f=3&t=4741&start=60) that I used in my [SNAKE.BAT game](http://www.dostips.com/forum/viewtopic.php?f=3&t=4741). OK, sorry. I'm done showing off now ;) – dbenham Jan 05 '15 at 18:07
  • I've played that. It was impressive. :) As long as [we're showing off](http://stackapps.com/questions/5038/)... That's not nearly as impressive as `snake.bat`, but it still makes SE easier to use.`` – rojo Jan 05 '15 at 18:17
  • Just found your [batch implementation of `tee -a`](http://stackoverflow.com/a/21841567/1683264). Mind: blown. – rojo Jan 05 '15 at 21:28
0

The difficulty is that your batch thread itself won't have its own PID. There's no graceful way to tell whether your batch script is running or when it has terminated. And there's no way to wake the dead, to let the script have the last word when a user red X's or Ctrl+C's. When it's over, it's over.

There are a few ways you can do what you want to do. Try them all and see which you prefer. Use dbenham's solution. His is correct. The following efforts are left here as an exercise in futility, although Solution 4 seems to work very well. In the end, it's still just a hack; whereas dbenham's redirection sleight-of-hand provides a correct implementation of lock files the way lock files are supposed to work.

...

Solution 1

One simple way is to use powershell to minimize the current window, re-launch your script with start /wait, then after completion call powershell again to restore.

@echo off
setlocal

set "lock=%temp%\~%~n0.lock"

if "%~1" neq "wrapped" (

    if exist "%lock%" (
        echo Only one instance is allowed.
        timeout /nobreak /t 3 >NUL
        exit /b
    )

    rem :: create lock file
    >"%lock%" echo 1

    rem :: minimize this console
    powershell -windowstyle minimized -command ""

    rem :: relaunch self with "wrapped" argument and wait for completion
    start /wait "" cmd /c "%~f0" wrapped

    rem :: delete lock file
    del "%lock%"

    rem :: restore window
    powershell -windowstyle normal -command ""

    goto :EOF

)

:: Main script goes here.
:loop
cls
echo Simulating script execution...
ping -n 2 0.0.0.0 >NUL
goto loop

This should be enough for casual use and should account for any cause of the batch file's termination short of taskkill /im "cmd.exe" /f or a reboot or power outage.


Solution 2

If you need a more bulletproof solution, you can get the current console window's PID and intermittently test that it still exists. start /min a helper window to watch for its parent window to die, then delete the lock file. And as long as you're creating a watcher anyway, might as well let that watcher be the lock file.

Biggest drawback to this method is that it requires to end your main script with exit to destroy the console window, whether you want it destroyed or not. There's also a second or two pause while the script figures out its parent's PID.

(Save this with a .bat extension and run it as you would any other batch script.)

@if (@a==@b) @end   /* JScript multiline comment

:: begin batch portion

@echo off
setlocal

:: locker will be a batch script to act as a .lock file
set "locker=%temp%\~%~nx0"

:: If lock file already exists
if exist "%locker%" (
    tasklist /v | find "cleanup helper" >NUL && (
        echo Only one instance allowed.
        timeout /nobreak /t 3 >NUL
        exit /b
    )
)

:: get PID of current cmd console window
for /f "delims=" %%I in ('cscript /nologo /e:Jscript "%~f0"') do (
    set "PID=%%I"
)

:: Create and run lock bat.
>"%locker%" echo @echo off
>>"%locker%" echo setlocal
>>"%locker%" echo echo Waiting for parent script to finish...
>>"%locker%" echo :begin
>>"%locker%" echo ping -n 2 0.0.0.0^>NUL
>>"%locker%" echo tasklist /fi "PID eq %PID%" ^| find "%PID%" ^>NUL ^&^& ^(
>>"%locker%" echo   goto begin
>>"%locker%" echo ^) ^|^| ^(
>>"%locker%" echo   del /q "%locker%" ^&^& exit
>>"%locker%" echo ^)

:: Launch cleanup watcher to catch ^C
start /min "%~nx0 cleanup helper" "%locker%"

:: ==================
:: Rest of script
:: blah
:: blah
:: blah
:: ==================

:end
echo Press any key to close this window.
pause >NUL
exit

:: end batch portion / begin JScript
:: https://stackoverflow.com/a/27514649/1683264
:: */

var oShell = WSH.CreateObject('wscript.shell'),
    johnConnor = oShell.Exec('%comspec% /k @echo;');

// returns PID of the direct child of explorer.exe
function getTopPID(PID, child) {
    var proc = GetObject("winmgmts:Win32_Process=" + PID);
    return (proc.name == 'explorer.exe') ? child : getTopPID(proc.ParentProcessID, PID);
}

var PID = getTopPID(johnConnor.ProcessID);
johnConnor.Terminate();

// output PID of console window
WSH.Echo(PID);

Solution 3

You can also test a lock file and see whether it's stale by setting a timestamp within the lock file, and setting that same timestamp in your console window title. Only problem with this is that the window title doesn't revert to normal if the user terminates with Ctrl+C, so you can't run the script twice without closing the cmd window. But closing the window and opening a new one for subsequent launches may not be too terrible a price to pay, as this is the simplest method described thusfar.

@echo off
setlocal

set "started=%time%"
set "lockfile=%temp%\~%~n0.lock"

if exist "%lockfile%" (
    <"%lockfile%" set /P "locktime="
) else (
    set "locktime=%started%"
)

tasklist /v | find "%locktime%" >NUL && (
    echo Only one instance allowed.
    timeout /nobreak /t 3 >NUL
    exit /b
)

title %~nx0 started at %started%
>"%lockfile%" echo %started%

:: rest of script here

echo Simulating script execution...
:loop
ping -n 2 0.0.0.0 >NUL
goto loop

Solution 4

Here's a bit more polished solution, combining methods 1 and 3. It re-launches itself in the same window, then sets the window title to a unique ID. When the script exits gracefully, the lock file is deleted. Whether the script exits gracefully or forcefully, the window title reverts back to its default. And if no window exists in the task list with a title matching the unique ID, the lock file is deemed stale and is overwritten. Otherwise, the script notifies the user that only one instance is allowed and exits. This is my favorite solution.

@echo off
setlocal

if "%~1" neq "wrapped" (
    cmd /c "%~f0" wrapped %*
    goto :EOF
)
:: remove "wrapped" first argument
shift /1

:: generate unique ID string
>"%temp%\~%~n0.a" echo %date% %time%
>NUL certutil -encode "%temp%\~%~n0.a" "%temp%\~%~n0.b"
for /f "usebackq EOL=- delims==" %%I in ("%temp%\~%~n0.b") do set "running_id=%%I"
del "%temp%\~%~n0.a" "%temp%\~%~n0.b"

set "lockfile=%temp%\~%~n0.lock"

if exist "%lockfile%" (
    <"%lockfile%" set /P "lock_id="
) else (
    set "lock_id=%running_id%"
)

tasklist /v | find "%lock_id%" >NUL && (
    echo Only one instance allowed.
    timeout /nobreak /t 3 >NUL
    exit /b
)

title %running_id%
>"%lockfile%" echo %running_id%

:: rest of script here

echo Press any key to exit gracefully, or Ctrl+C to break
pause >NUL

del "%lockfile%"
goto :EOF
Community
  • 1
  • 1
rojo
  • 24,000
  • 5
  • 55
  • 101