libnm: add nm_client_wait_shutdown() function for cleaning up NMClient

Add a fire-and-forget function to wait for shutdown to be complete.

It's not entirely trivial to ensure all resources of NMClient are
cleaned up. That matters only if NMClient uses a temporary GMainContext
that the user wants to release while the application continues. For
example, to do some short-lived operations an a worker thread. It's
not trivial also because glib provides no convenient API to integrate
a GMainContext in another GMainContext. We have that code as
nm_utils_g_main_context_create_integrate_source(), so add a helper
function to allow the user to do this.

The function allows to omit the callback, in which case the caller
wouldn't know when shutdown is complete. That would still be useful
however, when integrating the client's context into the caller's
context, so that the client's context gets automatically iterated
until completion.

The following test script will run out of file descriptors,
when wait_shutdown() is not used:

   #!/bin/python

   import gi

   gi.require_version("NM", "1.0")
   from gi.repository import NM, GLib

   for i in range(1200):
       print(f">>>{i}")

       ctx = GLib.MainContext()
       ctx.push_thread_default()
       nmc = NM.Client.new()
       ctx.pop_thread_default()

       def cb(unused, result, i):
           try:
               NM.Client.wait_shutdown_finish(result)
           except Exception:
               # cannot happen
               assert False
           else:
               print(f">>>>> {i} complete")

       nmc.wait_shutdown(True, None, cb, i)

       while GLib.MainContext.default().iteration(False):
           pass
This commit is contained in:
Thomas Haller
2022-10-05 12:22:51 +02:00
parent 2f5a2dc732
commit 88724ff169
5 changed files with 618 additions and 42 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: