Issues (12)

src/PjbServer/Tools/StandaloneServer.php (1 issue)

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