HIDARI日記(右)

そのときどき興味ある技術を中心にだらだら書いてます。内容は個人の見解であり、所属する企業を代表するものではありません。

PowershellからMSBuildを動かす

まあ別段MSBuildに限った話ではなくて、外部プロセスを起動したいときは同じ考え方ができますが。

やりたいこと

MSBuild.exe MyApp.sln /t:Rebuild /p:Configuration=Release

みたいなのをラップして

Build-VsProject -Path .\MyApp.sln -Options "/t:Rebuild /p:Configuration=Release"

で実行可能にすることを目指します。

どうやるか

パッと浮かぶのは Start-Process か Call Operator & を使う方法。

ただ Start-Process にしろ & にしろ、コンソールからちょろっと使うにはいいけど、 スクリプトで使うのは終了コードの取り方が面倒だったりして辛いかも。

なので System.Diagnostics.Process と, プロセス実行時の設定を行うための System.Diagnostics.ProcessStartInfo を自前で作る方法で行くことにします。

.NETのクラスを使うほうが安心感ありますし(謎。

書いていきましょう

System.Diagnostics.Process はこんな感じで書ける。

$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = MSBuild.exe
$pinfo.Arguments = "Path\to\MyApp.sln /t:Rebuild /p:Configuration=Release"
$pinfo.RedirectStandardOutput = $true
$pinfo.RedirectStandardError = $true
$pinfo.UseShellExecute = $false

$proc = New-Object System.Diagnostics.Process
$proc.StartInfo = $pinfo
$proc.Start() | Out-Null

$proc.RedirectStandardOutputプロパティと$proc.RedirectStanderdErrorプロパティの設定はお好みで。 このプロパティをtrueにしておくと、起動したプロセスの標準出力を取得できるようになります。

この時の注意点として、RedirectStandardOutputプロパティやRedirectStanderdErrorプロパティをTrueにするときは必ずUseShellExecuteプロパティをFalseに設定する必要があります。しないと例外が飛んできます(http://msdn.microsoft.com/ja-jp/library/system.diagnostics.processstartinfo.redirectstandardoutput(v=vs.110).aspx)。

$stdOut = $proc.StandardOutput.ReadToEnd()
$errOut = $proc.StandardError.ReadToEnd()

という感じに使うことができます。

終了コード欲しいよね

実行したプロセスの終了コードは$proc.ExitCodeプロパティで取得可能なので $proc.WaitForExit()$proc.HasExitプロパティなんかと合わせて使います。

$proc.StartInfo = $pinfo
$proc.Start() | Out-Null
$proc.WaitForExit()

if($proc.ExitCode -eq 0){
    Write-Host "Build has Succeedes!!"
}
else{
    Write-Host "Build has failed..."
}

さて、これで上手くいくと思うじゃろ?

WaitForExit()の罠

そんなことはなく。

$proc.WaitForExit()にはハマリポイントがあります。

$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = MSBuild.exe
$pinfo.Arguments = "Path\to\MyApp.sln /t:Rebuild /p:Configuration=Release"
$pinfo.RedirectStandardOutput = $true
$pinfo.RedirectStandardError = $true
$pinfo.UseShellExecute = $false

$proc = New-Object System.Diagnostics.Process
$proc.StartInfo = $pinfo
$proc.Start() | Out-Null
$proc.WaitForExit()

$stdOut = $proc.StandardOutput.ReadToEnd()
$errOut = $proc.StandardError.ReadToEnd()

とか素直に書いてしまうとプロセスが終了せずに固まってしまいます。 原因はMSBuildからの出力によってStandardOutputのバッファいっぱいになり、そこで処理が止まってしまうため。

これをいい感じに吸い出してやる必要があります。そのためにOutputDataReceivedイベントと$proc.BeginOutputReadLine() を使って非同期に出力を読み取ります。

手順は以下の通り。

  1. BSBuildの出力を保存する追記可能なストアとしてStringBuilderを用意
  2. OutputDataReceivedイベントで行いたい処理をスクリプトブロックに記述
  3. OutputDataReceivedのイベントハンドラを登録
  4. プロセスを実行
  5. $proc.BeginOutputReadLine()で読み取り開始
  6. プロセス終了後に読み取った出力を適当に書き出す

この流れで書いたのが以下のコードです。

$proc = New-Object System.Diagnostics.Process
$proc.StartInfo = $pinfo

# 出力の格納先としてStringBuilderを用意
$stdOutBuilder = New-Object -TypeName System.Text.StringBuilder
$stdErrBuilder = New-Object -TypeName System.Text.StringBuilder

# イベントを利用して出力を書き出す
# まずスクリプトブロックにイベントで行いたい処理を記述
# ここではStringBuilderに出力を格納する
$action = {
    if(![string]::IsNullOrEmpty($EventArgs.Data)){
        $Event.MessageData.AppendLine($EventArgs.Data)
    }
}

# イベントハンドラを登録
$stdOutEvent = Register-ObjectEvent -InputObject $proc -Action $action -EventName "OutputDataReceived" -MessageData $stdOutBuilder
$stdErrEvent = Register-ObjectEvent -InputObject $proc -Action $action -EventName "ErrorDataReceived" -MessageData $stdErrBuilder

# プロセスを開始
[void]$proc.Start()

# 非同期で出力の読み取りを開始
$proc.BeginOutputReadLine()
$proc.BeginErrorReadLine()
$proc.WaitForExit()

# 最後に読み取った出力を書き出す
$stdOutBuilder.ToString() | Out-File ".\BuildLog.txt"
$stdErrBuilder.ToString() | Out-File ".\ErrorLog.txt"

思った以上に面倒なコードになりました。

コード全体

というわけで最終的に出来上がった?コードはこんな感じでした。ちょっと高度な関数風にしてみました。

function Build-VsProject
{
    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact="Low")]
    param
    (
        [Parameter(
            Mandatory=$true,
            Position=0
        )]
        [alias("SolutionPath")]
        [string]
        $Path,

        [string]
        $Options
    )

    begin
    {
        $msb="C:\Program Files (x86)\MSBuild\12.0\Bin\MSBuild.exe"
    }

    process
    {
        if(-not(Test-Path -Path $msb)){throw "MSBuildNotFoundException"}

        Write-Host "Start Build $Path"

        $pinfo = New-Object System.Diagnostics.ProcessStartInfo
        $pinfo.FileName = $msb
        $pinfo.Arguments = "$Path /t:Rebuild /p:Configuration=Release"
        $pinfo.RedirectStandardOutput=$true
        $pinfo.RedirectStandardError=$true
        $pinfo.UseShellExecute=$false
        $pinfo.CreateNoWindow=$true

        $proc = New-Object System.Diagnostics.Process
        $proc.StartInfo = $pinfo

        $stdOutBuilder = New-Object -TypeName System.Text.StringBuilder
        $stdErrBuilder = New-Object -TypeName System.Text.StringBuilder

        $action = {
            if(![string]::IsNullOrEmpty($EventArgs.Data)){
                $Event.MessageData.AppendLine($EventArgs.Data)
            }
        }

        $stdOutEvent = Register-ObjectEvent -InputObject $proc -Action $action -EventName "OutputDataReceived" -MessageData $stdOutBuilder
        $stdErrEvent = Register-ObjectEvent -InputObject $proc -Action $action -EventName "ErrorDataReceived" -MessageData $stdErrBuilder
        [void]$proc.Start()
        $proc.BeginOutputReadLine()
        $proc.BeginErrorReadLine()
        $proc.WaitForExit()

        Unregister-Event -SourceIdentifier $stdOutEvent.Name
        Unregister-Event -SourceIdentifier $stdErrEvent.Name

        $stdOutBuilder.ToString() | Out-File ".\BuildLog.txt"
        $stdErrBuilder.ToString() | Out-File ".\ErrorLog.txt"

        if($proc.ExitCode -eq 0){
            Write-Host "Build has Succeedes!!"
        }
        else{
            Write-Host "Build has failed..."
        }

        return $proc.ExitCode
    }

    end
    {
    }
}

おわりに

みたいなことを書こうとしてるところでぎたぱそ先生がPowerShell で System.Diagnostic.Process にて BeginOutputReadLine() を使う - tech.guitarrapc.cómを投稿しておられるのに気づいて会社でぶったまげたことをご報告致します。

さすがMVP。わかりやすい...脱帽するしかないけど、これからも強く儚く生きていこうと思います。よろしくお願いします。

またこの記事を書くにあたり発破をかけて下さった@Posauneさんと@irofさんに感謝。