# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You 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 # Foo-Apache-Bar/x.y.z ($self->{rev}) = $self->{version} =~ m|/(\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->{revminor}) = $self->{version} =~ m|/\d\.(\d)|; if ($self->{revminor}) { debug "Matched Apache revminor $self->{version} $self->{revminor}"; } else { $self->{revminor} = 0; } $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 cgisock ); 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; my $vars = $self->{config}->{vars}; return $vars->{t_pid_file} || catfile $vars->{t_logs}, 'httpd.pid'; } sub dversion { my $self = shift; my $dv = "-D APACHE$self->{rev}"; if ($self->{rev} == 2 and $self->{revminor} == 4) { $dv .= " -D APACHE2_4"; } return $dv; } 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; my $args = $self->args; my $config = $self->{config}; my $vars = $config->{vars}; my $httpd = $vars->{httpd}; my $one_process = $self->{run}->{opts}->{'one-process'} ? $self->version_of(\%one_process) : ''; #XXX: threaded mpm does not respond to SIGTERM with -D ONE_PROCESS return "$httpd $one_process $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 { ## defaults to gdb if not set in %ENV or via -debug $command = "$debugger $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'; # XXX: FreeBSD 5.2+ # gdb 6.1 and before segfaults when trying to # debug httpd startup code. 6.5 has been proven # to work. FreeBSD typically installs this as # gdb65. # Is it worth it to check the debugger and os version # and die ? unless (grep { /^$opts->{debugger}/ } keys %debuggers) { error "$opts->{debugger} is not a supported debugger", "These are the supported debuggers: ". join ", ", sort keys %debuggers; die("\n"); } my $debugger = $opts->{debugger}; $debugger =~ s/\d+$//; my $method = "start_" . $debuggers{$debugger}; ## $opts->{debugger} is passed through unchanged ## so when we try to run it next, its found. $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 <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 return eval { my $r=Apache::TestRequest::GET('/index.html'); $r->code!=500 or $r->header('client-warning')!~/internal/i; } || 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;