1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474
|
#!/bin/python
###############################################################################
# An example that creates a NMClient instance for another GMainContext
# and iterates the context while doing an async D-Bus call.
#
# D-Bus is fundamentally async. libnm's NMClient API caches D-Bus objects
# on NetworkManager's D-Bus API. As such, it is "frozen" (with the current
# content of the cache) while not iterating the GMainContext. Only by iterating
# the GMainContext any events are processed and things change.
#
# This means, NMClient heavily uses GMainContext (and GDBusConnection)
# and to operate it, you need to iterate the GMainContext. The synchronous
# API (like NM.Client.new()) is for simple programs but usually not best
# for using NMClient for real applications.
#
# To learn more about GMainContext, read https://developer.gnome.org/documentation/tutorials/main-contexts.html
# When I say "mainloop" or "event loop", I mean GMainContext. GMainLoop is
# a small wrapper around GMainContext to run the context with a boolean
# flag.
#
# Usually, non trivial applications run the GMainContext (or GMainLoop)
# from the main() function and aside some setup and teardown, everything
# happens as events from the event loop.
# This example instead performs synchronous steps, and at the places where
# we need to get the result of some async operation, we iterate the GMainContext
# until we get the result. This may not be how a complex application works,
# but you might do this on a simpler application (like a script) that iterates
# the mainloop whenever it needs to wait for async operations to complete.
#
# Iterating the mainloop might dispatch any other sources that are ready.
# In this example nobody else is scheduling unrelated timers or events, but
# if that happens, your application needs to cope with that.
# E.g. while iterating the mainloop many times, still don't nest running the
# same main context (unless you really know what you do).
###############################################################################
import sys
import time
import traceback
import gi
gi.require_version("NM", "1.0")
from gi.repository import NM, GLib, Gio
###############################################################################
def log(msg=None, prefix=None, suffix="\n"):
# We use nm_utils_print(), because that uses the same logging
# mechanism as if you run with "LIBNM_CLIENT_DEBUG=trace". This
# ensures that messages are in sync.
if msg is None:
NM.utils_print(0, "\n")
return
if prefix is None:
prefix = f"[{time.monotonic():.5f}] "
NM.utils_print(0, f"{prefix}{msg}{suffix}")
def error_is_cancelled(e):
# Whether error is due to cancellation.
if isinstance(e, GLib.GError):
if e.domain == "g-io-error-quark" and e.code == Gio.IOErrorEnum.CANCELLED:
return True
return False
###############################################################################
# A Context manager for running a mainloop. Of course, this does
# not do anything magically. You can run the context/mainloop without
# this context object.
#
# This is just to show how we could iterate the GMainContext while waiting
# for an async reply. Note that many non-trivial applications that use glib
# would instead run the mainloop from the main function, only running it once,
# but for the entire duration of the program.
#
# This example and MainLoopRun instead assume that you iterate the maincontext
# for short durations at a time. In particular in this case, where there is
# a dedicated maincontext only for NMClient.
class MainLoopRun:
def __init__(self, info, ctx, timeout=None):
self._info = info
self._loop = GLib.MainLoop(ctx)
self.cancellable = Gio.Cancellable()
self._timeout = timeout
self.got_timeout = False
self.result = None
self.error = None
log(f"MainLoopRun[{self._info}]: create with timeout {self._timeout}")
def _timeout_cb(self, _):
log(f"MainLoopRun[{self._info}]: timeout")
self.got_timeout = True
self._detach()
self.cancellable.cancel()
return False
def _cancellable_cb(self):
log(f"MainLoopRun[{self._info}]: cancelled")
def _detach(self):
if self._timeout_source is not None:
self._timeout_source.destroy()
self._timeout_source = None
if self._cancellable_id is not None:
self.cancellable.disconnect(self._cancellable_id)
self._cancellable_id = None
def __enter__(self):
log(f"MainLoopRun[{self._info}]: enter")
self._timeout_source = None
if self._timeout is not None:
self._timeout_source = GLib.timeout_source_new(int(self._timeout * 1000))
self._timeout_source.set_callback(self._timeout_cb)
self._timeout_source.attach(self._loop.get_context())
self._cancellable_id = self.cancellable.connect(self._cancellable_cb)
self._loop.get_context().push_thread_default()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
# Exception happened.
log(f"MainLoopRun[{self._info}]: exit with exception")
else:
log(f"MainLoopRun[{self._info}]: exit: start mainloop")
self._loop.run()
if self.error is not None:
log(
f"MainLoopRun[{self._info}]: exit: complete with error {self.error}"
)
elif self.result is not None:
log(
f"MainLoopRun[{self._info}]: exit: complete with result {self.result}"
)
else:
log(f"MainLoopRun[{self._info}]: exit: complete with success")
self._detach()
self._loop.get_context().pop_thread_default()
return False
def quit(self):
log(f"MainLoopRun[{self._info}]: quit mainloop")
self._detach()
self._loop.quit()
###############################################################################
def get_bus():
# Let's get the GDBusConnection singleton by calling Gio.bus_get().
# Since we do everything async, use Gio.bus_get() instead Gio.bus_get_sync().
with MainLoopRun("get_bus", None, 1) as r:
def bus_get_cb(source, result, r):
try:
c = Gio.bus_get_finish(result)
except Exception as e:
r.error = e
else:
r.result = c
r.quit()
Gio.bus_get(Gio.BusType.SYSTEM, r.cancellable, bus_get_cb, r)
return r.result
###############################################################################
def create_nmc(dbus_connection):
# Show how to create and initialize a NMClient asynchronously.
#
# NMClient implements GAsyncInitableIface, it thus can be initialized
# asynchronously. That has actually an advantage, because the sync
# initialization (GInitableIface) requires to create an internal GMainContext
# which has an overhead.
#
# Also, split the GObject creation and the init_async() call in two.
# That allows to pass construct-only parameters, in particular like
# the instance_flags.
# Create a separate context for the NMClient. The NMClient is strongly
# tied to the context used at construct time.
ctx = GLib.MainContext()
ctx.push_thread_default()
log(f"[create_nmc]: use separate context for NMClient: ctx={ctx}")
try:
# We create a client asynchronously. There is synchronous
# NM.Client(), however that requires an internal GMainContext
# and has thus an overhead. Also, it's obviously blocking.
#
# Instead, we initialize it asynchronously, which means
# we need to iterate the main context. In this case, the
# context cannot have any other sources dispatched, but
# if there would be other sources, they might be dispatched
# while iterating (so this is waiting for the result, but
# may also dispatch unrelated sources (if any), which you would need
# to handle).
#
# Also, only when using the GObject constructor directly, we can
# suppress loading the permissions and pass a D-Bus connection.
nmc = NM.Client(
instance_flags=NM.ClientInstanceFlags.NO_AUTO_FETCH_PERMISSIONS,
dbus_connection=dbus_connection,
)
log(f"[create_nmc]: new NMClient instance: {nmc}")
finally:
# We actually don't need that the ctx is the current thread default
# later on. NMClient will automatically push it, when necessary.
ctx.pop_thread_default()
with MainLoopRun("create_mnc", nmc.get_main_context(), 2) as r:
def _async_init_cb(nmc, result, r):
try:
nmc.init_finish(result)
except Exception as e:
log(f"[create_nmc]: init_async() completed with error: {e}")
r.error = e
else:
log(f"[create_nmc]: init_async() completed with success")
r.quit()
log(f"[create_nmc]: start init_async()")
nmc.init_async(GLib.PRIORITY_DEFAULT, r.cancellable, _async_init_cb, r)
if r.error is None:
if nmc.get_nm_running():
log(
f"[create_nmc]: completed with success (daemon version: {nmc.get_version()}, D-Bus daemon unique name: {nmc.get_dbus_name_owner()})"
)
else:
log(f"[create_nmc]: completed with success (daemon not running)")
return nmc
if error_is_cancelled(r.error):
# Cancelled by us. This happened because we hit the timeout with
# MainLoopRun.
log(f"[create_nmc]: failed to initialize within timeout")
return None
if not nmc.get_dbus_connection():
# The NMClient has no D-Bus connection, it usually would try
# to get one via Gio.bus_get(), but it failed.
log(f"[create_nmc]: failed to create D-Bus connection: {r.error}")
return None
log(f"[create_nmc]: unexpected error creating NMClient ({r.error})")
# This actually should not happen. There is no other reason why
# initialization can fail.
assert False, "NMClient initialization is not supposed to fail"
###############################################################################
def make_call(nmc):
log("[make_call]: make some async D-Bus call")
if not nmc:
log("[make_call]: no NMClient. Skip")
return
with MainLoopRun("make_call", nmc.get_main_context(), 1) as r:
# There are two reasons why async operations are preferable with
# D-Bus and libnm:
#
# - pseudo blocking messes with the ordering of events (see https://smcv.pseudorandom.co.uk/2008/11/nonblocking/).
# - blocking prevents other things from happening and combining synchronous calls is more limited.
#
# So doing async operations is mostly interesting when performing multiple operations in
# parallel, or when we still want to handle other events while waiting for the reply.
# The example here does not cover that usage well, because there is only one thing happening.
def _dbus_call_cb(nmc, result, r):
try:
res = nmc.dbus_call_finish(result)
except Exception as e:
if error_is_cancelled(e):
log(
f"[make_call]: dbus_call() completed with cancellation after timeout"
)
else:
log(f"[make_call]: dbus_call() completed with error: {e}")
# I don't understand why, but if you hit this exception (e.g. by setting a low
# timeout) and pass the exception to the out context, then an additional reference
# to nmc is leaked, and destroy_nmc() will fail. Workaround
#
# r.error = e
r.error = str(e)
else:
log(
f"[make_call]: dbus_call() completed with success: {str(res)[:40]}..."
)
r.quit()
log(f"[make_call]: start GetPermissions call")
nmc.dbus_call(
NM.DBUS_PATH,
NM.DBUS_INTERFACE,
"GetPermissions",
GLib.Variant.new_tuple(),
GLib.VariantType("(a{ss})"),
1000,
r.cancellable,
_dbus_call_cb,
r,
)
return r.error is None
###############################################################################
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.
# While unrefing NMClient will cancel them right away, they are only
# reaped when we iterate the GMainContext some more. That means, if we don't
# want to leak the GMainContext and the pending operations, we must
# iterate it some more.
#
# To know how much more, there is nmc.get_context_busy_watcher(),
# We can subscribe a weak reference and keep iterating as long
# as the watcher is alive.
#
# Of course, this only applies if the application wishes to keep running
# but no longer iterating NMClient's GMainContext. Then you need to ensure
# that all pending operations in GMainContext are completed (by iterating it).
#
# In python, that is a bit tricky, because the caller of destroy_nmc()
# must give up its reference and pass it here via the @nmc_holder list.
# You must call destroy_nmc() without having any other reference on
# nmc.
#
# This is just an example. This relies that on this point we only have
# one reference to NMClient (and it's held by the nmc_holder list).
# Usually you wouldn't make assumptions about this. Instead, you just
# assume that you need to keep iterating the GMainContext as long as
# the context busy watcher is alive, regardless that at this point others
# might still hold references on the NMClient.
# Transfer the nmc reference out of the list.
(nmc,) = nmc_holder
nmc_holder.clear()
log(
f"[destroy_nmc]: destroying NMClient {nmc}: pyref={sys.getrefcount(nmc)}, ref_count={nmc.ref_count}, destroy_mode={destroy_mode}"
)
if destroy_mode == 0:
ctx = nmc.get_main_context()
finished = []
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
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)
while not finished:
log(f"[destroy_nmc]: iterating main context")
ctx.iteration(True)
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")
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
###############################################################################
def run1():
try:
dbus_connection = get_bus()
log()
nmc = create_nmc(dbus_connection)
log()
make_call(nmc)
log()
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:
log()
log("EXCEPTION:")
log(f"{e}")
for tb in traceback.format_exception(None, e, e.__traceback__):
for l in tb.split("\n"):
log(f">>> {l}")
return False
return True
if __name__ == "__main__":
if not run1():
sys.exit(1)
|