Symfony Process Component

Hôm nay chúng ta lại tiếp tục series tìm hiểu về component mà mình thấy hay ho của Symfony nhé. Lần này, mình sẽ giới thiệu về component Process giúp chúng ta thực thi các câu lệnh (của OS) nhé. Mình chỉ giới thiệu về nó và không nói thêm gì cả. Còn việc sử dụng nó vào mục đích gì thì đó là tùy vào việc mọi người thấy phù hợp với mục đích nào nhé 😄!

The Process Component

Process component giúp chúng ta thực thi các câu lệnh trong các tiến trình con (sub-process)

Cài đặt

Bạn có thể cài đặt Symfony Process thông qua hai cách sau:

Sau đó, bạn cần require file vendor/autoload.php để sử dụng cơ chế autoloading của Composer trước khi sử dụng component này.

Sử dụng

Process class cho phép chúng ta thực thi các câu lệnh trong tiến trình con:

use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;

$process = new Process('ls -lsa');
$process->run();

// executes after the command finishes
if (!$process->isSuccessful()) {
    throw new ProcessFailedException($process);
}

echo $process->getOutput();

Phương thức getOutput() sẽ luôn luôn trả về kết quả mà câu lệnh đã thực hiện và getErrorOutput() sẽ trả lại nội dung lỗi của câu lệnh (nếu có). Cách khác, bạn có thể sử dụng hai phương thức getIncrementalOutput()getIncrementalErrorOutput() để lấy kết quả của lần gọi cuối cùng.

Phương thức clearOutput() sẽ xóa nội dung của kết quả và clearErrorOutput() sẽ xóa nội dung lỗi.

Bạn có thể sử dụng Process với một vòng lặp để lấy nội dung trong khi kết quả được tạo ra. Mặc định, mỗi bước lặp sẽ đợi kết quả của bước lặp đó rồi mới chuyển sang bước lặp kế tiếp.

$process = new Process('ls -lsa');
$process->start();

foreach ($process as $type => $data) {
    if ($process::OUT === $type) {
        echo "\nRead from stdout: ".$data;
    } else { // $process::ERR === $type
        echo "\nRead from stderr: ".$data;
    }
}

Process component sử dụng một vòng lặp của PHP để lấy output trong quá trình nó được tạo. Bạn có thể tùy chỉnh vòng lặp đó thông qua phương thức getIterator():

$process = new Process('ls -lsa');
$process->start();
$iterator = $process->getIterator($process::ITER_SKIP_ERR | $process::ITER_KEEP_OUTPUT);
foreach ($iterator as $data) {
    echo $data."\n";
}

Phương thức mustRun() giống với phương thức run() ngoại trừ nó sẽ ném ra một ngoại lệ ProcessFailedException nếu tiến trình đó không được thực hiện thành công (tiến trình đó trả ra một status code khác 0):

use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

$process = new Process('ls -lsa');

try {
    $process->mustRun();

    echo $process->getOutput();
} catch (ProcessFailedException $e) {
    echo $e->getMessage();
}

Nhật kết quả theo thời gian thực

Khi bạn thực hiện một câu lệnh có thời gian thực thi dài (như việc tải một file bằng wget chẳng hạn), bạn có thể gửi lại cho người dùng kết quả theo thời gian thực bằng cách truyền một anonymous function vào phương thức run(). Mình thử một ví dụ để bạn có thể xem được nhé. Còn ví dụ trên doc của Symfony thì nó nhanh quá nên bạn không thấy được sự khác biệt đâu (yaoming):

$process = new Process('ssh -vT [email protected]');
$process->run(function ($type, $buffer) {
    if (Process::ERR === $type) {
        echo 'ERR > '.$buffer;
    } else {
        echo 'OUT > '.$buffer;
    }
});

Thực thi không đồng bộ

Bạn cũng có thể start một tiến trình phụ và sau đó để nó chạy không đồng bộ, nhận kết quả và trạng thái của câu lệnh trong tiến trình chính bất cứ khi nào bạn cần tới. Sử dụng phương thức start() để bắt đầu một một tiến trình không đồng bộ, phương thức isRunning() để kiểm tra xem tiến trình đó đã hoàn thành hay chưa và phương thức getOutput() để nhận về kết quả của tiến trình đó.

$process = new Process('ls -lsa');
$process->start();

while ($process->isRunning()) {
    // waiting for process to finish
}

echo $process->getOutput();

Bạn cũng có thể thực hiện các công việc khác trong khi đợi tiến trình đó kết thúc khi thực hiện câu lệnh bằng một tiến trình không đồng bộ:

$process = new Process('ls -lsa');
$process->start();

// ... do other things

$process->wait();

// ... do things after the process has finished

Phương thức wait() là một phương thức blocking. Có nghĩa là các đoạn code của chúng ta sẽ dừng lại ở đó và đợi cho đến khi tiến trình trước đó được hoàn thành.

Nếu một Response được gửi trước một tiến trình con trước khi nó hoàn thành thì tiến trình chủ sẽ bị hủy (tùy thuộc vào hệ điều hành). Có nghĩa rằng công việc của bạn sẽ bị dừng lại ngay lập tức do việc chạy một tiến trình không đồng bộ nó không giống với việc chạy một tiến trình đồng bộ. Nếu bạn muốn tiến trình của mình tồn tại theo vòng đời request/response, bạn có thể tận dụng sự kiện kernel.terminate và chạy lệnh không đồng bộ đó bên trong sự kiện này. Và bạn cần nhớ rằng, sự kiện kernel.terminate chỉ gọi được khi bạn sử dụng PHP-FPM.

Lưu ý, nếu bạn sử dụng sự kiện trên của PHP-FPM thì nó sẽ không phục vụ bất kỳ yêu cầu mới nào cho đến khi tiến trình con đó được kết thúc. Điều này có nghĩa rằng bạn sẽ chặn (block) bộ FPM pool nếu bạn không cẩn thận.

Phương thức wait() có thể nhận một tham số (không bắt buộc) là một callback mà có thể được gọi lặp đi lặp lại trong khi tiến trình đó đang chạy

$process = new Process('ls -lsa');
$process->start();

$process->wait(function ($type, $buffer) {
    if (Process::ERR === $type) {
        echo 'ERR > '.$buffer;
    } else {
        echo 'OUT > '.$buffer;
    }
});

Dừng một Process

Tất cả các tiến trình không đồng bộ đều có thể dừng lại bất cứ lúc nào bằng cách gọi phương thức stop(). Phương thức này nhận hai tham số là: thời gian chờ và tín hiệu sẽ gửi tới tiến trình đó. Một khi hết thời gian thì tín hiệu sẽ được gửi tới tiến trình đang chạy. Tín hiệu mặc định là SIGKILL. Bạn có thể đọc thêm về các process signals tại đây:

$process = new Process('ls -lsa');
$process->start();

// ... do other things

$process->stop(3, SIGINT);

Thực thi code PHP

Nếu bạn muốn thực thi code PHP, bạn có thể sử dụng PhpProcess:

use Symfony\Component\Process\PhpProcess;

$process = new PhpProcess(<<<EOF
    <?php echo 'Hello World'; ?>
EOF
);
$process->run();

Để đảm bảo rằng code của bạn có thể làm việc tốt trên nhiều nền tảng khác nhau, bạn có thể sử dụng class ProcessBuilder:

use Symfony\Component\Process\ProcessBuilder;

$builder = new ProcessBuilder(array('ls', '-lsa'));
$builder->getProcess()->run();

Trong trường hợp bạn dựng một trình điều khiển nhị phân (binary driver), bạn có thể sử dụng phương thức setPrefix() để thêm tiền tố cho tất cả các câu lệnh được tạo ra. Ví dụ dưới đây sẽ tạo ra hai tiến trình cho lệnh tar:

use Symfony\Component\Process\ProcessBuilder;

$builder = new ProcessBuilder();
$builder->setPrefix('/usr/bin/tar');

// '/usr/bin/tar' '--list' '--file=archive.tar.gz'
echo $builder
    ->setArguments(array('--list', '--file=archive.tar.gz'))
    ->getProcess()
    ->getCommandLine();

// '/usr/bin/tar' '-xzf' 'archive.tar.gz'
echo $builder
    ->setArguments(array('-xzf', 'archive.tar.gz'))
    ->getProcess()
    ->getCommandLine();

Thời gian chờ (Process timeout)

Bạn có thể giới hạn thời gian thực thi của một tiến trình bằng cách cài đặt thời gian chờ cho nó thông qua phương thức setTimeout() (tính bằng giây):

use Symfony\Component\Process\Process;

$process = new Process('ls -lsa');
$process->setTimeout(3600);
$process->run();

Nếu hết thời gian thì một ngoại lệ RuntimeException sẽ được ném ra. Chúng ta thử một ví dụ sau:

$process = new Process('ssh -T [email protected]');
$process->setTimeout(2);
$process->run();

Với ví dụ trên, bạn sẽ nhận được một ngoại lệ: PHP Fatal error: Uncaught exception 'Symfony\Component\Process\Exception\ProcessTimedOutException' with message 'The process "ssh -T [email protected]" exceeded the timeout of 2 seconds.'

Đối với các lệnh cần thời gian thực thi dài, bạn cần phải thực hiện việc kiểm tra thời gian chờ một cách thường xuyên:

$process->setTimeout(3600);
$process->start();

while ($condition) {
    // ...

    // check if the timeout is reached
    $process->checkTimeout();

    usleep(200000);
}

Thời gian chờ nhàn rỗi (Process Idle Timeout)

Ngược với thời gian chờ trước (timeout) thì thời gian chờ nhàn rỗi chỉ xem xét xem thời gian cuối cùng mà tiến trình đó trả kết quả:

use Symfony\Component\Process\Process;

$process = new Process('something-with-variable-runtime');
$process->setTimeout(3600);
$process->setIdleTimeout(60);
$process->run();

Với ví dụ trên, tiến trình đó chỉ được phép thực thi trong khoảng thời gian là 3600 giây hoặc nó không thực thi gì kể từ lần trả kết quả cuối cùng là 60 giây.

Process Signals

Khi bạn chạy một tiến trình không đồng bộ, bạn có thể gửi các tín hiệu POSIX tới tiến trình đó bằng phương thức signal():

use Symfony\Component\Process\Process;

$process = new Process('find / -name "rabbit"');
$process->start();

// will send a SIGKILL to the process
$process->signal(SIGKILL);

Do một số hạn chế trong PHP, nếu bạn sử dụng signals với Process component, bạn có thể phải thêm tiền tố vào lệnh của bạn với exec. Đọc thêm Symfony Issue#5759PHP Bug#39992 để tìm hiểu thêm lý do vì sao. POSIX signals không có sẵn trên nền tảng Windows. Đọc thêm PHP documentation để xem các signals phù hợp.

Process Pid

Bạn có thể truy cập pid của một tiến trình đang chạy thông qua phương thức getPid():

use Symfony\Component\Process\Process;

$process = new Process('/usr/bin/php worker.php');
$process->start();

$pid = $process->getPid();

Do một số hạn chế trong PHP, nếu bạn muốn lấy pid của một tiến trình đang chạy, bạn cần phải thêm tiền tố vào lệnh của bạn với exec. Đọc thêm Symfony Issue#5759 để tìm hiểu thêm lý do vì sao.

Disabling output

Trong một số trường hợp, để tiết kiệm bộ nhớ, bạn có thể bật/tắt việc xuất kết quả bằng phương thức disableOutput()enableOutput():

use Symfony\Component\Process\Process;

$process = new Process('/usr/bin/php worker.php');
$process->disableOutput();
$process->run();

Bạn không thể bât/tắt việc xuất kết quả khi tiến trình đang chạy. Một khi bạn đã tắt thì bạn không thể sử dụng các phương thức sau: getOutput(), getIncrementalOutput(), getErrorOutput(), getIncrementalErrorOutput(), setIdleTimeout(). Tuy nhiên, bạn vẫn có thể truyền một callback vào phương thức run(), start() hoặc mustRun() để xử lý dữ liệu ra của tiến trình đó.

Lời kết

Đến đây là kết thúc bài viết của mình để giới thiệu về component Process của Symfony. Hy vọng, bài viết này có ích cho một số ý tưởng của bạn (giống như việc viết một package auto deployment giống thằng Rocketeer nhưng với nhiều tính năng hay ho hơn chẳng hạn 😄)! Thân chào và hẹn gặp lại mọi người trong các bài viết tiếp theo.

Tham khảo: https://symfony.com/doc/current/components/process.html