Issues (47)

src/Comodojo/Daemon/Daemon.php (1 issue)

Labels
Severity
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
The method pushHandler() does not exist on Psr\Log\LoggerInterface. It seems like you code against a sub-type of Psr\Log\LoggerInterface such as Monolog\Logger. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

152
                $this->logger->/** @scrutinizer ignore-call */ 
153
                               pushHandler(new LogHandler());
Loading history...
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