WindowsバッチでMutexを使って二重起動を抑止する※ほぼPowerShell(笑)

Introduction

Windowsのバッチファイルで、二重起動を抑止するため色々頑張って見たところ、一旦はPIDファイルを作ってPIDファイルの有無及びPIDファイルに書かれたPIDでプロセス検索して、とか諸々の対処してなんとか10個位同時起動しても何とか二重起動を抑止できるような物ができた。

でも、二重起動のチェックだけで数秒掛かるし、理論的(?)にも完全に二重起動を抑止できないし、PowerShellもちょっと使っちゃったしで、微妙にイマイチだった。

そんな中、PowerShell使えば.NETのMutexとかいう機能を使って簡単に(?)二重起動を抑止できそうなことを突き止めた。

現状でもPowerShell使っているし、バッチからPowerShell叩いてバッチでもMutex使うサンプルを作ってみた(笑)

ざっくり処理の流れと課題整理

PowerShellでのMutexの詳細な処理は”mutex 二重起動 powershell”辺りのキーワードでググって頂くとして、流れとしてはざっくり、

  1. Mutexオブジェクト作る
  2. WaitOneメソッド叩いてMutex取得
  3. Mutex取得できなかったらオブジェクト開放して終了
    取得できたらやりたい処理して終わったらMutex解除とオブジェクト開放

こんな感じなんですが、バッチからPowerShell叩いてやるとなると、”終わったらロック解除とオブジェクト開放”ってのが問題。なぜなら、Mutex取得できたらオブジェクト残したまま処理をバッチに戻し、バッチの処理が終わったらMutex握ってるPowerShellに戻さなきゃならないからね。

課題解決は以下のような感じでしょうか。

  1. PowerShellをバックグラウンドで起動し、waitfoのシグナルが送られてくるまで待機。シグナル来たらロックファイルの有無でMutex取得可否を判定。
  2. バックグラウンドのMutexが取得できなかったらwaitforで親バッチシグナルだけ送る。
  3. バックグラウンドのPowerShellでMutexが取得できたら、ファイルを作成しwaitfor使って親バッチにシグナルを送る。同時にバッチが終わるまでwaitforで待機する。

で、出来上がったバッチはコレ(笑)

まずは、コード全体。

@echo off
setlocal enabledelayedexpansion

:: 自分のPIDを取得し、%MyPID%へ代入
for /f "usebackq" %%p in (
	`powershell -command ^
		$a^=[System.Diagnostics.Process]::GetCurrentProcess^(^).Id^;^
		$b^=^(gwmi Win32_Process ^^^| where {$_.ProcessId -eq $a}^).ParentProcessId^;^
		^(gwmi Win32_Process ^^^| where {$_.ProcessId -eq $b}^).ParentProcessId`
) do (
	set MyPID=%%p
)

:: MyPIDを基にロックファイル名決定
set lockfile=%~dp0%COMPUTERNAME%_%MyPID%.lock

:: ロックを取得する。ロックできたらロックファイルを作成する。
:: ※バッチ最後の後処理が実行されるまでプロセス残ります
start /b "" powershell -command ^
	sleep -m 100 ;^
	$mutex=New-Object System.Threading.Mutex^($false, """Global\%COMPUTERNAME%_%~n0"""^);^
	if ^($mutex.WaitOne^(0, $false^)^){^
		New-Item -ItemType file $env:lockfile ^>$null;^
		waitfor /si %MyPID%Start ;^
		waitfor %MyPID%Close ;^
		$mutex.ReleaseMutex^(^)^
	} else {^
		waitfor /si %MyPID%Start^
	};^
	$mutex.Close^(^);^
	exit;^
	trap [System.Threading.AbandonedMutexException] { continue } > nul 2>&1

:: ロック取得結果を待つ。ロックできなかったら(ロックファイルが無かったら)終了。
waitfor %MyPID%Start > nul 2>&1
if not exist %lockfile% (
	powershell -command Write-Host -ForegroundColor Red """%~nx0は既に起動されています"""
	pause
	exit
)

:: ロックファイルを削除して処理を開始する。
if exist %lockfile% del %lockfile%
echo %~nx0を実行します。
pause

:: 後処理
waitfor /si %MyPID%Close > nul 2>&1

exit

中身解説の前に一言(笑)

最初に言っておきますが、サンプルの6行目~9行目と、20行目~32行目まではPowerShellのワンライナーです(笑)

バッチでPowerShellをワンライナーで実行すると、諸々のエスケープ(コード中の”^”)が面倒ですけれど、.batと.ps1を別ファイルにしなくて済むし、なによりPowerShellのExecutionPolicyがRestrictedのままでもPowerShellスクリプトが動かせるのがメリットと個人的には感じています。

 5行目~12行目:PID取得

waitforのシグナル名は、同一ホスト内で重複はできないので、シグナル名をユニークするためPIDをシグナル名に含めることにする。

バッチでは自分自身のPIDを取得できないため、PowerShellを起動し親プロセスを辿っていく。

7行目で起動したPowerShellのPIDを取得、8行目でWMIからPowerShellを起動した親プロセス(forコマンドでPowerShellを起動したcmd.exe)のPIDを取得、9行目で更にそのcmd.exeの親プロセスのPIDを取得。

ちなみにforループ内では通常の ( ) や | の他、= や  ; も ^ でエスケープしないとならないのでワンライナーも更に読み難さアップ(笑)

15行目:ロックファイル作成

PID取得したら、ロックファイルのファイル名を環境変数に突っ込む。

一応、共有パスから複数端末で実行するケースを想定し、ファイル名にはコンピュータ名も入れるようにする。

19行目~32行目:Mutex取得

バックグラウンドでPowerShellを起動し、35行目のwaitforが待機状態になるよう念のためウェイト。0.1秒に根拠は無いです(^^;;

22行目でMutex取得。Mutexを取得できたら、ファイルを作って親バッチにシグナル送信して、親バッチからのシグナルを待機。
親バッチから(処理が終わって)シグナル送られて来たら、Mutexをリリース後MutexをクローズしてPowerShell終了。

Mutexが取得できなかったら、親バッチにシグナルだけ送ってMutexをクローズしてPowerShell終了。

35行目~40行目:シグナル待機

35行目のwaitforで、バックグラウンドのPowerShellからのシグナルを待機。シグナル受信後、ロックファイルが無ければメッセージを表示してバッチ終了。赤字でメッセージ表示しているのは、ただ何となく・・・(笑)

43行目~:バッチで行う処理

この時点でロックファイルは用無しなのでとっとと削除してやりたい処理を実行。サンプルではメッセージ表示とpauseだけですが(笑)

48行目:バックグラウンドのPowerShell終了

バッチを終わらせる前に、バックグラウンドでけなげに待機しているPowerShellにシグナル送って、終わらせてあげましょう\(^o^)/

ちなみにバッチをCTRL+Cで終わらせると、バックグラウンドのPowerShellも残ったままになるのでcmd.exeをウインドウの×ボタンで終わらせるかタスクマネージャからPowerShellのプロセスを落としましょう(^^;;

こんな感じでしょうか。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です