Index

ShellRunner

MMO\core\server · 1 symbol · generated 30 Sep 2026

class

ShellRunner

ShellRunner.php:78

A uniform command runner on top of any iShell -- the local Shell or a remote SSH -- giving every command the same result object with a real exit code, plus dry-run support and the handful of filesystem operations (exists, read, write, symlink, ...) that a deployment needs to perform identically here and over ssh.

class MMO\core\server\ShellRunner
Namespace
MMO\core\server
See
Shell::execSafe keeps its exit code on the shell object instead). run gives code written once against iShell the same ShellResult either way. - run gets that exit code back IN BAND, on stdout: ( <cmd> ) 2>&1 ; printf "\n__MMO_EXIT__%d" "$?". Over ssh it therefore never depends on ssh2 managing to report an exit_status for the channel (which it only resolves once the channel is drained and closed) -- the status is just more command output. - merging stderr into stdout by default keeps a command's own progress messages (git, tar, mongodump & co. all write those to stderr) out of the failure path entirely, and sidesteps having to read two streams at once at all. - a command can be recorded instead of run ($dryRun), which is what makes a deployment plan reviewable.
Why this rather than calling `$shell->exec()` directly: - `iShell::exec()` reports failure by throwing, and `iShell::execSafe()` reports it differently on either implementation ([`SSH::execSafe`](https://mmoframework.fr/documentation/mmo.core/php/server/SSH.md#ssh-execsafe) takes a 4th by-reference argument the interface doesn't know about, and NOTE: this used to live in `MMO\admin\helpers`, where it was written to work around two [`SSH`](https://mmoframework.fr/documentation/mmo.core/php/server/SSH.md#ssh) bugs on top of the above: `exec()` throwing on any stderr output whatever the exit code, and [`SSHStream::getContents`](https://mmoframework.fr/documentation/mmo.core/php/server/SSHStream.md#sshstream-getcontents) deadlocking on a command that wrote a lot to both streams at once. Both are fixed in `SSH` itself now, so what's left here is the uniform result object, the in-band exit code, dry-run and the filesystem helpers.
$sh = new ShellRunner($ssh); if (!$sh->run('git pull')->isOk()) {...} // never silently "succeeds" $sh->exec('tar -xzf archive.tgz -C /tmp/x'); // throws, with the real output, on any non-zero exit

Index

MARKER
SHA256
dryRun
readOnly
sha256Command
shell
skipped
createDir($path, $mode)Create $path (and its parents) on the target.
debugName()
exec($cmd, $cwd)Run $cmd, throwing on any non-zero exit code.
exists($path)
getShell()
getSkipped()
isDir($path)
isReadOnly($cmd)
isRemote()
quote($value)Quote one value as a single argument for whichever shell this runner talks to.
read($path)
readlink($path)
receive($remote, $local)Copy a file off the target.
run($cmd, $cwd, $mergeStderr)Run $cmd and always come back with a real exit code.
send($local, $remote)Copy a local file onto the target (a plain copy when the shell is local).
sha256($path)
symlink($link, $to)Point the symlink $link at $to, atomically: a temporary link is created beside it and mv -T'd over it, so the link is never missing -- unlike rm && ln, which leaves a window where the site serv…
test($cmd, $cwd)
write($path, $content)Write $content to $path on the target, atomically (write beside it, then mv over it) so a reader can never observe a half-written file -- which matters for the journal.
__construct($shell, $dryRun)

Constants2

MARKER

const MARKER = '__MMO_EXIT__'

SHA256

const SHA256 = ['sha256sum', 'shasum -a 256', 'openssl dgst -r -sha256']

Properties5

dryRun

bool $dryRun
Type
bool

readOnly

array $readOnly = […6 items]
Type
array

sha256Command

protected ?string $sha256Command = null
Type
null | string

shell

protected MMO\core\interfaces\iShell $shell
Type
iShell

skipped

protected array $skipped = []
Type
array

Methods20

createDir($path, $mode)

ShellRunner.php:213

Create $path (and its parents) on the target.

function createDir(string $path, ?int $mode = null): void
$pathstring
$mode = nullnull | int
returnsvoid

isReadOnly($cmd)

ShellRunner.php:122
function isReadOnly(string $cmd): bool
$cmdstring
returnsbooltrue if $cmd only reads, and may therefore run even during a dry run.

quote($value)

ShellRunner.php:139

Quote one value as a single argument for whichever shell this runner talks to.

function quote(string $value): stringNOTE: every path below used to go through `escapeshellarg()`, which quotes for the shell of the machine PHP runs on -- it says nothing about the *target's*. Running on Windows, it wraps the value in double quotes, which a POSIX remote shell happily accepts while still expanding `$(...)`/backticks inside it. The local path keeps `escapeshellarg()` (it is right, whatever this machine runs), the remote one always gets POSIX single quotes ([`SSH::quote`](https://mmoframework.fr/documentation/mmo.core/php/server/SSH.md#ssh-quote)).
$valuestring
returnsstring

run($cmd, $cwd, $mergeStderr)

ShellRunner.php:151

Run $cmd and always come back with a real exit code.

function run(string $cmd, ?string $cwd = null, bool $mergeStderr = true): MMO\core\server\ShellResult
$cmdstring
$cwd = nullnull | stringthe working directory (passed to the shell, never interpolated into $cmd).
$mergeStderr = trueboolfold stderr into stdout (the default: avoids SSH's stderr-means-failure rule and the two-stream deadlock). Pass false when you genuinely need the two separated.
returnsShellResult

send($local, $remote)

ShellRunner.php:301

Copy a local file onto the target (a plain copy when the shell is local).

function send(string $local, string $remote): void
$localstring
$remotestring
returnsvoid
throws

sha256($path)

ShellRunner.php:279
function sha256(string $path): ?stringNOTE the three commands are tried in order, and the first that works is remembered for this runner. `sha256sum` is GNU coreutils, which a machine we install is certain to have and a MANAGED webspace frequently is not: shared hosts ship busybox or the BSD tools, where the command is `shasum -a 256`, and some ship neither but do have openssl. It matters because the caller cannot tell "no checksum" from "a different checksum" -- [`ShipTask`](https://mmoframework.fr/documentation/mmo.admin/php/tasks/ShipTask.md#shiptask) compares this against a local hash and concludes the upload was corrupted, which is a confusing way to be told that a tool is missing.
$pathstring
returnsnull | stringthe sha256 of a file on the target, null if nothing on it can produce one.

write($path, $content)

ShellRunner.php:231

Write $content to $path on the target, atomically (write beside it, then mv over it) so a reader can never observe a half-written file -- which matters for the journal.

function write(string $path, string $content): void
$pathstring
$contentstring
returnsvoid

__construct($shell, $dryRun)

ShellRunner.php:103
function __construct(MMO\core\interfaces\iShell $shell, bool $dryRun = false)
$shelliShell
$dryRun = falsebool