基于swoole协程的守护脚本

AI摘要
该内容为一份基于Swoole扩展实现的PHP进程守护程序代码,属于技术实现分享。代码通过协程和信号处理机制,实现了对指定命令行进程的启动、监控、日志记录及异常退出后自动重启的功能,主要用于学习和熟悉Swoole的进程与协程管理API。

原版:github.com/swow/dontdie
仿写的swoole版本:

#!/usr/bin/env swoole-cli
<?php

declare(strict_types=1);

namespace DontDie;

use InvalidArgumentException;
use RuntimeException;
use Swoole\Process;
use Swoole\Coroutine;
use Swoole\Coroutine\System;
use Swoole\ExitException;
use Throwable;

use function Swoole\Coroutine\run;
use function Swoole\Coroutine\go;

use function array_slice;
use function date;
use function extension_loaded;
use function feof;
use function file_put_contents;
use function fread;
use function fwrite;
use function getcwd;
use function getenv;
use function getopt;
use function json_encode;

use function proc_close;
use function proc_get_status;
use function proc_open;
use function sleep;
use function sprintf;
use function usleep;

use const FILE_APPEND;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;
use const STDERR;

/** @param string[] $command */
function dontDie(array $command, string $nickname = ''): void
{
    $running = true;
    while (true) {
        $descriptorType = 'pipe'; /* PHP_OS_FAMILY === 'Windows' ? 'pipe' : 'pty'; */
        $proc = @proc_open(
            $command,
            [0 => ['redirect', 0], 1 => ['redirect', 1], 2 => [$descriptorType, 'w']],
            $pipes, null, null
        );
        if ($proc === false) {
            sleep(1);
            continue;
        }
        $status = proc_get_status($proc);
        $pid = $status['pid'];
        $cwd = getcwd();
        $enableTrace = getenv('DONTDIE_TRACE') === '1';
        if ($nickname === '') {
            $logFilename = $cwd . '/.dontdie.log';
        } else {
            $logFilename = $cwd . '/.dontdie.' . $nickname . '.log';
        }
        $log = static function (string|array $contents) use ($pid, $logFilename): void {
            $line =
                json_encode([
                    'pid' => $pid,
                    'time' => date('Y-m-d H:i:s'),
                    'contents' => $contents,
                ], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES) . "\n";
            file_put_contents($logFilename, $line, FILE_APPEND);
        };
        $trace = static function (string|array $contents) use ($log, $enableTrace): void {
            if ($enableTrace) {
                $log($contents);
            }
        };
        $log('process started');
        $processInfo = ['command' => $command];
        if ($nickname !== '') {
            $processInfo['nickname'] = $nickname;
        }
        $log($processInfo);

        // stderr 读取协程,防止管道阻塞导致子进程死锁
        $stderr = $pipes[2];
        $stderrWorker = go(static function () use ($stderr, $trace): void {
            $trace('redirect stderr start');
            try {
                while (!feof($stderr)) {
                    $data = @fread($stderr, 8192);
                    if ($data) {
                        @fwrite(STDERR, $data);
                    }
                }
            } catch (Throwable) {}
            $trace('redirect stderr done');
        });

        // 信号监听协程
        $signalProxy = static function (int $signal) use ($pid, &$running, $trace): void {
            $trace(sprintf('wait for signal %d start', $signal));
            System::waitSignal($signal);
            if (Coroutine::isCanceled()) {
                return;
            }
            $trace(sprintf('wait for signal %d done', $signal));
            @Process::kill($pid, $signal);
            $running = false;
        };
        $sigintWorker = go($signalProxy, SIGINT);
        $sigtermWorker = go($signalProxy, SIGTERM);
        // 不监听 SIGHUP:dontdie 总是后台运行,SIGHUP 来自终端断连
        // Swoole 的 waitSignal 会覆盖 nohup 的 SIG_IGN 导致误退出
        $sighupWorker = false;

        $trace('wait for process');
        try {
            $wait = System::wait();
            assert($wait['pid'] === $pid);
            $trace('wait status: ' . json_encode($wait));
            $trace('wait for process done');
        } catch (Throwable) {
            $trace('wait for process canceled');
        }

        // 清理工作协程
        foreach ([$stderrWorker, $sigintWorker, $sigtermWorker, $sighupWorker] as $workerId) {
            if ($workerId !== false && Coroutine::exists($workerId)) {
                Coroutine::cancel($workerId);
                $trace("canceled worker: {$workerId}");
            }
        }

        $trace('wait for process exit');
        for ($i = 0; $i < 110; $i++) {
            if (!$status['running']) {
                $exitCode = $status['exitcode'];
                break;
            }
            $status = proc_get_status($proc);
            usleep(($i < 100 ? 1 : 10) * 1000);
        }
        if (!isset($exitCode)) {
            Process::kill($status['pid'], SIGKILL);
            $exitCode = -1;
            $log('process killed');
        } else {
            $log('process exited');
            $log($status);
        }
        proc_close($proc);
        if (!$running) {
            $log('process totally exited');
            exit($exitCode);
        }
        $log('process restart...');
    }
}

if (!extension_loaded('swoole')) {
    throw new RuntimeException('Swoole extension is required');
}

if ($argc <= 1) {
    throw new InvalidArgumentException('No command specified');
}

$options = getopt('', ['nickname:'], $restIndex);
$command = array_slice($argv, $restIndex);

run(function () use ($command, $options) {
    try {
        dontDie(
            command: $command,
            nickname: $options['nickname'] ?? '',
        );
    } catch (ExitException $e) {
        echo 'exit with ' . $e->getStatus(), PHP_EOL;
    }
});

做这个是为了熟悉swoole的进程管理,swow用fread(STDERR)代替wait,swoole有这块的api就直接拿来用了。

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 2
CodingHePing

这个是swow开发的吧,还没研究过怎么样

2年前 评论
mrpzx001 (楼主) 2年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!