以下程式碼實現如下功能:
- 根據入參命令,啟動多個子程式
- 自動拉起異常退出的子程式
- 監聽系統結束程式訊號,並優雅給到子程式
使用示例:
php -d extension=swow index.php --command="php -d extension=swow test.php"
index.php程式碼:
<?php
namespace {
require __DIR__ . DIRECTORY_SEPARATOR . 'vendor/autoload.php';
use Guard\Parameter;
use Guard\Coordinator;
use Guard\Manager;
use Swow\Coroutine;
use Swow\Sync\WaitGroup;
use Symfony\Component\Process\Process;
if (Parameter::getHelp()) {
Parameter::displayHelp();
exit(0);
}
Coordinator::init();
$wg = new WaitGroup();
for ($workerId = 0; $workerId < Parameter::getWorkers(); $workerId++) {
$wg->add();
Coroutine::run(static function () use ($workerId, $wg): void {
try {
$env = ['PROCESS_WORKER_ID' => $workerId, 'PROCESS_WORKERS' => Parameter::getWorkers()];
restart:
$process = new Process(Parameter::getCommand(), null, $env);
$process->setTimeout(null);
$process->start(function ($type, $buffer) {
if (substr($buffer, 0 - strlen(PHP_EOL)) != PHP_EOL) {
$buffer .= PHP_EOL;
}
if (Process::ERR === $type) {
fwrite(Parameter::getLogfileStderr(), $buffer);
} else {
fwrite(Parameter::getLogfileStdout(), $buffer);
}
});
Manager::set($workerId, $process);
$process->wait();
Manager::del($workerId);
sleep(1);
if (Parameter::getAutoRestart() && Coordinator::$running) {
goto restart;
}
} catch (Throwable) {
} finally {
$wg->done();
}
});
}
$wg->add();
Coroutine::run(static function () use ($wg): void {
try {
while (Coordinator::$running) {
Coordinator::yield(3 * 1000);
}
Manager::stop();
} catch (Throwable) {
} finally {
$wg->done();
}
});
$wg->wait();
}
namespace Guard {
use Swow\Channel;
use Swow\ChannelException;
use Swow\Coroutine;
use Swow\Errno;
use Swow\Signal;
use Swow\SignalException;
use Symfony\Component\Process\Process;
use Throwable;
use InvalidArgumentException;
use RuntimeException;
class Coordinator
{
public static bool $running = true;
protected static Channel $ch;
protected static bool $initLock = false;
public static function yield(int $timeout): bool
{
try {
self::$ch->pop($timeout);
return !self::$ch->isAvailable();
} catch (ChannelException $exception) {
$code = $exception->getCode();
if ($code === Errno::ETIMEDOUT) {
return false;
}
if ($code === Errno::ECANCELED) {
return true;
}
if ($code === Errno::ECLOSING) {
return true;
}
if ($code === Errno::ECLOSED) {
return true;
}
fwrite(Parameter::getLogfileStderr(), self::formatThrowable($exception));
return true;
} catch (Throwable $throwable) {
fwrite(Parameter::getLogfileStderr(), self::formatThrowable($throwable));
return true;
}
}
public static function init(int $timeout = 60 * 1000)
{
if (self::$initLock) {
return;
}
self::$initLock = true;
self::$ch = new Channel();
if ($timeout < 5 * 1000) {
$timeout = 5 * 1000;
}
if ('\\' === DIRECTORY_SEPARATOR) {
$signals = [Signal::INT];
} else {
$signals = [Signal::TERM, Signal::INT, Signal::HUP];
}
foreach ($signals as $signal) {
Coroutine::run(static function () use ($timeout, $signal): void {
while (true) {
try {
Signal::wait($signal, $timeout);
if ($signal == Signal::HUP) {
continue;
}
self::$running = false;
if (self::$ch->isAvailable()) {
try {
self::$ch->close();
} catch (Throwable) {
}
}
break;
} catch (SignalException $exception) {
if ($exception->getCode() !== Errno::ETIMEDOUT) {
fwrite(Parameter::getLogfileStderr(), self::formatThrowable($exception));
}
continue;
} catch (Throwable $throwable) {
fwrite(Parameter::getLogfileStderr(), self::formatThrowable($throwable));
}
}
});
}
}
protected static function formatThrowable(Throwable $throwable): string
{
$message = $throwable->getMessage();
$message = trim($message);
if (strlen($message) == 0) {
$message = get_class($throwable);
}
return sprintf(
"%d --> %s in %s on line %d\nThrowable: %s\nStack trace:\n%s",
$throwable->getCode(),
$message,
$throwable->getFile(),
$throwable->getLine(),
get_class($throwable),
$throwable->getTraceAsString()
);
}
}
class Manager
{
protected static array $container = [];
public static function set(int $workerId, Process $process)
{
self::$container[$workerId] = $process;
}
public static function del(int $workerId)
{
unset(self::$container[$workerId]);
}
public static function stop()
{
foreach (self::$container as $process) {
if ($process->isRunning()) {
$process->signal(Signal::TERM);
}
}
$timeout = Parameter::getStopTimeout();
while ($timeout > 0) {
$stop = 0;
foreach (self::$container as $process) {
if (!$process->isRunning()) {
$stop++;
}
}
if ($stop == count(self::$container)) {
break;
}
$timeout--;
sleep(1);
}
foreach (self::$container as $process) {
if ($process->isRunning()) {
$process->signal(Signal::KILL);
}
}
}
}
class Parameter
{
protected static function argv($key): ?string
{
global $argv;
foreach ($argv as $v) {
if (stripos($v, $key) === 0) {
return trim(substr($v, strlen($key) + 1), '"');
}
}
return null;
}
protected const OPTIONS_HELP = '--help';
protected const OPTIONS_WORKERS = '--workers';
protected const OPTIONS_LOGFILE_STDERR = '--logfile_stderr';
protected const OPTIONS_LOGFILE_STDOUT = '--logfile_stdout';
protected const OPTIONS_COMMAND = '--command';
protected const OPTIONS_STOP_TIMEOUT = '--stop_timeout';
protected const OPTIONS_AUTO_RESTART = '--auto_restart';
public static function displayHelp()
{
$s = ['Options:'];
$space = strlen(self::OPTIONS_LOGFILE_STDERR) + 3;
$format = "%-{$space}s";
$s[] = sprintf($format, self::OPTIONS_HELP) . "Display help.";
$s[] = sprintf($format, self::OPTIONS_COMMAND) . "Command to execute";
$s[] = sprintf($format, self::OPTIONS_WORKERS) . "Number of worker processes to start, default --> " . self::getWorkers();
$s[] = sprintf($format, self::OPTIONS_LOGFILE_STDERR) . "Output STDERR file, default --> php://stderr";
$s[] = sprintf($format, self::OPTIONS_LOGFILE_STDOUT) . "Output STDOUT file, default --> php://stdout";
$s[] = sprintf($format, self::OPTIONS_STOP_TIMEOUT) . "Timeout to wait for the process to end, default --> " . self::getStopTimeout();
$s[] = sprintf($format, self::OPTIONS_AUTO_RESTART) . "Auto restart the closed process, default --> " . (self::getAutoRestart() ? 'true' : 'false');
echo implode(PHP_EOL, $s), PHP_EOL;
}
public static function getHelp(): bool
{
global $argv;
if (count($argv) == 1) {
return true;
}
return !is_null(self::argv(self::OPTIONS_HELP));
}
protected static int $workers = 0;
public static function getWorkers(): int
{
if (self::$workers > 0) {
return self::$workers;
}
self::$workers = (int)self::argv(self::OPTIONS_WORKERS);
if (self::$workers <= 0) {
$tmp = @file_get_contents('/proc/cpuinfo');
self::$workers = substr_count((string)$tmp, 'processor');
}
if (self::$workers <= 0) {
self::$workers = 2;
}
return self::$workers;
}
protected static $logfileStderr;
public static function getLogfileStderr()
{
if (is_resource(self::$logfileStderr)) {
return self::$logfileStderr;
}
$logfileStderr = (string)self::argv(self::OPTIONS_LOGFILE_STDERR);
if ($logfileStderr == '') {
self::$logfileStderr = STDERR;
return self::$logfileStderr;
}
if (!is_dir(dirname($logfileStderr))) {
throw new InvalidArgumentException("Cannot found directory $logfileStderr");
}
$tmp = fopen($logfileStderr, 'a+');
if (is_resource(!$tmp)) {
throw new RuntimeException("Cannot open file $logfileStderr");
}
self::$logfileStderr = $tmp;
return self::$logfileStderr;
}
protected static $logfileStdout;
public static function getLogfileStdout()
{
if (is_resource(self::$logfileStdout)) {
return self::$logfileStdout;
}
$logfileStdout = (string)self::argv(self::OPTIONS_LOGFILE_STDOUT);
if ($logfileStdout == '') {
self::$logfileStdout = STDOUT;
return self::$logfileStdout;
}
if (!is_dir(dirname($logfileStdout))) {
throw new InvalidArgumentException("Cannot found directory $logfileStdout");
}
$tmp = fopen($logfileStdout, 'a+');
if (is_resource(!$tmp)) {
throw new RuntimeException("Cannot open file $logfileStdout");
}
self::$logfileStdout = $tmp;
return self::$logfileStdout;
}
protected static array $command = [];
public static function getCommand(): array
{
if (count(self::$command) != 0) {
return self::$command;
}
$command = self::argv(self::OPTIONS_COMMAND);
foreach ((array)explode(" ", $command) as $item) {
$item = trim($item);
if ($item == '') {
continue;
}
self::$command[] = $item;
}
if (count(self::$command) == 0) {
throw new InvalidArgumentException('Please enter the command: ' . self::$logfileStdout);
}
return self::$command;
}
protected static int $stopTimeout = 0;
public static function getStopTimeout(): int
{
if (self::$stopTimeout > 0) {
return self::$stopTimeout;
}
self::$stopTimeout = (int)self::argv(self::OPTIONS_STOP_TIMEOUT);
if (self::$stopTimeout <= 0) {
self::$stopTimeout = 10;
}
return self::$stopTimeout;
}
protected static int $autoRestart = 0;
public static function getAutoRestart(): bool
{
if (self::$autoRestart == 0) {
$tmp = self::argv('--auto_restart');
if (is_null($tmp)) {
self::$autoRestart = 1;
} else {
self::$autoRestart = $tmp === 'false' ? 0 : 1;
}
}
return (bool)self::$autoRestart;
}
}
}
test.php程式碼:
<?php
use Guard\Parameter;
use Swow\Channel;
use Swow\ChannelException;
use Swow\Coroutine;
use Swow\Errno;
use Swow\Signal;
use Swow\SignalException;
require __DIR__ . DIRECTORY_SEPARATOR . 'vendor/autoload.php';
Coordinator::init();
$process_worker_id = getenv('PROCESS_WORKER_ID');
try {
$i = 0;
while (Coordinator::$running) {
if ($i == 3 && $process_worker_id == 0) {
throw new RuntimeException('執行異常');
}
$i++;
echo $process_worker_id . ' ' . date('Y-m-d H:i:s').PHP_EOL;
Coordinator::yield(3000);
}
echo $process_worker_id . ' ' . '收到程式結束訊號' . PHP_EOL;
sleep(1);
echo $process_worker_id . ' ' . '結束程式' . PHP_EOL;
}catch (RuntimeException $exception) {
fwrite(STDERR, $exception->getMessage());
}
class Coordinator
{
public static bool $running = true;
protected static Channel $ch;
protected static bool $initLock = false;
public static function yield(int $timeout): bool
{
try {
self::$ch->pop($timeout);
return !self::$ch->isAvailable();
} catch (ChannelException $exception) {
$code = $exception->getCode();
if ($code === Errno::ETIMEDOUT) {
return false;
}
if ($code === Errno::ECANCELED) {
return true;
}
if ($code === Errno::ECLOSING) {
return true;
}
if ($code === Errno::ECLOSED) {
return true;
}
fwrite(Parameter::getLogfileStderr(), self::formatThrowable($exception));
return true;
} catch (Throwable $throwable) {
fwrite(Parameter::getLogfileStderr(), self::formatThrowable($throwable));
return true;
}
}
public static function init(int $timeout = 60 * 1000)
{
if (self::$initLock) {
return;
}
self::$initLock = true;
self::$ch = new Channel();
if ($timeout < 5 * 1000) {
$timeout = 5 * 1000;
}
if ('\\' === DIRECTORY_SEPARATOR) {
$signals = [Signal::INT];
} else {
$signals = [Signal::TERM, Signal::INT, Signal::HUP];
}
foreach ($signals as $signal) {
Coroutine::run(static function () use ($timeout, $signal): void {
while (true) {
try {
Signal::wait($signal, $timeout);
if ($signal == Signal::HUP) {
continue;
}
self::$running = false;
if (self::$ch->isAvailable()) {
try {
self::$ch->close();
} catch (Throwable) {
}
}
break;
} catch (SignalException $exception) {
if ($exception->getCode() !== Errno::ETIMEDOUT) {
fwrite(Parameter::getLogfileStderr(), self::formatThrowable($exception));
}
continue;
} catch (Throwable $throwable) {
fwrite(Parameter::getLogfileStderr(), self::formatThrowable($throwable));
}
}
});
}
}
protected static function formatThrowable(Throwable $throwable): string
{
$message = $throwable->getMessage();
$message = trim($message);
if (strlen($message) == 0) {
$message = get_class($throwable);
}
return sprintf(
"%d --> %s in %s on line %d\nThrowable: %s\nStack trace:\n%s",
$throwable->getCode(),
$message,
$throwable->getFile(),
$throwable->getLine(),
get_class($throwable),
$throwable->getTraceAsString()
);
}
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結