Posh-SSH icon indicating copy to clipboard operation
Posh-SSH copied to clipboard

Invoke-SSHStreamShellCommand - properly wait until expected prompt, not a fixed time

Open JoseAPortilloJSC opened this issue 2 years ago • 3 comments

Hello darkoperator, Would it be possible to rewrite the Invoke-SSHStreamShellCommand cmdlet so it is more robust on a real network environment? I mean, not to wait for a hardcoded amount of time and then read all received text so far. The current implementation can lead to:

  • empty response (if the remote device takes more than 500ms to start responding and/or a network delay occurs)
  • truncated response (if the remote device takes more than 500ms to send the whole content and/or a network delay occurs).

The proper approach would be to wait for the expected prompt. It would suffice to implement it like done in Invoke-SSHStreamExpectAction, using $ShellStream.Expect(), something like this:

function Invoke-SSHStreamShellCommand {
    [CmdletBinding()]
    [Alias()]
    [OutputType([string])]  # proper output type
    Param (
    ...
        # Number of seconds to wait for a match.
        [Parameter(Mandatory=$false,
                   ValueFromPipelineByPropertyName=$true,
                   Position=3)]
        [int]
        $TimeOut = 10
    ...
    Process {
        Write-Verbose -Message "Executing command $($Command)."
        $ShellStream.WriteLine($Command)
        $ShellStream.ReadLine() | Out-Null  # trash command echo

        Write-Verbose -Message "Matching on pattern $($promptRegEx)"
        $out = $ShellStream.Expect($promptRegEx, (New-TimeSpan -Seconds $TimeOut))}

        $outputLines = $out.Split("`n")
        foreach ($line in $outputLines) {
            if ($line -notmatch $promptRegEx) {
                $line
            }
        }
    }
}

Thank you! Regards Jose

JoseAPortilloJSC avatar Aug 16 '23 09:08 JoseAPortilloJSC

I had the same problem too and can confirm the fix with Expect() works much better. Previously, I was having duplicate responses for multiple sent commands but now, every command sent has the expected response.

gabfv avatar Nov 02 '23 13:11 gabfv

This is the function I came up with. I usually include it in my code after 'Import-Module -Name Posh-SSH' to override the original in the module. I'm glad the Expect() version works better :)

# Custom override of Posh-SSH library's Invoke-SSHStreamShellCommand with Expect
function Invoke-SSHStreamShellCommand {
    [CmdletBinding()]
    [Alias()]
    [OutputType([string])]
    Param (
        # SSH stream to use for command execution.
        [Parameter(Mandatory = $true,
                   ValueFromPipelineByPropertyName = $true,
                   Position = 0)]
        [Renci.SshNet.ShellStream]
        $ShellStream,

        # Command to execute on SSHStream.
        [Parameter(Mandatory = $true,
                   ValueFromPipelineByPropertyName = $true,
                   Position = 1)]
        [string]
        $Command,

        [Parameter(Mandatory = $false,
                   ValueFromPipelineByPropertyName = $true,
                   Position=2)]
        [Alias("PrompPattern")]
        [string]
        $PromptPattern = '[\$%#>] $',

        # Number of seconds to wait for a match.
        [Parameter(Mandatory = $false,
                   ValueFromPipelineByPropertyName = $true,
                   Position=3)]
        [int]
        $TimeOut = 10
    )
    
    Begin {
        $promptRegEx = [regex]$PromptPattern
    }
    Process {
        Write-Verbose -Message "Executing command $($Command)."
        $ShellStream.WriteLine($Command)
        $ShellStream.ReadLine() | Out-Null  # trash command echo

        Write-Verbose -Message "Matching on pattern $($promptRegEx)"
        $out = $ShellStream.Expect($promptRegEx, (New-TimeSpan -Seconds $TimeOut))

        if ($out -ne $null) {
            $outputLines = $out.Split("`n")
            foreach ($line in $outputLines) {
                if ($line -notmatch $promptRegEx) {
                    $line
                }
            }
        }
    }
    End{}
}

JoseAPortilloJSC avatar Nov 02 '23 13:11 JoseAPortilloJSC

Thanks for sharing. The original function was a a example one I wrote for vyos and got the same questions on how to read and write to a stream so much I simply copied it as a command. Will look in to modifying it. Will need to set some time to setup some vms for it given I no longer do SSH automation

darkoperator avatar Nov 02 '23 21:11 darkoperator

Improved the regex and added timeout support. if it still has issues I will switch to expect. Update in release 3.2.2

darkoperator avatar Sep 01 '24 01:09 darkoperator