├── .github └── workflows │ └── default.yml ├── .phplint.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── behat.yml ├── composer.json ├── docker-compose.yml ├── features ├── bootstrap │ └── FeatureContext.php ├── process_control │ ├── admin_gets_information.feature │ ├── admin_starts_process.feature │ └── admin_stops_process.feature └── status_control │ ├── admin_checks_status.feature │ └── admin_controls_supervisor_process.feature ├── phpspec.yml ├── phpstan.neon ├── resources └── supervisord.conf └── src ├── Exception ├── Fault │ ├── AbnormalTerminationException.php │ ├── AlreadyAddedException.php │ ├── AlreadyStartedException.php │ ├── BadArgumentsException.php │ ├── BadNameException.php │ ├── BadSignalException.php │ ├── CantRereadException.php │ ├── FailedException.php │ ├── IncorrectParametersException.php │ ├── NoFileException.php │ ├── NotExecutableException.php │ ├── NotRunningException.php │ ├── ShutdownStateException.php │ ├── SignatureUnsupportedException.php │ ├── SpawnErrorException.php │ ├── StillRunningException.php │ ├── SuccessException.php │ └── UnknownMethodException.php ├── FaultCodes.php └── SupervisorException.php ├── Process.php ├── ProcessInterface.php ├── ProcessStates.php ├── ReloadResult.php ├── ReloadResultInterface.php ├── ServiceStates.php ├── Supervisor.php └── SupervisorInterface.php /.github/workflows/default.yml: -------------------------------------------------------------------------------- 1 | name : Test Suite 2 | 3 | on : 4 | push : 5 | pull_request : 6 | 7 | jobs : 8 | test : 9 | runs-on : ubuntu-latest 10 | 11 | strategy : 12 | matrix : 13 | php_version : [ '8.1' ] 14 | steps : 15 | - uses : actions/checkout@v2 16 | 17 | - name : Set up PHP 18 | uses : shivammathur/setup-php@v2 19 | with : 20 | php-version : ${{ matrix.php_version }} 21 | coverage : xdebug 22 | tools : composer:v2, cs2pr 23 | 24 | - name : Install Supervisor 25 | run : | 26 | sudo apt-get install -y --no-install-recommends supervisor 27 | 28 | - name : Install Composer dependencies 29 | run : | 30 | composer install --no-interaction 31 | 32 | - name : Run PHP Linter 33 | run : | 34 | vendor/bin/parallel-lint . --exclude vendor --checkstyle | cs2pr 35 | 36 | - name : Run PHPStan 37 | run : | 38 | vendor/bin/phpstan analyze --xdebug --memory-limit=-1 --error-format=checkstyle | cs2pr 39 | 40 | - name : Run CI Tests 41 | run : | 42 | composer run phpspec 43 | composer run behat 44 | -------------------------------------------------------------------------------- /.phplint.yml: -------------------------------------------------------------------------------- 1 | path: ./ 2 | jobs: 10 3 | cache: /tmp/phplint.cache 4 | extensions: 5 | - php 6 | exclude: 7 | - vendor 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 4.0.0 - 2020-07-01 4 | 5 | ### Added 6 | 7 | - PHP 7.3 minimum requirement 8 | - Depends directly on fXmlRpc library 9 | - `Supervisor\Supervisor->reloadConfig()` method call added 10 | 11 | ### Changed 12 | 13 | - Constructor of `Supervisor\Supervisor` now directly accepts an `fXmlRpc\ClientInterface` instead 14 | - Classes `Supervisor\Supervisor` and `Supervisor\Process` are final 15 | - The base exception is now called `Supervisor\Exception\SupervisorException` (to avoid confusion with fXmlRpc's own `FaultException`) and specific fault classes end in the `Exception` class name 16 | - Test suite updated 17 | 18 | ### Removed 19 | 20 | - `Supervisor\Connector` and fXmlRpc and Zend/XmlRpc connector classes removed 21 | 22 | ## 3.0.0 - 2015-01-13 23 | 24 | ### Added 25 | 26 | - PHP 5.4 minimum requirement 27 | - byte value check in configuration sections 28 | 29 | ### Changed 30 | 31 | - Refactors Connectors (interface changed) 32 | - Closes down API 33 | - Updates tests (uses PhpSpec and Behat) 34 | - Major API change (BC break!) 35 | - Configuration moved to a different package 36 | - Event moved to a different package 37 | - `isState` method is renamed to `checkState` (in both `Supervisor` and `Process`) 38 | - Process must wait for the response of stop in `restart` 39 | - `Section`s now use the name property instead of option 40 | - `Section`s are able to return/set separate properties as well 41 | - Updates dependencies 42 | - Process object is immutable 43 | 44 | ### Removed 45 | 46 | - Ability to pass `Process` object into `Supervisor` method calls: in case of different connector instances it could have led to an inconsistent state 47 | - Ability to construct `Process` object from name, use `Process::get` instead 48 | - Memory usage check form `Process` 49 | - Fluent interfaces 50 | - `setCredentials` method from `Connector` interface 51 | - `isLocal` method from `Connector` interface 52 | 53 | 54 | ## 2.0.1 - 2014-07-13 55 | 56 | ### Changed 57 | 58 | - Updates dependencies 59 | 60 | 61 | ## 2.0.0 - 2014-07-13 62 | 63 | ### Added 64 | 65 | - Zend XML-RPC connector 66 | - `AbstractNamedSection` 67 | 68 | ### Changed 69 | 70 | - Uses Guzzle as HTTP Client by default 71 | - Event and Event Listener restructure 72 | - Major test changes (unit, functional) 73 | 74 | ### Removed 75 | 76 | - HTTP client parts 77 | - API from `Supervisor` 78 | 79 | 80 | ## 1.2.0 - 2014-05-06 81 | 82 | ### Changed 83 | 84 | - Code coverage improved 85 | - Unit tests improved 86 | - Travis build improved 87 | - Minor fixes 88 | 89 | 90 | ## 1.1.1 - 2014-01-29 91 | 92 | ### Changed 93 | 94 | - Unit tests moved into Test namespace 95 | - Fixed license issues 96 | 97 | 98 | ## 1.1.0 - 2014-01-20 99 | 100 | ### Added 101 | 102 | - Symfony Commands 103 | - Symfony Console Application 104 | - Event Listeners 105 | - `isLocal` to Connectors and Supervisor 106 | - `SupervisorException` 107 | - `RpcInterfaceSection` 108 | 109 | ### Changed 110 | 111 | - Improved unit tests 112 | - Fixed several bugs 113 | 114 | ### Removed 115 | 116 | - `ResponseException` 117 | 118 | 119 | ## 1.0.0 - 2014-01-17 120 | 121 | ### Added 122 | 123 | - Initial release 124 | - Supervisor 125 | - Configuration 126 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.1-cli-alpine 2 | 3 | RUN apk add --no-cache bash zip git curl supervisor 4 | 5 | COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ 6 | 7 | RUN install-php-extensions @composer pcntl posix xdebug 8 | 9 | RUN mkdir /app 10 | WORKDIR /app 11 | 12 | CMD ["composer", "run", "ci"] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Márk Sági-Kazár 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Supervisor 2 | 3 | [![Latest Version](https://img.shields.io/github/release/supervisorphp/supervisor.svg?style=flat-square)](https://github.com/supervisorphp/supervisor/releases) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 5 | [![Test Suite](https://github.com/supervisorphp/supervisor/workflows/Test%20Suite/badge.svg?event=push)](https://github.com/supervisorphp/supervisor/actions) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/supervisorphp/supervisor.svg?style=flat-square)](https://packagist.org/packages/supervisorphp/supervisor) 7 | 8 | **PHP library for managing Supervisor through XML-RPC API.** 9 | 10 | ## Install 11 | 12 | Via Composer 13 | 14 | ```bash 15 | composer require supervisorphp/supervisor 16 | ``` 17 | 18 | ## Usage 19 | 20 | This library depends on the fast and powerful [fXmlRpc](https://github.com/lstrojny/fxmlrpc) library, which supports a number of adapters to use your preferred HTTP client to make connections. 21 | 22 | In the example below, we will use the popular Guzzle HTTP client library. 23 | 24 | This example requires some additional libraries to function. To include the necessary extra components, you can run: 25 | 26 | ```bash 27 | composer require guzzlehttp/guzzle:^7.0 28 | ``` 29 | 30 | This example shows how to pass authentication credentials to Guzzle, initiate the fXmlRpc client, and pass that to SupervisorPHP. 31 | 32 | ```php 33 | // Create Guzzle HTTP client 34 | $guzzleClient = new \GuzzleHttp\Client([ 35 | 'auth' => ['user', '123'], 36 | ]); 37 | 38 | // Pass the url and the guzzle client to the fXmlRpc Client 39 | $client = new fXmlRpc\Client( 40 | 'http://127.0.0.1:9001/RPC2', 41 | new fXmlRpc\Transport\PsrTransport( 42 | new GuzzleHttp\Psr7\HttpFactory(), 43 | $guzzleClient 44 | ) 45 | ); 46 | 47 | // Or, if connecting via a Unix Domain Socket 48 | $guzzleClient = new \GuzzleHttp\Client([ 49 | 'curl' => [ 50 | \CURLOPT_UNIX_SOCKET_PATH => '/var/run/supervisor.sock', 51 | ], 52 | ]); 53 | 54 | $client = new fXmlRpc\Client( 55 | 'http://localhost/RPC2', 56 | new fXmlRpc\Transport\PsrTransport( 57 | new GuzzleHttp\Psr7\HttpFactory(), 58 | $guzzleClient 59 | ) 60 | ); 61 | 62 | // Pass the client to the Supervisor library. 63 | $supervisor = new Supervisor\Supervisor($client); 64 | 65 | // returns Process object 66 | $process = $supervisor->getProcess('test_process'); 67 | 68 | // returns array of process info 69 | $supervisor->getProcessInfo('test_process'); 70 | 71 | // same as $supervisor->stopProcess($process); 72 | $supervisor->stopProcess('test_process'); 73 | 74 | // Don't wait for process start, return immediately 75 | $supervisor->startProcess($process, false); 76 | 77 | // returns true if running 78 | // same as $process->checkState(Process::RUNNING); 79 | $process->isRunning(); 80 | 81 | // returns process name 82 | echo $process; 83 | 84 | // returns process information 85 | $process->getPayload(); 86 | ``` 87 | 88 | ### Exception handling 89 | 90 | For each possible fault response there is an exception. These exceptions extend a [common exception](src/Exception/Fault.php), so you are able to catch a specific fault or all. When an unknown fault is returned from the server, an instance if the common exception is thrown. The list of fault responses and the appropriate exception can be found in the class. 91 | 92 | ```php 93 | /** @var \Supervisor\Supervisor $supervisor */ 94 | 95 | try { 96 | $supervisor->startProcess('process', true); 97 | } catch (\Supervisor\Exception\Fault\BadNameException $e) { 98 | // handle bad name error here 99 | } catch (\Supervisor\Exception\SupervisorException $e) { 100 | // handle any other errors here 101 | } 102 | ``` 103 | 104 | ## Configuration and Event listening 105 | 106 | [Configuration](https://github.com/supervisorphp/configuration) and [Event](https://github.com/supervisorphp/event) components have been moved into their own repository. 107 | 108 | ## Further info 109 | 110 | You can find the Supervisor XML-RPC documentation here: 111 | [http://supervisord.org/api.html](http://supervisord.org/api.html) 112 | 113 | ## Notice 114 | 115 | If you use PHP XML-RPC extension to parse responses (which is marked as *EXPERIMENTAL*). This can cause issues when you are trying to read/tail log of a PROCESS. Make sure you clean your log messages. The only information I found about this is a [comment](http://www.php.net/function.xmlrpc-decode#44213). 116 | 117 | ## Contributing 118 | 119 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 120 | 121 | ### Testing 122 | 123 | ```bash 124 | $ composer test 125 | ``` 126 | 127 | Functional tests (behat): 128 | 129 | ```bash 130 | $ behat 131 | ``` 132 | 133 | ### Docker Image 134 | 135 | This repository ships with a Docker Compose configuration and a Dockerfile for easy testing. Tests can be run via: 136 | 137 | ```bash 138 | docker-compose run --rm ci 139 | ``` 140 | 141 | ## Deprecated Libraries 142 | 143 | While this tries to be a complete Supervisor client, this isn't the first one. However some authors decided to deprecate their packages in favor of this: 144 | 145 | - [Supervisord PHP Client](https://github.com/mondalaci/supervisord-php-client) 146 | - [Indigo Supervisor](https://github.com/indigophp/supervisor) 147 | 148 | ## Credits 149 | 150 | - [László Monda](https://github.com/mondalaci) (author of Supervisord PHP Client) 151 | - [Márk Sági-Kazár](https://github.com/sagikazarmark) 152 | - [All Contributors](https://github.com/supervisorphp/supervisor/contributors) 153 | 154 | ## License 155 | 156 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 157 | -------------------------------------------------------------------------------- /behat.yml: -------------------------------------------------------------------------------- 1 | default: 2 | suites: 3 | default: 4 | contexts: 5 | - FeatureContext: 6 | - supervisord 7 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supervisorphp/supervisor", 3 | "description": "PHP library for managing Supervisor through XML-RPC API", 4 | "homepage": "http://supervisorphp.com", 5 | "license": "MIT", 6 | "keywords": [ 7 | "supervisor", 8 | "process manager" 9 | ], 10 | "authors": [ 11 | { 12 | "name": "Márk Sági-Kazár", 13 | "email": "mark.sagikazar@gmail.com" 14 | }, 15 | { 16 | "name": "Buster Neece", 17 | "email": "buster@busterneece.com" 18 | } 19 | ], 20 | "autoload": { 21 | "psr-4": { 22 | "Supervisor\\": "src/" 23 | } 24 | }, 25 | "minimum-stability": "dev", 26 | "prefer-stable": true, 27 | "require": { 28 | "php": ">=8.1", 29 | "lstrojny/fxmlrpc": ">=0.12", 30 | "psr/log": ">=1" 31 | }, 32 | "require-dev": { 33 | "ext-pcntl": "*", 34 | "ext-posix": "*", 35 | "behat/behat": "^3.0", 36 | "guzzlehttp/guzzle": "^7.5", 37 | "php-http/httplug": "^2.1", 38 | "php-http/message": "^1.8", 39 | "php-parallel-lint/php-console-highlighter": "^1", 40 | "php-parallel-lint/php-parallel-lint": "^1.3", 41 | "phpspec/phpspec": "^7", 42 | "phpstan/phpstan": "^1", 43 | "phpstan/phpstan-strict-rules": "^1", 44 | "roave/security-advisories": "dev-latest", 45 | "supervisorphp/configuration": "^0.3" 46 | }, 47 | "scripts": { 48 | "ci": [ 49 | "@composer install --prefer-dist --no-progress --no-suggest", 50 | "@phplint", 51 | "@phpstan", 52 | "@phpspec", 53 | "@behat" 54 | ], 55 | "phplint": "parallel-lint . --exclude vendor", 56 | "phpstan": "phpstan analyze --xdebug --memory-limit=-1", 57 | "phpspec": "phpspec run", 58 | "behat": "behat" 59 | }, 60 | "config": { 61 | "preferred-install": "dist", 62 | "discard-changes": true, 63 | "sort-packages": true, 64 | "allow-plugins": { 65 | "php-http/discovery": true 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | ci: 5 | image: supervisorphp/supervisor:latest 6 | build: 7 | context: . 8 | volumes: 9 | - ./:/app -------------------------------------------------------------------------------- /features/bootstrap/FeatureContext.php: -------------------------------------------------------------------------------- 1 | bin = $bin; 31 | } 32 | 33 | /** 34 | * @BeforeScenario 35 | */ 36 | public function setUpSupervisor(BeforeScenarioScope $scope) 37 | { 38 | $loader = new IniStringLoader(file_get_contents(__DIR__ . '/../../resources/supervisord.conf')); 39 | $this->configuration = $loader->load(); 40 | 41 | $supervisord = $this->configuration->getSection('supervisord'); 42 | $supervisord->setProperty('nodaemon', true); 43 | 44 | $this->setUpConnector(); 45 | } 46 | 47 | protected function setUpConnector() 48 | { 49 | $guzzleClient = new \GuzzleHttp\Client([ 50 | 'auth' => ['user', '123'], 51 | ]); 52 | 53 | $client = new Client( 54 | 'http://127.0.0.1:9001/RPC2', 55 | new \fXmlRpc\Transport\PsrTransport( 56 | new \GuzzleHttp\Psr7\HttpFactory(), 57 | $guzzleClient 58 | ) 59 | ); 60 | 61 | $this->supervisor = new Supervisor($client); 62 | } 63 | 64 | /** 65 | * @AfterScenario 66 | */ 67 | public function stopSupervisor(AfterScenarioScope $scope) 68 | { 69 | isset($this->process) and posix_kill($this->process, \SIGKILL); 70 | } 71 | 72 | /** 73 | * @Given I have Supervisor running 74 | */ 75 | public function iHaveSupervisorRunning() 76 | { 77 | $renderer = new Renderer(Renderer::ARRAY_MODE_CONCAT | Renderer::BOOLEAN_MODE_BOOL_STRING); 78 | $ini = $renderer->render($this->configuration->toArray()); 79 | 80 | file_put_contents($file = tempnam(sys_get_temp_dir(), 'supervisord_'), $ini); 81 | 82 | if ($this->supervisor->isConnected()) { 83 | posix_kill($this->supervisor->getPID(), \SIGKILL); 84 | } 85 | 86 | $command = sprintf('(%s --configuration %s > /dev/null 2>&1 & echo $!)&', $this->bin, $file); 87 | exec($command, $op); 88 | $this->process = (int)$op[0]; 89 | 90 | $c = 0; 91 | while (!$this->supervisor->isConnected() and $c < 100) { 92 | usleep(10000); 93 | $c++; 94 | } 95 | 96 | if ($c >= 100) { 97 | throw new \RuntimeException('Could not connect to supervisord'); 98 | } 99 | 100 | if ($this->process !== $this->supervisor->getPID()) { 101 | throw new \RuntimeException('Connected to supervisord with a different PID'); 102 | } 103 | } 104 | 105 | /** 106 | * @When I ask for the API version 107 | */ 108 | public function iAskForTheApiVersion() 109 | { 110 | $this->version = $this->supervisor->getAPIVersion(); 111 | } 112 | 113 | /** 114 | * @Then I should get at least :ver version 115 | */ 116 | public function iShouldGetAtLeastVersion($ver) 117 | { 118 | if (version_compare($this->version, $ver) == -1) { 119 | throw new \Exception(sprintf('Version "%s" does not match the minimum required "%s"', $this->version, 120 | $ver)); 121 | } 122 | } 123 | 124 | /** 125 | * @When I ask for Supervisor version 126 | */ 127 | public function iAskForSupervisorVersion() 128 | { 129 | $this->version = $this->supervisor->getVersion(); 130 | } 131 | 132 | /** 133 | * @Given my Supervisor instance is called :identifier 134 | */ 135 | public function mySupervisorInstanceIsCalled($identifier) 136 | { 137 | $supervisord = $this->configuration->getSection('supervisord'); 138 | $supervisord->setProperty('identifier', $identifier); 139 | } 140 | 141 | /** 142 | * @When I ask for Supervisor identification 143 | */ 144 | public function iAskForSupervisorIdentification() 145 | { 146 | $this->identifier = $this->supervisor->getIdentification(); 147 | } 148 | 149 | /** 150 | * @Then I should get :identifier as identifier 151 | */ 152 | public function iShouldGetAsIdentifier($identifier) 153 | { 154 | if ($this->identifier !== $identifier) { 155 | throw new \Exception(sprintf('Identification "%s" does not match the required "%s"', $this->identifier, 156 | $identifier)); 157 | } 158 | } 159 | 160 | /** 161 | * @When I ask for the state 162 | */ 163 | public function iAskForTheState() 164 | { 165 | $this->state = $this->supervisor->getState(); 166 | } 167 | 168 | /** 169 | * @Then I should get :code as statecode and :name as statename 170 | */ 171 | public function iShouldGetAsStatecodeAndAsStatename($code, $name) 172 | { 173 | if ($this->state['statecode'] != $code) { 174 | throw new \Exception(sprintf('State code "%s" does not match the required "%s"', $this->state['statecode'], 175 | $code)); 176 | } 177 | 178 | if ($this->state['statename'] !== $name) { 179 | throw new \Exception(sprintf('Statename "%s" does not match the required "%s"', $this->state['statename'], 180 | $name)); 181 | } 182 | } 183 | 184 | /** 185 | * @When I ask for the PID 186 | */ 187 | public function iAskForThePid() 188 | { 189 | $this->pid = $this->supervisor->getPID(); 190 | } 191 | 192 | /** 193 | * @Then I should get the real PID 194 | */ 195 | public function iShouldGetTheRealPid() 196 | { 197 | if ($this->process !== $this->pid) { 198 | throw new \Exception(sprintf('PID "%s" does not match the real "%s"', $this->pid, $this->process)); 199 | } 200 | } 201 | 202 | /** 203 | * @When I ask for the log 204 | */ 205 | public function iAskForTheLog() 206 | { 207 | $this->log = trim($this->supervisor->readLog(-(35 + strlen($this->process)), 0)); 208 | } 209 | 210 | /** 211 | * @Then I should get an INFO about supervisord started 212 | */ 213 | public function iShouldGetAnInfoAboutSupervisordStarted() 214 | { 215 | if ($this->log !== 'INFO supervisord started with pid ' . $this->process) { 216 | throw new \Exception(sprintf('The following log entry was expected: "%s", but we got this: "%s"', 217 | 'INFO supervisord started with pid ' . $this->process, $this->log)); 218 | } 219 | } 220 | 221 | /** 222 | * @When I try to call :action action 223 | */ 224 | public function iTryToCallAction($action) 225 | { 226 | $this->action = $action; 227 | $this->response = call_user_func([$this->supervisor, $action]); 228 | } 229 | 230 | /** 231 | * @When I check if the log is really empty 232 | */ 233 | public function iCheckIfTheLogIsReallyEmpty() 234 | { 235 | $this->log = trim($this->supervisor->readLog(-24, 0)); 236 | } 237 | 238 | /** 239 | * @Then I should get a success response 240 | */ 241 | public function iShouldGetASuccessResponse() 242 | { 243 | if ($this->response !== true) { 244 | throw new \Exception(sprintf('Action "%s" was unsuccessful', $this->action)); 245 | } 246 | } 247 | 248 | /** 249 | * @Then it should be stopped 250 | */ 251 | public function itShouldBeStopped() 252 | { 253 | if ($this->supervisor->isConnected() === true) { 254 | throw new \Exception('Supervisor is still available'); 255 | } 256 | } 257 | 258 | /** 259 | * @Then it should be running again 260 | */ 261 | public function itShouldBeRunningAgain() 262 | { 263 | if ($this->supervisor->isConnected() !== true) { 264 | throw new \Exception('Supervisor is unavailable'); 265 | } 266 | } 267 | 268 | /** 269 | * @Given I have a process called :process 270 | */ 271 | public function iHaveAProcessCalled($process) 272 | { 273 | $this->processName = $this->processes[] = $process; 274 | 275 | $program = new Section\Program($process, [ 276 | 'command' => exec('which ' . $process), 277 | ]); 278 | 279 | $this->configuration->addSection($program); 280 | } 281 | 282 | /** 283 | * @When I wait for start 284 | */ 285 | public function iWaitForStart() 286 | { 287 | usleep(100000); 288 | } 289 | 290 | /** 291 | * @When I get information about the processes 292 | */ 293 | public function iGetInformationAboutTheProcesses() 294 | { 295 | $processInfo = $this->supervisor->getAllProcessInfo(); 296 | $processNames = array_column($processInfo, 'name'); 297 | $this->processInfo = array_combine($processNames, $processInfo); 298 | } 299 | 300 | /** 301 | * @Then I should see running 302 | */ 303 | public function iShouldSeeRunning() 304 | { 305 | foreach ($this->processes as $process) { 306 | if (!isset($this->processInfo[$process]) or $this->processInfo[$process]['state'] < 10) { 307 | throw new \Exception(sprintf('Process "%s" is not running', $process)); 308 | } 309 | } 310 | } 311 | 312 | /** 313 | * @Given autostart is disabled 314 | */ 315 | public function autostartIsDisabled() 316 | { 317 | $program = $this->configuration->getSection('program:' . $this->processName); 318 | 319 | $program->setProperty('autostart', false); 320 | } 321 | 322 | /** 323 | * @When I get information about the processes before action 324 | */ 325 | public function iGetInformationAboutTheProcessesBeforeAction() 326 | { 327 | $processInfo = $this->supervisor->getAllProcessInfo(); 328 | $processNames = array_column($processInfo, 'name'); 329 | $this->firstProcessInfo = array_combine($processNames, $processInfo); 330 | } 331 | 332 | /** 333 | * @When I :action the process 334 | */ 335 | public function iTheProcess($action) 336 | { 337 | $this->action = $action . 'Process'; 338 | $this->response = call_user_func([$this->supervisor, $this->action], $this->processName, false); 339 | } 340 | 341 | /** 342 | * @Then I should see not running first 343 | */ 344 | public function iShouldSeeNotRunningFirst() 345 | { 346 | foreach ($this->processes as $process) { 347 | if (!isset($this->firstProcessInfo[$process]) or $this->firstProcessInfo[$process]['state'] > 0) { 348 | throw new \Exception(sprintf('Process "%s" is running', $process)); 349 | } 350 | } 351 | } 352 | 353 | /** 354 | * @When I :action the processes 355 | */ 356 | public function iTheProcesses($action) 357 | { 358 | $this->action = $action . 'AllProcesses'; 359 | $this->response = call_user_func([$this->supervisor, $this->action], true); 360 | } 361 | 362 | /** 363 | * @Then I should get a success response for all 364 | */ 365 | public function iShouldGetASuccessResponseForAll() 366 | { 367 | foreach ($this->response as $response) { 368 | if ($response['description'] !== 'OK') { 369 | throw new \Exception(sprintf('Action "%s" was unsuccessful', $this->action)); 370 | } 371 | } 372 | } 373 | 374 | /** 375 | * @Then I should see running first 376 | */ 377 | public function iShouldSeeRunningFirst() 378 | { 379 | foreach ($this->processes as $process) { 380 | if (!isset($this->firstProcessInfo[$process]) or $this->firstProcessInfo[$process]['state'] < 10) { 381 | throw new \Exception(sprintf('Process "%s" is not running before "%s"', $process, $this->action)); 382 | } 383 | } 384 | } 385 | 386 | /** 387 | * @Then I should see not running 388 | */ 389 | public function iShouldSeeNotRunning() 390 | { 391 | foreach ($this->processes as $process) { 392 | if (!isset($this->processInfo[$process]) or $this->processInfo[$process]['state'] > 0) { 393 | throw new \Exception(sprintf('Process "%s" is running', $process)); 394 | } 395 | } 396 | } 397 | 398 | /** 399 | * @Given it is part of group called :grp 400 | */ 401 | public function itIsPartOfGroupCalled($grp) 402 | { 403 | $this->groupName = $grp; 404 | 405 | $program = $this->configuration->getSection('program:' . $this->processName); 406 | $group = $this->configuration->getSection('group:' . $grp); 407 | 408 | if (is_null($group)) { 409 | $group = new Section\Group($grp, ['programs' => $this->processName]); 410 | $this->configuration->addSection($group); 411 | } else { 412 | $programs = $group->getProperty('programs'); 413 | $programs[] = $this->processName; 414 | $group->setProperty('programs', $programs); 415 | } 416 | } 417 | 418 | /** 419 | * @When I :action the processes in the group 420 | */ 421 | public function iTheProcessesInTheGroup($action) 422 | { 423 | $this->action = $action . 'ProcessGroup'; 424 | $this->response = call_user_func([$this->supervisor, $this->action], $this->groupName, false); 425 | } 426 | 427 | /** 428 | * @Then I should see them as part of the group 429 | */ 430 | public function iShouldSeeThemAsPartOfTheGroup() 431 | { 432 | foreach ($this->response as $response) { 433 | if ($response['group'] !== $this->groupName) { 434 | throw new \Exception(sprintf('Process "%s" is not part of the group "%s"', $response['name'], 435 | $this->groupName)); 436 | } 437 | } 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /features/process_control/admin_gets_information.feature: -------------------------------------------------------------------------------- 1 | Feature: Admin gets information 2 | In order to know what processes are running 3 | As an Admin 4 | I should be able to get information about them 5 | 6 | Scenario: 7 | Given I have a process called "cat" 8 | And I have Supervisor running 9 | When I wait for start 10 | And I get information about the processes 11 | Then I should see running 12 | 13 | Scenario: 14 | Given I have a process called "cat" 15 | And I have a process called "tee" 16 | And I have Supervisor running 17 | When I wait for start 18 | And I get information about the processes 19 | Then I should see running 20 | -------------------------------------------------------------------------------- /features/process_control/admin_starts_process.feature: -------------------------------------------------------------------------------- 1 | Feature: Admin starts process 2 | In order to make some processes running 3 | As an Admin 4 | I should be able to start them various ways 5 | 6 | Scenario: 7 | Given I have a process called "cat" 8 | And autostart is disabled 9 | And I have Supervisor running 10 | When I get information about the processes before action 11 | And I "start" the process 12 | And I get information about the processes 13 | Then I should see not running first 14 | Then I should get a success response 15 | And I should see running 16 | 17 | Scenario: 18 | Given I have a process called "cat" 19 | And autostart is disabled 20 | And I have a process called "tee" 21 | And autostart is disabled 22 | And I have Supervisor running 23 | When I get information about the processes before action 24 | And I "start" the processes 25 | And I get information about the processes 26 | Then I should see not running first 27 | Then I should get a success response for all 28 | And I should see running 29 | 30 | Scenario: 31 | Given I have a process called "cat" 32 | And autostart is disabled 33 | And it is part of group called "test" 34 | And I have a process called "tee" 35 | And autostart is disabled 36 | And it is part of group called "test" 37 | And I have Supervisor running 38 | When I get information about the processes before action 39 | And I "start" the processes in the group 40 | And I get information about the processes 41 | Then I should see not running first 42 | But I should see them as part of the group 43 | Then I should get a success response for all 44 | And I should see running 45 | -------------------------------------------------------------------------------- /features/process_control/admin_stops_process.feature: -------------------------------------------------------------------------------- 1 | Feature: Admin stops process 2 | In order to make some processes stopped 3 | As an Admin 4 | I should be able to stop them various ways 5 | 6 | Scenario: 7 | Given I have a process called "cat" 8 | And I have Supervisor running 9 | When I wait for start 10 | And I get information about the processes before action 11 | And I "stop" the process 12 | And I get information about the processes 13 | Then I should see running first 14 | Then I should get a success response 15 | And I should see not running 16 | 17 | Scenario: 18 | Given I have a process called "cat" 19 | And I have a process called "tee" 20 | And I have Supervisor running 21 | When I wait for start 22 | And I get information about the processes before action 23 | And I "stop" the processes 24 | And I get information about the processes 25 | Then I should see running first 26 | Then I should get a success response for all 27 | And I should see not running 28 | 29 | Scenario: 30 | Given I have a process called "cat" 31 | And it is part of group called "test" 32 | And I have a process called "tee" 33 | And it is part of group called "test" 34 | And I have Supervisor running 35 | When I wait for start 36 | And I get information about the processes before action 37 | And I "stop" the processes in the group 38 | And I get information about the processes 39 | Then I should see running first 40 | And I should see them as part of the group 41 | Then I should get a success response for all 42 | And I should see not running 43 | -------------------------------------------------------------------------------- /features/status_control/admin_checks_status.feature: -------------------------------------------------------------------------------- 1 | Feature: Admin checks status 2 | In order to display details 3 | As an Admin 4 | I should be able to get information about Supervisor 5 | 6 | Scenario: 7 | Given I have Supervisor running 8 | When I ask for the API version 9 | Then I should get at least "3.0" version 10 | 11 | Scenario: 12 | Given I have Supervisor running 13 | When I ask for Supervisor version 14 | Then I should get at least "3.0" version 15 | 16 | Scenario: 17 | Given my Supervisor instance is called "supervisor" 18 | And I have Supervisor running 19 | When I ask for Supervisor identification 20 | Then I should get "supervisor" as identifier 21 | 22 | Scenario: 23 | Given I have Supervisor running 24 | When I ask for the state 25 | Then I should get "1" as statecode and "RUNNING" as statename 26 | 27 | Scenario: 28 | Given I have Supervisor running 29 | When I ask for the PID 30 | Then I should get the real PID 31 | 32 | Scenario: 33 | Given I have Supervisor running 34 | When I ask for the log 35 | Then I should get an INFO about supervisord started 36 | -------------------------------------------------------------------------------- /features/status_control/admin_controls_supervisor_process.feature: -------------------------------------------------------------------------------- 1 | Feature: Admin controls Supervisor process 2 | In order to be able to manage Supervisor 3 | As an Admin 4 | I should be able to control the process itself 5 | 6 | Scenario: 7 | Given I have Supervisor running 8 | When I try to call "shutdown" action 9 | Then I should get a success response 10 | And it should be stopped 11 | 12 | Scenario: 13 | Given I have Supervisor running 14 | When I try to call "restart" action 15 | And I wait for start 16 | Then I should get a success response 17 | And it should be running again 18 | -------------------------------------------------------------------------------- /phpspec.yml: -------------------------------------------------------------------------------- 1 | suites: 2 | supervisor_suite: 3 | namespace: Supervisor 4 | psr4_prefix: Supervisor 5 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 5 3 | 4 | paths: 5 | - src 6 | -------------------------------------------------------------------------------- /resources/supervisord.conf: -------------------------------------------------------------------------------- 1 | [inet_http_server] 2 | port = *:9001 3 | username = user 4 | password = 123 5 | 6 | [supervisord] 7 | logfile = /tmp/supervisord.log 8 | pidfile = /tmp/supervisord.pid 9 | 10 | [rpcinterface:supervisor] 11 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 12 | -------------------------------------------------------------------------------- /src/Exception/Fault/AbnormalTerminationException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class AbnormalTerminationException extends SupervisorException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/Fault/AlreadyAddedException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class AlreadyAddedException extends SupervisorException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/Fault/AlreadyStartedException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class AlreadyStartedException extends SupervisorException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/Fault/BadArgumentsException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class BadArgumentsException extends SupervisorException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/Fault/BadNameException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class BadNameException extends SupervisorException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/Fault/BadSignalException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class BadSignalException extends SupervisorException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/Fault/CantRereadException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class CantRereadException extends SupervisorException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/Fault/FailedException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class FailedException extends SupervisorException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/Fault/IncorrectParametersException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class IncorrectParametersException extends SupervisorException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/Fault/NoFileException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class NoFileException extends SupervisorException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/Fault/NotExecutableException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class NotExecutableException extends SupervisorException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/Fault/NotRunningException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class NotRunningException extends SupervisorException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/Fault/ShutdownStateException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class ShutdownStateException extends SupervisorException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/Fault/SignatureUnsupportedException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class SignatureUnsupportedException extends SupervisorException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/Fault/SpawnErrorException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class SpawnErrorException extends SupervisorException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/Fault/StillRunningException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class StillRunningException extends SupervisorException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/Fault/SuccessException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class SuccessException extends SupervisorException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/Fault/UnknownMethodException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class UnknownMethodException extends SupervisorException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/FaultCodes.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Buster Neece 10 | */ 11 | enum FaultCodes: int 12 | { 13 | case UnknownMethod = 1; 14 | case IncorrectParameters = 2; 15 | case BadArguments = 3; 16 | case SignatureUnsupported = 4; 17 | case ShutdownState = 6; 18 | case BadName = 10; 19 | case BadSignal = 11; 20 | case NoFile = 20; 21 | case NotExecutable = 21; 22 | case Failed = 30; 23 | case AbnormalTermination = 40; 24 | case SpawnError = 50; 25 | case AlreadyStarted = 60; 26 | case NotRunning = 70; 27 | case Success = 80; 28 | case AlreadyAdded = 90; 29 | case StillRunning = 91; 30 | case CantReread = 92; 31 | 32 | public function getExceptionClass(): string 33 | { 34 | return match($this) { 35 | self::UnknownMethod => Fault\UnknownMethodException::class, 36 | self::IncorrectParameters => Fault\IncorrectParametersException::class, 37 | self::BadArguments => Fault\BadArgumentsException::class, 38 | self::SignatureUnsupported => Fault\SignatureUnsupportedException::class, 39 | self::ShutdownState => Fault\ShutdownStateException::class, 40 | self::BadName => Fault\BadNameException::class, 41 | self::BadSignal => Fault\BadSignalException::class, 42 | self::NoFile => Fault\NoFileException::class, 43 | self::NotExecutable => Fault\NotExecutableException::class, 44 | self::Failed => Fault\FailedException::class, 45 | self::AbnormalTermination => Fault\AbnormalTerminationException::class, 46 | self::SpawnError => Fault\SpawnErrorException::class, 47 | self::AlreadyStarted => Fault\AlreadyStartedException::class, 48 | self::NotRunning => Fault\NotRunningException::class, 49 | self::Success => Fault\SuccessException::class, 50 | self::AlreadyAdded => Fault\AlreadyAddedException::class, 51 | self::StillRunning => Fault\StillRunningException::class, 52 | self::CantReread => Fault\CantRereadException::class, 53 | }; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Exception/SupervisorException.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class SupervisorException extends \RuntimeException 16 | { 17 | /** 18 | * Creates a new Fault exception if a named one from the table above is present. 19 | */ 20 | public static function create(FaultException $faultException): FaultException|SupervisorException 21 | { 22 | $faultCode = $faultException->getFaultCode(); 23 | $faultString = $faultException->getFaultString(); 24 | 25 | $faultEnum = FaultCodes::tryFrom($faultCode); 26 | 27 | if (null === $faultEnum) { 28 | return $faultException; 29 | } 30 | 31 | $faultClass = $faultEnum->getExceptionClass(); 32 | return new $faultClass($faultString, $faultCode); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Process.php: -------------------------------------------------------------------------------- 1 | 11 | * @author Buster Neece 12 | */ 13 | final class Process implements ProcessInterface 14 | { 15 | public function __construct( 16 | private readonly array $payload = [] 17 | ) { 18 | } 19 | 20 | /** 21 | * @inheritDoc 22 | */ 23 | public function getPayload(): array 24 | { 25 | return $this->payload; 26 | } 27 | 28 | /** 29 | * @inheritDoc 30 | */ 31 | public function getName(): string 32 | { 33 | return $this->payload['name']; 34 | } 35 | 36 | /** 37 | * @inheritDoc 38 | */ 39 | public function getState(): ProcessStates 40 | { 41 | return ProcessStates::from($this->payload['state']); 42 | } 43 | 44 | /** 45 | * @inheritDoc 46 | */ 47 | public function isRunning(): bool 48 | { 49 | return $this->checkState(ProcessStates::Running); 50 | } 51 | 52 | /** 53 | * @inheritDoc 54 | */ 55 | public function checkState(int|ProcessStates $state): bool 56 | { 57 | if (is_int($state)) { 58 | $state = ProcessStates::tryFrom($state); 59 | } 60 | 61 | return $this->getState() === $state; 62 | } 63 | 64 | /** 65 | * @inheritDoc 66 | */ 67 | public function __toString(): string 68 | { 69 | return $this->getName(); 70 | } 71 | 72 | /** 73 | * @inheritDoc 74 | */ 75 | public function offsetGet($offset): mixed 76 | { 77 | return $this->payload[$offset] ?? null; 78 | } 79 | 80 | /** 81 | * @inheritDoc 82 | */ 83 | public function offsetExists($offset): bool 84 | { 85 | return isset($this->payload[$offset]); 86 | } 87 | 88 | /** 89 | * @inheritDoc 90 | */ 91 | public function offsetSet($offset, $value): void 92 | { 93 | throw new \LogicException('Process object cannot be altered'); 94 | } 95 | 96 | /** 97 | * @inheritDoc 98 | */ 99 | public function offsetUnset($offset): void 100 | { 101 | throw new \LogicException('Process object cannot be altered'); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/ProcessInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Buster Neece 10 | */ 11 | interface ProcessInterface extends \ArrayAccess 12 | { 13 | /** 14 | * Returns the process info array. 15 | */ 16 | public function getPayload(): array; 17 | 18 | /** 19 | * Returns the process name. 20 | */ 21 | public function getName(): string; 22 | 23 | /** 24 | * Checks whether the process is running. 25 | */ 26 | public function isRunning(): bool; 27 | 28 | /** 29 | * Checks whether the process is running. 30 | */ 31 | public function getState(): ProcessStates; 32 | 33 | /** 34 | * Checks if process is in the given state. 35 | */ 36 | public function checkState(int|ProcessStates $state): bool; 37 | 38 | /** 39 | * Returns process name. 40 | */ 41 | public function __toString(): string; 42 | } 43 | -------------------------------------------------------------------------------- /src/ProcessStates.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Buster Neece 10 | */ 11 | enum ProcessStates: int 12 | { 13 | case Stopped = 0; 14 | case Starting = 10; 15 | case Running = 20; 16 | case Backoff = 30; 17 | case Stopping = 40; 18 | case Exited = 100; 19 | case Fatal = 200; 20 | case Unknown = 1000; 21 | } 22 | -------------------------------------------------------------------------------- /src/ReloadResult.php: -------------------------------------------------------------------------------- 1 | added, $this->modified, $this->removed); 24 | } 25 | 26 | /** 27 | * @inheritDoc 28 | */ 29 | public function getAdded(): array 30 | { 31 | return $this->added; 32 | } 33 | 34 | /** 35 | * @inheritDoc 36 | */ 37 | public function getModified(): array 38 | { 39 | return $this->modified; 40 | } 41 | 42 | /** 43 | * @inheritDoc 44 | */ 45 | public function getRemoved(): array 46 | { 47 | return $this->removed; 48 | } 49 | 50 | public static function fromReloadConfig(array $reloadResult): self 51 | { 52 | [$added, $modified, $removed] = $reloadResult[0] ?? [null, null, null]; 53 | 54 | return new self( 55 | $added ?? [], 56 | $modified ?? [], 57 | $removed ?? [] 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/ReloadResultInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Buster Neece 10 | */ 11 | enum ServiceStates: int 12 | { 13 | case Shutdown = -1; 14 | case Restarting = 0; 15 | case Running = 1; 16 | case Fatal = 2; 17 | } 18 | -------------------------------------------------------------------------------- /src/Supervisor.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Buster Neece 17 | * 18 | * @method string getAPIVersion() 19 | * @method string getSupervisorVersion() 20 | * @method string getIdentification() 21 | * @method array getState() 22 | * @method int getPID() 23 | * @method string readLog(integer $offset, integer $limit) 24 | * @method bool clearLog() 25 | * @method bool shutdown() 26 | * @method bool restart() 27 | * @method array getProcessInfo(string $processName) 28 | * @method array getAllProcessInfo() 29 | * @method bool startProcess(string $name, boolean $wait = true) 30 | * @method array startAllProcesses(boolean $wait = true) 31 | * @method array startProcessGroup(string $name, boolean $wait = true) 32 | * @method bool stopProcess(string $name, boolean $wait = true) 33 | * @method array stopAllProcesses(boolean $wait = true) 34 | * @method array stopProcessGroup(string $name, boolean $wait = true) 35 | * @method bool sendProcessStdin(string $name, string $chars) 36 | * @method bool addProcessGroup(string $name) 37 | * @method bool removeProcessGroup(string $name) 38 | * @method string readProcessStdoutLog(string $name, integer $offset, integer $limit) 39 | * @method string readProcessStderrLog(string $name, integer $offset, integer $limit) 40 | * @method array tailProcessStdoutLog(string $name, integer $offset, integer $limit) 41 | * @method array tailProcessStderrLog(string $name, integer $offset, integer $limit) 42 | * @method bool clearProcessLogs(string $name) 43 | * @method array clearAllProcessLogs() 44 | * @method array reloadConfig() 45 | * @method bool signalProcess(string $name, string $signal) 46 | * @method array signalProcessGroup(string $name, string $signal) 47 | * @method array signalAllProcesses(string $signal) 48 | */ 49 | final class Supervisor implements SupervisorInterface 50 | { 51 | private readonly LoggerInterface $logger; 52 | 53 | public function __construct( 54 | private readonly ClientInterface $client, 55 | ?LoggerInterface $logger = null 56 | ) { 57 | $this->logger = $logger ?? new NullLogger(); 58 | } 59 | 60 | /** 61 | * @inheritDoc 62 | */ 63 | public function call(string $namespace, string $method, array $arguments = []): mixed 64 | { 65 | try { 66 | $this->logger->debug( 67 | sprintf('Supervisor call to "%s"', $namespace . '.' . $method), 68 | $arguments 69 | ); 70 | 71 | return $this->client->call($namespace . '.' . $method, $arguments); 72 | } catch (FaultException $faultException) { 73 | $this->logger->error( 74 | sprintf('Supervisor fault: ' . $faultException->getMessage()), 75 | [ 76 | 'faultString' => $faultException->getFaultString(), 77 | 'faultCode' => $faultException->getFaultCode(), 78 | ] 79 | ); 80 | 81 | throw SupervisorException::create($faultException); 82 | } 83 | } 84 | 85 | /** 86 | * @inheritDoc 87 | */ 88 | public function __call(string $method, array $arguments) 89 | { 90 | return $this->call('supervisor', $method, $arguments); 91 | } 92 | 93 | /** 94 | * @inheritDoc 95 | */ 96 | public function isConnected(): bool 97 | { 98 | try { 99 | $this->call('system', 'listMethods'); 100 | } catch (\Exception $e) { 101 | return false; 102 | } 103 | 104 | return true; 105 | } 106 | 107 | /** 108 | * @inheritDoc 109 | */ 110 | public function isRunning(): bool 111 | { 112 | return $this->checkState(ServiceStates::Running); 113 | } 114 | 115 | /** 116 | * @inheritDoc 117 | */ 118 | public function getServiceState(): ServiceStates 119 | { 120 | return ServiceStates::from($this->getState()['statecode']); 121 | } 122 | 123 | /** 124 | * @inheritDoc 125 | */ 126 | public function checkState(int|ServiceStates $checkState): bool 127 | { 128 | if (is_int($checkState)) { 129 | $checkState = ServiceStates::tryFrom($checkState); 130 | } 131 | 132 | return $this->getServiceState() === $checkState; 133 | } 134 | 135 | /** 136 | * @inheritDoc 137 | */ 138 | public function getAllProcesses(): array 139 | { 140 | $processes = $this->getAllProcessInfo(); 141 | 142 | foreach ($processes as $key => $processInfo) { 143 | $processes[$key] = new Process($processInfo); 144 | } 145 | 146 | return $processes; 147 | } 148 | 149 | /** 150 | * @inheritDoc 151 | */ 152 | public function getProcess(string $name): ProcessInterface 153 | { 154 | $process = $this->getProcessInfo($name); 155 | 156 | return new Process($process); 157 | } 158 | 159 | /** 160 | * @inheritDoc 161 | */ 162 | public function reloadAndApplyConfig( 163 | bool $wait = true, 164 | bool $stopModifiedGroups = true, 165 | bool $startNewProcesses = true 166 | ): ReloadResultInterface { 167 | $reloadResult = ReloadResult::fromReloadConfig( 168 | $this->reloadConfig() 169 | ); 170 | 171 | $reloadRemoved = $reloadResult->getRemoved(); 172 | if (!empty($reloadRemoved)) { 173 | $this->logger->debug('Removing supervisor groups.', $reloadRemoved); 174 | 175 | foreach ($reloadRemoved as $group) { 176 | $this->stopProcessGroup($group, $wait); 177 | $this->removeProcessGroup($group); 178 | } 179 | } 180 | 181 | $reloadModified = $reloadResult->getModified(); 182 | if (!empty($reloadModified)) { 183 | $this->logger->debug('Reloading modified supervisor groups.', $reloadModified); 184 | 185 | foreach ($reloadModified as $group) { 186 | if ($stopModifiedGroups) { 187 | $this->stopProcessGroup($group, $wait); 188 | $this->removeProcessGroup($group); 189 | } 190 | 191 | $this->addProcessGroup($group); 192 | 193 | if ($startNewProcesses) { 194 | $this->startProcessGroup($group); 195 | } 196 | } 197 | } 198 | 199 | $reloadAdded = $reloadResult->getAdded(); 200 | if (!empty($reloadAdded)) { 201 | $this->logger->debug('Adding new supervisor groups.', $reloadAdded); 202 | 203 | foreach ($reloadAdded as $group) { 204 | $this->addProcessGroup($group); 205 | 206 | if ($startNewProcesses) { 207 | $this->startProcessGroup($group); 208 | } 209 | } 210 | } 211 | 212 | return $reloadResult; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/SupervisorInterface.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Buster Neece 15 | * 16 | * @method string getAPIVersion() 17 | * @method string getSupervisorVersion() 18 | * @method string getIdentification() 19 | * @method array getState() 20 | * @method int getPID() 21 | * @method string readLog(integer $offset, integer $limit) 22 | * @method bool clearLog() 23 | * @method bool shutdown() 24 | * @method bool restart() 25 | * @method array getProcessInfo(string $processName) 26 | * @method array getAllProcessInfo() 27 | * @method bool startProcess(string $name, boolean $wait = true) 28 | * @method array startAllProcesses(boolean $wait = true) 29 | * @method array startProcessGroup(string $name, boolean $wait = true) 30 | * @method bool stopProcess(string $name, boolean $wait = true) 31 | * @method array stopAllProcesses(boolean $wait = true) 32 | * @method array stopProcessGroup(string $name, boolean $wait = true) 33 | * @method bool sendProcessStdin(string $name, string $chars) 34 | * @method bool addProcessGroup(string $name) 35 | * @method bool removeProcessGroup(string $name) 36 | * @method string readProcessStdoutLog(string $name, integer $offset, integer $limit) 37 | * @method string readProcessStderrLog(string $name, integer $offset, integer $limit) 38 | * @method array tailProcessStdoutLog(string $name, integer $offset, integer $limit) 39 | * @method array tailProcessStderrLog(string $name, integer $offset, integer $limit) 40 | * @method bool clearProcessLogs(string $name) 41 | * @method array clearAllProcessLogs() 42 | * @method array reloadConfig() 43 | * @method bool signalProcess(string $name, string $signal) 44 | * @method array signalProcessGroup(string $name, string $signal) 45 | * @method array signalAllProcesses(string $signal) 46 | */ 47 | interface SupervisorInterface 48 | { 49 | /** 50 | * Calls a method. 51 | * 52 | * @param string $namespace 53 | * @param string $method 54 | * @param array $arguments 55 | * 56 | * @return mixed 57 | */ 58 | public function call(string $namespace, string $method, array $arguments = []): mixed; 59 | 60 | /** 61 | * Magic __call method. 62 | * 63 | * Handles all calls to supervisor namespace 64 | * 65 | * @param string $method 66 | * @param array $arguments 67 | * 68 | * @return mixed 69 | */ 70 | public function __call(string $method, array $arguments); 71 | 72 | /** 73 | * Checks if a connection is present. 74 | * 75 | * It is done by sending a bump request to the server and catching any thrown exceptions 76 | */ 77 | public function isConnected(): bool; 78 | 79 | /** 80 | * Is service running? 81 | */ 82 | public function isRunning(): bool; 83 | 84 | /** 85 | * Get the supervisord service state. 86 | */ 87 | public function getServiceState(): ServiceStates; 88 | 89 | /** 90 | * Checks if supervisord is in given state. 91 | */ 92 | public function checkState(int|ServiceStates $checkState): bool; 93 | 94 | /** 95 | * Returns all processes as Process objects. 96 | * 97 | * @return ProcessInterface[] 98 | */ 99 | public function getAllProcesses(): array; 100 | 101 | /** 102 | * Returns a specific Process. 103 | * 104 | * @param string $name Process name or 'group:name' 105 | * 106 | * @return ProcessInterface 107 | */ 108 | public function getProcess(string $name): ProcessInterface; 109 | 110 | /** 111 | * Reload configuration and apply process changes immediately, i.e.: 112 | * - Start any processes newly added in the configuration, 113 | * - Stop and restart any processes modified in the configuration, and 114 | * - Stop any processes 115 | * 116 | * @param bool $wait Wait for processes to stop before continuing execution. 117 | * @param bool $stopModifiedGroups Fully stop all modified groups. 118 | * @param bool $startNewProcesses Start all processes in the process group. 119 | * 120 | * @throws SupervisorException 121 | * 122 | * @return ReloadResultInterface 123 | */ 124 | public function reloadAndApplyConfig( 125 | bool $wait = true, 126 | bool $stopModifiedGroups = true, 127 | bool $startNewProcesses = true 128 | ): ReloadResultInterface; 129 | } 130 | --------------------------------------------------------------------------------