eis: add eis_ping() and the matching EIS_EVENT_PONG

Identical to "ei: add ei_ping() and the matching EI_EVENT_PONG" but for
libeis.

This wraps around ei_connection.ping to allow a C API user to add
synchronization points.

Closes #69

Part-of: <https://gitlab.freedesktop.org/libinput/libei/-/merge_requests/316>
This commit is contained in:
Peter Hutterer 2024-12-04 11:17:56 +10:00 committed by Marge Bot
parent a278c7b371
commit e40796402b
14 changed files with 398 additions and 7 deletions

View file

@ -247,6 +247,7 @@ client_disconnect(struct eis_client *client,
case EIS_CLIENT_STATE_CONNECTING:
case EIS_CLIENT_STATE_CONNECTED:
client_drop_seats(client);
eis_connection_remove_pending_callbacks(client->connection);
eis_queue_disconnect_event(client);
eis_connection_event_disconnected(client->connection,
client->last_client_serial,

View file

@ -43,12 +43,8 @@ eis_connection_destroy(struct eis_connection *connection)
struct eis_client *client = eis_connection_get_client(connection);
eis_client_unregister_object(client, &connection->proto_object);
struct eis_pingpong *cb;
list_for_each_safe(cb, &connection->pending_pingpongs, link) {
list_remove(&cb->link);
eis_connection_ping_callback_unref(cb->user_data);
eis_pingpong_unref(cb);
}
/* Should be a noop */
eis_connection_remove_pending_callbacks(connection);
}
OBJECT_IMPLEMENT_REF(eis_connection);
@ -108,6 +104,17 @@ eis_connection_new(struct eis_client *client)
return connection; /* ref owned by caller */
}
void
eis_connection_remove_pending_callbacks(struct eis_connection *connection)
{
struct eis_callback *cb;
list_for_each_safe(cb, &connection->pending_pingpongs, link) {
list_remove(&cb->link);
eis_connection_ping_callback_unref(cb->user_data);
eis_callback_unref(cb);
}
}
static void
eis_connection_ping_callback_destroy(struct eis_connection_ping_callback *callback)
{
@ -136,6 +143,13 @@ eis_connection_ping_callback_get_client(struct eis_connection_ping_callback *cal
return eis_connection_get_client(connection);
}
struct eis *
eis_connection_ping_callback_get_context(struct eis_connection_ping_callback *callback)
{
struct eis_connection *connection = eis_connection_ping_callback_get_connection(callback);
return eis_connection_get_context(connection);
}
struct eis_connection_ping_callback *
eis_connection_ping_callback_new(struct eis_connection *connection,
eis_connection_ping_callback_done_t done,

View file

@ -53,6 +53,8 @@ OBJECT_DECLARE_UNREF(eis_connection);
struct eis_connection *
eis_connection_new(struct eis_client *client);
void
eis_connection_remove_pending_callbacks(struct eis_connection *connection);
/**
* Called when the ei_callback.done request is received after
@ -92,6 +94,7 @@ OBJECT_DECLARE_REF(eis_connection_ping_callback);
OBJECT_DECLARE_UNREF(eis_connection_ping_callback);
OBJECT_DECLARE_GETTER(eis_connection_ping_callback, connection, struct eis_connection *);
OBJECT_DECLARE_GETTER(eis_connection_ping_callback, client, struct eis_client *);
OBJECT_DECLARE_GETTER(eis_connection_ping_callback, context, struct eis *);
OBJECT_DECLARE_GETTER(eis_connection_ping_callback, user_data, void*);
DEFINE_UNREF_CLEANUP_FUNC(eis_connection_ping_callback);

View file

@ -58,6 +58,10 @@ eis_event_destroy(struct eis_event *event)
case EIS_EVENT_FRAME:
handled = true;
break;
case EIS_EVENT_PONG:
eis_ping_unref(event->pong.ping);
handled = true;
break;
}
if (!handled)
@ -188,6 +192,14 @@ eis_event_get_time(struct eis_event *event)
return event->timestamp;
}
_public_ struct eis_ping *
eis_event_pong_get_ping(struct eis_event *event)
{
require_event_type(event, NULL, EIS_EVENT_PONG);
return event->pong.ping;
}
_public_ bool
eis_event_seat_has_capability(struct eis_event *event, enum eis_device_capability cap)
{

View file

@ -63,6 +63,9 @@ struct eis_event {
struct {
uint32_t sequence;
} start_emulating;
struct {
struct eis_ping *ping;
} pong;
};
};

124
src/libeis-ping.c Normal file
View file

@ -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 <stdio.h>
#include "util-mem.h"
#include "util-macros.h"
#include "util-object.h"
#include "util-list.h"
#include "brei-shared.h"
#include "libeis-private.h"
#include "libeis-connection.h"
struct eis_ping {
struct object object;
uint64_t id;
void *user_data;
struct eis_client *client;
bool is_pending;
bool is_done;
};
static void
eis_ping_destroy(struct eis_ping *ping)
{
if (!ping->is_pending)
eis_client_unref(ping->client);
}
static
OBJECT_IMPLEMENT_CREATE(eis_ping);
_public_
OBJECT_IMPLEMENT_REF(eis_ping);
_public_
OBJECT_IMPLEMENT_UNREF_CLEANUP(eis_ping);
_public_
OBJECT_IMPLEMENT_GETTER(eis_ping, id, uint64_t);
_public_
OBJECT_IMPLEMENT_GETTER(eis_ping, user_data, void*);
_public_
OBJECT_IMPLEMENT_SETTER(eis_ping, user_data, void*);
static
OBJECT_IMPLEMENT_GETTER(eis_ping, client, struct eis_client*);
_public_ struct eis_ping *
eis_client_new_ping(struct eis_client *client)
{
static uint64_t id = 0;
struct eis_ping *ping = eis_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->client = eis_client_ref(client);
ping->is_pending = false;
ping->is_done = false;
return ping;
}
static void
on_pong(struct eis_connection_ping_callback *callback)
{
struct eis_ping *ping = eis_connection_ping_callback_get_user_data(callback);
ping->is_done = true;
struct eis_client *client = eis_connection_ping_callback_get_client(callback);
eis_queue_pong_event(client, ping);
/* eis_ping ref is removed in on_destroy */
}
static void
on_destroy(struct eis_connection_ping_callback *callback)
{
/* This is only called if we never receisved a pong */
_unref_(eis_ping) *ping = eis_connection_ping_callback_get_user_data(callback);
/* We never got a pong because we got disconnected. Queue a fake pong event */
if (!ping->is_done) {
struct eis_client *client = eis_connection_ping_callback_get_client(callback);
eis_queue_pong_event(client, ping);
}
}
_public_ void
eis_ping(struct eis_ping *ping)
{
struct eis_client *client = eis_ping_get_client(ping);
eis_client_unref(client);
ping->client = client;
ping->is_pending = true;
_unref_(eis_connection_ping_callback) *cb = eis_connection_ping_callback_new(client->connection,
on_pong,
on_destroy,
eis_ping_ref(ping));
eis_connection_ping(client->connection, cb);
}

View file

@ -92,6 +92,9 @@ eis_queue_connect_event(struct eis_client *client);
void
eis_queue_disconnect_event(struct eis_client *client);
void
eis_queue_pong_event(struct eis_client *client, struct eis_ping *ping);
void
eis_queue_seat_bind_event(struct eis_seat *seat, uint32_t capabilities);

View file

@ -118,6 +118,7 @@ eis_event_type_to_string(enum eis_event_type type)
CASE_RETURN_STRING(EIS_EVENT_CLIENT_DISCONNECT);
CASE_RETURN_STRING(EIS_EVENT_SEAT_BIND);
CASE_RETURN_STRING(EIS_EVENT_DEVICE_CLOSED);
CASE_RETURN_STRING(EIS_EVENT_PONG);
CASE_RETURN_STRING(EIS_EVENT_DEVICE_START_EMULATING);
CASE_RETURN_STRING(EIS_EVENT_DEVICE_STOP_EMULATING);
CASE_RETURN_STRING(EIS_EVENT_POINTER_MOTION);
@ -251,6 +252,15 @@ eis_queue_device_closed_event(struct eis_device *device)
eis_queue_event(e);
}
void
eis_queue_pong_event(struct eis_client *client, struct eis_ping *ping)
{
struct eis_event *e = eis_event_new_for_client(client);
e->type = EIS_EVENT_PONG;
e->pong.ping = eis_ping_ref(ping);
eis_queue_event(e);
}
void
eis_queue_frame_event(struct eis_device *device, uint64_t time)
{

View file

@ -147,6 +147,16 @@ struct eis_keymap;
*/
struct eis_touch;
/**
* @struct eis_ping
*
* A callback struct returned by eis_new_ping() to handle
* roundtrips to the client.
*
* @see eis_new_ping
*/
struct eis_ping;
/**
* @struct eis_region
* @ingroup libeis-region
@ -261,6 +271,11 @@ enum eis_event_type {
*/
EIS_EVENT_DEVICE_CLOSED,
/**
* Returned in response to eis_ping().
*/
EIS_EVENT_PONG = 90,
/**
* "Hardware" frame event. This event **must** be sent by the client
* and notifies the server that the previous set of events belong to
@ -527,6 +542,78 @@ eis_get_fd(struct eis *eis);
void
eis_dispatch(struct eis *eis);
/**
* 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
eis_ping_get_id(struct eis_ping *ping);
/**
* Increase the refcount of this struct by one. Use eis_ping_unref() to decrease
* the refcount.
*
* @return the argument passed into the function
*
* @since 1.4
*/
struct eis_ping *
eis_ping_ref(struct eis_ping *eis_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 eis_ping *
eis_ping_unref(struct eis_ping *eis_ping);
/**
* Set a custom data pointer for this struct. libeis will not look at or
* modify the pointer. Use eis_ping_get_user_data() to retrieve a previously set
* user data.
*
* @since 1.4
*/
void
eis_ping_set_user_data(struct eis_ping *eis_ping, void *user_data);
/**
* Return the custom data pointer for this struct. libeis will not look at or
* modify the pointer. Use eis_ping_set_user_data() to change the user data.
*
* @since 1.4
*/
void *
eis_ping_get_user_data(struct eis_ping *eis_ping);
/**
* Issue a roundtrip request to the client, resulting in
* an @ref EIS_EVENT_PONG event when this roundtrip has been processed. Events
* are processed in-order by the client implementation so this
* function can be used as synchronization point between events.
*
* If the client is disconnected before the roundtrip is complete,
* libeis will emulate a @ref EIS_EVENT_PONG event before @ref
* EIS_EVENT_DISCONNECT.
*
* @since 1.4
*/
void
eis_ping(struct eis_ping *ping);
/**
* Returns the next event in the internal event queue (or NULL) and removes
* it from the queue.
@ -580,6 +667,18 @@ eis_event_get_client(struct eis_event *event);
struct eis_seat *
eis_event_get_seat(struct eis_event *event);
/**
* Returns the associated @ref eis_ping struct with this event.
*
* For events of type other than @ref EIS_EVENT_PONG this function
* returns NULL.
*
* This does not increase the refcount of the eis_pong. Use eis_pong_ref()
* to ekeep a reference beyond the immediate scope.
*/
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.
@ -611,7 +710,17 @@ eis_event_get_device(struct eis_event *event);
uint64_t
eis_event_get_time(struct eis_event *event);
/**
* Create a new eis_ping object to trigger a round trip to the client.
* See eis_ping() for details.
*
* The returned @ref eis_ping is refcounted, use eis_ping_unref() to
* drop the reference.
*
* @since 1.4
*/
struct eis_ping *
eis_client_new_ping(struct eis_client *client);
struct eis_client *
eis_client_ref(struct eis_client *client);

View file

@ -114,6 +114,7 @@ if build_libeis
'libeis-keyboard.c',
'libeis-log.c',
'libeis-pingpong.c',
'libeis-ping.c',
'libeis-pointer-absolute.c',
'libeis-pointer.c',
'libeis-region.c',

View file

@ -909,6 +909,8 @@ _peck_dispatch_eis(struct peck *peck, int lineno)
else
process_event = tristate_no;
break;
case EIS_EVENT_PONG:
break;
case EIS_EVENT_FRAME:
/* Ensure we only expect frames when we expect them */
munit_assert_true(need_frame);
@ -1420,6 +1422,7 @@ peck_eis_event_type_name(enum eis_event_type type)
CASE_STRING(CLIENT_DISCONNECT);
CASE_STRING(SEAT_BIND);
CASE_STRING(DEVICE_CLOSED);
CASE_STRING(PONG);
CASE_STRING(DEVICE_START_EMULATING);
CASE_STRING(DEVICE_STOP_EMULATING);
CASE_STRING(POINTER_MOTION);

View file

@ -369,6 +369,7 @@ DEFINE_UNREF_CLEANUP_FUNC(eis_touch);
DEFINE_UNREF_CLEANUP_FUNC(eis_keymap);
DEFINE_UNREF_CLEANUP_FUNC(eis_seat);
DEFINE_UNREF_CLEANUP_FUNC(eis_region);
DEFINE_UNREF_CLEANUP_FUNC(eis_ping);
/* Macros intended just for readability to make it more obvious which part
of a test handles server vs client */

View file

@ -352,3 +352,105 @@ MUNIT_TEST(eistest_regions)
return MUNIT_OK;
}
MUNIT_TEST(test_eis_ping)
{
_unref_(peck) *peck = peck_new();
_unref_(eis_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_server(peck) {
peck_drain_eis(eis);
}
/* Create a ping object without having our own ref, object
* is kept alive until the returned pong event is destroyed */
with_server(peck) {
struct eis_client *client = peck_eis_get_default_client(peck);
_unref_(eis_ping) *ping = eis_client_new_ping(client);
eis_ping_set_user_data(ping, &userdata);
eis_ping(ping);
}
peck_dispatch_until_stable(peck);
with_server(peck) {
_unref_(eis_event) *e = peck_eis_next_event(eis, EIS_EVENT_PONG);
struct eis_ping *pong = eis_event_pong_get_ping(e);
munit_assert_not_null(pong);
munit_assert_ptr_equal(eis_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_server(peck) {
struct eis_client *client = peck_eis_get_default_client(peck);
ping = eis_client_new_ping(client);
eis_ping_set_user_data(ping, &userdata);
eis_ping(ping);
/* Keep the ref */
}
peck_dispatch_until_stable(peck);
with_server(peck) {
_unref_(eis_event) *e = peck_eis_next_event(eis, EIS_EVENT_PONG);
struct eis_ping *pong = eis_event_pong_get_ping(e);
munit_assert_ptr_equal(pong, ping);
munit_assert_int64(eis_ping_get_id(pong), ==, eis_ping_get_id(ping));
munit_assert_ptr_equal(eis_ping_get_user_data(pong), &userdata);
}
/* unref after the event above, in case that blows things up */
ping = eis_ping_unref(ping);
peck_mark(peck);
/* Send two pings, one we keep the ref to, one floating, then disconnect
* immediately */
with_server(peck) {
struct eis_client *client = peck_eis_get_default_client(peck);
ping = eis_client_new_ping(client);
eis_ping(ping);
_unref_(eis_ping) *floating = eis_client_new_ping(client);
eis_ping(floating);
eis_client_disconnect(client);
}
peck_dispatch_until_stable(peck);
with_server(peck) {
struct eis_event *e;
while ((e = eis_peek_event(eis))) {
bool found = eis_event_get_type(e) == EIS_EVENT_PONG;
eis_event_unref(e);
if (found)
break;
_unref_(eis_event) *ev = eis_get_event(eis);
}
_unref_(eis_event) *e1 = peck_eis_next_event(eis, EIS_EVENT_PONG);
struct eis_ping *pong = eis_event_pong_get_ping(e1);
munit_assert_ptr_equal(pong, ping);
_unref_(eis_event) *e2 = peck_eis_next_event(eis, EIS_EVENT_PONG);
pong = eis_event_pong_get_ping(e2);
munit_assert_ptr_not_equal(pong, ping);
}
return MUNIT_OK;
}

View file

@ -546,6 +546,11 @@ eis_demo_server_printf_handle_event(struct eis_demo_server *server,
eis_event_get_time(e));
}
break;
case EIS_EVENT_PONG:
{
colorprint("pong\n");
}
break;
default:
/* This is a demo server and we abort to make it easy to debug
* a missing implementation of some new event.