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
475
476
|
#!/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/SearchProvider/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)
|