Completed
Push — master ( e6b0f2...148d74 )
by Sébastien
03:16
created

StandaloneServer   B

Complexity

Total Complexity 39

Size/Duplication

Total Lines 345
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 81.53%

Importance

Changes 16
Bugs 1 Features 1
Metric Value
wmc 39
c 16
b 1
f 1
lcom 1
cbo 7
dl 0
loc 345
ccs 128
cts 157
cp 0.8153
rs 8.2857

10 Methods

Rating   Name   Duplication   Size   Complexity  
A isStarted() 0 4 1
A __construct() 0 18 4
C start() 0 69 10
C stop() 0 53 10
B getCommand() 0 24 2
A getPid() 0 16 3
A getOutput() 0 15 3
A isProcessRunning() 0 18 4
A restart() 0 5 1
A getConfig() 0 4 1
1
<?php
2
3
namespace PjbServer\Tools;
4
5
use PjbServer\Tools\Network\PortTester;
6
use Psr\Log\LoggerInterface;
7
use Psr\Log\NullLogger;
8
9
10
class StandaloneServer
11
{
12
13
    /**
14
     * @var int
15
     */
16
    protected $port;
17
18
    /**
19
     *
20
     * @var StandaloneServer\Config
21
     */
22
    protected $config;
23
24
    /**
25
     * Tells whether the standalone server is started
26
     * @var boolean
27
     */
28
    protected $started = false;
29
30
31
    /**
32
     *
33
     * @var PortTester
34
     */
35
    protected $portTester;
36
37
38
    /**
39
     * @var
40
     */
41
    protected $logger;
42
43
    /**
44
     * Constructor
45
     *
46
     * <code>
47
     *
48
     * $config = array(
49
     *      'port' => 8089,
50
     *
51
     *      // optionally
52
     *      'server_jar' => 'path/to/JavaBridge.jar'
53
     *      'java_bin' => 'path/to/java'
54
     * );
55
     * $server = new StandaloneServer($config);
56
     *
57
     * </code>
58
     *
59
     * @throws Exception\InvalidArgumentException
60
     * @param StandaloneServer\Config $config
61
     * @param LoggerInterface $logger
62
     *
63
     */
64 13
    public function __construct(StandaloneServer\Config $config, LoggerInterface $logger=null)
65
    {
66 13
        $this->config = $config;
67
68 13
        $curl_available = function_exists('curl_version');
69
70 13
        $this->portTester = new PortTester([
71 13
            'backend' => $curl_available ? PortTester::BACKEND_CURL : PortTester::BACKEND_STREAM_SOCKET,
72
            // Close timout ms could be adjusted for your system
73
            // It prevent that port availability testing does
74
            // not close quickly enough to allow standalone server binding
75 13
            'close_timeout_ms' => $curl_available ? null : 300
76 13
        ]);
77 13
        if ($logger === null) {
78 13
            $logger = new NullLogger();
79 13
        }
80 13
        $this->logger = $logger;
81 13
    }
82
83
    /**
84
     * Start the standalone server
85
     *
86
     * @throws Exception\RuntimeException
87
     *
88
     * @param int $timeout_ms maximum number of milliseconds to wait for server start
89
     * @return void
90
     */
91 11
    public function start($timeout_ms = 3000)
92
    {
93 11
        $port = $this->config->getPort();
94
95 11
        $this->logger->info("Starting standalone server on port $port.");
96
97 11
        if ($this->isStarted()) {
98 1
            $this->logger->info("Standalone server already running, skipping start.");
99 1
            return;
100
        }
101
102
103
104 11
        if (!$this->portTester->isAvailable('localhost', $port, 'http')) {
105
            $msg = "Cannot start server on port '$port', it's already in use.";
106 1
            $this->logger->error("Start failed: $msg");
107
            throw new Exception\RuntimeException($msg);
108
        }
109
110 11
        $command = $this->getCommand();
111
112 11
        $log_file = $this->config->getLogFile();
113 11
        $pid_file = $this->config->getPidFile();
114 11
        $cmd = sprintf("%s > %s 2>&1 & echo $! > %s", $command, $log_file, $pid_file);
115
116 11
        $this->logger->debug("Start server with: $cmd");
117 11
        exec($cmd);
118
119 11
        if (!file_exists($pid_file)) {
120
            $msg = "Server not started, pid file was not created in '$pid_file'";
121
            $this->logger->error("Start failed: $msg");
122
            throw new Exception\RuntimeException($msg);
123
        }
124 11
        if (!file_exists($log_file)) {
125
            $msg = "Server not started, log file was not created in '$log_file'";
126
            $this->logger->error("Start failed: $msg");
127
            throw new Exception\RuntimeException($msg);
128
        }
129
130
        // Loop for waiting correct start of phpjavabridge
131 11
        $started = false;
132 11
        $iterations = true;
133 11
        $refresh_us = 100 * 1000; // 100ms
134 11
        $timeout_us = $timeout_ms * 1000;
135 11
        $max_iterations = ceil($timeout_us / min([$refresh_us, $timeout_us]));
136
137 11
        while (!$started || $iterations > $max_iterations) {
138 11
            usleep($refresh_us);
139 11
            $log_file_content = file_get_contents($log_file);
140 11
            if (preg_match('/Exception/', $log_file_content)) {
141
                $msg = "Cannot start standalone server on port '$port', reason:\n";
142
                $msg .= $log_file_content;
143
                $this->logger->error("Start failed: $msg");
144
                throw new Exception\RuntimeException($msg);
145
            }
146
147 11
            $log_file_content = file_get_contents($log_file);
148 11
            if (preg_match('/JavaBridgeRunner started on/', $log_file_content)) {
149 11
                $started = true;
150 11
            }
151 11
            $iterations++;
152 11
        }
153 11
        if (!$started) {
154
            $msg = "Standalone server probably not started, timeout '$timeout_ms' reached before getting output";
155
            $this->logger->error("Start failed: $msg");
156
            throw new Exception\RuntimeException($msg);
157
        }
158 11
        $this->started = true;
159 11
    }
160
161
162
163
    /**
164
     * Stop the standalone server
165
     *
166
     * @throws Exception\RuntimeException
167
     * @param boolean $throws_exception wether to throw exception if pid or process cannot be found or killed.
168
     * @return void
169
     */
170 13
    public function stop($throws_exception = false)
171
    {
172 13
        $this->logger->info("Stopping server");
173 13
        $pid_file = $this->config->getPidFile();
174
        try {
175 13
            $pid = $this->getPid();
176 11
            $running = $this->isProcessRunning($throws_exception=true);
177 11
            if (!$running) {
178
                if ($throws_exception) {
179
                    $msg = "Cannot stop: pid exists ($pid) but server process is not running";
180
                    $this->logger->info("Stop failed: $msg");
181
                    throw new Exception\RuntimeException($msg);
182
                }
183
                return;
184
            }
185 13
        } catch (Exception\PidNotFoundException $e) {
186 10
            if ($throws_exception) {
187 1
                $msg = "Cannot stop server, pid file not found (was the server started ?)";
188 1
                $this->logger->info("Stop failed: $msg");
189 1
                throw new Exception\RuntimeException($msg);
190
            }
191 10
            return;
192
        }
193
194
195
        //$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...
196
        // Let sleep the process,
197
        // @todo: test sleep mith microseconds on different unix flavours
198 11
        $sleep_time = '0.2';
199 11
        $cmd = sprintf("kill %d; while ps -p %d; do sleep %s;done;", $pid, $pid, $sleep_time);
200
201 11
        exec($cmd, $output, $return_var);
202
        try {
203 11
            if ($return_var !== 0) {
204
                $msg = "Cannot kill standalone server process '$pid', seems to not exists.";
205
                $this->logger->info("Stop failed: $msg");
206
                throw new Exception\RuntimeException($msg);
207
            }
208 11
        } 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 11
        if (file_exists($pid_file)) {
218 11
            unlink($pid_file);
219 11
        }
220
221 11
        $this->started = false;
222 11
    }
223
224
    /**
225
     * Tells whether the standalone server is started
226
     * @return boolean
227
     */
228 11
    public function isStarted()
229
    {
230 11
        return $this->started;
231
    }
232
233
    /**
234
     * Return command used to start the standalone server
235
     * @return string
236
     */
237 11
    public function getCommand()
238
    {
239 11
        $port = $this->config->getPort();
240
241 11
        $java_bin = $this->config->getJavaBin();
242
243 11
        $jars = [];
244 11
        $classpaths = $this->config->getClasspaths();
245 11
        foreach ($classpaths as $classpath) {
0 ignored issues
show
Bug introduced by
The expression $classpaths of type string is not traversable.
Loading history...
246 11
            $jars[] = $classpath;
247 11
        }
248 11
        $jars[] = $this->config->getServerJar();
249 11
        $classpath = implode(':', $jars);
250
251 11
        $directives = ' -D' . implode(' -D', [
252 11
                    'php.java.bridge.daemon="false"',
253
                    'php.java.bridge.threads=30'
254 11
        ]);
255
256 11
        $command = sprintf('%s -cp "%s" %s php.java.bridge.Standalone SERVLET:%d',
257 11
                            $java_bin, $classpath, $directives, $port);
258
259 11
        return $command;
260
    }
261
262
263
    /**
264
     * Get standalone server pid number as it was stored during last start
265
     *
266
     * @throws Exception\PidNotFoundException
267
     * @return int
268
     */
269 13
    public function getPid()
270
    {
271 13
        $pid_file = $this->config->getPidFile();
272 13
        if (!file_exists($pid_file)) {
273 10
            $msg = "Pid file cannot be found '$pid_file'";
274 10
            $this->logger->error("Get PID failed: $msg");
275 10
            throw new Exception\PidNotFoundException($msg);
276
        }
277 11
        $pid = trim(file_get_contents($pid_file));
278 11
        if (!preg_match('/^[0-9]+$/', $pid)) {
279 1
            $msg = "Pid found '$pid_file' but no valid pid stored in it or corrupted file '$pid_file'.";
280 1
            $this->logger->error("Get PID failed: $msg");
281 1
            throw new Exception\PidCorruptedException($msg);
282
        }
283 11
        return (int) $pid;
284
    }
285
286
287
    /**
288
     * Return the content of the output_file
289
     * @throws Exception\RuntimeException
290
     * @return string
291
     */
292 3
    public function getOutput()
293
    {
294 3
        $log_file = $this->config->getLogFile();
295 3
        if (!file_exists($log_file)) {
296 1
            $msg = "Server output log file does not exists '$log_file'";
297 1
            $this->logger->error("Get server output failed: $msg");
298 1
            throw new Exception\RuntimeException($msg);
299 2
        } elseif (!is_readable($log_file)) {
300 1
            $msg = "Cannot read log file do to missing read permission '$log_file'";
301 1
            $this->logger->error("Get server output failed: $msg");
302 1
            throw new Exception\RuntimeException($msg);
303
        }
304 2
        $output = file_get_contents($log_file);
305 2
        return $output;
306
    }
307
308
309
    /**
310
     * Test whether the standalone server process
311
     * is effectively running
312
     *
313
     * @throws Exception\PidNotFoundException
314
     * @param boolean $throwsException if false discard exception if pidfile not exists
315
     * @return boolean
316
     */
317 11
    public function isProcessRunning($throwsException = false)
318
    {
319 11
        $running = false;
320
        try {
321 11
            $pid = $this->getPid();
322 11
            $cmd = sprintf("ps -j --no-headers -p %d", $pid);
323 11
            $this->logger->debug("Getting process with cmd: $cmd");
324 11
            $result = trim(shell_exec($cmd));
325 11
            if (preg_match("/^$pid/", $result)) {
326 11
                $running = true;
327 11
            }
328 11
        } catch (Exception\PidNotFoundException $e) {
329 1
            if ($throwsException) {
330
                throw $e;
331
            }
332
        }
333 11
        return $running;
334
    }
335
336
337
    /**
338
     * Restart the standalone server
339
     */
340 2
    public function restart()
341
    {
342 2
        $this->stop();
343 2
        $this->start();
344 2
    }
345
346
    /**
347
     * Return underlying configuration object
348
     * @return StandaloneServer\Config
349
     */
350 7
    public function getConfig()
351
    {
352 7
        return $this->config;
353
    }
354
}
355