/* * camel-operation.c * * 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. * * 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, see . * */ #include "evolution-data-server-config.h" #include #include #include #include #include "camel-msgport.h" #include "camel-operation.h" #define PROGRESS_DELAY 250 /* milliseconds */ #define TRANSIENT_DELAY 250 /* milliseconds */ #define POP_MESSAGE_DELAY 1 /* seconds */ typedef struct _StatusNode StatusNode; struct _StatusNode { volatile gint ref_count; CamelOperation *operation; guint source_id; /* for timeout or idle */ gchar *message; gint percent; }; struct _CamelOperationPrivate { GQueue status_stack; GCancellable *proxying; gulong proxying_handler_id; }; enum { STATUS, PUSH_MESSAGE, POP_MESSAGE, PROGRESS, LAST_SIGNAL }; static GRecMutex operation_lock; #define LOCK() g_rec_mutex_lock (&operation_lock) #define UNLOCK() g_rec_mutex_unlock (&operation_lock) static GQueue operation_list = G_QUEUE_INIT; static guint signals[LAST_SIGNAL]; G_DEFINE_TYPE_WITH_PRIVATE (CamelOperation, camel_operation, G_TYPE_CANCELLABLE) static StatusNode * status_node_new (void) { StatusNode *node; node = g_slice_new0 (StatusNode); node->ref_count = 1; return node; } static StatusNode * status_node_ref (StatusNode *node) { g_return_val_if_fail (node != NULL, NULL); g_return_val_if_fail (node->ref_count > 0, node); g_atomic_int_inc (&node->ref_count); return node; } static void status_node_unref (StatusNode *node) { g_return_if_fail (node != NULL); g_return_if_fail (node->ref_count > 0); if (g_atomic_int_dec_and_test (&node->ref_count)) { if (node->operation != NULL) g_object_unref (node->operation); if (node->source_id > 0) g_source_remove (node->source_id); g_free (node->message); g_slice_free (StatusNode, node); } } static gboolean operation_emit_status_cb (gpointer user_data) { StatusNode *node = user_data; StatusNode *head_node; gboolean emit_status; LOCK (); node->source_id = 0; /* Check if we've been preempted by another StatusNode, * or if we've been cancelled and popped off the stack. */ head_node = g_queue_peek_head (&node->operation->priv->status_stack); emit_status = (node == head_node); UNLOCK (); if (emit_status) g_signal_emit ( node->operation, signals[STATUS], 0, node->message, node->percent); return FALSE; } static void proxying_cancellable_cancelled_cb (GCancellable *cancellable, GCancellable *operation) { g_return_if_fail (CAMEL_IS_OPERATION (operation)); g_cancellable_cancel (operation); } static void operation_dispose (GObject *object) { CamelOperationPrivate *priv; priv = CAMEL_OPERATION (object)->priv; LOCK (); if (priv->proxying && priv->proxying_handler_id) { /* Intentionally avoid g_cancellable_disconnect(), because it can lock when the priv->proxying holds the last reference. */ g_signal_handler_disconnect (priv->proxying, priv->proxying_handler_id); priv->proxying_handler_id = 0; } g_clear_object (&priv->proxying); g_queue_remove (&operation_list, object); /* Because each StatusNode holds a reference to its * CamelOperation, the fact that we're being disposed * implies the stack should be empty now. */ g_warn_if_fail (g_queue_is_empty (&priv->status_stack)); UNLOCK (); /* Chain up to parent's dispose() method. */ G_OBJECT_CLASS (camel_operation_parent_class)->dispose (object); } static void camel_operation_class_init (CamelOperationClass *class) { GObjectClass *object_class; object_class = G_OBJECT_CLASS (class); object_class->dispose = operation_dispose; signals[STATUS] = g_signal_new ( "status", G_TYPE_FROM_CLASS (class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (CamelOperationClass, status), NULL, NULL, NULL, G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_INT); signals[PUSH_MESSAGE] = g_signal_new ( "push-message", G_TYPE_FROM_CLASS (class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING); signals[POP_MESSAGE] = g_signal_new ( "pop-message", G_TYPE_FROM_CLASS (class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); signals[PROGRESS] = g_signal_new ( "progress", G_TYPE_FROM_CLASS (class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_INT); } static void camel_operation_init (CamelOperation *operation) { operation->priv = camel_operation_get_instance_private (operation); g_queue_init (&operation->priv->status_stack); operation->priv->proxying = NULL; operation->priv->proxying_handler_id = 0; LOCK (); g_queue_push_tail (&operation_list, operation); UNLOCK (); } /** * camel_operation_new: * * Create a new camel operation handle. Camel operation handles can * be used in a multithreaded application (or a single operation * handle can be used in a non threaded appliation) to cancel running * operations and to obtain notification messages of the internal * status of messages. * * Returns: (transfer full): A new operation handle. **/ GCancellable * camel_operation_new (void) { return g_object_new (CAMEL_TYPE_OPERATION, NULL); } /** * camel_operation_new_proxy: * @cancellable: (nullable): a #GCancellable to proxy * * Proxies the @cancellable in a way that if it is cancelled, * then the returned cancellable is also cancelled, but when * the returned cancellable is cancelled, then it doesn't * influence the original cancellable. Other CamelOperation * actions being done on the returned cancellable are also * propagated to the @cancellable. * * The passed-in @cancellable can be %NULL, in which case * a plain CamelOperation is returned. * * This is useful when some operation can be cancelled from * elsewhere (like by a user), but also by the code on its own, * when it doesn't make sense to cancel also any larger operation * to which the passed-in cancellable belongs. * * Returns: (transfer full): A new operation handle, proxying @cancellable. * * Since: 3.24 **/ GCancellable * camel_operation_new_proxy (GCancellable *cancellable) { GCancellable *operation; CamelOperationPrivate *priv; operation = camel_operation_new (); if (!G_IS_CANCELLABLE (cancellable)) return operation; priv = CAMEL_OPERATION (operation)->priv; g_return_val_if_fail (priv != NULL, operation); priv->proxying = g_object_ref (cancellable); /* Intentionally avoid g_cancellable_connect(), because it can lock on call of g_cancellable_disconnect() when the priv->proxying holds the last reference. */ priv->proxying_handler_id = g_signal_connect_data (cancellable, "cancelled", G_CALLBACK (proxying_cancellable_cancelled_cb), operation, NULL, 0); if (g_cancellable_is_cancelled (cancellable)) proxying_cancellable_cancelled_cb (cancellable, operation); return operation; } /** * camel_operation_cancel_all: * * Cancel all outstanding operations. **/ void camel_operation_cancel_all (void) { GList *link; LOCK (); link = g_queue_peek_head_link (&operation_list); while (link != NULL) { GCancellable *cancellable = link->data; g_cancellable_cancel (cancellable); link = g_list_next (link); } UNLOCK (); } /** * camel_operation_push_message: * @cancellable: a #GCancellable or %NULL * @format: a standard printf() format string * @...: the parameters to insert into the format string * * Call this function to describe an operation being performed. * Call camel_operation_progress() to report progress on the operation. * Call camel_operation_pop_message() when the operation is complete. * * This function only works if @cancellable is a #CamelOperation cast as a * #GCancellable. If @cancellable is a plain #GCancellable or %NULL, the * function does nothing and returns silently. **/ void camel_operation_push_message (GCancellable *cancellable, const gchar *format, ...) { CamelOperation *operation; StatusNode *node; gchar *message; va_list ap; if (cancellable == NULL) return; if (G_OBJECT_TYPE (cancellable) == G_TYPE_CANCELLABLE) return; g_return_if_fail (CAMEL_IS_OPERATION (cancellable)); va_start (ap, format); message = g_strdup_vprintf (format, ap); va_end (ap); operation = CAMEL_OPERATION (cancellable); g_signal_emit (cancellable, signals[PUSH_MESSAGE], 0, message); if (operation->priv->proxying) camel_operation_push_message (operation->priv->proxying, "%s", message); LOCK (); node = status_node_new (); node->message = message; /* takes ownership */ node->operation = g_object_ref (operation); if (g_queue_is_empty (&operation->priv->status_stack)) { node->source_id = g_idle_add_full ( G_PRIORITY_DEFAULT_IDLE, operation_emit_status_cb, status_node_ref (node), (GDestroyNotify) status_node_unref); } else { GSource *source; source = g_timeout_source_new (TRANSIENT_DELAY); g_source_set_callback (source, operation_emit_status_cb, status_node_ref (node), (GDestroyNotify) status_node_unref); g_source_set_name (source, "[camel] operation_emit_status_cb"); node->source_id = g_source_attach (source, NULL); g_source_unref (source); } g_queue_push_head (&operation->priv->status_stack, node); UNLOCK (); } /** * camel_operation_pop_message: * @cancellable: a #GCancellable * * Pops the most recently pushed message. * * This function only works if @cancellable is a #CamelOperation cast as a * #GCancellable. If @cancellable is a plain #GCancellable or %NULL, the * function does nothing and returns silently. **/ void camel_operation_pop_message (GCancellable *cancellable) { CamelOperation *operation; StatusNode *node; if (cancellable == NULL) return; if (G_OBJECT_TYPE (cancellable) == G_TYPE_CANCELLABLE) return; g_return_if_fail (CAMEL_IS_OPERATION (cancellable)); operation = CAMEL_OPERATION (cancellable); g_signal_emit (cancellable, signals[POP_MESSAGE], 0); if (operation->priv->proxying) camel_operation_pop_message (operation->priv->proxying); LOCK (); node = g_queue_pop_head (&operation->priv->status_stack); if (node != NULL) { if (node->source_id > 0) { g_source_remove (node->source_id); node->source_id = 0; } status_node_unref (node); } node = g_queue_peek_head (&operation->priv->status_stack); if (node != NULL) { GSource *source; if (node->source_id != 0) g_source_remove (node->source_id); source = g_timeout_source_new_seconds (POP_MESSAGE_DELAY); g_source_set_callback (source, operation_emit_status_cb, status_node_ref (node), (GDestroyNotify) status_node_unref); g_source_set_name (source, "[camel] operation_emit_status_cb"); node->source_id = g_source_attach (source, NULL); g_source_unref (source); } UNLOCK (); } /** * camel_operation_progress: * @cancellable: a #GCancellable or %NULL * @percent: percent complete, 0 to 100. * * Report progress on the current operation. @percent reports the current * percentage of completion, which should be in the range of 0 to 100. * * This function only works if @cancellable is a #CamelOperation cast as a * #GCancellable. If @cancellable is a plain #GCancellable or %NULL, the * function does nothing and returns silently. **/ void camel_operation_progress (GCancellable *cancellable, gint percent) { CamelOperation *operation; StatusNode *node; if (cancellable == NULL) return; if (G_OBJECT_TYPE (cancellable) == G_TYPE_CANCELLABLE) return; g_return_if_fail (CAMEL_IS_OPERATION (cancellable)); operation = CAMEL_OPERATION (cancellable); g_signal_emit (cancellable, signals[PROGRESS], 0, percent); if (operation->priv->proxying) camel_operation_progress (operation->priv->proxying, percent); LOCK (); node = g_queue_peek_head (&operation->priv->status_stack); if (node != NULL) { node->percent = percent; /* Rate limit progress updates. */ if (node->source_id == 0) { GSource *source; source = g_timeout_source_new (PROGRESS_DELAY); g_source_set_callback (source, operation_emit_status_cb, status_node_ref (node), (GDestroyNotify) status_node_unref); g_source_set_name (source, "[camel] operation_emit_status_cb"); node->source_id = g_source_attach (source, NULL); g_source_unref (source); } } UNLOCK (); }