[gnome-software] Cancel plugin jobs if they take too much time



commit fa7216c1284d957acbf266f879a99ffcff06abee
Author: Richard Hughes <richard hughsie com>
Date:   Wed Jun 28 17:50:42 2017 +0100

    Cancel plugin jobs if they take too much time
    
    Create an event and show a message in the shell to make the plugin look bad.
    
    Fixes: https://bugzilla.gnome.org/show_bug.cgi?id=784251

 lib/gs-plugin-job-private.h     |    1 +
 lib/gs-plugin-job.c             |   29 +++++++++
 lib/gs-plugin-job.h             |    2 +
 lib/gs-plugin-loader.c          |  120 ++++++++++++++++++++++++++++++++++++---
 lib/gs-plugin-types.h           |    2 +
 lib/gs-plugin.c                 |    2 +
 lib/gs-utils.c                  |    6 +-
 plugins/dummy/gs-plugin-dummy.c |   70 ++++++++++++++++++++++-
 plugins/dummy/gs-self-test.c    |   32 ++++++++++
 src/gs-search-page.c            |    1 +
 src/gs-shell.c                  |    9 +++-
 11 files changed, 261 insertions(+), 13 deletions(-)
---
diff --git a/lib/gs-plugin-job-private.h b/lib/gs-plugin-job-private.h
index d1dd62d..447e21d 100644
--- a/lib/gs-plugin-job-private.h
+++ b/lib/gs-plugin-job-private.h
@@ -39,6 +39,7 @@ void                   gs_plugin_job_remove_refine_flags      (GsPluginJob    *self,
                                                                 GsPluginRefineFlags refine_flags);
 GsPluginFailureFlags    gs_plugin_job_get_failure_flags        (GsPluginJob    *self);
 guint                   gs_plugin_job_get_max_results          (GsPluginJob    *self);
+guint                   gs_plugin_job_get_timeout              (GsPluginJob    *self);
 guint64                         gs_plugin_job_get_age                  (GsPluginJob    *self);
 GsAppListSortFunc       gs_plugin_job_get_sort_func            (GsPluginJob    *self);
 gpointer                gs_plugin_job_get_sort_func_data       (GsPluginJob    *self);
diff --git a/lib/gs-plugin-job.c b/lib/gs-plugin-job.c
index 4fc13ca..a3ca361 100644
--- a/lib/gs-plugin-job.c
+++ b/lib/gs-plugin-job.c
@@ -33,6 +33,7 @@ struct _GsPluginJob
        GsPluginRefreshFlags     refresh_flags;
        GsPluginFailureFlags     failure_flags;
        guint                    max_results;
+       guint                    timeout;
        guint64                  age;
        GsPlugin                *plugin;
        GsPluginAction           action;
@@ -64,6 +65,7 @@ enum {
        PROP_REVIEW,
        PROP_MAX_RESULTS,
        PROP_PRICE,
+       PROP_TIMEOUT,
        PROP_LAST
 };
 
@@ -83,6 +85,8 @@ gs_plugin_job_to_string (GsPluginJob *self)
                g_autofree gchar *tmp = gs_plugin_failure_flags_to_string (self->failure_flags);
                g_string_append_printf (str, " with failure-flags=%s", tmp);
        }
+       if (self->timeout > 0)
+               g_string_append_printf (str, " with timeout=%u", self->timeout);
        if (self->age != 0) {
                if (self->age == G_MAXUINT) {
                        g_string_append (str, " with cache age=any");
@@ -218,6 +222,20 @@ gs_plugin_job_get_max_results (GsPluginJob *self)
 }
 
 void
+gs_plugin_job_set_timeout (GsPluginJob *self, guint timeout)
+{
+       g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+       self->timeout = timeout;
+}
+
+guint
+gs_plugin_job_get_timeout (GsPluginJob *self)
+{
+       g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), 0);
+       return self->timeout;
+}
+
+void
 gs_plugin_job_set_age (GsPluginJob *self, guint64 age)
 {
        g_return_if_fail (GS_IS_PLUGIN_JOB (self));
@@ -454,6 +472,9 @@ gs_plugin_job_get_property (GObject *obj, guint prop_id, GValue *value, GParamSp
        case PROP_MAX_RESULTS:
                g_value_set_uint (value, self->max_results);
                break;
+       case PROP_TIMEOUT:
+               g_value_set_uint (value, self->timeout);
+               break;
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
                break;
@@ -505,6 +526,9 @@ gs_plugin_job_set_property (GObject *obj, guint prop_id, const GValue *value, GP
        case PROP_MAX_RESULTS:
                gs_plugin_job_set_max_results (self, g_value_get_uint (value));
                break;
+       case PROP_TIMEOUT:
+               gs_plugin_job_set_timeout (self, g_value_get_uint (value));
+               break;
        case PROP_PRICE:
                gs_plugin_job_set_price (self, g_value_get_object (value));
                break;
@@ -606,6 +630,11 @@ gs_plugin_job_class_init (GsPluginJobClass *klass)
                                   G_PARAM_READWRITE);
        g_object_class_install_property (object_class, PROP_MAX_RESULTS, pspec);
 
+       pspec = g_param_spec_uint ("timeout", NULL, NULL,
+                                  0, G_MAXUINT, 60,
+                                  G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+       g_object_class_install_property (object_class, PROP_TIMEOUT, pspec);
+
        pspec = g_param_spec_object ("price", NULL, NULL,
                                     GS_TYPE_PRICE,
                                     G_PARAM_READWRITE);
diff --git a/lib/gs-plugin-job.h b/lib/gs-plugin-job.h
index 6d2f050..a7d72af 100644
--- a/lib/gs-plugin-job.h
+++ b/lib/gs-plugin-job.h
@@ -44,6 +44,8 @@ void           gs_plugin_job_set_failure_flags        (GsPluginJob    *self,
                                                         GsPluginFailureFlags failure_flags);
 void            gs_plugin_job_set_max_results          (GsPluginJob    *self,
                                                         guint           max_results);
+void            gs_plugin_job_set_timeout              (GsPluginJob    *self,
+                                                        guint           timeout);
 void            gs_plugin_job_set_age                  (GsPluginJob    *self,
                                                         guint64         age);
 void            gs_plugin_job_set_sort_func            (GsPluginJob    *self,
diff --git a/lib/gs-plugin-loader.c b/lib/gs-plugin-loader.c
index 902621b..2ff5a00 100644
--- a/lib/gs-plugin-loader.c
+++ b/lib/gs-plugin-loader.c
@@ -178,11 +178,16 @@ typedef void               (*GsPluginAdoptAppFunc)        (GsPlugin       *plugin,
 /* async helper */
 typedef struct {
        GsPluginLoader                  *plugin_loader;
+       GCancellable                    *cancellable;
+       GCancellable                    *cancellable_caller;
+       gulong                           cancellable_id;
        const gchar                     *function_name;
        const gchar                     *function_name_parent;
        GPtrArray                       *catlist;
        GsPluginJob                     *plugin_job;
        gboolean                         anything_ran;
+       guint                            timeout_id;
+       gboolean                         timeout_triggered;
        gchar                           **tokens;
 } GsPluginLoaderHelper;
 
@@ -200,9 +205,19 @@ gs_plugin_loader_helper_new (GsPluginLoader *plugin_loader, GsPluginJob *plugin_
 static void
 gs_plugin_loader_helper_free (GsPluginLoaderHelper *helper)
 {
+       if (helper->cancellable_id > 0) {
+               g_cancellable_disconnect (helper->cancellable_caller,
+                                         helper->cancellable_id);
+       }
        g_object_unref (helper->plugin_loader);
+       if (helper->timeout_id != 0)
+               g_source_remove (helper->timeout_id);
        if (helper->plugin_job != NULL)
                g_object_unref (helper->plugin_job);
+       if (helper->cancellable != NULL)
+               g_object_unref (helper->cancellable);
+       if (helper->cancellable_caller != NULL)
+               g_object_unref (helper->cancellable_caller);
        if (helper->catlist != NULL)
                g_ptr_array_unref (helper->catlist);
        g_strfreev (helper->tokens);
@@ -359,6 +374,8 @@ gs_plugin_loader_is_error_fatal (GsPluginFailureFlags failure_flags,
                if (g_error_matches (err, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AUTH_INVALID))
                        return TRUE;
        }
+       if (g_error_matches (err, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_TIMED_OUT))
+               return TRUE;
        return FALSE;
 }
 
@@ -378,15 +395,8 @@ gs_plugin_error_handle_failure (GsPluginLoaderHelper *helper,
                return TRUE;
        }
 
-       /* abort early to allow main thread to process */
-       flags = gs_plugin_job_get_failure_flags (helper->plugin_job);
-       if (gs_plugin_loader_is_error_fatal (flags, error_local)) {
-               if (error != NULL)
-                       *error = g_error_copy (error_local);
-               return FALSE;
-       }
-
        /* create event which is handled by the GsShell */
+       flags = gs_plugin_job_get_failure_flags (helper->plugin_job);
        if (flags & GS_PLUGIN_FAILURE_FLAGS_USE_EVENTS) {
                gs_plugin_loader_create_event_from_error (helper->plugin_loader,
                                                          gs_plugin_job_get_action (helper->plugin_job),
@@ -395,6 +405,13 @@ gs_plugin_error_handle_failure (GsPluginLoaderHelper *helper,
                                                          error_local);
        }
 
+       /* abort early to allow main thread to process */
+       if (gs_plugin_loader_is_error_fatal (flags, error_local)) {
+               if (error != NULL)
+                       *error = g_error_copy (error_local);
+               return FALSE;
+       }
+
        /* fallback to console warning */
        if ((flags & GS_PLUGIN_FAILURE_FLAGS_NO_CONSOLE) == 0) {
                if (!g_error_matches (error_local,
@@ -689,7 +706,29 @@ gs_plugin_loader_call_vfunc (GsPluginLoaderHelper *helper,
                break;
        }
        gs_plugin_loader_action_stop (helper->plugin_loader, plugin);
+
+       /* plugin did not return error on cancellable abort */
+       if (ret && g_cancellable_set_error_if_cancelled (cancellable, &error_local)) {
+               g_debug ("plugin did not return error with cancellable set");
+               gs_utils_error_convert_gio (&error_local);
+               ret = FALSE;
+       }
+
+       /* failed */
        if (!ret) {
+               /* we returned cancelled, but this was because of a timeout,
+                * so re-create error, throwing the plugin under the bus */
+               if (helper->timeout_triggered &&
+                   g_error_matches (error_local, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) {
+                       g_debug ("converting cancelled to timeout");
+                       g_clear_error (&error_local);
+                       g_set_error (&error_local,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_TIMED_OUT,
+                                    "Timeout was reached as %s took "
+                                    "too long to return results",
+                                    gs_plugin_get_name (plugin));
+               }
                return gs_plugin_error_handle_failure (helper,
                                                        plugin,
                                                        error_local,
@@ -2999,6 +3038,7 @@ gs_plugin_loader_process_thread_cb (GTask *task,
                                gs_app_set_state_recover (gs_plugin_job_get_app (helper->plugin_job));
                                gs_plugin_loader_pending_apps_remove (plugin_loader, helper);
                        }
+                       gs_utils_error_convert_gio (&error);
                        g_task_return_error (task, error);
                        return;
                }
@@ -3009,6 +3049,7 @@ gs_plugin_loader_process_thread_cb (GTask *task,
                helper->function_name = "gs_plugin_update_app";
                if (!gs_plugin_loader_generic_update (plugin_loader, helper,
                                                      cancellable, &error)) {
+                       gs_utils_error_convert_gio (&error);
                        g_task_return_error (task, error);
                        return;
                }
@@ -3023,6 +3064,7 @@ gs_plugin_loader_process_thread_cb (GTask *task,
            !g_settings_get_boolean (priv->settings, "download-updates")) {
                helper->function_name = "gs_plugin_add_updates_pending";
                if (!gs_plugin_loader_run_results (helper, cancellable, &error)) {
+                       gs_utils_error_convert_gio (&error);
                        g_task_return_error (task, error);
                        return;
                }
@@ -3106,6 +3148,7 @@ gs_plugin_loader_process_thread_cb (GTask *task,
        /* run refine() on each one if required */
        if (gs_plugin_job_get_refine_flags (helper->plugin_job) != 0) {
                if (!gs_plugin_loader_run_refine (helper, list, cancellable, &error)) {
+                       gs_utils_error_convert_gio (&error);
                        g_task_return_error (task, error);
                        return;
                }
@@ -3145,6 +3188,7 @@ gs_plugin_loader_process_thread_cb (GTask *task,
                gs_plugin_job_set_refine_flags (helper->plugin_job,
                                                GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON);
                if (!gs_plugin_loader_run_refine (helper, list, cancellable, &error)) {
+                       gs_utils_error_convert_gio (&error);
                        g_task_return_error (task, error);
                        return;
                }
@@ -3248,6 +3292,29 @@ gs_plugin_loader_process_thread_cb (GTask *task,
        g_task_return_pointer (task, g_object_ref (list), (GDestroyNotify) g_object_unref);
 }
 
+static gboolean
+gs_plugin_loader_job_timeout_cb (gpointer user_data)
+{
+       GsPluginLoaderHelper *helper = (GsPluginLoaderHelper *) user_data;
+
+       /* call the cancellable */
+       g_debug ("cancelling job as it took too long");
+       if (!g_cancellable_is_cancelled (helper->cancellable))
+               g_cancellable_cancel (helper->cancellable);
+
+       /* failed */
+       helper->timeout_triggered = TRUE;
+       helper->timeout_id = 0;
+       return G_SOURCE_REMOVE;
+}
+
+static void
+gs_plugin_loader_cancelled_cb (GCancellable *cancellable, GsPluginLoaderHelper *helper)
+{
+       /* just proxy this forward */
+       g_cancellable_cancel (helper->cancellable);
+}
+
 /**
  * gs_plugin_loader_job_process_async:
  *
@@ -3264,6 +3331,7 @@ gs_plugin_loader_job_process_async (GsPluginLoader *plugin_loader,
        GsPluginLoaderHelper *helper;
        GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
        g_autoptr(GTask) task = NULL;
+       g_autoptr(GCancellable) cancellable_job = g_cancellable_new ();
 
        g_return_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader));
        g_return_if_fail (GS_IS_PLUGIN_JOB (plugin_job));
@@ -3337,7 +3405,7 @@ gs_plugin_loader_job_process_async (GsPluginLoader *plugin_loader,
        }
 
        /* check required args */
-       task = g_task_new (plugin_loader, cancellable, callback, user_data);
+       task = g_task_new (plugin_loader, cancellable_job, callback, user_data);
        switch (action) {
        case GS_PLUGIN_ACTION_SEARCH:
        case GS_PLUGIN_ACTION_SEARCH_FILES:
@@ -3398,6 +3466,10 @@ gs_plugin_loader_job_process_async (GsPluginLoader *plugin_loader,
        g_task_set_task_data (task, helper, (GDestroyNotify) gs_plugin_loader_helper_free);
        gs_plugin_loader_job_debug (helper);
 
+       /* let the task cancel itself */
+       g_task_set_check_cancellable (task, FALSE);
+       g_task_set_return_on_cancel (task, FALSE);
+
        /* pre-tokenize search */
        if (action == GS_PLUGIN_ACTION_SEARCH) {
                const gchar *search = gs_plugin_job_get_search (plugin_job);
@@ -3411,6 +3483,36 @@ gs_plugin_loader_job_process_async (GsPluginLoader *plugin_loader,
                }
        }
 
+       /* jobs always have a valid cancellable, so proxy the caller */
+       helper->cancellable = g_object_ref (cancellable_job);
+       if (cancellable != NULL) {
+               helper->cancellable_caller = g_object_ref (cancellable);
+               helper->cancellable_id =
+                       g_cancellable_connect (helper->cancellable_caller,
+                                              G_CALLBACK (gs_plugin_loader_cancelled_cb),
+                                              helper, NULL);
+       }
+
+       /* set up a hang handler */
+       switch (action) {
+       case GS_PLUGIN_ACTION_GET_CATEGORY_APPS:
+       case GS_PLUGIN_ACTION_GET_FEATURED:
+       case GS_PLUGIN_ACTION_GET_INSTALLED:
+       case GS_PLUGIN_ACTION_GET_POPULAR:
+       case GS_PLUGIN_ACTION_GET_RECENT:
+       case GS_PLUGIN_ACTION_GET_UPDATES:
+       case GS_PLUGIN_ACTION_SEARCH:
+       case GS_PLUGIN_ACTION_SEARCH_FILES:
+       case GS_PLUGIN_ACTION_SEARCH_PROVIDES:
+               helper->timeout_id =
+                       g_timeout_add_seconds (gs_plugin_job_get_timeout (plugin_job),
+                                              gs_plugin_loader_job_timeout_cb,
+                                              helper);
+               break;
+       default:
+               break;
+       }
+
        /* run in a thread */
        g_task_run_in_thread (task, gs_plugin_loader_process_thread_cb);
 }
diff --git a/lib/gs-plugin-types.h b/lib/gs-plugin-types.h
index 960455e..244b323 100644
--- a/lib/gs-plugin-types.h
+++ b/lib/gs-plugin-types.h
@@ -91,6 +91,7 @@ typedef guint64 GsPluginFlags;
  * @GS_PLUGIN_ERROR_DELETE_FAILED:             The delete action failed
  * @GS_PLUGIN_ERROR_RESTART_REQUIRED:          A restart is required
  * @GS_PLUGIN_ERROR_AC_POWER_REQUIRED:         AC power is required
+ * @GS_PLUGIN_ERROR_TIMED_OUT:                 The job timed out
  *
  * The failure error types.
  **/
@@ -113,6 +114,7 @@ typedef enum {
        GS_PLUGIN_ERROR_DELETE_FAILED,
        GS_PLUGIN_ERROR_RESTART_REQUIRED,
        GS_PLUGIN_ERROR_AC_POWER_REQUIRED,
+       GS_PLUGIN_ERROR_TIMED_OUT,
        /*< private >*/
        GS_PLUGIN_ERROR_LAST
 } GsPluginError;
diff --git a/lib/gs-plugin.c b/lib/gs-plugin.c
index 6c098f1..2e2bc57 100644
--- a/lib/gs-plugin.c
+++ b/lib/gs-plugin.c
@@ -1664,6 +1664,8 @@ gs_plugin_error_to_string (GsPluginError error)
                return "restart-required";
        if (error == GS_PLUGIN_ERROR_AC_POWER_REQUIRED)
                return "ac-power-required";
+       if (error == GS_PLUGIN_ERROR_TIMED_OUT)
+               return "timed-out";
        return NULL;
 }
 
diff --git a/lib/gs-utils.c b/lib/gs-utils.c
index 359e30f..0e051e3 100644
--- a/lib/gs-utils.c
+++ b/lib/gs-utils.c
@@ -1,6 +1,6 @@
 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
  *
- * Copyright (C) 2013-2016 Richard Hughes <richard hughsie com>
+ * Copyright (C) 2013-2017 Richard Hughes <richard hughsie com>
  *
  * Licensed under the GNU General Public License Version 2
  *
@@ -678,11 +678,13 @@ gs_utils_error_convert_gio (GError **perror)
                return FALSE;
        switch (error->code) {
        case G_IO_ERROR_FAILED:
-       case G_IO_ERROR_TIMED_OUT:
        case G_IO_ERROR_NOT_FOUND:
        case G_IO_ERROR_EXISTS:
                error->code = GS_PLUGIN_ERROR_FAILED;
                break;
+       case G_IO_ERROR_TIMED_OUT:
+               error->code = GS_PLUGIN_ERROR_TIMED_OUT;
+               break;
        case G_IO_ERROR_NOT_SUPPORTED:
                error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED;
                break;
diff --git a/plugins/dummy/gs-plugin-dummy.c b/plugins/dummy/gs-plugin-dummy.c
index 481e974..6962852 100644
--- a/plugins/dummy/gs-plugin-dummy.c
+++ b/plugins/dummy/gs-plugin-dummy.c
@@ -1,6 +1,6 @@
 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
  *
- * Copyright (C) 2011-2013 Richard Hughes <richard hughsie com>
+ * Copyright (C) 2011-2017 Richard Hughes <richard hughsie com>
  *
  * Licensed under the GNU General Public License Version 2
  *
@@ -224,6 +224,64 @@ gs_plugin_url_to_app (GsPlugin *plugin,
        return TRUE;
 }
 
+typedef struct {
+       GMainLoop       *loop;
+       GCancellable    *cancellable;
+       guint            timer_id;
+       gulong           cancellable_id;
+} GsPluginDummyTimeoutHelper;
+
+static gboolean
+gs_plugin_dummy_timeout_hang_cb (gpointer user_data)
+{
+       GsPluginDummyTimeoutHelper *helper = (GsPluginDummyTimeoutHelper *) user_data;
+       helper->timer_id = 0;
+       g_debug ("timeout hang");
+       g_main_loop_quit (helper->loop);
+       return FALSE;
+}
+
+static void
+gs_plugin_dummy_timeout_cancelled_cb (GCancellable *cancellable, gpointer user_data)
+{
+       GsPluginDummyTimeoutHelper *helper = (GsPluginDummyTimeoutHelper *) user_data;
+       g_debug ("calling cancel");
+       g_main_loop_quit (helper->loop);
+}
+
+static void
+gs_plugin_dummy_timeout_helper_free (GsPluginDummyTimeoutHelper *helper)
+{
+       if (helper->cancellable_id != 0)
+               g_signal_handler_disconnect (helper->cancellable, helper->cancellable_id);
+       if (helper->timer_id != 0)
+               g_source_remove (helper->timer_id);
+       if (helper->cancellable != NULL)
+               g_object_unref (helper->cancellable);
+       g_main_loop_unref (helper->loop);
+       g_free (helper);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsPluginDummyTimeoutHelper, gs_plugin_dummy_timeout_helper_free)
+
+static void
+gs_plugin_dummy_timeout_add (guint timeout_ms, GCancellable *cancellable)
+{
+       g_autoptr(GsPluginDummyTimeoutHelper) helper = g_new0 (GsPluginDummyTimeoutHelper, 1);
+       helper->loop = g_main_loop_new (NULL, TRUE);
+       if (cancellable != NULL) {
+               helper->cancellable = g_object_ref (cancellable);
+               helper->cancellable_id =
+                       g_signal_connect (cancellable, "cancelled",
+                                         G_CALLBACK (gs_plugin_dummy_timeout_cancelled_cb),
+                                         helper);
+       }
+       helper->timer_id = g_timeout_add (timeout_ms,
+                                         gs_plugin_dummy_timeout_hang_cb,
+                                         helper);
+       g_main_loop_run (helper->loop);
+}
+
 gboolean
 gs_plugin_add_search (GsPlugin *plugin,
                      gchar **values,
@@ -235,6 +293,16 @@ gs_plugin_add_search (GsPlugin *plugin,
        g_autoptr(GsApp) app = NULL;
        g_autoptr(AsIcon) ic = NULL;
 
+       /* hang the plugin for 5 seconds */
+       if (g_strcmp0 (values[0], "hang") == 0) {
+               gs_plugin_dummy_timeout_add (5000, cancellable);
+               if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+                       gs_utils_error_convert_gio (error);
+                       return FALSE;
+               }
+               return TRUE;
+       }
+
        /* we're very specific */
        if (g_strcmp0 (values[0], "chiron") != 0)
                return TRUE;
diff --git a/plugins/dummy/gs-self-test.c b/plugins/dummy/gs-self-test.c
index 56dd3a5..3222ca4 100644
--- a/plugins/dummy/gs-self-test.c
+++ b/plugins/dummy/gs-self-test.c
@@ -371,6 +371,35 @@ gs_plugins_dummy_search_func (GsPluginLoader *plugin_loader)
 }
 
 static void
+gs_plugins_dummy_hang_func (GsPluginLoader *plugin_loader)
+{
+       g_autoptr(GCancellable) cancellable = g_cancellable_new ();
+       g_autoptr(GError) error = NULL;
+       g_autoptr(GPtrArray) events = NULL;
+       g_autoptr(GsAppList) list = NULL;
+       g_autoptr(GsPluginJob) plugin_job = NULL;
+
+       /* drop all caches */
+       gs_plugin_loader_setup_again (plugin_loader);
+
+       /* get search result based on addon keyword */
+       plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH,
+                                        "search", "hang",
+                                        "failure-flags", GS_PLUGIN_FAILURE_FLAGS_USE_EVENTS |
+                                                         GS_PLUGIN_FAILURE_FLAGS_NO_CONSOLE,
+                                        "timeout", 1, /* seconds */
+                                        NULL);
+       list = gs_plugin_loader_job_process (plugin_loader, plugin_job, cancellable, &error);
+       gs_test_flush_main_context ();
+       g_assert_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_TIMED_OUT);
+       g_assert (list == NULL);
+
+       /* ensure one event (plugin may also return error) */
+       events = gs_plugin_loader_get_events (plugin_loader);
+       g_assert_cmpint (events->len, ==, 1);
+}
+
+static void
 gs_plugins_dummy_search_invalid_func (GsPluginLoader *plugin_loader)
 {
        g_autoptr(GError) error = NULL;
@@ -705,6 +734,9 @@ main (int argc, char **argv)
        g_test_add_data_func ("/gnome-software/plugins/dummy/search",
                              plugin_loader,
                              (GTestDataFunc) gs_plugins_dummy_search_func);
+       g_test_add_data_func ("/gnome-software/plugins/dummy/hang",
+                             plugin_loader,
+                             (GTestDataFunc) gs_plugins_dummy_hang_func);
        g_test_add_data_func ("/gnome-software/plugins/dummy/search{invalid}",
                              plugin_loader,
                              (GTestDataFunc) gs_plugins_dummy_search_invalid_func);
diff --git a/src/gs-search-page.c b/src/gs-search-page.c
index a97209e..93ca1ad 100644
--- a/src/gs-search-page.c
+++ b/src/gs-search-page.c
@@ -271,6 +271,7 @@ gs_search_page_load (GsSearchPage *self)
        plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH,
                                         "search", self->value,
                                         "max-results", GS_SEARCH_PAGE_MAX_RESULTS,
+                                        "timeout", 10,
                                         "failure-flags", GS_PLUGIN_FAILURE_FLAGS_USE_EVENTS,
                                         "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON |
                                                         GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION |
diff --git a/src/gs-shell.c b/src/gs-shell.c
index 2b1808c..5dd7d72 100644
--- a/src/gs-shell.c
+++ b/src/gs-shell.c
@@ -1,6 +1,6 @@
 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
  *
- * Copyright (C) 2013-2016 Richard Hughes <richard hughsie com>
+ * Copyright (C) 2013-2017 Richard Hughes <richard hughsie com>
  * Copyright (C) 2013 Matthias Clasen <mclasen redhat com>
  *
  * Licensed under the GNU General Public License Version 2
@@ -1498,6 +1498,13 @@ gs_shell_show_event (GsShell *shell, GsPluginEvent *event)
        if (error == NULL)
                return FALSE;
 
+       /* name and shame the plugin */
+       if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_TIMED_OUT)) {
+               gs_shell_show_event_app_notify (shell, error->message,
+                                               GS_SHELL_EVENT_BUTTON_NONE);
+               return TRUE;
+       }
+
        /* split up the events by action */
        action = gs_plugin_event_get_action (event);
        switch (action) {


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