diff --git a/package-updates/index.cgi b/package-updates/index.cgi index 3b003b104..fcd90ffca 100755 --- a/package-updates/index.cgi +++ b/package-updates/index.cgi @@ -51,6 +51,16 @@ print &ui_hidden("mode", $in{'mode'}); print &ui_grid_table(\@grid, 2),"

\n"; print &ui_form_end(); +# Warn if the last check for updates failed, as the list may be stale +$updates_error = &get_updates_error(); +if ($updates_error) { + print &ui_tag('div', &ui_details({ + 'title' => $text{'index_updateserr'}, + 'content' => &ui_tag('tt', &html_escape($updates_error)), + 'class' => 'error', + 'html' => 1 }, 1), {'style' => 'margin-bottom: 0.333em;'}); + } + # Work out what packages to show @current = &list_current(1); diff --git a/package-updates/lang/en b/package-updates/lang/en index 54a83fe30..9c5d37413 100644 --- a/package-updates/lang/en +++ b/package-updates/lang/en @@ -35,6 +35,7 @@ index_action_note=Updates may also be installed by $1; disable external updates index_action_disable=Disable external updates index_err=Failed to fetch package list index_refresh=Refresh Available Packages +index_updateserr=The last check for updates failed, so the list below may be out of date index_noupdate=No update exists from version $1 index_caninstall=Available for installation index_webmin=Webmin module $1 @@ -138,6 +139,7 @@ refresh_clearing=Clearing package caches .. refresh_done=.. done refresh_available=Re-fetching available packages .. refresh_done3=.. found $1 possible updates +refresh_failed=.. failed, so the last known list of updates is kept : view_title=Package Details view_header=Updatable package information diff --git a/package-updates/package-updates-lib.pl b/package-updates/package-updates-lib.pl index 91c5a7af2..1eca50ef8 100644 --- a/package-updates/package-updates-lib.pl +++ b/package-updates/package-updates-lib.pl @@ -18,10 +18,14 @@ eval "use WebminCore;"; "dnf-automatic-notifyonly.timer", ); +# Cache files for the package lists and the error from a failed check for +# updates $available_cache_file = &cache_file_path("available.cache"); $current_cache_file = &cache_file_path("current.cache"); $updates_cache_file = &cache_file_path("updates.cache"); $held_updates_cache_file = &cache_file_path("held-updates.cache"); +$updates_error_file = &cache_file_path("updates.error"); +$failed_check_retry_mins = 15; # Minutes before a failed check is retried $cron_cmd = "$module_config_directory/update.pl"; $yum_cache_file = &cache_file_path("yumcache"); @@ -222,6 +226,32 @@ if (-r $file) { return ( ); } +# save_updates_error([error]) +# Saves the error from a failed check for updates, or removes the saved +# error if none is given +sub save_updates_error +{ +my ($err) = @_; +if ($err) { + # Save it for the module pages and the scheduled job to report + &open_tempfile(ERR, ">$updates_error_file"); + &print_tempfile(ERR, $err); + &close_tempfile(ERR); + } +else { + # No error, so stop warning about an earlier failure + unlink($updates_error_file); + } +} + +# get_updates_error() +# Returns the error if the last check for updates failed, or undef +sub get_updates_error +{ +return undef if (!-r $updates_error_file); +return &read_file_contents($updates_error_file); +} + # compare_versions(&pkg1, &pkg2) # Returns -1 if the version of pkg1 is older than pkg2, 1 if newer, 0 if same. sub compare_versions @@ -356,25 +386,47 @@ return $text{'hold_enotsupported'} if (!&supports_package_holds()); return &software::update_system_hold($packages, $hold); } -# updates_available(no-cache, [include-held]) +# updates_available([no-cache], [include-held]) # Returns an array of hash refs of package updates available, according to -# the update system, with caching. +# the update system, with caching. If the check fails, returns the last +# known list and saves the error for get_updates_error. sub updates_available { my ($nocache, $include_held) = @_; +# Held updates have their own cache file and in-memory list my $cache_file = $include_held ? $held_updates_cache_file : $updates_cache_file; my $cache = $include_held ? \@held_updates_available_cache : \@updates_available_cache; +# Load the list, unless a non-empty one is already in memory if (!scalar(@$cache)) { if ($nocache || &cache_expired($cache_file)) { # Get from original source @$cache = &software::update_system_updates($include_held); + if ($software::update_system_error) { + # The check failed, so keep the last known list rather + # than caching an empty one, and save the error + @$cache = &read_cache_file($cache_file); + &save_updates_error($software::update_system_error); + } + else { + # The check worked, so clear any earlier error + &save_updates_error(undef); + } + # Add the fields callers use, and cache the list foreach my $a (@$cache) { $a->{'update'} = $a->{'name'}; $a->{'system'} = $software::update_system; } &write_cache_file($cache_file, $cache); + if ($software::update_system_error) { + # Expire the cache after $failed_check_retry_mins + # minutes, so a failed check is retried soon but not on + # every page load. A shorter cache time is left alone. + my $retry = time() - $config{'cache_time'}*60*60 + + $failed_check_retry_mins*60; + utime($retry, $retry, $cache_file) if ($retry < time()); + } } else { # Use on-disk cache @@ -750,14 +802,23 @@ if ($pkg->{'system'} eq 'yum') { return undef; } +# flush_package_caches() +# Clears the package list caches, so the next lookups fetch fresh data. Also +# removes any saved error from a failed check for updates. sub flush_package_caches { unlink($current_cache_file); -unlink($updates_cache_file); -unlink($held_updates_cache_file); unlink($available_cache_file); +# Expire the update lists rather than deleting them, so a failed check can +# still fall back to the last known list +foreach my $f ($updates_cache_file, $held_updates_cache_file) { + utime(0, 0, $f) if (-e $f); + } +# Delete the remaining cache files and the saved error unlink($available_cache_file.'0'); unlink($available_cache_file.'1'); +unlink($updates_error_file); +# Clear the lists held in memory by this process @packages_available_cache = ( ); @updates_available_cache = ( ); @held_updates_available_cache = ( ); diff --git a/package-updates/update.cgi b/package-updates/update.cgi index 9d871700f..d4a717a65 100755 --- a/package-updates/update.cgi +++ b/package-updates/update.cgi @@ -62,7 +62,16 @@ elsif ($in{'refresh'} || $in{'refresh_top'}) { # Force re-fetch print $text{'refresh_available'},"
\n"; @avail = &list_possible_updates(); - print &text('refresh_done3', scalar(@avail)),"

\n"; + $err = &get_updates_error(); + if ($err) { + # The check failed, so show the error instead of the count + print $text{'refresh_failed'},"
\n"; + print "

",&html_escape($err),"
\n"; + } + else { + # The check worked, so show how many updates were found + print &text('refresh_done3', scalar(@avail)),"

\n"; + } &webmin_log("refresh"); &ui_print_footer($redir, $redirdesc); diff --git a/package-updates/update.pl b/package-updates/update.pl index 562de1a57..1a002976b 100755 --- a/package-updates/update.pl +++ b/package-updates/update.pl @@ -13,6 +13,11 @@ if ($ARGV[0] eq "--debug" || $ARGV[0] eq "-debug") { &clear_repository_cache(); @todo = &list_possible_updates(); +# If the check for updates failed, the list is only the last known one, so +# ignore it and report the failure instead +$checkerr = &get_updates_error(); +@todo = ( ) if ($checkerr); + # Install packages that are needed $tellcount = 0; %already = ( ); @@ -41,6 +46,15 @@ $newcount = 0; $tellbody = ""; %notified = ( ); %pending = ( ); + +# A failed check for updates counts as a failure, so the "any update fails" +# mode emails it. It is also tracked like a pending update, so new-only mode +# emails it once until a check works again. Its key is fixed, because DNF's +# error text varies between runs. +if ($checkerr) { + $fcount++; + $pending{'error'} = 1; + } foreach $t (@todo) { next if ($already{$t->{'update'}}); my $umsg = $t->{'security'} ? "security update" : "update"; @@ -104,6 +118,13 @@ else { $body .= $tellbody; } +# Report a failed check for updates first, unless only new updates are +# being reported and the failure was already reported +if ($checkerr && (!$newonly || $newcount)) { + $body = "Checking for updates failed :\n". + "$checkerr\n\n".$body; + } + if (@updated && $config{'sched_post_script'}) { my @unique_updated = &unique(@updated); my ($post_ok, $post_body) = &run_update_script( diff --git a/software/yum-lib.pl b/software/yum-lib.pl index 68328cec7..2c4e40a34 100755 --- a/software/yum-lib.pl +++ b/software/yum-lib.pl @@ -505,19 +505,48 @@ close(PKG); # update_system_updates([include-holds]) # Returns available package updates, optionally including version-locked ones. +# On DNF, a failed check returns an empty list and sets $update_system_error +# to the error output. Any other result clears it, so callers can tell a +# failure from no updates. sub update_system_updates { my ($include_holds) = @_; local @rv; local %done; +$update_system_error = undef; if ($yum_command =~ /dnf/) { + # With DNF, add the flag that also lists version-locked updates if + # asked, escaping * for the shell my $holdflag = $include_holds && &supports_update_system_holds() ? " ".&update_system_hold_flags() : ""; $holdflag =~ s/\*/\\*/g; - &open_execute_command(PKG, - "$yum_command$holdflag check-update 2>/dev/null", 1, 1); + + # Capture the exit status: DNF exits with 0 for no updates, 100 for + # updates found and any other status on failure. Stderr goes to a file + # rather than a second pipe, which could fill up and block DNF while + # stdout is still being read. Stdin comes from /dev/null so a prompt, + # such as for a repository key import, cannot hang. + my $out = ""; + my $errfile = &transname(); + my $ex = &execute_command("$yum_command$holdflag check-update", + "/dev/null", \$out, $errfile, undef, 1); + my $code = $ex >> 8; + my $err = &read_file_contents($errfile); + unlink($errfile); + if ($code != 0 && $code != 100) { + # The check failed, so return no updates and set the error from + # stderr, or from stdout or the exit status if stderr is empty + $err =~ s/\s+$//; + $update_system_error = $err || $out || + "$yum_command check-update exited with status $code"; + return ( ); + } + # Parse the captured output with the shared loop below + open(PKG, "<", \$out); } else { + # YUM wraps long entries onto indented lines, so join them first. + # Failed YUM checks are not detected. &open_execute_command(PKG, "$yum_command check-update 2>/dev/null | tr '\n' '#' | sed -e 's/# / /g' | tr '#' '\n'", 1, 1); } while() {