0

PowerShell スクリプト内から DTEXEC.exe コマンドを実行し、出力をキャプチャしてファイルに記録しようとしています。出力が不完全な場合があり、その理由と、それに対して何ができるかを理解しようとしています。ログに記録されていないように見える行は、最も興味深いものです。

DTEXEC: The package execution returned DTSER_SUCCESS(0)
Started:  10:58:43 a.m.
Finished: 10:59:24 a.m.
Elapsed:  41.484 seconds

~ 8 秒未満で実行されるパッケージでは、出力は常に不完全なように見えます。

私は .NETs System.Diagnostics.Process と ProcessStartInfo を使用してコマンドをセットアップして実行し、stdout と stderror をイベント ハンドラーにリダイレクトしています。イベント ハンドラーはそれぞれ StringBuilder に追加され、その後ディスクに書き込まれます。

問題は、タイミングの問題またはバッファリングの問題のように感じます. タイミングの問題を解決するために、Monitor.Enter/Exit を使用しようとしました。バッファリングの問題である場合、プロセスが stdout と stderror をバッファリングしないようにする方法がわかりません。

環境は - CLR バージョン 2 を実行する PowerShell 2 - SQL 2008 32 ビット DTEXEC.exe - ホスト オペレーティング システム: XP Service Pack 3 です。

コードは次のとおりです。

function Execute-SSIS-Package
{
    param([String]$fileName)

    $cmd = GetDTExecPath

    $proc = New-Object System.Diagnostics.Process
    $proc.StartInfo.FileName = $cmd
    $proc.StartInfo.Arguments = "/FILE ""$fileName"" /CHECKPOINTING OFF /REPORTING ""EWP"""
    $proc.StartInfo.RedirectStandardOutput = $True
    $proc.StartInfo.RedirectStandardError  = $True
    $proc.StartInfo.WorkingDirectory = Get-Location
    $proc.StartInfo.UseShellExecute = $False
    $proc.StartInfo.CreateNoWindow = $False

    Write-Host $proc.StartInfo.FileName $proc.StartInfo.Arguments

    $cmdOut = New-Object System.Text.StringBuilder

    $errorEvent = Register-ObjectEvent -InputObj $proc `
        -Event "ErrorDataReceived" `
        -MessageData $cmdOut `
        -Action `
        {
            param
            (
                [System.Object] $sender,
                [System.Diagnostics.DataReceivedEventArgs] $e
            )

            try
            {
                [System.Threading.Monitor]::Enter($Event.MessageData)
                Write-Host -ForegroundColor "DarkRed" $e.Data
                [void](($Event.MessageData).AppendLine($e.Data))
            }
            catch
            {
                Write-Host -ForegroundColor "Red" "Error capturing processes std error" $Error
            }
            finally
            {
                [System.Threading.Monitor]::Exit($Event.MessageData)
            }
        }

    $outEvent = Register-ObjectEvent -InputObj $proc `
        -Event "OutputDataReceived" `
        -MessageData $cmdOut `
        -Action `
        {
            param
            (
                [System.Object] $sender,
                [System.Diagnostics.DataReceivedEventArgs] $e
            )
            try
            {
                [System.Threading.Monitor]::Enter($Event.MessageData)
                #Write-Host $e.Data
                [void](($Event.MessageData).AppendLine($e.Data))
            }
            catch
            {
                Write-Host -ForegroundColor "Red" "Error capturing processes std output" $Error
            }
            finally
            {
                [System.Threading.Monitor]::Exit($Event.MessageData)
            }
        }

    $isStarted = $proc.Start()

    $proc.BeginOutputReadLine()
    $proc.BeginErrorReadLine()

    while (!$proc.HasExited)
    {
        Start-Sleep -Milliseconds 100
    }

    Start-Sleep -Milliseconds 1000

    $procExitCode = $proc.ExitCode
    $procStartTime = $proc.StartTime
    $procFinishTime = Get-Date

    $proc.Close()

    $proc.CancelOutputRead()
    $proc.CancelErrorRead()

    $result = New-Object PsObject -Property @{
        ExitCode = $procExitCode
        StartTime = $procStartTime
        FinishTime = $procFinishTime
        ElapsedTime = $procFinishTime.Subtract($procStartTime)
        StdErr = ""
        StdOut = $cmdOut.ToString()
    }

    return $result
}
4

2 に答える 2

1

出力が切り捨てられる理由は、Powershell が WaitForExit() から戻り、キュー内のすべての出力イベントを処理する前に HasExited プロパティを設定するためです。

解決策の 1 つは、イベントを処理できるように短いスリープで任意の時間をループすることです。Powershell のイベント処理はプリエンプティブではないように見えるため、1 回の長いスリープではイベントを処理できません。

より良い解決策は、Process で (Output イベントと Error イベントに加えて) Exited イベントも登録することです。このイベントはキューの最後であるため、このイベントが発生したときにフラグを設定すると、このフラグが設定されるまで短いスリープでループでき、すべての出力イベントを処理したことがわかります。

ブログに完全な解決策を書きましたが、コア スニペットは次のとおりです。

# Set up a pair of stringbuilders to which we can stream the process output
$global:outputSB = New-Object -TypeName "System.Text.StringBuilder";
$global:errorSB = New-Object -TypeName "System.Text.StringBuilder";
# Flag that shows that final process exit event has not yet been processed
$global:myprocessrunning = $true

$ps = new-object System.Diagnostics.Process
$ps.StartInfo.Filename = $target
$ps.StartInfo.WorkingDirectory = Split-Path $target -Parent
$ps.StartInfo.UseShellExecute = $false
$ps.StartInfo.RedirectStandardOutput = $true
$ps.StartInfo.RedirectStandardError = $true
$ps.StartInfo.CreateNoWindow = $true

# Register Asynchronous event handlers for Standard and Error Output
Register-ObjectEvent -InputObject $ps -EventName OutputDataReceived -action {
    if(-not [string]::IsNullOrEmpty($EventArgs.data)) {
        $global:outputSB.AppendLine(((get-date).toString('yyyyMMddHHmm')) + " " + $EventArgs.data)
    }
} | Out-Null
Register-ObjectEvent -InputObject $ps -EventName ErrorDataReceived -action {
    if(-not [string]::IsNullOrEmpty($EventArgs.data)) {
        $global:errorSB.AppendLine(((get-date).toString('yyyyMMddHHmm')) + " " + $EventArgs.data)
    }
} | Out-Null
Register-ObjectEvent -InputObject $ps -EventName Exited -action {
    $global:myprocessrunning = $false
} | Out-Null

$ps.start() | Out-Null
$ps.BeginOutputReadLine();
$ps.BeginErrorReadLine();

# We set a timeout after which time the process will be forceably terminated
$processTimeout = $timeoutseconds * 1000
while (($global:myprocessrunning -eq $true) -and ($processTimeout -gt 0)) {
    # We must use lots of shorts sleeps rather than a single long one otherwise events are not processed
    $processTimeout -= 50
    Start-Sleep -m 50
}
if ($processTimeout -le 0) {
    Add-Content -Path $logFile -Value (((get-date).toString('yyyyMMddHHmm')) + " PROCESS EXCEEDED EXECUTION ALLOWANCE AND WAS ABENDED!")
    $ps.Kill()
}

# Append the Standard and Error Output to log file, we don't use Add-Content as it appends a carriage return that is not required
[System.IO.File]::AppendAllText($logFile, $global:outputSB)
[System.IO.File]::AppendAllText($logFile, $global:errorSB)
于 2014-08-14T23:09:39.517 に答える
0

私の2セント...それはpowershellの問題ではなく、System.Diagnostics.Processクラスと基礎となるシェルの問題/バグです。StdError と StdOut をラップしてもすべてがキャッチされない場合や、基になるアプリケーションがコンソールに書き込む方法が原因で、「リッスン」ラッパー アプリケーションが無期限にハングする場合があります。(c/c++ の世界では、これを行うさまざまな方法があります [例: WriteFile、fprintf、cout など])

さらに、キャプチャする必要がある可能性のある出力が 2 つ以上ありますが、.net フレームワークはそれらの 2 つしか表示しません (それらが 2 つの主要なものである場合) [ヒントを提供し始めるので、ここでコマンドのリダイレクトに関するこの記事を参照してください)。

私の推測では(あなたの問題と私の両方の問題について)、低レベルのバッファフラッシュおよび/または参照カウントに関係しているということです。(深く知りたい場合は、ここから開始できます)

これを回避する 1 つの (非常にハックな) 方法は、プログラムを直接実行して実際に実行する代わりに、2>&1 を使用して cmd.exe への呼び出しでラップすることですが、この方法には独自の落とし穴と問題があります。

最も理想的な解決策は、実行可能ファイルにログ パラメータを設定し、プロセスの終了後にログ ファイルを解析することですが、ほとんどの場合、そのオプションはありません。

しかし、待ってください、私たちは powershell を使用しています...そもそもなぜ System.Diagnositics.Process を使用しているのですか? コマンドを直接呼び出すことができます:

$output = & (GetDTExecPath) /FILE "$fileName" /CHECKPOINTING OFF /REPORTING "EWP"
于 2012-10-30T19:54:45.267 に答える