allow to set daemon config via shell param
[tine20] / tine20 / library / Console / Daemon.php
1 <?php
2 /**
3  * Console_Daemon
4  * 
5  * @package     Console
6  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
7  * @copyright   Copyright (c) 2010-2017 Metaways Infosystems GmbH (http://www.metaways.de)
8  * @author      Lars Kneschke <l.kneschke@metaways.de>
9  */
10
11
12 /**
13  * Base class for console daemons
14  * 
15  * @package     Console
16  */
17 abstract class Console_Daemon
18 {
19     /**
20      * 
21      * @var array
22      */
23     protected $_children = array();
24     
25     /**
26      * @var Zend_Log
27      */
28     protected $_logger;
29     
30     /**
31      * @var Zend_Config
32      */
33     protected $_config;
34
35     protected $_isChild = false;
36     
37     /**
38      * @var array $_defaultConfig
39      */
40     protected static $_defaultConfig = array(
41         'general' => array(
42             'configfile' => null, 
43             'pidfile'    => null,
44             'daemonize'  => 0,
45             'logfile'    => null, //STDOUT
46             'loglevel'   => 3
47         )
48     );
49     
50     /**
51      * @var array
52      */
53     protected $_options = array(
54         'help|h'        => 'Display this help Message',
55         'config=s'      => 'path to configuration file',
56         'daemonize|d'   => 'become a daemon (fork to background)',
57         'pidfile|p=s'   => 'deamon pid file path',
58     );
59     
60     /**
61      * constructor
62      * 
63      * @param Zend_Config $config
64      */
65     public function __construct($config = NULL)
66     {
67         pcntl_signal(SIGCHLD, array($this, "handleSigCHLD"));
68         pcntl_signal(SIGHUP,  array($this, "handleSigHUP"));
69         pcntl_signal(SIGINT,  array($this, "handleSigINT"));
70         pcntl_signal(SIGTERM, array($this, "handleSigTERM"));
71
72         /** @noinspection PhpUndefinedFieldInspection */
73         if ($this->_getConfig()->general->daemonize == 1) {
74             $this->_becomeDaemon();
75         }
76         
77         $this->_getLogger()->info(__METHOD__ . '::' . __LINE__ .    " Started with pid: " . posix_getpid());
78
79         /** @noinspection PhpUndefinedFieldInspection */
80         if (isset($this->_getConfig()->general) && isset($this->_getConfig()->general->user) && isset($this->_getConfig()->general->group)) {
81             /** @noinspection PhpUndefinedFieldInspection */
82             $this->_changeIdentity($this->_getConfig()->general->user, $this->_getConfig()->general->group);
83         }
84     }
85     
86     /**
87      * get Zend_Config object
88      * 
89      * @return Zend_Config
90      */
91     public function getConfig()
92     {
93         return $this->_getConfig();
94     }
95     
96     /**
97      * get default config 
98      * 
99      * @return array
100      */
101     public static function getDefaultConfig()
102     {
103         return array_merge(self::$_defaultConfig, static::$_defaultConfig);
104     }
105     
106     /**
107      * get configured pidfile
108      */
109     public function getPidFile()
110     {
111         /** @noinspection PhpUndefinedFieldInspection */
112         return $this->_config->general->pidfile;
113     }
114     
115     abstract public function run();
116     
117     /**
118      * 
119      * @param  string  $_username
120      * @param  string  $_groupname
121      * @throws RuntimeException
122      */
123     protected function _changeIdentity($_username, $_groupname)
124     {
125         if(($userInfo = posix_getpwnam($_username)) === false) {
126             throw new RuntimeException("user $_username not found");
127         }
128         
129         if(($groupInfo = posix_getgrnam($_groupname)) === false) {
130             throw new RuntimeException("group $_groupname not found");
131         }
132
133         if(posix_setgid($groupInfo['gid']) !== true) { 
134             throw new RuntimeException("failed to change group to $_groupname");
135         }
136         
137         if(posix_setuid($userInfo['uid']) !== true) { 
138             throw new RuntimeException("failed to change user to $_username");
139         }
140     }
141
142     /**
143      * handle terminated children
144      *
145      * @param string $pid
146      * @param string $status
147      */
148     protected function _childTerminated($pid, /** @noinspection PhpUnusedParameterInspection */$status)
149     {
150         unset($this->_children[$pid]);
151     }
152     
153     /**
154      * @return Zend_Config
155      */
156     protected function _getConfig()
157     {
158         if (! $this->_config instanceof Zend_Config) {
159             $this->_config = new Zend_Config(self::getDefaultConfig(), TRUE);
160             
161             $this->_parseOptions($this->_config);
162             
163             $this->_loadConfigFile($this->_config);
164         }
165         
166         return $this->_config;
167     }
168     
169     /**
170      * return Zend_Log
171      */
172     protected function _getLogger()
173     {
174         if (! $this->_logger instanceof Zend_Log) {
175             $config = $this->_getConfig();
176             
177             $this->_logger = new Zend_Log();
178             /** @noinspection PhpUndefinedFieldInspection */
179             $this->_logger->addWriter(new Zend_Log_Writer_Stream($config->general->logfile ? $config->general->logfile : STDOUT));
180             /** @noinspection PhpUndefinedFieldInspection */
181             $this->_logger->addFilter(new Zend_Log_Filter_Priority((int) $config->general->loglevel));
182         }
183         
184         return $this->_logger;
185     }
186     
187     /**
188      * @param  Zend_Config  $config
189      * @return void
190      */
191     protected function _loadConfigFile(Zend_Config $config)
192     {
193         /** @noinspection PhpUndefinedFieldInspection */
194         if (file_exists($config->general->configfile)) {
195             try {
196                 /** @noinspection PhpUndefinedFieldInspection */
197                 $configIniFile = new Zend_Config_Ini($config->general->configfile);
198             } catch (Zend_Config_Exception $e) {
199                 /** @noinspection PhpUndefinedFieldInspection */
200                 fwrite(STDERR, "Error while parsing config file({$config->general->configfile}) " .  $e->getMessage() . PHP_EOL);
201                 exit(1);
202             }
203             
204             $config->merge($configIniFile);
205         }
206     }
207     
208     /**
209      * 
210      * @return number
211      */
212     protected function _forkChild()
213     {
214         $this->_beforeFork();
215         $childPid = pcntl_fork();
216         
217         if($childPid < 0) {
218             fwrite(STDERR, "Something went wrong while forking new child" . PHP_EOL);
219             exit(1);
220         }
221         
222         // fork was successful
223         $this->_afterFork($childPid);
224         
225         // add childPid to internal scoreboard
226         if($childPid > 0) {
227             $this->_children[$childPid] = $childPid;
228         } else {
229             // a child has no children
230             $this->_children = array();
231             $this->_isChild = true;
232         }
233         
234         return $childPid;
235     }
236     
237     /**
238      * template function intended to do cleanups before forking (e.g. disconnect database)
239      */
240     protected function _beforeFork()
241     {
242         
243     }
244     
245     /**
246      * template function intended to do init after forking (e.g. reconnect database)
247      * 
248      * @param $childPid
249      */
250     protected function _afterFork($childPid)
251     {
252         
253     }
254     
255     /**
256      * function fork into background (become a daemon)
257      * @return void|number
258      */
259     protected function _becomeDaemon()
260     {
261         $pidFile = $this->getPidFile();
262         
263         if ( $pidFile !== null && ! is_writable(dirname($pidFile))) {
264             fwrite(STDERR, "cannot write pidfile '{$pidFile}'" . PHP_EOL);
265             exit(1);
266         }
267         
268         $childPid = pcntl_fork();
269         
270         if ($childPid < 0) {
271             fwrite(STDERR, "Something went wrong while forking to background" . PHP_EOL);
272             exit;
273         }
274         
275         // fork was successfull
276         // we can finish the main process
277         if ($childPid > 0) {
278             #echo "We are master. Exiting main process now..." . PHP_EOL;
279             if ($pidFile !== null) {
280                 file_put_contents($pidFile, $childPid);
281             }
282             exit;
283         }
284         
285         // this is the code processed by the forked child
286         
287         //  become session leader
288         posix_setsid();
289         
290         # chdir('/');
291         # umask(0);
292         
293         return posix_getpid();
294     }
295     
296     /**
297      * parse commandline options
298      *
299      * @param Zend_Config $config
300      * @return Zend_Console_Getopt
301      */
302     protected function _parseOptions(Zend_Config $config)
303     {
304         try {
305             $opts = new Zend_Console_Getopt($this->_options);
306             $opts->parse();
307         } catch (Zend_Console_Getopt_Exception $e) {
308            fwrite(STDOUT, $e->getUsageMessage());
309            exit(1);
310         }
311
312         /** @noinspection PhpUndefinedFieldInspection */
313         if ($opts->h) {
314             fwrite(STDOUT, $opts->getUsageMessage());
315             
316             exit(0);
317         }
318
319         // pid file path
320         if (isset($opts->p)) {
321             /** @noinspection PhpUndefinedFieldInspection */
322             $config->general->pidfile = $opts->p;
323         }
324
325         // config file path
326         if (isset($opts->config)) {
327             /** @noinspection PhpUndefinedFieldInspection */
328             $config->general->configfile = $opts->config;
329         }
330
331         // become daemon
332         if (isset($opts->d)) {
333             /** @noinspection PhpUndefinedFieldInspection */
334             $config->general->daemonize = 1;
335         }
336         
337         return $opts;
338     }
339     
340     /**
341      * handle signal SIGTERM
342      */
343     public function handleSigTERM()
344     {
345         $this->_getLogger()->debug(__METHOD__ . '::' . __LINE__ .    " SIGTERM received, is child: " . var_export($this->_isChild, true) . " " . microtime(true));
346
347         // do we want to gracefully shut down?
348         if (true === $this->_gracefulShutDown()) {
349             return;
350         }
351
352         $this->_shutDown();
353     }
354
355     protected function _shutDown()
356     {
357         foreach($this->_children as $pid) {
358             $this->_getLogger()->debug(__METHOD__ . '::' . __LINE__ .    " send SIGTERM to child " . $pid);
359             posix_kill($pid, SIGTERM);
360         }
361
362         $pidFile = $this->getPidFile();
363
364         if ($pidFile) {
365             @unlink($pidFile);
366         }
367
368         exit(0);
369     }
370
371     protected function _gracefulShutDown()
372     {
373         return false;
374     }
375
376     /**
377      * handle signal SIGCHILD
378      */
379     public function handleSigCHLD()
380     {
381         while (($pid = pcntl_waitpid(0, $status, WNOHANG)) > 0) {
382             $this->_childTerminated($pid, $status);
383         }
384     }
385     
386     /**
387      * handle signal SIGHUP
388      */
389     public function handleSigHUP()
390     {
391         $this->_getLogger()->debug(__METHOD__ . '::' . __LINE__ .    " SIGHUP received, but we don't do anything in this case");
392     }
393     
394     /**
395      * handle signal SIGINT
396      */
397     public function handleSigINT()
398     {
399         $this->handleSigTERM();
400     }
401 }