2

I'm working on a build environment for our JavaScript project. We use require.js (r.js) to combine different js modules into one output js file. We use TeamCity and I wanted to configure Powershell build step that would call r.js, read it's standard output and exit code and pass that exit code back to TeamCity (by exiting from Powershell with this exit code) so that if the tasks fails (r.js comes back with exit code 1) it won't proceed with other build steps. Also I wanted the standard output of r.js to be saved in the TeamCity log to allow developers to quickly see the error causing r.js to stop.

This is the way how I can start r.js process with it's arguments, read it's exit code and use it to exit from Powershell:

$process = start-process r.js.cmd -ArgumentList "-o build-dev.js" -PassThru -Wait
exit $process.ExitCode

If I try to read standard output in this way before exiting:

Write-Host $process.StandardOutput.ReadToEnd();

I get this error, which probably suggests that I can't read StandardOutput in this way as it is a stream:

You cannot call a method on a null-valued expression. At line:1 char:45 + Write-Host $process.StandardOutput.ReadToEnd <<<< (); + CategoryInfo : InvalidOperation: (ReadToEnd:String) [], Runtime Exception + FullyQualifiedErrorId : InvokeMethodOnNull

Process exited with code 1

Now I found a way of running the process so that I can read the standard output but I'm not able to read the exit code:

$psi = New-object System.Diagnostics.ProcessStartInfo 
$psi.UseShellExecute = $false
$psi.RedirectStandardOutput = $true 
$psi.RedirectStandardError = $true 
#$psi.FileName = "r.js.cmd"
$psi.FileName = "C:\Users\Administrator\AppData\Roaming\npm\r.js.cmd"
$psi.Arguments = @("-o build-dev.js")
#$psi.WorkingDirectory = (Get-Location).Path;
$process = New-Object System.Diagnostics.Process 
$process.StartInfo = $psi 
$process.Start() | Out-Host
$process.WaitForExit()
$output = $process.StandardOutput.ReadToEnd()
$stderr = $process.StandardError.ReadToEnd()
sleep(10)
$exit_code = $process.ExitCode

write-host "========== OUTPUT =========="
write-host $output
write-host "========== ERROR =========="
write-host $stderr
write-host "========== EXIT CODE =========="
write-host $exit_code
write-host "========== $LastExitCode =========="
#write-host $LastExitCode
#Exit $exit_code

But again this returns the console output but the exit code is always 0 even if r.js returns 1 because I have error in my js scripts.

Could anyone advise how can I read standard output with start-process or how can I read exit code with New-object System.Diadnostics.ProcessStartInfo

I can attach screenshots with more details of my TC build step configuration and output saved to the build log if that would help answering the question.

6
  • 3
    Can't you just use $result = r.js.cmd -o build-dev.js; $exit_code = $LastExitCode? Commented Mar 7, 2014 at 17:11
  • Is 'r.js.cmd' a CMD script? If so, are you certain that it actually generating an exit code? If the CMD script is launching other process that does the build, it will need to not only report the exit code but also to bubble it up e.g. exit %buildProcessExitCode% Commented Mar 8, 2014 at 22:29
  • If link below is r.is.cmd, then it does NOT generate exit code. Will need to be modified to do so. Or rewritten in PS. code.google.com/p/yarse/source/browse/node_modules/.bin/… Commented Mar 8, 2014 at 22:34
  • @KeithHill If I try this: $result = r.js.cmd -o build-dev.js; $exit_code = $LastExitCode It gives me this: Result: Error: Line 14: Unexpected token { ... Which is fine as I intentionally created an error in js file to force r.js to fail and return error details and exit with code 1. So as you can see I get the standard output. But it gives me exit code 0 and not 1. I get correct exit code when I use start-process. Commented Mar 10, 2014 at 9:53
  • @andyb good suggestion, I will give it a try. I noticed that exit code is correctly read when using start-process with -PassThru -Wait and the -PassThru according to link Returns a process object for each process that the cmdlet started. By default, this cmdlet does not generate any output. This is probably the reason why powershell is still able to read the exit code from the cmd script running the node process that itself doesn't pass it. If I remove -PassThru I'm not able to read exit code correctly anymore. Commented Mar 10, 2014 at 10:12

2 Answers 2

1

I usually tackle this situation by using a variation of the start-process command mentioned in the question.

$outputLog = "outputFile.log"
$errLog = "errorFile.log"
$process = Start-Process r.js.cmd -ArgumentList "-o build-dev.js" -PassThru -RedirectStandardOutput $outputLog -RedirectStandardError $errLog -Wait
$exitCode = $process.ExitCode

Now the log files $outputLog and $errorLog will have the standard output and error contents.

Sign up to request clarification or add additional context in comments.

Comments

0

This is how you can do it. Cmd files are however not applications, they are associated with cmd so you might have to target cmd.exe and put the path to the cmd file as a parameter. Also UseShellExecute for process start info stops output and errors from being recorded so dont enable it if you need output. UseShellExecute is as if you put the $Path into the Win+R window. If its a png for example, windows will find an app that is supposed to open it and opens that file with it.

$Path = "C:\whatever.exe"
$WorkingDirectory = "C:\"
$CreateNoWindow = $true #$true to not create another window for console application
$Parameters = "-paremeters for -the app"
$WindowStyle = [Diagnostics.ProcessWindowStyle]::Normal

# prepare process start info
$processStartInfo = New-Object -TypeName 'System.Diagnostics.ProcessStartInfo' -ErrorAction 'Stop'
$processStartInfo.FileName = $Path
$processStartInfo.WorkingDirectory = $WorkingDirectory
$processStartInfo.ErrorDialog = $false
$processStartInfo.RedirectStandardOutput = $true
$processStartInfo.RedirectStandardError = $true
$processStartInfo.CreateNoWindow = $CreateNoWindow
If ($Parameters) { $processStartInfo.Arguments = $Parameters }
$processStartInfo.WindowStyle = $WindowStyle

#create a process and assign the process start info object to it
$process = New-Object -TypeName 'System.Diagnostics.Process' -ErrorAction 'Stop'
$process.StartInfo = $processStartInfo

#Add event handler to capture process's standard output redirection
[scriptblock]$processEventHandler = { If (-not [string]::IsNullOrEmpty($EventArgs.Data)) { $Event.MessageData.AppendLine($EventArgs.Data) } }
#create string builders to store the output and errors in
$stdOutBuilder = New-Object -TypeName 'System.Text.StringBuilder' -ArgumentList ''
$stdOutEvent = Register-ObjectEvent -InputObject $process -Action $processEventHandler -EventName 'OutputDataReceived' -MessageData $stdOutBuilder -ErrorAction 'Stop'
$stdErrBuilder = New-Object -TypeName 'System.Text.StringBuilder' -ArgumentList ''
$stdErrEvent = Register-ObjectEvent -InputObject $process -Action $processEventHandler -EventName 'ErrorDataReceived' -MessageData $stdErrBuilder -ErrorAction 'Stop'

#start the process
$null = $process.Start()
#begin reading the output and errors
$process.BeginOutputReadLine()
$process.BeginErrorReadLine()
#Instructs the Process component to wait indefinitely for the associated process to exit.
$process.WaitForExit()
#HasExited indicates that the associated process has terminated, either normally or abnormally. Wait until HasExited returns $true.
While (-not ($process.HasExited)) { $process.Refresh(); Start-Sleep -Seconds 1 }

## Get the exit code for the process
Try {
    [int32]$returnCode = $process.ExitCode
}
Catch [System.Management.Automation.PSInvalidCastException] {
    #  Catch exit codes that are out of int32 range
    [int32]$returnCode = 1
}

#unregister the events
If ($stdOutEvent) { Unregister-Event -SourceIdentifier $stdOutEvent.Name -ErrorAction 'Stop'; $stdOutEvent = $null }
If ($stdErrEvent) { Unregister-Event -SourceIdentifier $stdErrEvent.Name -ErrorAction 'Stop'; $stdErrEvent = $null }
$stdOut = $stdOutBuilder.ToString() -replace $null,''
$stdErr = $stdErrBuilder.ToString() -replace $null,''
## Free resources associated with the process, this does not cause process to exit
If ($process) { $process.Dispose() }
# check if we have output and return it
If ($stdOut) {
    Write-Output "Process output:$stdOut"
}

If ($stdErr) {
    Write-Output "Process errors:$stdErr"
}

# return exit code
Write-Output "Exit code:$returnCode"
Start-Sleep -Seconds 5
Exit $returnCode

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.