From 5f9e18107324da2fce5d37cc3545515e477b4ea2 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Tue, 12 Aug 2025 13:36:53 +1000 Subject: [PATCH] proto: add support for requesting devices Add support for a client to request the creation of a new device from the EIS implementation. This is necessary in situations where the devices created by the EIS implementation are not (or no longer) suitable for the client to function correctly. The primary use-case of this is the upcoming tablet tool support where a client may need to create multiple tablet tools in response to a new physical tool brought into proximity locally. Other use-cases include a client closing a device but requiring that device (or one with similar capabilities) later. The implementation in libei is straightforward - on the client side we have a new function to request the new device: ei_seat_request_device_with_capabilities() - on the server side we have a new event EIS_EVENT_SEAT_DEVICE_REQUESTED that can make use of the existing eis_event_seat_has_capability() API Part-of: --- proto/protocol.xml | 35 ++++++++++++++++++++++++++++++++++- src/libei-seat.c | 34 ++++++++++++++++++++++++++++++++++ src/libei.c | 2 +- src/libei.h | 25 +++++++++++++++++++++++++ src/libeis-client.c | 2 +- src/libeis-event.c | 6 +++++- src/libeis-private.h | 3 +++ src/libeis-seat.c | 35 +++++++++++++++++++++++++++++------ src/libeis.c | 11 +++++++++++ src/libeis.h | 17 +++++++++++++---- test/eierpecken.c | 3 +++ test/test-ei-seat.c | 40 ++++++++++++++++++++++++++++++++++++++++ test/test_protocol.py | 4 +++- 13 files changed, 202 insertions(+), 15 deletions(-) diff --git a/proto/protocol.xml b/proto/protocol.xml index c01c800..02411b8 100644 --- a/proto/protocol.xml +++ b/proto/protocol.xml @@ -420,7 +420,7 @@ - + An ei_seat represents a set of input devices that logically belong together. In most cases only one seat is present and all input devices on that seat share the same @@ -470,6 +470,39 @@ + + + + + Request a new device from the EIS implementation with the given capability + mask. If the EIS implementation creates a new device in response to this + request that device is announced via the ei_seat.device event as any other + device. + + The capabilities argument is a bitmask of one or more of the + masks representing an interface as provided in the ei_seat.capability event. + See the ei_seat.capability event documentation for examples. + + The newly created device(s), if any, may not provide all requested capabilities. + The EIS implementation may create multiple devices to accommodate all requested + capabilities. For example, a client requesting a device with the ei_keyboard and + ei_pointer capability may instead see one device with the ei_keyboard and one + device with the ei_pointer capability being created. + + If possible, the EIS implementation should create devices immediately in + response to this request so a client can utilize ei_connection.sync to + help identify those devices. This is a suggestion to help the client and not + a requirement. + + Requesting masks that are not supported in the ei_device's interface version + is a client bug and may result in disconnection. + + Requesting masks that are not currently bound via the most recent ei_seat.bind + is a client bug and may result in disconnection. + + + + diff --git a/src/libei-seat.c b/src/libei-seat.c index 9d2e49b..e35bf4e 100644 --- a/src/libei-seat.c +++ b/src/libei-seat.c @@ -349,3 +349,37 @@ ei_seat_unbind_capabilities(struct ei_seat *seat, ...) ei_seat_send_bind(seat, seat->capabilities.bound); } + +_public_ void +ei_seat_request_device_with_capabilities(struct ei_seat *seat, ...) +{ + switch (seat->state) { + case EI_SEAT_STATE_DONE: + break; + case EI_SEAT_STATE_NEW: + case EI_SEAT_STATE_REMOVED: + return; + } + + uint64_t mask = 0; + enum ei_device_capability cap; + + va_list args; + va_start(args, seat); + while ((cap = va_arg(args, enum ei_device_capability)) > 0) { + mask_add(mask, ei_seat_cap_mask(seat, cap)); + } + va_end(args); + + if (mask == 0) + return; + + /* Check if requested capabilities are a subset of bound capabilities */ + if (!mask_all(seat->capabilities.bound, mask)) { + struct ei *ei = ei_seat_get_context(seat); + log_bug_client(ei, "Requested capabilities are not a subset of the bound capabilities"); + return; + } + + ei_seat_request_request_device(seat, mask); +} diff --git a/src/libei.c b/src/libei.c index 7fe9a62..f4fdf0f 100644 --- a/src/libei.c +++ b/src/libei.c @@ -126,7 +126,7 @@ ei_create_context(bool is_sender, void *user_data) .ei_handshake = VERSION_V(1), .ei_callback = VERSION_V(1), .ei_pingpong = VERSION_V(1), - .ei_seat = VERSION_V(1), + .ei_seat = VERSION_V(2), .ei_device = VERSION_V(3), .ei_pointer = VERSION_V(1), .ei_pointer_absolute = VERSION_V(1), diff --git a/src/libei.h b/src/libei.h index 151b24e..56b370c 100644 --- a/src/libei.h +++ b/src/libei.h @@ -1089,6 +1089,31 @@ void ei_seat_unbind_capabilities(struct ei_seat *seat, ...) __attribute__((sentinel)); +/** + * @ingroup libei-seat + * + * Request a new device with (a subset of) the given capabilities from the EIS + * implementation. If the EIS implementation creates a device in response + * to this request the device will arrive via an event of type @ref + * EI_EVENT_DEVICE_ADDED. + * + * The device's capabilities may not match the requested capabilities. For + * example requesting a pointer + keyboard device may result in the creation + * of a pointer-only device or it may result in the creation of a pointer + + * keyboard + touch device. It is up to the caller to handle capability + * mismatches. + * + * This function should be used by a caller when the current set of devices + * is insufficient for the functionality the client requires. For example + * a client that has previously called ei_device_close() on a device may + * need a device again with similar capabilities. + * + * The capabilities must be a subset of the capabilities requested in + * ei_seat_bind_capabilities(). + */ +void +ei_seat_request_device_with_capabilities(struct ei_seat *seat, ...) +__attribute__((sentinel)); /** * @ingroup libei-seat diff --git a/src/libeis-client.c b/src/libeis-client.c index 22c2170..a60c5c8 100644 --- a/src/libeis-client.c +++ b/src/libeis-client.c @@ -521,7 +521,7 @@ eis_client_new(struct eis *eis, int fd) .ei_handshake = VERSION_V(1), .ei_callback = VERSION_V(1), .ei_pingpong = VERSION_V(1), - .ei_seat = VERSION_V(1), + .ei_seat = VERSION_V(2), .ei_device = flag_is_set(eis->flags, EIS_FLAG_DEVICE_READY) ? VERSION_V(3) : VERSION_V(2), .ei_pointer = VERSION_V(1), .ei_pointer_absolute = VERSION_V(1), diff --git a/src/libeis-event.c b/src/libeis-event.c index 125dc51..ffc9993 100644 --- a/src/libeis-event.c +++ b/src/libeis-event.c @@ -41,6 +41,7 @@ eis_event_destroy(struct eis_event *event) case EIS_EVENT_CLIENT_CONNECT: case EIS_EVENT_CLIENT_DISCONNECT: case EIS_EVENT_SEAT_BIND: + case EIS_EVENT_SEAT_DEVICE_REQUESTED: case EIS_EVENT_DEVICE_CLOSED: case EIS_EVENT_DEVICE_START_EMULATING: case EIS_EVENT_DEVICE_STOP_EMULATING: @@ -208,7 +209,10 @@ eis_event_pong_get_ping(struct eis_event *event) _public_ bool eis_event_seat_has_capability(struct eis_event *event, enum eis_device_capability cap) { - require_event_type(event, false, EIS_EVENT_SEAT_BIND); + require_event_type(event, + false, + EIS_EVENT_SEAT_BIND, + EIS_EVENT_SEAT_DEVICE_REQUESTED); switch (cap) { case EIS_DEVICE_CAP_POINTER: diff --git a/src/libeis-private.h b/src/libeis-private.h index b3634ef..bb5055f 100644 --- a/src/libeis-private.h +++ b/src/libeis-private.h @@ -103,6 +103,9 @@ eis_queue_sync_event(struct eis_client *client, object_id_t newid, uint32_t vers void eis_queue_seat_bind_event(struct eis_seat *seat, uint32_t capabilities); +void +eis_queue_seat_device_requested_event(struct eis_seat *seat, uint32_t capabilities); + void eis_queue_device_closed_event(struct eis_device *device); diff --git a/src/libeis-seat.c b/src/libeis-seat.c index bca387e..91e6df9 100644 --- a/src/libeis-seat.c +++ b/src/libeis-seat.c @@ -86,15 +86,11 @@ client_msg_release(struct eis_seat *seat) return NULL; } -static struct brei_result * -client_msg_bind(struct eis_seat *seat, uint64_t caps) +static inline uint32_t +convert_capabilities(uint64_t caps) { uint32_t capabilities = 0; - if (caps & ~seat->capabilities.proto_mask) - return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_VALUE, - "Invalid capabilities %#" PRIx64, caps); - /* Convert from protocol capabilities to our C API capabilities */ if (caps & bit(EIS_POINTER_INTERFACE_INDEX)) capabilities |= EIS_DEVICE_CAP_POINTER; @@ -109,14 +105,41 @@ client_msg_bind(struct eis_seat *seat, uint64_t caps) if (caps & bit(EIS_SCROLL_INTERFACE_INDEX)) capabilities |= EIS_DEVICE_CAP_SCROLL; + return capabilities; +} + +static struct brei_result * +client_msg_bind(struct eis_seat *seat, uint64_t caps) +{ + if (caps & ~seat->capabilities.proto_mask) + return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_VALUE, + "Invalid capabilities %#" PRIx64, caps); + + uint32_t capabilities = convert_capabilities(caps); + eis_seat_bind(seat, capabilities); return NULL; } +static struct brei_result * +client_msg_request_device(struct eis_seat *seat, uint64_t caps) +{ + if (caps == 0 || caps & ~seat->capabilities.proto_mask) + return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_VALUE, + "Invalid capabilities %#" PRIx64, caps); + + uint32_t capabilities = convert_capabilities(caps); + + eis_queue_seat_device_requested_event(seat, capabilities); + + return NULL; +} + static const struct eis_seat_interface interface = { .release = client_msg_release, .bind = client_msg_bind, + .request_device = client_msg_request_device, }; const struct eis_seat_interface * diff --git a/src/libeis.c b/src/libeis.c index 002ee7b..6996114 100644 --- a/src/libeis.c +++ b/src/libeis.c @@ -137,6 +137,7 @@ eis_event_type_to_string(enum eis_event_type type) CASE_RETURN_STRING(EIS_EVENT_CLIENT_CONNECT); CASE_RETURN_STRING(EIS_EVENT_CLIENT_DISCONNECT); CASE_RETURN_STRING(EIS_EVENT_SEAT_BIND); + CASE_RETURN_STRING(EIS_EVENT_SEAT_DEVICE_REQUESTED); CASE_RETURN_STRING(EIS_EVENT_DEVICE_CLOSED); CASE_RETURN_STRING(EIS_EVENT_DEVICE_READY); CASE_RETURN_STRING(EIS_EVENT_PONG); @@ -268,6 +269,16 @@ eis_queue_seat_bind_event(struct eis_seat *seat, uint32_t capabilities) eis_queue_event(e); } +void +eis_queue_seat_device_requested_event(struct eis_seat *seat, + uint32_t capabilities) +{ + struct eis_event *e = eis_event_new_for_seat(seat); + e->type = EIS_EVENT_SEAT_DEVICE_REQUESTED; + e->bind.capabilities = capabilities; + eis_queue_event(e); +} + void eis_queue_device_closed_event(struct eis_device *device) { diff --git a/src/libeis.h b/src/libeis.h index 17b2619..267b6b2 100644 --- a/src/libeis.h +++ b/src/libeis.h @@ -286,6 +286,14 @@ enum eis_event_type { */ EIS_EVENT_DEVICE_READY, + /** + * The client requested a device with the given capabilities + * on this seat. + * + * @since 1.6 + */ + EIS_EVENT_SEAT_DEVICE_REQUESTED, + /** * Returned in response to eis_ping(). */ @@ -779,11 +787,12 @@ struct eis_ping * eis_event_pong_get_ping(struct eis_event *event); /** - * For an event of type @ref EIS_EVENT_SEAT_BIND, return the capabilities - * requested by the client. + * For an event of type @ref EIS_EVENT_SEAT_BIND or @ref + * EIS_EVENT_SEAT_DEVICE_REQUESTED, return the capabilities requested by the + * client. * - * This is the set of *all* capabilities bound by the client as of this event, - * not just the changed ones. + * For events of type @ref EIS_EVENT_SEAT_BIND this is the set of *all* + * capabilities bound by the client as of this event, not just the changed ones. */ bool eis_event_seat_has_capability(struct eis_event *event, enum eis_device_capability cap); diff --git a/test/eierpecken.c b/test/eierpecken.c index 094b986..b7f8df2 100644 --- a/test/eierpecken.c +++ b/test/eierpecken.c @@ -1117,6 +1117,8 @@ _peck_dispatch_eis(struct peck *peck, int lineno) else process_event = tristate_no; break; + case EIS_EVENT_SEAT_DEVICE_REQUESTED: + break; case EIS_EVENT_DEVICE_CLOSED: if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_HANDLE_CLOSE_DEVICE)) process_event = tristate_yes; @@ -1686,6 +1688,7 @@ peck_eis_event_type_name(enum eis_event_type type) CASE_STRING(CLIENT_CONNECT); CASE_STRING(CLIENT_DISCONNECT); CASE_STRING(SEAT_BIND); + CASE_STRING(SEAT_DEVICE_REQUESTED); CASE_STRING(DEVICE_CLOSED); CASE_STRING(PONG); CASE_STRING(SYNC); diff --git a/test/test-ei-seat.c b/test/test-ei-seat.c index 7eeba56..c5065ca 100644 --- a/test/test-ei-seat.c +++ b/test/test-ei-seat.c @@ -173,3 +173,43 @@ MUNIT_TEST(test_ei_seat_bind_unbind_immediately) return MUNIT_OK; } + +MUNIT_TEST(test_ei_seat_request_device) +{ + _unref_(peck) *peck = peck_new(); + + peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_ALL); + + peck_dispatch_until_stable(peck); + + with_client(peck) { + struct ei_seat *seat = peck_ei_get_default_seat(peck); + ei_seat_request_device_with_capabilities(seat, + EI_DEVICE_CAP_POINTER, + EI_DEVICE_CAP_KEYBOARD, + NULL); + ei_seat_request_device_with_capabilities(seat, + EI_DEVICE_CAP_TOUCH, + EI_DEVICE_CAP_POINTER_ABSOLUTE, + NULL); + } + + peck_dispatch_until_stable(peck); + + with_server(peck) { + _unref_(eis_event) *e1 = peck_eis_next_event(eis, EIS_EVENT_SEAT_DEVICE_REQUESTED); + _unref_(eis_event) *e2 = peck_eis_next_event(eis, EIS_EVENT_SEAT_DEVICE_REQUESTED); + + munit_assert(eis_event_seat_has_capability(e1, EIS_DEVICE_CAP_POINTER)); + munit_assert(eis_event_seat_has_capability(e1, EIS_DEVICE_CAP_KEYBOARD)); + munit_assert(!eis_event_seat_has_capability(e1, EIS_DEVICE_CAP_TOUCH)); + munit_assert(!eis_event_seat_has_capability(e1, EIS_DEVICE_CAP_POINTER_ABSOLUTE)); + + munit_assert(!eis_event_seat_has_capability(e2, EIS_DEVICE_CAP_POINTER)); + munit_assert(!eis_event_seat_has_capability(e2, EIS_DEVICE_CAP_KEYBOARD)); + munit_assert(eis_event_seat_has_capability(e2, EIS_DEVICE_CAP_TOUCH)); + munit_assert(eis_event_seat_has_capability(e2, EIS_DEVICE_CAP_POINTER_ABSOLUTE)); + } + + return MUNIT_OK; +} diff --git a/test/test_protocol.py b/test/test_protocol.py index 98d2f14..85ddb16 100644 --- a/test/test_protocol.py +++ b/test/test_protocol.py @@ -590,7 +590,9 @@ class TestEiProtocol: assert ei.seats for seat in ei.seats: - assert seat.version == 1 # we have 100, but the server only has 1 + assert seat.version == VERSION_V( + 2 + ) # we have 100, but the server only has 1 for call in seat.calllog: if call.name == "Capability": assert call.args["mask"].bit_count() == 1