From 254e8be1c5070509dc940c85278b636b59f879e3 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Mon, 15 Dec 2025 10:41:21 +1000 Subject: [PATCH 1/5] ei: match the EI_EVENT_DEVICE_PAUSED docs with the protocol This is a behavior break if we're looking at the documentation only but the protocol has required the logical reset of the device since libei 0.5 - this here is just stale documentation that didn't get updated. And keeping the state across paused/resume is too hard to get right anyway. --- src/libei.h | 17 ++++++++++++++--- src/libeis.h | 11 ++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/libei.h b/src/libei.h index 151b24e..70fbc06 100644 --- a/src/libei.h +++ b/src/libei.h @@ -396,13 +396,24 @@ enum ei_event_type { /** * Any events sent from this device will be discarded until the next - * resume. The state of a device is not expected to change between - * pause/resume - for any significant state changes the server is - * expected to remove the device instead. + * resume. + * + * Pausing a device resets the logical state of the device to neutral. + * This includes: + * - any buttons or keys logically down are released + * - any modifiers logically down are released + * - any touches logically down are released + * + * Sender clients must wait until @ref EI_EVENT_DEVICE_RESUMED + * before sending events. */ EI_EVENT_DEVICE_PAUSED, + /** * The client may send events. + * + * Once resumed, a sender client may call ei_device_start_emulating() + * and begin emulating events. */ EI_EVENT_DEVICE_RESUMED, diff --git a/src/libeis.h b/src/libeis.h index 17b2619..27ce2de 100644 --- a/src/libeis.h +++ b/src/libeis.h @@ -1349,11 +1349,12 @@ eis_device_remove(struct eis_device *device); * a number of events from a device after it has been paused and must * update its internal state accordingly. * - * Pause/resume should only be used for short-term event delaying, a client - * will expect that the device's state has not changed between pause and - * resume. Where a device's state changes on the EIS implementation side (e.g. - * buttons or keys are forcibly released), the device should be removed and - * re-added as new device. + * Pausing a device resets the logical state of the device to neutral. + * This includes: + * - any buttons or keys logically down are released + * - any modifiers logically down are released + * - any touches logically down are released + * No events will be sent for these releases back to a neutral state. * * @param device A connected device */ From 14fd88c12ef4efa27b8c42b8240b92cfd57b21cb Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Wed, 10 Dec 2025 16:07:41 +1000 Subject: [PATCH 2/5] eis: quietly ignore double key presses and releases This is only implemented on the EIS side of things because that side is mostly what we want to protect (read: the compositors). The duplicates are still sent on the protocol. --- src/libeis-device.c | 16 ++++++++++++++++ src/libeis-device.h | 22 ++++++++++++++++++++++ test/test-ei-device.c | 20 ++++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/src/libeis-device.c b/src/libeis-device.c index a7bf00d..df2d009 100644 --- a/src/libeis-device.c +++ b/src/libeis-device.c @@ -428,6 +428,13 @@ client_msg_button(struct eis_button *button, uint32_t btn, uint32_t state) "Button event for non-button device"); } + if (btn >= KEY_CNT) + return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL, + "Button event for invalid button %x (KEY_CNT is %#x)", btn, KEY_CNT); + + if (!eis_device_update_key_button_state(device, btn, state)) + return NULL; + if (device->state == EIS_DEVICE_STATE_EMULATING) { eis_queue_pointer_button_event(device, btn, !!state); return NULL; @@ -598,6 +605,13 @@ client_msg_keyboard_key(struct eis_keyboard *keyboard, uint32_t key, uint32_t st "Key event for non-keyboard device"); } + if (key >= KEY_CNT) + return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL, + "Key event for invalid key %x (KEY_CNT is %#x)", key, KEY_CNT); + + if (!eis_device_update_key_button_state(device, key, state)) + return NULL; + if (device->state == EIS_DEVICE_STATE_EMULATING) { eis_queue_keyboard_key_event(device, key, !!state); return NULL; @@ -1447,6 +1461,8 @@ eis_device_pause(struct eis_device *device) device->state = EIS_DEVICE_STATE_PAUSED; eis_device_event_paused(device, eis_client_get_next_serial(client)); + + memset(device->key_button_state.down, 0, sizeof(device->key_button_state.down)); } _public_ void diff --git a/src/libeis-device.h b/src/libeis-device.h index 5f6f2a1..97e74fa 100644 --- a/src/libeis-device.h +++ b/src/libeis-device.h @@ -26,10 +26,14 @@ #include "libeis.h" +#include "util-bits.h" #include "util-object.h" #include "util-list.h" #include "brei-shared.h" +#define KEY_MAX 0x2ffU +#define KEY_CNT (KEY_MAX + 1) + enum eis_device_state { EIS_DEVICE_STATE_NEW, EIS_DEVICE_STATE_AWAITING_READY, @@ -75,6 +79,9 @@ struct eis_device { bool x_is_cancelled, y_is_cancelled; } scroll_state; + struct { + unsigned char down[NCHARS(KEY_CNT)]; + } key_button_state; }; struct eis_touch { @@ -118,6 +125,21 @@ OBJECT_DECLARE_GETTER(eis_device, button_interface, const struct eis_button_inte OBJECT_DECLARE_GETTER(eis_device, keyboard_interface, const struct eis_keyboard_interface *); OBJECT_DECLARE_GETTER(eis_device, touchscreen_interface, const struct eis_touchscreen_interface *); +static inline bool +eis_device_update_key_button_state(struct eis_device *device, uint32_t key_btn, uint32_t state) +{ + if (state) { + if (bit_is_set(device->key_button_state.down, key_btn)) + return false; + set_bit(device->key_button_state.down, key_btn); + } else { + if (!bit_is_set(device->key_button_state.down, key_btn)) + return false; + clear_bit(device->key_button_state.down, key_btn); + } + return true; +} + void eis_device_set_client_keymap(struct eis_device *device, enum eis_keymap_type type, diff --git a/test/test-ei-device.c b/test/test-ei-device.c index 261de35..df24604 100644 --- a/test/test-ei-device.c +++ b/test/test-ei-device.c @@ -390,10 +390,20 @@ MUNIT_TEST(test_ei_device_button_button) struct ei_device *device = peck_ei_get_default_pointer(peck); ei_device_button_button(device, BTN_LEFT, true); ei_device_frame(device, peck_ei_now(peck)); + + /* double press is quietly ignored */ + ei_device_button_button(device, BTN_LEFT, true); + ei_device_frame(device, peck_ei_now(peck)); + ei_device_button_button(device, BTN_RIGHT, true); ei_device_frame(device, peck_ei_now(peck)); ei_device_button_button(device, BTN_RIGHT, false); ei_device_frame(device, peck_ei_now(peck)); + + /* double release is quietly ignored */ + ei_device_button_button(device, BTN_RIGHT, false); + ei_device_frame(device, peck_ei_now(peck)); + ei_device_button_button(device, BTN_LEFT, false); ei_device_frame(device, peck_ei_now(peck)); } @@ -421,6 +431,7 @@ MUNIT_TEST(test_ei_device_button_button) munit_assert_int(eis_event_button_get_button(lu), ==, BTN_LEFT); munit_assert_false(eis_event_button_get_is_press(lu)); + peck_assert_no_eis_events(eis); } return MUNIT_OK; @@ -439,6 +450,15 @@ MUNIT_TEST(test_ei_device_keyboard_key) struct ei_device *device = peck_ei_get_default_keyboard(peck); ei_device_keyboard_key(device, KEY_Q, true); ei_device_frame(device, peck_ei_now(peck)); + + /* Double press is quietly ignored */ + ei_device_keyboard_key(device, KEY_Q, true); + ei_device_frame(device, peck_ei_now(peck)); + + ei_device_keyboard_key(device, KEY_Q, false); + ei_device_frame(device, peck_ei_now(peck)); + + /* Double release is quietly ignored */ ei_device_keyboard_key(device, KEY_Q, false); ei_device_frame(device, peck_ei_now(peck)); } From 2d8df18ab5e7297a6f55ec53e6dc403c73955f96 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Wed, 10 Dec 2025 16:25:09 +1000 Subject: [PATCH 3/5] eis: keep track of touch IDs and don't allow duplicate ones A client sending duplicate touch IDs will be disconnected but motion or end events for touches that no longer exist will be silently ignored. The tracking state uses a uint64_t to store currently valid touch ids - since the whole range of touch ids are uint32_t (including zero) this is the simple way of using an out-of-range marker value (UINT64_MAX) --- src/libeis-device.c | 70 ++++++++++++++++++++++++++++++++-- src/libeis-device.h | 6 +++ test/test_protocol.py | 87 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 3 deletions(-) diff --git a/src/libeis-device.c b/src/libeis-device.c index df2d009..4510ef1 100644 --- a/src/libeis-device.c +++ b/src/libeis-device.c @@ -25,6 +25,7 @@ #include "config.h" #include +#include #include "util-macros.h" #include "util-bits.h" @@ -641,6 +642,35 @@ eis_device_get_keyboard_interface(struct eis_device *device) return &keyboard_interface; } +/* Returns true and the position of the touch with the given ID, or + * false and the first position that is available + */ +static bool +find_touch(struct eis_device *device, uint32_t touchid, size_t *index) +{ + ssize_t first_available = -1; + for (size_t i = 0; i < ARRAY_LENGTH(device->touch_state.down); i++) { + if (device->touch_state.down[i] != UINT64_MAX) { + if (device->touch_state.down[i] == touchid) { + if (index) + *index = i; + return true; + } + } else if (first_available < 0) { + first_available = i; + } + } + + if (index) { + if (first_available < 0) + *index = EIS_MAX_TOUCHES; + else + *index = (size_t)first_available; + } + + return false; +} + static struct brei_result * client_msg_touch_down(struct eis_touchscreen *touchscreen, uint32_t touchid, float x, float y) @@ -655,6 +685,17 @@ client_msg_touch_down(struct eis_touchscreen *touchscreen, } if (device->state == EIS_DEVICE_STATE_EMULATING) { + size_t first_available; + if (find_touch(device, touchid, &first_available)) { + return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL, + "Touch down event for duplicated touch ID"); + } + + if (first_available >= EIS_MAX_TOUCHES) + return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_ERROR, + "Too many simultaneous touch events"); + + device->touch_state.down[first_available] = touchid; eis_queue_touch_down_event(device, touchid, x, y); return NULL; } @@ -676,13 +717,26 @@ client_msg_touch_motion(struct eis_touchscreen *touchscreen, } if (device->state == EIS_DEVICE_STATE_EMULATING) { - eis_queue_touch_motion_event(device, touchid, x, y); + /* Silently ignore motion for non-existing touches */ + if (find_touch(device, touchid, NULL)) + eis_queue_touch_motion_event(device, touchid, x, y); return NULL; } return maybe_error_on_device_state(device, "touch motion"); } +static bool +release_touch(struct eis_device *device, uint32_t touchid) +{ + size_t index; + bool rc = find_touch(device, touchid, &index); + if (rc) + device->touch_state.down[index] = UINT64_MAX; + + return rc; +} + static struct brei_result * client_msg_touch_up(struct eis_touchscreen *touchscreen, uint32_t touchid) { @@ -696,7 +750,8 @@ client_msg_touch_up(struct eis_touchscreen *touchscreen, uint32_t touchid) } if (device->state == EIS_DEVICE_STATE_EMULATING) { - eis_queue_touch_up_event(device, touchid); + if (release_touch(device, touchid)) + eis_queue_touch_up_event(device, touchid); return NULL; } @@ -722,7 +777,8 @@ client_msg_touch_cancel(struct eis_touchscreen *touchscreen, uint32_t touchid) } if (device->state == EIS_DEVICE_STATE_EMULATING) { - eis_queue_touch_cancel_event(device, touchid); + if (release_touch(device, touchid)) + eis_queue_touch_cancel_event(device, touchid); return NULL; } @@ -776,6 +832,10 @@ eis_seat_new_device(struct eis_seat *seat) list_append(&seat->devices, &device->link); + for (size_t i = 0; i < ARRAY_LENGTH(device->touch_state.down); i++) { + device->touch_state.down[i] = UINT64_MAX; + } + return eis_device_ref(device); } @@ -1463,6 +1523,10 @@ eis_device_pause(struct eis_device *device) eis_device_event_paused(device, eis_client_get_next_serial(client)); memset(device->key_button_state.down, 0, sizeof(device->key_button_state.down)); + + for (size_t i = 0; i < ARRAY_LENGTH(device->touch_state.down); i++) { + device->touch_state.down[i] = UINT64_MAX; + } } _public_ void diff --git a/src/libeis-device.h b/src/libeis-device.h index 97e74fa..3d060a0 100644 --- a/src/libeis-device.h +++ b/src/libeis-device.h @@ -34,6 +34,8 @@ #define KEY_MAX 0x2ffU #define KEY_CNT (KEY_MAX + 1) +#define EIS_MAX_TOUCHES 16 + enum eis_device_state { EIS_DEVICE_STATE_NEW, EIS_DEVICE_STATE_AWAITING_READY, @@ -82,6 +84,10 @@ struct eis_device { struct { unsigned char down[NCHARS(KEY_CNT)]; } key_button_state; + + struct { + uint64_t down[EIS_MAX_TOUCHES]; /* touch id */ + } touch_state; }; struct eis_touch { diff --git a/test/test_protocol.py b/test/test_protocol.py index 98d2f14..6c2e5c6 100644 --- a/test/test_protocol.py +++ b/test/test_protocol.py @@ -1259,3 +1259,90 @@ class TestEiProtocol: else: ei.callback_roundtrip() assert status.disconnected is False + + def test_touch_disconnect_on_duplicate_id(self, eis): + """ + Ensure EIS disconnects us when we use a duplicate touch id + """ + + ei = eis.ei + + @dataclass + class Status: + device: EiDevice = None + touchscreen: Optional[EiTouchscreen] = None + disconnected: bool = False + resumed: bool = False + serial: int = 0 + + status = Status() + + def on_interface(device, object, name, version, new_objects): + logger.debug( + "new capability", + device=device, + object=object, + name=name, + version=version, + ) + if name == InterfaceName.EI_TOUCHSCREEN: + assert status.touchscreen is None + status.touchscreen = new_objects["object"] + + def on_device_resumed(device, serial): + status.resumed = True + status.serial = serial + + def on_new_device(seat, device, version, new_objects): + logger.debug("new device", object=new_objects["device"]) + status.device = new_objects["device"] + status.device.connect("Interface", on_interface) + status.device.connect("Resumed", on_device_resumed) + + def on_new_object(o: Interface): + logger.debug("new object", object=o) + if o.name == InterfaceName.EI_SEAT: + ei.seat_fill_capability_masks(o) + o.connect("Device", on_new_device) + + ei.context.connect("register", on_new_object) + ei.dispatch() + + def on_disconnected(connection, last_serial, reason, explanation): + status.disconnected = True + + def on_connection(setup, serial, id, version, new_objects={}): + connection = new_objects["connection"] + connection.connect("Disconnected", on_disconnected) + + setup = ei.handshake + setup.connect("Connection", on_connection) + ei.init_default_sender_connection(interface_versions={"ei_touchscreen": 1}) + + ei.wait_for_seat() + seat = ei.seats[0] + ei.send(seat.Bind(seat.bind_mask([InterfaceName.EI_TOUCHSCREEN]))) + ei.wait_for(lambda: status.touchscreen and status.resumed) + + assert status.touchscreen is not None + + ei.send(status.device.StartEmulating(status.serial, 123)) + logger.debug("Sending touch events") + touchid = 1 + touchscreen = status.touchscreen + device = status.device + ei.send(touchscreen.Down(touchid, 10, 20)) + ei.send(device.Frame(status.serial, int(time.time()))) + ei.send(touchscreen.Motion(touchid, 10, 25)) + ei.send(device.Frame(status.serial, int(time.time()))) + + ei.send(touchscreen.Down(touchid, 10, 20)) + try: + ei.send(device.Frame(status.serial, int(time.time()))) + except BrokenPipeError: + pass + + ei.dispatch() + ei.wait_for(lambda: status.disconnected) + + assert status.disconnected is True From b141d108696526f4561849afb7da56ed7a3df96c Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Wed, 10 Dec 2025 16:30:33 +1000 Subject: [PATCH 4/5] eis: end any ongoing touch in response to touch_end Even if the client is no longer emulating (and we're not queuing an event) our local touch must be ended. Otherwise our touch will live on forever, despite the client thinking it has ended properly. This can't be triggered with libei because we can't send touch events while not emulating and the only way to get into this state is pausing the device - which already resets the state. But let's add a test case anyway, in the hope that one day it picks up a bug. --- src/libeis-device.c | 12 ++++-- test/test-ei-device.c | 90 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/src/libeis-device.c b/src/libeis-device.c index 4510ef1..a2345fb 100644 --- a/src/libeis-device.c +++ b/src/libeis-device.c @@ -749,8 +749,10 @@ client_msg_touch_up(struct eis_touchscreen *touchscreen, uint32_t touchid) "Touch up event for non-touch device"); } - if (device->state == EIS_DEVICE_STATE_EMULATING) { - if (release_touch(device, touchid)) + /* End the touch locally even if we're not emulating, but + * silently ignore touch end/cancel for non-existing touches */ + if (release_touch(device, touchid)) { + if (device->state == EIS_DEVICE_STATE_EMULATING) eis_queue_touch_up_event(device, touchid); return NULL; } @@ -776,8 +778,10 @@ client_msg_touch_cancel(struct eis_touchscreen *touchscreen, uint32_t touchid) "Touch cancel event for touchscreen version v1"); } - if (device->state == EIS_DEVICE_STATE_EMULATING) { - if (release_touch(device, touchid)) + /* End the touch locally even if we're not emulating, but + * silently ignore touch end/cancel for non-existing touches */ + if (release_touch(device, touchid)) { + if (device->state == EIS_DEVICE_STATE_EMULATING) eis_queue_touch_cancel_event(device, touchid); return NULL; } diff --git a/test/test-ei-device.c b/test/test-ei-device.c index df24604..782b8e4 100644 --- a/test/test-ei-device.c +++ b/test/test-ei-device.c @@ -1523,6 +1523,96 @@ MUNIT_TEST(test_ei_device_multitouch) return MUNIT_OK; } +MUNIT_TEST(test_ei_device_touch_up_after_paused) +{ + _unref_(peck) *peck = peck_new(); + _unref_(ei_device) *device = NULL; + _unref_(eis_device) *eis_device = NULL; + + peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_ALL); + peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ADD_TOUCH); + peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_AUTODEVICES); + peck_dispatch_until_stable(peck); + + _unref_(ei_touch) *t1 = NULL; + _unref_(ei_touch) *t2 = NULL; + + with_client(peck) { + device = ei_device_ref(peck_ei_get_default_touch(peck)); + t1 = ei_device_touch_new(device); + t2 = ei_device_touch_new(device); + ei_touch_down(t1, 1, 2); + ei_touch_down(t2, 3, 4); + ei_device_frame(device, peck_ei_now(peck)); + } + + peck_dispatch_until_stable(peck); + + with_server(peck) { + _unref_(eis_event) *down1 = peck_eis_touch_down(eis, 1, 2); + _unref_(eis_event) *down2 = peck_eis_touch_down(eis, 3, 4); + + peck_assert_no_eis_events(eis); /* drain the frame */ + + eis_device = eis_device_ref(eis_event_get_device(down1)); + eis_device_pause(eis_device); + } + + /* No ei dispatch here */ + peck_dispatch_eis(peck); + + with_client(peck) { + /* These events will arrive when the device is paused and + * are discarded by EIS */ + ei_touch_up(t1); + ei_touch_up(t2); + ei_device_frame(device, peck_ei_now(peck)); + ei_touch_unref(t1); + ei_touch_unref(t2); + } + + peck_dispatch_until_stable(peck); + + with_client(peck) { + _unref_(ei_event) *pause = peck_ei_next_event(ei, EI_EVENT_DEVICE_PAUSED); + } + + with_server(peck) { + /* touch up and empty frame were discarded */ + peck_assert_no_eis_events(eis); + eis_device_resume(eis_device); + } + + peck_dispatch_until_stable(peck); + + with_client(peck) { + ei_device_start_emulating(device, 123); + + /* The C API doesn't allow us to set a touch id + * so we can't really test for the correct behavior. + * All we can do is exercise most of the code by creating + * new touches and hope. + */ + t1 = ei_device_touch_new(device); + t2 = ei_device_touch_new(device); + ei_touch_down(t1, 1, 2); + ei_touch_down(t2, 3, 4); + ei_device_frame(device, peck_ei_now(peck)); + } + + peck_dispatch_until_stable(peck); + + with_server(peck) { + _unref_(eis_event) *down1 = peck_eis_touch_down(eis, 1, 2); + _unref_(eis_event) *down2 = peck_eis_touch_down(eis, 3, 4); + peck_assert_no_eis_events(eis); /* drain the frame */ + } + + peck_dispatch_until_stable(peck); + + return MUNIT_OK; +} + #if HAVE_MEMFD_CREATE MUNIT_TEST(test_ei_keymap_invalid) { From de6571b094b13989029674421aa1240163209b10 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Wed, 10 Dec 2025 15:17:22 +1000 Subject: [PATCH 5/5] proto: add a blurb about events to send during stop_emulating This should've been specified from day one but better late than never. Since stop_emulating is supposed to signal termination of a client emulating input, a logical assumption is that the device is set back to neutral. Let's point it out that the real behavior is within EIS but clients should behave predictably. --- proto/protocol.xml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/proto/protocol.xml b/proto/protocol.xml index c01c800..6d74daf 100644 --- a/proto/protocol.xml +++ b/proto/protocol.xml @@ -612,6 +612,12 @@ It is a protocol violation to send this request for a client of an ei_handshake.context_type other than sender. + + It is up to the EIS implementation to reset the device state when a + stop_emulating event is received. The recommendation is that the device + is set to a neutral state such that all touches, buttons, keys are logically up. + A client should send the corresponding events before stop_emulating + to avoid any ambiguity on event interpretation. @@ -1214,6 +1220,10 @@ It is a protocol violation to send this request for a client of an ei_handshake.context_type other than sender. + + A client should send a ei_button.button release event before + ei_device.stop_emulating to avoid any ambiguity on interpretation + of button events. @@ -1290,6 +1300,10 @@ It is a protocol violation to send this request for a client of an ei_handshake.context_type other than sender. + + A client should send a ei_key.key release event before + ei_device.stop_emulating to avoid any ambiguity on interpretation + of key events. @@ -1442,6 +1456,10 @@ It is a protocol violation to send a touch down in the same frame as a touch motion or touch up. + + A client should send a ei_touch.up or ei_touch.cancel event before + ei_device.stop_emulating to avoid any ambiguity on interpretation of touch + events.