lighttpd hackage: Smart module loading

February 21, 2011

I recently upgraded from Debian 5.0 to Debian 6.0, and it went great, except that lighttpd began to complain “Cannot load plugin mod_redirect more than once”. This is because the new default lighttpd configuration shipped with Debian 6.0 includes the ‘mod_redirect’ module, which was not included by default in Debian 5.0. The obvious solution to this problem would be to edit my own configuration and stop loading mod_redirect in my own configuration file, but this solution did not sit well with me.

It seems to me that it should be possible for any configuration file to specify the modules it requires, without depending on other config files. Sadly, lighttpd doesn’t support this mode of operation natively, which is why I came up with this short Perl script to dynamically load a module in lighttpd only when it’s not already loaded:

#!/usr/bin/perl
use strict;
use warnings;
use Proc::Pidfile;
my $PID;
eval { $PID = Proc::Pidfile->new };
exit if $@ and $@ =~ /already running/;
die "$@\n" if $@;
my $ppid = getppid;
my @blacklist;
my $bl_file = "/tmp/lighttpd_load_modules.${ppid}";
# See if the BL file exists, and is less than 5 seconds old
if ( -e $bl_file and (stat(_))[9] > time-5 ) {
    open (my $BL, $bl_file);
    @blacklist = split / /,<$BL>;
    close $BL;
}
open my $CF, "/usr/sbin/lighttpd -p -f /etc/lighttpd/lighttpd.conf |";
my $conf;
{
    local $/;
    $conf = $1 if <$CF> =~ /server.modules\s+=\s+\((.*?)\)/ms;
}
close $CF;
my %to_load;
@to_load{@ARGV} = ();
while ( $conf =~ /"(.*?)"/mg ) {
    delete $to_load{$1};
}
delete @to_load{ @blacklist };
exit unless %to_load;
print "server.modules += (\n";
print join( ",\n", map { "\t\"$_\"" } keys %to_load );
print "\n)\n";
open my $BL, "> $bl_file";
print $BL join(' ',@blacklist,keys %to_load);
close $BL;

To use this script, install it on your system somewhere (for instance as /usr/local/lighttpd_load_modules), then in your lighttpd configuration, and remove any lines that say server.modules += ( "mod_foo" ). Then replace them with include_shell "/usr/local/lighttpd_load_modules mod_foo mod_bar". For example:

My old configuration:

server.modules += (
    "mod_expire",
    "mod_fastcgi",
    "mod_redirect",
    "mod_rewrite",
    "mod_alias"
)

My new configuration:

include_shell "/usr/local/bin/lighttpd_load_modules mod_expire mod_fastcgi mod_redirect mod_rewrite mod_alias"

How the script works

The perl script takes advantage of lighttpd’s -p flag, which prints out the current configuration. It looks at the -p output, to see if the requested module is already loaded, and if it is it just exits without printing any output. If it’s not already in the server.modules list, however, it outputs the configuration necessary to add it.

The script calls lighttpd -p, which has to parse all the config files to create it’s output. This means that lighttpd -p ends up calling the script… which ends up calling lighttpd -p again… it would create a vicious infinite loop if it weren’t for the use of the Proc::Pidfile Perl module. You can use any locking mechanism you wish, if you don’t want to depend on an external Perl module, but the important part is that the script must exit (without any output) if it’s already running!

Note that this method of dynamic module loading can add up to several seconds to the lighttpd startup time, since it ends up spawning several extra processes during the start up process. For each time you use this script, lighttpd must:

  • Run /usr/local/bin/lighttpd_load_module, which then
  • Runs lighttpd -p, which then<
  • Runs /usr/local/bin/lighttpd_load_module, which then exits

Additionally, the script writes a temporary file, named with the parent process’s PID (this is so it’s easy to tell the difference between multiple runs of the script). This temporary file stores a list of modules that it detected ought to be loaded. This is so that if you use the script in multiple places in your configuration, the various calls of the script have some way to communicate with each other, so they don’t all try to load the same modules.