├── .gitignore ├── tests ├── Process │ ├── Stubs │ │ ├── StubNotExtendProcess.php │ │ ├── StubExtendProcess.php │ │ └── ImplementStandardProcess.php │ ├── ProcessControllerTest.php │ └── ProcessTest.php └── Http │ ├── RequestTest.php │ ├── ResponseTest.php │ └── LifecycleHandlerTest.php ├── src ├── Server │ ├── Interfaces │ │ └── Server.php │ └── Base │ │ ├── BaseServer.php │ │ ├── HttpServer.php │ │ └── WebsocketServer.php ├── Process │ ├── Exceptions │ │ ├── RuntimeException.php │ │ ├── OperationRejectedException.php │ │ └── SingletonException.php │ ├── Interfaces │ │ ├── StandardProcess.php │ │ └── PipeProcess.php │ ├── ProcessExecutor.php │ ├── ProcessIdFileTrait.php │ ├── ProcessController.php │ ├── ProcessManager.php │ └── Process.php └── Http │ ├── Exceptions │ └── InvalidSwooleResponseException.php │ ├── Lifecycle │ ├── Interfaces │ │ ├── ExceptionHandler.php │ │ └── RouteDispatcher.php │ ├── SubProcedure.php │ ├── Illuminate │ │ ├── LifecycleTrait.php │ │ ├── ExceptionHandler.php │ │ └── RouteDispatcher.php │ ├── HttpLifecycleTrait.php │ └── Handler.php │ ├── Kernel.php │ ├── Request.php │ ├── Response.php │ └── ServerProcess.php ├── phpunit.xml ├── .travis.yml ├── composer.json ├── Vagrantfile └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | Homestead.yaml 2 | vendor/ 3 | .idea/ 4 | .vagrant/ 5 | .project/ 6 | composer.lock -------------------------------------------------------------------------------- /tests/Process/Stubs/StubNotExtendProcess.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Process\Stubs; 10 | 11 | 12 | class StubNotExtendProcess 13 | { 14 | 15 | } -------------------------------------------------------------------------------- /src/Server/Interfaces/Server.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Server\Interfaces; 10 | 11 | 12 | interface Server 13 | { 14 | /** 15 | * @return \Swoole\Server 16 | */ 17 | public function createSwooleServer(); 18 | } -------------------------------------------------------------------------------- /src/Process/Exceptions/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Process\Exceptions; 10 | 11 | /** 12 | * Class RuntimeException 13 | * 14 | * 运行时异常 15 | * 16 | * @package Dybasedev\Keeper\Process\Exceptions 17 | */ 18 | class RuntimeException extends \Exception 19 | { 20 | 21 | } -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | ./tests/Process/ 7 | 8 | 9 | ./tests/Http/ 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/Process/Stubs/StubExtendProcess.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Process\Stubs; 10 | 11 | use Dybasedev\Keeper\Process\Process; 12 | 13 | class StubExtendProcess extends Process 14 | { 15 | /** 16 | * @inheritDoc 17 | */ 18 | public function process() 19 | { 20 | // 21 | } 22 | } -------------------------------------------------------------------------------- /src/Server/Base/BaseServer.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Server\Base; 10 | 11 | use Dybasedev\Keeper\Server\Interfaces\Server; 12 | 13 | abstract class BaseServer implements Server 14 | { 15 | /** 16 | * 事件注册 17 | * 18 | * @return void 19 | */ 20 | abstract public function eventRegister(); 21 | } -------------------------------------------------------------------------------- /src/Process/Exceptions/OperationRejectedException.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Process\Exceptions; 10 | 11 | /** 12 | * Class OperationRejectedException 13 | * 14 | * 操作被拒绝异常 15 | * 16 | * @package Dybasedev\Keeper\Process\Exceptions 17 | */ 18 | class OperationRejectedException extends \LogicException 19 | { 20 | 21 | } -------------------------------------------------------------------------------- /src/Http/Exceptions/InvalidSwooleResponseException.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Http\Exceptions; 10 | 11 | /** 12 | * Class InvalidSwooleResponseException 13 | * 14 | * 无效的 Swoole Response 实例异常 15 | * 16 | * @package Dybasedev\Keeper\Http\Exceptions 17 | */ 18 | class InvalidSwooleResponseException extends \InvalidArgumentException 19 | { 20 | 21 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | sudo: false 3 | php: 4 | - '5.6' 5 | - '7.0' 6 | - '7.1' 7 | 8 | before_install: 9 | - echo "extension = swoole.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini 10 | - echo "swoole.use_namespace = 1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini 11 | - pecl install -f swoole-1.8.2.tgz 12 | - travis_retry composer self-update 13 | 14 | install: 15 | composer install --no-interaction --prefer-dist --no-suggest 16 | 17 | script: vendor/bin/phpunit 18 | -------------------------------------------------------------------------------- /src/Process/Interfaces/StandardProcess.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Process\Interfaces; 10 | 11 | /** 12 | * Interface StandardProcess 13 | * 14 | * 标准进程 15 | * 16 | * @package Dybasedev\Keeper\Process\Interfaces 17 | */ 18 | interface StandardProcess 19 | { 20 | /** 21 | * 进程逻辑代码 22 | * 23 | * @return void 24 | */ 25 | public function process(); 26 | } -------------------------------------------------------------------------------- /tests/Process/Stubs/ImplementStandardProcess.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Process\Stubs; 10 | 11 | use Dybasedev\Keeper\Process\Interfaces\PipeProcess; 12 | use Dybasedev\Keeper\Process\Process; 13 | 14 | abstract class ImplementStandardProcess extends Process implements PipeProcess 15 | { 16 | /** 17 | * 进程逻辑代码 18 | * 19 | * @return void 20 | */ 21 | public function process() 22 | { 23 | // 24 | } 25 | } -------------------------------------------------------------------------------- /src/Server/Base/HttpServer.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Server\Base; 10 | 11 | use Swoole\Http\Request; 12 | use Swoole\Http\Response; 13 | 14 | abstract class HttpServer extends BaseServer 15 | { 16 | /** 17 | * 请求事件 18 | * 19 | * @param Request $request 20 | * @param Response $response 21 | * 22 | * @return void 23 | */ 24 | abstract public function onRequest(Request $request, Response $response); 25 | } -------------------------------------------------------------------------------- /src/Process/Interfaces/PipeProcess.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Process\Interfaces; 10 | 11 | /** 12 | * Interface PipeProcess 13 | * 14 | * 开启管道的进程 15 | * 16 | * @package Dybasedev\Keeper\Process\Interfaces 17 | */ 18 | interface PipeProcess extends StandardProcess 19 | { 20 | /** 21 | * 是否开启重定向标准输入输出 22 | * 23 | * @return bool 24 | */ 25 | public function isRedirectStdIO(); 26 | 27 | /** 28 | * 管道类型 29 | * 30 | * @return bool|int 31 | */ 32 | public function getPipeType(); 33 | } -------------------------------------------------------------------------------- /src/Http/Lifecycle/Interfaces/ExceptionHandler.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Http\Lifecycle\Interfaces; 10 | 11 | use Dybasedev\Keeper\Http\Response; 12 | use Symfony\Component\HttpKernel\Exception\HttpException; 13 | 14 | /** 15 | * Interface ExceptionHandler 16 | * 17 | * 异常处理器 18 | * 19 | * @package Dybasedev\Keeper\Http\Lifecycle\Interfaces 20 | */ 21 | interface ExceptionHandler 22 | { 23 | /** 24 | * 处理异常 25 | * 26 | * @param HttpException $exception 27 | * 28 | * @return Response 29 | */ 30 | public function handle(HttpException $exception); 31 | } -------------------------------------------------------------------------------- /src/Http/Lifecycle/Interfaces/RouteDispatcher.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Http\Lifecycle\Interfaces; 10 | 11 | use Closure; 12 | use Dybasedev\Keeper\Http\Request; 13 | use Symfony\Component\HttpFoundation\Response; 14 | 15 | interface RouteDispatcher 16 | { 17 | /** 18 | * 路由注册 19 | * 20 | * @param Closure $callback 21 | * 22 | * @return $this 23 | */ 24 | public function routesRegistrar(Closure $callback); 25 | 26 | /** 27 | * 调度 28 | * 29 | * @param Request $request 30 | * 31 | * @return Response 32 | */ 33 | public function dispatch(Request $request); 34 | } -------------------------------------------------------------------------------- /src/Http/Lifecycle/SubProcedure.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Http\Lifecycle; 10 | 11 | use Illuminate\Contracts\Container\Container; 12 | 13 | /** 14 | * Class SubProcedure 15 | * 16 | * 生命周期子过程 17 | * 18 | * @package Dybasedev\Keeper\Http\Lifecycle 19 | */ 20 | class SubProcedure 21 | { 22 | /** 23 | * @var Container 容器 24 | */ 25 | protected $container; 26 | 27 | /** 28 | * SubProcedure constructor. 29 | * 30 | * @param Container $container 31 | */ 32 | public function __construct(Container $container) 33 | { 34 | // 克隆一个容器,以保证其在整个生命周期内是独立的 35 | $this->container = clone $container; 36 | } 37 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chongyi/keeper", 3 | "description": "基于 Swoole 实现的后台多进程工具,可以快速创建拥有更多可能性的应用", 4 | "minimum-stability": "stable", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "chongyi", 9 | "email": "xpz3847878@163.com" 10 | } 11 | ], 12 | "require": { 13 | "laravel/homestead": "^3.0", 14 | "symfony/console": "^3.1", 15 | "symfony/http-foundation": "^3.3", 16 | "phpunit/phpunit": "^5.7", 17 | "josegonzalez/dotenv": "^3.1", 18 | "illuminate/container": "^5.5", 19 | "illuminate/routing": "^5.5", 20 | "illuminate/events": "^5.5" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Dybasedev\\Keeper\\": "src/" 25 | }, 26 | "classmap": [ 27 | "tests/" 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Http/Lifecycle/Illuminate/LifecycleTrait.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Http\Lifecycle\Illuminate; 10 | 11 | use Dybasedev\Keeper\Http\Lifecycle\Handler; 12 | 13 | /** 14 | * Trait LifecycleTrait 15 | * 16 | * @package Dybasedev\Keeper\Http\Lifecycle\Illuminate 17 | */ 18 | trait LifecycleTrait 19 | { 20 | /** 21 | * @param Handler $handler 22 | * 23 | * @return RouteDispatcher 24 | */ 25 | public function getRouteDispatcher(Handler $handler) 26 | { 27 | return new RouteDispatcher($handler); 28 | } 29 | 30 | /** 31 | * @param Handler $handler 32 | * 33 | * @return ExceptionHandler 34 | */ 35 | public function getExceptionHandler(Handler $handler) 36 | { 37 | return new ExceptionHandler($handler); 38 | } 39 | } -------------------------------------------------------------------------------- /src/Server/Base/WebsocketServer.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Server\Base; 10 | 11 | 12 | use Swoole\WebSocket\Frame; 13 | use Swoole\WebSocket\Server; 14 | 15 | abstract class WebsocketServer extends HttpServer 16 | { 17 | /** 18 | * 是否开启 HTTP 服务支持 19 | * 20 | * @var bool 21 | */ 22 | protected $enableHttpService = false; 23 | 24 | /** 25 | * @param bool $switcher 26 | * 27 | * @return $this 28 | */ 29 | public function enableHttp($switcher = true) 30 | { 31 | $this->enableHttpService = $switcher; 32 | 33 | return $this; 34 | } 35 | 36 | /** 37 | * 消息接收回调 38 | * 39 | * @param Server $server 40 | * @param Frame $frame 41 | * 42 | * @return mixed 43 | */ 44 | abstract public function onMessage(Server $server, Frame $frame); 45 | 46 | } -------------------------------------------------------------------------------- /src/Http/Kernel.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Http; 10 | 11 | use Illuminate\Container\Container as IlluminateContainer; 12 | use Illuminate\Contracts\Container\Container; 13 | 14 | /** 15 | * Class Kernel 16 | * 17 | * HTTP 组件核心,用于提供 Swoole 或其他托管 HTTP 服务核心的组件 18 | * 该组件提供包括在处理整个 HTTP 请求过程的服务以及扩展支持 19 | * 20 | * @package Dybasedev\Keeper\Http 21 | */ 22 | class Kernel 23 | { 24 | protected $services = []; 25 | 26 | /** 27 | * @var Container IoC 容器 28 | */ 29 | protected $container; 30 | 31 | /** 32 | * Kernel constructor. 33 | * 34 | * @param Container $container 35 | */ 36 | public function __construct(Container $container = null) 37 | { 38 | if (is_null($container)) { 39 | $container = new IlluminateContainer(); 40 | } 41 | 42 | $this->container = $container; 43 | } 44 | } -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'yaml' 3 | 4 | VAGRANTFILE_API_VERSION ||= "2" 5 | confDir = $confDir ||= File.expand_path("vendor/laravel/homestead", File.dirname(__FILE__)) 6 | 7 | homesteadYamlPath = "Homestead.yaml" 8 | homesteadJsonPath = "Homestead.json" 9 | afterScriptPath = "after.sh" 10 | aliasesPath = "aliases" 11 | 12 | require File.expand_path(confDir + '/scripts/homestead.rb') 13 | 14 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 15 | if File.exists? aliasesPath then 16 | config.vm.provision "file", source: aliasesPath, destination: "~/.bash_aliases" 17 | end 18 | 19 | if File.exists? homesteadYamlPath then 20 | Homestead.configure(config, YAML::load(File.read(homesteadYamlPath))) 21 | elsif File.exists? homesteadJsonPath then 22 | Homestead.configure(config, JSON.parse(File.read(homesteadJsonPath))) 23 | end 24 | 25 | if File.exists? afterScriptPath then 26 | config.vm.provision "shell", path: afterScriptPath 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /src/Process/Exceptions/SingletonException.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Process\Exceptions; 10 | 11 | /** 12 | * Class SingletonException 13 | * 14 | * 单例异常 15 | * 16 | * 作为单例运行时若发现已存在运行的实例则抛出该异常 17 | * 18 | * @package Dybasedev\Keeper\Process\Exceptions 19 | */ 20 | class SingletonException extends RuntimeException 21 | { 22 | /** 23 | * @var int 24 | */ 25 | public $runningInstanceProcessId; 26 | 27 | /** 28 | * @return int 29 | */ 30 | public function getRunningInstanceProcessId() 31 | { 32 | return $this->runningInstanceProcessId; 33 | } 34 | 35 | /** 36 | * @param int $runningInstanceProcessId 37 | * 38 | * @return SingletonException 39 | */ 40 | public function setRunningInstanceProcessId($runningInstanceProcessId) 41 | { 42 | $this->runningInstanceProcessId = $runningInstanceProcessId; 43 | 44 | return $this; 45 | } 46 | } -------------------------------------------------------------------------------- /src/Http/Lifecycle/Illuminate/ExceptionHandler.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Http\Lifecycle\Illuminate; 10 | 11 | use Dybasedev\Keeper\Http\Lifecycle\Handler; 12 | use Symfony\Component\HttpKernel\Exception\HttpException; 13 | use Symfony\Component\Debug\ExceptionHandler as SymfonyExceptionHandler; 14 | use Dybasedev\Keeper\Http\Lifecycle\Interfaces\ExceptionHandler as ExceptionHandlerInterface; 15 | use Dybasedev\Keeper\Http\Response; 16 | 17 | class ExceptionHandler implements ExceptionHandlerInterface 18 | { 19 | /** 20 | * @var Handler 21 | */ 22 | protected $handler; 23 | 24 | /** 25 | * ExceptionHandler constructor. 26 | * 27 | * @param Handler $handler 28 | */ 29 | public function __construct(Handler $handler) 30 | { 31 | $this->handler = $handler; 32 | } 33 | 34 | 35 | /** 36 | * 处理异常 37 | * 38 | * @param HttpException $exception 39 | * 40 | * @return Response 41 | */ 42 | public function handle(HttpException $exception) 43 | { 44 | $debug = isset($_ENV['debug']) && $_ENV['debug'] ? true : false; 45 | $handle = (new SymfonyExceptionHandler($debug)); 46 | 47 | return new Response($handle->getHtml($exception), $exception->getStatusCode(), $exception->getHeaders()); 48 | } 49 | } -------------------------------------------------------------------------------- /tests/Http/RequestTest.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Http; 10 | 11 | use Dybasedev\Keeper\Http\Request; 12 | use PHPUnit\Framework\TestCase; 13 | 14 | class RequestTest extends TestCase 15 | { 16 | protected $swooleRequest; 17 | 18 | public function testCreateRequestInstance() 19 | { 20 | $this->swooleRequest->get = ['foo' => 'test a', 'bar' => 'test b']; 21 | $request = Request::createFromSwooleRequest($this->swooleRequest); 22 | 23 | $this->assertEquals('test a', $request->get('foo')); 24 | $this->assertEquals('test b', $request->get('bar')); 25 | 26 | $this->swooleRequest->server = ['remote_addr' => '127.0.0.1', 'request_uri' => '/foo/bar']; 27 | $this->swooleRequest->header = ['accept' => 'text/html', 'accept-language' => 'zh_CN']; 28 | $request = Request::createFromSwooleRequest($this->swooleRequest); 29 | 30 | $this->assertEquals('127.0.0.1', $request->getClientIp()); 31 | $this->assertEquals('/foo/bar', $request->getRequestUri()); 32 | $this->assertEquals(['zh_CN'], $request->getLanguages()); 33 | $this->assertEquals(['text/html'], $request->getAcceptableContentTypes()); 34 | } 35 | 36 | /** 37 | * @inheritDoc 38 | */ 39 | protected function setUp() 40 | { 41 | parent::setUp(); 42 | 43 | $this->swooleRequest = $this->createMock(\Swoole\Http\Request::class); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/Http/Request.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Http; 10 | 11 | use Illuminate\Http\Request as IlluminateRequest; 12 | use Swoole\Http\Request as SwooleRequest; 13 | 14 | /** 15 | * Class Request 16 | * 17 | * 请求 18 | * 19 | * @package Dybasedev\Keeper\Http 20 | */ 21 | class Request extends IlluminateRequest 22 | { 23 | /** 24 | * 从 Swoole Request 实例创建请求实体 25 | * 26 | * @param SwooleRequest $request 27 | * 28 | * @return static 29 | */ 30 | public static function createFromSwooleRequest(SwooleRequest $request) 31 | { 32 | $get = isset($request->get) ? $request->get : []; 33 | $post = isset($request->post) ? $request->post : []; 34 | $files = isset($request->files) ? $request->files : []; 35 | $cookie = isset($request->cookie) ? $request->cookie : []; 36 | 37 | if (isset($request->server)) { 38 | $keys = array_map('strtoupper', array_keys($request->server)); 39 | $server = array_combine($keys, array_values($request->server)); 40 | } else { 41 | $server = []; 42 | } 43 | 44 | if (isset($request->header)) { 45 | $keys = array_map(function ($value) { 46 | return 'HTTP_' . str_replace('-', '_', strtoupper($value)); 47 | }, array_keys($request->header)); 48 | $server = array_merge($server, array_combine($keys, array_values($request->header))); 49 | } 50 | 51 | return new static($get, $post, [], $cookie, $files, $server); 52 | } 53 | } -------------------------------------------------------------------------------- /src/Process/ProcessExecutor.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Process; 10 | 11 | use Dybasedev\Keeper\Process\Exceptions\RuntimeException; 12 | 13 | /** 14 | * Class ProcessExecutor 15 | * 16 | * 外部进程执行器 17 | * 18 | * @package Dybasedev\Keeper\Process 19 | */ 20 | class ProcessExecutor extends Process 21 | { 22 | /** 23 | * @var string 24 | */ 25 | protected $executable; 26 | 27 | /** 28 | * @var array|string 29 | */ 30 | protected $arguments; 31 | 32 | /** 33 | * @param string $executable 34 | * 35 | * @return ProcessExecutor 36 | */ 37 | public function setExecutable($executable) 38 | { 39 | $this->executable = $executable; 40 | 41 | return $this; 42 | } 43 | 44 | /** 45 | * @param array|string $arguments 46 | * 47 | * @return ProcessExecutor 48 | */ 49 | public function setArguments($arguments) 50 | { 51 | $this->arguments = $arguments; 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * 进程逻辑代码 58 | * 59 | * @return void 60 | * 61 | * @throws RuntimeException 62 | */ 63 | public function process() 64 | { 65 | if (is_null($this->arguments)) { 66 | $this->arguments = []; 67 | } 68 | 69 | if (!is_array($this->arguments)) { 70 | $this->arguments = explode(' ', $this->arguments); 71 | } 72 | 73 | if (is_null($this->executable)) { 74 | throw new RuntimeException(); 75 | } 76 | 77 | // 外部执行代码 78 | $this->getSwooleProcess()->exec($this->executable, $this->arguments); 79 | } 80 | 81 | } -------------------------------------------------------------------------------- /src/Http/Lifecycle/Illuminate/RouteDispatcher.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Http\Lifecycle\Illuminate; 10 | 11 | 12 | use Closure; 13 | use Dybasedev\Keeper\Http\Lifecycle\Handler; 14 | use Dybasedev\Keeper\Http\Request; 15 | use Illuminate\Contracts\Container\Container; 16 | use Illuminate\Contracts\Events\Dispatcher as DispatcherInterface; 17 | use Illuminate\Events\Dispatcher; 18 | use Illuminate\Routing\Router; 19 | use Dybasedev\Keeper\Http\Lifecycle\Interfaces\RouteDispatcher as RouteDispatcherInterface; 20 | 21 | class RouteDispatcher implements RouteDispatcherInterface 22 | { 23 | /** 24 | * @var Handler 25 | */ 26 | protected $handler; 27 | 28 | /** 29 | * @var Container 30 | */ 31 | protected $container; 32 | 33 | /** 34 | * @var Router 35 | */ 36 | protected $router; 37 | 38 | /** 39 | * @var DispatcherInterface; 40 | */ 41 | protected $events; 42 | 43 | /** 44 | * IlluminateRouteDispatch constructor. 45 | * 46 | * @param Handler $kernel 47 | */ 48 | public function __construct(Handler $kernel) 49 | { 50 | $this->handler = $kernel; 51 | $this->container = $kernel->getContainer(); 52 | 53 | $this->events = new Dispatcher($this->container); 54 | $this->container->instance(DispatcherInterface::class, $this->events); 55 | $this->router = new Router($this->events, $this->container); 56 | $this->container->instance(Router::class, $this->router); 57 | } 58 | 59 | /** 60 | * @inheritDoc 61 | */ 62 | public function dispatch(Request $request) 63 | { 64 | return $this->router->dispatch($request); 65 | } 66 | 67 | /** 68 | * @inheritDoc 69 | */ 70 | public function routesRegistrar(Closure $callback) 71 | { 72 | $callback($this->router); 73 | 74 | return $this; 75 | } 76 | } -------------------------------------------------------------------------------- /tests/Http/ResponseTest.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Http; 10 | 11 | use Dybasedev\Keeper\Http\Exceptions\InvalidSwooleResponseException; 12 | use Dybasedev\Keeper\Http\Response; 13 | use PHPUnit\Framework\TestCase; 14 | 15 | class ResponseTest extends TestCase 16 | { 17 | /** 18 | * @var \PHPUnit_Framework_MockObject_MockObject|\Swoole\Http\Response 19 | */ 20 | protected $swooleResponse; 21 | 22 | public function testResponseContentSend() 23 | { 24 | $this->swooleResponse->expects($this->once())->method('end')->with($this->equalTo('response content')); 25 | 26 | (new Response('response content'))->setSwooleResponse($this->swooleResponse)->sendContent(); 27 | } 28 | 29 | public function testResponseHeaderSend() 30 | { 31 | $response = new Response('not found', 404); 32 | $response->headers->set('Cache-Control', 'no-cache, private'); 33 | $response->headers->set('Foo', 'Bar'); 34 | 35 | $this->swooleResponse->expects($this->once())->method('status')->with($this->equalTo(404)); 36 | $this->swooleResponse->expects($this->any()) 37 | ->method('header') 38 | ->withConsecutive( 39 | [$this->equalTo('Cache-Control'), $this->equalTo('no-cache, private')], 40 | [$this->equalTo('Date'), $this->equalTo($response->headers->get('Date'))], 41 | [$this->equalTo('Foo'), $this->equalTo('Bar')] 42 | ); 43 | 44 | $response->setSwooleResponse($this->swooleResponse)->sendHeaders(); 45 | } 46 | 47 | public function testSwooleResponseGetter() 48 | { 49 | $this->expectException(InvalidSwooleResponseException::class); 50 | 51 | $response = new Response(); 52 | $response->getSwooleResponse(); 53 | } 54 | 55 | /** 56 | * @inheritDoc 57 | */ 58 | protected function setUp() 59 | { 60 | parent::setUp(); 61 | 62 | $this->swooleResponse = $this->getMockBuilder(\Swoole\Http\Response::class) 63 | ->setMethods(['end', 'status', 'header']) 64 | ->getMock(); 65 | } 66 | 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/Http/Response.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Http; 10 | 11 | use Dybasedev\Keeper\Http\Exceptions\InvalidSwooleResponseException; 12 | use Swoole\Http\Response as SwooleResponse; 13 | use Symfony\Component\HttpFoundation\Cookie; 14 | use Symfony\Component\HttpFoundation\Response as SymfonyResponse; 15 | 16 | class Response extends SymfonyResponse 17 | { 18 | /** 19 | * @var SwooleResponse 20 | */ 21 | protected $swooleResponse; 22 | 23 | /** 24 | * @param SwooleResponse $swooleResponse 25 | * 26 | * @return Response 27 | */ 28 | public function setSwooleResponse(SwooleResponse $swooleResponse) 29 | { 30 | $this->swooleResponse = $swooleResponse; 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * @return SwooleResponse 37 | */ 38 | public function getSwooleResponse() 39 | { 40 | if (!$this->swooleResponse instanceof \Swoole\Http\Response) { 41 | throw new InvalidSwooleResponseException(); 42 | } 43 | 44 | return $this->swooleResponse; 45 | } 46 | 47 | /** 48 | * @inheritDoc 49 | */ 50 | public function sendHeaders() 51 | { 52 | /* RFC2616 - 14.18 says all Responses need to have a Date */ 53 | if (!$this->headers->has('Date')) { 54 | $this->setDate(\DateTime::createFromFormat('U', time())); 55 | } 56 | 57 | // headers 58 | foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) { 59 | foreach ($values as $value) { 60 | $this->getSwooleResponse()->header($name, $value); 61 | } 62 | } 63 | 64 | // status 65 | $this->getSwooleResponse()->status($this->statusCode); 66 | 67 | // cookies 68 | /** @var Cookie $cookie */ 69 | foreach ($this->headers->getCookies() as $cookie) { 70 | $this->swooleResponse->cookie($cookie->getName(), $cookie->getValue(), $cookie->getExpiresTime(), 71 | $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly()); 72 | } 73 | 74 | return $this; 75 | } 76 | 77 | /** 78 | * @inheritDoc 79 | */ 80 | public function sendContent() 81 | { 82 | $this->getSwooleResponse()->end($this->content); 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * @inheritDoc 89 | */ 90 | public function send() 91 | { 92 | $this->sendHeaders(); 93 | $this->sendContent(); 94 | 95 | return $this; 96 | } 97 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Keeper 2 | 3 | [![Build Status](https://travis-ci.org/chongyi/keeper.svg?branch=master)](https://travis-ci.org/chongyi/keeper) 4 | [![Latest Stable Version](https://poser.pugx.org/chongyi/keeper/v/stable)](https://packagist.org/packages/chongyi/keeper) 5 | [![License](https://poser.pugx.org/chongyi/keeper/license)](https://packagist.org/packages/chongyi/keeper) 6 | 7 | 基于 Swoole 的后台多进程程序脚手架,提供了基本的进程控制功能。在此基础你将有更多可能使用 PHP 完成一些在 FPM 环境下无法实现的功能。 8 | 9 | ## 说明 10 | 11 | 该项目的主要作用不是给一个限定思路下的框架,而是以一个松散的组织形式,提供一系列可用的组件。使用者可以根据需要,既可以利用大量的已有 `Trait` 12 | 快速构建一个项目,像用一个框架一样使用;亦可以自行根据已定义的接口自行实现细节逻辑,或以此项目为基础,构建自己的框架。 13 | 14 | ## 环境要求 15 | 16 | * PHP >= 5.6 17 | * Swoole >= 1.8.2 18 | 19 | ## 使用方法 20 | 21 | ### 一个简单的 HTTP 服务 22 | 23 | 1. 先定义一个用作实现 HTTP 服务子进程 24 | 25 | > 我们用到了脚手架自带的基于 Laravel Illuminate 路由组件实现的 HTTP 生命周期, 26 | > 这样可以以最少的代码快速实现一个优雅的 Web 程序。 27 | 28 | ```php 29 | get('/', function () { 44 | return 'hello, world'; 45 | }); 46 | }; 47 | } 48 | } 49 | ``` 50 | 51 | 2. 创建主进程 52 | 53 | ```php 54 | '0.0.0.0', 63 | 'port' => '19730', 64 | 'auto_reload' => false // 该子进程退出后是否自动重载 65 | ]; 66 | 67 | // 注册子进程 68 | $this->registerChildProcess(new Http($options)); 69 | } 70 | 71 | } 72 | ``` 73 | 74 | 3. 启动/重启/停止 75 | 76 | ```php 77 | setProcessIdFile('./pid')->setDaemon(true); 82 | 83 | // 启动 84 | $master->run(); 85 | 86 | // 重启 87 | $master->restart(); 88 | 89 | // 停止 90 | $master->stop(); 91 | ``` 92 | 93 | ## TODO list 94 | 95 | > 该清单会随着项目推进而变化,但对于已标示为完成的部分则不会存在较大变动(存在变动往往是由于架构优化进行拆分)。 96 | 97 | * [ ] 进程组件 98 | * [x] 子进程抽象类 99 | * [x] 进程启停管理器 100 | * [ ] 运行时进程控制器(目前可用,但仍在完善中) 101 | * [ ] 子进程通讯服务 102 | * [ ] HTTP 服务端相关组件 103 | * [x] 子进程抽象类组件 104 | * [x] 生命周期管理器 105 | * [x] 路由组件接口和基于 Illuminate\Routing 实现的 Trait 106 | * [ ] HTTP 会话管理器 107 | * [ ] WebSocket 子进程扩展抽象类 108 | * [ ] WebSocket 会话管理器 109 | * [ ] 标准服务端相关组件 110 | * [ ] TCP UDP 连接以及会话管理器 111 | * [ ] 标准客户端相关组件 112 | * [ ] 传输内容编码解码接口和可快速使用的协议封装 Trait 113 | 114 | ## License 115 | 116 | MIT License -------------------------------------------------------------------------------- /tests/Process/ProcessControllerTest.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Process; 10 | 11 | use Dybasedev\Keeper\Process\ProcessController; 12 | use PHPUnit\Framework\TestCase; 13 | use Dybasedev\Keeper\Process\Process; 14 | use Process\Stubs\StubExtendProcess; 15 | use Process\Stubs\StubNotExtendProcess; 16 | 17 | class ProcessControllerTest extends TestCase 18 | { 19 | protected $masterProcess; 20 | 21 | public function testMakeAndRegisterProcess() 22 | { 23 | $controller = new ProcessController($this->masterProcess); 24 | $controller->registerProcess($process = new StubExtendProcess()); 25 | $controller->registerProcess(StubExtendProcess::class, ['foo' => 'bar']); 26 | 27 | $this->assertAttributeEquals([$process, new StubExtendProcess(['foo' => 'bar'])], 'registeredProcesses', 28 | $controller); 29 | 30 | $this->expectException(\InvalidArgumentException::class); 31 | $controller->registerProcess(new StubNotExtendProcess()); 32 | } 33 | 34 | public function testBuildProcessViaBootstrap() 35 | { 36 | $controller = new ProcessController($this->masterProcess); 37 | 38 | $process1 = $this->getMockBuilder(Process::class) 39 | ->setMethods(['getProcessId', 'runWithProcessController', 'process']) 40 | ->getMock(); 41 | $process1->expects($this->any())->method('getProcessId')->willReturn(100); 42 | $process1->expects($this->once()) 43 | ->method('runWithProcessController') 44 | ->with($this->equalTo($controller)) 45 | ->willReturnSelf(); 46 | 47 | $process2 = $this->getMockBuilder(Process::class) 48 | ->setMethods(['getProcessId', 'runWithProcessController', 'process']) 49 | ->getMock(); 50 | $process2->expects($this->any())->method('getProcessId')->willReturn(200); 51 | $process2->expects($this->once()) 52 | ->method('runWithProcessController') 53 | ->with($this->equalTo($controller)) 54 | ->willReturnSelf(); 55 | 56 | $controller->registerProcesses([$process1, $process2]); 57 | $controller->bootstrap(); 58 | $this->assertAttributeEquals([100 => $process1, 200 => $process2], 'processes', $controller); 59 | } 60 | 61 | 62 | /** 63 | * @inheritDoc 64 | */ 65 | protected function setUp() 66 | { 67 | parent::setUp(); 68 | 69 | $this->masterProcess = $this->getMockBuilder(Process::class) 70 | ->setMethods(['getProcessId', 'process']) 71 | ->getMock(); 72 | $this->masterProcess->expects($this->any())->method('getProcessId')->willReturn(0); 73 | } 74 | 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/Http/Lifecycle/HttpLifecycleTrait.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Http\Lifecycle; 10 | 11 | use Dybasedev\Keeper\Http\Lifecycle\Interfaces\ExceptionHandler; 12 | use Dybasedev\Keeper\Http\Lifecycle\Interfaces\RouteDispatcher; 13 | use Dybasedev\Keeper\Http\Request; 14 | use Swoole\Http\Request as SwooleRequest; 15 | use Swoole\Http\Response as SwooleResponse; 16 | use Illuminate\Container\Container; 17 | use Illuminate\Contracts\Container\Container as ContainerInterface; 18 | 19 | /** 20 | * Trait HttpServiceLifecycleTrait 21 | * 22 | * Http 服务生命周期 Trait 23 | * 24 | * @package Dybasedev\Keeper\Http\Lifecycle 25 | */ 26 | trait HttpLifecycleTrait 27 | { 28 | /** 29 | * @var Handler 30 | */ 31 | protected $lifecycleHandler; 32 | 33 | /** 34 | * 当 Worker 启动时触发 35 | */ 36 | public function onWorkerStart() 37 | { 38 | // 创建生命周期管理器 39 | $this->lifecycleHandler = $this->createLifecycleHandler($this->getContainer()); 40 | $this->lifecycleHandler->setExceptionHandler($this->getExceptionHandler($this->lifecycleHandler)) 41 | ->setRouteDispatcher($this->getRouteDispatcher($this->lifecycleHandler) 42 | ->routesRegistrar($this->getRoutesRegistrar())); 43 | } 44 | 45 | /** 46 | * 创建生命周期管理器 47 | * 48 | * @param ContainerInterface $container 49 | * 50 | * @return Handler 51 | */ 52 | public function createLifecycleHandler(ContainerInterface $container) 53 | { 54 | return new Handler($container); 55 | } 56 | 57 | /** 58 | * 当请求进入时触发 59 | * 60 | * @param SwooleRequest $origin 61 | * @param SwooleResponse $responseControl 62 | */ 63 | public function onRequest(SwooleRequest $origin, SwooleResponse $responseControl) 64 | { 65 | $request = Request::createFromSwooleRequest($origin); 66 | 67 | $this->lifecycleHandler->dispatch($request)->setSwooleResponse($responseControl)->send(); 68 | } 69 | 70 | /** 71 | * @return Container 72 | */ 73 | abstract public function getContainer(); 74 | 75 | /** 76 | * 获取 Worker ID 77 | * 78 | * @return int 79 | */ 80 | abstract public function getWorkerId(); 81 | 82 | /** 83 | * 获取路由调度器 84 | * 85 | * @param Handler $handler 86 | * 87 | * @return RouteDispatcher 88 | */ 89 | abstract public function getRouteDispatcher(Handler $handler); 90 | 91 | /** 92 | * 获取路由注册器 93 | * 94 | * @return \Closure 95 | */ 96 | abstract public function getRoutesRegistrar(); 97 | 98 | /** 99 | * 异常处理器 100 | * 101 | * @param Handler $handler 102 | * 103 | * @return ExceptionHandler 104 | */ 105 | abstract public function getExceptionHandler(Handler $handler); 106 | } -------------------------------------------------------------------------------- /tests/Process/ProcessTest.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Process; 10 | 11 | use Dybasedev\Keeper\Process\Process; 12 | use Dybasedev\Keeper\Process\ProcessController; 13 | use PHPUnit\Framework\TestCase; 14 | use Process\Stubs\ImplementStandardProcess; 15 | 16 | class ProcessTest extends TestCase 17 | { 18 | 19 | public function testOptions() 20 | { 21 | /** @var Process $process */ 22 | $process = $this->getMockForAbstractClass(Process::class, [['options' => true]]); 23 | $this->assertTrue($process->getOptions()['options']); 24 | } 25 | 26 | public function testAutoReloadOption() 27 | { 28 | /** @var Process $process */ 29 | $process = $this->getMockForAbstractClass(Process::class); 30 | $this->assertTrue($process->isAutoReload()); 31 | 32 | $process = $this->getMockForAbstractClass(Process::class, [['auto_reload' => true]]); 33 | $this->assertTrue($process->isAutoReload()); 34 | 35 | $process = $this->getMockForAbstractClass(Process::class, [['auto_reload' => false]]); 36 | $this->assertFalse($process->isAutoReload()); 37 | } 38 | 39 | public function testTemporaryAutoReload() 40 | { 41 | /** @var Process $process */ 42 | $process = $this->getMockForAbstractClass(Process::class, [['auto_reload' => false]]); 43 | $process->runtime['temp_auto_reload'] = true; 44 | $this->assertTrue($process->isAutoReload()); 45 | $this->assertTrue($process->isTemporaryAutoReload()); 46 | 47 | $process->clearTemporaryAutoLoadStatus(); 48 | $this->assertFalse($process->isAutoReload()); 49 | $this->assertFalse($process->isTemporaryAutoReload()); 50 | 51 | $process = $this->getMockForAbstractClass(Process::class, [['auto_reload' => true]]); 52 | $process->runtime['temp_auto_reload'] = true; 53 | $this->assertTrue($process->isAutoReload()); 54 | $this->assertTrue($process->isTemporaryAutoReload()); 55 | 56 | $process->clearTemporaryAutoLoadStatus(); 57 | $this->assertTrue($process->isAutoReload()); 58 | $this->assertFalse($process->isTemporaryAutoReload()); 59 | } 60 | 61 | public function testProcessClone() 62 | { 63 | $options = ['foo' => true]; 64 | /** @var Process $process */ 65 | $process = $this->getMockForAbstractClass(Process::class, [$options]); 66 | $process->runWithProcessController($controller = $this->createMock(ProcessController::class)); 67 | 68 | $this->assertAttributeEquals($options, 'options', $process); 69 | $this->assertAttributeEquals($controller, 'withProcessController', $process); 70 | 71 | $clone = clone $process; 72 | $this->assertAttributeEquals($options, 'options', $clone); 73 | $this->assertAttributeEquals(null, 'withProcessController', $clone); 74 | } 75 | 76 | public function testBuildSwooleProcessInstance() 77 | { 78 | /** @var Process $process */ 79 | $process = $this->getMockForAbstractClass(ImplementStandardProcess::class); 80 | $process->expects($this->once())->method('isRedirectStdIO')->willReturn(true); 81 | $process->expects($this->once())->method('getPipeType')->willReturn(1); 82 | $this->assertInstanceOf(\Swoole\Process::class, $process->buildSwooleProcessInstance([$process, 'process'])); 83 | 84 | $process = $this->getMockForAbstractClass(Process::class); 85 | $this->assertInstanceOf(\Swoole\Process::class, $process->buildSwooleProcessInstance([$process, 'process'])); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/Process/ProcessIdFileTrait.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Process; 10 | 11 | use Dybasedev\Keeper\Process\Exceptions\SingletonException; 12 | use Swoole\Process; 13 | use Dybasedev\Keeper\Process\Exceptions\RuntimeException; 14 | 15 | /** 16 | * Trait ProcessIdFileTrait 17 | * 18 | * @package Dybasedev\Keeper\Process 19 | */ 20 | trait ProcessIdFileTrait 21 | { 22 | /** 23 | * @var string 24 | */ 25 | protected $processIdFile; 26 | 27 | /** 28 | * @var resource 29 | */ 30 | protected $processIdFileDescriptor = null; 31 | 32 | /** 33 | * @var bool 34 | */ 35 | protected $shutdownRunningInstance = false; 36 | 37 | /** 38 | * 获取进程 ID 39 | * 40 | * @return int 41 | */ 42 | abstract public function getProcessId(); 43 | 44 | /** 45 | * 刷新 PID 文件 46 | */ 47 | private function freshProcessIdFile() 48 | { 49 | ftruncate($this->processIdFileDescriptor, 0); 50 | fwrite($this->processIdFileDescriptor, $this->getProcessId()); 51 | } 52 | 53 | /** 54 | * 检查以保证进程单例 55 | * 56 | * @throws SingletonException 57 | */ 58 | private function singleGuarantee() 59 | { 60 | if ($this->hasProcessIdFile()) { 61 | $runningProcessId = $this->getProcessIdFromFile(); 62 | 63 | if ($runningProcessId !== false) { 64 | if (!$this->shutdownRunningInstance) { 65 | throw (new SingletonException())->setRunningInstanceProcessId($runningProcessId); 66 | } 67 | 68 | $fd = fopen($this->processIdFile, 'r+'); 69 | Process::kill($runningProcessId); 70 | 71 | // 阻塞,直至进程终止解锁 72 | flock($fd, LOCK_EX); 73 | flock($fd, LOCK_UN); 74 | fclose($fd); 75 | 76 | $this->singleGuarantee(); 77 | } 78 | } else { 79 | touch($this->processIdFile); 80 | $this->getProcessIdFromFile(); 81 | } 82 | } 83 | 84 | /** 85 | * 从文件获取 PID 86 | * 87 | * 若该 PID 文件未有进程加锁则认为其值不可取,返回 false。 88 | * 89 | * @return bool|int 90 | * 91 | * @throws RuntimeException 92 | */ 93 | private function getProcessIdFromFile() 94 | { 95 | $fileDescriptor = fopen($this->processIdFile, 'r+'); 96 | 97 | if (!$fileDescriptor) { 98 | throw new RuntimeException(); 99 | } 100 | 101 | if (flock($fileDescriptor, LOCK_EX | LOCK_NB)) { 102 | // 同步变更 PID 文件描述符至类属性 103 | $this->processIdFileDescriptor = $fileDescriptor; 104 | return false; 105 | } 106 | 107 | $processId = fread($fileDescriptor, 64); 108 | fclose($fileDescriptor); 109 | 110 | return (int)$processId; 111 | } 112 | 113 | /** 114 | * 是否存在 PID 文件 115 | * 116 | * @return bool 117 | */ 118 | private function hasProcessIdFile() 119 | { 120 | $processIdFilePath = dirname($this->processIdFile); 121 | 122 | if (!is_dir($processIdFilePath)) { 123 | mkdir($processIdFilePath, 0644, true); 124 | 125 | return false; 126 | } elseif (!is_file($this->processIdFile)) { 127 | return false; 128 | } 129 | 130 | return true; 131 | } 132 | 133 | /** 134 | * 清理 PID 文件 135 | */ 136 | private function clearProcessIdFile() 137 | { 138 | if ($this->processIdFileDescriptor) { 139 | flock($this->processIdFileDescriptor, LOCK_UN); 140 | fclose($this->processIdFileDescriptor); 141 | 142 | unlink($this->processIdFile); 143 | } 144 | } 145 | 146 | /** 147 | * 设置 PID 文件位置 148 | * 149 | * @param string $processIdFile 150 | * 151 | * @return $this 152 | */ 153 | public function setProcessIdFile($processIdFile) 154 | { 155 | $this->processIdFile = $processIdFile; 156 | 157 | return $this; 158 | } 159 | } -------------------------------------------------------------------------------- /tests/Http/LifecycleHandlerTest.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Http; 10 | 11 | use Dybasedev\Keeper\Http\Lifecycle\Handler; 12 | use Dybasedev\Keeper\Http\Lifecycle\Interfaces\ExceptionHandler; 13 | use Dybasedev\Keeper\Http\Lifecycle\Interfaces\RouteDispatcher; 14 | use Dybasedev\Keeper\Http\Request; 15 | use Dybasedev\Keeper\Http\Response; 16 | use Exception; 17 | use Illuminate\Contracts\Container\Container; 18 | use PHPUnit\Framework\TestCase; 19 | use Symfony\Component\HttpFoundation\JsonResponse; 20 | use Symfony\Component\HttpFoundation\Response as SymfonyResponse; 21 | use Symfony\Component\HttpKernel\Exception\HttpException; 22 | 23 | class LifecycleHandlerTest extends TestCase 24 | { 25 | /** 26 | * @var Handler 27 | */ 28 | protected $handler; 29 | 30 | public function testPrepareResponse() 31 | { 32 | $response = new Response(); 33 | $this->assertEquals($response, $this->handler->prepareResponse($response)); 34 | 35 | $response = new SymfonyResponse('foo', 403); 36 | $response->setCharset('gbk'); 37 | $this->assertInstanceOf(Response::class, $this->handler->prepareResponse($response)); 38 | $this->assertEquals('foo', $this->handler->prepareResponse($response)->getContent()); 39 | $this->assertEquals(403, $this->handler->prepareResponse($response)->getStatusCode()); 40 | $this->assertEquals($response->headers->all(), $this->handler->prepareResponse($response)->headers->all()); 41 | 42 | $response = new JsonResponse(['foo' => 'bar']); 43 | $this->assertInstanceOf(Response::class, $this->handler->prepareResponse($response)); 44 | $this->assertEquals('{"foo":"bar"}', $this->handler->prepareResponse($response)->getContent()); 45 | $this->assertEquals('application/json', 46 | $this->handler->prepareResponse($response)->headers->get('Content-Type')); 47 | 48 | $response = 'foo'; 49 | $this->assertInstanceOf(Response::class, $this->handler->prepareResponse($response)); 50 | $this->assertEquals('foo', $this->handler->prepareResponse($response)->getContent()); 51 | $this->assertEquals(200, $this->handler->prepareResponse($response)->getStatusCode()); 52 | } 53 | 54 | public function testExceptionHandler() 55 | { 56 | $this->assertInstanceOf(Response::class, $this->handler->handleException(new Exception())); 57 | $this->assertEquals('foo', $this->handler->handleException(new Exception('foo'))->getContent()); 58 | $this->assertEquals(500, $this->handler->handleException(new Exception())->getStatusCode()); 59 | $this->assertEquals(403, $this->handler->handleException(new HttpException(403))->getStatusCode()); 60 | 61 | $handler = $this->createMock(ExceptionHandler::class); 62 | $handler->expects($this->any())->method('handle')->with($this->isInstanceOf(HttpException::class))->will($this->returnCallback(function ($exception) { 63 | return new Response($exception->getMessage(), $exception->getStatusCode(), $exception->getHeaders()); 64 | })); 65 | 66 | $this->handler->setExceptionHandler($handler); 67 | $this->assertInstanceOf(SymfonyResponse::class, $this->handler->handleException(new Exception())); 68 | $this->assertEquals(500, $this->handler->handleException(new Exception())->getStatusCode()); 69 | $this->assertEquals(403, $this->handler->handleException(new HttpException(403))->getStatusCode()); 70 | } 71 | 72 | public function testDispatch() 73 | { 74 | $routeDispacther = $this->createMock(RouteDispatcher::class); 75 | $routeDispacther->expects($this->once())->method('dispatch')->willReturn($stub = new Response('foo')); 76 | $this->handler->setRouteDispatcher($routeDispacther); 77 | $this->assertEquals($stub, $this->handler->dispatch(new Request())); 78 | 79 | $routeDispacther = $this->createMock(RouteDispatcher::class); 80 | $routeDispacther->expects($this->once())->method('dispatch')->willThrowException(new Exception()); 81 | $this->handler->setRouteDispatcher($routeDispacther); 82 | $this->assertEquals(500, $this->handler->dispatch(new Request())->getStatusCode()); 83 | } 84 | 85 | /** 86 | * @inheritDoc 87 | */ 88 | protected function setUp() 89 | { 90 | parent::setUp(); 91 | 92 | /** @var Container $container */ 93 | $container = $this->createMock(Container::class);; 94 | $this->handler = new Handler($container); 95 | } 96 | 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/Http/Lifecycle/Handler.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Http\Lifecycle; 10 | 11 | use Dybasedev\Keeper\Http\Lifecycle\Interfaces\ExceptionHandler; 12 | use Dybasedev\Keeper\Http\Lifecycle\Interfaces\RouteDispatcher; 13 | use Dybasedev\Keeper\Http\Request; 14 | use Dybasedev\Keeper\Http\Response; 15 | use Dybasedev\Keeper\Process\Exceptions\RuntimeException; 16 | use Exception; 17 | use Illuminate\Contracts\Container\Container; 18 | use Symfony\Component\HttpFoundation\JsonResponse; 19 | use Symfony\Component\HttpFoundation\Response as SymfonyResponse; 20 | use Symfony\Component\HttpKernel\Exception\HttpException; 21 | 22 | /** 23 | * Class Kernel 24 | * 25 | * 生命周期管理器 26 | * 27 | * @package Dybasedev\Keeper\Http\Lifecycle 28 | */ 29 | class Handler 30 | { 31 | /** 32 | * @var Container 33 | */ 34 | protected $container; 35 | 36 | /** 37 | * @var RouteDispatcher 38 | */ 39 | protected $routeDispatcher; 40 | 41 | /** 42 | * @var ExceptionHandler 43 | */ 44 | protected $exceptionHandler; 45 | 46 | /** 47 | * Kernel constructor. 48 | * 49 | * @param Container $container 50 | */ 51 | public function __construct(Container $container) 52 | { 53 | $this->container = $container; 54 | $this->container->instance(static::class, $this); 55 | } 56 | 57 | /** 58 | * 设置 Route Dispatcher 59 | * 60 | * @param RouteDispatcher $dispatcher 61 | */ 62 | public function setRouteDispatcher(RouteDispatcher $dispatcher) 63 | { 64 | $this->routeDispatcher = $dispatcher; 65 | } 66 | 67 | /** 68 | * @return RouteDispatcher 69 | */ 70 | public function getRouteDispatcher() 71 | { 72 | return $this->routeDispatcher; 73 | } 74 | 75 | /** 76 | * 请求调度 77 | * 78 | * @param Request $request 79 | * 80 | * @return Response 81 | */ 82 | public function dispatch(Request $request) 83 | { 84 | try { 85 | return $this->prepareResponse($this->getRouteDispatcher()->dispatch($request)); 86 | } catch (Exception $exception) { 87 | return $this->prepareResponse($this->handleException($exception)); 88 | } 89 | } 90 | 91 | /** 92 | * 响应预处理 93 | * 94 | * @param string|\Symfony\Component\HttpFoundation\Response $response 95 | * 96 | * @return Response 输出一个 Dybasedev\Keeper\Http\Response 对象 97 | */ 98 | public function prepareResponse($response) 99 | { 100 | if (!$response instanceof Response) { 101 | if ($response instanceof SymfonyResponse) { 102 | $response = new Response($response->getContent(), $response->getStatusCode(), 103 | $response->headers->all()); 104 | } elseif (is_array($response)) { 105 | return $this->prepareResponse(new JsonResponse($response)); 106 | } else { 107 | $response = new Response($response); 108 | } 109 | } 110 | 111 | return $response; 112 | } 113 | 114 | /** 115 | * 获取 Container 116 | * 117 | * @return Container 118 | */ 119 | public function getContainer() 120 | { 121 | return $this->container; 122 | } 123 | 124 | /** 125 | * 设置异常处理器 126 | * 127 | * @param ExceptionHandler $handler 128 | * 129 | * @return $this 130 | */ 131 | public function setExceptionHandler(ExceptionHandler $handler) 132 | { 133 | $this->exceptionHandler = $handler; 134 | 135 | return $this; 136 | } 137 | 138 | /** 139 | * @param Exception $exception 140 | * 141 | * @return SymfonyResponse 142 | * 143 | * @throws RuntimeException 144 | */ 145 | public function handleException(Exception $exception) 146 | { 147 | // 所有异常统一转换为 Http 异常, 148 | // 异常处理器有必要根据其返回一个合理的 Http 响应 149 | if (!$exception instanceof HttpException) { 150 | $exception = new HttpException(500, $exception->getMessage(), $exception); 151 | } 152 | 153 | if (is_null($this->exceptionHandler)) { 154 | return new Response($exception->getMessage(), $exception->getStatusCode(), $exception->getHeaders()); 155 | } 156 | 157 | $response = $this->exceptionHandler->handle($exception); 158 | 159 | if ($response instanceof SymfonyResponse) { 160 | return $response; 161 | } 162 | 163 | throw new RuntimeException(); 164 | } 165 | 166 | } -------------------------------------------------------------------------------- /src/Process/ProcessController.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Process; 10 | 11 | use Dybasedev\Keeper\Process\Exceptions\RuntimeException; 12 | use Swoole\Process as SwooleProcess; 13 | 14 | /** 15 | * Class ProcessController 16 | * 17 | * (子)进程控制器 18 | * 19 | * @package Dybasedev\Keeper\Process 20 | */ 21 | class ProcessController 22 | { 23 | /** 24 | * @var Process 25 | */ 26 | protected $masterProcess; 27 | 28 | /** 29 | * @var array|Process 30 | */ 31 | protected $registeredProcesses; 32 | 33 | /** 34 | * @var bool 终止标识 35 | */ 36 | protected $terminate = false; 37 | 38 | /** 39 | * @var array|Process[] 40 | */ 41 | protected $processes; 42 | 43 | /** 44 | * @var \Closure[] 45 | */ 46 | protected $terminated = []; 47 | 48 | /** 49 | * ProcessController constructor. 50 | * 51 | * @param Process $masterProcess 52 | */ 53 | public function __construct(Process $masterProcess) 54 | { 55 | $this->masterProcess = $masterProcess; 56 | } 57 | 58 | /** 59 | * 注册进程类 60 | * 61 | * 该类应该继承自 Dybasedev\Keeper\Process\Process 62 | * 63 | * @param string|Process $process 注册的进程实例或类名,当是一个实例时,第二个参数将被忽略 64 | * @param array $options 65 | * 66 | * @return $this 67 | */ 68 | public function registerProcess($process, array $options = []) 69 | { 70 | if ($process instanceof Process) { 71 | $this->registeredProcesses[] = $process; 72 | } else { 73 | $this->registerProcess($this->makeProcess($process, $options)); 74 | } 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * 批量注册进程类 81 | * 82 | * @param \Iterator|array $processes 83 | */ 84 | public function registerProcesses($processes) 85 | { 86 | foreach ($processes as $describer => $body) { 87 | if ($body instanceof Process) { 88 | $this->registerProcess($body); 89 | } else { 90 | $this->registerProcess($describer, $body); 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * 启动 97 | */ 98 | public function bootstrap() 99 | { 100 | foreach ($this->registeredProcesses as $process) { 101 | $this->buildProcess($process); 102 | } 103 | } 104 | 105 | /** 106 | * 构建进程 107 | * 108 | * @param Process $process 109 | * 110 | * @return int 111 | * 112 | * @throws RuntimeException 113 | */ 114 | private function buildProcess(Process $process) 115 | { 116 | $process->runWithProcessController($this); 117 | 118 | $this->processes[$process->getProcessId()] = $process; 119 | 120 | return $process->getProcessId(); 121 | } 122 | 123 | /** 124 | * @param string $processName 125 | * @param array $options 126 | * 127 | * @return Process 128 | */ 129 | private function makeProcess($processName, array $options) 130 | { 131 | $process = new $processName($options); 132 | 133 | if (!$process instanceof Process) { 134 | throw new \InvalidArgumentException(); 135 | } 136 | 137 | return $process; 138 | } 139 | 140 | /** 141 | * 获取子进程结束事件回调 142 | * 143 | * @return \Closure 144 | */ 145 | public function getChildrenProcessShutdownHandler() 146 | { 147 | return function () { 148 | while ($ret = SwooleProcess::wait(false)) { 149 | if ($ret) { 150 | $process = clone $this->processes[$ret['pid']]; 151 | unset($this->processes[$ret['pid']]); 152 | 153 | if (!$this->terminate && $process->isAutoReload()) { 154 | $this->buildProcess($process); 155 | } 156 | } 157 | } 158 | 159 | if (!count($this->processes)) { 160 | foreach ($this->terminated as $terminated) { 161 | $terminated(); 162 | } 163 | 164 | exit(0); 165 | } 166 | }; 167 | } 168 | 169 | /** 170 | * 停止所有子进程 171 | */ 172 | public function terminate() 173 | { 174 | $this->terminate = true; 175 | 176 | foreach ($this->processes as $pid => $process) { 177 | $process->kill(); 178 | } 179 | } 180 | 181 | /** 182 | * 重新启动所有子进程 183 | * 184 | * 通过信号(默认 USR1)通知子进程自行重启 185 | */ 186 | public function reload() 187 | { 188 | foreach ($this->processes as $processId => $process) { 189 | $process->reload(); 190 | } 191 | } 192 | 193 | /** 194 | * 重新加载所有子进程 195 | * 196 | * 关闭并重新开启所有的子进程 197 | */ 198 | public function reopen() 199 | { 200 | foreach ($this->processes as $processId => $process) { 201 | $process->reopen(); 202 | } 203 | } 204 | 205 | /** 206 | * 注册终止回调 207 | * 208 | * @param \Closure $callback 209 | * 210 | * @return $this 211 | */ 212 | public function terminated(\Closure $callback) 213 | { 214 | $this->terminated[] = $callback; 215 | 216 | return $this; 217 | } 218 | 219 | /** 220 | * 获取 Master 进程 PID 221 | * 222 | * @return int 223 | */ 224 | public function getMasterProcessId() 225 | { 226 | return $this->masterProcess->getProcessId(); 227 | } 228 | } -------------------------------------------------------------------------------- /src/Http/ServerProcess.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Http; 10 | 11 | use Dybasedev\Keeper\Process\Process; 12 | use Illuminate\Container\Container; 13 | use Illuminate\Contracts\Container\Container as ContainerInterface; 14 | use Swoole\Http\Server; 15 | use Swoole\Http\Request; 16 | use Swoole\Http\Response; 17 | use Swoole\Process as SwooleProcess; 18 | 19 | /** 20 | * Class ServerProcess 21 | * 22 | * HTTP 服务器进程 23 | * 24 | * @package Dybasedev\Keeper\Http 25 | */ 26 | abstract class ServerProcess extends Process 27 | { 28 | /** 29 | * @var Server 30 | */ 31 | protected $server; 32 | 33 | /** 34 | * 该值与 $server 的区别在于其是在 worker start 后获取的 35 | * 36 | * @var Server 37 | */ 38 | protected $actualServer; 39 | 40 | /** 41 | * @var bool 42 | */ 43 | protected $worker = false; 44 | 45 | /** 46 | * @var int 47 | */ 48 | protected $workerId; 49 | 50 | /** 51 | * @var Container 52 | */ 53 | protected $container; 54 | 55 | /** 56 | * @inheritDoc 57 | */ 58 | public function process() 59 | { 60 | $this->server = $this->createSwooleServer(); 61 | 62 | $this->serverEventRegister(); 63 | 64 | $this->server->start(); 65 | } 66 | 67 | /** 68 | * @return \Closure 69 | */ 70 | protected function serverStartCallback() 71 | { 72 | return function () { 73 | $this->setProcessNameSuffix('', false); 74 | }; 75 | } 76 | 77 | /** 78 | * @return \Closure 79 | */ 80 | protected function workerStartCallback() 81 | { 82 | return function (Server $server, $workerId) { 83 | $this->actualServer = $server; 84 | $this->worker = true; 85 | $this->workerId = $workerId; 86 | $this->setProcessNameSuffix('worker#' . $workerId); 87 | 88 | $container = $this->getContainer(); 89 | $container->instance(SwooleProcess::class, $this->getSwooleProcess()); 90 | $container->instance(Server::class, $this->getActualServer()); 91 | $container->instance('workerId', $workerId); 92 | 93 | $this->onWorkerStart(); 94 | }; 95 | } 96 | 97 | /** 98 | * 获取容器 99 | * 100 | * @return Container 101 | */ 102 | public function getContainer() 103 | { 104 | if ($this->container) { 105 | return $this->container; 106 | } 107 | 108 | $this->container = Container::getInstance(); 109 | $this->container->instance(ContainerInterface::class, $this->container); 110 | 111 | return $this->container; 112 | } 113 | 114 | /** 115 | * @return \Closure 116 | */ 117 | protected function serverManagerStartCallback() 118 | { 119 | return function () { 120 | $this->setProcessNameSuffix('manager'); 121 | }; 122 | } 123 | 124 | /** 125 | * @return \Closure 126 | */ 127 | protected function httpRequestCallback() 128 | { 129 | return function (Request $request, Response $response) { 130 | $this->onRequest($request, $response); 131 | }; 132 | } 133 | 134 | /** 135 | * HTTP 请求事件 136 | * 137 | * @param Request $request 138 | * @param Response $response 139 | */ 140 | abstract function onRequest(Request $request, Response $response); 141 | 142 | /** 143 | * Worker 启动事件 144 | */ 145 | abstract function onWorkerStart(); 146 | 147 | /** 148 | * 获取 Worker 启动后的 Server 实例 149 | * 150 | * @return Server 151 | */ 152 | public function getActualServer() 153 | { 154 | return $this->actualServer; 155 | } 156 | 157 | /** 158 | * 判断是否是运行于 Worker 中 159 | * 160 | * @return bool 161 | */ 162 | public function isWorker() 163 | { 164 | return $this->worker; 165 | } 166 | 167 | /** 168 | * 获取 Worker ID 169 | * 170 | * @return int 171 | */ 172 | public function getWorkerId() 173 | { 174 | return $this->workerId; 175 | } 176 | 177 | /** 178 | * 创建 Swoole Server 实例 179 | * 180 | * @return Server 181 | */ 182 | public function createSwooleServer() 183 | { 184 | $server = new Server($this->options['host'] ?? '0.0.0.0', $this->options['port'] ?? 19730); 185 | $server->set([ 186 | 'worker_num' => $this->options['worker'] ?? 4, 187 | ]); 188 | 189 | return $server; 190 | } 191 | 192 | /** 193 | * 获取进程名设置 194 | * 195 | * @return string|null 196 | */ 197 | private function getProcessNamePrefix() 198 | { 199 | if (isset($this->options['process_name'])) { 200 | return $this->options['process_name']; 201 | } 202 | 203 | return null; 204 | } 205 | 206 | /** 207 | * 设置进程名后缀 208 | * 209 | * @param $suffix 210 | * @param bool $space 211 | */ 212 | private function setProcessNameSuffix($suffix, $space = true) 213 | { 214 | if ($processName = $this->getProcessNamePrefix()) { 215 | if ($space) { 216 | $suffix = ' ' . trim($suffix); 217 | } 218 | 219 | cli_set_process_title($processName . $suffix); 220 | } 221 | } 222 | 223 | /** 224 | * 服务器事件注册 225 | */ 226 | protected function serverEventRegister() 227 | { 228 | $this->server->on('start', $this->serverStartCallback()); 229 | $this->server->on('managerStart', $this->serverManagerStartCallback()); 230 | $this->server->on('workerStart', $this->workerStartCallback()); 231 | $this->server->on('request', $this->httpRequestCallback()); 232 | } 233 | } -------------------------------------------------------------------------------- /src/Process/ProcessManager.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Process; 10 | 11 | use Closure; 12 | use Dybasedev\Keeper\Process\Exceptions\OperationRejectedException; 13 | use Dybasedev\Keeper\Process\Exceptions\SingletonException; 14 | use Swoole\Process as SwProcess; 15 | 16 | /** 17 | * Class ProcessManager 18 | * 19 | * 标准主管理进程 20 | * 21 | * @package Dybasedev\Keeper\Process 22 | */ 23 | abstract class ProcessManager extends Process 24 | { 25 | use ProcessIdFileTrait; 26 | 27 | /** 28 | * @var bool 守护进程开关 29 | */ 30 | protected $daemon = false; 31 | 32 | /** 33 | * @var ProcessController 子进程控制器 34 | */ 35 | private $processController = null; 36 | 37 | /** 38 | * @var bool 39 | */ 40 | private $running = false; 41 | 42 | /** 43 | * @var array|Closure[] 44 | */ 45 | private $terminating = []; 46 | 47 | /** 48 | * @var array|Closure[] 49 | */ 50 | private $prepared = []; 51 | 52 | /** 53 | * @inheritDoc 54 | */ 55 | public function process() 56 | { 57 | try { 58 | $this->singleGuarantee(); 59 | 60 | if ($this->daemon) { 61 | $this->daemon(); 62 | } 63 | 64 | $this->freshProcessIdFile(); 65 | 66 | SwProcess::signal(SIGTERM, $this->onTerminating()); 67 | SwProcess::signal(SIGUSR1, $this->onReopen()); 68 | SwProcess::signal(SIGUSR2, $this->onReload()); 69 | 70 | $this->onPreparing(); 71 | 72 | foreach ($this->prepared as $callback) { 73 | $callback(); 74 | } 75 | 76 | $this->running = true; 77 | } catch (SingletonException $e) { 78 | fwrite(STDERR, "Have running instance (PID: {$e->runningInstanceProcessId}). Nothing to do.\n"); 79 | exit(1); 80 | } 81 | } 82 | 83 | abstract protected function onPreparing(); 84 | 85 | /** 86 | * 注册一个子进程实例 87 | * 88 | * @param Process $process 89 | * 90 | * @return $this 91 | */ 92 | public function registerChildProcess(Process $process) 93 | { 94 | if (is_null($this->processController)) { 95 | $this->processController = new ProcessController($this); 96 | SwProcess::signal(SIGCHLD, $this->processController->getChildrenProcessShutdownHandler()); 97 | 98 | $this->pushPreparedCallback(function () { 99 | $this->processController->bootstrap(); 100 | }); 101 | 102 | $this->pushTerminatingCallback(function () { 103 | $this->processController->terminate(); 104 | }); 105 | 106 | $this->processController->terminated(function () { 107 | $this->clearProcessIdFile(); 108 | }); 109 | } 110 | 111 | $this->processController->registerProcess($process); 112 | 113 | return $this; 114 | } 115 | 116 | /** 117 | * 压入一个预处理后的回调 118 | * 119 | * @param Closure $callback 120 | * 121 | * @return $this 122 | */ 123 | protected function pushPreparedCallback(Closure $callback) 124 | { 125 | $this->prepared[] = $callback; 126 | return $this; 127 | } 128 | 129 | /** 130 | * 压入一个终止时的回调 131 | * 132 | * @param Closure $callback 133 | * 134 | * @return $this 135 | */ 136 | protected function pushTerminatingCallback(Closure $callback) 137 | { 138 | $this->terminating[] = $callback; 139 | return $this; 140 | } 141 | 142 | /** 143 | * 终止事件 144 | * 145 | * @return Closure 146 | */ 147 | private function onTerminating() 148 | { 149 | return function () { 150 | if ($this->running) { 151 | foreach ($this->terminating as $callback) { 152 | $callback(); 153 | } 154 | 155 | if (!$this->processController) { 156 | $this->clearProcessIdFile(); 157 | } 158 | 159 | $this->running = false; 160 | } 161 | }; 162 | } 163 | 164 | /** 165 | * 重新加载事件 166 | * 167 | * 默认该操作会向所有子进程发起 USR1 信号,根据子进程注册参数会有差异 168 | * 169 | * @return Closure 170 | */ 171 | private function onReload() 172 | { 173 | return function () { 174 | $this->processController->reload(); 175 | }; 176 | } 177 | 178 | /** 179 | * 重新加载子进程事件 180 | * 181 | * 该操作会将所有子进程关闭并重新开启(或根据配置发起信号) 182 | * 183 | * @return Closure 184 | */ 185 | private function onReopen() 186 | { 187 | return function () { 188 | $this->processController->reopen(); 189 | }; 190 | } 191 | 192 | /** 193 | * @param bool $daemon 194 | * 195 | * @return $this 196 | */ 197 | public function setDaemon($daemon) 198 | { 199 | $this->daemon = $daemon; 200 | 201 | return $this; 202 | } 203 | 204 | /** 205 | * 重启 206 | * 207 | * @param bool $force 208 | */ 209 | public function restart($force = false) 210 | { 211 | try { 212 | $this->singleGuarantee(); 213 | $this->clearProcessIdFile(); 214 | 215 | if (!$force) { 216 | throw new OperationRejectedException(); 217 | } 218 | 219 | $this->run(); 220 | } catch (SingletonException $e) { 221 | $this->shutdownRunningInstance = true; 222 | $this->restart(true); 223 | } catch (OperationRejectedException $e) { 224 | fwrite(STDERR, "No instance can be restart.\n"); 225 | exit(2); 226 | } 227 | } 228 | 229 | /** 230 | * 停止 231 | */ 232 | public function stop() 233 | { 234 | $runningProcessId = $this->getProcessIdFromFile(); 235 | 236 | if ($runningProcessId === false) { 237 | fwrite(STDERR, "No running instance\n"); 238 | exit(4); 239 | } 240 | 241 | SwProcess::kill($runningProcessId); 242 | } 243 | } -------------------------------------------------------------------------------- /src/Process/Process.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://insp.top 7 | */ 8 | 9 | namespace Dybasedev\Keeper\Process; 10 | 11 | use Dybasedev\Keeper\Process\Interfaces\PipeProcess; 12 | use Dybasedev\Keeper\Process\Interfaces\StandardProcess; 13 | use Swoole\Process as SwooleProcess; 14 | 15 | /** 16 | * Class Process 17 | * 18 | * 标准进程类 19 | * 20 | * @package Dybasedev\Keeper\Process 21 | */ 22 | abstract class Process implements StandardProcess 23 | { 24 | /** 25 | * @var int Current process id. 26 | */ 27 | protected $processId; 28 | 29 | /** 30 | * @var SwooleProcess 31 | */ 32 | protected $swooleProcess; 33 | 34 | /** 35 | * @var int 36 | */ 37 | protected $masterId; 38 | 39 | /** 40 | * @var int 41 | */ 42 | protected $ownerGroupId = null; 43 | 44 | /** 45 | * @var int 46 | */ 47 | protected $ownerUserId = null; 48 | 49 | /** 50 | * @var array 51 | */ 52 | protected $options; 53 | 54 | /** 55 | * @var array 56 | */ 57 | public $runtime; 58 | 59 | /** 60 | * @var ProcessController 61 | */ 62 | protected $withProcessController = null; 63 | 64 | /** 65 | * @var resource 66 | */ 67 | public $pipe; 68 | 69 | /** 70 | * Process constructor. 71 | * 72 | * @param array $options 73 | */ 74 | public function __construct(array $options = []) 75 | { 76 | $this->options = $options; 77 | } 78 | 79 | /** 80 | * @param int $ownerGroupId 81 | * 82 | * @return Process 83 | */ 84 | public function setOwnerGroupId($ownerGroupId) 85 | { 86 | $this->ownerGroupId = $ownerGroupId; 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * @param int $ownerUserId 93 | * 94 | * @return Process 95 | */ 96 | public function setOwnerUserId($ownerUserId) 97 | { 98 | $this->ownerUserId = $ownerUserId; 99 | 100 | return $this; 101 | } 102 | 103 | /** 104 | * 启动进程 105 | * 106 | * @param int $masterId 107 | * 108 | * @return $this 109 | */ 110 | public function run($masterId = null) 111 | { 112 | $this->masterId = $masterId; 113 | 114 | if ($this->isTemporaryAutoReload()) { 115 | $this->clearTemporaryAutoLoadStatus(); 116 | } 117 | 118 | $swooleProcessInstance = $this->buildSwooleProcessInstance( 119 | $this->generateSwooleProcessCallback() 120 | ); 121 | 122 | $this->processId = $swooleProcessInstance->start(); 123 | $this->pipe = $swooleProcessInstance->pipe; 124 | $this->swooleProcess = $swooleProcessInstance; 125 | 126 | return $this; 127 | } 128 | 129 | /** 130 | * 通过控制器启动 131 | * 132 | * @param ProcessController $controller 133 | * 134 | * @return $this 135 | */ 136 | public function runWithProcessController($controller) 137 | { 138 | $this->withProcessController = $controller; 139 | 140 | return $this->run($controller->getMasterProcessId()); 141 | } 142 | 143 | /** 144 | * 清理临时自动重载状态 145 | */ 146 | public function clearTemporaryAutoLoadStatus() 147 | { 148 | unset($this->runtime['temp_auto_reload']); 149 | } 150 | 151 | /** 152 | * 变更所有者 153 | */ 154 | private function changeCurrentOwner() 155 | { 156 | if (!is_null($this->ownerUserId) && $this->ownerUserId != posix_getuid()) { 157 | posix_setuid($this->ownerUserId); 158 | } 159 | 160 | if (!is_null($this->ownerGroupId) && $this->ownerGroupId != posix_getgid()) { 161 | posix_setgid($this->ownerGroupId); 162 | } 163 | } 164 | 165 | /** 166 | * 是否自动重新加载 167 | * 168 | * @return bool 169 | */ 170 | public function isAutoReload() 171 | { 172 | if (!isset($this->options['auto_reload']) || 173 | $this->options['auto_reload'] === true || 174 | $this->isTemporaryAutoReload() 175 | ) { 176 | return true; 177 | } 178 | 179 | return false; 180 | } 181 | 182 | /** 183 | * 是否为临时的允许重新加载 184 | * 185 | * @return bool 186 | */ 187 | public function isTemporaryAutoReload() 188 | { 189 | if (isset($this->runtime['temp_auto_reload']) && $this->runtime['temp_auto_reload']) { 190 | return true; 191 | } 192 | 193 | return false; 194 | } 195 | 196 | /** 197 | * 获取该进程 ID 198 | * 199 | * @return int 200 | */ 201 | public function getProcessId() 202 | { 203 | return $this->processId; 204 | } 205 | 206 | /** 207 | * 获取设置项 208 | * 209 | * @return array 210 | */ 211 | public function getOptions() 212 | { 213 | return $this->options; 214 | } 215 | 216 | /** 217 | * 获取该进程 Swoole\Process 实例 218 | * 219 | * @return SwooleProcess 220 | */ 221 | public function getSwooleProcess() 222 | { 223 | return $this->swooleProcess; 224 | } 225 | 226 | /** 227 | * 获取该进程主(父)进程 ID 228 | * 229 | * @return int 230 | */ 231 | public function getMasterId() 232 | { 233 | return $this->masterId; 234 | } 235 | 236 | protected function daemon() 237 | { 238 | SwooleProcess::daemon(true, true); 239 | 240 | $this->processId = posix_getpid(); 241 | } 242 | 243 | public function kill($signal = SIGTERM) 244 | { 245 | SwooleProcess::kill($this->getProcessId(), $signal); 246 | } 247 | 248 | /** 249 | * 进程重启 250 | */ 251 | public function reload() 252 | { 253 | if ($this->withProcessController) { 254 | if (isset($this->options['on_reload'])) { 255 | if (isset($this->options['on_reload']['signal'])) { 256 | $this->kill($this->options['on_reload']['signal']); 257 | 258 | return; 259 | } elseif ($this->options['on_reload'] === false) { 260 | return; 261 | } 262 | } 263 | 264 | $this->kill(SIGUSR1); 265 | } 266 | } 267 | 268 | /** 269 | * 进程重载 270 | */ 271 | public function reopen() 272 | { 273 | if ($this->withProcessController) { 274 | if (isset($this->options['on_reopen'])) { 275 | if (isset($this->options['on_reopen']['signal'])) { 276 | $this->kill($this->options['on_reopen']['signal']); 277 | 278 | return; 279 | } elseif ($this->options['on_reopen'] === false) { 280 | return; 281 | } 282 | } 283 | 284 | if (!$this->isAutoReload()) { 285 | $this->runtime['temp_auto_reload'] = true; 286 | $this->kill(); 287 | } 288 | } 289 | } 290 | 291 | public function __clone() 292 | { 293 | $this->swooleProcess = null; 294 | $this->processId = null; 295 | $this->withProcessController = null; 296 | } 297 | 298 | /** 299 | * 生成 Swoole 进程回调 300 | * 301 | * @return \Closure 302 | */ 303 | protected function generateSwooleProcessCallback() 304 | { 305 | return function (SwooleProcess $process) { 306 | $this->swooleProcess = $process; 307 | $this->processId = $process->pid; 308 | 309 | // 对于超级管理员用户而言,该操作才会生效。 310 | // 该操作调用了 setuid 和 setgid 作改变当前进程的实际用户 ID。 311 | $this->changeCurrentOwner(); 312 | 313 | // 调用实际进程处理逻辑 314 | $this->process(); 315 | }; 316 | } 317 | 318 | /** 319 | * 构造 Swoole Process 实例 320 | * 321 | * @param callable $processCallback 322 | * 323 | * @return SwooleProcess 324 | */ 325 | public function buildSwooleProcessInstance($processCallback) 326 | { 327 | if ($this instanceof PipeProcess) { 328 | $process = new SwooleProcess($processCallback, $this->isRedirectStdIO(), $this->getPipeType()); 329 | } else { 330 | $process = new SwooleProcess($processCallback); 331 | } 332 | 333 | return $process; 334 | } 335 | } --------------------------------------------------------------------------------