PHP SSH command queue and connection pool β runs commands on an SSH host concurrently with completion callbacks.
composer install # install deps (requires ext-ssh2)
./vendor/bin/phpunit # run all tests
./vendor/bin/phpunit --filter Foo # run specific test
./vendor/bin/phpunit --coverage-text # run tests with coverage report- Namespace:
Detain\SshPool\βsrc/ - Entry:
src/SshPool.phpβ single class, all pool logic - Tests:
tests/SshPoolTest.phpβ PHPUnit + Mockery - Autoload: PSR-4 via
composer.json
SshPool flow:
- Construct with
($host, $port, $user, $pass, $pubKey, $privKey)β callsconnect()viassh2_connect()+ssh2_auth_pubkey_file() - Queue commands:
addCommand(string $cmd, ?string $id, $data, ?callable $callback, int $timeout): string - Execute:
run()loops β fills slots up to$maxThreads, callsprocessRunningCommands(),retryQueuedCommands() - Single synchronous exec:
runCommand(string $cmd): arrayβ returns['cmd', 'exitStatus', 'out', 'err']
Key properties:
$maxThreads = 50Β·$maxRetries = 0Β·$waitRetry = 15Β·$minConfigSize = 0$cmdQueue[]Β·$running[]Β·$queueAfter[]Β·$stdout[]Β·$stderr[]Β·$callbacks[]Β·$callbackData[]
Callback signature: function(string $cmd, string $id, $data, int $exitStatus, string $stdout, string $stderr)
Retry logic (handleCommandCompletion): retries when $exitStatus !== 0 OR strlen($stdout) < $minConfigSize and $retries < $maxRetries; defers via $queueAfter with time() + $waitRetry
Stream pattern (setupStreams / processRunningCommands):
$streamOut = ssh2_exec($this->conn, $cmd, 'vt100');
$streamErr = ssh2_fetch_stream($streamOut, SSH2_STREAM_STDERR);
stream_set_blocking($streamOut, false);
stream_set_blocking($streamErr, false);
// on EOF: stream_set_blocking($streamOut, true); stream_get_meta_data() for exit_statusExample usage:
$pool = new SshPool($host, 22, $user, $pass, $pubKey, $privKey);
$pool->addCommand('uptime', 'cmd1', null, function($cmd, $id, $data, $exitStatus, $stdout, $stderr) {
echo "Exit: $exitStatus, Output: $stdout\n";
});
$pool->run();$iddefaults touniqid()inaddCommand()β callers can supply stable IDsrun(true)= single-pass (non-blocking poll);run()= full blocking loopadjustThreads()clamps$maxThreadstomax(1, count($this->running))on stream failure- Output normalization:
rtrim(str_replace("\r", '', $stdout)) - Tests use
Mockery::mock(SshPool::class)->makePartial()->shouldAllowMockingProtectedMethods(); always callMockery::close()intearDown() - Require
ext-ssh2β document in any environment setup
IMPORTANT: Before every git commit, you MUST ensure Caliber syncs agent configs with the latest code changes.
First, check if the pre-commit hook is already installed:
grep -q "caliber" .git/hooks/pre-commit 2>/dev/null && echo "hook-active" || echo "no-hook"- If hook-active: the hook handles sync automatically β just commit normally. Tell the user: "Caliber will sync your agent configs automatically via the pre-commit hook."
- If no-hook: run Caliber manually before committing:
- Tell the user: "Caliber: Syncing agent configs with your latest changes..."
- Run:
caliber refresh && git add CLAUDE.md .claude/ .cursor/ .cursorrules .github/copilot-instructions.md .github/instructions/ AGENTS.md CALIBER_LEARNINGS.md 2>/dev/null - After it completes, briefly tell the user what Caliber updated. Then proceed with the commit.
If caliber is not found, tell the user: "This project uses Caliber for agent config sync. Run /setup-caliber to get set up."
Read CALIBER_LEARNINGS.md for patterns and anti-patterns learned from previous sessions.
These are auto-extracted from real tool usage β treat them as project-specific rules.