/[drupal]/contributions/modules/drush/includes/backend.inc
ViewVC logotype

Contents of /contributions/modules/drush/includes/backend.inc

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.19 - (show annotations) (download) (as text)
Wed Oct 28 11:57:56 2009 UTC (4 weeks, 4 days ago) by weitzman
Branch: MAIN
Changes since 1.18: +1 -2 lines
File MIME type: text/x-php
Minor coding standards fixes and one bug fix in includes/sitealias.inc
1 <?php
2 // $Id: backend.inc,v 1.18 2009/10/27 16:26:37 weitzman Exp $
3
4 /**
5 * @file Drush backend API
6 *
7 * When a drush command is called with the --backend option,
8 * it will buffer all output, and instead return a JSON encoded
9 * string containing all relevant information on the command that
10 * was just executed.
11 *
12 * Through this mechanism, it is possible for Drush commands to
13 * invoke each other.
14 *
15 * There are many cases where a command might wish to call another
16 * command in its own process, to allow the calling command to
17 * intercept and act on any errors that may occur in the script that
18 * was called.
19 *
20 * A simple example is if there exists an 'update' command for running
21 * update.php on a specific site. The original command might download
22 * a newer version of a module for installation on a site, and then
23 * run the update script in a separate process, so that in the case
24 * of an error running a hook_update_n function, the module can revert
25 * to a previously made database backup, and the previously installed code.
26 *
27 * By calling the script in a separate process, the calling script is insulated
28 * from any error that occurs in the called script, to the level that if a
29 * php code error occurs (ie: misformed file, missing parenthesis, whatever),
30 * it is still able to reliably handle any problems that occur.
31 *
32 * This is nearly a RESTful API. @see http://en.wikipedia.org/wiki/REST
33 *
34 * Instead of :
35 * http://[server]/[apipath]/[command]?[arg1]=[value1],[arg2]=[value2]
36 *
37 * It will call :
38 * [apipath] [command] --[arg1]=[value1] --[arg2]=[value2] --backend
39 *
40 * [apipath] in this case will be the path to the drush.php file.
41 * [command] is the command you would call, for instance 'status'.
42 *
43 * GET parameters will be passed as options to the script.
44 * POST parameters will be passed to the script as a JSON encoded associative array over STDIN.
45 *
46 * Because of this standard interface, Drush commands can also be executed on
47 * external servers through SSH pipes, simply by prepending, 'ssh username@server.com'
48 * in front of the command.
49 *
50 * If the key-based ssh authentication has been set up between the servers, this will just
51 * work, otherwise the user will be asked to enter a password.
52 */
53
54 /**
55 * Identify the JSON encoded output from a command.
56 */
57 define('DRUSH_BACKEND_OUTPUT_DELIMITER', 'DRUSH_BACKEND_OUTPUT_START>>>%s<<<DRUSH_BACKEND_OUTPUT_END');
58
59 function drush_backend_set_result($value) {
60 if (drush_get_context('DRUSH_BACKEND')) {
61 drush_set_context('BACKEND_RESULT', $value);
62 }
63 }
64
65 function drush_backend_get_result() {
66 return drush_get_context('BACKEND_RESULT');
67 }
68
69 function drush_backend_output() {
70 $data = array();
71
72 $data['output'] = ob_get_contents();
73 ob_end_clean();
74
75 $result_object = drush_backend_get_result();
76 if (isset($result_object)) {
77 $data['object'] = $result_object;
78 }
79
80 $error = drush_get_error();
81 $data['error_status'] = ($error) ? $error : DRUSH_SUCCESS;
82
83 $data['log'] = drush_get_log(); // Append logging information
84 // The error log is a more specific version of the log, and may be used by calling
85 // scripts to check for specific errors that have occurred.
86 $data['error_log'] = drush_get_error_log();
87
88 // Return the options that were set at the end of the process.
89 $data['context'] = drush_get_merged_options();
90 if (!drush_get_context('DRUSH_QUIET')) {
91 printf(DRUSH_BACKEND_OUTPUT_DELIMITER, json_encode($data));
92 }
93 }
94
95 /**
96 * Parse output returned from a Drush command.
97 *
98 * @param string
99 * The output of a drush command
100 * @param integrate
101 * Integrate the errors and log messages from the command into the current process.
102 *
103 * @return
104 * An associative array containing the data from the external command, or the string parameter if it
105 * could not be parsed successfully.
106 */
107 function drush_backend_parse_output($string, $integrate = TRUE) {
108 $regex = sprintf(DRUSH_BACKEND_OUTPUT_DELIMITER, '(.*)');
109
110 preg_match("/$regex/s", $string, $match);
111
112 if ($match[1]) {
113 // we have our JSON encoded string
114 $output = $match[1];
115 // remove the match we just made and any non printing characters
116 $string = trim(str_replace(sprintf(DRUSH_BACKEND_OUTPUT_DELIMITER, $match[1]), '', $string));
117 }
118
119 if ($output) {
120 $data = json_decode($output, TRUE);
121 if (is_array($data)) {
122 if ($integrate) {
123 _drush_backend_integrate($data);
124 }
125 return $data;
126 }
127 }
128 return $string;
129 }
130
131 /**
132 * Integrate log messages and error statuses into the current process.
133 *
134 * Output produced by the called script will be printed, errors will be set
135 * and log messages will be logged locally.
136 *
137 * @param data
138 * The associative array returned from the external command.
139 */
140 function _drush_backend_integrate($data) {
141 if (is_array($data['log'])) {
142 foreach($data['log'] as $log) {
143 if (!is_null($log['error'])) {
144 drush_set_error($log['error'], $log['message']);
145 }
146 else {
147 drush_log($log['message'], $log['type'], $log['error']);
148 }
149 }
150 }
151 // Output will either be printed, or buffered to the drush_backend_output command.
152 if (drush_cmp_error('DRUSH_APPLICATION_ERROR')) {
153 drush_set_error("DRUSH_APPLICATION_ERROR", dt("Output from failed command :\n !output", array('!output' => $data['output'])));
154 }
155 else {
156 print ($data['output']);
157 }
158
159 }
160
161 /**
162 * Call an external command using proc_open.
163 *
164 * @param cmd
165 * The command to execute. This command already needs to be properly escaped.
166 * @param data
167 * An associative array that will be JSON encoded and passed to the script being called.
168 * Objects are not allowed, as they do not json_decode gracefully.
169 *
170 * @return
171 * False if the command could not be executed, or did not return any output.
172 * If it executed successfully, it returns an associative array containing the command
173 * called, the output of the command, and the error code of the command.
174 */
175 function _drush_proc_open($cmd, $data = NULL, $context = NULL) {
176 $descriptorspec = array(
177 0 => array("pipe", "r"), // stdin is a pipe that the child will read from
178 1 => array("pipe", "w"), // stdout is a pipe that the child will write to
179 2 => array("pipe", "w") // stderr is a pipe the child will write to
180 );
181
182 $process = proc_open($cmd, $descriptorspec, $pipes, null, null, array('context' => $context));
183 if (is_resource($process)) {
184 if ($data) {
185 fwrite($pipes[0], json_encode($data)); // pass the data array in a JSON encoded string
186 }
187
188 $info = stream_get_meta_data($pipes[1]);
189 stream_set_blocking($pipes[1], TRUE);
190 stream_set_timeout($pipes[1], 1);
191 $string = '';
192 while (!feof($pipes[1]) && !$info['timed_out']) {
193 $string .= fgets($pipes[1], 4096);
194 $info = stream_get_meta_data($pipes[1]);
195 flush();
196 };
197
198 $info = stream_get_meta_data($pipes[2]);
199 stream_set_blocking($pipes[2], TRUE);
200 stream_set_timeout($pipes[2], 1);
201 while (!feof($pipes[2]) && !$info['timed_out']) {
202 $string .= fgets($pipes[2], 4096);
203 $info = stream_get_meta_data($pipes[2]);
204 flush();
205 };
206
207 fclose($pipes[0]);
208 fclose($pipes[1]);
209 fclose($pipes[2]);
210 $code = proc_close($process);
211 return array('cmd' => $cmd, 'output' => $string, 'code' => $code);
212 }
213 return false;
214 }
215
216 /**
217 * Invoke a drush backend command.
218 *
219 * @param command
220 * A defined drush command such as 'cron', 'status' or any of the available ones such as 'drush pm'.
221 * @param data
222 * Optional. An array containing options to pass to the call. Common options would be 'uri' if you want to call a command
223 * on a different site, or 'root', if you want to call a command using a different Drupal installation.
224 * Array items with a numeric key are treated as optional arguments to the command.
225 * @param method
226 * Optional. Defaults to 'GET'.
227 * If this parameter is set to 'POST', the $data array will be passed to the script being called as a JSON encoded string over
228 * the STDIN pipe of that process. This is preferable if you have to pass sensitive data such as passwords and the like.
229 * For any other value, the $data array will be collapsed down into a set of command line options to the script.
230 * @param integrate
231 * Optional. Defaults to TRUE.
232 * If TRUE, any error statuses or log messages will be integrated into the current process. This might not be what you want,
233 * if you are writing a command that operates on multiple sites.
234 * @param drush_path
235 * Optional. Defaults to the current drush.php file on the local machine, and
236 * to simply 'drush' (the drush script in the current PATH) on remote servers.
237 * You may also specify a different drush.php script explicitly. You will need
238 * to set this when calling drush on a remote server if 'drush' is not in the
239 * PATH on that machine.
240 * @param hostname
241 * Optional. A remote host to execute the drush command on.
242 * @param username
243 * Optional. Defaults to the current user. If you specify this, you can choose which module to send.
244 *
245 * @return
246 * If the command could not be completed successfully, FALSE.
247 * If the command was completed, this will return an associative array containing the data from drush_backend_output().
248 */
249 function drush_backend_invoke($command, $data = array(), $method = 'GET', $integrate = TRUE, $drush_path = NULL, $hostname = NULL, $username = NULL) {
250 $args = explode(" ", $command);
251 $command = array_shift($args);
252 return drush_backend_invoke_args($command, $args, $data, $method, $integrate, $drush_path, $hostname, $username);
253 }
254
255 function drush_backend_invoke_args($command, $args, $data = array(), $method = 'GET', $integrate = TRUE, $drush_path = NULL, $hostname = NULL, $username = NULL) {
256 $cmd = _drush_backend_generate_command($command, $args, $data, $method, $drush_path, $hostname, $username);
257 return _drush_backend_invoke($cmd, $data, $integrate);
258 }
259
260 /**
261 * Create a new pipe with proc_open, and attempt to parse the output.
262 *
263 * We use proc_open instead of exec or others because proc_open is best
264 * for doing bi-directional pipes, and we need to pass data over STDIN
265 * to the remote script.
266 *
267 * Exec also seems to exhibit some strangeness in keeping the returned
268 * data intact, in that it modifies the newline characters.
269 *
270 * @param cmd
271 * The complete command line call to use.
272 * @param data
273 * An associative array to pass to the remote script.
274 * @param integrate
275 * Integrate data from remote script with local process.
276 *
277 * @return
278 * If the command could not be completed successfully, FALSE.
279 * If the command was completed, this will return an associative array containing the data from drush_backend_output().
280 */
281 function _drush_backend_invoke($cmd, $data = null, $integrate = TRUE) {
282 drush_log(dt('Running: !cmd', array('!cmd' => $cmd)), 'command');
283 $proc = _drush_proc_open($cmd, $data);
284
285 if (($proc['code'] == DRUSH_APPLICATION_ERROR) && $integrate) {
286 drush_set_error('DRUSH_APPLICATION_ERROR', dt("The external command could not be executed due to an application error."));
287 }
288
289 if ($proc['output']) {
290 $values = drush_backend_parse_output($proc['output'], $integrate);
291 if (is_array($values)) {
292 return $values;
293 }
294 else {
295 return drush_set_error('DRUSH_FRAMEWORK_ERROR', dt("The command could not be executed successfully (returned: !return, code: %code)", array("!return" => $proc['output'], "%code" => $proc['code'])));
296 }
297 }
298 return FALSE;
299 }
300
301 /**
302 * Generate a command to execute.
303 *
304 * @param command
305 * A defined drush command such as 'cron', 'status' or any of the available ones such as 'drush pm'.
306 * @param args
307 * An array or arguments for the command.
308 * @param data
309 * Optional. An array containing options to pass to the remote script.
310 * Array items with a numeric key are treated as optional arguments to the command.
311 * This parameter is a reference, as any options that have been represented as either an option, or an argument will be removed.
312 * This allows you to pass the left over options as a JSON encoded string, without duplicating data.
313 * @param method
314 * Optional. Defaults to 'GET'.
315 * If this parameter is set to 'POST', the $data array will be passed to the script being called as a JSON encoded string over
316 * the STDIN pipe of that process. This is preferable if you have to pass sensitive data such as passwords and the like.
317 * For any other value, the $data array will be collapsed down into a set of command line options to the script.
318 * @param integrate
319 * Optional. Defaults to TRUE.
320 * If TRUE, any error statuses or log messages will be integrated into the current process. This might not be what you want,
321 * if you are writing a command that operates on multiple sites.
322 * @param drush_path
323 * Optional. Defaults to the current drush.php file on the local machine, and
324 * to simply 'drush' (the drush script in the current PATH) on remote servers.
325 * You may also specify a different drush.php script explicitly. You will need
326 * to set this when calling drush on a remote server if 'drush' is not in the
327 * PATH on that machine.
328 * @param hostname
329 * Optional. A remote host to execute the drush command on.
330 * @param username
331 * Optional. Defaults to the current user. If you specify this, you can choose which module to send.
332 *
333 * @return
334 * A text string representing a fully escaped command.
335 */
336 function _drush_backend_generate_command($command, $args, &$data, $method = 'GET', $drush_path = null, $hostname = null, $username = null) {
337 $drush_path = !is_null($drush_path) ? $drush_path : (is_null($hostname) ? DRUSH_COMMAND : 'drush'); // Call own drush.php file on local machines, or 'drush' on remote machines.
338 $data['root'] = ($data['root']) ? $data['root'] : drush_get_context('DRUSH_DRUPAL_ROOT');
339 $data['uri'] = array_key_exists('uri', $data) ? $data['uri'] : drush_get_context('DRUSH_URI');
340
341 $option_str = _drush_backend_argument_string($data, $method);
342 foreach ($data as $key => $arg) {
343 if (is_numeric($key)) {
344 $args[] = $arg;
345 unset($data[$key]);
346 }
347 }
348 foreach ($args as $arg) {
349 $command .= ' ' . escapeshellarg($arg);
350 }
351 $php='';
352 if (".php" == substr($drush_path, strlen($drush_path) - strlen(".php"))) {
353 $php = drush_find_php() . ' ';
354 }
355 // @TODO: Implement proper multi platform / multi server support.
356 $cmd = $php . sprintf(escapeshellcmd(" %s %s %s --backend"), escapeshellcmd($drush_path), $option_str, $command);
357
358 if (!is_null($hostname)) {
359 $username = (!is_null($username)) ? $username : get_current_user();
360 $cmd = sprintf("ssh -o PasswordAuthentication=no %s@%s %s", escapeshellarg($username), escapeshellarg($hostname), escapeshellarg($cmd));
361 }
362
363 return $cmd;
364 }
365
366 /**
367 * A small utility function to call a drush command in the background.
368 *
369 * Takes the same parameters as drush_backend_invoke, but forks a new
370 * process by calling the command using system() and adding a '&' at the
371 * end of the command.
372 *
373 * Use this if you don't care what the return value of the command may be.
374 */
375 function drush_backend_fork($command, $data, $drush_path = null, $hostname = null, $username = null) {
376 $data['quiet'] = TRUE;
377 $args = explode(" ", $command);
378 $command = array_shift($args);
379 $cmd = "(" . _drush_backend_generate_command($command, $args, $data, 'GET', $drush_path, $hostname, $username) . ' &) > /dev/null';
380 drush_log(dt("Forking : !cmd", array('!cmd' => $cmd)));
381 system($cmd);
382 }
383
384 /**
385 * Map the options to a string containing all the possible arguments and options.
386 *
387 * @param data
388 * Optional. An array containing options to pass to the remote script.
389 * Array items with a numeric key are treated as optional arguments to the command.
390 * This parameter is a reference, as any options that have been represented as either an option, or an argument will be removed.
391 * This allows you to pass the left over options as a JSON encoded string, without duplicating data.
392 * @param method
393 * Optional. Defaults to 'GET'.
394 * If this parameter is set to 'POST', the $data array will be passed to the script being called as a JSON encoded string over
395 * the STDIN pipe of that process. This is preferable if you have to pass sensitive data such as passwords and the like.
396 * For any other value, the $data array will be collapsed down into a set of command line options to the script.
397 * @return
398 * A properly formatted and escaped set of arguments and options to append to the drush.php shell command.
399 */
400 function _drush_backend_argument_string(&$data, $method = 'GET') {
401 // Named keys are options, numerically indexed keys are optional arguments.
402 $args = array();
403 $options = array();
404
405 foreach ($data as $key => $value) {
406 if (!is_array($value) && !is_object($value) && !is_null($value) && ($value != '')) {
407 if (is_numeric($key)) {
408 $args[$key] = $value;
409 }
410 else {
411 $options[$key] = $value;
412 }
413 }
414 }
415 if (array_key_exists('backend', $data)) {
416 unset($data['backend']);
417 }
418
419 $special = array('root', 'uri'); // These should be in the command line.
420 $option_str = '';
421 foreach ($options as $key => $value) {
422 if (($method != 'POST') || (($method == 'POST') && in_array($key, $special))) {
423 $option_str .= _drush_escape_option($key, $value);
424 unset($data[$key]); // Remove items in the data array.
425 }
426 }
427
428 return $option_str;
429 }
430
431 /**
432 * Return a properly formatted and escaped command line option
433 *
434 * @param key
435 * The name of the option.
436 * @param value
437 * The value of the option.
438 *
439 * @return
440 * If the value is set to TRUE, this function will return " --key"
441 * In other cases it will return " --key='value'"
442 */
443 function _drush_escape_option($key, $value = TRUE) {
444 if ($value !== TRUE) {
445 $option_str = " --$key=" . escapeshellarg($value);
446 }
447 else {
448 $option_str = " --$key";
449 }
450 return $option_str;
451 }
452
453 /**
454 * Read options fron STDIN during POST requests.
455 *
456 * This function will read any text from the STDIN pipe,
457 * and attempts to generate an associative array if valid
458 * JSON was received.
459 *
460 * @return
461 * An associative array of options, if successfull. Otherwise FALSE.
462 */
463 function _drush_backend_get_stdin() {
464 $fp = fopen('php://stdin', 'r');
465 stream_set_blocking($fp, FALSE);
466 $string = stream_get_contents($fp);
467 fclose($fp);
468 if (trim($string)) {
469 return json_decode($string, TRUE);
470 }
471 return FALSE;
472 }

  ViewVC Help
Powered by ViewVC 1.1.2