1 /*
2 * camel-operation.c
3 *
4 * This library is free software: you can redistribute it and/or modify it
5 * under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation.
7 *
8 * This library is distributed in the hope that it will be useful, but
9 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
11 * for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public License
14 * along with this library. If not, see <http://www.gnu.org/licenses/>.
15 *
16 */
17
18 #include "evolution-data-server-config.h"
19
20 #include <stdio.h>
21 #include <unistd.h>
22 #include <sys/time.h>
23
24 #include <nspr.h>
25
26 #include "camel-msgport.h"
27 #include "camel-operation.h"
28
29 #define PROGRESS_DELAY 250 /* milliseconds */
30 #define TRANSIENT_DELAY 250 /* milliseconds */
31 #define POP_MESSAGE_DELAY 1 /* seconds */
32
33 typedef struct _StatusNode StatusNode;
34
35 struct _StatusNode {
36 volatile gint ref_count;
37 CamelOperation *operation;
38 guint source_id; /* for timeout or idle */
39 gchar *message;
40 gint percent;
41 };
42
43 struct _CamelOperationPrivate {
44 GQueue status_stack;
45 GCancellable *proxying;
46 gulong proxying_handler_id;
47 };
48
49 enum {
50 STATUS,
51 PUSH_MESSAGE,
52 POP_MESSAGE,
53 PROGRESS,
54 LAST_SIGNAL
55 };
56
57 static GRecMutex operation_lock;
58 #define LOCK() g_rec_mutex_lock (&operation_lock)
59 #define UNLOCK() g_rec_mutex_unlock (&operation_lock)
60
61 static GQueue operation_list = G_QUEUE_INIT;
62
63 static guint signals[LAST_SIGNAL];
64
G_DEFINE_TYPE_WITH_PRIVATE(CamelOperation,camel_operation,G_TYPE_CANCELLABLE)65 G_DEFINE_TYPE_WITH_PRIVATE (CamelOperation, camel_operation, G_TYPE_CANCELLABLE)
66
67 static StatusNode *
68 status_node_new (void)
69 {
70 StatusNode *node;
71
72 node = g_slice_new0 (StatusNode);
73 node->ref_count = 1;
74
75 return node;
76 }
77
78 static StatusNode *
status_node_ref(StatusNode * node)79 status_node_ref (StatusNode *node)
80 {
81 g_return_val_if_fail (node != NULL, NULL);
82 g_return_val_if_fail (node->ref_count > 0, node);
83
84 g_atomic_int_inc (&node->ref_count);
85
86 return node;
87 }
88
89 static void
status_node_unref(StatusNode * node)90 status_node_unref (StatusNode *node)
91 {
92 g_return_if_fail (node != NULL);
93 g_return_if_fail (node->ref_count > 0);
94
95 if (g_atomic_int_dec_and_test (&node->ref_count)) {
96
97 if (node->operation != NULL)
98 g_object_unref (node->operation);
99
100 if (node->source_id > 0)
101 g_source_remove (node->source_id);
102
103 g_free (node->message);
104
105 g_slice_free (StatusNode, node);
106 }
107 }
108
109 static gboolean
operation_emit_status_cb(gpointer user_data)110 operation_emit_status_cb (gpointer user_data)
111 {
112 StatusNode *node = user_data;
113 StatusNode *head_node;
114 gboolean emit_status;
115
116 LOCK ();
117
118 node->source_id = 0;
119
120 /* Check if we've been preempted by another StatusNode,
121 * or if we've been cancelled and popped off the stack. */
122 head_node = g_queue_peek_head (&node->operation->priv->status_stack);
123 emit_status = (node == head_node);
124
125 UNLOCK ();
126
127 if (emit_status)
128 g_signal_emit (
129 node->operation,
130 signals[STATUS], 0,
131 node->message,
132 node->percent);
133
134 return FALSE;
135 }
136
137 static void
proxying_cancellable_cancelled_cb(GCancellable * cancellable,GCancellable * operation)138 proxying_cancellable_cancelled_cb (GCancellable *cancellable,
139 GCancellable *operation)
140 {
141 g_return_if_fail (CAMEL_IS_OPERATION (operation));
142
143 g_cancellable_cancel (operation);
144 }
145
146 static void
operation_dispose(GObject * object)147 operation_dispose (GObject *object)
148 {
149 CamelOperationPrivate *priv;
150
151 priv = CAMEL_OPERATION (object)->priv;
152
153 LOCK ();
154
155 if (priv->proxying && priv->proxying_handler_id) {
156 /* Intentionally avoid g_cancellable_disconnect(), because it can lock
157 when the priv->proxying holds the last reference. */
158 g_signal_handler_disconnect (priv->proxying, priv->proxying_handler_id);
159 priv->proxying_handler_id = 0;
160 }
161
162 g_clear_object (&priv->proxying);
163
164 g_queue_remove (&operation_list, object);
165
166 /* Because each StatusNode holds a reference to its
167 * CamelOperation, the fact that we're being disposed
168 * implies the stack should be empty now. */
169 g_warn_if_fail (g_queue_is_empty (&priv->status_stack));
170
171 UNLOCK ();
172
173 /* Chain up to parent's dispose() method. */
174 G_OBJECT_CLASS (camel_operation_parent_class)->dispose (object);
175 }
176
177 static void
camel_operation_class_init(CamelOperationClass * class)178 camel_operation_class_init (CamelOperationClass *class)
179 {
180 GObjectClass *object_class;
181
182 object_class = G_OBJECT_CLASS (class);
183 object_class->dispose = operation_dispose;
184
185 signals[STATUS] = g_signal_new (
186 "status",
187 G_TYPE_FROM_CLASS (class),
188 G_SIGNAL_RUN_LAST,
189 G_STRUCT_OFFSET (CamelOperationClass, status),
190 NULL, NULL, NULL,
191 G_TYPE_NONE, 2,
192 G_TYPE_STRING,
193 G_TYPE_INT);
194
195 signals[PUSH_MESSAGE] = g_signal_new (
196 "push-message",
197 G_TYPE_FROM_CLASS (class),
198 G_SIGNAL_RUN_LAST,
199 0,
200 NULL, NULL, NULL,
201 G_TYPE_NONE, 1,
202 G_TYPE_STRING);
203
204 signals[POP_MESSAGE] = g_signal_new (
205 "pop-message",
206 G_TYPE_FROM_CLASS (class),
207 G_SIGNAL_RUN_LAST,
208 0,
209 NULL, NULL, NULL,
210 G_TYPE_NONE, 0);
211
212 signals[PROGRESS] = g_signal_new (
213 "progress",
214 G_TYPE_FROM_CLASS (class),
215 G_SIGNAL_RUN_LAST,
216 0,
217 NULL, NULL, NULL,
218 G_TYPE_NONE, 1,
219 G_TYPE_INT);
220 }
221
222 static void
camel_operation_init(CamelOperation * operation)223 camel_operation_init (CamelOperation *operation)
224 {
225 operation->priv = camel_operation_get_instance_private (operation);
226
227 g_queue_init (&operation->priv->status_stack);
228 operation->priv->proxying = NULL;
229 operation->priv->proxying_handler_id = 0;
230
231 LOCK ();
232 g_queue_push_tail (&operation_list, operation);
233 UNLOCK ();
234 }
235
236 /**
237 * camel_operation_new:
238 *
239 * Create a new camel operation handle. Camel operation handles can
240 * be used in a multithreaded application (or a single operation
241 * handle can be used in a non threaded appliation) to cancel running
242 * operations and to obtain notification messages of the internal
243 * status of messages.
244 *
245 * Returns: (transfer full): A new operation handle.
246 **/
247 GCancellable *
camel_operation_new(void)248 camel_operation_new (void)
249 {
250 return g_object_new (CAMEL_TYPE_OPERATION, NULL);
251 }
252
253 /**
254 * camel_operation_new_proxy:
255 * @cancellable: (nullable): a #GCancellable to proxy
256 *
257 * Proxies the @cancellable in a way that if it is cancelled,
258 * then the returned cancellable is also cancelled, but when
259 * the returned cancellable is cancelled, then it doesn't
260 * influence the original cancellable. Other CamelOperation
261 * actions being done on the returned cancellable are also
262 * propagated to the @cancellable.
263 *
264 * The passed-in @cancellable can be %NULL, in which case
265 * a plain CamelOperation is returned.
266 *
267 * This is useful when some operation can be cancelled from
268 * elsewhere (like by a user), but also by the code on its own,
269 * when it doesn't make sense to cancel also any larger operation
270 * to which the passed-in cancellable belongs.
271 *
272 * Returns: (transfer full): A new operation handle, proxying @cancellable.
273 *
274 * Since: 3.24
275 **/
276 GCancellable *
camel_operation_new_proxy(GCancellable * cancellable)277 camel_operation_new_proxy (GCancellable *cancellable)
278 {
279 GCancellable *operation;
280 CamelOperationPrivate *priv;
281
282 operation = camel_operation_new ();
283
284 if (!G_IS_CANCELLABLE (cancellable))
285 return operation;
286
287 priv = CAMEL_OPERATION (operation)->priv;
288 g_return_val_if_fail (priv != NULL, operation);
289
290 priv->proxying = g_object_ref (cancellable);
291 /* Intentionally avoid g_cancellable_connect(), because it can lock on call
292 of g_cancellable_disconnect() when the priv->proxying holds the last
293 reference. */
294 priv->proxying_handler_id = g_signal_connect_data (cancellable, "cancelled",
295 G_CALLBACK (proxying_cancellable_cancelled_cb), operation, NULL, 0);
296
297 if (g_cancellable_is_cancelled (cancellable))
298 proxying_cancellable_cancelled_cb (cancellable, operation);
299
300 return operation;
301 }
302
303 /**
304 * camel_operation_cancel_all:
305 *
306 * Cancel all outstanding operations.
307 **/
308 void
camel_operation_cancel_all(void)309 camel_operation_cancel_all (void)
310 {
311 GList *link;
312
313 LOCK ();
314
315 link = g_queue_peek_head_link (&operation_list);
316
317 while (link != NULL) {
318 GCancellable *cancellable = link->data;
319
320 g_cancellable_cancel (cancellable);
321
322 link = g_list_next (link);
323 }
324
325 UNLOCK ();
326 }
327
328 /**
329 * camel_operation_push_message:
330 * @cancellable: a #GCancellable or %NULL
331 * @format: a standard printf() format string
332 * @...: the parameters to insert into the format string
333 *
334 * Call this function to describe an operation being performed.
335 * Call camel_operation_progress() to report progress on the operation.
336 * Call camel_operation_pop_message() when the operation is complete.
337 *
338 * This function only works if @cancellable is a #CamelOperation cast as a
339 * #GCancellable. If @cancellable is a plain #GCancellable or %NULL, the
340 * function does nothing and returns silently.
341 **/
342 void
camel_operation_push_message(GCancellable * cancellable,const gchar * format,...)343 camel_operation_push_message (GCancellable *cancellable,
344 const gchar *format, ...)
345 {
346 CamelOperation *operation;
347 StatusNode *node;
348 gchar *message;
349 va_list ap;
350
351 if (cancellable == NULL)
352 return;
353
354 if (G_OBJECT_TYPE (cancellable) == G_TYPE_CANCELLABLE)
355 return;
356
357 g_return_if_fail (CAMEL_IS_OPERATION (cancellable));
358
359 va_start (ap, format);
360 message = g_strdup_vprintf (format, ap);
361 va_end (ap);
362
363 operation = CAMEL_OPERATION (cancellable);
364
365 g_signal_emit (cancellable, signals[PUSH_MESSAGE], 0, message);
366
367 if (operation->priv->proxying)
368 camel_operation_push_message (operation->priv->proxying, "%s", message);
369
370 LOCK ();
371
372 node = status_node_new ();
373 node->message = message; /* takes ownership */
374 node->operation = g_object_ref (operation);
375
376 if (g_queue_is_empty (&operation->priv->status_stack)) {
377 node->source_id = g_idle_add_full (
378 G_PRIORITY_DEFAULT_IDLE,
379 operation_emit_status_cb,
380 status_node_ref (node),
381 (GDestroyNotify) status_node_unref);
382 } else {
383 node->source_id = g_timeout_add_full (
384 G_PRIORITY_DEFAULT, TRANSIENT_DELAY,
385 operation_emit_status_cb,
386 status_node_ref (node),
387 (GDestroyNotify) status_node_unref);
388 g_source_set_name_by_id (
389 node->source_id,
390 "[camel] operation_emit_status_cb");
391 }
392
393 g_queue_push_head (&operation->priv->status_stack, node);
394
395 UNLOCK ();
396 }
397
398 /**
399 * camel_operation_pop_message:
400 * @cancellable: a #GCancellable
401 *
402 * Pops the most recently pushed message.
403 *
404 * This function only works if @cancellable is a #CamelOperation cast as a
405 * #GCancellable. If @cancellable is a plain #GCancellable or %NULL, the
406 * function does nothing and returns silently.
407 **/
408 void
camel_operation_pop_message(GCancellable * cancellable)409 camel_operation_pop_message (GCancellable *cancellable)
410 {
411 CamelOperation *operation;
412 StatusNode *node;
413
414 if (cancellable == NULL)
415 return;
416
417 if (G_OBJECT_TYPE (cancellable) == G_TYPE_CANCELLABLE)
418 return;
419
420 g_return_if_fail (CAMEL_IS_OPERATION (cancellable));
421
422 operation = CAMEL_OPERATION (cancellable);
423
424 g_signal_emit (cancellable, signals[POP_MESSAGE], 0);
425
426 if (operation->priv->proxying)
427 camel_operation_pop_message (operation->priv->proxying);
428
429 LOCK ();
430
431 node = g_queue_pop_head (&operation->priv->status_stack);
432
433 if (node != NULL) {
434 if (node->source_id > 0) {
435 g_source_remove (node->source_id);
436 node->source_id = 0;
437 }
438 status_node_unref (node);
439 }
440
441 node = g_queue_peek_head (&operation->priv->status_stack);
442
443 if (node != NULL) {
444 if (node->source_id != 0)
445 g_source_remove (node->source_id);
446
447 node->source_id = g_timeout_add_seconds_full (
448 G_PRIORITY_DEFAULT, POP_MESSAGE_DELAY,
449 operation_emit_status_cb,
450 status_node_ref (node),
451 (GDestroyNotify) status_node_unref);
452 g_source_set_name_by_id (
453 node->source_id,
454 "[camel] operation_emit_status_cb");
455 }
456
457 UNLOCK ();
458 }
459
460 /**
461 * camel_operation_progress:
462 * @cancellable: a #GCancellable or %NULL
463 * @percent: percent complete, 0 to 100.
464 *
465 * Report progress on the current operation. @percent reports the current
466 * percentage of completion, which should be in the range of 0 to 100.
467 *
468 * This function only works if @cancellable is a #CamelOperation cast as a
469 * #GCancellable. If @cancellable is a plain #GCancellable or %NULL, the
470 * function does nothing and returns silently.
471 **/
472 void
camel_operation_progress(GCancellable * cancellable,gint percent)473 camel_operation_progress (GCancellable *cancellable,
474 gint percent)
475 {
476 CamelOperation *operation;
477 StatusNode *node;
478
479 if (cancellable == NULL)
480 return;
481
482 if (G_OBJECT_TYPE (cancellable) == G_TYPE_CANCELLABLE)
483 return;
484
485 g_return_if_fail (CAMEL_IS_OPERATION (cancellable));
486
487 operation = CAMEL_OPERATION (cancellable);
488
489 g_signal_emit (cancellable, signals[PROGRESS], 0, percent);
490
491 if (operation->priv->proxying)
492 camel_operation_progress (operation->priv->proxying, percent);
493
494 LOCK ();
495
496 node = g_queue_peek_head (&operation->priv->status_stack);
497
498 if (node != NULL) {
499 node->percent = percent;
500
501 /* Rate limit progress updates. */
502 if (node->source_id == 0) {
503 node->source_id = g_timeout_add_full (
504 G_PRIORITY_DEFAULT, PROGRESS_DELAY,
505 operation_emit_status_cb,
506 status_node_ref (node),
507 (GDestroyNotify) status_node_unref);
508 g_source_set_name_by_id (
509 node->source_id,
510 "[camel] operation_emit_status_cb");
511 }
512 }
513
514 UNLOCK ();
515 }
516