/* livepatch-tray.c * Copyright (C) 2019 Canonical Ltd * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor * Boston, MA 02110-1301 USA. */ #include #include #include "update-notifier.h" #include "livepatch-tray.h" #include "livepatch-utils.h" #include "trayappletui.h" #define LIVEPATCH_COMMON_DIRECTORY "/var/snap/canonical-livepatch/common" #define LIVEPATCH_SNAP_DIRECTORY "/snap/canonical-livepatch" #define TIMEOUT_SECONDS_DELAY 2 typedef struct _LivepatchTrayAppletPriv LivepatchTrayAppletPriv; struct _LivepatchTrayAppletPriv { GtkWidget *menuitem_enabled; GtkWidget *menuitem_desc; GFileMonitor *common_dir_monitor; GFileMonitor *snap_dir_monitor; guint timeout_id; }; static void on_settings_menuitem_activated(GObject *self, gpointer user_data) { g_autoptr(GDesktopAppInfo) info = NULL; g_autoptr(GdkAppLaunchContext) context = NULL; g_autoptr(GError) error = NULL; info = g_desktop_app_info_new(LIVEPATCH_DESKTOP_FILE); if (info == NULL) { g_warning("Could not find application '%s'", LIVEPATCH_DESKTOP_FILE); return; } context = gdk_display_get_app_launch_context(gdk_display_get_default()); if (!g_app_info_launch(G_APP_INFO(info), NULL, G_APP_LAUNCH_CONTEXT(context), &error)) { g_warning("Could not launch application '%s'", LIVEPATCH_DESKTOP_FILE); } } static void livepatch_trayicon_create_menu(TrayApplet *ta) { LivepatchTrayAppletPriv *priv = (LivepatchTrayAppletPriv *) ta->user_data; GtkWidget *menuitem; ta->menu = gtk_menu_new(); priv->menuitem_enabled = gtk_menu_item_new(); gtk_widget_set_sensitive(priv->menuitem_enabled, FALSE); gtk_menu_shell_append(GTK_MENU_SHELL(ta->menu), priv->menuitem_enabled); priv->menuitem_desc = gtk_menu_item_new(); gtk_widget_set_sensitive(priv->menuitem_desc, FALSE); gtk_menu_shell_append(GTK_MENU_SHELL(ta->menu), priv->menuitem_desc); menuitem = gtk_separator_menu_item_new(); gtk_menu_shell_append(GTK_MENU_SHELL(ta->menu), menuitem); menuitem = gtk_menu_item_new_with_label(_("Livepatch Settings…")); gtk_menu_shell_append(GTK_MENU_SHELL(ta->menu), menuitem); g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(on_settings_menuitem_activated), ta); gtk_widget_show_all(ta->menu); } static gboolean check_livepatch(TrayApplet *ta) { LivepatchTrayAppletPriv *priv = (LivepatchTrayAppletPriv *) ta->user_data; gboolean show_status_icon; g_autofree gchar *check_state = NULL; g_autofree gchar *state = NULL; g_autoptr(GError) error = NULL; show_status_icon = g_settings_get_boolean(ta->un->settings, SETTINGS_KEY_SHOW_LIVEPATCH_ICON); if (!show_status_icon || !livepatch_is_supported() || !livepatch_is_running ()) { tray_applet_ui_set_visible(ta, FALSE); priv->timeout_id = 0; return G_SOURCE_REMOVE; } tray_applet_ui_set_visible(ta, TRUE); gtk_menu_item_set_label(GTK_MENU_ITEM(priv->menuitem_enabled), _("Livepatch is on")); check_state = livepatch_get_check_state(&error); if (check_state == NULL) g_warning("Cannot get Livepatch check-state: %s", error->message); state = livepatch_get_state(&error); if (state == NULL) g_warning("Cannot get Livepatch state: %s", error->message); if (!g_strcmp0(check_state, "checked") && (!g_strcmp0(state, "applied") || !g_strcmp0(state, "nothing-to-apply"))) { ssize_t num_fixes; g_autofree gchar *label = NULL; tray_applet_ui_set_icon(ta, "livepatch-on"); num_fixes = livepatch_get_num_fixes(&error); if (num_fixes == -1) { g_warning("Cannot get applied Livepatch fixes: %s", error->message); num_fixes = 0; } gtk_widget_set_visible(priv->menuitem_desc, TRUE); if (num_fixes == 0) label = g_strdup(_("No current updates")); else label = g_strdup_printf(ngettext("%zd current update", "%zd current updates", num_fixes), num_fixes); gtk_menu_item_set_label(GTK_MENU_ITEM(priv->menuitem_desc), label); } else if (!g_strcmp0(check_state, "needs-check") || !g_strcmp0(state, "unapplied") || !g_strcmp0(state, "applying")) { /* Check livepatch status again */ return G_SOURCE_CONTINUE; } else { tray_applet_ui_set_icon(ta, "livepatch-warning"); if (check_state == NULL || !g_strcmp0(check_state, "check-failed")) { gtk_menu_item_set_label(GTK_MENU_ITEM(priv->menuitem_desc), _("An error occured when checking for Livepatch updates.")); } else { gtk_menu_item_set_label(GTK_MENU_ITEM(priv->menuitem_desc), _("An error occured when applying Livepatch updates.")); } } priv->timeout_id = 0; return G_SOURCE_REMOVE; } static void gsettings_visibility_changed_cb(GSettings *settings, gchar *key, gpointer user_data) { TrayApplet *ta = (TrayApplet *) user_data; LivepatchTrayAppletPriv *priv = (LivepatchTrayAppletPriv *) ta->user_data; if (priv->timeout_id <= 0) priv->timeout_id = g_timeout_add_seconds(0, (GSourceFunc) check_livepatch, ta); } static void livepatch_directory_changed_cb(GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, gpointer user_data) { TrayApplet *ta = (TrayApplet *) user_data; LivepatchTrayAppletPriv *priv = (LivepatchTrayAppletPriv *) ta->user_data; if (priv->timeout_id <= 0) priv->timeout_id = g_timeout_add_seconds(TIMEOUT_SECONDS_DELAY, (GSourceFunc) check_livepatch, ta); } void livepatch_tray_icon_init(TrayApplet *ta) { g_autoptr(GFile) livepatch_common_dir = NULL; g_autoptr(GFile) livepatch_snap_dir = NULL; g_autoptr(GFileMonitor) common_dir_monitor = NULL; g_autoptr(GFileMonitor) snap_dir_monitor = NULL; g_autoptr(GError) error = NULL; LivepatchTrayAppletPriv *priv; if (!livepatch_is_supported()) return; if (!livepatch_has_settings_ui()) { g_warning("There is no graphical application installed to manage " "Livepatch. The livepatch status icon will not be displayed."); return; } /* Monitor the directory LIVEPATCH_COMMON_DIRECTORY for changes to update the status of the indicator. */ livepatch_common_dir = g_file_new_for_path(LIVEPATCH_COMMON_DIRECTORY); common_dir_monitor = g_file_monitor_directory(livepatch_common_dir, G_FILE_MONITOR_NONE, NULL, &error); if (common_dir_monitor == NULL) { g_warning("Couldn't create directory monitor on %s. Error: %s\n", LIVEPATCH_COMMON_DIRECTORY, error->message); return; } /* We also need to monitor the directory LIVEPATCH_SNAP_DIRECTORY in order to detect if the snap was enabled/disabled. This consider the case canonical-livepatch is enabled but the snap is disabled. */ livepatch_snap_dir = g_file_new_for_path(LIVEPATCH_SNAP_DIRECTORY); snap_dir_monitor = g_file_monitor_directory(livepatch_snap_dir, G_FILE_MONITOR_WATCH_MOUNTS, NULL, &error); if (snap_dir_monitor == NULL) { g_warning("Couldn't create directory monitor on %s. Error: %s\n", LIVEPATCH_SNAP_DIRECTORY, error->message); return; } priv = g_new0(LivepatchTrayAppletPriv, 1); priv->common_dir_monitor = g_steal_pointer(&common_dir_monitor); priv->snap_dir_monitor = g_steal_pointer(&snap_dir_monitor); ta->user_data = priv; tray_applet_ui_ensure(ta); /* Menu initialization */ livepatch_trayicon_create_menu(ta); tray_applet_ui_set_menu(ta, ta->menu); g_signal_connect(ta->un->settings, "changed::"SETTINGS_KEY_SHOW_LIVEPATCH_ICON, G_CALLBACK(gsettings_visibility_changed_cb), ta); g_signal_connect(priv->common_dir_monitor, "changed", G_CALLBACK(livepatch_directory_changed_cb), ta); g_signal_connect(priv->snap_dir_monitor, "changed", G_CALLBACK(livepatch_directory_changed_cb), ta); /* We always run check_livepatch in a timeout beacuse this allows us to easily check again the status of livepatch if it is in a transistion state (e.g. it is downloading the patches or applying them, etc.) */ priv->timeout_id = g_timeout_add_seconds(TIMEOUT_SECONDS_DELAY, (GSourceFunc) check_livepatch, ta); }