Powershellで非同期にファイル変更を監視する

なんかさ、便利なものを公開してくれる人もいるわけじゃないですか。
でもさ、ファイル修正してからコマンド叩くの、面倒じゃないですか。
忘れるじゃないですか。
勝手に作れよ、と。
だから考えてみた。

追記 2014/12/15
Start-Watcherのリターンを、Watcher停止のクロージャに変更。
stuncloudさんが便利なハックをきめてくれたので、反映!

変更監視スクリプト

とりあえず、ファイルの変更を監視してみる。
Powershell ファイル変更監視 非同期」で検索してみたけど、お目当てのものが見つからないので作った。

function Start-Watcher() {
	Param(
		$ScriptBlock,
		[string]$Path,
		[string]$Filter = '*.*',
		[switch]$IncludeSubdirectories,
		[int]$InternalBufferSize = 8192,
		[System.IO.NotifyFilters]$NotifyFilter = [System.IO.NotifyFilters]::LastWrite -bor [System.IO.NotifyFilters]::FileName -bor [System.IO.NotifyFilters]::DirectoryName,
		$MessageData = $null
	)

	$watcher = New-Object System.IO.FileSystemWatcher
	$watcher.Path = $Path
	$watcher.Filter = $Filter
	$watcher.IncludeSubdirectories = $IncludeSubdirectories
	$watcher.InternalBufferSize = $InternalBufferSize
	$watcher.NotifyFilter = $NotifyFilter
	$watcher.EnableRaisingEvents = $true
	$ret = Register-ObjectEvent -InputObject $watcher -EventName Created -SourceIdentifier ('JunjunFileWatcher_Created_' + $Path + $Filter) -Action $ScriptBlock -MessageData $MessageData
	$ret = Register-ObjectEvent -InputObject $watcher -EventName Changed -SourceIdentifier ('JunjunFileWatcher_Changed_' + $Path + $Filter) -Action $ScriptBlock -MessageData $MessageData
	$ret = Register-ObjectEvent -InputObject $watcher -EventName Deleted -SourceIdentifier ('JunjunFileWatcher_Deleted_' + $Path + $Filter) -Action $ScriptBlock -MessageData $MessageData
	$ret = Register-ObjectEvent -InputObject $watcher -EventName Renamed -SourceIdentifier ('JunjunFileWatcher_Renamed_' + $Path + $Filter) -Action $ScriptBlock -MessageData $MessageData

	return {
		Remove-Watcher -Path $Path -Filter $Filter
	}.GetNewClosure()
}

function Remove-Watcher() {
	Param(
		[string]$Path,
		[string]$Filter = '*.*'
	)

	Remove-Timer ('JunjunFileWatcher_Created_' + $Path + $Filter)
	Remove-Timer ('JunjunFileWatcher_Changed_' + $Path + $Filter)
	Remove-Timer ('JunjunFileWatcher_Deleted_' + $Path + $Filter)
	Remove-Timer ('JunjunFileWatcher_Renamed_' + $Path + $Filter)
}

ま、.Netで書く処理をPowershellに直しただけです。
Remove-WatcherがRemove-Timerに依存してます。
以下から持ってくるか依存しないよう修正してください。
Powershellで非同期実行 - じゅんじゅんのきまぐれ

使用例

テスト。「C:\」に「.txt」ファイルが作成されたりしたらどう動くか。

Start-Watcher {
	Param($sender, $ev)
	if($ev.ChangeType -eq 'Renamed') {
		Write-Host $ev.ChangeType $ev.FullPath $ev.Name '<-' $ev.OldFullPath $ev.OldName
	} else {
		Write-Host $ev.ChangeType $ev.FullPath $ev.Name
	}
} 'C:\' '*.txt'

更新は使うエディターによって、出方が違うので注意が必要です。
(もしファイル更新が旧削除・新作成方式だと、Changedはないとか)
実験が終わったら、解除。

Remove-Watcher 'C:\' '*.txt'


現在のディレクトリーでSave-SignedScriptって関数を叩きたい場合。
「〜.ps1」が変更されて同じディレクトリーに「〜.Signed.ps1」が存在するなら叩く、とか。

$removeWatcher = Start-Watcher {
	Param($sender, $ev)
	if($ev.ChangeType -ne 'Deleted') {
		$f = Get-Item $ev.FullPath
		$SignedFilePath = "$($f.BaseName).Signed$($f.Extension)"
		if((Test-Path $SignedFilePath) -and ($f.LastWriteTime -gt (Get-Item $SignedFilePath).LastWriteTime)) {
			Save-SignedScript -FileInfo $f
		}
	}
} (Get-Location) '*.ps1'

何回か同時にイベントが発行することを想定して、LastWriteTimeもチェックしてます。
解除したくなったら、該当ディレクトリーで以下を

Remove-Watcher (Get-Location) '*.ps1'

また、Start-Watcherの戻りを受けていれば、以下のように解除可能。

&$removeWatcher

解説他

このWatcher処理は、処理中メインを占有するので、あまり重たい処理には向きません。
Start-Watcherの引数は、System.IO.FileSystemWatcherに渡すものです。
$ScriptBlockが遅くイベントが頻発する場合、内部バッファーが足りなくなってイベントを取りこぼすことがあります。
取りこぼしがまずいなら、$InternalBufferSizeを大きくしてください。(目安知らんけど)
C:\とか指定して$IncludeSubdirectories=$trueとかだと、悲惨なことが起きかねないので注意してください。


重めの処理をしたいなら、$ScriptBlock内でStart-Taskすれば良いと思いますよ。
Powershellで非同期実行 - じゅんじゅんのきまぐれ
なお、Start-Taskのスクリプトブロックに引数を渡せるようにしました。
また、Start-Watcherのスクリプトブロックにも$Event.MessageData経由で引数が渡せます。

Start-Watcher {
	Param($sender, $ev)
	Start-Task {
		$args[0].out += $args[1].name + $args[1].setting.startMsg
		Start-Sleep $args[1].setting.sleepTime
		$args[0].out += $args[1].name + $args[1].setting.endMsg
	} -MessageData @{name=$ev.Name; setting=$Event.MessageData}
} (Get-Location) '*.ps1' -MessageData @{startMsg=' start'; sleepTime=2; endMsg=' end'}

Start-Watcherの-MessageDataが、スクリプトブロック内の$Event.MessageDataになり、スクリプトブロック内にあるStart-Taskの-MessageDataは、そのスクリプトブロック内の$args[1]となっています。