#!/usr/bin/perl -wT

# multiversion/cluster aware pg_ctl wrapper; this also supplies the correct
# configuration parameters to 'start', and makes sure that a postmaster really
# stops on 'stop'.
#
# (C) 2005 Martin Pitt <mpitt@debian.org>

use lib '/usr/share/postgresql-common';
use Getopt::Long;
use POSIX qw/setsid setlocale LC_ALL :sys_wait_h/;
use PgCommon;
use Fcntl 'SEEK_SET';

# Check for known broken configurations of this cluster
sub check_valid_config {
    my %postgresql_conf = read_cluster_conf_file $version, $cluster,
        'postgresql.conf';
    my $log_statement_stats = config_bool $postgresql_conf{'log_statement_stats'};
    my $log_parser_stats = config_bool $postgresql_conf{'log_parser_stats'};
    my $log_planner_stats = config_bool $postgresql_conf{'log_planner_stats'};
    my $log_executor_stats = config_bool $postgresql_conf{'log_executor_stats'};

    # statement and other statistics are mutually exclusive
    if ($log_statement_stats && ($log_parser_stats || $log_planner_stats ||
	$log_executor_stats)) {
	error 'invalid postgresql.conf: log_statement_stats and the other log_*_stats options are mutually exclusive';
    }
}

# Return the PID from an existing PID file or undef if it does not exist.
# Arguments: <pid file path>
sub get_running_pid {
    return undef unless -e $_[0];

    if (open PIDFILE, $_[0]) {
	my $pid = <PIDFILE>;
	chomp $pid if defined $pid;
	close PIDFILE;
	return $pid;
    } else {
	return undef;
    }
}

# Check whether a pid file for the %info cluster is present and belongs to a
# running postmaster.
# Arguments: <pid file path>
sub check_running_postmaster {
    my $pid = get_running_pid $_[0];
    if (defined $pid and $pid =~ /^\d+$/) {
	if (open PS, '-|', '/bin/ps', '-o', 'comm', 'h', 'p', $pid) {
	    my $process = <PS>;
	    chomp $process if defined $process;
	    close PS;
	    if (defined $process and ($process eq 'postmaster' or $process eq 'postgres')) {
                return 1;
            }
        } else {
            error "Could not exec /bin/ps";
        }
    }
    return 0;

}

# If a pid file is already present, delete it if it is stale/invalid, or exit
# with a notice if it belongs to an already running postmaster.
sub start_check_pid_file {
    my $pidfile = $info{'pgdata'}.'/postmaster.pid';
    if (check_running_postmaster $pidfile) {
        print "Cluster is already running.\n";
        exit 2;
    }

    # Remove invalid or stale PID file
    if (-e $pidfile) {
	unlink $pidfile;
	print "Removed stale pid file.\n";
    }
}

# Check if a pid file is not present or it is invalid. If so, abort.
sub stop_check_pid_file {
    my $pidfile = $info{'pgdata'}.'/postmaster.pid';
    my $pid = get_running_pid $pidfile;
    return if (defined $pid and $pid =~ /^\d+$/); 
    if ($info{'running'}) {
	error 'pid file is invalid, please manually kill the stale server process.';
    }
    print "Cluster is not running.\n";
    exit 2;
}

# check if a cluster reliably connects or fails
# Arguments: <version> <cluster> <port> <socket dir>
sub cluster_port_ready {
    my ($v, $c, $p, $sd) = @_;
    my $psql = get_program_path 'psql', $v;
    error 'cluster_port_ready: could not find psql binary' unless $psql;
    my $n = 0;
    my $result = 0;


    # probe until we get three successful or failed connections in a row
    $ENV{'PGPASSWORD'} = 'foo'; # prevent hangs if superuser cannot connect withouth password
    my $out;
    while ($n < ($result ? 10 : 3)) {
        select undef, undef, undef, 0.5;
        $out = `$psql -h '$sd' --port $p -l 2>&1 >/dev/null`;

        if ($? == $result) {    
            $n++;
        } else {
            $n = 0;
        }
        $result = $?;
    }

    if ($out =~ 'FATAL:') {
	print STDERR "WARNING: connection to the database failed, disabling startup checks:\n$out\n";
	return cluster_port_running $v, $c, $p;
    }
    return !$result;
}

sub autovacuum_start {
    if ($version ge '8.1') {
	error 'PostgreSQL 8.1 and above has an integrated autovacuum daemon which cannot be controlled by this program.';
    }

    return unless $info{'avac_enable'};

    error "pg_autovacuum not found. Install postgresql-contrib-$version to get it" 
        unless $pg_autovacuum;

    return if -f $info{'pgdata'}.'/autovacuum.pid';

    my %avac_flags = ('avac_sleep_base' => '-s', 'avac_sleep_scale' => '-S',
                      'avac_vac_base'   => '-v', 'avac_vac_scale'   => '-V',
                      'avac_anal_base'  => '-a', 'avac_anal_scale'  => '-A',
                      'avac_logfile' => '-L', 'avac_debug' => '-d');

    @options = ('-p', $info{'port'}, '-H', $info{'socketdir'});

    foreach (keys %info) {
	if (my $option = $avac_flags{$_}) {
            push (@options, $option, $info{$_}) if $info{$_};
	}
    }

    # check that the cluster is actually running
    my $running = 0;
    for (my $attempt = 0; $attempt < 10; $attempt++) {
        select (undef, undef, undef, 0.5);
        if (cluster_port_running $version, $cluster, $info{'port'}) {
            $running = 1;
            last;
        }
    }
    error "Cannot start autovacuum daemon: cluster is not running" unless $running;

    # remember current size of the log
    my $logsize = -1;
    if (defined $info{'avac_logfile'}) {
        $logsize = (stat $info{'avac_logfile'})[7] if -r $info{'avac_logfile'};
    }

    if (!($pid = fork)) {
	setsid or die "setsid failed(): $!";
        close $_ for(STDOUT, STDIN, STDERR);
        chdir '/';
        exec $pg_autovacuum, @options;
	exit -1;
    } else {
	# wait a bit and check whether the daemon really started
        select (undef, undef, undef, 0.5);
	# clean up process if it exited
	waitpid($pid, WNOHANG);
	unless (kill 0, $pid) {
            print STDERR "The PostgreSQL autovacuum daemon failed to start.\n";
            if ($logsize >= 0) {
                print STDERR "Please check the log output:\n";
                open LOG, $info{'avac_logfile'} or 
                    error "Could not open log file " . $info{'avac_logfile'};
                seek LOG, $logsize, SEEK_SET;
                print STDERR $_ while <LOG>;
            } else {
                print STDERR "/etc/postgresql/$version/$cluster/autovacuum_log is missing or a dangling symlink.\n\n";
            }
	    exit 1;
	}

	# write PID file
        if(open my $AVAC_PID, '>', $info{'pgdata'}.'/autovacuum.pid') {
            print $AVAC_PID "$pid\n";
            close $AVAC_PID;
        }
        exit 0;
    }
}

sub autovacuum_stop {
    if ($version ge '8.1') {
	error 'PostgreSQL 8.1 and above has an integrated autovacuum daemon which cannot be controlled by this program.';
    }

    return unless $info{'avac_enable'};

    error "pg_autovacuum not found. Install postgresql-contrib-$version to get it" 
        unless $pg_autovacuum;

    if (open my $AVAC_PID, $info{'pgdata'}.'/autovacuum.pid') {
        $pid = <$AVAC_PID>;
        ($pid) = $pid =~ /^(\d+)\s*$/; # untaint
        kill ('TERM', $pid) if $pid;
        close $AVAC_PID;
        unlink $info{'pgdata'}.'/autovacuum.pid';
    }
}

sub autovacuum_restart {
    if ($version ge '8.1') {
	error 'PostgreSQL 8.1 and above has an integrated autovacuum daemon which cannot be controlled by this program.';
    }

    if (-f $info{'pgdata'}.'/autovacuum.pid') {
        autovacuum_stop;
        autovacuum_start;
    }
}

sub start {
    my $cdir = $info{'configdir'};

    check_valid_config;

    start_check_pid_file;

    # get locale used by initdb
    my ($lc_ctype, $lc_collate) = get_cluster_locales $version, $cluster;
    $lc_ctype or error ('Could not parse locale out of pg_controldata output');

    # check validity of locale
    unless (setlocale (LC_ALL, $lc_ctype)) {
	error ("The server must be started under the locale $lc_ctype which does not exist any more.")
    }

    # prepare environment
    %ENV = read_cluster_conf_file $version, $cluster, 'environment';
    $ENV{'LC_CTYPE'} = $lc_ctype;

    my $postmaster_opts = '';
    if (!(PgCommon::get_conf_value $version, $cluster, 'postgresql.conf', 'unix_socket_directory')) {
	$postmaster_opts .= '-c unix_socket_directory="' . $info{'socketdir'} . '"';
    }

    # versions 8.0+ support configurable conffile locations and external PID files
    if ($version ge '8.0') {
        $postmaster_opts .= " -c config_file=\"$cdir/postgresql.conf\""; 
	if (!(PgCommon::get_conf_value $version, $cluster, 'postgresql.conf', 'hba_file')) {
	    $postmaster_opts .= " -c hba_file=\"$cdir/pg_hba.conf\"";
	}
	if (!(PgCommon::get_conf_value $version, $cluster, 'postgresql.conf', 'ident_file')) {
	    $postmaster_opts .= " -c ident_file=\"$cdir/pg_ident.conf\""; 
	}

	if ((-d '/var/run/postgresql') && !defined (PgCommon::get_conf_value $version, $cluster, 'postgresql.conf', 'external_pid_file')) {
	    # check whether /var/run/postgresql/ is writeable as the cluster owner
	    my $vrp_writable;
	    if ($> == 0) {
		change_ugid $info{'owneruid'}, $info{'ownergid'};
		$vrp_writable = -w '/var/run/postgresql';
		$< = $> = 0;
		$( = $) = 0;
	    } else {
		$vrp_writable = -w '/var/run/postgresql';
	    }
	    if ($vrp_writable) {
		$postmaster_opts .= " -c external_pid_file=\"/var/run/postgresql/$version-$cluster.pid\"";
	    }
	}
    }

    $postmaster_opts .= ' ' . (join ' ', @postmaster_auxoptions);
    ($postmaster_opts) = $postmaster_opts =~ /(.*)/; # untaint

    @options = ($pg_ctl, 'start', '-D', $info{'pgdata'},'-l', $info{'logfile'},
	'-s', '-o', $postmaster_opts);

    # remember current size of the log
    $logsize = 0;
    unless (-e $info{'logfile'}) {
	open L, '>', $info{'logfile'} or 
	    error 'Could not create log file ' . $info{'logfile'};
	chmod 0640, $info{'logfile'};
	close L;
    }
    $logsize = (stat $info{'logfile'})[7];

    if (fork) {
        wait;
        error "could not exec $pgctl @options: $!" if $?;
    } else {
        setsid or error "could not start session: $!";
        exec $pg_ctl @options or error "could not exec $pgctl @options: $!";
    }

    # wait a bit until the socket exists
    $success = 0;
    $currlogsize = 0;
    my $pidfile = $info{'pgdata'}.'/postmaster.pid';
    for (my $attempt = 0; $attempt < 60; $attempt++) {
        select (undef, undef, undef, 0.5);
        $currlogsize = (stat $info{'logfile'})[7] if -r $info{'logfile'};
        if (cluster_port_running $version, $cluster, $info{'port'}) {
            $success = 1;
            last;
        }

        # if the postmaster wrote something, but the process does not exist any
        # more, there must be a problem and we can stop immediately
        last if ($currlogsize > $logsize && !check_running_postmaster $pidfile);
    }

    # OK, the server runs, now wait until it stabilized
    if ($success) {
	$success = cluster_port_ready $version, $cluster, $info{'port'}, $info{'socketdir'};
    }

    if ($success) {
        if ($version lt '8.1' && $info{'avac_enable'} && $pg_autovacuum) {
            autovacuum_start;
        }
    } else {
        print STDERR "The PostgreSQL server failed to start. Please check the log output:\n";
        open LOG, $info{'logfile'} or 
            error "Could not open log file " . $info{'logfile'};
        seek LOG, $logsize, SEEK_SET;
        print STDERR $_ while <LOG>;
        exit 1;
    }
}

sub stop {
    autovacuum_stop if $pg_autovacuum;

    stop_check_pid_file;

    if (!fork()) {
        close STDOUT;
        exec $pg_ctl, '-D', $info{'pgdata'}, '-s', '-w', '-m', 'fast', 'stop';
    } else {
        wait;
    }

    # try harder if "fast" mode does not work
    if (-f $info{'pgdata'}.'/postmaster.pid') {
        print "(does not shutdown gracefully, now stopping immediately)";
        system $pg_ctl, '-D', $info{'pgdata'}, '-s', '-w', '-m', 'immediate', 'stop';
    }

    # if that still not helps, use the big hammer
    if (-f $info{'pgdata'}.'/postmaster.pid') {
        print "(does not shutdown, killing the process)";
        if (open FPID, $info{'pgdata'}.'/postmaster.pid') {
            $pid = <FPID>;
            close FPID;
        }
        kill (9, $pid) if $pid;
        unlink $info{'pgdata'}.'/postmaster.pid';
    }

    # external_pid_file files are currently not removed by postmaster itself
    unlink "/var/run/postgresql/$version-$cluster.pid";
}

sub restart {
    stop if $info{'running'};
    start;
}

sub reload {
    exec $pg_ctl, '-D', $info{'pgdata'}, '-s', 'reload';
}

#
# main
#

exit 1 unless GetOptions ('o|options=s' => \@postmaster_auxoptions);

if ($#ARGV != 2) {
    print "Usage: $0 <version> <cluster> <action>\n";
    exit 1;
}

($version, $cluster, $action) = @ARGV;
($version) = $version =~ /^(\d+\.\d+)$/; # untaint
($cluster) = $cluster =~ /^([^'"\s]+)$/; # untaint
error 'specified cluster does not exist' unless $version && $cluster && cluster_exists $version, $cluster;
%info = cluster_info ($version, $cluster);

unless ($action eq 'stop' || $action eq 'autovac-stop') {
    error 'Cluster is disabled' if $info{'start'} eq 'disabled';
}

# untaint environment
$ENV{'PATH'} = '/bin:/usr/local/bin:/usr/bin';
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};

# check that owner uig/gid is valid
unless (getpwuid $info{'owneruid'}) {
    error 'The cluster is owned by user id '.$info{'owneruid'}.' which does not exist any more'
}
unless (getgrgid $info{'ownergid'}) {
    error 'The cluster is owned by group id '.$info{'ownergid'}.' which does not exist any more'
}

if ($> == 0) {
    chdir ('/var/lib/postgresql');
    change_ugid $info{'owneruid'}, $info{'ownergid'};
}

if( $> != $info{'owneruid'} ) {
    error 'You must run this program as the cluster owner ('.
        (getpwuid $info{'owneruid'})[0].')';
}

$pg_ctl = get_program_path 'pg_ctl', $version;
$pg_autovacuum = get_program_path 'pg_autovacuum', $version;

%actions = ('start' => \&start, 'stop' => \&stop, 'reload' => \&reload,
            'restart' => \&restart,
            'autovac-start' => \&autovacuum_start,
            'autovac-stop' => \&autovacuum_stop,
            'autovac-restart' => \&autovacuum_restart);

if ($actions{$action}) {
    $actions{$action}->();
} else {
    error 'Error: invalid action (must be one of: '. 
        (join ', ', keys %actions);
}

__END__

=head1 NAME

pg_ctlcluster - start/stop/restart/reload a PostgreSQL cluster

=head1 SYNOPSIS

B<pg_ctlcluster> [I<options>] I<cluster-version> I<cluster-name> I<action>

where I<action> = B<start>|B<stop>|B<restart>|B<reload>|B<autovac-start>|B<autovac-stop>|B<autovac-restart>

=head1 DESCRIPTION

This program controls the B<postmaster> server for a particular cluster. It
essentially wraps the L<pg_ctl(1)> command. It determines the cluster version
and data path and calls the right version of B<pg_ctl> with appropriate
configuration parameters and paths.

You have to start this program as the user who owns the database cluster or as
root.

=head1 ACTIONS

=over 4

=item B<start>

A log file for this specific cluster is created if it does not exist yet (by
default,
C</var/log/postgresql/postgresql->I<cluster-version>C<->I<cluster-name>C<.log>),
and a PostreSQL server process (L<postmaster(1)>) is started on it. If the
package B<postgresql-contrib->I<version> is installed, a B<pg_autovacuum>
process is started as well (unless this gets disabled in
C</etc/postgresql-common/autovacuum.conf> or a cluster-specific
C<autovacuum.conf> file). Please note that server version 8.1 and above does
internal autovacuuming. Exits with 0 on success, with 2 if the server is
already running, and with 1 on other failure conditions.

=item B<stop>

Stops the L<postmaster(1)> server (and B<pg_autovacuum>, if running) of the
given cluster with increasing force. Initially, the B<fast> mode is used which
rolls back all active transactions and thus shuts down cleanly. If that does
not work, shutdown is attempted again in B<immediate> mode, which can leave the
cluster in an inconsistent state and thus will lead to a recovery run at the
next start. If this still does not help, the B<postmaster> process is killed.
Exits with 0 on success, with 2 if the server is not running, and with 1 on
other failure conditions.

=item B<restart>

Stops the server if it is running and starts it (again). If B<pg_autovacuum> is
running on the server, it is restarted as well.

=item B<reload>

Causes the configuration files to be re-read without a full shutdown of the
server.

=item B<autovac-start> 

Starts a B<pg_autovacuum> process for an already running cluster. This normally
happens automatically along with B<start>. This command fails for PostgreSQL 8.1
and above since they do autovacuuming internally.

=item B<autovac-stop> 

Stops the B<pg_autovacuum> process for a running cluster. This normally happens
automatically along with B<stop>. This command fails for PostgreSQL 8.1 and above
since they do autovacuuming internally.

=item B<autovac-restart> 

Restarts a B<pg_autovacuum> process for an already running cluster. This
normally happens automatically along with B<restart>. This command fails for
PostgreSQL 8.1 and above since they do autovacuuming internally.

=back

=head1 OPTIONS

=over 4

=item B<-o> I<option>

Pass given I<option> as command line option to the C<postmaster> process. It is
possible to specify B<-o> multiple times. See L<postmaster(1)> for a
description of valid options.

=back

=head1 SEE ALSO

L<pg_ctl(1)>, L<pg_wrapper(1)>, L<pg_lsclusters(1)>, L<postmaster(1)>

=head1 AUTHOR

Martin Pitt L<E<lt>mpitt@debian.orgE<gt>>

