Completed
Push — phantomjs-path ( e18804...87a2de )
by Evstati
09:22
created

Driver_Phantomjs_Connection::_command()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 22
rs 8.9197
c 0
b 0
f 0
cc 4
eloc 12
nc 4
nop 3
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
	public function port($port = NULL)
53
	{
54
		if ($port !== NULL)
55
		{
56
			$this->_port = $port;
57
			return $this;
58
		}
59
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
68
	/**
69
	 * Getter / Setter of the current phantomjs server url
70
	 * @param  string $server
71
	 * @return string|Driver_Phantomjs_Connection
72
	 */
73
	public function server($server = NULL)
74
	{
75
		if ($server !== NULL)
76
		{
77
			$this->_server = $server;
78
			return $this;
79
		}
80
		return $this->_server;
81
	}
82
83
	/**
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
	}
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
	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
	 * 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
	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
		{
121
			$this->_pid_file = $pid_file;
122
			if (is_file($this->_pid_file))
123
			{
124
				$this->_kill(file_get_contents($pid_file));
125
				unlink($this->_pid_file);
126
			}
127
		}
128
129
		$this->_pid = $this->_start('phantom.js', $this->port(), 'phantomjs-connection.js', $log_file);
130
131
		if ($this->_pid_file)
132
		{
133
			file_put_contents($this->_pid_file, $this->_pid);
134
		}
135
136
		$self = $this;
137
138
		return Attempt::make(function() use ($self) {
139
			return $self->is_running();
140
		});
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
	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
	}
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
	public function command_url($command)
237
	{
238
		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
	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