From 1b5dd32c0bfc9c3e4569c25c393579f4e08e706f Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 9 Apr 2026 10:58:47 +0700 Subject: [PATCH] manager: track Adapter objects + clear list on disable Promote Adapter to a first-class manager object (Iwd_Adapter with PropertiesChanged subscription). iwd_manager_set_powered now drives the adapter directly, so Enable still works after Disable has torn down the device hash. State recomputation also looks at any powered adapter, and the popup hides the network list while state == IWD_STATE_OFF. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/e_mod_popup.c | 3 ++ src/iwd/iwd_adapter.c | 103 ++++++++++++++++++++++++++++++++++++++++++ src/iwd/iwd_adapter.h | 24 ++++++++++ src/iwd/iwd_device.c | 62 ++++++++++++++++++++++--- src/iwd/iwd_device.h | 2 + src/iwd/iwd_manager.c | 56 ++++++++++++++++++++--- src/meson.build | 1 + 7 files changed, 239 insertions(+), 12 deletions(-) create mode 100644 src/iwd/iwd_adapter.c create mode 100644 src/iwd/iwd_adapter.h diff --git a/src/e_mod_popup.c b/src/e_mod_popup.c index 2bbf140..4062fe3 100644 --- a/src/e_mod_popup.c +++ b/src/e_mod_popup.c @@ -82,6 +82,9 @@ _rebuild_list(Popup *p) if (!p->list || !e_iwd || !e_iwd->manager) return; elm_box_clear(p->list); + /* When the radio is off, hide the (now-stale) network list entirely. */ + if (iwd_manager_state(e_iwd->manager) == IWD_STATE_OFF) return; + const Eina_Hash *h = iwd_manager_networks(e_iwd->manager); if (!h) return; diff --git a/src/iwd/iwd_adapter.c b/src/iwd/iwd_adapter.c new file mode 100644 index 0000000..d73bee3 --- /dev/null +++ b/src/iwd/iwd_adapter.c @@ -0,0 +1,103 @@ +#include "iwd_adapter.h" +#include "iwd_dbus.h" +#include "iwd_props.h" +#include "iwd_manager.h" +#include +#include +#include + +static void +_prop_cb(void *data, const char *key, Eldbus_Message_Iter *v) +{ + Iwd_Adapter *a = data; + if (!strcmp(key, "Powered")) a->powered = iwd_props_bool(v); +} + +void +iwd_adapter_apply_props(Iwd_Adapter *a, Eldbus_Message_Iter *props) +{ + iwd_props_for_each(props, _prop_cb, a); +} + +static void +_on_props_changed(void *data, const Eldbus_Message *msg) +{ + Iwd_Adapter *a = data; + const char *iface; + Eldbus_Message_Iter *changed, *invalidated; + if (!eldbus_message_arguments_get(msg, "sa{sv}as", &iface, &changed, &invalidated)) + return; + if (strcmp(iface, IWD_IFACE_ADAPTER) != 0) return; + iwd_props_for_each(changed, _prop_cb, a); + if (a->manager) iwd_manager_notify(a->manager); +} + +Iwd_Adapter * +iwd_adapter_new(Eldbus_Connection *conn, const char *path, void *manager) +{ + Iwd_Adapter *a = calloc(1, sizeof(*a)); + if (!a) return NULL; + a->path = path ? strdup(path) : NULL; + a->manager = manager; + a->obj = eldbus_object_get(conn, IWD_BUS_NAME, path); + if (a->obj) + { + a->proxy = eldbus_proxy_get(a->obj, IWD_IFACE_ADAPTER); + if (a->proxy) + a->sh_props = eldbus_proxy_properties_changed_callback_add( + a->proxy, _on_props_changed, a); + } + return a; +} + +void +iwd_adapter_free(Iwd_Adapter *a) +{ + if (!a) return; + if (a->sh_props) eldbus_signal_handler_del(a->sh_props); + if (a->_props_proxy_keepalive) eldbus_proxy_unref(a->_props_proxy_keepalive); + if (a->proxy) eldbus_proxy_unref(a->proxy); + if (a->obj) eldbus_object_unref(a->obj); + free(a->path); + free(a); +} + +static void +_on_set_reply(void *data, const Eldbus_Message *msg, Eldbus_Pending *p EINA_UNUSED) +{ + const char *what = data; + const char *en, *em; + if (eldbus_message_error_get(msg, &en, &em)) + fprintf(stderr, "e_iwd: %s failed: %s: %s\n", what, en, em); + else + fprintf(stderr, "e_iwd: %s ok\n", what); +} + +void +iwd_adapter_set_powered(Iwd_Adapter *a, Eina_Bool on) +{ + if (!a || !a->obj) return; + + /* Call org.freedesktop.DBus.Properties.Set explicitly so we control the + * variant marshaling exactly. eldbus_proxy_property_set silently swallows + * Adapter.Powered on this iwd version. */ + Eldbus_Proxy *props = eldbus_proxy_get(a->obj, "org.freedesktop.DBus.Properties"); + if (!props) return; + + Eldbus_Message *msg = eldbus_proxy_method_call_new(props, "Set"); + Eldbus_Message_Iter *iter = eldbus_message_iter_get(msg); + const char *iface = IWD_IFACE_ADAPTER; + const char *prop = "Powered"; + eldbus_message_iter_basic_append(iter, 's', iface); + eldbus_message_iter_basic_append(iter, 's', prop); + Eldbus_Message_Iter *variant = eldbus_message_iter_container_new(iter, 'v', "b"); + Eina_Bool v = on; + eldbus_message_iter_basic_append(variant, 'b', v); + eldbus_message_iter_container_close(iter, variant); + + eldbus_proxy_send(props, msg, _on_set_reply, + on ? "Adapter.Powered=true" : "Adapter.Powered=false", -1); + /* Keep the props proxy alive on the adapter so the call isn't canceled. */ + if (a->_props_proxy_keepalive) eldbus_proxy_unref(a->_props_proxy_keepalive); + a->_props_proxy_keepalive = props; +} diff --git a/src/iwd/iwd_adapter.h b/src/iwd/iwd_adapter.h new file mode 100644 index 0000000..b62b007 --- /dev/null +++ b/src/iwd/iwd_adapter.h @@ -0,0 +1,24 @@ +#ifndef IWD_ADAPTER_H +#define IWD_ADAPTER_H + +#include +#include + +typedef struct _Iwd_Adapter +{ + char *path; + Eina_Bool powered; + Eldbus_Object *obj; + Eldbus_Proxy *proxy; + Eldbus_Proxy *_props_proxy_keepalive; + Eldbus_Signal_Handler *sh_props; + void *manager; +} Iwd_Adapter; + +Iwd_Adapter *iwd_adapter_new (Eldbus_Connection *conn, const char *path, void *manager); +void iwd_adapter_free(Iwd_Adapter *a); + +void iwd_adapter_apply_props(Iwd_Adapter *a, Eldbus_Message_Iter *props); +void iwd_adapter_set_powered(Iwd_Adapter *a, Eina_Bool on); + +#endif diff --git a/src/iwd/iwd_device.c b/src/iwd/iwd_device.c index 73b3a12..354cb89 100644 --- a/src/iwd/iwd_device.c +++ b/src/iwd/iwd_device.c @@ -2,6 +2,7 @@ #include "iwd_dbus.h" #include "iwd_props.h" #include "iwd_manager.h" +#include #include #include @@ -127,6 +128,8 @@ iwd_device_free(Iwd_Device *d) iwd_device_detach_station(d); if (d->sh_dev_props) eldbus_signal_handler_del(d->sh_dev_props); if (d->device_proxy) eldbus_proxy_unref(d->device_proxy); + if (d->adapter_proxy) eldbus_proxy_unref(d->adapter_proxy); + if (d->adapter_obj) eldbus_object_unref(d->adapter_obj); if (d->obj) eldbus_object_unref(d->obj); free(d->path); free(d->name); @@ -136,23 +139,70 @@ iwd_device_free(Iwd_Device *d) free(d); } +static void +_log_reply(void *data, const Eldbus_Message *msg, Eldbus_Pending *p EINA_UNUSED) +{ + const char *what = data; + const char *en, *em; + if (eldbus_message_error_get(msg, &en, &em)) + fprintf(stderr, "e_iwd: %s failed: %s: %s\n", what, en, em); + else + fprintf(stderr, "e_iwd: %s ok\n", what); +} + void iwd_device_set_powered(Iwd_Device *d, Eina_Bool on) { - if (!d || !d->device_proxy) return; - eldbus_proxy_property_set(d->device_proxy, "Powered", "b", &on, NULL, NULL); + if (!d) return; + Eina_Bool v = on; + + /* Toggle the radio at the Adapter level — that's what actually takes + * the interface up/down on modern iwd. Device.Powered is a no-op on + * many installs. Keep the adapter proxy alive on the device so the + * pending property_set isn't canceled. */ + if (d->adapter_path && d->obj && !d->adapter_proxy) + { + Eldbus_Connection *conn = eldbus_object_connection_get(d->obj); + d->adapter_obj = eldbus_object_get(conn, IWD_BUS_NAME, d->adapter_path); + if (d->adapter_obj) + d->adapter_proxy = eldbus_proxy_get(d->adapter_obj, IWD_IFACE_ADAPTER); + } + if (d->adapter_proxy) + eldbus_proxy_property_set(d->adapter_proxy, "Powered", "b", &v, _log_reply, + on ? "Adapter.Powered=true" + : "Adapter.Powered=false"); + else + fprintf(stderr, "e_iwd: set_powered: no Adapter for %s\n", + d->path ? d->path : "?"); + + if (d->device_proxy) + eldbus_proxy_property_set(d->device_proxy, "Powered", "b", &v, NULL, NULL); } void iwd_device_scan(Iwd_Device *d) { - if (!d || !d->station_proxy) return; - eldbus_proxy_call(d->station_proxy, "Scan", NULL, NULL, -1, ""); + if (!d) + { + fprintf(stderr, "e_iwd: scan: NULL device\n"); + return; + } + if (!d->station_proxy) + { + fprintf(stderr, "e_iwd: scan: no Station proxy on %s\n", + d->path ? d->path : "?"); + return; + } + eldbus_proxy_call(d->station_proxy, "Scan", _log_reply, "Scan", -1, ""); } void iwd_device_disconnect(Iwd_Device *d) { - if (!d || !d->station_proxy) return; - eldbus_proxy_call(d->station_proxy, "Disconnect", NULL, NULL, -1, ""); + if (!d || !d->station_proxy) + { + fprintf(stderr, "e_iwd: disconnect: missing Station proxy\n"); + return; + } + eldbus_proxy_call(d->station_proxy, "Disconnect", _log_reply, "Disconnect", -1, ""); } diff --git a/src/iwd/iwd_device.h b/src/iwd/iwd_device.h index a5aebce..9a8d857 100644 --- a/src/iwd/iwd_device.h +++ b/src/iwd/iwd_device.h @@ -31,6 +31,8 @@ struct _Iwd_Device Eldbus_Object *obj; Eldbus_Proxy *device_proxy; Eldbus_Proxy *station_proxy; + Eldbus_Object *adapter_obj; + Eldbus_Proxy *adapter_proxy; Eldbus_Signal_Handler *sh_dev_props; Eldbus_Signal_Handler *sh_sta_props; diff --git a/src/iwd/iwd_manager.c b/src/iwd/iwd_manager.c index 61bfe83..bc13db1 100644 --- a/src/iwd/iwd_manager.c +++ b/src/iwd/iwd_manager.c @@ -1,5 +1,6 @@ #include "iwd_manager.h" #include "iwd_dbus.h" +#include "iwd_adapter.h" #include "iwd_device.h" #include "iwd_network.h" #include @@ -15,6 +16,7 @@ struct _Iwd_Manager { Iwd_Dbus *dbus; Iwd_Agent *agent; + Eina_Hash *adapters; /* path → Iwd_Adapter * */ Eina_Hash *devices; /* path → Iwd_Device * */ Eina_Hash *networks; /* path → Iwd_Network * */ Eina_List *listeners; /* Listener * */ @@ -86,12 +88,27 @@ static void _recompute_state(Iwd_Manager *m) { Iwd_State s = IWD_STATE_OFF; + Eina_Bool any_powered = EINA_FALSE; + + /* Adapter.Powered is the source of truth for radio state — Device.Powered + * is a no-op on modern iwd, so don't let it lie to us. If we have no + * tracked adapter at all, fall back to "any device exists". */ + if (eina_hash_population(m->adapters) > 0) + { + Eina_Iterator *ait = eina_hash_iterator_data_new(m->adapters); + Iwd_Adapter *ap; + EINA_ITERATOR_FOREACH(ait, ap) + if (ap->powered) any_powered = EINA_TRUE; + eina_iterator_free(ait); + } + else + any_powered = (eina_hash_population(m->devices) > 0); + Eina_Iterator *it = eina_hash_iterator_data_new(m->devices); Iwd_Device *d; - Eina_Bool any_powered = EINA_FALSE; EINA_ITERATOR_FOREACH(it, d) { - if (d->powered) any_powered = EINA_TRUE; + if (!any_powered) continue; /* radio is down: ignore stale station */ if (!d->has_station) continue; if (d->scanning && s < IWD_STATE_SCANNING) s = IWD_STATE_SCANNING; if (d->station_state == IWD_STATION_CONNECTING && s < IWD_STATE_CONNECTING) s = IWD_STATE_CONNECTING; @@ -110,7 +127,17 @@ _on_iface_added(void *data, const char *path, const char *iface, Eldbus_Message_ Iwd_Manager *m = data; Eldbus_Connection *conn = iwd_dbus_conn(m->dbus); - if (!strcmp(iface, IWD_IFACE_DEVICE)) + if (!strcmp(iface, IWD_IFACE_ADAPTER)) + { + Iwd_Adapter *a = eina_hash_find(m->adapters, path); + if (!a) + { + a = iwd_adapter_new(conn, path, m); + if (a) eina_hash_add(m->adapters, path, a); + } + if (a) iwd_adapter_apply_props(a, props); + } + else if (!strcmp(iface, IWD_IFACE_DEVICE)) { Iwd_Device *d = eina_hash_find(m->devices, path); if (!d) @@ -167,6 +194,10 @@ _on_iface_removed(void *data, const char *path, const char *iface) { eina_hash_del(m->networks, path, NULL); } + else if (!strcmp(iface, IWD_IFACE_ADAPTER)) + { + eina_hash_del(m->adapters, path, NULL); + } iwd_manager_notify(m); } @@ -177,6 +208,7 @@ static void _on_name_vanished(void *data) { Iwd_Manager *m = data; + eina_hash_free_buckets(m->adapters); eina_hash_free_buckets(m->devices); eina_hash_free_buckets(m->networks); m->state = IWD_STATE_OFF; @@ -185,6 +217,7 @@ _on_name_vanished(void *data) /* ----- lifecycle ------------------------------------------------------- */ +static void _adapter_free_cb(void *d) { iwd_adapter_free(d); } static void _device_free_cb (void *d) { iwd_device_free(d); } static void _network_free_cb(void *d) { iwd_network_free(d); } @@ -193,6 +226,7 @@ iwd_manager_new(Eldbus_Connection *conn) { Iwd_Manager *m = calloc(1, sizeof(*m)); if (!m) return NULL; + m->adapters = eina_hash_string_superfast_new(_adapter_free_cb); m->devices = eina_hash_string_superfast_new(_device_free_cb); m->networks = eina_hash_string_superfast_new(_network_free_cb); m->state = IWD_STATE_OFF; @@ -214,6 +248,7 @@ iwd_manager_free(Iwd_Manager *m) if (!m) return; iwd_agent_free(m->agent); iwd_dbus_free(m->dbus); + eina_hash_free(m->adapters); eina_hash_free(m->devices); eina_hash_free(m->networks); Listener *li; @@ -239,8 +274,17 @@ void iwd_manager_set_powered(Iwd_Manager *m, Eina_Bool on) { if (!m) return; - Eina_Iterator *it = eina_hash_iterator_data_new(m->devices); - Iwd_Device *d; - EINA_ITERATOR_FOREACH(it, d) iwd_device_set_powered(d, on); + + if (!on) + { + Eina_Iterator *dit = eina_hash_iterator_data_new(m->devices); + Iwd_Device *d; + EINA_ITERATOR_FOREACH(dit, d) iwd_device_disconnect(d); + eina_iterator_free(dit); + } + + Eina_Iterator *it = eina_hash_iterator_data_new(m->adapters); + Iwd_Adapter *a; + EINA_ITERATOR_FOREACH(it, a) iwd_adapter_set_powered(a, on); eina_iterator_free(it); } diff --git a/src/meson.build b/src/meson.build index 3aa556b..7b069c1 100644 --- a/src/meson.build +++ b/src/meson.build @@ -4,6 +4,7 @@ e_iwd_sources = [ 'e_mod_gadget.c', 'e_mod_popup.c', 'iwd/iwd_dbus.c', + 'iwd/iwd_adapter.c', 'iwd/iwd_props.c', 'iwd/iwd_agent.c', 'iwd/iwd_manager.c',