1 | <?php namespace Comodojo\Daemon; |
||
2 | |||
3 | use \Comodojo\Daemon\Utils\Checks; |
||
4 | use \Comodojo\Daemon\Socket\Server as SocketServer; |
||
5 | use \Comodojo\Daemon\Worker\Manager as WorkerManager; |
||
6 | use \Comodojo\Daemon\Worker\Worker; |
||
7 | use \Comodojo\Daemon\Locker\PidLock; |
||
8 | use \Comodojo\Daemon\Listeners\WorkerWatchdog; |
||
9 | use \Comodojo\Daemon\Utils\ProcessTools; |
||
10 | use \Comodojo\Daemon\Console\LogHandler; |
||
11 | use \Comodojo\Foundation\Utils\ClassProperties; |
||
12 | use \Comodojo\Foundation\Events\Manager as EventsManager; |
||
13 | use \Psr\Log\LoggerInterface; |
||
14 | use \League\CLImate\CLImate; |
||
15 | use \Comodojo\Exception\SocketException; |
||
16 | use \Exception; |
||
17 | |||
18 | /** |
||
19 | * @package Comodojo Daemon |
||
20 | * @author Marco Giovinazzi <[email protected]> |
||
21 | * @license MIT |
||
22 | * |
||
23 | * LICENSE: |
||
24 | * |
||
25 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||
26 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||
27 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||
28 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||
29 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||
30 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||
31 | * THE SOFTWARE. |
||
32 | */ |
||
33 | |||
34 | abstract class Daemon extends Process { |
||
35 | |||
36 | protected $pidlock; |
||
37 | |||
38 | protected $socket; |
||
39 | |||
40 | protected $workers; |
||
41 | |||
42 | protected $console; |
||
43 | |||
44 | protected $is_active; |
||
45 | |||
46 | protected $is_supervisor; |
||
47 | |||
48 | protected static $default_properties = [ |
||
49 | 'pidfile' => 'daemon.pid', |
||
50 | 'sockethandler' => 'unix://daemon.sock', |
||
51 | 'socketbuffer' => 1024, |
||
52 | 'sockettimeout' => 2, |
||
53 | 'socketmaxconnections' => 10, |
||
54 | 'niceness' => 0, |
||
55 | 'arguments' => '\\Comodojo\\Daemon\\Console\\DaemonArguments', |
||
56 | 'description' => 'Comodojo Daemon' |
||
57 | ]; |
||
58 | |||
59 | /** |
||
60 | * Daemon constructor |
||
61 | * |
||
62 | * @param array $properties |
||
63 | * @param LoggerInterface $logger; |
||
64 | * @param EventsManager $events; |
||
65 | * |
||
66 | * @property EventsManager $events |
||
67 | * @property LoggerInterface $logger |
||
68 | * @property int $pid |
||
69 | */ |
||
70 | public function __construct($properties = [], LoggerInterface $logger = null, EventsManager $events = null) { |
||
71 | |||
72 | if ( !Checks::multithread() ) { |
||
73 | throw new Exception("Missing pcntl fork"); |
||
74 | } |
||
75 | |||
76 | $properties = ClassProperties::create(self::$default_properties)->merge($properties); |
||
77 | |||
78 | parent::__construct($properties->niceness, $logger, $events); |
||
79 | |||
80 | // prepare the pidlock |
||
81 | $this->pidlock = new PidLock($properties->pidfile); |
||
82 | |||
83 | // init the socket |
||
84 | $this->socket = new SocketServer( |
||
85 | $properties->sockethandler, |
||
86 | $this->logger, |
||
87 | $this->events, |
||
88 | $this, |
||
89 | $properties->socketbuffer, |
||
90 | $properties->sockettimeout, |
||
91 | $properties->socketmaxconnections |
||
92 | ); |
||
93 | |||
94 | // init the worker manager |
||
95 | $this->workers = new WorkerManager($this->logger, $this->events, $this); |
||
96 | |||
97 | // init the console |
||
98 | $this->console = new CLImate(); |
||
99 | $this->console->description($properties->description); |
||
100 | $args = new $properties->arguments; |
||
101 | $this->console->arguments->add($args::create()->export()); |
||
102 | |||
103 | } |
||
104 | |||
105 | /** |
||
106 | * Access the current socket |
||
107 | * |
||
108 | * @return SocketServer |
||
109 | */ |
||
110 | public function getSocket() { |
||
111 | return $this->socket; |
||
112 | } |
||
113 | |||
114 | /** |
||
115 | * Access the workers stack |
||
116 | * |
||
117 | * @return WorkerManager |
||
118 | */ |
||
119 | public function getWorkers() { |
||
120 | return $this->workers; |
||
121 | } |
||
122 | |||
123 | /** |
||
124 | * Setup method; it allows to inject code BEFORE the daemon spinup |
||
125 | * |
||
126 | */ |
||
127 | abstract public function setup(); |
||
128 | |||
129 | /** |
||
130 | * Parse console arguments and init the daemon |
||
131 | * |
||
132 | */ |
||
133 | public function init() { |
||
134 | |||
135 | $args = $this->console->arguments; |
||
136 | |||
137 | $args->parse(); |
||
138 | |||
139 | if ( $args->defined('hardstart') ) { |
||
140 | |||
141 | $this->hardstart(); |
||
142 | |||
143 | } |
||
144 | |||
145 | if ( $args->defined('daemon') ) { |
||
146 | |||
147 | $this->daemonize(); |
||
148 | |||
149 | } else if ( $args->defined('foreground') ) { |
||
150 | |||
151 | if ( $args->defined('verbose') ) { |
||
152 | $this->logger->pushHandler(new LogHandler()); |
||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
153 | } |
||
154 | |||
155 | $this->start(); |
||
156 | |||
157 | } else { |
||
158 | |||
159 | $this->console->pad()->green()->usage(); |
||
160 | $this->end(0); |
||
161 | |||
162 | } |
||
163 | |||
164 | } |
||
165 | |||
166 | /** |
||
167 | * Start as a daemon, forking main process and detaching it from terminal |
||
168 | * |
||
169 | */ |
||
170 | public function daemonize() { |
||
171 | |||
172 | // fork script |
||
173 | $pid = $this->fork(); |
||
174 | |||
175 | // detach from current terminal (if any) |
||
176 | $this->detach(); |
||
177 | |||
178 | // update pid reference (we have a new daemon) |
||
179 | $this->pid = $pid; |
||
180 | |||
181 | // start process daemon |
||
182 | $this->start(); |
||
183 | |||
184 | } |
||
185 | |||
186 | /** |
||
187 | * Start the process, creating socket and spinning up workers (if any) |
||
188 | * |
||
189 | */ |
||
190 | public function start() { |
||
191 | |||
192 | // we're activating! |
||
193 | $this->is_active = true; |
||
194 | |||
195 | $this->setup(); |
||
196 | |||
197 | foreach ( $this->workers as $name => $worker ) { |
||
198 | |||
199 | $this->workers->start($name); |
||
200 | |||
201 | } |
||
202 | |||
203 | $this->becomeSupervisor(); |
||
204 | |||
205 | // start listening on socket |
||
206 | try { |
||
207 | |||
208 | $this->socket->listen(); |
||
209 | |||
210 | } catch (SocketException $e) { |
||
211 | |||
212 | // something did wrong on socket... |
||
213 | $this->logger->error($e->getMessage()); |
||
214 | $this->stop(); |
||
215 | $this->end(0); |
||
216 | |||
217 | } |
||
218 | |||
219 | // loop closed; if I'm the supervisor, I should clean everything |
||
220 | if ( $this->is_supervisor && $this->is_active ) { |
||
221 | $this->stop(); |
||
222 | $this->end(0); |
||
223 | } |
||
224 | |||
225 | } |
||
226 | |||
227 | /** |
||
228 | * Stop the daemon, closing the socket and stopping all the workers (if any) |
||
229 | * |
||
230 | */ |
||
231 | public function stop() { |
||
232 | |||
233 | $this->logger->notice("Stopping daemon..."); |
||
234 | |||
235 | $this->events->removeAllListeners('daemon.posix.'.SIGCHLD); |
||
236 | |||
237 | $this->socket->close(); |
||
238 | |||
239 | $this->workers->stop(); |
||
240 | |||
241 | $this->pidlock->release(); |
||
242 | |||
243 | $this->is_active = false; |
||
244 | |||
245 | } |
||
246 | |||
247 | private function becomeSupervisor() { |
||
248 | |||
249 | $this->logger->notice("Initing supervisor subsystem"); |
||
250 | |||
251 | $this->is_supervisor = true; |
||
252 | |||
253 | // lock current PID |
||
254 | $this->pidlock->lock($this->pid); |
||
255 | |||
256 | try { |
||
257 | |||
258 | // connect socket |
||
259 | $this->socket->connect(); |
||
260 | |||
261 | } catch (SocketException $e) { |
||
262 | |||
263 | $this->logger->error("Supervisor error: ".$e->getMessage()); |
||
264 | $this->logger->notice("Shutting down process and childs"); |
||
265 | |||
266 | $this->stop(); |
||
267 | $this->end(1); |
||
268 | |||
269 | } |
||
270 | |||
271 | // Subscribe term events that could be catched |
||
272 | $this->events->subscribe('daemon.posix.'.SIGTERM, '\Comodojo\Daemon\Listeners\StopDaemon'); |
||
273 | $this->events->subscribe('daemon.posix.'.SIGINT, '\Comodojo\Daemon\Listeners\StopDaemon'); |
||
274 | |||
275 | if ( count($this->workers) > 0 ) { |
||
276 | // $this->events->subscribe('daemon.socket.loop', '\Comodojo\Daemon\Listeners\WorkerWatchdog'); |
||
277 | $this->events->subscribe('daemon.posix.'.SIGCHLD, '\Comodojo\Daemon\Listeners\WorkerWatchdog'); |
||
278 | } |
||
279 | |||
280 | } |
||
281 | |||
282 | /** |
||
283 | * Declass the daemon to a normal supervised process (daemon to worker transition) |
||
284 | * |
||
285 | */ |
||
286 | public function declass() { |
||
287 | |||
288 | // remove supervisor flag |
||
289 | $this->is_supervisor = false; |
||
290 | |||
291 | // Unsubscribe supervisor default events (if any) |
||
292 | $this->events->removeAllListeners('daemon.posix.'.SIGTERM); |
||
293 | $this->events->removeAllListeners('daemon.posix.'.SIGINT); |
||
294 | $this->events->removeAllListeners('daemon.socket.loop'); |
||
295 | |||
296 | // unset supervisor components |
||
297 | unset($this->pidlock); |
||
298 | unset($this->socket); |
||
299 | unset($this->workers); |
||
300 | unset($this->console); |
||
301 | |||
302 | } |
||
303 | |||
304 | private function fork() { |
||
305 | |||
306 | $pid = pcntl_fork(); |
||
307 | |||
308 | if ( $pid == -1 ) { |
||
309 | $this->logger->error('Could not create daemon (fork error)'); |
||
310 | $this->end(1); |
||
311 | } |
||
312 | |||
313 | if ( $pid ) { |
||
314 | $this->logger->notice("Daemon created with pid $pid"); |
||
315 | $this->end(0); |
||
316 | } |
||
317 | |||
318 | return ProcessTools::getPid(); |
||
319 | |||
320 | } |
||
321 | |||
322 | private function detach() { |
||
323 | |||
324 | if ( is_resource(STDOUT) ) fclose(STDOUT); |
||
325 | if ( is_resource(STDERR) ) fclose(STDERR); |
||
326 | if ( is_resource(STDIN) ) fclose(STDIN); |
||
327 | |||
328 | // become a session leader |
||
329 | $sid = posix_setsid(); |
||
330 | |||
331 | if ( $sid < 0 ) { |
||
332 | $this->logger->error("Unable to become session leader"); |
||
333 | $this->end(1); |
||
334 | } |
||
335 | |||
336 | } |
||
337 | |||
338 | private function hardstart() { |
||
339 | |||
340 | $this->pidlock->release(); |
||
341 | $this->socket->clean(); |
||
342 | |||
343 | } |
||
344 | |||
345 | } |
||
346 |