Octopus Deploy provides the ability to run PowerShell scripts natively. However, Octopus Deploy can only consume 3 streams when generating logging information:

  • Standard out
  • Warning
  • Error

Replacing use of Write-Verbose with Write-Output (standard out) within a script is often a flawed proposition. Doing so affects the behaviour of the script. This problem is demonstrated by the following example:

function Get-Value {
    Write-Verbose 'Getting a value'
    if (Get-Random -Minimum 0 -Maximum 2) {
        'Value'
    }
}
if (Get-Value) {
    Set-Value
}

Sending verbose to standard out in this scenario would break the script logic.

Rather than redirect within the source code, a better solution is to handle each stream separately. To handle these streams a PowerShell host implementation is required.

A PowerShell host can be created as follows:

$initialSessionState = [InitialSessionState]::CreateDefault()
$psHost = [PowerShell]::Create($initialSessionState)

A command or script can be executed under the host, either synchronously, or asynchronously. As the goal is to provide Octopus with access to logging information an asynchronous approach is more appropriate.

The example below shows asynchronous execution:

$initialSessionState = [InitialSessionState]::CreateDefault()
$psHost = [PowerShell]::Create($initialSessionState)
$asyncResult = $psHost.AddScript('Write-Verbose "Output" -Verbose').BeginInvoke()
do {
    Start-Sleep -Milliseconds 100
} until ($asyncResult.IsCompleted)

The host exposes access to streams such as Verbose via the streams property.

$psHost.Streams

Error       : {}
Progress    : {}
Verbose     : {Output}
Debug       : {}
Warning     : {}
Information : {}

Each of the streams implements a DataAdded event. When executing a command asynchronously the event can be used to react and retrieve information.

PS> Get-Member -InputObject $pshost.Streams.Verbose -Name DataAdded

   TypeName: System.Management.Automation.PSDataCollection`1[[System.Management.Automation.VerboseRecord,
System.Management.Automation, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]]

Name      MemberType Definition
----      ---------- ----------
DataAdded Event      System.EventHandler`1[System.Management.Automation.DataAddedEventArgs] DataAdded(System.Object,...

Register-ObjectEvent and Get-Event can be used to work with the event.

Adding these commands to the asynchronous example shows how the message might be accessed and sent to standard out.

$initialSessionState = [InitialSessionState]::CreateDefault()
$psHost = [PowerShell]::Create($initialSessionState)

Register-ObjectEvent $psHost.Streams.Verbose -EventName DataAdded

$asyncResult = $psHost.AddScript('Write-Verbose "Output" -Verbose').BeginInvoke()
do {
    Start-Sleep -Milliseconds 100
    foreach ($event in (Get-Event)) {
        $event.Sender[$event.SourceEventArgs.Index]
        $event | Remove-Event
    }
} until ($asyncResult.IsCompleted)

Rewriting the value at this point does not interfere with the executing command.

The script can be extended to handle the Debug and Information streams using the same method. The events are already correctly ordered. Removing events after handling each prevents repetition.

At this stage the script is executing, and verbose output is gathered, but standard out is not ignored. The wrapper script must be improved to retrieve any other output. To do this, a PSDataCollection must be supplied.

The PSDataCollection, like verbose stream, includes a DataAdded event. The content of the loop does not need to change to work with this.

using namespace System.Management.Automation

$initialSessionState = [InitialSessionState]::CreateDefault()
$psHost = [PowerShell]::Create($initialSessionState)

$inputObject = [PSDataCollection[PSObject]]::new()
$outputObject = [PSDataCollection[PSObject]]::new()

Register-ObjectEvent $outputObject -EventName DataAdded
Register-ObjectEvent $psHost.Streams.Verbose -EventName DataAdded

$script = '
    Write-Verbose "Verbose output" -Verbose
    "Standard output"
'
$asyncResult = $psHost.AddScript($script).
                       BeginInvoke($inputObject, $outputObject)
do {
    Start-Sleep -Milliseconds 100
    foreach ($event in (Get-Event)) {
        $event.Sender[$event.SourceEventArgs.Index]
        $event | Remove-Event
    }
} until ($asyncResult.IsCompleted)

This approach is the basis for a script which implements more flexible support for output streams. The script sits within the confines of a custom task template.