detain / sshpool
SSH command queue and connection pool runs tons of commands on a ssh host concurrently with completion callback.
Requires
- ext-ssh2: *
This package is auto-updated.
Last update: 2026-05-01 05:48:36 UTC
README
detain/sshpool is a small PHP library for running many shell commands
on a single SSH host concurrently. It maintains one SSH session, opens
multiple ssh2_exec channels in parallel up to a configurable cap,
collects each command's stdout / stderr / exit status, and fires a
per-command callback on completion. Commands can be queued while the
pool is already running, automatic retries with a back-off delay are
supported, and per-command timeouts kill long-running work.
Requirements
- PHP 8.0+
- The
ext-ssh2PHP extension - An SSH host you can authenticate to with a key pair
Password authentication is not currently wired through; the supported path is public/private key authentication via
ssh2_auth_pubkey_file.
Installation
composer require detain/sshpool
If you do not already have ext-ssh2 installed:
# Debian / Ubuntu sudo apt-get install php-ssh2 # RHEL / Alma / Rocky sudo dnf install php-pecl-ssh2
Quick Start
require 'vendor/autoload.php'; use Detain\SshPool\SshPool; $pool = new SshPool( 'example.com', // host 22, // port 'deploy', // ssh user '', // password (unused — pubkey auth) '/home/me/.ssh/id_rsa.pub', '/home/me/.ssh/id_rsa' ); $pool->setMaxThreads(10); $pool->setMaxRetries(2); foreach (['uptime', 'df -h', 'free -m'] as $cmd) { $pool->addCommand( $cmd, null, // auto id ['command' => $cmd], // user data passed to callback function ($cmd, $id, $data, $exit, $stdout, $stderr) { echo "[{$id}] {$cmd} (exit {$exit}):\n{$stdout}\n"; if ($stderr !== '') { fwrite(STDERR, "[{$id}] stderr: {$stderr}\n"); } }, 30 // 30s timeout ); } $pool->run();
Usage Patterns
One-shot synchronous command
For a single command where you want a synchronous result rather than a
callback, use runCommand():
$result = $pool->runCommand('hostname'); // $result === ['cmd' => 'hostname', 'exitStatus' => 0, 'out' => '...', 'err' => '']
runCommand() returns false if the channel cannot be opened.
Adding commands while the pool is running
addCommand() is safe to call from inside a completion callback. New
commands join $cmdQueue and the run loop picks them up on its next
iteration.
$pool->addCommand('first', null, null, function ($cmd, $id, $data, $exit) use ($pool) { if ($exit === 0) { // Chain a follow-up command. $pool->addCommand('second'); } }); $pool->run();
Non-blocking single iteration
run(true) performs one polling iteration (start any newly-available
commands, drain finished output, promote any due retries) and returns.
This is useful when you want to integrate the pool into your own event
loop.
while (!$done) { $done = $pool->run(true); // ... do other work ... }
Custom SSH connection methods
ssh2_connect() accepts an algorithm/method override array. Pass it
through the constructor's seventh argument when, for example, you need
to whitelist a deprecated host key algorithm against a legacy server:
$pool = new SshPool( $host, 22, $user, '', $pub, $priv, ['hostkey' => 'ssh-rsa'] );
Configuration
| Setter | Default | Effect |
|---|---|---|
setMaxThreads(int) |
50 |
Maximum number of channels open concurrently. Clamped to >= 1. |
setMaxRetries(int) |
0 |
Retry attempts on a non-zero exit status (or undersized output). 0 disables retry. |
setWaitRetry(int) |
15 |
Seconds to wait before promoting a retry back into the run queue. |
setMinConfigSize(int) |
0 |
Minimum stdout byte count required to consider a command successful. 0 disables. |
$pollInterval (public int) |
25000 |
Microseconds between run-loop polls in blocking mode. |
Public API
new SshPool( string $host, int $port, string $user, string $pass, string $pubKey, string $privKey, array $methods = [] ); string addCommand( string $cmd, ?string $id = null, mixed $data = null, ?callable $callback = null, int $timeout = 0 ); bool run(bool $once = false); array|false runCommand(string $cmd); void connect(); void disconnect(); resource|null getConnection();
Callback signature
function ( string $cmd, string $id, mixed $data, // user data passed to addCommand() int $exitStatus, // -1 if unknown / timed out string $stdout, string $stderr ): void;
Retry Semantics
A command is considered failed (and eligible for retry) when either:
- its exit status is non-zero, or
minConfigSize > 0andstrlen(stdout) < minConfigSize.
Failed commands are deferred for waitRetry seconds, then promoted
back into the run queue. The original timeout is preserved across
retries. After maxRetries attempts the completion callback is
invoked with the most recent exit status.
Timeouts
When addCommand($cmd, ..., $timeout) is given a non-zero $timeout,
the run loop closes the command's streams once
time() - start_time > $timeout. The callback is invoked just like
any other completion. If a real exit status was not yet observed, the
command is reported with exitStatus = -1.
The metadata property captures the actual duration and a boolean
timed_out flag per id:
$pool->metadata['my-id'] === [ 'exit_status' => -1, 'duration' => 30, 'timed_out' => true, ];
Development
composer install ./vendor/bin/phpunit # run the test suite ./vendor/bin/phpunit --filter Foo # run one test ./vendor/bin/phpunit --coverage-text
The tests use a FakeSshPool fixture
(tests/Fixtures/FakeSshPool.php) that bypasses ext-ssh2 so the
queue / callback / retry logic is verified without needing a live SSH
server.
License
GPL-3.0. See LICENSE.
Author
Joe Huss · detain@interserver.net