diff --git a/src/libei-connection.c b/src/libei-connection.c index 1f18534..4724bd9 100644 --- a/src/libei-connection.c +++ b/src/libei-connection.c @@ -44,11 +44,8 @@ ei_connection_destroy(struct ei_connection *connection) struct ei *ei = ei_connection_get_context(connection); ei_unregister_object(ei, &connection->proto_object); - struct ei_callback *cb; - list_for_each_safe(cb, &connection->pending_callbacks, link) { - list_remove(&cb->link); - ei_callback_unref(cb); - } + /* should be a noop */ + ei_connection_remove_pending_callbacks(connection); } OBJECT_IMPLEMENT_REF(ei_connection); @@ -101,6 +98,17 @@ ei_connection_new(struct ei *ei, object_id_t id, uint32_t version) return connection; /* ref owned by caller */ } +void +ei_connection_remove_pending_callbacks(struct ei_connection *connection) +{ + struct ei_callback *cb; + list_for_each_safe(cb, &connection->pending_callbacks, link) { + list_remove(&cb->link); + ei_connection_sync_callback_unref(cb->user_data); + ei_callback_unref(cb); + } +} + static void ei_connection_sync_callback_destroy(struct ei_connection_sync_callback *callback) { diff --git a/src/libei-connection.h b/src/libei-connection.h index 12745f0..d0a2a20 100644 --- a/src/libei-connection.h +++ b/src/libei-connection.h @@ -54,6 +54,9 @@ OBJECT_DECLARE_UNREF(ei_connection); struct ei_connection * ei_connection_new(struct ei *ei, object_id_t id, uint32_t version); +void +ei_connection_remove_pending_callbacks(struct ei_connection *connection); + /** * Called when the ei_callback.done event is received after * an ei_connection_sync() request. diff --git a/src/libei-event.c b/src/libei-event.c index deaaa05..2919987 100644 --- a/src/libei-event.c +++ b/src/libei-event.c @@ -49,6 +49,7 @@ ei_event_type_to_string(enum ei_event_type type) CASE_RETURN_STRING(EI_EVENT_DEVICE_PAUSED); CASE_RETURN_STRING(EI_EVENT_DEVICE_RESUMED); CASE_RETURN_STRING(EI_EVENT_KEYBOARD_MODIFIERS); + CASE_RETURN_STRING(EI_EVENT_PONG); CASE_RETURN_STRING(EI_EVENT_FRAME); CASE_RETURN_STRING(EI_EVENT_DEVICE_START_EMULATING); CASE_RETURN_STRING(EI_EVENT_DEVICE_STOP_EMULATING); @@ -96,6 +97,9 @@ ei_event_destroy(struct ei_event *event) case EI_EVENT_TOUCH_UP: case EI_EVENT_TOUCH_MOTION: break; + case EI_EVENT_PONG: + ei_ping_unref(event->pong.ping); + break; } ei_device_unref(event->device); ei_seat_unref(event->seat); @@ -204,6 +208,14 @@ ei_event_keyboard_get_xkb_group(struct ei_event *event) return event->modifiers.group; } +_public_ struct ei_ping * +ei_event_pong_get_ping(struct ei_event *event) +{ + require_event_type(event, NULL, EI_EVENT_PONG); + + return event->pong.ping; +} + _public_ double ei_event_pointer_get_dx(struct ei_event *event) { diff --git a/src/libei-event.h b/src/libei-event.h index 006b6bf..cc2db03 100644 --- a/src/libei-event.h +++ b/src/libei-event.h @@ -69,6 +69,9 @@ struct ei_event { struct { uint32_t sequence; } start_emulating; + struct { + struct ei_ping *ping; + } pong; }; }; diff --git a/src/libei-ping.c b/src/libei-ping.c new file mode 100644 index 0000000..454d26f --- /dev/null +++ b/src/libei-ping.c @@ -0,0 +1,124 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Copyright © 2024 Red Hat, Inc. + * + * 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 (including the next + * paragraph) 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 + +#include "util-mem.h" +#include "util-macros.h" +#include "util-object.h" +#include "util-list.h" +#include "brei-shared.h" + +#include "libei-private.h" +#include "libei-connection.h" + +struct ei_ping { + struct object object; + uint64_t id; + void *user_data; + + struct ei *context; + bool is_pending; + bool is_done; +}; + +static void +ei_ping_destroy(struct ei_ping *ping) +{ + if (!ping->is_pending) + ei_unref(ping->context); +} + +static +OBJECT_IMPLEMENT_CREATE(ei_ping); + +_public_ +OBJECT_IMPLEMENT_REF(ei_ping); +_public_ +OBJECT_IMPLEMENT_UNREF_CLEANUP(ei_ping); +_public_ +OBJECT_IMPLEMENT_GETTER(ei_ping, id, uint64_t); +_public_ +OBJECT_IMPLEMENT_GETTER(ei_ping, user_data, void*); +_public_ +OBJECT_IMPLEMENT_SETTER(ei_ping, user_data, void*); +static +OBJECT_IMPLEMENT_GETTER(ei_ping, context, struct ei*); + +_public_ struct ei_ping * +ei_new_ping(struct ei *ei) +{ + static uint64_t id = 0; + + struct ei_ping *ping = ei_ping_create(NULL); + ping->id = ++id; + /* Ref our context while it's pending (i.e. only the caller has the ref). + * Once it's pending we no longer need the ref. + */ + ping->context = ei_ref(ei); + ping->is_pending = false; + ping->is_done = false; + + return ping; +} + +static void +on_pong(struct ei_connection_sync_callback *callback) +{ + struct ei_ping *ping = ei_connection_sync_callback_get_user_data(callback); + ping->is_done = true; + + struct ei *ei = ei_connection_sync_callback_get_context(callback); + ei_queue_pong_event(ei, ping); + /* ei_ping ref is removed in on_destroy */ +} + +static void +on_destroy(struct ei_connection_sync_callback *callback) +{ + /* This is only called if we never received a pong */ + _unref_(ei_ping) *ping = ei_connection_sync_callback_get_user_data(callback); + + /* We never got a pong because we got disconnected. Queue a fake pong event */ + if (!ping->is_done) { + struct ei *ei = ei_connection_sync_callback_get_context(callback); + ei_queue_pong_event(ei, ping); + } +} + +_public_ void +ei_ping(struct ei_ping *ping) +{ + struct ei *ei = ei_ping_get_context(ping); + + ei_unref(ping->context); + ping->context = ei; + ping->is_pending = true; + + _unref_(ei_connection_sync_callback) *cb = ei_connection_sync_callback_new(ei, + on_pong, + on_destroy, + ei_ping_ref(ping)); + ei_connection_sync(ei->connection, cb); +} diff --git a/src/libei-private.h b/src/libei-private.h index ae8e347..b11ed0f 100644 --- a/src/libei-private.h +++ b/src/libei-private.h @@ -168,6 +168,9 @@ ei_queue_connect_event(struct ei *ei); void ei_queue_disconnect_event(struct ei *ei); +void +ei_queue_pong_event(struct ei *ei, struct ei_ping *ping); + void ei_queue_device_removed_event(struct ei_device *device); diff --git a/src/libei.c b/src/libei.c index 60d4969..7a7f438 100644 --- a/src/libei.c +++ b/src/libei.c @@ -326,6 +326,16 @@ ei_queue_disconnect_event(struct ei *ei) queue_event(ei, e); } +void +ei_queue_pong_event(struct ei *ei, struct ei_ping *ping) +{ + struct ei_event *e = ei_event_new(ei); + e->type = EI_EVENT_PONG; + e->pong.ping = ei_ping_ref(ping); + + queue_event(ei, e); +} + void ei_queue_seat_added_event(struct ei_seat *seat) { @@ -613,6 +623,7 @@ ei_disconnect(struct ei *ei) if (state != EI_STATE_NEW) { ei_connection_request_disconnect(ei->connection); + ei_connection_remove_pending_callbacks(ei->connection); } ei_queue_disconnect_event(ei); ei->state = EI_STATE_DISCONNECTED; diff --git a/src/libei.h b/src/libei.h index 33226b0..447a709 100644 --- a/src/libei.h +++ b/src/libei.h @@ -215,6 +215,16 @@ struct ei_region; */ struct ei_touch; +/** + * @struct ei_ping + * + * A callback struct returned by ei_new_ping() to handle + * roundtrips to the EIS implementation. + * + * @see ei_new_ping + */ +struct ei_ping; + /** * @enum ei_device_type * @ingroup libei-device @@ -412,6 +422,15 @@ enum ei_event_type { */ EI_EVENT_KEYBOARD_MODIFIERS, + /** + * Returned in response to ei_ping(). + * + * Note that in the ei protocol, the matching client request is called `sync` + * but for consistency in the libeis/libei API the C API uses + * ei_ping() with `EI_EVENT_PONG` as return. + */ + EI_EVENT_PONG = 90, + /** * "Hardware" frame event. This event **must** be sent by the server * and notifies the client that the previous set of events belong to @@ -807,6 +826,95 @@ ei_setup_backend_socket(struct ei *ei, const char *socketpath); int ei_get_fd(struct ei *ei); +/** + * Create a new ei_ping object to trigger a round trip to the EIS implementation. + * See ei_ping() for details. + * + * The returned @ref ei_ping is refcounted, use ei_ping_unref() to + * drop the reference. + * + * @since 1.4 + */ +struct ei_ping * +ei_new_ping(struct ei *ei); + +/** + * Return a unique, increasing id for this struct. This ID is assigned at + * struct creation and constant for the lifetime of the struct. The ID + * does not exist at the protocol level. + * + * This is a convenience API to make it easier to put this struct into + * e.g. a hashtable without having to use the pointer value itself. + * + * The ID increases by an unspecified amount. + * + * @since 1.4 + */ +uint64_t +ei_ping_get_id(struct ei_ping *ping); + +/** + * Increase the refcount of this struct by one. Use ei_ping_unref() to decrease + * the refcount. + * + * @return the argument passed into the function + * + * @since 1.4 + */ +struct ei_ping * +ei_ping_ref(struct ei_ping *ei_ping); + +/** + * Decrease the refcount of this struct by one. When the refcount reaches + * zero, the context disconnects from the server and all allocated resources + * are released. + * + * @return always NULL + * + * @since 1.4 + */ +struct ei_ping * +ei_ping_unref(struct ei_ping *ei_ping); + +/** + * Set a custom data pointer for this struct. libei will not look at or + * modify the pointer. Use ei_ping_get_user_data() to retrieve a previously set + * user data. + * + * @since 1.4 + */ +void +ei_ping_set_user_data(struct ei_ping *ei_ping, void *user_data); + +/** + * Return the custom data pointer for this struct. libei will not look at or + * modify the pointer. Use ei_ping_set_user_data() to change the user data. + * + * @since 1.4 + */ +void * +ei_ping_get_user_data(struct ei_ping *ei_ping); + +/** + * Issue a roundtrip request to the EIS implementation, resulting in + * an @ref EI_EVENT_PONG event when this roundtrip has been processed. Client + * requests are processed in-order by the EIS implementation so this + * function can be used as synchronization point between requests. + * + * If the client is disconnected before the roundtrip is complete, + * libei will emulate a @ref EI_EVENT_PONG event before @ref + * EI_EVENT_DISCONNECT. + * + * Note that in the ei protocol, the client request is `ei_connection.sync` + * followed by `ei_callback.done`. For consistency with the + * libeis API the C API uses ei_ping() with and @ref EI_EVENT_PONG as return + * event. + * + * @since 1.4 + */ +void +ei_ping(struct ei_ping *ping); + /** * Main event dispatching function. Reads events of the file descriptors * and processes them internally. Use ei_get_event() to retrieve the @@ -1819,6 +1927,18 @@ ei_touch_get_device(struct ei_touch *touch); struct ei_seat * ei_event_get_seat(struct ei_event *event); +/** + * Returns the associated @ref ei_ping struct with this event. + * + * For events of type other than @ref EI_EVENT_PONG this function + * returns NULL. + * + * This does not increase the refcount of the ei_pong. Use ei_pong_ref() + * to keep a reference beyond the immediate scope. + */ +struct ei_ping * +ei_event_pong_get_ping(struct ei_event *event); + /** * @ingroup libei-receiver * diff --git a/src/meson.build b/src/meson.build index 2c4f9e7..4881a29 100644 --- a/src/meson.build +++ b/src/meson.build @@ -50,6 +50,7 @@ if build_libei 'libei-keyboard.c', 'libei-log.c', 'libei-pingpong.c', + 'libei-ping.c', 'libei-pointer-absolute.c', 'libei-pointer.c', 'libei-region.c', diff --git a/test/eierpecken.c b/test/eierpecken.c index 06f2a8f..67302dc 100644 --- a/test/eierpecken.c +++ b/test/eierpecken.c @@ -1067,6 +1067,8 @@ _peck_dispatch_ei(struct peck *peck, int lineno) if (flag_is_set(peck->ei_behavior, PECK_EI_BEHAVIOR_HANDLE_PAUSED)) process_event = tristate_yes; break; + case EI_EVENT_PONG: + break; case EI_EVENT_FRAME: /* Ensure we only expect frames when we expect them */ munit_assert_true(need_frame); @@ -1381,6 +1383,7 @@ peck_ei_event_type_name(enum ei_event_type type) CASE_STRING(DEVICE_PAUSED); CASE_STRING(DEVICE_RESUMED); CASE_STRING(KEYBOARD_MODIFIERS); + CASE_STRING(PONG); CASE_STRING(FRAME); CASE_STRING(DEVICE_START_EMULATING); CASE_STRING(DEVICE_STOP_EMULATING); diff --git a/test/eierpecken.h b/test/eierpecken.h index 10eda7a..60c0aeb 100644 --- a/test/eierpecken.h +++ b/test/eierpecken.h @@ -359,6 +359,7 @@ DEFINE_UNREF_CLEANUP_FUNC(ei_touch); DEFINE_UNREF_CLEANUP_FUNC(ei_keymap); DEFINE_UNREF_CLEANUP_FUNC(ei_seat); DEFINE_UNREF_CLEANUP_FUNC(ei_region); +DEFINE_UNREF_CLEANUP_FUNC(ei_ping); DEFINE_UNREF_CLEANUP_FUNC(eis); DEFINE_UNREF_CLEANUP_FUNC(eis_client); diff --git a/test/test-ei.c b/test/test-ei.c index 697011e..c366002 100644 --- a/test/test-ei.c +++ b/test/test-ei.c @@ -797,3 +797,105 @@ MUNIT_TEST(test_ei_invalid_object_ids) return MUNIT_OK; } + +MUNIT_TEST(test_ei_ping) +{ + _unref_(peck) *peck = peck_new(); + _unref_(ei_ping) *ping = NULL; + int userdata = 123; + + peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_NONE); + peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_ALL); + peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ADD_POINTER); + peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_RESUME_DEVICE); + peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_AUTODEVICES); + peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_AUTOSTART); + + peck_dispatch_until_stable(peck); + + with_client(peck) { + peck_drain_ei(ei); + } + + /* Create a ping object without having our own ref, object + * is kept alive until the returned pong event is destroyed */ + with_client(peck) { + _unref_(ei_ping) *ping = ei_new_ping(ei); + ei_ping_set_user_data(ping, &userdata); + ei_ping(ping); + } + + peck_dispatch_until_stable(peck); + + with_client(peck) { + _unref_(ei_event) *e = peck_ei_next_event(ei, EI_EVENT_PONG); + struct ei_ping *pong = ei_event_pong_get_ping(e); + munit_assert_not_null(pong); + munit_assert_ptr_equal(ei_ping_get_user_data(pong), &userdata); + } + + peck_dispatch_until_stable(peck); + + /* Create a ping object this time keeping our own ref, object + * is kept alive until the returned pong event is destroyed */ + with_client(peck) { + ping = ei_new_ping(ei); + ei_ping_set_user_data(ping, &userdata); + ei_ping(ping); + /* Keep the ref */ + } + + peck_dispatch_until_stable(peck); + + with_client(peck) { + _unref_(ei_event) *e = peck_ei_next_event(ei, EI_EVENT_PONG); + struct ei_ping *pong = ei_event_pong_get_ping(e); + munit_assert_ptr_equal(pong, ping); + munit_assert_int64(ei_ping_get_id(pong), ==, ei_ping_get_id(ping)); + munit_assert_ptr_equal(ei_ping_get_user_data(pong), &userdata); + } + + /* unref after the event above, in case that blows things up */ + ping = ei_ping_unref(ping); + + peck_mark(peck); + + /* Send two pings, one we keep the ref to, one floating, then disconnect + * immediately */ + with_client(peck) { + ping = ei_new_ping(ei); + ei_ping(ping); + + _unref_(ei_ping) *floating = ei_new_ping(ei); + ei_ping(floating); + } + + with_server(peck) { + struct eis_client *client = peck_eis_get_default_client(peck); + eis_client_disconnect(client); + } + + peck_dispatch_until_stable(peck); + + with_client(peck) { + struct ei_event *e; + while ((e = ei_peek_event(ei))) { + bool found = ei_event_get_type(e) == EI_EVENT_PONG; + ei_event_unref(e); + + if (found) + break; + _unref_(ei_event) *ev = ei_get_event(ei); + } + + _unref_(ei_event) *e1 = peck_ei_next_event(ei, EI_EVENT_PONG); + struct ei_ping *pong = ei_event_pong_get_ping(e1); + munit_assert_ptr_equal(pong, ping); + + _unref_(ei_event) *e2 = peck_ei_next_event(ei, EI_EVENT_PONG); + pong = ei_event_pong_get_ping(e2); + munit_assert_ptr_not_equal(pong, ping); + } + + return MUNIT_OK; +} diff --git a/tools/ei-debug-events.c b/tools/ei-debug-events.c index 2bea0d3..a8b67c9 100644 --- a/tools/ei-debug-events.c +++ b/tools/ei-debug-events.c @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -286,6 +287,14 @@ print_modifiers_event(struct ei_event *event) printf(" locked: \"0x%x\"\n", ei_event_keyboard_get_xkb_mods_locked(event)); } +static void +print_pong_event(struct ei_event *event) +{ + struct ei_ping *ping = ei_event_pong_get_ping(event); + + printf(" id: %#" PRIx64 "\n", ei_ping_get_id(ping)); +} + int main(int argc, char **argv) { enum { @@ -443,6 +452,9 @@ int main(int argc, char **argv) case EI_EVENT_KEYBOARD_MODIFIERS: print_modifiers_event(e); break; + case EI_EVENT_PONG: + print_pong_event(e); + break; } } }