/* update-worker.c * * Copyright 2026 Alexander Vanhee * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * 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 . * * SPDX-License-Identifier: GPL-3.0-or-later */ #define G_LOG_DOMAIN "BAZAAR::UPDATE-WORKER" #include "config.h" #include #include #include #include #include "bz-update-history-data-point.h" #include "update-worker.h" #define DAILY_WINDOW_START_HOUR 6 #define DAILY_WINDOW_SPREAD_HOURS 6 #define STALE_CHECK_THRESHOLD_DAYS 7 #define STALE_NOTIFY_THROTTLE_DAYS 1 static gboolean network_is_restricted (void); static gboolean power_saver_is_enabled (void); static gboolean due_for_daily_check (GSettings *settings); static gboolean timestamp_more_than_days_ago (GSettings *settings, const char *key, guint days); static void maybe_notify_stale (GSettings *settings); static gboolean should_skip_extension_ref (FlatpakInstalledRef *iref); static gboolean on_operation_error (FlatpakTransaction *transaction, FlatpakTransactionOperation *operation, GError *error, int details, gpointer user_data); static gboolean update_installation (FlatpakInstallation *installation, GPtrArray *history_out); static void send_portal_notification (const char *id, const char *title, const char *body, const char *action, GVariant *action_target); static void send_update_notification (GPtrArray *history); int run_update_worker (int argc, char *argv[]) { g_autoptr (GSettings) settings = NULL; g_autoptr (GError) local_error = NULL; g_autoptr (FlatpakInstallation) system_inst = NULL; g_autoptr (GPtrArray) history = NULL; gboolean auto_update = FALSE; gboolean auto_update_notifications = FALSE; gboolean had_error = FALSE; g_autoptr (GDateTime) now = NULL; settings = g_settings_new (APPLICATION_ID); auto_update = g_settings_get_boolean (settings, "auto-update"); auto_update_notifications = g_settings_get_boolean (settings, "auto-update-notifications"); if (!auto_update) { g_debug ("Skipping update check: auto-update is disabled\n"); return EXIT_SUCCESS; } if (!due_for_daily_check (settings)) return EXIT_SUCCESS; if (network_is_restricted ()) { g_debug ("Skipping update check: no network or network is metered\n"); maybe_notify_stale (settings); return EXIT_SUCCESS; } if (power_saver_is_enabled ()) { g_debug ("Skipping update check: power saver is enabled\n"); maybe_notify_stale (settings); return EXIT_SUCCESS; } history = g_ptr_array_new_with_free_func (g_object_unref); system_inst = flatpak_installation_new_system (NULL, &local_error); if (system_inst == NULL) { g_warning ("Failed to open system installation: %s", local_error->message); g_clear_error (&local_error); had_error = TRUE; } else { flatpak_installation_set_no_interaction (system_inst, TRUE); if (!update_installation (system_inst, history)) had_error = TRUE; } #ifndef SANDBOXED_LIBFLATPAK { g_autoptr (FlatpakInstallation) user_inst = NULL; user_inst = flatpak_installation_new_user (NULL, &local_error); if (user_inst == NULL) { g_warning ("Failed to open user installation: %s", local_error->message); g_clear_error (&local_error); had_error = TRUE; } else { flatpak_installation_set_no_interaction (user_inst, TRUE); if (!update_installation (user_inst, history)) had_error = TRUE; } } #endif if (had_error) { g_debug ("Update process encountered errors\n"); maybe_notify_stale (settings); return EXIT_SUCCESS; } now = g_date_time_new_now_local (); g_settings_set_int64 (settings, "last-update-check", g_date_time_to_unix (now)); if (history->len == 0) { g_print ("No apps needed auto updating\n"); return EXIT_SUCCESS; } if (auto_update_notifications) send_update_notification (history); return EXIT_SUCCESS; } /* * Updates try to run once per day at a randomized hour between 6:00 and * 11:00 local time, to avoid every client updating at once. If more than a * day has passed since the last check then the randomization is skipped and * the check runs as soon as it's past 6:00. * * This logic is copied from GNOME Software. */ static gboolean due_for_daily_check (GSettings *settings) { gint64 last_check_unix = 0; gint hour_offset = 0; g_autoptr (GDateTime) now = NULL; g_autoptr (GDateTime) now_mid = NULL; gint now_hour = 0; gint year = 0, month = 0, day = 0; GTimeSpan day_interval = 0; last_check_unix = g_settings_get_int64 (settings, "last-update-check"); hour_offset = g_settings_get_int (settings, "update-check-hour-offset"); if (hour_offset < 0) { hour_offset = g_random_int_range (0, DAILY_WINDOW_SPREAD_HOURS); g_settings_set_int (settings, "update-check-hour-offset", hour_offset); } now = g_date_time_new_now_local (); if (last_check_unix == 0) return TRUE; { g_autoptr (GDateTime) last_check = NULL; g_autoptr (GDateTime) last_check_mid = NULL; last_check = g_date_time_new_from_unix_local (last_check_unix); if (last_check == NULL) return TRUE; g_date_time_get_ymd (last_check, &year, &month, &day); last_check_mid = g_date_time_new_local (year, month, day, 0, 0, 0); g_date_time_get_ymd (now, &year, &month, &day); now_mid = g_date_time_new_local (year, month, day, 0, 0, 0); day_interval = g_date_time_difference (now_mid, last_check_mid); } if (day_interval < G_TIME_SPAN_DAY) { g_debug ("Skipping update check: already checked today\n"); return FALSE; } now_hour = g_date_time_get_hour (now); if (day_interval < 2 * G_TIME_SPAN_DAY && now_hour < DAILY_WINDOW_START_HOUR + hour_offset) { g_debug ("Skipping update check: too early in the day\n"); return FALSE; } if (day_interval >= 2 * G_TIME_SPAN_DAY && now_hour < DAILY_WINDOW_START_HOUR) { g_debug ("Skipping update check: too early in the day\n"); return FALSE; } return TRUE; } static gboolean timestamp_more_than_days_ago (GSettings *settings, const char *key, guint days) { gint64 ts = 0; g_autoptr (GDateTime) then = NULL; g_autoptr (GDateTime) now = NULL; ts = g_settings_get_int64 (settings, key); if (ts == 0) return TRUE; then = g_date_time_new_from_unix_local (ts); if (then == NULL) return TRUE; now = g_date_time_new_now_local (); return g_date_time_difference (now, then) / G_TIME_SPAN_DAY >= days; } static void maybe_notify_stale (GSettings *settings) { if (!timestamp_more_than_days_ago (settings, "last-update-check", STALE_CHECK_THRESHOLD_DAYS)) return; if (!timestamp_more_than_days_ago (settings, "last-stale-notification", STALE_NOTIFY_THROTTLE_DAYS)) return; send_portal_notification ("bazaar-update-stale", _ ("Updates Are Out of Date"), _ ("Please check for available updates"), NULL, NULL); g_settings_set_int64 (settings, "last-stale-notification", g_date_time_to_unix (g_date_time_new_now_local ())); } static gboolean network_is_restricted (void) { GNetworkMonitor *monitor = NULL; monitor = g_network_monitor_get_default (); if (monitor == NULL) return FALSE; return g_network_monitor_get_network_metered (monitor); } static gboolean power_saver_is_enabled (void) { g_autoptr (GPowerProfileMonitor) monitor = NULL; monitor = g_power_profile_monitor_dup_default (); if (monitor == NULL) return FALSE; return g_power_profile_monitor_get_power_saver_enabled (monitor); } static gboolean should_skip_extension_ref (FlatpakInstalledRef *iref) { const char *ref_name = NULL; ref_name = flatpak_ref_get_name (FLATPAK_REF (iref)); return g_str_has_suffix (ref_name, ".Locale") || g_str_has_suffix (ref_name, ".Debug") || g_str_has_suffix (ref_name, ".Sources"); } static gboolean on_operation_error (FlatpakTransaction *transaction, FlatpakTransactionOperation *operation, GError *error, int details, gpointer user_data) { const char *ref = NULL; ref = flatpak_transaction_operation_get_ref (operation); g_warning ("Update failed for %s: %s", ref, error->message); return TRUE; } static gboolean update_installation (FlatpakInstallation *installation, GPtrArray *history_out) { g_autoptr (GError) local_error = NULL; g_autoptr (GPtrArray) update_refs = NULL; g_autoptr (FlatpakTransaction) transaction = NULL; g_autoptr (GPtrArray) pending = NULL; gboolean added_any = FALSE; gboolean ran = FALSE; gboolean success = TRUE; update_refs = flatpak_installation_list_installed_refs_for_update ( installation, NULL, &local_error); if (update_refs == NULL) { g_warning ("Failed to list updates: %s", local_error->message); return FALSE; } if (update_refs->len == 0) return TRUE; transaction = flatpak_transaction_new_for_installation (installation, NULL, &local_error); if (transaction == NULL) { g_warning ("Failed to create transaction: %s", local_error->message); return FALSE; } g_signal_connect (transaction, "operation-error", G_CALLBACK (on_operation_error), NULL); pending = g_ptr_array_new (); for (guint i = 0; i < update_refs->len; i++) { FlatpakInstalledRef *iref = NULL; g_autofree char *ref_fmt = NULL; const char *title = NULL; const char *id = NULL; const char *old_ver = NULL; iref = g_ptr_array_index (update_refs, i); if (should_skip_extension_ref (iref)) continue; ref_fmt = flatpak_ref_format_ref (FLATPAK_REF (iref)); id = flatpak_ref_get_name (FLATPAK_REF (iref)); title = flatpak_installed_ref_get_appdata_name (iref); if (title == NULL) title = id; old_ver = flatpak_installed_ref_get_appdata_version (iref); if (flatpak_transaction_add_update (transaction, ref_fmt, NULL, NULL, &local_error)) { BzUpdateHistoryDataPoint *point = NULL; added_any = TRUE; point = g_object_new (BZ_TYPE_UPDATE_HISTORY_DATA_POINT, "id", id, "title", title, "old-version", old_ver != NULL ? old_ver : "", "new-version", old_ver != NULL ? old_ver : "", NULL); g_ptr_array_add (history_out, point); g_ptr_array_add (pending, iref); g_ptr_array_add (pending, point); } else { g_warning ("Failed to queue update for %s: %s", ref_fmt, local_error->message); g_clear_error (&local_error); success = FALSE; } } if (!added_any) return success; ran = flatpak_transaction_run (transaction, NULL, &local_error); if (!ran && local_error != NULL) { g_warning ("Transaction did not complete cleanly: %s", local_error->message); success = FALSE; } for (guint i = 0; i < pending->len; i += 2) { FlatpakInstalledRef *iref = NULL; BzUpdateHistoryDataPoint *point = NULL; g_autoptr (FlatpakInstalledRef) updated_ref = NULL; g_autoptr (GError) resolve_error = NULL; const char *new_ver = NULL; iref = g_ptr_array_index (pending, i); point = g_ptr_array_index (pending, i + 1); updated_ref = flatpak_installation_get_installed_ref ( installation, flatpak_ref_get_kind (FLATPAK_REF (iref)), flatpak_ref_get_name (FLATPAK_REF (iref)), flatpak_ref_get_arch (FLATPAK_REF (iref)), flatpak_ref_get_branch (FLATPAK_REF (iref)), NULL, &resolve_error); if (updated_ref != NULL) { new_ver = flatpak_installed_ref_get_appdata_version (updated_ref); bz_update_history_data_point_set_new_version (point, new_ver != NULL ? new_ver : ""); } } return success; } static void send_portal_notification (const char *id, const char *title, const char *body, const char *action, GVariant *action_target) { g_autoptr (GDBusConnection) conn = NULL; g_autoptr (GError) error = NULL; GVariantBuilder notification = { 0 }; conn = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); if (conn == NULL) { g_warning ("Unable to reach session bus for notification: %s", error->message); return; } g_variant_builder_init (¬ification, G_VARIANT_TYPE_VARDICT); g_variant_builder_add (¬ification, "{sv}", "title", g_variant_new_string (title)); g_variant_builder_add (¬ification, "{sv}", "body", g_variant_new_string (body)); g_variant_builder_add (¬ification, "{sv}", "priority", g_variant_new_string ("normal")); if (action != NULL) { GVariantBuilder buttons = { 0 }; GVariantBuilder button = { 0 }; g_variant_builder_init (&button, G_VARIANT_TYPE_VARDICT); g_variant_builder_add (&button, "{sv}", "label", g_variant_new_string (_ ("Details"))); g_variant_builder_add (&button, "{sv}", "action", g_variant_new_string (action)); if (action_target != NULL) g_variant_builder_add (&button, "{sv}", "target", action_target); g_variant_builder_init (&buttons, G_VARIANT_TYPE ("aa{sv}")); g_variant_builder_add (&buttons, "a{sv}", &button); g_variant_builder_add (¬ification, "{sv}", "buttons", g_variant_builder_end (&buttons)); } g_dbus_connection_call_sync ( conn, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.Notification", "AddNotification", g_variant_new ("(sa{sv})", id, ¬ification), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); if (error != NULL) g_warning ("Failed to send notification: %s", error->message); } static void send_update_notification (GPtrArray *history) { g_autoptr (GString) body = NULL; g_autofree char *summary = NULL; guint n_updated = history->len; GVariantBuilder builder = { 0 }; guint i = 0; if (n_updated == 0) return; body = NULL; summary = NULL; i = 0; body = g_string_new (NULL); summary = g_strdup_printf ( ngettext ("%u App Updated", "%u Apps Updated", n_updated), n_updated); if (n_updated == 1) g_string_append_printf (body, _ ("%s has been updated."), bz_update_history_data_point_get_title (g_ptr_array_index (history, 0))); else if (n_updated == 2) g_string_append_printf (body, _ ("%s and %s have been updated."), bz_update_history_data_point_get_title (g_ptr_array_index (history, 0)), bz_update_history_data_point_get_title (g_ptr_array_index (history, 1))); else if (n_updated >= 3) g_string_append_printf (body, _ ("Includes %s, %s and %s."), bz_update_history_data_point_get_title (g_ptr_array_index (history, 0)), bz_update_history_data_point_get_title (g_ptr_array_index (history, 1)), bz_update_history_data_point_get_title (g_ptr_array_index (history, 2))); g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(sss)")); for (i = 0; i < history->len; i++) { BzUpdateHistoryDataPoint *point = NULL; const char *id = NULL; const char *old_ver = NULL; const char *new_ver = NULL; point = g_ptr_array_index (history, i); id = bz_update_history_data_point_get_id (point); old_ver = bz_update_history_data_point_get_old_version (point); new_ver = bz_update_history_data_point_get_new_version (point); g_variant_builder_add (&builder, "(sss)", id != NULL ? id : "", old_ver != NULL ? old_ver : "", new_ver != NULL ? new_ver : ""); } send_portal_notification ("bazaar-update", summary, body->str, "app.show-update-history", g_variant_builder_end (&builder)); }