Process: spouštění externích programů z PHP konečně bez bolesti
Spustit z PHP externí program a přečíst, co vypsal?
shell_exec('git pull') a je hotovo. Tohle ale stačí jen na ten
úplně nejjednodušší případ. Jakmile potřebujete znát návratový kód,
vidět chyby odděleně od běžného výstupu, bezpečně předat argument
z uživatelského vstupu nebo dát programu timeout, shell_exec()
přestane stačit. A proc_open(), který to všechno umí, má API
tak nepřívětivé, že se mu většina lidí raději vyhne.
V Nette Utils 4.1.4 přibyla třída Nette\Utils\Process,
která to dělá za vás. Spustíte proces a pak se ho ptáte: Běží ještě?
Co vypsal? Jak skončil? A ona se postará o roury, čekací smyčky
i o rozdíly mezi operačními systémy.
Jak spustit příkaz a získat návratový kód v PHP
Chcete spustit git pull a vědět, jestli prošel?
use Nette\Utils\Process;
Process::runExecutable('git', ['pull'])->ensureSuccess();
Tahle jediná řádka spustí git pull, počká na jeho
dokončení a vyhodí ProcessFailedException, pokud návratový
kód nebyl nula. Žádné kouzlení s exec() a
$retval, žádné is_resource(), žádná smyčka.
A když chcete výstup?
$process = Process::runExecutable('git', ['log', '-1', '--format=%H']);
echo $process->getStdOutput(); // standardní výstup
echo $process->getStdError(); // chybový výstup, zvlášť
echo $process->getExitCode(); // 0
getStdOutput() počká, až program doběhne, a vrátí
všechno, co zapsal na standardní výstup. Chybový výstup dostanete
odděleně, což shell_exec() neumí. Návratový kód taky. A na
rozdíl od nativních PHP funkcí třída při chybách nevrací
false, ale vyhazuje výjimky, takže žádné selhání
nepřehlédnete. (Potřebujete programu něco poslat na vstup? Předáte
stdin: 'data', nebo null a krmíte ho postupně přes
writeStdInput().)
runExecutable vs. runCommand (ochrana před shell injection)
Třída nabízí dva způsoby, jak proces spustit, a ten rozdíl není kosmetický.
runExecutable() spustí konkrétní program se seznamem
argumentů. Argumenty se programu předají přímo. Žádný shell se jich ani
nedotkne, takže nemusíte nic escapovat a hlavně nehrozí shell injection:
$file = $_GET['file']; // může to být cokoli, klidně '; rm -rf /'
$process = Process::runExecutable('wc', ['-l', $file]); // naprosto bezpečné
runCommand() naproti tomu předá celý řetězec
systémovému shellu (/bin/sh, na Windows
cmd.exe). Tím získáte roury, přesměrování, expanzi
proměnných, zkrátka všechno, co od shellu čekáte:
$process = Process::runCommand('git log --oneline | head -n 20');
Cena za pohodlí: shell ten řetězec parsuje, takže do něj nikdy
nedávejte neošetřený vstup zvenčí. Pravidlo je jednoduché: cokoli,
kde figuruje uživatelský vstup, jde přes runExecutable().
Streamování a čtení výstupu procesu v reálném čase
Spouštíte něco, co běží minuty (npm install,
composer update, build) a chcete vidět průběh, ne čekat na
finální výpis. Předejte wait() callback:
$process = Process::runExecutable('npm', ['install']);
$process->wait(function (string $stdOut, string $stdErr) {
echo $stdOut;
fwrite(STDERR, $stdErr);
});
Callback se zavolá pokaždé, když přijde nový výstup. Dostane to, co od
minulého volání přibylo na standardním a chybovém výstupu. Žádné
stream_select(), žádné přepínání rour do neblokujícího
režimu, žádné riziko deadlocku, když program vychrlí víc, než se vejde
do bufferu roury. O to vše se třída postará.
Nechcete callback? Odebírejte výstup sami metodou
consumeStdOutput(), vrátí kus, který přibyl od posledního
volání:
while ($process->isRunning()) {
echo $process->consumeStdOutput();
usleep(100_000);
}
echo $process->consumeStdOutput(); // doberte zbytek
Zápis velkého výstupu z procesu rovnou do souboru
Ne vždycky chcete výstup v paměti. Velký databázový dump pošlete rovnou do souboru:
Process::runExecutable('mysqldump', ['mydb'], stdout: 'backup.sql')->ensureSuccess();
Parametry stdout i stderr berou název souboru,
zapisovatelný resource, false (zahodit) nebo null
(zachytit do paměti, výchozí stav). Užitečné, když proces sype gigabajty,
ty v RAM mít nechcete.
Jak nastavit timeout pro externí program v PHP
Externí program se může zaseknout. Třída na to má parametr
$timeout (ve výchozím stavu 60 sekund):
try {
Process::runExecutable('slow-tool', timeout: 5.0)->wait();
} catch (Nette\Utils\ProcessTimeoutException $e) {
echo 'Trvalo to moc dlouho, proces byl ukončen.';
}
Když limit vyprší (kontroluje se, dokud na proces čekáte nebo z něj
čtete), proces se zabije a vyletí ProcessTimeoutException.
A zabije ho doopravdy: na Unixu signálem SIGKILL, na Windows přes
taskkill /F. Tedy bez SIGTERMu, který si umí zatvrzelý proces
ignorovat a nechat vás čekat donekonečna v proc_close(). Stejně
razantně funguje i metoda terminate(), a proces se ukončí
i automaticky, když zanikne jeho objekt Process.
Propojování procesů pomocí rour (pipes) v PHP
Chcete propojit výstup jednoho procesu na vstup druhého, přesně jako
rourou | v shellu, ale bez shellu? Předáte Process
jako $stdin:
$producer = Process::runExecutable('cat', ['big.log']);
$consumer = Process::runExecutable('grep', ['error'], stdin: $producer);
echo $consumer->getStdOutput();
A řetězit můžete, kolik chcete: a | b | c. Na Windows tohle
jako jediná věc nefunguje, tam výstup prvního procesu zachyťte a předejte
dalšímu jako řetězec. Jinak třída chodí na Linuxu, macOS i Windows
úplně stejně a rozdíly, o kterých nechcete vědět, řeší pod
kapotou.
Process najdete v Nette Utils 4.1.4 (dokumentace). Vyzkoušejte ho
a dejte nám vědět. A proc_open()? Toho se už nemusíte
dotknout.
Chcete-li odeslat komentář, přihlaste se