0

I'm running the DTEXEC.exe command from within a PowerShell script, trying to capture and log the output to a file. Sometimes the output is incomplete and I'm trying to figure out why this the case and what might be done about it. The lines that never seem to get logged are the most interesting:

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

The output always seems incomplete on packages that execute in less than ~ 8 seconds and this might be a clue (there isn't much output or they finish quickly).

I'm using .NETs System.Diagnostics.Process and ProcessStartInfo to setup and run the command, and I'm redirecting stdout and stderror to event handlers that each append to a StringBuilder which is subsequently written to disk.

The problem feels like a timing issue or a buffering issue. To solve the timing issue, I've attempted to use Monitor.Enter/Exit. If it's a buffering issue, I'm not sure how to force the Process to not buffer stdout and stderror.

The environment is - PowerShell 2 running CLR version 2 - SQL 2008 32-bit DTEXEC.exe - Host Operating System: XP Service Pack 3.

Here's the code:

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 Answers 2

0

My 2 cents...its not a powershell issue but an issue/bug in the System.Diagnostics.Process class and underlying shell. I've seen times when wrapping the StdError and StdOut does not catch everything, and other times when the 'listening' wrapper application will hang indefinitly because of HOW the underlying application writes to the console. (in the c/c++ world there are MANY different ways to do this, [e.g. WriteFile, fprintf, cout, etc])

In addition there are more than 2 outputs that may need to be captured, but the .net framework only shows you those two (given they are the two primary ones) [see this article about command redirection here as it starts to give hints).

My guess (for both your issue as well as mine) is that it has to do with some low-level buffer flushing and/or ref counting. (If you want to get deep, you can start here)

One (very hacky) way to get around this is instead of executing the program directly to actually execute wrap it in a call to cmd.exe with 2>&1, but this method has its own pitfalls and issues.

The most ideal solution is for the executable to have a logging parameter, and then go parse the log file after the process exits...but most of the time you don't have that option.

But wait, we're using powershell...why are you using System.Diagnositics.Process in the first place? you can just call the command directly:

$output = & (GetDTExecPath) /FILE "$fileName" /CHECKPOINTING OFF /REPORTING "EWP"
Sign up to request clarification or add additional context in comments.

Comments

0

The reason that your output is truncated is that Powershell returns from WaitForExit() and sets the HasExited property before it has processed all the output events in the queue.

One solution it to loop an arbitrary amount of time with short sleeps to allow the events to be processed; Powershell event processing appear to not be pre-emptive so a single long sleep does not allow events to process.

A much better solution is to also register for the Exited event (in addition to Output and Error events) on the Process. This event is the last in the queue so if you set a flag when this event occurs then you can loop with short sleeps until this flag is set and know that you have processed all the output events.

I have written up a full solution on my blog but the core snippet is:

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.