Question

Scenario: I needed a function to get STDOUT of a command run through SSH asynchronously. This has various uses including (and most importantly) reading files through SSH. An important feature of this function is that it is asynchronous, hence I can display output as it is returned from the server (or give an estimate of the file download progress). This differs from the common approach of using ssh_move() to download files.

function ssh_exec($dsn, $cmd, $return=true, $size_est=null){
    $BUFFER_SIZE = $return ? (1024 * 1024) : 1;
    debug('ssh2_exec '.$cmd);
    $stream = ssh2_exec(ssh_open($dsn), $cmd);
    debug('stream_set_blocking');
    stream_set_blocking($stream, true);
    debug('ssh2_fetch_stream');
    $stream_out = ssh2_fetch_stream($stream, SSH2_STREAM_STDIO);
    stream_set_blocking($stream_out, true);
    debug('stream_get_contents');
    $data = ''; $stime = $oldtime = microtime(true); $data_len = 0;
    if(!$return){
        write_message("\033[0m".'  Execution Output:'.PHP_EOL.'    ');
    }
    while(!feof($stream_out)){
        $buff = fread($stream_out, $BUFFER_SIZE);
        if($buff===false)throw new Exception('Unexpected result from fread().');
        if($buff===''){
            debug('Empty result from fread()...breaking.');
            break;
        }
        $data .= $buff;
        if($return){
            $buff_len = strlen($buff);
            $data_len += $buff_len;
            $newtime = microtime(true);
            debugo('stream_get_contents '.bytes_to_human($data_len)
                .' @ '.bytes_to_human($buff_len / ($newtime - $oldtime)).'/s'
                .' ['.($size_est ? number_format($data_len / $size_est * 100, 2) : '???').'%]');
            $oldtime = $newtime;
        }else{
            echo str_replace(PHP_EOL, PHP_EOL.'    ', $buff);
        }
    }
    if($return){
        debugo('stream_get_contents Transferred '.bytes_to_human(strlen($data)).' in '.number_format(microtime(true) - $stime, 2).'s');
        return $data;
    }
}

Usage: The function is used like so:

$dsn = 'ssh2.exec://root:pass@host/';
$size = ssh_size($dsn, 'bigfile.zip');
$zip = ssh_exec($dsn, 'cat bigfile.zip', true, $size);

Note 1: An explanation of some non-standard functions:

  • debug($message) - Writes debug message to console.
  • ssh_open($dsn) - Takes in an SSH URI and returns an SSH connection handle.
  • bytes_to_human($bytes) - Converts the number of bytes to human readable format (eg: 6gb)
  • debugo($message) - The same as debug() but overwrites last line.

Note 2: The parameter $size_est is used in the progress indicator; usually you'd first get the file size and then attempt to download it (as in my example). It is optional so that it can be ignored when you just want to run an SSH command.

The Problem: Running the same download operation via scp root@host:/bigfile.zip ./, I get speeds up to 1mb/s whereas this script seems to limit to 70kb/s. I'd like to know why and how to improve this.

Edit: Also, I'd like to know how/if $BUFFER_SIZE does any difference.

Was it helpful?

Solution

You should be able to use phpseclib, a pure PHP SSH implementation, to do this. eg.

$ssh->exec('cat bigfile.zip', false);

while (true) {
    $temp = $this->_get_channel_packet(NET_SSH2_CHANNEL_EXEC);
    if (is_bool($temp)) {
        break;
    }
    echo $temp;
}

Might actually be faster too.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top