Completed
Push — master ( db5913...083ff9 )
by Sébastien
02:23
created

StandaloneServer::isPortAvailable()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2.0078

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 12
ccs 7
cts 8
cp 0.875
rs 9.4286
cc 2
eloc 8
nc 2
nop 3
crap 2.0078
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 = array(
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
    );
48
49
    /**
50
     * Default configuration options
51
     * @var array
52
     */
53
    protected $default_config = array(
54
        'server_jar' => '{base_dir}/resources/pjb621_standalone/JavaBridge.jar',
55
        'java_bin' => 'java',
56
        'log_file' => '{base_dir}/resources/pjb621_standalone/logs/pjbserver-port{tcp_port}.log',
57
        'pid_file' => '{base_dir}/resources/pjb621_standalone/var/run/pjbserver-port{tcp_port}.pid'
58
    );
59
60
    /**
61
     *
62
     * @var PortTester
63
     */
64
    protected $portTester;
65
66
67
    /**
68
     * Constructor
69
     *
70
     * <code>
71
     *
72
     * $config = array(
73
     *      'port' => 8089,
74
     *
75
     *      // optionally
76
     *      'server_jar' => 'path/to/JavaBridge.jar'
77
     *      'java_bin' => 'path/to/java'
78
     * );
79
     * $server = new StandaloneServer($config);
80
     *
81
     * </code>
82
     *
83
     * @throws Exception\InvalidArgumentException
84
     * @param array $config
85
     */
86 10
    public function __construct(array $config)
87
    {
88 10
        if (!isset($config['port'])) {
89 3
            throw new Exception\InvalidArgumentException("Error missing required 'port' in config");
90 7
        } elseif (!filter_var($config['port'], FILTER_VALIDATE_INT)) {
91 1
            throw new Exception\InvalidArgumentException("Option 'port' must be numeric");
92
        }
93 6
        $config = array_merge($this->getDefaultConfig($config['port']), $config);
94 6
        $this->checkConfigRequiredArgs($config);
95 5
        $this->setServerPort($config['port']);
96 5
        $this->config = $config;
97
98 5
        $this->portTester = new PortTester(array(
99 5
            'backend' => PortTester::BACKEND_CURL,
100
            // Close timout ms could be adjusted for your system
101
            // It prevent that port availability testing does
102
            // not close quickly enough to allow standalone server binding
103
            'close_timeout_ms' => null
104 5
        ));
105 5
    }
106
107
    /**
108
     * Start the standalone server
109
     *
110
     * @throws Exception\RuntimeException
111
     * @throws Exce
112
     *
113
     * @param int $timeout_ms maximum number of milliseconds to wait for server start
114
     * @return void
115
     */
116 3
    public function start($timeout_ms = 3000)
117
    {
118
119 3
        if ($this->isStarted()) {
120
            return;
121
        }
122
123 3
        $port = $this->getServerPort();
124
125 3
        if (!$this->portTester->isAvailable('localhost', $port, 'http')) {
126
            $msg = "Cannot start server on port '$port', it's already in use.";
127
            throw new Exception\RuntimeException($msg);
128
        }
129
130 3
        $command = $this->getCommand();
131
132 3
        $log_file = $this->config['log_file'];
133 3
        $pid_file = $this->config['pid_file'];
134 3
        $cmd = sprintf("%s > %s 2>&1 & echo $! > %s", $command, $log_file, $pid_file);
135
136 3
        exec($cmd);
137
138 3
        if (!file_exists($pid_file)) {
139
            $msg = "Server not started, pid file was not created in '$pid_file'";
140
            throw new Exception\RuntimeException($msg);
141
        }
142 3
        if (!file_exists($log_file)) {
143
            $msg = "Server not started, log file was not created in '$log_file'";
144
            throw new Exception\RuntimeException($msg);
145
        }
146
147
        // Loop for waiting correct start of phpjavabridge
148 3
        $started = false;
149 3
        $iterations = true;
150 3
        $refresh_us = 100 * 1000; // 200ms
151 3
        $timeout_us = $timeout_ms * 1000;
152 3
        $max_iterations = ceil($timeout_us / min(array($refresh_us, $timeout_us)));
153
154 3
        while (!$started || $iterations > $max_iterations) {
155 3
            usleep($refresh_us);
156 3
            $log_file_content = file_get_contents($log_file);
157 3
            if (preg_match('/Exception/', $log_file_content)) {
158
                $msg = "Cannot start standalone server on port '$port', reason:\n";
159
                $msg .= $log_file_content;
160
                throw new Exception\RuntimeException($msg);
161
            }
162
163 3
            clearstatcache($clear_realpath_cache = false, $log_file);
164 3
            $log_file_content = file_get_contents($log_file);
165 3
            if (preg_match('/JavaBridgeRunner started on/', $log_file_content)) {
166 3
                $started = true;
167 3
            }
168 3
            $iterations++;
169 3
        }
170 3
        if (!$started) {
171
            $msg = "Standalone server probably not started, timeout '$timeout_ms' reached before getting output";
172
            throw new Exception\RuntimeException($msg);
173
        }
174 3
        $this->started = true;
175 3
    }
176
177
178
179
    /**
180
     * Stop the standalone server
181
     *
182
     * @throws Exception\RuntimeException
183
     * @param boolean $throws_exception wether to throw exception if pid or process cannot be found or killed.
184
     * @return void
185
     */
186 3
    public function stop($throws_exception = false)
187
    {
188 3
        $pid_file = $this->config['pid_file'];
189
190
        try {
191 3
            $pid = $this->getPid();
192 3
        } catch (Exception\RuntimeException $e) {
193
            $msg = "Cannot stop server, pid cannot be determined (was the server started ?)";
194
            if ($throws_exception) {
195
                throw new Exception\RuntimeException($msg);
196
            }
197
        }
198
199 3
        $cmd = "kill $pid";
200
201 3
        exec($cmd, $output, $return_var);
202
203
        try {
204 3
            if ($return_var !== 0) {
205
                $msg = "Cannot kill standalone server process '$pid', seems to not exists.";
206
                throw new Exception\RuntimeException($msg);
207
            }
208 3
        } catch (Exception\RuntimeException $e) {
209
            if ($throws_exception) {
210
                if (file_exists($pid_file)) {
211
                    unlink($pid_file);
212
                }
213
                throw $e;
214
            }
215
        }
216
217 3
        if (file_exists($pid_file)) {
218 3
            unlink($pid_file);
219 3
        }
220
221 3
        $this->started = false;
222 3
    }
223
224
    /**
225
     * Tells whether the standalone server is started
226
     * @return boolean
227
     */
228 3
    public function isStarted()
229
    {
230 3
        return $this->started;
231
    }
232
233
    /**
234
     * Return command used to start the standalone server
235
     * @return string
236
     */
237 3
    public function getCommand()
238
    {
239 3
        $port = $this->getServerPort();
240
241 3
        $java_bin = $this->config['java_bin'];
242
243 3
        $classpath = join(':', array(
244 3
            $this->config['server_jar'],
245 3
        ));
246
247 3
        $directives = ' -D' . join(' -D', array(
248 3
                    'php.java.bridge.daemon="false"',
249
                    'php.java.bridge.threads=50'
250 3
        ));
251
252 3
        $command = "$java_bin -cp $classpath $directives php.java.bridge.Standalone SERVLET:$port";
253 3
        return $command;
254
    }
255
256
257
    /**
258
     * Get runnin standalone server pid number
259
     *
260
     * @throws Exception\RuntimeException
261
     * @return int
262
     */
263 3
    public function getPid()
264
    {
265 3
        $pid_file = $this->config['pid_file'];
266 3
        if (!file_exists($pid_file)) {
267
            $msg = "Pid file cannot be found '$pid_file'";
268
            throw new Exception\RuntimeException($msg);
269
        }
270 3
        $pid = trim(file_get_contents($pid_file));
271 3
        if (!is_numeric($pid)) {
272
            $msg = "Pid found '$pid_file' but no valid pid stored in it.";
273
            throw new Exception\RuntimeException($msg);
274
        }
275 3
        return $pid;
276
    }
277
278
    /**
279
     * Restart the standalone server
280
     */
281
    public function restart()
282
    {
283
        $this->stop();
284
        $this->start();
285
    }
286
287
    /**
288
     * Return port on which standalone server listens
289
     * @return int
290
     */
291 4
    public function getServerPort()
292
    {
293 4
        return $this->port;
294
    }
295
296
    /**
297
     * Set port on which standalone server listens.
298
     *
299
     * @param int $port
300
     * @return void
301
     */
302 5
    protected function setServerPort($port)
303
    {
304 5
        $this->port = $port;
305 5
    }
306
307
    /**
308
     * Return standalone configuration
309
     *
310
     * @return array
311
     */
312 3
    public function getConfig()
313
    {
314 3
        return $this->config;
315
    }
316
317
    /**
318
     * Return default configuration options
319
     * @param int $port
320
     * @return array
321
     */
322 6
    protected function getDefaultConfig($port)
323
    {
324 6
        $base_dir = realpath(__DIR__ . '/../../../');
325 6
        $config = array();
326 6
        foreach ($this->default_config as $key => $value) {
327 6
            $tmp = str_replace('{base_dir}', $base_dir, $value);
328 6
            $tmp = str_replace('{tcp_port}', $port, $tmp);
329 6
            $config[$key] = $tmp;
330 6
        }
331 6
        return $config;
332
    }
333
334
    /**
335
     * Check configuration parameters
336
     * @throws Exception\InvalidArgumentException
337
     * @param array $config
338
     */
339 6
    protected function checkConfigRequiredArgs(array $config)
340
    {
341 6
        foreach ($this->required_arguments as $name => $type) {
342 6
            if (!isset($config[$name])) {
343
                $msg = "Missing option '$name' in Standalone server configuration";
344
                throw new Exception\InvalidArgumentException($msg);
345
            }
346 6
            if (is_long($type) && !filter_var($config[$name], $type)) {
347
                $msg = "Unsupported type in option '$name' in Standalone server configuration (required $type)";
348
                throw new Exception\InvalidArgumentException($msg);
349 6
            } elseif (is_string($type)) {
350
                switch ($type) {
351 6
                    case 'existing_file':
352 6
                        $file = $config[$name];
353 6
                        if (!file_exists($file)) {
354 1
                            $msg = "The '$name' file '$file 'does not exists.";
355 1
                            throw new Exception\InvalidArgumentException($msg);
356
                        }
357 5
                        break;
358 6
                    case 'file_with_existing_directory':
359 5
                        $file = $config[$name];
360 5
                        $info = pathinfo($file);
361 5
                        $dirname = $info['dirname'];
362 5
                        if (!is_dir($dirname) || $dirname == ".") {
363
                            $msg = "Option '$name' refer to an invalid or non-existent directory ($file)";
364
                            throw new Exception\InvalidArgumentException($msg);
365
                        }
366 5
                        if (is_dir($file)) {
367
                            $msg = "Option '$name' does not refer to a file but an existing directory ($file)";
368
                            throw new Exception\InvalidArgumentException($msg);
369
                        }
370 5
                        if (file_exists($file) && !is_writable($file)) {
371
                            $msg = "File specified in '$name' is not writable ($file)";
372
                            throw new Exception\InvalidArgumentException($msg);
373
                        }
374 5
                        break;
375
                }
376 6
            }
377 6
        }
378 5
    }
379
}
380