# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.

# XXX In order to support Windows, we have to make gd_redirect_output
# use Log4Perl or something instead of calling "logger". We probably
# also need to use Win32::Daemon or something like that to daemonize.

package Bugzilla::JobQueue::Runner;

use 5.10.1;
use strict;

use Cwd qw(abs_path);
use File::Basename;
use File::Copy;
use Pod::Usage;

use Bugzilla::Constants;
use Bugzilla::JobQueue;
use Bugzilla::Util qw(get_text);
BEGIN { eval "use parent qw(Daemon::Generic)"; }

our $VERSION = BUGZILLA_VERSION;

# Info we need to install/uninstall the daemon.
our $chkconfig = "/sbin/chkconfig";
our $initd = "/etc/init.d";
our $initscript = "bugzilla-queue";

# The Daemon::Generic docs say that it uses all sorts of
# things from gd_preconfig, but in fact it does not. The
# only thing it uses from gd_preconfig is the "pidfile"
# config parameter.
sub gd_preconfig {
    my $self = shift;

    $self->{_run_command} = 'subprocess_worker';
    my $pidfile = $self->{gd_args}{pidfile};
    if (!$pidfile) {
        $pidfile = bz_locations()->{datadir} . '/' . $self->{gd_progname} 
                   . ".pid";
    }
    return (pidfile => $pidfile);
}

# All config other than the pidfile has to be done in gd_getopt
# in order for it to be set up early enough.
sub gd_getopt {
    my $self = shift;

    $self->SUPER::gd_getopt();

    if ($self->{gd_args}{progname}) {
        $self->{gd_progname} = $self->{gd_args}{progname};
    }
    else {
        $self->{gd_progname} = basename($0);
    }

    # There are places that Daemon Generic's new() uses $0 instead of
    # gd_progname, which it really shouldn't, but this hack fixes it.
    $self->{_original_zero} = $0;
    $0 = $self->{gd_progname};
}

sub gd_postconfig {
    my $self = shift;
    # See the hack above in gd_getopt. This just reverses it
    # in case anything else needs the accurate $0.
    $0 = delete $self->{_original_zero};
}

sub gd_more_opt {
    my $self = shift;
    return (
        'pidfile=s' => \$self->{gd_args}{pidfile},
        'n=s'       => \$self->{gd_args}{progname},
    );
}

sub gd_usage {
    pod2usage({ -verbose => 0, -exitval => 'NOEXIT' });
    return 0
}

sub gd_can_install {
    my $self = shift;

    my $source_file;
    if ( -e "/etc/SuSE-release" ) {
        $source_file = "contrib/$initscript.suse";
    } else {
        $source_file = "contrib/$initscript.rhel";
    }
    my $dest_file = "$initd/$initscript";
    my $sysconfig = '/etc/sysconfig';
    my $config_file = "$sysconfig/$initscript";

    if (!-x $chkconfig  or !-d $initd) {
        return $self->SUPER::gd_can_install(@_);
    }

    return sub {
        if (!-w $initd) {
            print "You must run the 'install' command as root.\n";
            return;
        }
        if (-e $dest_file) {
            print "$initscript already in $initd.\n";
        }
        else {
            copy($source_file, $dest_file)
                or die "Could not copy $source_file to $dest_file: $!";
            chmod(0755, $dest_file)
                or die "Could not change permissions on $dest_file: $!";
        }

        system($chkconfig, '--add', $initscript);
        print "$initscript installed.",
              " To start the daemon, do \"$dest_file start\" as root.\n";

        if (-d $sysconfig and -w $sysconfig) {
            if (-e $config_file) {
                print "$config_file already exists.\n";
                return;
            }

            open(my $config_fh, ">", $config_file)
                or die "Could not write to $config_file: $!";
            my $directory = abs_path(dirname($self->{_original_zero}));
            my $owner_id = (stat $self->{_original_zero})[4];
            my $owner = getpwuid($owner_id);
            print $config_fh <<END;
#!/bin/sh
BUGZILLA="$directory"
# This user must have write access to Bugzilla's data/ directory.
USER=$owner
END
            close($config_fh);
        }
        else {
            print "Please edit $dest_file to configure the daemon.\n";
        }
    }
}

sub gd_can_uninstall {
    my $self = shift;

    if (-x $chkconfig and -d $initd) {
        return sub {
            if (!-e "$initd/$initscript") {
                print "$initscript not installed.\n";
                return;
            }
            system($chkconfig, '--del', $initscript);
            print "$initscript disabled.",
                  " To stop it, run: $initd/$initscript stop\n";
        }
    }

    return $self->SUPER::gd_can_install(@_);
}

sub gd_check {
    my $self = shift;

    # Get a count of all the jobs currently in the queue.
    my $jq = Bugzilla->job_queue();
    my @dbs = $jq->bz_databases();
    my $count = 0;
    foreach my $driver (@dbs) {
        $count += $driver->select_one('SELECT COUNT(*) FROM ts_job', []);
    }
    print get_text('job_queue_depth', { count => $count }) . "\n";
}

sub gd_setup_signals {
    my $self = shift;
    $self->SUPER::gd_setup_signals();
    $SIG{TERM} = sub { $self->gd_quit_event(); }
}

sub gd_quit_event {
    Bugzilla->job_queue->kill_worker();
    exit(1);
}

sub gd_other_cmd {
    my ($self, $do, $locked) = @_;
    if ($do eq "once") {
        $self->{_run_command} = 'work_once';
    } elsif ($do eq "onepass") {
        $self->{_run_command} = 'work_until_done';
    } else {
        $self->SUPER::gd_other_cmd($do, $locked);
    }
}

sub gd_run {
    my $self = shift;
    $self->_do_work($self->{_run_command});
}

sub _do_work {
    my ($self, $fn) = @_;

    my $jq = Bugzilla->job_queue();
    $jq->set_verbose($self->{debug});
    $jq->set_pidfile($self->{gd_pidfile});
    foreach my $module (values %{ Bugzilla::JobQueue->job_map() }) {
        eval "use $module";
        $jq->can_do($module);
    }
    $jq->$fn;
}

1;

__END__

=head1 NAME

Bugzilla::JobQueue::Runner - A class representing the daemon that runs the
job queue.

=head1 SYNOPSIS

 use Bugzilla::JobQueue::Runner;
 Bugzilla::JobQueue::Runner->new();

=head1 DESCRIPTION

This is a subclass of L<Daemon::Generic> that is used by L<jobqueue>
to run the Bugzilla job queue.

=head1 B<Methods in need of POD>

=over

=item gd_check

=item gd_run

=item gd_can_install

=item gd_quit_event

=item gd_other_cmd

=item gd_more_opt

=item gd_postconfig

=item gd_usage

=item gd_getopt

=item gd_preconfig

=item gd_can_uninstall

=item gd_setup_signals

=back
