backend.inc

  1. 8.0.x includes/backend.inc
  2. 6.x includes/backend.inc
  3. 7.x includes/backend.inc
  4. 3.x includes/backend.inc
  5. 4.x includes/backend.inc
  6. 5.x includes/backend.inc
  7. master includes/backend.inc

Drush backend API

When a drush command is called with the --backend option, it will buffer all output, and instead return a JSON encoded string containing all relevant information on the command that was just executed.

Through this mechanism, it is possible for Drush commands to invoke each other.

There are many cases where a command might wish to call another command in its own process, to allow the calling command to intercept and act on any errors that may occur in the script that was called.

A simple example is if there exists an 'update' command for running update.php on a specific site. The original command might download a newer version of a module for installation on a site, and then run the update script in a separate process, so that in the case of an error running a hook_update_n function, the module can revert to a previously made database backup, and the previously installed code.

By calling the script in a separate process, the calling script is insulated from any error that occurs in the called script, to the level that if a php code error occurs (ie: misformed file, missing parenthesis, whatever), it is still able to reliably handle any problems that occur.

This is nearly a RESTful API. Instead of : http://[server]/[apipath]/[command]?[arg1]=[value1],[arg2]=[value2]

It will call : [apipath] [command] --[arg1]=[value1] --[arg2]=[value2] --backend

[apipath] in this case will be the path to the drush.php file. [command] is the command you would call, for instance 'status'.

GET parameters will be passed as options to the script. POST parameters will be passed to the script as a JSON encoded associative array over STDIN.

Because of this standard interface, Drush commands can also be executed on external servers through SSH pipes, simply by prepending, 'ssh username@server.com' in front of the command.

If the key-based ssh authentication has been set up between the servers, this will just work, otherwise the user will be asked to enter a password.

See also

http://en.wikipedia.org/wiki/REST

Functions

Namesort descending Description
drush_backend_fork A small utility function to call a drush command in the background.
drush_backend_get_result
drush_backend_invoke Invoke a drush backend command.
drush_backend_invoke_args
drush_backend_output
drush_backend_parse_output Parse output returned from a Drush command.
drush_backend_set_result
_drush_backend_argument_string Map the options to a string containing all the possible arguments and options.
_drush_backend_generate_command Generate a command to execute.
_drush_backend_get_stdin Read options fron STDIN during POST requests.
_drush_backend_integrate Integrate log messages and error statuses into the current process.
_drush_backend_invoke Create a new pipe with proc_open, and attempt to parse the output.
_drush_escape_option Return a properly formatted and escaped command line option
_drush_proc_open Call an external command using proc_open.

Constants

Namesort descending Description
DRUSH_BACKEND_OUTPUT_DELIMITER Identify the JSON encoded output from a command.

File

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