mirror of
https://gitlab.freedesktop.org/NetworkManager/NetworkManager
synced 2024-10-02 22:38:01 +00:00
8bace23beb
- All internal source files (except "examples", which are not internal) should include "config.h" first. As also all internal source files should include "nm-default.h", let "config.h" be included by "nm-default.h" and include "nm-default.h" as first in every source file. We already wanted to include "nm-default.h" before other headers because it might contains some fixes (like "nm-glib.h" compatibility) that is required first. - After including "nm-default.h", we optinally allow for including the corresponding header file for the source file at hand. The idea is to ensure that each header file is self contained. - Don't include "config.h" or "nm-default.h" in any header file (except "nm-sd-adapt.h"). Public headers anyway must not include these headers, and internal headers are never included after "nm-default.h", as of the first previous point. - Include all internal headers with quotes instead of angle brackets. In practice it doesn't matter, because in our public headers we must include other headers with angle brackets. As we use our public headers also to compile our interal source files, effectively the result must be the same. Still do it for consistency. - Except for <config.h> itself. Include it with angle brackets as suggested by https://www.gnu.org/software/autoconf/manual/autoconf.html#Configuration-Headers
599 lines
15 KiB
C
599 lines
15 KiB
C
/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
|
|
/*
|
|
* libnm_glib -- Access network status & information from glib applications
|
|
*
|
|
* 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 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.
|
|
*
|
|
* Copyright (C) 2005 - 2008 Red Hat, Inc.
|
|
* Copyright (C) 2005 - 2008 Novell, Inc.
|
|
*/
|
|
|
|
#include "nm-default.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <dbus/dbus.h>
|
|
#include <dbus/dbus-glib-lowlevel.h>
|
|
|
|
#include "NetworkManager.h"
|
|
#include "libnm_glib.h"
|
|
|
|
#define DBUS_NO_SERVICE_ERROR "org.freedesktop.DBus.Error.ServiceDoesNotExist"
|
|
|
|
|
|
struct libnm_glib_ctx
|
|
{
|
|
unsigned char check;
|
|
|
|
GMainContext * g_main_ctx;
|
|
GMainLoop * g_main_loop;
|
|
DBusConnection * dbus_con;
|
|
guint dbus_watcher;
|
|
guint dbus_watch_interval;
|
|
|
|
gboolean thread_done;
|
|
gboolean thread_inited;
|
|
GThread * thread;
|
|
|
|
GSList * callbacks;
|
|
GMutex * callbacks_lock;
|
|
guint callback_id_last;
|
|
|
|
libnm_glib_state nm_state;
|
|
};
|
|
|
|
typedef struct libnm_glib_callback
|
|
{
|
|
guint id;
|
|
GMainContext * gmain_ctx;
|
|
libnm_glib_ctx * libnm_glib_ctx;
|
|
libnm_glib_callback_func func;
|
|
gpointer user_data;
|
|
} libnm_glib_callback;
|
|
|
|
|
|
static void _libnm_glib_schedule_dbus_watcher (libnm_glib_ctx *ctx);
|
|
static DBusConnection * _libnm_glib_dbus_init (gpointer *user_data, GMainContext *context);
|
|
static void _libnm_glib_update_state (libnm_glib_ctx *ctx, NMState state);
|
|
|
|
|
|
static void
|
|
_libnm_glib_nm_state_cb (DBusPendingCall *pcall, void *user_data)
|
|
{
|
|
DBusMessage * reply;
|
|
libnm_glib_ctx * ctx = (libnm_glib_ctx *) user_data;
|
|
NMState nm_state;
|
|
|
|
g_return_if_fail (pcall != NULL);
|
|
g_return_if_fail (ctx != NULL);
|
|
|
|
if (!(reply = dbus_pending_call_steal_reply (pcall)))
|
|
goto out;
|
|
|
|
if (dbus_message_get_type (reply) == DBUS_MESSAGE_TYPE_ERROR)
|
|
{
|
|
DBusError err;
|
|
|
|
dbus_error_init (&err);
|
|
dbus_set_error_from_message (&err, reply);
|
|
fprintf (stderr, "%s: dbus returned an error.\n (%s) %s\n", __func__, err.name, err.message);
|
|
dbus_error_free (&err);
|
|
dbus_message_unref (reply);
|
|
goto out;
|
|
}
|
|
|
|
if (dbus_message_get_args (reply, NULL, DBUS_TYPE_UINT32, &nm_state, DBUS_TYPE_INVALID))
|
|
_libnm_glib_update_state (ctx, nm_state);
|
|
|
|
dbus_message_unref (reply);
|
|
|
|
out:
|
|
dbus_pending_call_unref (pcall);
|
|
}
|
|
|
|
static void
|
|
_libnm_glib_get_nm_state (libnm_glib_ctx *ctx)
|
|
{
|
|
DBusMessage * message;
|
|
DBusPendingCall * pcall = NULL;
|
|
|
|
g_return_if_fail (ctx != NULL);
|
|
|
|
if ((message = dbus_message_new_method_call (NM_DBUS_SERVICE, NM_DBUS_PATH, NM_DBUS_INTERFACE, "state")))
|
|
{
|
|
dbus_connection_send_with_reply (ctx->dbus_con, message, &pcall, -1);
|
|
if (pcall)
|
|
dbus_pending_call_set_notify (pcall, _libnm_glib_nm_state_cb, ctx, NULL);
|
|
dbus_message_unref (message);
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
_libnm_glib_callback_helper (gpointer user_data)
|
|
{
|
|
libnm_glib_callback *cb_data = (libnm_glib_callback *)user_data;
|
|
|
|
g_return_val_if_fail (cb_data != NULL, FALSE);
|
|
g_return_val_if_fail (cb_data->func != NULL, FALSE);
|
|
g_return_val_if_fail (cb_data->libnm_glib_ctx != NULL, FALSE);
|
|
|
|
(*(cb_data->func)) (cb_data->libnm_glib_ctx, cb_data->user_data);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static void
|
|
_libnm_glib_schedule_single_callback (libnm_glib_ctx *ctx,
|
|
libnm_glib_callback *callback)
|
|
{
|
|
GSource *source;
|
|
|
|
g_return_if_fail (ctx != NULL);
|
|
g_return_if_fail (callback != NULL);
|
|
|
|
callback->libnm_glib_ctx = ctx;
|
|
|
|
source = g_idle_source_new ();
|
|
g_source_set_callback (source, _libnm_glib_callback_helper, callback, NULL);
|
|
g_source_attach (source, callback->gmain_ctx);
|
|
g_source_unref (source);
|
|
}
|
|
|
|
static void
|
|
_libnm_glib_unschedule_single_callback (libnm_glib_ctx *ctx,
|
|
libnm_glib_callback *callback)
|
|
{
|
|
GSource *source;
|
|
|
|
g_return_if_fail (ctx != NULL);
|
|
g_return_if_fail (callback != NULL);
|
|
|
|
source = g_main_context_find_source_by_user_data (callback->gmain_ctx, callback);
|
|
if (source)
|
|
g_source_destroy (source);
|
|
}
|
|
|
|
static void
|
|
_libnm_glib_call_callbacks (libnm_glib_ctx *ctx)
|
|
{
|
|
GSList *elem;
|
|
|
|
g_return_if_fail (ctx != NULL);
|
|
|
|
g_mutex_lock (ctx->callbacks_lock);
|
|
for (elem = ctx->callbacks; elem; elem = g_slist_next (elem))
|
|
{
|
|
libnm_glib_callback *callback = (libnm_glib_callback *)(elem->data);
|
|
if (callback)
|
|
_libnm_glib_schedule_single_callback (ctx, callback);
|
|
}
|
|
g_mutex_unlock (ctx->callbacks_lock);
|
|
}
|
|
|
|
|
|
static void
|
|
_libnm_glib_update_state (libnm_glib_ctx *ctx, NMState state)
|
|
{
|
|
libnm_glib_state old_state;
|
|
|
|
g_return_if_fail (ctx != NULL);
|
|
|
|
old_state = ctx->nm_state;
|
|
switch (state) {
|
|
case NM_STATE_CONNECTED_LOCAL:
|
|
case NM_STATE_CONNECTED_SITE:
|
|
case NM_STATE_CONNECTED_GLOBAL:
|
|
ctx->nm_state = LIBNM_ACTIVE_NETWORK_CONNECTION;
|
|
break;
|
|
case NM_STATE_ASLEEP:
|
|
case NM_STATE_CONNECTING:
|
|
case NM_STATE_DISCONNECTED:
|
|
case NM_STATE_DISCONNECTING:
|
|
ctx->nm_state = LIBNM_NO_NETWORK_CONNECTION;
|
|
break;
|
|
case NM_STATE_UNKNOWN:
|
|
default:
|
|
ctx->nm_state = LIBNM_NO_NETWORKMANAGER;
|
|
break;
|
|
}
|
|
|
|
if (old_state != ctx->nm_state)
|
|
_libnm_glib_call_callbacks (ctx);
|
|
}
|
|
|
|
|
|
static DBusHandlerResult
|
|
_libnm_glib_dbus_filter (DBusConnection *connection,
|
|
DBusMessage *message,
|
|
void *user_data)
|
|
{
|
|
libnm_glib_ctx *ctx = (libnm_glib_ctx *)user_data;
|
|
gboolean handled = TRUE;
|
|
DBusError error;
|
|
|
|
g_return_val_if_fail (ctx != NULL, DBUS_HANDLER_RESULT_NOT_YET_HANDLED);
|
|
g_return_val_if_fail (connection != NULL, DBUS_HANDLER_RESULT_NOT_YET_HANDLED);
|
|
g_return_val_if_fail (message != NULL, DBUS_HANDLER_RESULT_NOT_YET_HANDLED);
|
|
|
|
dbus_error_init (&error);
|
|
if (dbus_message_is_signal (message, DBUS_INTERFACE_LOCAL, "Disconnected"))
|
|
{
|
|
/* Try to reactivate our connection to dbus on the next pass through the event loop */
|
|
ctx->nm_state = LIBNM_NO_DBUS;
|
|
dbus_connection_close (ctx->dbus_con);
|
|
dbus_connection_unref (ctx->dbus_con);
|
|
ctx->dbus_con = NULL;
|
|
_libnm_glib_schedule_dbus_watcher (ctx);
|
|
}
|
|
else if (dbus_message_is_signal (message, DBUS_INTERFACE_DBUS, "NameOwnerChanged"))
|
|
{
|
|
/* New signal for dbus 0.23... */
|
|
char *service;
|
|
char *old_owner;
|
|
char *new_owner;
|
|
|
|
if ( dbus_message_get_args (message, &error,
|
|
DBUS_TYPE_STRING, &service,
|
|
DBUS_TYPE_STRING, &old_owner,
|
|
DBUS_TYPE_STRING, &new_owner,
|
|
DBUS_TYPE_INVALID))
|
|
{
|
|
if (strcmp (service, NM_DBUS_SERVICE) == 0)
|
|
{
|
|
gboolean old_owner_good = (old_owner && (strlen (old_owner) > 0));
|
|
gboolean new_owner_good = (new_owner && (strlen (new_owner) > 0));
|
|
|
|
if (!old_owner_good && new_owner_good) /* Equivalent to old ServiceCreated signal */
|
|
_libnm_glib_get_nm_state (ctx);
|
|
else if (old_owner_good && !new_owner_good) /* Equivalent to old ServiceDeleted signal */
|
|
ctx->nm_state = LIBNM_NO_NETWORKMANAGER;
|
|
}
|
|
}
|
|
}
|
|
else if ( dbus_message_is_signal (message, NM_DBUS_INTERFACE, "DeviceNowActive")
|
|
|| dbus_message_is_signal (message, NM_DBUS_INTERFACE, "DeviceNoLongerActive")
|
|
|| dbus_message_is_signal (message, NM_DBUS_INTERFACE, "DeviceActivating")
|
|
|| dbus_message_is_signal (message, NM_DBUS_INTERFACE, "DevicesChanged"))
|
|
{
|
|
_libnm_glib_get_nm_state (ctx);
|
|
}
|
|
else if (dbus_message_is_signal (message, NM_DBUS_INTERFACE, "StateChanged"))
|
|
{
|
|
NMState state = NM_STATE_UNKNOWN;
|
|
|
|
dbus_message_get_args (message, &error, DBUS_TYPE_UINT32, &state, DBUS_TYPE_INVALID);
|
|
_libnm_glib_update_state (ctx, state);
|
|
}
|
|
else
|
|
handled = FALSE;
|
|
|
|
if (dbus_error_is_set (&error))
|
|
dbus_error_free (&error);
|
|
|
|
return (handled ? DBUS_HANDLER_RESULT_HANDLED : DBUS_HANDLER_RESULT_NOT_YET_HANDLED);
|
|
}
|
|
|
|
|
|
/*
|
|
* libnm_glib_dbus_init
|
|
*
|
|
* Initialize a connection to dbus and set up our callbacks.
|
|
*
|
|
*/
|
|
static DBusConnection *
|
|
_libnm_glib_dbus_init (gpointer *user_data, GMainContext *context)
|
|
{
|
|
DBusConnection *connection = NULL;
|
|
DBusError error;
|
|
|
|
dbus_error_init (&error);
|
|
connection = dbus_bus_get_private (DBUS_BUS_SYSTEM, &error);
|
|
if (dbus_error_is_set (&error))
|
|
{
|
|
fprintf (stderr, "%s: error, %s raised:\n %s\n\n", __func__, error.name, error.message);
|
|
dbus_error_free (&error);
|
|
return (NULL);
|
|
}
|
|
if (!connection)
|
|
return NULL;
|
|
|
|
if (!dbus_connection_add_filter (connection, _libnm_glib_dbus_filter, user_data, NULL))
|
|
return (NULL);
|
|
|
|
dbus_connection_set_exit_on_disconnect (connection, FALSE);
|
|
dbus_connection_setup_with_g_main (connection, context);
|
|
|
|
dbus_error_init (&error);
|
|
dbus_bus_add_match (connection,
|
|
"type='signal',"
|
|
"interface='" DBUS_INTERFACE_DBUS "',"
|
|
"sender='" DBUS_SERVICE_DBUS "'",
|
|
&error);
|
|
if (dbus_error_is_set (&error))
|
|
dbus_error_free (&error);
|
|
|
|
dbus_error_init (&error);
|
|
dbus_bus_add_match (connection,
|
|
"type='signal',"
|
|
"interface='" NM_DBUS_INTERFACE "',"
|
|
"path='" NM_DBUS_PATH "',"
|
|
"sender='" NM_DBUS_SERVICE "'",
|
|
&error);
|
|
if (dbus_error_is_set (&error))
|
|
dbus_error_free (&error);
|
|
|
|
return (connection);
|
|
}
|
|
|
|
|
|
/*
|
|
* libnm_glib_dbus_watcher
|
|
*
|
|
* Repeatedly try to re-activate the connection to dbus.
|
|
*
|
|
*/
|
|
static gboolean
|
|
_libnm_glib_dbus_watcher (gpointer user_data)
|
|
{
|
|
libnm_glib_ctx *ctx = (libnm_glib_ctx *)user_data;
|
|
|
|
g_return_val_if_fail (ctx != NULL, FALSE);
|
|
|
|
ctx->dbus_watcher = 0;
|
|
|
|
if (!ctx->dbus_con)
|
|
ctx->dbus_con = _libnm_glib_dbus_init ((gpointer)ctx, ctx->g_main_ctx);
|
|
|
|
if (ctx->dbus_con)
|
|
{
|
|
/* Get NM's state right away after we reconnect */
|
|
_libnm_glib_get_nm_state (ctx);
|
|
ctx->dbus_watch_interval = 1000;
|
|
}
|
|
else
|
|
{
|
|
/* Wait 3 seconds longer each time we fail to reconnect to dbus,
|
|
* with a maximum wait of one minute.
|
|
*/
|
|
ctx->dbus_watch_interval = MIN(ctx->dbus_watch_interval + 3000, 60000);
|
|
|
|
/* Reschule ourselves if we _still_ don't have a connection to dbus */
|
|
_libnm_glib_schedule_dbus_watcher (ctx);
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
/*
|
|
* libnm_glib_schedule_dbus_watcher
|
|
*
|
|
* Schedule an idle handler in our main loop to repeatedly
|
|
* attempt to re-activate the dbus connection until connected.
|
|
*
|
|
*/
|
|
static void
|
|
_libnm_glib_schedule_dbus_watcher (libnm_glib_ctx *ctx)
|
|
{
|
|
g_return_if_fail (ctx != NULL);
|
|
|
|
if (ctx->dbus_watcher == 0)
|
|
{
|
|
GSource * source = g_timeout_source_new (ctx->dbus_watch_interval);
|
|
g_source_set_callback (source, _libnm_glib_dbus_watcher, (gpointer) ctx, NULL);
|
|
ctx->dbus_watcher = g_source_attach (source, ctx->g_main_ctx);
|
|
g_source_unref (source);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* libnm_glib_dbus_worker
|
|
*
|
|
* Main thread for libnm
|
|
*
|
|
*/
|
|
static gpointer
|
|
_libnm_glib_dbus_worker (gpointer user_data)
|
|
{
|
|
libnm_glib_ctx *ctx = (libnm_glib_ctx *)user_data;
|
|
|
|
g_return_val_if_fail (ctx != NULL, NULL);
|
|
|
|
/* If dbus isn't up yet, schedule an idle handler to check for dbus.
|
|
* We also need a way to reconnect to dbus if the connection ever goes
|
|
* down. Should probably be done by a timeout polling dbus_connection_is_connected()
|
|
* or by getting connection status out of libdbus or something.
|
|
*/
|
|
if (!(ctx->dbus_con = _libnm_glib_dbus_init ((gpointer) ctx, ctx->g_main_ctx)))
|
|
_libnm_glib_schedule_dbus_watcher (ctx);
|
|
else
|
|
_libnm_glib_get_nm_state (ctx);
|
|
|
|
ctx->thread_inited = TRUE;
|
|
g_main_loop_run (ctx->g_main_loop);
|
|
ctx->thread_done = TRUE;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
static void
|
|
_libnm_glib_ctx_free (libnm_glib_ctx *ctx)
|
|
{
|
|
g_return_if_fail (ctx != NULL);
|
|
|
|
if (ctx->check == 0xDD)
|
|
{
|
|
fprintf (stderr, "%s: context %p already freed!\n", __func__, ctx);
|
|
return;
|
|
}
|
|
|
|
if (ctx->g_main_ctx)
|
|
g_main_context_unref (ctx->g_main_ctx);
|
|
if (ctx->g_main_loop)
|
|
g_main_loop_unref (ctx->g_main_loop);
|
|
|
|
if (ctx->dbus_con)
|
|
{
|
|
dbus_connection_close (ctx->dbus_con);
|
|
dbus_connection_unref (ctx->dbus_con);
|
|
ctx->dbus_con = NULL;
|
|
}
|
|
|
|
if (ctx->callbacks_lock)
|
|
g_mutex_free (ctx->callbacks_lock);
|
|
|
|
g_slist_free_full (ctx->callbacks, g_free);
|
|
|
|
if (ctx->thread)
|
|
g_thread_join (ctx->thread);
|
|
|
|
memset (ctx, 0, sizeof (libnm_glib_ctx));
|
|
memset (&(ctx->check), 0xDD, sizeof (ctx->check));
|
|
g_free (ctx);
|
|
}
|
|
|
|
|
|
static libnm_glib_ctx *
|
|
_libnm_glib_ctx_new (void)
|
|
{
|
|
libnm_glib_ctx *ctx = g_malloc0 (sizeof (libnm_glib_ctx));
|
|
|
|
if (!(ctx->g_main_ctx = g_main_context_new ()))
|
|
goto error;
|
|
if (!(ctx->g_main_loop = g_main_loop_new (ctx->g_main_ctx, FALSE)))
|
|
goto error;
|
|
if (!(ctx->callbacks_lock = g_mutex_new ()))
|
|
goto error;
|
|
ctx->dbus_watch_interval = 1000;
|
|
|
|
return ctx;
|
|
|
|
error:
|
|
_libnm_glib_ctx_free (ctx);
|
|
return NULL;
|
|
}
|
|
|
|
|
|
libnm_glib_ctx *
|
|
libnm_glib_init (void)
|
|
{
|
|
libnm_glib_ctx *ctx = NULL;
|
|
|
|
nm_g_type_init ();
|
|
|
|
if (!g_thread_supported ())
|
|
g_thread_init (NULL);
|
|
dbus_g_thread_init ();
|
|
|
|
if (!(ctx = _libnm_glib_ctx_new ()))
|
|
return NULL;
|
|
|
|
ctx->thread = g_thread_create (_libnm_glib_dbus_worker, ctx, TRUE, NULL);
|
|
if (!ctx->thread)
|
|
goto error;
|
|
|
|
/* Wait until initialization of the thread */
|
|
while (!ctx->thread_inited)
|
|
g_usleep (G_USEC_PER_SEC / 20);
|
|
|
|
return ctx;
|
|
|
|
error:
|
|
_libnm_glib_ctx_free (ctx);
|
|
return NULL;
|
|
}
|
|
|
|
|
|
void
|
|
libnm_glib_shutdown (libnm_glib_ctx *ctx)
|
|
{
|
|
g_return_if_fail (ctx != NULL);
|
|
|
|
g_main_loop_quit (ctx->g_main_loop);
|
|
while (!ctx->thread_done)
|
|
g_usleep (G_USEC_PER_SEC / 20);
|
|
|
|
_libnm_glib_ctx_free (ctx);
|
|
}
|
|
|
|
|
|
libnm_glib_state
|
|
libnm_glib_get_network_state (const libnm_glib_ctx *ctx)
|
|
{
|
|
if (!ctx)
|
|
return LIBNM_INVALID_CONTEXT;
|
|
|
|
return ctx->nm_state;
|
|
}
|
|
|
|
|
|
guint
|
|
libnm_glib_register_callback (libnm_glib_ctx *ctx,
|
|
libnm_glib_callback_func func,
|
|
gpointer user_data,
|
|
GMainContext *g_main_ctx)
|
|
{
|
|
libnm_glib_callback *callback = NULL;
|
|
|
|
g_return_val_if_fail (ctx != NULL, 0);
|
|
g_return_val_if_fail (func != NULL, 0);
|
|
|
|
callback = g_malloc0 (sizeof (libnm_glib_callback));
|
|
|
|
callback->id = ++ (ctx->callback_id_last);
|
|
callback->func = func;
|
|
callback->gmain_ctx = g_main_ctx;
|
|
callback->libnm_glib_ctx = ctx;
|
|
callback->user_data = user_data;
|
|
|
|
g_mutex_lock (ctx->callbacks_lock);
|
|
ctx->callbacks = g_slist_append (ctx->callbacks, callback);
|
|
_libnm_glib_schedule_single_callback (ctx, callback);
|
|
g_mutex_unlock (ctx->callbacks_lock);
|
|
|
|
return (callback->id);
|
|
}
|
|
|
|
|
|
void
|
|
libnm_glib_unregister_callback (libnm_glib_ctx *ctx,
|
|
guint id)
|
|
{
|
|
GSList *elem;
|
|
|
|
g_return_if_fail (ctx != NULL);
|
|
g_return_if_fail (id > 0);
|
|
|
|
g_mutex_lock (ctx->callbacks_lock);
|
|
elem = ctx->callbacks;
|
|
while (elem)
|
|
{
|
|
libnm_glib_callback *callback = (libnm_glib_callback *)(elem->data);
|
|
if (callback && (callback->id == id))
|
|
{
|
|
_libnm_glib_unschedule_single_callback (ctx, callback);
|
|
ctx->callbacks = g_slist_remove_link (ctx->callbacks, elem);
|
|
break;
|
|
}
|
|
|
|
elem = g_slist_next (elem);
|
|
}
|
|
g_mutex_unlock (ctx->callbacks_lock);
|
|
}
|