diff --git a/plugins/rfkill/gsd-rfkill-manager.c b/plugins/rfkill/gsd-rfkill-manager.c index 50a3e2bf395fb215a22a4f4fc6dbdea15b1b1632..b38c31c86f452d6265effe34738b742c35a1e821 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/meson.build b/plugins/rfkill/meson.build index 42221449244f99c3a82357be4d12f972fd1201b7..83f654e80eddddedcf1dcff7563bc64367c6ea31 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 a5e49bc353e5809ba4a60499f1b12a858b78604a..0e4ed1b850205e7882755602755e378def162f5b 100644 --- a/plugins/rfkill/rfkill-glib.c +++ b/plugins/rfkill/rfkill-glib.c @@ -65,88 +65,194 @@ 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 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 CHANGE_ALL_TIMEOUT 500 +#define EVENT_FORGET_DELAY_MILLIS 500 -static const char *type_to_string (unsigned int type); +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, 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 -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_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); +} - if (rfkill->change_all_timeout_id != 0) { - g_source_remove (rfkill->change_all_timeout_id); - rfkill->change_all_timeout_id = 0; - } +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; } -/* Note that this can return %FALSE without setting @error. */ -gboolean -cc_rfkill_glib_send_change_all_event_finish (CcRfkillGlib *rfkill, - GAsyncResult *res, - GError **error) +static void +rfkill_task_data_forget_next (struct rfkill_task_data *t_data) { - g_return_val_if_fail (CC_RFKILL_IS_GLIB (rfkill), FALSE); - g_return_val_if_fail (g_task_is_valid (res, rfkill), FALSE); - g_return_val_if_fail (g_async_result_is_tagged (res, cc_rfkill_glib_send_change_all_event), FALSE); + struct forget_step *step; + if (t_data->forget_timeout_id != 0) + return; - return g_task_propagate_boolean (G_TASK (res), error); + 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 -write_change_all_again_done_cb (GObject *source_object, - GAsyncResult *res, - gpointer user_data) +forget_step_substract (struct forget_step *previous_step, struct forget_step *step) { - g_autoptr(GTask) task = G_TASK (user_data); - CcRfkillGlib *rfkill = g_task_get_source_object (task); - g_autoptr(GError) error = NULL; - gssize ret; + step->event_count -= previous_step->event_count; + step->delay_millis -= previous_step->delay_millis; +} - 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); +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); +} - /* If this @task has been cancelled, it may have been superceded. */ - if (rfkill->task == task) - g_clear_object (&rfkill->task); +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 -write_change_all_timeout_cb (CcRfkillGlib *rfkill) +rfkill_task_data_has_next_event (struct rfkill_task_data *t_data) { - struct rfkill_event *event; + return t_data->send_idx < t_data->events->len; +} - g_assert (rfkill->task != NULL); +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); +} - g_debug ("Sending second RFKILL_OP_CHANGE_ALL timed out"); +static gboolean +rfkill_task_data_undo_op_change_on_conflict ( + struct rfkill_task_data *t_data, + struct rfkill_event *change_event) +{ + gint i; - 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_return_val_if_fail (change_event->op == RFKILL_OP_CHANGE, FALSE); - g_clear_object (&rfkill->task); - rfkill->change_all_timeout_id = 0; + /* 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; - return G_SOURCE_REMOVE; + 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, + GAsyncResult *res, + GError **error) +{ + g_return_val_if_fail (CC_RFKILL_IS_GLIB (rfkill), FALSE); + g_return_val_if_fail (g_task_is_valid (res, rfkill), FALSE); + g_return_val_if_fail (g_async_result_is_tagged (res, cc_rfkill_glib_send_change_all_event), FALSE); + + 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 @@ -154,111 +260,117 @@ 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); + g_assert (!t_data->idling); + t_data->idling = TRUE; - 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; - } - - 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); + } else { + if (rfkill_task_data_has_next_event(t_data)) { + g_debug("CONTINUE send task"); + cc_rfkill_write_discrete_events_async (rfkill); + } else { + g_debug("WRITTEN send task"); + rfkill_task_data_on_all_events_written (t_data); + } + } } 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, (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 */ + 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, forget_queue_len: %d", t_data->events->len, t_data->send_idx, t_data->forget_step_queue->length); - /* Clear any previous task. */ - cancel_current_task (rfkill); - g_assert (rfkill->task == NULL); + if (t_data->idling) { + /* 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_return_if_fail (t_data->idling); + t_data->idling = FALSE; 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)); -} - -static const char * -type_to_string (unsigned int 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"; - } + 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)); +} + +gboolean cc_rfkill_is_ready (CcRfkillGlib *rfkill) { + return rfkill->stream != NULL; } -static const char * -op_to_string (unsigned int op) +const gchar +*cc_rfkill_type_to_string (enum rfkill_type r_type) { - switch (op) { + return (guint)r_type < NUM_RFKILL_TYPES ? rfkill_type_table[(guint)r_type] : "unknown"; +} + +const gchar +*cc_rfkill_op_to_string (enum rfkill_operation r_op) +{ + switch (r_op) { case RFKILL_OP_ADD: return "ADD"; case RFKILL_OP_DEL: @@ -268,110 +380,85 @@ 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) +cc_rfkill_glib_ensure_bluetooth_unblocking ( + CcRfkillGlib *rfkill, + struct rfkill_event *event) { - 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; + /* 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); } - - 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; + if (status != G_IO_STATUS_NORMAL || read < RFKILL_EVENT_SIZE_V1) + break; - print_event (&event); + print_event (&event); - event_ptr = g_memdup2 (&event, sizeof (event)); - events = g_list_prepend (events, event_ptr); + g_array_append_val (events, 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; + cc_rfkill_glib_ensure_bluetooth_unblocking (rfkill, &event); } - emit_changed_signal_and_free (rfkill, events); + g_signal_emit ( G_OBJECT (rfkill), signals[CHANGED], 0, events); return TRUE; } @@ -379,8 +466,26 @@ 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 +busypoll_cb (CcRfkillGlib *rfkill) +{ + return event_cb (rfkill->channel, G_IO_IN, rfkill); } static gboolean @@ -414,14 +519,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; } @@ -446,6 +559,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 +575,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 +716,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 0655eb484b0e6772892f4bc54ab4491a837df3b5..660618b9ad41da0ba23f85cae21c97acfd86f9a5 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 abb2c661316cadea6d324e8cb8a4a071494701e2..45669516b2e59dda128ffdac970ad0f6dd52409a 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 diff --git a/plugins/rfkill/test-mm-template.py b/plugins/rfkill/test-mm-template.py new file mode 100644 index 0000000000000000000000000000000000000000..78b2e9f55469cc2c9d7750fd87f00141f51eb1fc --- /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 0000000000000000000000000000000000000000..5ab7c5db23f0773e76338b4b1c850c5578b9b257 --- /dev/null +++ b/plugins/rfkill/test.py @@ -0,0 +1,743 @@ +#!/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.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): + """ + 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) + # Sleep 550ms to avoid Bluetooth change event being undone + time.sleep(0.55) + self.set_blocked('BT x', True) + 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') + + # 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))