Process: Spouštění externích programů
Třída Nette\Utils\Process umožňuje
z PHP spouštět externí programy: posílat jim vstup, číst jejich výstup a reagovat na to, jak skončily. Je to
přívětivý obal nad PHP funkcí proc_open(), který hlásí chyby vyhazováním výjimek místo vracení
false.
Instalace:
composer require nette/utils
Všechny příklady předpokládají vytvořený alias:
use Nette\Utils\Process;
Nejjednodušší použití
Chcete spustit program a přečíst, co vypsal? Stačí tohle:
$process = Process::runExecutable('git', ['log', '-1', '--format=%H']);
echo $process->getStdOutput();
První argument je program, který se má spustit, druhý je seznam jeho argumentů: totéž, co byste napsali na příkazové
řádce, jen rozdělené do pole. Metoda getStdOutput() počká, až program doběhne, a vrátí vše, co zapsal na
svůj standardní výstup.
To je celá myšlenka: spustíte proces a pak se ho ptáte: běží ještě, co vypsal, jak skončil. Zbytek této stránky probírá tyto otázky jednu po druhé.
Spuštění procesu
Proces lze spustit dvěma způsoby a stojí za to ten rozdíl pochopit.
static runExecutable(string
$executable, array $arguments=[], ?array $env=null, array $options=[], mixed $stdin='',
mixed $stdout=null, mixed $stderr=null, ?string $directory=null, ?float $timeout=60): Process
Spustí jeden konkrétní program se seznamem argumentů. Argumenty se programu předají přímo, takže nikdy nemusíte escapovat mezery, uvozovky ani jiné speciální znaky. A protože nevstupuje do hry žádný shell, nehrozí shell injection. Tohle je bezpečná volba, zvlášť když část příkazu pochází z uživatelského vstupu:
$file = $_GET['file']; // could be anything, even '; rm -rf /'
$process = Process::runExecutable('wc', ['-l', $file]); // perfectly safe
Pokud neuvedete plnou cestu, program se hledá v systémové proměnné PATH. Pro spuštění PHP skriptu se
hodí konstanta PHP_BINARY:
$process = Process::runExecutable(PHP_BINARY, ['-v']);
static runCommand(string $command,
?array $env=null, array $options=[], mixed $stdin='', mixed $stdout=null, mixed
$stderr=null, ?string $directory=null, ?float $timeout=60):
Process
Spustí příkaz zadaný řetězcem přes systémový shell (/bin/sh na Linuxu a macOS,
cmd.exe na Windows). Tím získáte možnosti shellu: roury |, přesměrování >,
expanzi proměnných, řetězení příkazů přes && a podobně:
$process = Process::runCommand('git log --oneline | head -n 20');
Protože ale shell celý řetězec parsuje, nikdy nesestavujte řetězec pro runCommand() z nedůvěryhodného
vstupu, což je klasická bezpečnostní díra. Když si nejste jisti, použijte raději runExecutable().
Při tolika parametrech je předávejte jako pojmenované argumenty, např.
Process::runExecutable('git', ['pull'], timeout: 30). Pole $options se předává přímo do
proc_open() pro pokročilé potřeby, jako třeba bypass_shell na Windows.
Proces běží na pozadí
Po spuštění proces běží souběžně s vaším PHP skriptem: runExecutable() i
runCommand() se vrátí okamžitě a nečekají, až proces doběhne. Vy rozhodujete, kdy (a zda) na něj
počkáte:
$process = Process::runExecutable('npm', ['install']);
// ... do other work here while npm runs ...
$process->wait(); // now block until it's done
V praxi voláte wait() přímo jen zřídka, protože getStdOutput(), getExitCode(),
isSuccess() i ensureSuccess() na proces počkají automaticky, než vám dají odpověď.
wait() zavolejte explicitně tehdy, když mu chcete předat callback.
isRunning(): bool
Vrací true, dokud proces běží, a false, jakmile doběhl nebo byl ukončen. Hodí se, když chcete
mezitím dělat něco jiného:
while ($process->isRunning()) {
// do something else for a while
usleep(100_000); // 100 ms
}
Jak skončil?
Každý dokončený proces má návratový kód: konvenčně 0 znamená úspěch a jakékoli jiné
číslo nějaký druh selhání (co přesně, záleží na programu).
getExitCode(): int
Vrátí návratový kód; pokud je potřeba, nejprve počká, až proces doběhne:
$code = Process::runExecutable('git', ['pull'])->getExitCode(); // e.g. 0
isSuccess(): bool
Zkratka pro „rovná se návratový kód 0?":
$process = Process::runExecutable('git', ['pull']);
if (!$process->isSuccess()) {
echo 'git failed: ' . $process->getStdError();
}
ensureSuccess(): void
Často chcete prostě jen to, aby program uspěl, a jinak hlasitě selhat. ensureSuccess() počká na proces a
vyhodí Nette\Utils\ProcessFailedException, pokud návratový kód není 0:
Process::runExecutable('git', ['pull'])->ensureSuccess();
// execution continues only if git succeeded
Čtení výstupu
Proces má dva oddělené výstupní proudy: standardní výstup (běžné výsledky) a chybový výstup (kam programy obvykle hlásí problémy a diagnostiku). Nette Utils tyto dva drží odděleně a ve výchozím nastavení oba zachytává do paměti, takže si je můžete přečíst, kdykoli budete chtít.
getStdOutput(): string
Počká, až proces doběhne, a vrátí vše, co zapsal na standardní výstup:
$process = Process::runExecutable('date');
echo $process->getStdOutput();
getStdError(): string
Totéž, ale pro chybový výstup:
$process = Process::runExecutable('some-tool', ['--do-stuff']);
if (!$process->isSuccess()) {
throw new RuntimeException('The tool failed: ' . $process->getStdError());
}
Pokud výstupní proud přesměrujete jinam (do souboru, do resource
nebo na false), není v paměti co vracet a příslušný getter vyhodí
Nette\InvalidStateException.
consumeStdOutput(): string
Někdy chcete vidět výstup tak, jak přichází, bez čekání na konec procesu, třeba abyste zobrazili průběh. Každé volání vrátí kus standardního výstupu, který se objevil od předchozího volání:
$process = Process::runExecutable('long-running-tool');
while ($process->isRunning()) {
echo $process->consumeStdOutput(); // prints whatever is new
usleep(100_000); // 100 ms
}
echo $process->consumeStdOutput(); // the final piece, produced just before it ended
Volání uvnitř poslední iterace smyčky už vrátí i to, co proces vypsal těsně před koncem, takže to volání
za smyčkou je jen pojistka. Pro chybový výstup existuje obdobné consumeStdError().
Sledování výstupu naživo
Místo pollování přes consumeStdOutput() můžete metodě wait() předat callback. Ten se zavolá
pokaždé, když se objeví nový výstup, což se hodí pro průběžné logování nebo přeposílání výstupu jinam:
$process = Process::runExecutable('npm', ['install']);
$process->wait(function (string $stdOut, string $stdErr) {
echo $stdOut; // forward standard output
fwrite(STDERR, $stdErr); // and standard error
});
Callback dostane dva řetězce: nová data standardního výstupu a nová data chybového výstupu od předchozího volání
(kterýkoli může být prázdný). Když se wait() vrátí, proces je dokončený a stále můžete volat
getExitCode(), getStdOutput() a další.
Posílání vstupu
Parametr $stdin říká, co proces čte na svém standardním vstupu. Přijímá několik různých věcí.
Řetězec se stane celým vstupem procesu:
$process = Process::runExecutable('wc', ['-c'], stdin: 'hello world');
echo $process->getStdOutput(); // 11
Čitelný resource (otevřený soubor, stream) se do vstupu zkopíruje:
$file = fopen('data.csv', 'r');
$process = Process::runExecutable('sort', stdin: $file);
null ponechá vstup otevřený, takže do něj můžete zapisovat postupně (viz níže).
Výchozí hodnotou je prázdný řetězec, což znamená, že proces dostane prázdný, okamžitě uzavřený vstup. To je rozumné výchozí chování: zabrání tomu, aby programy, které čtou vstup, navždy visely a čekaly na něco, co nikdy nepřijde.
writeStdInput(string $string): void
Když proces spustíte s stdin: null, vstup zůstane otevřený a vy ho plníte po částech. Až skončíte,
zavolejte closeStdInput(). Tím programu řeknete, že už žádný vstup nepřijde (pošle se konec
souboru, EOF):
$process = Process::runExecutable('some-repl', stdin: null);
$process->writeStdInput("first command\n");
$process->writeStdInput("second command\n");
$process->closeStdInput();
echo $process->getStdOutput();
Řetězec nebo stream předaný jako $stdin se zapíše celý najednou, ještě než se proces
pořádně rozběhne. Pokud je tento vstup velký a zároveň program produkuje hodně výstupu, aniž by si nejdřív
přečetl vstup, mohou obě strany uváznout v čekání na sebe navzájem. V tom (vzácném) případě použijte
stdin: null a writeStdInput(), abyste prokládali zápis čtením.
Řetězení procesů (piping)
Standardní výstup jednoho procesu můžete napojit přímo na standardní vstup druhého, přesně jako rourou |
v shellu. Stačí jako $stdin předat Process:
$producer = Process::runExecutable('cat', ['big.log']);
$consumer = Process::runExecutable('grep', ['error'], stdin: $producer);
echo $consumer->getStdOutput();
Zřetězit můžete libovolný počet procesů (a | b | c).
Řetězení procesů není podporováno na Windows (vyhodí Nette\NotSupportedException). Na
Windows zachyťte výstup prvního procesu pomocí getStdOutput() a předejte ho dalšímu jako řetězec.
Přesměrování výstupu jinam
Ve výchozím nastavení se standardní i chybový výstup zachytávají do paměti. Parametry $stdout a
$stderr umožňují poslat je místo toho jinam.
Název souboru pošle výstup do tohoto souboru:
Process::runExecutable('mysqldump', ['mydb'], stdout: 'backup.sql')
->ensureSuccess();
Zapisovatelný resource pošle výstup do tohoto streamu. Musí být podložený skutečným souborem (ne
php://memory apod.):
$log = fopen('build.log', 'a');
Process::runExecutable('make', stdout: $log, stderr: $log);
false výstup úplně zahodí (jde do /dev/null, resp. NUL na Windows):
Process::runExecutable('noisy-tool', stderr: false);
Přesměrování zároveň drží spotřebu paměti nízko: zachytávání do paměti je pohodlné, ale proces, který vypíše gigabajty, by spotřeboval gigabajty RAM, takže takový výstup zapisujte do souboru.
Proměnné prostředí
Parametr $env nastavuje proměnné prostředí, které proces uvidí. Ponechte null (výchozí), aby
zdědil prostředí aktuálního procesu, nebo předejte pole a nastavte si je sami:
// the current environment plus one extra variable
$process = Process::runExecutable('printenv', ['MY_VAR'], env: ['MY_VAR' => '123'] + getenv());
// a completely empty environment
$process = Process::runExecutable('some-tool', env: []);
Pracovní adresář
Parametr $directory nastavuje adresář, ve kterém proces startuje (výchozí je ten aktuální):
$process = Process::runExecutable('git', ['status'], directory: '/path/to/repo');
Časový limit
Parametr $timeout (v sekundách, výchozí 60) omezuje, jak dlouho budete na proces čekat. Pokud se
limit překročí během toho, co na proces čekáte nebo čtete jeho výstup, proces se zabije a vyhodí se
Nette\Utils\ProcessTimeoutException. Předáním null limit zrušíte:
$process = Process::runExecutable('slow-tool', timeout: 5.0);
try {
$process->wait();
} catch (Nette\Utils\ProcessTimeoutException $e) {
echo 'The tool took too long and was terminated.';
}
Limit se kontroluje jen tehdy, když jste uvnitř wait(), getExitCode(), getterů výstupu nebo
consume*(). Proces, který spustíte a pak na něj nikdy nečekáte, jím zabit není.
Ukončení procesu
terminate(): void
Okamžitě zabije proces, pokud ještě běží; pokud už doběhl, neudělá nic:
$process = Process::runExecutable('server');
// ...
$process->terminate();
Proces se také ukončí automaticky, když je jeho objekt Process zničen (například opustí platnost) a
ještě nedoběhl.
getPid(): ?int
Vrátí ID procesu (PID) operačního systému, dokud proces běží, nebo null, jakmile doběhl:
$pid = $process->getPid();
Když se něco pokazí
Chyby se vždy hlásí vyhozením výjimky, nikdy návratovou hodnotou:
Nette\Utils\ProcessFailedException |
proces se nepodařilo spustit, nebo byla zavolána ensureSuccess() a návratový kód nebyl 0 |
Nette\Utils\ProcessTimeoutException |
byl překročen limit daný parametrem $timeout |
Nette\InvalidArgumentException |
jako $stdin, $stdout nebo $stderr byla předána neplatná hodnota |
Nette\IOException |
soubor zadaný jako $stdout nebo $stderr se nepodařilo otevřít |
Nette\InvalidStateException |
čtení výstupu, který nebyl zachytáván, nebo zápis do STDIN, který je už zavřený |
Nette\NotSupportedException |
o řetězení procesů se pokusilo na Windows |
ProcessFailedException a ProcessTimeoutException dědí z PHP RuntimeException.