TnyIdleStopper
- From: Murray Cumming <murrayc murrayc com>
- To: tinymail-devel-list <tinymail-devel-list gnome org>
- Subject: TnyIdleStopper
- Date: Fri, 04 May 2007 12:09:24 +0200
As discussed on IRC, tinymail has a problem with the 2 refresh callbacks
given to tny_folder_refresh_async(). The status callback is sometimes
called after the main callback, because tinymail uses different
g_idle_add_full() priorities.
This causes modest to crash when quickly clicking on different folders.
This patch adds a generic TnyIdleStopper API that allows the main
callback to tell the status callback to stop. It uses a refcount to
decide when to release the shared gboolean (and the shared refcount). I
suspect that we should use it for other callbacks too, so I'd like to
put it in separeate .h/.c files.
I worry a bit that I am missing some simpler implementation but this API
at least looks quite straightforward to use. It seems to fix the crash
and the valgrind error.
--
Murray Cumming
murrayc murrayc com
www.murrayc.com
www.openismus.com
Index: libtinymail-camel/tny-camel-folder.c
===================================================================
--- libtinymail-camel/tny-camel-folder.c (revision 1884)
+++ libtinymail-camel/tny-camel-folder.c (working copy)
@@ -697,7 +697,143 @@
return;
}
+typedef struct _TnyIdleStopper TnyIdleStopper;
+/** TnyIdleStopper:
+ *
+ * This API can be used to allow 2 idle callbacks to cooperate,
+ * so that one idle callback (for instance the main callback)
+ * can tell the other one (for instance a repeated status/progress callback)
+ * to stop.
+ *
+ * This is necessary when the two idle callbacks have different priorities,
+ * meaning that the status/progress callback might be called after the main
+ * callback. The main callback would normally be expected to called last, and
+ * may therefore be the point at which other shared objects are finally
+ * released, meaning that their use in a further status/referesh callback might
+ * access invalid or released memory.
+ *
+ * Instantiate an TnyIdleStopper with tny_idle_stopper_new() and create a second shared
+ * instance with tny_idle_stopper_copy(). Calling tny_idle_stopper_stopped() on one
+ * instance will then cause tny_idle_stopper_is_stopped() to return TRUE for either
+ * instance.
+ * Call tny_idle_stopper_destroy() on each instance when you are finished with it.
+ */
+struct _TnyIdleStopper
+{
+ /* This is a pointer to a gboolean so we can share the value with both
+ * idle callbacks, so that each callback can stop the the other from calling
+ * the callbacks. */
+ gboolean* stopped;
+
+ /* This is a pointer an int so we can share a refcount,
+ * so we can release the shared stopped and ref_count only
+ * when nobody else needs to check stopped. */
+ gint* refcount;
+};
+
+/** tny_idle_stopper_new:
+ * @returns: A new TnyIdleStopper instance. Release this with tny_idle_stopper_destroy().
+ *
+ * Creates a new ref count object, with an initial ref count.
+ * Use tny_idle_stopper_copy() to create a second instance sharing
+ * the same underlying stopped status.
+ * You must call tny_idle_stopper_stop() on one of these instances,
+ * and call tny_idle_stopper_destroy() on all instances.
+ */
+TnyIdleStopper* tny_idle_stopper_new()
+{
+ TnyIdleStopper *result = g_new0(TnyIdleStopper, 1);
+
+ result->stopped = g_malloc0(sizeof(gboolean));
+ *(result->stopped) = FALSE;
+
+ result->refcount = g_malloc0(sizeof(gint));
+ *(result->refcount) = 1;
+
+ return result;
+}
+
+/** tny_idle_stopper_copy
+ * @stopper: The TnyIdleStopper instance to be shared.
+ *
+ * Create a shared copy of the stopper,
+ * so that you can call tny_idle_stopper_stop() on one instance,
+ * so that tny_idle_stopper_is_stopped() returns TRUE for ther other instance.
+ */
+TnyIdleStopper* tny_idle_stopper_copy (TnyIdleStopper *stopper)
+{
+ g_return_val_if_fail (stopper, NULL);
+ g_return_val_if_fail (stopper->refcount, NULL);
+
+ TnyIdleStopper *result = g_new0(TnyIdleStopper, 1);
+
+ /* Share the gboolean: */
+ result->stopped = stopper->stopped;
+
+ result->refcount = stopper->refcount;
+ ++(*(result->refcount));
+
+ return result;
+}
+
+/** tny_idle_stopper_stop:
+ * @stopper: The TnyIdleStopper instance.
+ *
+ * Call this to make tny_idle_stopper_is_stopped() return TRUE for all
+ * instances of the stopper.
+ */
+void tny_idle_stopper_stop (TnyIdleStopper *stopper)
+{
+ g_return_if_fail (stopper);
+ g_return_if_fail(stopper->stopped);
+ g_return_if_fail (stopper->refcount);
+
+ *(stopper->stopped) = TRUE;
+}
+
+/** tny_idle_stopper_destroy:
+ * @stopper: The TnyIdleStopper instance.
+ *
+ * Call this when you are sure that the callback will never be called again.
+ * For instance, in your GDestroyNotify callback provided to g_idle_add_full().
+ * Do not attempt to use @stopper after calling this.
+ */
+void tny_idle_stopper_destroy(TnyIdleStopper *stopper)
+{
+ g_return_if_fail (stopper);
+
+ --(*(stopper->refcount));
+
+ /* Free the shared variables if this is the last destroy: */
+ if(*(stopper->refcount) == 0) {
+ g_free (stopper->refcount);
+ stopper->refcount = 0;
+
+ g_free (stopper->stopped);
+ stopper->stopped = 0;
+ }
+
+ g_free (stopper);
+}
+
+/* tny_idle_stopper_is_stopped:
+ * @stopper: The TnyIdleStopper instance.
+ * @returns: Whether tny_idle_stopper_stop() has been called on one of the shared instances.
+ *
+ * This returns TRUE is tny_idle_stopper_stop() was called
+ * one one of the shared instances.
+ */
+gboolean tny_idle_stopper_is_stopped(TnyIdleStopper* stopper)
+{
+ g_return_val_if_fail(stopper, FALSE);
+ g_return_val_if_fail(stopper->stopped, FALSE);
+
+ /* If tny_idle_stopper_stop() was called,
+ * then pointer was freed and set to NULL: */
+ return *(stopper->stopped);
+}
+
typedef struct
{
TnyFolder *self;
@@ -705,13 +841,20 @@
TnyStatusCallback status_callback;
gpointer user_data;
gboolean cancelled;
+
+ /* This stops us from calling a status callback after the operation has
+ * finished. */
+ TnyIdleStopper* stopper;
+
guint depth;
GError *err;
TnySessionCamel *session;
} RefreshFolderInfo;
-
+/** This is the GDestroyNotify callback provided to g_idle_add_full()
+ * for tny_camel_folder_refresh_async_callback().
+ */
static void
tny_camel_folder_refresh_async_destroyer (gpointer thr_user_data)
{
@@ -728,6 +871,9 @@
_tny_session_stop_operation (info->session);
+ tny_idle_stopper_destroy (info->stopper);
+ info->stopper = NULL;
+
g_slice_free (RefreshFolderInfo, thr_user_data);
return;
@@ -744,6 +890,11 @@
if (info->callback)
info->callback (info->self, info->cancelled, &info->err, info->user_data);
+ /* Prevent status callbacks from being called after this
+ * (can happen because the 2 idle callbacks have different priorities)
+ * by causing tny_idle_stopper_is_stopped() to return TRUE. */
+ tny_idle_stopper_stop (info->stopper);
+
tny_folder_change_set_new_all_count (change, priv->cached_length);
tny_folder_change_set_new_unread_count (change, priv->unread_length);
notify_folder_observers_about (self, change);
@@ -771,6 +922,9 @@
g_object_unref (G_OBJECT (info->minfo->self));
g_free (info->what);
+ tny_idle_stopper_destroy (info->minfo->stopper);
+ info->minfo->stopper = NULL;
+
g_slice_free (RefreshFolderInfo, info->minfo);
g_slice_free (ProgressInfo, data);
@@ -782,7 +936,13 @@
{
ProgressInfo *info = data;
RefreshFolderInfo *minfo = info->minfo;
-
+
+ /* Do not call the status callback after the main callback
+ * has already been called, because we should assume that
+ * the user_data is invalid after that time: */
+ if (tny_idle_stopper_is_stopped (minfo->stopper))
+ return FALSE;
+
if (minfo && minfo->status_callback)
{
TnyStatus *status = tny_status_new (TNY_FOLDER_STATUS,
@@ -804,7 +964,7 @@
*
* This is non-public API documentation
*
- * The reason why we need to copy thist stuff is because it seems Camel has some
+ * The reason why we need to copy this stuff is because it seems Camel has some
* of its stuff allocated on the stack. When you do g_idle tricks, it's possible
* that by the time the g_idle happens, the stack allocation is already killed.
*
@@ -829,6 +989,11 @@
info->minfo->status_callback = oinfo->status_callback;
info->minfo->user_data = oinfo->user_data;
info->oftotal = oftotal;
+
+ /* Share the TnyIdleStopper, so one callback can tell the other to stop,
+ * because they may be called in an unexpected sequence: */
+ /* This is destroyed in the idle GDestroyNotify callback. */
+ info->minfo->stopper = tny_idle_stopper_copy(oinfo->stopper);
if (info->oftotal < 1)
info->oftotal = 1;
@@ -969,6 +1134,7 @@
return;
}
+ /* Idle info for the status callback: */
info = g_slice_new (RefreshFolderInfo);
info->session = TNY_FOLDER_PRIV_GET_SESSION (priv);
info->err = NULL;
@@ -977,14 +1143,22 @@
info->status_callback = status_callback;
info->user_data = user_data;
info->depth = g_main_depth ();
+
+ /* Use a ref count because we do not know which of the 2 idle callbacks
+ * will be the last, and we can only unref self in the last callback:
+ * This is destroyed in the idle GDestroyNotify callback.
+ */
+ info->stopper = tny_idle_stopper_new();
/* thread reference */
g_object_ref (G_OBJECT (self));
_tny_camel_folder_reason (priv);
+ /* This will cause the idle status callback to be called,
+ * via _tny_camel_account_start_camel_operation,
+ * and also calls the idle main callback: */
thread = g_thread_create (tny_camel_folder_refresh_async_thread,
info, FALSE, NULL);
-
return;
}
Index: libtinymail/tny-folder.c
===================================================================
--- libtinymail/tny-folder.c (revision 1884)
+++ libtinymail/tny-folder.c (working copy)
@@ -529,8 +529,8 @@
/**
* tny_folder_refresh_async:
* @self: a TnyFolder object
- * @callback: The callback handler
- * @status_callback: A callback for status notifications
+ * @callback: The callback handler. This is called last.
+ * @status_callback: A callback for status notifications. This is never called after @callback.
* @user_data: user data for the callback
*
* Refresh @self and call back when finished. This gets the summary information
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]