├── .gitignore ├── DaemonController.php ├── README.md ├── composer.json └── controllers └── WatcherDaemonController.php /.gitignore: -------------------------------------------------------------------------------- 1 | # phpstorm project files 2 | .idea 3 | 4 | # netbeans project files 5 | nbproject 6 | 7 | # zend studio for eclipse project files 8 | .buildpath 9 | .project 10 | .settings 11 | 12 | # windows thumbnail cache 13 | Thumbs.db 14 | 15 | # composer vendor dir 16 | /vendor 17 | 18 | # composer itself is not needed 19 | composer.phar 20 | composer.lock 21 | 22 | # Mac DS_Store Files 23 | .DS_Store 24 | 25 | # phpunit itself is not needed 26 | phpunit.phar 27 | # local phpunit config 28 | /phpunit.xml 29 | -------------------------------------------------------------------------------- /DaemonController.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | abstract class DaemonController extends Controller 15 | { 16 | 17 | const EVENT_BEFORE_JOB = "EVENT_BEFORE_JOB"; 18 | const EVENT_AFTER_JOB = "EVENT_AFTER_JOB"; 19 | 20 | const EVENT_BEFORE_ITERATION = "event_before_iteration"; 21 | const EVENT_AFTER_ITERATION = "event_after_iteration"; 22 | 23 | /** 24 | * @var $demonize boolean Run controller as Daemon 25 | * @default false 26 | */ 27 | public $demonize = false; 28 | 29 | /** 30 | * @var $isMultiInstance boolean allow daemon create a few instances 31 | * @see $maxChildProcesses 32 | * @default false 33 | */ 34 | public $isMultiInstance = false; 35 | 36 | /** 37 | * @var $parentPID int main procces pid 38 | */ 39 | protected $parentPID; 40 | 41 | /** 42 | * @var $maxChildProcesses int max daemon instances 43 | * @default 10 44 | */ 45 | public $maxChildProcesses = 10; 46 | 47 | /** 48 | * @var $currentJobs [] array of running instances 49 | */ 50 | protected static $currentJobs = []; 51 | 52 | /** 53 | * @var int Memory limit for daemon, must bee less than php memory_limit 54 | * @default 32M 55 | */ 56 | protected $memoryLimit = 268435456; 57 | 58 | /** 59 | * @var boolean used for soft daemon stop, set 1 to stop 60 | */ 61 | private static $stopFlag = false; 62 | 63 | /** 64 | * @var int Delay between task list checking 65 | * @default 5sec 66 | */ 67 | protected $sleep = 5; 68 | 69 | protected $pidDir = "@runtime/daemons/pids"; 70 | 71 | protected $logDir = "@runtime/daemons/logs"; 72 | 73 | private $stdIn; 74 | private $stdOut; 75 | private $stdErr; 76 | 77 | /** 78 | * Init function 79 | */ 80 | public function init() 81 | { 82 | parent::init(); 83 | 84 | //set PCNTL signal handlers 85 | pcntl_signal(SIGTERM, ['vyants\daemon\DaemonController', 'signalHandler']); 86 | pcntl_signal(SIGINT, ['vyants\daemon\DaemonController', 'signalHandler']); 87 | pcntl_signal(SIGHUP, ['vyants\daemon\DaemonController', 'signalHandler']); 88 | pcntl_signal(SIGUSR1, ['vyants\daemon\DaemonController', 'signalHandler']); 89 | pcntl_signal(SIGCHLD, ['vyants\daemon\DaemonController', 'signalHandler']); 90 | } 91 | 92 | function __destruct() 93 | { 94 | $this->deletePid(); 95 | } 96 | 97 | /** 98 | * Adjusting logger. You can override it. 99 | */ 100 | protected function initLogger() 101 | { 102 | $targets = \Yii::$app->getLog()->targets; 103 | foreach ($targets as $name => $target) { 104 | $target->enabled = false; 105 | } 106 | $config = [ 107 | 'levels' => ['error', 'warning', 'trace', 'info'], 108 | 'logFile' => \Yii::getAlias($this->logDir) . DIRECTORY_SEPARATOR . $this->getProcessName() . '.log', 109 | 'logVars' => [], 110 | 'except' => [ 111 | 'yii\db\*', // Don't include messages from db 112 | ], 113 | ]; 114 | $targets['daemon'] = new \yii\log\FileTarget($config); 115 | \Yii::$app->getLog()->targets = $targets; 116 | \Yii::$app->getLog()->init(); 117 | } 118 | 119 | /** 120 | * Daemon worker body 121 | * 122 | * @param $job 123 | * 124 | * @return boolean 125 | */ 126 | abstract protected function doJob($job); 127 | 128 | /** 129 | * Base action, you can\t override or create another actions 130 | * @return bool 131 | * @throws NotSupportedException 132 | */ 133 | final public function actionIndex() 134 | { 135 | if ($this->demonize) { 136 | $pid = pcntl_fork(); 137 | if ($pid == -1) { 138 | $this->halt(self::EXIT_CODE_ERROR, 'pcntl_fork() rise error'); 139 | } elseif ($pid) { 140 | $this->cleanLog(); 141 | $this->halt(self::EXIT_CODE_NORMAL); 142 | } else { 143 | posix_setsid(); 144 | $this->closeStdStreams(); 145 | } 146 | } 147 | $this->changeProcessName(); 148 | 149 | //run loop 150 | return $this->loop(); 151 | } 152 | 153 | /** 154 | * Set new process name 155 | */ 156 | protected function changeProcessName() 157 | { 158 | //rename process 159 | if (version_compare(PHP_VERSION, '5.5.0') >= 0) { 160 | cli_set_process_title($this->getProcessName()); 161 | } else { 162 | if (function_exists('setproctitle')) { 163 | setproctitle($this->getProcessName()); 164 | } else { 165 | \Yii::error('Can\'t find cli_set_process_title or setproctitle function'); 166 | } 167 | } 168 | } 169 | 170 | /** 171 | * Close std streams and open to /dev/null 172 | * need some class properties 173 | */ 174 | protected function closeStdStreams() 175 | { 176 | if (is_resource(STDIN)) { 177 | fclose(STDIN); 178 | $this->stdIn = fopen('/dev/null', 'r'); 179 | } 180 | if (is_resource(STDOUT)) { 181 | fclose(STDOUT); 182 | $this->stdOut = fopen('/dev/null', 'ab'); 183 | } 184 | if (is_resource(STDERR)) { 185 | fclose(STDERR); 186 | $this->stdErr = fopen('/dev/null', 'ab'); 187 | } 188 | } 189 | 190 | /** 191 | * Prevent non index action running 192 | * 193 | * @param \yii\base\Action $action 194 | * 195 | * @return bool 196 | * @throws NotSupportedException 197 | */ 198 | public function beforeAction($action) 199 | { 200 | if (parent::beforeAction($action)) { 201 | $this->initLogger(); 202 | if ($action->id != "index") { 203 | throw new NotSupportedException( 204 | "Only index action allowed in daemons. So, don't create and call another" 205 | ); 206 | } 207 | 208 | return true; 209 | } else { 210 | return false; 211 | } 212 | } 213 | 214 | /** 215 | * Возвращает доступные опции 216 | * 217 | * @param string $actionID 218 | * 219 | * @return array 220 | */ 221 | public function options($actionID) 222 | { 223 | return [ 224 | 'demonize', 225 | 'taskLimit', 226 | 'isMultiInstance', 227 | 'maxChildProcesses', 228 | ]; 229 | } 230 | 231 | /** 232 | * Extract current unprocessed jobs 233 | * You can extract jobs from DB (DataProvider will be great), queue managers (ZMQ, RabbiMQ etc), redis and so on 234 | * 235 | * @return array with jobs 236 | */ 237 | abstract protected function defineJobs(); 238 | 239 | /** 240 | * Fetch one task from array of tasks 241 | * 242 | * @param Array 243 | * 244 | * @return mixed one task 245 | */ 246 | protected function defineJobExtractor(&$jobs) 247 | { 248 | return array_shift($jobs); 249 | } 250 | 251 | /** 252 | * Main Loop 253 | * 254 | * * @return boolean 0|1 255 | */ 256 | final private function loop() 257 | { 258 | if (file_put_contents($this->getPidPath(), getmypid())) { 259 | $this->parentPID = getmypid(); 260 | \Yii::trace('Daemon ' . $this->getProcessName() . ' pid ' . getmypid() . ' started.'); 261 | while (!self::$stopFlag) { 262 | if (memory_get_usage() > $this->memoryLimit) { 263 | \Yii::trace('Daemon ' . $this->getProcessName() . ' pid ' . 264 | getmypid() . ' used ' . memory_get_usage() . ' bytes on ' . $this->memoryLimit . 265 | ' bytes allowed by memory limit'); 266 | break; 267 | } 268 | $this->trigger(self::EVENT_BEFORE_ITERATION); 269 | $this->renewConnections(); 270 | $jobs = $this->defineJobs(); 271 | if ($jobs && !empty($jobs)) { 272 | while (($job = $this->defineJobExtractor($jobs)) !== null) { 273 | //if no free workers, wait 274 | if ($this->isMultiInstance && (count(static::$currentJobs) >= $this->maxChildProcesses)) { 275 | \Yii::trace('Reached maximum number of child processes. Waiting...'); 276 | while (count(static::$currentJobs) >= $this->maxChildProcesses) { 277 | sleep(1); 278 | pcntl_signal_dispatch(); 279 | } 280 | \Yii::trace( 281 | 'Free workers found: ' . 282 | ($this->maxChildProcesses - count(static::$currentJobs)) . 283 | ' worker(s). Delegate tasks.' 284 | ); 285 | } 286 | pcntl_signal_dispatch(); 287 | $this->runDaemon($job); 288 | } 289 | } else { 290 | sleep($this->sleep); 291 | } 292 | pcntl_signal_dispatch(); 293 | $this->trigger(self::EVENT_AFTER_ITERATION); 294 | } 295 | 296 | \Yii::info('Daemon ' . $this->getProcessName() . ' pid ' . getmypid() . ' is stopped.'); 297 | 298 | return self::EXIT_CODE_NORMAL; 299 | } 300 | $this->halt(self::EXIT_CODE_ERROR, 'Can\'t create pid file ' . $this->getPidPath()); 301 | } 302 | 303 | /** 304 | * Delete pid file 305 | */ 306 | protected function deletePid() 307 | { 308 | $pid = $this->getPidPath(); 309 | if (file_exists($pid)) { 310 | if (file_get_contents($pid) == getmypid()) { 311 | unlink($this->getPidPath()); 312 | } 313 | } else { 314 | \Yii::error('Can\'t unlink pid file ' . $this->getPidPath()); 315 | } 316 | } 317 | 318 | /** 319 | * PCNTL signals handler 320 | * 321 | * @param int $signo 322 | * @param array $siginfo 323 | * @param null $status 324 | */ 325 | final static function signalHandler($signo, $siginfo = [], $status = null) 326 | { 327 | switch ($signo) { 328 | case SIGINT: 329 | case SIGTERM: 330 | //shutdown 331 | self::$stopFlag = true; 332 | break; 333 | case SIGHUP: 334 | //restart, not implemented 335 | break; 336 | case SIGUSR1: 337 | //user signal, not implemented 338 | break; 339 | case SIGCHLD: 340 | $pid = $siginfo['pid'] ?? null; 341 | if (!$pid) { 342 | $pid = pcntl_waitpid(-1, $status, WNOHANG); 343 | } 344 | while ($pid > 0) { 345 | if ($pid && isset(static::$currentJobs[$pid])) { 346 | unset(static::$currentJobs[$pid]); 347 | } 348 | $pid = pcntl_waitpid(-1, $status, WNOHANG); 349 | } 350 | break; 351 | } 352 | } 353 | 354 | /** 355 | * Tasks runner 356 | * 357 | * @param string $job 358 | * 359 | * @return boolean 360 | */ 361 | final public function runDaemon($job) 362 | { 363 | if ($this->isMultiInstance) { 364 | $this->flushLog(); 365 | $pid = pcntl_fork(); 366 | if ($pid == -1) { 367 | return false; 368 | } elseif ($pid !== 0) { 369 | static::$currentJobs[$pid] = true; 370 | 371 | return true; 372 | } else { 373 | $this->cleanLog(); 374 | $this->renewConnections(); 375 | //child process must die 376 | $this->trigger(self::EVENT_BEFORE_JOB); 377 | $status = $this->doJob($job); 378 | $this->trigger(self::EVENT_AFTER_JOB); 379 | if ($status) { 380 | $this->halt(self::EXIT_CODE_NORMAL); 381 | } else { 382 | $this->halt(self::EXIT_CODE_ERROR, 'Child process #' . $pid . ' return error.'); 383 | } 384 | } 385 | } else { 386 | $this->trigger(self::EVENT_BEFORE_JOB); 387 | $status = $this->doJob($job); 388 | $this->trigger(self::EVENT_AFTER_JOB); 389 | 390 | return $status; 391 | } 392 | } 393 | 394 | /** 395 | * Stop process and show or write message 396 | * 397 | * @param $code int -1|0|1 398 | * @param $message string 399 | */ 400 | protected function halt($code, $message = null) 401 | { 402 | if ($message !== null) { 403 | if ($code == self::EXIT_CODE_ERROR) { 404 | \Yii::error($message); 405 | if (!$this->demonize) { 406 | $message = Console::ansiFormat($message, [Console::FG_RED]); 407 | } 408 | } else { 409 | \Yii::trace($message); 410 | } 411 | if (!$this->demonize) { 412 | $this->writeConsole($message); 413 | } 414 | } 415 | if ($code !== -1) { 416 | \Yii::$app->end($code); 417 | } 418 | } 419 | 420 | /** 421 | * Renew connections 422 | * @throws \yii\base\InvalidConfigException 423 | * @throws \yii\db\Exception 424 | */ 425 | protected function renewConnections() 426 | { 427 | if (isset(\Yii::$app->db)) { 428 | \Yii::$app->db->close(); 429 | \Yii::$app->db->open(); 430 | } 431 | } 432 | 433 | /** 434 | * Show message in console 435 | * 436 | * @param $message 437 | */ 438 | private function writeConsole($message) 439 | { 440 | $out = Console::ansiFormat('[' . date('d.m.Y H:i:s') . '] ', [Console::BOLD]); 441 | $this->stdout($out . $message . "\n"); 442 | } 443 | 444 | /** 445 | * @param string $daemon 446 | * 447 | * @return string 448 | */ 449 | public function getPidPath($daemon = null) 450 | { 451 | $dir = \Yii::getAlias($this->pidDir); 452 | if (!file_exists($dir)) { 453 | mkdir($dir, 0744, true); 454 | } 455 | $daemon = $this->getProcessName($daemon); 456 | 457 | return $dir . DIRECTORY_SEPARATOR . $daemon; 458 | } 459 | 460 | /** 461 | * @return string 462 | */ 463 | public function getProcessName($route = null) 464 | { 465 | if (is_null($route)) { 466 | $route = \Yii::$app->requestedRoute; 467 | } 468 | 469 | return str_replace(['/index', '/'], ['', '.'], $route); 470 | } 471 | 472 | /** 473 | * If in daemon mode - no write to console 474 | * 475 | * @param string $string 476 | * 477 | * @return bool|int 478 | */ 479 | public function stdout($string) 480 | { 481 | if (!$this->demonize && is_resource(STDOUT)) { 482 | return parent::stdout($string); 483 | } else { 484 | return false; 485 | } 486 | } 487 | 488 | /** 489 | * If in daemon mode - no write to console 490 | * 491 | * @param string $string 492 | * 493 | * @return int 494 | */ 495 | public function stderr($string) 496 | { 497 | if (!$this->demonize && is_resource(\STDERR)) { 498 | return parent::stderr($string); 499 | } else { 500 | return false; 501 | } 502 | } 503 | 504 | /** 505 | * Empty log queue 506 | */ 507 | protected function cleanLog() 508 | { 509 | \Yii::$app->log->logger->messages = []; 510 | } 511 | 512 | /** 513 | * Empty log queue 514 | */ 515 | protected function flushLog($final = false) 516 | { 517 | \Yii::$app->log->logger->flush($final); 518 | } 519 | } 520 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Daemons system for Yii2 2 | ======================= 3 | Extension provides functionality for simple daemons creation and control 4 | 5 | Installation 6 | ------------ 7 | 8 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 9 | 10 | Either run 11 | 12 | ``` 13 | php composer.phar require --prefer-dist vyants/yii2-daemon "*" 14 | ``` 15 | 16 | or add 17 | 18 | ``` 19 | "vyants/yii2-daemon": "*" 20 | ``` 21 | 22 | to the require section of your `composer.json` file. 23 | 24 | ### Setting WatcherDaemon 25 | WatcherDaemon is the main daemon and provides from box. This daemon check another daemons and run, if it need. 26 | Do the following steps: 27 | 28 | 1. Create in you console controllers path file WatcherDaemonController.php with following content: 29 | ``` 30 | sleep); 42 | //TODO: modify list, or get it from config, it does not matter 43 | $daemons = [ 44 | ['className' => 'OneDaemonController', 'enabled' => true], 45 | ['className' => 'AnotherDaemonController', 'enabled' => false] 46 | ]; 47 | return $daemons; 48 | } 49 | } 50 | ``` 51 | 2. No one checks the Watcher. Watcher should run continuously. Add it to your crontab: 52 | ``` 53 | * * * * * /path/to/yii/project/yii watcher-daemon --demonize=1 54 | ``` 55 | Watcher can't start twice, only one instance can work in the one moment. 56 | 57 | Usage 58 | ----- 59 | ### Create new daemons 60 | 1. Create in you console controllers path file {NAME}DaemonController.php with following content: 61 | ``` 62 | getQueue(); 128 | while (count($channel->callbacks)) { 129 | try { 130 | $channel->wait(null, true, 5); 131 | } catch (\PhpAmqpLib\Exception\AMQPTimeoutException $timeout) { 132 | 133 | } catch (\PhpAmqpLib\Exception\AMQPRuntimeException $runtime) { 134 | \Yii::error($runtime->getMessage()); 135 | $this->channel = null; 136 | $this->connection = null; 137 | } 138 | } 139 | return false; 140 | } 141 | 142 | /** 143 | * @param AMQPMessage $job 144 | * @return bool 145 | * @throws NotSupportedException 146 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) 147 | */ 148 | protected function doJob($job) 149 | { 150 | $result = false; 151 | 152 | //do somethink here and set $result 153 | 154 | if ($result) { 155 | $this->ask($job); 156 | } else { 157 | $this->nask($job); 158 | } 159 | return $result; 160 | } 161 | 162 | 163 | /** 164 | * @return AMQPChannel 165 | * @throws InvalidParamException 166 | */ 167 | protected function getQueue() 168 | { 169 | if ($this->channel == null) { 170 | if ($this->connection == null) { 171 | if (isset(\Yii::$app->params['rabbit'])) { 172 | $rabbit = \Yii::$app->params['rabbit']; 173 | } else { 174 | throw new InvalidParamException('Bad config RabbitMQ'); 175 | } 176 | $this->connection = new AMQPStreamConnection($rabbit['host'], $rabbit['port'], $rabbit['user'], $rabbit['password']); 177 | } 178 | 179 | $this->channel = $this->connection->channel(); 180 | 181 | $this->channel->exchange_declare($this->exchange, $this->type, false, true, false); 182 | 183 | $args = []; 184 | 185 | if ($this->dlx) { 186 | $args['x-dead-letter-exchange'] = ['S', $this->exchange]; 187 | $args['x-dead-letter-routing-key'] = ['S',$this->dlx]; 188 | } 189 | if ($this->max_length) { 190 | $args['x-max-length'] = ['I', $this->max_length]; 191 | } 192 | if ($this->max_bytes) { 193 | $args['x-max-length-bytes'] = ['I', $this->max_bytes]; 194 | } 195 | if ($this->max_priority) { 196 | $args['x-max-priority'] = ['I', $this->max_priority]; 197 | } 198 | 199 | list($queue_name, ,) = $this->channel->queue_declare($this->queue_name, false, true, false, false, false, $args); 200 | 201 | foreach ($this->binding_keys as $binding_key) { 202 | $this->channel->queue_bind($queue_name, $this->exchange, $binding_key); 203 | } 204 | 205 | $this->channel->basic_consume($queue_name, '', false, false, false, false, [$this, 'doJob']); 206 | } 207 | 208 | return $this->channel; 209 | } 210 | 211 | 212 | /** 213 | * @param $job 214 | */ 215 | protected function ask($job) 216 | { 217 | $job->delivery_info['channel']->basic_ack($job->delivery_info['delivery_tag']); 218 | } 219 | 220 | /** 221 | * @param $job 222 | */ 223 | protected function nask($job) 224 | { 225 | $job->delivery_info['channel']->basic_nack($job->delivery_info['delivery_tag']); 226 | } 227 | } 228 | ``` 229 | ### Daemon settings (propeties) 230 | In your daemon you can override parent properties: 231 | * `$demonize` - if 0 daemon is not running as daemon, only as simple console application. It's needs for debug. 232 | * `$memoryLimit` - if daemon reach this limit - daemon stop work. It prevent memory leaks. After stopping WatcherDaemon run this daemon again. 233 | * `$sleep` - delay between checking for new task, daemon will not sleep if task list is full. 234 | * `$pidDir` - dir where daemons pids is located 235 | * `$logDir` - dir where daemons logs is located 236 | * `$isMultiInstance` - this option allow daemon create self copy for each task. That is, the daemon can simultaneously perform multiple tasks. This is useful when one task requires some time and server resources allows perform many such task. 237 | * `$maxChildProcesses` - only if `$isMultiInstance=true`. The maximum number of daemons instances. If the maximum number is reached - the system waits until at least one child process to terminate. 238 | 239 | If you want to change logging preferences, you may override the function initLogger. Example: 240 | 241 | ``` 242 | /** 243 | * Adjusting logger. You can override it. 244 | */ 245 | protected function initLogger() 246 | { 247 | 248 | $targets = \Yii::$app->getLog()->targets; 249 | foreach ($targets as $name => $target) { 250 | $target->enabled = false; 251 | } 252 | $config = [ 253 | 'levels' => ['error', 'warning', 'trace', 'info'], 254 | 'logFile' => \Yii::getAlias($this->logDir) . DIRECTORY_SEPARATOR . $this->shortClassName() . '.log', 255 | 'logVars'=>[], // Don't log all variables 256 | 'exportInterval'=>1, // Write each message to disk 257 | 'except' => [ 258 | 'yii\db\*', // Don't include messages from db 259 | ], 260 | ]; 261 | $targets['daemon'] = new \yii\log\FileTarget($config); 262 | \Yii::$app->getLog()->targets = $targets; 263 | \Yii::$app->getLog()->init(); 264 | // Flush each message 265 | \Yii::$app->getLog()->flushInterval = 1; 266 | } 267 | ``` 268 | 269 | 270 | ### Installing Proctitle on PHP < 5.5.0 271 | 272 | You will need proctitle extension on your server to be able to isntall yii2-daemon. For a debian 7 you can use these commands: 273 | 274 | ``` 275 | # pecl install channel://pecl.php.net/proctitle-0.1.2 276 | # echo "extension=proctitle.so" > /etc/php5/mods-available/proctitle.ini 277 | # php5enmod proctitle 278 | ``` 279 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vyants/yii2-daemon", 3 | "description": "Extension provides functionality for simple daemons creation and control", 4 | "type": "yii2-extension", 5 | "keywords": ["yii2","extension", "daemon"], 6 | "license": "BSD-3-Clause", 7 | "authors": [ 8 | { 9 | "name": "Vladimir Yants", 10 | "email": "vladimir.yants@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "yiisoft/yii2": "*", 15 | "ext-pcntl": "*", 16 | "ext-posix": "*" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "vyants\\daemon\\": "" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /controllers/WatcherDaemonController.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | abstract class WatcherDaemonController extends DaemonController 13 | { 14 | /** 15 | * @var string subfolder in console/controllers 16 | */ 17 | public $daemonFolder = 'daemons'; 18 | 19 | /** 20 | * @var boolean flag for first iteration 21 | */ 22 | protected $firstIteration = true; 23 | 24 | /** 25 | * Prevent double start 26 | */ 27 | public function init() 28 | { 29 | $pid_file = $this->getPidPath(); 30 | if (file_exists($pid_file) && ($pid = file_get_contents($pid_file)) && file_exists("/proc/$pid")) { 31 | $this->halt(self::EXIT_CODE_ERROR, 'Another Watcher is already running.'); 32 | } 33 | parent::init(); 34 | } 35 | 36 | /** 37 | * Job processing body 38 | * 39 | * @param $job array 40 | * 41 | * @return boolean 42 | */ 43 | protected function doJob($job) 44 | { 45 | $pid_file = $this->getPidPath($job['daemon']); 46 | 47 | \Yii::trace('Check daemon ' . $job['daemon']); 48 | if (file_exists($pid_file)) { 49 | $pid = file_get_contents($pid_file); 50 | if ($this->isProcessRunning($pid)) { 51 | if ($job['enabled']) { 52 | \Yii::trace('Daemon ' . $job['daemon'] . ' running and working fine'); 53 | 54 | return true; 55 | } else { 56 | \Yii::warning('Daemon ' . $job['daemon'] . ' running, but disabled in config. Send SIGTERM signal.'); 57 | if (isset($job['hardKill']) && $job['hardKill']) { 58 | posix_kill($pid, SIGKILL); 59 | } else { 60 | posix_kill($pid, SIGTERM); 61 | } 62 | 63 | return true; 64 | } 65 | } 66 | } 67 | \Yii::error('Daemon pid not found.'); 68 | if ($job['enabled']) { 69 | \Yii::trace('Try to run daemon ' . $job['daemon'] . '.'); 70 | $command_name = $job['daemon'] . DIRECTORY_SEPARATOR . 'index'; 71 | //flush log before fork 72 | $this->flushLog(true); 73 | //run daemon 74 | $pid = pcntl_fork(); 75 | if ($pid === -1) { 76 | $this->halt(self::EXIT_CODE_ERROR, 'pcntl_fork() returned error'); 77 | } elseif ($pid === 0) { 78 | $this->cleanLog(); 79 | \Yii::$app->requestedRoute = $command_name; 80 | \Yii::$app->runAction("$command_name", ['demonize' => 1]); 81 | $this->halt(0); 82 | } else { 83 | $this->initLogger(); 84 | \Yii::trace('Daemon ' . $job['daemon'] . ' is running with pid ' . $pid); 85 | } 86 | } 87 | \Yii::trace('Daemon ' . $job['daemon'] . ' is checked.'); 88 | 89 | return true; 90 | } 91 | 92 | /** 93 | * @return array 94 | */ 95 | protected function defineJobs() 96 | { 97 | if ($this->firstIteration) { 98 | $this->firstIteration = false; 99 | } else { 100 | sleep($this->sleep); 101 | } 102 | 103 | return $this->getDaemonsList(); 104 | } 105 | 106 | /** 107 | * Daemons for check. Better way - get it from database 108 | * [ 109 | * ['daemon' => 'one-daemon', 'enabled' => true] 110 | * ... 111 | * ['daemon' => 'another-daemon', 'enabled' => false] 112 | * ] 113 | * @return array 114 | */ 115 | abstract protected function getDaemonsList(); 116 | 117 | /** 118 | * @param $pid 119 | * 120 | * @return bool 121 | */ 122 | public function isProcessRunning($pid) 123 | { 124 | return file_exists("/proc/$pid"); 125 | } 126 | } 127 | --------------------------------------------------------------------------------