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 属性。

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

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

我在我的博客上写了一个完整的解决方案,但核心片段是:

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

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

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

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

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

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

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