#!/usr/local/cpanel/3rdparty/bin/perl

#                                      Copyright 2026 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 autofixer2::unblock_c7_v110_upcp;

use strict;
use warnings;

BEGIN { unshift @INC, '/usr/local/cpanel'; }

use Cpanel::OS               ();
use Cpanel::Version::Compare ();
use Cpanel::SafeRun::Object  ();
use Cpanel::Logger           ();
use Cpanel::ProcessCheck     ();
use Fcntl                    ();
use POSIX                    ();

our $TARGET_VERSION_MAX = '11.110.0.52';
our @REQUIRED_PACKAGES  = ('boost169-program-options');

our $YUM_RETRY_COUNT = 4;
our $YUM_RETRY_SLEEP = 30;

our $UPCP_WAIT_INTERVAL  = 60;
our $UPCP_WAIT_MAX       = 3600;
our $upcp_kicker_pidfile = '/var/run/unblock_c7_v110_upcp.upcp.pid';

exit run() unless caller;

# CPANEL-52975
#
# CentOS 7 hosts running cPanel 11.110.0.x where x <= 52 fail sysup with
# "Needed system packages were not installed: boost169-program-options" because
# .52 added boost169-program-options to the C7 required-packages list and the
# package lives in EPEL. Install epel-release, install boost169-program-options,
# then kick a fresh upcp --force so the host immediately re-attempts the
# upgrade that was previously blocked.
sub run {
    return 0 unless supported_on_this_major( 110, 110 );
    return 0 unless Cpanel::OS::major() == 7 && Cpanel::OS::distro() eq 'centos';

    my $current = _read_version();
    return 0 unless length $current;
    return 0 if Cpanel::Version::Compare::compare( $current, '>', $TARGET_VERSION_MAX );

    my $log = Cpanel::Logger->new;

    return 0 unless _yum_install_with_retries( $log, 'epel-release' );
    $log->warn( "Fail to install required packages" ) unless _yum_install_with_retries( $log, @REQUIRED_PACKAGES ); # optional

    _kick_upcp($log);

    return 0;
}

sub _yum_install_with_retries {
    my ( $log, @packages ) = @_;
    my $label = join( ' ', @packages );
    for my $attempt ( 1 .. $YUM_RETRY_COUNT ) {
        my $run = Cpanel::SafeRun::Object->new(
            'program' => '/usr/bin/yum',
            'args'    => [ 'install', '-y', @packages ],
        );
        my $rc = $run->error_code();
        return 1 if !$rc;
        $log->warn("unblock_c7_v110_upcp: attempt $attempt/$YUM_RETRY_COUNT: yum install of $label failed (rc=$rc)");
        _yum_retry_sleep($YUM_RETRY_SLEEP) if $attempt < $YUM_RETRY_COUNT;
    }
    $log->warn("unblock_c7_v110_upcp: gave up installing $label after $YUM_RETRY_COUNT attempts");
    return 0;
}

sub _yum_retry_sleep {
    sleep $_[0];
    return;
}

# Fire-and-forget detached child running upcp --force.
sub _kick_upcp {
    my ($log) = @_;
    my $pid = fork();
    if ( !defined $pid ) {
        $log->warn("unblock_c7_v110_upcp: fork() failed, not invoking upcp: $!");
        return;
    }
    return if $pid;

    chdir('/');
    POSIX::setsid();
    for ( 3 .. 1024 ) {
        POSIX::close($_);
    }
    open STDIN,  '<', '/dev/null';
    open STDOUT, '>', '/dev/null';
    open STDERR, '>', '/dev/null';

    exit 0 unless _claim_upcp_kicker_lock();

    if ( _wait_for_upcp_to_finish() ) {
        exec '/usr/local/cpanel/scripts/upcp', '--force';
        exit 1;
    }
    exit 0;
}

sub _claim_upcp_kicker_lock {
    for ( 1, 2 ) {
        if (
            sysopen(
                my $fh, $upcp_kicker_pidfile,
                Fcntl::O_WRONLY() | Fcntl::O_CREAT() | Fcntl::O_EXCL() | Fcntl::O_NOFOLLOW()
            )
        ) {
            print {$fh} "$$\n";
            close $fh;
            return 1;
        }
        my $existing = _read_kicker_pidfile();
        return 0 if defined $existing && $existing =~ /^\d+$/ && _pid_is_alive($existing);
        unlink $upcp_kicker_pidfile or return 0;
    }
    return 0;
}

sub _pid_is_alive {
    my ($pid) = @_;
    return 1 if kill( 0, $pid );
    return 1 if $! == POSIX::EPERM();
    return 0;
}

sub _read_kicker_pidfile {
    sysopen( my $fh, $upcp_kicker_pidfile, Fcntl::O_RDONLY() | Fcntl::O_NOFOLLOW() ) or return;
    my $pid = readline($fh);
    close $fh;
    return unless defined $pid;
    chomp $pid;
    return $pid;
}

# Returns 1 if the caller should still exec upcp (i.e., no other upcp is running).
sub _wait_for_upcp_to_finish {
    my $waited = 0;
    while ( $waited < $UPCP_WAIT_MAX ) {
        my %pids = Cpanel::ProcessCheck::previouspids( 'process' => 'upcp' );
        delete $pids{$$};
        return 1 if !scalar keys %pids;

        _upcp_wait_sleep($UPCP_WAIT_INTERVAL);
        $waited += $UPCP_WAIT_INTERVAL;
    }
    return 1;
}

sub _upcp_wait_sleep {
    sleep $_[0];
    return;
}

sub _read_version {
    open( my $fh, '<', '/usr/local/cpanel/version' ) or return '';
    my $version = readline($fh);
    close $fh;
    return '' unless defined $version;
    chomp $version;
    return $version;
}

sub supported_on_this_major {
    my ( $min_ver, $max_ver ) = @_;

    my $major = get_major_version();

    return 0 if $major < $min_ver;
    return 0 if $major > $max_ver;

    return 1;
}

sub get_major_version {
    my $major_version;

    if ( open( my $fh, '<', '/usr/local/cpanel/version' ) ) {
        my $full_version = <$fh>;
        close($fh);
        if ( length $full_version ) {
            chomp $full_version;
            ($major_version) = $full_version =~ /^[0-9]+\.([0-9]+)/;
        }
    }

    return $major_version || 30;
}

1;
