如何防止运行同一批批文件两次以仅允许一个实例?

如何防止运行同一批批文件两次以仅允许一个实例?

我需要检测批处理文件是否已在运行。

在批处理中,我只是循环并重复执行一些任务。有时循环无法完成。我在预定的时间运行批处理文件。运行时,它应该检查先前的实例是否存在并且是否健康。如果不存在,它应该启动一个新实例。如果存在的实例不健康/过时,新实例应该终止/取消旧进程。

一些理论:

  1. 如何检测批处理文件中的先前实例?我可以给出类似 ID 的东西吗?
  2. 我可以使用脉冲机制来表示批次是健康的;即全局变量、新创建的虚拟文件、虚拟文件中的时间戳等。
  3. 我能否检测到一种“超时”,如果最后一个脉冲已过期,则允许一个新的实例......

是的,我都希望将它们放在批处理文件中 - 而不是 Windows 应用程序 - 我认为对于批处理文件爱好者并将这个问题视为挑战的人来说这并不难!

答案1

从批处理文件的角度来看,这相当困难,主要是因为批处理文件没有保证的方法来检测跨进程的实例。

在我看来,我认为你用错了工具。虽然这确实是一个挑战,但更重要的问题是“它有用吗?”

我在预定的时间运行批处理文件。

这告诉我你应该使用调度程序,比如任务调度程序。任务调度程序将保证你的文件只有一个实例,即使它在另一个用户上运行。

例如,如果您每 30 分钟运行一次任务,则将任务配置为运行 30 分钟。然后,将“如果任务已在运行”设置设置为“不创建新实例”。或者,您可以选择“终止旧实例”、“运行另一个实例”或“旧任务完成后立即启动新任务”。

对于 Vista 及更高版本:http://technet.microsoft.com/en-us/library/cc748993.aspx#BKMK_cmd
对于 Xp 及以上版本:http://technet.microsoft.com/en-us/library/bb490996.aspx

具有挑战性的方法是创建状态。

  1. 您的批处理文件进入执行状态
  2. 您的批处理文件循环
  3. 您的批处理文件结束

您可以在 %allusersprofile% 中创建一个任意文件,例如>>Startmybatchfile.txt echo 1针对每个州,然后检查它是否存在。这也有助于解决问题。但不要最后删除该文件。只需将其更改为“结束状态”即可。这样就可以确定您的批处理文件是否具有确实已完成并且不再运行。

我不会使用全局变量,因为它需要管理员权限才能运行。

您也可以使用 %temp% 变量,但该变量因用户而异。我想如果您是单点登录用户计算机,那不是问题。

答案2

这是我最新、最值得信赖的脚本:

:init
set "started="
2>nul (
 9>"%~f0.lock" (
  set "started=1"
  call :start
 )
)
@if defined started (
    del "%~f0.lock" >nul 2>nul
) else (
    echo Process aborted: "%~f0" is already running
    @ping localhost > nul
)

exit /b




:start
cd /d %~dp0
:: REST OF THE SCRIPT

答案3

我发现最好的方法是在批处理脚本上设置标题。

从那里,您可以使用 tasklist 根据标题名称查询正在运行的进程数。下面的代码片段中,process_count=2 以下的任何值都表示进程未运行,3 表示恰好有 1 个实例正在运行,如果高于 3,则表示有超过 3 个进程正在运行。

有点黑客风格,但批量就是这样。

对于连续循环的脚本,我不够聪明,无法想出一种方法将开始时间写入标题字段并从另一个批处理脚本中查询该时间。我曾想过复制并重命名文件名和时间,然后执行该操作,但对我来说,除了直接查询之外的任何方法都比它的价值更混乱。

是的,我可以将其放入文本文件,但我担心缓存损坏。

@REM ########################
:COUNT_PROCESS_INSTANCES
@REM @@@@@@@@@@@@@@@@@@@@@@@@

SET /A PROCESS_COUNTER=0


@REM No way to inject a counter into this for statement.
FOR /F "DELIMS=" %%A in ('tasklist /FI "WINDOWTITLE EQ %USERNAME%:  %PROCESS_NAME_TO_COUNT%*"') do (CALL :SUB_INCRIMENT_PROCESS_COUNTER)

@REM If one instance is running, or no instances, that's fine.  If more than one instance is running, time to exterminate.

@ECHO PROCESS COUNTER IS: "%PROCESS_COUNTER%"
SET PROCESS_NAME_TO_COUNT=NULL

GOTO :EOF

:SUB_INCRIMENT_PROCESS_COUNTER
SET /A PROCESS_COUNTER=%PROCESS_COUNTER%+1
GOTO :EOF

@REM @@@@@@@@@@@@@@@@@@@@@@@@
@REM ########################

此外,对于任何感兴趣的人,以下是如何找到当前正在执行的进程的 PID。现在,如果您将变量传递到子脚本中,则可以通过文本文档将该变量传递回链中(我认为还有另一种方法可以做到这一点,但我记不清了)。

@REM @@@@@@@@@@@@@@@@@@@@@@@@
:FIND_SELF_PID
@REM ########################
@REM Leaving this here for later if needed.  This was a byproduct of a bad approach.

FOR /F "TOKENS=1,2,*" %%A in ('tasklist /FI "WINDOWTITLE EQ %USERNAME%:  %CURRENT_TITLE%"') do (SET SELF_PID=%%B)

echo %self_pid%

GOTO :EOF
@REM @@@@@@@@@@@@@@@@@@@@@@@@
@REM ########################

答案4

我找到了一个解决方案,它利用文本文件来跟踪 bat 文件的所有先前的 PID。它会尝试悄悄地杀死它们,然后将当前 PID 添加到列表中。

如果您不想终止旧的、已经存在的进程,只需将包含“taskkill”的行替换为您想要执行的任何操作。

(可能需要您以管理员身份运行才能获得终止重复进程的权限。如果您不想每次都以管理员身份运行,请参阅下面的权限提升代码以了解可选实现。)

@echo off
set WorkingDir=%cd% 
if exist MostRecentPID.txt ( del "PIDinfo.txt"  /f /q ) > nul
cd ..\..\..\..\..\..\..
title mycmd
tasklist /v /fo csv | findstr /i "mycmd" > %WorkingDir%\PIDinfo.txt
set /p PIDinfo=<%WorkingDir%\PIDinfo.txt

REM below, the 11 means get substring starting a position 11 with length of 5 characters. The tasklist command gives a long and verbose value so this will get just the PID part of the string.
set PID5chars=%PIDinfo:~11,5%
set PID4chars=%PIDinfo:~11,4%

if exist PreviousPIDs.txt (
    for /F "tokens=*" %%A in (PreviousPIDs.txt) do taskkill.exe /F /T /PID %%A > nul 2>&1
    goto CheckIfFourCharPID
)

:CheckIfFourCharPID
if %PID4chars% gtr 8100 (
    for /F "tokens=*" %%A in (PreviousPIDs.txt) do taskkill.exe /F /T /PID %%A > nul 2>&1
    echo %PID4chars% >> "PreviousPIDs.txt"
) else (
    echo %PID5chars% >> "PreviousPIDs.txt"
)

解释:(警告:非常技术性)

-此解决方案获取 tasklist 命令的子字符串以仅获取 PID。cmd.exe 的 PID 不会大于 18100,因此请检查 PID4chars 是否大于 8100,这样我们就知道它是 4 位数字还是 5 位数字

情况 1:5 位数字 PID(例如 17504)的 PI​​D5chars 值为 17504,PID4chars 值为 1750,因此我们将 PID5chars 添加到要终止的 PID 的文本文件中

情况 2:4 位数字 PID(例如 8205)的 PI​​D5chars 值为 8205",PID4chars 值为 8205,因此我们将 PID4chars 添加到要终止的 PID 的文本文件中

情况 3:4 位数字 PID(例如 4352)的 PI​​D5chars 值为 4352”,PID4chars 值为 4352,因此我们将 PID4chars 添加到要终止的 PID 的文本文件中


可选许可海拔代码

(将其放在你的 bat 文件的顶部,它将以管理员身份自动运行。)

    @echo off
    setlocal DisableDelayedExpansion

    set "batchPath=%~0"
    for %%k in (%0) do set batchName=%%~nk
    cd ..\..\..\..\..\..\..\..
    if exist %cd%\Temp (
    set temp=%cd%\Temp
    goto vbsGetPrivileges
    )
    if exist %cd%\Windows\Temp (
    set temp=%cd%\Windows\Temp
    goto vbsGetPrivileges
    )
    set temp=%cd%

    :vbsGetPrivileges
    set "vbsGetPrivileges=%temp%\OEgetPriv_%batchName%.vbs"
    setlocal EnableDelayedExpansion

    :CheckIfRunningAsAdmin
    net session >nul 2>&1
    if %ERRORLEVEL% == 0 ( 
        goto gotPrivileges 
    ) else ( goto ElevatePermissions )

    :ElevatePermissions
    if '%1'=='ELEV' (echo ELEV & shift /1 & goto gotPrivileges)
    ECHO Set UAC = CreateObject^("Shell.Application"^) > "%vbsGetPrivileges%"
    ECHO args = "ELEV " >> "%vbsGetPrivileges%"
    ECHO For Each strArg in WScript.Arguments >> "%vbsGetPrivileges%"
    ECHO args = args ^& strArg ^& " "  >> "%vbsGetPrivileges%"
    ECHO Next >> "%vbsGetPrivileges%"
    ECHO UAC.ShellExecute "!batchPath!", args, "", "runas", 1 >> "%vbsGetPrivileges%"
    "%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" %*
    exit /B

    :gotPrivileges
    setlocal & pushd .
    cd /d %~dp0
    if '%1'=='ELEV' (del "%vbsGetPrivileges%" 1>nul 2>nul  &  shift /1)
    net session >nul 2>&1
    if %ERRORLEVEL% == 0 (
        goto Continue
    ) else (
        REM unable to elevate permissions so tell user to run file as admin manually
        echo Please re-run this file as administrator. Press any key to exit...
        pause > nul
        goto Exit
     )

    :Continue
    <insert rest of code here>

相关内容