Completed
Push — master ( 3a30c8...8579f0 )
by Sébastien
03:13
created

StandaloneServer::isRunning()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3.0175

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 14
ccs 7
cts 8
cp 0.875
rs 9.4285
cc 3
eloc 9
nc 3
nop 0
crap 3.0175
1
<?php
2
3
namespace PjbServer\Tools;
4
5
use PjbServer\Tools\Network\PortTester;
6
7
/*
8
  java -Djava.awt.headless="true"
9
  -Dphp.java.bridge.threads=50
10
  -Dphp.java.bridge.base=/usr/lib/php/modules
11
  -Dphp.java.bridge.php_exec=/usr/local/bin/php-cgi
12
  -Dphp.java.bridge.default_log_file=
13
  -Dphp.java.bridge.default_log_level=5
14
  -Dphp.java.bridge.daemon="false"
15
  -jar JavaBridge.jar
16
 * sudo netstat -anltp|grep :8089
17
 */
18
19
class StandaloneServer
20
{
21
22
    /**
23
     * @var int
24
     */
25
    protected $port;
26
27
    /**
28
     *
29
     * @var array
30
     */
31
    protected $config;
32
33
    /**
34
     * Tells whether the standalone server is started
35
     * @var boolean
36
     */
37
    protected $started = false;
38
39
    /**
40
     * @var array
41
     */
42
    protected $required_arguments = [
43
        'port' => 'FILTER_VALIDATE_INT',
44
        'server_jar' => 'existing_file',
45
        'log_file' => 'file_with_existing_directory',
46
        'pid_file' => 'file_with_existing_directory',
47
        'autoload_path' => 'existing_directory'
48
    ];
49
50
    /**
51
     * Default configuration options
52
     * @var array
53
     */
54
    protected $default_config = [
55
        'server_jar' => '{base_dir}/resources/pjb621_standalone/JavaBridge.jar',
56
        'java_bin' => 'java',
57
        'log_file' => '{base_dir}/resources/pjb621_standalone/logs/pjbserver-port{tcp_port}.log',
58
        'pid_file' => '{base_dir}/resources/pjb621_standalone/var/run/pjbserver-port{tcp_port}.pid',
59
        'autoload_path' => '{base_dir}/resources/autoload'
60
    ];
61
62
    /**
63
     *
64
     * @var PortTester
65
     */
66
    protected $portTester;
67
68
69
    /**
70
     * Constructor
71
     *
72
     * <code>
73
     *
74
     * $config = array(
75
     *      'port' => 8089,
76
     *
77
     *      // optionally
78
     *      'server_jar' => 'path/to/JavaBridge.jar'
79
     *      'java_bin' => 'path/to/java'
80
     * );
81
     * $server = new StandaloneServer($config);
82
     *
83
     * </code>
84
     *
85
     * @throws Exception\InvalidArgumentException
86
     * @param array $config
87
     */
88 19
    public function __construct(array $config)
89
    {
90 19
        if (!isset($config['port'])) {
91 3
            throw new Exception\InvalidArgumentException("Error missing required 'port' in config");
92 16
        } elseif (!filter_var($config['port'], FILTER_VALIDATE_INT)) {
93 1
            throw new Exception\InvalidArgumentException("Option 'port' must be numeric");
94
        }
95 15
        $config = array_merge($this->getDefaultConfig($config['port']), $config);
96 15
        $this->checkConfigRequiredArgs($config);
97 14
        $this->setServerPort($config['port']);
98 14
        $this->config = $config;
99
100 14
        $curl_available = function_exists('curl_version');
101
102 14
        $this->portTester = new PortTester([
103 14
            'backend' => $curl_available ? PortTester::BACKEND_CURL : PortTester::BACKEND_STREAM_SOCKET,
104
            // Close timout ms could be adjusted for your system
105
            // It prevent that port availability testing does
106
            // not close quickly enough to allow standalone server binding
107 14
            'close_timeout_ms' => $curl_available ? null : 300
108 14
        ]);
109 14
    }
110
111
    /**
112
     * Start the standalone server
113
     *
114
     * @throws Exception\RuntimeException
115
     * @throws Exce
116
     *
117
     * @param int $timeout_ms maximum number of milliseconds to wait for server start
118
     * @return void
119
     */
120 11
    public function start($timeout_ms = 3000)
121
    {
122 11
        if ($this->isStarted()) {
123 1
            return;
124
        }
125
126 11
        $port = $this->getServerPort();
127
128 11
        if (!$this->portTester->isAvailable('localhost', $port, 'http')) {
129
            $msg = "Cannot start server on port '$port', it's already in use.";
130
            throw new Exception\RuntimeException($msg);
131
        }
132
133 11
        $command = $this->getCommand();
134
135 11
        $log_file = $this->config['log_file'];
136 11
        $pid_file = $this->config['pid_file'];
137 11
        $cmd = sprintf("%s > %s 2>&1 & echo $! > %s", $command, $log_file, $pid_file);
138
139 11
        exec($cmd);
140
141 11
        if (!file_exists($pid_file)) {
142
            $msg = "Server not started, pid file was not created in '$pid_file'";
143
            throw new Exception\RuntimeException($msg);
144
        }
145 11
        if (!file_exists($log_file)) {
146
            $msg = "Server not started, log file was not created in '$log_file'";
147
            throw new Exception\RuntimeException($msg);
148
        }
149
150
        // Loop for waiting correct start of phpjavabridge
151 11
        $started = false;
152 11
        $iterations = true;
153 11
        $refresh_us = 100 * 1000; // 100ms
154 11
        $timeout_us = $timeout_ms * 1000;
155 11
        $max_iterations = ceil($timeout_us / min([$refresh_us, $timeout_us]));
156
157 11
        while (!$started || $iterations > $max_iterations) {
158 11
            usleep($refresh_us);
159 11
            $log_file_content = file_get_contents($log_file);
160 11
            if (preg_match('/Exception/', $log_file_content)) {
161
                $msg = "Cannot start standalone server on port '$port', reason:\n";
162
                $msg .= $log_file_content;
163
                throw new Exception\RuntimeException($msg);
164
            }
165
166 11
            $log_file_content = file_get_contents($log_file);
167 11
            if (preg_match('/JavaBridgeRunner started on/', $log_file_content)) {
168 11
                $started = true;
169 11
            }
170 11
            $iterations++;
171 11
        }
172 11
        if (!$started) {
173
            $msg = "Standalone server probably not started, timeout '$timeout_ms' reached before getting output";
174
            throw new Exception\RuntimeException($msg);
175
        }
176 11
        $this->started = true;
177 11
    }
178
179
180
181
    /**
182
     * Stop the standalone server
183
     *
184
     * @throws Exception\RuntimeException
185
     * @param boolean $throws_exception wether to throw exception if pid or process cannot be found or killed.
186
     * @return void
187
     */
188 12
    public function stop($throws_exception = false)
189
    {
190 12
        $pid_file = $this->config['pid_file'];
191
        try {
192 12
            $pid = $this->getPid();
193 11
            $running = $this->isProcessRunning($throws_exception=true);
194 11
            if (!$running) {
195
                if ($throws_exception) {
196
                    $msg = "Cannot stop: pid exists ($pid) but server process is not running";
197
                    throw new Exception\RuntimeException($msg);
198
                }
199
                return;
200
            }
201 12
        } catch (Exception\PidNotFoundException $e) {
202 9
            if ($throws_exception) {
203 1
                $msg = "Cannot stop server, pid file not found (was the server started ?)";
204 1
                throw new Exception\RuntimeException($msg);
205
            }
206 9
            return;
207
        }
208
209
210
        //$cmd = "kill $pid";
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
211
        // Let sleep the process,
212
        // @todo: test sleep mith microseconds on different unix flavours
213 11
        $sleep_time = '0.2';
214 11
        $cmd = sprintf("kill %d; while ps -p %d; do sleep %s;done;", $pid, $pid, $sleep_time);
215
216 11
        exec($cmd, $output, $return_var);
217
        try {
218 11
            if ($return_var !== 0) {
219
                $msg = "Cannot kill standalone server process '$pid', seems to not exists.";
220
                throw new Exception\RuntimeException($msg);
221
            }
222 11
        } catch (Exception\RuntimeException $e) {
223
            if ($throws_exception) {
224
                if (file_exists($pid_file)) {
225
                    unlink($pid_file);
226
                }
227
                throw $e;
228
            }
229
        }
230
231 11
        if (file_exists($pid_file)) {
232 11
            unlink($pid_file);
233 11
        }
234
235 11
        $this->started = false;
236 11
    }
237
238
    /**
239
     * Tells whether the standalone server is started
240
     * @return boolean
241
     */
242 11
    public function isStarted()
243
    {
244 11
        return $this->started;
245
    }
246
247
    /**
248
     * Return command used to start the standalone server
249
     * @return string
250
     */
251 11
    public function getCommand()
252
    {
253 11
        $port = $this->getServerPort();
254
255 11
        $java_bin = $this->config['java_bin'];
256
257 11
        $jars = [];
258 11
        $autoload_path = $this->config['autoload_path'];
259 11
        $files = glob("$autoload_path/*.jar");
260 11
        foreach ($files as $file) {
261 11
            $jars[] = $file;
262 11
        }
263 11
        $jars[] = $this->config['server_jar'];
264
265 11
        $classpath = implode(':', $jars);
266
267 11
        $directives = ' -D' . implode(' -D', [
268 11
                    'php.java.bridge.daemon="false"',
269
                    'php.java.bridge.threads=30'
270 11
        ]);
271
272 11
        $command = "$java_bin -cp $classpath $directives php.java.bridge.Standalone SERVLET:$port";
273 11
        return $command;
274
    }
275
276
277
    /**
278
     * Get runnin standalone server pid number
279
     *
280
     * @throws Exception\PidNotFoundException
281
     * @return int
282
     */
283 12
    public function getPid()
284
    {
285 12
        $pid_file = $this->config['pid_file'];
286 12
        if (!file_exists($pid_file)) {
287 9
            $msg = "Pid file cannot be found '$pid_file'";
288 9
            throw new Exception\PidNotFoundException($msg);
289
        }
290 11
        $pid = trim(file_get_contents($pid_file));
291 11
        if (!preg_match('/^[0-9]+$/', $pid)) {
292 1
            $msg = "Pid found '$pid_file' but no valid pid stored in it or corrupted file '$pid_file'.";
293 1
            throw new Exception\PidCorruptedException($msg);
294
        }
295 11
        return (int) $pid;
296
    }
297
298
299
    /**
300
     * Return the content of the output_file
301
     * @throws \RuntimeException
302
     * @return string
303
     */
304 3
    public function getOutput()
305
    {
306 3
        $log_file = $this->config['log_file'];
307 3
        if (!file_exists($log_file)) {
308 1
            throw new Exception\RuntimeException("Server output log file does not exists '$log_file'");
309 2
        } elseif (!is_readable($log_file)) {
310 1
            throw new Exception\RuntimeException("Cannot read log file do to missing read permission '$log_file'");
311
        }
312 2
        $output = file_get_contents($log_file);
313 2
        return $output;
314
    }
315
316
317
    /**
318
     * Test whether the standalone server process
319
     * is effectively running
320
     *
321
     * @throws Exception\PidNotFoundException
322
     * @param $throwsException if false discard exception if pidfile not exists
323
     * @return boolean
324
     */
325 11
    public function isProcessRunning($throwsException = false)
326
    {
327 11
        $running = false;
328
        try {
329 11
            $pid = $this->getPid();
330 11
            $result = trim(shell_exec(sprintf("ps -j --no-headers -p %d", $pid)));
331 11
            if (preg_match("/^$pid/", $result)) {
332 11
                $running = true;
333 11
            }
334 11
        } catch (Exception\PidNotFoundException $e) {
335 1
            if ($throwsException) {
336
                throw $e;
337
            }
338
        }
339 11
        return $running;
340
    }
341
342
343
    /**
344
     * Restart the standalone server
345
     */
346 2
    public function restart()
347
    {
348 2
        $this->stop();
349 2
        $this->start();
350 2
    }
351
352
    /**
353
     * Return port on which standalone server listens
354
     * @return int
355
     */
356 12
    public function getServerPort()
357
    {
358 12
        return $this->port;
359
    }
360
361
    /**
362
     * Set port on which standalone server listens.
363
     *
364
     * @param int $port
365
     * @return void
366
     */
367 14
    protected function setServerPort($port)
368
    {
369 14
        $this->port = $port;
370 14
    }
371
372
    /**
373
     * Return standalone configuration
374
     *
375
     * @return array
376
     */
377 7
    public function getConfig()
378
    {
379 7
        return $this->config;
380
    }
381
382
    /**
383
     * Return default configuration options
384
     * @param int $port
385
     * @return array
386
     */
387 15
    protected function getDefaultConfig($port)
388
    {
389 15
        $base_dir = realpath(__DIR__ . '/../../../');
390 15
        $config = [];
391 15
        foreach ($this->default_config as $key => $value) {
392 15
            $tmp = str_replace('{base_dir}', $base_dir, $value);
393 15
            $tmp = str_replace('{tcp_port}', $port, $tmp);
394 15
            $config[$key] = $tmp;
395 15
        }
396 15
        return $config;
397
    }
398
399
    /**
400
     * Check configuration parameters
401
     * @throws Exception\InvalidArgumentException
402
     * @param array $config
403
     */
404 15
    protected function checkConfigRequiredArgs(array $config)
405
    {
406 15
        foreach ($this->required_arguments as $name => $type) {
407 15
            if (!isset($config[$name])) {
408
                $msg = "Missing option '$name' in Standalone server configuration";
409
                throw new Exception\InvalidArgumentException($msg);
410
            }
411 15
            if (is_long($type) && !filter_var($config[$name], $type)) {
412
                $msg = "Unsupported type in option '$name' in Standalone server configuration (required $type)";
413
                throw new Exception\InvalidArgumentException($msg);
414 15
            } elseif (is_string($type)) {
415
                switch ($type) {
416 15
                    case 'existing_file':
417 15
                        $file = $config[$name];
418 15
                        if (!file_exists($file)) {
419 1
                            $msg = "The '$name' file '$file 'does not exists.";
420 1
                            throw new Exception\InvalidArgumentException($msg);
421
                        }
422 14
                        break;
423 15
                    case 'existing_directory':
424 14
                        $directory = $config[$name];
425 14
                        if (!is_dir($directory)) {
426
                            $msg = "The '$name' directory '$directory 'does not exists.";
427
                            throw new Exception\InvalidArgumentException($msg);
428
                        }
429 14
                        break;
430
431 15
                    case 'file_with_existing_directory':
432 14
                        $file = $config[$name];
433 14
                        $info = pathinfo($file);
434 14
                        $dirname = $info['dirname'];
435 14
                        if (!is_dir($dirname) || $dirname == ".") {
436
                            $msg = "Option '$name' refer to an invalid or non-existent directory ($file)";
437
                            throw new Exception\InvalidArgumentException($msg);
438
                        }
439 14
                        if (is_dir($file)) {
440
                            $msg = "Option '$name' does not refer to a file but an existing directory ($file)";
441
                            throw new Exception\InvalidArgumentException($msg);
442
                        }
443 14
                        if (file_exists($file) && !is_writable($file)) {
444
                            $msg = "File specified in '$name' is not writable ($file)";
445
                            throw new Exception\InvalidArgumentException($msg);
446
                        }
447 14
                        break;
448
                }
449 15
            }
450 15
        }
451 14
    }
452
}
453