#!/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_cl6_v110_upcp;

use strict;
use warnings;

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

use Cpanel::OS               ();
use Cpanel::Update::Config   ();
use Cpanel::Version::Compare ();
use Cpanel::Logger           ();
use Cpanel::ProcessCheck     ();
use File::Copy               ();
use Fcntl                    ();
use POSIX                    ();

our $CPANEL_TIER         = 'cl6110';
our $TARGET_VERSION_MAX  = '11.110.0.50';    # only run when current build is at or below this
our $cpupdate_conf       = '/etc/cpupdate.conf';
our $backup_path         = '/etc/cpupdate.conf.cp52976.bak';

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

exit run() unless caller;

# CPANEL-52976
#
# CloudLinux 6 hosts running cPanel 11.110.0.x are blocked at .50 because the
# .52 build dropped CL6 support. cPanel maintains a parallel update tier
# 'cl6110' specifically for these hosts. Set CPANEL=cl6110 in /etc/cpupdate.conf
# and kick a fresh upcp --force so the host immediately re-attempts the upgrade
# onto the cl6110 tier.
sub run {
    return 0 unless supported_on_this_major( 110, 110 );
    return 0 unless Cpanel::OS::major() == 6;

    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;

    _backup_cpupdate_conf($log);

    my %cfg = Cpanel::Update::Config::load();
    $cfg{'CPANEL'} = $CPANEL_TIER;
    Cpanel::Update::Config::save( \%cfg );

    $log->info("unblock_cl6_v110_upcp: set CPANEL=$CPANEL_TIER in $cpupdate_conf");

    _kick_upcp($log);

    return 0;
}

sub _backup_cpupdate_conf {
    my ($log) = @_;
    return if -e $backup_path;
    return if !-e $cpupdate_conf;
    if ( !File::Copy::copy( $cpupdate_conf, $backup_path ) ) {
        $log->warn("unblock_cl6_v110_upcp: failed to back up $cpupdate_conf to $backup_path: $!");
        return;
    }
    if ( my $mode = ( stat $cpupdate_conf )[2] ) {
        chmod( $mode & 07777, $backup_path );
    }
    return;
}

# Fire-and-forget detached child running upcp --force.
sub _kick_upcp {
    my ($log) = @_;
    my $pid = fork();
    if ( !defined $pid ) {
        $log->warn("unblock_cl6_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;
