2013/12/05

はじめに

この記事はPowerShell Advent Calendar 2013の5日目の記事です。

突然ですが、PowerShellにはJobが完了するまで待機するWait-Jobコマンドレットというのがあります。これはその名の通り、パイプラインから入力したJobオブジェクトがすべて(あるいはどれか一つが)完了状態になるまでスクリプトの実行を待機する効果があります。

当然ながらWait-JobはJobオブジェクトにしか利用できませんが、任意の入力オブジェクトに対して待機条件を指定してやれば、その条件を満たすまで実行を停止するコマンドがあると便利なんじゃないかな?と常々思っていたので書いてみました。

Wait-State関数
function Wait-State
{
    [CmdletBinding(DefaultParameterSetName="ByProperty")]
    param(
        [Parameter(ValueFromPipeline=$true)]
        [PSObject]$InputObject,
        [Parameter(Position=1,Mandatory=$true,ParameterSetName="ByProperty")]
        [string]$Property,
        [Parameter(Position=2,ParameterSetName="ByProperty")]
        [object]$Value,
        [Parameter(Position=1,Mandatory=$true,ParameterSetName="ScriptBlock")]
        [Alias("Script")]
        [ScriptBlock]$FilterScript,
        [Parameter()]
        [switch]
        $Any,
        [Parameter()]
        [switch]
        $IgnoreImmutable,
        [Parameter()]
        [switch]
        $PassThru,
        [Parameter()]
        [switch]
        $AllOutput,
        [Parameter()]
        [int]
        $IntervalSec=1,
        [Parameter()]
        [int]
        $TimeoutSec=60
    )

    begin
    {
        $objects = @()
        $watch = New-Object System.Diagnostics.StopWatch
        $watch.Start()
        $firstChecked = $false
    }

    process
    {
        foreach($o in $InputObject)
        {
            $objects += $o
        }
    }

    end
    {
        while($true)
        {
            $remains = @()
            foreach($o in $objects)
            {
                if($firstChecked)
                {
                    if($o.Refresh)
                    {
                        $o.Refresh()
                    }
                }

                if($null -ne $FilterScript)
                {
                    if($o|&{process{&$FilterScript}})
                    {
                        if($PassThru)
                        {
                            if((!$IgnoreImmutable -or ($IgnoreImmutable -and $firstChecked)))
                            {
                                $o
                            }
                        }
                    }
                    else
                    {
                        $remains += $o
                    }
                }
                else
                {
                    if($Value -eq $o.$Property  -and (!$IgnoreImmutable -or ($IgnoreImmutable -and $firstChecked)))
                    {
                        if($PassThru)
                        {
                            if((!$IgnoreImmutable -or ($IgnoreImmutable -and $firstChecked)))
                            {
                                $o
                            }
                        }
                    }
                    else
                    {
                        $remains += $o
                    }
                }
            }

            if($remains.Length -eq 0)
            {
                break
            }
            elseif($Any -and $remains.Length -lt $objects.Length)
            {
                if($AllOutput -and $PassThru)
                {
                    $remains
                }
                break
            }
            elseif($watch.Elapsed.TotalSeconds -ge $TimeoutSec)
            {
                if($AllOutput -and $PassThru)
                {
                    $remains
                }
                break
            }
            
            $objects = @($remains)
            $remains = @()
            
            $firstChecked = $true

            Start-Sleep -Seconds $IntervalSec
        }
    }
}
コマンド構文
Wait-State [-Property] <string> [[-Value] <Object>] [-InputObject <psobject>] [-Any] [-IgnoreImmutable] [-PassThru] [-AllOutput] [-IntervalSec <int>] [-TimeoutSec <int>]  [<CommonParameters>]

Wait-State [-FilterScript] <scriptblock> [-InputObject <psobject>] [-Any] [-IgnoreImmutable] [-PassThru] [-AllOutput] [-IntervalSec <int>] [-TimeoutSec <int>]  [<CommonParameters>]
パラメータ

-InputObject:入力オブジェクト。パイプライン入力可。
-Property:変更を確認するプロパティ名。
-Value:-Propertyで指定のプロパティ値が、このパラメータに指定する値になるまで待機する。
-FilterScript:プロパティを指定する代わりに待機条件をスクリプトブロックで指定する。
-Any:入力のどれか一つが条件を満たすまで待機するようにする。(省略時は入力が全部条件を満たすまで待機)
-PassThru:入力オブジェクトが待機条件を満たした時点で、そのオブジェクトを出力する。省略時は出力なし。
-IgnoreImmutable:最初から条件を満たしている場合は出力しない。-PassThruと併用。
-AllOutput:タイムアウトした場合や-Any指定時に一部のオブジェクトしか出力していない場合でも、最終的に未出力のすべてのオブジェクトを出力してから終了する。-PassThruと併用。
-IntervalSec:プロパティ値のチェック、もしくは待機条件スクリプトの実行の間隔秒数を指定。デフォルト1秒。
-TimeoutSec:最大待機秒数。デフォルト60秒。この時間を過ぎると条件を満たしていなくても待機を終了する。

使用例
# 停止しているサービスがすべて開始するまで待機する。
Get-Service |? Status -eq Stopped | Wait-State -Property Status -Value Running

# 上記と同じだが、開始したサービスを逐次表示する。
Get-Service |? Status -eq Stopped | Wait-State -Property Status -Value Running -PassThru

# 停止しているサービスが少なくとも1つ開始するまで待機する。
Get-Service |? Status -eq Stopped | Wait-State -Property Status -Value Running -Any

# プロセスのワーキングセットが100MBを超えた段階で逐次表示する。
Get-Process | Wait-State {$_.WorkingSet -ge 100MB} -PassThru

# 上記と同じだが、最初から100MBを超えてるものは出力しない。
Get-Process | Wait-State {$_.WorkingSet -ge 100MB} -PassThru -IgnoreImmutable

# ディレクトリ内のファイル容量がすべて50KBを超えるまで待機し、出力のFileInfo配列を変数に代入。
$files = Get-ChildItem | Wait-State {$_.Length -ge 50KB} -PassThru -AllOutput -TimeoutSec 3600
問題点

プロパティ値を取得するときにリアルタイムに値が反映されないオブジェクト(要するにGetした時点のプロパティ値がずっと固定されてるもの)に対しては正しく動作しません。というか、PowerShellで扱うオブジェクトはほとんどそうなんじゃないかと思います(汗

ServiceControllerオブジェクト、Processオブジェクト、FileInfoオブジェクト、DirectoryInfoオブジェクトについては、Refreshメソッドを実行すると、プロパティ値を現在の値に更新してくれるので、それを利用してプロパティ値を監視できるようにはしています。

それ以外についても監視できるようにするには、たぶんそれぞれのオブジェクトに応じた監視方法を地道に調査して実装していくしかないんじゃないかなあと思います。

INotifyPropertyChangedインターフェースを実装したクラスについては、PropertyChangedイベントをSubscribeしてプロパティ値の変更を追跡できるようにしてみようとちょっと思ったんですが、PowerShellで扱うオブジェクトにINotifyPropertyChangedを実装したクラスのものってそんなにあるんだろうか?と疑問を覚えたのでやめました。

WMIオブジェクトについては何か共通の方法でプロパティ値変更を監視できないかなあと思ったんですが、結局IntervalSec間隔でクエリを発行する方法になってしまい、低コストで行う方法がちょっと思いつきませんでした。

ただ、-FilterScriptパラメータをサポートしているので、ここに書くことでいかようにも待機条件をカスタマイズできるので、極端な話、条件スクリプトブロックに{(Get-Hoge -Name $_.Name).Property -eq “ほげ”}みたいなコードを書いてゴリ押しすることもできるかと思います。

感想

というわけで、なんだか微妙な成果になって恐縮ですが、なんで無いんだろうと思っていた関数を実際に書いてみると、無い理由が分かったりするものなんだなあ、と思ったりした次第です。

スクリプトの解説を何もしてないですが、あえて解説する程のものでもないこともないですが、まあ長くなるのでやめときます。

ただ、入力オブジェクトを一旦全部取得してから、後続パイプラインに流し込む例としていくらか参考になるかもしれません。(beginで入れ物を用意して、processで詰めて、endでメインの処理を書くだけですけど)

あとはフィルタースクリプトブロックの実装方法の一例としても参考になるかも? スクリプトブロックを二重にして$_に対象オブジェクトがきちんと格納されるようにする方法、若干トリッキーな気もしますが正式にはどう書くのが良いのか不明なのでこうしてみました。


Copyright © 2005-2016 Daisuke Mutaguchi All rights reserved

mailto: mutaguchi at roy.hi-ho.ne.jp

Awards

Books

Twitter