[gnome-software] lib: Add metered data scheduling to the core



commit a41e2863b0f94da6e2d91f9d10ef4c71643bc558
Author: Philip Withnall <withnall endlessm com>
Date:   Fri Mar 15 17:08:48 2019 +0000

    lib: Add metered data scheduling to the core
    
    This adds some utility functions for accessing a metered data scheduler,
    exposed in the public API if GS_ENABLE_EXPERIMENTAL_MOGWAI is defined
    before the library headers are included.
    
    Using this unstable API from out of tree plugins is not supported.
    
    Support will be added to some of the in-tree plugins in following
    commits, allowing them to schedule their large downloads using the
    system scheduler, in accordance with the user’s metered data policy.
    
    This adds an optional dependency on libmogwai-schedule-client.
    
    It will be the responsibility of individual plugins to ensure that large
    downloads (roughly, >100KB) are scheduled using the scheduler. Smaller
    downloads don’t have to be, as they consume negligible bandwidth.
    
    For now, remove use of the download-updates setting, as the policy it
    enforced is implemented more flexibly in the download scheduler daemon.
    
    Signed-off-by: Philip Withnall <withnall endlessm com>

 lib/gnome-software.h    |   1 +
 lib/gs-metered.c        | 266 ++++++++++++++++++++++++++++++++++++++++++++++++
 lib/gs-metered.h        |  28 +++++
 lib/gs-plugin-vfuncs.h  |  10 ++
 lib/meson.build         |   6 ++
 meson.build             |   5 +
 meson_options.txt       |   1 +
 src/gs-update-monitor.c |  15 ++-
 src/meson.build         |   4 +
 9 files changed, 334 insertions(+), 2 deletions(-)
---
diff --git a/lib/gnome-software.h b/lib/gnome-software.h
index 2833d0ff..94ea14b1 100644
--- a/lib/gnome-software.h
+++ b/lib/gnome-software.h
@@ -16,6 +16,7 @@
 #include <gs-app-collation.h>
 #include <gs-autocleanups.h>
 #include <gs-category.h>
+#include <gs-metered.h>
 #include <gs-os-release.h>
 #include <gs-plugin.h>
 #include <gs-plugin-vfuncs.h>
diff --git a/lib/gs-metered.c b/lib/gs-metered.c
new file mode 100644
index 00000000..62440490
--- /dev/null
+++ b/lib/gs-metered.c
@@ -0,0 +1,266 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2019 Endless Mobile, Inc.
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+/**
+ * SECTION:gs-metered
+ * @title: Metered Data Utilities
+ * @include: gnome-software.h
+ * @stability: Unstable
+ * @short_description: Utility functions to help with metered data handling
+ *
+ * Metered data handling is provided by Mogwai, which implements a download
+ * scheduler to control when, and in which order, large downloads happen on
+ * the system.
+ *
+ * All large downloads from gs_plugin_download() or gs_plugin_download_app()
+ * calls should be scheduled using Mogwai, which will notify gnome-software
+ * when those downloads can start and stop, according to system policy.
+ *
+ * The functions in this file make interacting with the scheduling daemon a
+ * little simpler. Since all #GsPlugin method calls happen in worker threads,
+ * typically without a #GMainContext, all interaction with the scheduler should
+ * be blocking. libmogwai-schedule-client was designed to be asynchronous; so
+ * these helpers make it synchronous.
+ *
+ * Since: 2.34
+ */
+
+#include "config.h"
+
+#include <glib.h>
+
+#ifdef HAVE_MOGWAI
+#include <libmogwai-schedule-client/scheduler.h>
+#endif
+
+#include "gs-metered.h"
+
+
+#ifdef HAVE_MOGWAI
+
+/* FIXME: Backported from https://gitlab.gnome.org/GNOME/glib/merge_requests/983.
+ * Drop once we can depend on a version of GLib which includes it .*/
+typedef void MainContextPusher;
+
+static inline MainContextPusher *
+main_context_pusher_new (GMainContext *main_context)
+{
+  g_main_context_push_thread_default (main_context);
+  return (MainContextPusher *) main_context;
+}
+
+static inline void
+main_context_pusher_free (MainContextPusher *pusher)
+{
+  g_main_context_pop_thread_default ((GMainContext *) pusher);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (MainContextPusher, main_context_pusher_free)
+
+
+typedef struct
+{
+       gboolean *out_download_now;  /* (unowned) */
+       GMainContext *context;  /* (unowned) */
+} DownloadNowData;
+
+static void
+download_now_cb (GObject    *obj,
+                 GParamSpec *pspec,
+                 gpointer    user_data)
+{
+       DownloadNowData *data = user_data;
+       *data->out_download_now = mwsc_schedule_entry_get_download_now (MWSC_SCHEDULE_ENTRY (obj));
+       g_main_context_wakeup (data->context);
+}
+
+typedef struct
+{
+       GError **out_error;  /* (unowned) */
+       GMainContext *context;  /* (unowned) */
+} InvalidatedData;
+
+static void
+invalidated_cb (MwscScheduleEntry *entry,
+                const GError      *error,
+                gpointer           user_data)
+{
+       InvalidatedData *data = user_data;
+       *data->out_error = g_error_copy (error);
+       g_main_context_wakeup (data->context);
+}
+
+#endif  /* HAVE_MOGWAI */
+
+/**
+ * gs_metered_block_on_download_scheduler:
+ * @parameters: (nullable): a #GVariant of type `a{sv}` specifying parameters
+ *    for the schedule entry, or %NULL to pass no parameters
+ * @cancellable: (nullable): a #GCancellable, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Create a schedule entry with the given @parameters, and block until
+ * permission is given to download.
+ *
+ * FIXME: This will currently ignore later revocations of that download
+ * permission, and does not support creating a schedule entry per app.
+ *
+ * If a schedule entry cannot be created, or if @cancellable is cancelled,
+ * an error will be set and %FALSE returned.
+ *
+ * The keys understood by @parameters are listed in the documentation for
+ * mwsc_scheduler_schedule_async().
+ *
+ * This function will likely be called from a #GsPluginLoader worker thread.
+ *
+ * Returns: %TRUE on success, %FALSE otherwise
+ * Since: 2.34
+ */
+gboolean
+gs_metered_block_on_download_scheduler (GVariant      *parameters,
+                                        GCancellable  *cancellable,
+                                        GError       **error)
+{
+#ifdef HAVE_MOGWAI
+       g_autoptr(MwscScheduler) scheduler = NULL;
+       g_autoptr(MwscScheduleEntry) schedule_entry = NULL;
+       g_autofree gchar *parameters_str = NULL;
+       g_autoptr(GMainContext) context = NULL;
+       g_autoptr(MainContextPusher) pusher = NULL;
+
+       parameters_str = (parameters != NULL) ? g_variant_print (parameters, TRUE) : g_strdup ("(none)");
+       g_debug ("%s: Waiting with parameters: %s", G_STRFUNC, parameters_str);
+
+       /* Push the context early so that the #MwscScheduler is created to run within it. */
+       context = g_main_context_new ();
+       pusher = main_context_pusher_new (context);
+
+       /* Wait until the download can be scheduled.
+        * FIXME: In future, downloads could be split up by app, so they can all
+        * be scheduled separately and, for example, higher priority ones could
+        * be scheduled with a higher priority. This would have to be aware of
+        * dependencies. */
+       scheduler = mwsc_scheduler_new (cancellable, error);
+       if (scheduler == NULL)
+               return FALSE;
+
+       /* Create a schedule entry for the group of downloads.
+        * FIXME: The underlying OSTree code supports resuming downloads
+        * (at a granularity of individual objects), so it should be
+        * possible to plumb through here. */
+       schedule_entry = mwsc_scheduler_schedule (scheduler, parameters, cancellable,
+                                                 error);
+       if (schedule_entry == NULL)
+               return FALSE;
+
+       /* Wait until the download is allowed to proceed. */
+       if (!mwsc_schedule_entry_get_download_now (schedule_entry)) {
+               gboolean download_now = FALSE;
+               g_autoptr(GError) invalidated_error = NULL;
+               gulong notify_id, invalidated_id;
+               DownloadNowData download_now_data = { &download_now, context };
+               InvalidatedData invalidated_data = { &invalidated_error, context };
+
+               notify_id = g_signal_connect (schedule_entry, "notify::download-now",
+                                             (GCallback) download_now_cb, &download_now_data);
+               invalidated_id = g_signal_connect (schedule_entry, "invalidated",
+                                                  (GCallback) invalidated_cb, &invalidated_data);
+
+               while (!download_now && invalidated_error == NULL &&
+                      !g_cancellable_is_cancelled (cancellable))
+                       g_main_context_iteration (context, TRUE);
+
+               g_signal_handler_disconnect (schedule_entry, invalidated_id);
+               g_signal_handler_disconnect (schedule_entry, notify_id);
+
+               if (!download_now && invalidated_error != NULL) {
+                       g_propagate_error (error, g_steal_pointer (&invalidated_error));
+                       return FALSE;
+               } else if (!download_now && g_cancellable_set_error_if_cancelled (cancellable, error)) {
+                       return FALSE;
+               }
+
+               g_assert (download_now);
+       }
+
+       g_debug ("%s: Allowed to download", G_STRFUNC);
+#else  /* if !HAVE_MOGWAI */
+       g_debug ("%s: Allowed to download (Mogwai support compiled out)", G_STRFUNC);
+#endif  /* !HAVE_MOGWAI */
+
+       return TRUE;
+}
+
+/**
+ * gs_metered_block_app_on_download_scheduler:
+ * @app: a #GsApp to get the scheduler parameters from
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Version of gs_metered_block_on_download_scheduler() which extracts the
+ * download parameters from the given @app.
+ *
+ * Returns: %TRUE on success, %FALSE otherwise
+ * Since: 2.34
+ */
+gboolean
+gs_metered_block_app_on_download_scheduler (GsApp         *app,
+                                            GCancellable  *cancellable,
+                                            GError       **error)
+{
+       g_auto(GVariantDict) parameters_dict = G_VARIANT_DICT_INIT (NULL);
+       g_autoptr(GVariant) parameters = NULL;
+       guint64 download_size = gs_app_get_size_download (app);
+
+       /* Currently no plugins support resumable downloads. This may change in
+        * future, in which case this parameter should be refactored. */
+       g_variant_dict_insert (&parameters_dict, "resumable", "b", FALSE);
+
+       if (download_size != 0 && download_size != GS_APP_SIZE_UNKNOWABLE) {
+               g_variant_dict_insert (&parameters_dict, "size-minimum", "t", download_size);
+               g_variant_dict_insert (&parameters_dict, "size-maximum", "t", download_size);
+       }
+
+       parameters = g_variant_ref_sink (g_variant_dict_end (&parameters_dict));
+
+       return gs_metered_block_on_download_scheduler (parameters, cancellable, error);
+}
+
+/**
+ * gs_metered_block_app_list_on_download_scheduler:
+ * @app_list: a #GsAppList to get the scheduler parameters from
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Version of gs_metered_block_on_download_scheduler() which extracts the
+ * download parameters from the apps in the given @app_list.
+ *
+ * Returns: %TRUE on success, %FALSE otherwise
+ * Since: 2.34
+ */
+gboolean
+gs_metered_block_app_list_on_download_scheduler (GsAppList     *app_list,
+                                                 GCancellable  *cancellable,
+                                                 GError       **error)
+{
+       g_auto(GVariantDict) parameters_dict = G_VARIANT_DICT_INIT (NULL);
+       g_autoptr(GVariant) parameters = NULL;
+
+       /* Currently no plugins support resumable downloads. This may change in
+        * future, in which case this parameter should be refactored. */
+       g_variant_dict_insert (&parameters_dict, "resumable", "b", FALSE);
+
+       /* FIXME: Currently this creates a single Mogwai schedule entry for the
+        * entire app list. Eventually, we probably want one schedule entry per
+        * app being downloaded, so that they can be individually prioritised.
+        * However, that requires much deeper integration into the download
+        * code, and Mogwai does not currently support that level of
+        * prioritisation, so go with this simple implementation for now. */
+       parameters = g_variant_ref_sink (g_variant_dict_end (&parameters_dict));
+
+       return gs_metered_block_on_download_scheduler (parameters, cancellable, error);
+}
diff --git a/lib/gs-metered.h b/lib/gs-metered.h
new file mode 100644
index 00000000..4550a62e
--- /dev/null
+++ b/lib/gs-metered.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2019 Endless Mobile, Inc.
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+#include "gs-app.h"
+#include "gs-app-list.h"
+
+G_BEGIN_DECLS
+
+gboolean gs_metered_block_on_download_scheduler (GVariant      *parameters,
+                                                 GCancellable  *cancellable,
+                                                 GError       **error);
+gboolean gs_metered_block_app_on_download_scheduler (GsApp         *app,
+                                                     GCancellable  *cancellable,
+                                                     GError       **error);
+gboolean gs_metered_block_app_list_on_download_scheduler (GsAppList     *app_list,
+                                                          GCancellable  *cancellable,
+                                                          GError       **error);
+
+G_END_DECLS
diff --git a/lib/gs-plugin-vfuncs.h b/lib/gs-plugin-vfuncs.h
index 1d406d99..4ae65e54 100644
--- a/lib/gs-plugin-vfuncs.h
+++ b/lib/gs-plugin-vfuncs.h
@@ -664,6 +664,11 @@ gboolean    gs_plugin_update_app                   (GsPlugin       *plugin,
  * Downloads the application and any dependencies ready to be installed or
  * updated.
  *
+ * Plugins are expected to schedule downloads using the system download
+ * scheduler if appropriate (if the download is not guaranteed to be under a few
+ * hundred kilobytes, for example), so that the user’s metered data preferences
+ * are honoured.
+ *
  * Plugins are expected to send progress notifications to the UI using
  * gs_app_set_progress() using the passed in @app.
  *
@@ -692,6 +697,11 @@ gboolean    gs_plugin_download_app                 (GsPlugin       *plugin,
  *
  * Downloads a list of applications ready to be installed or updated.
  *
+ * Plugins are expected to schedule downloads using the system download
+ * scheduler if appropriate (if the download is not guaranteed to be under a few
+ * hundred kilobytes, for example), so that the user’s metered data preferences
+ * are honoured.
+ *
  * Returns: %TRUE for success or if not relevant
  **/
 gboolean        gs_plugin_download                     (GsPlugin       *plugin,
diff --git a/lib/meson.build b/lib/meson.build
index 86c7d378..c4a88fb5 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -40,6 +40,7 @@ install_headers([
     'gs-app.h',
     'gs-app-list.h',
     'gs-category.h',
+    'gs-metered.h',
     'gs-os-release.h',
     'gs-plugin.h',
     'gs-plugin-event.h',
@@ -62,6 +63,10 @@ librarydeps = [
   valgrind,
 ]
 
+if get_option('mogwai')
+  librarydeps += mogwai_schedule_client
+endif
+
 if get_option('polkit')
   librarydeps += polkit
 endif
@@ -75,6 +80,7 @@ libgnomesoftware = static_library(
     'gs-debug.c',
     'gs-ioprio.c',
     'gs-ioprio.h',
+    'gs-metered.c',
     'gs-os-release.c',
     'gs-plugin.c',
     'gs-plugin-event.c',
diff --git a/meson.build b/meson.build
index 37206d7d..b9d361c0 100644
--- a/meson.build
+++ b/meson.build
@@ -107,6 +107,11 @@ json_glib = dependency('json-glib-1.0', version : '>= 1.2.0')
 libm = cc.find_library('m', required: false)
 libsoup = dependency('libsoup-2.4', version : '>= 2.52.0')
 
+if get_option('mogwai')
+  mogwai_schedule_client = dependency('mogwai-schedule-client-0', version : '>= 0.2.0')
+  conf.set('HAVE_MOGWAI', 1)
+endif
+
 if get_option('valgrind')
   message(meson.version())
   # urgh, meson is broken
diff --git a/meson_options.txt b/meson_options.txt
index 0f961d9a..14174f07 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -18,3 +18,4 @@ option('external_appstream', type : 'boolean', value : false, description : 'ena
 option('valgrind', type : 'boolean', value : true, description : 'enable Valgrind debugging integration')
 option('gtk_doc', type : 'boolean', value : true, description : 'enable API reference')
 option('hardcoded_popular', type : 'boolean', value : true, description : 'enable hardcoded-popular apps 
plugin')
+option('mogwai', type : 'boolean', value : false, description : 'enable metered data support using Mogwai')
diff --git a/src/gs-update-monitor.c b/src/gs-update-monitor.c
index c6bd9b96..084fbf70 100644
--- a/src/gs-update-monitor.c
+++ b/src/gs-update-monitor.c
@@ -382,6 +382,7 @@ get_updates_finished_cb (GObject *object, GAsyncResult *res, gpointer data)
        guint64 security_timestamp_old = 0;
        g_autoptr(GError) error = NULL;
        g_autoptr(GsAppList) apps = NULL;
+       gboolean download_updates;
 
        /* get result */
        apps = gs_plugin_loader_job_process_finish (GS_PLUGIN_LOADER (object), res, &error);
@@ -416,9 +417,19 @@ get_updates_finished_cb (GObject *object, GAsyncResult *res, gpointer data)
 
        g_debug ("got %u updates", gs_app_list_length (apps));
 
-       /* download any updates if auto-updates are turned on */
-       if (g_settings_get_boolean (monitor->settings, "download-updates")) {
+#ifdef HAVE_MOGWAI
+       download_updates = TRUE;
+#else
+       download_updates = g_settings_get_boolean (monitor->settings, "download-updates");
+#endif
+
+       if (download_updates) {
                g_autoptr(GsPluginJob) plugin_job = NULL;
+
+               /* download any updates; individual plugins are responsible for deciding
+                * whether it’s appropriate to unconditionally download the updates, or
+                * to schedule the download in accordance with the user’s metered data
+                * preferences */
                plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_DOWNLOAD,
                                                 "list", apps,
                                                 NULL);
diff --git a/src/meson.build b/src/meson.build
index fde9ebd6..e63fe104 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -107,6 +107,10 @@ if get_option('gspell')
   gnome_software_dependencies += [gspell]
 endif
 
+if get_option('mogwai')
+  gnome_software_dependencies += [mogwai_schedule_client]
+endif
+
 executable(
   'gnome-software',
   resources_src,


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]