2
:: dostuff.bat
@echo off
:: insert long-running process call here
: End

What can I add to this batch file to make it terminate if it's already running in another process when it's executed?

lance
  • 16,092
  • 19
  • 77
  • 136

6 Answers6

8

Well, if there can be only exactly one instance ever, then you can probably do this by creating a dummy file somewhere, probably in the temp directory:

copy NUL "%TEMP%\dostuff_is_doing_stuff.tmp"

you then remove it after you're done:

del "%TEMP%\dostuff_is_doing_stuff.tmp"

and at the start of the batch file you can check whether that file exists and exit accordingly:

if exist "%TEMP%\dostuff_is_doing_stuff.tmp" (
    echo DoStuff is already running. Exiting ...
    exit /b 1
)

Similarly, you can do that by changing the window title, which should also be more robust against a Ctrl+C'ed or crashed batch file.

Set the title:

title DoStuff

Re-set it afterwards:

title Not doing stuff

Check for it:

tasklist /FI "WINDOWTITLE eq DoStuff"

However, this has one problem: When cmd runs as administrator you'll get "Administrator: DoStuff" as the title. Which doesn't match anymore with tasklist. You can hack-solve it by also checking for "Administrator: DoStuff" but that might look different depending on the Windows UI language, so it's probably not the best solution.

Joey
  • 344,408
  • 85
  • 689
  • 683
  • The tasklist idea is great. Thank you. How do I write a test against that command's output? That is, when I run tasklist in the batchfile, what's the logic to know if it told me the batch file was already running or not? – lance Jan 21 '10 at 16:38
  • Hm, sorry for that tasklist stuff. I thought tasklist would respond with an exit code; you could then use them with `if errorlevel ...` but tasklist always returns 0, regardless of whether a window was found or not. That sucks a bit. You can still capture the output with `for /f` but I'd advise against it since that's inherently brittle (localization and stuff). – Joey Jan 21 '10 at 17:52
  • Right. When that didn't work, I fired off my question. I can pipe tasklist's results into findstr, which returns a conditional errorlevel. I don't like it, but I never concern myself with aesthetics when I'm authoring batch files (especially for personal use, as in this case). – lance Jan 21 '10 at 20:52
  • Ah, right, `findstr` eluded me at the moment. Yes, that'd be easier still. Still, I don't particularly like relying on the output being in a particular language or format. – Joey Jan 21 '10 at 21:51
  • Concur. I'll be looking, simply, for cmd.exe in the output. I wonder if that changes from the locale/region (or version of Windows, even). – lance Jan 21 '10 at 22:53
  • Hm, right, one can look for the success message or for the failure message. The success one should be safe, indeed. I can't think straight today, apparently. – Joey Jan 22 '10 at 00:08
  • @Joey Does not `title/tasklist` approach lead to a potential race condition? – AlexD Sep 07 '15 at 17:39
  • @AlexD: Yes, potentially. But it's hard to find a solution with batch files that's both race-free (the filesystem approach would be, if you try to create a unique file and exit if that fails) and robust against a batch file that doesn't exit normally (in which case you have no way of removing the file). – Joey Sep 07 '15 at 17:58
4

There is no way to do this in a clean way without using a custom external tool (You want something that creates a mutex AND runs (and waits for) your external command, this way, if the process is killed, the mutex dies with it)

Batch:

@echo off
echo starting long running process
REM calling dir here just to get a long running operation
onecmd.exe cmd.exe /C dir /S /B \*
echo done...bye

C++ helper app:

//Minimal error checking in this sample ;)
#include <Windows.h>
#include <TCHAR.h>

int main()
{
    const TCHAR myguid[]=_T("50D6C9DA-8135-42de-ACFE-EC223C214DD7");
    PROCESS_INFORMATION pi;
    STARTUPINFO si={sizeof(STARTUPINFO)};

    HANDLE mutex=CreateMutex(NULL,true,myguid);
    DWORD gle=GetLastError();
    if (!mutex || gle==ERROR_ALREADY_EXISTS) 
    {
        CloseHandle(mutex);
        return gle;
    }

    TCHAR*p=GetCommandLine();
    if (*p=='"' && *(++p)) {
        while (*p && *p!='"')++p;if (*p)++p;
    }
    else 
        while (*p && *p!=' ')++p;
    while(*p==' ')++p;

    if (CreateProcess(0,p,0,0,false,0,0,0,&si,&pi)) 
    {
        DWORD ec;
        WaitForSingleObject(pi.hProcess,INFINITE);
        GetExitCodeProcess(pi.hProcess,&ec);
        CloseHandle(pi.hThread);
        CloseHandle(pi.hProcess);
        return ec;
    }
    gle=GetLastError();
    CloseHandle(mutex);
    return gle;
}
Anders
  • 97,548
  • 12
  • 110
  • 164
2

The best, simple, charming way to do it:

@echo off
set /a count=0
for /f "skip=3 USEBACKQ delims=" %%a in (`wmic process where "name='cmd.exe' and commandline like '%%%~nx0%%'" get processid`) do set /a count+=1
if %count% gtr 1 exit
echo 我要运行, Please run me....
pause
untitled
  • 161
  • 1
  • 5
1

You mostly can't in kind way.

First of because it is not trivial to do. You have to find the correct one cmd.exe process when in general it is not quite easy to "do all things right".

Second, termination may be is not enough to stop the script, because one script can run another script in cycle, so just terminating one will not lead to complete termination of another. More over, it can lead to a broken execution because you have to terminate the whole process tree from the root "atomically" to properly stop the execution.

Instead of "search to terminate" you can just "don't execute if already running". The technique is the same as in unix shell scripting: try to create some unique directory and lock it from removing by write redirection into a file in that directory.

i am using the script names something like exec_once_or_exit.bat:

@echo off

setlocal

set "LOCK_DIR_NAME_SUFFIX=%DATE%_%TIME%"
set "LOCK_DIR_NAME_SUFFIX=%LOCK_DIR_NAME_SUFFIX::=_%"
set "LOCK_DIR_NAME_SUFFIX=%LOCK_DIR_NAME_SUFFIX:/=_%"
set "LOCK_DIR_NAME_SUFFIX=%LOCK_DIR_NAME_SUFFIX:-=_%"
set "LOCK_DIR_NAME_SUFFIX=%LOCK_DIR_NAME_SUFFIX:.=_%"
set "LOCK_DIR_NAME_SUFFIX=%LOCK_DIR_NAME_SUFFIX:,=_%"
set LASTERROR=0

rem cleanup if leaked by crash or ctrl-c, won't be removed if already acquired because of write redirection lock
rmdir /S /Q "%TEMP%\lock_%~1" >nul 2>&1

mkdir "%TEMP%\lock_%~1" && (
  rem IMPL to use "goto :EOF" in next command instead of "exit /b %ERRORLEVEL%" under "block" command - "( )"
  call :IMPL %%*
  goto :EOF
)
exit /b -1024

:IMPL
rem call IMPL2 to recover exit code from commands like "exit /b"
call :IMPL2 %%* 9> "%TEMP%\lock_%~1\lock0_%LOCK_DIR_NAME_SUFFIX%.txt"
set LASTERROR=%ERRORLEVEL%
rmdir /S /Q "%TEMP%\lock_%~1"
exit /b %LASTERROR%

:IMPL2
rem a batch script must be always call over the "call" prefix
if "%~n2" == "bat" call %%2 %%3 %%4 %%5 %%6 %%7 %%8 %%9 && goto :EOF
if not "%~n2" == "bat" (
  %2 %3 %4 %5 %6 %7 %8 %9
)
goto :EOF

Usage:

exec_once_or_exit.bat <UniqueNameForLock> <PathToExecutable> <8Arguments>
exec_once_or_exit.bat <UniqueNameForLock> <BatchCommand(s)>

Explanation:

The mkdir command will return non zero exit code if directory already exists or can't be created for some reason, and 0 if created.

The LOCK_DIR_NAME_SUFFIX is used to have unique name for the file in the directory. Is uses all these replacements over specific characters because %DATE% and %TIME% variable's format depending on the format of date and time in the windows localization page.

Redirection over the stream id 9 is used to lock the file in directory for write and so locks the parent directory itself from removing. If the stream for some reason can't be redirected then the script terminates immediately by the cmd.exe w/o further execution which is enough for the mutual exclusion of execution. So, because the redirection happens before the execution there is no fear that something can execute before the redirection.

If crash or ctrl-c is happened then the directory will remain on the storage (hdd, ssd, that ever else), so to clean up it in that case the script tryes to remove the whole directory with all files inside before the mkdir.

The only thing what can happen here wrong is that the no one may acquire the execution which seems not so frequent to happen. At least it is better than "more than one at a time".

Andry
  • 2,273
  • 29
  • 28
0

TWO FILES:

-START.BAT-

if exist "%TEMP%\dostuff_is_doing_stuff.tmp" (   
exit /b 1
)

copy NULL "%TEMP%\dostuff_is_doing_stuff.tmp"

cmd /K COMMANDS.bat

-COMMANDS.BAT-

REM ...do something

REM ...do something

REM ...do something    

DEL "%TEMP%\dostuff_is_doing_stuff.tmp"
Stewie Griffin
  • 14,889
  • 11
  • 39
  • 70
0

Start->Run:

echo cmd /k -^|->-.bat&-

Violates the rules because (many) more than one instance runs simultaneously- but it's a fun way to get free food: bet your coworker lunch that he can't kill it without cold-booting.

druid
  • 51
  • 4