【问题标题】:Captured Output of command run by PowerShell Is Sometimes IncompletePowerShell 运行的命令的捕获输出有时不完整
【发布时间】:2014-09-01 16:09:12
【问题描述】:

我在 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
}

【问题讨论】:

    标签: powershell powershell-2.0


    【解决方案1】:

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

    一种解决方案是循环任意数量的时间和短睡眠以允许处理事件; Powershell 事件处理似乎不是抢先式的,因此单个长时间睡眠不允许事件处理。

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

    我已经写了一个完整的解决方案on my blog,但核心 sn-p 是:

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

    【讨论】:

      【解决方案2】:

      我的 2 美分...这不是 powershell 问题,而是 System.Diagnostics.Process 类和底层 shell 中的问题/错误。我曾经见过包装 StdError 和 StdOut 并不能捕获所有内容的时候,而在其他时候,由于底层应用程序写入控制台的方式,“侦听”包装器应用程序将无限期挂起。 (在 c/c++ 世界中,有许多不同的方法可以做到这一点,[例如 WriteFile、fprintf、cout 等])

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

      我的猜测(对于您和我的问题)是它与一些低级缓冲区刷新和/或引用计数有关。 (想深入,可以入手here

      解决此问题的一种(非常老套的)方法不是直接执行程序来实际执行,而是使用 2>&1 将其包装在对 cmd.exe 的调用中,但这种方法有其自身的缺陷和问题。

      最理想的解决方案是让可执行文件有一个日志参数,然后在进程退出后去解析日志文件……但大多数时候你没有那个选项。

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

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

      【讨论】:

        猜你喜欢
        • 2014-10-27
        • 2023-03-25
        • 2018-10-01
        • 2018-05-04
        • 2014-07-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多