Revision: 111 Author: matt Date: 2006-08-23 16:55:00 +0000 (Wed, 23 Aug 2006)
Log Message: ----------- More AIO fixes and updates Make keep-alive work Added shutdown to console Modified Paths: -------------- trunk/lib/AxKit2/Client.pm trunk/lib/AxKit2/Connection.pm trunk/lib/AxKit2/Console.pm trunk/lib/AxKit2/Plugin.pm trunk/plugins/aio/uri_to_file trunk/plugins/uri_to_file Added Paths: ----------- trunk/plugins/aio/serve_file Modified: trunk/lib/AxKit2/Client.pm =================================================================== --- trunk/lib/AxKit2/Client.pm 2006-08-22 22:22:18 UTC (rev 110) +++ trunk/lib/AxKit2/Client.pm 2006-08-23 16:55:00 UTC (rev 111) @@ -102,10 +102,8 @@ my @hooks; MAINLOOP: for my $plugin ($conf->plugins) { - my $plug = plugin_instance($plugin) || next; - for my $h ($plug->hooks($hook)) { - push @hooks, [$plugin, $plug, $h]; - } + my $plug = $PLUGINS{$plugin} || next; + push @hooks, map { [$plugin, $plug, $_] } $plug->hooks($hook); } $self->_run_hooks($hook, [EMAIL PROTECTED], [EMAIL PROTECTED]); @@ -153,6 +151,7 @@ return $meth->($self, $r[0], $r[1], @$args); } } + return @r; } sub log { @@ -212,6 +211,23 @@ } } +sub hook_write_body_data { + my $self = shift; + my ($ret) = $self->run_hooks('write_body_data'); + if ($ret == CONTINUATION) { + die "Continuations not supported on write_body_data"; + } + elsif ($ret == DECLINED) { + return; + } + elsif ($ret == OK || $ret == DONE) { + return 1; + } + else { + $self->default_error_out($ret); + } +} + sub hook_post_read_request { my $self = shift; $self->run_hooks('post_read_request', @_); @@ -329,6 +345,7 @@ } elsif ($ret == OK) { $out->output($self) if $out; + $self->write(sub { $self->http_response_sent() }); } else { $self->default_error_out($ret); @@ -389,16 +406,19 @@ # stolen shamelessly from httpd-2.2.2/modules/http/http_protocol.c sub default_error_out { my ($self, $code, $extras) = @_; - + $self->headers_out->code($code); - $self->headers_out->header('Content-Type', 'text/html'); - $self->send_http_headers; if ($code == NOT_MODIFIED) { + $self->send_http_headers; # The 304 response MUST NOT contain a message-body return; } + $self->headers_out->header('Content-Type', 'text/html'); + $self->headers_out->header('Connection', 'close'); + $self->send_http_headers; + $self->write("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n" . "<HTML><HEAD>\n" . "<TITLE>$code ".$self->headers_out->http_code_english."</TITLE>\n" . Modified: trunk/lib/AxKit2/Connection.pm =================================================================== --- trunk/lib/AxKit2/Connection.pm 2006-08-22 22:22:18 UTC (rev 110) +++ trunk/lib/AxKit2/Connection.pm 2006-08-23 16:55:00 UTC (rev 111) @@ -36,8 +36,10 @@ sock_closed pause_count continuation + keep_alive_count ); +use constant KEEP_ALIVE_MAX => 100; use constant CLEANUP_TIME => 5; # every N seconds use constant MAX_HTTP_HEADER_LENGTH => 102400; # 100k @@ -56,6 +58,7 @@ $self->{closed} = 0; $self->{ditch_leading_rn} = 0; # TODO - work out how to set that... $self->{server_config} = $servconf; + $self->{keep_alive_count} = 0; $self->{notes} = {}; $self->log(LOGINFO, "Connection from " . $self->peer_addr_string); @@ -112,7 +115,7 @@ sub max_connect_time { 180 } sub event_err { my AxKit2::Connection $self = shift; $self->close("Error") } sub event_hup { my AxKit2::Connection $self = shift; $self->close("Disconnect (HUP)") } -sub close { my AxKit2::Connection $self = shift; $self->{sock_closed}++; $self->SUPER::close(@_) } +sub close { my AxKit2::Connection $self = shift; $self->{sock_closed}++; $self->{notes} = undef; $self->SUPER::close(@_) } sub event_read { my AxKit2::Connection $self = shift; @@ -171,6 +174,22 @@ $self->hook_post_read_request($self->{headers_in}); } +sub event_write { + my AxKit2::Connection $self = shift; + $self->{alive_time} = time; + + if ($self->hook_write_body_data) { + return; + } + + # if hook_write_body_data didn't want to send anything, we just pump + # whatever's in the queue to go out. + if ($self->write(undef)) { + # Everything sent. No need to watch for write notifications any more. + $self->watch_write(0); + } +} + sub headers_out { my AxKit2::Connection $self = shift; @_ and $self->{headers_out} = shift; @@ -201,6 +220,19 @@ $self->{headers_out} = AxKit2::HTTPHeaders->new_response; $self->{headers_out}->header(Date => http_date()); $self->{headers_out}->header(Server => "AxKit-2/v$AxKit2::VERSION"); + if ($hd->header('Connection') =~ /\bkeep-alive\b/i) { + # client asked for keep alive. Do we? + $self->{keep_alive_count}++; + if ($self->{keep_alive_count} > KEEP_ALIVE_MAX) { + $self->{headers_out}->header(Connection => 'close'); + } + else { + $self->{headers_out}->header(Connection => 'Keep-Alive'); + $self->{headers_out}->header('Keep-Alive' => + "timeout=" . $self->max_idle_time . + ", max=" . (KEEP_ALIVE_MAX - $self->{keep_alive_count})); + } + } # This starts off the chain reaction of the main state machine $self->hook_uri_translation($hd, $hd->request_uri); @@ -212,6 +244,8 @@ sub http_response_sent { my AxKit2::Connection $self = $_[0]; + $self->log(LOGDEBUG, "Response sent"); + return 0 if $self->{sock_closed}; # close if we're supposed to @@ -240,7 +274,7 @@ # now since we're doing persistence, uncork so the last packet goes. # we will recork when we're processing a new request. # TODO: Disabled because this seemed mostly relevant to Perlbal... - #$self->tcp_cork(0); + $self->tcp_cork(0); # reset state $self->{alive_time} = $self->{create_time} = time; @@ -248,6 +282,7 @@ $self->{headers_in} = undef; $self->{headers_out} = undef; $self->{http_headers_sent} = 0; + $self->{notes} = {}; # NOTE: because we only speak 1.0 to clients they can't have # pipeline in a read that we haven't read yet. Modified: trunk/lib/AxKit2/Console.pm =================================================================== --- trunk/lib/AxKit2/Console.pm 2006-08-22 22:22:18 UTC (rev 110) +++ trunk/lib/AxKit2/Console.pm 2006-08-23 16:55:00 UTC (rev 111) @@ -314,6 +314,10 @@ $self->write($output); } +sub cmd_shutdown { + Danga::Socket->SetPostLoopCallback(sub { 0 }); +} + # Cleanup routine to get rid of timed out sockets sub _do_cleanup { my $now = time; Modified: trunk/lib/AxKit2/Plugin.pm =================================================================== --- trunk/lib/AxKit2/Plugin.pm 2006-08-22 22:22:18 UTC (rev 110) +++ trunk/lib/AxKit2/Plugin.pm 2006-08-23 16:55:00 UTC (rev 111) @@ -25,7 +25,7 @@ # DON'T FORGET - edit "AVAILABLE HOOKS" below. our @hooks = qw( logging connect pre_request post_read_request body_data uri_translation - mime_map access_control authentication authorization fixup + mime_map access_control authentication authorization fixup write_body_data xmlresponse response response_sent disconnect error ); our %hooks = map { $_ => 1 } @hooks; Added: trunk/plugins/aio/serve_file =================================================================== --- trunk/plugins/aio/serve_file 2006-08-22 22:22:18 UTC (rev 110) +++ trunk/plugins/aio/serve_file 2006-08-23 16:55:00 UTC (rev 111) @@ -0,0 +1,181 @@ +#!/usr/bin/perl -w + +# Copyright 2001-2006 The Apache Software Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +=head1 NAME + +serve_file - Plugin for serving raw files + +=head1 SYNOPSIS + + Plugin serve_file + +=head1 DESCRIPTION + +This plugin turns AxKit2 into a normal every-day httpd. Yay! + +Most httpds need to serve plain files. Things like favicon.ico and robots.txt +that any sane web server would be lost without. So just load this plugin after +all the others, and if your other plugins DECLINE to deliver the content, this +kind little plugin will happily deliver your file without making any changes +to it whatsoever. Ain't that nice? + +=head1 CONFIG + +None. + +=cut + +use AxKit2::Utils qw(http_date); + +sub register { + my $self = shift; + $self->register_hook('response' => 'hook_response1'); + $self->register_hook('response' => 'hook_response2'); +} + +sub hook_response1 { + my ($self, $hd) = @_; + + my $ct = $hd->mime_type; + + # set default return value + $self->client->notes('serve_file_retcode', DECLINED); + + my $client = $self->client; + + if ($hd->request_method eq 'GET' || $hd->request_method eq 'HEAD') { + # and once we have it, start serving + $self->client->watch_read(0); + + my $file = $hd->filename; + $self->log(LOGINFO, "Serving file: $file"); + + IO::AIO::aio_stat($file, sub { + #print "STAT returned\n"; + if (!-e _) { + $client->notes('serve_file_retcode', NOT_FOUND); + return $client->finish_continuation; + } + + # we only serve files here... + if (!-f _) { + $client->notes('serve_file_retcode', BAD_REQUEST); + return $client->finish_continuation; + } + + my $mtime = http_date((stat(_))[9]); + my $ifmod = $client->headers_in->header('If-Modified-Since') || ""; + + my $ifmod_len = 0; + if ($ifmod =~ s/; length=(\d+)//) { + $ifmod_len = $1; + } + + my $modified = $ifmod ? ($ifmod ne $mtime) : 1; + + my $size = -s _; + + $modified++ if $ifmod_len && $ifmod_len != $size; + + if (!$modified) { + $client->notes('serve_file_retcode', NOT_MODIFIED); + return $client->finish_continuation; + } + + $client->headers_out->header("Last-Modified", $mtime); + $client->headers_out->header("Content-Length", $size); + $client->headers_out->header("Content-Type", $ct); + + $client->send_http_headers; + + $client->notes('serve_file_retcode', OK); + + if ($hd->request_method eq 'HEAD') { + return $client->finish_continuation; + } + + IO::AIO::aio_open($file, 0, 0, sub { + #print "OPEN returned\n"; + my $fh = shift; + + if ($client->{closed}) { + return CORE::close($fh); + } + + if (!$fh) { + $client->notes('serve_file_retcode', SERVER_ERROR); + return $client->close('aio_open_failure'); + } + + $client->notes('serve_file_bytes_remaining', $size); + + $client->watch_write(1); + + my $send_sub = sub { + my $remaining = $client->notes('serve_file_bytes_remaining'); + #print "sending $remaining bytes...\n"; + if ($remaining <= 0) { + CORE::close($fh); + $client->watch_write(0); + return $client->finish_continuation; + } + IO::AIO::aio_sendfile($client->sock, $fh, + ($size - $remaining), $remaining, + sub { + my $sent = shift; + return unless $sent >= 0; + my $r = $client->notes('serve_file_bytes_remaining'); + $r -= $sent; + $client->notes('serve_file_bytes_remaining', $r); + $client->notes('serve_file_ready_for_more', 1); + }); + $client->notes('serve_file_ready_for_more', 0); + }; + + $client->notes('serve_file_ready_for_more', 1); + $client->notes('serve_file_send_sub', $send_sub); + }); + + return; # we're not done until aio_open is done... + }); + + return CONTINUATION; + } + + return DECLINED; +} + +sub hook_response2 { + my $self = shift; + return $self->client->notes('serve_file_retcode'); +} + +sub hook_write_body_data { + my $self = shift; + return OK unless $self->client->notes('serve_file_ready_for_more'); + my $sub = $self->client->notes('serve_file_send_sub'); + $sub->(); + if ($self->client->notes('serve_file_bytes_remaining')) { + return OK; + } + else { + $self->client->watch_write(0); + # close the circular reference... + $self->client->notes('serve_file_send_sub', undef); + return DONE; + } +} Modified: trunk/plugins/aio/uri_to_file =================================================================== --- trunk/plugins/aio/uri_to_file 2006-08-22 22:22:18 UTC (rev 110) +++ trunk/plugins/aio/uri_to_file 2006-08-23 16:55:00 UTC (rev 111) @@ -77,6 +77,10 @@ $uri = uri_decode($uri); + if ($uri =~ /\.\./) { + return BAD_REQUEST; + } + my $root = $self->config->path; $uri =~ s/^\Q$root// || die "$uri did not match config path $root"; @@ -100,7 +104,6 @@ $self->log(LOGINFO, "aio_stat($path) returned ($_[0])"); if ($_[0]) { # error (usually file didn't exist). - print "error\n"; while ($path =~ /\// && !-f $path) { $path =~ s/(\/[^\/]*)$//; $path_info = $1 . $path_info; @@ -116,7 +119,6 @@ } } elsif (-d _) { - print "dir\n"; # URI didn't end in a slash - need to redirect if ($original_uri !~ /\/$/) { $self->log(LOGINFO, "redirecting to $original_uri/$removed"); @@ -130,15 +132,13 @@ } } elsif (-f _) { - print "file\n"; - return; + return $client->finish_continuation; } else { # neither a dir nor a file die "Unknown entity type: $path"; } - print "Setting filename to $path\n"; $hd->filename($path); $client->finish_continuation; @@ -150,7 +150,6 @@ # This allows us to return REDIRECT above sub hook_uri_translation2 { my $self = shift; - - print "Continuation returns!\n"; + $self->log(LOGDEBUG, "uri_to_file continuation finished"); return $self->client->notes('uri_to_file_retcode'); } Modified: trunk/plugins/uri_to_file =================================================================== --- trunk/plugins/uri_to_file 2006-08-22 22:22:18 UTC (rev 110) +++ trunk/plugins/uri_to_file 2006-08-23 16:55:00 UTC (rev 111) @@ -63,7 +63,6 @@ $self->log(LOGINFO, "translate: $uri"); - $uri =~ s/(\?.*)//; my $removed = $1 || ''; @@ -71,6 +70,10 @@ $uri = uri_decode($uri); + if ($uri =~ /\.\./) { + return BAD_REQUEST; + } + my $root = $self->config->path; $uri =~ s/^\Q$root// || die "$uri did not match config path $root";