diff --git a/meson.build b/meson.build index 9131343..6a45f33 100644 --- a/meson.build +++ b/meson.build @@ -50,6 +50,7 @@ src_libei = [ 'src/libei-seat.c', 'src/libei-socket.c', 'src/libei-fd.c', + 'src/libei-property.c', 'src/libei-proto.h', 'src/libei-proto.c', 'src/libei-region.c', @@ -102,6 +103,7 @@ src_libeis = [ 'src/libeis-device.c', 'src/libeis-event.c', 'src/libeis-log.c', + 'src/libeis-property.c', 'src/libeis-region.c', 'src/libeis-seat.c', 'src/libeis-socket.c', diff --git a/proto/ei.proto b/proto/ei.proto index b5be3ca..8ba87d0 100644 --- a/proto/ei.proto +++ b/proto/ei.proto @@ -166,6 +166,12 @@ message Frame { uint32 deviceid = 1; } +message Property { + string name = 1; + string value = 2; + uint32 permissions = 3; +}; + message ClientMessage { oneof msg { Connect connect = 1; @@ -189,6 +195,7 @@ message ClientMessage { Frame frame = 19; StartEmulating start_emulating = 20; StopEmulating stop_emulating = 21; + Property property = 22; } } @@ -269,6 +276,7 @@ message ServerMessage { DeviceResumed device_resumed = 11; DevicePaused device_paused = 12; KeyboardModifiers keyboard_modifiers = 13; + Property property = 14; } } diff --git a/src/libei-event.c b/src/libei-event.c index 5774bad..4cbaa28 100644 --- a/src/libei-event.c +++ b/src/libei-event.c @@ -48,6 +48,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_PROPERTY); } assert(!"Unhandled event type"); @@ -67,6 +68,10 @@ ei_event_destroy(struct ei_event *event) case EI_EVENT_DEVICE_RESUMED: case EI_EVENT_KEYBOARD_MODIFIERS: break; + case EI_EVENT_PROPERTY: + free(event->prop.name); + free(event->prop.value); + break; } ei_device_unref(event->device); ei_seat_unref(event->seat); @@ -155,3 +160,31 @@ ei_event_keyboard_get_xkb_group(struct ei_event *event) return event->modifiers.group; } + +_public_ const char * +ei_event_property_get_name(struct ei_event *event) +{ + require_event_type(event, NULL, + EI_EVENT_PROPERTY); + + return event->prop.name; +} + +_public_ const char * +ei_event_property_get_value(struct ei_event *event) +{ + require_event_type(event, NULL, + EI_EVENT_PROPERTY); + + return event->prop.value; +} + +_public_ uint32_t +ei_event_property_get_permissions(struct ei_event *event) +{ + require_event_type(event, 0, + EI_EVENT_PROPERTY); + + return event->prop.permissions; +} + diff --git a/src/libei-private.h b/src/libei-private.h index f0e6e42..d3b5188 100644 --- a/src/libei-private.h +++ b/src/libei-private.h @@ -58,6 +58,8 @@ struct ei { struct list seats; char *name; + struct list properties; + struct { ei_log_handler handler; enum ei_log_priority priority; @@ -173,9 +175,34 @@ struct ei_event { struct ei_seat *seat; /* NULL if device is non-NULL */ struct ei_device *device; - struct ei_xkb_modifiers modifiers; + union { + struct ei_xkb_modifiers modifiers; + struct { + char *name; + char *value; + uint32_t permissions; + } prop; + }; }; +struct ei_property { + struct object object; + struct list link; + char *name; + char *value; + uint32_t permissions; +}; + +struct ei_property * +ei_property_unref(struct ei_property *prop); + +struct ei_property * +ei_find_property(struct ei *ei, const char *name); + +int +ei_property_update(struct ei *ei, const char *name, + const char *value, uint32_t permissions); + struct ei_event * ei_event_new(struct ei *ei); @@ -204,6 +231,9 @@ ei_seat_remove(struct ei_seat *seat); void ei_seat_drop(struct ei_seat *seat); +int +ei_send_property(struct ei *ei, const char *name, const char *value, uint32_t permissions); + int ei_send_seat_bind(struct ei_seat *seat, uint32_t capabilities); diff --git a/src/libei-property.c b/src/libei-property.c new file mode 100644 index 0000000..18914ac --- /dev/null +++ b/src/libei-property.c @@ -0,0 +1,172 @@ +/* + * Copyright © 2021 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 "config.h" + +#include + +#include "util-strings.h" +#include "util-object.h" + +#include "libei-private.h" + +static void +ei_property_destroy(struct ei_property *prop) +{ + list_remove(&prop->link); + free(prop->name); + free(prop->value); +} + +static +OBJECT_IMPLEMENT_CREATE(ei_property); +OBJECT_IMPLEMENT_UNREF(ei_property); + + +#define _PERM(p_, mask_) (((p_) & (mask_)) == (mask_)) +#define PERM_R(p_) _PERM((p_)->permissions, EI_PROPERTY_PERM_READ) +#define PERM_RW(p_) _PERM((p_)->permissions, EI_PROPERTY_PERM_READ|EI_PROPERTY_PERM_WRITE) +#define PERM_RD(p_) _PERM((p_)->permissions, EI_PROPERTY_PERM_READ|EI_PROPERTY_PERM_DELETE) + +struct ei_property * +ei_find_property(struct ei *ei, const char *name) +{ + struct ei_property *prop; + list_for_each(prop, &ei->properties, link) { + if (streq(prop->name, name)) { + return prop; + } + } + + return NULL; +} + +static struct ei_property * +ei_property_new(struct ei *ei, const char *name, + const char *value, uint32_t permissions) +{ + struct ei_property *prop = ei_property_create(NULL); + + prop->name = xstrdup(name); + prop->value = xstrdup(value); + prop->permissions = permissions & EI_PROPERTY_PERM_ALL; + + /* Initial ref is owned by this list */ + list_append(&ei->properties, &prop->link); + + return prop; +} + +static int +ei_property_delete(struct ei *ei, struct ei_property *prop) +{ + if (!PERM_RD(prop)) + return -EACCES; + + free(prop->value); + prop->value = NULL; + ei_property_unref(prop); + + return 0; +} + +static int +ei_property_change(struct ei *ei, struct ei_property *prop, + const char *value, uint32_t permissions) +{ + if (!PERM_RW(prop)) + return -EACCES; + + if (prop->value != value) { + free(prop->value); + prop->value = xstrdup(value); + } + prop->permissions = permissions; + + return 0; +} + +int +ei_property_update(struct ei *ei, const char *name, + const char *value, uint32_t permissions) +{ + struct ei_property *prop = ei_find_property(ei, name); + int rc = 0; + + if (!prop) { + ei_property_new(ei, name, value, permissions); + } else if (value) { + rc = ei_property_change(ei, prop, value, permissions); + } else { + rc = ei_property_delete(ei, prop); + } + + return rc; +} + +_public_ int +ei_property_set_with_permissions(struct ei *ei, const char *name, const char *value, + uint32_t permissions) +{ + if (strstartswith(name, "ei.") || strstartswith(name, "eis.")) + return -EACCES; + + struct ei_property *prop = ei_find_property(ei, name); + if (prop && permissions && (prop->permissions | permissions) != prop->permissions) + return -EPERM; + + int rc = ei_property_update(ei, name, value, permissions); + if (rc == 0) + ei_send_property(ei, name, value, permissions); + + return rc; +} + +_public_ int +ei_property_set(struct ei *ei, const char *name, const char *value) +{ + uint32_t permissions = EI_PROPERTY_PERM_ALL; + struct ei_property *prop = ei_find_property(ei, name); + + if (prop) + permissions = prop->permissions; + + return ei_property_set_with_permissions(ei, name, value, permissions); +} + +_public_ uint32_t +ei_property_get_permissions(struct ei *ei, const char *name) +{ + struct ei_property *prop = ei_find_property(ei, name); + + return (prop && PERM_R(prop)) ? prop->permissions : 0; +} + +_public_ const char * +ei_property_get(struct ei *ei, const char *name) +{ + struct ei_property *prop = ei_find_property(ei, name); + + return (prop && PERM_R(prop)) ? prop->value : NULL; +} diff --git a/src/libei-proto.c b/src/libei-proto.c index 650869d..bf63271 100644 --- a/src/libei-proto.c +++ b/src/libei-proto.c @@ -125,6 +125,12 @@ ei_proto_handle_message(struct ei *ei, rc = call(device_paused, ei, proto->device_paused->deviceid); break; + case SERVER_MESSAGE__MSG_PROPERTY: + rc = call(property, ei, + proto->property->name, + proto->property->value[0] ? proto->property->value : NULL, + proto->property->permissions); + break; default: rc = -EBADMSG; break; @@ -167,6 +173,7 @@ log_wire_message(struct ei *ei, const ClientMessage *msg, int error) MSG_STRING_CASE(CONFIGURE_NAME); MSG_STRING_CASE(CONFIGURE_CAPS); MSG_STRING_CASE(FRAME); + MSG_STRING_CASE(PROPERTY); } if (message == NULL) assert(!"Unimplemented message type"); @@ -443,6 +450,18 @@ ei_proto_send_frame(struct ei_device *device) return ei_proto_send_msg(ei_device_get_context(device), &msg); } +static int +ei_proto_send_property(struct ei *ei, const char *name, const char *value, uint32_t permissions) +{ + prepare_msg(PROPERTY, Property, property); + + property.name = (char*)name; + property.value = value ? (char*)value : ""; + property.permissions = permissions; + + return ei_proto_send_msg(ei, &msg); +} + static const struct ei_proto_requests requests = { .connect = ei_proto_send_connect, .connect_done = ei_proto_send_connect_done, @@ -464,6 +483,7 @@ static const struct ei_proto_requests requests = { .touch_motion = ei_proto_send_touch_motion, .touch_up = ei_proto_send_touch_up, .frame = ei_proto_send_frame, + .property = ei_proto_send_property, }; const struct ei_proto_requests * diff --git a/src/libei-proto.h b/src/libei-proto.h index 94bd356..d013fdd 100644 --- a/src/libei-proto.h +++ b/src/libei-proto.h @@ -54,6 +54,9 @@ struct ei_proto_interface { int (*keyboard_modifiers)(struct ei *ei, uint32_t deviceid, uint32_t depressed, uint32_t latched, uint32_t locked, uint32_t group); + int (*property)(struct ei *ei, + const char *name, const char *value, + uint32_t permissions); }; struct ei_proto_requests { @@ -80,6 +83,9 @@ struct ei_proto_requests { uint32_t tid, double x, double y); int (*touch_up)(struct ei_device *device, uint32_t tid); int (*frame)(struct ei_device *device); + int (*property)(struct ei *ei, + const char *name, const char *value, + uint32_t permissions); }; int diff --git a/src/libei.c b/src/libei.c index e5de6bf..0a6f4de 100644 --- a/src/libei.c +++ b/src/libei.c @@ -24,10 +24,12 @@ #include "config.h" #include +#include #include #include #include + #include "util-io.h" #include "util-macros.h" #include "util-object.h" @@ -80,6 +82,11 @@ ei_destroy(struct ei *ei) ei->backend = NULL; sink_unref(ei->sink); free(ei->name); + + struct ei_property *prop; + list_for_each_safe(prop, &ei->properties, link) { + ei_property_unref(prop); + } } static @@ -96,6 +103,33 @@ OBJECT_IMPLEMENT_GETTER(ei, user_data, void *); DEFINE_UNREF_CLEANUP_FUNC(ei_device); DEFINE_UNREF_CLEANUP_FUNC(ei_region); +static void +set_prop_cmdline(struct ei *ei) +{ + char path[PATH_MAX]; + + xsnprintf(path, sizeof(path), "/proc/%u/cmdline", getpid()); + _cleanup_close_ int fd = open(path, O_RDONLY); + if (fd < 0) + return; + + int len; + if ((len = read(fd, path, sizeof(path) - 1)) < 0) + return; + path[len] = '\0'; + + ei_property_update(ei, "ei.application.cmdline", path, EI_PROPERTY_PERM_NONE); +} + +static void +set_prop_pid(struct ei *ei) +{ + char pid[64]; + + xsnprintf(pid, sizeof(pid), "%u", getpid()); + ei_property_update(ei, "ei.application.pid", pid, EI_PROPERTY_PERM_NONE); +} + _public_ struct ei * ei_new(void *user_data) { @@ -105,6 +139,7 @@ ei_new(void *user_data) ei->requests = ei_proto_get_requests(); list_init(&ei->event_queue); list_init(&ei->seats); + list_init(&ei->properties); ei_log_set_handler(ei, NULL); ei_log_set_priority(ei, EI_LOG_PRIORITY_INFO); @@ -115,6 +150,9 @@ ei_new(void *user_data) ei->user_data = user_data; ei->backend = NULL; + set_prop_pid(ei); + set_prop_cmdline(ei); + return steal(&ei); } @@ -277,6 +315,19 @@ queue_keyboard_modifiers_event(struct ei_device *device, queue_event(ei, e); } +static void +queue_property_event(struct ei *ei, const char *name, + const char *value, uint32_t permissions) +{ + struct ei_event *e = ei_event_new(ei); + e->type = EI_EVENT_PROPERTY; + e->prop.name = xstrdup(name); + e->prop.value = xstrdup(value); + e->prop.permissions = permissions; + + queue_event(ei, e); +} + void ei_disconnect(struct ei *ei) { @@ -465,6 +516,17 @@ handle_msg_keyboard_modifiers(struct ei *ei, uint32_t deviceid, return 0; } +static int +handle_msg_property(struct ei *ei, const char *name, const char *value, + uint32_t permissions) +{ + int rc = ei_property_update(ei, name, value, permissions); + if (rc == 0) + queue_property_event(ei, name, value, permissions); + + return 0; +} + static int handle_msg_device_removed(struct ei *ei, uint32_t deviceid) { @@ -507,6 +569,19 @@ handle_msg_device_paused(struct ei *ei, uint32_t deviceid) return 0; } +int +ei_send_property(struct ei *ei, const char *name, const char *value, uint32_t permissions) +{ + /* properties before CONNECTED_DONE are handled in a custom way */ + if (ei->state == EI_STATE_NEW || ei->state == EI_STATE_DISCONNECTED) + return 0; + + int rc = ei->requests->property(ei, name, value, permissions); + if (rc) + ei_disconnect(ei); + return rc; +} + int ei_send_close_device(struct ei_device *device) { @@ -798,6 +873,7 @@ static const struct ei_proto_interface intf_state_connected = { .device_keymap = handle_msg_device_keymap, .device_done = handle_msg_device_added_done, .keyboard_modifiers = handle_msg_keyboard_modifiers, + .property = handle_msg_property, }; static const struct ei_proto_interface *interfaces[] = { @@ -858,6 +934,13 @@ ei_set_connection(struct ei *ei, int fd) ei->source = source_ref(source); ei->state = EI_STATE_BACKEND; rc = ei->requests->connect(ei); + + struct ei_property *prop; + list_for_each_safe(prop, &ei->properties, link) { + if (rc == 0) + rc = ei->requests->property(ei, prop->name, prop->value, prop->permissions); + } + if (rc == 0) { rc = ei->requests->connect_done(ei); } diff --git a/src/libei.h b/src/libei.h index 0173b4b..5bdeb03 100644 --- a/src/libei.h +++ b/src/libei.h @@ -142,6 +142,22 @@ enum ei_device_capability { EI_DEVICE_CAP_TOUCH, }; +/** + * @enum ei_property_permission + * + * A set of masks for operations permitted on properties. Note that property + * permissions only affect the libei client, the server has full access to the + * properties at any time. + */ +enum ei_property_permission { + EI_PROPERTY_PERM_NONE = 0, + EI_PROPERTY_PERM_READ = (1 << 0), + EI_PROPERTY_PERM_WRITE = (1 << 1), + EI_PROPERTY_PERM_DELETE = (1 << 2), + + EI_PROPERTY_PERM_ALL = (EI_PROPERTY_PERM_READ|EI_PROPERTY_PERM_WRITE|EI_PROPERTY_PERM_DELETE), +}; + /** * @enum ei_keymap_type * @@ -179,6 +195,17 @@ enum ei_event_type { */ EI_EVENT_DISCONNECT, + /** + * The server has added, removed, or changed a property. + * + * This event is not generated for properties without the @ref + * EI_PROPERTY_PERM_READ set. + * + * libei guarantees that a @ref EI_EVENT_DISCONNECT is provided to + * the caller even where the server does not send one. + */ + EI_EVENT_PROPERTY, + /** * The server has added a seat available to this client. * @@ -329,6 +356,59 @@ ei_log_get_priority(const struct ei *ei); void ei_configure_name(struct ei * ei, const char *name); +/** + * See ei_property_set_with_permissions(), but the permissions are + * left as-is. If the property does not exist, it is created with permissions + * @ref EI_PROPERTY_PERM_ALL. + */ +int +ei_property_set(struct ei *ei, const char *property, const char *value); + +/** + * Change, create or delete a property. + * + * - If the property does not yet exist, it is created with the given @ref + * ei_property_permission. + * - If the property exists and @a value is not NULL and the @ref + * EI_PROPERTY_WRITE permission is set on the property, the value is changed + * to the new value and the property's permissions are updated to the given + * permission. + * - If the property exists and @a value is NULL and the @ref + * EI_PROPERTY_WRITE and @ref EI_PROPERTY_DELETE permission is set on the + * property, the property is deleted. + * + * Note that property permissions only apply to the libei client, they do not + * apply to the EIS server which can read, modify and delete any property + * regardless of permissions. + * + * Permissions can only be reduced, not increased and the permissions applied + * to the property are always the binary AND of @a permissions and the + * existing permissions for this property (if any). + * + * @return 0 on success or a negative errno on failure + * @retval -EACCESS The client does not have sufficient permissions for this + * operation + * @retval -EPERM The permissions mask is too loose + */ +int +ei_property_set_with_permissions(struct ei *ei, const char *property, const char *value, + uint32_t permission); + +/** + * Return the permissions mask for the given property. If the property does + * not exist or does not have the @ref EI_PROPERTY_READ permission, + * this function returns zero. + */ +uint32_t +ei_property_get_permissions(struct ei *ei, const char *property); + +/** + * Return the value of the given property or NULL if the property is not set + * or does not have the @ref EI_PROPERTY_READ permission. + */ +const char * +ei_property_get(struct ei *ei, const char *property); + /** * Set this ei context to use the socket backend. The ei context will * connect to the socket at the given path and initiate the conversation @@ -1169,6 +1249,39 @@ ei_touch_get_device(struct ei_touch *touch); struct ei_seat * ei_event_get_seat(struct ei_event *event); +/** + * For an event of type @ref EI_EVENT_PROPERTY, get the property name + * that has changed. + */ +const char * +ei_event_property_get_name(struct ei_event *event); + +/** + * For an event of type @ref EI_EVENT_PROPERTY, get the new property + * value (at the time of the event). + * + * A property value of NULL indicates the property has been deleted. + * + * Note that the property value as seen in the event and the property value on + * the ei context itself may differ. The latter always reflects the current + * state of the property while the event contains the value of the property + * at the time of the event. + */ +const char * +ei_event_property_get_value(struct ei_event *event); + +/** + * For an event of type @ref EI_EVENT_PROPERTY, get the permissions of the + * property (at the time of the event). + * + * Note that the property permissions as seen in the event and the property + * permissiosn on the ei context itself may differ. The latter always reflects + * the current state of the property while the event contains the permissions of + * the property at the time of the event. + */ +uint32_t +ei_event_property_get_permissions(struct ei_event *event); + /** * @} */ diff --git a/src/libeis-client.c b/src/libeis-client.c index 29959f1..52f8841 100644 --- a/src/libeis-client.c +++ b/src/libeis-client.c @@ -47,6 +47,11 @@ eis_client_destroy(struct eis_client *client) source_remove(client->source); source_unref(client->source); list_remove(&client->link); + + struct eis_property *prop; + list_for_each_safe(prop, &client->properties, link) { + eis_property_unref(prop); + } } static @@ -549,8 +554,36 @@ client_msg_disconnect(struct eis_client *client) return -ECANCELED; } +static int +client_msg_property(struct eis_client *client, + const char *name, const char *value, + uint32_t permissions) +{ + struct eis_property *prop = eis_property_find(client, name); + + /* The client cannot increase property permissions */ + if (prop) + permissions = prop->permissions & permissions; + + eis_property_update(client, name, value, permissions); + + return 0; +} + +static int +client_msg_property_with_event(struct eis_client *client, + const char *name, const char *value, + uint32_t permissions) +{ + int rc = client_msg_property(client, name, value, permissions); + if (rc == 0) + eis_queue_property_event(client, name, value, permissions); + return rc; +} + static const struct eis_proto_interface intf_state_new = { .connect = client_msg_connect, + .property = client_msg_property, .connect_done = client_msg_connect_done, .disconnect = client_msg_disconnect, .configure_name = client_msg_configure_name, @@ -566,6 +599,7 @@ static const struct eis_proto_interface intf_state_connecting = { static const struct eis_proto_interface intf_state_connected = { .disconnect = client_msg_disconnect, + .property = client_msg_property_with_event, .bind_seat = client_msg_bind_seat, .unbind_seat = client_msg_unbind_seat, .close_device = client_msg_close_device, @@ -676,6 +710,7 @@ eis_client_new(struct eis *eis, int fd) client->id = ++client_id; list_init(&client->seats); list_init(&client->seats_pending); + list_init(&client->properties); struct source *s = source_new(fd, client_dispatch, client); int rc = sink_add_source(eis->sink, s); diff --git a/src/libeis-event.c b/src/libeis-event.c index eb32e93..9b9500a 100644 --- a/src/libeis-event.c +++ b/src/libeis-event.c @@ -58,6 +58,11 @@ eis_event_destroy(struct eis_event *event) case EIS_EVENT_FRAME: handled = true; break; + case EIS_EVENT_CLIENT_PROPERTY: + handled = true; + free(event->prop.name); + free(event->prop.value); + break; } if (!handled) @@ -367,3 +372,31 @@ eis_event_touch_get_y(struct eis_event *event) return event->touch.y; } + +_public_ const char * +eis_event_property_get_name(struct eis_event *event) +{ + require_event_type(event, NULL, + EIS_EVENT_CLIENT_PROPERTY); + + return event->prop.name; +} + +_public_ const char * +eis_event_property_get_value(struct eis_event *event) +{ + require_event_type(event, NULL, + EIS_EVENT_CLIENT_PROPERTY); + + return event->prop.value; +} + +_public_ uint32_t +eis_event_property_get_permissions(struct eis_event *event) +{ + require_event_type(event, 0, + EIS_EVENT_CLIENT_PROPERTY); + + return event->prop.permissions; +} + diff --git a/src/libeis-private.h b/src/libeis-private.h index 90ab808..601d62d 100644 --- a/src/libeis-private.h +++ b/src/libeis-private.h @@ -71,9 +71,12 @@ struct eis_client { enum eis_client_state state; char *name; + struct list seats; struct list seats_pending; + struct list properties; + struct { enum { CLIENT_CAP_POLICY_ALLOW, @@ -170,6 +173,11 @@ struct eis_event { uint32_t touchid; double x, y; } touch; + struct { + char *name; + char *value; + uint32_t permissions; + } prop; }; }; @@ -190,6 +198,24 @@ struct eis_xkb_modifiers { uint32_t group; }; +struct eis_property { + struct object object; + struct list link; + char *name; + char *value; + uint32_t permissions; +}; + +struct eis_property * +eis_property_unref(struct eis_property *prop); + +void +eis_property_update(struct eis_client *client, const char *name, + const char *value, uint32_t permissions); + +struct eis_property * +eis_property_find(struct eis_client *client, const char *name); + void eis_init_object(struct eis *eis, struct object *parent); @@ -313,6 +339,10 @@ eis_queue_connect_event(struct eis_client *client); void eis_queue_disconnect_event(struct eis_client *client); +void +eis_queue_property_event(struct eis_client *client, const char *name, + const char *value, uint32_t permissions); + void eis_queue_seat_bind_event(struct eis_seat *seat, uint32_t capabilities); diff --git a/src/libeis-property.c b/src/libeis-property.c new file mode 100644 index 0000000..a74d4a9 --- /dev/null +++ b/src/libeis-property.c @@ -0,0 +1,150 @@ +/* + * Copyright © 2021 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 "config.h" + +#include + +#include "util-strings.h" +#include "util-object.h" + +#include "libeis-private.h" +#include "libeis-proto.h" + +static void +eis_property_destroy(struct eis_property *prop) +{ + list_remove(&prop->link); + free(prop->name); + free(prop->value); +} + +static +OBJECT_IMPLEMENT_CREATE(eis_property); +OBJECT_IMPLEMENT_UNREF(eis_property); + +struct eis_property * +eis_property_find(struct eis_client *client, const char *name) +{ + struct eis_property *prop; + list_for_each(prop, &client->properties, link) { + if (streq(prop->name, name)) { + return prop; + } + } + + return NULL; +} + +static struct eis_property * +eis_client_property_new(struct eis_client *client, const char *name, + const char *value, uint32_t permissions) +{ + assert(name); + assert(value); + + struct eis_property *prop = eis_property_create(NULL); + prop->name = xstrdup(name); + prop->value = xstrdup(value); + prop->permissions = permissions & EIS_PROPERTY_PERM_ALL; + + /* initial ref is owned by this list */ + list_append(&client->properties, &prop->link); + + return prop; +} + +static void +eis_property_delete(struct eis_client *client, struct eis_property *prop) +{ + eis_property_unref(prop); +} + +static void +eis_property_change(struct eis_client *client, struct eis_property *prop, + const char *value, uint32_t permissions) +{ + if (prop->value != value) { + free(prop->value); + prop->value = xstrdup(value); + } + prop->permissions = permissions; +} + +void +eis_property_update(struct eis_client *client, const char *name, + const char *value, uint32_t permissions) +{ + struct eis_property *prop = eis_property_find(client, name); + + if (!prop) { + eis_client_property_new(client, name, value, permissions); + } else if (value) { + eis_property_change(client, prop, value, permissions); + } else { + eis_property_delete(client, prop); + } +} + +_public_ void +eis_client_property_set_with_permissions(struct eis_client *client, const char *name, + const char *value, uint32_t permissions) +{ + if (strstartswith(name, "ei.") || strstartswith(name, "eis.")) + return; + + eis_property_update(client, name, value, permissions); + + /* We should have a check to see if anything actually changed, but meh */ + struct eis *eis = eis_client_get_context(client); + eis->requests->property(client, name, value, permissions); +} + +_public_ void +eis_client_property_set(struct eis_client *client, const char *name, const char *value) +{ + uint32_t permissions = EIS_PROPERTY_PERM_ALL; + struct eis_property *prop = eis_property_find(client, name); + + if (prop) + permissions = prop->permissions; + + return eis_client_property_set_with_permissions(client, name, value, permissions); +} + +_public_ uint32_t +eis_client_property_get_permissions(struct eis_client *client, const char *name) +{ + struct eis_property *prop = eis_property_find(client, name); + + return prop ? prop->permissions : 0; +} + +_public_ const char * +eis_client_property_get(struct eis_client *client, const char *name) +{ + struct eis_property *prop = eis_property_find(client, name); + + return prop ? prop->value : NULL; +} diff --git a/src/libeis-proto.c b/src/libeis-proto.c index 14fda87..2d391d3 100644 --- a/src/libeis-proto.c +++ b/src/libeis-proto.c @@ -57,6 +57,7 @@ log_wire_message(struct eis *eis, const ServerMessage *msg) MSG_STRING_CASE(DEVICE_RESUMED); MSG_STRING_CASE(DEVICE_PAUSED); MSG_STRING_CASE(KEYBOARD_MODIFIERS); + MSG_STRING_CASE(PROPERTY); break; default: assert(!"Unimplemented message type"); @@ -248,6 +249,20 @@ eis_proto_send_device_resumed(struct eis_device *device) return eis_proto_send_msg(eis_device_get_client(device), &msg); } +static int +eis_proto_send_property(struct eis_client *client, const char *name, + const char *value, uint32_t permissions) +{ + + prepare_msg(PROPERTY, Property, property); + + property.name = (char*)name; + property.value = (char*)value; + property.permissions = permissions; + + return eis_proto_send_msg(client, &msg); +} + static const struct eis_proto_requests requests = { .disconnected = eis_proto_send_disconnected, .connected = eis_proto_send_connected, @@ -261,6 +276,7 @@ static const struct eis_proto_requests requests = { .device_done = eis_proto_send_device_done, .device_region = eis_proto_send_device_region, .keyboard_modifiers = eis_proto_send_keyboard_modifiers, + .property = eis_proto_send_property, }; const struct eis_proto_requests * @@ -398,6 +414,11 @@ eis_proto_handle_message(struct eis_client *client, case CLIENT_MESSAGE__MSG_FRAME: rc = call(frame, client, proto->frame->deviceid); break; + case CLIENT_MESSAGE__MSG_PROPERTY: + rc = call(property, client, proto->property->name, + proto->property->value[0] ? proto->property->value : NULL, + proto->property->permissions); + break; default: rc = -EBADMSG; break; diff --git a/src/libeis-proto.h b/src/libeis-proto.h index bcc6089..2c66fb4 100644 --- a/src/libeis-proto.h +++ b/src/libeis-proto.h @@ -60,6 +60,8 @@ struct eis_proto_interface { bool policy_is_allow, uint32_t allow, uint32_t deny); int (*frame) (struct eis_client *client, uint32_t deviceid); + int (*property)(struct eis_client *client, const char *name, + const char *value, uint32_t permissions); }; struct eis_proto_requests { @@ -77,6 +79,8 @@ struct eis_proto_requests { const struct eis_region *region); int (*keyboard_modifiers)(struct eis_device *device, const struct eis_xkb_modifiers *mods); + int (*property)(struct eis_client *client, const char *name, + const char *value, uint32_t permissions); }; int diff --git a/src/libeis.c b/src/libeis.c index aad39a5..004b8f7 100644 --- a/src/libeis.c +++ b/src/libeis.c @@ -105,6 +105,7 @@ eis_event_type_to_string(enum eis_event_type type) switch(type) { CASE_RETURN_STRING(EIS_EVENT_CLIENT_CONNECT); CASE_RETURN_STRING(EIS_EVENT_CLIENT_DISCONNECT); + CASE_RETURN_STRING(EIS_EVENT_CLIENT_PROPERTY); CASE_RETURN_STRING(EIS_EVENT_SEAT_BIND); CASE_RETURN_STRING(EIS_EVENT_SEAT_UNBIND); CASE_RETURN_STRING(EIS_EVENT_DEVICE_CLOSED); @@ -154,6 +155,18 @@ eis_queue_disconnect_event(struct eis_client *client) eis_queue_event(e); } +void +eis_queue_property_event(struct eis_client *client, const char *name, const char *value, + uint32_t permissions) +{ + struct eis_event *e = eis_event_new_for_client(client); + e->type = EIS_EVENT_CLIENT_PROPERTY; + e->prop.name = xstrdup(name); + e->prop.value = xstrdup(value); + e->prop.permissions = permissions; + eis_queue_event(e); +} + void eis_queue_seat_bind_event(struct eis_seat *seat, uint32_t capabilities) { diff --git a/src/libeis.h b/src/libeis.h index 1590189..f62b873 100644 --- a/src/libeis.h +++ b/src/libeis.h @@ -82,6 +82,22 @@ enum eis_device_capability { EIS_DEVICE_CAP_TOUCH, }; +/** + * @enum eis_property_permission + * + * A set of masks for operations permitted on properties. Note that property + * permissions only affect the libei client, the server has full access to the + * properties at any time. + */ +enum eis_property_permission { + EIS_PROPERTY_PERM_NONE = 0, + EIS_PROPERTY_PERM_READ = (1 << 0), + EIS_PROPERTY_PERM_WRITE = (1 << 1), + EIS_PROPERTY_PERM_DELETE = (1 << 2), + + EIS_PROPERTY_PERM_ALL = (EIS_PROPERTY_PERM_READ|EIS_PROPERTY_PERM_WRITE|EIS_PROPERTY_PERM_DELETE), +}; + enum eis_keymap_type { EIS_KEYMAP_TYPE_XKB = 1, }; @@ -100,6 +116,11 @@ enum eis_event_type { */ EIS_EVENT_CLIENT_DISCONNECT, + /** + * A client property has been added, changed, or deleted. + */ + EIS_EVENT_CLIENT_PROPERTY, + /** * The client wants to bind to a seat. Devices associated with this * seat should be sent to the client. @@ -220,6 +241,56 @@ eis_get_user_data(struct eis *eis); void eis_set_user_data(struct eis *eis, void *user_data); +/** + * See eis_client_property_set_with_permissions(), but the permissions are + * left as-is. If the property does not exist, it is created with permissions + * @ref EIS_PROPERTY_PERM_ALL. + */ +void +eis_client_property_set(struct eis_client *client, const char *property, const char *value); + +/** + * Change, create or delete a property. + * + * - If the property does not yet exist, it is created with the given @ref + * eis_property_permission. + * - If the property exists and @a value is not NULL, the value is changed to + * the new value. + * - If the property exists and @a value is NULL, the property is deleted. + * + * If the property is not deleted, the permissions are updated to the given @a + * permissions. + * + * Properties should use a name in the form "namespace.name" or + * "namespace.name.subname". Two namespaces are reserved for use by this + * library: "eis.*" and "ei.*". Attempting to change a property within that + * namespace will be silently ignored. + */ +void +eis_client_property_set_with_permissions(struct eis_client *client, + const char *property, const char *value, + uint32_t permission); + +/** + * Return the permissions mask for the given property. If the property does + * not exist, this function returns zero. + */ +uint32_t +eis_client_property_get_permissions(struct eis_client *client, const char *propert); + +/** + * Return the value of the given property or NULL if the property is not set. + * + * The following property names are automatically set by libei: + * - "ei.application.pid" - the PID of the libei process + * - "ei.application.cmdline" - The first element of /proc//cmdline of + * the libei process + * + */ +const char * +eis_client_property_get(struct eis_client *client, const char *property); + + /** * Initialize the context with a UNIX socket name. * If the path does not start with / it is relative to $XDG_RUNTIME_DIR. @@ -416,6 +487,29 @@ eis_event_get_type(struct eis_event *event); struct eis_client * eis_event_get_client(struct eis_event *event); +/** + * For an event of type @ref EIS_EVENT_CLIENT_PROPERTY, get the property name + * that has changed. + */ +const char * +eis_event_property_get_name(struct eis_event *event); + +/** + * For an event of type @ref EIS_EVENT_CLIENT_PROPERTY, get the new property + * value (at the time of the event). + * + * A property value of NULL indicates the property has been deleted. + */ +const char * +eis_event_property_get_value(struct eis_event *event); + +/** + * For an event of type @ref EIS_EVENT_CLIENT_PROPERTY, get the permissions + * of the property (at the time of the event). + */ +uint32_t +eis_event_property_get_permissions(struct eis_event *event); + struct eis_seat * eis_event_get_seat(struct eis_event *event); diff --git a/test/eierpecken.c b/test/eierpecken.c index cca7f95..f15b258 100644 --- a/test/eierpecken.c +++ b/test/eierpecken.c @@ -1012,6 +1012,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(PROPERTY); } #undef CASE_STRING assert(!"Unhandled ei event type"); @@ -1032,6 +1033,7 @@ peck_eis_event_type_name(enum eis_event_type type) switch (type) { CASE_STRING(CLIENT_CONNECT); CASE_STRING(CLIENT_DISCONNECT); + CASE_STRING(CLIENT_PROPERTY); CASE_STRING(SEAT_BIND); CASE_STRING(SEAT_UNBIND); CASE_STRING(DEVICE_CLOSED); diff --git a/test/test-ei.c b/test/test-ei.c index a480dce..c5f3d16 100644 --- a/test/test-ei.c +++ b/test/test-ei.c @@ -23,7 +23,11 @@ #include "config.h" +#include +#include + #include "util-munit.h" +#include "util-strings.h" #include "eierpecken.h" DEFINE_UNREF_CLEANUP_FUNC(peck); @@ -45,6 +49,422 @@ MUNIT_TEST(test_ei_ref_unref) return MUNIT_OK; } +MUNIT_TEST(test_ei_initial_properties) +{ + _unref_(peck) *peck = peck_new(); + char pid[64]; + char cmdline[PATH_MAX]; + + xsnprintf(pid, sizeof(pid), "%u", getpid()); + xsnprintf(cmdline, sizeof(cmdline), "/proc/%u/cmdline", getpid()); + + int fd = open(cmdline, O_RDONLY); + munit_assert_int(fd, >=, 0); + int len = read(fd, cmdline, sizeof(cmdline) - 1); + munit_assert_int(len, >=, 0); + cmdline[len] = '\0'; + + peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_NONE); + peck_dispatch_until_stable(peck); + + with_server(peck) { + _unref_(eis_event) *connect = + peck_eis_next_event(eis, EIS_EVENT_CLIENT_CONNECT); + struct eis_client *client = eis_event_get_client(connect); + + const char *pidprop = eis_client_property_get(client, "ei.application.pid"); + munit_assert_ptr_not_null(pidprop); + munit_assert_string_equal(pidprop, pid); + + const char *cmdprop = eis_client_property_get(client, "ei.application.cmdline"); + munit_assert_ptr_not_null(cmdprop); + munit_assert_string_equal(cmdprop, cmdline); + } + + return MUNIT_OK; +} + +MUNIT_TEST(test_ei_properties) +{ + _unref_(peck) *peck = peck_new(); + + peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_ALL); + peck_dispatch_until_stable(peck); + + with_client(peck) { + ei_property_set(ei, "foo.bar", "value"); + } + + peck_dispatch_until_stable(peck); + + with_server(peck) { + _unref_(eis_event) *e = + peck_eis_next_event(eis, EIS_EVENT_CLIENT_PROPERTY); + const char *name = eis_event_property_get_name(e); + const char *value = eis_event_property_get_value(e); + + munit_assert_string_equal(name, "foo.bar"); + munit_assert_string_equal(value, "value"); + } + + with_client(peck) { + int rc = ei_property_set(ei, "foo.bar", "newval"); + munit_assert_int(rc, ==, 0); + } + + peck_dispatch_until_stable(peck); + + with_server(peck) { + _unref_(eis_event) *e = + peck_eis_next_event(eis, EIS_EVENT_CLIENT_PROPERTY); + const char *name = eis_event_property_get_name(e); + const char *value = eis_event_property_get_value(e); + + munit_assert_string_equal(name, "foo.bar"); + munit_assert_string_equal(value, "newval"); + } + + with_client(peck) { + int rc = ei_property_set(ei, "foo.bar", NULL); + munit_assert_int(rc, ==, 0); + } + + peck_dispatch_until_stable(peck); + + with_server(peck) { + _unref_(eis_event) *e = + peck_eis_next_event(eis, EIS_EVENT_CLIENT_PROPERTY); + const char *name = eis_event_property_get_name(e); + const char *value = eis_event_property_get_value(e); + + munit_assert_string_equal(name, "foo.bar"); + munit_assert_ptr_null(value); + } + + return MUNIT_OK; +} + +MUNIT_TEST(test_ei_properties_reserved) +{ + _unref_(peck) *peck = peck_new(); + + peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_ALL); + peck_dispatch_until_stable(peck); + + with_client(peck) { + int rc; + + rc = ei_property_set(ei, "ei.bar", "value"); + munit_assert_int(rc, ==, -EACCES); + + rc = ei_property_set(ei, "eis.bar", "value"); + munit_assert_int(rc, ==, -EACCES); + } + + peck_dispatch_until_stable(peck); + + with_server(peck) { + peck_assert_no_eis_events(eis); + } + + peck_dispatch_until_stable(peck); + + return MUNIT_OK; +} + +MUNIT_TEST(test_ei_properties_permissions) +{ + _unref_(peck) *peck = peck_new(); + + peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_ALL); + peck_dispatch_until_stable(peck); + + with_client(peck) { + ei_property_set_with_permissions(ei, "cannot.delete", "v1", + EI_PROPERTY_PERM_READ|EI_PROPERTY_PERM_WRITE); + ei_property_set_with_permissions(ei, "cannot.write", "v2", + EI_PROPERTY_PERM_READ|EI_PROPERTY_PERM_DELETE); + ei_property_set_with_permissions(ei, "cannot.read", "v3", + EI_PROPERTY_PERM_DELETE|EI_PROPERTY_PERM_WRITE); + + const char *value; + int rc; + + /* can't delete but we can write to it */ + value = ei_property_get(ei, "cannot.delete"); + munit_assert_string_equal(value, "v1"); + rc = ei_property_set(ei, "cannot.delete", NULL); + munit_assert_int(rc, ==, -EACCES); + value = ei_property_get(ei, "cannot.delete"); + munit_assert_string_equal(value, "v1"); + rc = ei_property_set(ei, "cannot.delete", "newv1"); + munit_assert_int(rc, ==, 0); + value = ei_property_get(ei, "cannot.delete"); + munit_assert_string_equal(value, "newv1"); + + /* can't modify it but we can delete it */ + value = ei_property_get(ei, "cannot.write"); + munit_assert_string_equal(value, "v2"); + rc = ei_property_set(ei, "cannot.write", "newv2"); + munit_assert_int(rc, ==, -EACCES); + value = ei_property_get(ei, "cannot.write"); + munit_assert_string_equal(value, "v2"); + rc = ei_property_set(ei, "cannot.write", NULL); + munit_assert_int(rc, ==, 0); + value = ei_property_get(ei, "cannot.write"); + munit_assert_ptr_equal(value, NULL); + + value = ei_property_get(ei, "cannot.read"); + munit_assert_ptr_equal(value, NULL); + rc = ei_property_set(ei, "cannot.read", "newv3"); + munit_assert_int(rc, ==, -EACCES); + value = ei_property_get(ei, "cannot.read"); + munit_assert_ptr_equal(value, NULL); + rc = ei_property_set(ei, "cannot.read", NULL); + munit_assert_int(rc, ==, -EACCES); + } + + return MUNIT_OK; +} + +MUNIT_TEST(test_ei_properties_permissions_drop) +{ + _unref_(peck) *peck = peck_new(); + + peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_ALL); + peck_dispatch_until_stable(peck); + + with_client(peck) { + int rc; + ei_property_set_with_permissions(ei, "test.perms", "v1", + EI_PROPERTY_PERM_READ|EI_PROPERTY_PERM_WRITE|EI_PROPERTY_PERM_DELETE); + + /* downgrading permissions, can't delete anymore */ + rc = ei_property_set_with_permissions(ei, "test.perms", "v1", + EI_PROPERTY_PERM_READ|EI_PROPERTY_PERM_WRITE); + munit_assert_int(rc, ==, 0); + rc = ei_property_set(ei, "test.perms", NULL); + munit_assert_int(rc, ==, -EACCES); + + /* upgrading permissions should fail */ + rc = ei_property_set_with_permissions(ei, "test.perms", "v1", + EI_PROPERTY_PERM_READ|EI_PROPERTY_PERM_WRITE|EI_PROPERTY_PERM_DELETE); + munit_assert_int(rc, ==, -EPERM); + + /* Drop the write permission */ + rc = ei_property_set_with_permissions(ei, "test.perms", "v1", + EI_PROPERTY_PERM_READ); + munit_assert_int(rc, ==, 0); + rc = ei_property_set(ei, "test.perms", "new value"); + munit_assert_int(rc, ==, -EACCES); + + /* We need write permissions to change the permissions */ + rc = ei_property_set_with_permissions(ei, "test.perms", "v1", + EI_PROPERTY_PERM_NONE); + munit_assert_int(rc, ==, -EACCES); + } + + return MUNIT_OK; +} + +MUNIT_TEST(test_ei_properties_events) +{ + _unref_(peck) *peck = peck_new(); + + peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_ALL); + peck_dispatch_until_stable(peck); + + with_client(peck) { + ei_property_set(ei, "test.events", "v1"); + ei_property_set(ei, "test.events", "v2"); + } + + peck_dispatch_until_stable(peck); + + with_server(peck) { + _unref_(eis_event) *e1 = + peck_eis_next_event(eis, EIS_EVENT_CLIENT_PROPERTY); + const char *name = eis_event_property_get_name(e1); + const char *value = eis_event_property_get_value(e1); + munit_assert_string_equal(name, "test.events"); + munit_assert_string_equal(value, "v1"); + + struct eis_client *client = eis_event_get_client(e1); + const char *propval = eis_client_property_get(client, "test.events"); + /* + * This probably needs fixing: because we processed the second + * event already, the actual property value is different to + * our current event value. + */ + munit_assert_string_equal(propval, "v2"); + + _unref_(eis_event) *e2 = + peck_eis_next_event(eis, EIS_EVENT_CLIENT_PROPERTY); + name = eis_event_property_get_name(e2); + value = eis_event_property_get_value(e2); + munit_assert_string_equal(name, "test.events"); + munit_assert_string_equal(value, "v2"); + } + + /* delete it */ + with_client(peck) { + ei_property_set(ei, "test.events", "v3"); + ei_property_set(ei, "test.events", NULL); + } + + peck_dispatch_until_stable(peck); + + with_server(peck) { + _unref_(eis_event) *e3 = + peck_eis_next_event(eis, EIS_EVENT_CLIENT_PROPERTY); + const char *name = eis_event_property_get_name(e3); + const char *value = eis_event_property_get_value(e3); + munit_assert_string_equal(name, "test.events"); + munit_assert_string_equal(value, "v3"); + + struct eis_client *client = eis_event_get_client(e3); + const char *propval = eis_client_property_get(client, "test.events"); + /* + * This probably needs fixing: because we processed the second + * event already, the actual property value is different to + * our current event value. + */ + munit_assert_ptr_null(propval); + + _unref_(eis_event) *e4 = + peck_eis_next_event(eis, EIS_EVENT_CLIENT_PROPERTY); + name = eis_event_property_get_name(e4); + value = eis_event_property_get_value(e4); + munit_assert_string_equal(name, "test.events"); + munit_assert_ptr_null(value); + } + + return MUNIT_OK; +} + +MUNIT_TEST(test_ei_properties_events_server) +{ + _unref_(peck) *peck = peck_new(); + + peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_ALL); + peck_dispatch_until_stable(peck); + + with_server(peck) { + struct eis_client *client = peck_eis_get_default_client(peck); + eis_client_property_set(client, "test.events", "v1"); + eis_client_property_set(client, "test.events", "v2"); + } + + peck_dispatch_until_stable(peck); + + with_client(peck) { + _unref_(ei_event) *e1 = + peck_ei_next_event(ei, EI_EVENT_PROPERTY); + const char *name = ei_event_property_get_name(e1); + const char *value = ei_event_property_get_value(e1); + munit_assert_string_equal(name, "test.events"); + munit_assert_string_equal(value, "v1"); + + const char *propval = ei_property_get(ei, "test.events"); + /* + * This probably needs fixing: because we processed the second + * event already, the actual property value is different to + * our current event value. + */ + munit_assert_string_equal(propval, "v2"); + + _unref_(ei_event) *e2 = + peck_ei_next_event(ei, EI_EVENT_PROPERTY); + name = ei_event_property_get_name(e2); + value = ei_event_property_get_value(e2); + munit_assert_string_equal(name, "test.events"); + munit_assert_string_equal(value, "v2"); + } + + /* delete it */ + with_server(peck) { + struct eis_client *client = peck_eis_get_default_client(peck); + eis_client_property_set(client, "test.events", "v3"); + eis_client_property_set(client, "test.events", NULL); + } + + peck_dispatch_until_stable(peck); + + with_client(peck) { + _unref_(ei_event) *e3 = + peck_ei_next_event(ei, EI_EVENT_PROPERTY); + const char *name = ei_event_property_get_name(e3); + const char *value = ei_event_property_get_value(e3); + munit_assert_string_equal(name, "test.events"); + munit_assert_string_equal(value, "v3"); + + const char *propval = ei_property_get(ei, "test.events"); + /* + * This probably needs fixing: because we processed the second + * event already, the actual property value is different to + * our current event value. + */ + munit_assert_ptr_null(propval); + + _unref_(ei_event) *e4 = + peck_ei_next_event(ei, EI_EVENT_PROPERTY); + name = ei_event_property_get_name(e4); + value = ei_event_property_get_value(e4); + munit_assert_string_equal(name, "test.events"); + munit_assert_ptr_null(value); + } + + return MUNIT_OK; +} + +MUNIT_TEST(test_ei_properties_events_pingpong) +{ + _unref_(peck) *peck = peck_new(); + + peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_ALL); + peck_dispatch_until_stable(peck); + + with_server(peck) { + struct eis_client *client = peck_eis_get_default_client(peck); + eis_client_property_set(client, "test.events", "ping"); + } + + peck_dispatch_until_stable(peck); + + with_client(peck) { + _unref_(ei_event) *e = + peck_ei_next_event(ei, EI_EVENT_PROPERTY); + const char *name = ei_event_property_get_name(e); + const char *value = ei_event_property_get_value(e); + munit_assert_string_equal(name, "test.events"); + munit_assert_string_equal(value, "ping"); + + const char *propval = ei_property_get(ei, "test.events"); + munit_assert_string_equal(propval, "ping"); + + ei_property_set(ei, "test.events", "pong"); + propval = ei_property_get(ei, "test.events"); + munit_assert_string_equal(propval, "pong"); + } + + peck_dispatch_until_stable(peck); + + with_server(peck) { + _unref_(eis_event) *e = + peck_eis_next_event(eis, EIS_EVENT_CLIENT_PROPERTY); + const char *name = eis_event_property_get_name(e); + const char *value = eis_event_property_get_value(e); + munit_assert_string_equal(name, "test.events"); + munit_assert_string_equal(value, "pong"); + + struct eis_client *client = eis_event_get_client(e); + const char *propval = eis_client_property_get(client, "test.events"); + munit_assert_string_equal(propval, "pong"); + } + + return MUNIT_OK; +} + MUNIT_TEST(test_ei_disconnect_immediately) { _unref_(peck) *peck = peck_new();