困难在于您的批处理线程本身不会有自己的 PID。没有优雅的方法来判断您的批处理脚本是否正在运行或何时终止。并且没有办法唤醒死者,让脚本在用户红色 X 或 Ctrl+C 时拥有最终决定权。当它结束时,它就结束了。
有几种方法可以做你想做的事。全部尝试一下,看看你喜欢哪个。使用dbenham's solution。他是正确的。尽管解决方案 4 似乎工作得很好,但下面的努力留在这里作为徒劳的练习。最后,它仍然只是一个 hack;而 dbenham 的重定向技巧提供了锁定文件的正确实现方式,应该锁定文件的工作方式。
...
解决方案 1
一种简单的方法是使用powershell 最小化当前窗口,使用start /wait 重新启动您的脚本,然后在完成后再次调用powershell 以恢复。
@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
这对于临时使用来说应该足够了,并且应该解释批处理文件在taskkill /im "cmd.exe" /f 之外终止或重新启动或断电的任何原因。
解决方案 2
如果您需要更安全的解决方案,您可以获取当前控制台窗口的 PID,并间歇性地测试它是否仍然存在。 start /min 一个辅助窗口来监视它的父窗口死掉,然后删除锁定文件。而且,只要您无论如何都要创建一个观察者,不妨让那个观察者成为锁定文件。
此方法的最大缺点是它需要以exit 结束主脚本以销毁控制台窗口,无论您是否希望它被销毁。在脚本计算其父级的 PID 时,还会出现一两秒的停顿。
(使用 .bat 扩展名保存它,然后像运行任何其他批处理脚本一样运行它。)
@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);
解决方案 3
您还可以通过在锁定文件中设置时间戳并在控制台窗口标题中设置相同的时间戳来测试锁定文件并查看它是否过时。唯一的问题是,如果用户使用 Ctrl+C 终止,窗口标题不会恢复正常,因此您不能在不关闭的情况下运行脚本两次cmd 窗口。但是关闭窗口并为后续发布打开一个新窗口可能不会付出太可怕的代价,因为这是迄今为止描述的最简单的方法。
@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
解决方案 4
这里有一个更精致的解决方案,结合了方法 1 和 3。它在同一个窗口中重新启动自身,然后将窗口标题设置为唯一 ID。当脚本正常退出时,锁定文件被删除。无论脚本是正常退出还是强制退出,窗口标题都会恢复为默认值。如果任务列表中不存在标题与唯一 ID 匹配的窗口,则锁定文件被视为过时并被覆盖。否则,脚本会通知用户只允许一个实例并退出。这是我最喜欢的解决方案。
@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