捕获的PowerShell运行命令输出有时不完整

时间:2012-10-28 22:25:45

标签: powershell powershell-v2.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
}

2 个答案:

答案 0 :(得分:0)

我的2美分......它不是powershell问题,而是System.Diagnostics.Process类和底层shell中的问题/错误。我已经看到包装StdError和StdOut没有捕获所有内容的时候,有时候'listen'包装器应用程序将因为底层应用程序如何写入控制台而无限期挂起。 (在c / c ++世界中有很多不同的方法,[例如WriteFile,fprintf,cout等])

此外,可能需要捕获超过2个输出,但.net框架仅显示这两个(假设它们是两个主要的)[请参阅有关命令重定向here的文章它开始给出提示)。

我的猜测(无论是你的问题还是我的问题)都是因为它与一些低级缓冲区刷新和/或引用计数有关。 (如果你想深入,你可以开始here

一种(非常hacky)解决这个问题的方法是直接执行程序以实际执行包含在cmd.exe中使用2>& 1的包装,但这种方法有其自身的缺陷和问题。 / p>

最理想的解决方案是让可执行文件具有日志记录参数,然后在进程退出后解析日志文件...但大多数情况下您没有该选项。

但是等等,我们正在使用PowerShell ......为什么你首先使用System.Diagnositics.Process?你可以直接调用命令:

$output = & (GetDTExecPath) /FILE "$fileName" /CHECKPOINTING OFF /REPORTING "EWP"

答案 1 :(得分:0)

输出被截断的原因是Powershell从WaitForExit()返回并在处理队列中的所有输出事件之前设置HasExited属性。

一种解决方案,它可以通过短暂的休眠来循环任意数量的时间以允许处理事件; Powershell事件处理似乎没有先发制人,因此单次长时间睡眠不允许事件处理。

更好的解决方案是在Process上注册Exited事件(除了Output和Error事件)。此事件是队列中的最后一个事件,因此如果您在此事件发生时设置了一个标志,那么您可以使用短暂休眠循环,直到设置此标志并知道您已处理了所有输出事件。

我已经写了一个完整的解决方案on my blog,但核心代码段是:

# 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)