#!/usr/bin/perl ################################################################################ # Mageia Online # # # # Copyright (C) 2003-2010 Mandriva # # 2010-2016 Mageia # # # # Daouda Lo # # Thierry Vignaud # # # # This program is free software; you can redistribute it and/or modify # # it under the terms of the GNU General Public License Version 2 as # # published by the Free Software Foundation. # # # # This program is distributed in the hope that it will be useful, # # but WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # # GNU General Public License for more details. # # # # You should have received a copy of the GNU General Public License # # along with this program; if not, see . # ################################################################################ use strict; use POSIX qw(:signal_h :sys_wait_h); use lib qw(/usr/lib/libDrakX); use standalone; # for explanations use common; use run_program; use feature 'state'; BEGIN { unshift @::textdomains, 'mgaonline' } use mygtk3 qw(gtknew); #- do not import gtkadd which conflicts with ugtk3 version use ugtk3 qw(:all); use lib qw(/usr/lib/libDrakX/drakfirsttime); use mgaonline; use mgaapplet; use mgaapplet_gui; use Gtk3::Notify '-init', 'mgaapplet'; use Net::DBus qw(:typing); use Rpmdrake::open_db; POSIX::sigprocmask(SIG_BLOCK, POSIX::SigSet->new(SIGCHLD)); if (!find { $_ eq '--auto-update' } @ARGV) { if (my $pid = mgaonline::is_running('mgaapplet')) { if ($::testing) { warn "mgaapplet already running ($pid)\n"; } else { die "mgaapplet already running ($pid)\n"; } } } my $online_site = "http://www.mageia.org/"; my ($menu, $timeout, $network_timeout, $state_global, $sub_state); my ($download_dir); { my $temp_urpm = Rpmdrake::open_db::fast_open_urpmi_db(); $root = $temp_urpm->{root}; $download_dir = $temp_urpm->{cachedir}; } read_sys_config(); $config{UPDATE_FREQUENCY} ||= 3*60*60; # default to 3hours if ($::testing || -f "$root/var/lib/mageia-prepare-upgrade/state") { $config{FIRST_CHECK_DELAY} = 1 * 1000; # 1 second } else { $config{FIRST_CHECK_DELAY} ||= 5 * 60 * 1000; # default to 5 minutes } $config{DISTRO_CHECK_DELAY} ||= 60*60*24; # default : one day my %state = ( delayed => { colour => [ 'busy' ], menu => [ 'check' ], do_not_use_bubble => 1, tt => [ #-PO: here %s will be replaced by the local time (eg: "Will check updates at 14:03:50" N("Will check updates at %s", POSIX::strftime("%T", localtime(time() + $config{FIRST_CHECK_DELAY}/1000))) ], }, okay => { colour => [ 'okay' ], menu => [ 'check' ], do_not_use_bubble => 1, tt => [ N_("Your system is up-to-date") ] }, critical => { colour => [ 'noconf' ], menu => [ 'check' ], tt => [ N_("Service configuration problem. Please check logs and send mail to support\@mageiaonline.com") ] }, busy => { colour => [ 'busy' ], menu => [], do_not_use_bubble => 1, tt => [ N_("Please wait, finding available packages...") ] }, updates => { colour => [ 'updates' ], menu => [ 'update', 'check' ], tt => [ N_("New updates are available for your system") ] }, new_distribution => { colour => [ 'bundle' ], menu => [ 'upgrade_distro', 'check' ], urgency => 'low', tt => [ N("A new version of Mageia distribution has been released") ] }, no_more_supported => { colour => [ 'disabled' ], menu => [ 'check' ], urgency => 'low', tt => [] }, disconnected => { colour => [ 'disconnect' ], menu => [ 'confNetwork' ], tt => [ N_("Network is down. Please configure your network") ], do_not_use_bubble => mgaonline::is_running('net_applet'), }, disabled => { colour => [ 'disabled' ], menu => [], tt => [ N_("Service is not activated. Please click on \"Online Website\"") ] }, locked => { colour => [ 'noconf' ], menu => [ 'check' ], tt => [ N_("urpmi database locked") ], do_not_use_bubble => 1, }, loop_locked => { colour => [ 'noconf' ], menu => [ 'check' ], tt => [ N_("urpmi database locked") ], }, notsupported => { colour => [ 'disabled' ], menu => [], tt => [ N_("Release not supported (too old release, or development release)") ] }, no_update_medium => { colour => [ 'noconf' ], menu => [ 'check' ], tt => [ N_("No medium found. You must add some media through 'Software Media Manager'.") ], }, no_enabled_medium => { colour => [ 'noconf' ], menu => [ 'check' ], tt => [ N("You already have at least one update medium configured, but all of them are currently disabled. You should run the Software Media Manager to enable at least one (check it in the \"%s\" column). Then, restart \"%s\".", N("Enabled"), 'mgaapplet') ], }, ); my %actions = ( 'update' => { name => N("Install updates"), launch => \&installUpdates }, 'check' => { name => N("Check Updates"), launch => \&checkUpdates }, 'confNetwork' => { name => N("Configure Network"), launch => \&configNetwork }, 'upgrade_distro' => { name => N("Upgrade the system"), launch => \&upgrade }, ); my $icon = Gtk3::StatusIcon->new; #$icon->shape_combine_mask($img, 0, 0); $icon->signal_connect(popup_menu => sub { my ($_icon, $button, $time) = @_; $menu and $menu->popup(undef, undef, undef, undef, $button, $time); }); $icon->signal_connect(activate => sub { my %click_actions = ( no_update_medium => \&add_media, no_enabled_medium => \&add_media, updates => \&installUpdates, new_distribution => \&upgrade, no_more_supported => \&no_more_supported, ); my $action = $state_global; # default to updates rather than distro upgrade: if ($action eq 'new_distribution' && $sub_state eq 'updates') { $action = 'updates'; } $click_actions{$action}->() if ref $click_actions{$action}; }); foreach my $opt (@ARGV) { if (member($opt, qw(--force -f))) { setAutoStart('TRUE') } if ($opt =~ /--(rpm-root|urpmi-root)=(.*)/) { $::rpmdrake_options{$1}[0] = $2; } } my ($download_all); my ($new_distro, $no_more_supported); my ($current_apimdv_distro); get_product_id(); shouldStart() or die "$localfile should be set to TRUE: please use --force or -f option to launch applet\n"; go2State('delayed'); Glib::Timeout->add_seconds($config{FIRST_CHECK_DELAY}/1000, sub { POSIX::sigprocmask(SIG_UNBLOCK, POSIX::SigSet->new(SIGCHLD)); $SIG{CHLD} = \&harvester; # schedule future checks: setup_cyclic_check(); # perform a test after initial delay: checkNetwork(); checkUpdates(); 0; }); $SIG{USR1} = 'IGNORE'; $SIG{USR2} = 'IGNORE'; $SIG{HUP} = \&restart_applet; run_program::raw({ detach => 1 }, 'ionice', '-p', $$, '-n7'); my $bubble = Gtk3::Notify::Notification->new('', ''); Gtk3->main; ugtk3::exit(0); sub gnome_shell_exit_overview() { eval { Net::DBus->session->get_service('org.gnome.Shell')->get_object('/org/gnome/Shell', 'org.freedesktop.DBus.Properties')->Set('org.gnome.Shell', 'OverviewActive', dbus_boolean(0)) }; eval { Net::DBus->session->get_service('org.gnome.Shell')->get_object('/org/gnome/Shell', 'org.gnome.Shell')->Eval('Main.messageTray._trayState==2 && Main.messageTray.toggle();') }; } sub is_there_a_new_distributions() { # sanity check for cooker/cauldron: return if $product_id->{branch} eq 'Devel'; my @distros = get_distro_list(); return if !@distros; # do not do anything if current distribution isn't listed on api.mdv.com: return if !member($product_id->{version}, map { $_->{version} } @distros); # only compare first distro: if it's not the same as the currently installed one, # then it's the most recent release: my $new_distribution = $distros[0]; if (-e get_stale_upgrade_filename()) { $new_distro = $new_distribution; return 1; } $current_apimdv_distro = find_current_distro(@distros); $no_more_supported = $current_apimdv_distro->{obsoleted_by}; refresh_no_more_supported_msg(); if ($no_more_supported) { $new_distro = find { $_->{version} eq $no_more_supported } @distros; } if ($new_distribution && $new_distribution->{version} ne $product_id->{version}) { $new_distro = $new_distribution; log::explanations(sprintf("new '%s' distribution was released on %s", $new_distro->{version}, $new_distro->{release_date})); return 1; } } my ($mdv_update_pid, $checker_pid, $media_manager_pid, $locked_count); # FIXME: flushing a hash would be less error prone when adding new stuff: sub clean_distro_cache() { undef $new_distro; undef $no_more_supported; } sub is_false { my ($s) = @_; !text2bool($s) && $s != 1; } sub process_state { my ($state) = @_; log::explanations($state->{log}); $sub_state = $state->{status}; if ($sub_state eq 'locked') { $locked_count++; $sub_state = 'loop_locked' if $locked_count > 10; } else { $locked_count = 0; } # busy critical delayed disabled disconnected locked loop_locked new_distribution no_enabled_medium no_more_supported no_update_medium notsupported okay update if (!member($sub_state, qw(okay))) { go2State($sub_state); } elsif ($no_more_supported && !text2bool($local_config{DO_NOT_ASK_FOR_NO_MORE_SUPPORTED})) { go2State('no_more_supported'); } elsif ($new_distro && is_false($config{DO_NOT_ASK_FOR_DISTRO_UPGRADE}) && is_false($local_config{DO_NOT_ASK_FOR_DISTRO_UPGRADE})) { go2State('new_distribution'); } else { go2State($sub_state); } } # Signal management sub harvester { my ($_signame, $_clean) = @_; my ($childpid, @pids); my $schedule_checks; do { $childpid = waitpid(-1, &WNOHANG); my $status = $? >> 8; if ($mdv_update_pid && $mdv_update_pid == $childpid) { undef $mdv_update_pid; # make sure to not report new distro after distro upgrade: clean_distro_cache(); $schedule_checks = 1; } elsif ($checker_pid && $checker_pid == $childpid) { undef $checker_pid; my ($state) = grep { $_->{code} eq $status } values %comm_codes; if ($state) { process_state($state); } } elsif ($media_manager_pid && $media_manager_pid == $childpid) { undef $media_manager_pid; $schedule_checks = 1; } push @pids, $childpid; } while $childpid > 0; Glib::Timeout->add(200, sub { silentCheck(); 0 }) if $schedule_checks; return @pids; } sub restart_applet() { local $SIG{CHLD} = 'DEFAULT'; log::explanations(N("Received SIGHUP (probably an upgrade has finished), restarting applet.")); { redo if wait() > 0 } exec($0, '--auto-update'); } # FIXME: we can run many drakconnect when network is down: sub configNetwork() { log::explanations(N_("Launching drakconnect\n")); fork_exec("/usr/sbin/drakconnect"); } sub confirm_upgrade() { local $mygtk3::left_padding = 0; my $warn_me = text2bool($local_config{DO_NOT_ASK_FOR_DISTRO_UPGRADE}); my $w = new_portable_dialog(N("New version of Mageia distribution")); my ($temp_dir, $box); my $browse; $browse = gtksignal_connect( Gtk3::FileChooserButton->new(N("Browse"), 'select-folder'), 'current-folder-changed' => sub { $temp_dir = $_[0]->get_current_folder; my $ok = -d $temp_dir && ! -l $temp_dir && ((stat($temp_dir))[4] == 0); $ok or ask_warn(N("Error"), N("You must choose a directory owned by the super administrator!")); }); $browse->set_current_folder($download_dir); my $res = fill_n_run_portable_dialog($w, [ get_banner(), gtknew('Label_Left', text => N("A new version of Mageia distribution has been released."), @common), gtknew('HButtonBox', layout => 'start', children_tight => [ new_link_button($new_distro->{url}, N("More info about this new version")), ]), gtknew('Label_Left', text => N("Do you want to upgrade to the '\%s' distribution?", $new_distro->{name} || $new_distro->{version}), @common), gtknew('CheckButton', text => N("Do not ask me next time"), active_ref => \$warn_me), gtknew('CheckButton', text => N("Download all packages at once") . "\n" . N("(Warning: You will need quite a lot of free space)"), active_ref => \$download_all, sensitive_ref => \$browse, toggled => sub { $box and $box->set_sensitive($download_all) }, ), $box = gtknew('HBox', sensitive => $download_all, children => [ 0, gtknew('Label_Left', text => N("Where to download packages:")), 1, $browse, ]), create_okcancel($w, N("Next"), N("Cancel")), ]); setVar('DO_NOT_ASK_FOR_DISTRO_UPGRADE', bool2text($warn_me)); $local_config{DO_NOT_ASK_FOR_DISTRO_UPGRADE} = bool2text($warn_me); if ($res) { my $ok = -d $temp_dir && ! -l $temp_dir && ((stat($temp_dir))[4] == 0); $ok or goto &confirm_upgrade; $download_dir = $temp_dir; really_confirm_upgrade(); } else { return 0; } } sub get_obsolete_message_() { N("Maintenance for this Mageia version has ended. No more updates will be delivered for this system."); } sub get_obsolete_message() { join("\n\n", get_obsolete_message_(), N("In order to keep your system secure, you can:"), ); } sub refresh_no_more_supported_msg() { my $basic_msg = get_obsolete_message_(); my $distro = N("Mageia"); my $msg = N("You should upgrade to a newer version of the %s distribution.", $distro); $state{no_more_supported}{tt}[0] = join(' ', $basic_msg, $msg); } sub no_more_supported_choice() { local $mygtk3::left_padding = 0; my $warn_me = text2bool($local_config{DO_NOT_ASK_FOR_NO_MORE_SUPPORTED}); # FIXME: just tell radio buttons' children to wrap instead: local $mgaapplet_gui::width = 580; my $w = new_portable_dialog(N("Your distribution is no longer supported")); my ($b1, $b2); my $choice = $no_more_supported ne 'none' ? 'upgrade' : undef; my @widgets = ( get_banner(N("New version of Mageia distribution")), gtknew('Label_Left', text => get_obsolete_message() . "\n", @common), ($no_more_supported ne 'none' ? ( gtknew('VBox', children_tight => [ $b2 = gtknew('RadioButton', text => N("Do you want to upgrade to the '\%s' distribution?", $new_distro->{name} || $new_distro->{version}), toggled => sub { ($choice, $warn_me) = ('upgrade', undef) if $_[0]->get_active; }, if_($b1, group => $b1)), new_link_button($new_distro->{url}, N("More info about this new version")), ]), gtknew('HSeparator'), ) : ()), gtknew('RadioButton', text => N("Do not ask me next time"), toggled => sub { $choice = 'nothing' if $_[0]->get_active; $warn_me = $_[0]->get_active }, group => $b1 || $b2), create_okcancel($w, N("Next"), N("Cancel")), ); #$_ and $_->set_border_width(8) foreach $b1, $b2, $b3; # explicitely wrap too long message: foreach ($b1, $b2) { next if !$_ || !$_->get_child; $_->get_child->set_line_wrap(1); $_->get_child->set_size_request($width-50, -1); } my $res = fill_n_run_portable_dialog($w, \@widgets); setVar('DO_NOT_ASK_FOR_NO_MORE_SUPPORTED', bool2text($warn_me)); $local_config{DO_NOT_ASK_FOR_NO_MORE_SUPPORTED} = bool2text($warn_me); if ($res) { return $choice; } else { return 0; } } my $no_more_supported_wip; sub no_more_supported() { return if $mdv_update_pid || $no_more_supported_wip; gnome_shell_exit_overview(); $no_more_supported_wip = 1; my $choice = no_more_supported_choice(); if ($choice eq 'upgrade') { really_confirm_upgrade() and real_upgrade(); } elsif ($choice eq 'nothing') { $icon->set_visible(0); } undef $no_more_supported_wip; } sub really_confirm_upgrade() { local $mygtk3::left_padding = 0; my $w = ugtk3->new(N("New version of Mageia distribution"), width => $width + 20); # estimated package size: my $c; foreach (run_program::get_stdout('rpm', '-qa', '--qf', '%{Archivesize}\n')) { $c += $_; } $c = formatXiB($c); { # use wizard button order (for both 2008.1 & 2009.0): local $::isWizard = 1; local $w->{pop_it} = 0; local $::isInstall = 1; gtkadd($w->{window}, gtknew('VBox', children_tight => [ get_banner(), gtknew('Label_Left', text => N("This upgrade requires high bandwidth network connection (cable, xDSL, ...) and may take several hours to complete."), @common), gtknew('Label_Left', text => N("Estimated download data will be %s", $c), @common), gtknew('Label_Left', text => N("You should close all other running applications before continuing.") . (detect_devices::isLaptop() ? ' ' . N("You should put your laptop on AC and favor ethernet connection over wifi, if available.") : ''), @common), create_okcancel($w, N("Next"), N("Cancel")), ]), ); } $w->{ok}->grab_focus; return $w->main; } sub upgrade() { return if $mdv_update_pid; gnome_shell_exit_overview(); return if !confirm_upgrade(); real_upgrade(); } sub real_upgrade() { $mdv_update_pid = fork_exec('mgaapplet-upgrade-helper', "--new_distro_version=$new_distro->{version}", if_($::testing, '--testing'), if_($download_all, "--download-all=$download_dir"), if_($root, "--urpmi-root=$root")); } sub add_media() { return if $media_manager_pid; log::explanations("Launching 'Software Media Manager'"); gnome_shell_exit_overview(); $media_manager_pid = fork_exec('/usr/sbin/edit-urpm-sources.pl', '--no-splash', if_($root, "--urpmi-root=$root")); } sub installUpdates() { return if $mdv_update_pid; log::explanations(N_("Launching MageiaUpdate\n")); gnome_shell_exit_overview(); my $program = find { -x "/usr/bin/$_" } qw(MageiaUpdate MandrivaUpdate); $mdv_update_pid = fork_exec($program, '--no-media-update', '--no-confirmation', '--no-splash', if_($root, "--urpmi-root=$root")); silentCheck(); gtkflush(); } sub silentCheck() { state $check_time; my $new_time = time(); if (!$check_time || $new_time - $check_time > $config{DISTRO_CHECK_DELAY}) { clean_distro_cache(); $check_time = $new_time; is_there_a_new_distributions(); } return if $mdv_update_pid || $checker_pid; log::explanations(N_("Computing new updates...\n")); go2State('busy'); # are there any updates ? $checker_pid = fork_exec('mgaapplet-update-checker', $root); if (!$checker_pid) { log::explanations("cannot fork: %s", "update checker ($!)"); go2State('critical'); } } sub setup_cyclic_check() { $network_timeout = Glib::Timeout->add(2000, sub { checkNetwork(); 1 }); $timeout = Glib::Timeout->add_seconds($config{UPDATE_FREQUENCY}, sub { checkUpdates(); 1; }); } sub getTime() { my $d = localtime(); $d =~ s/\s+/_/g; $d; } sub setLastTime() { my $date = getTime(); setVar('LASTCHECK', $date); } sub checkNetwork() { return if $checker_pid; require network::tools; if (!network::tools::has_network_connection()) { # do not notify if already done: return if member($state_global, qw(disconnected)); log::explanations(N_("Checking Network: seems disabled\n")); go2State('disconnected'); } elsif (member($state_global, qw(disconnected))) { silentCheck(); #- state has changed, update } } sub checkUpdates() { member($state_global, qw(disconnected)) or silentCheck(); } sub go2State { my ($state) = @_; $menu->destroy if $menu; $menu = setState($state); $state_global = $state; gtkflush(); } sub shouldStart() { to_bool($local_config{AUTOSTART} ne 'FALSE'); } sub about_dialog() { my $ver = 1; # automatically set from spec file my $url = $online_site; $url =~ s/^https:/http:/; my $w = gtknew('AboutDialog', name => N("Mageia Online %s", $ver), copyright => N("Copyright (C) %s by %s", 'Mandriva', '2001-2010') . "\n" . N("Copyright (C) %s by %s", 'Mageia', '2010-2016'), license => join('', cat_('/usr/share/common-licenses/GPL')), icon => '/usr/share/icons/mini/mgaonline.png', comments => N("Mageia Online gives access to Mageia web services."), website => $url, website_label => N("Online WebSite"), authors => [ 'Thierry Vignaud ' ], translator_credits => #-PO: put here name(s) and email(s) of translator(s) (eg: "John Smith ") N("_: Translator(s) name(s) & email(s)\n"), transient_for => $::main_window, modal => 1, position_policy => 'center-on-parent', ); $w->show_all; $w->run; return 1; } sub setState { my ($state) = @_; my $checkme; state $previous_state; my @arr = @{$state{$state}{menu}}; my $tmp = eval { gtkcreate_pixbuf($state{$state}{colour}[0]) }; $icon->set_from_pixbuf($tmp) if $tmp; $icon->set_tooltip_text(formatAlaTeX(translate($state{$state}{tt}[0]))); my @invisible_states = qw(delayed okay disconnected locked); $icon->set_visible(!member($state, @invisible_states)); # do not show icon while checking if previously hidden: $icon->set_visible(0) if $state eq 'busy' && member($previous_state, @invisible_states); $previous_state = $state; gtkflush(); # so that bubbles are displayed on right icon if ($state{$state}{tt}[0] && $icon->isa('Gtk3::StatusIcon') && !$state{$state}{do_not_use_bubble}) { $bubble->clear_actions; $bubble->update(N("Warning"), formatAlaTeX(translate($state{$state}{tt}[0])) . "\n", '/usr/share/icons/mgaonline.png'); if ($state eq 'new_distribution') { $bubble->add_action('upgrade', N("More Information"), sub { upgrade() }); if ($sub_state eq 'updates') { push @arr, 'update'; } } elsif ($state eq 'no_more_supported') { $bubble->add_action('no_more', N("More Information"), sub { no_more_supported() }); if ($sub_state eq 'updates') { push @arr, 'update'; } } elsif ($state eq 'updates') { unshift @arr, 'upgrade_distro' if $new_distro; $bubble->add_action('updates', N("Install updates"), sub { installUpdates() }); } elsif (member($state, qw(no_enabled_medium no_update_medium))) { $bubble->add_action('add_med', N("Add media"), sub { add_media() }); } $bubble->set_urgency($state{$state}{urgency}) if $state{$state}{urgency}; my $timeout = 5000; $bubble->set_timeout($timeout); # both need to be in a eval block in case notification daemon isn't running: Glib::Timeout->add($timeout, sub { eval { $bubble->close }; 0 }); eval { $bubble->show }; warn ">> ERR:$@" if $@; } my $menu = Gtk3::Menu->new; foreach (@arr) { my $action = $actions{$_}; next if !ref($action->{launch}); $menu->append(gtksignal_connect(gtkshow(Gtk3::MenuItem->new_with_label($action->{name})), activate => $action->{launch})); } $menu->append(gtkshow(Gtk3::SeparatorMenuItem->new)); $menu->append(gtksignal_connect(gtkshow(Gtk3::MenuItem->new_with_label(N("About..."))), activate => \&about_dialog)); $menu->append(gtksignal_connect(gtkshow(Gtk3::MenuItem->new_with_label(N("Updates Configuration"))), activate => sub { run_program::raw({ detach => 1 }, 'mgaapplet-config') })); $menu->append(gtksignal_connect(gtkset_active($checkme = Gtk3::CheckMenuItem->new_with_label(N("Always launch on startup")), shouldStart()), toggled => sub { setAutoStart(uc(bool2text($checkme->get_active))) })); $checkme->show; $menu->append(gtksignal_connect(gtkshow(Gtk3::MenuItem->new_with_label(N("Quit"))), activate => sub { mainQuit() })); $menu; } sub setAutoStart { my $state = shift; my $date = getTime(); if (-f $localfile) { setVar('AUTOSTART', $state); } else { output_p($localfile, qq(AUTOSTART=$state LASTCHECK=$date )); } } sub mainQuit() { # setAutoStart('FALSE'); Glib::Source->remove($timeout) if $timeout; Glib::Source->remove($network_timeout) if $network_timeout; Gtk3->main_quit; }