diff --git a/networkmanager_dmenu b/networkmanager_dmenu index e092396..be7fd59 100755 --- a/networkmanager_dmenu +++ b/networkmanager_dmenu @@ -810,6 +810,7 @@ def process_ap(nm_ap, is_active, adapter): nm_ap, ) LOOP.run() + wait_for_activation(adapter) else: sec = ap_security(nm_ap) if "802.1X" in sec: @@ -831,9 +832,11 @@ def activate_cb(dev, res, data): Finishing here only means NM took the request, the activation itself settles long after nmdm has exited, so do not claim it succeeded. """ + global ACTIVE_CONN try: conn = dev.activate_connection_finish(res) if conn is not None: + ACTIVE_CONN = conn notify(f"Connecting to {conn.get_id()}") else: notify( @@ -848,6 +851,73 @@ def activate_cb(dev, res, data): LOOP.quit() +def wait_for_device_activated(device, timeout=30): + """Block until the device has finished activating + + NetworkManager ties the activation of a connection's secondaries (the VPN + profiles listed in `connection.secondaries`) to the D-Bus client that + asked for it, so returning as soon as the activation request is accepted + leaves the VPN to be torn down again the moment nmdm exits. Stay on the + bus until the device reports ACTIVATED, the state it only reaches once + its secondaries are up. + + Args: device - NM.Device + timeout - seconds to wait before giving up + + """ + if device.get_state() == NM.DeviceState.ACTIVATED: + return + + timed_out = False + + def on_state_changed(dev, new_state, old_state, reason): + if new_state == NM.DeviceState.ACTIVATED: + LOOP.quit() + + def on_timeout(): + nonlocal timed_out + timed_out = True + LOOP.quit() + return GLib.SOURCE_REMOVE + + handler_id = device.connect("state-changed", on_state_changed) + timeout_id = GLib.timeout_add_seconds(timeout, on_timeout) + LOOP.run() + if not timed_out: + GLib.source_remove(timeout_id) + # NM.Device shadows GObject's `disconnect`, and its version disconnects + # the device from the network instead of the signal handler + device.handler_disconnect(handler_id) + if timed_out: + notify( + f"{device.get_iface()} did not finish activating", + f"Gave up waiting after {timeout} seconds", + urgency="critical", + ) + + +def wait_for_activation(device=None): + """Block until the activation just requested has finished + + `process_ap` and `set_new_connection` activate against an adapter and + pass it in. `process_vpngsm` has no adapter to pass, so take the device + from the connection NetworkManager just activated instead. + + Args: device - NM.Device to wait on, or None to use the device of the + connection that was activated + + """ + if ACTIVE_CONN is None: + # The request itself failed, there is nothing left to wait for + return + if device is None: + devices = ACTIVE_CONN.get_devices() + if not devices: + return + device = devices[0] + wait_for_device_activated(device) + + def deactivate_cb(dev, res, data): """Notification if deactivate connection completed successfully""" try: @@ -872,8 +942,12 @@ def process_vpngsm(con, activate): CLIENT.activate_connection_async( con, None, None, None, activate_cb, con ) - else: - CLIENT.deactivate_connection_async(con, None, deactivate_cb, con) + LOOP.run() + # Saved connections are activated without an adapter, so the device + # to wait on is whichever one NetworkManager picked + wait_for_activation() + return + CLIENT.deactivate_connection_async(con, None, deactivate_cb, con) LOOP.run() @@ -1432,6 +1506,7 @@ def set_new_connection(nm_ap, nm_pw, adapter): profile, adapter, nm_ap.get_path(), None, verify_conn, profile ) LOOP.run() + wait_for_activation(adapter) def create_wifi_profile(nm_ap, password, adapter): @@ -1499,6 +1574,7 @@ def verify_conn(client, result, data): is an error. """ + global ACTIVE_CONN conn = None try: act_conn = client.add_and_activate_connection_finish(result) @@ -1524,6 +1600,7 @@ def verify_conn(client, result, data): conn.delete_async(None, cleanup_cb, None) return else: + ACTIVE_CONN = act_conn notify(f"Connecting to {conn.get_id()}") LOOP.quit() @@ -1709,7 +1786,8 @@ def run(): # pylint: disable=too-many-locals def main(): """Main. Enables script to be re-run after a WiFi rescan""" - global CLIENT, CONNS, LOOP + global CLIENT, CONNS, LOOP, ACTIVE_CONN + ACTIVE_CONN = None CLIENT = NM.Client.new(None) LOOP = GLib.MainLoop() CONNS = CLIENT.get_connections()