Several other web frameworks have lightweight built in web servers, that can be used to radically simplify getting started with development. For example with Django you can use the runserver command, with RoR there is script/server, and so forth. It occurred to me that we can pretty easily offer the same functionality - PHP sockets are not as fun as sockets in other languages, but they do work well enough.

So this morning, I had a poke around and came across https://github.com/youngj/Envaya/tree/master/scripts/httpserver, which is a nice, simple (32K) and functional MIT licensed http server for PHP. It handles serving static files and also the various environment mangling needed to get the headers back and forth. My adding about 10 lines of code, we can add a Drush command wrapper that can feed it information about the current site it should serve from, and let you choose your port.

If you are using this approach, you don't need to mess around with Apache vhosts or symlinks each time you add/switch sites (which can be a barrier for new developers, as well as annoying for people who work on many sites) - you can just type "drush rs" for the appropriate directory or site-alias, and a http server is started up specifically for that site. When you are done, just hit ctrl-c and it goes away.

Anyway - it seems to be working really nicely. You can browse around, log on etc, static files come down fine. A couple of todos I have noticed are that it won't work with multisite yet (need to bootstrap to conf and feed it the site name as the host) and logout doesn't seem to work (a redirect problem perhaps). The other thing we need to do is figure out what to do with the 3 httpserver files - I am guessing they can't be committed directly, so I guess we need to fetch them on demand somehow (like console.php, although perhaps we could do use git somehow).

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

greg.1.anderson’s picture

Nice!

greg.1.anderson’s picture

Perhaps we should move table.inc from includes to a new 'lib' directory, and .gitignore the whole thing.

Also, maybe drush should default the port to 8888 instead of 8080, as 8080 commonly has an apache dev server on it.

Owen Barton’s picture

FileSize
28.28 KB

This fixes the redirect/logout issue, and changes the default port to 8888. I think moving the 3rd party includes to a separate directory is a good idea (we might be able to auto-bootstrap phpunit into there too).

Getting this to work with multisite is looking a bit tricky - if we set HTTP_HOST, we can get the right sites directory, but then all the site links are broken (i.e. pointing to http://sitename.com, when we need to go to http://localhost:8888). We could bootstrap Drupal ourselves, rather than running it as a cgi, but that makes things quite a bit more complex - I have experimented a bit with doing a drush redispatch or pcntl_fork (which could be super speedy in principle, since the parent could be pre-bootstrapped). Any ideas?

msonnabaum’s picture

Haven't tried it yet but saw this in the modules feed the other day which looks somewhat similar:

http://drupal.org/project/instaweb

Owen Barton’s picture

I hadn't seen that - it looks pretty cool too (and weird how they both came to exist within hours of each other!). It looks like it depend on lighttpd - I think one advantage of the httpserver code I am using is that it depends on nothing but a php that was built with --enable-sockets (which is very nearly all of them, as far as I can tell). I think serious developers (who need to play test out .htaccess rules, SSL stuff etc), will probably want to stick with Apache and co. at least some of the time, but I think this style of running sites for development can be really handy for new developers and people who switch sites/environments a lot. Together with sqlite in Drupal 7, this opens the door to a working standalone Drupal development bundle that does not need root access (no system-wide daemons or configuration) and has no dependencies at all other than php itself - a DAMP for the Linux/VPS/OS X world, so to speak. I think something like that has real potential for getting newcomers rolling fast (who knows, we might snag the occasional Django/RoR dev too!).

znerol’s picture

Funny, I actually was looking for a webserver implemented in pure PHP in order to build a drush module around it, but was not able to find one. Until I threw together instaweb the other day, I had a solution with a shell script which did the exact same thing.

hailu’s picture

Owen this looks awesome!!

znerol’s picture

I really, really like this solution. It seems to work pretty well.

I've worked out a way to serve specific sites in a multisite setup: In order to trick drupals bootstrap process into picking up the correct site-configuration we need to ensure that HTTP_HOST and/or SERVER_NAME contain something that maps to the corresponding configuration directory. The obvious solution is to simply use the name of the configuration directory itself (e.g. default or example.com). We can easily obtain that using:

<?php $http_host = basename(drush_get_context('DRUSH_DRUPAL_SITE_ROOT')); ?>

Now drush -l http://example.com runserver will start a drupal instance with the correct site configuration.

The second problem is to get rid off the mangled HTTP_HOST as soon as drupal is bootstrapped such that links and css includes do not point at a live domain (example.com in this case). This is a bit harder, bat also manageable. We could do it by manually altering $base_url in settings.php such that it points to the address of our development server (running at e.g. hxxp://localhost:8888/). It is however possible to supply the ini-option auto_prepend_file pointing to a PHP script created on the fly with the corresponding code on the php-cgi command line. That's what is implemented in the version below.

It would be nicer if there was a way to set a global variable directly via the command line or the environment, like this we would not need to create the temporary file.

I've inlined the code, so it is easier to comment it.

<?php
function drush_core_runserver($port = '8888') {
  // Fabricate value for $base_url which gets injected into settings.php via
  // auto_prepend_file.
  $base_url = "http://localhost:$port";
  $prepend_php = '<?php $base_url="' . $base_url . '";?>';

  $prepend_path = tempnam(sys_get_temp_dir(), 'drush-runserver-');
  $prepend_fd = fopen($prepend_path, 'w');
  fwrite($prepend_fd, $prepend_php);
  fclose($prepend_fd);

  // Map HTTP_HOST and SERVER_NAME to last path component of site-root in order
  // to trick drupal into picking up the correct configuration.
  $http_host = basename(drush_get_context('DRUSH_DRUPAL_SITE_ROOT'));

  $server = new DrupalServer(array(
      'port' => $port,
      'serverid' => 'Drush runserver',
      'php_cgi' => 'php-cgi --define auto_prepend_file="' . $prepend_path . '"',
      'http_host' => $http_host,
  ));
  $server->run_forever();
}

class DrupalServer extends HTTPServer {
  public $http_host;

  function route_request($request) {
    $cgi_env = array();

    // Optain REMOTE_ADDR from socket
    $remote_addr = "";
    if (socket_getpeername($request->socket, $address)) {
        $cgi_env['REMOTE_ADDR'] = $address;
    }

    // Handle static files and php scripts accessed directly
    $uri = $request->uri;
    $doc_root = DRUPAL_ROOT;
    $path = $doc_root . $uri;
    if (is_file(realpath($path))) {
      if (preg_match('#\.php$#', $uri)) {
        // SCRIPT_NAME is equal to uri if it does exist on disk
        $cgi_env['SCRIPT_NAME'] = $uri;
        return $this->get_php_response($request, $path, $cgi_env);
      }
      return $this->get_static_response($request, $path);
    }

    // Rewrite clean-urls
    $cgi_env['QUERY_STRING'] = 'q=' . ltrim($uri, '/');
    if ($request->query_string != "") {
      $cgi_env['QUERY_STRING'] .= '&' . $request->query_string;
    }

    $cgi_env['SCRIPT_NAME'] = '/index.php';
    $cgi_env['HTTP_HOST'] = $this->http_host;
    $cgi_env['SERVER_NAME'] = $this->http_host;

    return $this->get_php_response($request, $doc_root . '/index.php', $cgi_env);
  }
}
?>
Owen Barton’s picture

This looks great - thanks for taking a look at this. I used auto_prepend_file() in Drubuntu (to provide a "global" $conf array) - very sneaky including it here as a php-cgi parameter! I am not sure we need to write out the file for each site though - we could add "$cgi_env['QUERY_STRING'] .= '&runserver_base_url=http://localhost:' . $port"; (or some other php accessible cgi variable of our choosing - ideally one that is not sourced from external data) to the routing code and then provide a static audo_prepended file that contains "$base_url = $_GET['runserver_base_url'];".

Other notes - I like the REMOTE_ADDR improvement, and I also noticed you fixed querystring handling (another todo I forgot to note here) - yay! I think I was using a more specific value than DRUSH_DRUPAL_SITE_ROOT though, which could avoid the basename. I think it is a context called DRUSH_DRUPAL_SITE or something similar (can check environment.inc).

znerol’s picture

Ah nice! It is also possible to inject $base_url via cgi environment, (e.g. $cgi_env['RUNSERVER_BASE_URL'] = $this->base_url somewhere in route_request) and extract that again in the static auto-prepend file using $base_url = $_SERVER['RUNSERVER_BASE_URL'];

I think the code fragment with REMOTE_ADDR should be submitted upstream, I'll go for that now.

Do you think that this command should go into drush core or would it make sense to maintain it in a third party module - instaweb in this case?

mightyiam’s picture

What I did to solve the issue for me is write the little myapache script:
https://launchpad.net/myapache

It runs apache 2 as a user. It has a simple config file. It does me good service :)

This is for Ubuntu and probably Debian.

znerol’s picture

I think the code fragment with REMOTE_ADDR should be submitted upstream, I'll go for that now.

Done & included: https://github.com/youngj/Envaya/issues/112

I had the chance to test the stuff under windows and it runs, albeit a little bit slow, but that also might be the virtual machine setup. This means that there is a way to run a development version of drupal using runserver and sqlite without having to setup apache and mysql on a windows machine. That might indeed lower the entry barrier for new devs and themers.

Owen Barton’s picture

Wow - that's pretty incredible that it runs on windows. Yay!

Owen Barton’s picture

FileSize
29.57 KB

Updated patch including znerol's fixes, and implementing the $base_url fix via an auto_prepend and a CGI environment variable. Apart from more tests, I think the only remaining todo is to add code to automatically fetch the httpserver.inc files - perhaps in a dedicated vendor drop directory as Mark suggested.

moshe weitzman’s picture

Once those todos are in, this looks commit worthy to me. A new /lib directory sounds good.

Nice work, folks.

Owen Barton’s picture

I was wondering about the location of a /lib directory - one possibility is to put it in the users ~/.drush directory, rather than the Drush root or include directory. This would make Drush upgrades much simpler, and also simplify sitewide/distro installations (where the user wouldn't have write permissions to the Drush directory) , but I am concerned it may also be confusing for users having scattered files. We could also have some kind of drushrc style fallback system where we write wherever we have access and check multiple places when trying to include library files - not sure that is really necessary though. Any thoughts?

znerol’s picture

We also might contact the author and ask him if he'd be willing to release the code under two licenses (MIT and GPL).

greg.1.anderson’s picture

The other thing we need to worry about if we move lib outside of the drush root is the potential for different versions of drush to step on each other with libs. Right now it is simple enough; we use the same table.inc and should use the same http lib for all major versions of drush (that need them), but it is a consideration.

We could also consider using #1172044: Cache command files / Add drush cache API to store these includes, but doing so might require some small adjustment to the API, which currently is storing data, not files. Don't know if that is worthwhile.

Regarding #17, if we just checked in the libs, that would fork them (unless we were careful to never make modifications), and I'm not sure that is good. I also think we should handle the http server and the console table the same way, which means we would need permission from two sources.

In the short term, I think that storing lib at the drush root is sufficient.

Owen Barton’s picture

That's a good point - we could get around the conflicting libs by versioning the library directories - i.e. ~/.drush/lib/Console_Table-1.1.4/, but I agree that we should probably just go with /lib in the Drush root for the time being.

In terms of committing the code, it is against Drupal.org Git rules to commit 3rd party php code libraries - even if they are GPL licensed - See http://drupal.org/node/422996.

perusio’s picture

Why not treat this in a more generic way. I can write a similar script for Nginx that boostraps a dev version of the site.

It's something that newbies have problems with since the most practical way of working with Nginx locally is by using different ports for different sites.

Would you be interested? Or should I start a sandbox project for it?

Owen Barton’s picture

Given that there is not really much code in the current command and the way we interact with the php http server is pretty specific, I am not sure there is much benefit in adding an abstraction layer here. I think for real newbies the pure php server is about as easy as it can get - and folks with enough experience to want a ngnix/httpd/lighttpd will be OK dl'ing a separate commandfile for this. Perhaps it could hook into the same command-name as this somehow, or just use a derivative (runserver-ngnix, for example).

Owen Barton’s picture

For reference, looks like the httpserver code has moved into it's own repo at https://github.com/youngj/httpserver, which is helpful for us.

boombatower’s picture

subscribe.

chx’s picture

yes, yes, YES!

znerol’s picture

There is a minor annoyance with the current implementation of httpserver. Because non-blocking io is only employed client-side but not in the code dispatching requests towards cgi-scripts, no two requests may be handled concurrently. Consequently when drupal tries to determine whether the net is reachable by connecting to itself, the request will time out which leads to a delay when loading admin-pages. Until this is fixed we need to instruct users to put $conf['drupal_http_request_fails'] = FALSE; to their settings.php. Unfortunately it is not possible to inject this variable analogus to $base_path.

moshe weitzman’s picture

We could just append that $conf instruction to the bottom of settings.php if needed. Thats usually a bad idea, but this is a relatively safe change IMO. We might need to bootstrap to CONFIG phase in order to know if this is needed.

settings.php won't always be writable but we'll do what we can.

greg.1.anderson’s picture

My dev / stage / live workflows always involve using a separate settings.php for each site anyway, so I think that as long as we warn before changing it and include a comment in the modified file, #25 / #26 is okay.

Owen Barton’s picture

Actually there is a sneaky way to inject $conf pre-bootstrap I discovered for Drubuntu - I'll try and roll a patch soon that includes this and the /inc stuff.

moshe weitzman’s picture

Status: Needs review » Needs work

call to undefined function socket_create()

We should check for presence of php socket extension. I don't have it, and don't see how to get it using macports. Is this extension typically present?

Owen Barton’s picture

PHP needs to be compiled with --enable-sockets - I'll add a check for this.

chx’s picture

znerol’s picture

#25 is resolved. The author just released a version which also does not block on cgi-bin side using a stream wrapper. In the same turn he switched over from socket api to stream api, therefore #28 - #31 might be resolved also.

It might be possible however that $conf['drupal_http_request_fails'] = FALSE; still is required on windows, but I'm not able to confirm that right now.

msonnabaum’s picture

You can also get it by downloading the php source, doing ./configure --with-sockets=shared; make and copy modules/sockets.so into your extensions dir.

Owen Barton’s picture

Goodness - look at this: https://github.com/youngj/httpserver/commit/9fa27f503c79308dedd5eb4667b3...

no longer requires php sockets extension
Jesse Young (author)
16 minutes ago

and also this (which might resolve the drupal_http_request_fails issue - although I have a workaround for that already, which we still need to Windows) - https://github.com/youngj/httpserver/commit/fe3924526c653b580e4d9c465a4a...

allow HTTPResponse objects to specify content as a stream, instead of loading the entire response into memory as a string. allows multiple connections to process responses concurrently (although windows will block on php requests)
Jesse Young (author)
about 4 hours ago

Jesse - are you reading this thread somehow, or just trying to prove synchronicity theories ;)

It does still need php 5.3 (pretty reasonable, given what it is doing) - so I will keep the check for that. Patch on it's way.

adunar’s picture

@owen: haha, yes, znerol told me about this thread, and inspired the change to avoid blocking on php-cgi responses.

-Jesse

Owen Barton’s picture

FileSize
16.7 KB

@adunar - happy you found us - as you can see, we are big fans of your work here :)

@all - have done quite a bit of work on this tonight - very excited about how this is shaping up.

Attempt at summary of the changes below:

- Check for and create (if needed the) lib directory on bootstrap. I also added an option for the lib directory and .git.ignore to cover /lib in the default location.

- Moved table.inc to /lib - this involved quite a bit of refactoring of the table function (it was getting a silly number of nested ifs) so it can use a standard download function it shares with pm release history, and also move over the table.inc file from includes. Thinking about upgrades (and potentially multiple drush versions sharing a systemwide lib) I put table.inc in a versioned subdirectory, and also kept the capital T, so it matches the tarball better.

- I added code to make a git clone of httpserver and checkout a specified revision. I figure git is getting pretty much ubiquitous, and managing upgrades is much easier than with tarballs (and hence we don't need to version the /lib subdirectory for httpserver. Given the pace it is moving, we may want to upgrade fairly frequently until we release Drush 5.0.

- The lib code could probably use some more abstraction if we start adding more libraries, but I didn't want to go overboard given that we only have 2 right now. If we add more we may also want to add a dedicated "drush lib-install" type command, rather than lazy loading as we do now, to make lives easier for distro maintainers and sysadmins who want Drush non-writable.

- I added an attribution for the httpserver library - probably not technically needed since we don't distribute, but I would like to acknowledge Jesse and Envaya's great work here.

- I added arguments for address and port to bind to ($addr was just added to httpserver). I used the "addr:port" format, since I think that is pretty intuitive, is the same as Django runserver, and means you can easily have an optional address (port only) without needing to switch the args the wrong way.

- I also added an option for the php-cgi, since on some systems it might live somewhere else, or not be on the users path etc. You can add params in here too.

- I added an argument to allow you to inject $conf overrides into the running site. This adds the $conf['drupal_http_request_fails'] = FALSE; by default, but you could extend it pretty easily (in a drushrc/alias, not on the command line, since it is a key-value array) to fix up variables you like for development, for example disabling css aggregation.

- I added validation for php 5.3 and findable php-cgi.

- Our class is now in a separate file, since we have a configurable (and hence conditionally included) httpserver.php that it depends on.

Before I forget, I thought it would be really interesting (for Drush, Drupal and the httpserver project!) to see how simpletest runs on this platform - it would be a good way to dig up some edge cases in the 3 projects.

We could also continue the experiment I started with bootstrapping Drupal directly in Drush (not as a CGI) calling menu_execute_active_handler(); and feeding the response and headers into the response object. This should be reasonably possible with Drupal 7 static resets, and may have some interesting performance use cases. When I tried the main problem I had was duplicate headers on the second request, reset wasn't working for whatever reason. Probably many other issues too :)

Owen Barton’s picture

Status: Needs work » Needs review
moshe weitzman’s picture

Looking great. Just did a code review, without testing ...

On updating, we do git fetch and git checkout. Should we not do a merge instead so we preserve any local changes? Is a git checkout really desirable here? Will it not make life a bit more complicated for folks who checkout drush via git as well?

It looks like you added a file_exists() call in drush_download_file() which acts as a cache of sorts. I do something similar at #1173776: Optionally cache release XML and tarballs for pm-download and pm-updatecode. The latter of these these two patches will need a reroll I think.

Owen Barton’s picture

Yeah - I am having second thoughts about the git checkout also. It shouldn't make any difference for people who checkout Drush via git, I think - git just ignores it and Drush will update it to the right revision (I think you are right a merge would be preferable too). However, I don't think git is quite as ubiquitous as we would like and I think a tarball method may be necessary, especially since the point of this is to minimize dependencies needed to run Drupal. I would kind of like to have some kind of git option too though - if nothing else it makes sending patches upstream so much easier. One idea that would keep the code simple would be to use a git submodule for this (and recommend people use --recursive when git cloning drush), and then fallback to a tarball if the submodule directory is not present.

moshe weitzman’s picture

Lets skip the submodule please. I really think we just need an http fetch. Upstream contributors can replace their files with a checkout if they want.

Owen Barton’s picture

You mean download/extract the tarball, or fetching files individually? Do you have any thoughts about version numbers in directory names or no (considering updates etc)?

moshe weitzman’s picture

fetching tarball i think. sorry, i was unclear. yeah, the library upgrade thingie you built looks useful to me.

also, i forgot to mention that runserver should be a top level folder under commands, not under 'core'. my .02

Owen Barton’s picture

Tarball sounds good to me - we already have the code to do that in the wget package handler - I can just pull it out to general purpose function.

In terms of top level command v.s. core, I don't have any strong opinions. the "core" group is certainly getting pretty crowded, but on the other hand a group of 1 seems a bit odd too.

greg.1.anderson’s picture

Maybe if we renamed it to something like 'run-site' or 'site-server' or some variant thereof, we could put it in commands/site along with site-install.drush.inc and upgrade.drush.inc.

Owen Barton’s picture

FileSize
20.58 KB

Updated patch that uses tarballs for both libraries - I think this is the way to go. I abstracted out the code from the wget handler into 2 fairly low level functions, and added a higher level "library getter" function that both libraries can use. Tested each of the download methods (added a flag so curl follows redirects), also dl with wget, non-writable /lib and a few other scenarios.

I didn't reorganize the core command grouping yet - seems like that might be a good thing to explore at our code sprint...anyone got index cards :)

adunar’s picture

BTW, commit 354b9142 fixes a bug introduced yesterday that caused all PHP requests to show up as HTTP status 200 in the server log:
https://github.com/youngj/httpserver/commit/354b9142bf0cfd73063e28604d11...

If anyone wants to contribute to the httpserver project directly, you're welcome to fork it and submit pull requests via github. I think unit tests are probably its biggest need at the moment. (Or if somebody could figure out how to run multiple concurrent php-cgi processes on Windows without blocking.)

moshe weitzman’s picture

There are a few files in the runserver folder (not the lib folder). I think a top level command folder makes sense.

Note that drush_download_file() was committed yesterday along with wget changes so this likely needs to deal with some conflicts.

Owen Barton’s picture

Issue tags: +Needs tests
FileSize
25.51 KB

Attached patch resolves conflicts with HEAD, including some bits of refactoring/simplification of the download and tarball functions that made sense as I worked through it. I tested the pm/wget and caching functionality as well as the library fetch function, and both seem to be working as they should. I moved the runserver command to it's own directory - it is a tad unusual as the command name is the same as the commandfile group name - it seems to work though, not sure if it is confusing from a user point of view. I have bumped the httpserver library to 354b9142.

Owen Barton’s picture

FileSize
25.58 KB

Fixes an issue with --drupal-project-rename for dl.

Owen Barton’s picture

After some more testing I committed this. Leaving open, since I think this needs some tests (especially the download/tar/lib functions, since they have several consumers.

Owen Barton’s picture

Component: Core Commands » Tests
Category: feature » task
Status: Needs review » Needs work

Fixing categories.

Also, I posted a proof of concept based on the ideas in #5 at #1200738: Command to get started with Drupal with zero configuration.

Owen Barton’s picture

I have been working on making the Drupal core tests pass, since that is a really exhaustive test for this command (not that we shouldn't have some basic tests ourselves of course). I just committed one fix to runserver that makes cron.php and index.php (when called directly) work correctly. I patched another issue with httpserver which is at https://github.com/youngj/httpserver/issues/1. I need to let all the tests run through again, but it's looking pretty good.

I am still interested in figuring out if we can bootstrap Drupal from Drush (rather than via php-cgi) and call it within DrupalServer->route_request() - probably calling a subclassed HTTPResponse that adds a get_drupal_response() method. The main complexity is passing headers back and forth, and ensuring we have a clean (enough) environment for subsequent requests (we don't want to send double cookie headers etc). This could be via a pctl_fork, or via some kind of command that works through invoke_process, perhaps passing the page and headers via JSON (no need to parse to HTTP and back, since we own both ends). If anyone wants to experiment with this some more, please feel free :)

moshe weitzman’s picture

proc_open seems preferable over pcntl_fork. see #771448: Use proc_open() instead of pcntl_fork() in simpletest.

Owen Barton’s picture

Note for reference that there are a few major improvements to runserver at #1200738: Command to get started with Drupal with zero configuration (that should help make this more testable) as well as an experimental non php-cgi page delivery approach at #1248030: Alternative(s) to php-cgi with runserver

RobLoach’s picture

Submitted a PR to HTTPServer as well: https://github.com/youngj/httpserver/pull/4

PHP 5.4 also comes with a built-in web server: http://www.php.net/manual/en/features.commandline.webserver.php

greg.1.anderson’s picture

Version: » 8.x-6.x-dev
Status: Needs work » Closed (won't fix)
Issue tags: +Needs migration

This issue was marked closed (won't fix) because Drush has moved to Github.

If desired, you may copy this task to our Github project and then post a link here to the new issue. Please also change the status of this issue to closed (duplicate).

Please ask support questions on Drupal Answers.