From 1c76b326fdac310e0fa14b256743585b619a5ba1 Mon Sep 17 00:00:00 2001 From: Owen Barton Date: Mon, 27 Jun 2011 12:46:53 -0700 Subject: [PATCH] Feature #1200738 quick-drupal command (download, install, serve and login to Drupal with minimal configuration and dependencies) and runserver improvements --- commands/core/core.drush.inc | 123 +++++++++++++++++++++++++++++- commands/core/watchdog.drush.inc | 5 +- commands/runserver/runserver-drupal.inc | 52 +++++++++++-- commands/runserver/runserver-prepend.php | 54 +++++++++---- commands/runserver/runserver.drush.inc | 115 +++++++++++++++++++++++----- includes/command.inc | 4 +- includes/exec.inc | 51 ++++++++++++ 7 files changed, 359 insertions(+), 45 deletions(-) diff --git a/commands/core/core.drush.inc b/commands/core/core.drush.inc index 29d5747..ffea225 100644 --- a/commands/core/core.drush.inc +++ b/commands/core/core.drush.inc @@ -271,9 +271,9 @@ function core_drush_command() { $items['batch-process'] = array( 'description' => 'Process operations in the specified batch set', 'hidden' => TRUE, - 'arguments' => array( - 'batch-id' => 'The batch id that will be processed', - ), + 'arguments' => array( + 'batch-id' => 'The batch id that will be processed', + ), 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_LOGIN, ); @@ -291,6 +291,23 @@ function core_drush_command() { 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_DRUSH, ); + $items['core-quick-drupal'] = array( + 'description' => 'Download, install, serve and login to Drupal with minimal configuration and dependencies.', + 'bootstrap' => DRUSH_BOOTSTRAP_DRUSH, + 'aliases' => array('qd'), + 'arguments' => array( + 'site' => 'Short name for the site to be created - used as a directory name and as sqlite file name. Optional - if omitted timestamped "quick-drupal" directory will be used instead.', + 'projects' => 'A list of projects to download into the new site. If projects contain extensions (modules or themes) with the same name they will be enabled by default. See --enable option to control this behaviour further.', + ), + 'examples' => array( + 'drush qd' => 'Download and install stable release of Drupal into a timestamped directory, start server, and open the site logged in as admin.', + 'drush qd --profile=minimal --dev --cache --default-major=8 --yes' => 'Fire up dev release of Drupal site with minimal install profile.', + 'drush qd testsite devel --server=:8081/admin --browser=firefox --cache --yes' => 'Fire up stable release (using the cache) of Drupal site called "testsite", download and enable devel module, start a server on port 8081 and open /admin in firefox.', + 'drush qd commercesite --core=commerce_kickstart --profile=commerce_kickstart --cache --yes --watchdog' => 'Download and install the "Commerce Kickstart" distribution/install profile, display watchdog messages on the server console.', + ), + ); + // Add in options/engines. + drush_core_quick_drupal_options($items); return $items; } @@ -688,8 +705,108 @@ function _drush_core_is_named_in_array($key, $the_array) { } /** + * Callback for core-quick-drupal command. + */ +function drush_core_quick_drupal() { + $requests = FALSE; + $args = func_get_args(); + if (empty($args)) { + $name = 'quick-drupal-' . gmdate('YmdHis', $_SERVER['REQUEST_TIME']); + } + else { + $name = array_shift($args); + if (!empty($args)) { + $requests = pm_parse_arguments($args, FALSE); + } + } + $base = getcwd() . '/' . $name; + drush_set_option('destination', $base); + drush_set_option('backend', TRUE); + $core = drush_get_option('core', 'drupal'); + drush_set_option('drupal-project-rename', $core); + if (drush_invoke('pm-download', array($core)) === FALSE) { + return drush_set_error('QUICK_DRUPAL_CORE_DOWNLOAD_FAIL', 'Drupal core download/extract failed.'); + } + drush_set_option('root', $base . '/' . $core); + if (!drush_get_option('db-url', FALSE)) { + drush_set_option('db-url', 'sqlite:' . $base . '/' . $name . '.sqlite'); + } + if (!drush_bootstrap_to_phase(DRUSH_BOOTSTRAP_DRUPAL_ROOT)) { + return drush_set_error('QUICK_DRUPAL_ROOT_LOCATE_FAIL', 'Unable to locate Drupal root directory.'); + } + if ($requests) { + // Unset --destination, so that downloads go to the site directories. + drush_unset_option('destination'); + if (drush_invoke('pm-download', $requests) === FALSE) { + return drush_set_error('QUICK_DRUPAL_PROJECT_DOWNLOAD_FAIL', 'Project download/extract failed.'); + } + } + drush_invoke('site-install', array(drush_get_option('profile'))); + // Log in with the admin user. + drush_set_option('user', '1'); + if (!drush_bootstrap_to_phase(DRUSH_BOOTSTRAP_DRUPAL_LOGIN)) { + return drush_set_error('QUICK_DRUPAL_INSTALL_FAIL', 'Drupal core install failed.'); + } + $enable = pm_parse_arguments(drush_get_option('enable', $requests)); + if (!empty($enable)) { + if (drush_invoke('pm-enable', $enable) === FALSE) { + return drush_set_error('QUICK_DRUPAL_PROJECT_ENABLE_FAIL', 'Project enable failed.'); + } + } + drush_print(dt('Login URL: ') . drush_invoke('user-login')); + if ($server = drush_get_option('server', '/')) { + drush_invoke('runserver', array($server)); + } +} + +/** + * Include options and engines for core-quick-drupal command, aggregated from + * other command options that are available. We prefix option descriptons, + * to make the long list more navigable. + * + * @param $items + * The core commandfile command array, by reference. Used to include + * site-install options and add options and engines for core-quick-drupal. + */ +function drush_core_quick_drupal_options(&$items) { + $options = array( + 'core' => 'Drupal core to download. Defaults to "drupal" (latest stable version).', + 'profile' => 'The install profile to use. Defaults to standard.', + 'enable' => 'Specific extensions (modules or themes) to enable. By default, extensions with the same name as requested projects will be enabled automatically.', + 'server' => 'Host IP address and port number to bind to and path to open in web browser (hyphen to clear a default path), all elements optional. See runserver examples for shorthand.', + 'no-server' => 'Avoid starting runserver (and browser) for the created Drupal site.', + 'browser' => 'Optional name of a browser to open site in. If omitted the OS default browser will be used. Set --no-browser to disable.', + ); + $pm = pm_drush_command(); + foreach ($pm['pm-download']['options'] as $option => $description) { + $options[$option] = 'Download option: ' . $description; + } + // Unset a few options that are not usable here, as we control them ourselves + // or they are otherwise implied by the environment. + unset($options['destination']); + unset($options['drupal-project-rename']); + unset($options['default-major']); + unset($options['use-site-dir']); + foreach ($items['site-install']['options'] as $option => $description) { + $options[$option] = 'Site install option: ' . $description; + } + unset($options['sites-subdir']); + $runserver = runserver_drush_command(); + foreach ($runserver['runserver']['options'] as $option => $description) { + $options[$option] = 'Runserver option: ' . $description; + } + unset($options['user']); + $items['core-quick-drupal']['options'] = $options; + $items['core-quick-drupal']['engines'] = $pm['pm-download']['engines']; +} + +/** * Command callback. Runs "naked" php scripts * and drush "shebang" scripts ("#!/usr/bin/env drush"). + * + * @params + * Command arguments, optional. First argument is site name, remaining + * argument(s) are contrib modules to install. */ function drush_core_php_script() { $found = FALSE; diff --git a/commands/core/watchdog.drush.inc b/commands/core/watchdog.drush.inc index f1da505..48490cc 100644 --- a/commands/core/watchdog.drush.inc +++ b/commands/core/watchdog.drush.inc @@ -223,7 +223,10 @@ function core_watchdog_format_result($result, $full = FALSE) { // Message. if (drush_drupal_major_version() >= 6) { - $variables = unserialize($result->variables); + $variables = $result->variables; + if (is_string($variables)) { + $variables = unserialize(); + } if (is_array($variables)) { $result->message = strtr($result->message, $variables); } diff --git a/commands/runserver/runserver-drupal.inc b/commands/runserver/runserver-drupal.inc index a4ca965..d860580 100644 --- a/commands/runserver/runserver-drupal.inc +++ b/commands/runserver/runserver-drupal.inc @@ -10,12 +10,14 @@ * Extends the HTTPServer class, handling request routing and environment. */ class DrupalServer extends HTTPServer { - public $http_host; + // We pass in variables, rather than querying options here, to allow this to + // potentially be used in other commands. + public $site, $path, $conf_inject, $user, $watchdog, $debug, $first_request_complete; /** * This is the equivalent of .htaccess, passing requests to files if they * exist, and all other requests to index.php. We also set a number - * of CGI environment variables here. + * of CGI environment variables here. */ function route_request($request) { $cgi_env = array(); @@ -23,16 +25,24 @@ class DrupalServer extends HTTPServer { // We pass in the effective base_url to our auto_prepend_script via the cgi // environment. This allows Drupal to generate working URLs to this http // server, whilst finding the correct multisite from the HTTP_HOST header. - $cgi_env['RUNSERVER_BASE_URL'] = 'http://localhost:' . $this->port; + $cgi_env['RUNSERVER_BASE_URL'] = 'http://' . $this->addr . ':' . $this->port; // We pass in an array of $conf overrides using the same approach. // By default we set drupal_http_request_fails to FALSE, as the httpserver - // is unable to process simultanious requests on some systems. + // is unable to process simultaneous requests on some systems. // This is available as an option for developers to pass in their own // favorite $conf overrides (e.g. disabling css aggregation). - $conf_inject = drush_get_option('conf-inject', array('drupal_http_request_fails' => FALSE)); + $conf_inject = $this->conf_inject; $cgi_env['RUNSERVER_CONF'] = urlencode(serialize($conf_inject)); + // We pass in the specified user (if set) - should be a fully loaded user + // object. This will automatically log this user in the browser during the + // first request (but not subsequent requests, to allow logging out). + if (!empty($this->user) && $this->user->uid && $this->first_request_complete !== TRUE) { + $this->first_request_complete = TRUE; + $cgi_env['RUNSERVER_USER'] = urlencode(serialize($this->user)); + } + // Handle static files and php scripts accessed directly $uri = $request->uri; $doc_root = DRUPAL_ROOT; @@ -53,8 +63,38 @@ class DrupalServer extends HTTPServer { } $cgi_env['SCRIPT_NAME'] = '/index.php'; - $cgi_env['HTTP_HOST'] = $cgi_env['SERVER_NAME'] = $this->http_host; + $cgi_env['HTTP_HOST'] = $cgi_env['SERVER_NAME'] = $this->site; return $this->get_php_response($request, $doc_root . '/index.php', $cgi_env); } + + /** + * Override get started event. + */ + function listening() { + drush_print(dt('HTTP server listening on !addr, port !port (see http://!hostname:!port/), serving site !site...', array('!addr' => $this->addr, '!hostname' => $this->hostname, '!port' => $this->port, '!site' => $this->site))); + if (!empty($this->path)) { + drush_start_browser($this->path); + } + } + + /** + * Override request done event. + */ + function request_done($request) { + drush_print(trim($this->get_log_line($request), "\n")); + $headers = $request->response->headers; + if ($this->watchdog && isset($headers['X-Runserver-Watchdog'])) { + $results = unserialize(urldecode($headers['X-Runserver-Watchdog'])); + foreach ($results as $result) { + $result = (object)$result; + $result->uid = $result->user->uid; + $result = core_watchdog_format_result($result, TRUE); + drush_print("Watchdog: {$result->date} ({$result->severity}, {$result->type}, {$result->user->name}) {$result->message}", 2); + } + } + if ($this->debug) { + drush_print_r($request); + } + } } diff --git a/commands/runserver/runserver-prepend.php b/commands/runserver/runserver-prepend.php index 8f89d94..15a835c 100644 --- a/commands/runserver/runserver-prepend.php +++ b/commands/runserver/runserver-prepend.php @@ -1,29 +1,51 @@ 'Runs a lightweight built in http server for development.', - 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_SITE, + 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_FULL, 'arguments' => array( - 'addr:port' => 'Host IP address and port number to bind to (default 127.0.0.1:8888). The IP is optional, in which case just pass in the numeric port.', + 'addr:port/path' => 'Host IP address and port number to bind to and path to open in web browser. Format is addr:port/path, default 127.0.0.1:8888, all elements optional. See examples for shorthand.', ), 'options' => array( 'php-cgi' => 'Name of the php-cgi binary. If it is not on your current $PATH you should include the full path. You can include command line parameters to pass into php-cgi.', 'conf-inject' => 'Key-value array of variables to override in the $conf array for the running site. By default disables drupal_http_request_fails to avoid errors on Windows (which supports only one connection at a time). Note that as this is a key-value array, it can only be specified in a drushrc or alias file, and not on the command line.', + 'default-server' => 'A default addr:port/path to use for any values not specified as an argument.', + 'user' => 'If opening a web browser, automatically log in as this user (user ID or username)', + 'watchdog' => 'Collect and integrate watchdog messages from each request into the log', + 'dns' => 'Resolve hostnames/IPs using DNS/rDNS (if possible) to determine binding IPs and/or human friendly hostnames for URLs and browser.', ), 'aliases' => array('rs'), + 'examples' => array( + 'drush rs 8080' => 'Start runserver on 127.0.0.1, port 8080.', + 'drush rs 10.0.0.28:80' => 'Start runserver on 10.0.0.28, port 80.', + 'drush rs --php-cgi=php5-cgi --dns localhost:8888/user' => 'Start runserver on localhost (using rDNS to determine binding IP), port 8888, and open /user in browser. Use "php5-cgi" as the php-cgi binary.', + 'drush rs /' => 'Start runserver on default IP/port (127.0.0.1, port 8888), and open / in browser.', + 'drush rs --default-server=127.0.0.1:8080/ -' => 'Use a default (would be specified in your drushrc) that starts runserver on port 8080, and opens a browser to the front page. Set path to a single hyphen path in argument to prevent opening browser for this session.', + 'drush rs --watchdog :9000/admin' => 'Start runserver on 127.0.0.1, port 9000, and open /admin in browser, including any watchdog messages for the session in the log. Note that you need a colon when you specify port and path, but no IP.', + ), ); return $items; } @@ -75,7 +87,9 @@ function drush_core_runserver_validate() { /** * Callback for runserver command. */ -function drush_core_runserver($addrport = '8888') { +function drush_core_runserver($uri = NULL) { + global $user; + // Fetch httpserver to our /lib directory, if needed. $lib = drush_get_option('lib', DRUSH_BASE_PATH . '/lib'); $httpserverfile = $lib . '/' . DRUSH_HTTPSERVER_DIR_BASE . substr(DRUSH_HTTPSERVER_VERSION, 0, 7) . '/httpserver.php'; @@ -91,30 +105,95 @@ function drush_core_runserver($addrport = '8888') { // Include the library and our class that extends it. require_once $httpserverfile; require_once 'runserver-drupal.inc'; - - // Determine configuration. - if (is_numeric($addrport)) { - $addr = '127.0.0.1'; - $port = $addrport; + + // We pass in the currently logged in user (if set via the --user option), + // which will automatically log this user in the browser during the first + // request. + if (drush_get_option('user', FALSE)) { + drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_LOGIN); + } + + // Determine active configuration. + $drush_default = array( + 'host' => '127.0.0.1', + 'port' => '8888', + 'path' => '', + ); + $user_default = runserver_parse_uri(drush_get_option('default-server', '127.0.0.1:8888')); + $uri = runserver_parse_uri($uri) + $user_default + $drush_default; + if (ltrim($uri['path'], '/') == '-') { + // Allow a path of a single hyphen to clear a default path. + $uri['path'] = ''; } - else { - $addrport = explode(':', $addrport); - if (count($addrport) !== 2 && is_numeric($addrport[1])) { - return drush_set_error('RUNSERVER_INVALID_ADDRPORT', dt('Invalid address/port argument - should be either numeric (port only), or in the "host:port" format..')); + + // Determine and set the new URI. + $hostname = $addr = $uri['host']; + if (drush_get_option('dns', FALSE)) { + if (ip2long($hostname)) { + $hostname = gethostbyaddr($hostname); + } + else { + $addr = gethostbyname($hostname); } - $addr = $addrport[0]; - $port = $addrport[1]; } - + drush_set_context('DRUSH_URI', 'http://' . $hostname . ':' . $uri['port']); + // We delete any registered files here, since they are not caught by Ctrl-C. _drush_delete_registered_files(); - + // Create a new server instance and start it running. $server = new DrupalServer(array( 'addr' => $addr, - 'port' => $port, - 'serverid' => 'Drush runserver', + 'port' => $uri['port'], + 'path' => $uri['path'], + 'hostname' => $hostname, + 'site' => drush_get_context('DRUSH_DRUPAL_SITE', 'default'), + 'server_id' => 'Drush runserver', 'php_cgi' => drush_get_option('php-cgi', 'php-cgi') . ' --define auto_prepend_file="' . DRUSH_BASE_PATH . '/commands/runserver/runserver-prepend.php"', + 'conf_inject' => drush_get_option('conf-inject', array('drupal_http_request_fails' => FALSE)), + 'user' => $user, + 'watchdog' => drush_get_option('watchdog', FALSE), + 'debug' => drush_get_context('DRUSH_DEBUG'), )); $server->run_forever(); } + +/** + * Parse a URI or partial URI (including just a port, host IP or path). + * + * @param $uri + * String that can contain partial URI. + * @return array + * URI array as returned by parse_url. + */ +function runserver_parse_uri($uri) { + if ($uri[0] == ':') { + // ':port/path' shorthand, insert a placeholder hostname to allow parsing. + $uri = 'placeholder-hostname' . $uri; + } + $first_part = substr($uri, 0, strpos($uri, '/')); + if (ip2long($first_part)) { + // 'IP/path' shorthand, insert a schema to allow parsing. + $uri = 'http://' . $uri; + } + $uri = parse_url($uri); + if (empty($uri)) { + return drush_set_error('RUNSERVER_INVALID_ADDRPORT', dt('Invalid argument - should be in the "host:port/path" format, numeric (port only) or non-numeric (path only).')); + } + if (count($uri) == 1 && isset($uri['path'])) { + if (is_numeric($uri['path'])) { + // Port only shorthand. + $uri['port'] = $uri['path']; + unset($uri['path']); + } + else if (ip2long($uri['path'])) { + // IP only shorthand. + $uri['host'] = $uri['path']; + unset($uri['path']); + } + } + if (isset($uri['host']) && $uri['host'] == 'placeholder-hostname') { + unset($uri['host']); + } + return $uri; +} \ No newline at end of file diff --git a/includes/command.inc b/includes/command.inc index ff30e4b..76e98e0 100644 --- a/includes/command.inc +++ b/includes/command.inc @@ -126,6 +126,7 @@ function _drush_invoke($command, $args, $defined_in_commandfile) { // Only the 'main' callback can send data to backend. if ($var_hook == $hook) { drush_backend_set_result($result); + $return = $result; } _drush_log_drupal_messages(); if (drush_get_error() || ($result === FALSE)) { @@ -163,9 +164,10 @@ function _drush_invoke($command, $args, $defined_in_commandfile) { drush_log(dt("Changes made in !func have been rolled back.", array('!func' => $func)), 'rollback'); } } + return !$rollback; } - return !$rollback; + return $return; } /** diff --git a/includes/exec.inc b/includes/exec.inc index c00154a..464b48c 100644 --- a/includes/exec.inc +++ b/includes/exec.inc @@ -290,5 +290,56 @@ function drush_shell_exec_output() { } /** + * Starts a background browser/tab for the current site or a specified URL. + * + * Uses a non-blocking proc_open call, so Drush execution will continue. + * + * @param $uri + * Optional URI or site path to open in browser. If omitted, or if a site path + * is specified, the current site home page uri will be prepended if the sites + * hostname resolves. + * @return + * TRUE if browser was opened, FALSE if browser was disabled by the user or a, + * default browser could not be found. + */ +function drush_start_browser($uri = NULL) { + if (!parse_url($uri, PHP_URL_HOST)) { + $site = drush_get_context('DRUSH_URI'); + $host = parse_url($site, PHP_URL_HOST); + // Validate that the host part of the URL resolves, so we don't attempt to + // open the browser for http://default or similar invalid hosts. + $hosterror = gethostbyname($host) == $host; + $iperror = ip2long($host) && gethostbyaddr($host) == $host; + if ($hosterror && $iperror) { + drush_log(dt('!host does not appear to be a resolvable hostname or IP, not starting browser.', array('!host' => $host)), 'warning'); + return FALSE; + } + $uri = $site . '/' . ltrim($uri, '/'); + } + if ($browser = drush_get_option('browser', TRUE)) { + if ($browser === TRUE) { + // See if we can find an OS helper to open URLs in default browser. + if (drush_shell_exec('which xdg-open')) { + $browser = 'xdg-open'; + } + else if (drush_shell_exec('which open')) { + $browser = 'open'; + } + else { + // Can't find a valid browser. + $browser = FALSE; + } + } + if ($browser && !drush_get_context('DRUSH_SIMULATE')) { + $pipes = array(); + drush_log(dt('Opening browser at !uri', array('!uri' => $uri))); + proc_close(proc_open($browser . ' ' . drush_escapeshellarg($uri) . ' 2> /dev/null &', array(), $pipes)); + return TRUE; + } + } + return FALSE; +} + +/** * @} End of "defgroup commandwrappers". */ -- 1.7.1