#!/usr/local/cpanel/3rdparty/bin/perl
# cpanel - scripts/try-later Copyright 2022 cPanel, L.L.C.
# All rights reserved.
# copyright@cpanel.net http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited
use strict;
use warnings;
use Cpanel::Alarm ();
use Cpanel::Binaries ();
use Cpanel::SafeRun::Errors ();
use Cpanel::SafeRun::Object ();
use Cpanel::Usage ();
use DateTime ();
use IPC::Open3 ();
use Cpanel::Version::Full ();
my $act_finally;
my $action_command;
my $at_args;
my $check_command;
my $delay = 5;
my $max_retries;
my $skip_first;
my $has_jobs;
my $at_cmd = Cpanel::Binaries::path('at');
my $atd_cmd = Cpanel::Binaries::path('atd');
if ( !-x $at_cmd || !-x $atd_cmd ) {
print_usage_and_exit('System "at" command required to run this utility.');
}
Cpanel::Usage::wrap_options(
\@ARGV,
\&print_usage_and_exit,
{
'act-finally' => \$act_finally,
'action' => \$action_command,
'at' => \$at_args,
'check' => \$check_command,
'delay' => \$delay,
'max-retries' => \$max_retries,
'skip-first' => \$skip_first,
'has-jobs' => \$has_jobs,
},
);
if ($has_jobs) {
# exit 0 : queue is empty
# exit 1 : queue has at least one job
exit try_later_has_jobs();
}
if ( !$action_command ) {
print_usage_and_exit('An action command is required.');
}
if ( !$check_command ) {
print_usage_and_exit('A check command is required');
}
# The extra parens are necessary.
if ( $max_retries && ( $max_retries !~ m/^\d+$/ || $max_retries < 1 ) ) {
print_usage_and_exit('Invalid value for --max-retries');
}
# if we're skipping running the check immediately, then
# we need to add a retry as it is decremented during
# do_later
if ( $skip_first && $max_retries ) {
++$max_retries;
}
if ( $delay && $delay =~ m/^\d+$/ && $delay > 0 ) {
# at seems to subtract a minute from the now +, so adding
# an extra minute seems to make it more understandable
++$delay;
$at_args = "now + $delay minutes";
}
elsif ($delay) {
print_usage_and_exit('Invalid value for --delay');
}
check() unless $skip_first;
if ( $max_retries == 1 ) {
if ($act_finally) {
exit run_command($action_command);
}
exit;
}
if ( !start_atd() ) {
print "Unable to start 'atd', which is required to run this utility.\n";
exit 1;
}
do_later();
sub check {
if ( run_command($check_command) ) {
return;
}
exit run_command($action_command);
}
sub do_later {
my %arg_for_name = (
'--act-finally' => $act_finally,
'--action' => $action_command,
'--at' => $at_args,
'--check' => $check_command,
);
my $me = '/usr/local/cpanel/scripts/try-later';
# during a fast upgrade / downgrade we could disappear
# we should empty the at queue before upgrading or downgrading
exit unless -x $me;
my @self_command = ($me);
if ($max_retries) {
--$max_retries;
push @self_command, '--max-retries', $max_retries;
}
while ( my ( $name, $arg ) = each %arg_for_name ) {
next if !length $arg;
push @self_command, $name, "'$arg'";
}
# _job_tag() is used to identify jobs in queue
# could be used to clean the at queue when launching an upgrade
my $stdin = _job_tag() . "\nif [ -x $me ]; then \n" . join( ' ', @self_command ) . "\nfi\n";
my $result = Cpanel::SafeRun::Object->new(
'program' => $at_cmd,
'args' => [$at_args],
'stdin' => $stdin,
);
exit( $result->error_code() // 0 );
}
sub start_atd {
# Before we start atd, we need to check for stale jobs so that if atd has
# been disabled, we don't unleash an angry horde of ancient jobs on the
# system when we re-enable it.
my $alarm = Cpanel::Alarm->new( 60, sub { print "Unable to start 'atd' (required for try-later)\n"; exit 1; } );
my $atq_cmd = Cpanel::Binaries::path('atq');
my $atrm_cmd = Cpanel::Binaries::path('atrm');
my @jobs;
my @check_cmd = (
'/usr/local/cpanel/scripts/cpservice',
'atd',
'status'
);
return if !-x $check_cmd[0];
# Don't bother starting atd if it's already running.
Cpanel::SafeRun::Errors::saferunnoerror(@check_cmd);
return 1 unless $?;
return unless -x $atq_cmd;
open( my $fh, '-|', $atq_cmd );
while ( defined( my $line = <$fh> ) ) {
next unless $line =~ m/(\d+)\s+(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})/;
push @jobs, $1 if DateTime->new( year => $2, month => $3, day => $4, hour => $5, minute => $6 ) <= DateTime->now();
}
close($fh);
if (@jobs) {
return unless -x $atrm_cmd;
return if system( $atrm_cmd, @jobs );
}
my @enable_cmd = (
'/usr/local/cpanel/scripts/cpservice',
'atd',
'enable'
);
return if !-x $enable_cmd[0];
# Sometimes if atd exits uncleanly (e.g. with kill -9), simply trying to
# start it won't work. This is true of CentOS 5, but not CentOS 6. So
# instead, we call restart to stop it first to make sure that all the
# appropriate state is cleaned up, and then start it again.
my @start_cmd = (
'/usr/local/cpanel/scripts/cpservice',
'atd',
'restart'
);
return if !-x $start_cmd[0];
return if system @enable_cmd;
return !system @start_cmd;
}
sub _job_tag {
return "# cPanel try-later version " . Cpanel::Version::Full::getversion();
}
sub try_later_has_jobs {
my @results = Cpanel::SafeRun::Errors::saferunallerrors( Cpanel::Binaries::path('atq') );
foreach (@results) {
next unless $_ =~ /^(\d+)/;
my $jid = $1;
my $job = Cpanel::SafeRun::Errors::saferunallerrors( $at_cmd, '-c', $jid );
my $tag = _job_tag();
my $regexp = qr{$tag};
return 1 if $job =~ /^$regexp/m;
}
return 0;
}
# This function exists because we may be running under atd. If we are, and we
# produce output of any sort, the system administrator will receive an email
# entitled "Output from your job", which will only serve to confuse them.
# Consequently, we suppress all output here.
sub run_command {
my ($command) = @_;
local *STDOUT = *STDOUT;
local *STDERR = *STDERR;
open( STDOUT, ">", "/dev/null" ) or die;
open( STDERR, ">", "/dev/null" ) or die;
return system($command);
}
sub print_usage_and_exit {
my ($error) = @_;
my %options = (
'act-finally' => 'Perform action when retries run out',
'action' => 'Command to run when a check succeeds',
'at' => 'Args to specify when the at command will retry the check',
'check' => 'Command to run to check whether or not to run the action',
'delay' => 'Specify a delay in minutes after which to check and act (default 5)',
'help' => 'Brief help message',
'max-retries' => 'Maximum attempts to retry before giving up (default infinite)',
'skip-first' => 'Skip the first check command',
'has-jobs' => 'Check if the try-later queue is empty or not ( exit with 0 if queue is empty )'
);
if ( defined $error ) {
print $error, "\n\n";
}
print "Usage: $0 ";
print "[options]\n\n";
print " Options:\n";
while ( my ( $opt, $desc ) = each %options ) {
print " --$opt";
my $space = 12 - length $opt;
( 0 < $space ) ? print ' ' x $space : print ' ';
print "$desc\n";
}
print "\n";
print "This utility will execute a check command at the configured interval. If the\n";
print "check command returns in error, it will be retried later as often as allowed by\n";
print "max-retries. When the check succeeds, the action command will be run.";
print "\n";
exit 1 if defined $error;
exit;
}
Mr. DellatioNx196 GaLers xh3LL Backd00r 1.0, Coded By Mr. DellatioNx196 - Bogor BlackHat