gem-graph-client/libide/threading/ide-task.c

2185 lines
58 KiB
C

/* ide-task.c
*
* Copyright 2018 Christian Hergert
*
* 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; either
* version 2.1 of the License, or (at your option) any later version.
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#define G_LOG_DOMAIN "ide-task"
#include "config.h"
#include <libide-core.h>
#include "ide-task.h"
#include "ide-thread-pool.h"
#include "ide-thread-private.h"
/* From GDK_PRIORITY_REDRAW */
#define PRIORITY_REDRAW (G_PRIORITY_HIGH_IDLE + 20)
#if 0
# define ENABLE_TIME_CHART
#endif
/**
* SECTION:ide-task
* @title: IdeTask
* @short_description: asynchronous task management
*
* #IdeTask is meant to be an improved form of #GTask. There are a few
* deficiencies in #GTask that have made it unsuitable for certain use cases.
*
* #GTask does not provide a way to guarantee that the source object,
* task data, and unused results are freed with in a given #GMainContext.
* #IdeTask addresses this by having a more flexible result and object
* ownership control.
*
* Furthermore, #IdeTask allows consumers to force disposal from a given
* thread so that the data is released there.
*
* #IdeTask also supports chaining tasks together which makes it simpler
* to avoid doing duplicate work by instead simply chaining the tasks.
*
* There are some costs to this design. It uses the main context a bit
* more than #GTask may use it.
*
* #IdeTask allows setting a task kind which determines which thread pool
* the task will be executed (and throttled) on.
*
* Because #IdeTask needs more control over result life-cycles (for chaining
* results), additional return methods have been provided. Consumers should
* use ide_task_return_boxed() when working with boxed types as it allows us
* to copy the result to another task. Additionally, ide_task_return_object()
* provides a simplified API over ide_task_return_pointer() which also allows
* copying the result to chained tasks.
*
* Since: 3.32
*/
typedef struct
{
/*
* The pointer we were provided.
*/
gpointer data;
/*
* The destroy notify for @data. We should only call this from the
* main context associated with the task.
*/
GDestroyNotify data_destroy;
} IdeTaskData;
typedef enum
{
IDE_TASK_RESULT_NONE,
IDE_TASK_RESULT_CANCELLED,
IDE_TASK_RESULT_BOOLEAN,
IDE_TASK_RESULT_INT,
IDE_TASK_RESULT_ERROR,
IDE_TASK_RESULT_OBJECT,
IDE_TASK_RESULT_BOXED,
IDE_TASK_RESULT_POINTER,
} IdeTaskResultType;
typedef struct
{
/*
* The type of result stored in our union @u.
*/
IdeTaskResultType type;
/*
* To ensure that we can pass ownership back to the main context
* from our worker thread, we need to be able to stash the reference
* here in our result. It is also convenient as we need access to it
* from the main context callback anyway.
*/
IdeTask *task;
/*
* Additionally, we need to allow passing our main context reference
* back so that it cannot be finalized in our thread.
*/
GMainContext *main_context;
/*
* Priority for our GSource attached to @main_context.
*/
gint complete_priority;
/*
* The actual result information, broken down by result @type.
*/
union {
gboolean v_bool;
gssize v_int;
GError *v_error;
GObject *v_object;
struct {
GType type;
gpointer pointer;
} v_boxed;
struct {
gpointer pointer;
GDestroyNotify destroy;
} v_pointer;
} u;
} IdeTaskResult;
typedef struct
{
IdeTask *task;
GMainContext *main_context;
gint priority;
} IdeTaskCancel;
typedef struct
{
/*
* @global_link is used to store a pointer to the task in the global
* queue during the lifetime of the task. This is a debugging feature
* so that we can dump the list of active tasks from the debugger.
*/
GList global_link;
/*
* Controls access to our private data. We only access structure
* data while holding this mutex to ensure that we have consistency
* between threads which could be accessing internals.
*/
GMutex mutex;
/*
* The source object for the GAsyncResult interface. If we have set
* release_on_propagate, this will be released when the task propagate
* function is called.
*/
gpointer source_object;
/*
* The cancellable that we're monitoring for task cancellation.
*/
GCancellable *cancellable;
/*
* If ide_task_set_return_on_cancel() has been set, then we might be
* listening for changes. Handling this will queue a completion
*/
gulong cancel_handler;
/*
* The callback to execute upon completion of the operation. It will
* be called from @main_contect after the operation completes.
*/
GAsyncReadyCallback callback;
gpointer user_data;
/*
* The name for the task. This string is interned so you should not
* use dynamic names. They are meant to simplify the process of
* debugging what task failed.
*/
const gchar *name;
/*
* The GMainContext that was the thread default when the task was
* created. Most operations are proxied back to this context so that
* the consumer does not need to worry about thread safety.
*/
GMainContext *main_context;
/*
* The task data that has been set for the task. Task data is released
* from a callback in the #GMainContext if changed outside the main
* context.
*/
IdeTaskData *task_data;
/*
* The result for the task. If release_on_propagate as set to %FALSE,
* then this may be kept around so that ide_task_chain() can be used to
* duplicate the result to another task. This is convenient when multiple
* async funcs race to do some work, allowing just a single winner with all
* the callers getting the same result.
*/
IdeTaskResult *result;
/*
* ide_task_chain() allows us to propagate the result of this task to
* another task (for a limited number of result types). This is the
* list of those tasks.
*/
GPtrArray *chained;
/*
* If ide_task_run_in_thread() is called, this will be set to the func
* that should be called from within the thread.
*/
IdeTaskThreadFunc thread_func;
/*
* If we're running in a thread, we'll stash the value here until we
* can complete things cleanly and pass ownership back as one operation.
*/
IdeTaskResult *thread_result;
/*
* The source tag for the task, which can be used to determine what
* the task is from a debugger as well as to verify correctness
* in async finish functions.
*/
gpointer source_tag;
#ifdef ENABLE_TIME_CHART
/* The time the task was created */
gint64 begin_time;
#endif
/*
* Our priority for scheduling tasks in the particular workqueue.
*/
gint priority;
/*
* The priority for completing the result back on the main context. This
* defaults to a value lower than gtk redraw priority to ensure that gtk
* has higher priority than task completion.
*/
gint complete_priority;
/*
* While we're waiting for our return callback, this is set to our
* source id. We use that to know we need to block on the main loop
* in case the user calls ide_task_propagate_*() synchronously without
* round-triping to the main loop.
*/
guint return_source;
/*
* Our kind of task, which is used to determine what thread pool we
* can use when running threaded work. This can be used to help choke
* lots of work down to a relatively small number of threads.
*/
IdeTaskKind kind : 8;
/*
* If the task has been completed, which is to say that the callback
* dispatch has occurred in @main_context.
*/
guint completed : 1;
/*
* If we should check @cancellable before returning the result. If set
* to true, and the cancellable was cancelled, an error will be returned
* even if the task completed successfully.
*/
guint check_cancellable : 1;
/*
* If we should synthesize completion from a GCancellable::cancelled
* event instead of waiting for the task to complete normally.
*/
guint return_on_cancel : 1;
/*
* If we should release the source object and task data after we've
* dispatched the callback (or the callback was NULL). This allows us
* to ensure that various dependent data are released in the main
* context. This is the default and helps ensure thread-safety.
*/
guint release_on_propagate : 1;
/*
* Protect against multiple return calls, and given the developer a good
* warning so they catch this early.
*/
guint return_called : 1;
/*
* If we got a result that was a cancellation, then we mark it here so
* that we can deal with it cleanly later.
*/
guint got_cancel : 1;
/*
* If we have dispatched to a thread already.
*/
guint thread_called : 1;
} IdeTaskPrivate;
static void async_result_init_iface (GAsyncResultIface *iface);
static void ide_task_data_free (IdeTaskData *task_data);
static void ide_task_result_free (IdeTaskResult *result);
static gboolean ide_task_return_cb (gpointer user_data);
static void ide_task_release (IdeTask *self,
gboolean force);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeTaskData, ide_task_data_free);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeTaskResult, ide_task_result_free);
G_DEFINE_TYPE_WITH_CODE (IdeTask, ide_task, G_TYPE_OBJECT,
G_ADD_PRIVATE (IdeTask)
G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_RESULT, async_result_init_iface))
enum {
PROP_0,
PROP_COMPLETED,
N_PROPS
};
static GParamSpec *properties [N_PROPS];
static GQueue global_task_list = G_QUEUE_INIT;
G_LOCK_DEFINE (global_task_list);
static void
ide_task_cancel_free (IdeTaskCancel *cancel)
{
g_clear_pointer (&cancel->main_context, g_main_context_unref);
g_clear_object (&cancel->task);
g_slice_free (IdeTaskCancel, cancel);
}
static const gchar *
result_type_name (IdeTaskResultType type)
{
switch (type)
{
case IDE_TASK_RESULT_NONE:
return "none";
case IDE_TASK_RESULT_CANCELLED:
return "cancelled";
case IDE_TASK_RESULT_INT:
return "int";
case IDE_TASK_RESULT_POINTER:
return "pointer";
case IDE_TASK_RESULT_OBJECT:
return "object";
case IDE_TASK_RESULT_BOXED:
return "boxed";
case IDE_TASK_RESULT_BOOLEAN:
return "boolean";
case IDE_TASK_RESULT_ERROR:
return "error";
default:
return NULL;
}
}
static void
ide_task_data_free (IdeTaskData *task_data)
{
if (task_data->data_destroy != NULL)
task_data->data_destroy (task_data->data);
g_slice_free (IdeTaskData, task_data);
}
static IdeTaskResult *
ide_task_result_copy (const IdeTaskResult *src)
{
IdeTaskResult *dst;
dst = g_slice_new0 (IdeTaskResult);
dst->type = src->type;
switch (src->type)
{
case IDE_TASK_RESULT_INT:
dst->u.v_int = src->u.v_int;
break;
case IDE_TASK_RESULT_BOOLEAN:
dst->u.v_bool = src->u.v_bool;
break;
case IDE_TASK_RESULT_ERROR:
dst->u.v_error = g_error_copy (src->u.v_error);
break;
case IDE_TASK_RESULT_OBJECT:
dst->u.v_object = src->u.v_object ? g_object_ref (src->u.v_object) : NULL;
break;
case IDE_TASK_RESULT_BOXED:
dst->u.v_boxed.type = src->u.v_boxed.type;
dst->u.v_boxed.pointer = g_boxed_copy (src->u.v_boxed.type, src->u.v_boxed.pointer);
break;
case IDE_TASK_RESULT_POINTER:
g_critical ("Cannot proxy raw pointers for task results");
break;
case IDE_TASK_RESULT_CANCELLED:
case IDE_TASK_RESULT_NONE:
default:
break;
}
return g_steal_pointer (&dst);
}
static void
ide_task_result_free (IdeTaskResult *result)
{
if (result == NULL)
return;
switch (result->type)
{
case IDE_TASK_RESULT_POINTER:
if (result->u.v_pointer.destroy)
result->u.v_pointer.destroy (result->u.v_pointer.pointer);
break;
case IDE_TASK_RESULT_ERROR:
g_error_free (result->u.v_error);
break;
case IDE_TASK_RESULT_BOXED:
if (result->u.v_boxed.pointer)
g_boxed_free (result->u.v_boxed.type, result->u.v_boxed.pointer);
break;
case IDE_TASK_RESULT_OBJECT:
g_clear_object (&result->u.v_object);
break;
case IDE_TASK_RESULT_BOOLEAN:
case IDE_TASK_RESULT_INT:
case IDE_TASK_RESULT_NONE:
case IDE_TASK_RESULT_CANCELLED:
default:
break;
}
g_clear_object (&result->task);
g_clear_pointer (&result->main_context, g_main_context_unref);
g_slice_free (IdeTaskResult, result);
}
/*
* ide_task_complete:
* @result: (transfer full): the result to complete
*
* queues the completion for the task. make sure that you've
* set the result->task, main_context, and priority first.
*
* This is designed to allow stealing the last reference from
* a worker thread and pass it back to the main context.
*
* Returns: a gsource identifier
*/
static guint
ide_task_complete (IdeTaskResult *result)
{
GSource *source;
guint ret;
g_assert (result != NULL);
g_assert (IDE_IS_TASK (result->task));
g_assert (result->main_context);
source = g_idle_source_new ();
g_source_set_name (source, "[ide-task] complete result");
g_source_set_ready_time (source, -1);
g_source_set_callback (source, ide_task_return_cb, result, NULL);
g_source_set_priority (source, result->complete_priority);
ret = g_source_attach (source, result->main_context);
g_source_unref (source);
return ret;
}
static void
ide_task_thread_func (gpointer data)
{
g_autoptr(GObject) source_object = NULL;
g_autoptr(GCancellable) cancellable = NULL;
g_autoptr(IdeTask) task = data;
IdeTaskPrivate *priv = ide_task_get_instance_private (task);
gpointer task_data = NULL;
IdeTaskThreadFunc thread_func;
g_assert (IDE_IS_TASK (task));
g_mutex_lock (&priv->mutex);
source_object = priv->source_object ? g_object_ref (priv->source_object) : NULL;
cancellable = priv->cancellable ? g_object_ref (priv->cancellable) : NULL;
if (priv->task_data)
task_data = priv->task_data->data;
thread_func = priv->thread_func;
priv->thread_func = NULL;
g_mutex_unlock (&priv->mutex);
g_assert (thread_func != NULL);
thread_func (task, source_object, task_data, cancellable);
g_clear_object (&source_object);
g_clear_object (&cancellable);
g_mutex_lock (&priv->mutex);
/*
* We've delayed our ide_task_return() until we reach here, so now
* we can steal our object instance and complete the task along with
* ensuring the object wont be finalized from this thread.
*/
if (priv->thread_result)
{
IdeTaskResult *result = g_steal_pointer (&priv->thread_result);
g_assert (result->task == task);
g_clear_object (&result->task);
result->task = g_steal_pointer (&task);
priv->return_source = ide_task_complete (g_steal_pointer (&result));
g_assert (source_object == NULL);
g_assert (cancellable == NULL);
g_assert (task == NULL);
}
else
{
/* The task did not return a value while in the thread func! GTask
* doesn't support this, but its useful to us in a number of ways, so
* we'll begrudgingly support it but the best we can do is drop our
* reference from the thread.
*/
}
g_mutex_unlock (&priv->mutex);
g_assert (source_object == NULL);
g_assert (cancellable == NULL);
}
static void
ide_task_dispose (GObject *object)
{
IdeTask *self = (IdeTask *)object;
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_assert (IDE_IS_TASK (self));
ide_task_release (self, TRUE);
g_mutex_lock (&priv->mutex);
g_clear_pointer (&priv->result, ide_task_result_free);
g_mutex_unlock (&priv->mutex);
G_OBJECT_CLASS (ide_task_parent_class)->dispose (object);
}
static void
ide_task_finalize (GObject *object)
{
IdeTask *self = (IdeTask *)object;
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
G_LOCK (global_task_list);
g_queue_unlink (&global_task_list, &priv->global_link);
G_UNLOCK (global_task_list);
if (!priv->return_called)
g_critical ("%s [%s] finalized before completing",
G_OBJECT_TYPE_NAME (self),
priv->name ?: "unnamed");
else if (priv->chained && priv->chained->len)
g_critical ("%s [%s] finalized before dependents were notified",
G_OBJECT_TYPE_NAME (self),
priv->name ?: "unnamed");
else if (priv->thread_func)
g_critical ("%s [%s] finalized while thread_func is active",
G_OBJECT_TYPE_NAME (self),
priv->name ?: "unnamed");
else if (!priv->completed)
g_critical ("%s [%s] finalized before completion",
G_OBJECT_TYPE_NAME (self),
priv->name ?: "unnamed");
g_assert (priv->return_source == 0);
g_assert (priv->result == NULL);
g_assert (priv->task_data == NULL);
g_assert (priv->source_object == NULL);
g_assert (priv->chained == NULL);
g_assert (priv->thread_result == NULL);
g_clear_pointer (&priv->main_context, g_main_context_unref);
g_clear_object (&priv->cancellable);
g_mutex_clear (&priv->mutex);
G_OBJECT_CLASS (ide_task_parent_class)->finalize (object);
}
static void
ide_task_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
IdeTask *self = IDE_TASK (object);
switch (prop_id)
{
case PROP_COMPLETED:
g_value_set_boolean (value, ide_task_get_completed (self));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
ide_task_class_init (IdeTaskClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->dispose = ide_task_dispose;
object_class->finalize = ide_task_finalize;
object_class->get_property = ide_task_get_property;
properties [PROP_COMPLETED] =
g_param_spec_boolean ("completed",
"Completed",
"If the task has completed",
FALSE,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (object_class, N_PROPS, properties);
/* This can be called multiple times, so we use this to allow
* unit tests to work without having to expose the function as
* public API.
*/
_ide_thread_pool_init (FALSE);
}
static void
ide_task_init (IdeTask *self)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_mutex_init (&priv->mutex);
priv->check_cancellable = TRUE;
priv->release_on_propagate = TRUE;
priv->priority = G_PRIORITY_DEFAULT;
priv->complete_priority = PRIORITY_REDRAW + 1;
priv->main_context = g_main_context_ref_thread_default ();
priv->global_link.data = self;
G_LOCK (global_task_list);
g_queue_push_tail_link (&global_task_list, &priv->global_link);
G_UNLOCK (global_task_list);
}
/**
* ide_task_get_source_object: (skip)
* @self: a #IdeTask
*
* Gets the #GObject used when creating the source object.
*
* As this does not provide ownership transfer of the #GObject, it is a
* programmer error to call this function outside of a thread worker called
* from ide_task_run_in_thread() or outside the #GMainContext that is
* associated with the task.
*
* If you need to access the object in other scenarios, you must use the
* g_async_result_get_source_object() which provides a full reference to the
* source object, safely. You are responsible for ensuring that you do not
* release the object in a manner that is unsafe for the source object.
*
* Returns: (transfer none) (nullable) (type GObject.Object): a #GObject or %NULL
*
* Since: 3.32
*/
gpointer
ide_task_get_source_object (IdeTask *self)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
gpointer ret;
g_return_val_if_fail (IDE_IS_TASK (self), NULL);
g_mutex_lock (&priv->mutex);
ret = priv->source_object;
g_mutex_unlock (&priv->mutex);
return ret;
}
/**
* ide_task_new:
* @source_object: (type GObject.Object) (nullable): a #GObject or %NULL
* @cancellable: (nullable): a #GCancellable or %NULL
* @callback: (scope async) (nullable): a #GAsyncReadyCallback or %NULL
* @user_data: closure data for @callback
*
* Creates a new #IdeTask.
*
* #IdeTask is similar to #GTask but provides some additional guarantees
* such that by default, the source object, task data, and unused results
* are guaranteed to be finalized in the #GMainContext associated with
* the task itself.
*
* Returns: (transfer full): an #IdeTask
*
* Since: 3.32
*/
IdeTask *
(ide_task_new) (gpointer source_object,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
g_autoptr(IdeTask) self = NULL;
IdeTaskPrivate *priv;
g_return_val_if_fail (!source_object || G_IS_OBJECT (source_object), NULL);
g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), NULL);
self = g_object_new (IDE_TYPE_TASK, NULL);
priv = ide_task_get_instance_private (self);
priv->source_object = source_object ? g_object_ref (source_object) : NULL;
priv->cancellable = cancellable ? g_object_ref (cancellable) : NULL;
priv->callback = callback;
priv->user_data = user_data;
#ifdef ENABLE_TIME_CHART
priv->begin_time = g_get_monotonic_time ();
#endif
return g_steal_pointer (&self);
}
/**
* ide_task_is_valid:
* @self: (nullable) (type IdeTask): a #IdeTask
* @source_object: (nullable): a #GObject or %NULL
*
* Checks if @source_object matches the object the task was created with.
*
* Returns: %TRUE is source_object matches
*
* Since: 3.32
*/
gboolean
ide_task_is_valid (gpointer self,
gpointer source_object)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
return IDE_IS_TASK (self) && priv->source_object == source_object;
}
/**
* ide_task_get_completed:
* @self: a #IdeTask
*
* Gets the "completed" property. This is %TRUE after the callback used when
* creating the task has been executed.
*
* The property will be notified using g_object_notify() exactly once in the
* same #GMainContext as the callback.
*
* Returns: %TRUE if the task has completed
*
* Since: 3.32
*/
gboolean
ide_task_get_completed (IdeTask *self)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
gboolean ret;
g_return_val_if_fail (IDE_IS_TASK (self), FALSE);
g_mutex_lock (&priv->mutex);
ret = priv->completed;
g_mutex_unlock (&priv->mutex);
return ret;
}
gint
ide_task_get_priority (IdeTask *self)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
gint ret;
g_return_val_if_fail (IDE_IS_TASK (self), 0);
g_mutex_lock (&priv->mutex);
ret = priv->priority;
g_mutex_unlock (&priv->mutex);
return ret;
}
void
ide_task_set_priority (IdeTask *self,
gint priority)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_return_if_fail (IDE_IS_TASK (self));
g_mutex_lock (&priv->mutex);
priv->priority = priority;
g_mutex_unlock (&priv->mutex);
}
gint
ide_task_get_complete_priority (IdeTask *self)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
gint ret;
g_return_val_if_fail (IDE_IS_TASK (self), 0);
g_mutex_lock (&priv->mutex);
ret = priv->complete_priority;
g_mutex_unlock (&priv->mutex);
return ret;
}
void
ide_task_set_complete_priority (IdeTask *self,
gint complete_priority)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_return_if_fail (IDE_IS_TASK (self));
g_mutex_lock (&priv->mutex);
priv->complete_priority = complete_priority;
g_mutex_unlock (&priv->mutex);
}
/**
* ide_task_get_cancellable:
* @self: a #IdeTask
*
* Gets the #GCancellable for the task.
*
* Returns: (transfer none) (nullable): a #GCancellable or %NULL
*
* Since: 3.32
*/
GCancellable *
ide_task_get_cancellable (IdeTask *self)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_return_val_if_fail (IDE_IS_TASK (self), NULL);
return priv->cancellable;
}
static void
ide_task_deliver_result (IdeTask *self,
IdeTaskResult *result)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_assert (IDE_IS_TASK (self));
g_assert (result != NULL);
g_assert (result->task == NULL);
g_assert (result->main_context == NULL);
/* This task was chained from another task. This completes the result
* and we should dispatch the callback. To simplify the dispatching and
* help prevent any re-entrancy issues, we defer back to the main context
* to complete the operation.
*/
result->task = g_object_ref (self);
result->main_context = g_main_context_ref (priv->main_context);
result->complete_priority = priv->complete_priority;
g_mutex_lock (&priv->mutex);
priv->return_called = TRUE;
priv->return_source = ide_task_complete (g_steal_pointer (&result));
g_mutex_unlock (&priv->mutex);
}
static void
ide_task_release (IdeTask *self,
gboolean force)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_autoptr(IdeTaskData) task_data = NULL;
g_autoptr(GObject) source_object = NULL;
g_autoptr(GPtrArray) chained = NULL;
g_assert (IDE_IS_TASK (self));
g_mutex_lock (&priv->mutex);
if (force || priv->release_on_propagate)
{
source_object = g_steal_pointer (&priv->source_object);
task_data = g_steal_pointer (&priv->task_data);
chained = g_steal_pointer (&priv->chained);
}
g_mutex_unlock (&priv->mutex);
if (chained)
{
for (guint i = 0; i < chained->len; i++)
{
IdeTask *task = g_ptr_array_index (chained, i);
ide_task_return_new_error (task,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"Error synthesized for task, parent task disposed");
}
}
}
static gboolean
ide_task_return_cb (gpointer user_data)
{
g_autoptr(IdeTask) self = NULL;
g_autoptr(IdeTaskResult) result = user_data;
g_autoptr(IdeTaskResult) result_copy = NULL;
g_autoptr(GCancellable) cancellable = NULL;
g_autoptr(GObject) source_object = NULL;
g_autoptr(GPtrArray) chained = NULL;
GAsyncReadyCallback callback = NULL;
gpointer callback_data = NULL;
IdeTaskPrivate *priv;
g_assert (result != NULL);
g_assert (IDE_IS_TASK (result->task));
/* We steal the task object, because we only stash it in the result
* structure to get it here. And if we held onto it, we would have
* a reference cycle.
*/
self = g_steal_pointer (&result->task);
priv = ide_task_get_instance_private (self);
#ifdef ENABLE_TIME_CHART
g_message ("TASK-END: %s: duration=%lf",
priv->name,
(g_get_monotonic_time () - priv->begin_time) / (gdouble)G_USEC_PER_SEC);
#endif
g_mutex_lock (&priv->mutex);
g_assert (priv->return_source != 0);
priv->return_source = 0;
if (priv->got_cancel && priv->result != NULL)
{
/* We can discard this since we already handled a result for the
* task. We delivered this here just so that we could finalize
* any objects back inside them main context.
*/
g_mutex_unlock (&priv->mutex);
return G_SOURCE_REMOVE;
}
g_assert (priv->result == NULL);
g_assert (priv->return_called == TRUE);
priv->result = g_steal_pointer (&result);
callback = priv->callback;
callback_data = priv->user_data;
priv->callback = NULL;
priv->user_data = NULL;
source_object = priv->source_object ? g_object_ref (priv->source_object) : NULL;
cancellable = priv->cancellable ? g_object_ref (priv->cancellable) : NULL;
chained = g_steal_pointer (&priv->chained);
/* Make a private copy of the result data if we're going to need to notify
* other tasks of our result. We can't guarantee the result in @task will
* stay alive during our dispatch callbacks, so we need to have a copy.
*/
if (chained != NULL && chained->len > 0)
result_copy = ide_task_result_copy (priv->result);
g_mutex_unlock (&priv->mutex);
if (callback)
callback (source_object, G_ASYNC_RESULT (self), callback_data);
if (chained)
{
for (guint i = 0; i < chained->len; i++)
{
IdeTask *other = g_ptr_array_index (chained, i);
g_autoptr(IdeTaskResult) other_result = ide_task_result_copy (result_copy);
ide_task_deliver_result (other, g_steal_pointer (&other_result));
}
}
g_mutex_lock (&priv->mutex);
priv->completed = TRUE;
g_mutex_unlock (&priv->mutex);
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_COMPLETED]);
ide_task_release (self, FALSE);
return G_SOURCE_REMOVE;
}
static gboolean
ide_task_return_dummy_cb (gpointer data)
{
return G_SOURCE_REMOVE;
}
static void
ide_task_return (IdeTask *self,
IdeTaskResult *result)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_autoptr(GMutexLocker) locker = NULL;
g_assert (IDE_IS_TASK (self));
g_assert (result != NULL);
g_assert (result->task == NULL);
locker = g_mutex_locker_new (&priv->mutex);
if (priv->cancel_handler && priv->cancellable)
{
g_cancellable_disconnect (priv->cancellable, priv->cancel_handler);
priv->cancel_handler = 0;
}
if (priv->return_called)
{
GSource *source;
if (result->type == IDE_TASK_RESULT_CANCELLED)
{
/* We already had a result, and now raced to be notified of
* cancellation. We can safely free this result even if we're
* currently in a worker thread.
*/
ide_task_result_free (result);
return;
}
/* If we haven't been cancelled, then we reached this path multiple
* times by programmer error.
*/
if (!priv->got_cancel)
g_critical ("Attempted to set result on task [%s] multiple times", priv->name);
/*
* This task has already returned, but we need to ensure that we pass
* the data back to the main context so that it is freed appropriately.
*/
source = g_idle_source_new ();
g_source_set_name (source, "[ide-task] finalize task result");
g_source_set_ready_time (source, -1);
g_source_set_callback (source,
ide_task_return_dummy_cb,
result,
(GDestroyNotify)ide_task_result_free);
g_source_attach (source, priv->main_context);
g_source_unref (source);
return;
}
priv->return_called = TRUE;
if (result->type == IDE_TASK_RESULT_CANCELLED)
priv->got_cancel = TRUE;
result->task = g_object_ref (self);
result->main_context = g_main_context_ref (priv->main_context);
result->complete_priority = priv->complete_priority;
/* We can queue the result immediately if we're not being called
* while we're inside of a ide_task_run_in_thread() callback. Otherwise,
* that thread cleanup must complete this to ensure objects cannot
* be finalized in that thread.
*/
if (!priv->thread_called || IDE_IS_MAIN_THREAD ())
priv->return_source = ide_task_complete (result);
else if (priv->return_on_cancel && result->type == IDE_TASK_RESULT_CANCELLED)
priv->return_source = ide_task_complete (result);
else
priv->thread_result = result;
}
/**
* ide_task_return_int:
* @self: a #IdeTask
* @result: the result for the task
*
* Sets the result of the task to @result.
*
* Other tasks depending on the result will be notified after returning
* to the #GMainContext of the task.
*
* Since: 3.32
*/
void
ide_task_return_int (IdeTask *self,
gssize result)
{
IdeTaskResult *ret;
g_return_if_fail (IDE_IS_TASK (self));
ret = g_slice_new0 (IdeTaskResult);
ret->type = IDE_TASK_RESULT_INT;
ret->u.v_int = result;
ide_task_return (self, g_steal_pointer (&ret));
}
/**
* ide_task_return_boolean:
* @self: a #IdeTask
* @result: the result for the task
*
* Sets the result of the task to @result.
*
* Other tasks depending on the result will be notified after returning
* to the #GMainContext of the task.
*
* Since: 3.32
*/
void
ide_task_return_boolean (IdeTask *self,
gboolean result)
{
IdeTaskResult *ret;
g_return_if_fail (IDE_IS_TASK (self));
ret = g_slice_new0 (IdeTaskResult);
ret->type = IDE_TASK_RESULT_BOOLEAN;
ret->u.v_bool = !!result;
ide_task_return (self, g_steal_pointer (&ret));
}
/**
* ide_task_return_boxed: (skip)
* @self: a #IdeTask
* @result_type: the #GType of the boxed type
* @result: (transfer full): the result to be returned
*
* This is similar to ide_task_return_pointer(), but allows the task to
* know the boxed #GType so that the result may be propagated to chained
* tasks.
*
* Since: 3.32
*/
void
ide_task_return_boxed (IdeTask *self,
GType result_type,
gpointer result)
{
IdeTaskResult *ret;
g_return_if_fail (IDE_IS_TASK (self));
g_return_if_fail (result_type != G_TYPE_INVALID);
g_return_if_fail (G_TYPE_IS_BOXED (result_type));
ret = g_slice_new0 (IdeTaskResult);
ret->type = IDE_TASK_RESULT_BOXED;
ret->u.v_boxed.type = result_type;
ret->u.v_boxed.pointer = result;
ide_task_return (self, g_steal_pointer (&ret));
}
/**
* ide_task_return_object:
* @self: a #IdeTask
* @instance: (transfer full) (type GObject.Object): a #GObject instance
*
* Returns a new object instance.
*
* Takes ownership of @instance to allow saving a reference increment and
* decrement by the caller.
*
* Since: 3.32
*/
void
ide_task_return_object (IdeTask *self,
gpointer instance)
{
IdeTaskResult *ret;
g_return_if_fail (IDE_IS_TASK (self));
g_return_if_fail (!instance || G_IS_OBJECT (instance));
ret = g_slice_new0 (IdeTaskResult);
ret->type = IDE_TASK_RESULT_OBJECT;
ret->u.v_object = instance;
ide_task_return (self, g_steal_pointer (&ret));
}
/**
* ide_task_return_pointer: (skip)
* @self: a #IdeTask
* @data: the data to return
* @destroy: an optional #GDestroyNotify to cleanup data if no handler
* propagates the result
*
* Returns a new raw pointer.
*
* Note that pointers cannot be chained to other tasks, so you may not
* use ide_task_chain() in conjunction with a task returning a pointer
* using ide_task_return_pointer().
*
* If you need task chaining with pointers, see ide_task_return_boxed()
* or ide_task_return_object().
*
* Since: 3.32
*/
void
(ide_task_return_pointer) (IdeTask *self,
gpointer data,
GDestroyNotify destroy)
{
IdeTaskResult *ret;
g_return_if_fail (IDE_IS_TASK (self));
ret = g_slice_new0 (IdeTaskResult);
ret->type = IDE_TASK_RESULT_POINTER;
ret->u.v_pointer.pointer = data;
ret->u.v_pointer.destroy = destroy;
ide_task_return (self, g_steal_pointer (&ret));
}
/**
* ide_task_return_error:
* @self: a #IdeTask
* @error: (transfer full): a #GError
*
* Sets @error as the result of the #IdeTask
*
* Since: 3.32
*/
void
ide_task_return_error (IdeTask *self,
GError *error)
{
IdeTaskResult *ret;
g_return_if_fail (IDE_IS_TASK (self));
ret = g_slice_new0 (IdeTaskResult);
ret->type = IDE_TASK_RESULT_ERROR;
ret->u.v_error = error;
ide_task_return (self, g_steal_pointer (&ret));
}
/**
* ide_task_return_new_error:
* @self: a #IdeTask
* @error_domain: the error domain of the #GError
* @error_code: the error code for the #GError
* @format: the printf-style format string
*
* Creates a new #GError and sets it as the result for the task.
*
* Since: 3.32
*/
void
ide_task_return_new_error (IdeTask *self,
GQuark error_domain,
gint error_code,
const gchar *format,
...)
{
GError *error;
va_list args;
g_return_if_fail (IDE_IS_TASK (self));
va_start (args, format);
error = g_error_new_valist (error_domain, error_code, format, args);
va_end (args);
ide_task_return_error (self, g_steal_pointer (&error));
}
/**
* ide_task_return_error_if_cancelled:
* @self: a #IdeTask
*
* Returns a new #GError if the cancellable associated with the task
* has been cancelled. If so, %TRUE is returned, otherwise %FALSE.
*
* If the source object related to the task is an #IdeObject and that
* object has had been requested to destroy, it too will be considered
* a cancellation state.
*
* Returns: %TRUE if the task was cancelled and error returned.
*
* Since: 3.32
*/
gboolean
ide_task_return_error_if_cancelled (IdeTask *self)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
gboolean failed;
g_return_val_if_fail (IDE_IS_TASK (self), FALSE);
g_mutex_lock (&priv->mutex);
failed = g_cancellable_is_cancelled (priv->cancellable) ||
(IDE_IS_OBJECT (priv->source_object) &&
ide_object_in_destruction (IDE_OBJECT (priv->source_object)));
g_mutex_unlock (&priv->mutex);
if (failed)
ide_task_return_new_error (self,
G_IO_ERROR,
G_IO_ERROR_CANCELLED,
"The task was cancelled");
return failed;
}
/**
* ide_task_set_release_on_propagate:
* @self: a #IdeTask
* @release_on_propagate: if data should be released on propagate
*
* Setting this to %TRUE (the default) ensures that the task will release all
* task data and source_object references after executing the configured
* callback. This is useful to ensure that dependent objects are finalized
* in the thread-default #GMainContext the task was created in.
*
* Generally, you want to leave this as %TRUE to ensure thread-safety on the
* dependent objects and task data.
*
* Since: 3.32
*/
void
ide_task_set_release_on_propagate (IdeTask *self,
gboolean release_on_propagate)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_return_if_fail (IDE_IS_TASK (self));
release_on_propagate = !!release_on_propagate;
g_mutex_lock (&priv->mutex);
priv->release_on_propagate = release_on_propagate;
g_mutex_unlock (&priv->mutex);
}
/**
* ide_task_set_source_tag:
* @self: a #IdeTask
* @source_tag: a tag to identify the task, usual a function pointer
*
* Sets the source tag for the task. Generally this is a function pointer
* of the function that created the task.
*
* Since: 3.32
*/
void
ide_task_set_source_tag (IdeTask *self,
gpointer source_tag)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_return_if_fail (IDE_IS_TASK (self));
g_mutex_lock (&priv->mutex);
priv->source_tag = source_tag;
g_mutex_unlock (&priv->mutex);
}
/**
* ide_task_set_check_cancellable:
* @self: a #IdeTask
* @check_cancellable: %TRUE if the cancellable should be checked
*
* Setting @check_cancellable to %TRUE (the default) ensures that the
* #GCancellable used when creating the #IdeTask is checked for cancellation
* before propagating a result. If cancelled, an error will be returned
* instead of the result.
*
* Since: 3.32
*/
void
ide_task_set_check_cancellable (IdeTask *self,
gboolean check_cancellable)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_return_if_fail (IDE_IS_TASK (self));
check_cancellable = !!check_cancellable;
g_mutex_lock (&priv->mutex);
priv->check_cancellable = check_cancellable;
g_mutex_unlock (&priv->mutex);
}
/**
* ide_task_run_in_thread: (skip)
* @self: a #IdeTask
* @thread_func: a function to execute on a worker thread
*
* Scheules @thread_func to be executed on a worker thread.
*
* @thread_func must complete the task from the worker thread using one of
* ide_task_return_boolean(), ide_task_return_int(), or
* ide_task_return_pointer().
*
* Since: 3.32
*/
void
ide_task_run_in_thread (IdeTask *self,
IdeTaskThreadFunc thread_func)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_autoptr(GError) error = NULL;
g_return_if_fail (IDE_IS_TASK (self));
g_return_if_fail (thread_func != NULL);
g_mutex_lock (&priv->mutex);
if (priv->completed == TRUE)
{
g_critical ("Task already completed, cannot run in thread");
goto unlock;
}
if (priv->thread_called)
{
g_critical ("Run in thread already called, cannot run again");
goto unlock;
}
priv->thread_called = TRUE;
priv->thread_func = thread_func;
ide_thread_pool_push_with_priority ((IdeThreadPoolKind)priv->kind,
priv->priority,
ide_task_thread_func,
g_object_ref (self));
unlock:
g_mutex_unlock (&priv->mutex);
if (error != NULL)
ide_task_return_error (self, g_steal_pointer (&error));
}
static IdeTaskResult *
ide_task_propagate_locked (IdeTask *self,
IdeTaskResultType expected_type,
GError **error)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
IdeTaskResult *ret = NULL;
g_assert (IDE_IS_TASK (self));
g_assert (expected_type > IDE_TASK_RESULT_NONE);
if (priv->result == NULL)
{
g_autoptr(GMainContext) context = g_main_context_ref (priv->main_context);
while (priv->return_source)
{
g_mutex_unlock (&priv->mutex);
g_main_context_iteration (context, FALSE);
g_mutex_lock (&priv->mutex);
}
}
if (priv->result == NULL)
{
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"No result available for task");
}
else if (priv->result->type == IDE_TASK_RESULT_ERROR)
{
if (error != NULL)
*error = g_error_copy (priv->result->u.v_error);
}
else if ((priv->check_cancellable && g_cancellable_is_cancelled (priv->cancellable)) ||
priv->result->type == IDE_TASK_RESULT_CANCELLED)
{
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_CANCELLED,
"The operation was cancelled");
}
else if (IDE_IS_OBJECT (priv->source_object) &&
ide_object_in_destruction (IDE_OBJECT (priv->source_object)))
{
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_CANCELLED,
"The object was destroyed while the task executed");
}
else if (priv->result->type != expected_type)
{
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"Task expected result of %s got %s",
result_type_name (expected_type),
result_type_name (priv->result->type));
}
else
{
g_assert (priv->result != NULL);
g_assert (priv->result->type == expected_type);
if (priv->release_on_propagate)
ret = g_steal_pointer (&priv->result);
else if (priv->result->type == IDE_TASK_RESULT_POINTER)
ret = g_steal_pointer (&priv->result);
else
ret = ide_task_result_copy (priv->result);
g_assert (ret != NULL);
}
return ret;
}
gboolean
ide_task_propagate_boolean (IdeTask *self,
GError **error)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_autoptr(GMutexLocker) locker = NULL;
g_autoptr(IdeTaskResult) res = NULL;
g_return_val_if_fail (IDE_IS_TASK (self), FALSE);
locker = g_mutex_locker_new (&priv->mutex);
if (!(res = ide_task_propagate_locked (self, IDE_TASK_RESULT_BOOLEAN, error)))
return FALSE;
return res->u.v_bool;
}
gpointer
ide_task_propagate_boxed (IdeTask *self,
GError **error)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_autoptr(GMutexLocker) locker = NULL;
g_autoptr(IdeTaskResult) res = NULL;
g_return_val_if_fail (IDE_IS_TASK (self), NULL);
locker = g_mutex_locker_new (&priv->mutex);
if (!(res = ide_task_propagate_locked (self, IDE_TASK_RESULT_BOXED, error)))
return NULL;
return g_boxed_copy (res->u.v_boxed.type, res->u.v_boxed.pointer);
}
gssize
ide_task_propagate_int (IdeTask *self,
GError **error)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_autoptr(GMutexLocker) locker = NULL;
g_autoptr(IdeTaskResult) res = NULL;
g_return_val_if_fail (IDE_IS_TASK (self), 0);
locker = g_mutex_locker_new (&priv->mutex);
if (!(res = ide_task_propagate_locked (self, IDE_TASK_RESULT_INT, error)))
return 0;
return res->u.v_int;
}
/**
* ide_task_propagate_object:
* @self: a #IdeTask
* @error: a location for a #GError, or %NULL
*
* Returns an object if the task completed with an object. Otherwise, %NULL
* is returned.
*
* @error is set if the task completed with an error.
*
* Returns: (transfer full) (type GObject.Object): a #GObject or %NULL
* and @error may be set.
*
* Since: 3.32
*/
gpointer
ide_task_propagate_object (IdeTask *self,
GError **error)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_autoptr(GMutexLocker) locker = NULL;
g_autoptr(IdeTaskResult) res = NULL;
g_return_val_if_fail (IDE_IS_TASK (self), NULL);
locker = g_mutex_locker_new (&priv->mutex);
if (!(res = ide_task_propagate_locked (self, IDE_TASK_RESULT_OBJECT, error)))
return NULL;
return g_steal_pointer (&res->u.v_object);
}
gpointer
ide_task_propagate_pointer (IdeTask *self,
GError **error)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_autoptr(GMutexLocker) locker = NULL;
g_autoptr(IdeTaskResult) res = NULL;
gpointer ret;
g_return_val_if_fail (IDE_IS_TASK (self), NULL);
locker = g_mutex_locker_new (&priv->mutex);
if (!(res = ide_task_propagate_locked (self, IDE_TASK_RESULT_POINTER, error)))
return NULL;
ret = g_steal_pointer (&res->u.v_pointer.pointer);
res->u.v_pointer.destroy = NULL;
return ret;
}
/**
* ide_task_chain:
* @self: a #IdeTask
* @other_task: a #IdeTask
*
* Causes the result of @self to also be delivered to @other_task.
*
* This API is useful in situations when you want to avoid doing the same
* work multiple times, and can share the result between mutliple async
* operations requesting the same work.
*
* Users of this API must make sure one of two things is true. Either they
* have called ide_task_set_release_on_propagate() with @self and set
* release_on_propagate to %FALSE, or @self has not yet completed.
*
* Since: 3.32
*/
void
ide_task_chain (IdeTask *self,
IdeTask *other_task)
{
IdeTaskPrivate *self_priv = ide_task_get_instance_private (self);
g_return_if_fail (IDE_IS_TASK (self));
g_return_if_fail (IDE_IS_TASK (other_task));
g_return_if_fail (self != other_task);
g_mutex_lock (&self_priv->mutex);
if (self_priv->result)
{
IdeTaskResult *copy = ide_task_result_copy (self_priv->result);
if (copy != NULL)
ide_task_deliver_result (other_task, g_steal_pointer (&copy));
else
ide_task_return_new_error (other_task,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"Result could not be copied to task");
}
else
{
if (self_priv->chained == NULL)
self_priv->chained = g_ptr_array_new_with_free_func (g_object_unref);
g_ptr_array_add (self_priv->chained, g_object_ref (other_task));
}
g_mutex_unlock (&self_priv->mutex);
}
gpointer
ide_task_get_source_tag (IdeTask *self)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
gpointer ret;
g_return_val_if_fail (IDE_IS_TASK (self), NULL);
g_mutex_lock (&priv->mutex);
ret = priv->source_tag;
g_mutex_unlock (&priv->mutex);
return ret;
}
IdeTaskKind
ide_task_get_kind (IdeTask *self)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
IdeTaskKind kind;
g_return_val_if_fail (IDE_IS_TASK (self), 0);
g_mutex_lock (&priv->mutex);
kind = priv->kind;
g_mutex_unlock (&priv->mutex);
return kind;
}
void
ide_task_set_kind (IdeTask *self,
IdeTaskKind kind)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_return_if_fail (IDE_IS_TASK (self));
g_return_if_fail (kind >= IDE_TASK_KIND_DEFAULT);
g_return_if_fail (kind < IDE_TASK_KIND_LAST);
g_mutex_lock (&priv->mutex);
priv->kind = kind;
g_mutex_unlock (&priv->mutex);
}
/**
* ide_task_get_task_data: (skip)
* @self: a #IdeTask
*
* Gets the task data previously set with ide_task_set_task_data().
*
* Returns: (transfer none): previously registered task data or %NULL
*
* Since: 3.32
*/
gpointer
ide_task_get_task_data (IdeTask *self)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
gpointer task_data = NULL;
g_assert (IDE_IS_TASK (self));
g_mutex_lock (&priv->mutex);
if (priv->task_data)
task_data = priv->task_data->data;
g_mutex_unlock (&priv->mutex);
return task_data;
}
static gboolean
ide_task_set_task_data_cb (gpointer data)
{
IdeTaskData *task_data = data;
ide_task_data_free (task_data);
return G_SOURCE_REMOVE;
}
void
(ide_task_set_task_data) (IdeTask *self,
gpointer task_data,
GDestroyNotify task_data_destroy)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_autoptr(IdeTaskData) old_task_data = NULL;
g_autoptr(IdeTaskData) new_task_data = NULL;
g_return_if_fail (IDE_IS_TASK (self));
new_task_data = g_slice_new0 (IdeTaskData);
new_task_data->data = task_data;
new_task_data->data_destroy = task_data_destroy;
g_mutex_lock (&priv->mutex);
if (priv->return_called)
{
g_critical ("Cannot set task data after returning value");
goto unlock;
}
old_task_data = g_steal_pointer (&priv->task_data);
priv->task_data = g_steal_pointer (&new_task_data);
if (priv->thread_called && old_task_data)
{
GSource *source;
source = g_idle_source_new ();
g_source_set_name (source, "[ide-task] finalize task data");
g_source_set_ready_time (source, -1);
g_source_set_callback (source,
ide_task_set_task_data_cb,
NULL, NULL);
g_source_set_priority (source, priv->priority);
g_source_attach (source, priv->main_context);
g_source_unref (source);
}
unlock:
g_mutex_unlock (&priv->mutex);
}
static gboolean
ide_task_cancel_cb (gpointer user_data)
{
IdeTask *self = user_data;
IdeTaskResult *ret;
g_assert (IDE_IS_TASK (self));
ret = g_slice_new0 (IdeTaskResult);
ret->type = IDE_TASK_RESULT_CANCELLED;
ide_task_return (self, g_steal_pointer (&ret));
return G_SOURCE_REMOVE;
}
static void
ide_task_cancellable_cancelled_cb (GCancellable *cancellable,
IdeTaskCancel *cancel)
{
GSource *source;
g_assert (G_IS_CANCELLABLE (cancellable));
g_assert (cancel != NULL);
g_assert (IDE_IS_TASK (cancel->task));
g_assert (cancel->main_context != NULL);
/*
* This can be called synchronously from g_cancellable_connect(), which
* could still be holding priv->mutex. So we need to queue the cancellation
* request back through the main context.
*/
source = g_idle_source_new ();
g_source_set_name (source, "[ide-task] cancel task");
g_source_set_ready_time (source, -1);
g_source_set_callback (source, ide_task_cancel_cb, g_object_ref (cancel->task), g_object_unref);
g_source_set_priority (source, cancel->priority);
g_source_attach (source, cancel->main_context);
g_source_unref (source);
}
/**
* ide_task_get_return_on_cancel:
* @self: a #IdeTask
*
* Gets the return_on_cancel value, which means the task will return
* immediately when the #GCancellable is cancelled.
*
* Since: 3.32
*/
gboolean
ide_task_get_return_on_cancel (IdeTask *self)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
gboolean ret;
g_return_val_if_fail (IDE_IS_TASK (self), FALSE);
g_mutex_lock (&priv->mutex);
ret = priv->return_on_cancel;
g_mutex_unlock (&priv->mutex);
return ret;
}
/**
* ide_task_set_return_on_cancel:
* @self: a #IdeTask
* @return_on_cancel: if the task should return immediately when the
* #GCancellable has been cancelled.
*
* Setting @return_on_cancel to %TRUE ensures that the task will cancel
* immediately when #GCancellable::cancelled is emitted by the configured
* cancellable.
*
* Setting this requires that the caller can ensure the configured #GMainContext
* will outlive the threaded worker so that task state can be freed in a delayed
* fashion.
*
* Since: 3.32
*/
void
ide_task_set_return_on_cancel (IdeTask *self,
gboolean return_on_cancel)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_autoptr(GMutexLocker) locker = NULL;
g_return_if_fail (IDE_IS_TASK (self));
locker = g_mutex_locker_new (&priv->mutex);
if (priv->cancellable == NULL)
return;
return_on_cancel = !!return_on_cancel;
if (priv->return_on_cancel != return_on_cancel)
{
priv->return_on_cancel = return_on_cancel;
if (return_on_cancel)
{
IdeTaskCancel *cancel;
/* This creates a reference cycle, but it gets destroyed when the
* appropriate ide_task_return() API is called.
*/
cancel = g_slice_new0 (IdeTaskCancel);
cancel->main_context = g_main_context_ref (priv->main_context);
cancel->task = g_object_ref (self);
cancel->priority = priv->priority;
priv->cancel_handler =
g_cancellable_connect (priv->cancellable,
G_CALLBACK (ide_task_cancellable_cancelled_cb),
g_steal_pointer (&cancel),
(GDestroyNotify)ide_task_cancel_free);
}
else
{
if (priv->cancel_handler)
{
g_cancellable_disconnect (priv->cancellable, priv->cancel_handler);
priv->cancel_handler = 0;
}
}
}
}
void
ide_task_report_new_error (gpointer source_object,
GAsyncReadyCallback callback,
gpointer callback_data,
gpointer source_tag,
GQuark domain,
gint code,
const gchar *format,
...)
{
g_autoptr(IdeTask) task = NULL;
GError *error;
va_list args;
va_start (args, format);
error = g_error_new_valist (domain, code, format, args);
va_end (args);
task = ide_task_new (source_object, NULL, callback, callback_data);
ide_task_set_source_tag (task, source_tag);
ide_task_return_error (task, g_steal_pointer (&error));
}
/**
* ide_task_get_name:
* @self: a #IdeTask
*
* Gets the name assigned for the task.
*
* Returns: (nullable): a string or %NULL
*
* Since: 3.32
*/
const gchar *
ide_task_get_name (IdeTask *self)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
const gchar *ret;
g_return_val_if_fail (IDE_IS_TASK (self), NULL);
g_mutex_lock (&priv->mutex);
ret = priv->name;
g_mutex_unlock (&priv->mutex);
return ret;
}
/**
* ide_task_set_name:
* @self: a #IdeTask
*
* Sets a useful name for the task.
*
* This string is interned, so it is best to avoid dynamic names as
* that can result in lots of unnecessary strings being interned for
* the lifetime of the process.
*
* This name may be used in various g_critical() messages which can
* be useful in troubleshooting.
*
* If using #IdeTask from C, a default name is set using the source
* file name and line number.
*
* Since: 3.32
*/
void
ide_task_set_name (IdeTask *self,
const gchar *name)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_return_if_fail (IDE_IS_TASK (self));
name = g_intern_string (name);
g_mutex_lock (&priv->mutex);
priv->name = name;
g_mutex_unlock (&priv->mutex);
#ifdef ENABLE_TIME_CHART
g_message ("TASK-BEGIN: %s", name);
#endif
}
/**
* ide_task_had_error:
* @self: a #IdeTask
*
* Checks to see if the task had an error.
*
* Returns: %TRUE if an error has occurred
*
* Since: 3.32
*/
gboolean
ide_task_had_error (IdeTask *self)
{
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
gboolean ret;
g_return_val_if_fail (IDE_IS_TASK (self), FALSE);
g_mutex_lock (&priv->mutex);
ret = (priv->result != NULL && priv->result->type == IDE_TASK_RESULT_ERROR) ||
(priv->thread_result != NULL && priv->thread_result->type == IDE_TASK_RESULT_ERROR);
g_mutex_unlock (&priv->mutex);
return ret;
}
static gpointer
ide_task_get_user_data (GAsyncResult *result)
{
IdeTask *self = (IdeTask *)result;
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_assert (IDE_IS_TASK (self));
return priv->user_data;
}
static GObject *
ide_task_get_source_object_full (GAsyncResult *result)
{
IdeTask *self = (IdeTask *)result;
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_assert (IDE_IS_TASK (self));
return priv->source_object ? g_object_ref (priv->source_object) : NULL;
}
static gboolean
ide_task_is_tagged (GAsyncResult *result,
gpointer source_tag)
{
IdeTask *self = (IdeTask *)result;
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_assert (IDE_IS_TASK (self));
return source_tag == priv->source_tag;
}
static void
async_result_init_iface (GAsyncResultIface *iface)
{
iface->get_user_data = ide_task_get_user_data;
iface->get_source_object = ide_task_get_source_object_full;
iface->is_tagged = ide_task_is_tagged;
}
void
_ide_dump_tasks (void)
{
guint i = 0;
G_LOCK (global_task_list);
for (const GList *iter = global_task_list.head; iter; iter = iter->next)
{
IdeTask *self = iter->data;
IdeTaskPrivate *priv = ide_task_get_instance_private (self);
g_printerr ("[%02d]: %s %s\n", i++, priv->name,
priv->completed ? "completed" : "");
}
G_UNLOCK (global_task_list);
}