Completed
Push — master ( a6bc20...1e51c9 )
by Tim
8s
created

MultiThreadedServer::shutdown()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 9
Ratio 100 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 9
loc 9
ccs 0
cts 7
cp 0
rs 9.6666
cc 3
eloc 4
nc 2
nop 0
crap 12
1
<?php
2
3
/**
4
 * \AppserverIo\Server\Servers\MultiThreadedServer
5
 *
6
 * NOTICE OF LICENSE
7
 *
8
 * This source file is subject to the Open Software License (OSL 3.0)
9
 * that is available through the world-wide-web at this URL:
10
 * http://opensource.org/licenses/osl-3.0.php
11
 *
12
 * PHP version 5
13
 *
14
 * @author    Johann Zelger <[email protected]>
15
 * @copyright 2015 TechDivision GmbH <[email protected]>
16
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
17
 * @link      https://github.com/appserver-io/server
18
 * @link      http://www.appserver.io
19
 */
20
21
namespace AppserverIo\Server\Servers;
22
23
use AppserverIo\Logger\LoggerUtils;
24
use AppserverIo\Server\Dictionaries\ServerStateKeys;
25
use AppserverIo\Server\Interfaces\ServerInterface;
26
use AppserverIo\Server\Interfaces\ServerContextInterface;
27
use AppserverIo\Server\Exceptions\ModuleNotFoundException;
28
use AppserverIo\Server\Exceptions\ConnectionHandlerNotFoundException;
29
use AppserverIo\Server\Interfaces\ModuleConfigurationAwareInterface;
30
31
/**
32
 * A multithreaded server implemenation.
33
 *
34
 * @author    Johann Zelger <[email protected]>
35
 * @copyright 2015 TechDivision GmbH <[email protected]>
36
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
37
 * @link      https://github.com/appserver-io/server
38
 * @link      http://www.appserver.io
39
 */
40
41
class MultiThreadedServer extends \Thread implements ServerInterface
42
{
43
44
    /**
45
     * Holds the server context instance
46
     *
47
     * @var \AppserverIo\Server\Interfaces\ServerContextInterface The server context instance
48
     */
49
    protected $serverContext;
50
51
    /**
52
     * TRUE if the server has been started successfully, else FALSE.
53
     *
54
     * @var \AppserverIo\Server\Dictionaries\ServerStateKeys
55
     */
56
    protected $serverState;
57
58
    /**
59
     * Constructs the server instance
60
     *
61
     * @param \AppserverIo\Server\Interfaces\ServerContextInterface $serverContext The server context instance
62
     */
63
    public function __construct(ServerContextInterface $serverContext)
64
    {
65
        // initialize the server state
66
        $this->serverState = ServerStateKeys::WAITING_FOR_INITIALIZATION;
67
        // set context
68
        $this->serverContext = $serverContext;
69
        // start server thread
70
        $this->start();
71
    }
72
73
    /**
74
     * Returns the config instance
75
     *
76
     * @return \AppserverIo\Server\Interfaces\ServerContextInterface
77
     */
78
    public function getServerContext()
79
    {
80
        return $this->serverContext;
81
    }
82
83
    /**
84
     * Shutdown the workers and stop the server.
85
     *
86
     * @return void
87
     */
88
    public function stop()
89
    {
90
        $this->synchronized(function ($self) {
91
            $self->serverState = ServerStateKeys::HALT;
92
        }, $this);
93
94
        do {
95
            // query whether application state key is SHUTDOWN or not
96
            $waitForShutdown = $this->synchronized(function ($self) {
97
                return $self->serverState !== ServerStateKeys::SHUTDOWN;
98
            }, $this);
99
100
            // wait one second more
101
            sleep(1);
102
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
103
        } while ($waitForShutdown);
104
    }
105
106
    /**
107
     * Starts the server's worker as defined in configuration
108
     *
109
     * @return void
110
     *
111
     * @throws \AppserverIo\Server\Exceptions\ModuleNotFoundException
112
     * @throws \AppserverIo\Server\Exceptions\ConnectionHandlerNotFoundException
113
     */
114
    public function run()
115
    {
116
117
        // register shutdown handler
118
        register_shutdown_function(array(&$this, "shutdown"));
119
120
        // register a custom exception handler
121
        set_exception_handler(array(&$this, "handleException"));
122
123
        // set current dir to base dir for relative dirs
124
        chdir(SERVER_BASEDIR);
125
126
        // setup autoloader
127
        require SERVER_AUTOLOADER;
128
129
        // init server context
130
        $serverContext = $this->getServerContext();
131
132
        // init config var for shorter calls
133
        $serverConfig = $serverContext->getServerConfig();
134
135
        // init server name
136
        $serverName = $serverConfig->getName();
137
138
        // initialize the profile logger and the thread context
139
        $profileLogger = null;
140
        if ($serverContext->hasLogger(LoggerUtils::PROFILE)) {
141
            $profileLogger = $serverContext->getLogger(LoggerUtils::PROFILE);
142
            $profileLogger->appendThreadContext($serverName);
143
        }
144
145
        // init logger
146
        $logger = $serverContext->getLogger();
147
        $logger->debug(
148
            sprintf("starting %s (%s)", $serverName, __CLASS__)
149
        );
150
151
        try {
152
            // get class names
153
            $socketType = $serverConfig->getSocketType();
154
            $workerType = $serverConfig->getWorkerType();
155
            $streamContextType = $serverConfig->getStreamContextType();
156
157
            // init stream context for server connection
158
            $streamContext = new $streamContextType();
159
            // set socket backlog to 1024 for perform many concurrent connections
160
            $streamContext->setOption('socket', 'backlog', 1024);
161
162
            // check if ssl server config
163
            if ($serverConfig->getTransport() === 'ssl') {
164
                // path to local certificate file on filesystem. It must be a PEM encoded file which contains your
165
                // certificate and private key. It can optionally contain the certificate chain of issuers.
166
                $streamContext->setOption('ssl', 'local_cert', $this->realPath($serverConfig->getCertPath()));
167
168
                // query whether or not a passphrase has been set
169
                if ($passphrase = $serverConfig->getPassphrase()) {
170
                    $streamContext->setOption('ssl', 'passphrase', $passphrase);
171
                }
172
                // query whether or not DH param as been specified
173
                if ($dhParamPath = $serverConfig->getDhParamPath()) {
174
                    $streamContext->setOption('ssl', 'dh_param', $this->realPath($dhParamPath));
175
                }
176
                // query whether or not a private key as been specified
177
                if ($privateKeyPath = $serverConfig->getPrivateKeyPath()) {
178
                    $streamContext->setOption('ssl', 'local_pk', $this->realPath($privateKeyPath));
179
                }
180
                // set the passed crypto method
181
                if ($cryptoMethod = $serverConfig->getCryptoMethod()) {
182
                    $streamContext->setOption('ssl', 'crypto_method', $this->explodeConstants($cryptoMethod));
183
                }
184
                // set the passed peer name
185
                if ($peerName = $serverConfig->getPeerName()) {
186
                    $streamContext->setOption('ssl', 'peer_name', $peerName);
187
                }
188
                // set the ECDH curve to use
189
                if ($ecdhCurve = $serverConfig->getEcdhCurve()) {
190
                    $streamContext->setOption('ssl', 'ecdh_curve', $ecdhCurve);
191
                }
192
193
                // require verification of SSL certificate used and peer name
194
                $streamContext->setOption('ssl', 'verify_peer', $serverConfig->getVerifyPeer());
195
                $streamContext->setOption('ssl', 'verify_peer_name', $serverConfig->getVerifyPeerName());
196
                // allow self-signed certificates. requires verify_peer
197
                $streamContext->setOption('ssl', 'allow_self_signed', $serverConfig->getAllowSelfSigned());
198
                // if set, disable TLS compression this can help mitigate the CRIME attack vector
199
                $streamContext->setOption('ssl', 'disable_compression', $serverConfig->getDisableCompression());
200
                // optimizations for forward secrecy and ciphers
201
                $streamContext->setOption('ssl', 'honor_cipher_order', $serverConfig->getHonorCipherOrder());
202
                $streamContext->setOption('ssl', 'single_ecdh_use', $serverConfig->getSingleEcdhUse());
203
                $streamContext->setOption('ssl', 'single_dh_use', $serverConfig->getSingleDhUse());
204
                $streamContext->setOption('ssl', 'ciphers', $serverConfig->getCiphers());
205
206
                // set all domain specific certificates
207
                foreach ($serverConfig->getCertificates() as $certificate) {
208
                    // try to set ssl certificates
209
                    // validation checks are made there and we want the server started in case of invalid ssl context
210
                    try {
211
                        $streamContext->addSniServerCert($certificate['domain'], $certificate['certPath']);
212
                    } catch (\Exception $e) {
213
                        // log exception message
214
                        $logger->error($e->getMessage());
215
                    }
216
                }
217
            }
218
219
            // inject stream context to server context for further modification in modules init function
220
            $serverContext->injectStreamContext($streamContext);
0 ignored issues
show
Documentation introduced by
$streamContext is of type object, but the function expects a resource.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
221
222
            // initialization has been successful
223
            $this->serverState = ServerStateKeys::INITIALIZATION_SUCCESSFUL;
224
225
            // init modules array
226
            $modules = array();
227
228
            // initiate server modules
229
            foreach ($serverConfig->getModules() as $moduleConfiguration) {
230
                // check if module type exists
231
                if (class_exists($moduleType = $moduleConfiguration->getType()) === false) {
232
                    throw new ModuleNotFoundException($moduleType);
233
                }
234
235
                // instantiate module type
236
                $module = new $moduleType();
237
238
                // query whether or not we've to inject the module configuration
239
                if ($module instanceof ModuleConfigurationAwareInterface) {
240
                    $module->injectModuleConfiguration($moduleConfiguration);
241
                }
242
243
                // append the initialized module to the list
244
                $modules[$moduleName = $module->getModuleName()] = $module;
245
246
                // debug log that the module has successfully been initialized
247
                $logger->debug(
248
                    sprintf("%s init %s module (%s)", $serverName, $moduleType::MODULE_NAME, $moduleType)
249
                );
250
251
                // init module with serverContext (this)
252
                $modules[$moduleName]->init($serverContext);
253
            }
254
255
            // modules has been initialized successfully
256
            $this->serverState = ServerStateKeys::MODULES_INITIALIZED;
257
258
            // init connection handler array
259
            $connectionHandlers = array();
260
            // initiate server connection handlers
261
            $connectionHandlersTypes = $serverConfig->getConnectionHandlers();
262
            foreach ($connectionHandlersTypes as $connectionHandlerType) {
263
                // check if conenction handler type exists
264
                if (!class_exists($connectionHandlerType)) {
265
                    throw new ConnectionHandlerNotFoundException($connectionHandlerType);
266
                }
267
                // instantiate connection handler type
268
                $connectionHandlers[$connectionHandlerType] = new $connectionHandlerType();
269
270
                $logger->debug(
271
                    sprintf("%s init connectionHandler (%s)", $serverName, $connectionHandlerType)
272
                );
273
274
                // init connection handler with serverContext (this)
275
                $connectionHandlers[$connectionHandlerType]->init($serverContext);
276
                // inject modules
277
                $connectionHandlers[$connectionHandlerType]->injectModules($modules);
278
            }
279
280
            // connection handlers has been initialized successfully
281
            $this->serverState = ServerStateKeys::CONNECTION_HANDLERS_INITIALIZED;
282
283
            // prepare the socket
284
            $localSocket = sprintf(
285
                '%s://%s:%d',
286
                $serverConfig->getTransport(),
287
                $serverConfig->getAddress(),
288
                $serverConfig->getPort()
289
            );
290
291
            // prepare the socket flags
292
            $flags = $this->explodeConstants($serverConfig->getFlags());
293
294
            // setup server bound on local adress
295
            $serverConnection = $socketType::getServerInstance($localSocket, $flags, $streamContext->getResource());
296
297
            // sockets has been started
298
            $this->serverState = ServerStateKeys::SERVER_SOCKET_STARTED;
299
300
            $logger->debug(
301
                sprintf("%s starting %s workers (%s)", $serverName, $serverConfig->getWorkerNumber(), $workerType)
302
            );
303
304
            // setup and start workers
305
            $workers = array();
306
            for ($i = 1; $i <= $serverConfig->getWorkerNumber(); ++$i) {
307
                $workers[$i] = new $workerType(
308
                    $serverConnection->getConnectionResource(),
309
                    $serverContext,
310
                    $connectionHandlers
311
                );
312
313
                $logger->debug(sprintf("Successfully started worker %s", $workers[$i]->getThreadId()));
314
            }
315
316
            // connection handlers has been initialized successfully
317
            $this->serverState = ServerStateKeys::WORKERS_INITIALIZED;
318
319
            $logger->info(
320
                sprintf("%s listing on %s:%s...", $serverName, $serverConfig->getAddress(), $serverConfig->getPort())
321
            );
322
323
            // watch dog for all workers to restart if it's needed while server is up
324
            while ($this->serverState === ServerStateKeys::WORKERS_INITIALIZED) {
325
                // iterate all workers
326
                for ($i = 1; $i <= $serverConfig->getWorkerNumber(); ++$i) {
327
                    // check if worker should be restarted
328
                    if ($workers[$i]->shouldRestart()) {
329
                        $logger->debug(
330
                            sprintf("%s restarting worker #%s (%s)", $serverName, $i, $workerType)
331
                        );
332
333
                        // unset origin worker ref
334
                        unset($workers[$i]);
335
                        // build up and start new worker instance
336
                        $workers[$i] = new $workerType(
337
                            $serverConnection->getConnectionResource(),
338
                            $serverContext,
339
                            $connectionHandlers
340
                        );
341
                    }
342
                }
343
344
                if ($profileLogger) {
345
                    // profile the worker shutdown beeing processed
346
                    $profileLogger->debug(sprintf('Server %s waiting for shutdown', $serverName));
347
                }
348
349
                // sleep for 1 second to lower system load
350
                usleep(1000000);
351
            }
352
353
            // print a message with the number of initialized workers
354
            $logger->debug(sprintf('Now shutdown server %s (%d workers)', $serverName, sizeof($workers)));
355
356
            // prepare the URL and the options for the shutdown requests
357
            $scheme = $serverConfig->getTransport() == 'tcp' ? 'http' : 'https';
358
359
            // prepare the URL for the request to shutdown the workers
360
            $url =  sprintf('%s://%s:%d', $scheme, $serverConfig->getAddress(), $serverConfig->getPort());
361
362
            // create a context for the HTTP/HTTPS connection
363
            $context  = stream_context_create(
364
                array(
365
                    'http' => array(
366
                        'method'  => 'GET',
367
                        'header'  => "Connection: close\r\n"
368
                    ),
369
                    'https' => array(
370
                        'method'  => 'GET',
371
                        'header'  => "Connection: close\r\n"
372
                    ),
373
                    'ssl' => array(
374
                        'verify_peer'      => false,
375
                        'verify_peer_name' => false
376
                    )
377
                )
378
            );
379
380
            // try to shutdown all workers
381
            while (sizeof($workers) > 0) {
382
                // iterate all workers
383
                for ($i = 1; $i <= $serverConfig->getWorkerNumber(); ++$i) {
384
                    // check if worker should be restarted
385
                    if (isset($workers[$i]) && $workers[$i]->shouldRestart()) {
386
                        // unset worker, it has been shutdown successfully
387
                        unset($workers[$i]);
388
                    } elseif (isset($workers[$i]) && $workers[$i]->shouldRestart() === false) {
389
                        // send a request to shutdown running worker
390
                        @file_get_contents($url, false, $context);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
391
                        // don't flood the remaining workers
392
                        usleep(10000);
393
                    } else {
394
                        // send a debug log message that worker has been shutdown
395
                        $logger->debug("Worker $i successfully been shutdown ...");
396
                    }
397
                }
398
            }
399
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
400
        } catch (\Exception $e) {
401
            // log error message
402
            $logger->error($e->getMessage());
403
        }
404
405
        // close the server sockets if opened before
406
        if ($serverConnection) {
407
            $serverConnection->close();
0 ignored issues
show
Bug introduced by
The variable $serverConnection does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
408
            // mark the server state as shutdown
409
            $this->synchronized(function ($self) {
410
                $self->serverState = ServerStateKeys::SHUTDOWN;
411
            }, $this);
412
        }
413
414
        // send a debug log message that connection has been closed and server has been shutdown
415
        $logger->info("Successfully closed connection and shutdown server $serverName");
416
    }
417
418
    /**
419
     * Explodes the constants from the passed string.
420
     *
421
     * @param string $values The string with the constants to explode
422
     *
423
     * @return integer The exploded constants
424
     */
425
    protected function explodeConstants($values)
426
    {
427
428
        // initialize the constants
429
        $constants = null;
430
431
        // explode the constants
432
        foreach (explode('|', $values) as $value) {
433
            $constant = trim($value);
434
            if (empty($constant) === false) {
435
                $constants += constant($constant);
436
            }
437
        }
438
439
        // return the constants
440
        return $constants;
441
    }
442
443
    /**
444
     * Return's the real path, if not already.
445
     *
446
     * @param string $path The path to return the real path for
447
     *
448
     * @return string The real path
449
     */
450
    protected function realPath($path)
451
    {
452
453
        // take care for the OS
454
        $path = str_replace('/', DIRECTORY_SEPARATOR, $path);
455
456
        // check if relative or absolute path was given
457
        if (strpos($path, '/') === false) {
458
            $path = SERVER_BASEDIR . $path;
459
        }
460
461
        // return the real path
462
        return $path;
463
    }
464
465
    /**
466
     * Does shutdown logic for worker if something breaks in process.
467
     *
468
     * @return void
469
     */
470 View Code Duplication
    public function shutdown()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
471
    {
472
        // check if there was a fatal error caused shutdown
473
        $lastError = error_get_last();
474
        if ($lastError['type'] === E_ERROR || $lastError['type'] === E_USER_ERROR) {
475
            // log error
476
            $this->getServerContext()->getLogger()->error($lastError['message']);
477
        }
478
    }
479
480
    /**
481
     * Writes the passed exception stack trace to the system logger.
482
     *
483
     * @param \Exception $e The exception to be handled
484
     *
485
     * @return void
486
     */
487
    public function handleException(\Exception $e)
488
    {
489
        $this->getServerContext()->getLogger()->error($e->__toString());
490
    }
491
}
492