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