home *** CD-ROM | disk | FTP | other *** search
- # Copyright 2001-2004 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.
- #
- package Apache::TestServer;
-
- use strict;
- use warnings FATAL => 'all';
-
- use Config;
- use Socket ();
- use File::Spec::Functions qw(catfile);
-
- use Apache::TestTrace;
- use Apache::TestRun;
- use Apache::TestConfig ();
- use Apache::TestRequest ();
-
- use constant COLOR => Apache::TestConfig::COLOR;
- use constant WIN32 => Apache::TestConfig::WIN32;
-
- my $CTRL_M = COLOR ? "\r" : "\n";
-
- # some debuggers use the same syntax as others, so we reuse the same
- # code by using the following mapping
- my %debuggers = (
- gdb => 'gdb',
- ddd => 'gdb',
- valgrind => 'valgrind',
- strace => 'strace',
- );
-
- sub new {
- my $class = shift;
- my $config = shift;
-
- my $self = bless {
- config => $config || Apache::TestConfig->thaw,
- }, $class;
-
- $self->{name} = join ':',
- map { $self->{config}->{vars}->{$_} } qw(servername port);
-
- $self->{port_counter} = $self->{config}->{vars}->{port};
-
- $self;
- }
-
- # call this when you already know where httpd is
- sub post_config {
- my($self) = @_;
-
- $self->{version} = $self->{config}->httpd_version || '';
- $self->{mpm} = $self->{config}->httpd_mpm || '';
-
- # try to get the revision number from the standard Apache version
- # string and various variations made by distributions which mangle
- # that string
-
- # Apache/2.0.50-dev
- ($self->{rev}) = $self->{version} =~ m|^Apache/(\d)\.|;
-
- # Apache-AdvancedExtranetServer/1.3.29 (Mandrake Linux/1mdk)
- ($self->{rev}) ||= $self->{version} =~ m|^Apache.*?/(\d)\.|;
-
- # IBM_HTTP_SERVER/1.3.19 Apache/1.3.20 (Unix)
- ($self->{rev}) ||= $self->{version} =~ m|^.*?Apache.*?/(\d)\.|;
-
- if ($self->{rev}) {
- debug "Matched Apache revision $self->{version} $self->{rev}";
- }
- else {
- # guessing is not good as it'll only mislead users
- # and we can't die since a config object is required
- # during Makefile.PL's write_perlscript when path to httpd may
- # be unknown yet. so default to non-existing version 0 for now.
- # and let TestRun.pm figure out the required pieces
- debug "can't figure out Apache revision, from string: " .
- "'$self->{version}', using a non-existing revision 0";
- $self->{rev} = 0; # unknown
- }
-
- $self;
- }
-
- sub version_of {
- my($self, $thing) = @_;
- die "Can't figure out what Apache server generation we are running"
- unless $self->{rev};
-
- $thing->{$self->{rev}};
- }
-
- my @apache_logs = qw(
- error_log access_log httpd.pid
- apache_runtime_status rewrite_log
- ssl_engine_log ssl_request_log
- );
-
- sub clean {
- my $self = shift;
-
- my $dir = $self->{config}->{vars}->{t_logs};
-
- for (@apache_logs) {
- my $file = catfile $dir, $_;
- if (unlink $file) {
- debug "unlink $file";
- }
- }
- }
-
- sub pid_file {
- my $self = shift;
- catfile $self->{config}->{vars}->{t_logs}, 'httpd.pid';
- }
-
- sub dversion {
- my $self = shift;
- "-D APACHE$self->{rev}";
- }
-
- sub config_defines {
- my $self = shift;
-
- my @defines = ();
-
- for my $item (qw(useithreads)) {
- next unless $Config{$item} and $Config{$item} eq 'define';
- push @defines, "-D PERL_\U$item";
- }
-
- if (my $defines = $self->{config}->{vars}->{defines}) {
- push @defines, map { "-D $_" } split " ", $defines;
- }
-
- "@defines";
- }
-
- sub args {
- my $self = shift;
- my $vars = $self->{config}->{vars};
- my $dversion = $self->dversion; #for .conf version conditionals
- my $defines = $self->config_defines;
-
- "-d $vars->{serverroot} -f $vars->{t_conf_file} $dversion $defines";
- }
-
- my %one_process = (1 => '-X', 2 => '-D ONE_PROCESS');
-
- sub start_cmd {
- my $self = shift;
- #XXX: threaded mpm does not respond to SIGTERM with -D ONE_PROCESS
- my $args = $self->args;
- return "$self->{config}->{vars}->{httpd} $args";
- }
-
- sub default_gdbinit {
- my $gdbinit = "";
- my @sigs = qw(PIPE);
-
- for my $sig (@sigs) {
- for my $flag (qw(pass nostop)) {
- $gdbinit .= "handle SIG$sig $flag\n";
- }
- }
-
- $gdbinit;
- }
-
- sub strace_cmd {
- my($self, $strace, $file) = @_;
- #XXX truss, ktrace, etc.
- "$strace -f -o $file -s1024";
- }
-
- sub valgrind_cmd {
- my($self, $valgrind) = @_;
- "$valgrind -v --leak-check=yes --show-reachable=yes --error-limit=no";
- }
-
- sub start_valgrind {
- my $self = shift;
- my $opts = shift;
-
- my $config = $self->{config};
- my $args = $self->args;
- my $one_process = $self->version_of(\%one_process);
- my $valgrind_cmd = $self->valgrind_cmd($opts->{debugger});
- my $httpd = $config->{vars}->{httpd};
-
- my $command = "$valgrind_cmd $httpd $one_process $args";
-
- debug $command;
- system $command;
- }
-
- sub start_strace {
- my $self = shift;
- my $opts = shift;
-
- my $config = $self->{config};
- my $args = $self->args;
- my $one_process = $self->version_of(\%one_process);
- my $file = catfile $config->{vars}->{t_logs}, 'strace.log';
- my $strace_cmd = $self->strace_cmd($opts->{debugger}, $file);
- my $httpd = $config->{vars}->{httpd};
-
- $config->genfile($file); #just mark for cleanup
-
- my $command = "$strace_cmd $httpd $one_process $args";
-
- debug $command;
- system $command;
- }
-
- sub start_gdb {
- my $self = shift;
- my $opts = shift;
-
- my $debugger = $opts->{debugger};
- my @breakpoints = @{ $opts->{breakpoint} || [] };
- my $config = $self->{config};
- my $args = $self->args;
- my $one_process = $self->version_of(\%one_process);
-
- my $file = catfile $config->{vars}->{serverroot}, '.gdb-test-start';
- my $fh = $config->genfile($file);
-
- print $fh default_gdbinit();
-
- if (@breakpoints) {
- print $fh "b ap_run_pre_config\n";
- print $fh "run $one_process $args\n";
- print $fh "finish\n";
- for (@breakpoints) {
- print $fh "b $_\n"
- }
- print $fh "continue\n";
- }
- else {
- print $fh "run $one_process $args\n";
- }
- close $fh;
-
- my $command;
- my $httpd = $config->{vars}->{httpd};
-
- if ($debugger eq 'ddd') {
- $command = qq{ddd --gdb --debugger "gdb -command $file" $httpd};
- }
- else {
- $command = "gdb $httpd -command $file";
- }
-
- $self->note_debugging;
- debug $command;
- system $command;
-
- unlink $file;
- }
-
- sub debugger_file {
- my $self = shift;
- catfile $self->{config}->{vars}->{serverroot}, '.debugging';
- }
-
- #make a note that the server is running under the debugger
- #remove note when this process exits via END
-
- sub note_debugging {
- my $self = shift;
- my $file = $self->debugger_file;
- my $fh = $self->{config}->genfile($file);
- eval qq(END { unlink "$file" });
- }
-
- sub start_debugger {
- my $self = shift;
- my $opts = shift;
-
- $opts->{debugger} ||= $ENV{MP_DEBUGGER} || 'gdb';
-
- unless ($debuggers{ $opts->{debugger} }) {
- error "$opts->{debugger} is not a supported debugger",
- "These are the supported debuggers: ".
- join ", ", sort keys %debuggers;
- die("\n");
- }
-
- my $method = "start_" . $debuggers{ $opts->{debugger} };
- $self->$method($opts);
- }
-
- sub pid {
- my $self = shift;
- my $file = $self->pid_file;
- my $fh = Symbol::gensym();
- open $fh, $file or do {
- return 0;
- };
-
- # try to avoid the race condition when the pid file was created
- # but not yet written to
- for (1..8) {
- last if -s $file > 0;
- select undef, undef, undef, 0.25;
- }
-
- chomp(my $pid = <$fh> || '');
- $pid;
- }
-
- sub select_next_port {
- my $self = shift;
-
- my $max_tries = 100; #XXX
- while ($max_tries-- > 0) {
- return $self->{port_counter}
- if $self->port_available(++$self->{port_counter});
- }
-
- return 0;
- }
-
- sub port_available {
- my $self = shift;
- my $port = shift || $self->{config}->{vars}->{port};
- local *S;
-
- my $proto = getprotobyname('tcp');
-
- socket(S, Socket::PF_INET(),
- Socket::SOCK_STREAM(), $proto) || die "socket: $!";
- setsockopt(S, Socket::SOL_SOCKET(),
- Socket::SO_REUSEADDR(),
- pack("l", 1)) || die "setsockopt: $!";
-
- if (bind(S, Socket::sockaddr_in($port, Socket::INADDR_ANY()))) {
- close S;
- return 1;
- }
- else {
- return 0;
- }
- }
-
- =head2 stop()
-
- attempt to stop the server.
-
- returns:
-
- on success: $pid of the server
- on failure: -1
-
- =cut
-
- sub stop {
- my $self = shift;
- my $aborted = shift;
-
- if (WIN32) {
- require Win32::Process;
- my $obj = $self->{config}->{win32obj};
- my $pid = -1;
- if ($pid = $obj ? $obj->GetProcessID : $self->pid) {
- if (kill(0, $pid)) {
- Win32::Process::KillProcess($pid, 0);
- warning "server $self->{name} shutdown";
- }
- }
- unlink $self->pid_file if -e $self->pid_file;
- return $pid;
- }
-
- my $pid = 0;
- my $tries = 3;
- my $tried_kill = 0;
-
- my $port = $self->{config}->{vars}->{port};
-
- while ($self->ping) {
- #my $state = $tried_kill ? "still" : "already";
- #print "Port $port $state in use\n";
-
- if ($pid = $self->pid and !$tried_kill++) {
- if (kill TERM => $pid) {
- warning "server $self->{name} shutdown";
- sleep 1;
-
- for (1..6) {
- if (! $self->ping) {
- if ($_ == 1) {
- unlink $self->pid_file if -e $self->pid_file;
- return $pid;
- }
- last;
- }
- if ($_ == 1) {
- warning "port $port still in use...";
- }
- else {
- print "...";
- }
- sleep $_;
- }
-
- if ($self->ping) {
- error "\nserver was shutdown but port $port ".
- "is still in use, please shutdown the service ".
- "using this port or select another port ".
- "for the tests";
- }
- else {
- print "done\n";
- }
- }
- else {
- error "kill $pid failed: $!";
- }
- }
- else {
- error "port $port is in use, ".
- "cannot determine server pid to shutdown";
- return -1;
- }
-
- if (--$tries <= 0) {
- error "cannot shutdown server on Port $port, ".
- "please shutdown manually";
- unlink $self->pid_file if -e $self->pid_file;
- return -1;
- }
- }
-
- unlink $self->pid_file if -e $self->pid_file;
- return $pid;
- }
-
- sub ping {
- my $self = shift;
- my $pid = $self->pid;
-
- if ($pid and kill 0, $pid) {
- return $pid;
- }
- elsif (! $self->port_available) {
- return -1;
- }
-
- return 0;
- }
-
- sub failed_msg {
- my $self = shift;
- my($log, $rlog) = $self->{config}->error_log;
- my $log_file_info = -e $log ?
- "please examine $rlog" :
- "$rlog wasn't created, start the server in the debug mode";
- error "@_ ($log_file_info)";
- }
-
- #this doesn't work well on solaris or hpux at the moment
- use constant USE_SIGCHLD => $^O eq 'linux';
-
- sub start {
- my $self = shift;
-
- my $old_pid = -1;
- if (WIN32) {
- # Stale PID files (e.g. left behind from a previous test run
- # that crashed) cannot be trusted on Windows because PID's are
- # re-used too frequently, so just remove it. If there is an old
- # server still running then the attempt to start a new one below
- # will simply fail because the port will be unavailable.
- if (-f $self->pid_file) {
- error "Removing old PID file -- " .
- "Unclean shutdown of previous test run?\n";
- unlink $self->pid_file;
- }
- $old_pid = 0;
- }
- else {
- $old_pid = $self->stop;
- }
- my $cmd = $self->start_cmd;
- my $config = $self->{config};
- my $vars = $config->{vars};
- my $httpd = $vars->{httpd} || 'unknown';
-
- if ($old_pid == -1) {
- return 0;
- }
-
- local $| = 1;
-
- unless (-x $httpd) {
- my $why = -e $httpd ? "is not executable" : "does not exist";
- error "cannot start server: httpd ($httpd) $why";
- return 0;
- }
-
- print "$cmd\n";
- my $old_sig;
-
- if (WIN32) {
- #make sure only 1 process is started for win32
- #else Kill will only shutdown the parent
- my $one_process = $self->version_of(\%one_process);
- require Win32::Process;
- my $obj;
- # We need the "1" below to inherit the calling processes
- # handles when running Apache::TestSmoke so as to properly
- # dup STDOUT/STDERR
- Win32::Process::Create($obj,
- $httpd,
- "$cmd $one_process",
- 1,
- Win32::Process::NORMAL_PRIORITY_CLASS(),
- '.');
- unless ($obj) {
- die "Could not start the server: " .
- Win32::FormatMessage(Win32::GetLastError());
- }
- $config->{win32obj} = $obj;
- }
- else {
- $old_sig = $SIG{CHLD};
-
- if (USE_SIGCHLD) {
- # XXX: try not to be POSIX dependent
- require POSIX;
-
- #XXX: this is not working well on solaris or hpux
- $SIG{CHLD} = sub {
- while ((my $child = waitpid(-1, POSIX::WNOHANG())) > 0) {
- my $status = $? >> 8;
- #error "got child exit $status";
- if ($status) {
- my $msg = "server has died with status $status";
- $self->failed_msg("\n$msg");
- Apache::TestRun->new(test_config => $config)->scan_core;
- kill SIGTERM => $$;
- }
- }
- };
- }
-
- defined(my $pid = fork) or die "Can't fork: $!";
- unless ($pid) { # child
- my $status = system "$cmd";
- if ($status) {
- $status = $? >> 8;
- #error "httpd didn't start! $status";
- }
- CORE::exit $status;
- }
- }
-
- while ($old_pid and $old_pid == $self->pid) {
- warning "old pid file ($old_pid) still exists";
- sleep 1;
- }
-
- my $version = $self->{version};
- my $mpm = $config->{mpm} || "";
- $mpm = "($mpm MPM)" if $mpm;
- print "using $version $mpm\n";
-
- my $timeout = $vars->{startup_timeout} ||
- $ENV{APACHE_TEST_STARTUP_TIMEOUT} ||
- 60;
-
- my $start_time = time;
- my $preamble = "${CTRL_M}waiting $timeout seconds for server to start: ";
- print $preamble unless COLOR;
- while (1) {
- my $delta = time - $start_time;
- print COLOR
- ? ($preamble, sprintf "%02d:%02d", (gmtime $delta)[1,0])
- : '.';
- sleep 1;
- if ($self->pid) {
- print $preamble, "ok (waited $delta secs)\n";
- last;
- }
- elsif ($delta > $timeout) {
- my $suggestion = $timeout + 300;
- print $preamble, "not ok\n";
- error <<EOI;
- giving up after $delta secs. If you think that your system
- is slow or overloaded try again with a longer timeout value.
- by setting the environment variable APACHE_TEST_STARTUP_TIMEOUT
- to a high value (e.g. $suggestion) and repeat the last command.
- EOI
- last;
- }
- }
-
- # now that the server has started don't abort the test run if it
- # dies
- $SIG{CHLD} = $old_sig || 'DEFAULT';
-
- if (my $pid = $self->pid) {
- print "server $self->{name} started\n";
-
- my $vh = $config->{vhosts};
- my $by_port = sub { $vh->{$a}->{port} <=> $vh->{$b}->{port} };
-
- for my $module (sort $by_port keys %$vh) {
- print "server $vh->{$module}->{name} listening ($module)\n",
- }
-
- if ($config->configure_proxy) {
- print "tests will be proxied through $vars->{proxy}\n";
- }
- }
- else {
- $self->failed_msg("server failed to start!");
- return 0;
- }
-
- return 1 if $self->wait_till_is_up($timeout);
-
- $self->failed_msg("failed to start server!");
- return 0;
- }
-
-
- # wait till the server is up and return 1
- # if the waiting times out returns 0
- sub wait_till_is_up {
- my($self, $timeout) = @_;
- my $config = $self->{config};
- my $sleep_interval = 1; # secs
-
- my $server_up = sub {
- local $SIG{__WARN__} = sub {}; #avoid "cannot connect ..." warnings
- # avoid fatal errors when LWP is not available
- my $r = eval { Apache::TestRequest::GET('/index.html') };
- return !$@ && defined $r ? $r->code : 0;
- };
-
- if ($server_up->()) {
- return 1;
- }
-
- my $start_time = time;
- my $preamble = "${CTRL_M}still waiting for server to warm up: ";
- print $preamble unless COLOR;
- while (1) {
- my $delta = time - $start_time;
- print COLOR
- ? ($preamble, sprintf "%02d:%02d", (gmtime $delta)[1,0])
- : '.';
- sleep $sleep_interval;
- if ($server_up->()) {
- print "${CTRL_M}the server is up (waited $delta secs) \n";
- return 1;
- }
- elsif ($delta > $timeout) {
- print "${CTRL_M}the server is down, giving up after $delta secs\n";
- return 0;
- }
- else {
- # continue
- }
- }
- }
-
- 1;
-