Completed
Pull Request — master (#26)
by Evstati
12:11 queued 09:24
created

Driver_Phantomjs_Connection::is_running()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 1
cts 1
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Openbuildings\Spiderling;
4
5
/**
6
 * Connect to phantomjs service, optionally start one if not present on a new port.
7
 * Send requests to phantomjs
8
 *
9
 * @package    Openbuildings\Spiderling
10
 * @author     Ivan Kerin
11
 * @copyright  (c) 2013 OpenBuildings Ltd.
12
 * @license    http://spdx.org/licenses/BSD-3-Clause
13
 */
14
class Driver_Phantomjs_Connection {
15
16
	/**
17
	 * The file storing the pid of the current phantomjs server process
18
	 * @var string
19
	 */
20
	protected $_pid_file;
21
22
	/**
23
	 * The pid of the current phantomjs server process
24
	 * @var string
25
	 */
26
	protected $_pid;
27
28
	/**
29
	 * Ulr of the phantomjs server
30
	 * @var string
31
	 */
32
	protected $_server = 'http://localhost';
33
34
	/**
35
	 * Port of the phantomjs server
36
	 * @var string
37
	 */
38
	protected $_port;
39
40
	/**
41
	 * Phantomjs binary
42
	 * @var string
43
	 */
44
	protected $_phantomjs_binary = 'phantomjs';
45
46 15
	/**
47
	 * Getter / Setter of the phantomjs server port.
48 15
	 * If none is set it tries to fine an unused port between 4445 and 5000
49 15
	 * @param  string $port
50 2
	 * @return string|Driver_Phantomjs_Connection
51 2
	 */
52
	public function port($port = NULL)
53
	{
54 15
		if ($port !== NULL)
55 15
		{
56 2
			$this->_port = $port;
57 2
			return $this;
58
		}
59 15
60
		if ( ! $this->_port)
61
		{
62
			$this->_port = Network::ephimeral_port($this->host(), 4445, 5000);
0 ignored issues
show
Security Bug introduced by
It seems like $this->host() targeting Openbuildings\Spiderling...omjs_Connection::host() can also be of type false; however, Openbuildings\Spiderling\Network::ephimeral_port() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
Documentation Bug introduced by
It seems like \Openbuildings\Spiderlin...is->host(), 4445, 5000) of type integer or boolean is incompatible with the declared type string of property $_port.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
63
		}
64
65
		return $this->_port;
66
	}
67 15
68
	/**
69 15
	 * Getter / Setter of the current phantomjs server url
70 15
	 * @param  string $server
71 2
	 * @return string|Driver_Phantomjs_Connection
72 2
	 */
73
	public function server($server = NULL)
74 15
	{
75
		if ($server !== NULL)
76
		{
77
			$this->_server = $server;
78
			return $this;
79
		}
80
		return $this->_server;
81 2
	}
82
83 2
	/**
84
	 * Get the host of the current phantomjs server (without the protocol part)
85
	 * @return string
86
	 */
87
	public function host()
88
	{
89
		return parse_url($this->server(), PHP_URL_HOST);
90 1
	}
91
92 1
	/**
93
	 * Getter, get the current phantomjs server process pid
94
	 * @return string
95 3
	 */
96
	public function pid()
97
	{
98 3
		return $this->_pid;
99 2
	}
100 2
101 3
	public function __construct($server = NULL)
102
	{
103
		if ($server)
104
		{
105
			$this->server($server);
106
		}
107
	}
108
109
	/**
110
	 * Start a new phantomjs server, optionally provide pid_file and log file.
111 2
	 * If you provide a pid_file, it will kill the process currently running on that pid, before starting the new one
112
	 * If the start is unsuccessfull it will return FALSE
113
	 * @param  string $pid_file
114 2
	 * @param  string $log_file
115 1
	 * @return boolean
116 1
	 */
117 1
	public function start($pid_file = NULL, $log_file = '/dev/null')
118 1
	{
119 1
		if ($pid_file)
0 ignored issues
show
Bug Best Practice introduced by
The expression $pid_file of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
120 1
		{
121 1
			$this->_pid_file = $pid_file;
122
			if (is_file($this->_pid_file))
123 2
			{
124
				$this->_kill(file_get_contents($pid_file));
125 2
				unlink($this->_pid_file);
126 2
			}
127 1
		}
128 1
129
		$this->_pid = $this->_start('phantom.js', $this->port(), 'phantomjs-connection.js', $log_file);
130 2
131
		if ($this->_pid_file)
132
		{
133 2
			file_put_contents($this->_pid_file, $this->_pid);
134 2
		}
135
136
		$self = $this;
137
138
		return Attempt::make(function() use ($self) {
139
			return $self->is_running();
140
		});
141 2
	}
142
143 2
	/**
144
	 * Check if the phantomjs server has been started
145
	 * @return boolean
146
	 */
147
	public function is_started()
148
	{
149
		return (bool) $this->_pid;
150 2
	}
151
152 2
	/**
153
	 * Check if the phantomjs server is actually running (the port is taken)
154
	 * @return boolean
155
	 */
156
	public function is_running()
157
	{
158
		return ! Network::is_port_open($this->host(), $this->port());
0 ignored issues
show
Security Bug introduced by
It seems like $this->host() targeting Openbuildings\Spiderling...omjs_Connection::host() can also be of type false; however, Openbuildings\Spiderling\Network::is_port_open() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
159 2
	}
160
161 2
	/**
162 2
	 * Gracefully stop the phantomjs server. Return FALSE on failure. Clear the pid_file if set
163 2
	 * @return boolean
164 2
	 */
165 2
	public function stop()
166
	{
167 2
		if ($this->is_started())
168 2
		{
169 1
			$this->delete('session');
170 1
			$this->_pid = NULL;
171 2
		}
172
173
		if ($this->_pid_file AND is_file($this->_pid_file))
174 2
		{
175 2
			unlink($this->_pid_file);
176
		}
177
		$self = $this;
178
179
		return Attempt::make(function() use ($self) {
180
			return ! $self->is_running();
181
		});
182 1
	}
183
184 1
	/**
185
	 * Getter - get the current pid_file
186
	 * @return string
187
	 */
188
	public function pid_file()
189
	{
190
		return $this->_pid_file;
191
	}
192 10
193
	/**
194 10
	 * Perform a get request on the phantomjs server
195
	 * @param  string $command
196
	 * @return mixed
197
	 */
198
	public function get($command)
199
	{
200
		return $this->call($command);
201
	}
202
203 10
	/**
204
	 * Perform a post request on the phantomjs server
205 10
	 * @param  string $command
206 10
	 * @param  array  $params
207 10
	 * @return mixed
208
	 */
209 10
	public function post($command, array $params)
210
	{
211
		$options = array();
212
		$options[CURLOPT_POST] = TRUE;
213
		$options[CURLOPT_POSTFIELDS] = http_build_query($params);
214
215
		return $this->call($command, $options);
216
	}
217 3
218
	/**
219 3
	 * Perform a delete request on the phantomjs server
220 3
	 * @param  string $command
221
	 * @return mixed
222 3
	 */
223
	public function delete($command)
224
	{
225
		$options = array();
226
		$options[CURLOPT_CUSTOMREQUEST] = 'DELETE';
227
228
		return $this->call($command, $options);
229
	}
230 15
231
	/**
232 15
	 * Get the full url of a command (including server and port)
233
	 * @param  string $command
234
	 * @return string
235
	 */
236
	public function command_url($command)
237
	{
238
		return rtrim($this->server(), '/').':'.$this->port().'/'.$command;
239
	}
240
241 14
	/**
242
	 * Set phantomjs binary location
243 14
	 * @param string $binary
244 14
	 * @throws Exception
245 14
	 */
246 14
	public function set_phantomjs_binary($binary)
247
	{
248 14
		$this->_phantomjs_binary = $binary;
249
	}
250 14
251
	/**
252 14
	 * Perform a custom request on the phantomjs server, using curl
253 14
	 * @param  string $command
254 14
	 * @param  array  $options
255 14
	 * @return mixed
256
	 */
257 14
	protected function call($command, array $options = array())
258 14
	{
259
		$curl = curl_init();
260 14
		$options[CURLOPT_URL] = $this->command_url($command);
261
		$options[CURLOPT_RETURNTRANSFER] = TRUE;
262
		$options[CURLOPT_FOLLOWLOCATION] = TRUE;
263 14
264
		curl_setopt_array($curl, $options);
265 14
266 14
		$raw = '';
267
268 14
		Attempt::make(function() use ($curl, & $raw) {
269
			$raw = trim(curl_exec($curl));
270 14
			return curl_getinfo($curl, CURLINFO_HTTP_CODE) == 200;
271
		});
272
273
		$error = curl_error($curl);
274
		$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
275
276
		curl_close($curl);
277
278
		if ($error)
279
			throw new Exception_Driver('Curl ":command" throws exception :error', array(':command' => $command, ':error' => $error));
280
281
		if ($code != 200)
282
			throw new Exception_Driver('Unexpected response from the panthomjs for :command: :code', array(':command' => $command, ':code' => $code));
283
284
		$result = json_decode($raw, TRUE);
285
286
		return $result;
287
	}
288
289
	/**
290
	 * Start a phantomjs server in the background. Set port, server js file, additional files and log file.
291
	 *
292
	 * @param  string  $file       the server js file
293
	 * @param  integer $port       the port to start the server on
294
	 * @param  string  $additional additional file, passed to the js server
295
	 * @param  string  $log_file
296
	 * @return string the pid of the newly started process
297
	 * @throws Exception
298
	 */
299
	protected function _start($file, $port, $additional = NULL, $log_file = '/dev/null')
300
	{
301
		if ( ! Network::is_port_open('localhost', $port)) {
302
			throw new Exception('Port :port is already taken', [':port' => $port]);
303
		}
304
305
		if ($log_file !== '/dev/null' AND ! is_file($log_file)) {
306
			throw new Exception('Log file (:log_file) must be a file or /dev/null', [':log_file' => $log_file]);
307
		}
308
309
		return shell_exec(strtr('nohup :command > :log 2> :log & echo $!', array(
310
			':command' => $this->_command($file, $port, $additional),
311
			':log' => $log_file,
312
		)));
313
	}
314
315
	/**
316
	 * kill a server on a given pid
317
	 * @param  string $pid
318
	 */
319
	protected function _kill($pid)
320
	{
321
		shell_exec('kill '.$pid);
322
	}
323
324
	/**
325
	 * Return the command to start the phantomjs server
326
	 *
327
	 * @param  string  $file       the server js file
328
	 * @param  integer $port
329
	 * @param  string  $additional additional js file
330
	 * @return string
331
	 * @throws Exception
332
	 */
333
	protected function _command($file, $port, $additional = NULL)
334
	{
335
		$dir = realpath(__DIR__.'/../../../../../assets').'/';
336
337
		$file = $dir.$file;
338
339
		if ( ! is_file($file)) {
340
			throw new Exception('Cannot start phantomjs: file :file is not found', [':file' => $file]);
341
		}
342
		if ($additional)
0 ignored issues
show
Bug Best Practice introduced by
The expression $additional of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
343
		{
344
			if ( ! is_file($file)) {
345
				throw new Exception(
346
					'Cannot start phantomjs: file :additional is not found',
347
					[':additional' => $additional]
348
				);
349
			}
350
			$additional = $dir.$additional;
351
		}
352
353
		return $this->_phantomjs_binary." --ssl-protocol=any --ignore-ssl-errors=true {$file} {$port} {$additional}";
354
	}
355
}
356