#!/usr/local/bin/perl use strict; use warnings; =head1 NAME site-mason_handler.fcgi - run Mason on shared host as FastCGI =head1 COPYRIGHT & LICENSE Copyright 2006-2010 Joe Pepersack (joe@pepersack.net) This program is free software; you may use it under the terms of the Creative Commons Attribution-Share Alike 3.0 United States License =head1 SYNOPSIS FastCGI handler script to use Mason on a shared hosting provider, such as Dreamhost. It is assumed that this script will reside in $docroot/cgi-bin/ If you are running multiple Mason sites under the same username, it is useful to give each handler a distinct name so that you can use ps or top to determine which process belongs to which site Some additional configuration is needed in .htaccess in order to use this. At a minimum you need something like: AddHandler fastcgi-script .fcgi Action html-mason /cgi-bin/site-masonhandler.fcgi SetHandler html-mason The following is highly recommended as well: Order allow,deny Deny from all Note while this is geared towards using FastCGI it will work just fine under regular CGI. Just change the AddHandler line in .htacces to: AddHandler cgi-script .fcgi This is highly useful when you are debugging, because under FastCGI, some errors will cause the browser to hang for 60 seconds until the session times out. Using CGI avoids this; revert to fastcgi when your bugs are squashed. Unfortunatly, dhandler processing is borked on dreamhost - requests for non-existant files get 404'ed before the mason handler ever fires. This means we can't simply use $mason->handle_cgi_object($cgi); To work around this, we can use mod_rewrite to send any request for anything that is not a file and not a directory to a dummy page. If you want dhandlers to work, the following is required in .htaccess: RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /dummy.html [L] Because this workaround breaks DirectoryIndex handling (which works correctly otherwise), the root dhandler should be something like: <%perl> my $page = $r->path_info; $page .= 'index.html' if $page =~ /\/$/; if ( $m->comp_exists($page) ) { $m->redirect($page); } else { $m->redirect('/error/404.html'); } This can be overridden in any subdirectory by giving it a 'real' dhandler You could use $m->comp instead of $m->redirect for index pages, but this breaks down if you have methods defined in your autohandler that you want to override in the target component. If you used $m->comp, the autohandler would inherit it's methods from the dhandler instead of the target component. As of mid-2008, Dreamhost has Mason pre-installed, at least on my host (Eastman), as well as many other CPAN modules. If you use any 'unsupported' cpan modules or a custom module (EG catalyst), you will have to add your module directory to @INC yourself. YMMV if you're on a different server. If you're on a private server, you should probably be using mod_perl instead of FastCGI anyway. =head1 SEE ALSO The documentation for CGI::Fast and HTML::Mason =cut # --------------------------------------------------------------------------- # #### CHANGE THIS FOR YOUR SITE #### my $home = '/home/yourname'; # ~ on your Dreamhost account my $site = 'yoursite.tld'; # the name of your site # any changes beyond this point are optional my $mason_root = "$home/$site/"; # (usually docroot) my $mason_data = "$home/.masondata-$site/"; # (should not be under docroot) my $logfile = "$home/mason-${site}.log"; # name of custom log or undef # if $logfile = undef, log messages will go to the apache errorlog # flags for optional features my $verbosity = 1; # enable custom logging level 0 - N my $usedb = 0; # create and use global db handle # module dependencies use CGI::Fast qw| :standard -compile :all -tabindex -utf8 |; use CGI::Carp; use HTML::Mason; use HTML::Mason::CGIHandler; use Time::HiRes qw| time gettimeofday |; use POSIX qw| strftime |; use DBI; # add any additional CPAN or custom modules your app requires here # use My::Module; # turn logging on if so configured if ( $verbosity > 0 && defined($logfile) ) { open LOG, '>>', $logfile; LOG->autoflush(1); } logmsg( 'Starting FastCGI session', 0 ); # globals variables to share with mason # using $::VAR is safe under FastCGI; mod_perl, not so much $::DBH = init_db(); # persistent database handle $::STATS = { init_time => now() }; # time this session started # variables defined under the mason_globals config option are global to Mason # but are not shared between Mason and the CGI wrapper # the values would likely be set in a <%once> section in a syshandler object. #my ($var); #$::MASON_GLOBALS = [ qw( $var ) ]; $::MASON_GLOBALS = []; my $mason = init_mason(); # Persistent HTML::Mason::CGIHandler object # This runs once per request. # CGI::Fast->new blocks until a # request comes in. # If Apache sees that all existing handlers are still working when a new # request comes in, it will spawn a new handler process. while ( my $cgi = CGI::Fast->new ) { my $start = time(); charset('utf-8'); # The dhandler workaround hack sets $ENV{PATH_INFO} to /dummy.html # if the user requests anything nonexistent, so we need to restore # the "real" value, which is in $ENV{REQUEST_URI} my $real_uri = $ENV{REQUEST_URI}; ++$::STATS->{hits}; ++$::STATS->{views}{$real_uri}; logmsg( "Processing request $real_uri", 3 ); if ( $real_uri eq '/dummy.html' ) { # if someone actually requested the dummy, give them an error $ENV{PATH_INFO} = '/error/404.html'; # be sure this exists!!! } else { # otherwise, revert to what was actually requested $ENV{PATH_INFO} = $real_uri; } # and pass the buck to mason $mason->handle_request(); # finally, spit out some benchmarking data; my $ms = elapsed_time($start); $::STATS->{totaltime} += $ms; logmsg( sprintf( '%6.2f ms %s', $ms, $real_uri ), 1 ); } END { # Since mod_fastcgi manages processes via signals, this will never get # run if we're runing under the fastcgi-script handler. Therefore, # if we absolutely, positively need to run some cleanup code before # exiting, we need to use signal handling to catch PIPE, TERM, and USR1. # See: http://www.fastcgi.com/docs/faq.html for more info logmsg( 'Shutting down', 0 ); $::DBH->disconnect if $usedb; close LOG; } # --------------------------------------------------------------------------- # sub init_mason { # initialize the mason object my $h; eval { logmsg( 'Initializing Mason CGIHandler', 2 ); $h = HTML::Mason::CGIHandler->new( comp_root => $mason_root, data_dir => $mason_data, allow_globals => $::MASON_GLOBALS, error_mode => "output", default_escape_flags => "h" ); } or do { croak "Could not create mason cgihandler: $@"; }; return $h; } # --------------------------------------------------------------------------- # sub init_db { # create database handle return undef unless $usedb; # bail if db disabled logmsg( 'initializing db handle', 2 ); # read db config from file. This is a single line, pipe delimited # DatabaseName|HostName|UserName|Password open CRED, '<', "$home/.dbcredentials"; my $cred = ; chomp($cred); my ( $db, $host, $username, $password ) = split( /\|/, $cred ); my $dsn = "DBI:mysql:database=$db;host=$host"; close CRED; return ( DBI->connect_cached( $dsn, $username, $password ) ); } # --------------------------------------------------------------------------- # sub now { # get the system time and return it in ISO format (yyyy-mm-dd 23:59) return ( strftime( "%Y-%m-%d %T", localtime() ) ); } # --------------------------------------------------------------------------- # sub elapsed_time { # given a start time, return the elapsed time in milliseconds my $start = shift; return sprintf "%.3f", ( ( time() - $start ) * 1000 ); } # --------------------------------------------------------------------------- # sub logmsg { my ( $message, $level ) = @_; $level = 1 unless defined($level); # Write a message to the configured log if the message level is at the # current verbosity level or higher. # # This extra layer of logging may seem redundant, but many years spent in # systems operations have taught me the value of instrumenting your code # (particuarly when you get paged at 3am to resolve an issue) return if $level > $verbosity; # bail immediately if not verbose enough if ( defined $logfile ) { my $ts = now(); print LOG "[$ts] $message\n"; } else { carp($message); } } # --------------------------------------------------------------------------- #