Process: Dış Programları Çalıştırma
Nette\Utils\Process, PHP'den dış
programları çalıştırmanızı sağlar: onlara girdi verir, çıktılarını okur ve nasıl sonlandıklarına göre tepki
verirsiniz. PHP'nin proc_open() fonksiyonunun, hataları false döndürerek değil istisna fırlatarak
bildiren dost canlısı bir sargısıdır.
Kurulum:
composer require nette/utils
Tüm örnekler, aşağıdaki takma adın tanımlandığını varsayar:
use Nette\Utils\Process;
En Basit Kullanım
Bir programı çalıştırıp ne yazdırdığını okumak mı istiyorsunuz? Tek gereken bu:
$process = Process::runExecutable('git', ['log', '-1', '--format=%H']);
echo $process->getStdOutput();
İlk argüman çalıştırılacak program, ikincisi ise argümanlarının listesidir: komut satırına yazacağınız şeylerin
aynısı, yalnızca bir diziye bölünmüş hâli. getStdOutput() metodu programın bitmesini bekler ve standart
çıktısına yazdığı her şeyi döndürür.
Bütün fikir bu: bir süreç başlatırsınız, sonra ona sorular sorarsınız: hâlâ çalışıyor mu, ne yazdırdı, nasıl bitti. Bu sayfanın geri kalanı bu soruları tek tek ele alıyor.
Süreç Başlatma
Süreç başlatmanın iki yolu var ve aradaki farkı anlamak önemli.
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
Belirli bir programı argüman listesiyle çalıştırır. Argümanlar programa doğrudan aktarılır, dolayısıyla boşlukları, tırnakları ya da başka özel karakterleri asla kaçışlamanız gerekmez. Araya kabuk girmediğinden shell injection riski de yoktur. Özellikle komutun herhangi bir parçası kullanıcı girdisinden geliyorsa güvenli seçim budur:
$file = $_GET['file']; // her şey olabilir, hatta '; rm -rf /'
$process = Process::runExecutable('wc', ['-l', $file]); // tümüyle güvenli
Tam yol vermezseniz program sistemin PATH değişkeninde aranır. Bir PHP betiği çalıştırmak için
PHP_BINARY sabiti işe yarar:
$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
Bir komut dizesini sistem kabuğu üzerinden çalıştırır (Linux ve macOS'ta /bin/sh, Windows'ta
cmd.exe). Bu size kabuğun olanaklarını verir: borular |, yönlendirmeler >,
değişken genişletme, && ile komut zincirleme vb.:
$process = Process::runCommand('git log --oneline | head -n 20');
Ama kabuk dizenin tamamını ayrıştırdığından, bir runCommand() dizesini asla güvenilmeyen girdiden
kurmayın; bu klasik bir güvenlik açığıdır. Kuşkuya düştüğünüzde runExecutable() kullanın.
Bu kadar çok parametre olduğundan bunları adlandırılmış argüman olarak verin, örneğin
Process::runExecutable('git', ['pull'], timeout: 30). $options dizisi, Windows'taki
bypass_shell gibi ileri düzey ihtiyaçlar için proc_open() fonksiyonuna aktarılır.
Süreç Arka Planda Çalışır
Başlatıldıktan sonra süreç, PHP betiğinizle yan yana çalışır: runExecutable() ve
runCommand() hemen geri döner, sürecin bitmesini beklemez. Ne zaman (ve beklenip beklenmeyeceğine) siz karar
verirsiniz:
$process = Process::runExecutable('npm', ['install']);
// ... npm çalışırken burada başka işler yapın ...
$process->wait(); // şimdi bitene kadar bekle
Uygulamada wait() metodunu kendiniz nadiren çağırırsınız; çünkü getStdOutput(),
getExitCode(), isSuccess() ve ensureSuccess() size yanıt vermeden önce süreci zaten
otomatik olarak bekler. wait() metodunu, ona bir callback vermek
istediğinizde açıkça çağırın.
isRunning(): bool
Süreç hâlâ çalışıyorken true, bittiğinde ya da sonlandırıldığında false döndürür. Bu
arada başka işler yapmak için kullanışlıdır:
while ($process->isRunning()) {
// bir süre başka bir şey yap
usleep(100_000); // 100 ms
}
Nasıl Bitti?
Biten her sürecin bir çıkış kodu vardır: geleneksel olarak 0 başarıyı, başka herhangi bir sayı
ise bir tür başarısızlığı gösterir (tam olarak neyi, programa bağlıdır).
getExitCode(): int
Çıkış kodunu döndürür; gerekiyorsa önce sürecin bitmesini bekler:
$code = Process::runExecutable('git', ['pull'])->getExitCode(); // örneğin 0
isSuccess(): bool
„Çıkış kodu 0 mıydı?“ sorusunun kısayolu:
$process = Process::runExecutable('git', ['pull']);
if (!$process->isSuccess()) {
echo 'git failed: ' . $process->getStdError();
}
ensureSuccess(): void
Çoğu zaman programın yalnızca başarılı olmasını, aksi hâlde gürültülü biçimde başarısız olmasını
istersiniz. ensureSuccess() süreci bekler ve çıkış kodu 0 değilse
Nette\Utils\ProcessFailedException fırlatır:
Process::runExecutable('git', ['pull'])->ensureSuccess();
// yürütme yalnızca git başarılı olduysa sürer
Çıktıyı Okuma
Bir sürecin iki ayrı çıktı akışı vardır: standart çıktı (normal sonuçlar) ve standart hata (programların genellikle sorunları ve tanılama bilgilerini bildirdiği yer). Nette Utils bu ikisini ayrı tutar ve varsayılan olarak ikisini de belleğe yakalar; böylece dilediğiniz zaman okuyabilirsiniz.
getStdOutput(): string
Sürecin bitmesini bekler ve standart çıktıya yazdığı her şeyi döndürür:
$process = Process::runExecutable('date');
echo $process->getStdOutput();
getStdError(): string
Aynısı, ama standart hata için:
$process = Process::runExecutable('some-tool', ['--do-stuff']);
if (!$process->isSuccess()) {
throw new RuntimeException('The tool failed: ' . $process->getStdError());
}
Bir çıktı akışını başka yere yönlendirirseniz (bir
dosyaya, bir resource'a ya da false'a), bellekte döndürülecek bir şey kalmaz ve ilgili getter
Nette\InvalidStateException fırlatır.
consumeStdOutput(): string
Bazen çıktıyı, sürecin bitmesini beklemeden geldikçe görmek istersiniz; örneğin ilerlemeyi göstermek için. Her çağrı, standart çıktının bir önceki çağrıdan bu yana ortaya çıkan parçasını döndürür:
$process = Process::runExecutable('long-running-tool');
while ($process->isRunning()) {
echo $process->consumeStdOutput(); // yeni olan neyse yazdırır
usleep(100_000); // 100 ms
}
echo $process->consumeStdOutput(); // bitmeden hemen önce üretilen son parça
Döngüden sonraki consumeStdOutput() önemlidir: süreç son çıktısını, döngü içindeki son çağrıdan
sonra ama döngü onun çıktığını fark etmeden önceki usleep() sırasında yazmış olabilir. (Bunun yerine
döngü içindeki bir çağrı sırasında bittiyse, o çağrı zaten her şeyi döndürmüştür ve bu çağrı boş bir dize
döndürür.) Standart hata için de consumeStdError() vardır.
Çıktıyı Canlı İzleme
consumeStdOutput() ile yoklama yapmak yerine wait() metoduna bir callback verebilirsiniz. Yeni
çıktı geldikçe her seferinde çağrılır; bu da canlı günlükleme ya da çıktıyı bir yere iletmek için harikadır:
$process = Process::runExecutable('npm', ['install']);
$process->wait(function (string $stdOut, string $stdErr) {
echo $stdOut; // standart çıktıyı ilet
fwrite(STDERR, $stdErr); // ve standart hatayı
});
Callback iki dize alır: bir önceki çağrıdan bu yana gelen yeni standart çıktı verisi ve yeni standart hata verisi
(ikisi de boş olabilir). wait() geri döndüğünde süreç bitmiştir ve getExitCode(),
getStdOutput() ve diğerlerini yine de çağırabilirsiniz.
Girdi Gönderme
$stdin parametresi, sürecin standart girdisinde ne okuyacağını belirtir. Birkaç farklı şey kabul eder.
Bir dize sürecin girdisinin tamamı olur:
$process = Process::runExecutable('wc', ['-c'], stdin: 'hello world');
echo $process->getStdOutput(); // 11
Okunabilir bir resource (açık bir dosya, bir akış) girdiye kopyalanır:
$file = fopen('data.csv', 'r');
$process = Process::runExecutable('sort', stdin: $file);
null, girdiyi açık tutar; böylece ona kademeli olarak yazabilirsiniz (aşağıya bakın).
Varsayılan boş bir dizedir; bu da sürecin boş ve hemen kapatılmış bir girdi almasını sağlar. Mantıklı varsayılan budur: girdi okuyan programların asla gelmeyecek bir şeyi sonsuza dek beklemesini engeller.
writeStdInput(string $string): void
Süreci stdin: null ile başlattığınızda girdi açık kalır ve onu parça parça beslersiniz. İşiniz
bittiğinde closeStdInput() çağırın. Bu, programa artık girdi gelmeyeceğini bildirir (dosya sonu
gönderir):
$process = Process::runExecutable('some-repl', stdin: null);
$process->writeStdInput("first command\n");
$process->writeStdInput("second command\n");
$process->closeStdInput();
echo $process->getStdOutput();
$stdin olarak verilen bir dize ya da akış, süreç asıl işine başlamadan önce tek seferde
yazılır. Bu girdi büyükse ve program girdisini okumadan önce bol miktarda çıktı üretiyorsa, iki taraf da birbirini
bekleyerek takılabilir. Bu (ender) durumda stdin: null ile writeStdInput() kullanıp yazmayla okumayı
birbirine geçirin.
Süreçleri Zincirleme (Boru)
Tıpkı kabuktaki | borusu gibi, bir sürecin standart çıktısını doğrudan bir başkasının standart
girdisine bağlayabilirsiniz. $stdin olarak bir Process vermeniz yeterli:
$producer = Process::runExecutable('cat', ['big.log']);
$consumer = Process::runExecutable('grep', ['error'], stdin: $producer);
echo $consumer->getStdOutput();
İstediğiniz kadar süreci zincirleyebilirsiniz (a | b | c).
Süreçleri boruyla birbirine bağlamak Windows'ta desteklenmez (Nette\NotSupportedException
fırlatır). Windows'ta ilk sürecin çıktısını getStdOutput() ile yakalayın ve bir sonrakine dize
olarak verin.
Çıktıyı Başka Yere Yönlendirme
Varsayılan olarak standart çıktı ve standart hata belleğe yakalanır. $stdout ve $stderr
parametreleri bunları bunun yerine başka bir yere göndermenizi sağlar.
Bir dosya adı çıktıyı o dosyaya gönderir:
Process::runExecutable('mysqldump', ['mydb'], stdout: 'backup.sql')
->ensureSuccess();
Yazılabilir bir resource çıktıyı o akışa gönderir. Gerçek bir dosyaya dayanmalıdır
(php://memory ve benzerleri olmaz):
$log = fopen('build.log', 'a');
Process::runExecutable('make', stdout: $log, stderr: $log);
false çıktıyı tümüyle atar (/dev/null'a, Windows'ta NUL'a gider):
Process::runExecutable('noisy-tool', stderr: false);
Yönlendirme bellek kullanımını da düşük tutar: belleğe yakalamak elverişlidir, ama gigabaytlarca yazdıran bir süreç gigabaytlarca RAM kullanır; bu yüzden böyle çıktıları bir dosyaya yazın.
Ortam Değişkenleri
$env parametresi, sürecin göreceği ortam değişkenlerini belirler. Geçerli sürecin ortamını devralmak
için null (varsayılan) bırakın ya da kendiniz ayarlamak için bir dizi verin:
// geçerli ortam artı fazladan bir değişken
$process = Process::runExecutable('printenv', ['MY_VAR'], env: ['MY_VAR' => '123'] + getenv());
// tümüyle boş bir ortam
$process = Process::runExecutable('some-tool', env: []);
Çalışma Dizini
$directory parametresi, sürecin başlayacağı dizini belirler (varsayılan olarak geçerli dizindir):
$process = Process::runExecutable('git', ['status'], directory: '/path/to/repo');
Zaman Sınırı
$timeout parametresi (saniye cinsinden, varsayılan 60), süreci ne kadar bekleyeceğinizi
sınırlar. Siz onu beklerken ya da çıktısını okurken sınıra ulaşılırsa süreç sonlandırılır ve
Nette\Utils\ProcessTimeoutException fırlatılır. Sınırı kaldırmak için null verin:
$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.';
}
Sınır yalnızca wait(), getExitCode(), çıktı getter'ları ya da consume*()
içindeyken denetlenir. Başlatıp hiç beklemediğiniz bir süreç bu sınır yüzünden sonlandırılmaz.
Süreci Durdurma
terminate(): void
Süreç hâlâ çalışıyorsa onu hemen sonlandırır; zaten bitmişse hiçbir şey yapmaz:
$process = Process::runExecutable('server');
// ...
$process->terminate();
Bir süreç, Process nesnesi bitmeden yok edildiğinde (örneğin kapsamın dışına çıktığında) da otomatik
olarak sonlandırılır. İstediğiniz bu değilse süreci nesneden ayırın:
detach(): void
Süreci nesneden ayırır: arka planda çalışmayı sürdürür ve nesne yok edildiğinde artık sonlandırılmaz. Bir daemon'u ya da PHP betiğinin kendisinden bile uzun yaşayan bir arka plan işini böyle başlatırsınız:
$process = Process::runExecutable('worker', stdout: 'worker.log', stderr: false);
$process->detach();
// süreç, $process yok edildikten sonra da çalışmayı sürdürür
Ayırdıktan sonra çıktıyı kimse okumayacağı için çıktı bellekte yakalanmamalıdır: onu bir dosyaya, bir resource'a
ya da false'a yönlendirin, aksi hâlde detach()
Nette\InvalidStateException fırlatır. Ayırma sırasında standart girdi ve çıktı boruları kapatılır.
Yalnızca yıkıcının davranışı değişir. wait() ve getExitCode() yine sürecin bitmesini
bekler (ve $timeout yine geçerlidir, aşıldığında süreci sonlandırır), terminate() de onu yine
sonlandırır.
POSIX sistemlerinde, betiğiniz çalışırken biten ayrılmış bir süreç, betik sonlanana dek süreç listesinde zombi olarak görünür. Zararsızdır ve kendiliğinden kaybolur.
getPid(): ?int
Süreç çalışırken işletim sistemi süreç kimliğini (PID) döndürür, bittiğinde null döndürür:
$pid = $process->getPid();
Bir Şeyler Ters Gittiğinde
Hatalar her zaman istisna fırlatılarak bildirilir, asla dönüş değeriyle değil:
Nette\Utils\ProcessFailedException |
süreç başlatılamadı ya da ensureSuccess() çağrıldı ve çıkış kodu 0 değildi |
Nette\Utils\ProcessTimeoutException |
$timeout sınırı aşıldı |
Nette\InvalidArgumentException |
$stdin, $stdout ya da $stderr olarak geçersiz bir değer verildi |
Nette\IOException |
$stdout ya da $stderr olarak verilen bir dosya açılamadı |
Nette\InvalidStateException |
yakalanmamış çıktının okunması, zaten kapatılmış bir STDIN'e yazılması ya da çıktısı bellekte yakalanan bir sürecin ayrılması |
Nette\NotSupportedException |
Windows'ta süreç borulaması denendi |
ProcessFailedException ve ProcessTimeoutException, PHP'nin RuntimeException sınıfını
genişletir.