Search code examples
powershellpowershell-remoting

Is it possible to send 'out of band data' from a remote Invoke-Command call?


I have a system in which I'm remoting into a single machine at a time and running commands, scripts, etc. It would be useful to be able to effectively return log messages from the remote script in "realtime". Some code to get an idea of what I'm trying to do.

Note that both the local Log-*Msg functions log to a database (and tee to standard out/err as appropriate). Also note that we have analogous Log-*Msg methods on the remote side (loaded from a module) that are meant to pitched back across the wire and recorded in the DB as if the local Log-*Msg function was called.

Local Methods

function Exec-Remote {
  param(
    [ValidateNotNull()]
    [System.Management.Automation.Runspaces.PSSession]
    $Session=$(throw "Session is mandatory ($($MyInvocation.MyCommand))"),

    $argumentList,
    $scriptBlock
  )

  if($argumentList -is [scriptblock]) {$scriptBlock = $argumentList}
  if($scriptBlock -eq $null) { throw 'Scriptblock is required'}

  Invoke-Command -Session $Session -ArgumentList $argumentList -scriptBlock $scriptBlock | Filter-RemoteLogs
}

Filter Filter-RemoteLogs {
  if($_ -isnot [string]) { return $_ }

  if($_.StartsWith('Log-VerboseMsg:')) {
    Log-VerboseMsg $_.Replace("Log-VerboseMsg:", "") | Out-Null
    return
  }
  if($_.StartsWith('Log-WarningMsg:')) {
    Log-WarningMsg $_.Replace("Log-WarningMsg:", "") | Out-Null
    return
  }
  if($_.StartsWith('Log-UserMsg:')) {
    Log-UserMsg $_.Replace("Log-UserMsg:", "") | Out-Null
    return
  }
  else { return $_ }
}

Example Remote Method

On the remote side I have a module that gets loaded with a few logging functions, here's one such function:

function Log-VerboseMsg {
  param([ValidateNotNullOrEmpty()] $msg)

  "Log-VerboseMsg:$msg"
}

For the most part it works, I can do the following

$val = Exec-Remote -Session $PSSession {
  Log-VerboseMsg 'A test log message!'
  return $true
}

And have it do the right thing transparently.

However, it fails in the following scenario.

$val = Exec-Remote -Session $PSSession {
  function Test-Logging {
    Log-VerboseMsg 'A test log message!'
    return $true
  }
  $aVariable = Test-Logging
  Do-ALongRunningOperation

  return $aVariable
}

The above will not return anything until the 'long running operation' completes.

My question to you is the following.

Is there a way for me to reliably do this in Powershell? In some form, if the approach I'm using is really that terrible, feel free to lambast me and explain why.

NOTE: connecting to the DB from the remote environment and recording the log messages will not always be possible, so while that approach could work, for my specific needs it isn't sufficient.


Solution

  • In PowerShell v5 you can use new information stream for this. You should modify local functions as following:

    function Exec-Remote {
      param(
        [ValidateNotNull()]
        [System.Management.Automation.Runspaces.PSSession]
        $Session=$(throw "Session is mandatory ($($MyInvocation.MyCommand))"),
    
        $argumentList,
        $scriptBlock
      )
    
      if($argumentList -is [scriptblock]) {$scriptBlock = $argumentList}
      if($scriptBlock -eq $null) { throw 'Scriptblock is required'}
    
      # 6>&1 will redirect information stream to output, so Filter-RemoteLogs can process it.
      Invoke-Command -Session $Session -ArgumentList $argumentList -scriptBlock $scriptBlock 6>&1 | Filter-RemoteLogs
    }
    
    Filter Filter-RemoteLogs {
      # Function should be advanced, so we can call $PSCmdlet.WriteInformation.
      [CmdletBinding()]
      param(
        [Parameter(ValueFromPipeline)]
        [PSObject]$InputObject
      )
    
      if(
        # If it is InformationRecord.
        ($InputObject -is [Management.Automation.InformationRecord]) -and
        # And if it come from informational steam.
        ($WriteInformationStream=$InputObject.PSObject.Properties['WriteInformationStream']) -and
        ($WriteInformationStream.Value)
      ) {
        # If it is our InformationRecord.
        if($InputObject.Tags-contains'MyLoggingInfomation') {
          # Write it to log.
          &"Log-$($InputObject.MessageData.LogType)Msg" $InputObject.MessageData.Message | Out-Null
        } else {
          # Return not our InformationRecord to informational stream.
          $PSCmdlet.WriteInformation($InputObject)
        }
      } else {
        # Return other objects to output stream.
        $PSCmdlet.WriteObject($InputObject)
      }
    }
    

    And remote logging functions should write to information stream:

    function Log-VerboseMsg {
      param([ValidateNotNullOrEmpty()] $msg)
      Write-Information ([PSCustomObject]@{Message=$msg;LogType='Verbose'}) MyLoggingInfomation
    }
    function Log-WarningMsg {
      param([ValidateNotNullOrEmpty()] $msg)
      Write-Information ([PSCustomObject]@{Message=$msg;LogType='Warning'}) MyLoggingInfomation
    }
    function Log-UserMsg {
      param([ValidateNotNullOrEmpty()] $msg)
      Write-Information ([PSCustomObject]@{Message=$msg;LogType='User'}) MyLoggingInfomation
    }