Files
webmin/package-updates/update.cgi
Ilia Ross 9faa2b4323 Fix failed DNF update checks showing no updates
This PR fixes the Package Updates module reporting "no updates" when dnf check-update fails.

DNF prints its error to stderr and exits with status 1, but the module only read stdout, so a failed check looked like an empty list and was cached for hours. Users could go weeks or months without seeing that updates were blocked!

The module now checks the exit status. When a check fails, it keeps the last known list, shows the error on the module and refresh pages, and retries after 15 minutes instead of caching an empty result.

Scheduled checks email the failure instead of acting on the old list, and in new-only mode the failure is emailed once until a check succeeds again.

Fixes https://forum.virtualmin.com/t/no-warning-of-dnf-error-suppressing-update/138052
2026-09-25 02:10:53 +02:00

249 lines
7.5 KiB
Perl
Executable File

#!/usr/local/bin/perl
# Update selected packages
require './package-updates-lib.pl';
&ReadParse();
if ($in{'redir'}) {
$redir = $in{'redir'};
$redirdesc = $in{'redirdesc'};
}
elsif ($in{'redirdesc'}) {
$redir = $ENV{'HTTP_REFERER'};
$redirdesc = $in{'redirdesc'};
}
else {
$redir = "index.cgi?mode=".&urlize($in{'mode'}).
"&search=".&urlize($in{'search'});
$redirdesc = $text{'index_return'};
$redir = $redir =~ /tab=/ ? $redir :
$redir =~ /\?/ ? "$redir&tab=pkgs" : "$redir?tab=pkgs";
}
$hold_action = $in{'hold'} ? 1 : $in{'unhold'} ? 0 : undef;
if (defined($hold_action)) {
# Hold or unhold selected packages
&supports_package_holds() || &error($text{'hold_enotsupported'});
@holdpkgs = split(/\0/, $in{'u'});
@holdpkgs || &error($text{'hold_enone'});
@current = &list_current(1);
%current = map { $_->{'name'}."/".$_->{'system'}, 1 } @current;
@held = &list_package_holds();
@holdnames = ( );
foreach $ps (@holdpkgs) {
($p, $s) = split(/\//, $ps, 2);
$current{$p."/".$s} || &error(&text('hold_enotinstalled', $p));
$s eq $software::update_system ||
&error(&text('hold_esystem', $p));
if (!$hold_action && !&package_is_held($p, \@held)) {
&error(&text('hold_enotheld', $p));
}
push(@holdnames, $p);
}
@holdnames = &unique(@holdnames);
$err = &update_package_holds(\@holdnames, $hold_action);
&error(&text($hold_action ? 'hold_efailed' : 'unhold_efailed', $err))
if ($err);
&flush_package_caches();
$logaction = $hold_action ? 'hold' : 'unhold';
&webmin_log($logaction, "packages", scalar(@holdnames),
{ 'packages' => \@holdnames });
&redirect("index.cgi?mode=".&urlize($in{'mode'}).
"&search=".&urlize($in{'search'}));
}
elsif ($in{'refresh'} || $in{'refresh_top'}) {
&ui_print_unbuffered_header(undef, $text{'refresh_title'}, "");
# Clear all caches
print $text{'refresh_clearing'},"<br>\n";
&flush_package_caches();
&clear_repository_cache();
print $text{'refresh_done'},"<p>\n";
# Force re-fetch
print $text{'refresh_available'},"<br>\n";
@avail = &list_possible_updates();
$err = &get_updates_error();
if ($err) {
# The check failed, so show the error instead of the count
print $text{'refresh_failed'},"<br>\n";
print "<pre>",&html_escape($err),"</pre>\n";
}
else {
# The check worked, so show how many updates were found
print &text('refresh_done3', scalar(@avail)),"<p>\n";
}
&webmin_log("refresh");
&ui_print_footer($redir, $redirdesc);
}
else {
# Upgrade some packages
my @pkgs = split(/\0/, $in{'u'});
@pkgs || &error($text{'update_enone'});
$allow_held = 0;
if ($in{'mode'} eq 'held') {
# Only Held updates can override a hold for one transaction.
&supports_package_holds() || &error($text{'hold_enotsupported'});
@held = &list_package_holds();
foreach $ps (@pkgs) {
($p, $s) = split(/\//, $ps, 2);
$s eq $software::update_system &&
&package_is_held($p, \@held) ||
&error(&text('update_enotheld', $p));
}
$allow_held = 1;
}
$install_flags = $allow_held ? &software::update_system_hold_flags() :
$in{'flags'};
&ui_print_unbuffered_header(undef,
$in{'mode'} eq 'new' ? $text{'update_title2'} : $text{'update_title'}, "");
# Save this CGI from being killed by a webmin or apache upgrade
$SIG{'TERM'} = 'IGNORE';
$SIG{'PIPE'} = 'IGNORE';
# Work out what will be done, if possible
@ops = ( );
if (!$in{'confirm'}) {
print $text{'update_ops'},"<p>\n";
@pkgnames = ( );
foreach my $ps (@pkgs) {
($p, $s) = split(/\//, $ps);
push(@pkgnames, $p);
}
@ops = &list_package_operations(join(" ", @pkgnames), $s,
$install_flags);
&error($text{'update_enoheldops'}) if (!@ops && $allow_held);
}
if (@ops) {
# Ask first
my $getconfform = sub {
my ($bottom) = @_;
my $bottom_sel;
$bottom_sel = 'data-outside-of-viewport'
if ($bottom);
my $confform = &ui_form_start("update.cgi", "post", undef, $bottom_sel);
$confform .= &ui_hidden("mode", $in{'mode'});
$confform .= &ui_hidden("search", $in{'search'});
$confform .= &ui_hidden("redir", $in{'redir'});
$confform .= &ui_hidden("redirdesc", $in{'redirdesc'});
foreach $ps (@pkgs) {
$confform .= &ui_hidden("u", $ps);
}
$confform .= &ui_alert_box($text{'update_heldnote'},
'warn', undef, undef, '')
if ($allow_held && !$bottom);
$confform .= &text('update_rusure', scalar(@ops)),"<p>\n"
if (!$bottom);
$confform .= &ui_form_end([ [ "confirm",
$allow_held ? $text{'update_confirmheld'} :
$text{'update_confirm'} ] ]);
};
print &$getconfform();
# Show table of all depends
@current = &list_current(1);
print &ui_columns_start([ $text{'index_name'},
$text{'update_oldver'},
$text{'update_newver'},
$text{'index_desc'},
], 100);
foreach $p (@ops) {
($c) = grep { $_->{'name'} eq $p->{'name'} &&
$_->{'system'} eq $p->{'system'} } @current;
if (!$c && !@avail) {
# Only get available if needed
@avail = &list_available(0);
}
($a) = grep { $_->{'name'} eq $p->{'name'} &&
$_->{'system'} eq $p->{'system'} } @avail;
print &ui_columns_row([
$p->{'name'},
$c ? $c->{'version'}
: "<i>$text{'update_none'}</i>",
$p->{'version'},
$c ? $c->{'desc'} :
$a ? $a->{'desc'} : '',
]);
}
print &ui_columns_end();
print &$getconfform(1), &ui_hide_outside_of_viewport();
}
else {
# Check if a reboot was required before
$reboot_before = &check_reboot_required();
# Do it
$msg = $in{'mode'} eq 'new' ? 'update_pkg2' : 'update_pkg';
&start_update_progress([ map { (split(/\//, $_))[0] } @pkgs ]);
if ($config{'update_multiple'} && @pkgs > 1) {
# Update all packages at once
@pkgnames = ( );
foreach my $ps (@pkgs) {
($p, $s) = split(/\//, $ps);
push(@pkgnames, $p);
$pkgsystem ||= $s;
}
print &text($msg, "<tt>".&html_escape(join(" ", @pkgnames))."</tt>"),
"<br>\n";
print "<ul data-package-updates='1'>\n";
@got = &package_install_multiple(
\@pkgnames, $pkgsystem, $in{'mode'} eq 'new', $install_flags);
print "</ul><br>\n";
}
else {
# Do them one by one in a loop
foreach my $ps (@pkgs) {
($p, $s) = split(/\//, $ps);
next if ($donedep{$p});
print &text($msg, "<tt>@{[&html_escape($p)]}</tt>"),"<br>\n";
print "<ul data-package-updates='2'>\n";
@pgot = &package_install(
$p, $s, $in{'mode'} eq 'new', $install_flags);
foreach $g (@pgot) {
$donedep{$g}++;
}
push(@got, @pgot);
print "</ul><br>\n";
}
}
if (@got) {
print &text('update_ok', scalar(@got)),"<p>\n";
}
else {
print $text{'update_failed'},"<p>\n";
}
&end_update_progress(\@pkgs);
# Refresh collected package info
print $text{'refresh_available'},"<br>\n";
if (&foreign_check("system-status")) {
&foreign_require("system-status");
&system_status::refresh_possible_packages(\@got);
}
# Refresh collected package info
if (&foreign_check("virtual-server") && @got) {
&foreign_require("virtual-server");
&virtual_server::refresh_possible_packages(\@got);
}
print $text{'refresh_done'},"<p>\n";
# Check if a reboot is required now
if (!$reboot_before && &check_reboot_required() &&
&foreign_check("init")) {
print &ui_form_start(
"@{[&get_webprefix()]}/init/reboot.cgi");
print &ui_hidden("confirm", 1);
print "<b>",$text{'update_rebootdesc'},"</b><p>\n";
print &ui_form_end(
[ [ undef, $text{'update_reboot'} ] ]);
}
&webmin_log("update", "packages", scalar(@got),
{ 'got' => \@got });
}
&ui_print_footer($redir, $redirdesc);
}