diff --git a/COPYING b/COPYING index 779ecbd55aab1d00cd34fdab34c75dcc98e61014..86febebf10557d332182dc743b015f77bf484015 100644 --- a/COPYING +++ b/COPYING @@ -1,4 +1,4 @@ -SPDX-License-Identifier: MIT and LGPL-2.1-or-later and Apache-2.0 +SPDX-License-Identifier: MIT and LGPL-2.1-or-later and Apache-2.0 and Zlib The overall license of steam-runtime-tools and pressure-vessel is LGPL-2.1-or-later <https://spdx.org/licenses/LGPL-2.1-or-later.html>, @@ -20,27 +20,9 @@ which can be found in the file `COPYING.LGPL-2.1`: License along with this library. If not, see <http://www.gnu.org/licenses/>. Some of the individual source files are under the more permissive -MIT/X11 license, and can be extracted into proprietary projects -without needing to follow the terms of the LGPL: - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be included - in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +MIT/X11 or Zlib licenses, and can be extracted into proprietary projects +without needing to follow the terms of the LGPL. See `COPYING.MIT` and +`COPYING.Zlib` or the individual source files for details. See individual source files for copyright and licensing details, and in particular pressure-vessel/THIRD-PARTY.md for details of third-party diff --git a/COPYING.MIT b/COPYING.MIT new file mode 100644 index 0000000000000000000000000000000000000000..06bc0920b135e3ed839e08a513a169f81e7ce6be --- /dev/null +++ b/COPYING.MIT @@ -0,0 +1,20 @@ +SPDX-License-Identifier: MIT + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/COPYING.Zlib b/COPYING.Zlib new file mode 100644 index 0000000000000000000000000000000000000000..88aaec01ee2d4197a53d624a4f43e28b0f02e74c --- /dev/null +++ b/COPYING.Zlib @@ -0,0 +1,17 @@ +SPDX-License-Identifier: Zlib + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. diff --git a/bin/input-monitor.c b/bin/input-monitor.c new file mode 100644 index 0000000000000000000000000000000000000000..6d5bc0f0136d83efd1a42f935adf3be744c2efae --- /dev/null +++ b/bin/input-monitor.c @@ -0,0 +1,962 @@ +/* + * Input device monitor, adapted from SDL code. + * + * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org> + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: Zlib + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#include <libglnx.h> + +#include <steam-runtime-tools/steam-runtime-tools.h> + +#include "steam-runtime-tools/glib-backports-internal.h" +#include "steam-runtime-tools/input-device-internal.h" +#include "steam-runtime-tools/json-glib-backports-internal.h" +#include "steam-runtime-tools/utils-internal.h" + +#include <stdio.h> + +#include <glib-unix.h> + +#include <json-glib/json-glib.h> + +#define RECORD_SEPARATOR "\x1e" + +static FILE *original_stdout = NULL; +static gboolean opt_one_line = FALSE; +static gboolean opt_seq = FALSE; +static gboolean opt_verbose = FALSE; + +static void +jsonify_flags (JsonBuilder *builder, + GType flags_type, + unsigned int values) +{ + GFlagsClass *class; + GFlagsValue *flags_value; + + g_return_if_fail (G_TYPE_IS_FLAGS (flags_type)); + + class = g_type_class_ref (flags_type); + + while (values != 0) + { + flags_value = g_flags_get_first_value (class, values); + + if (flags_value == NULL) + break; + + json_builder_add_string_value (builder, flags_value->value_nick); + values &= ~flags_value->value; + } + + if (values) + { + gchar *rest = g_strdup_printf ("0x%x", values); + + json_builder_add_string_value (builder, rest); + + g_free (rest); + } + + g_type_class_unref (class); +} + +static void +print_json (JsonBuilder *builder) +{ + g_autoptr(JsonGenerator) generator = json_generator_new (); + g_autofree gchar *text = NULL; + JsonNode *root; + + root = json_builder_get_root (builder); + generator = json_generator_new (); + json_generator_set_root (generator, root); + + if (opt_seq) + fputs (RECORD_SEPARATOR, original_stdout); + + if (!opt_one_line) + json_generator_set_pretty (generator, TRUE); + + text = json_generator_to_data (generator, NULL); + + if (fputs (text, original_stdout) < 0) + g_warning ("Unable to write output: %s", g_strerror (errno)); + + if (fputs ("\n", original_stdout) < 0) + g_warning ("Unable to write final newline: %s", g_strerror (errno)); +} + +static void +add_uevent_member (JsonBuilder *builder, + const char *name, + const char *value) +{ + const char *start = value; + const char *end; + + /* value is conceptually a single string, but we + * present it as an array of lines here, because that's + * a lot easier to read! */ + json_builder_set_member_name (builder, name); + json_builder_begin_array (builder); + + while (*start != '\0') + { + g_autofree gchar *valid = NULL; + + while (*start == '\n') + start++; + + if (*start == '\0') + break; + + end = strchrnul (start, '\n'); + + valid = g_utf8_make_valid (start, end - start); + json_builder_add_string_value (builder, valid); + + if (*end == '\0') + break; + + start = end + 1; + } + + json_builder_end_array (builder); +} + +static void +append_evdev_hex (GString *buf, + const unsigned long *bits, + size_t n_longs) +{ + size_t i, j; + + for (i = 0; i < n_longs; i++) + { + unsigned long word = bits[i]; + + for (j = 0; j < sizeof (long); j++) + { + unsigned char byte = (word >> (CHAR_BIT * j)) & 0xff; + g_string_append_printf (buf, "%02x ", byte); + } + g_string_append_c (buf, ' '); + } +} + +static void +added (SrtInputDeviceMonitor *monitor, + SrtInputDevice *dev, + void *user_data) +{ + g_autoptr(JsonBuilder) builder = json_builder_new (); + + json_builder_begin_object (builder); + { + json_builder_set_member_name (builder, "added"); + json_builder_begin_object (builder); + { + g_auto(GStrv) udev_properties = NULL; + const char *sys_path; + struct + { + const char *name; + const char *phys; + const char *uniq; + const char *manufacturer; + unsigned int bus_type, vendor_id, product_id, version; + } id, blank_id = {}; + unsigned long bits[LONGS_FOR_BITS (HIGHEST_EVENT_CODE)]; + SrtInputDeviceTypeFlags type_flags; + + json_builder_set_member_name (builder, "interface_flags"); + json_builder_begin_array (builder); + jsonify_flags (builder, SRT_TYPE_INPUT_DEVICE_INTERFACE_FLAGS, + srt_input_device_get_interface_flags (dev)); + json_builder_end_array (builder); + + type_flags = srt_input_device_get_type_flags (dev); + json_builder_set_member_name (builder, "type_flags"); + json_builder_begin_array (builder); + jsonify_flags (builder, SRT_TYPE_INPUT_DEVICE_TYPE_FLAGS, + type_flags); + json_builder_end_array (builder); + + json_builder_set_member_name (builder, "dev_node"); + json_builder_add_string_value (builder, + srt_input_device_get_dev_node (dev)); + json_builder_set_member_name (builder, "subsystem"); + json_builder_add_string_value (builder, + srt_input_device_get_subsystem (dev)); + json_builder_set_member_name (builder, "sys_path"); + json_builder_add_string_value (builder, + srt_input_device_get_sys_path (dev)); + + id = blank_id; + + if (srt_input_device_get_identity (dev, + &id.bus_type, + &id.vendor_id, + &id.product_id, + &id.version)) + { + { + g_autofree gchar *s = g_strdup_printf ("0x%04x", id.bus_type); + json_builder_set_member_name (builder, "bus_type"); + json_builder_add_string_value (builder, s); + } + { + g_autofree gchar *s = g_strdup_printf ("0x%04x", id.vendor_id); + json_builder_set_member_name (builder, "vendor_id"); + json_builder_add_string_value (builder, s); + } + { + g_autofree gchar *s = g_strdup_printf ("0x%04x", id.product_id); + json_builder_set_member_name (builder, "product_id"); + json_builder_add_string_value (builder, s); + } + { + g_autofree gchar *s = g_strdup_printf ("0x%04x", id.version); + json_builder_set_member_name (builder, "version"); + json_builder_add_string_value (builder, s); + } + } + + if (srt_input_device_get_interface_flags (dev) + & SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT) + { + SrtInputDeviceTypeFlags guessed_flags; + + json_builder_set_member_name (builder, "evdev"); + json_builder_begin_object (builder); + + if (srt_input_device_get_event_types (dev, bits, G_N_ELEMENTS (bits)) > 0) + { + json_builder_set_member_name (builder, "types"); + json_builder_begin_array (builder); + { +#define BIT(x) \ + do \ + { \ + if (test_bit_checked (EV_ ## x, bits, G_N_ELEMENTS (bits))) \ + { \ + json_builder_add_string_value (builder, #x); \ + } \ + } \ + while (0) + + BIT (SYN); + BIT (KEY); + BIT (REL); + BIT (ABS); + BIT (MSC); + BIT (SW); + BIT (LED); + BIT (SND); + BIT (REP); + BIT (FF); + BIT (PWR); + BIT (FF_STATUS); + +#undef BIT + } + json_builder_end_array (builder); + + if (opt_verbose) + { + g_autoptr(GString) buf = g_string_new (""); + + append_evdev_hex (buf, bits, LONGS_FOR_BITS (EV_MAX)); + json_builder_set_member_name (builder, "raw_types"); + json_builder_add_string_value (builder, buf->str); + } + } + + if (srt_input_device_get_event_capabilities (dev, EV_ABS, + bits, G_N_ELEMENTS (bits)) > 0) + { + json_builder_set_member_name (builder, "absolute_axes"); + json_builder_begin_array (builder); + { +#define BIT(x) \ + do \ + { \ + if (test_bit_checked (ABS_ ## x, bits, G_N_ELEMENTS (bits))) \ + { \ + json_builder_add_string_value (builder, #x); \ + } \ + } \ + while (0) + + BIT (X); + BIT (Y); + BIT (Z); + BIT (RX); + BIT (RY); + BIT (RZ); + BIT (THROTTLE); + BIT (RUDDER); + BIT (WHEEL); + BIT (GAS); + BIT (BRAKE); + BIT (HAT0X); + BIT (HAT0Y); + BIT (HAT1X); + BIT (HAT1Y); + BIT (HAT2X); + BIT (HAT2Y); + BIT (HAT3X); + BIT (HAT3Y); + BIT (PRESSURE); + BIT (DISTANCE); + BIT (TILT_X); + BIT (TILT_Y); + BIT (TOOL_WIDTH); + BIT (VOLUME); + BIT (MISC); + BIT (RESERVED); + BIT (MT_SLOT); + +#undef BIT + } + json_builder_end_array (builder); + + if (opt_verbose) + { + g_autoptr(GString) buf = g_string_new (""); + + append_evdev_hex (buf, bits, LONGS_FOR_BITS (ABS_MAX)); + json_builder_set_member_name (builder, "raw_abs"); + json_builder_add_string_value (builder, buf->str); + } + } + + if (srt_input_device_get_event_capabilities (dev, EV_REL, + bits, G_N_ELEMENTS (bits)) > 0) + { + json_builder_set_member_name (builder, "relative_axes"); + json_builder_begin_array (builder); + { +#define BIT(x) \ + do \ + { \ + if (test_bit_checked (REL_ ## x, bits, G_N_ELEMENTS (bits))) \ + { \ + json_builder_add_string_value (builder, #x); \ + } \ + } \ + while (0) + + BIT (X); + BIT (Y); + BIT (Z); + BIT (RX); + BIT (RY); + BIT (RZ); + BIT (HWHEEL); + BIT (DIAL); + BIT (WHEEL); + BIT (MISC); + BIT (RESERVED); + BIT (WHEEL_HI_RES); + BIT (HWHEEL_HI_RES); + +#undef BIT + } + json_builder_end_array (builder); + + if (opt_verbose) + { + g_autoptr(GString) buf = g_string_new (""); + + append_evdev_hex (buf, bits, LONGS_FOR_BITS (REL_MAX)); + json_builder_set_member_name (builder, "raw_rel"); + json_builder_add_string_value (builder, buf->str); + } + } + + if (srt_input_device_get_event_capabilities (dev, EV_KEY, + bits, G_N_ELEMENTS (bits)) > 0) + { + json_builder_set_member_name (builder, "keys"); + json_builder_begin_array (builder); + { +#define BIT(x) \ + do \ + { \ + if (test_bit_checked (x, bits, G_N_ELEMENTS (bits))) \ + { \ + json_builder_add_string_value (builder, #x); \ + } \ + } \ + while (0) + + /* We don't show all the keyboard keys here because + * that would be ridiculous, but we do show a + * selection that should be enough to tell the + * difference between keyboards, mice, joysticks + * and so on. We do show most joystick buttons. */ + + /* Gamepads */ + BIT (BTN_A); /* aka BTN_GAMEPAD, BTN_SOUTH */ + BIT (BTN_B); + BIT (BTN_C); + BIT (BTN_X); + BIT (BTN_Y); + BIT (BTN_Z); + BIT (BTN_TL); + BIT (BTN_TR); + BIT (BTN_TL2); + BIT (BTN_TR2); + BIT (BTN_SELECT); + BIT (BTN_START); + BIT (BTN_MODE); + BIT (BTN_THUMBL); + BIT (BTN_THUMBR); + /* Not all gamepads have a digital d-pad, some only + * represent it as the hat0x and hat0y absolute axes; + * but some do have it */ + BIT (BTN_DPAD_UP); + BIT (BTN_DPAD_DOWN); + BIT (BTN_DPAD_LEFT); + BIT (BTN_DPAD_RIGHT); + + /* Flight sticks and similar joysticks */ + BIT (BTN_TRIGGER); + BIT (BTN_THUMB); + BIT (BTN_THUMB2); + BIT (BTN_TOP); + BIT (BTN_TOP2); + BIT (BTN_PINKIE); + BIT (BTN_BASE); + BIT (BTN_BASE2); + BIT (BTN_BASE3); + BIT (BTN_BASE4); + BIT (BTN_BASE5); + BIT (BTN_BASE6); + BIT (BTN_DEAD); + BIT (BTN_TRIGGER_HAPPY); + + /* Steering wheels */ + BIT (BTN_GEAR_DOWN); + BIT (BTN_GEAR_UP); + + /* Keyboards */ + BIT (KEY_ESC); + BIT (KEY_0); + BIT (KEY_A); + BIT (KEY_KP0); + BIT (KEY_PLAY); + + /* Mice and friends. BTN_LEFT is an alias for + * BTN_MOUSE, but we use BTN_MOUSE here as a + * hint that the rest are also mouse buttons. */ + BIT (BTN_MOUSE); + BIT (BTN_RIGHT); + BIT (BTN_MIDDLE); + BIT (BTN_SIDE); + BIT (BTN_EXTRA); + BIT (BTN_FORWARD); + BIT (BTN_BACK); + BIT (BTN_TASK); + BIT (BTN_DIGI); + BIT (KEY_MACRO1); + + /* Generic buttons that nobody knows what they do... */ + BIT (BTN_0); + +#undef BIT + } + json_builder_end_array (builder); + + if (opt_verbose) + { + g_autoptr(GString) buf = g_string_new (""); + + append_evdev_hex (buf, bits, LONGS_FOR_BITS (KEY_MAX)); + json_builder_set_member_name (builder, "raw_keys"); + json_builder_add_string_value (builder, buf->str); + } + } + + if (srt_input_device_get_input_properties (dev, bits, + G_N_ELEMENTS (bits)) > 0) + { + json_builder_set_member_name (builder, "input_properties"); + json_builder_begin_array (builder); + { +#define BIT(x) \ + do \ + { \ + if (test_bit_checked (INPUT_PROP_ ## x, bits, G_N_ELEMENTS (bits))) \ + { \ + json_builder_add_string_value (builder, #x); \ + } \ + } \ + while (0) + + BIT (POINTER); + BIT (DIRECT); + BIT (BUTTONPAD); + BIT (SEMI_MT); + BIT (TOPBUTTONPAD); + BIT (POINTING_STICK); + BIT (ACCELEROMETER); + +#undef BIT + } + json_builder_end_array (builder); + + if (opt_verbose) + { + g_autoptr(GString) buf = g_string_new (""); + + append_evdev_hex (buf, bits, LONGS_FOR_BITS (REL_MAX)); + json_builder_set_member_name (builder, "raw_input_properties"); + json_builder_add_string_value (builder, buf->str); + } + } + + guessed_flags = srt_input_device_guess_type_flags_from_event_capabilities (dev); + + if (opt_verbose || guessed_flags != type_flags) + { + json_builder_set_member_name (builder, "guessed_type_flags"); + json_builder_begin_array (builder); + jsonify_flags (builder, SRT_TYPE_INPUT_DEVICE_TYPE_FLAGS, + guessed_flags); + json_builder_end_array (builder); + } + + json_builder_end_object (builder); + } + + udev_properties = srt_input_device_dup_udev_properties (dev); + + if (udev_properties != NULL) + { + gsize i; + + json_builder_set_member_name (builder, "udev_properties"); + json_builder_begin_array (builder); + + for (i = 0; udev_properties[i] != NULL; i++) + { + g_autofree gchar *valid = NULL; + + valid = g_utf8_make_valid (udev_properties[i], -1); + json_builder_add_string_value (builder, valid); + } + + json_builder_end_array (builder); + } + + if (opt_verbose) + { + g_autofree gchar *uevent = srt_input_device_dup_uevent (dev); + + if (uevent != NULL) + add_uevent_member (builder, "uevent", uevent); + } + + sys_path = srt_input_device_get_hid_sys_path (dev); + + id = blank_id; + + if (srt_input_device_get_hid_identity (dev, + &id.bus_type, + &id.vendor_id, + &id.product_id, + &id.name, + &id.phys, + &id.uniq) + || sys_path != NULL) + { + json_builder_set_member_name (builder, "hid_ancestor"); + json_builder_begin_object (builder); + + json_builder_set_member_name (builder, "sys_path"); + json_builder_add_string_value (builder, sys_path); + + json_builder_set_member_name (builder, "name"); + json_builder_add_string_value (builder, id.name); + + { + g_autofree gchar *s = g_strdup_printf ("0x%04x", id.bus_type); + json_builder_set_member_name (builder, "bus_type"); + json_builder_add_string_value (builder, s); + } + { + g_autofree gchar *s = g_strdup_printf ("0x%04x", id.vendor_id); + json_builder_set_member_name (builder, "vendor_id"); + json_builder_add_string_value (builder, s); + } + { + g_autofree gchar *s = g_strdup_printf ("0x%04x", id.product_id); + json_builder_set_member_name (builder, "product_id"); + json_builder_add_string_value (builder, s); + } + + json_builder_set_member_name (builder, "uniq"); + json_builder_add_string_value (builder, id.uniq); + + if (opt_verbose) + { + g_autofree gchar *uevent = srt_input_device_dup_hid_uevent (dev); + + json_builder_set_member_name (builder, "phys"); + json_builder_add_string_value (builder, id.phys); + + if (uevent != NULL) + add_uevent_member (builder, "uevent", uevent); + } + + json_builder_end_object (builder); + } + + sys_path = srt_input_device_get_input_sys_path (dev); + + id = blank_id; + + if (srt_input_device_get_input_identity (dev, + &id.bus_type, + &id.vendor_id, + &id.product_id, + &id.version, + &id.name, + &id.phys, + &id.uniq) + || sys_path != NULL) + { + json_builder_set_member_name (builder, "input_ancestor"); + json_builder_begin_object (builder); + + json_builder_set_member_name (builder, "sys_path"); + json_builder_add_string_value (builder, sys_path); + + json_builder_set_member_name (builder, "name"); + json_builder_add_string_value (builder, id.name); + + { + g_autofree gchar *s = g_strdup_printf ("0x%04x", id.bus_type); + json_builder_set_member_name (builder, "bus_type"); + json_builder_add_string_value (builder, s); + } + { + g_autofree gchar *s = g_strdup_printf ("0x%04x", id.vendor_id); + json_builder_set_member_name (builder, "vendor_id"); + json_builder_add_string_value (builder, s); + } + { + g_autofree gchar *s = g_strdup_printf ("0x%04x", id.product_id); + json_builder_set_member_name (builder, "product_id"); + json_builder_add_string_value (builder, s); + } + { + g_autofree gchar *s = g_strdup_printf ("0x%04x", id.version); + json_builder_set_member_name (builder, "version"); + json_builder_add_string_value (builder, s); + } + + if (opt_verbose) + { + g_autofree gchar *uevent = srt_input_device_dup_input_uevent (dev); + + json_builder_set_member_name (builder, "phys"); + json_builder_add_string_value (builder, id.phys); + + if (uevent != NULL) + add_uevent_member (builder, "uevent", uevent); + } + + json_builder_end_object (builder); + } + + sys_path = srt_input_device_get_usb_device_sys_path (dev); + + id = blank_id; + + if (srt_input_device_get_usb_device_identity (dev, + &id.vendor_id, + &id.product_id, + &id.version, + &id.manufacturer, + &id.name, + &id.uniq) + || sys_path != NULL) + { + json_builder_set_member_name (builder, "usb_device_ancestor"); + json_builder_begin_object (builder); + + json_builder_set_member_name (builder, "sys_path"); + json_builder_add_string_value (builder, sys_path); + + { + g_autofree gchar *s = g_strdup_printf ("0x%04x", id.vendor_id); + json_builder_set_member_name (builder, "vendor_id"); + json_builder_add_string_value (builder, s); + } + { + g_autofree gchar *s = g_strdup_printf ("0x%04x", id.product_id); + json_builder_set_member_name (builder, "product_id"); + json_builder_add_string_value (builder, s); + } + { + g_autofree gchar *s = g_strdup_printf ("0x%04x", id.version); + json_builder_set_member_name (builder, "version"); + json_builder_add_string_value (builder, s); + } + + json_builder_set_member_name (builder, "manufacturer"); + json_builder_add_string_value (builder, id.manufacturer); + json_builder_set_member_name (builder, "product"); + json_builder_add_string_value (builder, id.name); + json_builder_set_member_name (builder, "serial"); + json_builder_add_string_value (builder, id.uniq); + + if (opt_verbose) + { + g_autofree gchar *uevent = srt_input_device_dup_usb_device_uevent (dev); + + if (uevent != NULL) + add_uevent_member (builder, "uevent", uevent); + } + + json_builder_end_object (builder); + } + } + json_builder_end_object (builder); + } + json_builder_end_object (builder); + + print_json (builder); +} + +static void +removed (SrtInputDeviceMonitor *monitor, + SrtInputDevice *dev, + void *user_data) +{ + g_autoptr(JsonBuilder) builder = json_builder_new (); + + json_builder_begin_object (builder); + { + json_builder_set_member_name (builder, "removed"); + json_builder_begin_object (builder); + { + /* Only print enough details to identify the object */ + json_builder_set_member_name (builder, "dev_node"); + json_builder_add_string_value (builder, + srt_input_device_get_dev_node (dev)); + json_builder_set_member_name (builder, "sys_path"); + json_builder_add_string_value (builder, + srt_input_device_get_sys_path (dev)); + } + json_builder_end_object (builder); + } + json_builder_end_object (builder); + + print_json (builder); +} + +static void +all_for_now (SrtInputDeviceMonitor *source, + void *user_data) +{ + g_autoptr(SrtInputDeviceMonitor) monitor = NULL; + SrtInputDeviceMonitor **monitor_p = user_data; + + if (srt_input_device_monitor_get_flags (source) & SRT_INPUT_DEVICE_MONITOR_FLAGS_ONCE) + { + monitor = g_steal_pointer (monitor_p); + g_assert (monitor == NULL || monitor == source); + g_clear_object (&monitor); + } + + if (opt_seq) + fputs (RECORD_SEPARATOR, original_stdout); + + fputs ("{\"all-for-now\": true}\n", original_stdout); +} + +static gboolean +interrupt_cb (void *user_data) +{ + g_autoptr(SrtInputDeviceMonitor) monitor = NULL; + SrtInputDeviceMonitor **monitor_p = user_data; + + monitor = g_steal_pointer (monitor_p); + g_clear_object (&monitor); + + return G_SOURCE_CONTINUE; +} + +static SrtInputDeviceMonitorFlags opt_mode = SRT_INPUT_DEVICE_MONITOR_FLAGS_NONE; +static SrtInputDeviceMonitorFlags opt_flags = SRT_INPUT_DEVICE_MONITOR_FLAGS_NONE; +static gboolean opt_evdev = FALSE; +static gboolean opt_hidraw = FALSE; +static gboolean opt_print_version = FALSE; + +static gboolean +opt_once_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + opt_flags |= SRT_INPUT_DEVICE_MONITOR_FLAGS_ONCE; + return TRUE; +} + +static gboolean +opt_direct_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + opt_mode = SRT_INPUT_DEVICE_MONITOR_FLAGS_DIRECT; + return TRUE; +} + +static gboolean +opt_udev_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + opt_mode = SRT_INPUT_DEVICE_MONITOR_FLAGS_UDEV; + return TRUE; +} + +static const GOptionEntry option_entries[] = +{ + { "direct", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_direct_cb, + "Find devices using /dev and /sys", NULL }, + { "evdev", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_evdev, + "List evdev event devices", NULL }, + { "hidraw", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_hidraw, + "List raw HID devices", NULL }, + { "once", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_once_cb, + "Print devices that are initially discovered, then exit", NULL }, + { "one-line", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_one_line, + "Print one device per line [default: pretty-print as concatenated JSON]", + NULL }, + { "seq", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_seq, + "Output application/json-seq [default: pretty-print as concatenated JSON]", + NULL }, + { "udev", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_udev_cb, + "Find devices using udev", NULL }, + { "verbose", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_verbose, + "Be more verbose", NULL }, + { "version", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_print_version, + "Print version number and exit", NULL }, + { NULL } +}; + +static gboolean +run (int argc, + char **argv, + GError **error) +{ + g_autoptr(GOptionContext) option_context = NULL; + g_autoptr(SrtInputDeviceMonitor) monitor = NULL; + guint int_handler; + + _srt_setenv_disable_gio_modules (); + + option_context = g_option_context_new (""); + g_option_context_add_main_entries (option_context, option_entries, NULL); + + if (!g_option_context_parse (option_context, &argc, &argv, error)) + return FALSE; + + if (opt_print_version) + { + /* Output version number as YAML for machine-readability, + * inspired by `ostree --version` and `docker version` */ + g_print ( + "%s:\n" + " Package: steam-runtime-tools\n" + " Version: %s\n", + g_get_prgname (), VERSION); + return TRUE; + } + + /* stdout is reserved for machine-readable output, so avoid having + * things like g_debug() pollute it. */ + original_stdout = _srt_divert_stdout_to_stderr (error); + + if (original_stdout == NULL) + return FALSE; + + int_handler = g_unix_signal_add (SIGINT, interrupt_cb, &monitor); + monitor = srt_input_device_monitor_new (opt_mode | opt_flags); + + if (opt_evdev) + srt_input_device_monitor_request_evdev (monitor); + + if (opt_hidraw) + srt_input_device_monitor_request_raw_hid (monitor); + + if (opt_evdev + opt_hidraw == 0) + { + /* Subscribe to everything by default */ + srt_input_device_monitor_request_evdev (monitor); + srt_input_device_monitor_request_raw_hid (monitor); + } + + g_signal_connect (monitor, "added", G_CALLBACK (added), NULL); + g_signal_connect (monitor, "removed", G_CALLBACK (removed), NULL); + g_signal_connect (monitor, "all-for-now", G_CALLBACK (all_for_now), &monitor); + + if (!srt_input_device_monitor_start (monitor, error)) + return FALSE; + + while (monitor != NULL) + g_main_context_iteration (NULL, TRUE); + + if (fclose (original_stdout) != 0) + g_warning ("Unable to close stdout: %s", g_strerror (errno)); + + g_source_remove (int_handler); + return TRUE; +} + +int +main (int argc, + char **argv) +{ + g_autoptr(GError) error = NULL; + + if (run (argc, argv, &error)) + return 0; + + if (error == NULL) + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_FAILED, "Assertion failure"); + + g_printerr ("%s: %s\n", g_get_prgname (), error->message); + + if (error->domain == G_OPTION_ERROR) + return 2; + + return 1; +} diff --git a/bin/input-monitor.md b/bin/input-monitor.md new file mode 100644 index 0000000000000000000000000000000000000000..5066fd1c077a5561b19677486308f348a6aa5f8f --- /dev/null +++ b/bin/input-monitor.md @@ -0,0 +1,109 @@ +--- +title: steam-runtime-input-monitor +section: 1 +... + +# NAME + +steam-runtime-input-monitor - list input devices + +# SYNOPSIS + +**steam-runtime-input-monitor** [*OPTIONS*] + +# DESCRIPTION + +**steam-runtime-input-monitor** lists input devices. + +# OPTIONS + +**--direct** +: List input devices by reading `/dev` and `/sys`. + This currently only lists input devices for which at least read access + to the underlying device node is available. + +**--evdev** +: List all input event devices. + +**--hidraw** +: List all raw HID devices. + +**--once** +: Discover the available input devices, print their details and exit. + +**--one-line** +: Print one event per line ("newline-delimited JSON", unless **--seq** + is also used). By default they are pretty-printed with + readable indentation, which is convenient for a human reader but + less suitable for machine-readable use. + +**--seq** +: Print events in **application/json-seq** format (RFC 7464): + each event is printed as U+001E RECORD SEPARATOR, followed by + a JSON object, followed by U+000A LINE FEED. + +**--udev** +: List input devices by contacting udevd. This is usually not + expected to work inside a container. + +**--version** +: Print the version number as YAML and exit. + +# OUTPUT + +**steam-runtime-input-monitor** prints one or more *events* on +standard output, in JSON format. The precise format is determined +by the **--one-line** and **--seq** options. + +By default, all known input devices are listed. Either +**--evdev** or **--hidraw** can be used to request only a subset of +input devices. For convenience, if no options requesting a subset of +input devices are given, this is treated as equivalent to using options +that request *all* input devices. + +Options to limit devices more selectively (for example, only evdev +devices that have the **ID_INPUT_JOYSTICK** udev property) are likely +to be added in future versions. + +By default, an implementation is chosen automatically. The **--direct** +and **--udev** options override this, with the last one used +taking precedence. + +Each event is a JSON object with a single key. Consumers should ignore +unknown keys to allow for future expansion. + +**all-for-now** +: The initial device enumeration has finished. If the **--once** option + was used, **steam-runtime-input-monitor** will exit. If not, it will + contine to monitor device hotplug events. + + The value is **true**. + +**added** +: A device was added. The value is an object describing the device, + with the following keys and values: + + **dev_node** + : The device node in `/dev` for this device. + + **sys_path** + : The device directory in `/sys` for this device. + + **subsystem** + : The Linux kernel subsystem, either **input** or **hidraw**. + + Additional keys and values are likely to be added in future versions. + +**removed** +: A device was removed. The value is an object with keys **dev_node** + and **sys_path**, as above. + +# EXIT STATUS + +0 +: Success. + +Non-zero +: A fatal error occurred. + +<!-- vim:set sw=4 sts=4 et: --> diff --git a/bin/meson.build b/bin/meson.build index 897d3c989a8c840e35be47f2352373f30eeb3493..d9b667ccbd838c3a23c010fcd4b7a265ba0f66f7 100644 --- a/bin/meson.build +++ b/bin/meson.build @@ -41,9 +41,20 @@ executable( install_rpath : bin_rpath, ) +executable( + 'steam-runtime-input-monitor', + 'input-monitor.c', + dependencies : [gio_unix, glib, gobject, json_glib, libglnx_dep, libsteamrt_static_dep], + install : true, + # Use the adjacent json-glib, ignoring LD_LIBRARY_PATH even if set + build_rpath : bin_rpath, + install_rpath : bin_rpath, +) + if get_option('man') foreach bin_name : [ 'check-requirements', + 'input-monitor', 'system-info', ] custom_target( diff --git a/debian/control b/debian/control index 4afc66f5889f02f79db1001cb0461b1e72cbd66c..9cda84983dceea1081e115b12bca603cb5178e84 100644 --- a/debian/control +++ b/debian/control @@ -14,6 +14,7 @@ Build-Depends: libglib2.0-dev, libjson-glib-dev (>= 1.0), libtheora0 <!nocheck>, + libudev1 <!nocheck> | libudev0 <!nocheck>, libva-dev, libvdpau-dev, libvulkan-dev, diff --git a/steam-runtime-tools/direct-input-device-internal.h b/steam-runtime-tools/direct-input-device-internal.h new file mode 100644 index 0000000000000000000000000000000000000000..310b6b9805ca62e90f3beb6ca94a1cfcfd06e5aa --- /dev/null +++ b/steam-runtime-tools/direct-input-device-internal.h @@ -0,0 +1,67 @@ +/*<private_header>*/ +/* + * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org> + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: Zlib + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#pragma once + +#include "steam-runtime-tools/glib-backports-internal.h" +#include "steam-runtime-tools/input-device.h" + +#define SRT_TYPE_DIRECT_INPUT_DEVICE (_srt_direct_input_device_get_type ()) + +typedef struct _SrtDirectInputDevice SrtDirectInputDevice; +typedef struct _SrtDirectInputDeviceClass SrtDirectInputDeviceClass; + +#define SRT_TYPE_DIRECT_INPUT_DEVICE (_srt_direct_input_device_get_type ()) +#define SRT_DIRECT_INPUT_DEVICE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SRT_TYPE_DIRECT_INPUT_DEVICE, SrtDirectInputDevice)) +#define SRT_IS_DIRECT_INPUT_DEVICE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SRT_TYPE_DIRECT_INPUT_DEVICE)) +#define SRT_DIRECT_INPUT_DEVICE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), SRT_TYPE_DIRECT_INPUT_DEVICE, SrtDirectInputDeviceClass)) +#define SRT_DIRECT_INPUT_DEVICE_CLASS(c) (G_TYPE_CHECK_CLASS_CAST ((c), SRT_TYPE_DIRECT_INPUT_DEVICE, SrtDirectInputDeviceClass)) +#define SRT_IS_DIRECT_INPUT_DEVICE_CLASS(c) (G_TYPE_CHECK_CLASS_TYPE ((c), SRT_TYPE_DIRECT_INPUT_DEVICE)) + +G_GNUC_INTERNAL +GType _srt_direct_input_device_get_type (void); + +#define SRT_TYPE_DIRECT_INPUT_DEVICE_MONITOR (_srt_direct_input_device_monitor_get_type ()) +#define SRT_DIRECT_INPUT_DEVICE_MONITOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SRT_TYPE_DIRECT_INPUT_DEVICE_MONITOR, SrtDirectInputDeviceMonitor)) +#define SRT_IS_DIRECT_INPUT_DEVICE_MONITOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SRT_TYPE_DIRECT_INPUT_DEVICE_MONITOR)) +#define SRT_DIRECT_INPUT_DEVICE_MONITOR_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), SRT_TYPE_DIRECT_INPUT_DEVICE_MONITOR, SrtDirectInputDeviceMonitorClass)) +#define SRT_DIRECT_INPUT_DEVICE_MONITOR_CLASS(c) (G_TYPE_CHECK_CLASS_CAST ((c), SRT_TYPE_DIRECT_INPUT_DEVICE_MONITOR, SrtDirectInputDeviceMonitorClass)) +#define SRT_IS_DIRECT_INPUT_DEVICE_MONITOR_CLASS(c) (G_TYPE_CHECK_CLASS_TYPE ((c), SRT_TYPE_DIRECT_INPUT_DEVICE_MONITOR)) + +typedef struct _SrtDirectInputDeviceMonitor SrtDirectInputDeviceMonitor; +typedef struct _SrtDirectInputDeviceMonitorClass SrtDirectInputDeviceMonitorClass; + +G_GNUC_INTERNAL +GType _srt_direct_input_device_monitor_get_type (void); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtDirectInputDevice, g_object_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtDirectInputDeviceMonitor, g_object_unref) + +static inline SrtInputDeviceMonitor * +srt_direct_input_device_monitor_new (SrtInputDeviceMonitorFlags flags) +{ + return g_object_new (SRT_TYPE_DIRECT_INPUT_DEVICE_MONITOR, + "flags", flags, + NULL); +} diff --git a/steam-runtime-tools/direct-input-device.c b/steam-runtime-tools/direct-input-device.c new file mode 100644 index 0000000000000000000000000000000000000000..6c9c1a82aa8e307472e52e1ace145a7ae76e082b --- /dev/null +++ b/steam-runtime-tools/direct-input-device.c @@ -0,0 +1,1109 @@ +/* + * Direct /dev,/sys-based input device monitor, very loosely based on SDL code. + * + * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org> + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: Zlib + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#include "steam-runtime-tools/direct-input-device-internal.h" + +#include <linux/input.h> +#include <sys/inotify.h> +#include <sys/ioctl.h> + +#include <glib-unix.h> +#include <libglnx.h> + +#include "steam-runtime-tools/input-device.h" + +#include "steam-runtime-tools/glib-backports-internal.h" +#include "steam-runtime-tools/input-device-internal.h" +#include "steam-runtime-tools/utils-internal.h" + +static void srt_direct_input_device_iface_init (SrtInputDeviceInterface *iface); +static void srt_direct_input_device_monitor_iface_init (SrtInputDeviceMonitorInterface *iface); + +static void +remove_last_component_in_place (char *path) +{ + char *slash = strrchr (path, '/'); + + if (slash == NULL) + *path = '\0'; + else + *slash = '\0'; +} + +/* + * @path: The path to a device directory in /sys + * + * Returns: (nullable) (transfer full): The closest ancestor of @path + * that is an input device with evdev capabilities, or %NULL if not found + */ +static gchar * +find_input_ancestor (const char *path) +{ + g_autofree char *ancestor = NULL; + + for (ancestor = g_strdup (path); + g_str_has_prefix (ancestor, "/sys/"); + remove_last_component_in_place (ancestor)) + { + g_autofree char *subsys = g_build_filename (ancestor, "subsystem", NULL); + g_autofree char *caps = g_build_filename (ancestor, "capabilities", "ev", NULL); + g_autofree char *target = glnx_readlinkat_malloc (AT_FDCWD, subsys, + NULL, NULL); + const char *slash = NULL; + + if (!g_file_test (caps, G_FILE_TEST_IS_REGULAR)) + continue; + + if (target != NULL) + slash = strrchr (target, '/'); + + if (slash == NULL || strcmp (slash + 1, "input") != 0) + continue; + + return g_steal_pointer (&ancestor); + } + + return NULL; +} + +/* + * @path: The path to a device directory in /sys + * @subsystem: A desired subsystem, such as "hid" or "usb", or %NULL to accept any + * @devtype: A desired device type, such as "usb_device", or %NULL to accept any + * @uevent_out: (out) (optional): Optionally return the text of /sys/.../uevent + * + * Returns: (nullable) (transfer full): The closest ancestor of @path + * that has a subsystem of @subsystem, or %NULL if not found. + */ +static gchar * +get_ancestor_with_subsystem_devtype (const char *path, + const char *subsystem, + const char *devtype, + gchar **uevent_out) +{ + g_autofree char *ancestor = NULL; + + for (ancestor = g_strdup (path); + g_str_has_prefix (ancestor, "/sys/"); + remove_last_component_in_place (ancestor)) + { + g_autofree char *uevent = g_build_filename (ancestor, "uevent", NULL); + g_autofree char *text = NULL; + + /* If it doesn't have a uevent file, it isn't a real device */ + if (!g_file_get_contents (uevent, &text, NULL, NULL)) + continue; + + if (subsystem != NULL) + { + g_autofree char *subsys = g_build_filename (ancestor, "subsystem", NULL); + g_autofree char *target = glnx_readlinkat_malloc (AT_FDCWD, subsys, + NULL, NULL); + const char *slash = NULL; + + if (target != NULL) + slash = strrchr (target, '/'); + + if (slash == NULL || strcmp (slash + 1, subsystem) != 0) + continue; + } + + if (devtype != NULL + && !_srt_input_device_uevent_field_equals (text, "DEVTYPE", devtype)) + continue; + + if (uevent_out != NULL) + *uevent_out = g_steal_pointer (&text); + + return g_steal_pointer (&ancestor); + } + + return NULL; +} + +static GQuark quark_hidraw = 0; +static GQuark quark_input = 0; + +struct _SrtDirectInputDevice +{ + GObject parent; + + char *sys_path; + gchar *dev_node; + GQuark subsystem; + + struct + { + gchar *sys_path; + } hid_ancestor; + + struct + { + gchar *sys_path; + } input_ancestor; + + struct + { + gchar *sys_path; + gchar *manufacturer; + gchar *product; + gchar *serial; + guint32 product_id; + guint32 vendor_id; + guint32 device_version; + } usb_device_ancestor; + + struct + { + SrtEvdevCapabilities caps; + gchar *name; + gchar *phys; + gchar *uniq; + guint32 bus_type; + guint32 product_id; + guint32 vendor_id; + guint32 version; + } evdev; + + struct + { + gchar *name; + gchar *phys; + gchar *uniq; + guint32 bus_type; + guint32 product_id; + guint32 vendor_id; + } hid; + + SrtInputDeviceInterfaceFlags iface_flags; +}; + +struct _SrtDirectInputDeviceClass +{ + GObjectClass parent; +}; + +struct _SrtDirectInputDeviceMonitor +{ + GObject parent; + GHashTable *devices; + GMainContext *monitor_context; + GSource *monitor_source; + SrtInputDeviceMonitorFlags flags; + gboolean want_evdev; + gboolean want_hidraw; + int inotify_fd; + int dev_watch; + int devinput_watch; + enum + { + NOT_STARTED = 0, + STARTED, + STOPPED + } state; +}; + +struct _SrtDirectInputDeviceMonitorClass +{ + GObjectClass parent; +}; + +G_DEFINE_TYPE_WITH_CODE (SrtDirectInputDevice, + _srt_direct_input_device, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (SRT_TYPE_INPUT_DEVICE, + srt_direct_input_device_iface_init)) + +G_DEFINE_TYPE_WITH_CODE (SrtDirectInputDeviceMonitor, + _srt_direct_input_device_monitor, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (SRT_TYPE_INPUT_DEVICE_MONITOR, + srt_direct_input_device_monitor_iface_init)) + +static void +_srt_direct_input_device_init (SrtDirectInputDevice *self) +{ +} + +static void +srt_direct_input_device_finalize (GObject *object) +{ + SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (object); + + g_clear_pointer (&self->sys_path, free); + g_clear_pointer (&self->dev_node, g_free); + g_clear_pointer (&self->hid_ancestor.sys_path, g_free); + g_clear_pointer (&self->input_ancestor.sys_path, g_free); + g_clear_pointer (&self->usb_device_ancestor.sys_path, g_free); + g_clear_pointer (&self->usb_device_ancestor.manufacturer, g_free); + g_clear_pointer (&self->usb_device_ancestor.product, g_free); + g_clear_pointer (&self->usb_device_ancestor.serial, g_free); + g_clear_pointer (&self->hid.name, g_free); + g_clear_pointer (&self->hid.phys, g_free); + g_clear_pointer (&self->hid.uniq, g_free); + g_clear_pointer (&self->evdev.name, g_free); + g_clear_pointer (&self->evdev.phys, g_free); + g_clear_pointer (&self->evdev.uniq, g_free); + + G_OBJECT_CLASS (_srt_direct_input_device_parent_class)->finalize (object); +} + +static void +_srt_direct_input_device_class_init (SrtDirectInputDeviceClass *cls) +{ + GObjectClass *object_class = G_OBJECT_CLASS (cls); + + object_class->finalize = srt_direct_input_device_finalize; +} + +static SrtInputDeviceInterfaceFlags +srt_direct_input_device_get_interface_flags (SrtInputDevice *device) +{ + SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device); + + return self->iface_flags; +} + +static const char * +srt_direct_input_device_get_dev_node (SrtInputDevice *device) +{ + SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device); + + return self->dev_node; +} + +static const char * +srt_direct_input_device_get_subsystem (SrtInputDevice *device) +{ + SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device); + + return g_quark_to_string (self->subsystem); +} + +static const char * +srt_direct_input_device_get_sys_path (SrtInputDevice *device) +{ + SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device); + + return self->sys_path; +} + +static const SrtEvdevCapabilities * +srt_direct_input_device_peek_event_capabilities (SrtInputDevice *device) +{ + SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device); + + return &self->evdev.caps; +} + +static const char * +srt_direct_input_device_get_hid_sys_path (SrtInputDevice *device) +{ + SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device); + + return self->hid_ancestor.sys_path; +} + +static gboolean +srt_direct_input_device_get_hid_identity (SrtInputDevice *device, + unsigned int *bus_type, + unsigned int *vendor_id, + unsigned int *product_id, + const char **name, + const char **phys, + const char **uniq) +{ + SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device); + + if (self->hid_ancestor.sys_path == NULL + && (self->iface_flags & SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID) == 0) + return FALSE; + + if (bus_type != NULL) + *bus_type = self->hid.bus_type; + + if (vendor_id != NULL) + *vendor_id = self->hid.vendor_id; + + if (product_id != NULL) + *product_id = self->hid.product_id; + + if (name != NULL) + *name = self->hid.name; + + if (phys != NULL) + *phys = self->hid.phys; + + if (uniq != NULL) + *uniq = self->hid.uniq; + + return TRUE; +} + +static const char * +srt_direct_input_device_get_input_sys_path (SrtInputDevice *device) +{ + SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device); + + return self->input_ancestor.sys_path; +} + +static gboolean +srt_direct_input_device_get_input_identity (SrtInputDevice *device, + unsigned int *bus_type, + unsigned int *vendor_id, + unsigned int *product_id, + unsigned int *version, + const char **name, + const char **phys, + const char **uniq) +{ + SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device); + + if (self->input_ancestor.sys_path == NULL + && (self->iface_flags & SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT) == 0) + return FALSE; + + if (bus_type != NULL) + *bus_type = self->evdev.bus_type; + + if (vendor_id != NULL) + *vendor_id = self->evdev.vendor_id; + + if (product_id != NULL) + *product_id = self->evdev.product_id; + + if (version != NULL) + *version = self->evdev.version; + + if (name != NULL) + *name = self->evdev.name; + + if (phys != NULL) + *phys = self->evdev.phys; + + if (uniq != NULL) + *uniq = self->evdev.uniq; + + return TRUE; +} + +static const char * +srt_direct_input_device_get_usb_device_sys_path (SrtInputDevice *device) +{ + SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device); + + return self->usb_device_ancestor.sys_path; +} + +static gboolean +srt_direct_input_device_get_usb_device_identity (SrtInputDevice *device, + unsigned int *vendor_id, + unsigned int *product_id, + unsigned int *device_version, + const char **manufacturer, + const char **product, + const char **serial) +{ + SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device); + + if (self->usb_device_ancestor.sys_path == NULL) + return FALSE; + + if (vendor_id != NULL) + *vendor_id = self->usb_device_ancestor.vendor_id; + + if (product_id != NULL) + *product_id = self->usb_device_ancestor.product_id; + + if (device_version != NULL) + *device_version = self->usb_device_ancestor.device_version; + + if (manufacturer != NULL) + *manufacturer = self->usb_device_ancestor.manufacturer; + + if (product != NULL) + *product = self->usb_device_ancestor.product; + + if (serial != NULL) + *serial = self->usb_device_ancestor.serial; + + return TRUE; +} + +static void +srt_direct_input_device_iface_init (SrtInputDeviceInterface *iface) +{ +#define IMPLEMENT(x) iface->x = srt_direct_input_device_ ## x + + IMPLEMENT (get_interface_flags); + IMPLEMENT (get_dev_node); + IMPLEMENT (get_sys_path); + IMPLEMENT (get_subsystem); + IMPLEMENT (peek_event_capabilities); + + IMPLEMENT (get_hid_sys_path); + IMPLEMENT (get_input_sys_path); + IMPLEMENT (get_usb_device_sys_path); + + IMPLEMENT (get_hid_identity); + IMPLEMENT (get_input_identity); + IMPLEMENT (get_usb_device_identity); + +#undef IMPLEMENT +} + +typedef enum +{ + PROP_0, + PROP_FLAGS, + PROP_IS_ACTIVE, + N_PROPERTIES +} Property; + +static void +_srt_direct_input_device_monitor_init (SrtDirectInputDeviceMonitor *self) +{ + self->monitor_context = g_main_context_ref_thread_default (); + self->state = NOT_STARTED; + self->devices = g_hash_table_new_full (g_str_hash, + g_str_equal, + NULL, + g_object_unref); + self->inotify_fd = -1; + self->dev_watch = -1; + self->devinput_watch = -1; +} + +static void +srt_direct_input_device_monitor_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + SrtDirectInputDeviceMonitor *self = SRT_DIRECT_INPUT_DEVICE_MONITOR (object); + + switch ((Property) prop_id) + { + case PROP_IS_ACTIVE: + g_value_set_boolean (value, (self->state == STARTED)); + break; + + case PROP_FLAGS: + g_value_set_flags (value, self->flags); + break; + + case PROP_0: + case N_PROPERTIES: + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +srt_direct_input_device_monitor_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + SrtDirectInputDeviceMonitor *self = SRT_DIRECT_INPUT_DEVICE_MONITOR (object); + + switch ((Property) prop_id) + { + case PROP_FLAGS: + /* Construct-only */ + g_return_if_fail (self->flags == 0); + self->flags = g_value_get_flags (value); + break; + + case PROP_IS_ACTIVE: + case PROP_0: + case N_PROPERTIES: + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +srt_direct_input_device_monitor_dispose (GObject *object) +{ + srt_input_device_monitor_stop (SRT_INPUT_DEVICE_MONITOR (object)); + G_OBJECT_CLASS (_srt_direct_input_device_monitor_parent_class)->dispose (object); +} + +static void +srt_direct_input_device_monitor_finalize (GObject *object) +{ + srt_input_device_monitor_stop (SRT_INPUT_DEVICE_MONITOR (object)); + G_OBJECT_CLASS (_srt_direct_input_device_monitor_parent_class)->finalize (object); +} + +static void +_srt_direct_input_device_monitor_class_init (SrtDirectInputDeviceMonitorClass *cls) +{ + GObjectClass *object_class = G_OBJECT_CLASS (cls); + + object_class->get_property = srt_direct_input_device_monitor_get_property; + object_class->set_property = srt_direct_input_device_monitor_set_property; + object_class->dispose = srt_direct_input_device_monitor_dispose; + object_class->finalize = srt_direct_input_device_monitor_finalize; + + g_object_class_override_property (object_class, PROP_FLAGS, "flags"); + g_object_class_override_property (object_class, PROP_IS_ACTIVE, "is-active"); + + quark_hidraw = g_quark_from_static_string ("hidraw"); + quark_input = g_quark_from_static_string ("input"); +} + +/* + * Get a sysfs attribute that is a string. + * + * On success, set *out and return TRUE. + * On failure, leave *out untouched and return FALSE. + */ +static gboolean +dup_string (const char *sys_path, + const char *attribute, + gchar **out) +{ + g_autofree gchar *text = NULL; + g_autofree gchar *child = NULL; + + if (sys_path == NULL) + return FALSE; + + child = g_build_filename (sys_path, attribute, NULL); + + if (!g_file_get_contents (child, &text, NULL, NULL)) + return FALSE; + + g_strchomp (text); + + if (out != NULL) + *out = g_steal_pointer (&text); + + return TRUE; +} + +/* + * Get a sysfs attribute that is a uint32 (or smaller) in hexadecimal + * (with or without 0x prefix). + * + * On success, set *out and return TRUE. + * On failure, leave *out untouched and return FALSE. + */ +static gboolean +get_uint32_hex (const char *sys_path, + const char *attribute, + guint32 *out) +{ + g_autofree gchar *buf = NULL; + const char *tmp; + guint64 ret; + gchar *endptr; + + if (!dup_string (sys_path, attribute, &buf)) + return FALSE; + + tmp = buf; + + if (tmp[0] == '0' && (tmp[1] == 'x' || tmp[1] == 'X')) + tmp += 2; + + ret = g_ascii_strtoull (tmp, &endptr, 16); + + if (endptr == NULL + || (*endptr != '\0' && *endptr != '\n') + || ret > G_MAXUINT32) + return FALSE; + + if (out != NULL) + *out = (guint32) ret; + + return TRUE; +} + +static void +read_hid_ancestor (SrtDirectInputDevice *device) +{ + g_autofree gchar *uevent = NULL; + + if (device->hid_ancestor.sys_path == NULL) + return; + + if (!dup_string (device->hid_ancestor.sys_path, "uevent", &uevent)) + return; + + _srt_get_identity_from_hid_uevent (uevent, + &device->hid.bus_type, + &device->hid.vendor_id, + &device->hid.product_id, + &device->hid.name, + &device->hid.phys, + &device->hid.uniq); +} + +static void +read_input_ancestor (SrtDirectInputDevice *device) +{ + if (device->input_ancestor.sys_path == NULL) + return; + + get_uint32_hex (device->input_ancestor.sys_path, "id/bustype", + &device->evdev.bus_type); + get_uint32_hex (device->input_ancestor.sys_path, "id/vendor", + &device->evdev.vendor_id); + get_uint32_hex (device->input_ancestor.sys_path, "id/product", + &device->evdev.product_id); + get_uint32_hex (device->input_ancestor.sys_path, "id/version", + &device->evdev.version); + dup_string (device->input_ancestor.sys_path, "name", &device->evdev.name); + dup_string (device->input_ancestor.sys_path, "phys", &device->evdev.phys); + dup_string (device->input_ancestor.sys_path, "uniq", &device->evdev.uniq); +} + +static void +read_usb_device_ancestor (SrtDirectInputDevice *device) +{ + if (device->usb_device_ancestor.sys_path == NULL) + return; + + get_uint32_hex (device->usb_device_ancestor.sys_path, "idVendor", + &device->usb_device_ancestor.vendor_id); + get_uint32_hex (device->usb_device_ancestor.sys_path, "idProduct", + &device->usb_device_ancestor.product_id); + get_uint32_hex (device->usb_device_ancestor.sys_path, "bcdDevice", + &device->usb_device_ancestor.device_version); + dup_string (device->usb_device_ancestor.sys_path, "manufacturer", + &device->usb_device_ancestor.manufacturer); + dup_string (device->usb_device_ancestor.sys_path, "product", + &device->usb_device_ancestor.product); +} + +static void +add_device (SrtDirectInputDeviceMonitor *self, + const char *devnode, + GQuark subsystem) +{ + SrtDirectInputDevice *device = NULL; + const char *slash = strrchr (devnode, '/'); + g_autofree char *sys_symlink = NULL; + int fd; + + /* Never add a device for a second time */ + if (g_hash_table_contains (self->devices, devnode)) + return; + + if (slash == NULL || slash[1] == '\0') + return; + + device = g_object_new (SRT_TYPE_DIRECT_INPUT_DEVICE, NULL); + device->dev_node = g_strdup (devnode); + device->subsystem = subsystem; + + sys_symlink = g_build_filename ("/sys/class", + g_quark_to_string (subsystem), + slash + 1, NULL); + device->sys_path = realpath (sys_symlink, NULL); + + if (device->sys_path == NULL) + { + g_debug ("unable to get real path of %s: %s", + sys_symlink, g_strerror (errno)); + g_object_unref (device); + return; + } + + if (subsystem == quark_hidraw) + device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID; + + g_debug ("Trying to open %s", devnode); + + fd = open (devnode, O_RDONLY | O_NONBLOCK | _SRT_INPUT_DEVICE_ALWAYS_OPEN_FLAGS); + + if (fd >= 0) + { + /* The permissions are already OK for use */ + g_debug ("Opened %s", devnode); + device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE; + + if (_srt_get_identity_from_raw_hid (fd, + &device->hid.bus_type, + &device->hid.vendor_id, + &device->hid.product_id, + &device->hid.name, + &device->hid.phys, + &device->hid.uniq)) + { + g_debug ("%s is raw HID: bus type 0x%04x, vendor 0x%04x, product 0x%04x, \"%s\"", + devnode, + device->hid.bus_type, + device->hid.vendor_id, + device->hid.product_id, + device->hid.name); + device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID; + } + + if (_srt_get_identity_from_evdev (fd, + &device->evdev.bus_type, + &device->evdev.vendor_id, + &device->evdev.product_id, + &device->evdev.version, + &device->evdev.name, + &device->evdev.phys, + &device->evdev.uniq)) + { + g_debug ("%s is evdev: bus type 0x%04x, vendor 0x%04x, product 0x%04x, version 0x%04x, \"%s\"", + devnode, + device->evdev.bus_type, + device->evdev.vendor_id, + device->evdev.product_id, + device->evdev.version, + device->evdev.name); + device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT; + } + + _srt_evdev_capabilities_set_from_evdev (&device->evdev.caps, fd); + close (fd); + } + else + { + /* We'll get another chance after the permissions get updated */ + g_debug ("Unable to open %s to identify it: %s", devnode, g_strerror (errno)); + g_object_unref (device); + return; + } + + fd = open (devnode, O_RDWR | O_NONBLOCK | _SRT_INPUT_DEVICE_ALWAYS_OPEN_FLAGS); + + if (fd >= 0) + { + g_debug ("Opened %s rw", devnode); + device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE; + close (fd); + } + + if ((device->iface_flags & (SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT + | SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID)) == 0) + { + g_debug ("%s is neither evdev nor raw HID, ignoring", devnode); + g_object_unref (device); + return; + } + + device->hid_ancestor.sys_path = get_ancestor_with_subsystem_devtype (device->sys_path, + "hid", NULL, + NULL); + read_hid_ancestor (device); + device->input_ancestor.sys_path = find_input_ancestor (device->sys_path); + read_input_ancestor (device); + + if (device->hid.bus_type == BUS_USB || device->evdev.bus_type == BUS_USB) + { + device->usb_device_ancestor.sys_path = get_ancestor_with_subsystem_devtype (device->sys_path, + "usb", + "usb_device", + NULL); + read_usb_device_ancestor (device); + } + + g_hash_table_replace (self->devices, device->dev_node, device); + _srt_input_device_monitor_emit_added (SRT_INPUT_DEVICE_MONITOR (self), + SRT_INPUT_DEVICE (device)); +} + +static void +remove_device (SrtDirectInputDeviceMonitor *self, + const char *devnode) +{ + void *device = NULL; + + g_debug ("Removing device %s", devnode); + + if (g_hash_table_lookup_extended (self->devices, devnode, NULL, &device)) + { + g_hash_table_steal (self->devices, devnode); + _srt_input_device_monitor_emit_removed (SRT_INPUT_DEVICE_MONITOR (self), + device); + g_object_unref (device); + } +} + +static gboolean +srt_direct_input_device_monitor_cb (int fd, + GIOCondition condition, + void *user_data) +{ + SrtDirectInputDeviceMonitor *self = SRT_DIRECT_INPUT_DEVICE_MONITOR (user_data); + union + { + struct inotify_event event; + char storage[4096]; + char enough_for_inotify[sizeof (struct inotify_event) + NAME_MAX + 1]; + } buf; + ssize_t bytes; + size_t remain = 0; + size_t len; + + g_return_val_if_fail (SRT_IS_DIRECT_INPUT_DEVICE_MONITOR (self), G_SOURCE_REMOVE); + + bytes = read (self->inotify_fd, &buf, sizeof (buf)); + + if (bytes > 0) + remain = (size_t) bytes; + + while (remain > 0) + { + g_return_val_if_fail (remain >= sizeof (struct inotify_event), G_SOURCE_REMOVE); + + if (buf.event.len > 0) + { + if (buf.event.wd == self->dev_watch) + { + if (g_str_has_prefix (buf.event.name, "hidraw") + && _srt_str_is_integer (buf.event.name + strlen ("hidraw"))) + { + g_autofree gchar *path = g_build_filename ("/dev", + buf.event.name, + NULL); + + if (buf.event.mask & (IN_CREATE | IN_MOVED_TO | IN_ATTRIB)) + add_device (self, path, quark_hidraw); + else if (buf.event.mask & (IN_DELETE | IN_MOVED_FROM)) + remove_device (self, path); + } + } + else if (buf.event.wd == self->devinput_watch) + { + if (g_str_has_prefix (buf.event.name, "event") + && _srt_str_is_integer (buf.event.name + strlen ("event"))) + { + g_autofree gchar *path = g_build_filename ("/dev/input", + buf.event.name, + NULL); + + if (buf.event.mask & (IN_CREATE | IN_MOVED_TO | IN_ATTRIB)) + add_device (self, path, quark_input); + else if (buf.event.mask & (IN_DELETE | IN_MOVED_FROM)) + remove_device (self, path); + } + } + } + + len = sizeof (struct inotify_event) + buf.event.len; + remain -= len; + + if (remain != 0) + memmove (&buf.storage[0], &buf.storage[len], remain); + } + + return G_SOURCE_CONTINUE; +} + +static void +srt_direct_input_device_monitor_request_raw_hid (SrtInputDeviceMonitor *monitor) +{ + SrtDirectInputDeviceMonitor *self = SRT_DIRECT_INPUT_DEVICE_MONITOR (monitor); + + g_return_if_fail (SRT_IS_DIRECT_INPUT_DEVICE_MONITOR (monitor)); + g_return_if_fail (self->state == NOT_STARTED); + + self->want_hidraw = TRUE; +} + +static void +srt_direct_input_device_monitor_request_evdev (SrtInputDeviceMonitor *monitor) +{ + SrtDirectInputDeviceMonitor *self = SRT_DIRECT_INPUT_DEVICE_MONITOR (monitor); + + g_return_if_fail (SRT_IS_DIRECT_INPUT_DEVICE_MONITOR (monitor)); + g_return_if_fail (self->state == NOT_STARTED); + + self->want_evdev = TRUE; +} + +static gboolean +enumerate_cb (gpointer user_data) +{ + SrtDirectInputDeviceMonitor *self = SRT_DIRECT_INPUT_DEVICE_MONITOR (user_data); + + if (self->want_hidraw) + { + g_auto(GLnxDirFdIterator) iter = { FALSE }; + g_autoptr(GError) error = NULL; + + if (!glnx_dirfd_iterator_init_at (AT_FDCWD, "/dev", TRUE, &iter, &error)) + { + g_debug ("Unable to open /dev/: %s", error->message); + } + + while (error == NULL) + { + struct dirent *dent; + + if (!glnx_dirfd_iterator_next_dent (&iter, &dent, NULL, &error)) + { + g_debug ("Unable to iterate over /dev/: %s", + error->message); + break; + } + + if (dent == NULL) + break; + + if (g_str_has_prefix (dent->d_name, "hidraw") + && _srt_str_is_integer (dent->d_name + strlen ("hidraw"))) + { + g_autofree gchar *path = g_build_filename ("/dev", dent->d_name, NULL); + add_device (self, path, quark_hidraw); + } + } + } + + if (self->want_evdev) + { + g_auto(GLnxDirFdIterator) iter = { FALSE }; + g_autoptr(GError) error = NULL; + + if (!glnx_dirfd_iterator_init_at (AT_FDCWD, "/dev/input", TRUE, &iter, + &error)) + { + g_debug ("Unable to open /dev/input/: %s", error->message); + } + + while (error == NULL) + { + struct dirent *dent; + + if (!glnx_dirfd_iterator_next_dent (&iter, &dent, NULL, &error)) + { + g_debug ("Unable to iterate over /dev/input/: %s", + error->message); + break; + } + + if (dent == NULL) + break; + + if (g_str_has_prefix (dent->d_name, "event") + && _srt_str_is_integer (dent->d_name + strlen ("event"))) + { + g_autofree gchar *path = g_build_filename ("/dev/input", dent->d_name, NULL); + add_device (self, path, quark_input); + } + } + } + + _srt_input_device_monitor_emit_all_for_now (SRT_INPUT_DEVICE_MONITOR (self)); + return G_SOURCE_REMOVE; +} + +static gboolean +srt_direct_input_device_monitor_start (SrtInputDeviceMonitor *monitor, + GError **error) +{ + SrtDirectInputDeviceMonitor *self = SRT_DIRECT_INPUT_DEVICE_MONITOR (monitor); + + g_return_val_if_fail (SRT_IS_DIRECT_INPUT_DEVICE_MONITOR (monitor), FALSE); + g_return_val_if_fail (self->state == NOT_STARTED, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + self->state = STARTED; + + if ((self->flags & SRT_INPUT_DEVICE_MONITOR_FLAGS_ONCE) == 0 + && (self->want_hidraw || self->want_evdev)) + { + self->inotify_fd = inotify_init1 (IN_NONBLOCK | IN_CLOEXEC); + + if (self->inotify_fd < 0) + return glnx_throw_errno_prefix (error, "inotify_init1"); + + if (self->want_hidraw) + { + self->dev_watch = inotify_add_watch (self->inotify_fd, "/dev", + IN_CREATE | IN_DELETE | IN_MOVE | IN_ATTRIB); + + if (self->dev_watch < 0) + return glnx_throw_errno_prefix (error, "inotify_add_watch"); + } + + if (self->want_evdev) + { + self->devinput_watch = inotify_add_watch (self->inotify_fd, "/dev/input", + IN_CREATE | IN_DELETE | IN_MOVE | IN_ATTRIB); + + if (self->devinput_watch < 0) + return glnx_throw_errno_prefix (error, "inotify_add_watch"); + } + + self->monitor_source = g_unix_fd_source_new (self->inotify_fd, G_IO_IN); + g_source_set_callback (self->monitor_source, + (GSourceFunc) G_CALLBACK (srt_direct_input_device_monitor_cb), + self, NULL); + g_source_set_priority (self->monitor_source, G_PRIORITY_DEFAULT); + g_source_attach (self->monitor_source, self->monitor_context); + } + + /* Make sure the signals for the initial batch of devices are emitted in + * the correct main-context */ + g_main_context_invoke_full (self->monitor_context, G_PRIORITY_DEFAULT, + enumerate_cb, g_object_ref (self), + g_object_unref); + return TRUE; +} + +static void +srt_direct_input_device_monitor_stop (SrtInputDeviceMonitor *monitor) +{ + SrtDirectInputDeviceMonitor *self = SRT_DIRECT_INPUT_DEVICE_MONITOR (monitor); + + self->state = STOPPED; + + if (self->monitor_source != NULL) + g_source_destroy (self->monitor_source); + + g_clear_pointer (&self->monitor_source, g_source_unref); + g_clear_pointer (&self->monitor_context, g_main_context_unref); + g_clear_pointer (&self->devices, g_hash_table_unref); + + if (self->inotify_fd >= 0) + { + close (self->inotify_fd); + self->dev_watch = -1; + self->devinput_watch = -1; + self->inotify_fd = -1; + } +} + +static void +srt_direct_input_device_monitor_iface_init (SrtInputDeviceMonitorInterface *iface) +{ +#define IMPLEMENT(x) iface->x = srt_direct_input_device_monitor_ ## x + IMPLEMENT (request_evdev); + IMPLEMENT (request_raw_hid); + IMPLEMENT (start); + IMPLEMENT (stop); +#undef IMPLEMENT +} diff --git a/steam-runtime-tools/input-device-internal.h b/steam-runtime-tools/input-device-internal.h new file mode 100644 index 0000000000000000000000000000000000000000..c18c8ea9e9dd0fd4f95c2402af554470ab8d90c6 --- /dev/null +++ b/steam-runtime-tools/input-device-internal.h @@ -0,0 +1,251 @@ +/*<private_header>*/ +/* + * Input device internals, with parts based on SDL code. + * + * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org> + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: Zlib + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#pragma once + +#include "steam-runtime-tools/input-device.h" + +#include <linux/input.h> + +#define _SRT_INPUT_DEVICE_ALWAYS_OPEN_FLAGS (O_CLOEXEC | O_NOCTTY) + +/* Macros for the bitfield encoding used by the EVIOCGBIT ioctl. */ +#define BITS_PER_LONG (sizeof (unsigned long) * CHAR_BIT) +#define LONGS_FOR_BITS(x) ((((x)-1)/BITS_PER_LONG)+1) +#define CHOOSE_BIT(x) ((x)%BITS_PER_LONG) +#define CHOOSE_LONG(x) ((x)/BITS_PER_LONG) + +/* Note that this always returns 0 or 1, so it can be considered to return + * a gboolean */ +#define test_bit(bit, ulongs) ((ulongs[CHOOSE_LONG(bit)] >> CHOOSE_BIT(bit)) & 1) + +#define set_bit(bit, ulongs) do { ulongs[CHOOSE_LONG(bit)] |= (1UL << CHOOSE_BIT(bit)); } while (0) + +/* + * @bit_number: A bit number, where 0 is the least significant bit of bits[0] + * @bits: (in) (array length=n_longs): Bitfields encoded as longs, where + * the most significant bit of bits[0] is one place less significant than + * the least significant bit of bits[1] + * @n_longs: The number of elements in @bits + * + * Decode the bitfield encoding used by the EVIOCGBIT ioctl. + */ +static inline gboolean +test_bit_checked (unsigned int bit_number, + const unsigned long *bits, + size_t n_longs) +{ + if (CHOOSE_LONG (bit_number) >= n_longs) + return FALSE; + + return test_bit (bit_number, bits); +} + +/* We assume a buffer large enough for all the keyboard/button codes is + * also sufficient for all the less numerous event types. */ +#define HIGHEST_EVENT_CODE (KEY_MAX) +G_STATIC_ASSERT (KEY_MAX >= EV_MAX); +G_STATIC_ASSERT (KEY_MAX >= ABS_MAX); +G_STATIC_ASSERT (KEY_MAX >= REL_MAX); +G_STATIC_ASSERT (KEY_MAX >= FF_MAX); + +/* Some of the event codes we're interested in weren't in in older kernels, + * like the one whose headers we use for SteamRT 1 'scout'. */ + +#ifndef ABS_RESERVED +# define ABS_RESERVED 0x2e +#endif + +G_STATIC_ASSERT (ABS_RESERVED < KEY_MAX); + +#ifndef REL_RESERVED +# define REL_RESERVED 0x0a +#endif + +#ifndef REL_WHEEL_HI_RES +# define REL_WHEEL_HI_RES 0x0b +#endif + +#ifndef REL_HWHEEL_HI_RES +# define REL_HWHEEL_HI_RES 0x0c +#endif + +G_STATIC_ASSERT (REL_HWHEEL_HI_RES < KEY_MAX); + +#ifndef BTN_DPAD_UP + /* We assume that these were all defined together */ +# define BTN_DPAD_UP 0x220 +# define BTN_DPAD_DOWN 0x221 +# define BTN_DPAD_LEFT 0x222 +# define BTN_DPAD_RIGHT 0x223 +#endif + +#ifndef KEY_MACRO1 +# define KEY_MACRO1 0x290 +#endif + +G_STATIC_ASSERT (KEY_MACRO1 < KEY_MAX); + +#ifndef INPUT_PROP_TOPBUTTONPAD +# define INPUT_PROP_TOPBUTTONPAD 0x04 +#endif +#ifndef INPUT_PROP_POINTING_STICK +# define INPUT_PROP_POINTING_STICK 0x05 +#endif +#ifndef INPUT_PROP_ACCELEROMETER +# define INPUT_PROP_ACCELEROMETER 0x06 +#endif + +G_STATIC_ASSERT (INPUT_PROP_ACCELEROMETER < INPUT_PROP_MAX); + +typedef struct +{ + unsigned long ev[LONGS_FOR_BITS (EV_MAX)]; + unsigned long keys[LONGS_FOR_BITS (KEY_MAX)]; + unsigned long abs[LONGS_FOR_BITS (ABS_MAX)]; + unsigned long rel[LONGS_FOR_BITS (REL_MAX)]; + unsigned long ff[LONGS_FOR_BITS (FF_MAX)]; + unsigned long props[LONGS_FOR_BITS (INPUT_PROP_MAX)]; +} SrtEvdevCapabilities; + +const unsigned long *_srt_evdev_capabilities_get_bits (const SrtEvdevCapabilities *caps, + unsigned int type, + size_t *n_longs); +gboolean _srt_evdev_capabilities_set_from_evdev (SrtEvdevCapabilities *caps, + int fd); +void _srt_evdev_capabilities_dump (const SrtEvdevCapabilities *caps); +SrtInputDeviceTypeFlags _srt_evdev_capabilities_guess_type (const SrtEvdevCapabilities *caps); + +struct _SrtInputDeviceInterface +{ + GTypeInterface parent; + + SrtInputDeviceTypeFlags (*get_type_flags) (SrtInputDevice *device); + SrtInputDeviceInterfaceFlags (*get_interface_flags) (SrtInputDevice *device); + + const char * (*get_dev_node) (SrtInputDevice *device); + const char * (*get_sys_path) (SrtInputDevice *device); + const char * (*get_subsystem) (SrtInputDevice *device); + gchar ** (*dup_udev_properties) (SrtInputDevice *device); + gchar * (*dup_uevent) (SrtInputDevice *device); + gboolean (*get_identity) (SrtInputDevice *device, + unsigned int *bus_type, + unsigned int *vendor_id, + unsigned int *product_id, + unsigned int *version); + const SrtEvdevCapabilities *(*peek_event_capabilities) (SrtInputDevice *device); + + const char * (*get_hid_sys_path) (SrtInputDevice *device); + gchar * (*dup_hid_uevent) (SrtInputDevice *device); + gboolean (*get_hid_identity) (SrtInputDevice *device, + unsigned int *bus_type, + unsigned int *vendor_id, + unsigned int *product_id, + const char **name, + const char **phys, + const char **uniq); + + const char * (*get_input_sys_path) (SrtInputDevice *device); + gchar * (*dup_input_uevent) (SrtInputDevice *device); + gboolean (*get_input_identity) (SrtInputDevice *device, + unsigned int *bus_type, + unsigned int *vendor_id, + unsigned int *product_id, + unsigned int *version, + const char **name, + const char **phys, + const char **uniq); + + const char * (*get_usb_device_sys_path) (SrtInputDevice *device); + gchar * (*dup_usb_device_uevent) (SrtInputDevice *device); + gboolean (*get_usb_device_identity) (SrtInputDevice *device, + unsigned int *vendor_id, + unsigned int *product_id, + unsigned int *device_version, + const char **manufacturer, + const char **product, + const char **serial); + + int (*open_device) (SrtInputDevice *device, + int mode_and_flags, + GError **error); +}; + +gboolean _srt_input_device_check_open_flags (int mode_and_flags, + GError **error); + +struct _SrtInputDeviceMonitorInterface +{ + GTypeInterface parent; + + /* Signals */ + void (*added) (SrtInputDeviceMonitor *monitor, + SrtInputDevice *device); + void (*removed) (SrtInputDeviceMonitor *monitor, + SrtInputDevice *device); + void (*all_for_now) (SrtInputDeviceMonitor *monitor); + + /* Virtual methods */ + void (*request_raw_hid) (SrtInputDeviceMonitor *monitor); + void (*request_evdev) (SrtInputDeviceMonitor *monitor); + gboolean (*start) (SrtInputDeviceMonitor *monitor, + GError **error); + void (*stop) (SrtInputDeviceMonitor *monitor); +}; + +void _srt_input_device_monitor_emit_added (SrtInputDeviceMonitor *monitor, + SrtInputDevice *device); +void _srt_input_device_monitor_emit_removed (SrtInputDeviceMonitor *monitor, + SrtInputDevice *device); +void _srt_input_device_monitor_emit_all_for_now (SrtInputDeviceMonitor *monitor); + +gboolean _srt_get_identity_from_evdev (int fd, + guint32 *bus_type, + guint32 *vendor, + guint32 *product, + guint32 *version, + gchar **name, + gchar **phys, + gchar **uniq); +gboolean _srt_get_identity_from_raw_hid (int fd, + guint32 *bus_type, + guint32 *vendor, + guint32 *product, + gchar **name, + gchar **phys, + gchar **uniq); +gchar *_srt_input_device_uevent_field (const char *text, + const char *key); +gboolean _srt_input_device_uevent_field_equals (const char *text, + const char *key, + const char *want_value); +gboolean _srt_get_identity_from_hid_uevent (const char *text, + guint32 *bus_type, + guint32 *product_id, + guint32 *vendor_id, + gchar **name, + gchar **phys, + gchar **uniq); diff --git a/steam-runtime-tools/input-device.c b/steam-runtime-tools/input-device.c new file mode 100644 index 0000000000000000000000000000000000000000..0ef2ca891b6864d0b9da30c72192e9b9cc1f24ff --- /dev/null +++ b/steam-runtime-tools/input-device.c @@ -0,0 +1,2021 @@ +/* + * Input device internals, with parts based on SDL code. + * + * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org> + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: Zlib + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#include "steam-runtime-tools/input-device.h" +#include "steam-runtime-tools/input-device-internal.h" + +#include <linux/hidraw.h> +#include <linux/input.h> + +#include <libglnx.h> + +#include "steam-runtime-tools/enums.h" + +#include "steam-runtime-tools/direct-input-device-internal.h" +#include "steam-runtime-tools/udev-input-device-internal.h" + +#ifndef HIDIOCGRAWUNIQ +/* added in Linux 5.6, will fail on older kernels; this should + * be fine, we'll just report a NULL unique ID */ +#define HIDIOCGRAWUNIQ(len) _IOC(_IOC_READ, 'H', 0x08, len) +#endif + +/* + * Set @error and return %FALSE if @mode_and_flags is unsupported. + */ +gboolean +_srt_input_device_check_open_flags (int mode_and_flags, + GError **error) +{ + int mode; + int unhandled_flags; + + mode = mode_and_flags & (O_RDONLY|O_WRONLY|O_RDWR); + unhandled_flags = mode_and_flags & ~mode; + + if (unhandled_flags & O_NONBLOCK) + unhandled_flags &= ~O_NONBLOCK; + + if (unhandled_flags != 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "Flags not supported: 0x%x", unhandled_flags); + return FALSE; + } + + switch (mode) + { + case O_RDONLY: + case O_WRONLY: + case O_RDWR: + break; + + default: + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "Mode not supported: 0x%x", mode); + return FALSE; + } + + return TRUE; +} + +/** + * SrtInputDevice: + * + * An input device. + * + * Take additional references with g_object_ref(), release references + * with g_object_unref(). + */ +/** + * SrtInputDeviceInterface: + * + * The interface implemented by each SrtInputDevice. + */ +G_DEFINE_INTERFACE (SrtInputDevice, srt_input_device, G_TYPE_OBJECT) + +static const char * +srt_input_device_default_get_null_string (SrtInputDevice *device) +{ + return NULL; +} + +static gchar ** +srt_input_device_default_get_null_strv (SrtInputDevice *device) +{ + return NULL; +} + +/** + * srt_input_device_get_interface_flags: + * @device: An object implementing #SrtInputDeviceInterface + * + * Return flags describing how the input device can be used. + * + * Returns: Flags describing the input device. + */ +SrtInputDeviceInterfaceFlags +srt_input_device_get_interface_flags (SrtInputDevice *device) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + + g_return_val_if_fail (iface != NULL, SRT_INPUT_DEVICE_INTERFACE_FLAGS_NONE); + + return iface->get_interface_flags (device); +} + +static SrtInputDeviceInterfaceFlags +srt_input_device_default_get_interface_flags (SrtInputDevice *device) +{ + return SRT_INPUT_DEVICE_INTERFACE_FLAGS_NONE; +} + +/** + * srt_input_device_get_type_flags: + * @device: An object implementing #SrtInputDeviceInterface + * + * Return flags describing what sort of device this is. + * If possible, these will be taken from a data source such as udev's + * input_id builtin, which will be treated as authoritative. + * + * Returns: Flags describing the input device. + */ +SrtInputDeviceTypeFlags +srt_input_device_get_type_flags (SrtInputDevice *device) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + + g_return_val_if_fail (iface != NULL, SRT_INPUT_DEVICE_TYPE_FLAGS_NONE); + + return iface->get_type_flags (device); +} + +/** + * srt_input_device_gues_type_flags_from_event_capabilities: + * @device: An object implementing #SrtInputDeviceInterface + * + * Return flags describing what sort of device this is. + * Unlike srt_input_device_get_type_flags(), this function always + * tries to guess the type flags from the event capabilities, which + * can be used in diagnostic tools to highlight devices that might be + * misidentified when only their event capabilities are available. + * + * Returns: Flags describing the input device. + */ +SrtInputDeviceTypeFlags +srt_input_device_guess_type_flags_from_event_capabilities (SrtInputDevice *device) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + const SrtEvdevCapabilities *caps; + + g_return_val_if_fail (iface != NULL, 0); + + caps = iface->peek_event_capabilities (device); + + if (caps == NULL) + return SRT_INPUT_DEVICE_TYPE_FLAGS_NONE; + + return _srt_evdev_capabilities_guess_type (caps); +} + +#define srt_input_device_default_get_type_flags \ + srt_input_device_guess_type_flags_from_event_capabilities + +/** + * srt_input_device_get_dev_node: + * @device: An object implementing #SrtInputDeviceInterface + * + * Return the path of the device node in /dev that is implemented by this + * input device, or %NULL if not known or if the device does not have a + * corresponding device node. + * + * For processes in a container, it is not guaranteed that this path will + * exist in the container's /dev. + * + * The returned string will not be freed as long as @device is referenced, + * but the device node in /dev might be deleted, or even reused for a different + * device. + * + * Returns: (nullable) (transfer none): A path in /dev that will not be + * freed as long as a reference to @device is held, or %NULL + */ +const char * +srt_input_device_get_dev_node (SrtInputDevice *device) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + + g_return_val_if_fail (iface != NULL, NULL); + + return iface->get_dev_node (device); +} + +/** + * srt_input_device_get_sys_path: + * @device: An object implementing #SrtInputDeviceInterface + * + * Return the path of the device directory in /sys that represents this + * input device. + * + * For processes in a container, it is not guaranteed that this path will + * exist in the container's /sys. + * + * The returned string will not be freed as long as @device is referenced, + * but the directory in /sys might be deleted, or even reused for a different + * device. + * + * Returns: (nullable) (transfer none): A path in /sys that will not be + * freed as long as a reference to @device is held, or %NULL + */ +const char * +srt_input_device_get_sys_path (SrtInputDevice *device) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + + g_return_val_if_fail (iface != NULL, NULL); + + return iface->get_sys_path (device); +} + +/** + * srt_input_device_get_subsystem: + * @device: An object implementing #SrtInputDeviceInterface + * + * Return the subsystem in which this device exists, typically input + * or hidraw, or %NULL if not known. + * + * Returns: (nullable) (transfer none): A short string that will not be + * freed as long as a reference to @device is held, or %NULL + */ +const char * +srt_input_device_get_subsystem (SrtInputDevice *device) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + + g_return_val_if_fail (iface != NULL, NULL); + + return iface->get_subsystem (device); +} + +/** + * srt_input_device_dup_uevent: + * @device: An object implementing #SrtInputDeviceInterface + * + * Return the `uevent` data from the kernel. + * + * Returns: (nullable) (transfer full): A multi-line string, or %NULL. + * Free with g_free(). + */ +gchar * +srt_input_device_dup_uevent (SrtInputDevice *device) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + + g_return_val_if_fail (iface != NULL, NULL); + + return iface->dup_uevent (device); +} + +static gchar * +read_uevent (const char *sys_path) +{ + g_autofree gchar *uevent_path = NULL; + g_autofree gchar *contents = NULL; + + if (sys_path == NULL) + return NULL; + + uevent_path = g_build_filename (sys_path, "uevent", NULL); + + if (g_file_get_contents (uevent_path, &contents, NULL, NULL)) + return g_steal_pointer (&contents); + + return NULL; +} + +/* + * Default implementation: read the file in /sys. The kernel provides this, + * so it should always be present, except in containers. + */ +static gchar * +srt_input_device_default_dup_uevent (SrtInputDevice *device) +{ + return read_uevent (srt_input_device_get_sys_path (device)); +} + +static gchar * +srt_input_device_default_dup_hid_uevent (SrtInputDevice *device) +{ + return read_uevent (srt_input_device_get_hid_sys_path (device)); +} + +static gchar * +srt_input_device_default_dup_input_uevent (SrtInputDevice *device) +{ + return read_uevent (srt_input_device_get_input_sys_path (device)); +} + +static gchar * +srt_input_device_default_dup_usb_device_uevent (SrtInputDevice *device) +{ + return read_uevent (srt_input_device_get_usb_device_sys_path (device)); +} + +/** + * srt_input_device_dup_udev_properties: + * @device: An object implementing #SrtInputDeviceInterface + * + * Return the udev properties of this input device, if available, in + * the same format as `environ`. g_environ_getenv() can be used to + * process them. + * + * Returns: (nullable) (transfer full): udev properties, or %NULL. + * Free with g_strfreev(). + */ +gchar ** +srt_input_device_dup_udev_properties (SrtInputDevice *device) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + + g_return_val_if_fail (iface != NULL, NULL); + + return iface->dup_udev_properties (device); +} + +/** + * srt_input_device_get_identity: + * @device: An object implementing #SrtInputDeviceInterface + * @bus_type: (out): Used to return the bus type from `<linux/input.h>`, + * usually `BUS_USB` or `BUS_BLUETOOTH` + * @vendor_id: (out): Used to return the vendor ID, namespaced by + * the @bus_type, or 0 if unavailable + * @product_id: (out): Used to return the product ID, namespaced by + * the @vendor_id, or 0 if unavailable + * @version: (out): Used to return the device version, or 0 if unavailable + * + * Attempt to identify the device. If available, return the bus type, + * the vendor ID, the product ID and/or the device version via "out" + * parameters. + * + * The source of the information is unspecified. Use + * srt_input_device_get_hid_identity(), + * srt_input_device_get_input_identity() and/or + * srt_input_device_get_usb_device_identity() if a specific source + * is desired. + * + * Returns: %TRUE if information was available + */ +gboolean +srt_input_device_get_identity (SrtInputDevice *device, + unsigned int *bus_type, + unsigned int *vendor_id, + unsigned int *product_id, + unsigned int *version) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + + g_return_val_if_fail (iface != NULL, FALSE); + + return iface->get_identity (device, bus_type, vendor_id, product_id, version); +} + +static gboolean +srt_input_device_default_get_identity (SrtInputDevice *device, + unsigned int *bus_type, + unsigned int *vendor_id, + unsigned int *product_id, + unsigned int *version) +{ + if (srt_input_device_get_input_identity (device, + bus_type, vendor_id, product_id, + version, NULL, NULL, NULL)) + return TRUE; + + if (srt_input_device_get_hid_identity (device, + bus_type, vendor_id, product_id, + NULL, NULL, NULL)) + { + if (version != NULL) + { + *version = 0; + + srt_input_device_get_usb_device_identity (device, NULL, NULL, version, + NULL, NULL, NULL); + } + + return TRUE; + } + + if (srt_input_device_get_usb_device_identity (device, + vendor_id, product_id, version, + NULL, NULL, NULL)) + { + if (bus_type != NULL) + *bus_type = BUS_USB; + + return TRUE; + } + + return FALSE; +} + +const unsigned long * +_srt_evdev_capabilities_get_bits (const SrtEvdevCapabilities *caps, + unsigned int type, + size_t *n_longs) +{ + const unsigned long *buf = NULL; + size_t n = 0; + + switch (type) + { + case 0: + buf = &caps->ev[0]; + n = G_N_ELEMENTS (caps->ev); + break; + + case EV_KEY: + buf = &caps->keys[0]; + n = G_N_ELEMENTS (caps->keys); + break; + + case EV_ABS: + buf = &caps->abs[0]; + n = G_N_ELEMENTS (caps->abs); + break; + + case EV_REL: + buf = &caps->rel[0]; + n = G_N_ELEMENTS (caps->rel); + break; + + case EV_FF: + buf = &caps->ff[0]; + n = G_N_ELEMENTS (caps->ff); + break; + + case EV_MSC: + case EV_SW: + case EV_LED: + case EV_SND: + case EV_REP: + case EV_PWR: + case EV_FF_STATUS: + case EV_MAX: + default: + break; + } + + *n_longs = n; + return buf; +} + +/* Format to print an unsigned long in hex, zero-padded to full size + * if possible. */ +#if G_MAXULONG == 0xffffffffUL +# define HEX_LONG_FORMAT "08lx" +#elif defined(__LP64__) +# define HEX_LONG_FORMAT "016lx" +#else +# warning Unsupported architecture, assuming ILP32 +# define HEX_LONG_FORMAT "08lx" +#endif + +void +_srt_evdev_capabilities_dump (const SrtEvdevCapabilities *caps) +{ + gsize i; + + for (i = 0; i < G_N_ELEMENTS (caps->ev); i++) + g_debug ("ev[%zu]: %" HEX_LONG_FORMAT, i, caps->ev[i]); + + for (i = 0; i < G_N_ELEMENTS (caps->keys); i++) + g_debug ("keys[%zu]: %" HEX_LONG_FORMAT, i, caps->keys[i]); + + for (i = 0; i < G_N_ELEMENTS (caps->abs); i++) + g_debug ("abs[%zu]: %" HEX_LONG_FORMAT, i, caps->abs[i]); + + for (i = 0; i < G_N_ELEMENTS (caps->rel); i++) + g_debug ("rel[%zu]: %" HEX_LONG_FORMAT, i, caps->rel[i]); + + for (i = 0; i < G_N_ELEMENTS (caps->props); i++) + g_debug ("props[%zu]: %" HEX_LONG_FORMAT, i, caps->props[i]); +} + +/** + * srt_input_device_get_event_capabilities: + * @device: An object implementing #SrtInputDeviceInterface + * @type: An event type from `<linux/input.h>` such as `EV_KEY`, + * or 0 to get the supported event types + * @storage: (optional) (out caller-allocates) (array length=n_longs): An + * array of @n_longs unsigned long values that will receive the bitfield, + * allocated by the caller. Bit number 0 is the least significant bit + * of storage[0] and so on up to the most significant bit of storage[0], + * which is one place less significant than the least significant bit + * of storage[1] if present. + * @n_longs: The length of @storage, or 0 if @storage is %NULL + * + * Fill a buffer with the event capabilities in the same encoding used + * for the EVIOCGBIT ioctl, or query how large that buffer would have to be. + * + * Bits in @storage above the highest known event value will be zeroed. + * + * If @storage is too small, high event values will not be represented. + * For example, if @type is `EV_KEY` and @n_longs is 1, then @storage + * will only indicate whether the first 32 or 64 key event codes are + * supported, and will not indicate anything about the level of support + * for `KEY_RIGHTALT` (event code 100). + * + * If @type is not a supported type, all of @storage will be zeroed and + * 0 will be returned. + * + * Returns: The number of unsigned long values that would have been + * required for the highest possible event of type @type, which might + * be greater than @n_longs + */ +size_t +srt_input_device_get_event_capabilities (SrtInputDevice *device, + unsigned int type, + unsigned long *storage, + size_t n_longs) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + const SrtEvdevCapabilities *caps; + const unsigned long *buf = NULL; + size_t n = 0; + + g_return_val_if_fail (iface != NULL, 0); + g_return_val_if_fail (storage != NULL || n_longs == 0, 0); + + if (n_longs != 0) + memset (storage, '\0', n_longs * sizeof (long)); + + caps = iface->peek_event_capabilities (device); + + if (caps != NULL) + buf = _srt_evdev_capabilities_get_bits (caps, type, &n); + + if (storage != NULL && buf != NULL && n_longs != 0 && n != 0) + memcpy (storage, buf, MIN (n_longs, n) * sizeof (long)); + + return n; +} + +static const SrtEvdevCapabilities * +srt_input_device_default_peek_event_capabilities (SrtInputDevice *device) +{ + return NULL; +} + +/** + * srt_input_device_has_event_type: + * @device: An object implementing #SrtInputDeviceInterface + * @type: %EV_KEY, %EV_ABS, %EV_REL, %EV_FF or another evdev event type + * + * If the @device is an evdev device implementing the given event + * type, return %TRUE. Otherwise return %FALSE. + * + * Returns: %TRUE if the object implements the given event type. + */ +gboolean +srt_input_device_has_event_type (SrtInputDevice *device, + unsigned int type) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + const SrtEvdevCapabilities *caps; + + g_return_val_if_fail (iface != NULL, 0); + + caps = iface->peek_event_capabilities (device); + + return (caps != NULL + && type <= EV_MAX + && test_bit_checked (type, caps->ev, G_N_ELEMENTS (caps->ev))); +} + +/** + * srt_input_device_get_event_types: + * @device: An object implementing #SrtInputDeviceInterface + * @storage: (optional) (out caller-allocates) (array length=n_longs): An + * array of @n_longs unsigned long values that will receive the bitfield, + * allocated by the caller. Bit number 0 is the least significant bit + * of storage[0] and so on up to the most significant bit of storage[0], + * which is one place less significant than the least significant bit + * of storage[1] if present. + * @n_longs: The length of @storage, or 0 if @storage is %NULL + * + * Fill a buffer with the supported event types in the same encoding used + * for the EVIOCGBIT ioctl, or query how large that buffer would have to be. + * This is the same as + * `srt_input_device_get_event_capabilities (device, 0, ...)`, + * except that bit numbers in @storage reflect event types, for example + * bit number 3 (`storage[0] & (1 << 3)`) represents + * event type 3 (`EV_ABS`). + * + * Returns: The number of unsigned long values that would have been + * required for the highest possible event of type @type, which might + * be greater than @n_longs + */ +size_t +srt_input_device_get_event_types (SrtInputDevice *device, + unsigned long *storage, + size_t n_longs) +{ + return srt_input_device_get_event_capabilities (device, 0, storage, n_longs); +} + +/** + * srt_input_device_has_input_property: + * @device: An object implementing #SrtInputDeviceInterface + * @input_prop: An input property such as `INPUT_PROP_POINTER` + * + * If the @device is an evdev device with the given input + * property, return %TRUE. Otherwise return %FALSE. + * + * Returns: %TRUE if the object has the given input property. + */ +gboolean +srt_input_device_has_input_property (SrtInputDevice *device, + unsigned int input_prop) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + const SrtEvdevCapabilities *caps; + + g_return_val_if_fail (iface != NULL, 0); + + caps = iface->peek_event_capabilities (device); + + return (caps != NULL + && input_prop <= INPUT_PROP_MAX + && test_bit_checked (input_prop, caps->props, G_N_ELEMENTS (caps->props))); +} + +/** + * srt_input_device_get_input_properties: + * @device: An object implementing #SrtInputDeviceInterface + * @storage: (optional) (out caller-allocates) (array length=n_longs): An + * array of @n_longs unsigned long values that will receive the bitfield, + * allocated by the caller. Bit number 0 is the least significant bit + * of storage[0] and so on up to the most significant bit of storage[0], + * which is one place less significant than the least significant bit + * of storage[1] if present. + * @n_longs: The length of @storage, or 0 if @storage is %NULL + * + * Fill a buffer with the input device properties in the same encoding used + * for the EVIOCGPROP ioctl, or query how large that buffer would have to be. + * + * Bit numbers in @storage reflect input properties, for example + * bit number 6 (`storage[0] & (1 << 6)`) represents + * input property 6 (`INPUT_PROP_ACCELEROMETER`). + * + * Returns: The number of unsigned long values that would have been + * required for the highest possible event of type @type, which might + * be greater than @n_longs + */ +size_t +srt_input_device_get_input_properties (SrtInputDevice *device, + unsigned long *storage, + size_t n_longs) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + const SrtEvdevCapabilities *caps; + + g_return_val_if_fail (iface != NULL, 0); + g_return_val_if_fail (storage != NULL || n_longs == 0, 0); + + if (n_longs != 0) + memset (storage, '\0', n_longs * sizeof (long)); + + caps = iface->peek_event_capabilities (device); + + if (caps == NULL) + return 0; + + if (storage != NULL && n_longs != 0) + memcpy (storage, caps->props, + MIN (n_longs, G_N_ELEMENTS (caps->props)) * sizeof (long)); + + return G_N_ELEMENTS (caps->props); +} + +/** + * srt_input_device_has_event_capability: + * @device: An object implementing #SrtInputDeviceInterface + * @type: `EV_KEY`, `EV_ABS`, `EV_REL`, `EV_FF` or another evdev event type + * @code: A bit appropriate for the given evdev event type; + * for example, if @type is `EV_KEY`, then @code might be `KEY_BACKSPACE` + * or `BTN_X` + * + * If the @device is an evdev device implementing the given event + * type and code, return %TRUE. Otherwise return %FALSE. + * + * This is currently only implemented for `EV_KEY`, `EV_ABS`, `EV_REL` + * and `EV_FF` (the interesting event types for game controllers), and + * will return %FALSE for more exotic event types. + * + * Returns: %TRUE if the object implements the given event type. + */ +gboolean +srt_input_device_has_event_capability (SrtInputDevice *device, + unsigned int type, + unsigned int code) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + const SrtEvdevCapabilities *caps; + const unsigned long *buf = NULL; + size_t n = 0; + + g_return_val_if_fail (iface != NULL, 0); + + caps = iface->peek_event_capabilities (device); + + if (caps == NULL) + return FALSE; + + buf = _srt_evdev_capabilities_get_bits (caps, type, &n); + return test_bit_checked (code, buf, n); +} + +/** + * srt_input_device_get_hid_sys_path: + * @device: An object implementing #SrtInputDeviceInterface + * + * Return the path of the device directory in /sys that represents this + * input device's closest ancestor that is a Human Interface Device. + * Many, but not all, input devices have a HID ancestor. If there is no + * applicable HID device, return %NULL. + * + * For processes in a container, it is not guaranteed that this path will + * exist in the container's /sys. + * + * The returned string will not be freed as long as @device is referenced, + * but the directory in /sys might be deleted, or even reused for a different + * device. + * + * Returns: (nullable) (transfer none): A path in /sys that will not be + * freed as long as a reference to @device is held, or %NULL + */ +const char * +srt_input_device_get_hid_sys_path (SrtInputDevice *device) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + + g_return_val_if_fail (iface != NULL, NULL); + + return iface->get_hid_sys_path (device); +} + +/** + * srt_input_device_dup_hid_uevent: + * @device: An object implementing #SrtInputDeviceInterface + * + * Return the uevent data structure similar to + * srt_input_device_dup_uevent(), but for the ancestor device returned + * by srt_input_device_get_hid_sys_path(). + * + * Returns: (nullable) (transfer full): A multi-line string, or %NULL. + * Free with g_free(). + */ +gchar * +srt_input_device_dup_hid_uevent (SrtInputDevice *device) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + + g_return_val_if_fail (iface != NULL, NULL); + + return iface->dup_hid_uevent (device); +} + +/** + * srt_input_device_get_hid_identity: + * @device: An object implementing #SrtInputDeviceInterface + * @bus_type: (out): Used to return the bus type from `<linux/input.h>`, + * usually `BUS_USB` or `BUS_BLUETOOTH` + * @vendor_id: (out): Used to return the vendor ID, namespaced by + * the @bus_type, or 0 if unavailable + * @product_id: (out): Used to return the product ID, namespaced by + * the @vendor_id, or 0 if unavailable + * @name: (out) (transfer none): Used to return a human-readable name + * that usually combines the vendor and product, or empty or %NULL + * if unavailable + * @phys: (out) (transfer none): Used to return how the device is + * physically attached, or empty or %NULL if unavailable + * @uniq: (out) (transfer none): Used to return the device's serial number + * or other unique identifier, or empty or %NULL if unavailable + * + * Attempt to identify the device. If available, return details via + * "out" parameters. + * + * Returns: %TRUE if this is an evdev device and information was available + */ +gboolean +srt_input_device_get_hid_identity (SrtInputDevice *device, + unsigned int *bus_type, + unsigned int *vendor_id, + unsigned int *product_id, + const char **name, + const char **phys, + const char **uniq) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + + g_return_val_if_fail (iface != NULL, FALSE); + + return iface->get_hid_identity (device, bus_type, vendor_id, product_id, + name, phys, uniq); +} + +static gboolean +srt_input_device_default_get_hid_identity (SrtInputDevice *device, + unsigned int *bus_type, + unsigned int *vendor_id, + unsigned int *product_id, + const char **name, + const char **phys, + const char **uniq) +{ + return FALSE; +} + +/** + * srt_input_device_get_usb_device_sys_path: + * @device: An object implementing #SrtInputDeviceInterface + * + * If the @device is associated with a USB device, return the path in + * /sys representing the Linux `usb_device`. If not, return %NULL. + * + * For processes in a container, it is not guaranteed that this path will + * exist in the container's /sys. + * + * The returned string will not be freed as long as @device is referenced, + * but the directory in /sys might be deleted, or even reused for a different + * device. + * + * Returns: (nullable) (transfer none): A path in /sys that will not be + * freed as long as a reference to @device is held, or %NULL + */ +const char * +srt_input_device_get_usb_device_sys_path (SrtInputDevice *device) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + + g_return_val_if_fail (iface != NULL, NULL); + + return iface->get_usb_device_sys_path (device); +} + +/** + * srt_input_device_dup_usb_device_uevent: + * @device: An object implementing #SrtInputDeviceInterface + * + * Return the uevent data structure similar to + * srt_input_device_dup_uevent(), but for the ancestor device returned + * by srt_input_device_get_usb_device_sys_path(). + * + * Returns: (nullable) (transfer full): A multi-line string, or %NULL. + * Free with g_free(). + */ +gchar * +srt_input_device_dup_usb_device_uevent (SrtInputDevice *device) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + + g_return_val_if_fail (iface != NULL, NULL); + + return iface->dup_usb_device_uevent (device); +} + +/** + * srt_input_device_get_usb_device_identity: + * @device: An object implementing #SrtInputDeviceInterface + * @bus_type: (out): Used to return the bus type from `<linux/input.h>`, + * usually `BUS_USB` or `BUS_BLUETOOTH` + * @vendor_id: (out): Used to return the vendor ID, namespaced by + * the @bus_type, or 0 if unavailable + * @product_id: (out): Used to return the product ID, namespaced by + * the @vendor_id, or 0 if unavailable + * @device_version: (out): Used to return the product version, or 0 if unavailable + * @manufacturer: (out) (transfer none): Used to return the human-readable name + * of the manufacturer, or empty or %NULL if unavailable + * @product: (out) (transfer none): Used to return the human-readable name + * of the product, or empty or %NULL if unavailable + * @serial: (out) (transfer none): Used to return the device's serial number + * or other unique identifier, or empty or %NULL if unavailable + * + * Attempt to identify the device. If available, return details via + * "out" parameters. + * + * Returns: %TRUE if this is a USB device and information was available + */ +gboolean +srt_input_device_get_usb_device_identity (SrtInputDevice *device, + unsigned int *vendor_id, + unsigned int *product_id, + unsigned int *device_version, + const char **manufacturer, + const char **product, + const char **serial) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + + g_return_val_if_fail (iface != NULL, FALSE); + + return iface->get_usb_device_identity (device, + vendor_id, product_id, device_version, + manufacturer, product, serial); +} + +static gboolean +srt_input_device_default_get_usb_device_identity (SrtInputDevice *device, + unsigned int *vendor_id, + unsigned int *product_id, + unsigned int *device_version, + const char **manufacturer, + const char **product, + const char **serial) +{ + return FALSE; +} + +/** + * srt_input_device_get_input_sys_path: + * @device: An object implementing #SrtInputDeviceInterface + * + * If the @device has an ancestor device that advertises evdev input + * capabilities, return the path in /sys for that device. Otherwise + * return %NULL. + * + * For processes in a container, it is not guaranteed that this path will + * exist in the container's /sys. + * + * The returned string will not be freed as long as @device is referenced, + * but the directory in /sys might be deleted, or even reused for a different + * device. + * + * Returns: (nullable) (transfer none): A path in /sys that will not be + * freed as long as a reference to @device is held, or %NULL + */ +const char * +srt_input_device_get_input_sys_path (SrtInputDevice *device) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + + g_return_val_if_fail (iface != NULL, NULL); + + return iface->get_input_sys_path (device); +} + +/** + * srt_input_device_dup_input_uevent: + * @device: An object implementing #SrtInputDeviceInterface + * + * Return the uevent data structure similar to + * srt_input_device_dup_uevent(), but for the ancestor device returned + * by srt_input_device_get_input_sys_path(). + * + * Returns: (nullable) (transfer full): A multi-line string, or %NULL. + * Free with g_free(). + */ +gchar * +srt_input_device_dup_input_uevent (SrtInputDevice *device) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + + g_return_val_if_fail (iface != NULL, NULL); + + return iface->dup_input_uevent (device); +} + +/** + * srt_input_device_get_input_identity: + * @device: An object implementing #SrtInputDeviceInterface + * @bus_type: (out): Used to return the bus type from `<linux/input.h>`, + * usually `BUS_USB` or `BUS_BLUETOOTH` + * @vendor_id: (out): Used to return the vendor ID, namespaced by + * the @bus_type, or 0 if unavailable + * @product_id: (out): Used to return the product ID, namespaced by + * the @vendor_id, or 0 if unavailable + * @version: (out): Used to return the product version, or 0 if unavailable + * @name: (out) (transfer none): Used to return a human-readable name + * that usually combines the vendor and product, or empty or %NULL + * if unavailable + * @phys: (out) (transfer none): Used to return how the device is + * physically attached, or empty or %NULL if unavailable + * @uniq: (out) (transfer none): Used to return the device's serial number + * or other unique identifier, or empty or %NULL if unavailable + * + * Attempt to identify the device. If available, return details via + * "out" parameters. + * + * Returns: %TRUE if this is an evdev device and information was available + */ +gboolean +srt_input_device_get_input_identity (SrtInputDevice *device, + unsigned int *bus_type, + unsigned int *vendor_id, + unsigned int *product_id, + unsigned int *version, + const char **name, + const char **phys, + const char **uniq) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + + g_return_val_if_fail (iface != NULL, FALSE); + + return iface->get_input_identity (device, + bus_type, vendor_id, product_id, version, + name, phys, uniq); +} + +static gboolean +srt_input_device_default_get_input_identity (SrtInputDevice *device, + unsigned int *bus_type, + unsigned int *vendor_id, + unsigned int *product_id, + unsigned int *version, + const char **name, + const char **phys, + const char **uniq) +{ + return FALSE; +} + +/** + * srt_input_device_open: + * @device: An object implementing #SrtInputDeviceInterface + * @flags: Flags affecting how the device is opened + * @error: Used to report the error on failure + * + * @flags must include one of: O_RDONLY, O_WRONLY, or O_RDWR. + * + * @flags may include zero or more of: O_NONBLOCK. + * + * The file descriptor is always opened with O_CLOEXEC and O_NOCTTY. + * Explicitly specifying those flags is not allowed. + * + * Returns: A file descriptor owned by the caller, which must be closed + * with close(2) after use, or a negative number on error. + */ +int +srt_input_device_open (SrtInputDevice *device, + int flags, + GError **error) +{ + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + + g_return_val_if_fail (iface != NULL, -1); + g_return_val_if_fail (error == NULL || *error == NULL, -1); + + if (!_srt_input_device_check_open_flags (flags, error)) + return -1; + + return iface->open_device (device, flags, error); +} + +static int +srt_input_device_default_open_device (SrtInputDevice *device, + int flags, + GError **error) +{ + const char *devnode = NULL; + int fd = -1; + + if (!_srt_input_device_check_open_flags (flags, error)) + return -1; + + devnode = srt_input_device_get_dev_node (device); + + if (devnode == NULL) + { + glnx_throw (error, "Device has no device node"); + return -1; + } + + fd = open (devnode, flags | _SRT_INPUT_DEVICE_ALWAYS_OPEN_FLAGS); + + if (fd < 0) + { + glnx_throw_errno_prefix (error, + "Unable to open device node \"%s\"", + devnode); + return -1; + } + + return fd; +} + +static void +srt_input_device_default_init (SrtInputDeviceInterface *iface) +{ +#define IMPLEMENT2(x, y) iface->x = srt_input_device_default_ ## y +#define IMPLEMENT(x) IMPLEMENT2 (x, x) + + IMPLEMENT (get_type_flags); + IMPLEMENT (get_interface_flags); + IMPLEMENT2 (get_dev_node, get_null_string); + IMPLEMENT2 (get_sys_path, get_null_string); + IMPLEMENT2 (get_subsystem, get_null_string); + IMPLEMENT (get_identity); + IMPLEMENT (peek_event_capabilities); + + IMPLEMENT2 (get_hid_sys_path, get_null_string); + IMPLEMENT (get_hid_identity); + IMPLEMENT2 (get_input_sys_path, get_null_string); + IMPLEMENT (get_input_identity); + IMPLEMENT2 (get_usb_device_sys_path, get_null_string); + IMPLEMENT (get_usb_device_identity); + + IMPLEMENT2 (dup_udev_properties, get_null_strv); + + IMPLEMENT (dup_uevent); + IMPLEMENT (dup_hid_uevent); + IMPLEMENT (dup_input_uevent); + IMPLEMENT (dup_usb_device_uevent); + + IMPLEMENT (open_device); + +#undef IMPLEMENT +#undef IMPLEMENT2 +} + +/** + * SrtInputDeviceMonitor: + * + * An object to enumerate and monitor input devices. + * + * Take additional references with g_object_ref(), release references + * with g_object_unref(). + * + * New input devices are signalled by the #SrtInputDeviceMonitor::added signal. + * An input device being removed is signalled by the + * #SrtInputDeviceMonitor::removed signal. The end of the initial device + * enumeration is signalled by the #SrtInputDeviceMonitor::all-for-now + * signal. + * + * All of these signals are emitted in whatever #GMainContext was + * the thread-default main context at the time the #SrtInputDeviceMonitor + * was created (in other words, their callbacks are called in the thread + * that is iterating the chosen #GMainContext). If you are using threads + * other than the main thread, use g_main_context_push_thread_default() + * to ensure that an appropriate main context will be used. + */ +/** + * SrtInputDeviceMonitorInterface: + * + * The interface implemented by each SrtInputDeviceMonitor. + */ +G_DEFINE_INTERFACE (SrtInputDeviceMonitor, srt_input_device_monitor, G_TYPE_OBJECT) + +static guint monitor_signal_added = 0; +static guint monitor_signal_removed = 0; +static guint monitor_signal_all_for_now = 0; + +static const SrtInputDeviceMonitorFlags mode_flags = (SRT_INPUT_DEVICE_MONITOR_FLAGS_UDEV + | SRT_INPUT_DEVICE_MONITOR_FLAGS_DIRECT); + +/* + * @monitor_out: (out) (not optional): + * @error: (inout) (not optional): + */ +static gboolean +try_udev (SrtInputDeviceMonitorFlags flags, + SrtInputDeviceMonitor **monitor_out, + GError **error) +{ + /* Don't try it again if we already failed, to avoid another warning */ + if (*error != NULL) + return FALSE; + + *monitor_out = srt_udev_input_device_monitor_new (flags, error); + + if (*monitor_out != NULL) + return TRUE; + + /* We usually expect this to succeed, so log a warning if it fails */ + g_warning ("Unable to initialize udev input device monitor: %s", + (*error)->message); + return FALSE; +} + +/** + * srt_input_device_monitor_new: + * @flags: Flags affecting the behaviour of the input device monitor. + * + * Return an object that can be used to enumerate and monitor input + * devices. + * + * Take additional references with g_object_ref(), release references + * with g_object_unref(). + * + * Returns: the input device monitor + */ +SrtInputDeviceMonitor * +srt_input_device_monitor_new (SrtInputDeviceMonitorFlags flags) +{ + SrtInputDeviceMonitor *udev = NULL; + g_autoptr(GError) udev_error = NULL; + + if (__builtin_popcount (flags & mode_flags) > 1) + g_warning ("Requesting more than one of UDEV and DIRECT " + "monitoring has undefined results: 0x%x", flags); + + /* First see whether the caller expressed a preference */ + + if (flags & SRT_INPUT_DEVICE_MONITOR_FLAGS_UDEV) + { + if (try_udev (flags, &udev, &udev_error)) + return udev; + } + + if (flags & SRT_INPUT_DEVICE_MONITOR_FLAGS_DIRECT) + return srt_direct_input_device_monitor_new (flags); + + /* Prefer a direct monitor if we're in a container */ + if (g_file_test ("/.flatpak-info", G_FILE_TEST_EXISTS) + || g_file_test ("/run/pressure-vessel", G_FILE_TEST_EXISTS) + || g_file_test ("/run/host", G_FILE_TEST_EXISTS)) + return srt_direct_input_device_monitor_new (flags); + + if (try_udev (flags, &udev, &udev_error)) + return udev; + + /* Fall back to direct monitoring if we don't have libudev */ + return srt_direct_input_device_monitor_new (flags); +} + +static void +srt_input_device_monitor_default_do_nothing (SrtInputDeviceMonitor *monitor) +{ +} + +/** + * srt_input_device_monitor_get_flags: + * @device: An object implementing #SrtInputDeviceMonitorInterface + * + * Return flags describing the input device monitor. + * + * Returns: Flags describing the input device monitor. + */ +SrtInputDeviceMonitorFlags +srt_input_device_monitor_get_flags (SrtInputDeviceMonitor *monitor) +{ + SrtInputDeviceMonitorFlags flags = SRT_INPUT_DEVICE_MONITOR_FLAGS_NONE; + SrtInputDeviceMonitorInterface *iface = SRT_INPUT_DEVICE_MONITOR_GET_INTERFACE (monitor); + + g_return_val_if_fail (iface != NULL, flags); + + g_object_get (monitor, + "flags", &flags, + NULL); + return flags; +} + +/** + * srt_input_device_monitor_is_active: + * @monitor: The input device monitor + * + * Return %TRUE if srt_input_device_monitor_start() has been called + * successfully, and srt_input_device_monitor_stop() has not subsequently + * been called. + * + * Returns: %TRUE if active + */ +gboolean +srt_input_device_monitor_is_active (SrtInputDeviceMonitor *monitor) +{ + gboolean ret = FALSE; + SrtInputDeviceMonitorInterface *iface = SRT_INPUT_DEVICE_MONITOR_GET_INTERFACE (monitor); + + g_return_val_if_fail (iface != NULL, FALSE); + + g_object_get (monitor, + "is-active", &ret, + NULL); + return ret; +} + +/** + * srt_input_device_monitor_request_raw_hid: + * @monitor: The input device monitor + * + * Tell the input device monitor to return all raw HID devices. + * + * If neither this method nor srt_input_device_monitor_request_evdev() is + * called, no devices will be found. + * + * This function cannot be called if srt_input_device_monitor_start() + * or srt_input_device_monitor_stop() have already been called. + */ +void +srt_input_device_monitor_request_raw_hid (SrtInputDeviceMonitor *monitor) +{ + SrtInputDeviceMonitorInterface *iface = SRT_INPUT_DEVICE_MONITOR_GET_INTERFACE (monitor); + + g_return_if_fail (iface != NULL); + + iface->request_raw_hid (monitor); +} + +/** + * srt_input_device_monitor_request_evdev: + * @monitor: The input device monitor + * + * Tell the input device monitor to return all evdev (event) devices. + * + * If neither this method nor srt_input_device_monitor_request_raw_hid() is + * called, no devices will be found. + * + * This function cannot be called if srt_input_device_monitor_start() + * or srt_input_device_monitor_stop() have already been called. + */ +void +srt_input_device_monitor_request_evdev (SrtInputDeviceMonitor *monitor) +{ + SrtInputDeviceMonitorInterface *iface = SRT_INPUT_DEVICE_MONITOR_GET_INTERFACE (monitor); + + g_return_if_fail (iface != NULL); + + iface->request_evdev (monitor); +} + +/** + * srt_input_device_monitor_start: + * @monitor: The input device monitor + * + * Start to watch for input devices. + * + * The device monitor will emit signals in the thread-default main + * context of the thread where this function was called + * (see g_main_context_push_thread_default() for details). + * + * The SrtInputDeviceMonitor::added signal will be emitted when + * a matching input device is detected. + * If the monitor is watching for both %SRT_INPUT_DEVICE_FLAGS_EVENT + * and %SRT_INPUT_DEVICE_FLAGS_RAW_HID devices, one signal will be + * emitted for each one. + * + * The SrtInputDeviceMonitor::removed signal will be emitted when + * a matching input device is removed. + * + * This function cannot be called if srt_input_device_monitor_start() + * or srt_input_device_monitor_stop() have already been called. + */ +gboolean +srt_input_device_monitor_start (SrtInputDeviceMonitor *monitor, + GError **error) +{ + SrtInputDeviceMonitorInterface *iface = SRT_INPUT_DEVICE_MONITOR_GET_INTERFACE (monitor); + + g_return_val_if_fail (iface != NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + return iface->start (monitor, error); +} + +static gboolean +srt_input_device_monitor_default_start (SrtInputDeviceMonitor *monitor, + GError **error) +{ + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Not implemented"); + return FALSE; +} + +/** + * srt_input_device_monitor_stop: + * @monitor: The input device monitor + * + * Stop the input device monitor. It still exists in memory until all + * references are released, but will stop signalling new events. + */ +void +srt_input_device_monitor_stop (SrtInputDeviceMonitor *monitor) +{ + SrtInputDeviceMonitorInterface *iface = SRT_INPUT_DEVICE_MONITOR_GET_INTERFACE (monitor); + + g_return_if_fail (iface != NULL); + + iface->stop (monitor); +} + +static void +srt_input_device_monitor_default_init (SrtInputDeviceMonitorInterface *iface) +{ +#define IMPLEMENT2(x, y) iface->x = srt_input_device_monitor_default_ ## y +#define IMPLEMENT(x) IMPLEMENT2 (x, x) + IMPLEMENT2 (request_raw_hid, do_nothing); + IMPLEMENT2 (request_evdev, do_nothing); + IMPLEMENT (start); + IMPLEMENT2 (stop, do_nothing); +#undef IMPLEMENT + + /** + * SrtInputDeviceMonitor::added: + * @monitor: The input device monitor + * @device: The device + * + * Emitted when an input device is added. + */ + monitor_signal_added = + g_signal_new ("added", + SRT_TYPE_INPUT_DEVICE_MONITOR, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (SrtInputDeviceMonitorInterface, added), + NULL, NULL, NULL, + G_TYPE_NONE, 1, + SRT_TYPE_INPUT_DEVICE); + + /** + * SrtInputDeviceMonitor::removed: + * @monitor: The input device monitor + * @device: The device + * + * Emitted when an input device is removed. + */ + monitor_signal_removed = + g_signal_new ("removed", + SRT_TYPE_INPUT_DEVICE_MONITOR, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (SrtInputDeviceMonitorInterface, removed), + NULL, NULL, NULL, + G_TYPE_NONE, 1, + SRT_TYPE_INPUT_DEVICE); + + /** + * SrtInputDeviceMonitor::all-for-now: + * @monitor: The input device monitor + * + * Emitted when the initial batch of input devices has been discovered. + */ + monitor_signal_all_for_now = + g_signal_new ("all-for-now", + SRT_TYPE_INPUT_DEVICE_MONITOR, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (SrtInputDeviceMonitorInterface, all_for_now), + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + /** + * SrtInputDeviceMonitor::flags: + * + * Flags affecting the input device monitor. + */ + g_object_interface_install_property (iface, + g_param_spec_flags ("flags", + "Flags", + "Flags affecting the input device monitor", + SRT_TYPE_INPUT_DEVICE_MONITOR_FLAGS, + SRT_INPUT_DEVICE_MONITOR_FLAGS_NONE, + G_PARAM_READWRITE + | G_PARAM_CONSTRUCT_ONLY + | G_PARAM_STATIC_STRINGS)); + + /** + * SrtInputDeviceMonitor::is-active: + * + * The value of srt_input_device_monitor_is_active(). + */ + g_object_interface_install_property (iface, + g_param_spec_boolean ("is-active", + "Is active", + "TRUE if started and not stopped", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); +} + +void +_srt_input_device_monitor_emit_added (SrtInputDeviceMonitor *monitor, + SrtInputDevice *device) +{ + g_debug ("Added %p", device); + g_signal_emit (monitor, monitor_signal_added, 0, device); +} + +void +_srt_input_device_monitor_emit_removed (SrtInputDeviceMonitor *monitor, + SrtInputDevice *device) +{ + g_debug ("Removed %p", device); + g_signal_emit (monitor, monitor_signal_removed, 0, device); +} + +void +_srt_input_device_monitor_emit_all_for_now (SrtInputDeviceMonitor *monitor) +{ + g_debug ("All for now"); + g_signal_emit (monitor, monitor_signal_all_for_now, 0); +} + +static gboolean +_srt_get_caps_from_evdev (int fd, + unsigned int type, + unsigned long *bitmask, + size_t bitmask_len_longs) +{ + size_t bitmask_len_bytes = bitmask_len_longs * sizeof (*bitmask); + + memset (bitmask, 0, bitmask_len_bytes); + + if (ioctl (fd, EVIOCGBIT (type, bitmask_len_bytes), bitmask) < 0) + return FALSE; + + return TRUE; +} + +gboolean +_srt_evdev_capabilities_set_from_evdev (SrtEvdevCapabilities *caps, + int fd) +{ + if (_srt_get_caps_from_evdev (fd, 0, caps->ev, G_N_ELEMENTS (caps->ev))) + { + _srt_get_caps_from_evdev (fd, EV_KEY, caps->keys, G_N_ELEMENTS (caps->keys)); + _srt_get_caps_from_evdev (fd, EV_ABS, caps->abs, G_N_ELEMENTS (caps->abs)); + _srt_get_caps_from_evdev (fd, EV_REL, caps->rel, G_N_ELEMENTS (caps->rel)); + _srt_get_caps_from_evdev (fd, EV_FF, caps->ff, G_N_ELEMENTS (caps->ff)); + ioctl (fd, EVIOCGPROP (sizeof (caps->props)), caps->props); + return TRUE; + } + + memset (caps, 0, sizeof (*caps)); + return FALSE; +} + +gboolean +_srt_get_identity_from_evdev (int fd, + guint32 *bus_type, + guint32 *vendor, + guint32 *product, + guint32 *version, + gchar **name, + gchar **phys, + gchar **uniq) +{ + struct input_id iid = {}; + char buf[256] = { '\0' }; + + if (bus_type != NULL) + *bus_type = 0; + + if (vendor != NULL) + *vendor = 0; + + if (product != NULL) + *product = 0; + + if (ioctl (fd, EVIOCGID, &iid) < 0) + { + g_debug ("EVIOCGID: %s", g_strerror (errno)); + return FALSE; + } + + if (name != NULL + && ioctl (fd, EVIOCGNAME (sizeof (buf) - 1), buf) == 0) + *name = g_strdup (buf); + + memset (buf, '\0', sizeof (buf)); + + if (phys != NULL + && ioctl (fd, EVIOCGPHYS (sizeof (buf) - 1), buf) == 0) + *phys = g_strdup (buf); + + memset (buf, '\0', sizeof (buf)); + + if (uniq != NULL + && ioctl (fd, EVIOCGUNIQ (sizeof (buf) - 1), buf) == 0) + *uniq = g_strdup (buf); + + if (bus_type != NULL) + *bus_type = iid.bustype; + + if (vendor != NULL) + *vendor = iid.vendor; + + if (product != NULL) + *product = iid.product; + + if (version != NULL) + *version = iid.version; + + return TRUE; +} + +gboolean +_srt_get_identity_from_raw_hid (int fd, + guint32 *bus_type, + guint32 *vendor, + guint32 *product, + gchar **name, + gchar **phys, + gchar **uniq) +{ + char buf[256] = { '\0' }; + struct hidraw_devinfo devinfo = {}; + + if (bus_type != NULL) + *bus_type = 0; + + if (vendor != NULL) + *vendor = 0; + + if (product != NULL) + *product = 0; + + if (ioctl (fd, HIDIOCGRAWINFO, &devinfo) < 0) + { + g_debug ("HIDIOCGRAWINFO: %s", g_strerror (errno)); + return FALSE; + } + + if (name != NULL + && ioctl (fd, HIDIOCGRAWNAME (sizeof (buf) - 1), buf) == 0) + *name = g_strdup (buf); + + memset (buf, '\0', sizeof (buf)); + + if (phys != NULL + && ioctl (fd, HIDIOCGRAWPHYS (sizeof (buf) - 1), buf) == 0) + *phys = g_strdup (buf); + + memset (buf, '\0', sizeof (buf)); + + if (uniq != NULL + && ioctl (fd, HIDIOCGRAWUNIQ (sizeof (buf) - 1), buf) == 0) + *uniq = g_strdup (buf); + + if (bus_type != NULL) + *bus_type = devinfo.bustype; + + if (vendor != NULL) + *vendor = devinfo.vendor; + + if (product != NULL) + *product = devinfo.product; + + return TRUE; +} + +/* + * Returns: The field @key from @text, or %NULL if not found. + */ +gchar * +_srt_input_device_uevent_field (const char *text, + const char *key) +{ + const char *line = text; + size_t key_len = strlen (key); + + for (line = text; + line != NULL && *line != '\0'; + line = strchr (line, '\n')) + { + while (*line == '\n') + line++; + + /* If the line starts with KEY=, we're looking at the right field */ + if (strncmp (line, key, key_len) == 0 + && line[key_len] == '=') + { + const char *value = line + key_len + 1; + const char *after = strchrnul (line, '\n'); + + g_assert (after != NULL); + g_assert (after >= value); + g_assert (*after == '\n' || *after == '\0'); + return g_strndup (value, after - value); + } + } + + return NULL; +} + +/* + * Returns: %TRUE if @text contains KEY=WANT_VALUE, preceded by + * beginning-of-string or a newline, and followed by a newline + * or end-of-string. + */ +gboolean +_srt_input_device_uevent_field_equals (const char *text, + const char *key, + const char *want_value) +{ + const char *line = text; + size_t key_len = strlen (key); + size_t val_len = strlen (want_value); + + for (line = text; + line != NULL && *line != '\0'; + line = strchr (line, '\n')) + { + while (*line == '\n') + line++; + + /* If the line starts with KEY=, we're looking at the right field */ + if (strncmp (line, key, key_len) == 0 + && line[key_len] == '=') + { + const char *real_value = line + key_len + 1; + char after; + + if (strncmp (real_value, want_value, val_len) != 0) + return FALSE; + + after = real_value[val_len]; + + return (after == '\0' || after == '\n'); + } + } + + return FALSE; +} + +gboolean +_srt_get_identity_from_hid_uevent (const char *text, + guint32 *bus_type, + guint32 *vendor_id, + guint32 *product_id, + gchar **name, + gchar **phys, + gchar **uniq) +{ + g_autofree gchar *id = _srt_input_device_uevent_field (text, "HID_ID"); + struct + { + guint bus_type; + guint vendor_id; + guint product_id; + } tmp; + int used; + + if (id == NULL) + return FALSE; + + if (sscanf (id, "%x:%x:%x%n", + &tmp.bus_type, &tmp.vendor_id, &tmp.product_id, &used) != 3 + || ((size_t) used) != strlen (id)) + return FALSE; + + if (name != NULL) + *name = _srt_input_device_uevent_field (text, "HID_NAME"); + + if (phys != NULL) + *phys = _srt_input_device_uevent_field (text, "HID_PHYS"); + + if (uniq != NULL) + *uniq = _srt_input_device_uevent_field (text, "HID_UNIQ"); + + if (bus_type != NULL) + *bus_type = tmp.bus_type; + + if (vendor_id != NULL) + *vendor_id = tmp.vendor_id; + + if (product_id != NULL) + *product_id = tmp.product_id; + + return TRUE; +} + +/* _srt_evdev_capabilities_guess_type relies on all the joystick axes + * being in the first unsigned long. */ +G_STATIC_ASSERT (ABS_HAT3Y < BITS_PER_LONG); +#define JOYSTICK_ABS_AXES \ + ((1 << ABS_X) | (1 << ABS_Y) \ + | (1 << ABS_RX) | (1 << ABS_RY) \ + | (1 << ABS_THROTTLE) | (1 << ABS_RUDDER) \ + | (1 << ABS_WHEEL) | (1 << ABS_GAS) | (1 << ABS_BRAKE) \ + | (1 << ABS_HAT0X) | (1 << ABS_HAT0Y) \ + | (1 << ABS_HAT1X) | (1 << ABS_HAT1Y) \ + | (1 << ABS_HAT2X) | (1 << ABS_HAT2Y) \ + | (1 << ABS_HAT3X) | (1 << ABS_HAT3Y)) + +static const unsigned int first_mouse_button = BTN_MOUSE; +static const unsigned int last_mouse_button = BTN_JOYSTICK - 1; + +static const unsigned int first_joystick_button = BTN_JOYSTICK; +static const unsigned int last_joystick_button = BTN_GAMEPAD - 1; + +static const unsigned int first_gamepad_button = BTN_GAMEPAD; +static const unsigned int last_gamepad_button = BTN_DIGI - 1; + +static const unsigned int first_dpad_button = BTN_DPAD_UP; +static const unsigned int last_dpad_button = BTN_DPAD_RIGHT; + +static const unsigned int first_extra_joystick_button = BTN_TRIGGER_HAPPY; +static const unsigned int last_extra_joystick_button = BTN_TRIGGER_HAPPY40; + +/* + * Guess the type of device from the input capabilities. + * + * We cannot copy what udev does for licensing reasons (it's GPL-licensed), + * so this is a reimplementation, variously taking inspiration from: + * + * - kernel documentation (https://www.kernel.org/doc/Documentation/input/) + * - libmanette + * - SDL + * - Wine dlls/winebus.sys + * - udev + */ +SrtInputDeviceTypeFlags +_srt_evdev_capabilities_guess_type (const SrtEvdevCapabilities *caps) +{ + SrtInputDeviceTypeFlags flags = SRT_INPUT_DEVICE_TYPE_FLAGS_NONE; + unsigned int i; + gboolean has_joystick_axes = FALSE; + gboolean has_joystick_buttons = FALSE; + + /* Some properties let us be fairly sure about a device */ + if (test_bit (INPUT_PROP_ACCELEROMETER, caps->props)) + { + g_debug ("INPUT_PROP_ACCELEROMETER => is accelerometer"); + flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_ACCELEROMETER; + } + + if (test_bit (INPUT_PROP_POINTING_STICK, caps->props)) + { + g_debug ("INPUT_PROP_POINTING_STICK => is pointing stick"); + flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_POINTING_STICK; + } + + if (test_bit (INPUT_PROP_BUTTONPAD, caps->props) + || test_bit (INPUT_PROP_TOPBUTTONPAD, caps->props)) + { + g_debug ("INPUT_PROP_[TOP]BUTTONPAD => is touchpad"); + flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHPAD; + } + + /* Devices with a stylus or pen are assumed to be graphics tablets */ + if (test_bit (BTN_STYLUS, caps->keys) + || test_bit (BTN_TOOL_PEN, caps->keys)) + { + g_debug ("Stylus or pen => is tablet"); + flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_TABLET; + } + + /* Devices that accept a finger touch are assumed to be touchpads or + * touchscreens. + * + * In Steam we mostly only care about these as a way to + * reject non-joysticks, so we're not very precise here yet. + * + * SDL assumes that TOUCH means a touchscreen and FINGER + * means a touchpad. */ + if (flags == SRT_INPUT_DEVICE_TYPE_FLAGS_NONE + && (test_bit (BTN_TOOL_FINGER, caps->keys) + || test_bit (BTN_TOUCH, caps->keys) + || test_bit (INPUT_PROP_SEMI_MT, caps->props))) + { + g_debug ("Finger or touch or semi-MT => is touchpad or touchscreen"); + + if (test_bit (INPUT_PROP_POINTER, caps->props)) + flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHPAD; + else + flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHSCREEN; + } + + /* Devices with mouse buttons are ... probably mice? */ + if (flags == SRT_INPUT_DEVICE_TYPE_FLAGS_NONE) + { + for (i = first_mouse_button; i <= last_mouse_button; i++) + { + if (test_bit (i, caps->keys)) + { + g_debug ("Mouse button => mouse"); + flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_MOUSE; + } + } + } + + if (flags == SRT_INPUT_DEVICE_TYPE_FLAGS_NONE) + { + for (i = ABS_X; i < ABS_Z; i++) + { + if (!test_bit (i, caps->abs)) + break; + } + + /* If it has 3 axes and no buttons it's probably an accelerometer. */ + if (i == ABS_Z && !test_bit (EV_KEY, caps->ev)) + { + g_debug ("3 left axes and no buttons => accelerometer"); + flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_ACCELEROMETER; + } + + /* Same for RX..RZ (e.g. Wiimote) */ + for (i = ABS_RX; i < ABS_RZ; i++) + { + if (!test_bit (i, caps->abs)) + break; + } + + if (i == ABS_RZ && !test_bit (EV_KEY, caps->ev)) + { + g_debug ("3 right axes and no buttons => accelerometer"); + flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_ACCELEROMETER; + } + } + + /* Bits 1 to 31 are ESC, numbers and Q to D, which SDL and udev both + * consider to be enough to count as a fully-functioned keyboard. */ + if ((caps->keys[0] & 0xfffffffe) == 0xfffffffe) + { + g_debug ("First few keys => keyboard"); + flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_KEYBOARD; + } + + /* If we have *any* keys, consider it to be something a bit + * keyboard-like. Bits 0 to 63 are all keyboard keys. + * Make sure we stop before reaching KEY_UP which is sometimes + * used on game controller mappings, e.g. for the Wiimote. */ + for (i = 0; i < (64 / BITS_PER_LONG); i++) + { + if (caps->keys[i] != 0) + flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS; + } + + if (caps->abs[0] & JOYSTICK_ABS_AXES) + has_joystick_axes = TRUE; + + /* Flight stick buttons */ + for (i = first_joystick_button; i <= last_joystick_button; i++) + { + if (test_bit (i, caps->keys)) + has_joystick_buttons = TRUE; + } + + /* Gamepad buttons (Xbox, PS3, etc.) */ + for (i = first_gamepad_button; i <= last_gamepad_button; i++) + { + if (test_bit (i, caps->keys)) + has_joystick_buttons = TRUE; + } + + /* Gamepad digital dpad */ + for (i = first_dpad_button; i <= last_dpad_button; i++) + { + if (test_bit (i, caps->keys)) + has_joystick_buttons = TRUE; + } + + /* Steering wheel gear-change buttons */ + for (i = BTN_GEAR_DOWN; i <= BTN_GEAR_UP; i++) + { + if (test_bit (i, caps->keys)) + has_joystick_buttons = TRUE; + } + + /* Reserved space for extra game-controller buttons, e.g. on Corsair + * gaming keyboards */ + for (i = first_extra_joystick_button; i <= last_extra_joystick_button; i++) + { + if (test_bit (i, caps->keys)) + has_joystick_buttons = TRUE; + } + + if (test_bit (last_mouse_button, caps->keys)) + { + /* Mice with a very large number of buttons can apparently + * overflow into the joystick-button space, but they're still not + * joysticks. */ + has_joystick_buttons = FALSE; + } + + /* TODO: Do we want to consider BTN_0 up to BTN_9 to be joystick buttons? + * libmanette and SDL look for BTN_1, udev does not. + * + * They're used by some game controllers, like BTN_1 and BTN_2 for the + * Wiimote, BTN_1..BTN_9 for the SpaceTec SpaceBall and BTN_0..BTN_3 + * for Playstation dance pads, but they're also used by + * non-game-controllers like Logitech mice. For now we entirely ignore + * these buttons: they are not evidence that it's a joystick, but + * neither are they evidence that it *isn't* a joystick. */ + + /* We consider it to be a joystick if there is some evidence that it is, + * and no evidence that it's something else. + * + * Unlike SDL, we accept devices with only axes and no buttons as a + * possible joystick, unless they have X/Y/Z axes in which case we + * assume they're accelerometers. */ + if ((has_joystick_buttons || has_joystick_axes) + && (flags == SRT_INPUT_DEVICE_TYPE_FLAGS_NONE)) + { + g_debug ("Looks like a joystick"); + flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK; + } + + /* If we have *any* keys below BTN_MISC, consider it to be something + * a bit keyboard-like, but don't rule out *also* being considered + * to be a joystick (again for e.g. the Wiimote). */ + for (i = 0; i < (BTN_MISC / BITS_PER_LONG); i++) + { + if (caps->keys[i] != 0) + flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS; + } + + /* Also non-exclusive: don't rule out a device being a joystick and + * having a switch */ + if (test_bit (EV_SW, caps->ev)) + flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_SWITCH; + + return flags; +} diff --git a/steam-runtime-tools/input-device.h b/steam-runtime-tools/input-device.h new file mode 100644 index 0000000000000000000000000000000000000000..c2d1ca79644abf9fbcfba29c7b1a932d8617e907 --- /dev/null +++ b/steam-runtime-tools/input-device.h @@ -0,0 +1,258 @@ +/* + * Input device internals, with parts based on SDL code. + * + * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org> + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: Zlib + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#pragma once + +#if !defined(_SRT_IN_SINGLE_HEADER) && !defined(_SRT_COMPILATION) +#error "Do not include directly, use <steam-runtime-tools/steam-runtime-tools.h>" +#endif + +#include <gio/gio.h> + +#include "steam-runtime-tools/macros.h" + +/** + * SrtInputDeviceTypeFlags: + * @SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK: A joystick, gamepad, steering wheel + * or other game controller (udev ID_INPUT_JOYSTICK) + * @SRT_INPUT_DEVICE_TYPE_FLAGS_ACCELEROMETER: An accelerometer, either motion + * controls in a game controller such as Playstation 3 "sixaxis" controllers + * or in the computer itself (udev ID_INPUT_ACCELEROMETER). + * Note that unlike SDL, SrtInputDeviceMonitor always considers + * accelerometers to be their own device type distinct from joysticks; + * there is no equivalent of SDL_HINT_ACCELEROMETER_AS_JOYSTICK. + * @SRT_INPUT_DEVICE_TYPE_FLAGS_KEYBOARD: Keyboards with a somewhat full set + * of keys (udev ID_INPUT_KEYBOARD) + * @SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS: Any device with keyboard keys, however + * incomplete (udev ID_INPUT_KEY) + * @SRT_INPUT_DEVICE_TYPE_FLAGS_MOUSE: A mouse or mouse-like pointer + * controller (udev ID_INPUT_MOUSE) + * @SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHPAD: A touchpad, perhaps built in to a + * game controller like the Playstation 4 controller, or perhaps used + * as a mouse replacement as in most laptops (udev ID_INPUT_TOUCHPAD) + * @SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHSCREEN: A touchscreen (udev ID_INPUT_TOUCHSCREEN) + * @SRT_INPUT_DEVICE_TYPE_FLAGS_TABLET: A graphics tablet (udev ID_INPUT_TABLET) + * @SRT_INPUT_DEVICE_TYPE_FLAGS_TABLET_PAD: A graphics tablet with buttons + * (udev ID_INPUT_TABLET_PAD) + * @SRT_INPUT_DEVICE_TYPE_FLAGS_POINTING_STICK: A mouse-like control similar to + * the IBM/Lenovo Trackpoint (udev ID_INPUT_POINTINGSTICK) + * @SRT_INPUT_DEVICE_TYPE_FLAGS_SWITCH: A switch, such as a laptop lid being + * opened (udev ID_INPUT_SWITCH) + * + * Flags describing a type of input device. An input device can fall into + * one or more of these categories. + */ +typedef enum +{ + SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK = (1 << 0), + SRT_INPUT_DEVICE_TYPE_FLAGS_ACCELEROMETER = (1 << 1), + SRT_INPUT_DEVICE_TYPE_FLAGS_KEYBOARD = (1 << 2), + SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS = (1 << 3), + SRT_INPUT_DEVICE_TYPE_FLAGS_MOUSE = (1 << 4), + SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHPAD = (1 << 5), + SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHSCREEN = (1 << 6), + SRT_INPUT_DEVICE_TYPE_FLAGS_TABLET = (1 << 7), + SRT_INPUT_DEVICE_TYPE_FLAGS_TABLET_PAD = (1 << 8), + SRT_INPUT_DEVICE_TYPE_FLAGS_POINTING_STICK = (1 << 9), + SRT_INPUT_DEVICE_TYPE_FLAGS_SWITCH = (1 << 10), + SRT_INPUT_DEVICE_TYPE_FLAGS_NONE = 0 +} SrtInputDeviceTypeFlags; + +/** + * SrtInputDeviceInterfaceFlags: + * @SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT: evdev event device nodes, + * typically /dev/input/event* + * @SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID: Raw USB or Bluetooth HID + * device nodes, typically /dev/hidraw* + * @SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE: Only report device nodes that appear to be + * openable in read-only mode + * @SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE: Only report device nodes that appear to be + * openable in read/write mode + * + * Flags describing the interface offered by an input device. + */ +typedef enum +{ + SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT = (1 << 0), + SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID = (1 << 1), + SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE = (1 << 2), + SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE = (1 << 3), + SRT_INPUT_DEVICE_INTERFACE_FLAGS_NONE = 0 +} SrtInputDeviceInterfaceFlags; + +#define SRT_TYPE_INPUT_DEVICE (srt_input_device_get_type ()) +#define SRT_INPUT_DEVICE(x) (G_TYPE_CHECK_INSTANCE_CAST ((x), SRT_TYPE_INPUT_DEVICE, SrtInputDevice)) +#define SRT_IS_INPUT_DEVICE(x) (G_TYPE_CHECK_INSTANCE_TYPE ((x), SRT_TYPE_INPUT_DEVICE)) +#define SRT_INPUT_DEVICE_GET_INTERFACE(x) (G_TYPE_INSTANCE_GET_INTERFACE ((x), SRT_TYPE_INPUT_DEVICE, SrtInputDeviceInterface)) + +typedef struct _SrtInputDevice SrtInputDevice; +typedef struct _SrtInputDeviceInterface SrtInputDeviceInterface; + +_SRT_PUBLIC +GType srt_input_device_get_type (void); + +_SRT_PUBLIC +SrtInputDeviceInterfaceFlags srt_input_device_get_interface_flags (SrtInputDevice *device); +_SRT_PUBLIC +SrtInputDeviceTypeFlags srt_input_device_get_type_flags (SrtInputDevice *device); +_SRT_PUBLIC +const char *srt_input_device_get_dev_node (SrtInputDevice *device); +_SRT_PUBLIC +const char *srt_input_device_get_sys_path (SrtInputDevice *device); +_SRT_PUBLIC +const char *srt_input_device_get_subsystem (SrtInputDevice *device); +_SRT_PUBLIC +gchar **srt_input_device_dup_udev_properties (SrtInputDevice *device); +_SRT_PUBLIC +gchar *srt_input_device_dup_uevent (SrtInputDevice *device); +_SRT_PUBLIC +gboolean srt_input_device_get_identity (SrtInputDevice *device, + unsigned int *bus_type, + unsigned int *vendor_id, + unsigned int *product_id, + unsigned int *version); +_SRT_PUBLIC +size_t srt_input_device_get_event_capabilities (SrtInputDevice *device, + unsigned int type, + unsigned long *storage, + size_t n_longs); +_SRT_PUBLIC +size_t srt_input_device_get_event_types (SrtInputDevice *device, + unsigned long *storage, + size_t n_longs); +_SRT_PUBLIC +gboolean srt_input_device_has_event_type (SrtInputDevice *device, + unsigned int type); +_SRT_PUBLIC +gboolean srt_input_device_has_event_capability (SrtInputDevice *device, + unsigned int type, + unsigned int code); +_SRT_PUBLIC +gboolean srt_input_device_has_input_property (SrtInputDevice *device, + unsigned int input_prop); +_SRT_PUBLIC +size_t srt_input_device_get_input_properties (SrtInputDevice *device, + unsigned long *storage, + size_t n_longs); +_SRT_PUBLIC +SrtInputDeviceTypeFlags srt_input_device_guess_type_flags_from_event_capabilities (SrtInputDevice *device); + +_SRT_PUBLIC +const char *srt_input_device_get_hid_sys_path (SrtInputDevice *device); +_SRT_PUBLIC +gchar *srt_input_device_dup_hid_uevent (SrtInputDevice *device); +_SRT_PUBLIC +gboolean srt_input_device_get_hid_identity (SrtInputDevice *device, + unsigned int *bus_type, + unsigned int *vendor_id, + unsigned int *product_id, + const char **name, + const char **phys, + const char **uniq); + +_SRT_PUBLIC +const char *srt_input_device_get_input_sys_path (SrtInputDevice *device); +_SRT_PUBLIC +gchar *srt_input_device_dup_input_uevent (SrtInputDevice *device); +_SRT_PUBLIC +gboolean srt_input_device_get_input_identity (SrtInputDevice *device, + unsigned int *bus_type, + unsigned int *vendor_id, + unsigned int *product_id, + unsigned int *version, + const char **name, + const char **phys, + const char **uniq); + +_SRT_PUBLIC +const char *srt_input_device_get_usb_device_sys_path (SrtInputDevice *device); +_SRT_PUBLIC +gchar *srt_input_device_dup_usb_device_uevent (SrtInputDevice *device); +_SRT_PUBLIC +gboolean srt_input_device_get_usb_device_identity (SrtInputDevice *device, + unsigned int *vendor_id, + unsigned int *product_id, + unsigned int *device_version, + const char **manufacturer, + const char **product, + const char **serial); + +_SRT_PUBLIC +int srt_input_device_open (SrtInputDevice *device, + int flags, + GError **error); + +/** + * SrtInputDeviceMonitorFlags: + * @SRT_INPUT_DEVICE_MONITOR_FLAGS_ONCE: Enumerate the devices that were + * available when monitoring starts, and then stop monitoring. + * @SRT_INPUT_DEVICE_MONITOR_FLAGS_UDEV: Prefer to get devices from udev. + * @SRT_INPUT_DEVICE_MONITOR_FLAGS_DIRECT: Prefer to get devices by + * monitoring /dev, /sys directly. + * @SRT_INPUT_DEVICE_MONITOR_FLAGS_NONE: No special behaviour. + * + * Flags affecting the behaviour of the input device monitor. + */ +typedef enum +{ + SRT_INPUT_DEVICE_MONITOR_FLAGS_ONCE = (1 << 0), + SRT_INPUT_DEVICE_MONITOR_FLAGS_UDEV = (1 << 1), + SRT_INPUT_DEVICE_MONITOR_FLAGS_DIRECT = (1 << 2), + SRT_INPUT_DEVICE_MONITOR_FLAGS_NONE = 0 +} SrtInputDeviceMonitorFlags; + +#define SRT_TYPE_INPUT_DEVICE_MONITOR (srt_input_device_monitor_get_type ()) +#define SRT_INPUT_DEVICE_MONITOR(x) (G_TYPE_CHECK_INSTANCE_CAST ((x), SRT_TYPE_INPUT_DEVICE_MONITOR, SrtInputDeviceMonitor)) +#define SRT_IS_INPUT_DEVICE_MONITOR(x) (G_TYPE_CHECK_INSTANCE_TYPE ((x), SRT_TYPE_INPUT_DEVICE_MONITOR)) +#define SRT_INPUT_DEVICE_MONITOR_GET_INTERFACE(x) (G_TYPE_INSTANCE_GET_INTERFACE ((x), SRT_TYPE_INPUT_DEVICE_MONITOR, SrtInputDeviceMonitorInterface)) + +typedef struct _SrtInputDeviceMonitor SrtInputDeviceMonitor; +typedef struct _SrtInputDeviceMonitorInterface SrtInputDeviceMonitorInterface; + +_SRT_PUBLIC +GType srt_input_device_monitor_get_type (void); + +_SRT_PUBLIC +SrtInputDeviceMonitor *srt_input_device_monitor_new (SrtInputDeviceMonitorFlags flags); + +_SRT_PUBLIC +SrtInputDeviceMonitorFlags srt_input_device_monitor_get_flags (SrtInputDeviceMonitor *monitor); +_SRT_PUBLIC +void srt_input_device_monitor_request_raw_hid (SrtInputDeviceMonitor *monitor); +_SRT_PUBLIC +void srt_input_device_monitor_request_evdev (SrtInputDeviceMonitor *monitor); + +_SRT_PUBLIC +gboolean srt_input_device_monitor_is_active (SrtInputDeviceMonitor *monitor); +_SRT_PUBLIC +gboolean srt_input_device_monitor_start (SrtInputDeviceMonitor *monitor, + GError **error); +_SRT_PUBLIC +void srt_input_device_monitor_stop (SrtInputDeviceMonitor *monitor); + +#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC +G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtInputDevice, g_object_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtInputDeviceMonitor, g_object_unref) +#endif diff --git a/steam-runtime-tools/meson.build b/steam-runtime-tools/meson.build index 6c0acce1c1f5ecc6f5a60ad4595b13942f88d6c4..f92ef4792c4e63d16b36494b46d6e7bb65045a3d 100644 --- a/steam-runtime-tools/meson.build +++ b/steam-runtime-tools/meson.build @@ -28,10 +28,14 @@ libsteamrt_sources = [ 'cpu-feature.c', 'desktop-entry-internal.h', 'desktop-entry.c', + 'direct-input-device-internal.h', + 'direct-input-device.c', 'glib-backports.c', 'glib-backports-internal.h', 'graphics-internal.h', 'graphics.c', + 'input-device-internal.h', + 'input-device.c', 'json-glib-backports.c', 'json-glib-backports-internal.h', 'json-utils.c', @@ -46,9 +50,13 @@ libsteamrt_sources = [ 'resolve-in-sysroot.c', 'runtime-internal.h', 'runtime.c', + 'simple-input-device-internal.h', + 'simple-input-device.c', 'steam-internal.h', 'steam.c', 'system-info.c', + 'udev-input-device-internal.h', + 'udev-input-device.c', 'utils-internal.h', 'utils.c', 'xdg-portal.c', @@ -59,6 +67,7 @@ libsteamrt_public_headers = [ 'cpu-feature.h', 'desktop-entry.h', 'graphics.h', + 'input-device.h', 'library.h', 'locale.h', 'macros.h', diff --git a/steam-runtime-tools/simple-input-device-internal.h b/steam-runtime-tools/simple-input-device-internal.h new file mode 100644 index 0000000000000000000000000000000000000000..ecdd4bdc5e83d902dd3e0488b19bf4891f1899a5 --- /dev/null +++ b/steam-runtime-tools/simple-input-device-internal.h @@ -0,0 +1,110 @@ +/*<private_header>*/ +/* + * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org> + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: Zlib + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#pragma once + +#include "steam-runtime-tools/glib-backports-internal.h" +#include "steam-runtime-tools/input-device-internal.h" + +#include <json-glib/json-glib.h> + +#define SRT_TYPE_SIMPLE_INPUT_DEVICE (_srt_simple_input_device_get_type ()) + +typedef struct _SrtSimpleInputDevice SrtSimpleInputDevice; +typedef struct _SrtSimpleInputDeviceClass SrtSimpleInputDeviceClass; + +#define SRT_TYPE_SIMPLE_INPUT_DEVICE (_srt_simple_input_device_get_type ()) +#define SRT_SIMPLE_INPUT_DEVICE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SRT_TYPE_SIMPLE_INPUT_DEVICE, SrtSimpleInputDevice)) +#define SRT_IS_SIMPLE_INPUT_DEVICE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SRT_TYPE_SIMPLE_INPUT_DEVICE)) +#define SRT_SIMPLE_INPUT_DEVICE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), SRT_TYPE_SIMPLE_INPUT_DEVICE, SrtSimpleInputDeviceClass)) +#define SRT_SIMPLE_INPUT_DEVICE_CLASS(c) (G_TYPE_CHECK_CLASS_CAST ((c), SRT_TYPE_SIMPLE_INPUT_DEVICE, SrtSimpleInputDeviceClass)) +#define SRT_IS_SIMPLE_INPUT_DEVICE_CLASS(c) (G_TYPE_CHECK_CLASS_TYPE ((c), SRT_TYPE_SIMPLE_INPUT_DEVICE)) + +G_GNUC_INTERNAL +GType _srt_simple_input_device_get_type (void); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtSimpleInputDevice, g_object_unref) + +struct _SrtSimpleInputDevice +{ + GObject parent; + + gchar *dev_node; + gchar *sys_path; + gchar *subsystem; + gchar **udev_properties; + gchar *uevent; + + struct + { + gchar *sys_path; + gchar *uevent; + gchar *name; + gchar *phys; + gchar *uniq; + guint32 bus_type; + guint32 product_id; + guint32 vendor_id; + } hid_ancestor; + + struct + { + gchar *sys_path; + gchar *uevent; + gchar *name; + gchar *phys; + gchar *uniq; + guint32 bus_type; + guint32 product_id; + guint32 vendor_id; + guint32 version; + } input_ancestor; + + struct + { + gchar *sys_path; + gchar *uevent; + gchar *manufacturer; + gchar *product; + gchar *serial; + guint32 product_id; + guint32 vendor_id; + guint32 device_version; + } usb_device_ancestor; + + SrtEvdevCapabilities evdev_caps; + SrtInputDeviceInterfaceFlags iface_flags; + SrtInputDeviceTypeFlags type_flags; + guint32 bus_type; + guint32 vendor_id; + guint32 product_id; + guint32 version; +}; + +struct _SrtSimpleInputDeviceClass +{ + GObjectClass parent; +}; + +SrtSimpleInputDevice *_srt_simple_input_device_new_from_json (JsonObject *obj); diff --git a/steam-runtime-tools/simple-input-device.c b/steam-runtime-tools/simple-input-device.c new file mode 100644 index 0000000000000000000000000000000000000000..a45ca41ca952870394bb1422da52f38ac8fe66e1 --- /dev/null +++ b/steam-runtime-tools/simple-input-device.c @@ -0,0 +1,548 @@ +/* + * An input device loaded from JSON or similar. + * + * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org> + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: Zlib + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#include "steam-runtime-tools/simple-input-device-internal.h" + +#include <libglnx.h> + +#include "steam-runtime-tools/enums.h" +#include "steam-runtime-tools/glib-backports-internal.h" +#include "steam-runtime-tools/input-device.h" +#include "steam-runtime-tools/input-device-internal.h" +#include "steam-runtime-tools/json-utils-internal.h" +#include "steam-runtime-tools/utils-internal.h" + +static void srt_simple_input_device_iface_init (SrtInputDeviceInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (SrtSimpleInputDevice, + _srt_simple_input_device, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (SRT_TYPE_INPUT_DEVICE, + srt_simple_input_device_iface_init)) + +static void +_srt_simple_input_device_init (SrtSimpleInputDevice *self) +{ +} + +static void +srt_simple_input_device_finalize (GObject *object) +{ + SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (object); + + g_free (self->dev_node); + g_free (self->sys_path); + g_free (self->subsystem); + g_strfreev (self->udev_properties); + g_free (self->uevent); + + g_free (self->hid_ancestor.sys_path); + g_free (self->hid_ancestor.uevent); + g_free (self->hid_ancestor.name); + g_free (self->hid_ancestor.phys); + g_free (self->hid_ancestor.uniq); + + g_free (self->input_ancestor.sys_path); + g_free (self->input_ancestor.uevent); + g_free (self->input_ancestor.name); + g_free (self->input_ancestor.phys); + g_free (self->input_ancestor.uniq); + + g_free (self->usb_device_ancestor.sys_path); + g_free (self->usb_device_ancestor.uevent); + g_free (self->usb_device_ancestor.manufacturer); + g_free (self->usb_device_ancestor.product); + g_free (self->usb_device_ancestor.serial); + + G_OBJECT_CLASS (_srt_simple_input_device_parent_class)->finalize (object); +} + +static void +_srt_simple_input_device_class_init (SrtSimpleInputDeviceClass *cls) +{ + GObjectClass *object_class = G_OBJECT_CLASS (cls); + + object_class->finalize = srt_simple_input_device_finalize; +} + +static SrtInputDeviceInterfaceFlags +srt_simple_input_device_get_interface_flags (SrtInputDevice *device) +{ + SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device); + + return self->iface_flags; +} + +static SrtInputDeviceTypeFlags +srt_simple_input_device_get_type_flags (SrtInputDevice *device) +{ + SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device); + + return self->type_flags; +} + +static const char * +srt_simple_input_device_get_dev_node (SrtInputDevice *device) +{ + SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device); + + return self->dev_node; +} + +static const char * +srt_simple_input_device_get_subsystem (SrtInputDevice *device) +{ + SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device); + + return self->subsystem; +} + +static const char * +srt_simple_input_device_get_sys_path (SrtInputDevice *device) +{ + SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device); + + return self->sys_path; +} + +static gboolean +srt_simple_input_device_get_identity (SrtInputDevice *device, + unsigned int *bus_type, + unsigned int *vendor_id, + unsigned int *product_id, + unsigned int *version) +{ + SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device); + + if (self->bus_type == 0 + && self->vendor_id == 0 + && self->product_id == 0 + && self->version == 0) + return FALSE; + + if (bus_type != NULL) + *bus_type = self->bus_type; + + if (vendor_id != NULL) + *vendor_id = self->vendor_id; + + if (product_id != NULL) + *product_id = self->product_id; + + if (version != NULL) + *version = self->version; + + return TRUE; +} + +static gchar ** +srt_simple_input_device_dup_udev_properties (SrtInputDevice *device) +{ + SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device); + + return g_strdupv (self->udev_properties); +} + +static gchar * +srt_simple_input_device_dup_uevent (SrtInputDevice *device) +{ + SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device); + + return g_strdup (self->uevent); +} + +static const SrtEvdevCapabilities * +srt_simple_input_device_peek_event_capabilities (SrtInputDevice *device) +{ + SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device); + + return &self->evdev_caps; +} + +static const char * +srt_simple_input_device_get_hid_sys_path (SrtInputDevice *device) +{ + SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device); + + return self->hid_ancestor.sys_path; +} + +static gboolean +srt_simple_input_device_get_hid_identity (SrtInputDevice *device, + unsigned int *bus_type, + unsigned int *vendor_id, + unsigned int *product_id, + const char **name, + const char **phys, + const char **uniq) +{ + SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device); + + if (self->hid_ancestor.sys_path == NULL) + return FALSE; + + if (bus_type != NULL) + *bus_type = self->hid_ancestor.bus_type; + + if (vendor_id != NULL) + *vendor_id = self->hid_ancestor.vendor_id; + + if (product_id != NULL) + *product_id = self->hid_ancestor.product_id; + + if (name != NULL) + *name = self->hid_ancestor.name; + + if (phys != NULL) + *phys = self->hid_ancestor.phys; + + if (uniq != NULL) + *uniq = self->hid_ancestor.uniq; + + return TRUE; +} + +static gchar * +srt_simple_input_device_dup_hid_uevent (SrtInputDevice *device) +{ + SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device); + + return g_strdup (self->hid_ancestor.uevent); +} + +static const char * +srt_simple_input_device_get_input_sys_path (SrtInputDevice *device) +{ + SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device); + + return self->input_ancestor.sys_path; +} + +static gboolean +srt_simple_input_device_get_input_identity (SrtInputDevice *device, + unsigned int *bus_type, + unsigned int *vendor_id, + unsigned int *product_id, + unsigned int *version, + const char **name, + const char **phys, + const char **uniq) +{ + SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device); + + if (self->input_ancestor.sys_path == NULL) + return FALSE; + + if (bus_type != NULL) + *bus_type = self->input_ancestor.bus_type; + + if (vendor_id != NULL) + *vendor_id = self->input_ancestor.vendor_id; + + if (product_id != NULL) + *product_id = self->input_ancestor.product_id; + + if (version != NULL) + *version = self->input_ancestor.version; + + if (name != NULL) + *name = self->input_ancestor.name; + + if (phys != NULL) + *phys = self->input_ancestor.phys; + + if (uniq != NULL) + *uniq = self->input_ancestor.uniq; + + return TRUE; +} + +static gchar * +srt_simple_input_device_dup_input_uevent (SrtInputDevice *device) +{ + SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device); + + return g_strdup (self->input_ancestor.uevent); +} + +static const char * +srt_simple_input_device_get_usb_device_sys_path (SrtInputDevice *device) +{ + SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device); + + return self->usb_device_ancestor.sys_path; +} + +static gboolean +srt_simple_input_device_get_usb_device_identity (SrtInputDevice *device, + unsigned int *vendor_id, + unsigned int *product_id, + unsigned int *version, + const char **manufacturer, + const char **product, + const char **serial) +{ + SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device); + + if (self->usb_device_ancestor.sys_path == NULL) + return FALSE; + + if (vendor_id != NULL) + *vendor_id = self->usb_device_ancestor.vendor_id; + + if (product_id != NULL) + *product_id = self->usb_device_ancestor.product_id; + + if (version != NULL) + *version = self->usb_device_ancestor.device_version; + + if (manufacturer != NULL) + *manufacturer = self->usb_device_ancestor.manufacturer; + + if (product != NULL) + *product = self->usb_device_ancestor.product; + + if (serial != NULL) + *serial = self->usb_device_ancestor.serial; + + return TRUE; +} + +static gchar * +srt_simple_input_device_dup_usb_device_uevent (SrtInputDevice *device) +{ + SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device); + + return g_strdup (self->usb_device_ancestor.uevent); +} + +static void +srt_simple_input_device_iface_init (SrtInputDeviceInterface *iface) +{ +#define IMPLEMENT(x) iface->x = srt_simple_input_device_ ## x + + IMPLEMENT (get_type_flags); + IMPLEMENT (get_interface_flags); + IMPLEMENT (get_dev_node); + IMPLEMENT (get_sys_path); + IMPLEMENT (get_subsystem); + IMPLEMENT (get_identity); + IMPLEMENT (dup_udev_properties); + IMPLEMENT (dup_uevent); + IMPLEMENT (peek_event_capabilities); + + IMPLEMENT (get_hid_sys_path); + IMPLEMENT (get_hid_identity); + IMPLEMENT (get_input_sys_path); + IMPLEMENT (get_input_identity); + IMPLEMENT (get_usb_device_sys_path); + IMPLEMENT (get_usb_device_identity); + + IMPLEMENT (dup_hid_uevent); + IMPLEMENT (dup_input_uevent); + IMPLEMENT (dup_usb_device_uevent); + +#undef IMPLEMENT +} + +static gchar * +dup_json_string_member (JsonObject *obj, + const gchar *name) +{ + JsonNode *node = json_object_get_member (obj, name); + + if (node == NULL) + return NULL; + + /* This returns NULL without error if it is a non-string */ + return json_node_dup_string (node); +} + +static unsigned long +get_json_hex_member (JsonObject *obj, + const gchar *name) +{ + JsonNode *node = json_object_get_member (obj, name); + const char *s; + + if (node == NULL) + return 0; + + s = json_node_get_string (node); + + if (s == NULL) + return 0; + + if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) + s += 2; + + return strtoul (s, NULL, 16); +} + +static JsonObject * +get_json_object_member (JsonObject *obj, + const char *name) +{ + JsonNode *node = json_object_get_member (obj, name); + + if (node != NULL && JSON_NODE_HOLDS_OBJECT (node)) + return json_node_get_object (node); + + return NULL; +} + +static gchar * +dup_json_uevent (JsonObject *obj) +{ + return _srt_json_object_dup_array_of_lines_member (obj, "uevent"); +} + +static void +get_json_evdev_caps (JsonObject *obj, + const char *name, + unsigned long *longs, + size_t n_longs) +{ + JsonNode *node = json_object_get_member (obj, name); + /* The first pointer that is out of bounds for longs */ + unsigned char *limit = (unsigned char *) &longs[n_longs]; + /* The output position in longs */ + unsigned char *out = (unsigned char *) longs; + /* The input position in the string we are parsing */ + const char *iter; + size_t i; + + if (node == NULL) + return; + + iter = json_node_get_string (node); + + if (iter == NULL) + return; + + while (*iter != '\0') + { + unsigned int this_byte; + int used; + + while (*iter == ' ') + iter++; + + if (sscanf (iter, "%x%n", &this_byte, &used) == 1) + iter += used; + else + break; + + if (out < limit) + *(out++) = (unsigned char) this_byte; + else + break; + } + + for (i = 0; i < n_longs; i++) + longs[i] = GULONG_FROM_LE (longs[i]); +} + +SrtSimpleInputDevice * +_srt_simple_input_device_new_from_json (JsonObject *obj) +{ + SrtSimpleInputDevice *self = g_object_new (SRT_TYPE_SIMPLE_INPUT_DEVICE, + NULL); + JsonObject *sub; + + self->sys_path = dup_json_string_member (obj, "sys_path"); + self->dev_node = dup_json_string_member (obj, "dev_node"); + self->subsystem = dup_json_string_member (obj, "subsystem"); + self->bus_type = get_json_hex_member (obj, "bus_type"); + self->vendor_id = get_json_hex_member (obj, "vendor_id"); + self->product_id = get_json_hex_member (obj, "product_id"); + self->version = get_json_hex_member (obj, "version"); + + self->iface_flags = srt_get_flags_from_json_array (SRT_TYPE_INPUT_DEVICE_INTERFACE_FLAGS, + obj, + "interface_flags", + SRT_INPUT_DEVICE_INTERFACE_FLAGS_NONE); + self->type_flags = srt_get_flags_from_json_array (SRT_TYPE_INPUT_DEVICE_TYPE_FLAGS, + obj, + "type_flags", + SRT_INPUT_DEVICE_TYPE_FLAGS_NONE); + + if ((sub = get_json_object_member (obj, "evdev")) != NULL) + { + get_json_evdev_caps (sub, "raw_types", self->evdev_caps.ev, + G_N_ELEMENTS (self->evdev_caps.ev)); + get_json_evdev_caps (sub, "raw_abs", self->evdev_caps.abs, + G_N_ELEMENTS (self->evdev_caps.abs)); + get_json_evdev_caps (sub, "raw_rel", self->evdev_caps.rel, + G_N_ELEMENTS (self->evdev_caps.rel)); + get_json_evdev_caps (sub, "raw_keys", self->evdev_caps.keys, + G_N_ELEMENTS (self->evdev_caps.keys)); + get_json_evdev_caps (sub, "raw_input_properties", + self->evdev_caps.props, + G_N_ELEMENTS (self->evdev_caps.props)); + } + + self->udev_properties = _srt_json_object_dup_strv_member (obj, "udev_properties", NULL); + self->uevent = dup_json_uevent (obj); + + if ((sub = get_json_object_member (obj, "hid_ancestor")) != NULL) + { + self->hid_ancestor.sys_path = dup_json_string_member (sub, "sys_path"); + self->hid_ancestor.name = dup_json_string_member (sub, "name"); + self->hid_ancestor.bus_type = get_json_hex_member (sub, "bus_type"); + self->hid_ancestor.vendor_id = get_json_hex_member (sub, "vendor_id"); + self->hid_ancestor.product_id = get_json_hex_member (sub, "product_id"); + self->hid_ancestor.uniq = dup_json_string_member (sub, "uniq"); + self->hid_ancestor.phys = dup_json_string_member (sub, "phys"); + self->hid_ancestor.uevent = dup_json_uevent (sub); + } + + if ((sub = get_json_object_member (obj, "input_ancestor")) != NULL) + { + self->input_ancestor.sys_path = dup_json_string_member (sub, "sys_path"); + self->input_ancestor.name = dup_json_string_member (sub, "name"); + self->input_ancestor.bus_type = get_json_hex_member (sub, "bus_type"); + self->input_ancestor.vendor_id = get_json_hex_member (sub, "vendor_id"); + self->input_ancestor.product_id = get_json_hex_member (sub, "product_id"); + self->input_ancestor.version = get_json_hex_member (sub, "version"); + self->input_ancestor.uniq = dup_json_string_member (sub, "uniq"); + self->input_ancestor.phys = dup_json_string_member (sub, "phys"); + self->input_ancestor.uevent = dup_json_uevent (sub); + } + + if ((sub = get_json_object_member (obj, "usb_device_ancestor")) != NULL) + { + self->usb_device_ancestor.sys_path = dup_json_string_member (sub, "sys_path"); + self->usb_device_ancestor.vendor_id = get_json_hex_member (sub, "vendor_id"); + self->usb_device_ancestor.product_id = get_json_hex_member (sub, "product_id"); + self->usb_device_ancestor.device_version = get_json_hex_member (sub, "version"); + self->usb_device_ancestor.manufacturer = dup_json_string_member (sub, "manufacturer"); + self->usb_device_ancestor.product = dup_json_string_member (sub, "product"); + self->usb_device_ancestor.serial = dup_json_string_member (sub, "serial"); + self->usb_device_ancestor.uevent = dup_json_uevent (sub); + } + + return self; +} diff --git a/steam-runtime-tools/steam-runtime-tools.h b/steam-runtime-tools/steam-runtime-tools.h index 29e02af44daeb3d28bf12958e8d6a828e884c2c3..c6009f7c81d70e17bca872da67e81f29f7542307 100644 --- a/steam-runtime-tools/steam-runtime-tools.h +++ b/steam-runtime-tools/steam-runtime-tools.h @@ -32,6 +32,7 @@ #include <steam-runtime-tools/desktop-entry.h> #include <steam-runtime-tools/enums.h> #include <steam-runtime-tools/graphics.h> +#include <steam-runtime-tools/input-device.h> #include <steam-runtime-tools/library.h> #include <steam-runtime-tools/locale.h> #include <steam-runtime-tools/macros.h> diff --git a/steam-runtime-tools/udev-input-device-internal.h b/steam-runtime-tools/udev-input-device-internal.h new file mode 100644 index 0000000000000000000000000000000000000000..9ee0d36c83568935f1c0a62620eaa30eafb3a186 --- /dev/null +++ b/steam-runtime-tools/udev-input-device-internal.h @@ -0,0 +1,68 @@ +/* + * libudev-based input device monitor, loosely based on SDL code. + * + * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org> + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: Zlib + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#pragma once + +#include "steam-runtime-tools/glib-backports-internal.h" +#include "steam-runtime-tools/input-device.h" + +typedef struct _SrtUdevInputDevice SrtUdevInputDevice; +typedef struct _SrtUdevInputDeviceClass SrtUdevInputDeviceClass; + +#define SRT_TYPE_UDEV_INPUT_DEVICE (_srt_udev_input_device_get_type ()) +#define SRT_UDEV_INPUT_DEVICE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SRT_TYPE_UDEV_INPUT_DEVICE, SrtUdevInputDevice)) +#define SRT_IS_UDEV_INPUT_DEVICE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SRT_TYPE_UDEV_INPUT_DEVICE)) +#define SRT_UDEV_INPUT_DEVICE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), SRT_TYPE_UDEV_INPUT_DEVICE, SrtUdevInputDeviceClass)) +#define SRT_UDEV_INPUT_DEVICE_CLASS(c) (G_TYPE_CHECK_CLASS_CAST ((c), SRT_TYPE_UDEV_INPUT_DEVICE, SrtUdevInputDeviceClass)) +#define SRT_IS_UDEV_INPUT_DEVICE_CLASS(c) (G_TYPE_CHECK_CLASS_TYPE ((c), SRT_TYPE_UDEV_INPUT_DEVICE)) + +G_GNUC_INTERNAL +GType _srt_udev_input_device_get_type (void); + +#define SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR (_srt_udev_input_device_monitor_get_type ()) +#define SRT_UDEV_INPUT_DEVICE_MONITOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR, SrtUdevInputDeviceMonitor)) +#define SRT_IS_UDEV_INPUT_DEVICE_MONITOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR)) +#define SRT_UDEV_INPUT_DEVICE_MONITOR_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR, SrtUdevInputDeviceMonitorClass)) +#define SRT_UDEV_INPUT_DEVICE_MONITOR_CLASS(c) (G_TYPE_CHECK_CLASS_CAST ((c), SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR, SrtUdevInputDeviceMonitorClass)) +#define SRT_IS_UDEV_INPUT_DEVICE_MONITOR_CLASS(c) (G_TYPE_CHECK_CLASS_TYPE ((c), SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR)) + +typedef struct _SrtUdevInputDeviceMonitor SrtUdevInputDeviceMonitor; +typedef struct _SrtUdevInputDeviceMonitorClass SrtUdevInputDeviceMonitorClass; + +G_GNUC_INTERNAL +GType _srt_udev_input_device_monitor_get_type (void); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtUdevInputDevice, g_object_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtUdevInputDeviceMonitor, g_object_unref) + +static inline SrtInputDeviceMonitor * +srt_udev_input_device_monitor_new (SrtInputDeviceMonitorFlags flags, + GError **error) +{ + return g_initable_new (SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR, + NULL /* cancellable */, error, + "flags", flags, + NULL); +} diff --git a/steam-runtime-tools/udev-input-device.c b/steam-runtime-tools/udev-input-device.c new file mode 100644 index 0000000000000000000000000000000000000000..fcccc075ce9c18a97756d1ba6eee6685c3337570 --- /dev/null +++ b/steam-runtime-tools/udev-input-device.c @@ -0,0 +1,1337 @@ +/* + * libudev-based input device monitor, loosely based on SDL code. + * + * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org> + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: Zlib + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#include "steam-runtime-tools/udev-input-device-internal.h" + +#include <dlfcn.h> +#include <linux/input.h> + +#include <glib-unix.h> +#include <libglnx.h> + +#include "steam-runtime-tools/glib-backports-internal.h" +#include "steam-runtime-tools/input-device.h" +#include "steam-runtime-tools/input-device-internal.h" +#include "steam-runtime-tools/utils-internal.h" + +static void srt_udev_input_device_iface_init (SrtInputDeviceInterface *iface); +static void srt_udev_input_device_monitor_iface_init (SrtInputDeviceMonitorInterface *iface); +static void srt_udev_input_device_monitor_initable_iface_init (GInitableIface *iface); + +struct udev; +struct udev_list_entry; +struct udev_device; +struct udev_monitor; +struct udev_enumerate; + +static struct +{ + void *handle; + + struct udev *(*udev_new) (void); + struct udev *(*udev_unref) (struct udev *); + + const char *(*udev_list_entry_get_name) (struct udev_list_entry *); + struct udev_list_entry *(*udev_list_entry_get_next) (struct udev_list_entry *); + + struct udev_device *(*udev_device_new_from_syspath) (struct udev *, + const char *); + const char *(*udev_device_get_action) (struct udev_device *); + const char *(*udev_device_get_devnode) (struct udev_device *); + const char *(*udev_device_get_subsystem) (struct udev_device *); + const char *(*udev_device_get_syspath) (struct udev_device *); + struct udev_list_entry *(*udev_device_get_properties_list_entry) (struct udev_device *); + const char *(*udev_device_get_property_value) (struct udev_device *, const char *); + const char *(*udev_device_get_sysattr_value) (struct udev_device *, const char *); + struct udev_device *(*udev_device_get_parent_with_subsystem_devtype) (struct udev_device *, const char *, const char *); + struct udev_device *(*udev_device_ref) (struct udev_device *); + struct udev_device *(*udev_device_unref) (struct udev_device *); + + struct udev_monitor *(*udev_monitor_new_from_netlink) (struct udev *, + const char *); + int (*udev_monitor_filter_add_match_subsystem_devtype) (struct udev_monitor *, + const char *, + const char *); + int (*udev_monitor_enable_receiving) (struct udev_monitor *); + struct udev_device *(*udev_monitor_receive_device) (struct udev_monitor *); + int (*udev_monitor_get_fd) (struct udev_monitor *); + struct udev_monitor *(*udev_monitor_unref) (struct udev_monitor *); + + struct udev_enumerate *(*udev_enumerate_new) (struct udev *); + int (*udev_enumerate_add_match_subsystem) (struct udev_enumerate *, + const char *); + int (*udev_enumerate_scan_devices) (struct udev_enumerate *); + struct udev_list_entry *(*udev_enumerate_get_list_entry) (struct udev_enumerate *); + struct udev_enumerate *(*udev_enumerate_unref) (struct udev_enumerate *); +} symbols; + +static gboolean +has_all_flags (unsigned int have, + unsigned int want) +{ + return ((have & want) == want); +} + +static gboolean +get_evdev_caps_from_udev (struct udev_device *dev, + const char *attr, + unsigned long *bitmask, + size_t bitmask_len_longs) +{ + g_autofree char *text = NULL; + const char *value; + char *word; + int i; + unsigned long v; + + memset (bitmask, 0, bitmask_len_longs * sizeof (*bitmask)); + value = symbols.udev_device_get_sysattr_value (dev, attr); + + if (!value) + return FALSE; + + text = g_strdup (value); + i = 0; + + while ((word = strrchr (text, ' ')) != NULL) + { + v = strtoul (word + 1, NULL, 16); + + if (i < bitmask_len_longs) + bitmask[i] = v; + + ++i; + *word = '\0'; + } + + v = strtoul (text, NULL, 16); + + if (i < bitmask_len_longs) + bitmask[i] = v; + + return TRUE; +} + +static struct udev_device * +find_input_ancestor (struct udev_device *dev) +{ + struct udev_device *ancestor; + + for (ancestor = dev; + ancestor != NULL; + ancestor = symbols.udev_device_get_parent_with_subsystem_devtype (ancestor, "input", NULL)) + { + if (symbols.udev_device_get_sysattr_value (ancestor, "capabilities/ev") != NULL) + return ancestor; + } + + return NULL; +} + +struct _SrtUdevInputDevice +{ + GObject parent; + + struct udev_device *dev; /* owned */ + + SrtEvdevCapabilities evdev_caps; + + struct + { + struct udev_device *dev; /* borrowed from child dev */ + gchar *name; + gchar *phys; + gchar *uniq; + guint32 bus_type; + guint32 product_id; + guint32 vendor_id; + } hid_ancestor; + + struct + { + struct udev_device *dev; /* borrowed from child dev */ + gchar *name; + gchar *phys; + gchar *uniq; + guint32 bus_type; + guint32 product_id; + guint32 vendor_id; + guint32 version; + } input_ancestor; + + struct + { + struct udev_device *dev; /* borrowed from child dev */ + guint32 product_id; + guint32 vendor_id; + guint32 device_version; + } usb_device_ancestor; + + SrtInputDeviceTypeFlags type_flags; + SrtInputDeviceInterfaceFlags iface_flags; +}; + +struct _SrtUdevInputDeviceClass +{ + GObjectClass parent; +}; + +struct _SrtUdevInputDeviceMonitor +{ + GObject parent; + + struct udev *context; + struct udev_monitor *monitor; + GHashTable *devices; + GMainContext *monitor_context; + GSource *monitor_source; + + gboolean want_evdev; + gboolean want_hidraw; + SrtInputDeviceMonitorFlags flags; + enum + { + NOT_STARTED = 0, + STARTED, + STOPPED + } state; +}; + +struct _SrtUdevInputDeviceMonitorClass +{ + GObjectClass parent; +}; + +G_DEFINE_TYPE_WITH_CODE (SrtUdevInputDevice, + _srt_udev_input_device, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (SRT_TYPE_INPUT_DEVICE, + srt_udev_input_device_iface_init)) + +G_DEFINE_TYPE_WITH_CODE (SrtUdevInputDeviceMonitor, + _srt_udev_input_device_monitor, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, + srt_udev_input_device_monitor_initable_iface_init) + G_IMPLEMENT_INTERFACE (SRT_TYPE_INPUT_DEVICE_MONITOR, + srt_udev_input_device_monitor_iface_init)) + +static void +_srt_udev_input_device_init (SrtUdevInputDevice *self) +{ +} + +static void +srt_udev_input_device_finalize (GObject *object) +{ + SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (object); + + g_clear_pointer (&self->dev, symbols.udev_device_unref); + g_free (self->hid_ancestor.name); + g_free (self->hid_ancestor.phys); + g_free (self->hid_ancestor.uniq); + g_free (self->input_ancestor.name); + g_free (self->input_ancestor.phys); + g_free (self->input_ancestor.uniq); + + G_OBJECT_CLASS (_srt_udev_input_device_parent_class)->finalize (object); +} + +static void +_srt_udev_input_device_class_init (SrtUdevInputDeviceClass *cls) +{ + GObjectClass *object_class = G_OBJECT_CLASS (cls); + + object_class->finalize = srt_udev_input_device_finalize; +} + +/* + * Get a sysfs attribute that is a string. + * + * On success, set *out and return TRUE. + * On failure, leave *out untouched and return FALSE. + */ +static gboolean +get_string (struct udev_device *dev, + const char *attribute, + const gchar **out) +{ + const char *tmp; + + if (dev == NULL) + return FALSE; + + tmp = symbols.udev_device_get_sysattr_value (dev, attribute); + + if (tmp == NULL) + return FALSE; + + if (out != NULL) + *out = tmp; + + return TRUE; +} + +/* + * Get a sysfs attribute that is a uint32 (or smaller) in hexadecimal + * (with or without 0x prefix). + * + * On success, set *out and return TRUE. + * On failure, leave *out untouched and return FALSE. + */ +static gboolean +get_uint32_hex (struct udev_device *dev, + const char *attribute, + guint32 *out) +{ + const char *tmp; + guint64 ret; + gchar *endptr; + + if (!get_string (dev, attribute, &tmp)) + return FALSE; + + if (tmp[0] == '0' && (tmp[1] == 'x' || tmp[1] == 'X')) + tmp += 2; + + ret = g_ascii_strtoull (tmp, &endptr, 16); + + if (endptr == NULL || *endptr != '\0' || ret > G_MAXUINT32) + return FALSE; + + if (out != NULL) + *out = (guint32) ret; + + return TRUE; +} + +static SrtInputDeviceTypeFlags +srt_udev_input_device_get_type_flags (SrtInputDevice *device) +{ + SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device); + + return self->type_flags; +} + +static SrtInputDeviceInterfaceFlags +srt_udev_input_device_get_interface_flags (SrtInputDevice *device) +{ + SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device); + + return self->iface_flags; +} + +static const char * +srt_udev_input_device_get_dev_node (SrtInputDevice *device) +{ + SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device); + + return symbols.udev_device_get_devnode (self->dev); +} + +static const char * +srt_udev_input_device_get_subsystem (SrtInputDevice *device) +{ + SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device); + + return symbols.udev_device_get_subsystem (self->dev); +} + +static const char * +srt_udev_input_device_get_sys_path (SrtInputDevice *device) +{ + SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device); + + return symbols.udev_device_get_syspath (self->dev); +} + +static gchar ** +srt_udev_input_device_dup_udev_properties (SrtInputDevice *device) +{ + SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device); + struct udev_list_entry *entries; + struct udev_list_entry *entry; + g_autoptr(GPtrArray) arr = g_ptr_array_new_with_free_func (g_free); + + entries = symbols.udev_device_get_properties_list_entry (self->dev); + + for (entry = entries; + entry != NULL; + entry = symbols.udev_list_entry_get_next (entry)) + { + const char *name = symbols.udev_list_entry_get_name (entry); + const char *value = symbols.udev_device_get_property_value (self->dev, name); + + g_ptr_array_add (arr, g_strdup_printf ("%s=%s", name, value)); + } + + g_ptr_array_add (arr, NULL); + return (gchar **) g_ptr_array_free (g_steal_pointer (&arr), FALSE); +} + +static gchar * +srt_udev_input_device_dup_uevent (SrtInputDevice *device) +{ + SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device); + + return g_strdup (symbols.udev_device_get_sysattr_value (self->dev, "uevent")); +} + +static const SrtEvdevCapabilities * +srt_udev_input_device_peek_event_capabilities (SrtInputDevice *device) +{ + SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device); + + return &self->evdev_caps; +} + +static const char * +srt_udev_input_device_get_hid_sys_path (SrtInputDevice *device) +{ + SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device); + + if (self->hid_ancestor.dev == NULL) + return NULL; + + return symbols.udev_device_get_syspath (self->hid_ancestor.dev); +} + +static gboolean +srt_udev_input_device_get_hid_identity (SrtInputDevice *device, + unsigned int *bus_type, + unsigned int *vendor_id, + unsigned int *product_id, + const char **name, + const char **phys, + const char **uniq) +{ + SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device); + + if (self->hid_ancestor.dev == NULL + && !has_all_flags (self->iface_flags, + SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID + | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE)) + return FALSE; + + if (bus_type != NULL) + *bus_type = self->hid_ancestor.bus_type; + + if (vendor_id != NULL) + *vendor_id = self->hid_ancestor.vendor_id; + + if (product_id != NULL) + *product_id = self->hid_ancestor.product_id; + + if (name != NULL + && !get_string (self->hid_ancestor.dev, "name", name)) + *name = NULL; + + if (phys != NULL + && !get_string (self->hid_ancestor.dev, "phys", phys)) + *phys = NULL; + + if (uniq != NULL + && !get_string (self->hid_ancestor.dev, "uniq", uniq)) + *uniq = NULL; + + return TRUE; +} + +static const char * +srt_udev_input_device_get_input_sys_path (SrtInputDevice *device) +{ + SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device); + + if (self->input_ancestor.dev == NULL) + return NULL; + + return symbols.udev_device_get_syspath (self->input_ancestor.dev); +} + +static gboolean +srt_udev_input_device_get_input_identity (SrtInputDevice *device, + unsigned int *bus_type, + unsigned int *vendor_id, + unsigned int *product_id, + unsigned int *version, + const char **name, + const char **phys, + const char **uniq) +{ + SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device); + + if (self->input_ancestor.dev == NULL + && !has_all_flags (self->iface_flags, + SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT + | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE)) + return FALSE; + + if (bus_type != NULL) + *bus_type = self->input_ancestor.bus_type; + + if (vendor_id != NULL) + *vendor_id = self->input_ancestor.vendor_id; + + if (product_id != NULL) + *product_id = self->input_ancestor.product_id; + + if (version != NULL) + *version = self->input_ancestor.version; + + if (name != NULL + && !get_string (self->input_ancestor.dev, "name", name)) + *name = NULL; + + if (phys != NULL + && !get_string (self->input_ancestor.dev, "phys", phys)) + *phys = NULL; + + if (uniq != NULL + && !get_string (self->input_ancestor.dev, "uniq", uniq)) + *uniq = NULL; + + return TRUE; +} + +static const char * +srt_udev_input_device_get_usb_device_sys_path (SrtInputDevice *device) +{ + SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device); + + if (self->usb_device_ancestor.dev == NULL) + return NULL; + + return symbols.udev_device_get_syspath (self->usb_device_ancestor.dev); +} + +static gboolean +srt_udev_input_device_get_usb_device_identity (SrtInputDevice *device, + unsigned int *vendor_id, + unsigned int *product_id, + unsigned int *device_version, + const char **manufacturer, + const char **product, + const char **serial) +{ + SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device); + + if (self->usb_device_ancestor.dev == NULL) + return FALSE; + + if (vendor_id != NULL) + *vendor_id = self->usb_device_ancestor.vendor_id; + + if (product_id != NULL) + *product_id = self->usb_device_ancestor.product_id; + + if (device_version != NULL) + *device_version = self->usb_device_ancestor.device_version; + + if (manufacturer != NULL + && !get_string (self->usb_device_ancestor.dev, + "manufacturer", manufacturer)) + *manufacturer = NULL; + + if (product != NULL + && !get_string (self->usb_device_ancestor.dev, + "product", product)) + *product = NULL; + + if (serial != NULL + && !get_string (self->usb_device_ancestor.dev, + "serial", serial)) + *serial = NULL; + + return TRUE; +} + +static gchar * +srt_udev_input_device_dup_hid_uevent (SrtInputDevice *device) +{ + SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device); + + if (self->hid_ancestor.dev == NULL) + return NULL; + + return g_strdup (symbols.udev_device_get_sysattr_value (self->hid_ancestor.dev, "uevent")); +} + +static gchar * +srt_udev_input_device_dup_input_uevent (SrtInputDevice *device) +{ + SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device); + + if (self->input_ancestor.dev == NULL) + return NULL; + + return g_strdup (symbols.udev_device_get_sysattr_value (self->input_ancestor.dev, "uevent")); +} + +static gchar * +srt_udev_input_device_dup_usb_device_uevent (SrtInputDevice *device) +{ + SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device); + + if (self->usb_device_ancestor.dev == NULL) + return NULL; + + return g_strdup (symbols.udev_device_get_sysattr_value (self->usb_device_ancestor.dev, "uevent")); +} + +static void +srt_udev_input_device_iface_init (SrtInputDeviceInterface *iface) +{ +#define IMPLEMENT(x) iface->x = srt_udev_input_device_ ## x + + IMPLEMENT (get_type_flags); + IMPLEMENT (get_interface_flags); + IMPLEMENT (get_dev_node); + IMPLEMENT (get_sys_path); + IMPLEMENT (get_subsystem); + IMPLEMENT (dup_udev_properties); + IMPLEMENT (dup_uevent); + IMPLEMENT (peek_event_capabilities); + + IMPLEMENT (get_hid_sys_path); + IMPLEMENT (get_input_sys_path); + IMPLEMENT (get_usb_device_sys_path); + + IMPLEMENT (get_hid_identity); + IMPLEMENT (get_input_identity); + IMPLEMENT (get_usb_device_identity); + + IMPLEMENT (dup_hid_uevent); + IMPLEMENT (dup_input_uevent); + IMPLEMENT (dup_usb_device_uevent); + +#undef IMPLEMENT +} + +typedef enum +{ + PROP_0, + PROP_FLAGS, + PROP_IS_ACTIVE, + N_PROPERTIES +} Property; + +static void +_srt_udev_input_device_monitor_init (SrtUdevInputDeviceMonitor *self) +{ + self->monitor_context = g_main_context_ref_thread_default (); + self->state = NOT_STARTED; + self->devices = g_hash_table_new_full (g_str_hash, + g_str_equal, + NULL, + g_object_unref); +} + +static void +srt_udev_input_device_monitor_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (object); + + switch ((Property) prop_id) + { + case PROP_FLAGS: + g_value_set_flags (value, self->flags); + break; + + case PROP_IS_ACTIVE: + g_value_set_boolean (value, (self->state == STARTED)); + break; + + case PROP_0: + case N_PROPERTIES: + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +srt_udev_input_device_monitor_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (object); + + switch ((Property) prop_id) + { + case PROP_FLAGS: + /* Construct-only */ + g_return_if_fail (self->flags == 0); + self->flags = g_value_get_flags (value); + break; + + case PROP_IS_ACTIVE: + case PROP_0: + case N_PROPERTIES: + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +srt_udev_input_device_monitor_dispose (GObject *object) +{ + srt_input_device_monitor_stop (SRT_INPUT_DEVICE_MONITOR (object)); + G_OBJECT_CLASS (_srt_udev_input_device_monitor_parent_class)->dispose (object); +} + +static void +srt_udev_input_device_monitor_finalize (GObject *object) +{ + srt_input_device_monitor_stop (SRT_INPUT_DEVICE_MONITOR (object)); + G_OBJECT_CLASS (_srt_udev_input_device_monitor_parent_class)->finalize (object); +} + +static void +_srt_udev_input_device_monitor_class_init (SrtUdevInputDeviceMonitorClass *cls) +{ + GObjectClass *object_class = G_OBJECT_CLASS (cls); + + object_class->get_property = srt_udev_input_device_monitor_get_property; + object_class->set_property = srt_udev_input_device_monitor_set_property; + object_class->dispose = srt_udev_input_device_monitor_dispose; + object_class->finalize = srt_udev_input_device_monitor_finalize; + + g_object_class_override_property (object_class, PROP_FLAGS, "flags"); + g_object_class_override_property (object_class, PROP_IS_ACTIVE, "is-active"); +} + +static gboolean +srt_udev_input_device_monitor_initable_init (GInitable *initable, + GCancellable *cancellable G_GNUC_UNUSED, + GError **error) +{ + static const char * const libs[] = { "libudev.so.1", "libudev.so.0" }; + void *handle = NULL; + const char *lib = NULL; + const char *which = NULL; + gsize i; + + /* Only initialize once per process */ + if (symbols.handle != NULL) + return TRUE; + + for (i = 0; i < G_N_ELEMENTS (libs); i++) + { + lib = libs[i]; + handle = dlopen (lib, RTLD_NOW|RTLD_LOCAL|RTLD_NODELETE); + + if (handle != NULL) + break; + } + + if (handle == NULL) + { + /* Try the first library again, for the side-effect of getting the + * error message */ + lib = libs[0]; + handle = dlopen (lib, RTLD_NOW|RTLD_LOCAL|RTLD_NODELETE); + } + + if (handle == NULL) + return glnx_throw (error, "Unable to load %s: %s", lib, dlerror ()); + +#define SYMBOL(name) \ + do \ + { \ + void *symbol; \ + \ + which = #name; \ + symbol = dlsym (handle, which); \ + \ + if (symbol != NULL) \ + symbols.name = (__typeof__ (symbols.name)) G_CALLBACK (symbol); \ + else \ + goto fail; \ + } \ + while (0) + + SYMBOL (udev_new); + SYMBOL (udev_unref); + + SYMBOL (udev_list_entry_get_name); + SYMBOL (udev_list_entry_get_next); + + SYMBOL (udev_device_new_from_syspath); + SYMBOL (udev_device_get_action); + SYMBOL (udev_device_get_devnode); + SYMBOL (udev_device_get_subsystem); + SYMBOL (udev_device_get_syspath); + SYMBOL (udev_device_get_properties_list_entry); + SYMBOL (udev_device_get_property_value); + SYMBOL (udev_device_get_sysattr_value); + SYMBOL (udev_device_get_parent_with_subsystem_devtype); + SYMBOL (udev_device_ref); + SYMBOL (udev_device_unref); + + SYMBOL (udev_monitor_new_from_netlink); + SYMBOL (udev_monitor_filter_add_match_subsystem_devtype); + SYMBOL (udev_monitor_enable_receiving); + SYMBOL (udev_monitor_receive_device); + SYMBOL (udev_monitor_get_fd); + SYMBOL (udev_monitor_unref); + + SYMBOL (udev_enumerate_new); + SYMBOL (udev_enumerate_add_match_subsystem); + SYMBOL (udev_enumerate_scan_devices); + SYMBOL (udev_enumerate_get_list_entry); + SYMBOL (udev_enumerate_unref); + +#undef SYMBOL + + symbols.handle = handle; + return TRUE; + +fail: + return glnx_throw (error, "Unable to find symbol %s in %s: %s", + which, lib, dlerror ()); +} + +static void +read_hid_ancestor (SrtUdevInputDevice *device) +{ + const char *uevent; + + if (device->hid_ancestor.dev == NULL) + return; + + uevent = symbols.udev_device_get_sysattr_value (device->hid_ancestor.dev, + "uevent"); + _srt_get_identity_from_hid_uevent (uevent, + &device->hid_ancestor.bus_type, + &device->hid_ancestor.vendor_id, + &device->hid_ancestor.product_id, + &device->hid_ancestor.name, + &device->hid_ancestor.phys, + &device->hid_ancestor.uniq); +} + +static void +read_input_ancestor (SrtUdevInputDevice *device) +{ + if (device->input_ancestor.dev == NULL) + return; + + get_uint32_hex (device->input_ancestor.dev, "id/bustype", + &device->input_ancestor.bus_type); + get_uint32_hex (device->input_ancestor.dev, "id/vendor", + &device->input_ancestor.vendor_id); + get_uint32_hex (device->input_ancestor.dev, "id/product", + &device->input_ancestor.product_id); + get_uint32_hex (device->input_ancestor.dev, "id/version", + &device->input_ancestor.version); + + get_evdev_caps_from_udev (device->input_ancestor.dev, + "capabilities/ev", + device->evdev_caps.ev, + G_N_ELEMENTS (device->evdev_caps.ev)); + get_evdev_caps_from_udev (device->input_ancestor.dev, + "capabilities/abs", + device->evdev_caps.abs, + G_N_ELEMENTS (device->evdev_caps.abs)); + get_evdev_caps_from_udev (device->input_ancestor.dev, + "capabilities/rel", + device->evdev_caps.rel, + G_N_ELEMENTS (device->evdev_caps.rel)); + get_evdev_caps_from_udev (device->input_ancestor.dev, + "capabilities/key", + device->evdev_caps.keys, + G_N_ELEMENTS (device->evdev_caps.keys)); + get_evdev_caps_from_udev (device->input_ancestor.dev, + "capabilities/ff", + device->evdev_caps.ff, + G_N_ELEMENTS (device->evdev_caps.ff)); + + /* These are not really capabilities as such, but the encoding is + * the same */ + get_evdev_caps_from_udev (device->input_ancestor.dev, + "properties", + device->evdev_caps.props, + G_N_ELEMENTS (device->evdev_caps.props)); +} + +static void +read_usb_device_ancestor (SrtUdevInputDevice *device) +{ + if (device->usb_device_ancestor.dev == NULL) + return; + + get_uint32_hex (device->usb_device_ancestor.dev, "idVendor", + &device->usb_device_ancestor.vendor_id); + get_uint32_hex (device->usb_device_ancestor.dev, "idProduct", + &device->usb_device_ancestor.product_id); + get_uint32_hex (device->usb_device_ancestor.dev, "bcdDevice", + &device->usb_device_ancestor.device_version); +} + +static gboolean +get_boolean_property (struct udev_device *dev, + const char *name, + gboolean def) +{ + const char *value = symbols.udev_device_get_property_value (dev, name); + + if (value == NULL) + return def; + + if (g_strcmp0 (value, "1") == 0) + return TRUE; + + if (g_strcmp0 (value, "0") == 0) + return FALSE; + + g_debug ("Boolean udev property %s has unexpected value %s", + name, value); + return def; +} + +static void +add_device (SrtUdevInputDeviceMonitor *self, + struct udev_device *dev) +{ + SrtUdevInputDevice *device = NULL; + const char *syspath; + const char *devnode; + gboolean want = FALSE; + gboolean is_evdev = FALSE; + gboolean is_hidraw = FALSE; + const char *slash; + int fd; + + syspath = symbols.udev_device_get_syspath (dev); + devnode = symbols.udev_device_get_devnode (dev); + + if (G_UNLIKELY (syspath == NULL)) + { + g_warning ("not adding udev_device with NULL syspath"); + return; + } + + /* If we added a device at this /sys path already, don't add it again. + * (This is for the "change" action, and the rarer "bind" action.) */ + if (g_hash_table_lookup_extended (self->devices, syspath, NULL, NULL)) + { + g_debug ("ignoring device \"%s\" which we already have", syspath); + return; + } + + if (devnode == NULL) + { + /* We only care about the devices that could, in principle, be + * opened */ + g_debug ("ignoring device \"%s\" with NULL device node", syspath); + return; + } + + slash = strrchr (syspath, '/'); + + if (slash != NULL) + { + if (self->want_hidraw && + g_str_has_prefix (slash + 1, "hidraw") && + _srt_str_is_integer (slash + 1 + strlen ("hidraw"))) + want = is_hidraw = TRUE; + + if (self->want_evdev && + g_str_has_prefix (slash + 1, "event") && + _srt_str_is_integer (slash + 1 + strlen ("event"))) + want = is_evdev = TRUE; + } + + if (!want) + { + g_debug ("ignoring uninteresting udev_device \"%s\"", syspath); + return; + } + + g_debug ("Adding device %s", syspath); + + device = g_object_new (SRT_TYPE_UDEV_INPUT_DEVICE, NULL); + device->dev = symbols.udev_device_ref (dev); + + if (is_evdev) + device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT; + + if (is_hidraw) + device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID; + + fd = open (devnode, O_RDONLY | O_NONBLOCK | _SRT_INPUT_DEVICE_ALWAYS_OPEN_FLAGS); + + if (fd >= 0) + { + device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE; + + if (_srt_get_identity_from_raw_hid (fd, + &device->hid_ancestor.bus_type, + &device->hid_ancestor.vendor_id, + &device->hid_ancestor.product_id, + NULL, + NULL, + NULL)) + { + g_debug ("%s is raw HID: bus type 0x%04x, vendor 0x%04x, product 0x%04x", + devnode, + device->hid_ancestor.bus_type, + device->hid_ancestor.vendor_id, + device->hid_ancestor.product_id); + device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID; + } + + if (_srt_get_identity_from_evdev (fd, + &device->input_ancestor.bus_type, + &device->input_ancestor.vendor_id, + &device->input_ancestor.product_id, + &device->input_ancestor.version, + NULL, + NULL, + NULL)) + { + g_debug ("%s is evdev: bus type 0x%04x, vendor 0x%04x, product 0x%04x, version 0x%04x", + devnode, + device->input_ancestor.bus_type, + device->input_ancestor.vendor_id, + device->input_ancestor.product_id, + device->input_ancestor.version); + device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT; + } + + _srt_evdev_capabilities_set_from_evdev (&device->evdev_caps, fd); + close (fd); + } + + fd = open (devnode, O_RDWR | O_NONBLOCK | _SRT_INPUT_DEVICE_ALWAYS_OPEN_FLAGS); + + if (fd >= 0) + { + device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE; + close (fd); + } + + device->hid_ancestor.dev = symbols.udev_device_get_parent_with_subsystem_devtype (device->dev, + "hid", + NULL); + read_hid_ancestor (device); + device->input_ancestor.dev = find_input_ancestor (device->dev); + read_input_ancestor (device); + + if (device->hid_ancestor.bus_type == BUS_USB + || device->input_ancestor.bus_type == BUS_USB) + { + device->usb_device_ancestor.dev = symbols.udev_device_get_parent_with_subsystem_devtype (device->dev, + "usb", + "usb_device"); + read_usb_device_ancestor (device); + } + + if (get_boolean_property (device->dev, "ID_INPUT_JOYSTICK", FALSE)) + device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK; + + if (get_boolean_property (device->dev, "ID_INPUT_ACCELEROMETER", FALSE)) + device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_ACCELEROMETER; + + if (get_boolean_property (device->dev, "ID_INPUT_KEYBOARD", FALSE)) + device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_KEYBOARD; + + if (get_boolean_property (device->dev, "ID_INPUT_KEY", FALSE)) + device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS; + + if (get_boolean_property (device->dev, "ID_INPUT_MOUSE", FALSE)) + device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_MOUSE; + + if (get_boolean_property (device->dev, "ID_INPUT_TOUCHPAD", FALSE)) + device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHPAD; + + if (get_boolean_property (device->dev, "ID_INPUT_TOUCHSCREEN", FALSE)) + device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHSCREEN; + + if (get_boolean_property (device->dev, "ID_INPUT_TABLET", FALSE)) + device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_TABLET; + + if (get_boolean_property (device->dev, "ID_INPUT_TABLET_PAD", FALSE)) + device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_TABLET_PAD; + + if (get_boolean_property (device->dev, "ID_INPUT_POINTING_STICK", FALSE)) + device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_POINTING_STICK; + + if (get_boolean_property (device->dev, "ID_INPUT_SWITCH", FALSE)) + device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_SWITCH; + + if (device->type_flags == SRT_INPUT_DEVICE_TYPE_FLAGS_NONE) + { + /* Older versions of udev put classes on input devices */ + const char *value = symbols.udev_device_get_property_value (device->dev, + "ID_CLASS"); + + if (g_strcmp0 (value, "joystick") == 0) + device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK; + else if (g_strcmp0 (value, "mouse") == 0) + device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_MOUSE; + else if (g_strcmp0 (value, "kbd") == 0) + device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_KEYBOARD; + } + + if (device->type_flags == SRT_INPUT_DEVICE_TYPE_FLAGS_NONE + && !get_boolean_property (device->dev, "ID_INPUT", FALSE)) + { + g_debug ("input_id builtin did not run, guessing device type..."); + device->type_flags = _srt_evdev_capabilities_guess_type (&device->evdev_caps); + } + + g_hash_table_replace (self->devices, (char *) syspath, device); + _srt_input_device_monitor_emit_added (SRT_INPUT_DEVICE_MONITOR (self), + SRT_INPUT_DEVICE (device)); +} + +static void +remove_device (SrtUdevInputDeviceMonitor *self, + struct udev_device *dev) +{ + void *device = NULL; + const char *syspath; + + syspath = symbols.udev_device_get_syspath (dev); + + if (G_UNLIKELY (syspath == NULL)) + { + g_warning ("trying to remove udev_device with NULL syspath"); + symbols.udev_device_unref (dev); + return; + } + + g_debug ("Removing device %s", syspath); + + if (g_hash_table_lookup_extended (self->devices, syspath, NULL, &device)) + { + g_hash_table_steal (self->devices, syspath); + _srt_input_device_monitor_emit_removed (SRT_INPUT_DEVICE_MONITOR (self), + device); + g_object_unref (device); + } +} + +static gboolean +srt_udev_input_device_monitor_cb (int fd, + GIOCondition condition, + void *user_data) +{ + SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (user_data); + struct udev_device *dev; + const char *action; + + g_return_val_if_fail (SRT_IS_UDEV_INPUT_DEVICE_MONITOR (self), G_SOURCE_REMOVE); + + dev = symbols.udev_monitor_receive_device (self->monitor); + + if (dev == NULL) + return G_SOURCE_CONTINUE; + + action = symbols.udev_device_get_action (dev); + + if (g_strcmp0 (action, "remove") == 0) + remove_device (self, dev); + else + add_device (self, dev); + + g_clear_pointer (&dev, symbols.udev_device_unref); + return G_SOURCE_CONTINUE; +} + +static void +srt_udev_input_device_monitor_request_raw_hid (SrtInputDeviceMonitor *monitor) +{ + SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (monitor); + + g_return_if_fail (SRT_IS_UDEV_INPUT_DEVICE_MONITOR (monitor)); + g_return_if_fail (self->state == NOT_STARTED); + + self->want_hidraw = TRUE; +} + +static void +srt_udev_input_device_monitor_request_evdev (SrtInputDeviceMonitor *monitor) +{ + SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (monitor); + + g_return_if_fail (SRT_IS_UDEV_INPUT_DEVICE_MONITOR (monitor)); + g_return_if_fail (self->state == NOT_STARTED); + + self->want_evdev = TRUE; +} + +static gboolean +enumerate_cb (gpointer user_data) +{ + SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (user_data); + + if (self->want_hidraw || self->want_evdev) + { + struct udev_enumerate *enumerator = NULL; + struct udev_list_entry *devs; + struct udev_list_entry *item; + + enumerator = symbols.udev_enumerate_new (self->context); + + if (enumerator == NULL) + { + g_warning ("udev_enumerate_new: %s", g_strerror (errno)); + goto out; + } + + if (self->want_evdev) + symbols.udev_enumerate_add_match_subsystem (enumerator, "input"); + + if (self->want_hidraw) + symbols.udev_enumerate_add_match_subsystem (enumerator, "hidraw"); + + symbols.udev_enumerate_scan_devices (enumerator); + devs = symbols.udev_enumerate_get_list_entry (enumerator); + + for (item = devs; item != NULL; item = symbols.udev_list_entry_get_next (item)) + { + const char *syspath = symbols.udev_list_entry_get_name (item); + struct udev_device *dev; + + dev = symbols.udev_device_new_from_syspath (self->context, syspath); + + if (dev != NULL) + add_device (self, dev); + else + g_warning ("udev_device_new_from_syspath \"%s\": %s", + syspath, g_strerror (errno)); + + symbols.udev_device_unref (dev); + } + + symbols.udev_enumerate_unref (enumerator); + } + +out: + _srt_input_device_monitor_emit_all_for_now (SRT_INPUT_DEVICE_MONITOR (self)); + + return G_SOURCE_REMOVE; +} + +static gboolean +srt_udev_input_device_monitor_start (SrtInputDeviceMonitor *monitor, + GError **error) +{ + SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (monitor); + + g_return_val_if_fail (SRT_IS_UDEV_INPUT_DEVICE_MONITOR (monitor), FALSE); + g_return_val_if_fail (self->state == NOT_STARTED, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + /* If this assertion fails, g_initable_init() failed or wasn't called */ + g_return_val_if_fail (symbols.handle != NULL, FALSE); + + self->state = STARTED; + + self->context = symbols.udev_new (); + + if (self->context == NULL) + return glnx_throw_errno_prefix (error, "udev_new"); + + if ((self->flags & SRT_INPUT_DEVICE_MONITOR_FLAGS_ONCE) == 0 + && (self->want_hidraw || self->want_evdev)) + { + int fd; + + self->monitor = symbols.udev_monitor_new_from_netlink (self->context, "udev"); + + if (self->monitor == NULL) + return glnx_throw_errno_prefix (error, "udev_monitor_new_from_netlink"); + + if (self->want_evdev) + symbols.udev_monitor_filter_add_match_subsystem_devtype (self->monitor, "input", NULL); + + if (self->want_hidraw) + symbols.udev_monitor_filter_add_match_subsystem_devtype (self->monitor, "hidraw", NULL); + + symbols.udev_monitor_enable_receiving (self->monitor); + + fd = symbols.udev_monitor_get_fd (self->monitor); + + if (fd < 0) + { + errno = -fd; + return glnx_throw_errno_prefix (error, "udev_monitor_get_fd"); + } + + self->monitor_source = g_unix_fd_source_new (fd, G_IO_IN); + g_source_set_callback (self->monitor_source, + (GSourceFunc) G_CALLBACK (srt_udev_input_device_monitor_cb), + self, NULL); + g_source_set_priority (self->monitor_source, G_PRIORITY_DEFAULT); + g_source_attach (self->monitor_source, self->monitor_context); + } + + /* Make sure the signals for the initial batch of devices are emitted in + * the correct main-context */ + g_main_context_invoke_full (self->monitor_context, G_PRIORITY_DEFAULT, + enumerate_cb, g_object_ref (self), + g_object_unref); + + return TRUE; +} + +static void +srt_udev_input_device_monitor_stop (SrtInputDeviceMonitor *monitor) +{ + SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (monitor); + + self->state = STOPPED; + + if (self->monitor_source != NULL) + g_source_destroy (self->monitor_source); + + g_clear_pointer (&self->monitor_source, g_source_unref); + g_clear_pointer (&self->monitor_context, g_main_context_unref); + g_clear_pointer (&self->monitor, symbols.udev_monitor_unref); + g_clear_pointer (&self->context, symbols.udev_unref); + g_clear_pointer (&self->devices, g_hash_table_unref); +} + +static void +srt_udev_input_device_monitor_iface_init (SrtInputDeviceMonitorInterface *iface) +{ +#define IMPLEMENT(x) iface->x = srt_udev_input_device_monitor_ ## x + IMPLEMENT (request_raw_hid); + IMPLEMENT (request_evdev); + IMPLEMENT (start); + IMPLEMENT (stop); +#undef IMPLEMENT +} + +static void +srt_udev_input_device_monitor_initable_iface_init (GInitableIface *iface) +{ + iface->init = srt_udev_input_device_monitor_initable_init; +} diff --git a/steam-runtime-tools/utils-internal.h b/steam-runtime-tools/utils-internal.h index 3a7f0fba5871441d952de90dd25b7b23d188cc4d..7e06650ce58a47fbef63880cb3268263afc1df48 100644 --- a/steam-runtime-tools/utils-internal.h +++ b/steam-runtime-tools/utils-internal.h @@ -101,3 +101,5 @@ gboolean _srt_file_test_in_sysroot (const char *sysroot, G_GNUC_INTERNAL const char * const *_srt_peek_environ_nonnull (void); G_GNUC_INTERNAL void _srt_setenv_disable_gio_modules (void); + +G_GNUC_INTERNAL gboolean _srt_str_is_integer (const char *str); diff --git a/steam-runtime-tools/utils.c b/steam-runtime-tools/utils.c index 074a8ad3b19daec307893830f3e7938f097554f2..5601a1778b291ea0d469f3629d41ba793e031c47 100644 --- a/steam-runtime-tools/utils.c +++ b/steam-runtime-tools/utils.c @@ -961,3 +961,28 @@ _srt_setenv_disable_gio_modules (void) g_setenv ("GIO_USE_VFS", "local", TRUE); g_setenv ("GIO_MODULE_DIR", "/nonexistent", TRUE); } + +/* + * Return TRUE if @str points to one or more decimal digits, followed + * by nul termination. This is the same as Python bytes.isdigit(). + */ +gboolean +_srt_str_is_integer (const char *str) +{ + const char *p; + + g_return_val_if_fail (str != NULL, FALSE); + + if (*str == '\0') + return FALSE; + + for (p = str; + *p != '\0'; + p++) + { + if (!g_ascii_isdigit (*p)) + break; + } + + return (*p == '\0'); +} diff --git a/sysroot/run-in-sysroot.py b/sysroot/run-in-sysroot.py index b22b927c63e6f3b4f94a1eebaf56e5edaea4aaa2..73d07ceebda0e76b45a750b95ebb565447089ffe 100755 --- a/sysroot/run-in-sysroot.py +++ b/sysroot/run-in-sysroot.py @@ -110,6 +110,8 @@ def main(): '--tmpfs', '/tmp', '--tmpfs', '/var/tmp', '--tmpfs', '/home', + '--tmpfs', '/run', + '--tmpfs', '/run/host', '--bind', abs_srcdir, abs_srcdir, '--bind', abs_builddir, abs_builddir, ] diff --git a/tests/input-device.c b/tests/input-device.c new file mode 100644 index 0000000000000000000000000000000000000000..659faeecb5f40cb5f1d0e227ec6bb0faf4acc3ee --- /dev/null +++ b/tests/input-device.c @@ -0,0 +1,2541 @@ +/* + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include <libglnx.h> + +#include <steam-runtime-tools/steam-runtime-tools.h> + +#include <linux/input.h> + +#include <glib.h> +#include <glib/gstdio.h> +#include <glib-object.h> + +#include "steam-runtime-tools/json-glib-backports-internal.h" +#include "steam-runtime-tools/utils-internal.h" +#include "mock-input-device.h" +#include "test-utils.h" + +static const char *argv0; + +typedef struct +{ + enum { MOCK, DIRECT, UDEV } type; +} Config; + +static const Config defconfig = +{ + .type = MOCK, +}; + +static const Config direct_config = +{ + .type = DIRECT, +}; + +static const Config udev_config = +{ + .type = UDEV, +}; + +typedef struct +{ + const Config *config; + gchar *srcdir; + gchar *builddir; + GMainContext *monitor_context; + GPtrArray *log; + gboolean skipped; +} Fixture; + +static void +setup (Fixture *f, + gconstpointer context) +{ + const Config *config = context; + GStatBuf sb; + + f->srcdir = g_strdup (g_getenv ("G_TEST_SRCDIR")); + f->builddir = g_strdup (g_getenv ("G_TEST_BUILDDIR")); + + if (f->srcdir == NULL) + f->srcdir = g_path_get_dirname (argv0); + + if (f->builddir == NULL) + f->builddir = g_path_get_dirname (argv0); + + if (config == NULL) + f->config = &defconfig; + else + f->config = config; + + if (f->config->type == DIRECT && g_stat ("/dev/input", &sb) != 0) + { + g_test_skip ("/dev/input not available"); + f->skipped = TRUE; + } + + f->log = g_ptr_array_new_with_free_func (g_free); +} + +static void +teardown (Fixture *f, + gconstpointer context) +{ + G_GNUC_UNUSED const Config *config = context; + + g_free (f->srcdir); + g_free (f->builddir); + + g_clear_pointer (&f->log, g_ptr_array_unref); + g_clear_pointer (&f->monitor_context, g_main_context_unref); +} + +#define VENDOR_VALVE 0x28de +#define PRODUCT_VALVE_STEAM_CONTROLLER 0x1142 + +static SrtSimpleInputDevice * +load_json (Fixture *f, + const char *filename) +{ + g_autoptr(SrtSimpleInputDevice) simple = NULL; + g_autoptr(JsonParser) parser = NULL; + g_autoptr(GError) error = NULL; + JsonNode *node; + JsonObject *object; + JsonObject *added; + g_autofree gchar *path = NULL; + + parser = json_parser_new (); + + path = g_build_filename (f->srcdir, "input-monitor-outputs", + filename, NULL); + json_parser_load_from_file (parser, path, &error); + g_assert_no_error (error); + node = json_parser_get_root (parser); + g_assert_nonnull (node); + object = json_node_get_object (node); + g_assert_nonnull (object); + added = json_object_get_object_member (object, "added"); + g_assert_nonnull (added); + simple = _srt_simple_input_device_new_from_json (added); + g_assert_nonnull (simple); + + return g_steal_pointer (&simple); +} + +static void +test_input_device_from_json_no_details (Fixture *f) +{ + g_autoptr(SrtSimpleInputDevice) simple = load_json (f, "no-details.json"); + SrtInputDevice *dev = SRT_INPUT_DEVICE (simple); + struct + { + unsigned int bus_type; + unsigned int vendor_id; + unsigned int product_id; + unsigned int version; + } identity; + unsigned long bits[LONGS_FOR_BITS (HIGHEST_EVENT_CODE)]; + g_autofree gchar *uevent = NULL; + + g_assert_cmphex (srt_input_device_get_interface_flags (dev), + ==, SRT_INPUT_DEVICE_INTERFACE_FLAGS_NONE); + g_assert_cmphex (srt_input_device_get_type_flags (dev), + ==, SRT_INPUT_DEVICE_TYPE_FLAGS_NONE); + g_assert_cmpstr (srt_input_device_get_dev_node (dev), ==, NULL); + g_assert_cmpstr (srt_input_device_get_subsystem (dev), ==, NULL); + g_assert_cmpstr (srt_input_device_get_sys_path (dev), ==, NULL); + + g_assert_false (srt_input_device_get_identity (dev, + NULL, NULL, NULL, NULL)); + g_assert_false (srt_input_device_get_identity (dev, + &identity.bus_type, + &identity.vendor_id, + &identity.product_id, + &identity.version)); + + g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, 0, + bits, + G_N_ELEMENTS (bits)), + ==, 1); + g_assert_cmphex (bits[0], ==, 0); + g_assert_cmphex (bits[1], ==, 0); + + g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, EV_ABS, + bits, + G_N_ELEMENTS (bits)), + >=, 1); + g_assert_cmphex (bits[0], ==, 0); + + g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, EV_REL, + bits, + G_N_ELEMENTS (bits)), + >=, 1); + g_assert_cmphex (bits[0], ==, 0); + + g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, EV_KEY, + bits, + G_N_ELEMENTS (bits)), + >=, 1); + g_assert_cmphex (bits[0], ==, 0); + g_assert_cmpuint (srt_input_device_get_input_properties (dev, bits, + G_N_ELEMENTS (bits)), + ==, 1); + g_assert_cmphex (bits[0], ==, 0); + + uevent = srt_input_device_dup_uevent (dev); + g_assert_cmpstr (uevent, ==, NULL); + + g_assert_cmpstr (srt_input_device_get_hid_sys_path (dev), ==, NULL); + g_assert_false (srt_input_device_get_hid_identity (dev, + NULL, NULL, NULL, + NULL, NULL, NULL)); + uevent = srt_input_device_dup_hid_uevent (dev); + g_assert_cmpstr (uevent, ==, NULL); + + g_assert_cmpstr (srt_input_device_get_input_sys_path (dev), ==, NULL); + g_assert_false (srt_input_device_get_input_identity (dev, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL)); + uevent = srt_input_device_dup_input_uevent (dev); + g_assert_cmpstr (uevent, ==, NULL); + + g_assert_cmpstr (srt_input_device_get_usb_device_sys_path (dev), ==, NULL); + g_assert_false (srt_input_device_get_usb_device_identity (dev, + NULL, NULL, NULL, + NULL, NULL, NULL)); + uevent = srt_input_device_dup_usb_device_uevent (dev); + g_assert_cmpstr (uevent, ==, NULL); +} + +static void +test_input_device_from_json_odd (Fixture *f) +{ + g_autoptr(SrtSimpleInputDevice) simple = load_json (f, "odd.json"); + SrtInputDevice *dev = SRT_INPUT_DEVICE (simple); + struct + { + unsigned int bus_type; + unsigned int vendor_id; + unsigned int product_id; + const char *name; + const char *phys; + const char *uniq; + } hid_identity; + struct + { + unsigned int bus_type; + unsigned int vendor_id; + unsigned int product_id; + unsigned int version; + const char *name; + const char *phys; + const char *uniq; + } input_identity; + struct + { + unsigned int vendor_id; + unsigned int product_id; + unsigned int version; + const char *manufacturer; + const char *product; + const char *serial; + } usb_identity; + unsigned long bits[LONGS_FOR_BITS (HIGHEST_EVENT_CODE)]; + + g_assert_cmphex (srt_input_device_get_interface_flags (dev), + ==, SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID); + g_assert_cmphex (srt_input_device_get_type_flags (dev), + ==, SRT_INPUT_DEVICE_TYPE_FLAGS_NONE); + g_assert_cmpstr (srt_input_device_get_dev_node (dev), ==, NULL); + g_assert_cmpstr (srt_input_device_get_subsystem (dev), ==, NULL); + g_assert_cmpstr (srt_input_device_get_sys_path (dev), ==, NULL); + + g_assert_false (srt_input_device_get_identity (dev, + NULL, NULL, NULL, NULL)); + + g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, 0, + bits, + G_N_ELEMENTS (bits)), + ==, 1); + g_assert_cmphex (bits[0], ==, 0); + + g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, EV_ABS, + bits, + G_N_ELEMENTS (bits)), + >=, 1); +#if defined(__LP64__) + g_assert_cmphex (bits[0], ==, 0x0807060504030201UL); +#elif defined(__i386__) + g_assert_cmphex (bits[0], ==, 0x04030201UL); + g_assert_cmphex (bits[1], ==, 0x08070605UL); +#endif + + g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, EV_REL, + bits, + G_N_ELEMENTS (bits)), + >=, 1); + g_assert_cmphex (bits[0], ==, 0); + + g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, EV_KEY, + bits, + G_N_ELEMENTS (bits)), + >=, 1); + g_assert_cmphex (bits[0], ==, 0); + g_assert_cmpuint (srt_input_device_get_input_properties (dev, bits, + G_N_ELEMENTS (bits)), + ==, 1); +#if defined(__LP64__) + g_assert_cmphex (bits[0], ==, 0x2143658778563412UL); +#elif defined(__i386__) + g_assert_cmphex (bits[0], ==, 0x78563412UL); +#endif + g_assert_cmphex (bits[1], ==, 0); + + g_assert_true (srt_input_device_get_hid_identity (dev, + NULL, NULL, NULL, + NULL, NULL, NULL)); + g_assert_true (srt_input_device_get_hid_identity (dev, + &hid_identity.bus_type, + &hid_identity.vendor_id, + &hid_identity.product_id, + &hid_identity.name, + &hid_identity.phys, + &hid_identity.uniq)); + g_assert_cmphex (hid_identity.bus_type, ==, 0xfff1); + g_assert_cmphex (hid_identity.vendor_id, ==, 0xfff1); + g_assert_cmphex (hid_identity.product_id, ==, 0xfff1); + g_assert_cmpstr (hid_identity.name, ==, "Acme Weird Device"); + g_assert_cmpstr (hid_identity.phys, ==, NULL); + g_assert_cmpstr (hid_identity.uniq, ==, "12345678"); + + g_assert_true (srt_input_device_get_input_identity (dev, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL)); + g_assert_true (srt_input_device_get_input_identity (dev, + &input_identity.bus_type, + &input_identity.vendor_id, + &input_identity.product_id, + &input_identity.version, + &input_identity.name, + &input_identity.phys, + &input_identity.uniq)); + g_assert_cmphex (input_identity.bus_type, ==, 0xfff2); + g_assert_cmphex (input_identity.vendor_id, ==, 0xfff2); + g_assert_cmphex (input_identity.product_id, ==, 0xfff2); + g_assert_cmphex (input_identity.version, ==, 0); + g_assert_cmpstr (input_identity.name, ==, NULL); + g_assert_cmpstr (input_identity.phys, ==, NULL); + g_assert_cmpstr (input_identity.uniq, ==, "1234-5678"); + + g_assert_cmpstr (srt_input_device_get_usb_device_sys_path (dev), ==, "/..."); + g_assert_true (srt_input_device_get_usb_device_identity (dev, + NULL, NULL, NULL, + NULL, NULL, NULL)); + g_assert_true (srt_input_device_get_usb_device_identity (dev, + &usb_identity.vendor_id, + &usb_identity.product_id, + &usb_identity.version, + &usb_identity.manufacturer, + &usb_identity.product, + &usb_identity.serial)); + g_assert_cmphex (usb_identity.vendor_id, ==, 0xfff3); + g_assert_cmphex (usb_identity.product_id, ==, 0xfff3); + g_assert_cmphex (usb_identity.version, ==, 0); + g_assert_cmpstr (usb_identity.manufacturer, ==, NULL); + g_assert_cmpstr (usb_identity.product, ==, NULL); + g_assert_cmpstr (usb_identity.serial, ==, "12:34:56:78"); +} + +static void +test_input_device_from_json_steam_controller (Fixture *f) +{ + g_autoptr(SrtSimpleInputDevice) simple = load_json (f, "steam-controller.json"); + SrtInputDevice *dev = SRT_INPUT_DEVICE (simple); + struct + { + unsigned int bus_type; + unsigned int vendor_id; + unsigned int product_id; + unsigned int version; + } identity; + struct + { + unsigned int bus_type; + unsigned int vendor_id; + unsigned int product_id; + const char *name; + const char *phys; + const char *uniq; + } hid_identity; + struct + { + unsigned int bus_type; + unsigned int vendor_id; + unsigned int product_id; + unsigned int version; + const char *name; + const char *phys; + const char *uniq; + } input_identity; + struct + { + unsigned int vendor_id; + unsigned int product_id; + unsigned int version; + const char *manufacturer; + const char *product; + const char *serial; + } usb_identity; + unsigned long bits[LONGS_FOR_BITS (HIGHEST_EVENT_CODE)]; + g_autofree gchar *uevent = NULL; + + g_assert_cmphex (srt_input_device_get_interface_flags (dev), + ==, SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT); + g_assert_cmphex (srt_input_device_get_type_flags (dev), + ==, (SRT_INPUT_DEVICE_TYPE_FLAGS_KEYBOARD + | SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS + | SRT_INPUT_DEVICE_TYPE_FLAGS_MOUSE)); + g_assert_cmpstr (srt_input_device_get_dev_node (dev), + ==, "/dev/input/event20"); + g_assert_cmpstr (srt_input_device_get_subsystem (dev), + ==, "input"); + g_assert_cmpstr (srt_input_device_get_sys_path (dev), + ==, "/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.1/1-1.1:1.0/0003:28DE:1142.00DD/input/input308/event20"); + + g_assert_true (srt_input_device_get_identity (dev, + NULL, NULL, NULL, NULL)); + g_assert_true (srt_input_device_get_identity (dev, + &identity.bus_type, + &identity.vendor_id, + &identity.product_id, + &identity.version)); + /* Using magic numbers rather than a #define here so that it's easier + * to validate against the JSON */ + g_assert_cmphex (identity.bus_type, ==, 0x0003); + g_assert_cmphex (identity.vendor_id, ==, 0x28de); + g_assert_cmphex (identity.product_id, ==, 0x1142); + g_assert_cmphex (identity.version, ==, 0x0111); + + g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, 0, + bits, + G_N_ELEMENTS (bits)), + ==, 1); + g_assert_cmphex (bits[0], ==, 0x120017); + g_assert_cmphex (bits[1], ==, 0); + + g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, EV_ABS, + bits, + G_N_ELEMENTS (bits)), + >=, 1); + g_assert_cmphex (bits[0], ==, 0); + + g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, EV_REL, + bits, + G_N_ELEMENTS (bits)), + >=, 1); + g_assert_cmphex (bits[0], ==, 0x0903); + + g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, EV_KEY, + bits, + G_N_ELEMENTS (bits)), + >=, 1); +#if defined(__LP64__) + g_assert_cmphex (bits[0], ==, 0xfffffffffffffffeUL); + g_assert_cmphex (bits[1], ==, 0xe080ffdf01cfffffUL); + g_assert_cmphex (bits[2], ==, 0); + g_assert_cmphex (bits[3], ==, 0); + g_assert_cmphex (bits[4], ==, 0x1f0000); + g_assert_cmphex (bits[5], ==, 0); +#elif defined(__i386__) + g_assert_cmphex (bits[0], ==, 0xfffffffeUL); + g_assert_cmphex (bits[1], ==, 0xffffffffUL); + g_assert_cmphex (bits[2], ==, 0x01cfffffUL); + g_assert_cmphex (bits[3], ==, 0xe080ffdfUL); + g_assert_cmphex (bits[4], ==, 0); + g_assert_cmphex (bits[5], ==, 0); + g_assert_cmphex (bits[6], ==, 0); + g_assert_cmphex (bits[7], ==, 0); + g_assert_cmphex (bits[8], ==, 0x1f0000); + g_assert_cmphex (bits[9], ==, 0); + g_assert_cmphex (bits[10], ==, 0); + g_assert_cmphex (bits[11], ==, 0); +#endif + g_assert_cmpuint (srt_input_device_get_input_properties (dev, bits, + G_N_ELEMENTS (bits)), + ==, 1); + g_assert_cmphex (bits[0], ==, 0); + + uevent = srt_input_device_dup_uevent (dev); + g_assert_cmpstr (uevent, ==, + "MAJOR=13\n" + "MINOR=84\n" + "DEVNAME=input/event20\n"); + g_clear_pointer (&uevent, g_free); + + g_assert_cmpstr (srt_input_device_get_hid_sys_path (dev), + ==, "/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.1/1-1.1:1.0/0003:28DE:1142.00DD"); + g_assert_true (srt_input_device_get_hid_identity (dev, + NULL, NULL, NULL, + NULL, NULL, NULL)); + g_assert_true (srt_input_device_get_hid_identity (dev, + &hid_identity.bus_type, + &hid_identity.vendor_id, + &hid_identity.product_id, + &hid_identity.name, + &hid_identity.phys, + &hid_identity.uniq)); + g_assert_cmphex (hid_identity.bus_type, ==, 0x0003); + g_assert_cmphex (hid_identity.vendor_id, ==, 0x28de); + g_assert_cmphex (hid_identity.product_id, ==, 0x1142); + g_assert_cmpstr (hid_identity.name, ==, "Valve Software Steam Controller"); + g_assert_cmpstr (hid_identity.phys, ==, "usb-0000:00:14.0-1.1/input0"); + g_assert_cmpstr (hid_identity.uniq, ==, ""); + uevent = srt_input_device_dup_hid_uevent (dev); + g_assert_cmpstr (uevent, ==, + "DRIVER=hid-steam\n" + "HID_ID=0003:000028DE:00001142\n" + "HID_NAME=Valve Software Steam Controller\n" + "HID_PHYS=usb-0000:00:14.0-1.1/input0\n" + "HID_UNIQ=\n" + "MODALIAS=hid:b0003g0001v000028DEp00001142\n"); + g_clear_pointer (&uevent, g_free); + + g_assert_cmpstr (srt_input_device_get_input_sys_path (dev), + ==, "/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.1/1-1.1:1.0/0003:28DE:1142.00DD/input/input308"); + g_assert_true (srt_input_device_get_input_identity (dev, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL)); + g_assert_true (srt_input_device_get_input_identity (dev, + &input_identity.bus_type, + &input_identity.vendor_id, + &input_identity.product_id, + &input_identity.version, + &input_identity.name, + &input_identity.phys, + &input_identity.uniq)); + g_assert_cmphex (input_identity.bus_type, ==, 0x0003); + g_assert_cmphex (input_identity.vendor_id, ==, 0x28de); + g_assert_cmphex (input_identity.product_id, ==, 0x1142); + g_assert_cmphex (input_identity.version, ==, 0x0111); + g_assert_cmpstr (input_identity.name, ==, "Valve Software Steam Controller"); + g_assert_cmpstr (input_identity.phys, ==, "usb-0000:00:14.0-1.1/input0"); + g_assert_cmpstr (input_identity.uniq, ==, NULL); + uevent = srt_input_device_dup_input_uevent (dev); + g_assert_cmpstr (uevent, ==, + "PRODUCT=3/28de/1142/111\n" + "NAME=\"Valve Software Steam Controller\"\n" + "PHYS=\"usb-0000:00:14.0-1.1/input0\"\n" + "UNIQ=\"\"\n" + "PROP=0\n" + "EV=120017\n" + "KEY=1f0000 0 0 e080ffdf01cfffff fffffffffffffffe\n" + "REL=903\n" + "MSC=10\n" + "LED=1f\n" + "MODALIAS=input:b0003v28DEp1142e0111-e0,1,2,4,11,14,k77,7D,7E,7F,110,111,112,113,114,r0,1,8,B,am4,l0,1,2,3,4,sfw\n"); + g_clear_pointer (&uevent, g_free); + + g_assert_cmpstr (srt_input_device_get_usb_device_sys_path (dev), + ==, "/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.1"); + g_assert_true (srt_input_device_get_usb_device_identity (dev, + NULL, NULL, NULL, + NULL, NULL, NULL)); + g_assert_true (srt_input_device_get_usb_device_identity (dev, + &usb_identity.vendor_id, + &usb_identity.product_id, + &usb_identity.version, + &usb_identity.manufacturer, + &usb_identity.product, + &usb_identity.serial)); + g_assert_cmphex (usb_identity.vendor_id, ==, 0x28de); + g_assert_cmphex (usb_identity.product_id, ==, 0x1142); + g_assert_cmphex (usb_identity.version, ==, 0x0001); + g_assert_cmpstr (usb_identity.manufacturer, ==, "Valve Software"); + g_assert_cmpstr (usb_identity.product, ==, "Steam Controller"); + g_assert_cmpstr (usb_identity.serial, ==, NULL); + uevent = srt_input_device_dup_usb_device_uevent (dev); + g_assert_cmpstr (uevent, ==, + "MAJOR=189\n" + "MINOR=66\n" + "DEVNAME=bus/usb/001/067\n" + "DEVTYPE=usb_device\n" + "DRIVER=usb\n" + "PRODUCT=28de/1142/1\n" + "TYPE=0/0/0\n" + "BUSNUM=001\n" + "DEVNUM=067\n"); + g_clear_pointer (&uevent, g_free); +} + +static void +test_input_device_from_json (Fixture *f, + gconstpointer context) +{ + test_input_device_from_json_no_details (f); + test_input_device_from_json_odd (f); + test_input_device_from_json_steam_controller (f); +} + +typedef struct +{ + const char *name; + guint16 bus_type; + guint16 vendor_id; + guint16 product_id; + guint16 version; + guint8 ev[(EV_MAX + 1) / 8]; + guint8 keys[(KEY_MAX + 1) / 8]; + guint8 abs[(ABS_MAX + 1) / 8]; + guint8 rel[(REL_MAX + 1) / 8]; + guint8 ff[(FF_MAX + 1) / 8]; + guint8 props[INPUT_PROP_MAX / 8]; + SrtInputDeviceTypeFlags expected; +} GuessTest; + +/* + * Test-cases for guessing a device type from its capabilities. + * + * The bytes in ev, etc. are in little-endian byte order, the same as + * the JSON output from input-monitor. Trailing zeroes can be omitted. + */ +#define ZEROx4 0, 0, 0, 0 +#define ZEROx8 ZEROx4, ZEROx4 +#define FFx4 0xff, 0xff, 0xff, 0xff +#define FFx8 FFx4, FFx4 + +static const GuessTest guess_tests[] = +{ + { + .name = "Xbox 360 wired USB controller", + .bus_type = 0x0003, + .vendor_id = 0x045e, + .product_id = 0x028e, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, KEY, ABS, FF */ + .ev = { 0x0b, 0x00, 0x20 }, + /* X, Y, Z, RX, RY, RZ, HAT0X, HAT0Y */ + .abs = { 0x3f, 0x00, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* A, B, X, Y, TL, TR, SELECT, START, MODE, THUMBL, THUMBR */ + /* 0x100 */ ZEROx4, 0x00, 0x00, 0xdb, 0x7c, + }, + }, + { + .name = "X-Box One Elite", + .bus_type = 0x0003, + .vendor_id = 0x045e, + .product_id = 0x02e3, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, KEY, ABS */ + .ev = { 0x0b }, + /* X, Y, Z, RX, RY, RZ, HAT0X, HAT0Y */ + .abs = { 0x3f, 0x00, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* A, B, X, Y, TL, TR, SELECT, START, MODE, THUMBL, THUMBR */ + /* 0x100 */ ZEROx4, 0x00, 0x00, 0xdb, 0x7c, + }, + }, + { + .name = "X-Box One S via Bluetooth", + .bus_type = 0x0005, + .vendor_id = 0x045e, + .product_id = 0x02e0, + .version = 0x1130, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, KEY, ABS */ + .ev = { 0x0b }, + /* X, Y, Z, RX, RY, RZ, HAT0X, HAT0Y */ + .abs = { 0x3f, 0x00, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* A, B, X, Y, TL, TR, SELECT, START, MODE, THUMBL, THUMBR */ + /* 0x100 */ ZEROx4, 0x00, 0x00, 0xdb, 0x7c, + }, + }, + { + .name = "X-Box One S wired", + .bus_type = 0x0003, + .vendor_id = 0x045e, + .product_id = 0x02ea, + .version = 0x0301, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, KEY, ABS */ + .ev = { 0x0b }, + /* X, Y, Z, RX, RY, RZ, HAT0X, HAT0Y */ + .abs = { 0x3f, 0x00, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* A, B, X, Y, TL, TR, SELECT, START, MODE, THUMBL, THUMBR */ + /* 0x100 */ ZEROx4, 0x00, 0x00, 0xdb, 0x7c, + }, + }, + { + .name = "DualShock 4 - gamepad", + .bus_type = 0x0003, + .vendor_id = 0x054c, + .product_id = 0x09cc, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, KEY, ABS, MSC, FF */ + /* Some versions only have 0x0b, SYN, KEY, ABS, like the + * Bluetooth example below */ + .ev = { 0x1b, 0x00, 0x20 }, + /* X, Y, Z, RX, RY, RZ, HAT0X, HAT0Y */ + .abs = { 0x3f, 0x00, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* A, B, X, Y, TL, TR, TL2, TR2, SELECT, START, MODE, + * THUMBL, THUMBR */ + /* 0x100 */ ZEROx4, 0x00, 0x00, 0xdb, 0x7f, + }, + }, + { + .name = "DualShock 4 - gamepad via Bluetooth", + .bus_type = 0x0005, + .vendor_id = 0x054c, + .product_id = 0x09cc, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, KEY, ABS */ + .ev = { 0x0b }, + /* X, Y, Z, RX, RY, RZ, HAT0X, HAT0Y */ + .abs = { 0x3f, 0x00, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* A, B, X, Y, TL, TR, TL2, TR2, SELECT, START, MODE, + * THUMBL, THUMBR */ + /* 0x100 */ ZEROx4, 0x00, 0x00, 0xdb, 0x7f, + }, + }, + { + .name = "DualShock 4 - touchpad", + .bus_type = 0x0003, + .vendor_id = 0x054c, + .product_id = 0x09cc, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHPAD, + /* SYN, KEY, ABS */ + .ev = { 0x0b }, + /* X, Y, multitouch */ + .abs = { 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x60, 0x02 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* Left mouse button */ + /* 0x100 */ 0x00, 0x00, 0x01, 0x00, ZEROx4, + /* BTN_TOOL_FINGER and some multitouch stuff */ + /* 0x140 */ 0x20, 0x24, 0x00, 0x00 + }, + /* POINTER, BUTTONPAD */ + .props = { 0x05 }, + }, + { + .name = "DualShock 4 - accelerometer", + .bus_type = 0x0003, + .vendor_id = 0x054c, + .product_id = 0x09cc, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_ACCELEROMETER, + /* SYN, ABS, MSC */ + .ev = { 0x19 }, + /* X, Y, Z, RX, RY, RZ */ + .abs = { 0x3f }, + /* ACCELEROMETER */ + .props = { 0x40 }, + }, + { + .name = "DualShock 4 via USB dongle", + .bus_type = 0x0003, + .vendor_id = 0x054c, + .product_id = 0x0ba0, + .version = 0x8111, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, ABS, KEY */ + .ev = { 0x0b }, + /* X, Y, Z, RX, RY, RZ, HAT0X, HAT0Y */ + .abs = { 0x3f, 0x00, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* A, B, X, Y, TL, TR, TL2, TR2, SELECT, START, MODE, + * THUMBL, THUMBR */ + /* 0x100 */ ZEROx4, 0x00, 0x00, 0xdb, 0x7f, + }, + }, + { + .name = "DualShock 3 - gamepad", + .bus_type = 0x0003, + .vendor_id = 0x054c, + .product_id = 0x0268, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, KEY, ABS, MSC, FF */ + .ev = { 0x1b, 0x00, 0x20 }, + /* X, Y, Z, RX, RY, RZ */ + .abs = { 0x3f }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* A, B, X, Y, TL, TR, TL2, TR2, SELECT, START, MODE, + * THUMBL, THUMBR */ + /* 0x100 */ ZEROx4, 0x00, 0x00, 0xdb, 0x7f, + /* 0x140 */ ZEROx8, + /* 0x180 */ ZEROx8, + /* 0x1c0 */ ZEROx8, + /* Digital dpad */ + /* 0x200 */ ZEROx4, 0x0f, 0x00, 0x00, 0x00, + }, + }, + { + .name = "DualShock 3 - accelerometer", + .bus_type = 0x0003, + .vendor_id = 0x054c, + .product_id = 0x0268, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_ACCELEROMETER, + /* SYN, ABS */ + .ev = { 0x09 }, + /* X, Y, Z */ + .abs = { 0x07 }, + /* ACCELEROMETER */ + .props = { 0x40 }, + }, + { + .name = "Steam Controller - gamepad", + .bus_type = 0x0003, + .vendor_id = 0x28de, + .product_id = 0x1142, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, KEY, ABS */ + .ev = { 0x0b }, + /* X, Y, RX, RY, HAT0X, HAT0Y, HAT2X, HAT2Y */ + .abs = { 0x1b, 0x00, 0x33 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* A, B, X, Y, TL, TR, TL2, TR2, SELECT, START, MODE, + * THUMBL, THUMBR, joystick THUMB, joystick THUMB2 */ + /* 0x100 */ ZEROx4, 0x06, 0x00, 0xdb, 0x7f, + /* GEAR_DOWN, GEAR_UP */ + /* 0x140 */ 0x00, 0x00, 0x03, 0x00, ZEROx4, + /* 0x180 */ ZEROx8, + /* 0x1c0 */ ZEROx8, + /* Digital dpad */ + /* 0x200 */ ZEROx4, 0x0f, 0x00, 0x00, 0x00, + }, + }, + { + /* Present to support lizard mode, even if no Steam Controller + * is connected */ + .name = "Steam Controller - dongle", + .bus_type = 0x0003, + .vendor_id = 0x28de, + .product_id = 0x1142, + .expected = (SRT_INPUT_DEVICE_TYPE_FLAGS_KEYBOARD + | SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS + | SRT_INPUT_DEVICE_TYPE_FLAGS_MOUSE), + /* SYN, KEY, REL, MSC, LED, REP */ + .ev = { 0x17, 0x00, 0x12 }, + /* X, Y, mouse wheel, high-res mouse wheel */ + .rel = { 0x03, 0x09 }, + .keys = { + /* 0x00 */ 0xfe, 0xff, 0xff, 0xff, FFx4, + /* 0x40 */ 0xff, 0xff, 0xcf, 0x01, 0xdf, 0xff, 0x80, 0xe0, + /* 0x80 */ ZEROx8, + /* 0xc0 */ ZEROx8, + /* 0x100 */ 0x00, 0x00, 0x1f, 0x00, ZEROx4, + }, + }, + { + .name = "Guitar Hero for PS3", + .bus_type = 0x0003, + .vendor_id = 0x12ba, + .product_id = 0x0100, + .version = 0x0110, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, KEY, ABS */ + .ev = { 0x0b }, + /* X, Y, Z, RZ, HAT0X, HAT0Y */ + .abs = { 0x27, 0x00, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* ABC, XYZ, TL, TR, TL2, TR2, SELECT, START, MODE */ + /* 0x100 */ ZEROx4, 0x00, 0x00, 0xff, 0x1f, + }, + }, + { + .name = "G27 Racing Wheel, 0003:046d:c29b v0111", + .bus_type = 0x0003, + .vendor_id = 0x046d, + .product_id = 0xc29b, + .version = 0x0111, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, KEY, ABS */ + .ev = { 0x0b }, + /* X, Y, Z, RZ, HAT0X, HAT0Y */ + .abs = { 0x27, 0x00, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* 16 buttons: TRIGGER, THUMB, THUMB2, TOP, TOP2, PINKIE, BASE, + * BASE2..BASE6, unregistered event codes 0x12c-0x12e, DEAD */ + /* 0x100 */ ZEROx4, 0xff, 0xff, 0x00, 0x00, + /* 0x140 */ ZEROx8, + /* 0x180 */ ZEROx8, + /* 0x1c0 */ ZEROx8, + /* 0x200 */ ZEROx8, + /* 0x240 */ ZEROx8, + /* 0x280 */ ZEROx8, + /* TRIGGER_HAPPY1..TRIGGER_HAPPY7 */ + /* 0x2c0 */ 0x7f, + }, + }, + { + .name = "Logitech Driving Force, 0003:046d:c294 v0100", + .bus_type = 0x0003, + .vendor_id = 0x046d, + .product_id = 0xc294, + .version = 0x0100, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, KEY, ABS */ + .ev = { 0x0b }, + /* X, Y, RZ, HAT0X, HAT0Y */ + .abs = { 0x23, 0x00, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* 12 buttons: TRIGGER, THUMB, THUMB2, TOP, TOP2, PINKIE, BASE, + * BASE2..BASE6 */ + /* 0x100 */ ZEROx4, 0xff, 0x0f, 0x00, 0x00, + }, + }, + { + .name = "Logitech Dual Action", + .bus_type = 0x0003, + .vendor_id = 0x046d, + .product_id = 0xc216, + .version = 0x0110, + /* Logitech RumblePad 2 USB, 0003:046d:c218 v0110, is the same + * except for having force feedback, which we don't use in our + * heuristic */ + /* Jess Tech GGE909 PC Recoil Pad, 0003:0f30:010b v0110, is the same */ + /* 8BitDo SNES30, 0003:2dc8:ab20 v0110, is the same */ + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, KEY, ABS */ + .ev = { 0x0b }, + /* X, Y, Z, RZ, HAT0X, HAT0Y */ + .abs = { 0x27, 0x00, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* 12 buttons: TRIGGER, THUMB, THUMB2, TOP, TOP2, PINKIE, BASE, + * BASE2..BASE6 */ + /* 0x100 */ ZEROx4, 0xff, 0x0f, 0x00, 0x00, + }, + }, + { + .name = "Saitek ST290 Pro flight stick", + .bus_type = 0x0003, + .vendor_id = 0x06a3, + .product_id = 0x0160, /* 0x0460 seems to be the same */ + .version = 0x0100, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, KEY, ABS, MSC */ + .ev = { 0x1b }, + /* X, Y, Z, RZ, HAT0X, HAT0Y */ + .abs = { 0x27, 0x00, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* TRIGGER, THUMB, THUMB2, TOP, TOP2, PINKIE */ + /* 0x100 */ ZEROx4, 0x3f, 0x00, 0x00, 0x00, + }, + }, + { + .name = "Saitek X52 Pro Flight Control System", + .bus_type = 0x0003, + .vendor_id = 0x06a3, + .product_id = 0x0762, + .version = 0x0111, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + .ev = { 0x0b }, + /* XYZ, RXYZ, throttle, hat0, MISC, unregistered event code 0x29 */ + .abs = { 0x7f, 0x00, 0x03, 0x00, 0x00, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* 16 buttons: TRIGGER, THUMB, THUMB2, TOP, TOP2, PINKIE, BASE, + * BASE2..BASE6, unregistered event codes 0x12c-0x12e, DEAD */ + /* 0x100 */ ZEROx4, 0xff, 0xff, 0x00, 0x00, + /* 0x140 */ ZEROx8, + /* 0x180 */ ZEROx8, + /* 0x1c0 */ ZEROx8, + /* 0x200 */ ZEROx8, + /* 0x240 */ ZEROx8, + /* 0x280 */ ZEROx8, + /* TRIGGER_HAPPY1..TRIGGER_HAPPY23 */ + /* 0x2c0 */ 0xff, 0xff, 0x7f, + }, + }, + { + .name = "Logitech Extreme 3D", + .bus_type = 0x0003, + .vendor_id = 0x046d, + .product_id = 0xc215, + .version = 0x0110, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, KEY, ABS, MSC */ + .ev = { 0x0b }, + /* X, Y, RZ, throttle, hat 0 */ + .abs = { 0x63, 0x00, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* 12 buttons: TRIGGER, THUMB, THUMB2, TOP, TOP2, PINKIE, BASE, + * BASE2..BASE6 */ + /* 0x100 */ ZEROx4, 0xff, 0x0f, 0x00, 0x00, + }, + }, + { + .name = "Hori Real Arcade Pro VX-SA", + .bus_type = 0x0003, + .vendor_id = 0x24c6, + .product_id = 0x5501, + .version = 0x0533, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, KEY, ABS */ + .ev = { 0x0b }, + /* X, Y, Z, RX, RY, RZ, hat 0 */ + .abs = { 0x3f, 0x00, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* A, B, X, Y, TL, TR, SELECT, START, MODE, THUMBL, THUMBR */ + /* 0x100 */ ZEROx4, 0x00, 0x00, 0xdb, 0x7c, + }, + }, + { + .name = "Switch Pro Controller via Bluetooth", + .bus_type = 0x0005, + .vendor_id = 0x057e, + .product_id = 0x2009, + .version = 0x0001, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, KEY, ABS */ + .ev = { 0x0b }, + /* X, Y, RX, RY, hat 0 */ + .abs = { 0x1b, 0x00, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* 16 buttons: TRIGGER, THUMB, THUMB2, TOP, TOP2, PINKIE, BASE, + * BASE2..BASE6, unregistered event codes 0x12c-0x12e, DEAD */ + /* 0x100 */ ZEROx4, 0xff, 0xff, 0x00, 0x00, + /* 0x140 */ ZEROx8, + /* 0x180 */ ZEROx8, + /* 0x1c0 */ ZEROx8, + /* 0x200 */ ZEROx8, + /* 0x240 */ ZEROx8, + /* 0x280 */ ZEROx8, + /* TRIGGER_HAPPY1..TRIGGER_HAPPY2 */ + /* 0x2c0 */ 0x03, + }, + }, + { + .name = "Switch Pro Controller via USB", + .bus_type = 0x0003, + .vendor_id = 0x057e, + .product_id = 0x2009, + .version = 0x0111, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, KEY, ABS */ + .ev = { 0x0b }, + /* X, Y, Z, RZ, HAT0X, HAT0Y */ + .abs = { 0x27, 0x00, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + }, + }, + { + .name = "Thrustmaster Racing Wheel FFB", + /* Mad Catz FightStick TE S+ PS4, 0003:0738:8384:0111 v0111 + * (aka Street Fighter V Arcade FightStick TES+) + * is functionally the same */ + .bus_type = 0x0003, + .vendor_id = 0x044f, + .product_id = 0xb66d, + .version = 0x0110, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + .ev = { 0x0b }, + /* XYZ, RXYZ, hat 0 */ + .abs = { 0x3f, 0x00, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* ABC, XYZ, TL, TR, TL2, TR2, SELECT, START, MODE, + * THUMBL */ + /* 0x100 */ ZEROx4, 0x00, 0x00, 0xff, 0x3f, + }, + }, + { + .name = "Thrustmaster T.Flight Hotas X", + .bus_type = 0x0003, + .vendor_id = 0x044f, + .product_id = 0xb108, + .version = 0x0100, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + .ev = { 0x0b }, + /* XYZ, RZ, throttle, hat 0 */ + .abs = { 0x67, 0x00, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* trigger, thumb, thumb2, top, top2, pinkie, base, + * base2..base6 */ + /* 0x100 */ ZEROx4, 0xff, 0x0f + }, + }, + { + .name = "8BitDo N30 Pro 2", + .bus_type = 0x0003, + .vendor_id = 0x2dc8, + .product_id = 0x9015, + .version = 0x0111, + /* 8BitDo NES30 Pro, 0003:2dc8:9001 v0111, is the same */ + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + .ev = { 0x0b }, + /* XYZ, RZ, gas, brake, hat0 */ + .abs = { 0x27, 0x06, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* ABC, XYZ, TL, TR, TL2, TR2, select, start, mode, thumbl, + * thumbr */ + /* 0x100 */ ZEROx4, 0x00, 0x00, 0xff, 0x7f, + }, + }, + { + .name = "Retro Power SNES-style controller, 0003:0079:0011 v0110", + .bus_type = 0x0003, + .vendor_id = 0x0079, + .product_id = 0x0011, + .version = 0x0110, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + .ev = { 0x0b }, + /* X, Y */ + .abs = { 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* trigger, thumb, thumb2, top, top2, pinkie, base, + * base2..base4 */ + /* 0x100 */ ZEROx4, 0xff, 0x03, 0x00, 0x00, + }, + }, + { + .name = "Wiimote - buttons", + .bus_type = 0x0005, + .vendor_id = 0x057e, + .product_id = 0x0306, + .version = 0x8600, + /* This one is a bit weird because some of the buttons are mapped + * to the arrow, page up and page down keys, so it's a joystick + * with a subset of a keyboard attached */ + .expected = (SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK + | SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS), + /* SYN, KEY */ + .ev = { 0x03 }, + .keys = { + /* 0x00 */ ZEROx8, + /* left, right, up down */ + /* 0x40 */ ZEROx4, 0x80, 0x16, 0x00, 0x00, + /* 0x80 */ ZEROx8, + /* 0xc0 */ ZEROx8, + /* BTN_1, BTN_2, BTN_A, BTN_B, BTN_MODE */ + /* 0x100 */ 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x10, + /* 0x140 */ ZEROx8, + /* next (page down), previous (page up) */ + /* 0x180 */ 0x00, 0x00, 0x80, 0x10, ZEROx4, + }, + }, + { + .name = "Wiimote - Motion Plus or accelerometer", + .bus_type = 0x0005, + .vendor_id = 0x057e, + .product_id = 0x0306, + .version = 0x8600, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_ACCELEROMETER, + /* SYN, ABS */ + .ev = { 0x09 }, + /* RX, RY, RZ */ + .abs = { 0x38 }, + }, + { + .name = "Wiimote - IR positioning", + .bus_type = 0x0005, + .vendor_id = 0x057e, + .product_id = 0x0306, + .version = 0x8600, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, ABS */ + .ev = { 0x09 }, + /* HAT0 to HAT3 */ + .abs = { 0x00, 0x1f }, + }, + { + .name = "Wiimote - Nunchuck", + .bus_type = 0x0005, + .vendor_id = 0x057e, + .product_id = 0x0306, + .version = 0x8600, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, KEY, ABS */ + .ev = { 0x0b }, + /* RX, RY, RZ, hat 0 */ + .abs = { 0x38, 0x00, 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* C and Z buttons */ + /* 0x100 */ ZEROx4, 0x00, 0x00, 0x24, 0x00, + }, + }, + { + /* Flags guessed from kernel source code */ + .name = "Wiimote - Classic Controller", + .expected = (SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK + | SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS), + /* SYN, KEY, ABS */ + .ev = { 0x0b }, + /* Hat 1-3 */ + .abs = { 0x00, 0x1c }, + .keys = { + /* 0x00 */ ZEROx8, + /* left, right, up down */ + /* 0x40 */ ZEROx4, 0x80, 0x16, 0x00, 0x00, + /* 0x80 */ ZEROx8, + /* 0xc0 */ ZEROx8, + /* A, B, X, Y, MODE, TL, TL2, TR, TR2 */ + /* 0x100 */ ZEROx4, 0x00, 0x13, 0xdb, 0x10, + /* 0x140 */ ZEROx8, + /* next, previous */ + /* 0x180 */ 0x00, 0x00, 0x80, 0x10, ZEROx4, + }, + }, + { + /* Flags guessed from kernel source code */ + .name = "Wiimote - Balance Board", + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, KEY, ABS */ + .ev = { 0x0b }, + /* Hat 0-1 */ + .abs = { 0x00, 0x0f }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* A */ + /* 0x100 */ ZEROx4, 0x00, 0x00, 0x01, 0x00, + }, + }, + { + /* Flags guessed from kernel source code */ + .name = "Wiimote - Wii U Pro Controller", + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK, + /* SYN, KEY, ABS */ + .ev = { 0x0b }, + /* X, Y, RX, RY */ + .abs = { 0x1b }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* A, B, X, Y, TL, TR, TL2, TR2, SELECT, START, MODE, + * THUMBL, THUMBR */ + /* 0x100 */ ZEROx4, 0x00, 0x00, 0xdb, 0x7f, + /* 0x140 */ ZEROx8, + /* 0x180 */ ZEROx8, + /* 0x1c0 */ ZEROx8, + /* Digital dpad */ + /* 0x200 */ ZEROx4, 0x0f, 0x00, 0x00, 0x00, + }, + }, + { + .name = "Synaptics TM3381-002 (Thinkpad X280 trackpad)", + .bus_type = 0x001d, /* BUS_RMI */ + .vendor_id = 0x06cb, + .product_id = 0x0000, + .version = 0x0000, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHPAD, + /* SYN, KEY, ABS */ + .ev = { 0x0b }, + /* X, Y, pressure, multitouch */ + .abs = { 0x03, 0x00, 0x00, 0x01, 0x00, 0x80, 0xf3, 0x06 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* Left mouse button */ + /* 0x100 */ 0x00, 0x00, 0x01, 0x00, ZEROx4, + /* BTN_TOOL_FINGER and some multitouch gestures */ + /* 0x140 */ 0x20, 0xe5 + }, + /* POINTER, BUTTONPAD */ + .props = { 0x05 }, + }, + { + .name = "TPPS/2 Elan TrackPoint (Thinkpad X280)", + .bus_type = 0x0011, /* BUS_I8042 */ + .vendor_id = 0x0002, + .product_id = 0x000a, + .version = 0x0000, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_POINTING_STICK, + /* SYN, KEY, REL */ + .ev = { 0x07 }, + /* X, Y */ + .rel = { 0x03 }, + .keys = { + /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8, + /* Left, middle, right mouse buttons */ + /* 0x100 */ 0x00, 0x00, 0x07, + }, + /* POINTER, POINTING_STICK */ + .props = { 0x21 }, + }, + { + .name = "Thinkpad ACPI buttons", + .expected = (SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS | + SRT_INPUT_DEVICE_TYPE_FLAGS_SWITCH), + /* SYN, KEY, MSC, SW */ + .ev = { 0x33 }, + .keys = { + /* 0x00 */ ZEROx8, + /* 0x40 */ ZEROx4, 0x00, 0x00, 0x0e, 0x01, + /* 0x80 */ 0x00, 0x50, 0x11, 0x51, 0x00, 0x28, 0x00, 0xc0, + /* 0xc0 */ 0x04, 0x20, 0x10, 0x02, 0x1b, 0x70, 0x01, 0x00, + /* 0x100 */ ZEROx8, + /* 0x140 */ ZEROx4, 0x00, 0x00, 0x50, 0x00, + /* 0x180 */ ZEROx8, + /* 0x1c0 */ 0x00, 0x00, 0x04, 0x18, ZEROx4, + /* 0x200 */ ZEROx8, + /* 0x240 */ 0x40, 0x00, 0x01, 0x00, ZEROx4, + }, + }, + { + .name = "PC speaker", + .bus_type = 0x0010, /* BUS_ISA */ + .vendor_id = 0x001f, + .product_id = 0x0001, + .version = 0x0100, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_NONE, + /* SYN, SND */ + .ev = { 0x01, 0x00, 0x04 }, + }, + { + .name = "ALSA headphone detection, etc.", + .bus_type = 0x0000, + .vendor_id = 0x0000, + .product_id = 0x0000, + .version = 0x0000, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_SWITCH, + /* SYN, SW */ + .ev = { 0x21 }, + }, + { + /* Assumed to be a reasonably typical i8042 (PC AT) keyboard */ + .name = "Thinkpad T520 and X280 keyboards", + .bus_type = 0x0011, /* BUS_I8042 */ + .vendor_id = 0x0001, + .product_id = 0x0001, + .version = 0xab54, + .expected = (SRT_INPUT_DEVICE_TYPE_FLAGS_KEYBOARD + | SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS), + /* SYN, KEY, MSC, LED, REP */ + .ev = { 0x13, 0x00, 0x12 }, + .keys = { + /* 0x00 */ 0xfe, 0xff, 0xff, 0xff, FFx4, + /* 0x40 */ 0xff, 0xff, 0xef, 0xff, 0xdf, 0xff, 0xff, 0xfe, + /* 0x80 */ 0x01, 0xd0, 0x00, 0xf8, 0x78, 0x30, 0x80, 0x03, + /* 0xc0 */ 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0x00, + }, + }, + { + .name = "Thinkpad X280 sleep button", + .bus_type = 0x0019, /* BUS_HOST */ + .vendor_id = 0x0000, + .product_id = 0x0003, + .version = 0x0000, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS, + /* SYN, KEY */ + .ev = { 0x03 }, + .keys = { + /* 0x00 */ ZEROx8, + /* 0x40 */ ZEROx8, + /* KEY_SLEEP */ + /* 0x80 */ 0x00, 0x40, + }, + }, + { + .name = "Thinkpad X280 lid switch", + .bus_type = 0x0019, /* BUS_HOST */ + .vendor_id = 0x0000, + .product_id = 0x0005, + .version = 0x0000, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_SWITCH, + /* SYN, SW */ + .ev = { 0x21 }, + }, + { + .name = "Thinkpad X280 power button", + .bus_type = 0x0019, /* BUS_HOST */ + .vendor_id = 0x0000, + .product_id = 0x0001, + .version = 0x0000, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS, + /* SYN, KEY */ + .ev = { 0x03 }, + .keys = { + /* 0x00 */ ZEROx8, + /* KEY_POWER */ + /* 0x40 */ ZEROx4, 0x00, 0x00, 0x10, 0x00, + }, + }, + { + .name = "Thinkpad X280 video bus", + .bus_type = 0x0019, /* BUS_HOST */ + .vendor_id = 0x0000, + .product_id = 0x0006, + .version = 0x0000, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS, + /* SYN, KEY */ + .ev = { 0x03 }, + .keys = { + /* 0x00 */ ZEROx8, + /* 0x40 */ ZEROx8, + /* 0x80 */ ZEROx8, + /* brightness control, video mode, display off */ + /* 0xc0 */ ZEROx4, 0x0b, 0x00, 0x3e, 0x00, + }, + }, + { + .name = "Thinkpad X280 extra buttons", + .bus_type = 0x0019, /* BUS_HOST */ + .vendor_id = 0x17aa, + .product_id = 0x5054, + .version = 0x4101, + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS, + /* SYN, KEY */ + .ev = { 0x03 }, + .keys = { + /* 0x00 */ ZEROx8, + /* 0x40 */ ZEROx4, 0x00, 0x00, 0x0e, 0x01, + /* 0x80 */ 0x00, 0x50, 0x11, 0x51, 0x00, 0x28, 0x00, 0xc0, + /* 0xc0 */ 0x04, 0x20, 0x10, 0x02, 0x1b, 0x70, 0x01, 0x00, + /* 0x100 */ ZEROx8, + /* 0x140 */ ZEROx4, 0x00, 0x00, 0x50, 0x00, + /* 0x180 */ ZEROx8, + /* 0x1c0 */ 0x00, 0x00, 0x04, 0x18, ZEROx4, + /* 0x200 */ ZEROx8, + /* 0x240 */ 0x40, 0x00, 0x01, 0x00, ZEROx4, + }, + }, + { + .name = "Thinkpad USB keyboard with Trackpoint - keyboard", + .bus_type = 0x0003, + .vendor_id = 0x17ef, + .product_id = 0x6009, + .expected = (SRT_INPUT_DEVICE_TYPE_FLAGS_KEYBOARD + | SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS), + /* SYN, KEY, MSC, LED, REP */ + .ev = { 0x13, 0x00, 0x12 }, + .keys = { + /* 0x00 */ 0xfe, 0xff, 0xff, 0xff, FFx4, + /* 0x40 */ 0xff, 0xff, 0xef, 0xff, 0xdf, 0xff, 0xbe, 0xfe, + /* 0x80 */ 0xff, 0x57, 0x40, 0xc1, 0x7a, 0x20, 0x9f, 0xff, + /* 0xc0 */ 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + }, + }, + { + .name = "Thinkpad USB keyboard with Trackpoint - Trackpoint", + .bus_type = 0x0003, + .vendor_id = 0x17ef, + .product_id = 0x6009, + /* For some reason the special keys like mute and wlan toggle + * show up here instead of, or in addition to, as part of + * the keyboard - so we report this as having keys too. */ + .expected = (SRT_INPUT_DEVICE_TYPE_FLAGS_MOUSE + | SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS), + /* SYN, KEY, REL, MSC, LED */ + .ev = { 0x17, 0x00, 0x02 }, + /* X, Y */ + .rel = { 0x03 }, + .keys = { + /* 0x00 */ ZEROx8, + /* 0x40 */ ZEROx4, 0x00, 0x00, 0x1e, 0x00, + /* 0x80 */ 0x00, 0xcc, 0x11, 0x01, 0x78, 0x40, 0x00, 0xc0, + /* 0xc0 */ 0x00, 0x20, 0x10, 0x00, 0x0b, 0x50, 0x00, 0x00, + /* Mouse buttons: left, right, middle, "task" */ + /* 0x100 */ 0x00, 0x00, 0x87, 0x68, ZEROx4, + /* 0x140 */ ZEROx4, 0x00, 0x00, 0x10, 0x00, + /* 0x180 */ ZEROx4, 0x00, 0x00, 0x40, 0x00, + }, + }, + { + .name = "No information", + .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_NONE, + } +}; + +static void +test_input_device_guess (Fixture *f, + gconstpointer context) +{ + size_t i; + + for (i = 0; i < G_N_ELEMENTS (guess_tests); i++) + { + const GuessTest *t = &guess_tests[i]; + SrtEvdevCapabilities caps; + size_t j; + SrtInputDeviceTypeFlags actual; + + g_test_message ("%s", t->name); + + /* The real SrtEvdevCapabilities rounds the sizes up to the next + * 32- or 64-bit boundary, and GuessTest rounds them up to the next + * 8-bit boundary, so GuessTest is the same size or smaller. */ + g_assert_cmpuint (sizeof (caps.ev), >=, sizeof (t->ev)); + g_assert_cmpuint (sizeof (caps.keys), >=, sizeof (t->keys)); + g_assert_cmpuint (sizeof (caps.abs), >=, sizeof (t->abs)); + g_assert_cmpuint (sizeof (caps.rel), >=, sizeof (t->rel)); + g_assert_cmpuint (sizeof (caps.ff), >=, sizeof (t->ff)); + g_assert_cmpuint (sizeof (caps.props), >=, sizeof (t->props)); + + memset (&caps, '\0', sizeof (caps)); + memcpy (caps.ev, t->ev, sizeof (t->ev)); + memcpy (caps.keys, t->keys, sizeof (t->keys)); + memcpy (caps.abs, t->abs, sizeof (t->abs)); + memcpy (caps.rel, t->rel, sizeof (t->rel)); + memcpy (caps.ff, t->ff, sizeof (t->ff)); + memcpy (caps.props, t->props, sizeof (t->props)); + + for (j = 0; j < G_N_ELEMENTS (caps.ev); j++) + caps.ev[j] = GULONG_FROM_LE (caps.ev[j]); + + for (j = 0; j < G_N_ELEMENTS (caps.keys); j++) + caps.keys[j] = GULONG_FROM_LE (caps.keys[j]); + + for (j = 0; j < G_N_ELEMENTS (caps.abs); j++) + caps.abs[j] = GULONG_FROM_LE (caps.abs[j]); + + for (j = 0; j < G_N_ELEMENTS (caps.rel); j++) + caps.rel[j] = GULONG_FROM_LE (caps.rel[j]); + + for (j = 0; j < G_N_ELEMENTS (caps.ff); j++) + caps.ff[j] = GULONG_FROM_LE (caps.ff[j]); + + for (j = 0; j < G_N_ELEMENTS (caps.props); j++) + caps.props[j] = GULONG_FROM_LE (caps.props[j]); + + _srt_evdev_capabilities_dump (&caps); + + /* Now we can check whether our guess works */ + actual = _srt_evdev_capabilities_guess_type (&caps); + g_assert_cmphex (actual, ==, t->expected); + } +} + +static void +test_input_device_identity_from_hid_uevent (Fixture *f, + gconstpointer context) +{ + static const char text[] = + "DRIVER=hid-steam\n" + "HID_ID=0003:000028DE:00001142\n" + "HID_NAME=Valve Software Steam Controller\n" + "HID_PHYS=usb-0000:00:14.0-1.1/input0\n" + "HID_UNIQ=serialnumber\n" + "MODALIAS=hid:b0003g0001v000028DEp00001142\n"; + guint32 bus_type, vendor_id, product_id; + g_autofree gchar *name = NULL; + g_autofree gchar *phys = NULL; + g_autofree gchar *uniq = NULL; + + g_assert_true (_srt_get_identity_from_hid_uevent (text, + &bus_type, + &vendor_id, + &product_id, + &name, + &phys, + &uniq)); + g_assert_cmphex (bus_type, ==, 0x0003); + g_assert_cmphex (vendor_id, ==, 0x28de); + g_assert_cmphex (product_id, ==, 0x1142); + g_assert_cmpstr (name, ==, "Valve Software Steam Controller"); + g_assert_cmpstr (phys, ==, "usb-0000:00:14.0-1.1/input0"); + /* Real Steam Controllers don't expose a serial number here, but it's + * a better test if we include one */ + g_assert_cmpstr (uniq, ==, "serialnumber"); +} + +#define VENDOR_SONY 0x0268 +#define PRODUCT_SONY_PS3 0x054c + +/* These aren't in the real vendor/product IDs, but we add them here + * to make the test able to distinguish. They look a bit like HID, + * EVDE(v) and USB, if you squint. */ +#define HID_MARKER 0x41D00000 +#define EVDEV_MARKER 0xE7DE0000 +#define USB_MARKER 0x05B00000 + +/* The test below assumes EV_MAX doesn't increase its value */ +G_STATIC_ASSERT (EV_MAX <= 31); +/* Same for INPUT_PROP_MAX */ +G_STATIC_ASSERT (INPUT_PROP_MAX <= 31); + +static void +test_input_device_usb (Fixture *f, + gconstpointer context) +{ + g_autoptr(MockInputDevice) mock_device = mock_input_device_new (); + SrtInputDevice *device = SRT_INPUT_DEVICE (mock_device); + SrtSimpleInputDevice *simple = SRT_SIMPLE_INPUT_DEVICE (mock_device); + SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device); + g_auto(GStrv) udev_properties = NULL; + g_autofree gchar *uevent = NULL; + g_autofree gchar *hid_uevent = NULL; + g_autofree gchar *input_uevent = NULL; + g_autofree gchar *usb_uevent = NULL; + struct + { + unsigned int bus_type; + unsigned int vendor_id; + unsigned int product_id; + unsigned int version; + } identity = { 1, 1, 1, 1 }; + struct + { + unsigned int bus_type; + unsigned int vendor_id; + unsigned int product_id; + const char *name; + const char *phys; + const char *uniq; + } hid_identity = { 1, 1, 1, "x", "x", "x" }; + struct + { + unsigned int bus_type; + unsigned int vendor_id; + unsigned int product_id; + unsigned int version; + const char *name; + const char *phys; + const char *uniq; + } input_identity = { 1, 1, 1, 1, "x", "x", "x" }; + struct + { + unsigned int vendor_id; + unsigned int product_id; + unsigned int version; + const char *manufacturer; + const char *product; + const char *serial; + } usb_identity = { 1, 1, 1, "x", "x", "x" }; + unsigned long evbits; + /* Initialize the first two to nonzero to check that they get zeroed */ + unsigned long bits[LONGS_FOR_BITS (HIGHEST_EVENT_CODE)] = { 0xa, 0xb }; + gsize i; + + simple->iface_flags = (SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT + | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE); + simple->dev_node = g_strdup ("/dev/input/event0"); + simple->sys_path = g_strdup ("/sys/devices/mock/usb/hid/input/input0/event0"); + simple->subsystem = g_strdup ("input"); + simple->udev_properties = g_new0 (gchar *, 2); + simple->udev_properties[0] = g_strdup ("ID_INPUT_JOYSTICK=1"); + simple->udev_properties[1] = NULL; + simple->uevent = g_strdup ("A=a\nB=b\n"); + /* This is a semi-realistic PS3 controller. */ + simple->type_flags = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK; + simple->bus_type = BUS_USB; + simple->vendor_id = VENDOR_SONY; + simple->product_id = PRODUCT_SONY_PS3; + simple->version = 0x8111; + + /* We don't set all the bits, just enough to be vaguely realistic */ + set_bit (EV_KEY, simple->evdev_caps.ev); + set_bit (EV_ABS, simple->evdev_caps.ev); + set_bit (BTN_A, simple->evdev_caps.keys); + set_bit (BTN_B, simple->evdev_caps.keys); + set_bit (BTN_TL, simple->evdev_caps.keys); + set_bit (BTN_TR, simple->evdev_caps.keys); + set_bit (ABS_X, simple->evdev_caps.abs); + set_bit (ABS_Y, simple->evdev_caps.abs); + set_bit (ABS_RX, simple->evdev_caps.abs); + set_bit (ABS_RY, simple->evdev_caps.abs); + + g_debug ("Mock device capabilities:"); + _srt_evdev_capabilities_dump (&simple->evdev_caps); + + simple->hid_ancestor.sys_path = g_strdup ("/sys/devices/mock/usb/hid"); + simple->hid_ancestor.uevent = g_strdup ("HID=yes\n"); + /* The part in square brackets isn't present on the real device, but + * makes this test more thorough by letting us distinguish. */ + simple->hid_ancestor.name = g_strdup ("Sony PLAYSTATION(R)3 Controller [hid]"); + simple->hid_ancestor.phys = g_strdup ("usb-0000:00:14.0-1/input0"); + simple->hid_ancestor.uniq = g_strdup ("12:34:56:78:9a:bc"); + simple->hid_ancestor.bus_type = HID_MARKER | BUS_USB; + simple->hid_ancestor.vendor_id = HID_MARKER | VENDOR_SONY; + simple->hid_ancestor.product_id = HID_MARKER | PRODUCT_SONY_PS3; + + simple->input_ancestor.sys_path = g_strdup ("/sys/devices/mock/usb/hid/input"); + simple->input_ancestor.uevent = g_strdup ("INPUT=yes\n"); + simple->input_ancestor.name = g_strdup ("Sony PLAYSTATION(R)3 Controller [input]"); + simple->input_ancestor.phys = NULL; + simple->input_ancestor.uniq = NULL; + simple->input_ancestor.bus_type = EVDEV_MARKER | BUS_USB; + simple->input_ancestor.vendor_id = EVDEV_MARKER | VENDOR_SONY; + simple->input_ancestor.product_id = EVDEV_MARKER | PRODUCT_SONY_PS3; + simple->input_ancestor.version = EVDEV_MARKER | 0x8111; + + simple->usb_device_ancestor.sys_path = g_strdup ("/sys/devices/mock/usb"); + simple->usb_device_ancestor.uevent = g_strdup ("USB=usb_device\n"); + simple->usb_device_ancestor.vendor_id = USB_MARKER | VENDOR_SONY; + simple->usb_device_ancestor.product_id = USB_MARKER | PRODUCT_SONY_PS3; + simple->usb_device_ancestor.device_version = USB_MARKER | 0x0100; + simple->usb_device_ancestor.manufacturer = g_strdup ("Sony"); + simple->usb_device_ancestor.product = g_strdup ("PLAYSTATION(R)3 Controller"); + simple->usb_device_ancestor.serial = NULL; + + g_assert_cmphex (srt_input_device_get_type_flags (device), ==, + SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK); + g_assert_cmpuint (srt_input_device_get_interface_flags (device), ==, + SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT + | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE); + g_assert_cmpstr (srt_input_device_get_dev_node (device), ==, + "/dev/input/event0"); + g_assert_cmpstr (srt_input_device_get_sys_path (device), ==, + "/sys/devices/mock/usb/hid/input/input0/event0"); + g_assert_cmpstr (srt_input_device_get_subsystem (device), ==, "input"); + + uevent = srt_input_device_dup_uevent (device); + g_assert_cmpstr (uevent, ==, "A=a\nB=b\n"); + + g_assert_cmpstr (srt_input_device_get_hid_sys_path (device), ==, + "/sys/devices/mock/usb/hid"); + hid_uevent = srt_input_device_dup_hid_uevent (device); + g_assert_cmpstr (hid_uevent, ==, "HID=yes\n"); + + g_assert_cmpstr (srt_input_device_get_input_sys_path (device), ==, + "/sys/devices/mock/usb/hid/input"); + input_uevent = srt_input_device_dup_input_uevent (device); + g_assert_cmpstr (input_uevent, ==, "INPUT=yes\n"); + + g_assert_cmpstr (srt_input_device_get_usb_device_sys_path (device), ==, + "/sys/devices/mock/usb"); + usb_uevent = srt_input_device_dup_usb_device_uevent (device); + g_assert_cmpstr (usb_uevent, ==, "USB=usb_device\n"); + + udev_properties = srt_input_device_dup_udev_properties (device); + g_assert_nonnull (udev_properties); + g_assert_cmpstr (udev_properties[0], ==, "ID_INPUT_JOYSTICK=1"); + g_assert_cmpstr (udev_properties[1], ==, NULL); + + g_assert_true (srt_input_device_get_identity (device, + NULL, NULL, NULL, NULL)); + g_assert_true (srt_input_device_get_identity (device, + &identity.bus_type, + &identity.vendor_id, + &identity.product_id, + &identity.version)); + g_assert_cmphex (identity.bus_type, ==, BUS_USB); + g_assert_cmphex (identity.vendor_id, ==, VENDOR_SONY); + g_assert_cmphex (identity.product_id, ==, PRODUCT_SONY_PS3); + g_assert_cmphex (identity.version, ==, 0x8111); + + g_assert_true (srt_input_device_get_hid_identity (device, + NULL, NULL, NULL, + NULL, NULL, NULL)); + g_assert_true (srt_input_device_get_hid_identity (device, + &hid_identity.bus_type, + &hid_identity.vendor_id, + &hid_identity.product_id, + &hid_identity.name, + &hid_identity.phys, + &hid_identity.uniq)); + g_assert_cmphex (hid_identity.bus_type, ==, HID_MARKER | BUS_USB); + g_assert_cmphex (hid_identity.vendor_id, ==, HID_MARKER | VENDOR_SONY); + g_assert_cmphex (hid_identity.product_id, ==, HID_MARKER | PRODUCT_SONY_PS3); + g_assert_cmpstr (hid_identity.name, ==, "Sony PLAYSTATION(R)3 Controller [hid]"); + g_assert_cmpstr (hid_identity.phys, ==, "usb-0000:00:14.0-1/input0"); + g_assert_cmpstr (hid_identity.uniq, ==, "12:34:56:78:9a:bc"); + + g_assert_true (srt_input_device_get_input_identity (device, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL)); + g_assert_true (srt_input_device_get_input_identity (device, + &input_identity.bus_type, + &input_identity.vendor_id, + &input_identity.product_id, + &input_identity.version, + &input_identity.name, + &input_identity.phys, + &input_identity.uniq)); + g_assert_cmphex (input_identity.bus_type, ==, EVDEV_MARKER | BUS_USB); + g_assert_cmphex (input_identity.vendor_id, ==, EVDEV_MARKER | VENDOR_SONY); + g_assert_cmphex (input_identity.product_id, ==, EVDEV_MARKER | PRODUCT_SONY_PS3); + g_assert_cmphex (input_identity.version, ==, EVDEV_MARKER | 0x8111); + g_assert_cmpstr (input_identity.name, ==, "Sony PLAYSTATION(R)3 Controller [input]"); + g_assert_cmpstr (input_identity.phys, ==, NULL); + g_assert_cmpstr (input_identity.uniq, ==, NULL); + + g_assert_true (srt_input_device_get_usb_device_identity (device, + NULL, NULL, NULL, + NULL, NULL, NULL)); + g_assert_true (srt_input_device_get_usb_device_identity (device, + &usb_identity.vendor_id, + &usb_identity.product_id, + &usb_identity.version, + &usb_identity.manufacturer, + &usb_identity.product, + &usb_identity.serial)); + g_assert_cmphex (usb_identity.vendor_id, ==, USB_MARKER | VENDOR_SONY); + g_assert_cmphex (usb_identity.product_id, ==, USB_MARKER | PRODUCT_SONY_PS3); + g_assert_cmpstr (usb_identity.manufacturer, ==, "Sony"); + g_assert_cmpstr (usb_identity.product, ==, "PLAYSTATION(R)3 Controller"); + g_assert_cmpstr (usb_identity.serial, ==, NULL); + + g_debug ("Capabilities internally:"); + _srt_evdev_capabilities_dump (iface->peek_event_capabilities (device)); + + /* This assumes EV_MAX doesn't increase its value */ + g_assert_cmpuint (srt_input_device_get_event_types (device, NULL, 0), + ==, 1); + g_assert_cmpuint (srt_input_device_get_event_types (device, &evbits, 1), + ==, 1); + g_assert_cmphex (evbits, ==, simple->evdev_caps.ev[0]); + g_assert_cmphex (evbits & (1 << EV_KEY), ==, 1 << EV_KEY); + g_assert_cmphex (evbits & (1 << EV_ABS), ==, 1 << EV_ABS); + g_assert_cmphex (evbits & (1 << EV_SW), ==, 0); + g_assert_cmphex (evbits & (1 << EV_MSC), ==, 0); + g_assert_cmpint (srt_input_device_has_event_type (device, EV_KEY), ==, TRUE); + g_assert_cmpint (srt_input_device_has_event_type (device, EV_SW), ==, FALSE); + g_assert_cmpint (srt_input_device_has_event_capability (device, 0, EV_KEY), + ==, TRUE); + g_assert_cmpint (srt_input_device_has_event_capability (device, 0, EV_SW), + ==, FALSE); + + g_assert_cmpuint (srt_input_device_get_event_capabilities (device, 0, + bits, + G_N_ELEMENTS (bits)), + ==, 1); + g_assert_cmphex (bits[0], ==, evbits); + + for (i = 1; i < G_N_ELEMENTS (bits); i++) + g_assert_cmphex (bits[i], ==, 0); + + g_assert_cmpuint (srt_input_device_get_event_capabilities (device, EV_KEY, + bits, + G_N_ELEMENTS (bits)), + >, 1); + /* Low KEY_ codes are keyboard keys, which we don't have */ + g_assert_cmphex (bits[0], ==, 0); + g_assert_cmpint (test_bit (BTN_A, bits), ==, 1); + g_assert_cmpint (test_bit (BTN_STYLUS, bits), ==, 0); + g_assert_cmpint (test_bit (KEY_SEMICOLON, bits), ==, 0); + g_assert_cmpmem (bits, + MIN (sizeof (bits), sizeof (simple->evdev_caps.keys)), + simple->evdev_caps.keys, + MIN (sizeof (bits), sizeof (simple->evdev_caps.keys))); + + /* ABS axes also match */ + g_assert_cmpuint (srt_input_device_get_event_capabilities (device, EV_ABS, + bits, + G_N_ELEMENTS (bits)), + >=, 1); + g_assert_cmpint (test_bit (ABS_X, bits), ==, 1); + g_assert_cmpint (test_bit (ABS_Z, bits), ==, 0); + g_assert_cmpmem (bits, + MIN (sizeof (bits), sizeof (simple->evdev_caps.abs)), + simple->evdev_caps.abs, + MIN (sizeof (bits), sizeof (simple->evdev_caps.abs))); + + /* REL axes also match (in fact we don't have any, but we still store + * the bitfield) */ + g_assert_cmpuint (srt_input_device_get_event_capabilities (device, EV_REL, + bits, + G_N_ELEMENTS (bits)), + >=, 1); + g_assert_cmpmem (bits, + MIN (sizeof (bits), sizeof (simple->evdev_caps.rel)), + simple->evdev_caps.rel, + MIN (sizeof (bits), sizeof (simple->evdev_caps.rel))); + + /* We don't support EV_SW */ + g_assert_cmpuint (srt_input_device_get_event_capabilities (device, EV_SW, + bits, + G_N_ELEMENTS (bits)), + ==, 0); + + for (i = 1; i < G_N_ELEMENTS (bits); i++) + g_assert_cmphex (bits[i], ==, 0); + + g_assert_cmpuint (srt_input_device_get_input_properties (device, + bits, + G_N_ELEMENTS (bits)), + ==, 1); + g_assert_cmphex (bits[0], ==, 0); + g_assert_cmpint (srt_input_device_has_input_property (device, INPUT_PROP_SEMI_MT), + ==, FALSE); + + for (i = 1; i < G_N_ELEMENTS (bits); i++) + g_assert_cmphex (bits[i], ==, 0); +} + +static gboolean +in_monitor_main_context (Fixture *f) +{ + if (f->monitor_context == NULL) + return g_main_context_is_owner (g_main_context_default ()); + + return g_main_context_is_owner (f->monitor_context); +} + +static void +device_added_cb (SrtInputDeviceMonitor *monitor, + SrtInputDevice *device, + gpointer user_data) +{ + Fixture *f = user_data; + g_autoptr(GError) error = NULL; + g_autofree gchar *message = NULL; + struct + { + unsigned int bus_type; + unsigned int vendor_id; + unsigned int product_id; + unsigned int version; + } identity = { 1, 1, 1, 1 }; + struct + { + unsigned int bus_type; + unsigned int vendor_id; + unsigned int product_id; + const char *name; + const char *phys; + const char *uniq; + } hid_identity = { 1, 1, 1, "x", "x", "x" }; + struct + { + unsigned int bus_type; + unsigned int vendor_id; + unsigned int product_id; + unsigned int version; + const char *name; + const char *phys; + const char *uniq; + } input_identity = { 1, 1, 1, 1, "x", "x", "x" }; + struct + { + unsigned int vendor_id; + unsigned int product_id; + unsigned int version; + const char *manufacturer; + const char *product; + const char *serial; + } usb_identity = { 1, 1, 1, "x", "x", "x" }; + SrtInputDeviceInterfaceFlags iface_flags; + int fd; + + g_assert_true (SRT_IS_INPUT_DEVICE_MONITOR (monitor)); + g_assert_true (SRT_IS_INPUT_DEVICE (device)); + + message = g_strdup_printf ("added device: %s", + srt_input_device_get_dev_node (device)); + g_debug ("%s: %s", G_OBJECT_TYPE_NAME (monitor), message); + + iface_flags = srt_input_device_get_interface_flags (device); + + if (srt_input_device_get_identity (device, + &identity.bus_type, + &identity.vendor_id, + &identity.product_id, + &identity.version)) + { + g_assert_true (srt_input_device_get_identity (device, + NULL, NULL, NULL, NULL)); + } + else + { + g_assert_false (srt_input_device_get_identity (device, + NULL, NULL, NULL, NULL)); + /* previous contents are untouched */ + g_assert_cmphex (identity.bus_type, ==, 1); + g_assert_cmphex (identity.vendor_id, ==, 1); + g_assert_cmphex (identity.product_id, ==, 1); + g_assert_cmphex (identity.version, ==, 1); + } + + if (srt_input_device_get_hid_identity (device, + &hid_identity.bus_type, + &hid_identity.vendor_id, + &hid_identity.product_id, + &hid_identity.name, + &hid_identity.phys, + &hid_identity.uniq)) + { + g_assert_true (srt_input_device_get_hid_identity (device, + NULL, NULL, NULL, + NULL, NULL, NULL)); + } + else + { + g_assert_false (srt_input_device_get_hid_identity (device, + NULL, NULL, NULL, + NULL, NULL, NULL)); + /* previous contents are untouched */ + g_assert_cmphex (hid_identity.bus_type, ==, 1); + g_assert_cmphex (hid_identity.vendor_id, ==, 1); + g_assert_cmphex (hid_identity.product_id, ==, 1); + g_assert_cmpstr (hid_identity.name, ==, "x"); + g_assert_cmpstr (hid_identity.phys, ==, "x"); + g_assert_cmpstr (hid_identity.uniq, ==, "x"); + } + + if (srt_input_device_get_input_identity (device, + &input_identity.bus_type, + &input_identity.vendor_id, + &input_identity.product_id, + &input_identity.version, + &input_identity.name, + &input_identity.phys, + &input_identity.uniq)) + { + g_assert_true (srt_input_device_get_input_identity (device, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL)); + } + else + { + g_assert_false (srt_input_device_get_input_identity (device, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL)); + /* previous contents are untouched */ + g_assert_cmphex (input_identity.bus_type, ==, 1); + g_assert_cmphex (input_identity.vendor_id, ==, 1); + g_assert_cmphex (input_identity.product_id, ==, 1); + g_assert_cmphex (input_identity.version, ==, 1); + g_assert_cmpstr (input_identity.name, ==, "x"); + g_assert_cmpstr (input_identity.phys, ==, "x"); + g_assert_cmpstr (input_identity.uniq, ==, "x"); + } + + if (srt_input_device_get_usb_device_identity (device, + &usb_identity.vendor_id, + &usb_identity.product_id, + &usb_identity.version, + &usb_identity.manufacturer, + &usb_identity.product, + &usb_identity.serial)) + { + g_assert_true (srt_input_device_get_usb_device_identity (device, + NULL, NULL, NULL, + NULL, NULL, NULL)); + } + else + { + g_assert_false (srt_input_device_get_usb_device_identity (device, + NULL, NULL, NULL, + NULL, NULL, NULL)); + /* previous contents are untouched */ + g_assert_cmphex (usb_identity.vendor_id, ==, 1); + g_assert_cmphex (usb_identity.product_id, ==, 1); + g_assert_cmphex (usb_identity.version, ==, 1); + g_assert_cmpstr (usb_identity.manufacturer, ==, "x"); + g_assert_cmpstr (usb_identity.product, ==, "x"); + g_assert_cmpstr (usb_identity.serial, ==, "x"); + } + + fd = srt_input_device_open (device, O_RDONLY | O_NONBLOCK, &error); + + if (iface_flags & SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE) + { + g_assert_no_error (error); + g_assert_cmpint (fd, >=, 0); + } + else + { + g_assert_nonnull (error); + g_assert_cmpint (fd, <, 0); + } + + glnx_close_fd (&fd); + g_clear_error (&error); + + fd = srt_input_device_open (device, O_RDWR | O_NONBLOCK, &error); + + if (iface_flags & SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE) + { + g_assert_no_error (error); + g_assert_cmpint (fd, >=, 0); + } + else + { + g_assert_nonnull (error); + g_assert_cmpint (fd, <, 0); + } + + glnx_close_fd (&fd); + g_clear_error (&error); + + /* Unsupported flags (currently everything except O_NONBLOCK) are + * not allowed */ + fd = srt_input_device_open (device, O_RDONLY | O_SYNC, &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT); + glnx_close_fd (&fd); + g_clear_error (&error); + + /* For the mock device monitor, we know exactly what to expect, so + * we can compare the expected log with what actually happened. For + * real device monitors, we don't know what's physically present, + * so we have to just emit debug messages. */ + if (f->config->type == MOCK) + { + g_autofree gchar *uevent = NULL; + g_autofree gchar *hid_uevent = NULL; + g_autofree gchar *input_uevent = NULL; + g_autofree gchar *usb_uevent = NULL; + g_auto(GStrv) udev_properties = NULL; + unsigned long evbits; + unsigned long bits[LONGS_FOR_BITS (HIGHEST_EVENT_CODE)]; + gsize i; + + g_assert_cmphex (srt_input_device_get_type_flags (device), ==, + SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK); + g_assert_cmpuint (srt_input_device_get_interface_flags (device), ==, + SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT + | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE + | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE); + + g_assert_cmphex (identity.bus_type, ==, BUS_USB); + g_assert_cmphex (identity.vendor_id, ==, VENDOR_VALVE); + g_assert_cmphex (identity.product_id, ==, PRODUCT_VALVE_STEAM_CONTROLLER); + g_assert_cmphex (identity.version, ==, 0x0111); + + g_assert_cmphex (hid_identity.bus_type, ==, HID_MARKER | BUS_USB); + g_assert_cmphex (hid_identity.vendor_id, ==, HID_MARKER | VENDOR_VALVE); + g_assert_cmphex (hid_identity.product_id, ==, HID_MARKER | PRODUCT_VALVE_STEAM_CONTROLLER); + g_assert_cmpstr (hid_identity.name, ==, "Valve Software Steam Controller"); + g_assert_cmpstr (hid_identity.phys, ==, "[hid]usb-0000:00:14.0-1.2/input1"); + g_assert_cmpstr (hid_identity.uniq, ==, ""); + + g_assert_cmphex (input_identity.bus_type, ==, EVDEV_MARKER | BUS_USB); + g_assert_cmphex (input_identity.vendor_id, ==, EVDEV_MARKER | VENDOR_VALVE); + g_assert_cmphex (input_identity.product_id, ==, EVDEV_MARKER | PRODUCT_VALVE_STEAM_CONTROLLER); + g_assert_cmphex (input_identity.version, ==, EVDEV_MARKER | 0x0111); + g_assert_cmpstr (input_identity.name, ==, "Wireless Steam Controller"); + g_assert_cmpstr (input_identity.phys, ==, "[input]usb-0000:00:14.0-1.2/input1"); + g_assert_cmpstr (input_identity.uniq, ==, "12345678"); + + g_assert_cmphex (usb_identity.vendor_id, ==, USB_MARKER | VENDOR_VALVE); + g_assert_cmphex (usb_identity.product_id, ==, USB_MARKER | PRODUCT_VALVE_STEAM_CONTROLLER); + g_assert_cmphex (usb_identity.version, ==, USB_MARKER | 0x0001); + g_assert_cmpstr (usb_identity.manufacturer, ==, "Valve Software"); + g_assert_cmpstr (usb_identity.product, ==, "Steam Controller"); + g_assert_cmpstr (usb_identity.serial, ==, NULL); + + uevent = srt_input_device_dup_uevent (device); + g_assert_cmpstr (uevent, ==, "ONE=1\nTWO=2\n"); + + udev_properties = srt_input_device_dup_udev_properties (device); + g_assert_nonnull (udev_properties); + g_assert_cmpstr (udev_properties[0], ==, "ID_INPUT_JOYSTICK=1"); + g_assert_cmpstr (udev_properties[1], ==, NULL); + + g_assert_cmpstr (srt_input_device_get_hid_sys_path (device), ==, + "/sys/devices/mock/usb/hid"); + hid_uevent = srt_input_device_dup_hid_uevent (device); + g_assert_cmpstr (hid_uevent, ==, "HID=yes\n"); + + g_assert_cmpstr (srt_input_device_get_input_sys_path (device), ==, + "/sys/devices/mock/usb/hid/input"); + input_uevent = srt_input_device_dup_input_uevent (device); + g_assert_cmpstr (input_uevent, ==, "INPUT=yes\n"); + + g_assert_cmpstr (srt_input_device_get_usb_device_sys_path (device), ==, + "/sys/devices/mock/usb"); + usb_uevent = srt_input_device_dup_usb_device_uevent (device); + g_assert_cmpstr (usb_uevent, ==, "USB=usb_device\n"); + + /* This assumes EV_MAX doesn't increase its value */ + g_assert_cmpuint (srt_input_device_get_event_types (device, NULL, 0), + ==, 1); + g_assert_cmpuint (srt_input_device_get_event_types (device, &evbits, 1), + ==, 1); + g_assert_cmphex (evbits & (1 << EV_KEY), ==, 1 << EV_KEY); + g_assert_cmphex (evbits & (1 << EV_ABS), ==, 1 << EV_ABS); + g_assert_cmphex (evbits & (1 << EV_SW), ==, 0); + g_assert_cmphex (evbits & (1 << EV_MSC), ==, 0); + g_assert_cmpint (srt_input_device_has_event_type (device, EV_KEY), ==, TRUE); + g_assert_cmpint (srt_input_device_has_event_type (device, EV_SW), ==, FALSE); + g_assert_cmpint (srt_input_device_has_event_capability (device, 0, EV_KEY), + ==, TRUE); + g_assert_cmpint (srt_input_device_has_event_capability (device, 0, EV_SW), + ==, FALSE); + + g_assert_cmpuint (srt_input_device_get_event_capabilities (device, 0, + bits, + G_N_ELEMENTS (bits)), + ==, 1); + g_assert_cmphex (bits[0], ==, evbits); + + for (i = 1; i < G_N_ELEMENTS (bits); i++) + g_assert_cmphex (bits[i], ==, 0); + + g_assert_cmpuint (srt_input_device_get_event_capabilities (device, EV_KEY, + bits, + G_N_ELEMENTS (bits)), + >, 1); + /* Low KEY_ codes are keyboard keys, which we don't have */ + g_assert_cmphex (bits[0], ==, 0); + g_assert_cmpint (test_bit (BTN_A, bits), ==, 1); + g_assert_cmpint (test_bit (BTN_STYLUS, bits), ==, 0); + g_assert_cmpint (test_bit (KEY_SEMICOLON, bits), ==, 0); + + /* ABS axes also match */ + g_assert_cmpuint (srt_input_device_get_event_capabilities (device, EV_ABS, + bits, + G_N_ELEMENTS (bits)), + >=, 1); + g_assert_cmpint (test_bit (ABS_X, bits), ==, 1); + g_assert_cmpint (test_bit (ABS_Z, bits), ==, 0); + + /* REL axes also match (in fact we don't have any, but we still store + * the bitfield) */ + g_assert_cmpuint (srt_input_device_get_event_capabilities (device, EV_REL, + bits, + G_N_ELEMENTS (bits)), + >=, 1); + + for (i = 1; i < G_N_ELEMENTS (bits); i++) + g_assert_cmphex (bits[i], ==, 0); + + /* We don't support EV_SW */ + g_assert_cmpuint (srt_input_device_get_event_capabilities (device, EV_SW, + bits, + G_N_ELEMENTS (bits)), + ==, 0); + + for (i = 1; i < G_N_ELEMENTS (bits); i++) + g_assert_cmphex (bits[i], ==, 0); + + g_assert_cmpuint (srt_input_device_get_input_properties (device, + bits, + G_N_ELEMENTS (bits)), + ==, 1); + /* The mock implementation unrealistically sets INPUT_PROP_POINTER, + * just so we have something nonzero to test against */ + g_assert_cmphex (bits[0], ==, (1 << INPUT_PROP_POINTER)); + g_assert_cmpint (srt_input_device_has_input_property (device, INPUT_PROP_POINTER), + ==, TRUE); + g_assert_cmpint (srt_input_device_has_input_property (device, INPUT_PROP_SEMI_MT), + ==, FALSE); + + for (i = 1; i < G_N_ELEMENTS (bits); i++) + g_assert_cmphex (bits[i], ==, 0); + + g_ptr_array_add (f->log, g_steal_pointer (&message)); + } + + g_assert_true (in_monitor_main_context (f)); +} + +static void +device_removed_cb (SrtInputDeviceMonitor *monitor, + SrtInputDevice *device, + gpointer user_data) +{ + Fixture *f = user_data; + g_autofree gchar *message = NULL; + + g_assert_true (SRT_IS_INPUT_DEVICE_MONITOR (monitor)); + g_assert_true (SRT_IS_INPUT_DEVICE (device)); + + message = g_strdup_printf ("removed device: %s", + srt_input_device_get_dev_node (device)); + g_debug ("%s: %s", G_OBJECT_TYPE_NAME (monitor), message); + + if (f->config->type == MOCK) + g_ptr_array_add (f->log, g_steal_pointer (&message)); + + g_assert_true (in_monitor_main_context (f)); +} + +static void +all_for_now_cb (SrtInputDeviceMonitor *monitor, + gpointer user_data) +{ + Fixture *f = user_data; + + g_assert_true (SRT_IS_INPUT_DEVICE_MONITOR (monitor)); + + g_ptr_array_add (f->log, g_strdup ("all for now")); + g_debug ("%s: %s", + G_OBJECT_TYPE_NAME (monitor), + (const char *) g_ptr_array_index (f->log, f->log->len - 1)); + + g_assert_true (in_monitor_main_context (f)); +} + +static gboolean +done_cb (gpointer user_data) +{ + gboolean *done = user_data; + + *done = TRUE; + return G_SOURCE_REMOVE; +} + +/* This is the equivalent of g_idle_add() for a non-default main context */ +static guint +idle_add_in_context (GSourceFunc function, + gpointer data, + GMainContext *context) +{ + g_autoptr(GSource) idler = g_idle_source_new (); + + g_source_set_callback (idler, function, data, NULL); + return g_source_attach (idler, context); +} + +static SrtInputDeviceMonitor * +input_device_monitor_new (Fixture *f, + SrtInputDeviceMonitorFlags flags) +{ + switch (f->config->type) + { + case DIRECT: + flags |= SRT_INPUT_DEVICE_MONITOR_FLAGS_DIRECT; + return srt_input_device_monitor_new (flags); + + case UDEV: + flags |= SRT_INPUT_DEVICE_MONITOR_FLAGS_UDEV; + return srt_input_device_monitor_new (flags); + + case MOCK: + default: + return SRT_INPUT_DEVICE_MONITOR (mock_input_device_monitor_new (flags)); + } +} + +/* + * Test the basic behaviour of an input device monitor: + * - start + * - do initial enumeration + * - watch for new devices + * - emit signals in the correct main context + * - stop + */ +static void +test_input_device_monitor (Fixture *f, + gconstpointer context) +{ + g_autoptr(SrtInputDeviceMonitor) monitor = NULL; + g_autoptr(GError) error = NULL; + gboolean ok; + gboolean did_default_idle = FALSE; + gboolean did_context_idle = FALSE; + guint i; + + if (f->skipped) + return; + + f->monitor_context = g_main_context_new (); + + /* To check that the signals get emitted in the correct main-context, + * temporarily set a new thread-default main-context while we create + * the monitor. */ + g_main_context_push_thread_default (f->monitor_context); + { + monitor = input_device_monitor_new (f, SRT_INPUT_DEVICE_MONITOR_FLAGS_NONE); + } + g_main_context_pop_thread_default (f->monitor_context); + + g_assert_nonnull (monitor); + + srt_input_device_monitor_request_evdev (monitor); + srt_input_device_monitor_request_raw_hid (monitor); + + g_signal_connect (monitor, "added", G_CALLBACK (device_added_cb), f); + g_signal_connect (monitor, "removed", G_CALLBACK (device_removed_cb), f); + g_signal_connect (monitor, "all-for-now", G_CALLBACK (all_for_now_cb), f); + + /* Note that the signals are emitted in the main-context that was + * thread-default at the time we created the object, not the + * main-context that called start(). */ + ok = srt_input_device_monitor_start (monitor, &error); + g_debug ("start() returned"); + g_assert_no_error (error); + g_assert_true (ok); + g_ptr_array_add (f->log, g_strdup ("start() returned")); + + g_idle_add (done_cb, &did_default_idle); + idle_add_in_context (done_cb, &did_context_idle, f->monitor_context); + + i = 0; + + g_assert_cmpuint (f->log->len, >, i); + g_assert_cmpstr (g_ptr_array_index (f->log, i++), ==, + "start() returned"); + /* There's nothing else in the log yet */ + g_assert_cmpuint (f->log->len, ==, i); + + /* Iterating the default main context does not deliver signals */ + while (!did_default_idle) + g_main_context_iteration (NULL, TRUE); + + g_assert_cmpuint (f->log->len, ==, i); + + /* Iterating the main context that was thread-default at the time we + * constructed the monitor *does* deliver signals */ + while (!did_context_idle) + g_main_context_iteration (f->monitor_context, TRUE); + + /* For the mock device monitor, we can predict which devices will be added, + * so we log them and assert about them. For real device monitors we + * can't reliably do this. */ + if (f->config->type == MOCK) + { + g_assert_cmpuint (f->log->len, >, i); + g_assert_cmpstr (g_ptr_array_index (f->log, i++), ==, + "added device: /dev/input/event0"); + } + + g_assert_cmpuint (f->log->len, >, i); + g_assert_cmpstr (g_ptr_array_index (f->log, i++), ==, + "all for now"); + + if (f->config->type == MOCK) + { + g_assert_cmpuint (f->log->len, >, i); + g_assert_cmpstr (g_ptr_array_index (f->log, i++), ==, + "added device: /dev/input/event-connected-briefly"); + g_assert_cmpuint (f->log->len, >, i); + g_assert_cmpstr (g_ptr_array_index (f->log, i++), ==, + "removed device: /dev/input/event-connected-briefly"); + } + + g_assert_cmpuint (f->log->len, ==, i); + + /* Explicitly stop it here. We test not explicitly stopping in the + * other test-case */ + srt_input_device_monitor_stop (monitor); + + /* It's possible that not all the memory used is freed until we have + * iterated the main-context one last time */ + did_context_idle = FALSE; + idle_add_in_context (done_cb, &did_context_idle, f->monitor_context); + + while (!did_context_idle) + g_main_context_iteration (f->monitor_context, TRUE); +} + +/* + * Test things we couldn't test in the previous test-case: + * - the ONCE flag, which disables monitoring + * - using our thread-default main-context throughout + */ +static void +test_input_device_monitor_once (Fixture *f, + gconstpointer context) +{ + g_autoptr(SrtInputDeviceMonitor) monitor = NULL; + g_autoptr(GError) error = NULL; + gboolean ok; + gboolean done = FALSE; + guint i; + + if (f->skipped) + return; + + monitor = input_device_monitor_new (f, SRT_INPUT_DEVICE_MONITOR_FLAGS_ONCE); + g_assert_nonnull (monitor); + + srt_input_device_monitor_request_evdev (monitor); + srt_input_device_monitor_request_raw_hid (monitor); + + g_signal_connect (monitor, "added", G_CALLBACK (device_added_cb), f); + g_signal_connect (monitor, "removed", G_CALLBACK (device_removed_cb), f); + g_signal_connect (monitor, "all-for-now", G_CALLBACK (all_for_now_cb), f); + + ok = srt_input_device_monitor_start (monitor, &error); + g_debug ("start() returned"); + g_assert_no_error (error); + g_assert_true (ok); + g_ptr_array_add (f->log, g_strdup ("start() returned")); + + g_idle_add (done_cb, &done); + + while (!done) + g_main_context_iteration (NULL, TRUE); + + i = 0; + + /* Because the same main context was the thread-default at the + * time we created the object and at the time we called start(), + * the first batch of signals arrive even before start() has returned. */ + if (f->config->type == MOCK) + { + g_assert_cmpuint (f->log->len, >, i); + g_assert_cmpstr (g_ptr_array_index (f->log, i++), ==, + "added device: /dev/input/event0"); + } + + g_assert_cmpuint (f->log->len, >, i); + g_assert_cmpstr (g_ptr_array_index (f->log, i++), ==, + "all for now"); + g_assert_cmpuint (f->log->len, >, i); + g_assert_cmpstr (g_ptr_array_index (f->log, i++), ==, + "start() returned"); + g_assert_cmpuint (f->log->len, ==, i); + + /* Don't explicitly stop it here. We test explicitly stopping in the + * other test-case */ + g_clear_object (&monitor); + + /* It's possible that not all the memory used is freed until we have + * iterate the main-context one last time */ + done = FALSE; + g_idle_add (done_cb, &done); + + while (!done) + g_main_context_iteration (NULL, TRUE); +} + +int +main (int argc, + char **argv) +{ + argv0 = argv[0]; + g_test_init (&argc, &argv, NULL); + + g_test_add ("/input-device/from-json", Fixture, NULL, + setup, test_input_device_from_json, teardown); + g_test_add ("/input-device/guess", Fixture, NULL, + setup, test_input_device_guess, teardown); + g_test_add ("/input-device/identity-from-hid-uevent", Fixture, NULL, + setup, test_input_device_identity_from_hid_uevent, teardown); + g_test_add ("/input-device/usb", Fixture, NULL, + setup, test_input_device_usb, teardown); + g_test_add ("/input-device/monitor/mock", Fixture, NULL, + setup, test_input_device_monitor, teardown); + g_test_add ("/input-device/monitor-once/mock", Fixture, NULL, + setup, test_input_device_monitor_once, teardown); + g_test_add ("/input-device/monitor/direct", Fixture, &direct_config, + setup, test_input_device_monitor, teardown); + g_test_add ("/input-device/monitor-once/direct", Fixture, &direct_config, + setup, test_input_device_monitor_once, teardown); + g_test_add ("/input-device/monitor/udev", Fixture, &udev_config, + setup, test_input_device_monitor, teardown); + g_test_add ("/input-device/monitor-once/udev", Fixture, &udev_config, + setup, test_input_device_monitor_once, teardown); + + return g_test_run (); +} diff --git a/tests/input-monitor-outputs/no-details.json b/tests/input-monitor-outputs/no-details.json new file mode 100644 index 0000000000000000000000000000000000000000..dcff69967f9ec1fce40023be4e6b45b92c7ec37d --- /dev/null +++ b/tests/input-monitor-outputs/no-details.json @@ -0,0 +1 @@ +{"added" : {}} diff --git a/tests/input-monitor-outputs/odd.json b/tests/input-monitor-outputs/odd.json new file mode 100644 index 0000000000000000000000000000000000000000..ab91a8b321a08e2325ec172a2a822d234f572652 --- /dev/null +++ b/tests/input-monitor-outputs/odd.json @@ -0,0 +1,36 @@ +{ + "added" : { + "interface_flags" : [ + "raw-hid", + "__future__" + ], + "type_flags" : [ + "__future__" + ], + "evdev" : { + "raw_abs" : "01 02 03 04 05 06 07 08 ff ff ff ff", + "raw_input_properties" : "12 34 56 78 87 65 43 21 ff ff ff ff" + }, + "hid_ancestor" : { + "sys_path" : "/...", + "name" : "Acme Weird Device", + "bus_type" : "0xfff1", + "vendor_id" : "0xfff1", + "product_id" : "0xfff1", + "uniq" : "12345678" + }, + "input_ancestor" : { + "sys_path" : "/...", + "bus_type" : "0xfff2", + "vendor_id" : "0xfff2", + "product_id" : "0xfff2", + "uniq" : "1234-5678" + }, + "usb_device_ancestor" : { + "sys_path" : "/...", + "vendor_id" : "0xfff3", + "product_id" : "0xfff3", + "serial" : "12:34:56:78" + } + } +} diff --git a/tests/input-monitor-outputs/steam-controller.json b/tests/input-monitor-outputs/steam-controller.json new file mode 100644 index 0000000000000000000000000000000000000000..6065ff8eef79781a3165ae38feef91ab77409a1c --- /dev/null +++ b/tests/input-monitor-outputs/steam-controller.json @@ -0,0 +1,155 @@ +{ + "added" : { + "interface_flags" : [ + "event" + ], + "type_flags" : [ + "keyboard", + "has-keys", + "mouse" + ], + "dev_node" : "/dev/input/event20", + "subsystem" : "input", + "sys_path" : "/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.1/1-1.1:1.0/0003:28DE:1142.00DD/input/input308/event20", + "bus_type" : "0x0003", + "vendor_id" : "0x28de", + "product_id" : "0x1142", + "version" : "0x0111", + "evdev" : { + "types" : [ + "SYN", + "KEY", + "REL", + "MSC", + "LED", + "REP" + ], + "raw_types" : "17 00 12 00 00 00 00 00 ", + "absolute_axes" : [], + "raw_abs" : "00 00 00 00 00 00 00 00 ", + "relative_axes" : [ + "X", + "Y", + "WHEEL", + "WHEEL_HI_RES" + ], + "raw_rel" : "03 09 00 00 00 00 00 00 ", + "keys" : [ + "KEY_ESC", + "KEY_0", + "KEY_A", + "KEY_KP0", + "BTN_MOUSE", + "BTN_RIGHT", + "BTN_MIDDLE", + "BTN_SIDE", + "BTN_EXTRA" + ], + "raw_keys" : "fe ff ff ff ff ff ff ff ff ff cf 01 df ff 80 e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ", + "input_properties" : [], + "raw_input_properties" : "00 00 00 00 00 00 00 00 ", + "guessed_type_flags" : [ + "keyboard", + "has-keys", + "mouse" + ] + }, + "udev_properties" : [ + "BACKSPACE=guess", + "DEVLINKS=/dev/input/by-id/usb-Valve_Software_Steam_Controller-event-mouse /dev/input/by-path/pci-0000:00:14.0-usb-0:1.1:1.0-event-mouse", + "DEVNAME=/dev/input/event20", + "DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.1/1-1.1:1.0/0003:28DE:1142.00DD/input/input308/event20", + "ID_BUS=usb", + "ID_INPUT=1", + "ID_INPUT_KEY=1", + "ID_INPUT_KEYBOARD=1", + "ID_INPUT_MOUSE=1", + "ID_MODEL=Steam_Controller", + "ID_MODEL_ENC=Steam\\x20Controller", + "ID_MODEL_ID=1142", + "ID_PATH=pci-0000:00:14.0-usb-0:1.1:1.0", + "ID_PATH_TAG=pci-0000_00_14_0-usb-0_1_1_1_0", + "ID_REVISION=0001", + "ID_SERIAL=Valve_Software_Steam_Controller", + "ID_TYPE=hid", + "ID_USB_DRIVER=usbhid", + "ID_USB_INTERFACES=:030101:030000:", + "ID_USB_INTERFACE_NUM=00", + "ID_VENDOR=Valve_Software", + "ID_VENDOR_ENC=Valve\\x20Software", + "ID_VENDOR_ID=28de", + "LIBINPUT_DEVICE_GROUP=3/28de/1142:usb-0000:00:14.0-1", + "MAJOR=13", + "MINOR=84", + "SUBSYSTEM=input", + "TAGS=:power-switch:", + "USEC_INITIALIZED=1162877255953", + "XKBLAYOUT=gb", + "XKBMODEL=pc105" + ], + "uevent" : [ + "MAJOR=13", + "MINOR=84", + "DEVNAME=input/event20" + ], + "hid_ancestor" : { + "sys_path" : "/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.1/1-1.1:1.0/0003:28DE:1142.00DD", + "name" : "Valve Software Steam Controller", + "bus_type" : "0x0003", + "vendor_id" : "0x28de", + "product_id" : "0x1142", + "uniq" : "", + "phys" : "usb-0000:00:14.0-1.1/input0", + "uevent" : [ + "DRIVER=hid-steam", + "HID_ID=0003:000028DE:00001142", + "HID_NAME=Valve Software Steam Controller", + "HID_PHYS=usb-0000:00:14.0-1.1/input0", + "HID_UNIQ=", + "MODALIAS=hid:b0003g0001v000028DEp00001142" + ] + }, + "input_ancestor" : { + "sys_path" : "/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.1/1-1.1:1.0/0003:28DE:1142.00DD/input/input308", + "name" : "Valve Software Steam Controller", + "bus_type" : "0x0003", + "vendor_id" : "0x28de", + "product_id" : "0x1142", + "version" : "0x0111", + "phys" : "usb-0000:00:14.0-1.1/input0", + "uevent" : [ + "PRODUCT=3/28de/1142/111", + "NAME=\"Valve Software Steam Controller\"", + "PHYS=\"usb-0000:00:14.0-1.1/input0\"", + "UNIQ=\"\"", + "PROP=0", + "EV=120017", + "KEY=1f0000 0 0 e080ffdf01cfffff fffffffffffffffe", + "REL=903", + "MSC=10", + "LED=1f", + "MODALIAS=input:b0003v28DEp1142e0111-e0,1,2,4,11,14,k77,7D,7E,7F,110,111,112,113,114,r0,1,8,B,am4,l0,1,2,3,4,sfw" + ] + }, + "usb_device_ancestor" : { + "sys_path" : "/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.1", + "vendor_id" : "0x28de", + "product_id" : "0x1142", + "version" : "0x0001", + "manufacturer" : "Valve Software", + "product" : "Steam Controller", + "serial" : null, + "uevent" : [ + "MAJOR=189", + "MINOR=66", + "DEVNAME=bus/usb/001/067", + "DEVTYPE=usb_device", + "DRIVER=usb", + "PRODUCT=28de/1142/1", + "TYPE=0/0/0", + "BUSNUM=001", + "DEVNUM=067" + ] + } + } +} diff --git a/tests/meson.build b/tests/meson.build index b2d5b1db772a355ebc60b5adc754d730136059b9..f23d73fbb8c48f68608f072e66c3e19f79bad610 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -31,6 +31,15 @@ tests = [ {'name': 'architecture'}, {'name': 'desktop-entry'}, {'name': 'graphics'}, + { + 'name': 'input-device', + 'static': true, + 'sources': [ + 'input-device.c', + 'mock-input-device.c', + 'mock-input-device.h', + ], + }, {'name': 'json-utils', 'static': true}, {'name': 'library'}, {'name': 'locale'}, @@ -104,6 +113,7 @@ install_subdir('expectations_with_missings', install_dir : tests_dir) # Vulkan *.json files. (The order of EGL *.json files is well-defined.) install_subdir('fake-icds', install_dir : tests_dir) install_subdir('fake-steam-runtime', install_dir : tests_dir) +install_subdir('input-monitor-output', install_dir : tests_dir) install_subdir('json-report', install_dir : tests_dir) meson.add_postconf_script( @@ -153,9 +163,11 @@ foreach test_info : tests deps = [libsteamrt_dep, test_utils_dep] endif + sources = test_info.get('sources', files(test_name + '.c')) + exe = executable( 'test-' + test_name, - files(test_name + '.c'), + sources, c_args : [ '-D_SRT_MULTIARCH="' + multiarch + '"', ], diff --git a/tests/mock-input-device.c b/tests/mock-input-device.c new file mode 100644 index 0000000000000000000000000000000000000000..123e411f73e3f17036f531a334f4859e1386e498 --- /dev/null +++ b/tests/mock-input-device.c @@ -0,0 +1,450 @@ +/* + * Mock input device monitor, loosely based on SDL code. + * + * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org> + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: Zlib + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#include "mock-input-device.h" + +#include <linux/input.h> + +#include <glib-unix.h> +#include <libglnx.h> + +#include "steam-runtime-tools/glib-backports-internal.h" +#include "steam-runtime-tools/input-device.h" +#include "steam-runtime-tools/input-device-internal.h" +#include "steam-runtime-tools/utils-internal.h" + +#define VENDOR_VALVE 0x28de +#define PRODUCT_VALVE_STEAM_CONTROLLER 0x1142 + +/* These aren't in the real vendor/product IDs, but we add them here + * to make the test able to distinguish. They look a bit like HID, + * EVDE(v) and USB, if you squint. */ +#define HID_MARKER 0x41D00000 +#define EVDEV_MARKER 0xE7DE0000 +#define USB_MARKER 0x05B00000 + +static void mock_input_device_iface_init (SrtInputDeviceInterface *iface); +static void mock_input_device_monitor_iface_init (SrtInputDeviceMonitorInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (MockInputDevice, + mock_input_device, + SRT_TYPE_SIMPLE_INPUT_DEVICE, + G_IMPLEMENT_INTERFACE (SRT_TYPE_INPUT_DEVICE, + mock_input_device_iface_init)) + +G_DEFINE_TYPE_WITH_CODE (MockInputDeviceMonitor, + mock_input_device_monitor, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (SRT_TYPE_INPUT_DEVICE_MONITOR, + mock_input_device_monitor_iface_init)) + +static void +mock_input_device_init (MockInputDevice *self) +{ +} + +static void +mock_input_device_class_init (MockInputDeviceClass *cls) +{ +} + +static int +mock_input_device_open_device (SrtInputDevice *device, + int flags, + GError **error) +{ + SrtInputDeviceInterfaceFlags iface_flags; + const char *devnode = NULL; + int fd = -1; + + if (!_srt_input_device_check_open_flags (flags, error)) + return -1; + + iface_flags = srt_input_device_get_interface_flags (device); + devnode = srt_input_device_get_dev_node (device); + + if (devnode == NULL) + { + glnx_throw (error, "Device has no device node"); + return -1; + } + + /* We aren't really going to open the device node, so provide a somewhat + * realistic permissions check. */ + switch (flags & (O_RDONLY | O_RDWR | O_WRONLY)) + { + case O_RDONLY: + if (!(iface_flags & SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, + "Device node cannot be read"); + return -1; + } + + break; + + case O_RDWR: + case O_WRONLY: + if (!(iface_flags & SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, + "Device node cannot be written"); + return -1; + } + + break; + + default: + /* _srt_input_device_check_open_flags should have caught this */ + g_return_val_if_reached (-1); + } + + /* This is a mock device, so open /dev/null instead of the real + * device node. */ + fd = open ("/dev/null", flags | _SRT_INPUT_DEVICE_ALWAYS_OPEN_FLAGS); + + if (fd < 0) + { + glnx_throw_errno_prefix (error, + "Unable to open device node \"%s\"", + devnode); + return -1; + } + + return fd; +} + +static void +mock_input_device_iface_init (SrtInputDeviceInterface *iface) +{ +#define IMPLEMENT(x) iface->x = mock_input_device_ ## x + + IMPLEMENT (open_device); + +#undef IMPLEMENT +} + +MockInputDevice * +mock_input_device_new (void) +{ + return g_object_new (MOCK_TYPE_INPUT_DEVICE, + NULL); +} + +typedef enum +{ + PROP_0, + PROP_FLAGS, + PROP_IS_ACTIVE, + N_PROPERTIES +} Property; + +static void +mock_input_device_monitor_init (MockInputDeviceMonitor *self) +{ + self->monitor_context = g_main_context_ref_thread_default (); + self->state = NOT_STARTED; + self->devices = g_hash_table_new_full (NULL, + NULL, + g_object_unref, + NULL); +} + +static void +mock_input_device_monitor_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + MockInputDeviceMonitor *self = MOCK_INPUT_DEVICE_MONITOR (object); + + switch ((Property) prop_id) + { + case PROP_IS_ACTIVE: + g_value_set_boolean (value, (self->state == STARTED)); + break; + + case PROP_FLAGS: + g_value_set_flags (value, self->flags); + break; + + case PROP_0: + case N_PROPERTIES: + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +mock_input_device_monitor_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + MockInputDeviceMonitor *self = MOCK_INPUT_DEVICE_MONITOR (object); + + switch ((Property) prop_id) + { + case PROP_FLAGS: + /* Construct-only */ + g_return_if_fail (self->flags == 0); + self->flags = g_value_get_flags (value); + break; + + case PROP_IS_ACTIVE: + case PROP_0: + case N_PROPERTIES: + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +mock_input_device_monitor_dispose (GObject *object) +{ + srt_input_device_monitor_stop (SRT_INPUT_DEVICE_MONITOR (object)); + G_OBJECT_CLASS (mock_input_device_monitor_parent_class)->dispose (object); +} + +static void +mock_input_device_monitor_finalize (GObject *object) +{ + srt_input_device_monitor_stop (SRT_INPUT_DEVICE_MONITOR (object)); + G_OBJECT_CLASS (mock_input_device_monitor_parent_class)->finalize (object); +} + +static void +mock_input_device_monitor_class_init (MockInputDeviceMonitorClass *cls) +{ + GObjectClass *object_class = G_OBJECT_CLASS (cls); + + object_class->get_property = mock_input_device_monitor_get_property; + object_class->set_property = mock_input_device_monitor_set_property; + object_class->dispose = mock_input_device_monitor_dispose; + object_class->finalize = mock_input_device_monitor_finalize; + + g_object_class_override_property (object_class, PROP_IS_ACTIVE, "is-active"); + g_object_class_override_property (object_class, PROP_FLAGS, "flags"); +} + +MockInputDeviceMonitor * +mock_input_device_monitor_new (SrtInputDeviceMonitorFlags flags) +{ + return g_object_new (MOCK_TYPE_INPUT_DEVICE_MONITOR, + "flags", flags, + NULL); +} + +void +mock_input_device_monitor_add (MockInputDeviceMonitor *self, + MockInputDevice *device) +{ + /* Ref it first in case the last ref to it is in the hash table */ + g_autoptr(MockInputDevice) ref = g_object_ref (device); + + mock_input_device_monitor_remove (self, device); + + g_hash_table_add (self->devices, g_steal_pointer (&ref)); + + _srt_input_device_monitor_emit_added (SRT_INPUT_DEVICE_MONITOR (self), + SRT_INPUT_DEVICE (device)); +} + +void +mock_input_device_monitor_remove (MockInputDeviceMonitor *self, + MockInputDevice *device) +{ + if (g_hash_table_steal (self->devices, device)) + { + _srt_input_device_monitor_emit_removed (SRT_INPUT_DEVICE_MONITOR (self), + SRT_INPUT_DEVICE (device)); + g_object_unref (device); + } +} + +static void +add_steam_controller (MockInputDeviceMonitor *self, + const char *tail, + MockInputDevice **dev_out) +{ + g_autoptr(MockInputDevice) mock = mock_input_device_new (); + SrtSimpleInputDevice *device = SRT_SIMPLE_INPUT_DEVICE (mock); + g_autoptr(GPtrArray) arr = g_ptr_array_new_full (1, g_free); + + device->iface_flags = (SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT + | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE + | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE); + device->dev_node = g_strdup_printf ("/dev/input/event%s", tail); + device->sys_path = g_strdup_printf ("/sys/devices/mock/usb/hid/input/input0/event%s", + tail); + device->subsystem = g_strdup ("input"); + device->uevent = g_strdup ("ONE=1\nTWO=2\n"); + + device->hid_ancestor.sys_path = g_strdup ("/sys/devices/mock/usb/hid"); + device->hid_ancestor.uevent = g_strdup ("HID=yes\n"); + + device->input_ancestor.sys_path = g_strdup ("/sys/devices/mock/usb/hid/input"); + device->input_ancestor.uevent = g_strdup ("INPUT=yes\n"); + + device->usb_device_ancestor.sys_path = g_strdup ("/sys/devices/mock/usb"); + device->usb_device_ancestor.uevent = g_strdup ("USB=usb_device\n"); + + g_ptr_array_add (arr, g_strdup ("ID_INPUT_JOYSTICK=1")); + g_ptr_array_add (arr, NULL); + device->udev_properties = (gchar **) g_ptr_array_free (g_steal_pointer (&arr), FALSE); + + /* This is a semi-realistic Steam Controller. */ + device->type_flags = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK; + device->bus_type = BUS_USB; + device->vendor_id = VENDOR_VALVE; + device->product_id = PRODUCT_VALVE_STEAM_CONTROLLER; + device->version = 0x0111; + /* We don't set all the bits, just enough to be vaguely realistic */ + set_bit (EV_KEY, device->evdev_caps.ev); + set_bit (EV_ABS, device->evdev_caps.ev); + set_bit (BTN_A, device->evdev_caps.keys); + set_bit (BTN_B, device->evdev_caps.keys); + set_bit (BTN_X, device->evdev_caps.keys); + set_bit (BTN_Y, device->evdev_caps.keys); + set_bit (ABS_X, device->evdev_caps.abs); + set_bit (ABS_Y, device->evdev_caps.abs); + set_bit (ABS_RX, device->evdev_caps.abs); + set_bit (ABS_RY, device->evdev_caps.abs); + /* This is unrealistic, but it's hard to test the properties if their + * value is zero */ + set_bit (INPUT_PROP_POINTER, device->evdev_caps.props); + + /* The part in square brackets isn't present on the real device, but + * makes this test more thorough by letting us distinguish. */ + device->hid_ancestor.name = g_strdup ("Valve Software Steam Controller"); + device->hid_ancestor.phys = g_strdup ("[hid]usb-0000:00:14.0-1.2/input1"); + device->hid_ancestor.uniq = g_strdup (""); + device->hid_ancestor.bus_type = HID_MARKER | BUS_USB; + device->hid_ancestor.vendor_id = HID_MARKER | VENDOR_VALVE; + device->hid_ancestor.product_id = HID_MARKER | PRODUCT_VALVE_STEAM_CONTROLLER; + + device->input_ancestor.name = g_strdup ("Wireless Steam Controller"); + device->input_ancestor.phys = g_strdup ("[input]usb-0000:00:14.0-1.2/input1"); + device->input_ancestor.uniq = g_strdup ("12345678"); + device->input_ancestor.bus_type = EVDEV_MARKER | BUS_USB; + device->input_ancestor.vendor_id = EVDEV_MARKER | VENDOR_VALVE; + device->input_ancestor.product_id = EVDEV_MARKER | PRODUCT_VALVE_STEAM_CONTROLLER; + device->input_ancestor.version = EVDEV_MARKER | 0x0111; + + device->usb_device_ancestor.vendor_id = USB_MARKER | VENDOR_VALVE; + device->usb_device_ancestor.product_id = USB_MARKER | PRODUCT_VALVE_STEAM_CONTROLLER; + device->usb_device_ancestor.device_version = USB_MARKER | 0x0001; + device->usb_device_ancestor.manufacturer = g_strdup ("Valve Software"); + device->usb_device_ancestor.product = g_strdup ("Steam Controller"); + device->usb_device_ancestor.serial = NULL; + + mock_input_device_monitor_add (self, mock); + + if (dev_out != NULL) + *dev_out = g_steal_pointer (&mock); +} + +static gboolean +briefly_add_steam_controller_in_idle_cb (gpointer user_data) +{ + MockInputDeviceMonitor *self = MOCK_INPUT_DEVICE_MONITOR (user_data); + g_autoptr(MockInputDevice) device = NULL; + + add_steam_controller (self, "-connected-briefly", &device); + mock_input_device_monitor_remove (self, device); + return G_SOURCE_REMOVE; +} + +static gboolean +enumerate_cb (gpointer user_data) +{ + MockInputDeviceMonitor *self = MOCK_INPUT_DEVICE_MONITOR (user_data); + + add_steam_controller (self, "0", NULL); + _srt_input_device_monitor_emit_all_for_now (SRT_INPUT_DEVICE_MONITOR (self)); + return G_SOURCE_REMOVE; +} + +static gboolean +mock_input_device_monitor_start (SrtInputDeviceMonitor *monitor, + GError **error) +{ + MockInputDeviceMonitor *self = MOCK_INPUT_DEVICE_MONITOR (monitor); + + g_return_val_if_fail (MOCK_IS_INPUT_DEVICE_MONITOR (monitor), FALSE); + g_return_val_if_fail (self->state == NOT_STARTED, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + self->state = STARTED; + + /* Make sure the signals for the initial batch of devices are emitted in + * the correct main-context */ + g_main_context_invoke_full (self->monitor_context, G_PRIORITY_DEFAULT, + enumerate_cb, g_object_ref (self), + g_object_unref); + + if ((self->flags & SRT_INPUT_DEVICE_MONITOR_FLAGS_ONCE) == 0) + { + GSource *source = g_idle_source_new (); + + g_source_set_callback (source, + briefly_add_steam_controller_in_idle_cb, + self, NULL); + g_source_set_priority (source, G_PRIORITY_DEFAULT); + g_source_attach (source, self->monitor_context); + self->sources = g_list_prepend (self->sources, g_steal_pointer (&source)); + } + + return TRUE; +} + +static void +mock_input_device_monitor_stop (SrtInputDeviceMonitor *monitor) +{ + MockInputDeviceMonitor *self = MOCK_INPUT_DEVICE_MONITOR (monitor); + + self->state = STOPPED; + + while (self->sources != NULL) + { + GSource *s = self->sources->data; + + self->sources = g_list_delete_link (self->sources, self->sources); + g_source_destroy (s); + g_source_unref (s); + } + + g_clear_pointer (&self->monitor_context, g_main_context_unref); + g_clear_pointer (&self->devices, g_hash_table_unref); +} + +static void +mock_input_device_monitor_iface_init (SrtInputDeviceMonitorInterface *iface) +{ +#define IMPLEMENT(x) iface->x = mock_input_device_monitor_ ## x + IMPLEMENT (start); + IMPLEMENT (stop); +#undef IMPLEMENT +} diff --git a/tests/mock-input-device.h b/tests/mock-input-device.h new file mode 100644 index 0000000000000000000000000000000000000000..afca72098ec74a8c797ce5549799910bd762b4a1 --- /dev/null +++ b/tests/mock-input-device.h @@ -0,0 +1,102 @@ +/* + * Mock input device monitor, loosely based on SDL code. + * + * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org> + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: Zlib + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#pragma once + +#include <linux/input.h> + +#include <glib-unix.h> +#include <libglnx.h> + +#include "steam-runtime-tools/glib-backports-internal.h" +#include "steam-runtime-tools/input-device.h" +#include "steam-runtime-tools/input-device-internal.h" +#include "steam-runtime-tools/simple-input-device-internal.h" + +typedef struct _MockInputDevice MockInputDevice; +typedef struct _MockInputDeviceClass MockInputDeviceClass; + +GType mock_input_device_get_type (void); +#define MOCK_TYPE_INPUT_DEVICE (mock_input_device_get_type ()) +#define MOCK_INPUT_DEVICE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), MOCK_TYPE_INPUT_DEVICE, MockInputDevice)) +#define MOCK_IS_INPUT_DEVICE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), MOCK_TYPE_INPUT_DEVICE)) +#define MOCK_INPUT_DEVICE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), MOCK_TYPE_INPUT_DEVICE, MockInputDeviceClass)) +#define MOCK_INPUT_DEVICE_CLASS(c) (G_TYPE_CHECK_CLASS_CAST ((c), MOCK_TYPE_INPUT_DEVICE, MockInputDeviceClass)) +#define MOCK_IS_INPUT_DEVICE_CLASS(c) (G_TYPE_CHECK_CLASS_TYPE ((c), MOCK_TYPE_INPUT_DEVICE)) + +struct _MockInputDevice +{ + SrtSimpleInputDevice parent; +}; + +struct _MockInputDeviceClass +{ + SrtSimpleInputDeviceClass parent; +}; + +MockInputDevice *mock_input_device_new (void); + +typedef struct _MockInputDeviceMonitor MockInputDeviceMonitor; +typedef struct _MockInputDeviceMonitorClass MockInputDeviceMonitorClass; + +GType mock_input_device_monitor_get_type (void); +#define MOCK_TYPE_INPUT_DEVICE_MONITOR (mock_input_device_monitor_get_type ()) +#define MOCK_INPUT_DEVICE_MONITOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), MOCK_TYPE_INPUT_DEVICE_MONITOR, MockInputDeviceMonitor)) +#define MOCK_IS_INPUT_DEVICE_MONITOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), MOCK_TYPE_INPUT_DEVICE_MONITOR)) +#define MOCK_INPUT_DEVICE_MONITOR_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), MOCK_TYPE_INPUT_DEVICE_MONITOR, MockInputDeviceMonitorClass)) +#define MOCK_INPUT_DEVICE_MONITOR_CLASS(c) (G_TYPE_CHECK_CLASS_CAST ((c), MOCK_TYPE_INPUT_DEVICE_MONITOR, MockInputDeviceMonitorClass)) +#define MOCK_IS_INPUT_DEVICE_MONITOR_CLASS(c) (G_TYPE_CHECK_CLASS_TYPE ((c), MOCK_TYPE_INPUT_DEVICE_MONITOR)) + +struct _MockInputDeviceMonitor +{ + GObject parent; + + GMainContext *monitor_context; + GHashTable *devices; + GList *sources; + + SrtInputDeviceMonitorFlags flags; + enum + { + NOT_STARTED = 0, + STARTED, + STOPPED + } state; +}; + +struct _MockInputDeviceMonitorClass +{ + GObjectClass parent; +}; + +MockInputDeviceMonitor *mock_input_device_monitor_new (SrtInputDeviceMonitorFlags flags); + +void mock_input_device_monitor_add (MockInputDeviceMonitor *self, + MockInputDevice *device); +void mock_input_device_monitor_remove (MockInputDeviceMonitor *self, + MockInputDevice *device); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (MockInputDevice, g_object_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (MockInputDeviceMonitor, g_object_unref) diff --git a/tests/utils.c b/tests/utils.c index 25c70547c87fc504dd5ac07d1740d71f57b302e2..b7bb9102c94256d5de7bc36c382bbe9e8f7eee2f 100644 --- a/tests/utils.c +++ b/tests/utils.c @@ -29,6 +29,7 @@ #include <glib/gstdio.h> #include <glib-object.h> +#include "steam-runtime-tools/input-device-internal.h" #include "steam-runtime-tools/utils-internal.h" #include "test-utils.h" @@ -79,6 +80,88 @@ test_avoid_gvfs (Fixture *f, G_OBJECT_TYPE (local)); } +static void +test_evdev_bits (Fixture *f, + gconstpointer context) +{ + unsigned long words[] = { 0x00020001, 0x00080005 }; + +#ifdef __i386__ + g_assert_cmpuint (BITS_PER_LONG, ==, 32); + g_assert_cmpuint (LONGS_FOR_BITS (1), ==, 1); + g_assert_cmpuint (LONGS_FOR_BITS (32), ==, 1); + g_assert_cmpuint (LONGS_FOR_BITS (33), ==, 2); + g_assert_cmpuint (CHOOSE_BIT (0), ==, 0); + g_assert_cmpuint (CHOOSE_BIT (31), ==, 31); + g_assert_cmpuint (CHOOSE_BIT (32), ==, 0); + g_assert_cmpuint (CHOOSE_BIT (33), ==, 1); + g_assert_cmpuint (CHOOSE_BIT (63), ==, 31); + g_assert_cmpuint (CHOOSE_BIT (64), ==, 0); + g_assert_cmpuint (CHOOSE_LONG (0), ==, 0); + g_assert_cmpuint (CHOOSE_LONG (31), ==, 0); + g_assert_cmpuint (CHOOSE_LONG (32), ==, 1); + g_assert_cmpuint (CHOOSE_LONG (33), ==, 1); + g_assert_cmpuint (CHOOSE_LONG (63), ==, 1); + g_assert_cmpuint (CHOOSE_LONG (64), ==, 2); +#elif defined(__LP64__) + g_assert_cmpuint (BITS_PER_LONG, ==, 64); + g_assert_cmpuint (LONGS_FOR_BITS (1), ==, 1); + g_assert_cmpuint (LONGS_FOR_BITS (64), ==, 1); + g_assert_cmpuint (LONGS_FOR_BITS (65), ==, 2); + g_assert_cmpuint (CHOOSE_BIT (0), ==, 0); + g_assert_cmpuint (CHOOSE_BIT (63), ==, 63); + g_assert_cmpuint (CHOOSE_BIT (64), ==, 0); + g_assert_cmpuint (CHOOSE_BIT (65), ==, 1); + g_assert_cmpuint (CHOOSE_BIT (127), ==, 63); + g_assert_cmpuint (CHOOSE_BIT (128), ==, 0); + g_assert_cmpuint (CHOOSE_LONG (0), ==, 0); + g_assert_cmpuint (CHOOSE_LONG (63), ==, 0); + g_assert_cmpuint (CHOOSE_LONG (64), ==, 1); + g_assert_cmpuint (CHOOSE_LONG (65), ==, 1); + g_assert_cmpuint (CHOOSE_LONG (127), ==, 1); + g_assert_cmpuint (CHOOSE_LONG (128), ==, 2); +#endif + + /* Among bits 0 to 15, only bit 0 (0x1) is set */ + g_assert_cmpuint (test_bit_checked (0, words, G_N_ELEMENTS (words)), ==, 1); + g_assert_cmpuint (test_bit_checked (1, words, G_N_ELEMENTS (words)), ==, 0); + g_assert_cmpuint (test_bit_checked (15, words, G_N_ELEMENTS (words)), ==, 0); + + /* Among bits 16 to 31, only bit 17 (0x2 << 16) is set */ + g_assert_cmpuint (test_bit_checked (16, words, G_N_ELEMENTS (words)), ==, 0); + g_assert_cmpuint (test_bit_checked (17, words, G_N_ELEMENTS (words)), ==, 1); + g_assert_cmpuint (test_bit_checked (18, words, G_N_ELEMENTS (words)), ==, 0); + g_assert_cmpuint (test_bit_checked (31, words, G_N_ELEMENTS (words)), ==, 0); + +#ifdef __i386__ + /* Among bits 32 to 63, only bits 32 (0x1 << 32), 34 (0x4 << 32) + * and 51 (0x8 << 48) are set, and they don't count as set unless we + * allow ourselves to look that far */ + g_assert_cmpuint (test_bit_checked (32, words, 1), ==, 0); + g_assert_cmpuint (test_bit_checked (32, words, G_N_ELEMENTS (words)), ==, 1); + g_assert_cmpuint (test_bit_checked (33, words, G_N_ELEMENTS (words)), ==, 0); + g_assert_cmpuint (test_bit_checked (34, words, 1), ==, 0); + g_assert_cmpuint (test_bit_checked (34, words, G_N_ELEMENTS (words)), ==, 1); + g_assert_cmpuint (test_bit_checked (35, words, G_N_ELEMENTS (words)), ==, 0); + g_assert_cmpuint (test_bit_checked (50, words, G_N_ELEMENTS (words)), ==, 0); + g_assert_cmpuint (test_bit_checked (51, words, G_N_ELEMENTS (words)), ==, 1); + g_assert_cmpuint (test_bit_checked (52, words, G_N_ELEMENTS (words)), ==, 0); +#elif defined(__LP64__) + /* Among bits 64 to 127, only bits 64 (0x1 << 64), 66 (0x4 << 64) + * and 83 (0x8 << 80) are set, and they don't count as set unless we + * allow ourselves to look that far */ + g_assert_cmpuint (test_bit_checked (64, words, 1), ==, 0); + g_assert_cmpuint (test_bit_checked (64, words, G_N_ELEMENTS (words)), ==, 1); + g_assert_cmpuint (test_bit_checked (65, words, G_N_ELEMENTS (words)), ==, 0); + g_assert_cmpuint (test_bit_checked (66, words, 1), ==, 0); + g_assert_cmpuint (test_bit_checked (66, words, G_N_ELEMENTS (words)), ==, 1); + g_assert_cmpuint (test_bit_checked (67, words, G_N_ELEMENTS (words)), ==, 0); + g_assert_cmpuint (test_bit_checked (82, words, G_N_ELEMENTS (words)), ==, 0); + g_assert_cmpuint (test_bit_checked (83, words, G_N_ELEMENTS (words)), ==, 1); + g_assert_cmpuint (test_bit_checked (84, words, G_N_ELEMENTS (words)), ==, 0); +#endif +} + typedef struct { const char *name; @@ -216,6 +299,77 @@ filter_gameoverlayrenderer (Fixture *f, g_free (filtered_preload); } +static void +test_str_is_integer (Fixture *f, + gconstpointer context) +{ + g_assert_false (_srt_str_is_integer ("")); + g_assert_false (_srt_str_is_integer ("no")); + g_assert_true (_srt_str_is_integer ("1")); + g_assert_true (_srt_str_is_integer ("123456789012345678901234567890")); + g_assert_false (_srt_str_is_integer ("1.23")); + g_assert_false (_srt_str_is_integer ("x23")); + g_assert_false (_srt_str_is_integer ("23a")); +} + +static const char uevent[] = +"DRIVER=lenovo\n" +"HID_ID=0003:000017EF:00006009\n" +"HID_NAME=Lite-On Technology Corp. ThinkPad USB Keyboard with TrackPoint\n" +"HID_PHYS=usb-0000:00:14.0-2/input0\n" +"HID_UNIQ=\n" +"MODALIAS=hid:b0003g0000v000017EFp00006009\n"; + +static struct +{ + const char *key; + const char *value; +} uevent_parsed[] = +{ + { "DRIVER", "lenovo" }, + { "HID_ID", "0003:000017EF:00006009" }, + { "HID_NAME", "Lite-On Technology Corp. ThinkPad USB Keyboard with TrackPoint" }, + { "HID_PHYS", "usb-0000:00:14.0-2/input0" }, + { "HID_UNIQ", "" }, + { "MODALIAS", "hid:b0003g0000v000017EFp00006009" } +}; + +static const char no_newline[] = "DRIVER=lenovo"; + +static void +test_uevent_field (Fixture *f, + gconstpointer context) +{ + gsize i; + + g_assert_false (_srt_input_device_uevent_field_equals (no_newline, "DRIVER", "")); + g_assert_false (_srt_input_device_uevent_field_equals (no_newline, "DRIVER", "lenov")); + g_assert_true (_srt_input_device_uevent_field_equals (no_newline, "DRIVER", "lenovo")); + g_assert_false (_srt_input_device_uevent_field_equals (no_newline, "DRIVER", "lenovoo")); + + g_assert_false (_srt_input_device_uevent_field_equals (uevent, "DRIVER", "lenov")); + g_assert_false (_srt_input_device_uevent_field_equals (uevent, "DRIVER", "lenovoo")); + g_assert_false (_srt_input_device_uevent_field_equals (uevent, "HID_ID", "0003:000017EF:0000600")); + g_assert_false (_srt_input_device_uevent_field_equals (uevent, "HID_ID", "0003:000017EF:000060099")); + g_assert_false (_srt_input_device_uevent_field_equals (uevent, "HID_UNIQ", "x")); + g_assert_false (_srt_input_device_uevent_field_equals (uevent, "MODALIAS", "nope")); + g_assert_false (_srt_input_device_uevent_field_equals (uevent, "NOPE", "")); + g_assert_false (_srt_input_device_uevent_field_equals (uevent, "NOPE", "nope")); + + for (i = 0; i < G_N_ELEMENTS (uevent_parsed); i++) + { + g_autofree gchar *v = NULL; + + v = _srt_input_device_uevent_field (uevent, uevent_parsed[i].key); + g_assert_cmpstr (v, ==, uevent_parsed[i].value); + g_assert_true (_srt_input_device_uevent_field_equals (uevent, + uevent_parsed[i].key, + uevent_parsed[i].value)); + } + + g_assert_null (_srt_input_device_uevent_field (uevent, "NOPE")); +} + int main (int argc, char **argv) @@ -224,10 +378,16 @@ main (int argc, g_test_init (&argc, &argv, NULL); g_test_add ("/utils/avoid-gvfs", Fixture, NULL, setup, test_avoid_gvfs, teardown); + g_test_add ("/utils/evdev-bits", Fixture, NULL, + setup, test_evdev_bits, teardown); g_test_add ("/utils/test-file-in-sysroot", Fixture, NULL, setup, test_file_in_sysroot, teardown); g_test_add ("/utils/filter_gameoverlayrenderer", Fixture, NULL, setup, filter_gameoverlayrenderer, teardown); + g_test_add ("/utils/str_is_integer", Fixture, NULL, + setup, test_str_is_integer, teardown); + g_test_add ("/utils/uevent-field", Fixture, NULL, + setup, test_uevent_field, teardown); return g_test_run (); }