# 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
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;
    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;

    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 {
        $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;
