This is an automated email from the git hooks/post-receive script.

git pushed a commit to reference refs/pull/133/head
in repository enlightenment.

View the commit online.

commit d1e86725b4215ece8b352b3bd3715cdaa3af43bb
Author: [email protected] <[email protected]>
AuthorDate: Sun May 17 18:01:03 2026 -0600

    fix(networkmanager): adopt active connection on cold start with late property arrival
    
    SYMPTOM
    On a cold start of Enlightenment (not a restart), if already connected via
    NetworkManager, the shelf gadget shows a bare default icon instead of the
    full connection icon (signal band, lock overlay, traffic). Restarting E in
    the same session fixes it, indicating the race is initialization-time only.
    
    ROOT CAUSE
    Active-connection and active-AP adoption was one-shot at module startup with
    no retry mechanism. The _device_prop_changed handler only monitored the State
    key. On cold start, NetworkManager/D-Bus frequently returns ActiveConnection="/"
    and ActiveAccessPoint="/" in the initial Device.GetAll call; the real object
    paths arrive moments later via PropertiesChanged signals. Since the device was
    already fully connected, no State transition fired to retrigger adoption. The
    one-shot adoption guard (dev->state < 100 || !ap_path || !active_conn_path)
    never re-evaluated, leaving the connection unadopted until the next E restart
    (which works because warm D-Bus returns consistent properties immediately).
    
    FIX
    Make adoption re-evaluable instead of one-shot:
    
    1. Factor WiFi and Ethernet adoption logic into _try_wifi_adopt() and
       _try_ethernet_adopt() helpers, preserving all existing semantics:
       probe_generation bump, watcher setup/teardown, "first active device wins".
    
    2. Update _device_prop_changed to also handle the ActiveConnection key and
       trigger _try_*_adopt() when state or active-connection path changes.
    
    3. Add _device_wifi_prop_changed handler on the wireless proxy to track
       ActiveAccessPoint changes (persisted in new NM_Device.active_ap_path field).
       Snapshot adoption state to distinguish cold-start adoption from genuine
       roam, ensuring exactly one gadget refresh.
    
    4. Add _notify_manager_update call in _device_get_aps_cb after AP list
       population, guarded by nm_manager existence check. This handles the
       secondary race where AP-list completion races ahead of adoption,
       preventing blank gadgets when properties are populated late.
    
    5. Add NM_Device.wifi_prop_changed_handler and active_ap_path fields, freed
       in _device_free before wireless_proxy unref (consistent with existing
       ap_added/removed handler cleanup).
    
    VERIFICATION
    Code-level state-machine argument: the refactored helpers close all
    previously-failing property-arrival orderings (ActiveConnection or
    ActiveAccessPoint arriving after initial GetAll, in any sequence).
    No double-adoption due to "first wins" guard. No eldbus leak/UAF; signal
    handlers are properly registered and freed in device teardown. Normal warm
    path and ethernet path unaffected.
    
    Verified via code review (approved). The race itself is timing-dependent and
    not reproducibly triggered in a harness; verification is state-machine
    argument plus implementation review.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---
 src/modules/networkmanager/e_networkmanager.c | 321 +++++++++++++++++++-------
 src/modules/networkmanager/e_networkmanager.h |  10 +-
 2 files changed, 247 insertions(+), 84 deletions(-)

diff --git a/src/modules/networkmanager/e_networkmanager.c b/src/modules/networkmanager/e_networkmanager.c
index c36736c49..9a49488a0 100644
--- a/src/modules/networkmanager/e_networkmanager.c
+++ b/src/modules/networkmanager/e_networkmanager.c
@@ -327,6 +327,8 @@ static void _manager_active_conn_watch_free(struct NM_Manager *nm);
 static void _manager_ip4_watch_free(struct NM_Manager *nm);
 static void _manager_watch_ip4(struct NM_Manager *nm,
                                const char *ip4config_path);
+static void _try_wifi_adopt(struct NM_Device *dev);
+static void _try_ethernet_adopt(struct NM_Device *dev);
 
 static void
 _device_get_aps_cb(void *data, const Eldbus_Message *msg,
@@ -359,7 +361,18 @@ _device_get_aps_cb(void *data, const Eldbus_Message *msg,
         DBG("Added AP %s to device %s", ap_path, dev->path);
      }
 
+   /* Guard: nm_manager may have been freed if NM exited while GetAccessPoints
+    * was in flight.  Mirror the pattern in _device_wifi_props_cb. */
+   if (!nm_manager) return;
+
    _notify_aps_changed(nm_manager);
+
+   /* If the manager already has an active AP, the gadget should be refreshed
+    * now that AP properties (strength, security, frequency) are populated.
+    * This covers the secondary race where AP-list completion races ahead of
+    * the adoption probe. */
+   if (nm_manager->active_ap_path)
+     _notify_manager_update(nm_manager);
 }
 
 static void
@@ -423,6 +436,7 @@ _device_prop_changed(void *data, const Eldbus_Message *msg)
    struct NM_Device *dev = data;
    Eldbus_Message_Iter *changed_props, *invalidated, *dict, *var;
    const char *iface, *key;
+   Eina_Bool try_adopt = EINA_FALSE;
 
    if (!eldbus_message_arguments_get(msg, "sa{sv}as",
                                      &iface, &changed_props, &invalidated))
@@ -441,6 +455,205 @@ _device_prop_changed(void *data, const Eldbus_Message *msg)
                   dev->state = state;
                   DBG("Device %s state -> %u", dev->path, state);
                   _notify_manager_update(nm_manager);
+                  try_adopt = EINA_TRUE;
+               }
+          }
+        else if (!strcmp(key, "ActiveConnection"))
+          {
+             /* On cold start NM may report ActiveConnection="/" in the initial
+              * Device.GetAll and only populate it later via PropertiesChanged.
+              * Capture it here so that _try_*_adopt can succeed on retry. */
+             const char *aconn_path;
+             if (eldbus_message_iter_arguments_get(var, "o", &aconn_path))
+               {
+                  if (aconn_path && strcmp(aconn_path, "/") != 0)
+                    {
+                       free(dev->active_conn_path);
+                       dev->active_conn_path = strdup(aconn_path);
+                       DBG("Device %s ActiveConnection -> %s", dev->path, aconn_path);
+                    }
+                  else
+                    {
+                       free(dev->active_conn_path);
+                       dev->active_conn_path = NULL;
+                    }
+                  try_adopt = EINA_TRUE;
+               }
+          }
+     }
+
+   /* Re-attempt adoption when state or active-connection path changes.
+    * This closes the cold-start race where the initial Device.GetAll returned
+    * ActiveConnection="/" before NM had settled, so the one-shot
+    * _device_wifi_props_cb / _device_get_props_cb adoption attempt failed.
+    *
+    * Skip if nm_manager already has an adopted connection — the "first wins"
+    * guard in the helpers would no-op anyway, but this avoids a redundant
+    * WiFi GetAll on every State change during normal operation. */
+   if (try_adopt && nm_manager && !nm_manager->active_connection_path &&
+       dev->state >= 100 && dev->active_conn_path)
+     {
+        if (dev->type == NM_DEVICE_TYPE_ETHERNET)
+          _try_ethernet_adopt(dev);
+        else if (dev->type == NM_DEVICE_TYPE_WIFI)
+          {
+             /* Re-issue WiFi GetAll only if one is not already pending.
+              * _device_wifi_props_cb stores the result in dev->active_ap_path
+              * and calls _try_wifi_adopt(). */
+             if (!dev->pending.get_wifi_props)
+               dev->pending.get_wifi_props =
+                  eldbus_proxy_call(dev->proxy, "GetAll",
+                                    _device_wifi_props_cb, dev,
+                                    -1, "s", NM_IFACE_WIFI);
+          }
+     }
+}
+
+/* -------------------------------------------------------------------------- */
+/* Active-connection adoption helpers                                          */
+/*                                                                             */
+/* Factored out of _device_get_props_cb (Ethernet) and _device_wifi_props_cb  */
+/* (WiFi) so that _device_prop_changed can re-attempt adoption when a         */
+/* cold-start race caused the initial one-shot attempt to fail.               */
+/* -------------------------------------------------------------------------- */
+
+static void
+_try_ethernet_adopt(struct NM_Device *dev)
+{
+   if (!nm_manager) return;
+   if (dev->state < 100 || !dev->active_conn_path) return;
+
+   /* First active device wins — skip if another already adopted. */
+   if (nm_manager->active_connection_path) return;
+
+   DBG("Ethernet device %s adopting connection %s (state=%u)",
+       dev->path, dev->active_conn_path, dev->state);
+
+   nm_manager->probe_generation++;
+   _manager_active_conn_watch_free(nm_manager);
+   _manager_ip4_watch_free(nm_manager);
+
+   eina_stringshare_replace(&nm_manager->active_connection_path,
+                            dev->active_conn_path);
+   nm_manager->active_conn_type = NM_DEVICE_TYPE_ETHERNET;
+   eina_stringshare_replace(&nm_manager->active_ap_path, NULL);
+
+   {
+      Eldbus_Object *aobj =
+         eldbus_object_get(conn, NM_BUS_NAME, dev->active_conn_path);
+      nm_manager->active_conn_obj = aobj;
+      nm_manager->active_conn_proxy = eldbus_proxy_get(aobj, NM_IFACE_PROPS);
+      nm_manager->active_conn_signal_handler =
+         eldbus_proxy_signal_handler_add(nm_manager->active_conn_proxy,
+                                         "PropertiesChanged",
+                                         _active_conn_prop_changed,
+                                         nm_manager);
+   }
+
+   if (dev->ip4_path)
+     _manager_watch_ip4(nm_manager, dev->ip4_path);
+
+   _notify_manager_update(nm_manager);
+}
+
+static void
+_try_wifi_adopt(struct NM_Device *dev)
+{
+   if (!nm_manager) return;
+   if (dev->state < 100 || !dev->active_ap_path || !dev->active_conn_path)
+     return;
+
+   /* First active device wins — skip if another already adopted. */
+   if (nm_manager->active_connection_path) return;
+
+   DBG("WiFi device %s adopting conn=%s ap=%s (state=%u)",
+       dev->path, dev->active_conn_path, dev->active_ap_path, dev->state);
+
+   nm_manager->probe_generation++;
+   _manager_active_conn_watch_free(nm_manager);
+   _manager_ip4_watch_free(nm_manager);
+
+   nm_manager->active_conn_type = NM_DEVICE_TYPE_WIFI;
+   eina_stringshare_replace(&nm_manager->active_ap_path, dev->active_ap_path);
+   eina_stringshare_replace(&nm_manager->active_connection_path,
+                            dev->active_conn_path);
+
+   {
+      Eldbus_Object *aobj =
+         eldbus_object_get(conn, NM_BUS_NAME, dev->active_conn_path);
+      nm_manager->active_conn_obj = aobj;
+      nm_manager->active_conn_proxy = eldbus_proxy_get(aobj, NM_IFACE_PROPS);
+      nm_manager->active_conn_signal_handler =
+         eldbus_proxy_signal_handler_add(nm_manager->active_conn_proxy,
+                                         "PropertiesChanged",
+                                         _active_conn_prop_changed,
+                                         nm_manager);
+   }
+
+   if (dev->ip4_path)
+     _manager_watch_ip4(nm_manager, dev->ip4_path);
+
+   _notify_manager_update(nm_manager);
+   enm_saved_connections_get(nm_manager);
+}
+
+/* Handle PropertiesChanged on the Device.Wireless interface.
+ * Watches for ActiveAccessPoint changes that may arrive after the initial
+ * Device.Wireless.GetAll callback has already run (another cold-start window:
+ * NM returns ActiveAccessPoint="/" in GetAll but sets it shortly after). */
+static void
+_device_wifi_prop_changed(void *data, const Eldbus_Message *msg)
+{
+   struct NM_Device *dev = data;
+   Eldbus_Message_Iter *changed_props, *invalidated, *dict, *var;
+   const char *iface, *key;
+
+   if (!eldbus_message_arguments_get(msg, "sa{sv}as",
+                                     &iface, &changed_props, &invalidated))
+     return;
+
+   while (eldbus_message_iter_get_and_next(changed_props, 'e', &dict))
+     {
+        if (!eldbus_message_iter_arguments_get(dict, "sv", &key, &var))
+          continue;
+
+        if (!strcmp(key, "ActiveAccessPoint"))
+          {
+             const char *path;
+             if (eldbus_message_iter_arguments_get(var, "o", &path))
+               {
+                  free(dev->active_ap_path);
+                  if (path && strcmp(path, "/") != 0)
+                    {
+                       dev->active_ap_path = strdup(path);
+                       DBG("Device %s ActiveAccessPoint -> %s", dev->path, path);
+                    }
+                  else
+                    dev->active_ap_path = NULL;
+
+                  /* Snapshot whether this device is ALREADY the adopted device
+                   * before attempting adoption.  If it was already adopted the
+                   * AP change is a genuine roam and must refresh the gadget.
+                   * If it was not yet adopted, _try_wifi_adopt() below will
+                   * call _notify_manager_update() itself — running the roam
+                   * path on top of that would be a spurious double update. */
+                  Eina_Bool was_adopted = (nm_manager && dev->active_conn_path &&
+                                           nm_manager->active_connection_path &&
+                                           !strcmp(nm_manager->active_connection_path,
+                                                   dev->active_conn_path));
+
+                  /* Re-attempt adoption if not yet done — this closes the window
+                   * where NM emits AP after the initial GetAll returned "/". */
+                  _try_wifi_adopt(dev);
+
+                  /* Refresh the gadget only for a genuine roam (device was
+                   * already the adopted device before this AP change). */
+                  if (was_adopted && nm_manager && dev->active_ap_path)
+                    {
+                       eina_stringshare_replace(&nm_manager->active_ap_path,
+                                                dev->active_ap_path);
+                       _notify_manager_update(nm_manager);
+                    }
                }
           }
      }
@@ -527,7 +740,9 @@ _device_get_props_cb(void *data, const Eldbus_Message *msg,
      {
         Eldbus_Object *obj = eldbus_proxy_object_get(dev->proxy);
 
-        /* Re-get object to get the wireless interface proxy */
+        /* Re-get object to get the wireless interface proxy.
+         * eldbus_proxy_get() does NOT increment the object refcount — the
+         * object ref is already held by dev->proxy's parent object. */
         dev->wireless_proxy = eldbus_proxy_get(obj, NM_IFACE_WIFI);
 
         dev->ap_added_handler =
@@ -538,6 +753,14 @@ _device_get_props_cb(void *data, const Eldbus_Message *msg,
                                            "AccessPointRemoved",
                                            _device_ap_removed, dev);
 
+        /* Watch for ActiveAccessPoint changes on the WiFi interface.
+         * This is the signal that fires when NM settles its AP association
+         * after the initial GetAll returned "/" (cold-start race). */
+        dev->wifi_prop_changed_handler =
+           eldbus_proxy_signal_handler_add(dev->wireless_proxy,
+                                           "PropertiesChanged",
+                                           _device_wifi_prop_changed, dev);
+
         dev->pending.get_aps = eldbus_proxy_call(dev->wireless_proxy,
                                                   "GetAccessPoints",
                                                   _device_get_aps_cb, dev,
@@ -551,51 +774,11 @@ _device_get_props_cb(void *data, const Eldbus_Message *msg,
                              _device_wifi_props_cb, dev,
                              -1, "s", NM_IFACE_WIFI);
      }
-   else if (dev->type == NM_DEVICE_TYPE_ETHERNET && dev->state >= 100 &&
-            dev->active_conn_path)
+   else if (dev->type == NM_DEVICE_TYPE_ETHERNET)
      {
-        /* Guard: nm_manager may have been freed if NM exited while this
-         * D-Bus reply was in flight. */
-        if (!nm_manager) return;
-
-        /* First active device wins — skip if another already adopted. */
-        if (nm_manager->active_connection_path) return;
-
-        /* Ethernet device is active — set manager state directly without
-         * an extra ActiveConnection.GetAll round-trip. */
-        DBG("Ethernet device %s active (state=%u), adopting connection %s",
-            dev->path, dev->state, dev->active_conn_path);
-
-        nm_manager->probe_generation++;
-        _manager_active_conn_watch_free(nm_manager);
-        _manager_ip4_watch_free(nm_manager);
-
-        eina_stringshare_replace(&nm_manager->active_connection_path,
-                                 dev->active_conn_path);
-
-        nm_manager->active_conn_type = NM_DEVICE_TYPE_ETHERNET;
-
-        eina_stringshare_replace(&nm_manager->active_ap_path, NULL);
-
-        /* Set up persistent watcher on the active connection object */
-        {
-           Eldbus_Object *aobj =
-              eldbus_object_get(conn, NM_BUS_NAME, dev->active_conn_path);
-           nm_manager->active_conn_obj = aobj;
-           nm_manager->active_conn_proxy = eldbus_proxy_get(aobj,
-                                                            NM_IFACE_PROPS);
-           nm_manager->active_conn_signal_handler =
-              eldbus_proxy_signal_handler_add(nm_manager->active_conn_proxy,
-                                              "PropertiesChanged",
-                                              _active_conn_prop_changed,
-                                              nm_manager);
-        }
-
-        if (dev->ip4_path)
-          _manager_watch_ip4(nm_manager, dev->ip4_path);
-
-        _notify_manager_update(nm_manager);
-        /* Saved connections are WiFi-specific — skip for Ethernet */
+        /* Attempt adoption via the factored helper — it checks state>=100 and
+         * active_conn_path internally, and handles the "first wins" guard. */
+        _try_ethernet_adopt(dev);
      }
 }
 
@@ -647,43 +830,13 @@ _device_wifi_props_cb(void *data, const Eldbus_Message *msg,
    DBG("Device %s ActiveAccessPoint=%s state=%u",
        dev->path, ap_path ?: "(none)", dev->state);
 
-   /* Only adopt this device as the active connection if it is in activated
-    * state (>= 100) and has an active AP. */
-   if (dev->state < 100 || !ap_path || !dev->active_conn_path)
-     return;
+   /* Persist the active AP path on the device so that _try_wifi_adopt() can
+    * be called from multiple entry points (here, _device_wifi_prop_changed)
+    * without re-issuing GetAll. */
+   free(dev->active_ap_path);
+   dev->active_ap_path = ap_path ? strdup(ap_path) : NULL;
 
-   /* First active device wins — skip if another already adopted. */
-   if (nm_manager->active_connection_path)
-     return;
-
-   nm_manager->probe_generation++;
-   _manager_active_conn_watch_free(nm_manager);
-   _manager_ip4_watch_free(nm_manager);
-
-   nm_manager->active_conn_type = NM_DEVICE_TYPE_WIFI;
-
-   eina_stringshare_replace(&nm_manager->active_ap_path, ap_path);
-   eina_stringshare_replace(&nm_manager->active_connection_path,
-                            dev->active_conn_path);
-
-   /* Set up persistent watcher on the active connection object */
-   {
-      Eldbus_Object *aobj =
-         eldbus_object_get(conn, NM_BUS_NAME, dev->active_conn_path);
-      nm_manager->active_conn_obj = aobj;
-      nm_manager->active_conn_proxy = eldbus_proxy_get(aobj, NM_IFACE_PROPS);
-      nm_manager->active_conn_signal_handler =
-         eldbus_proxy_signal_handler_add(nm_manager->active_conn_proxy,
-                                         "PropertiesChanged",
-                                         _active_conn_prop_changed,
-                                         nm_manager);
-   }
-
-   if (dev->ip4_path)
-     _manager_watch_ip4(nm_manager, dev->ip4_path);
-
-   _notify_manager_update(nm_manager);
-   enm_saved_connections_get(nm_manager);
+   _try_wifi_adopt(dev);
 }
 
 static struct NM_Device *
@@ -738,10 +891,16 @@ _device_free(struct NM_Device *dev)
 
    free(dev->interface);
    free(dev->active_conn_path);
+   free(dev->active_ap_path);
    free(dev->ip4_path);
 
    if (dev->wireless_proxy)
      {
+        if (dev->wifi_prop_changed_handler)
+          {
+             eldbus_signal_handler_del(dev->wifi_prop_changed_handler);
+             dev->wifi_prop_changed_handler = NULL;
+          }
         if (dev->ap_added_handler)
           {
              eldbus_signal_handler_del(dev->ap_added_handler);
diff --git a/src/modules/networkmanager/e_networkmanager.h b/src/modules/networkmanager/e_networkmanager.h
index c98d68c3e..788318346 100644
--- a/src/modules/networkmanager/e_networkmanager.h
+++ b/src/modules/networkmanager/e_networkmanager.h
@@ -112,6 +112,7 @@ struct NM_Device
    EINA_INLIST;
 
    Eldbus_Signal_Handler *prop_changed_handler;
+   Eldbus_Signal_Handler *wifi_prop_changed_handler; /* PropertiesChanged on wireless_proxy */
    Eldbus_Signal_Handler *ap_added_handler;
    Eldbus_Signal_Handler *ap_removed_handler;
 
@@ -119,9 +120,12 @@ struct NM_Device
    enum NM_Device_Type type;
    uint32_t            state;
 
-   /* Transient paths read from Device.GetAll — used during startup probe
-    * to avoid an extra ActiveConnection.GetAll round-trip. */
-   char *active_conn_path; /* ActiveConnection object path */
+   /* Transient paths read from Device.GetAll / Device.Wireless.GetAll — kept
+    * up-to-date via PropertiesChanged so that re-adoption can be attempted
+    * whenever any of the three conditions (state>=100, active_conn_path,
+    * active_ap_path) first becomes satisfied after a cold-start miss. */
+   char *active_conn_path; /* ActiveConnection object path (Device iface) */
+   char *active_ap_path;   /* ActiveAccessPoint object path (Wireless iface) */
    char *ip4_path;         /* Ip4Config object path */
 
    Eina_Inlist  *access_points; /* NM_Access_Point inlist, WiFi only */

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.

Reply via email to