>>%s<< $data['output']))); } else { print ($data['output']); } } /** * Call an external command using proc_open. * * @param cmd * The command to execute. This command already needs to be properly escaped. * @param data * An associative array that will be JSON encoded and passed to the script being called. * Objects are not allowed, as they do not json_decode gracefully. * * @return * False if the command could not be executed, or did not return any output. * If it executed successfully, it returns an associative array containing the command * called, the output of the command, and the error code of the command. */ function _drush_proc_open($cmd, $data = NULL, $context = NULL) { $descriptorspec = array( 0 => array("pipe", "r"), // stdin is a pipe that the child will read from 1 => array("pipe", "w"), // stdout is a pipe that the child will write to 2 => array("pipe", "w") // stderr is a pipe the child will write to ); $process = proc_open($cmd, $descriptorspec, $pipes, null, null, array('context' => $context)); if (is_resource($process)) { if ($data) { fwrite($pipes[0], json_encode($data)); // pass the data array in a JSON encoded string } $info = stream_get_meta_data($pipes[1]); stream_set_blocking($pipes[1], TRUE); stream_set_timeout($pipes[1], 1); $string = ''; while (!feof($pipes[1]) && !$info['timed_out']) { $string .= fgets($pipes[1], 4096); $info = stream_get_meta_data($pipes[1]); flush(); }; $info = stream_get_meta_data($pipes[2]); stream_set_blocking($pipes[2], TRUE); stream_set_timeout($pipes[2], 1); while (!feof($pipes[2]) && !$info['timed_out']) { $string .= fgets($pipes[2], 4096); $info = stream_get_meta_data($pipes[2]); flush(); }; fclose($pipes[0]); fclose($pipes[1]); fclose($pipes[2]); $code = proc_close($process); return array('cmd' => $cmd, 'output' => $string, 'code' => $code); } return FALSE; } /** * Invoke a drush backend command. * * @param command * A defined drush command such as 'cron', 'status' or any of the available ones such as 'drush pm'. * @param data * Optional. An array containing options to pass to the call. Common options would be 'uri' if you want to call a command * on a different site, or 'root', if you want to call a command using a different Drupal installation. * Array items with a numeric key are treated as optional arguments to the command. * @param method * Optional. Defaults to 'GET'. * If this parameter is set to 'POST', the $data array will be passed to the script being called as a JSON encoded string over * the STDIN pipe of that process. This is preferable if you have to pass sensitive data such as passwords and the like. * For any other value, the $data array will be collapsed down into a set of command line options to the script. * @param integrate * Optional. Defaults to TRUE. * If TRUE, any error statuses or log messages will be integrated into the current process. This might not be what you want, * if you are writing a command that operates on multiple sites. * @param drush_path * Optional. Defaults to the current drush.php file on the local machine, and * to simply 'drush' (the drush script in the current PATH) on remote servers. * You may also specify a different drush.php script explicitly. You will need * to set this when calling drush on a remote server if 'drush' is not in the * PATH on that machine. * @param hostname * Optional. A remote host to execute the drush command on. * @param username * Optional. Defaults to the current user. If you specify this, you can choose which module to send. * * @deprecated Command name includes arguments, and these are not quote-escaped in any way. * @see drush_backend_invoke_args and @see drush_invoke_process for better options. * * @return * If the command could not be completed successfully, FALSE. * If the command was completed, this will return an associative array containing the data from drush_backend_output(). */ function drush_backend_invoke($command, $data = array(), $method = 'GET', $integrate = TRUE, $drush_path = NULL, $hostname = NULL, $username = NULL) { $args = explode(" ", $command); $command = array_shift($args); return drush_backend_invoke_args($command, $args, $data, $method, $integrate, $drush_path, $hostname, $username); } /** * A variant of drush_backend_invoke() which specifies command and arguments separately. * * @deprecated Is not as extensible as drush_backend_invoke_sitealias; @see http://drupal.org/node/766080 * @see drush_invoke_sitealias_args() and @see drush_invoke_process and * @see drush_backend_invoke_sitealias() for better options. */ function drush_backend_invoke_args($command, $args, $data = array(), $method = 'GET', $integrate = TRUE, $drush_path = NULL, $hostname = NULL, $username = NULL, $ssh_options = NULL) { $cmd = _drush_backend_generate_command($command, $args, $data, $method, $drush_path, $hostname, $username, $ssh_options); return _drush_backend_invoke($cmd, $data, array_key_exists('#integrate', $data) ? $data['#integrate'] : $integrate); } /** * Execute a new local or remote command in a new process. * * @param site_record * An array containing information used to generate the command. * 'remote-host' * Optional. A remote host to execute the drush command on. * 'remote-user' * Optional. Defaults to the current user. If you specify this, you can choose which module to send. * 'ssh-options' * Optional. Defaults to "-o PasswordAuthentication=no" * 'path-aliases' * Optional; contains paths to folders and executables useful to the command. * '%drush-script' * Optional. Defaults to the current drush.php file on the local machine, and * to simply 'drush' (the drush script in the current PATH) on remote servers. * You may also specify a different drush.php script explicitly. You will need * to set this when calling drush on a remote server if 'drush' is not in the * PATH on that machine. * @param command * A defined drush command such as 'cron', 'status' or any of the available ones such as 'drush pm'. * @param args * An array of arguments for the command. * @param data * Optional. An array containing options to pass to the remote script. * Array items with a numeric key are treated as optional arguments to the command. * This parameter is a reference, as any options that have been represented as either an option, or an argument will be removed. * This allows you to pass the left over options as a JSON encoded string, without duplicating data. * Parameters that begin with a '#' are not passed on, but are used to affect * the operation of backend invoke. Available options include: * '#integrate' * Print the output and merge the logs and error codes into * the data structures for the running drush command. This causes * the command to act as if it were called directly, without using * backend invoke, while still running it in a separate process. * Function results are still available. * '#interactive' * The output is displayed immediately, as it is produced, and it is * possible for the user to send keyboard input to the command being * executed. If interactive mode is used, then the command output, * logs, etc. are NOT returned to the caller. * '#override-simulated' * Backend invoke will run the command even if DRUSH_SIMULATE is set. * This is useful to run backend commands that fetch data that will * be used by the simulated command. For example, sql-sync looks up * the database options via backend invoke of sql-conf with override * simulated set so that the sql-sync operation can be simulated using * actual database setting values. * @param method * Optional. Defaults to 'GET'. * If this parameter is set to 'POST', the $data array will be passed to the script being called as a JSON encoded string over * the STDIN pipe of that process. This is preferable if you have to pass sensitive data such as passwords and the like. * For any other value, the $data array will be collapsed down into a set of command line options to the script. * @param integrate * Optional. Defaults to TRUE. * If TRUE, any error statuses or log messages will be integrated into the current process. This might not be what you want, * if you are writing a command that operates on multiple sites. * * @return * A text string representing a fully escaped command. */ function drush_backend_invoke_sitealias($site_record, $command, $args, $data = array(), $method = 'GET', $integrate = TRUE) { $cmd = _drush_backend_generate_command_sitealias($site_record, $command, $args, $data, $method); return _drush_backend_invoke($cmd, $data, array_key_exists('#integrate', $data) ? $data['#integrate'] : $integrate); } /** * Create a new pipe with proc_open, and attempt to parse the output. * * We use proc_open instead of exec or others because proc_open is best * for doing bi-directional pipes, and we need to pass data over STDIN * to the remote script. * * Exec also seems to exhibit some strangeness in keeping the returned * data intact, in that it modifies the newline characters. * * @param cmd * The complete command line call to use. * @param data * An associative array to pass to the remote script. * @param integrate * Integrate data from remote script with local process. * * @return * If the command could not be completed successfully, FALSE. * If the command was completed, this will return an associative array containing the data from drush_backend_output(). */ function _drush_backend_invoke($cmd, $data = null, $integrate = TRUE) { if (drush_get_context('DRUSH_SIMULATE') && !array_key_exists('#override-simulated', $data)) { drush_print(dt('Simulating backend invoke: !cmd', array('!cmd' => $cmd))); return FALSE; } drush_log(dt('Backend invoke: !cmd', array('!cmd' => $cmd)), 'command'); if (array_key_exists('#interactive', $data)) { drush_log(dt("executing !cmd", array('!cmd', $cmd))); drush_op_system($cmd); } else { $proc = _drush_proc_open($cmd, $data); if (($proc['code'] == DRUSH_APPLICATION_ERROR) && $integrate) { drush_set_error('DRUSH_APPLICATION_ERROR', dt("The external command could not be executed due to an application error.")); } if ($proc['output']) { $values = drush_backend_parse_output($proc['output'], $integrate); if (is_array($values)) { return $values; } else { 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']))); } } } return FALSE; } /** * Generate a command to execute. * * @param command * A defined drush command such as 'cron', 'status' or any of the available ones such as 'drush pm'. * @param args * An array of arguments for the command. * @param data * Optional. An array containing options to pass to the remote script. * Array items with a numeric key are treated as optional arguments to the command. * This parameter is a reference, as any options that have been represented as either an option, or an argument will be removed. * This allows you to pass the left over options as a JSON encoded string, without duplicating data. * @param method * Optional. Defaults to 'GET'. * If this parameter is set to 'POST', the $data array will be passed to the script being called as a JSON encoded string over * the STDIN pipe of that process. This is preferable if you have to pass sensitive data such as passwords and the like. * For any other value, the $data array will be collapsed down into a set of command line options to the script. * @param drush_path * Optional. Defaults to the current drush.php file on the local machine, and * to simply 'drush' (the drush script in the current PATH) on remote servers. * You may also specify a different drush.php script explicitly. You will need * to set this when calling drush on a remote server if 'drush' is not in the * PATH on that machine. * @param hostname * Optional. A remote host to execute the drush command on. * @param username * Optional. Defaults to the current user. If you specify this, you can choose which module to send. * * @return * A text string representing a fully escaped command. * * @deprecated Is not as flexible as recommended command. @see _drush_backend_generate_command_sitealias(). */ function _drush_backend_generate_command($command, $args, &$data, $method = 'GET', $drush_path = null, $hostname = null, $username = null, $ssh_options = NULL) { return _drush_backend_generate_command_sitealias( array( 'remote-host' => $hostname, 'remote-user' => $username, 'ssh-options' => $ssh_options, 'path-aliases' => array( '%drush-script' => $drush_path, ), ), $command, $args, $data, $method); } /** * Generate a command to execute. * * @param site_record * An array containing information used to generate the command. * 'remote-host' * Optional. A remote host to execute the drush command on. * 'remote-user' * Optional. Defaults to the current user. If you specify this, you can choose which module to send. * 'ssh-options' * Optional. Defaults to "-o PasswordAuthentication=no" * 'path-aliases' * Optional; contains paths to folders and executables useful to the command. * '%drush-script' * Optional. Defaults to the current drush.php file on the local machine, and * to simply 'drush' (the drush script in the current PATH) on remote servers. * You may also specify a different drush.php script explicitly. You will need * to set this when calling drush on a remote server if 'drush' is not in the * PATH on that machine. * @param command * A defined drush command such as 'cron', 'status' or any of the available ones such as 'drush pm'. * @param args * An array of arguments for the command. * @param data * Optional. An array containing options to pass to the remote script. * Array items with a numeric key are treated as optional arguments to the command. * This parameter is a reference, as any options that have been represented as either an option, or an argument will be removed. * This allows you to pass the left over options as a JSON encoded string, without duplicating data. * @param method * Optional. Defaults to 'GET'. * If this parameter is set to 'POST', the $data array will be passed to the script being called as a JSON encoded string over * the STDIN pipe of that process. This is preferable if you have to pass sensitive data such as passwords and the like. * For any other value, the $data array will be collapsed down into a set of command line options to the script. * * @return * A text string representing a fully escaped command. */ function _drush_backend_generate_command_sitealias($site_record, $command, $args, &$data, $method = 'GET') { $drush_path = null; $hostname = array_key_exists('remote-host', $site_record) ? $site_record['remote-host'] : null; $username = array_key_exists('remote-user', $site_record) ? $site_record['remote-user'] : null; $ssh_options = array_key_exists('ssh-options', $site_record) ? $site_record['ssh-options'] : null; $drush_path = NULL; if (array_key_exists('path-aliases', $site_record)) { if (array_key_exists('%drush-script', $site_record['path-aliases'])) { $drush_path = $site_record['path-aliases']['%drush-script']; } } if (drush_is_local_host($hostname)) { $hostname = null; } $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. $data['root'] = array_key_exists('root', $data) ? $data['root'] : drush_get_context('DRUSH_DRUPAL_ROOT'); $data['uri'] = array_key_exists('uri', $data) ? $data['uri'] : drush_get_context('DRUSH_URI'); $os = drush_os($site_record); $option_str = _drush_backend_argument_string($data, $method, $os); foreach ($data as $key => $arg) { if (is_numeric($key)) { $args[] = $arg; unset($data[$key]); } } foreach ($args as $arg) { $command .= ' ' . drush_escapeshellarg($arg, $os); } $interactive = ' ' . (empty($data['#interactive']) ? '' : ' > `tty`') . ' 2>&1'; // @TODO: Implement proper multi platform / multi server support. $cmd = escapeshellcmd($drush_path) . " " . $option_str . " " . $command . (empty($data['#interactive']) ? " --backend" : ""); if (!is_null($hostname)) { $username = (!is_null($username)) ? $username : get_current_user(); $ssh_options = (!is_null($ssh_options)) ? $ssh_options : drush_get_option('ssh-options', "-o PasswordAuthentication=no"); $cmd = "ssh " . $ssh_options . " " . drush_escapeshellarg($username, $os) . "@" . drush_escapeshellarg($hostname, $os) . " " . drush_escapeshellarg($cmd . ' 2>&1', $os) . $interactive; } else { $cmd .= $interactive; } return $cmd; } /** * A small utility function to call a drush command in the background. * * Takes the same parameters as drush_backend_invoke, but forks a new * process by calling the command using system() and adding a '&' at the * end of the command. * * Use this if you don't care what the return value of the command may be. */ function drush_backend_fork($command, $data, $drush_path = null, $hostname = null, $username = null) { $data['quiet'] = TRUE; $args = explode(" ", $command); $command = array_shift($args); $cmd = "(" . _drush_backend_generate_command($command, $args, $data, 'GET', $drush_path, $hostname, $username) . ' &) > /dev/null'; drush_op_system($cmd); } /** * Map the options to a string containing all the possible arguments and options. * * @param data * Optional. An array containing options to pass to the remote script. * Array items with a numeric key are treated as optional arguments to the command. * This parameter is a reference, as any options that have been represented as either an option, or an argument will be removed. * This allows you to pass the left over options as a JSON encoded string, without duplicating data. * @param method * Optional. Defaults to 'GET'. * If this parameter is set to 'POST', the $data array will be passed to the script being called as a JSON encoded string over * the STDIN pipe of that process. This is preferable if you have to pass sensitive data such as passwords and the like. * For any other value, the $data array will be collapsed down into a set of command line options to the script. * @return * A properly formatted and escaped set of arguments and options to append to the drush.php shell command. */ function _drush_backend_argument_string(&$data, $method = 'GET', $os = NULL) { // Named keys are options, numerically indexed keys are optional arguments. $args = array(); $options = array(); foreach ($data as $key => $value) { if (!is_array($value) && !is_object($value) && !is_null($value) && ($value != '')) { if (is_numeric($key)) { $args[$key] = $value; } elseif (substr($key,0,1) != '#') { $options[$key] = $value; } } } if (array_key_exists('backend', $data)) { unset($data['backend']); } $special = array('root', 'uri'); // These should be in the command line. $option_str = ''; foreach ($options as $key => $value) { if (($method != 'POST') || (($method == 'POST') && in_array($key, $special))) { $option_str .= _drush_escape_option($key, $value, $os); unset($data[$key]); // Remove items in the data array. } } return $option_str; } /** * Return a properly formatted and escaped command line option * * @param key * The name of the option. * @param value * The value of the option. * * @return * If the value is set to TRUE, this function will return " --key" * In other cases it will return " --key='value'" */ function _drush_escape_option($key, $value = TRUE, $os = NULL) { if ($value !== TRUE) { $option_str = " --$key=" . drush_escapeshellarg($value, $os); } else { $option_str = " --$key"; } return $option_str; } /** * Read options fron STDIN during POST requests. * * This function will read any text from the STDIN pipe, * and attempts to generate an associative array if valid * JSON was received. * * @return * An associative array of options, if successfull. Otherwise FALSE. */ function _drush_backend_get_stdin() { $fp = fopen('php://stdin', 'r'); stream_set_blocking($fp, FALSE); $string = stream_get_contents($fp); fclose($fp); if (trim($string)) { return json_decode($string, TRUE); } return FALSE; }