Completed
Push — master ( 5a989c...f558eb )
by Tim
12s
created

MultiThreadedServer   B

Complexity

Total Complexity 39

Size/Duplication

Total Lines 416
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 0%

Importance

Changes 15
Bugs 3 Features 4
Metric Value
wmc 39
c 15
b 3
f 4
lcom 1
cbo 6
dl 0
loc 416
ccs 0
cts 214
cp 0
rs 8.2857

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A getServerContext() 0 4 1
A stop() 0 17 2
F run() 0 295 30
A explodeConstants() 0 17 3
A realPath() 0 14 2
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
        // set current dir to base dir for relative dirs
117
        chdir(SERVER_BASEDIR);
118
119
        // setup autoloader
120
        require SERVER_AUTOLOADER;
121
122
        // init server context
123
        $serverContext = $this->getServerContext();
124
125
        // init config var for shorter calls
126
        $serverConfig = $serverContext->getServerConfig();
127
128
        // init server name
129
        $serverName = $serverConfig->getName();
130
131
        // initialize the profile logger and the thread context
132
        $profileLogger = null;
133
        if ($serverContext->hasLogger(LoggerUtils::PROFILE)) {
134
            $profileLogger = $serverContext->getLogger(LoggerUtils::PROFILE);
135
            $profileLogger->appendThreadContext($serverName);
136
        }
137
138
        // init logger
139
        $logger = $serverContext->getLogger();
140
        $logger->debug(
141
            sprintf("starting %s (%s)", $serverName, __CLASS__)
142
        );
143
144
        try {
145
            // get class names
146
            $socketType = $serverConfig->getSocketType();
147
            $workerType = $serverConfig->getWorkerType();
148
            $streamContextType = $serverConfig->getStreamContextType();
149
150
            // init stream context for server connection
151
            $streamContext = new $streamContextType();
152
            // set socket backlog to 1024 for perform many concurrent connections
153
            $streamContext->setOption('socket', 'backlog', 1024);
154
155
            // check if ssl server config
156
            if ($serverConfig->getTransport() === 'ssl') {
157
                // path to local certificate file on filesystem. It must be a PEM encoded file which contains your
158
                // certificate and private key. It can optionally contain the certificate chain of issuers.
159
                $streamContext->setOption('ssl', 'local_cert', $this->realPath($serverConfig->getCertPath()));
160
161
                // query whether or not a passphrase has been set
162
                if ($passphrase = $serverConfig->getPassphrase()) {
163
                    $streamContext->setOption('ssl', 'passphrase', $passphrase);
164
                }
165
                // query whether or not DH param as been specified
166
                if ($dhParamPath = $serverConfig->getDhParamPath()) {
167
                    $streamContext->setOption('ssl', 'dh_param', $this->realPath($dhParamPath));
168
                }
169
                // query whether or not a private key as been specified
170
                if ($privateKeyPath = $serverConfig->getPrivateKeyPath()) {
171
                    $streamContext->setOption('ssl', 'local_pk', $this->realPath($privateKeyPath));
172
                }
173
                // set the passed crypto method
174
                if ($cryptoMethod = $serverConfig->getCryptoMethod()) {
175
                    $streamContext->setOption('ssl', 'crypto_method', $this->explodeConstants($cryptoMethod));
176
                }
177
                // set the passed peer name
178
                if ($peerName = $serverConfig->getPeerName()) {
179
                    $streamContext->setOption('ssl', 'peer_name', $peerName);
180
                }
181
                // set the ECDH curve to use
182
                if ($ecdhCurve = $serverConfig->getEcdhCurve()) {
183
                    $streamContext->setOption('ssl', 'ecdh_curve', $ecdhCurve);
184
                }
185
186
                // require verification of SSL certificate used and peer name
187
                $streamContext->setOption('ssl', 'verify_peer', $serverConfig->getVerifyPeer());
188
                $streamContext->setOption('ssl', 'verify_peer_name', $serverConfig->getVerifyPeerName());
189
                // allow self-signed certificates. requires verify_peer
190
                $streamContext->setOption('ssl', 'allow_self_signed', $serverConfig->getAllowSelfSigned());
191
                // if set, disable TLS compression this can help mitigate the CRIME attack vector
192
                $streamContext->setOption('ssl', 'disable_compression', $serverConfig->getDisableCompression());
193
                // optimizations for forward secrecy
194
                $streamContext->setOption('ssl', 'honor_cipher_order', $serverConfig->getHonorCipherOrder());
195
                $streamContext->setOption('ssl', 'single_ecdh_use', $serverConfig->getSingleEcdhUse());
196
                $streamContext->setOption('ssl', 'single_dh_use', $serverConfig->getSingleDhUse());
197
198
                // set all domain specific certificates
199
                foreach ($serverConfig->getCertificates() as $certificate) {
200
                    // try to set ssl certificates
201
                    // validation checks are made there and we want the server started in case of invalid ssl context
202
                    try {
203
                        $streamContext->addSniServerCert($certificate['domain'], $certificate['certPath']);
204
                    } catch (\Exception $e) {
205
                        // log exception message
206
                        $logger->error($e->getMessage());
207
                    }
208
                }
209
            }
210
211
            // inject stream context to server context for further modification in modules init function
212
            $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...
213
214
            // initialization has been successful
215
            $this->serverState = ServerStateKeys::INITIALIZATION_SUCCESSFUL;
216
217
            // init modules array
218
            $modules = array();
219
220
            // initiate server modules
221
            foreach ($serverConfig->getModules() as $moduleConfiguration) {
222
                // check if module type exists
223
                if (class_exists($moduleType = $moduleConfiguration->getType()) === false) {
224
                    throw new ModuleNotFoundException($moduleType);
225
                }
226
227
                // instantiate module type
228
                $module = new $moduleType();
229
230
                // query whether or not we've to inject the module configuration
231
                if ($module instanceof ModuleConfigurationAwareInterface) {
232
                    $module->injectModuleConfiguration($moduleConfiguration);
233
                }
234
235
                // append the initialized module to the list
236
                $modules[$moduleName = $module->getModuleName()] = $module;
237
238
                // debug log that the module has successfully been initialized
239
                $logger->debug(
240
                    sprintf("%s init %s module (%s)", $serverName, $moduleType::MODULE_NAME, $moduleType)
241
                );
242
243
                // init module with serverContext (this)
244
                $modules[$moduleName]->init($serverContext);
245
            }
246
247
            // modules has been initialized successfully
248
            $this->serverState = ServerStateKeys::MODULES_INITIALIZED;
249
250
            // init connection handler array
251
            $connectionHandlers = array();
252
            // initiate server connection handlers
253
            $connectionHandlersTypes = $serverConfig->getConnectionHandlers();
254
            foreach ($connectionHandlersTypes as $connectionHandlerType) {
255
                // check if conenction handler type exists
256
                if (!class_exists($connectionHandlerType)) {
257
                    throw new ConnectionHandlerNotFoundException($connectionHandlerType);
258
                }
259
                // instantiate connection handler type
260
                $connectionHandlers[$connectionHandlerType] = new $connectionHandlerType();
261
262
                $logger->debug(
263
                    sprintf("%s init connectionHandler (%s)", $serverName, $connectionHandlerType)
264
                );
265
266
                // init connection handler with serverContext (this)
267
                $connectionHandlers[$connectionHandlerType]->init($serverContext);
268
                // inject modules
269
                $connectionHandlers[$connectionHandlerType]->injectModules($modules);
270
            }
271
272
            // connection handlers has been initialized successfully
273
            $this->serverState = ServerStateKeys::CONNECTION_HANDLERS_INITIALIZED;
274
275
            // prepare the socket
276
            $localSocket = sprintf(
277
                '%s://%s:%d',
278
                $serverConfig->getTransport(),
279
                $serverConfig->getAddress(),
280
                $serverConfig->getPort()
281
            );
282
283
            // prepare the socket flags
284
            $flags = $this->explodeConstants($serverConfig->getFlags());
285
286
            // setup server bound on local adress
287
            $serverConnection = $socketType::getServerInstance($localSocket, $flags, $streamContext->getResource());
288
289
            // sockets has been started
290
            $this->serverState = ServerStateKeys::SERVER_SOCKET_STARTED;
291
292
            $logger->debug(
293
                sprintf("%s starting %s workers (%s)", $serverName, $serverConfig->getWorkerNumber(), $workerType)
294
            );
295
296
            // setup and start workers
297
            $workers = array();
298
            for ($i = 1; $i <= $serverConfig->getWorkerNumber(); ++$i) {
299
                $workers[$i] = new $workerType(
300
                    $serverConnection->getConnectionResource(),
301
                    $serverContext,
302
                    $connectionHandlers
303
                );
304
305
                $logger->debug(sprintf("Successfully started worker %s", $workers[$i]->getThreadId()));
306
            }
307
308
            // connection handlers has been initialized successfully
309
            $this->serverState = ServerStateKeys::WORKERS_INITIALIZED;
310
311
            $logger->info(
312
                sprintf("%s listing on %s:%s...", $serverName, $serverConfig->getAddress(), $serverConfig->getPort())
313
            );
314
315
            // watch dog for all workers to restart if it's needed while server is up
316
            while ($this->serverState === ServerStateKeys::WORKERS_INITIALIZED) {
317
                // iterate all workers
318
                for ($i = 1; $i <= $serverConfig->getWorkerNumber(); ++$i) {
319
                    // check if worker should be restarted
320
                    if ($workers[$i]->shouldRestart()) {
321
                        $logger->debug(
322
                            sprintf("%s restarting worker #%s (%s)", $serverName, $i, $workerType)
323
                        );
324
325
                        // unset origin worker ref
326
                        unset($workers[$i]);
327
                        // build up and start new worker instance
328
                        $workers[$i] = new $workerType(
329
                            $serverConnection->getConnectionResource(),
330
                            $serverContext,
331
                            $connectionHandlers
332
                        );
333
                    }
334
                }
335
336
                if ($profileLogger) {
337
                    // profile the worker shutdown beeing processed
338
                    $profileLogger->debug(sprintf('Server %s waiting for shutdown', $serverName));
339
                }
340
341
                // sleep for 1 second to lower system load
342
                usleep(1000000);
343
            }
344
345
            // print a message with the number of initialized workers
346
            $logger->debug(sprintf('Now shutdown server %s (%d workers)', $serverName, sizeof($workers)));
347
348
            // prepare the URL and the options for the shutdown requests
349
            $scheme = $serverConfig->getTransport() == 'tcp' ? 'http' : 'https';
350
351
            // prepare the URL for the request to shutdown the workers
352
            $url =  sprintf('%s://%s:%d', $scheme, $serverConfig->getAddress(), $serverConfig->getPort());
353
354
            // create a context for the HTTP/HTTPS connection
355
            $context  = stream_context_create(
356
                array(
357
                    'http' => array(
358
                        'method'  => 'GET',
359
                        'header'  => "Connection: close\r\n"
360
                    ),
361
                    'https' => array(
362
                        'method'  => 'GET',
363
                        'header'  => "Connection: close\r\n"
364
                    ),
365
                    'ssl' => array(
366
                        'verify_peer'      => false,
367
                        'verify_peer_name' => false
368
                    )
369
                )
370
            );
371
372
            // try to shutdown all workers
373
            while (sizeof($workers) > 0) {
374
                // iterate all workers
375
                for ($i = 1; $i <= $serverConfig->getWorkerNumber(); ++$i) {
376
                    // check if worker should be restarted
377
                    if (isset($workers[$i]) && $workers[$i]->shouldRestart()) {
378
                        // unset worker, it has been shutdown successfully
379
                        unset($workers[$i]);
380
                    } elseif (isset($workers[$i]) && $workers[$i]->shouldRestart() === false) {
381
                        // send a request to shutdown running worker
382
                        @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...
383
                        // don't flood the remaining workers
384
                        usleep(10000);
385
                    } else {
386
                        // send a debug log message that worker has been shutdown
387
                        $logger->debug("Worker $i successfully been shutdown ...");
388
                    }
389
                }
390
            }
391
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
392
        } catch (\Exception $e) {
393
            // log error message
394
            $logger->error($e->getMessage());
395
        }
396
397
        // close the server sockets if opened before
398
        if ($serverConnection) {
399
            $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...
400
            // mark the server state as shutdown
401
            $this->synchronized(function ($self) {
402
                $self->serverState = ServerStateKeys::SHUTDOWN;
403
            }, $this);
404
        }
405
406
        // send a debug log message that connection has been closed and server has been shutdown
407
        $logger->info("Successfully closed connection and shutdown server $serverName");
408
    }
409
410
    /**
411
     * Explodes the constants from the passed string.
412
     *
413
     * @param string $values The string with the constants to explode
414
     *
415
     * @return integer The exploded constants
416
     */
417
    protected function explodeConstants($values)
418
    {
419
420
        // initialize the constants
421
        $constants = null;
422
423
        // explode the constants
424
        foreach (explode('|', $values) as $value) {
425
            $constant = trim($value);
426
            if (empty($constant) === false) {
427
                $constants += constant($constant);
428
            }
429
        }
430
431
        // return the constants
432
        return $constants;
433
    }
434
435
    /**
436
     * Return's the real path, if not already.
437
     *
438
     * @param string $path The path to return the real path for
439
     *
440
     * @return string The real path
441
     */
442
    protected function realPath($path)
443
    {
444
445
        // take care for the OS
446
        $path = str_replace('/', DIRECTORY_SEPARATOR, $path);
447
448
        // check if relative or absolute path was given
449
        if (strpos($path, '/') === false) {
450
            $path = SERVER_BASEDIR . $path;
451
        }
452
453
        // return the real path
454
        return $path;
455
    }
456
}
457