From 13ee635df51c9fdcd1bba23ebfc37009e1894c6a Mon Sep 17 00:00:00 2001 From: Jan Zickermann Date: Sat, 4 Feb 2023 13:24:25 +0100 Subject: [PATCH 1/3] rfkill: Rework airplane mode Previously, the airplane mode (APM) behavior did not align with users' expectations. Now, APM temporarily blocks all enabled RF devices when activated. It is possible to enable blocked devices during APM. However, enabling a WWAN devices will disable APM. Newly added devices in APM are blocked. When APM is deactivated, all previously enabled devices are re-enabled. Closes: https://gitlab.gnome.org/GNOME/gnome-settings-daemon/-/issues/288 --- plugins/rfkill/gsd-rfkill-manager.c | 512 ++++++++++++++++++---------- plugins/rfkill/rfkill-glib.c | 396 ++++++++++----------- plugins/rfkill/rfkill-glib.h | 42 ++- plugins/rfkill/rfkill.h | 14 + 4 files changed, 557 insertions(+), 407 deletions(-) diff --git a/plugins/rfkill/gsd-rfkill-manager.c b/plugins/rfkill/gsd-rfkill-manager.c index 50a3e2bf3..b38c31c86 100644 --- a/plugins/rfkill/gsd-rfkill-manager.c +++ b/plugins/rfkill/gsd-rfkill-manager.c @@ -36,7 +36,7 @@ * --object-path /org/gnome/SettingsDaemon/Rfkill \ * --method org.freedesktop.DBus.Properties.Set \ * "org.gnome.SettingsDaemon.Rfkill" \ - * "BluetoothAirplaneMode" \ + * "BluetoothBlocked" \ * "" * * and @@ -46,7 +46,7 @@ * --object-path /org/gnome/SettingsDaemon/Rfkill \ * --method org.freedesktop.DBus.Properties.Set \ * "org.gnome.SettingsDaemon.Rfkill" \ - * "WwanAirplaneMode" \ + * "WwanBlocked" \ * "" */ @@ -73,6 +73,18 @@ struct _GsdRfkillManager GHashTable *killswitches; GHashTable *bt_killswitches; GHashTable *wwan_killswitches; + /* The airplane mode (APM) temporarally blocks all enabled RF devices when activated. + Except WWAN devices any device may be unblocked during APM. + Unblocking a WWAN device during APM disables APM. + Devices added during APM are blocked. + When APM is deactivated all temporarally blocked devices are unblocked/enabled.*/ + gboolean apm_active; + gboolean apm_active_want; + guint apm_throttle_id; + /* WWAN enabled state of NM before APM. Needs to be explicitly resotred. */ + gboolean apm_nm_wwan_blocked; + /* Set of blocked rfkill device ids of devices which were enabled before APM. */ + GHashTable *apm_blocked_devices; /* In addition to using the rfkill kernel subsystem (which is exposed by wlan, wimax, bluetooth, nfc, @@ -100,20 +112,37 @@ struct _GsdRfkillManager #define GSD_RFKILL_DBUS_NAME GSD_DBUS_NAME ".Rfkill" #define GSD_RFKILL_DBUS_PATH GSD_DBUS_PATH "/Rfkill" +#define APM_THROTTLE_MS 50 + static const gchar introspection_xml[] = "" " " " " " " +"" " " +" " " " " " +"" " " +" " +"" " " +" " +"" " " +" " +"" " " +"" +" " +"" " " +" " +"" " " +" " " " ""; @@ -124,6 +153,7 @@ static void gsd_rfkill_manager_shutdown (GApplication *app); G_DEFINE_TYPE (GsdRfkillManager, gsd_rfkill_manager, G_TYPE_APPLICATION) + static void gsd_rfkill_manager_class_init (GsdRfkillManagerClass *klass) { @@ -138,14 +168,21 @@ gsd_rfkill_manager_init (GsdRfkillManager *manager) { } +static gboolean engine_switch_devices (GsdRfkillManager *manager, + enum rfkill_type r_type, + GArray *device_indices, + gboolean enable); +static gboolean engine_set_airplane_mode (GsdRfkillManager *manager, + gboolean enable); + static gboolean -engine_get_airplane_mode_helper (GHashTable *killswitches) +engine_all_rfkill_devices_blocked (GHashTable *killswitches, gboolean no_devices) { GHashTableIter iter; gpointer key, value; if (g_hash_table_size (killswitches) == 0) - return FALSE; + return no_devices; g_hash_table_iter_init (&iter, killswitches); while (g_hash_table_iter_next (&iter, &key, &value)) { @@ -162,13 +199,13 @@ engine_get_airplane_mode_helper (GHashTable *killswitches) } static gboolean -engine_get_bluetooth_airplane_mode (GsdRfkillManager *manager) +engine_get_bluetooth_blocked (GsdRfkillManager *manager) { - return engine_get_airplane_mode_helper (manager->bt_killswitches); + return engine_all_rfkill_devices_blocked (manager->bt_killswitches, manager->apm_active); } static gboolean -engine_get_bluetooth_hardware_airplane_mode (GsdRfkillManager *manager) +engine_get_bluetooth_hardware_blocked (GsdRfkillManager *manager) { GHashTableIter iter; gpointer key, value; @@ -193,33 +230,30 @@ engine_get_bluetooth_hardware_airplane_mode (GsdRfkillManager *manager) } static gboolean -engine_get_has_bluetooth_airplane_mode (GsdRfkillManager *manager) +engine_get_bluetooth_available (GsdRfkillManager *manager) { return (g_hash_table_size (manager->bt_killswitches) > 0); } static gboolean -engine_get_wwan_airplane_mode (GsdRfkillManager *manager) +engine_get_wwan_blocked (GsdRfkillManager *manager) { - gboolean is_airplane; - - is_airplane = engine_get_airplane_mode_helper (manager->wwan_killswitches); - /* Try our luck with Modem Manager too. Check only if no rfkill * devices found, or if rfkill reports all devices to be down. - * (Airplane mode will be disabled if at least one device is up, + * (Blocked is FALSE if at least one device is up, * so if rfkill says no device is up, check any device is up via * Network Manager (which in turn, is handled via Modem Manager)) */ - if (g_hash_table_size (manager->wwan_killswitches) == 0 || is_airplane) - if (manager->wwan_interesting) - is_airplane = !manager->wwan_enabled; - - return is_airplane; + return engine_all_rfkill_devices_blocked ( + manager->wwan_killswitches, + /* No rfkill devices -> check that WWAN is blocked by NM or APM */ + (manager->wwan_interesting && !manager->wwan_enabled) || manager->apm_active) && + /* All rfkill devices blocked -> check that WWAN of NM is off */ + (!manager->wwan_interesting || !manager->wwan_enabled); } static gboolean -engine_get_wwan_hardware_airplane_mode (GsdRfkillManager *manager) +engine_get_wwan_hardware_blocked (GsdRfkillManager *manager) { GHashTableIter iter; gpointer key, value; @@ -244,7 +278,7 @@ engine_get_wwan_hardware_airplane_mode (GsdRfkillManager *manager) } static gboolean -engine_get_has_wwan_airplane_mode (GsdRfkillManager *manager) +engine_get_wwan_available (GsdRfkillManager *manager) { return (g_hash_table_size (manager->wwan_killswitches) > 0 || manager->wwan_interesting); @@ -253,15 +287,11 @@ engine_get_has_wwan_airplane_mode (GsdRfkillManager *manager) static gboolean engine_get_airplane_mode (GsdRfkillManager *manager) { - if (!manager->wwan_interesting) - return engine_get_airplane_mode_helper (manager->killswitches); - /* wwan enabled? then airplane mode is off (because an USB modem - could be on in this state) */ - return engine_get_airplane_mode_helper (manager->killswitches) && !manager->wwan_enabled; + return manager->apm_active_want; } static gboolean -engine_get_hardware_airplane_mode (GsdRfkillManager *manager) +engine_get_hardware_blocked (GsdRfkillManager *manager) { GHashTableIter iter; gpointer key, value; @@ -285,6 +315,7 @@ engine_get_hardware_airplane_mode (GsdRfkillManager *manager) return TRUE; } + static gboolean engine_get_has_airplane_mode (GsdRfkillManager *manager) { @@ -301,9 +332,34 @@ engine_get_should_show_airplane_mode (GsdRfkillManager *manager) (g_strcmp0 (manager->chassis_type, "container") != 0); } +static const struct { + const char* prop_name; + gboolean (*getter) (GsdRfkillManager *); +} ENGINE_PROP_GETTERS[] = { + {"AirplaneMode", engine_get_airplane_mode}, + {"HardwareAirplaneMode", engine_get_hardware_blocked}, + {"HardwareBlocked", engine_get_hardware_blocked}, + {"ShouldShowAirplaneMode", engine_get_should_show_airplane_mode}, + {"HasAirplaneMode", engine_get_has_airplane_mode}, + {"BluetoothAirplaneMode", engine_get_bluetooth_blocked}, + {"BluetoothBlocked", engine_get_bluetooth_blocked}, + {"BluetoothHardwareAirplaneMode", engine_get_bluetooth_hardware_blocked}, + {"BluetoothHardwareBlocked", engine_get_bluetooth_hardware_blocked}, + {"BluetoothHasAirplaneMode", engine_get_bluetooth_available}, + {"BluetoothAvailable", engine_get_bluetooth_available}, + {"WwanAirplaneMode", engine_get_wwan_blocked}, + {"WwanBlocked", engine_get_wwan_blocked}, + {"WwanHardwareAirplaneMode", engine_get_wwan_hardware_blocked}, + {"WwanHardwareBlocked", engine_get_wwan_hardware_blocked}, + {"WwanHasAirplaneMode", engine_get_wwan_available}, + {"WwanAvailable", engine_get_wwan_available}, + {NULL, NULL} +}; + static void engine_properties_changed (GsdRfkillManager *manager) { + guint i; GVariantBuilder props_builder; GVariant *props_changed = NULL; @@ -313,26 +369,10 @@ engine_properties_changed (GsdRfkillManager *manager) g_variant_builder_init (&props_builder, G_VARIANT_TYPE ("a{sv}")); - g_variant_builder_add (&props_builder, "{sv}", "AirplaneMode", - g_variant_new_boolean (engine_get_airplane_mode (manager))); - g_variant_builder_add (&props_builder, "{sv}", "HardwareAirplaneMode", - g_variant_new_boolean (engine_get_hardware_airplane_mode (manager))); - g_variant_builder_add (&props_builder, "{sv}", "HasAirplaneMode", - g_variant_new_boolean (engine_get_has_airplane_mode (manager))); - g_variant_builder_add (&props_builder, "{sv}", "ShouldShowAirplaneMode", - g_variant_new_boolean (engine_get_should_show_airplane_mode (manager))); - g_variant_builder_add (&props_builder, "{sv}", "BluetoothAirplaneMode", - g_variant_new_boolean (engine_get_bluetooth_airplane_mode (manager))); - g_variant_builder_add (&props_builder, "{sv}", "BluetoothHardwareAirplaneMode", - g_variant_new_boolean (engine_get_bluetooth_hardware_airplane_mode (manager))); - g_variant_builder_add (&props_builder, "{sv}", "BluetoothHasAirplaneMode", - g_variant_new_boolean (engine_get_has_bluetooth_airplane_mode (manager))); - g_variant_builder_add (&props_builder, "{sv}", "WwanAirplaneMode", - g_variant_new_boolean (engine_get_wwan_airplane_mode (manager))); - g_variant_builder_add (&props_builder, "{sv}", "WwanHardwareAirplaneMode", - g_variant_new_boolean (engine_get_wwan_hardware_airplane_mode (manager))); - g_variant_builder_add (&props_builder, "{sv}", "WwanHasAirplaneMode", - g_variant_new_boolean (engine_get_has_wwan_airplane_mode (manager))); + for (i = 0; ENGINE_PROP_GETTERS[i].prop_name != NULL; i++) { + g_variant_builder_add (&props_builder, "{sv}", ENGINE_PROP_GETTERS[i].prop_name, + g_variant_new_boolean (ENGINE_PROP_GETTERS[i].getter (manager))); + } props_changed = g_variant_new ("(s@a{sv}@as)", GSD_RFKILL_DBUS_NAME, g_variant_builder_end (&props_builder), @@ -346,32 +386,38 @@ engine_properties_changed (GsdRfkillManager *manager) props_changed, NULL); } + static void -rfkill_changed (CcRfkillGlib *rfkill, - GList *events, +rfkill_changed (CcRfkillGlib *rfkill, + GArray *events, GsdRfkillManager *manager) { - GList *l; + gint i; int value; - for (l = events; l != NULL; l = l->next) { - struct rfkill_event *event = l->data; - const gchar *type = ""; - - if (event->type == RFKILL_TYPE_BLUETOOTH) - type = "Bluetooth "; - else if (event->type == RFKILL_TYPE_WWAN) - type = "WWAN "; + for (i = 0; i < events->len; i++) { + struct rfkill_event *event = &g_array_index (events, struct rfkill_event, i); + value = cc_rfkill_event_state (event); + if (event->type == RFKILL_TYPE_WWAN && + value == RFKILL_STATE_UNBLOCKED && + event->op == RFKILL_OP_CHANGE && + manager->apm_active) + { + /* Disable APM since a WWAN device has been enabled */ + engine_set_airplane_mode (manager, FALSE); + } switch (event->op) { case RFKILL_OP_ADD: + if (manager->apm_active && !event->soft) { + /* Device added during active airplane mode must be soft blocked */ + g_autoptr(GArray) indices = NULL; + indices = g_array_new (FALSE, FALSE, sizeof(gint)); + g_array_append_val (indices, event->idx); + g_hash_table_add (manager->apm_blocked_devices, GINT_TO_POINTER(event->idx)); + engine_switch_devices (manager, event->type, indices, FALSE); + } case RFKILL_OP_CHANGE: - if (event->hard) - value = RFKILL_STATE_HARD_BLOCKED; - else if (event->soft) - value = RFKILL_STATE_SOFT_BLOCKED; - else - value = RFKILL_STATE_UNBLOCKED; g_hash_table_insert (manager->killswitches, GINT_TO_POINTER (event->idx), @@ -384,9 +430,9 @@ rfkill_changed (CcRfkillGlib *rfkill, g_hash_table_insert (manager->wwan_killswitches, GINT_TO_POINTER (event->idx), GINT_TO_POINTER (value)); - g_debug ("%s %srfkill with ID %d", - event->op == RFKILL_OP_ADD ? "Added" : "Changed", - type, event->idx); + g_debug ("%s %s rfkill with ID %d", + cc_rfkill_op_to_string (event->op), + cc_rfkill_type_to_string (event->type), event->idx); break; case RFKILL_OP_DEL: g_hash_table_remove (manager->killswitches, @@ -397,7 +443,8 @@ rfkill_changed (CcRfkillGlib *rfkill, else if (event->type == RFKILL_TYPE_WWAN) g_hash_table_remove (manager->wwan_killswitches, GINT_TO_POINTER (event->idx)); - g_debug ("Removed %srfkill with ID %d", type, event->idx); + g_debug ("Removed %s rfkill with ID %d", + cc_rfkill_type_to_string (event->type), event->idx); break; } } @@ -444,64 +491,211 @@ set_wwan_complete (GObject *object, } } + + static gboolean -engine_set_bluetooth_airplane_mode (GsdRfkillManager *manager, - gboolean enable) +engine_switch_devices (GsdRfkillManager *manager, + enum rfkill_type r_type, GArray *device_indices, gboolean enable) { - cc_rfkill_glib_send_change_all_event (manager->rfkill, RFKILL_TYPE_BLUETOOTH, - enable, manager->cancellable, rfkill_set_cb, manager); - + g_autoptr(GString) idx_str = NULL; + if (!cc_rfkill_is_ready (manager->rfkill) || + (device_indices != NULL && device_indices->len < 1)) + return FALSE; + if (device_indices != NULL) { + guint i; + idx_str = g_string_sized_new (device_indices->len*2); + for (i = 0; i < device_indices->len; i++) { + g_string_append_printf (idx_str, " %d", g_array_index (device_indices, gint, i)); + } + } + g_debug ("%s %s devices: %s", + enable ? "Unblock" : "Block", + cc_rfkill_type_to_string (r_type), + idx_str != NULL ? idx_str->str : "ALL"); + cc_rfkill_glib_send_change_all_event ( + manager->rfkill, r_type, device_indices, enable); return TRUE; } static gboolean -engine_set_wwan_airplane_mode (GsdRfkillManager *manager, - gboolean enable) +remove_missing_or_unblocked (gpointer key, gpointer value, gpointer user_data) { - cc_rfkill_glib_send_change_all_event (manager->rfkill, RFKILL_TYPE_WWAN, - enable, manager->cancellable, rfkill_set_cb, manager); + GHashTable *ks; + ks = (GHashTable*)user_data; + return !g_hash_table_contains (ks, key) || GPOINTER_TO_INT (g_hash_table_lookup (ks, key)) == RFKILL_STATE_UNBLOCKED; +} + +static gboolean +engine_switch_nm_wwan(GsdRfkillManager *manager, gboolean enable) +{ + /* Note: we set the the NM property even if there are no modems, so we don't + need to resync when one is plugged in */ if (manager->nm_client) { g_dbus_proxy_call (manager->nm_client, "org.freedesktop.DBus.Properties.Set", g_variant_new ("(ssv)", "org.freedesktop.NetworkManager", "WwanEnabled", - g_variant_new_boolean (!enable)), + g_variant_new_boolean (enable)), G_DBUS_CALL_FLAGS_NONE, -1, /* timeout */ manager->cancellable, set_wwan_complete, NULL); } + return TRUE; +} + +static gboolean +engine_enable_airplane_mode (GsdRfkillManager *manager) +{ + GHashTableIter iter; + gpointer key, value; + + /* Activate airplane mode */ + manager->apm_active = TRUE; + g_debug("enable apm with wwan_enabled: %s", manager->wwan_enabled ? "true" : "false"); + manager->apm_nm_wwan_blocked = manager->wwan_enabled; + + /* Store currently active devices to restore when APM is disabled */ + g_hash_table_iter_init (&iter, manager->killswitches); + while (g_hash_table_iter_next (&iter, &key, &value)) { + if (GPOINTER_TO_INT (value) == RFKILL_STATE_UNBLOCKED) { + g_hash_table_add (manager->apm_blocked_devices, key); + } + } + /* Disable all devices */ + engine_switch_devices (manager, RFKILL_TYPE_ALL, NULL, FALSE); + engine_switch_nm_wwan (manager, FALSE); + + g_debug ("Airplane Mode: enabled"); return TRUE; } +static gboolean +engine_disable_airplane_mode (GsdRfkillManager *manager) +{ + g_autoptr(GArray) indices_array = NULL; + gpointer *keys; + guint keys_count; + guint i; + + /* Deactivate airplane mode */ + manager->apm_active = FALSE; + manager->apm_nm_wwan_blocked = FALSE; + + /* Avoid restoring missing or already unblocked devices */ + g_hash_table_foreach_remove (manager->apm_blocked_devices, + remove_missing_or_unblocked, manager->killswitches); + + /* Move all stored indices into output array */ + keys = g_hash_table_get_keys_as_array (manager->apm_blocked_devices, &keys_count); + indices_array = g_array_sized_new (FALSE, TRUE, sizeof(gint), keys_count); + + for (i = 0; i < keys_count; i++) { + gint key; + key = GPOINTER_TO_INT(keys[i]); + g_array_append_val (indices_array, key); + } + g_free (keys); + g_hash_table_remove_all (manager->apm_blocked_devices); + + + /* Restore/Enable devices which were active before APM. + Leave devices which were disabled before APM unchanged. */ + engine_switch_devices (manager, RFKILL_TYPE_ALL, indices_array, TRUE); + if (manager->apm_nm_wwan_blocked) { + engine_switch_nm_wwan(manager, TRUE); + } + manager->apm_nm_wwan_blocked = FALSE; + + g_debug ("Airplane Mode: disabled"); + return TRUE; +} + +static void +sync_airplane_mode (GsdRfkillManager *manager) +{ + if (manager->apm_active != manager->apm_active_want) { + if (manager->apm_active_want) { + engine_enable_airplane_mode (manager); + } else { + engine_disable_airplane_mode (manager); + } + } +} + +static guint +engine_apm_sync_throttle_done (GsdRfkillManager *manager) +{ + manager->apm_throttle_id = 0; + sync_airplane_mode (manager); + return G_SOURCE_REMOVE; +} + +static void +engine_apm_sync_throttle (GsdRfkillManager *manager) { + if (!manager->apm_throttle_id) { + sync_airplane_mode (manager); + manager->apm_throttle_id = g_timeout_add (APM_THROTTLE_MS, + (GSourceFunc)engine_apm_sync_throttle_done, + manager); + } +} + +static void +engine_apm_sync_throttle_clear (GsdRfkillManager *manager) { + if (manager->apm_throttle_id) { + g_source_remove (manager->apm_throttle_id); + manager->apm_throttle_id = 0; + } +} + static gboolean engine_set_airplane_mode (GsdRfkillManager *manager, gboolean enable) { - cc_rfkill_glib_send_change_all_event (manager->rfkill, RFKILL_TYPE_ALL, - enable, manager->cancellable, rfkill_set_cb, manager); + gboolean change; - /* Note: we set the the NM property even if there are no modems, so we don't - need to resync when one is plugged in */ - if (manager->nm_client) { - g_dbus_proxy_call (manager->nm_client, - "org.freedesktop.DBus.Properties.Set", - g_variant_new ("(ssv)", - "org.freedesktop.NetworkManager", - "WwanEnabled", - g_variant_new_boolean (!enable)), - G_DBUS_CALL_FLAGS_NONE, - -1, /* timeout */ - manager->cancellable, - set_wwan_complete, NULL); + change = manager->apm_active_want != enable; + manager->apm_active_want = enable; + if (change) { + engine_apm_sync_throttle (manager); } + return change; +} - return TRUE; +static gboolean +engine_set_bluetooth_blocked (GsdRfkillManager *manager, + gboolean enable) +{ + return engine_switch_devices (manager, RFKILL_TYPE_BLUETOOTH, NULL, !enable); +} + + +static gboolean +engine_set_wwan_blocked (GsdRfkillManager *manager, + gboolean enable) +{ + engine_switch_nm_wwan (manager, !enable); + engine_switch_devices (manager, RFKILL_TYPE_WWAN, NULL, !enable); + return TRUE; } + +static const struct { + const gchar* prop_name; + gboolean (*setter) (GsdRfkillManager *, gboolean); +} ENGINE_PROP_SETTERS[] = { + {"AirplaneMode", engine_set_airplane_mode}, + {"BluetoothAirplaneMode", engine_set_bluetooth_blocked}, + {"BluetoothBlocked", engine_set_bluetooth_blocked}, + {"WwanAirplaneMode", engine_set_wwan_blocked}, + {"WwanBlocked", engine_set_wwan_blocked}, + {NULL, NULL} +}; + static gboolean handle_set_property (GDBusConnection *connection, const gchar *sender, @@ -512,24 +706,23 @@ handle_set_property (GDBusConnection *connection, GError **error, gpointer user_data) { - GsdRfkillManager *manager = GSD_RFKILL_MANAGER (user_data); - - if (g_strcmp0 (property_name, "AirplaneMode") == 0) { - gboolean airplane_mode; - g_variant_get (value, "b", &airplane_mode); - return engine_set_airplane_mode (manager, airplane_mode); - } else if (g_strcmp0 (property_name, "BluetoothAirplaneMode") == 0) { - gboolean airplane_mode; - g_variant_get (value, "b", &airplane_mode); - return engine_set_bluetooth_airplane_mode (manager, airplane_mode); - } - - if (g_strcmp0 (property_name, "WwanAirplaneMode") == 0) { - gboolean airplane_mode; - g_variant_get (value, "b", &airplane_mode); - return engine_set_wwan_airplane_mode (manager, airplane_mode); + guint i; + gboolean bool_value; + GsdRfkillManager *manager; + + manager = GSD_RFKILL_MANAGER (user_data); + for (i = 0; ENGINE_PROP_SETTERS[i].prop_name != NULL; i++) { + if (g_strcmp0 (property_name, ENGINE_PROP_SETTERS[i].prop_name) == 0) { + gboolean prop_changed; + g_variant_get (value, "b", &bool_value); + prop_changed = ENGINE_PROP_SETTERS[i].setter (manager, bool_value); + if (prop_changed) { + engine_properties_changed (manager); + } + return TRUE; + } } - + *error = g_error_new_literal (G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, "Unknown property!"); return FALSE; } @@ -542,7 +735,11 @@ handle_get_property (GDBusConnection *connection, GError **error, gpointer user_data) { - GsdRfkillManager *manager = GSD_RFKILL_MANAGER (user_data); + guint i; + gboolean bool_value; + GsdRfkillManager *manager; + + manager = GSD_RFKILL_MANAGER (user_data); /* Check session pointer as a proxy for whether the manager is in the start or stop state */ @@ -550,64 +747,11 @@ handle_get_property (GDBusConnection *connection, return NULL; } - if (g_strcmp0 (property_name, "AirplaneMode") == 0) { - gboolean airplane_mode; - airplane_mode = engine_get_airplane_mode (manager); - return g_variant_new_boolean (airplane_mode); - } - - if (g_strcmp0 (property_name, "HardwareAirplaneMode") == 0) { - gboolean hw_airplane_mode; - hw_airplane_mode = engine_get_hardware_airplane_mode (manager); - return g_variant_new_boolean (hw_airplane_mode); - } - - if (g_strcmp0 (property_name, "ShouldShowAirplaneMode") == 0) { - gboolean should_show_airplane_mode; - should_show_airplane_mode = engine_get_should_show_airplane_mode (manager); - return g_variant_new_boolean (should_show_airplane_mode); - } - - if (g_strcmp0 (property_name, "HasAirplaneMode") == 0) { - gboolean has_airplane_mode; - has_airplane_mode = engine_get_has_airplane_mode (manager); - return g_variant_new_boolean (has_airplane_mode); - } - - if (g_strcmp0 (property_name, "BluetoothAirplaneMode") == 0) { - gboolean airplane_mode; - airplane_mode = engine_get_bluetooth_airplane_mode (manager); - return g_variant_new_boolean (airplane_mode); - } - - if (g_strcmp0 (property_name, "BluetoothHardwareAirplaneMode") == 0) { - gboolean hw_airplane_mode; - hw_airplane_mode = engine_get_bluetooth_hardware_airplane_mode (manager); - return g_variant_new_boolean (hw_airplane_mode); - } - - if (g_strcmp0 (property_name, "BluetoothHasAirplaneMode") == 0) { - gboolean has_airplane_mode; - has_airplane_mode = engine_get_has_bluetooth_airplane_mode (manager); - return g_variant_new_boolean (has_airplane_mode); - } - - if (g_strcmp0 (property_name, "WwanAirplaneMode") == 0) { - gboolean airplane_mode; - airplane_mode = engine_get_wwan_airplane_mode (manager); - return g_variant_new_boolean (airplane_mode); - } - - if (g_strcmp0 (property_name, "WwanHardwareAirplaneMode") == 0) { - gboolean hw_airplane_mode; - hw_airplane_mode = engine_get_wwan_hardware_airplane_mode (manager); - return g_variant_new_boolean (hw_airplane_mode); - } - - if (g_strcmp0 (property_name, "WwanHasAirplaneMode") == 0) { - gboolean has_airplane_mode; - has_airplane_mode = engine_get_has_wwan_airplane_mode (manager); - return g_variant_new_boolean (has_airplane_mode); + for (i = 0; ENGINE_PROP_GETTERS[i].prop_name != NULL; i++) { + if (g_strcmp0 (property_name, ENGINE_PROP_GETTERS[i].prop_name) == 0) { + bool_value = ENGINE_PROP_GETTERS[i].getter (manager); + return g_variant_new_boolean (bool_value); + } } return NULL; @@ -654,6 +798,7 @@ on_bus_gotten (GObject *source_object, NULL); manager->session = gnome_settings_bus_get_session_proxy (); + manager->rfkill_input_inhibit_binding = g_object_bind_property (manager->session, "session-is-active", manager->rfkill, "rfkill-input-inhibited", G_BINDING_SYNC_CREATE); @@ -669,10 +814,17 @@ sync_wwan_enabled (GsdRfkillManager *manager) if (property == NULL) { /* GDBus telling us NM went down */ + g_debug ("sync_wwan_enabled: GDBus telling us NM went down."); return; } manager->wwan_enabled = g_variant_get_boolean (property); + g_debug ("sync_wwan_enabled: WwanEnabled: %s", manager->wwan_enabled ? "true" : "false"); + + if (manager->apm_active && manager->wwan_enabled) { + /* Disable APM since WWAN is now enabled */ + engine_set_airplane_mode (manager, FALSE); + } engine_properties_changed (manager); g_variant_unref (property); @@ -696,6 +848,7 @@ nm_properties_changed (GDBusProxy *proxy, } } + static void on_nm_proxy_gotten (GObject *source, GAsyncResult *result, @@ -790,19 +943,27 @@ gsd_rfkill_manager_startup (GApplication *application) manager->killswitches = g_hash_table_new (g_direct_hash, g_direct_equal); manager->bt_killswitches = g_hash_table_new (g_direct_hash, g_direct_equal); manager->wwan_killswitches = g_hash_table_new (g_direct_hash, g_direct_equal); + + manager->apm_active = FALSE; + manager->apm_nm_wwan_blocked = FALSE; + manager->apm_blocked_devices = g_hash_table_new (g_direct_hash, g_direct_equal); + manager->rfkill = cc_rfkill_glib_new (); g_signal_connect (G_OBJECT (manager->rfkill), "changed", G_CALLBACK (rfkill_changed), manager); - if (!cc_rfkill_glib_open (manager->rfkill, &local_error)) { + manager->cancellable = g_cancellable_new (); + + if (!cc_rfkill_glib_open (manager->rfkill, manager->cancellable, rfkill_set_cb, manager, &local_error)) { g_warning ("Error setting up rfkill: %s", local_error->message); g_clear_error (&local_error); } - manager->cancellable = g_cancellable_new (); manager->chassis_type = gnome_settings_get_chassis_type (); + g_debug ("Chassis_type: %s", manager->chassis_type); + g_dbus_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, NULL, /* g-interface-info */ @@ -836,12 +997,13 @@ gsd_rfkill_manager_shutdown (GApplication *application) { GsdRfkillManager *manager = GSD_RFKILL_MANAGER (application); - g_debug ("Stopping rfkill manager"); + g_debug ("Stopping rfkill manager - nameid: %d -", manager->name_id); if (manager->name_id != 0) { g_bus_unown_name (manager->name_id); manager->name_id = 0; } + engine_apm_sync_throttle_clear (manager); g_clear_pointer (&manager->introspection_data, g_dbus_node_info_unref); g_clear_object (&manager->connection); @@ -852,6 +1014,10 @@ gsd_rfkill_manager_shutdown (GApplication *application) g_clear_pointer (&manager->bt_killswitches, g_hash_table_destroy); g_clear_pointer (&manager->wwan_killswitches, g_hash_table_destroy); + manager->apm_active = FALSE; + manager->apm_nm_wwan_blocked = FALSE; + g_clear_pointer(&manager->apm_blocked_devices, g_hash_table_destroy); + if (manager->cancellable) { g_cancellable_cancel (manager->cancellable); g_clear_object (&manager->cancellable); diff --git a/plugins/rfkill/rfkill-glib.c b/plugins/rfkill/rfkill-glib.c index a5e49bc35..bc8342265 100644 --- a/plugins/rfkill/rfkill-glib.c +++ b/plugins/rfkill/rfkill-glib.c @@ -65,31 +65,47 @@ struct _CcRfkillGlib { gboolean noinput; int noinput_fd; - /* Pending Bluetooth enablement. - * If (@change_all_timeout_id != 0), then (task != NULL). The converse - * does not necessarily hold. */ - guint change_all_timeout_id; GTask *task; + GCancellable *cancellable; + GAsyncReadyCallback task_callback; + gpointer task_user_data; +}; + +struct rfkill_task_data { + GArray *events; + guint send_idx; }; G_DEFINE_TYPE (CcRfkillGlib, cc_rfkill_glib, G_TYPE_OBJECT) -#define CHANGE_ALL_TIMEOUT 500 +static void cc_rfkill_write_discrete_events_async (CcRfkillGlib *rfkill); -static const char *type_to_string (unsigned int type); +static struct rfkill_task_data * +rfkill_task_data_new (guint event_count) +{ + struct rfkill_task_data *t_data; + t_data = g_new0 (struct rfkill_task_data, 1); + t_data->events = g_array_sized_new (FALSE, FALSE, sizeof(struct rfkill_event), event_count); + return t_data; +} static void -cancel_current_task (CcRfkillGlib *rfkill) +rfkill_task_data_free (struct rfkill_task_data *t_data) { - if (rfkill->task != NULL) { - g_cancellable_cancel (g_task_get_cancellable (rfkill->task)); - g_clear_object (&rfkill->task); - } + g_array_unref (t_data->events); + g_free (t_data); +} - if (rfkill->change_all_timeout_id != 0) { - g_source_remove (rfkill->change_all_timeout_id); - rfkill->change_all_timeout_id = 0; - } +static struct rfkill_event * +rfkill_task_data_next_event (struct rfkill_task_data *t_data) +{ + return &g_array_index (t_data->events, struct rfkill_event, t_data->send_idx++); +} + +static gboolean +rfkill_task_data_has_next_event (struct rfkill_task_data *t_data) +{ + return t_data->send_idx < t_data->events->len; } /* Note that this can return %FALSE without setting @error. */ @@ -105,160 +121,121 @@ cc_rfkill_glib_send_change_all_event_finish (CcRfkillGlib *rfkill, return g_task_propagate_boolean (G_TASK (res), error); } -static void -write_change_all_again_done_cb (GObject *source_object, - GAsyncResult *res, - gpointer user_data) -{ - g_autoptr(GTask) task = G_TASK (user_data); - CcRfkillGlib *rfkill = g_task_get_source_object (task); - g_autoptr(GError) error = NULL; - gssize ret; - - g_debug ("Finished writing second RFKILL_OP_CHANGE_ALL event"); - - ret = g_output_stream_write_finish (G_OUTPUT_STREAM (source_object), res, &error); - if (ret < 0) - g_task_return_error (task, g_steal_pointer (&error)); - else - g_task_return_boolean (task, ret >= 0); - - /* If this @task has been cancelled, it may have been superceded. */ - if (rfkill->task == task) - g_clear_object (&rfkill->task); -} - -static gboolean -write_change_all_timeout_cb (CcRfkillGlib *rfkill) -{ - struct rfkill_event *event; - - g_assert (rfkill->task != NULL); - - g_debug ("Sending second RFKILL_OP_CHANGE_ALL timed out"); - - event = g_task_get_task_data (rfkill->task); - g_task_return_new_error (rfkill->task, - G_IO_ERROR, G_IO_ERROR_TIMED_OUT, - "Enabling rfkill for %s timed out", - type_to_string (event->type)); - - g_clear_object (&rfkill->task); - rfkill->change_all_timeout_id = 0; - - return G_SOURCE_REMOVE; -} static void write_change_all_done_cb (GObject *source_object, GAsyncResult *res, gpointer user_data) { - g_autoptr(GTask) task = G_TASK (user_data); + GTask *task = G_TASK (user_data); CcRfkillGlib *rfkill = g_task_get_source_object (task); g_autoptr(GError) error = NULL; gssize ret; - struct rfkill_event *event; + struct rfkill_task_data *t_data; - g_debug ("Sending original RFKILL_OP_CHANGE_ALL event done"); + t_data = g_task_get_task_data (task); - event = g_task_get_task_data (task); ret = g_output_stream_write_finish (G_OUTPUT_STREAM (source_object), res, &error); if (ret < 0) { + g_debug("ERROR send task"); g_task_return_error (task, g_steal_pointer (&error)); - goto bail; - } else if (event->soft == 1 || - event->type != RFKILL_TYPE_BLUETOOTH) { - g_task_return_boolean (task, ret >= 0); - goto bail; + } else { + if (rfkill_task_data_has_next_event(t_data)) { + g_debug("CONTINUE send task"); + cc_rfkill_write_discrete_events_async (rfkill); + return; + } else { + g_task_return_boolean (task, ret >= 0); + g_debug("WRITTEN send task"); + } } - - g_assert (rfkill->change_all_timeout_id == 0); - rfkill->change_all_timeout_id = g_timeout_add (CHANGE_ALL_TIMEOUT, - (GSourceFunc) write_change_all_timeout_cb, - rfkill); - - return; - -bail: - /* If this @task has been cancelled, it may have been superceded. */ - if (rfkill->task == task) - g_clear_object (&rfkill->task); + /* Task is done */ + g_clear_object (&rfkill->task); } void cc_rfkill_glib_send_change_all_event (CcRfkillGlib *rfkill, guint rfkill_type, - gboolean enable, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data) + GArray *indices, + gboolean enable) { - g_autoptr(GTask) task = NULL; - struct rfkill_event *event; - g_autoptr(GCancellable) task_cancellable = NULL; + struct rfkill_task_data *t_data; + guint i, prev_event_count, event_count; g_return_if_fail (CC_RFKILL_IS_GLIB (rfkill)); + g_return_if_fail (rfkill->cancellable); g_return_if_fail (rfkill->stream); - task_cancellable = g_cancellable_new (); - g_signal_connect_object (cancellable, "cancelled", - (GCallback) g_cancellable_cancel, - task_cancellable, - G_CONNECT_SWAPPED); - /* Now check if it is cancelled already */ - if (g_cancellable_is_cancelled (cancellable)) - g_cancellable_cancel (task_cancellable); + event_count = indices == NULL ? 1 : indices->len; + g_return_if_fail (event_count >= 1); + + if (rfkill->task == NULL) { + /* Create new task */ + rfkill->task = g_task_new (rfkill, + rfkill->cancellable, + rfkill->task_callback, + rfkill->task_user_data); + g_task_set_source_tag (rfkill->task, cc_rfkill_glib_send_change_all_event); + + t_data = rfkill_task_data_new (event_count); + g_task_set_task_data (rfkill->task, t_data, (GDestroyNotify) rfkill_task_data_free); + } else { + /* Append events to the current task */ + t_data = g_task_get_task_data(rfkill->task); + } - task = g_task_new (rfkill, task_cancellable, callback, user_data); - g_task_set_source_tag (task, cc_rfkill_glib_send_change_all_event); + /* Add rfkill events to task data. */ + prev_event_count = t_data->events->len; + g_array_set_size (t_data->events, prev_event_count + event_count); + for (i = 0; i < event_count; i++) { + struct rfkill_event e = { + .idx = indices == NULL ? -1 : g_array_index (indices, gint, i), + .op = indices == NULL ? RFKILL_OP_CHANGE_ALL : RFKILL_OP_CHANGE, + .type = rfkill_type, + .soft = enable ? 0 : 1, + .hard = 0 + }; + g_array_index (t_data->events, struct rfkill_event, prev_event_count + i) = e; + } + g_debug("Send task with %d events. send_index: %d", t_data->events->len, t_data->send_idx); - /* Clear any previous task. */ - cancel_current_task (rfkill); - g_assert (rfkill->task == NULL); + if (t_data->send_idx == 0) { + /* Start async write loop task */ + g_debug("START send task"); + cc_rfkill_write_discrete_events_async (rfkill); + } +} - /* Start writing out a new event. */ - event = g_new0 (struct rfkill_event, 1); - event->op = RFKILL_OP_CHANGE_ALL; - event->type = rfkill_type; - event->soft = enable ? 1 : 0; +static void +cc_rfkill_write_discrete_events_async (CcRfkillGlib *rfkill) +{ + struct rfkill_task_data *t_data; + g_return_if_fail (rfkill->task != NULL); - g_task_set_task_data (task, event, g_free); - rfkill->task = g_object_ref (task); - rfkill->change_all_timeout_id = 0; + t_data = g_task_get_task_data (rfkill->task); + g_return_if_fail (rfkill_task_data_has_next_event (t_data)); g_output_stream_write_async (rfkill->stream, - event, sizeof(struct rfkill_event), - G_PRIORITY_DEFAULT, - task_cancellable, write_change_all_done_cb, - g_object_ref (task)); + rfkill_task_data_next_event (t_data), sizeof(struct rfkill_event), + G_PRIORITY_DEFAULT, + g_task_get_cancellable (rfkill->task), write_change_all_done_cb, + g_object_ref (rfkill->task)); } -static const char * -type_to_string (unsigned int type) +gboolean cc_rfkill_is_ready (CcRfkillGlib *rfkill) { + return rfkill->stream != NULL; +} + +const gchar +*cc_rfkill_type_to_string (enum rfkill_type r_type) { - switch (type) { - case RFKILL_TYPE_ALL: - return "ALL"; - case RFKILL_TYPE_WLAN: - return "WLAN"; - case RFKILL_TYPE_BLUETOOTH: - return "BLUETOOTH"; - case RFKILL_TYPE_UWB: - return "UWB"; - case RFKILL_TYPE_WIMAX: - return "WIMAX"; - case RFKILL_TYPE_WWAN: - return "WWAN"; - default: - return "UNKNOWN"; - } + return (guint)r_type < NUM_RFKILL_TYPES ? rfkill_type_table[(guint)r_type] : "unknown"; } -static const char * -op_to_string (unsigned int op) +const gchar +*cc_rfkill_op_to_string (enum rfkill_operation r_op) { - switch (op) { + switch (r_op) { case RFKILL_OP_ADD: return "ADD"; case RFKILL_OP_DEL: @@ -268,110 +245,62 @@ op_to_string (unsigned int op) case RFKILL_OP_CHANGE_ALL: return "CHANGE_ALL"; default: - g_assert_not_reached (); + return "UNKNOWN"; } } +const guint +cc_rfkill_event_state (struct rfkill_event *event) +{ + return event->hard + ? RFKILL_STATE_HARD_BLOCKED + : event->soft + ? RFKILL_STATE_SOFT_BLOCKED + : RFKILL_STATE_UNBLOCKED; +} static void print_event (struct rfkill_event *event) { g_debug ("RFKILL event: idx %u type %u (%s) op %u (%s) soft %u hard %u", event->idx, - event->type, type_to_string (event->type), - event->op, op_to_string (event->op), + event->type, cc_rfkill_type_to_string (event->type), + event->op, cc_rfkill_op_to_string (event->op), event->soft, event->hard); } static gboolean -got_change_event (GList *events) -{ - GList *l; - - g_assert (events != NULL); - - for (l = events ; l != NULL; l = l->next) { - struct rfkill_event *event = l->data; - - if (event->op == RFKILL_OP_CHANGE) - return TRUE; - } - - return FALSE; -} - -static void -emit_changed_signal_and_free (CcRfkillGlib *rfkill, - GList *events) -{ - if (events == NULL) - return; - - g_signal_emit (G_OBJECT (rfkill), - signals[CHANGED], - 0, events); - - if (rfkill->change_all_timeout_id > 0 && - got_change_event (events)) { - struct rfkill_event *event; - - g_debug ("Received a change event after a RFKILL_OP_CHANGE_ALL event, re-sending RFKILL_OP_CHANGE_ALL"); - - event = g_task_get_task_data (rfkill->task); - g_output_stream_write_async (rfkill->stream, - event, sizeof(struct rfkill_event), - G_PRIORITY_DEFAULT, - g_task_get_cancellable (rfkill->task), - write_change_all_again_done_cb, - g_object_ref (rfkill->task)); - - g_source_remove (rfkill->change_all_timeout_id); - rfkill->change_all_timeout_id = 0; - } - - g_list_free_full (events, g_free); -} - -static gboolean -event_cb (GIOChannel *source, - GIOCondition condition, +event_cb (GIOChannel *source, + GIOCondition condition, CcRfkillGlib *rfkill) { - GList *events; + g_autoptr(GArray) events = NULL; - events = NULL; + if (!(condition & G_IO_IN)) { + g_debug ("Something unexpected happened on rfkill fd"); + return FALSE; + } - if (condition & G_IO_IN) { + events = g_array_sized_new (FALSE, FALSE, sizeof(struct rfkill_event), 1); + while (TRUE) { + gsize read; GIOStatus status; struct rfkill_event event = { 0 }; - gsize read; status = g_io_channel_read_chars (source, (char *) &event, - sizeof(event), + RFKILL_EVENT_SIZE_V1, &read, NULL); - while (status == G_IO_STATUS_NORMAL && read >= RFKILL_EVENT_SIZE_V1) { - struct rfkill_event *event_ptr; - - print_event (&event); + if (status != G_IO_STATUS_NORMAL || read < RFKILL_EVENT_SIZE_V1) + break; - event_ptr = g_memdup2 (&event, sizeof (event)); - events = g_list_prepend (events, event_ptr); + print_event (&event); - status = g_io_channel_read_chars (source, - (char *) &event, - sizeof(event), - &read, - NULL); - } - events = g_list_reverse (events); - } else { - g_debug ("Something unexpected happened on rfkill fd"); - return FALSE; + g_array_append_val (events, event); } - emit_changed_signal_and_free (rfkill, events); + g_signal_emit ( G_OBJECT (rfkill), signals[CHANGED], 0, events); return TRUE; } @@ -379,8 +308,20 @@ event_cb (GIOChannel *source, static void cc_rfkill_glib_init (CcRfkillGlib *rfkill) { + rfkill->udev = NULL; rfkill->device_file = NULL; + + rfkill->stream = NULL; + rfkill->channel = NULL; + rfkill->watch_id = 0; + + rfkill->noinput = FALSE; rfkill->noinput_fd = -1; + + rfkill->task = NULL; + rfkill->cancellable = NULL; + rfkill->task_callback = NULL; + rfkill->task_user_data = NULL; } static gboolean @@ -446,6 +387,7 @@ uevent_cb (GUdevClient *client, rfkill->device_file = g_strdup (g_udev_device_get_device_file (device)); } else { g_warning ("rfkill udev device does not have a device file!"); + return; } if (!_cc_rfkill_glib_open (rfkill, &error)) @@ -461,20 +403,38 @@ uevent_cb (GUdevClient *client, } gboolean -cc_rfkill_glib_open (CcRfkillGlib *rfkill, - GError **error) +cc_rfkill_glib_open (CcRfkillGlib *rfkill, + GCancellable *cancellable, + GAsyncReadyCallback task_callback, + gpointer task_user_data, + GError **error) { - const char * const subsystems[] = { "misc", NULL }; GUdevDevice *device; - rfkill->udev = g_udev_client_new (subsystems); - g_debug ("Setting up uevent listener"); - g_signal_connect (rfkill->udev, "uevent", G_CALLBACK (uevent_cb), rfkill); + if (rfkill->udev == NULL) { + const char * const subsystems[] = { "misc", NULL }; + rfkill->udev = g_udev_client_new (subsystems); + g_debug ("Setting up uevent listener"); + g_signal_connect (rfkill->udev, "uevent", G_CALLBACK (uevent_cb), rfkill); + } /* Simulate uevent if device already exists. */ device = g_udev_client_query_by_subsystem_and_name (rfkill->udev, "misc", "rfkill"); - if (device) + if (device) { uevent_cb (rfkill->udev, "add", device, rfkill); + } else { + g_error_new_literal ( G_FILE_ERROR, + G_FILE_ERROR_NOENT, + "Device not found! devname=rfkill subsystem=misc"); + return FALSE; + } + + if (cancellable != NULL ) { + g_clear_object (&rfkill->cancellable); + rfkill->cancellable = g_object_ref (cancellable); + } + rfkill->task_callback = task_callback; + rfkill->task_user_data = task_user_data; return TRUE; } @@ -584,7 +544,11 @@ cc_rfkill_glib_finalize (GObject *object) { CcRfkillGlib *rfkill = CC_RFKILL_GLIB (object); - cancel_current_task (rfkill); + if (rfkill->cancellable) { + g_cancellable_cancel (rfkill->cancellable); + g_clear_object (&rfkill->cancellable); + } + g_clear_object (&rfkill->task); /* cleanup monitoring */ if (rfkill->watch_id > 0) { diff --git a/plugins/rfkill/rfkill-glib.h b/plugins/rfkill/rfkill-glib.h index 0655eb484..660618b9a 100644 --- a/plugins/rfkill/rfkill-glib.h +++ b/plugins/rfkill/rfkill-glib.h @@ -33,24 +33,30 @@ G_BEGIN_DECLS #define CC_RFKILL_TYPE_GLIB cc_rfkill_glib_get_type () G_DECLARE_FINAL_TYPE (CcRfkillGlib, cc_rfkill_glib, CC_RFKILL, GLIB, GObject) -CcRfkillGlib *cc_rfkill_glib_new (void); -gboolean cc_rfkill_glib_open (CcRfkillGlib *rfkill, - GError **error); - -gboolean cc_rfkill_glib_get_rfkill_input_inhibited (CcRfkillGlib *rfkill); -void cc_rfkill_glib_set_rfkill_input_inhibited (CcRfkillGlib *rfkill, - gboolean noinput); - -void cc_rfkill_glib_send_change_all_event (CcRfkillGlib *rfkill, - guint rfkill_type, - gboolean enable, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data); - -gboolean cc_rfkill_glib_send_change_all_event_finish (CcRfkillGlib *rfkill, - GAsyncResult *res, - GError **error); +CcRfkillGlib *cc_rfkill_glib_new (void); +gboolean cc_rfkill_glib_open (CcRfkillGlib *rfkill, + GCancellable *cancellable, + GAsyncReadyCallback task_callback, + gpointer task_user_data, + GError **error); + +gboolean cc_rfkill_glib_get_rfkill_input_inhibited (CcRfkillGlib *rfkill); +void cc_rfkill_glib_set_rfkill_input_inhibited (CcRfkillGlib *rfkill, + gboolean noinput); + +void cc_rfkill_glib_send_change_all_event (CcRfkillGlib *rfkill, + guint rfkill_type, + GArray *indices, + gboolean enable); + +gboolean cc_rfkill_glib_send_change_all_event_finish (CcRfkillGlib *rfkill, + GAsyncResult *res, + GError **error); + +gboolean cc_rfkill_is_ready (CcRfkillGlib *rfkill); +const gchar *cc_rfkill_type_to_string (enum rfkill_type r_type); +const gchar *cc_rfkill_op_to_string (enum rfkill_operation r_op); +const guint cc_rfkill_event_state (struct rfkill_event *event); G_END_DECLS diff --git a/plugins/rfkill/rfkill.h b/plugins/rfkill/rfkill.h index abb2c6613..45669516b 100644 --- a/plugins/rfkill/rfkill.h +++ b/plugins/rfkill/rfkill.h @@ -37,6 +37,7 @@ * @RFKILL_TYPE_WWAN: switch is on a wireless WAN device. * @RFKILL_TYPE_GPS: switch is on a GPS device. * @RFKILL_TYPE_FM: switch is on a FM radio device. + * @RFKILL_TYPE_NFC: switch is on a NFC device. * @NUM_RFKILL_TYPES: number of defined rfkill types */ enum rfkill_type { @@ -48,9 +49,22 @@ enum rfkill_type { RFKILL_TYPE_WWAN, RFKILL_TYPE_GPS, RFKILL_TYPE_FM, + RFKILL_TYPE_NFC, NUM_RFKILL_TYPES, }; +static const char* const rfkill_type_table[NUM_RFKILL_TYPES] = { + [RFKILL_TYPE_ALL] = "all", + [RFKILL_TYPE_WLAN] = "wlan", + [RFKILL_TYPE_BLUETOOTH] = "bluetooth", + [RFKILL_TYPE_UWB] = "uwb", + [RFKILL_TYPE_WIMAX] = "wimax", + [RFKILL_TYPE_WWAN] = "wwan", + [RFKILL_TYPE_GPS] = "gps", + [RFKILL_TYPE_FM] = "fm", + [RFKILL_TYPE_NFC] = "nfc", +}; + /** * enum rfkill_operation - operation types * @RFKILL_OP_ADD: a device was added -- GitLab From bcbc749927617618f5ed647d31b8ce271bba3ad1 Mon Sep 17 00:00:00 2001 From: Jan Zickermann Date: Thu, 9 Feb 2023 00:00:33 +0100 Subject: [PATCH 2/3] rfkill: Test udev conn and airplane mode We test the rfkill plugin with dbusmock and umockdev in python. The setup of the test is similar to the one by the power plugin. --- plugins/rfkill/meson.build | 24 + plugins/rfkill/rfkill-glib.c | 22 +- plugins/rfkill/test-mm-template.py | 63 +++ plugins/rfkill/test.py | 700 +++++++++++++++++++++++++++++ 4 files changed, 805 insertions(+), 4 deletions(-) create mode 100644 plugins/rfkill/test-mm-template.py create mode 100755 plugins/rfkill/test.py diff --git a/plugins/rfkill/meson.build b/plugins/rfkill/meson.build index 422214492..83f654e80 100644 --- a/plugins/rfkill/meson.build +++ b/plugins/rfkill/meson.build @@ -17,6 +17,7 @@ deps += [ m_dep ] + executable( 'gsd-' + plugin_name, sources, @@ -27,3 +28,26 @@ executable( install_rpath: gsd_pkglibdir, install_dir: gsd_libexecdir ) + +# Tests for: +# (1) udev connection of CcRfkillGlib +# (2) airplane mode of GsdRfkillManager +test_py = find_program('test.py') +envs = environment() +#envs.prepend('G_DEBUG', 'fatal-warnings') +envs.set('BUILDDIR', meson.current_build_dir()) +envs.set('TOP_BUILDDIR', meson.build_root()) +envs.set('LD_PRELOAD', 'libumockdev-preload.so.0') +envs.set('NO_AT_BRIDGE', '1') + +envs.set('RFKILL_USE_BUSYPOLL', '1') + +foreach i : [ 1, 2 ] + test( + 'test-rfkill @0@/2'.format(i), + test_py, + args: [ 'RfkillPluginTest@0@'.format(i) ], + env: envs, + timeout: 60 + ) +endforeach diff --git a/plugins/rfkill/rfkill-glib.c b/plugins/rfkill/rfkill-glib.c index bc8342265..ed06b8f4c 100644 --- a/plugins/rfkill/rfkill-glib.c +++ b/plugins/rfkill/rfkill-glib.c @@ -324,6 +324,12 @@ cc_rfkill_glib_init (CcRfkillGlib *rfkill) rfkill->task_user_data = NULL; } +static gboolean +busypoll_cb (CcRfkillGlib *rfkill) +{ + return event_cb (rfkill->channel, G_IO_IN, rfkill); +} + static gboolean _cc_rfkill_glib_open (CcRfkillGlib *rfkill, GError **error) @@ -355,14 +361,22 @@ _cc_rfkill_glib_open (CcRfkillGlib *rfkill, rfkill->channel = g_io_channel_unix_new (fd); g_io_channel_set_encoding (rfkill->channel, NULL, NULL); g_io_channel_set_buffered (rfkill->channel, FALSE); - rfkill->watch_id = g_io_add_watch (rfkill->channel, - G_IO_IN | G_IO_HUP | G_IO_ERR, - (GIOFunc) event_cb, - rfkill); + if (g_getenv ("RFKILL_USE_BUSYPOLL")) { + /* test.py needs this since umockdev does not support poll */ + rfkill->watch_id = g_timeout_add (20, (GSourceFunc) busypoll_cb, rfkill); + } else { + rfkill->watch_id = g_io_add_watch (rfkill->channel, + G_IO_IN | G_IO_HUP | G_IO_ERR, + (GIOFunc) event_cb, + rfkill); + } /* Setup write stream */ rfkill->stream = g_unix_output_stream_new (fd, TRUE); + g_debug("Rfkill io stream and %s have been started", + g_getenv ("RFKILL_USE_BUSYPOLLING") ? "busypoll" : "watch"); + return TRUE; } diff --git a/plugins/rfkill/test-mm-template.py b/plugins/rfkill/test-mm-template.py new file mode 100644 index 000000000..78b2e9f55 --- /dev/null +++ b/plugins/rfkill/test-mm-template.py @@ -0,0 +1,63 @@ +''' +ModemManager1 mock template +This creates the expected methods org.freedesktop.ModemManager1 (without functionality) +''' + +__author__ = 'Jan Zickermann ' +__copyright__ = '(C) 2023 Jan Zickermann' +__license__ = 'GPL v2 or later' + + +import dbus +import dbusmock +from dbusmock import MOCK_IFACE + +BUS_NAME = 'org.freedesktop.ModemManager1' +MAIN_OBJ = '/org/freedesktop/ModemManager1' +SYSTEM_BUS = True +IS_OBJECT_MANAGER = True + + +def scan_devices(self): + pass + + +def set_logging(self, level): + pass + + +def report_kernel_event(self, properties): + pass + + +def inhibit_device(uid, inhibit): + pass + + +def load(mock, parameters): + mock.scan_devices = scan_devices + mock.set_logging = set_logging + mock.report_kernel_event = report_kernel_event + mock.inhibit_device = inhibit_device + mock.AddProperty('', 'Version', parameters.get( + 'Version', '1.18.12-1.fc37-mock')) + mock.AddMethods('', [ + ('ScanDevices', '', '', 'self.scan_devices(self)'), + ('SetLogging', 's', '', 'self.set_logging(self, args[0])'), + ('ReportKernelEvent', 'a{sv}', '', + 'self.report_kernel_event(self, args[0])'), + ('InhibitDevice', 'sb', '', + 'self.inhibit_device(self, args[0], args[1])'), + ]) + mock.object_manager_emit_added(MAIN_OBJ) + + +@dbus.service.method(MOCK_IFACE, in_signature='s', out_signature='s') +def AddDummyDevice(self, device_name): + path = '/'.join([MAIN_OBJ, device_name]) + self.AddObject(path, '.'.join([BUS_NAME, 'DummyDevice']), { + 'DeviceType': dbus.UInt32(11), + 'State': dbus.UInt32(22), + }, []) + self.object_manager_emit_added(path) + return path diff --git a/plugins/rfkill/test.py b/plugins/rfkill/test.py new file mode 100755 index 000000000..d10d4c6de --- /dev/null +++ b/plugins/rfkill/test.py @@ -0,0 +1,700 @@ +#!/usr/bin/python3 -u +'''GNOME settings daemon tests for rfkill plugin.''' + +import gi +import dbus +from dbus.mainloop.glib import DBusGMainLoop +import struct + +__author__ = 'Jan Zickermann ' +__copyright__ = '(C) 2023 Jan Zickermann' +__license__ = 'GPL v2 or later' + +import unittest +import sys +import os +from pathlib import Path +import logging +import time + +test_dir = Path(__file__).parent +project_root = test_dir.parent.parent.absolute() +builddir = os.environ.get('BUILDDIR', str(test_dir)) +top_builddir = os.environ.get('TOP_BUILDDIR', str(project_root)) + +sys.path.insert(0, str(project_root / 'tests')) +sys.path.insert(0, str(builddir)) +import gsdtestcase # noqa +import dbusmock # noqa + + +DBusGMainLoop(set_as_default=True) + +gi.require_version('UMockdev', '1.0') +from gi.repository import UMockdev # noqa + + +# Defined in: https://github.com/torvalds/linux/blob/master/include/uapi/linux/rfkill.h +RFKILL_TYPE_ALL = 0 +RFKILL_TYPE_WLAN = 1 +RFKILL_TYPE_BLUETOOTH = 2 +RFKILL_TYPE_UWB = 3 +RFKILL_TYPE_WIMAX = 4 +RFKILL_TYPE_WWAN = 5 +RFKILL_TYPE_GPS = 6 +RFKILL_TYPE_FM = 7 +RFKILL_TYPE_NFC = 8 + +RFKILL_STATE_SOFT_BLOCKED = 1 +RFKILL_STATE_UNBLOCKED = 1 +RFKILL_STATE_HARD_BLOCKED = 2 + +rfkill_event = 'IBBBB' +size_of_rfkill_event = struct.calcsize(rfkill_event) + +RFKILL_OP_ADD = 0 +RFKILL_OP_DEL = 1 +RFKILL_OP_CHANGE = 2 +RFKILL_OP_CHANGE_ALL = 3 + +# Defined in: https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/errno.h +EAGAIN = 41 +ENOMSG = 42 +# Direction bits defined in: https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/ioctl.h +RFKILL_IOC_MAGIC = 'R' +RFKILL_IOC_NOINPUT = 1 +RFKILL_IOCTL_NOINPUT = (ord(RFKILL_IOC_MAGIC) << 8) | RFKILL_IOC_NOINPUT + +DEVICES = [ + # name, type_id, soft, hard + ['WWAN 1', RFKILL_TYPE_WWAN, 0, 0], + ['Bluetooth 1', RFKILL_TYPE_BLUETOOTH, 0, 0], + ['Bluetooth 2', RFKILL_TYPE_BLUETOOTH, 0, 0], + ['Bluetooth 3', RFKILL_TYPE_BLUETOOTH, 1, 0], + ['Bluetooth 4', RFKILL_TYPE_BLUETOOTH, 0, 1], + ['Bluetooth 5', RFKILL_TYPE_BLUETOOTH, 1, 1], + ['NFC 1', RFKILL_TYPE_NFC, 0, 0], +] + +log = logging.getLogger() + + +def setup_logger(name): + global log + formatter = logging.Formatter( + fmt='%(name)s: %(levelname)-8s: %(asctime)s.%(msecs)03d: %(message)s', datefmt='%H:%M:%S') + stdout_handler = logging.StreamHandler(stream=sys.stdout) + stdout_handler.setFormatter(formatter) + log = logging.getLogger(name) + log.setLevel(logging.DEBUG) + log.addHandler(stdout_handler) + + label = f'TEST - {name}' + log.info(f'\n {"_" * 80} \n|{" " * 80}|\n|{label:^80s}|\n|{"_" * 80}|') + + +class RfkillHandlerMock(): + """ + Mock the misc udev 'rfkill' at /dev/rfkill. + Implemented here: https://github.com/torvalds/linux/blob/master/net/rfkill/core.c#L1373 + (Mocking rfkill_fop_poll is not supported by umockdev thus we rely on RFKILL_USE_BUSYPOLLING=1) + """ + + def __init__(self, testbed, test_id): + handler = UMockdev.IoctlBase() + self.handler = handler + self.testbed = testbed + self.error = False + + def fop(name): + def fop_func(_, client): + try: + return getattr(self, name)(client) + except: + log.exception(f'"{name}" call has failed!') + self.error = True + return False + return fop_func + handler.connect('handle-ioctl', fop('rfkill_fop_ioctl')) + handler.connect('handle-read', fop('rfkill_fop_read')) + handler.connect('handle-write', fop('rfkill_fop_write')) + + log.info('[RfkillHandlerMock] attach rfkill mocked udev handler') + testbed.add_from_string( + 'P: /devices/rfkill\nN: rfkill\nE: SUBSYSTEM=misc\nE: DEVNAME=/dev/rfkill') + testbed.attach_ioctl('/dev/rfkill', handler) + self.reset(test_id) + time.sleep(0.05) + # testbed.uevent('/dev/rfkill', 'add') + + def reset(self, test_id=''): + self.noinput = False + + self.devices = {i: [*d] for i, d in enumerate(DEVICES)} + if 'no_wwan' in test_id: + self.devices = { + i: d for i, d in self.devices.items() if d[1] != RFKILL_TYPE_WWAN} + if 'no_bluetooth' in test_id: + self.devices = {i: d for i, d in self.devices.items( + ) if d[1] != RFKILL_TYPE_BLUETOOTH} + self.next_index = len(self.devices.keys()) + log.info( + f'Setup devices: [{", ".join([str((i,d[0])) for i,d in self.devices.items()])}]') + + self.pending_events = [] + for idx in self.devices.keys(): + self.rfkill_send_events(idx, RFKILL_OP_ADD) + + def rfkill_blocked(self, idx, blocked=None, hard=False, typeid=RFKILL_TYPE_ALL): + if typeid != RFKILL_TYPE_ALL and self.rfkill_type(idx) != typeid: + return None + field_idx = 3 if hard else 2 + old_blocked = self.devices[idx][field_idx] + if blocked is None: + return old_blocked + new_blocked = 1 if blocked else 0 + self.devices[idx][field_idx] = new_blocked + if old_blocked != new_blocked: + self.rfkill_send_events(idx, RFKILL_OP_CHANGE) + return new_blocked + + def rfkill_name(self, idx): + return self.devices[idx][0] + + def rfkill_idx_by_name(self, name): + return [i for i, d in self.devices.items() if d[0] == name][0] + + def rfkill_type(self, idx): + return self.devices[idx][1] + + def rfkill_add(self, name, typeid, soft, hard): + self.devices[self.next_index] = [name, typeid, soft, hard] + self.rfkill_send_events(self.next_index, RFKILL_OP_ADD) + self.next_index += 1 + + def rfkill_del_by_name(self, name): + idx = self.rfkill_idx_by_name(name) + self.rfkill_send_events(idx, RFKILL_OP_DEL) + del self.devices[idx] + + def rfkill_send_events(self, idx, op): + _, type_id, soft, hard = self.devices[idx] + ev = struct.pack(rfkill_event, idx, type_id, op, soft, hard) + self.pending_events.append(ev) + + def rfkill_fop_ioctl(self, client): + req = client.get_request() + arg_data = client.get_arg().retrieve() + log.info( + f'[RfkillHandlerMock] IOCTL req: {req:b} NOINPUT: {RFKILL_IOCTL_NOINPUT:b} arg_data: {arg_data}') + if req == RFKILL_IOCTL_NOINPUT: + no_inp_long, = struct.unpack('l', arg_data) + if no_inp_long != 0: + self.noinput = False + client.complete(-ENOMSG, ENOMSG) + log.info(f'[RfkillHandlerMock] NO_INPUT ({no_inp_long} != 0)') + else: + self.noinput = True + client.complete(0, 0) + log.info(f'[RfkillHandlerMock] NO_INPUT - ok: {no_inp_long}') + return True + return False + + def rfkill_fop_read(self, client): + req = client.get_request() + log.info( + f'[RfkillHandlerMock] read: req {req:b}') + if not self.pending_events: + log.info(f'[RfkillHandlerMock] -> EAGAIN') + client.complete(-EAGAIN, EAGAIN) + return True + ev = self.pending_events.pop(0) + log.info(f'[RfkillHandlerMock] -> ev {ev}') + + arg = client.get_arg() + arg.update(0, ev) + # Return count of successfully read bytes + client.complete(len(ev), 0) + return True + + def rfkill_fop_write(self, client): + req = client.get_request() + arg_data = client.get_arg().retrieve() + log.info( + f'[RfkillHandlerMock] write: req {req:b} arg {arg_data}') + if len(arg_data) < size_of_rfkill_event: + log.warning( + f'[RfkillHandlerMock] too few bytes received {len(arg_data)} (expected {size_of_rfkill_event})') + return False + + if len(arg_data) > size_of_rfkill_event: + # This appears to be the real /dev/rfkill behavior (eventhough the count of read bytes is returned) + log.warning( + f'[RfkillHandlerMock] too many bytes received {len(arg_data)} (expected {size_of_rfkill_event})') + return False + + idx, typeid, op, soft, hard = struct.unpack( + rfkill_event, arg_data[:size_of_rfkill_event]) + + log.info( + f'[RfkillHandlerMock] user has written rfkill_event: [idx = {idx}, typeid = {typeid}, op = {op}, soft = {soft}, hard = {hard}]') + if op == RFKILL_OP_CHANGE_ALL: + for i in self.devices.keys(): + self.rfkill_blocked(i, soft, typeid=typeid) + elif op == RFKILL_OP_CHANGE: + self.rfkill_blocked(idx, soft, typeid=typeid) + log.info(f'[RfkillHandlerMock] -> Devices after write: {self.devices}') + # return count of successfully written bytes + client.complete(len(arg_data), 0) + return True + + +class PropAccessor: + def __init__(self, dbus_obj, interface=''): + self.prop_dbus_obj = dbus.Interface(dbus_obj, dbus.PROPERTIES_IFACE) + self.interface = interface + self.wait_sec_on_set = 0.05 + + def __getattr__(self, name): + return self.prop_dbus_obj.Get(self.interface, name) + + def __setattr__(self, name, value): + if name[0].upper() == name[0]: + if type(value) != bool: + raise ValueError('setattr expected bool') + self.prop_dbus_obj.Set( + self.interface, name, dbus.Boolean(value, variant_level=1)) + # Wait for Set to propagate + if self.wait_sec_on_set: + time.sleep(self.wait_sec_on_set) + else: + super().__setattr__(name, value) + + +class RfkillPluginTest(gsdtestcase.GSDTestCase): + '''Test the rfkill plugin''' + + gsd_plugin = 'rfkill' + gsd_plugin_case = 'Rfkill' + + main_bus = 'org.gnome.SettingsDaemon.' + gsd_plugin_case + main_obj_path = '/org/gnome/SettingsDaemon/' + gsd_plugin_case + + def setUp(self): + test_id = self.id() + setup_logger('.'.join(test_id.split('.')[1:])) + + # Setup umockdev testbed + self.testbed = UMockdev.Testbed.new() + self.addCleanup(self.cleanup_testbed) + os.environ['UMOCKDEV_DIR'] = self.testbed.get_root_dir() + + self.udev_mock = RfkillHandlerMock(self.testbed, test_id) + + self.mock_chassis('desktop' if '_on_desktop' in test_id else 'laptop') + + self.mock_modemmanager('no_modem' not in test_id) + + if 'no_nm' not in test_id: + # start mock of network manager + self.mock_networkmanager(True) + self.assertTrue(self.nm.WwanEnabled, '[init] wwan state') + + self.start_session() + self.addCleanup(self.stop_session) + + self.obj_session_mgr = self.session_bus_con.get_object( + 'org.gnome.SessionManager', '/org/gnome/SessionManager') + + self.start_mutter() + self.addCleanup(self.stop_mutter) + + env = os.environ.copy() + self.start_plugin(env) + self.addCleanup(self.stop_plugin) + + self.rfkill_obj = self.session_bus_con.get_object(self.main_bus, self.main_obj_path) + self.rfk = PropAccessor(self.rfkill_obj, self.main_bus) + + # always start with zero idle time + self.reset_idle_timer() + + self.p_notify_log.clear() + log.info('---- START ----') + + def tearDown(self): + log.info('---- STOP ----') + self.assertTrue( not self.udev_mock.error, 'RfkillHandlerMock error should not occur') + super().tearDown() + + def mock_chassis(self, chassis): + self.chassis_p = self.spawn_server( + 'org.freedesktop.hostname1', + '/org/freedesktop/hostname1', + 'org.freedesktop.hostname1', + system_bus=True + ) + self.addCleanup(self.stop_process, self.chassis_p) + + self.dbus_chassis_mock = dbus.Interface(self.system_bus_con.get_object( + 'org.freedesktop.hostname1', + '/org/freedesktop/hostname1' + ), dbusmock.MOCK_IFACE) + self.dbus_chassis_mock.AddProperty( + '', 'Chassis', dbus.String(chassis, variant_level=2)) + + def mock_modemmanager(self, wwanInteresting): + (self.mm_p, self.obj_mm) = self.spawn_server_template( + str(test_dir / 'test-mm-template.py'), {}) + self.addCleanup(self.stop_process, self.mm_p) + + self.dbus_mm_mock = dbus.Interface(self.obj_mm, dbusmock.MOCK_IFACE) + if wwanInteresting: + self.dbus_mm_mock.AddDummyDevice('ABCModem') + + def mock_networkmanager(self, wwanEnabled): + (self.nm_p, _) = self.spawn_server_template( + 'networkmanager', {'NetworkingEnabled': True, 'WwanEnabled': wwanEnabled}) + self.addCleanup(self.stop_process, self.nm_p) + + self.obj_nm = self.system_bus_con.get_object( + 'org.freedesktop.NetworkManager', '/org/freedesktop/NetworkManager') + self.dbus_nm_mock = dbus.Interface(self.obj_nm, dbusmock.MOCK_IFACE) + self.nm = PropAccessor(self.obj_nm) + + def cleanup_testbed(self): + del self.testbed + + def set_blocked(self, name, blocked, hard=False): + idx = self.udev_mock.rfkill_idx_by_name(name) + self.udev_mock.rfkill_blocked(idx, blocked, hard=hard) + + def soft_blocked(self, name, hard=False): + idx = self.udev_mock.rfkill_idx_by_name(name) + return self.udev_mock.rfkill_blocked(idx, None, hard=hard) + + def del_device(self, name): + self.udev_mock.rfkill_del_by_name(name) + + def add_device(self, *args): + self.udev_mock.rfkill_add(*args) + + def expect_event(self, action, typeid, idx): + type_name = { + RFKILL_TYPE_WWAN: 'wwan', + RFKILL_TYPE_BLUETOOTH: 'bluetooth', + RFKILL_TYPE_NFC: 'nfc', + }[typeid] + check_str = f'{action} {type_name} rfkill with ID {idx}' + log.info(f'Expecting event: "{check_str}"...') + self.plugin_log.check_line(check_str.encode(), 1) + + +class RfkillPluginTest1(RfkillPluginTest): + """ + Test correct syncing via rfkill udev connection in CcRfkillGlib + """ + + def test_rfkill_setup(self): + log.info('[setup] rfkill udev should be detected...') + self.plugin_log.check_line(b'Rfkill device has been created', 1) + self.plugin_log.check_line(b'Opened rfkill device after uevent', 1) + self.plugin_log.check_line(b'Opened rfkill-input inhibitor', 1) + self.assertTrue(self.udev_mock.noinput, + '[setup] CcRfkillGlib should set to inhibit input...') + + def test_rfkill_listen(self): + time.sleep(0.1) + for idx, dev in self.udev_mock.devices.items(): + self.expect_event('ADD', dev[1], idx) + + for idx in [1, 2]: + self.set_blocked(f'Bluetooth {idx}', True) + self.expect_event('CHANGE', RFKILL_TYPE_BLUETOOTH, idx) + + self.set_blocked(f'Bluetooth {idx}', True, hard=True) + self.expect_event('CHANGE', RFKILL_TYPE_BLUETOOTH, idx) + + self.del_device(f'Bluetooth {idx}') + self.expect_event('Removed', RFKILL_TYPE_BLUETOOTH, idx) + + expected_idx = self.udev_mock.next_index + self.add_device('Bluetooth x', RFKILL_TYPE_BLUETOOTH, 0, 0) + self.expect_event('ADD', RFKILL_TYPE_BLUETOOTH, expected_idx) + self.del_device('Bluetooth x') + self.expect_event('Removed', RFKILL_TYPE_BLUETOOTH, expected_idx) + + def test_rfkill_hw_block_no_modem(self): + self.assertTrue(not self.rfk.BluetoothBlocked, + '[initial] bluetooth should not be blocked') + self.assertTrue(not self.rfk.BluetoothHardwareBlocked, + '[initial] bluetooth should not be hardware blocked') + self.assertTrue(not self.rfk.WwanBlocked, + '[initial] wwan should should not be blocked') + self.assertTrue(not self.rfk.WwanHardwareBlocked, + '[initial] wwan should should not be hardware blocked') + + for dev in self.udev_mock.devices.values(): + if dev[1] == RFKILL_TYPE_BLUETOOTH: + if dev[3] != 1: + self.assertTrue(not self.rfk.BluetoothHardwareBlocked, + '[before] bluetooth should not be hardware blocked') + self.set_blocked(dev[0], True, hard=True) + time.sleep(0.05) + self.assertTrue(self.rfk.BluetoothHardwareBlocked, + '[after] bluetooth should be hardware blocked') + self.assertTrue(self.rfk.BluetoothBlocked, + '[finally] bluetooth should be blocked') + + self.assertTrue(not self.rfk.WwanHardwareBlocked, + '[before] wwan should not hardware blocked') + self.set_blocked('WWAN 1', True, hard=True) + self.set_blocked('WWAN 1', True) + self.add_device('WWAN 2', RFKILL_TYPE_WWAN, 0, 1) + time.sleep(0.05) + self.assertTrue(self.rfk.WwanHardwareBlocked, + '[after] wwan should be hardware blocked') + self.assertTrue(self.rfk.WwanBlocked, + '[after] wwan should should be blocked') + + # Add not hardware blocked WWAN device + self.add_device('WWAN 3', RFKILL_TYPE_WWAN, 1, 0) + time.sleep(0.05) + self.assertTrue(not self.rfk.WwanHardwareBlocked, + '[finally] wwan should not be hardware blocked') + self.assertTrue(self.rfk.WwanBlocked, + '[finally] wwan should should be blocked') + + def test_rfkill_write_congestion(self): + for i in range(20): + self.add_device(f'Bluetooth extra {i}', RFKILL_TYPE_BLUETOOTH, 0, 0) + time.sleep(0.05) + self.assertTrue(not self.rfk.AirplaneMode, '[initally] APM should be disabled') + self.assertTrue(not self.rfk.BluetoothBlocked, '[initally] bluetooth should not be blocked') + self.assertTrue(not self.rfk.WwanBlocked, '[initally] wwan should not be blocked') + self.rfk.AirplaneMode = True + self.assertTrue(self.rfk.AirplaneMode, '[before] APM should be enabled') + self.assertTrue(self.rfk.BluetoothBlocked, '[before] bluetooth should be blocked') + self.assertTrue(self.rfk.WwanBlocked, '[before] wwan should be blocked') + + # Test quickly toggling does not break APM + self.rfk.wait_sec_on_set = 0 + for _ in range(10): + self.rfk.AirplaneMode = False + self.rfk.AirplaneMode = True + self.rfk.AirplaneMode = False + time.sleep(0.05) + + self.assertTrue(not self.rfk.AirplaneMode, '[after] APM should be disabled') + self.assertTrue(not self.rfk.BluetoothBlocked, '[after] bluetooth should not be blocked') + self.assertTrue(not self.rfk.WwanBlocked, '[after] wwan should not be blocked') + + +class RfkillPluginTest2(RfkillPluginTest): + """ + Test correct AirplaneMode toggling behavior of GsdRfkillManager + """ + + def test_rfkill_not_shown_on_desktop(self): + self.assertTrue(not self.rfk.ShouldShowAirplaneMode, + '[desktop] should not show airplane mode') + + def test_availble_no_wwan(self): + self.assertTrue(self.rfk.BluetoothAvailable, + '[no_wwan] bluetooth should be available') + self.assertTrue(self.rfk.WwanAvailable, + '[no_wwan] wwan should be available (since modem device available)') + + def test_availble_no_bluetooth_no_modem(self): + self.assertTrue(not self.rfk.BluetoothAvailable, + '[no_bluetooth_no_modem] bluetooth should not be available') + self.assertTrue(self.rfk.WwanAvailable, + '[no_bluetooth_no_modem] wwan should be available (with WWAN rfkill device)') + self.del_device('WWAN 1') + time.sleep(0.1) + self.assertTrue(not self.rfk.WwanAvailable, + '[no_bluetooth_no_modem] wwan should not be available (with wwan_interesting set to FALSE)') + + def test_airplane_mode_no_bluetooth_no_wwan_no_modem(self): + self.assertTrue(self.rfk.ShouldShowAirplaneMode, + '[laptop] should show airplane mode') + self.assertTrue(not self.rfk.BluetoothAvailable, + '[initally] bluetooth should not be available') + self.assertTrue(not self.rfk.BluetoothBlocked, + '[initally] bluetooth should not be blocked') + self.assertTrue(not self.rfk.WwanAvailable, + '[initally] wwan should be available') + self.assertTrue(not self.rfk.WwanBlocked, + '[initally] wwan should not be blocked') + self.assertTrue(not self.rfk.AirplaneMode, + '[initally] airplane mode should be disabled') + + # Enable airplane mode + self.assertTrue(not self.rfk.AirplaneMode, + '[before] airplane mode should be disabled') + self.rfk.AirplaneMode = True + self.assertTrue(self.rfk.AirplaneMode, + '[apm] airplane mode should be enabled') + + self.assertTrue(self.rfk.WwanBlocked, + '[apm] wwan should appear as blocked') + self.assertTrue(self.rfk.BluetoothBlocked, + '[apm] bluetooth should appear as blocked') + + # Disable airplane mode + self.assertTrue(self.rfk.AirplaneMode, + '[apm] airplane mode should be enabled') + self.rfk.AirplaneMode = False + self.assertTrue(not self.rfk.AirplaneMode, + '[after] airplane mode should be disabled') + + def test_airplane_mode(self): + self.assertTrue(self.rfk.ShouldShowAirplaneMode, + '[laptop] should show airplane mode') + self.assertTrue(self.rfk.BluetoothAvailable, + '[initally] bluetooth should be available') + self.assertTrue(not self.rfk.BluetoothBlocked, + '[initally] bluetooth should not be blocked') + self.assertTrue(self.rfk.WwanAvailable, + '[initally] wwan should be available') + self.assertTrue(not self.rfk.WwanBlocked, + '[initally] wwan should not be blocked') + self.assertTrue(not self.rfk.AirplaneMode, + '[initally] airplane mode should be disabled') + + # Block and unblock Bluetooth + self.rfk.BluetoothBlocked = True + self.assertTrue(self.rfk.BluetoothBlocked, + 'toggling bluetooth should work') + self.rfk.BluetoothBlocked = False + self.assertTrue(not self.rfk.BluetoothBlocked, + '[before] bluetooth should be enabled') + + # Block and unblock WWAN + self.rfk.WwanBlocked = True + self.assertTrue(self.rfk.WwanBlocked) + self.rfk.WwanBlocked = False + self.assertTrue(not self.rfk.WwanBlocked) + + # Block WWAN + self.rfk.WwanBlocked = True + self.assertTrue(self.rfk.WwanBlocked, + '[before] wwan should be disabled') + + # Enable airplane mode + self.assertTrue(not self.rfk.AirplaneMode, + '[before] airplane mode should be disabled') + self.rfk.AirplaneMode = True + self.assertTrue(self.rfk.AirplaneMode, + '[apm] airplane mode should be enabled') + + self.assertTrue(self.rfk.WwanBlocked, + '[apm] wwan should be disabled') + self.assertTrue(self.rfk.BluetoothBlocked, + '[apm] bluetooth should be disabled') + + # Unblock and block Bluetooth + self.rfk.BluetoothBlocked = False + self.assertTrue(not self.rfk.BluetoothBlocked, + '[apm] it should be possible to enable bluetooth during apm') + self.rfk.BluetoothBlocked = True + self.assertTrue(self.rfk.BluetoothBlocked, + '[apm] it should be possible to disable bluetooth during apm') + + # Disable airplane mode + self.assertTrue(self.rfk.AirplaneMode, + '[apm] airplane mode should be enabled') + self.rfk.AirplaneMode = False + self.assertTrue(not self.rfk.AirplaneMode, + '[after] airplane mode should be disabled') + + # Check that state has been restored + self.assertTrue(self.rfk.WwanBlocked, + '[after] wwan should be disabled') + self.assertTrue(not self.rfk.BluetoothBlocked, + '[after] bluetooth should be enabled (restored)') + + def test_airplane_mode_disabled_by_wwan_unblock(self): + + self.assertTrue(self.rfk.WwanAvailable, + '[before] wwan should be available') + self.assertTrue(not self.rfk.WwanBlocked, + '[before] wwan should not be blocked') + self.rfk.BluetoothBlocked = True + self.assertTrue(self.rfk.BluetoothBlocked, + '[before] bluetooth should be blocked') + + self.rfk.AirplaneMode = True + self.assertTrue(self.rfk.BluetoothBlocked, + '[apm] bluetooth should be blocked') + + # Unblock WWAN during APM -> this should disable APM + self.assertTrue(self.rfk.WwanBlocked, + '[apm] wwan should be blocked') + self.rfk.WwanBlocked = False + + # Check that airplane mode disables when WWAN is enabled + self.assertTrue(not self.rfk.AirplaneMode, + '[after] airplane mode should be disabled') + + # Check state restored after APM + self.assertTrue(self.rfk.BluetoothBlocked, + '[after] bluetooth should be blocked') + self.assertTrue(not self.rfk.WwanBlocked, + '[after] wwan should not be blocked') + + def test_airplane_mode_handle_device_change(self): + self.rfk.AirplaneMode = True + self.assertTrue(self.rfk.BluetoothBlocked, + '[apm] bluetooth should be blocked') + + # Add unblocked Bluetooth device x + self.add_device('BT x', RFKILL_TYPE_BLUETOOTH, 0, 0) + time.sleep(0.05) + self.assertTrue(self.soft_blocked('BT x'), + '[apm add-bt] BT x should be soft blocked') + self.assertTrue(self.rfk.BluetoothBlocked, + '[apm add-bt] all bluetooth devices should be blocked') + self.assertTrue(self.soft_blocked('Bluetooth 1')) + self.rfk.BluetoothBlocked = False + self.assertTrue(not self.soft_blocked('Bluetooth 1')) + self.assertTrue(not self.soft_blocked('BT x'), + '[apm bt] BT x should not be soft blocked') + self.assertTrue(not self.rfk.BluetoothBlocked, + '[apm bt] not all bluetooth devices should be blocked') + # Add already blocked Bluetooth device y + self.add_device('BT y', RFKILL_TYPE_BLUETOOTH, 1, 0) + # Add hardblocked Bluetooth device z + self.add_device('BT z', RFKILL_TYPE_BLUETOOTH, 0, 1) + self.set_blocked('BT x', True) + time.sleep(0.05) + self.assertTrue(self.soft_blocked('BT z'), '[apm add-bt] BT z should be soft blocked') + self.assertTrue(self.soft_blocked('BT x'), '[apm change-bt] BT x should be able to change to blocked') + + # Add blocked WWAN device + self.add_device('WWAN x', RFKILL_TYPE_WWAN, 1, 0) + self.add_device('WWAN y', RFKILL_TYPE_WWAN, 0, 1) + time.sleep(0.05) + self.assertTrue(self.rfk.AirplaneMode, '[apm add-wwan] should not react to adding blocked wwan') + + # Add unblocked WWAN device + self.add_device('WWAN z', RFKILL_TYPE_WWAN, 0, 0) + time.sleep(0.05) + self.assertTrue(self.soft_blocked('WWAN z'), '[apm add-wwan] should block wwan') + self.assertTrue(self.rfk.AirplaneMode, '[apm add-wwan] should not change APM') + + # Disable APM by unblocking a WWAN device + self.set_blocked('WWAN z', False) + time.sleep(0.05) + self.assertTrue(not self.soft_blocked('WWAN z'), '[apm change-wwan] should soft unblock wwan') + self.assertTrue(not self.rfk.AirplaneMode, '[apm change-wwan] should disable APM after unblocking wwan') + + self.assertTrue(not self.soft_blocked('Bluetooth 1'), '[after] unblock device after APM') + # Should restore BT x but not BY y + self.assertTrue(not self.soft_blocked('BT x'), '[after] BT x should be unblocked') + self.assertTrue(self.soft_blocked('BT y'), '[after] BT y should remain blocked') + self.assertTrue(not self.soft_blocked('BT z'), '[after] BT z should remain unblocked') + +# avoid writing to stderr +unittest.main(testRunner=unittest.TextTestRunner( + stream=sys.stdout, verbosity=2)) -- GitLab From b05eeb755d74a83d07493607f8b1b785527c6702 Mon Sep 17 00:00:00 2001 From: Jan Zickermann Date: Tue, 28 Feb 2023 10:40:00 +0100 Subject: [PATCH 3/3] rfkill-glib: Force bluetooth unblock While unblocking bluetooth, dependent devices may reappear. Systemd-rfkill may restore these new devices as blocked. We want the user to also be able to unblock these devices. Thus, this workaround undoes blocking CHANGE events for 500ms. Closes: https://gitlab.gnome.org/GNOME/gnome-settings-daemon/-/issues/332 --- plugins/rfkill/rfkill-glib.c | 174 +++++++++++++++++++++++++++++++++-- plugins/rfkill/test.py | 47 +++++++++- 2 files changed, 211 insertions(+), 10 deletions(-) diff --git a/plugins/rfkill/rfkill-glib.c b/plugins/rfkill/rfkill-glib.c index ed06b8f4c..0e4ed1b85 100644 --- a/plugins/rfkill/rfkill-glib.c +++ b/plugins/rfkill/rfkill-glib.c @@ -71,31 +71,117 @@ struct _CcRfkillGlib { gpointer task_user_data; }; +struct forget_step { + gint event_count; + gint delay_millis; +}; + struct rfkill_task_data { GArray *events; guint send_idx; + GHookFunc task_success_hook; + gpointer task_done_hook_data; + GQueue *forget_step_queue; + guint forget_timeout_id; + gint64 forget_timeout_start; + gboolean idling; }; + G_DEFINE_TYPE (CcRfkillGlib, cc_rfkill_glib, G_TYPE_OBJECT) +#define EVENT_FORGET_DELAY_MILLIS 500 + static void cc_rfkill_write_discrete_events_async (CcRfkillGlib *rfkill); +static void rfkill_task_data_forget_next (struct rfkill_task_data *t_data); static struct rfkill_task_data * -rfkill_task_data_new (guint event_count) +rfkill_task_data_new (guint event_count, GHookFunc task_success_hook, gpointer user_data) { struct rfkill_task_data *t_data; t_data = g_new0 (struct rfkill_task_data, 1); t_data->events = g_array_sized_new (FALSE, FALSE, sizeof(struct rfkill_event), event_count); + t_data->task_success_hook = task_success_hook; + t_data->task_done_hook_data = user_data; + t_data->forget_step_queue = g_queue_new (); + t_data->idling = TRUE; return t_data; } static void rfkill_task_data_free (struct rfkill_task_data *t_data) { + g_clear_handle_id (&t_data->forget_timeout_id, g_source_remove); + g_queue_free_full (t_data->forget_step_queue, (GDestroyNotify) g_free); g_array_unref (t_data->events); g_free (t_data); } +static gboolean +rfkill_task_data_forget_cb (struct rfkill_task_data *t_data) +{ + g_autofree struct forget_step *step = NULL; + + step = g_queue_pop_head (t_data->forget_step_queue); + + /* Forget events which have been send and are older than 500ms */ + g_debug("FORGET %d task events", step->event_count); + g_return_val_if_fail (step->event_count <= t_data->send_idx, G_SOURCE_REMOVE); + t_data->send_idx -= step->event_count; + g_array_remove_range (t_data->events, 0, step->event_count); + + t_data->forget_timeout_id = 0; + rfkill_task_data_forget_next (t_data); + return G_SOURCE_REMOVE; +} + +static void +rfkill_task_data_forget_next (struct rfkill_task_data *t_data) +{ + struct forget_step *step; + if (t_data->forget_timeout_id != 0) + return; + + if (g_queue_is_empty(t_data->forget_step_queue)) { + if (t_data->idling) + /* Task is done since all events have been send and forgotten */ + t_data->task_success_hook (t_data->task_done_hook_data); + } else { + /* Forget next chunk of written events */ + step = g_queue_peek_head (t_data->forget_step_queue); + t_data->forget_timeout_start = g_get_monotonic_time(); + t_data->forget_timeout_id = g_timeout_add (step->delay_millis, + (GSourceFunc) rfkill_task_data_forget_cb, + t_data); + } +} + +static void +forget_step_substract (struct forget_step *previous_step, struct forget_step *step) +{ + step->event_count -= previous_step->event_count; + step->delay_millis -= previous_step->delay_millis; +} + + +static void +rfkill_task_data_on_all_events_written (struct rfkill_task_data *t_data) +{ + struct forget_step *step = g_new (struct forget_step, 1); + step->event_count = t_data->send_idx; + step->delay_millis = EVENT_FORGET_DELAY_MILLIS; + + /* Schedule a forget step after all previous forget steps */ + g_queue_foreach(t_data->forget_step_queue, (GFunc) forget_step_substract, step); + if (t_data->forget_timeout_id > 0) + step->delay_millis += (g_get_monotonic_time() - t_data->forget_timeout_start) / 1000; + g_return_if_fail (step->event_count > 0); + g_return_if_fail (step->delay_millis >= 0); + + g_queue_push_tail (t_data->forget_step_queue, step); + rfkill_task_data_forget_next (t_data); +} + static struct rfkill_event * rfkill_task_data_next_event (struct rfkill_task_data *t_data) { @@ -108,6 +194,46 @@ rfkill_task_data_has_next_event (struct rfkill_task_data *t_data) return t_data->send_idx < t_data->events->len; } +static gboolean +rfkill_event_includes_change (struct rfkill_event *major_event, struct rfkill_event *change_event) +{ + return (major_event->type == RFKILL_TYPE_ALL || major_event->type == change_event->type) && + (major_event->op == RFKILL_OP_CHANGE_ALL || major_event->idx == change_event->idx); +} + +static gboolean +rfkill_task_data_undo_op_change_on_conflict ( + struct rfkill_task_data *t_data, + struct rfkill_event *change_event) +{ + gint i; + + g_return_val_if_fail (change_event->op == RFKILL_OP_CHANGE, FALSE); + + /* Undo the given change_event if we find a conflicting change operation */ + for (i = t_data->events->len-1; i >= 0; i--) + { + struct rfkill_event *event; + + event = &g_array_index (t_data->events, struct rfkill_event, i); + if (rfkill_event_includes_change (event, change_event)) { + if (i < t_data->send_idx && event->soft != change_event->soft) { + struct rfkill_event undo_event; + + undo_event = *change_event; + undo_event.soft = event->soft; + g_array_append_val (t_data->events, undo_event); + g_debug ("[undo_op_change] Undo change event"); + return TRUE; + } else { + g_debug ("[undo_op_change] Already undone change event"); + return FALSE; + } + } + } + return FALSE; +} + /* Note that this can return %FALSE without setting @error. */ gboolean cc_rfkill_glib_send_change_all_event_finish (CcRfkillGlib *rfkill, @@ -121,6 +247,13 @@ cc_rfkill_glib_send_change_all_event_finish (CcRfkillGlib *rfkill, return g_task_propagate_boolean (G_TASK (res), error); } +static void +cc_rfkill_task_success_cb (CcRfkillGlib *rfkill) +{ + g_debug("DONE send task"); + g_task_return_boolean (rfkill->task, TRUE); + g_clear_object (&rfkill->task); +} static void write_change_all_done_cb (GObject *source_object, @@ -134,23 +267,23 @@ write_change_all_done_cb (GObject *source_object, struct rfkill_task_data *t_data; t_data = g_task_get_task_data (task); + g_assert (!t_data->idling); + t_data->idling = TRUE; ret = g_output_stream_write_finish (G_OUTPUT_STREAM (source_object), res, &error); if (ret < 0) { g_debug("ERROR send task"); g_task_return_error (task, g_steal_pointer (&error)); + g_clear_object (&rfkill->task); } else { if (rfkill_task_data_has_next_event(t_data)) { g_debug("CONTINUE send task"); cc_rfkill_write_discrete_events_async (rfkill); - return; } else { - g_task_return_boolean (task, ret >= 0); g_debug("WRITTEN send task"); + rfkill_task_data_on_all_events_written (t_data); } } - /* Task is done */ - g_clear_object (&rfkill->task); } void @@ -177,7 +310,7 @@ cc_rfkill_glib_send_change_all_event (CcRfkillGlib *rfkill, rfkill->task_user_data); g_task_set_source_tag (rfkill->task, cc_rfkill_glib_send_change_all_event); - t_data = rfkill_task_data_new (event_count); + t_data = rfkill_task_data_new (event_count, (GHookFunc) cc_rfkill_task_success_cb, rfkill); g_task_set_task_data (rfkill->task, t_data, (GDestroyNotify) rfkill_task_data_free); } else { /* Append events to the current task */ @@ -197,9 +330,9 @@ cc_rfkill_glib_send_change_all_event (CcRfkillGlib *rfkill, }; g_array_index (t_data->events, struct rfkill_event, prev_event_count + i) = e; } - g_debug("Send task with %d events. send_index: %d", t_data->events->len, t_data->send_idx); + g_debug("Send task with %d events. send_index: %d, forget_queue_len: %d", t_data->events->len, t_data->send_idx, t_data->forget_step_queue->length); - if (t_data->send_idx == 0) { + if (t_data->idling) { /* Start async write loop task */ g_debug("START send task"); cc_rfkill_write_discrete_events_async (rfkill); @@ -214,6 +347,8 @@ cc_rfkill_write_discrete_events_async (CcRfkillGlib *rfkill) t_data = g_task_get_task_data (rfkill->task); g_return_if_fail (rfkill_task_data_has_next_event (t_data)); + g_return_if_fail (t_data->idling); + t_data->idling = FALSE; g_output_stream_write_async (rfkill->stream, rfkill_task_data_next_event (t_data), sizeof(struct rfkill_event), @@ -268,6 +403,27 @@ print_event (struct rfkill_event *event) event->soft, event->hard); } +static void +cc_rfkill_glib_ensure_bluetooth_unblocking ( + CcRfkillGlib *rfkill, + struct rfkill_event *event) +{ + /* When unblocking a Bluetooth device (with gsd-rfkill), systemd-rfkill may try to block + * its child Bluetooth devices, in an attempt to restore the previous blocking state. + * + * In order to ensure that a Bluetooth device is successfully unblocked with all its children, + * we undo any external blocking operation, until 500ms have passed since unblocking. + */ + if (rfkill->task != NULL && event->op == RFKILL_OP_CHANGE && + event->type == RFKILL_TYPE_BLUETOOTH && event->soft) { + struct rfkill_task_data *t_data; + + t_data = g_task_get_task_data (rfkill->task); + if (rfkill_task_data_undo_op_change_on_conflict (t_data, event) && t_data->idling) + cc_rfkill_write_discrete_events_async (rfkill); + } +} + static gboolean event_cb (GIOChannel *source, GIOCondition condition, @@ -298,6 +454,8 @@ event_cb (GIOChannel *source, print_event (&event); g_array_append_val (events, event); + + cc_rfkill_glib_ensure_bluetooth_unblocking (rfkill, &event); } g_signal_emit ( G_OBJECT (rfkill), signals[CHANGED], 0, events); diff --git a/plugins/rfkill/test.py b/plugins/rfkill/test.py index d10d4c6de..5ab7c5db2 100755 --- a/plugins/rfkill/test.py +++ b/plugins/rfkill/test.py @@ -483,12 +483,53 @@ class RfkillPluginTest1(RfkillPluginTest): self.rfk.AirplaneMode = False self.rfk.AirplaneMode = True self.rfk.AirplaneMode = False - time.sleep(0.05) + time.sleep(0.1) self.assertTrue(not self.rfk.AirplaneMode, '[after] APM should be disabled') self.assertTrue(not self.rfk.BluetoothBlocked, '[after] bluetooth should not be blocked') self.assertTrue(not self.rfk.WwanBlocked, '[after] wwan should not be blocked') + def test_rfkill_write_is_forceful(self): + # Systemd-rfkill may restore a Bluetooth device as blocked during an unblock task. + # Gsd-rfkill should be forceful during its unblocking task and undo these blocking operations. + self.rfk.BluetoothBlocked = True + for i in range(1,6): + self.del_device(f'Bluetooth {i}') + time.sleep(0.05) + self.assertTrue(not self.rfk.BluetoothBlocked, '[before] should have deleted all bluetooth devices') + self.rfk.BluetoothBlocked = False + time.sleep(0.05) + # Add blocked child + self.add_device('Bluetooth blocked child', RFKILL_TYPE_BLUETOOTH, 1, 0) + time.sleep(0.05) + self.assertTrue(self.rfk.BluetoothBlocked, + '[after-add] should not undo bluetooth block with RFKILL_OP_ADD') + self.del_device('Bluetooth blocked child') + time.sleep(0.05) + self.assertTrue(not self.rfk.BluetoothBlocked, '[mid] bluetooth should not be blocked') + + # Add unblocked child + self.add_device(f'Bluetooth child 0', RFKILL_TYPE_BLUETOOTH, 0, 0) + # Add blocked child which gets unblocked + self.add_device(f'Bluetooth child 1', RFKILL_TYPE_BLUETOOTH, 1, 0) + self.set_blocked(f'Bluetooth child 1', False) + # Add unblocked child which gets blocked + self.add_device(f'Bluetooth child 2', RFKILL_TYPE_BLUETOOTH, 0, 0) + self.set_blocked(f'Bluetooth child 2', True) + time.sleep(0.05) + self.assertTrue(not self.rfk.BluetoothBlocked, + '[after-add-change] should undo bluetooth RFKILL_OP_CHANGE') + for i in range(3): + self.assertTrue(not self.soft_blocked(f'Bluetooth child {i}'), + f'[after-add-change] child {i} not blocked') + self.set_blocked(f'Bluetooth child {i}', True) + time.sleep(0.05) + self.assertTrue(not self.rfk.BluetoothBlocked, + '[after-change] should undo bluetooth RFKILL_OP_CHANGE') + time.sleep(0.5) + self.assertTrue(not self.rfk.BluetoothBlocked, '[after 500ms] bluetooth should not be blocked') + + class RfkillPluginTest2(RfkillPluginTest): """ @@ -666,8 +707,10 @@ class RfkillPluginTest2(RfkillPluginTest): self.add_device('BT y', RFKILL_TYPE_BLUETOOTH, 1, 0) # Add hardblocked Bluetooth device z self.add_device('BT z', RFKILL_TYPE_BLUETOOTH, 0, 1) + # Sleep 550ms to avoid Bluetooth change event being undone + time.sleep(0.55) self.set_blocked('BT x', True) - time.sleep(0.05) + time.sleep(0.1) self.assertTrue(self.soft_blocked('BT z'), '[apm add-bt] BT z should be soft blocked') self.assertTrue(self.soft_blocked('BT x'), '[apm change-bt] BT x should be able to change to blocked') -- GitLab