はじめに
先日 Windowsバッチまとめ という記事をアップしましたが、追加情報です。
バッチをベースに色々仕組みを考えていると、やっぱりどうにもこうにもバッチだけだと出来なさそうなことが色々あります。だからと言ってじゃぁ一切合切PowerShellに切り替えるかというとそういう状況でも無く、必要な部分だけVBScriptとかPowerShellとか使えばいいじゃん、ということで対応したことがあるのでその辺も備忘録としてまとめておきます。
(こういう行き当たりばったりの継ぎはぎはあんまり良くないかもしれませんが、まぁ手っ取り早いんで。)
プロセスIDを取得する
バッチとして稼働した際の、自分自身のプロセスIDを取得したい、ということがあります。(タスクマネージャーで見るとイメージ名としてはcmd.exeになると思います。)
バッチだと難しそうなので、PowerShellに頼ってみました。
例えば、ある処理を一定間隔で実行するような常駐のバッチをバックグラウンドで(画面を出さずに)実行しているようなケースを想定します。プロセスIDをどこかに出力させておくと、外からそのプロセスID指定でtaskkillすると常駐のバッチを停止させることができます。
@echo off
setlocal enabledelayedexpansion
cd %~dp0
set PIDFile=pid.txt
set sleepTime=60
powershell "Get-WmiObject win32_process -filter processid=$pid | ForEach-Object{$_.parentprocessid;}" > %PIDFile%
:Loop
rem *************************
rem 一定間隔で実行したい処理
rem *************************
timeout /T %sleepTime% /nobreak > nul
goto Loop
:End
powershell ...
という一文がPIDを取得してファイルに書き出している箇所です。PowerShellでは自分自身の親プロセスのPIDを取得する処理をおこなっています。(バッチからPowerShell実行すると子プロセスで処理が行われるので、求めるPIDはPowerShellの処理からみると自分自身の親プロセスとなる。)
Loopの中でLoopを抜けるための判定ロジック(どこかのフラグ見てフラグ立ってたら終了とか)入れるのがよいのかもしれませんが、sleepの時間によっては終了までタイムラグが発生してしまったり、不足の自体でハングしてしまったりする場合は困るし。あとは、こうしておくと、PIDのファイル有無で稼働中かどうかの判定がしやすかったりする。
停止用のバッチのサンプルはこちら。
@echo off
setlocal enabledelayedexpansion
cd %~dp0
set PIDFile=pid.txt
if exist "%PIDFile%" (
set /p targetPID=<%PIDFile%
tasklist /fi "PID eq !targetPID!" | find "cmd.exe" > nul
if not ERRORLEVEL 1 (
goto RUNNING
) else (
del %PIDFile%
goto NOT_RUNNING
)
) else (
goto NOT_RUNNING
)
:RUNNING
echo kill process: !targetPID!
taskkill /F /PID !targetPID!
del !PIDFile!
goto END
:NOT_RUNNING
echo not running
goto END
:END
常駐のバッチなので、プロセスイメージ名としてcmd.exeで、かつ、PIDがマッチする場合にkillするようにしています。
(厳密にいうと、この例だと常駐のバッチが不測の事態でプロセスごと落ちていて、たまたま別のcmd.exeが同じプロセスIDを使ってしまっていたとすると、意図しないプロセスを殺してしまう可能性はあります。どこまで厳密にやるかですが、必要に応じてkillするときの判定ロジックを増やすなりしましょう。)
日付情報を取得する
バッチでは既定の環境変数として%date%というのがあり、当日の日付を取得することができます。しかし、昨日とか明日とか一週間前とか数日後とか、今日を基準として相対的に数日ずれた日付情報を取得することが困難です。
月によって日数違うし、年によってはうるう年とかあるし。
バッチだとdate型みたいなのが無いようなのですが、VBScriptだと簡単にできるようなので、当日を基準とした日付を取得するサブルーチンを作ってみました。
Dim difference
difference=WScript.Arguments(0)
WScript.Echo DateAdd("d",difference,Date())
バッチから利用するサンプルはこちら。
@echo off
set difference=%1
for /f "tokens=1-3 delims=/" %%a in ('cscript //nologo getDate.vbs %difference%') do (
set year=%%a
set month=%%b
set day=%%c
)
set date=%year%/%month%/%day%
echo date: %date%
echo year: %year%
echo month: %month%
echo day: %day%
cscript ...
という箇所がVBScriptを呼び出しているところです。for文を利用してVBScript呼び出しコマンドの実行結果を変数で受けています。
わざわざ年月日で分けて受け取らなくてもよいかもしれないですが、こうしておけば区切り文字(/)変えたりするのも簡単ですし。
実行例はこちら。
それぞれ、当日、30日前、20日後の日付取得例です。
c:\temp\batch_test>handle_date.bat 0
date: 2016/10/15
year: 2016
month: 10
day: 15
c:\temp\batch_test>handle_date.bat -30
date: 2016/09/15
year: 2016
month: 09
day: 15
c:\temp\batch_test>handle_date.bat 20
date: 2016/11/04
year: 2016
month: 11
day: 04
バックグラウンドでのバッチ起動
Windowsバッチまとめ でも、バッチから非同期で別のバッチの呼び出し方法については書いたが(startとか)、呼ばれるサブルーチン側での終了方法をきちんと意識しておかないというのが結構面倒だと思います。
例えば、start /bで別の子バッチ呼びだす場合、子バッチはexitで終わらないと子プロセスが残ってしまう。子バッチは汎用的に使えるルーチンだから子バッチにしている訳で、それをコマンドプロンプトからも単体で使おうとしたり、callで呼び出そうとすると、最後exit発行されると親ごと終了してしまうので具合が悪かったりします。まぁ同期的に呼ぶものと非同期的に呼ぶものは分けて作ればいいのかもしれませんが、その辺意識するのが面倒だなぁと。
で、バックグランドでバッチ起動用のVBScriptを用意してそれをかませてあげれば、呼ばれる子バッチの作りを意識しなくてもよさそうなので使ってみました。
'argument: "cmd /c xxx.bat"
Set ws = CreateObject("Wscript.Shell")
ws.run WScript.Arguments(0), vbhide
...
cscript //nologo cmd_background.vbs "cmd /c test_sub.bat arg1"
..
これで、子バッチ(上の例だとtest_sub.bat)が非同期/別プロセス/バックグラウンドで実行され、かつexit発行しなくてもプロセスが残らない。