Completed
Pull Request — master (#26)
by Evstati
02:33
created

Driver_Phantomjs_Connection::_start()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.1755

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 7
cts 9
cp 0.7778
rs 9.2
c 0
b 0
f 0
cc 4
eloc 8
nc 3
nop 4
crap 4.1755
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
	/**
47
	 * Getter / Setter of the phantomjs server port.
48
	 * If none is set it tries to fine an unused port between 4445 and 5000
49
	 * @param  string $port
50
	 * @return string|Driver_Phantomjs_Connection
51
	 */
52 2
	public function port($port = NULL)
53
	{
54 2
		if ($port !== NULL)
55 2
		{
56 2
			$this->_port = $port;
57 2
			return $this;
58
		}
59
60 2
		if ( ! $this->_port)
61 2
		{
62 1
			$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 1
		}
64
65 2
		return $this->_port;
66
	}
67
68
	/**
69
	 * Getter / Setter of the current phantomjs server url
70
	 * @param  string $server
71
	 * @return string|Driver_Phantomjs_Connection
72
	 */
73 2
	public function server($server = NULL)
74
	{
75 2
		if ($server !== NULL)
76 2
		{
77 2
			$this->_server = $server;
78 2
			return $this;
79
		}
80 2
		return $this->_server;
81
	}
82
83
	/**
84
	 * Get the host of the current phantomjs server (without the protocol part)
85
	 * @return string
86
	 */
87 1
	public function host()
88
	{
89 1
		return parse_url($this->server(), PHP_URL_HOST);
90
	}
91
92
	/**
93
	 * Getter, get the current phantomjs server process pid
94
	 * @return string
95
	 */
96
	public function pid()
97
	{
98
		return $this->_pid;
99
	}
100
101 2
	public function __construct($server = NULL)
102
	{
103
		if ($server)
104 2
		{
105 2
			$this->server($server);
106 2
		}
107 2
	}
108
109
	/**
110
	 * Start a new phantomjs server, optionally provide pid_file and log file.
111
	 * 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
	 * @param  string $log_file
115
	 * @return boolean
116
	 */
117 1
	public function start($pid_file = NULL, $log_file = '/dev/null')
118
	{
119
		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 1
			if (is_file($this->_pid_file))
123 1
			{
124 1
				$this->_kill(file_get_contents($pid_file));
125 1
				unlink($this->_pid_file);
126 1
			}
127 1
		}
128
129 1
		$this->_pid = $this->_start('phantom.js', $this->port(), 'phantomjs-connection.js', $log_file);
130
131 1
		if ($this->_pid_file)
132 1
		{
133 1
			file_put_contents($this->_pid_file, $this->_pid);
134 1
		}
135
136 1
		$self = $this;
137
138
		return Attempt::make(function() use ($self) {
139 1
			return $self->is_running();
140 1
		});
141
	}
142
143
	/**
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
	}
151
152
	/**
153
	 * Check if the phantomjs server is actually running (the port is taken)
154
	 * @return boolean
155
	 */
156 1
	public function is_running()
157
	{
158 1
		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
	}
160
161
	/**
162
	 * Gracefully stop the phantomjs server. Return FALSE on failure. Clear the pid_file if set
163
	 * @return boolean
164
	 */
165
	public function stop()
166
	{
167
		if ($this->is_started())
168
		{
169
			$this->delete('session');
170
			$this->_pid = NULL;
171
		}
172
173
		if ($this->_pid_file AND is_file($this->_pid_file))
174
		{
175
			unlink($this->_pid_file);
176
		}
177
		$self = $this;
178
179
		return Attempt::make(function() use ($self) {
180
			return ! $self->is_running();
181
		});
182
	}
183
184
	/**
185
	 * Getter - get the current pid_file
186
	 * @return string
187
	 */
188
	public function pid_file()
189
	{
190
		return $this->_pid_file;
191
	}
192
193
	/**
194
	 * 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
	/**
204
	 * Perform a post request on the phantomjs server
205
	 * @param  string $command
206
	 * @param  array  $params
207
	 * @return mixed
208
	 */
209
	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
218
	/**
219
	 * Perform a delete request on the phantomjs server
220
	 * @param  string $command
221
	 * @return mixed
222
	 */
223
	public function delete($command)
224
	{
225
		$options = array();
226
		$options[CURLOPT_CUSTOMREQUEST] = 'DELETE';
227
228
		return $this->call($command, $options);
229
	}
230
231
	/**
232
	 * Get the full url of a command (including server and port)
233
	 * @param  string $command
234
	 * @return string
235
	 */
236 1
	public function command_url($command)
237
	{
238 1
		return rtrim($this->server(), '/').':'.$this->port().'/'.$command;
239
	}
240
241
	/**
242
	 * Set phantomjs binary location
243
	 * @param string $binary
244
	 * @throws Exception
245
	 */
246
	public function set_phantomjs_binary($binary)
247
	{
248
		$this->_phantomjs_binary = $binary;
249
	}
250
251
	/**
252
	 * Perform a custom request on the phantomjs server, using curl
253
	 * @param  string $command
254
	 * @param  array  $options
255
	 * @return mixed
256
	 */
257
	protected function call($command, array $options = array())
258
	{
259
		$curl = curl_init();
260
		$options[CURLOPT_URL] = $this->command_url($command);
261
		$options[CURLOPT_RETURNTRANSFER] = TRUE;
262
		$options[CURLOPT_FOLLOWLOCATION] = TRUE;
263
264
		curl_setopt_array($curl, $options);
265
266
		$raw = '';
267
268
		Attempt::make(function() use ($curl, & $raw) {
269
			$raw = trim(curl_exec($curl));
270
			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 1
	protected function _start($file, $port, $additional = NULL, $log_file = '/dev/null')
300
	{
301 1
		if ( ! Network::is_port_open('localhost', $port)) {
302
			throw new Exception('Port :port is already taken', array(':port' => $port));
303
		}
304
305 1
		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', array(':log_file' => $log_file));
307
		}
308
309 1
		return shell_exec(strtr('nohup :command > :log 2> :log & echo $!', array(
310 1
			':command' => $this->_command($file, $port, $additional),
311 1
			':log' => $log_file,
312 1
		)));
313
	}
314
315
	/**
316
	 * kill a server on a given pid
317
	 * @param  string $pid
318
	 */
319 1
	protected function _kill($pid)
320
	{
321 1
		shell_exec('kill '.$pid);
322 1
	}
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 1
	protected function _command($file, $port, $additional = NULL)
334
	{
335 1
		$dir = realpath(__DIR__.'/../../../../../assets').'/';
336
337 1
		$file = $dir.$file;
338
339 1
		if ( ! is_file($file)) {
340
			throw new Exception('Cannot start phantomjs: file :file is not found', array(':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 1
		{
344 1
			if ( ! is_file($file)) {
345
				throw new Exception(
346
					'Cannot start phantomjs: file :additional is not found',
347
					array(':additional' => $additional)
348
				);
349
			}
350 1
			$additional = $dir.$additional;
351 1
		}
352
353 1
		return $this->_phantomjs_binary." --ssl-protocol=any --ignore-ssl-errors=true {$file} {$port} {$additional}";
354
	}
355
}
356