libnm: merge branch 'th/nmclient-wait-shutdown'

https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/merge_requests/1414
This commit is contained in:
Thomas Haller 2022-10-14 17:53:38 +02:00
commit fd1965e7bb
No known key found for this signature in database
GPG key ID: 29C2366E4DFC5728
7 changed files with 683 additions and 48 deletions

View file

@ -329,7 +329,7 @@ def make_call(nmc):
###############################################################################
def destroy_nmc(nmc_holder):
def destroy_nmc(nmc_holder, destroy_mode):
# The way to shutdown an NMClient is just by unrefing it.
#
# At any moment, can an NMClient instance have pending async operations.
@ -362,53 +362,77 @@ def destroy_nmc(nmc_holder):
(nmc,) = nmc_holder
nmc_holder.clear()
if not nmc:
log(f"[destroy_nmc]: nothing to destroy")
return
log(
f"[destroy_nmc]: destroying NMClient {nmc}: pyref={sys.getrefcount(nmc)}, ref_count={nmc.ref_count}"
f"[destroy_nmc]: destroying NMClient {nmc}: pyref={sys.getrefcount(nmc)}, ref_count={nmc.ref_count}, destroy_mode={destroy_mode}"
)
ctx = nmc.get_main_context()
if destroy_mode == 0:
ctx = nmc.get_main_context()
finished = []
finished = []
def _weak_ref_cb():
log(f"[destroy_nmc]: context busy watcher is gone")
finished.clear()
finished.append(True)
def _weak_ref_cb():
log(f"[destroy_nmc]: context busy watcher is gone")
finished.clear()
finished.append(True)
# We take a weak ref on the context-busy-watcher object and give up
# our reference on nmc. This must be the last reference, which initiates
# the shutdown of the NMClient.
weak_ref = nmc.get_context_busy_watcher().weak_ref(_weak_ref_cb)
del nmc
# We take a weak ref on the context-busy-watcher object and give up
# our reference on nmc. This must be the last reference, which initiates
# the shutdown of the NMClient.
weak_ref = nmc.get_context_busy_watcher().weak_ref(_weak_ref_cb)
del nmc
def _timeout_cb(unused):
if not finished:
# Somebody else holds a reference to the NMClient and keeps
# it alive. We cannot properly clean up.
log(
f"[destroy_nmc]: ERROR: timeout waiting for context busy watcher to be gone"
)
finished.append(False)
return False
def _timeout_cb(unused):
if not finished:
# Somebody else holds a reference to the NMClient and keeps
# it alive. We cannot properly clean up.
log(
f"[destroy_nmc]: ERROR: timeout waiting for context busy watcher to be gone"
)
finished.append(False)
return False
timeout_source = GLib.timeout_source_new(1000)
timeout_source.set_callback(_timeout_cb)
timeout_source.attach(ctx)
timeout_source = GLib.timeout_source_new(1000)
timeout_source.set_callback(_timeout_cb)
timeout_source.attach(ctx)
while not finished:
log(f"[destroy_nmc]: iterating main context")
ctx.iteration(True)
while not finished:
log(f"[destroy_nmc]: iterating main context")
ctx.iteration(True)
timeout_source.destroy()
timeout_source.destroy()
log(f"[destroy_nmc]: done: {finished[0]}")
if not finished[0]:
weak_ref.unref()
raise Exception("Failure to destroy NMClient: something keeps it alive")
log(f"[destroy_nmc]: done: {finished[0]}")
if not finished[0]:
weak_ref.unref()
raise Exception("Failure to destroy NMClient: something keeps it alive")
else:
if destroy_mode == 1:
ctx = GLib.MainContext.default()
else:
# Run the maincontext of the NMClient.
ctx = nmc.get_main_context()
with MainLoopRun("destroy_nmc", ctx, 2) as r:
def _wait_shutdown_cb(source_unused, result, r):
try:
NM.Client.wait_shutdown_finish(result)
except Exception as e:
if error_is_cancelled(e):
log(
f"[destroy_nmc]: wait_shutdown() completed with cancellation after timeout"
)
else:
log(f"[destroy_nmc]: wait_shutdown() completed with error: {e}")
else:
log(f"[destroy_nmc]: wait_shutdown() completed with success")
r.quit()
nmc.wait_shutdown(True, r.cancellable, _wait_shutdown_cb, r)
del nmc
###############################################################################
@ -425,11 +449,18 @@ def run1():
make_call(nmc)
log()
# To cleanup the NMClient, we need to give up the reference. Move
# it to a list, and destroy_nmc() will take care of it.
nmc_holder = [nmc]
del nmc
destroy_nmc(nmc_holder)
if not nmc:
log(f"[destroy_nmc]: nothing to destroy")
else:
# To cleanup the NMClient, we need to give up the reference. Move
# it to a list, and destroy_nmc() will take care of it.
nmc_holder = [nmc]
del nmc
# In the example, there are three modes how the destroy is
# implemented.
destroy_nmc(nmc_holder, destroy_mode=1)
log()
log("done")
except Exception as e:

View file

@ -1847,5 +1847,7 @@ global:
libnm_1_42_0 {
global:
nm_client_wait_shutdown;
nm_client_wait_shutdown_finish;
nm_setting_ovs_interface_get_ofport_request;
} libnm_1_40_0;

View file

@ -287,7 +287,7 @@ typedef struct {
guint dbsid_nm_vpn_connection_state_changed;
guint dbsid_nm_check_permissions;
NMClientInstanceFlags instance_flags : 3;
NMClientInstanceFlags instance_flags : 5;
NMTernary permissions_state : 3;
@ -7277,7 +7277,8 @@ nml_cleanup_context_busy_watcher_on_idle(GObject *context_busy_watcher_take, GMa
static void
_init_start_complete(NMClient *self, GError *error_take)
{
NMClientPrivate *priv = NM_CLIENT_GET_PRIVATE(self);
gs_unref_object NMClient *self_keep_alive = g_object_ref(self);
NMClientPrivate *priv = NM_CLIENT_GET_PRIVATE(self);
NML_NMCLIENT_LOG_D(
self,
@ -7285,6 +7286,11 @@ _init_start_complete(NMClient *self, GError *error_take)
priv->init_data->is_sync ? "sync" : "async",
NM_PRINT_FMT_QUOTED(error_take, "error: ", error_take->message, "", "success"));
priv->instance_flags |= (error_take ? NM_CLIENT_INSTANCE_FLAGS_INITIALIZED_BAD
: NM_CLIENT_INSTANCE_FLAGS_INITIALIZED_GOOD);
_notify(self, PROP_INSTANCE_FLAGS);
nml_init_data_return(g_steal_pointer(&priv->init_data), error_take);
}
@ -7585,8 +7591,18 @@ set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *ps
/* construct */
v_uint = g_value_get_uint(value);
g_return_if_fail(!NM_FLAGS_ANY(v_uint, ~((guint) NM_CLIENT_INSTANCE_FLAGS_ALL)));
v_uint &= ((guint) NM_CLIENT_INSTANCE_FLAGS_ALL);
/* Silently ignore "initialized-{good,bad}" flags. They are only set internally
* and cannot be change by the user. However, accept the caller to set them,
* so that
* nmc.props.instance_flags = nmc.props.instance_flags | NM.ClientInstanceFlags.NO_AUTO_FETCH_PERMISSIONS
* works. */
v_uint &= ~((guint) (NM_CLIENT_INSTANCE_FLAGS_INITIALIZED_GOOD
| NM_CLIENT_INSTANCE_FLAGS_INITIALIZED_BAD));
g_return_if_fail(!NM_FLAGS_ANY(v_uint, ~((guint) NM_CLIENT_INSTANCE_FLAGS_ALL_WRITABLE)));
v_uint &= ((guint) NM_CLIENT_INSTANCE_FLAGS_ALL_WRITABLE);
if (!priv->instance_flags_constructed) {
priv->instance_flags_constructed = TRUE;
@ -8175,6 +8191,9 @@ nm_client_class_init(NMClientClass *client_class)
* property to know whether permissions are ready. Note that permissions are only fetched
* when NMClient has a D-Bus name owner.
*
* The flags %NM_CLIENT_INSTANCE_FLAGS_INITIALIZED_GOOD and %NM_CLIENT_INSTANCE_FLAGS_INITIALIZED_BAD
* cannot be set, however they will be returned by the getter after initialization completes.
*
* Since: 1.24
*/
obj_properties[PROP_INSTANCE_FLAGS] = g_param_spec_uint(
@ -8813,6 +8832,299 @@ nm_client_async_initable_iface_init(GAsyncInitableIface *iface)
iface->init_finish = init_finish;
}
/*****************************************************************************/
typedef struct {
GCancellable *cancellable;
GSource *integration_source;
GTask *task;
GSource *idle_source;
/* A weakref to nm_client_get_context_busy_watcher() */
GWeakRef weak_ref;
gulong cancellable_id;
guint64 log_ptr;
int result;
} WaitShutdownData;
G_LOCK_DEFINE_STATIC(wait_shutdown_mutex);
static NM_CACHED_QUARK_FCN("nm.client.wait-shutdown", _wait_shutdown_get_quark);
static void
_wait_shutdown_data_free(gpointer user_data)
{
WaitShutdownData *data = user_data;
nm_g_slice_free(data);
}
static gboolean
_wait_shutdown_idle_cb(gpointer user_data)
{
WaitShutdownData *data = user_data;
gs_unref_object GTask *task = NULL;
int result;
nm_clear_g_source_inst(&data->idle_source);
task = g_steal_pointer(&data->task);
result = g_atomic_int_get(&data->result);
nm_assert(NM_IN_SET(result, 0, 1));
NML_DBUS_LOG(_NML_NMCLIENT_LOG_LEVEL_COERCE(NML_DBUS_LOG_LEVEL_TRACE),
"nmclient[" NM_HASH_OBFUSCATE_PTR_FMT
"]: wait-shutdown (" NM_HASH_OBFUSCATE_PTR_FMT ")"
"%s",
data->log_ptr,
NM_HASH_OBFUSCATE_PTR(data),
!result ? " cancelled" : " completed");
if (!result) {
GError *error = NULL;
nm_utils_error_set_cancelled(&error, FALSE, NULL);
g_task_return_error(task, error);
} else
g_task_return_boolean(task, TRUE);
return G_SOURCE_CONTINUE;
}
static void
_wait_shutdown_data_clear(WaitShutdownData *data, gboolean result)
{
gs_unref_object GObject *context_busy_watcher = NULL;
if (!g_atomic_int_compare_and_exchange(&data->result, -1, !!result)) {
/* There was a race and the result is already provided. Nothing to
* do, except, if "result" indicates cancellation (FALSE), set data->result
* to FALSE to. Aside that, the completion is already in progress. */
if (!result)
g_atomic_int_compare_and_exchange(&data->result, TRUE, FALSE);
return;
}
nm_clear_g_signal_handler(data->cancellable, &data->cancellable_id);
nm_clear_g_source_inst(&data->integration_source);
g_clear_object(&data->cancellable);
if (!result) {
/* This was a cancellation. We likely still have the qdata tracked.
* We need to remove it. */
context_busy_watcher = g_weak_ref_get(&data->weak_ref);
if (context_busy_watcher) {
GPtrArray *qdata_arr;
G_LOCK(wait_shutdown_mutex);
qdata_arr = g_object_get_qdata(context_busy_watcher, _wait_shutdown_get_quark());
if (qdata_arr && g_ptr_array_remove_fast(qdata_arr, data)) {
/* data->task had an additional reference, we return it now. */
g_object_unref(data->task);
}
G_UNLOCK(wait_shutdown_mutex);
}
}
g_weak_ref_clear(&data->weak_ref);
/* We don't complete right away, instead always schedule an idle action
* on the caller's context. */
data->idle_source = nm_g_source_attach(
nm_g_idle_source_new(G_PRIORITY_DEFAULT_IDLE, _wait_shutdown_idle_cb, data, NULL),
g_task_get_context(data->task));
}
static void
_wait_shutdown_qdata_cb(gpointer user_data)
{
gs_unref_ptrarray GPtrArray *qdata_arr = user_data;
while (qdata_arr->len > 0) {
WaitShutdownData *data;
data = g_ptr_array_remove_index_fast(qdata_arr, qdata_arr->len - 1);
_wait_shutdown_data_clear(data, TRUE);
/* data->task had an additional reference, we return it now. */
g_object_unref(data->task);
}
}
static void
_wait_shutdown_cancelled_cb(GCancellable *cancellable, gpointer user_data)
{
_wait_shutdown_data_clear(g_task_get_task_data(user_data), FALSE);
}
/**
* nm_client_wait_shutdown:
* @client: the #NMClient to shutdown.
* @integrate_maincontext: whether to hook the client's maincontext
* in the current thread default. Otherwise, you must ensure
* that the client's maincontext gets iterated so that it can complete.
* By integrating the maincontext in the current thread default, you
* may instead only iterate the latter.
* @cancellable: (allow-none): the #GCancellable to abort the shutdown.
* @callback: (nullable): a #GAsyncReadyCallback to call when the request
* is satisfied or %NULL if you don't care about the result of the
* method invocation.
* @user_data: the data to pass to @callback
*
* The way to stop #NMClient is by unrefing it. That will cancel all
* internally pending async operations. However, as async operations in
* NMClient use GTask, hence they cannot complete right away. Instead,
* their (internal) result callback still needs to be dispatched by iterating
* the client's main context.
*
* You thus cannot stop iterating the client's main context until
* everything is wrapped up. nm_client_get_context_busy_watcher()
* helps to watch how long that will be.
*
* This function automates that waiting. Like all glib async operations
* this honors the current g_main_context_get_thread_default().
*
* In any case, to complete the shutdown, nm_client_get_main_context()
* must be iterated. If the current g_main_context_get_thread_default() is
* the same as nm_client_get_main_context(), then @integrate_maincontext
* is ignored. In that case, the caller is required to iterate the context
* for shutdown to complete. Otherwise, if g_main_context_get_thread_default()
* differs from nm_client_get_main_context() and @integrate_maincontext
* is %FALSE, the caller must make sure that both contexts are iterated
* until completion. Otherwise, if @integrate_maincontext is %TRUE, then
* nm_client_get_main_context() will be integrated in g_main_context_get_thread_default().
* This means, the caller gives nm_client_get_main_context() up until the waiting
* completes, the function will acquire the context and hook it into
* g_main_context_get_thread_default().
* It is a bug to request @integrate_maincontext while having nm_client_get_main_context()
* acquired or iterated otherwise because a context can only be acquired once
* at a time.
*
* Shutdown can only complete after all references to @client were released.
*
* It is possible to call this function multiple times for the same client.
* But note that with @integrate_maincontext the client's context is acquired,
* which can only be done once at a time.
*
* It is permissible to start waiting before the objects is fully initialized.
*
* The function really allows two separate things. To get a notification (callback) when
* shutdown is complete, and to integrate the client's context in another context.
* The latter case is useful if the client has a separate context and you hand it
* over to another GMainContext to wrap up.
*
* The main use is to have a NMClient and a separate GMainContext on a worker
* thread. When being done, you can hand over the cleanup of the context
* to g_main_context_default(), assuming that the main thread iterates
* the default context. In that case, you don't need to care about passing
* a callback to know when shutdown completed.
*
* Since: 1.42
*/
void
nm_client_wait_shutdown(NMClient *client,
gboolean integrate_maincontext,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
NMClientPrivate *priv;
WaitShutdownData *data;
gs_unref_object GTask *task = NULL;
GQuark quark = _wait_shutdown_get_quark();
GPtrArray *qdata_arr;
GSource *integration_source = NULL;
g_return_if_fail(NM_IS_CLIENT(client));
g_return_if_fail(!cancellable || G_IS_CANCELLABLE(cancellable));
priv = NM_CLIENT_GET_PRIVATE(client);
task = nm_g_task_new(NULL, cancellable, nm_client_wait_shutdown, callback, user_data);
if (integrate_maincontext && g_task_get_context(task) != priv->main_context) {
integration_source = nm_utils_g_main_context_create_integrate_source(priv->main_context);
g_return_if_fail(integration_source);
g_source_attach(integration_source, g_task_get_context(task));
}
data = g_slice_new(WaitShutdownData);
*data = (WaitShutdownData){
.cancellable = nm_g_object_ref(cancellable),
.task = g_object_ref(task),
.result = -1,
.integration_source = integration_source,
.log_ptr = NM_HASH_OBFUSCATE_PTR(client),
};
/* The "data" itself stays alive as long as the task lives. That's important, because
* the callbacks _wait_shutdown_weak_ref_cb and _wait_shutdown_cancelled_cb
* rely on accessing "data", which must live long enough. */
g_task_set_task_data(task, data, _wait_shutdown_data_free);
g_weak_ref_init(&data->weak_ref, priv->context_busy_watcher);
NML_DBUS_LOG(_NML_NMCLIENT_LOG_LEVEL_COERCE(NML_DBUS_LOG_LEVEL_TRACE),
"nmclient[" NM_HASH_OBFUSCATE_PTR_FMT
"]: wait-shutdown (" NM_HASH_OBFUSCATE_PTR_FMT ")"
"%s",
data->log_ptr,
NM_HASH_OBFUSCATE_PTR(data),
integration_source ? " (integrated main source)" : "");
/* I don't think g_object_weak_ref() + GWeakRef can actually be used in
* a race-free way here, because g_object_weak_ref() has no GDestroyNotify
* and cannot keep task alive to avoid races. Instead, implement a weak pointer
* notification via the qdata.
*
* Yes, getting cancellation thread-safe is rather complicated here! I think
* the code is correct though, and you can cancel the operation from
* any thread without races. */
G_LOCK(wait_shutdown_mutex);
qdata_arr = g_object_get_qdata(priv->context_busy_watcher, quark);
if (!qdata_arr) {
qdata_arr = g_ptr_array_new();
g_object_set_qdata_full(priv->context_busy_watcher,
quark,
qdata_arr,
_wait_shutdown_qdata_cb);
}
/* data->task gets an additional reference, take it now. */
g_object_ref(data->task);
g_ptr_array_add(qdata_arr, data);
G_UNLOCK(wait_shutdown_mutex);
if (data->cancellable) {
/* Take an additional reference on task. */
data->cancellable_id = g_cancellable_connect(data->cancellable,
G_CALLBACK(_wait_shutdown_cancelled_cb),
g_object_ref(task),
g_object_unref);
}
}
/**
* nm_client_wait_shutdown_finish:
* @result: a #GAsyncResult obtained from the #GAsyncReadyCallback passed to nm_client_wait_shutdown()
* @error: return location for error or %NULL
*
* Returns: %TRUE if waiting is complete successfully. In that case, all resources of the
* nmclient are wrapped up and released. This can only fail by user cancellation.
*
* Since: 1.42.
*/
gboolean
nm_client_wait_shutdown_finish(GAsyncResult *result, GError **error)
{
g_return_val_if_fail(nm_g_task_is_valid(result, NULL, nm_client_wait_shutdown), FALSE);
return g_task_propagate_boolean(G_TASK(result), error);
}
/*****************************************************************************
* Backported symbols. Usually, new API is only added in new major versions
* of NetworkManager (that is, on "master" branch). Sometimes however, we might

View file

@ -227,7 +227,16 @@ typedef enum {
/*****************************************************************************/
#define NM_CLIENT_INSTANCE_FLAGS_ALL ((NMClientInstanceFlags) 0x1)
#define NM_CLIENT_INSTANCE_FLAGS_ALL \
((NMClientInstanceFlags) (NM_CLIENT_INSTANCE_FLAGS_NO_AUTO_FETCH_PERMISSIONS \
| NM_CLIENT_INSTANCE_FLAGS_INITIALIZED_GOOD \
| NM_CLIENT_INSTANCE_FLAGS_INITIALIZED_BAD))
#define NM_CLIENT_INSTANCE_FLAGS_ALL_WRITABLE \
((NMClientInstanceFlags) (NM_CLIENT_INSTANCE_FLAGS_ALL \
& ~(( \
NMClientInstanceFlags) (NM_CLIENT_INSTANCE_FLAGS_INITIALIZED_GOOD \
| NM_CLIENT_INSTANCE_FLAGS_INITIALIZED_BAD))))
typedef struct {
GType (*get_o_type_fcn)(void);

View file

@ -8,7 +8,10 @@
#include <sys/types.h>
#include <signal.h>
#include "libnm-client-impl/nm-dbus-helpers.h"
#include "libnm-client-test/nm-test-libnm-utils.h"
#include "libnm-glib-aux/nm-time-utils.h"
static struct {
GMainLoop *loop;
@ -1349,6 +1352,240 @@ test_connection_invalid(void)
/*****************************************************************************/
typedef struct {
NMClient *nmc;
NMOptionBool completed;
} WaitShutdownNmcInitData;
static void
_wait_shutdown_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
gs_free_error GError *error = NULL;
gboolean b;
guint *p_pending_ops = user_data;
g_assert(!source);
if (nmtst_get_rand_bool()) {
b = nm_client_wait_shutdown_finish(result, &error);
g_assert(b == !error);
}
g_assert_cmpint(*p_pending_ops, >, 0);
(*p_pending_ops)--;
}
static void
_wait_shutdown_nmc_init_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
WaitShutdownNmcInitData *data = user_data;
gs_free_error GError *error = NULL;
gboolean b;
g_assert(data->nmc == NM_CLIENT(source));
g_assert(data->completed == NM_OPTION_BOOL_DEFAULT);
b = g_async_initable_init_finish(G_ASYNC_INITABLE(source), result, &error);
nmtst_assert_success(b, error);
g_assert(b == TRUE);
g_assert(NM_FLAGS_HAS(nm_client_get_instance_flags(data->nmc),
NM_CLIENT_INSTANCE_FLAGS_INITIALIZED_GOOD));
data->completed = TRUE;
}
static void
test_client_wait_shutdown(void)
{
gs_unref_ptrarray GPtrArray *contexts =
g_ptr_array_new_with_free_func((GDestroyNotify) g_main_context_unref);
nmtstc_auto_service_cleanup NMTstcServiceInfo *sinfo = NULL;
int i_run;
const int N_RUN = 50;
gs_free_error GError *error = NULL;
gs_unref_object GDBusConnection *dbus_connection = NULL;
guint pending_ops = 0;
int i;
gint64 until_msec;
/* The test creates NMClient instances in various ways (with different
* context of g_main_context_default()) and with sync/asnyc/no initialization.
*
* Then it calls nm_client_wait_shutdown() and checks that all callbacks
* got invoked.
*
* You can also manually bump N_RUN and convince yourself that there are no
* leaks.
*/
sinfo = nmtstc_service_init();
if (!nmtstc_service_available(sinfo))
return;
/* Have 5 contexts for creating clients (+ the default one). */
g_ptr_array_add(contexts, g_main_context_ref(g_main_context_default()));
for (i = 0; i < 5; i++)
g_ptr_array_add(contexts, g_main_context_new());
dbus_connection = g_bus_get_sync(_nm_dbus_bus_type(), NULL, &error);
nmtst_assert_success(dbus_connection, error);
for (i_run = 0; i_run < N_RUN; i_run++) {
gs_unref_object GCancellable *init_cancellable = g_cancellable_new();
gs_unref_object NMClient *nmc = NULL;
nm_auto_pop_gmaincontext GMainContext *client_context = NULL;
gboolean b;
gboolean context_integrated = FALSE;
gs_unref_object GCancellable *cancellable_1 = NULL;
GMainContext *ctx;
/* Choose a random context for the client. */
ctx = contexts->pdata[nmtst_get_rand_uint32() % contexts->len];
if (ctx == g_main_context_default()) {
/* don't use the main context explicitly. */
} else if (!g_main_context_acquire(ctx)) {
/* the context is in use, we cannot use it. */
} else {
g_main_context_release(ctx);
client_context = g_main_context_ref(ctx);
g_main_context_push_thread_default(client_context);
}
nmc = g_object_new(
NM_TYPE_CLIENT,
NM_CLIENT_INSTANCE_FLAGS,
nmtst_get_rand_one_case_in(5) ? NM_CLIENT_INSTANCE_FLAGS_NO_AUTO_FETCH_PERMISSIONS : 0,
NM_CLIENT_DBUS_CONNECTION,
nmtst_get_rand_one_case_in(3) ? NULL : dbus_connection,
NULL);
if (nmtst_get_rand_bool()) {
/* randomly already pop the context before initializing nmc. */
nm_clear_pointer(&client_context, nm_g_main_context_pop_and_unref);
}
if (nmtst_get_rand_bool()) {
nm_auto_pop_gmaincontext GMainContext *context =
nm_g_main_context_push_thread_default(NULL);
/* It is also allowed to start waiting before initialization started. Randomly do that. */
pending_ops++;
nm_client_wait_shutdown(
nmc,
nmtst_true_once(&context_integrated, nmtst_get_rand_bool()),
(cancellable_1 = nmtst_get_rand_bool() ? g_cancellable_new() : NULL),
_wait_shutdown_cb,
&pending_ops);
}
/* randomly do some initialization... */
switch (nmtst_get_rand_uint32() % 4) {
case 0:
/* Don't start initialization. */
break;
case 1:
/* Do sync initialization. */
b = g_initable_init(G_INITABLE(nmc),
nmtst_get_rand_bool() ? init_cancellable : NULL,
&error);
nmtst_assert_success(b, error);
break;
case 2:
/* Start async initialization in the background, but don't wait for it complete. */
g_async_initable_init_async(G_ASYNC_INITABLE(nmc),
G_PRIORITY_DEFAULT,
nmtst_get_rand_bool() ? init_cancellable : NULL,
NULL,
NULL);
break;
default:
{
/* Do async initialization, and iterate the main context until done. */
WaitShutdownNmcInitData data = {
.completed = NM_OPTION_BOOL_DEFAULT,
.nmc = nmc,
};
g_async_initable_init_async(G_ASYNC_INITABLE(nmc),
G_PRIORITY_DEFAULT,
nmtst_get_rand_bool() ? init_cancellable : NULL,
_wait_shutdown_nmc_init_cb,
&data);
while (data.completed != TRUE) {
g_main_context_iteration(context_integrated ? g_main_context_default()
: nm_client_get_main_context(nmc),
TRUE);
}
break;
}
}
if (!context_integrated && nmtst_get_rand_bool()) {
nm_clear_g_cancellable(&cancellable_1);
}
if (nmtst_get_rand_bool())
nm_clear_pointer(&client_context, nm_g_main_context_pop_and_unref);
{
gs_unref_object GCancellable *cancellable = NULL;
pending_ops++;
nm_client_wait_shutdown(
nmc,
nmtst_true_once(&context_integrated, nmtst_get_rand_bool()),
(cancellable = nmtst_get_rand_bool() ? g_cancellable_new() : NULL),
_wait_shutdown_cb,
&pending_ops);
}
if (nmtst_get_rand_bool())
g_clear_object(&nmc);
for (i = 0; i < (int) contexts->len; i++) {
GMainContext *context = contexts->pdata[i];
if (nmtst_get_rand_bool()) {
/* iterate next time. */
continue;
}
if (!g_main_context_acquire(context)) {
/* It's integrated, skip. */
continue;
}
g_main_context_release(context);
while (g_main_context_iteration(context, FALSE)) {
/* pass */
}
}
}
until_msec = nm_utils_get_monotonic_timestamp_msec() + 10000;
while (pending_ops > 0 && nm_utils_get_monotonic_timestamp_msec() < until_msec) {
for (i = contexts->len; i > 0; i--) {
GMainContext *context = contexts->pdata[i - 1];
if (!g_main_context_acquire(context)) {
/* integrated, skip. */
continue;
}
g_main_context_release(context);
while (g_main_context_iteration(context, FALSE)) {
/* pass */
}
}
}
g_assert_cmpint(pending_ops, ==, 0);
}
/*****************************************************************************/
NMTST_DEFINE();
int
@ -1369,6 +1606,7 @@ main(int argc, char **argv)
g_test_add_func("/libnm/activate-virtual", test_activate_virtual);
g_test_add_func("/libnm/device-connection-compatibility", test_device_connection_compatibility);
g_test_add_func("/libnm/connection/invalid", test_connection_invalid);
g_test_add_func("/libnm/test_client_wait_shutdown", test_client_wait_shutdown);
return g_test_run();
}

View file

@ -22,12 +22,21 @@ G_BEGIN_DECLS
* can be disabled. You can toggle this flag to enable and disable automatic
* fetching of the permissions. Watch also nm_client_get_permissions_state()
* to know whether the permissions are up to date.
* @NM_CLIENT_INSTANCE_FLAGS_INITIALIZED_GOOD: as #NMClient is an GInitable
* and GAsyncInitable, nm_client_get_instance_flags() returns this flag
* once initialization completed with success. This flag cannot be set
* as NM_CLIENT_INSTANCE_FLAGS property. Since: 1.42.
* @NM_CLIENT_INSTANCE_FLAGS_INITIALIZED_BAD: like @NM_CLIENT_INSTANCE_FLAGS_INITIALIZED_GOOD
* indicates that the instance completed initialization with failure. In that
* case the instance is unusable. Since: 1.42.
*
* Since: 1.24
*/
typedef enum /*< flags >*/ {
NM_CLIENT_INSTANCE_FLAGS_NONE = 0,
NM_CLIENT_INSTANCE_FLAGS_NO_AUTO_FETCH_PERMISSIONS = 1,
NM_CLIENT_INSTANCE_FLAGS_NO_AUTO_FETCH_PERMISSIONS = 0x1,
NM_CLIENT_INSTANCE_FLAGS_INITIALIZED_GOOD = 0x2,
NM_CLIENT_INSTANCE_FLAGS_INITIALIZED_BAD = 0x4,
} NMClientInstanceFlags;
#define NM_TYPE_CLIENT (nm_client_get_type())
@ -499,6 +508,18 @@ gboolean nm_utils_file_is_certificate(const char *filename);
gboolean nm_utils_file_is_private_key(const char *filename, gboolean *out_encrypted);
gboolean nm_utils_file_is_pkcs12(const char *filename);
/*****************************************************************************/
NM_AVAILABLE_IN_1_42
void nm_client_wait_shutdown(NMClient *client,
gboolean integrate_maincontext,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data);
NM_AVAILABLE_IN_1_42
gboolean nm_client_wait_shutdown_finish(GAsyncResult *result, GError **error);
G_END_DECLS
#endif /* __NM_CLIENT_H__ */

View file

@ -1191,6 +1191,28 @@ nmtst_get_rand_word_length(GRand *rand)
/*****************************************************************************/
static inline gboolean
nmtst_true_once(gboolean *state, gboolean new_val)
{
/* Returns only once a TRUE flag. When returning TRUE,
* it will be remembered in "state" and future invocations
* return FALSE.
*
* Also, if "new_val" is FALSE, it won't return TRUE.
*
* The point is to do an action once (depending on "new_val"),
* and remember it in "state".
*/
if (!new_val)
return FALSE;
if (*state)
return FALSE;
*state = TRUE;
return TRUE;
}
/*****************************************************************************/
static inline gboolean
nmtst_g_source_assert_not_called(gpointer user_data)
{