mirror of
https://gitlab.freedesktop.org/libinput/libei.git
synced 2025-12-25 21:00:07 +01:00
liboeffis is a wrapper library for the RemoteDesktop communication
libei used to have direct portal support code (see the git history) but: - that code was a custom proposed portal that never went anywhere - libei has slowly changed to be more an input event transport layer since it is now also used sending events *to* a libei context - a number of libei users will never need the DBus code, either because they don't want it or because they talk Dbus themselves na ddon't need this abstraction. Luckily, it's quite easy to move this into a separate library with a simple API that does, effectively, the same trick as the old portal backend. This API is aiming to be as simple as possible because the tools that require anything more complex should talk to DBus directly. An example tool that uses the API to retrieve an EIS fd over the RemoteDesktop portal is included in this patch. "Öffis" is a German word meaning public transport. It also sounds like the French Œuf, the word for egg. Co-authored-by: Olivier Fourdan <ofourdan@redhat.com>
This commit is contained in:
parent
8fc654bfb0
commit
eeefb3dc00
13 changed files with 2018 additions and 6 deletions
|
|
@ -32,14 +32,14 @@ variables:
|
|||
# See the documentation here: #
|
||||
# https://wayland.freedesktop.org/libinput/doc/latest/building_libinput.html #
|
||||
###############################################################################
|
||||
FEDORA_PACKAGES: 'git diffutils gcc gcc-c++ pkgconf-pkg-config meson systemd-devel protobuf-c-devel libxkbcommon-devel doxygen'
|
||||
FEDORA_PACKAGES: 'git diffutils gcc gcc-c++ pkgconf-pkg-config meson systemd-devel protobuf-c-devel libxkbcommon-devel doxygen python3-attrs python3-pytest python3-dbusmock'
|
||||
############################ end of package lists #############################
|
||||
|
||||
# these tags should be updated each time the list of packages is updated
|
||||
# changing these will force rebuilding the associated image
|
||||
# Note: these tags have no meaning and are not tied to a particular
|
||||
# libinput version
|
||||
FEDORA_TAG: '2022-12-05.0'
|
||||
FEDORA_TAG: '2022-12-05.1'
|
||||
|
||||
FDO_UPSTREAM_REPO: libinput/libei
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ variables:
|
|||
# See the documentation here: #
|
||||
# https://wayland.freedesktop.org/libinput/doc/latest/building_libinput.html #
|
||||
###############################################################################
|
||||
FEDORA_PACKAGES: 'git diffutils gcc gcc-c++ pkgconf-pkg-config meson systemd-devel protobuf-c-devel libxkbcommon-devel doxygen'
|
||||
FEDORA_PACKAGES: 'git diffutils gcc gcc-c++ pkgconf-pkg-config meson systemd-devel protobuf-c-devel libxkbcommon-devel doxygen python3-attrs python3-pytest python3-dbusmock'
|
||||
############################ end of package lists #############################
|
||||
|
||||
# these tags should be updated each time the list of packages is updated
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
#
|
||||
|
||||
# We're happy to rebuild all containers when one changes.
|
||||
.default_tag: &default_tag '2022-12-05.0'
|
||||
.default_tag: &default_tag '2022-12-05.1'
|
||||
|
||||
distributions:
|
||||
- name: fedora
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -2,10 +2,12 @@ libei
|
|||
=====
|
||||
|
||||
**libei** is a library for Emulated Input, primarily aimed at the Wayland
|
||||
stack. It provides three parts:
|
||||
stack. It provides four parts:
|
||||
- 🥚 EI (Emulated Input) for the client side (`libei`)
|
||||
- 🍦 EIS (Emulated Input Server) for the server side (`libeis`)
|
||||
- 🍚 REIS (Restrictions for the EIS) for the portal in between (`libreis`)
|
||||
- 🚌 oeffis is an optional helper library for DBus communication with the
|
||||
XDG RemoteDesktop portal (`liboeffis`)
|
||||
|
||||
The communication between these parts is an implementation detail, neither
|
||||
client nor server need to care about the details. Let's call it the BRidge
|
||||
|
|
@ -370,3 +372,13 @@ to verify which client is allowed to emulate input devices at the
|
|||
kernel level (polkit? config files?). This however is out of scope for **libei**.
|
||||
An example uinput server is implemented in the `eis-server-demo` in the
|
||||
with libei repository.
|
||||
|
||||
# liboeffis
|
||||
|
||||
In a Wayland and/or sandboxed environment, emulating input events requires
|
||||
going through the XDG RemoteDesktop portal. This portal is available on DBus
|
||||
but communication with DBus is often cumbersome, especially for small tools.
|
||||
|
||||
`liboeffis` is an optional library that provides the communication with the
|
||||
portal, sufficient to start a RemoteDesktop session and retrieve the file
|
||||
descriptor to the EIS implementation.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ src_doxygen = files(
|
|||
# source files
|
||||
'../src/libei.h',
|
||||
'../src/libeis.h',
|
||||
'../src/liboeffis.h',
|
||||
# style files
|
||||
'doxygen-awesome.css',
|
||||
)
|
||||
|
|
|
|||
55
meson.build
55
meson.build
|
|
@ -2,7 +2,7 @@ project('libei', 'c',
|
|||
version: '0.3',
|
||||
license: 'MIT',
|
||||
default_options: [ 'c_std=gnu99', 'warning_level=2' ],
|
||||
meson_version: '>= 0.50.0')
|
||||
meson_version: '>= 0.57.0')
|
||||
|
||||
pkgconfig = import('pkgconfig')
|
||||
|
||||
|
|
@ -224,6 +224,36 @@ executable('ei-debug-events',
|
|||
dependencies: [dep_libutil, dep_libei, dep_libevdev],
|
||||
install: true)
|
||||
|
||||
dep_systemd = dependency('libsystemd', required: get_option('liboeffis'))
|
||||
build_oeffis = dep_systemd.found()
|
||||
if build_oeffis
|
||||
src_liboeffis = 'src/liboeffis.c'
|
||||
deps_liboeffis = [dep_libutil, dep_systemd]
|
||||
|
||||
lib_liboeffis = shared_library('oeffis',
|
||||
src_liboeffis,
|
||||
dependencies: deps_liboeffis,
|
||||
gnu_symbol_visibility: 'hidden',
|
||||
install: true
|
||||
)
|
||||
install_headers('src/liboeffis.h')
|
||||
|
||||
dep_liboeffis = declare_dependency(link_with: lib_liboeffis,
|
||||
include_directories: 'src')
|
||||
|
||||
pkgconfig.generate(lib_liboeffis,
|
||||
filebase: 'liboeffis',
|
||||
name: 'libOeffis',
|
||||
description: 'RemoteDesktop portal DBus helper library',
|
||||
version: meson.project_version(),
|
||||
libraries: lib_liboeffis,
|
||||
)
|
||||
|
||||
executable('oeffis-demo-tool',
|
||||
'tools/oeffis-demo-tool.c',
|
||||
dependencies: [dep_libutil, dep_liboeffis])
|
||||
endif
|
||||
|
||||
# tests
|
||||
if get_option('tests')
|
||||
subproject('munit', default_options: 'werror=false')
|
||||
|
|
@ -265,6 +295,28 @@ if get_option('tests')
|
|||
c_args: ['-D_enable_tests_'],
|
||||
dependencies: [dep_unittest, dep_libutil, dep_protobuf]))
|
||||
|
||||
if build_oeffis
|
||||
test('unit-tests-oeffis',
|
||||
executable('unit-tests-oeffis',
|
||||
'test/unit-tests.c',
|
||||
src_liboeffis,
|
||||
include_directories: 'src',
|
||||
c_args: ['-D_enable_tests_'],
|
||||
dependencies: deps_liboeffis + [dep_unittest]))
|
||||
|
||||
env = environment()
|
||||
env.set('LD_LIBRARY_PATH', meson.current_build_dir())
|
||||
pymod = import('python')
|
||||
pymod.find_installation('python3', modules: ['pytest', 'attr', 'dbusmock'])
|
||||
pytest = find_program('pytest-3', 'pytest')
|
||||
test('pytest', pytest,
|
||||
args: ['--verbose', '--log-level=DEBUG'],
|
||||
suite: 'python',
|
||||
workdir: meson.current_source_dir() / 'test',
|
||||
env: env,
|
||||
)
|
||||
endif
|
||||
|
||||
lib_eierpecken = static_library('eierpecken',
|
||||
'test/eierpecken.h',
|
||||
'test/eierpecken.c',
|
||||
|
|
@ -290,6 +342,7 @@ if get_option('tests')
|
|||
'--leak-check=full',
|
||||
'--gen-suppressions=all',
|
||||
'--error-exitcode=3' ],
|
||||
exclude_suites: ['python'], # we don't want to valgrind python tests
|
||||
timeout_multiplier : 100)
|
||||
else
|
||||
message('valgrind not found, disabling valgrind test suite')
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
option('documentation', type: 'boolean', value: 'false', description: 'Enable/disable building the API documentation')
|
||||
option('tests', type: 'boolean', value: 'true', description: 'Enable/disable tests')
|
||||
option('liboeffis', type: 'feature', value: 'auto', description: 'Build liboeffis.so')
|
||||
|
|
|
|||
753
src/liboeffis.c
Normal file
753
src/liboeffis.c
Normal file
|
|
@ -0,0 +1,753 @@
|
|||
/* SPDX-License-Identifier: MIT */
|
||||
/*
|
||||
* Copyright © 2022 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 <systemd/sd-bus.h>
|
||||
|
||||
#include "util-io.h"
|
||||
#include "util-macros.h"
|
||||
#include "util-object.h"
|
||||
#include "util-sources.h"
|
||||
#include "util-strings.h"
|
||||
#include "util-time.h"
|
||||
#include "util-version.h"
|
||||
|
||||
#include "liboeffis.h"
|
||||
|
||||
_Static_assert(sizeof(enum oeffis_event_type) == sizeof(int), "Invalid enum size");
|
||||
_Static_assert(sizeof(enum oeffis_device) == sizeof(uint32_t), "Invalid enum size");
|
||||
|
||||
/* oeffis is simple enough that we don't really need debugging or a log
|
||||
* handler. If you want this on, just define it */
|
||||
/* #define log_debug(...) fprintf(stderr, "DEBUG " __VA_ARGS__) */
|
||||
#define log_debug(...) /* */
|
||||
|
||||
static void
|
||||
portal_init(struct oeffis *oeffis, const char *busname);
|
||||
static void
|
||||
portal_start(struct oeffis *oeffis);
|
||||
|
||||
enum oeffis_state {
|
||||
OEFFIS_STATE_NEW,
|
||||
OEFFIS_STATE_CREATE_SESSION,
|
||||
OEFFIS_STATE_SESSION_CREATED,
|
||||
OEFFIS_STATE_STARTED,
|
||||
OEFFIS_STATE_CONNECTED_TO_EIS,
|
||||
OEFFIS_STATE_DISCONNECTED, /* used for closed as well since
|
||||
internally it's the same thing */
|
||||
};
|
||||
|
||||
struct oeffis {
|
||||
struct object object;
|
||||
void *user_data;
|
||||
struct sink *sink;
|
||||
|
||||
enum oeffis_state state;
|
||||
uint32_t devices;
|
||||
|
||||
/* We can have a maximum of 3 events (connected, optional closed,
|
||||
* disconnected) so we can have the event queue be a fixed
|
||||
* null-terminated array, have a pointer to the current element and
|
||||
* shift that on with every event */
|
||||
enum oeffis_event_type event_queue[4];
|
||||
/* Points to the next client-visible event in the event queue. only
|
||||
* advanced by the client */
|
||||
enum oeffis_event_type *next_event;
|
||||
|
||||
int eis_fd;
|
||||
|
||||
/* NULL until OEFFIS_STATE_DISCONNECTED */
|
||||
char *error_message;
|
||||
|
||||
/* internal epollfd tickler */
|
||||
struct source *epoll_tickler_source;
|
||||
int pipefd[2];
|
||||
|
||||
/* sd-bus pieces */
|
||||
struct source *bus_source;
|
||||
sd_bus *bus;
|
||||
sd_bus_slot *slot_request_response; /* Re-used for Request.Response */
|
||||
sd_bus_slot *slot_session_closed;
|
||||
char *busname;
|
||||
char *session_path;
|
||||
char *sender_name;
|
||||
};
|
||||
|
||||
static void
|
||||
oeffis_destroy(struct oeffis *oeffis)
|
||||
{
|
||||
free(oeffis->error_message);
|
||||
sink_unref(oeffis->sink);
|
||||
xclose(oeffis->eis_fd);
|
||||
xclose(oeffis->pipefd[0]);
|
||||
xclose(oeffis->pipefd[1]);
|
||||
|
||||
free(oeffis->sender_name);
|
||||
free(oeffis->session_path);
|
||||
free(oeffis->busname);
|
||||
sd_bus_close_unref(oeffis->bus);
|
||||
sd_bus_slot_unref(oeffis->slot_request_response);
|
||||
sd_bus_slot_unref(oeffis->slot_session_closed);
|
||||
}
|
||||
|
||||
static
|
||||
OBJECT_IMPLEMENT_CREATE(oeffis);
|
||||
_public_
|
||||
OBJECT_IMPLEMENT_REF(oeffis);
|
||||
_public_
|
||||
OBJECT_IMPLEMENT_UNREF_CLEANUP(oeffis);
|
||||
_public_
|
||||
OBJECT_IMPLEMENT_SETTER(oeffis, user_data, void *);
|
||||
_public_
|
||||
OBJECT_IMPLEMENT_GETTER(oeffis, user_data, void *);
|
||||
_public_
|
||||
OBJECT_IMPLEMENT_GETTER(oeffis, error_message, const char *);
|
||||
|
||||
DEFINE_UNREF_CLEANUP_FUNC(source);
|
||||
|
||||
static void
|
||||
tickle(struct oeffis *oeffis)
|
||||
{
|
||||
write(oeffis->pipefd[1], "kitzel", 6);
|
||||
}
|
||||
|
||||
static void
|
||||
_printf_(2, 3)
|
||||
oeffis_disconnect(struct oeffis *oeffis, const char *fmt, ...)
|
||||
{
|
||||
if (oeffis->state == OEFFIS_STATE_DISCONNECTED)
|
||||
return;
|
||||
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
oeffis->state = OEFFIS_STATE_DISCONNECTED;
|
||||
oeffis->error_message = xvasprintf(fmt, args);
|
||||
va_end(args);
|
||||
|
||||
*oeffis->next_event = OEFFIS_EVENT_DISCONNECTED;
|
||||
|
||||
oeffis->eis_fd = xclose(oeffis->eis_fd);
|
||||
|
||||
tickle(oeffis);
|
||||
|
||||
/* FIXME: need to so more here? */
|
||||
}
|
||||
|
||||
static void
|
||||
tickled(struct source *source, void *data)
|
||||
{
|
||||
/* Nothing to do here, just drain the data */
|
||||
char buf[64];
|
||||
read(source_get_fd(source), buf, sizeof(buf));
|
||||
}
|
||||
|
||||
_public_ struct oeffis *
|
||||
oeffis_new(void *user_data)
|
||||
{
|
||||
_unref_(oeffis) *oeffis = oeffis_create(NULL);
|
||||
|
||||
oeffis->state = OEFFIS_STATE_NEW;
|
||||
oeffis->user_data = user_data;
|
||||
oeffis->next_event = oeffis->event_queue;
|
||||
oeffis->eis_fd = -1;
|
||||
oeffis->pipefd[0] = -1;
|
||||
oeffis->pipefd[1] = -1;
|
||||
|
||||
oeffis->sink = sink_new();
|
||||
if (!oeffis->sink)
|
||||
return NULL;
|
||||
|
||||
/* set up a pipe we can write to to force the epoll to wake up even when
|
||||
* nothing else happens */
|
||||
int rc = pipe2(oeffis->pipefd, O_CLOEXEC|O_NONBLOCK);
|
||||
if (rc < 0)
|
||||
return NULL;
|
||||
|
||||
_unref_(source) *s = source_new(oeffis->pipefd[0], tickled, NULL);
|
||||
sink_add_source(oeffis->sink, s);
|
||||
|
||||
return steal(&oeffis);
|
||||
}
|
||||
|
||||
_public_ int
|
||||
oeffis_get_fd(struct oeffis *oeffis)
|
||||
{
|
||||
return sink_get_fd(oeffis->sink);
|
||||
}
|
||||
|
||||
_public_ int
|
||||
oeffis_get_eis_fd(struct oeffis *oeffis)
|
||||
{
|
||||
if (oeffis->state != OEFFIS_STATE_CONNECTED_TO_EIS) {
|
||||
errno = ENODEV;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return dup(oeffis->eis_fd);
|
||||
}
|
||||
|
||||
_public_ enum oeffis_event_type
|
||||
oeffis_get_event(struct oeffis *oeffis)
|
||||
{
|
||||
enum oeffis_event_type e = *oeffis->next_event;
|
||||
|
||||
if (e != OEFFIS_EVENT_NONE)
|
||||
oeffis->next_event++;
|
||||
|
||||
assert(oeffis->next_event < oeffis->event_queue + ARRAY_LENGTH(oeffis->event_queue));
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
_public_ void
|
||||
oeffis_create_session(struct oeffis *oeffis, uint32_t devices)
|
||||
{
|
||||
oeffis_create_session_on_bus(oeffis, "org.freedesktop.portal.Desktop", devices);
|
||||
}
|
||||
|
||||
_public_ void
|
||||
oeffis_create_session_on_bus(struct oeffis *oeffis, const char *busname, uint32_t devices)
|
||||
{
|
||||
if (oeffis->state != OEFFIS_STATE_NEW)
|
||||
return;
|
||||
|
||||
oeffis->devices = devices;
|
||||
oeffis->state = OEFFIS_STATE_CREATE_SESSION;
|
||||
portal_init(oeffis, busname);
|
||||
}
|
||||
|
||||
_public_ void
|
||||
oeffis_dispatch(struct oeffis *oeffis)
|
||||
{
|
||||
sink_dispatch(oeffis->sink);
|
||||
}
|
||||
|
||||
static int
|
||||
oeffis_set_eis_fd(struct oeffis *oeffis, int eisfd)
|
||||
{
|
||||
if (oeffis->state != OEFFIS_STATE_STARTED)
|
||||
return -EALREADY;
|
||||
|
||||
oeffis->state = OEFFIS_STATE_CONNECTED_TO_EIS;
|
||||
oeffis->eis_fd = eisfd;
|
||||
*oeffis->next_event = OEFFIS_EVENT_CONNECTED_TO_EIS;
|
||||
|
||||
tickle(oeffis);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
oeffis_close(struct oeffis *oeffis)
|
||||
{
|
||||
switch (oeffis->state) {
|
||||
case OEFFIS_STATE_NEW:
|
||||
oeffis_disconnect(oeffis, "Bug: Received Session.Close in state NEW.");
|
||||
break;
|
||||
case OEFFIS_STATE_CREATE_SESSION:
|
||||
case OEFFIS_STATE_SESSION_CREATED:
|
||||
case OEFFIS_STATE_CONNECTED_TO_EIS:
|
||||
case OEFFIS_STATE_STARTED:
|
||||
*oeffis->next_event = OEFFIS_EVENT_CLOSED;
|
||||
tickle(oeffis);
|
||||
oeffis->state = OEFFIS_STATE_DISCONNECTED;
|
||||
break;
|
||||
case OEFFIS_STATE_DISCONNECTED:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/********************************************** DBus implementation **************************************************/
|
||||
|
||||
static char *
|
||||
sender_name(sd_bus *bus)
|
||||
{
|
||||
_cleanup_free_ char *sender = NULL;
|
||||
const char *name = NULL;
|
||||
|
||||
if (sd_bus_get_unique_name(bus, &name) != 0)
|
||||
return NULL;
|
||||
|
||||
sender = xstrdup(name + 1); /* drop initial : */
|
||||
|
||||
for (unsigned i = 0; sender[i]; i++) {
|
||||
if (sender[i] == '.')
|
||||
sender[i] = '_';
|
||||
}
|
||||
|
||||
return steal(&sender);
|
||||
}
|
||||
static char *
|
||||
xdp_token(void)
|
||||
{
|
||||
/* next for easier debugging, rand() so we don't ever conflict in
|
||||
* real life situations */
|
||||
static uint32_t next = 0;
|
||||
return xaprintf("oeffis_%u_%d", next++, rand());
|
||||
}
|
||||
|
||||
static char *
|
||||
xdp_request_path(char *sender_name, char *token)
|
||||
{
|
||||
return xaprintf("/org/freedesktop/portal/desktop/request/%s/%s", sender_name, token);
|
||||
}
|
||||
|
||||
static char *
|
||||
xdp_session_path(char *sender_name, char *token)
|
||||
{
|
||||
return xaprintf("/org/freedesktop/portal/desktop/session/%s/%s", sender_name, token);
|
||||
}
|
||||
|
||||
static void
|
||||
portal_connect_to_eis(struct oeffis *oeffis)
|
||||
{
|
||||
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
|
||||
_unref_(sd_bus_message) *response = NULL;
|
||||
sd_bus *bus = oeffis->bus;
|
||||
int eisfd;
|
||||
|
||||
int rc = sd_bus_call_method(bus, oeffis->busname,
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"ConnectToEIS",
|
||||
&error,
|
||||
&response,
|
||||
"oa{sv}",
|
||||
oeffis->session_path,
|
||||
0);
|
||||
|
||||
if (rc < 0) {
|
||||
oeffis_disconnect(oeffis, "Failed to call ConnectToEIS: %s", strerror(-rc));
|
||||
return;
|
||||
}
|
||||
|
||||
rc = sd_bus_message_read(response, "h", &eisfd);
|
||||
if (rc < 0) {
|
||||
oeffis_disconnect(oeffis, "Unable to get fd from portal: %s", strerror(-rc));
|
||||
return;
|
||||
}
|
||||
|
||||
/* the fd is owned by the message */
|
||||
rc = xerrno(dup(eisfd));
|
||||
if (rc < 0) {
|
||||
oeffis_disconnect(oeffis, "Failed to dup fd: %s", strerror(-rc));
|
||||
return;
|
||||
} else {
|
||||
eisfd = rc;
|
||||
int flags = fcntl(eisfd, F_GETFL, 0);
|
||||
fcntl(eisfd, F_SETFL, flags | O_NONBLOCK);
|
||||
}
|
||||
|
||||
log_debug("Got fd %d from portal\n", eisfd);
|
||||
|
||||
rc = oeffis_set_eis_fd(oeffis, eisfd);
|
||||
if (rc < 0)
|
||||
oeffis_disconnect(oeffis, "Failed to set the fd: %s", strerror(-rc));
|
||||
}
|
||||
|
||||
static int
|
||||
session_closed_received(sd_bus_message *m, void *userdata, sd_bus_error *error)
|
||||
{
|
||||
struct oeffis *oeffis = userdata;
|
||||
|
||||
oeffis_close(oeffis);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
dbus_dispatch(struct source *source, void *data)
|
||||
{
|
||||
struct oeffis *oeffis = data;
|
||||
sd_bus *bus = oeffis->bus;
|
||||
|
||||
int rc;
|
||||
do {
|
||||
rc = sd_bus_process(bus, NULL);
|
||||
} while (rc > 0);
|
||||
|
||||
if (rc < 0)
|
||||
oeffis_disconnect(oeffis, "dbus processing failed with %s", strerror(-rc));
|
||||
}
|
||||
|
||||
static int
|
||||
portal_setup_request(struct oeffis *oeffis, sd_bus_message_handler_t response_handler,
|
||||
char **token_return, sd_bus_slot **slot_return)
|
||||
{
|
||||
sd_bus *bus = oeffis->bus;
|
||||
_unref_(sd_bus_slot) *slot = NULL;
|
||||
_cleanup_free_ char *token = xdp_token();
|
||||
_cleanup_free_ char *handle = xdp_request_path(oeffis->sender_name, token);
|
||||
|
||||
int rc = sd_bus_match_signal(bus, &slot,
|
||||
oeffis->busname,
|
||||
handle,
|
||||
"org.freedesktop.portal.Request",
|
||||
"Response",
|
||||
response_handler,
|
||||
oeffis);
|
||||
if (rc < 0) {
|
||||
oeffis_disconnect(oeffis, "Failed to subscribe to Request.Response signal: %s", strerror(-rc));
|
||||
return rc;
|
||||
}
|
||||
|
||||
*token_return = steal(&token);
|
||||
*slot_return = steal(&slot);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
portal_start_response_received(sd_bus_message *m, void *userdata, sd_bus_error *error)
|
||||
{
|
||||
struct oeffis *oeffis = userdata;
|
||||
|
||||
/* We'll only get this signal once */
|
||||
oeffis->slot_request_response = sd_bus_slot_unref(oeffis->slot_request_response);
|
||||
|
||||
unsigned int response;
|
||||
int rc = sd_bus_message_read(m, "u", &response);
|
||||
if (rc < 0) {
|
||||
oeffis_disconnect(oeffis, "Failed to read response from signal: %s", strerror(-rc));
|
||||
return 0;
|
||||
}
|
||||
|
||||
log_debug("Portal Start reponse is %u\n", response);
|
||||
if (response != 0) {
|
||||
oeffis_disconnect(oeffis, "Portal denied Start");
|
||||
return 0;
|
||||
}
|
||||
|
||||
oeffis->state = OEFFIS_STATE_STARTED;
|
||||
|
||||
/* Response includes the the device bitmask but we don't care about this here */
|
||||
|
||||
/* Don't need a separate state here, ConnectToEIS is synchronous */
|
||||
portal_connect_to_eis(oeffis);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
portal_start(struct oeffis *oeffis)
|
||||
{
|
||||
_cleanup_free_ char *token = NULL;
|
||||
_unref_(sd_bus_slot) *request_slot = NULL;
|
||||
|
||||
int rc = portal_setup_request(oeffis, portal_start_response_received, &token, &request_slot);
|
||||
if (rc < 0)
|
||||
return;
|
||||
|
||||
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
|
||||
_unref_(sd_bus_message) *response = NULL;
|
||||
sd_bus *bus = oeffis->bus;
|
||||
rc = sd_bus_call_method(bus,
|
||||
oeffis->busname,
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"Start",
|
||||
&error,
|
||||
&response,
|
||||
"osa{sv}",
|
||||
oeffis->session_path,
|
||||
"", /* parent window */
|
||||
1,
|
||||
"handle_token", /* string key */
|
||||
"s", token /* variant string */
|
||||
);
|
||||
|
||||
if (rc < 0) {
|
||||
oeffis_disconnect(oeffis, "Failed to call method: %s", strerror(-rc));
|
||||
return;
|
||||
}
|
||||
|
||||
const char *path = NULL;
|
||||
rc = sd_bus_message_read(response, "o", &path);
|
||||
if (rc < 0) {
|
||||
oeffis_disconnect(oeffis, "Failed to parse Start reply: %s", strerror(-rc));
|
||||
return;
|
||||
}
|
||||
|
||||
oeffis->slot_request_response = sd_bus_slot_ref(request_slot);
|
||||
return;
|
||||
}
|
||||
|
||||
static int
|
||||
portal_select_devices_response_received(sd_bus_message *m, void *userdata, sd_bus_error *error)
|
||||
{
|
||||
struct oeffis *oeffis = userdata;
|
||||
|
||||
/* We'll only get this signal once */
|
||||
oeffis->slot_request_response = sd_bus_slot_unref(oeffis->slot_request_response);
|
||||
|
||||
unsigned int response;
|
||||
int rc = sd_bus_message_read(m, "u", &response);
|
||||
if (rc < 0) {
|
||||
oeffis_disconnect(oeffis, "Failed to read response from signal: %s", strerror(-rc));
|
||||
return 0;
|
||||
}
|
||||
|
||||
log_debug("Portal SelectDevices reponse is %u\n", response);
|
||||
if (response != 0) {
|
||||
oeffis_disconnect(oeffis, "Portal denied SelectDevices");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Response includes the the device bitmask but we don't care about this here */
|
||||
portal_start(oeffis);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
portal_select_devices(struct oeffis *oeffis)
|
||||
{
|
||||
sd_bus *bus = oeffis->bus;
|
||||
|
||||
_cleanup_free_ char *token = NULL;
|
||||
_unref_(sd_bus_slot) *request_slot = NULL;
|
||||
int rc = portal_setup_request(oeffis, portal_select_devices_response_received, &token, &request_slot);
|
||||
if (rc < 0)
|
||||
return;
|
||||
|
||||
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
|
||||
_unref_(sd_bus_message) *response = NULL;
|
||||
rc = sd_bus_call_method(bus,
|
||||
oeffis->busname,
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"SelectDevices",
|
||||
&error,
|
||||
&response,
|
||||
"oa{sv}",
|
||||
oeffis->session_path,
|
||||
2,
|
||||
"handle_token", /* string key */
|
||||
"s", token, /* variant string */
|
||||
"types", /* string key */
|
||||
"u", oeffis->devices
|
||||
);
|
||||
|
||||
if (rc < 0) {
|
||||
oeffis_disconnect(oeffis, "Failed to call method: %s", strerror(-rc));
|
||||
return;
|
||||
}
|
||||
|
||||
const char *path = NULL;
|
||||
rc = sd_bus_message_read(response, "o", &path);
|
||||
if (rc < 0) {
|
||||
oeffis_disconnect(oeffis, "Failed to parse Start reply: %s", strerror(-rc));
|
||||
return;
|
||||
}
|
||||
|
||||
oeffis->slot_request_response = sd_bus_slot_ref(request_slot);
|
||||
}
|
||||
|
||||
static int
|
||||
portal_create_session_response_received(sd_bus_message *m, void *userdata, sd_bus_error *error)
|
||||
{
|
||||
struct oeffis *oeffis = userdata;
|
||||
|
||||
/* We'll only get this signal once */
|
||||
oeffis->slot_request_response = sd_bus_slot_unref(oeffis->slot_request_response);
|
||||
|
||||
unsigned int response;
|
||||
int rc = sd_bus_message_read(m, "u", &response);
|
||||
if (rc < 0) {
|
||||
oeffis_disconnect(oeffis, "Failed to read response from signal: %s", strerror(-rc));
|
||||
return 0;
|
||||
}
|
||||
|
||||
log_debug("Portal CreateSession reponse is %u\n", response);
|
||||
|
||||
const char *session_handle = NULL;
|
||||
if (response == 0) {
|
||||
const char *key;
|
||||
rc = sd_bus_message_read(m, "a{sv}", 1, &key, "s", &session_handle);
|
||||
if (rc < 0) {
|
||||
oeffis_disconnect(oeffis, "Failed to read session handle from signal: %s", strerror(-rc));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!streq(key, "session_handle")) {
|
||||
oeffis_disconnect(oeffis, "Invalid or unhandled option: %s", key);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (response != 0) {
|
||||
oeffis_disconnect(oeffis, "Portal denied CreateSession");
|
||||
return 0;
|
||||
}
|
||||
|
||||
oeffis->session_path = xstrdup(session_handle);
|
||||
oeffis->state = OEFFIS_STATE_SESSION_CREATED;
|
||||
|
||||
portal_select_devices(oeffis);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
portal_init(struct oeffis *oeffis, const char *busname)
|
||||
{
|
||||
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
|
||||
_unref_(sd_bus) *bus = NULL;
|
||||
_unref_(sd_bus_message) *response = NULL;
|
||||
const char *path = NULL;
|
||||
|
||||
int rc = sd_bus_open_user(&bus);
|
||||
if (rc < 0) {
|
||||
oeffis_disconnect(oeffis, "Failed to init dbus: %s", strerror(-rc));
|
||||
return;
|
||||
}
|
||||
|
||||
oeffis->sender_name = sender_name(bus);
|
||||
if (!oeffis->sender_name) {
|
||||
oeffis_disconnect(oeffis, "Failed to parse sender name");
|
||||
return;
|
||||
}
|
||||
|
||||
oeffis->bus = sd_bus_ref(bus);
|
||||
oeffis->busname = xstrdup(busname);
|
||||
|
||||
uint32_t version;
|
||||
rc = sd_bus_get_property_trivial(bus, busname,
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"version",
|
||||
&error,
|
||||
'u',
|
||||
&version);
|
||||
if (rc < 0) {
|
||||
oeffis_disconnect(oeffis, "Failed to get RemoteDesktop.version: %s", strerror(sd_bus_error_get_errno(&error)));
|
||||
return;
|
||||
} else if (version < VERSION_V(2)) {
|
||||
oeffis_disconnect(oeffis, "RemoteDesktop.version is %u, we need 2", version);
|
||||
return;
|
||||
}
|
||||
log_debug("RemoteDesktop.version is %u\n", version);
|
||||
|
||||
_cleanup_free_ char *token = NULL;
|
||||
_unref_(sd_bus_slot) *request_slot = NULL;
|
||||
rc = portal_setup_request(oeffis, portal_create_session_response_received, &token, &request_slot);
|
||||
if (rc < 0)
|
||||
return;
|
||||
|
||||
_unref_(sd_bus_slot) *session_slot = NULL;
|
||||
_cleanup_free_ char *session_token = xdp_token();
|
||||
_cleanup_free_ char *session_handle = xdp_session_path(oeffis->sender_name, session_token);
|
||||
rc = sd_bus_match_signal(bus, &session_slot,
|
||||
busname,
|
||||
session_handle,
|
||||
"org.freedesktop.portal.Session",
|
||||
"Closed",
|
||||
session_closed_received,
|
||||
oeffis);
|
||||
if (rc < 0) {
|
||||
oeffis_disconnect(oeffis, "Failed to subscribe to Session.Closed signal: %s", strerror(-rc));
|
||||
return;
|
||||
}
|
||||
|
||||
rc = sd_bus_call_method(bus,
|
||||
busname,
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.RemoteDesktop",
|
||||
"CreateSession",
|
||||
&error,
|
||||
&response,
|
||||
"a{sv}", 2,
|
||||
"handle_token", /* string key */
|
||||
"s", token, /* variant string */
|
||||
"session_handle_token", /* string key */
|
||||
"s", session_token /* variant string */
|
||||
);
|
||||
|
||||
if (rc < 0) {
|
||||
oeffis_disconnect(oeffis, "Failed to call method: %s", strerror(-rc));
|
||||
return;
|
||||
}
|
||||
|
||||
rc = sd_bus_message_read(response, "o", &path);
|
||||
if (rc < 0) {
|
||||
oeffis_disconnect(oeffis, "Failed to parse CreateSession reply: %s", strerror(-rc));
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug("Portal Response object is %s\n", path);
|
||||
|
||||
_unref_(source) *s = source_new(sd_bus_get_fd(bus), dbus_dispatch, oeffis);
|
||||
source_never_close_fd(s); /* the bus object handles the fd */
|
||||
rc = sink_add_source(oeffis->sink, s);
|
||||
if (rc == 0) {
|
||||
oeffis->bus_source = source_ref(s);
|
||||
oeffis->slot_request_response = sd_bus_slot_ref(request_slot);
|
||||
oeffis->slot_session_closed = sd_bus_slot_ref(session_slot);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef _enable_tests_
|
||||
#include "util-munit.h"
|
||||
|
||||
MUNIT_TEST(test_init_unref)
|
||||
{
|
||||
struct oeffis *oeffis = oeffis_new(NULL);
|
||||
|
||||
munit_assert_int(oeffis->state, ==, OEFFIS_STATE_NEW);
|
||||
munit_assert_not_null(oeffis->sink);
|
||||
munit_assert_int(oeffis->eis_fd, ==, -1);
|
||||
|
||||
struct oeffis *refd = oeffis_ref(oeffis);
|
||||
munit_assert_ptr_equal(oeffis, refd);
|
||||
munit_assert_int(oeffis->object.refcount, ==, 2);
|
||||
|
||||
struct oeffis *unrefd = oeffis_unref(oeffis);
|
||||
munit_assert_null(unrefd);
|
||||
|
||||
unrefd = oeffis_unref(oeffis);
|
||||
munit_assert_null(unrefd);
|
||||
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
MUNIT_TEST(test_failed_connect)
|
||||
{
|
||||
struct oeffis *oeffis = oeffis_new(NULL);
|
||||
enum oeffis_event_type event;
|
||||
|
||||
oeffis_create_session_on_bus(oeffis, "foo.bar.Example", 0);
|
||||
while ((event = oeffis_get_event(oeffis)) == OEFFIS_EVENT_NONE)
|
||||
oeffis_dispatch(oeffis);
|
||||
munit_assert_int(event, ==, OEFFIS_EVENT_DISCONNECTED);
|
||||
munit_assert_int(oeffis->state, ==, OEFFIS_STATE_DISCONNECTED);
|
||||
munit_assert_not_null(oeffis->error_message);
|
||||
|
||||
oeffis_unref(oeffis);
|
||||
|
||||
return MUNIT_OK;
|
||||
}
|
||||
#endif
|
||||
239
src/liboeffis.h
Normal file
239
src/liboeffis.h
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
/* SPDX-License-Identifier: MIT */
|
||||
/*
|
||||
* Copyright © 2022 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/**
|
||||
* @addtogroup oeffis liboeffis - An XDG RemoteDesktop portal wrapper API
|
||||
*
|
||||
* liboeffis is a helper library for applications that do not want to or cannot
|
||||
* interact with the XDG RemoteDesktop DBus portal directly.
|
||||
*
|
||||
* liboeffis will:
|
||||
* - connect to the DBus session bus and the ``org.freedesktop.portal.Desktop`` bus name
|
||||
* - Start a ``org.freedesktop.portal.RemoteDesktop`` session, select the devices and invoke
|
||||
* ``RemoteDesktop.ConnectToEIS()``
|
||||
* - Close everything in case of error or disconnection
|
||||
*
|
||||
* liboeffis is intentionally kept simple, any more complex needs should be
|
||||
* handled by an application talking to DBus directly.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* @struct oeffis
|
||||
*
|
||||
* The main context to interact with liboeffis. A liboeffis context is a single
|
||||
* connection to DBus session bus and must only be used once.
|
||||
*
|
||||
* @note A caller must keep the oeffis context alive after a successful
|
||||
* connection. Destroying the context results in the DBus connection being
|
||||
* closed and that again results in our EIS fd being invalidated by the
|
||||
* portal and/or the EIS implementation.
|
||||
*
|
||||
* An @ref oeffis context is refcounted, see oeffis_unref().
|
||||
*/
|
||||
struct oeffis;
|
||||
|
||||
/**
|
||||
* Create a new oeffis context. The context is refcounted and must be
|
||||
* released with oeffis_unref().
|
||||
*/
|
||||
struct oeffis *
|
||||
oeffis_new(void *user_data);
|
||||
|
||||
/**
|
||||
* Increase the refcount of this struct by one. Use oeffis_unref() to decrease
|
||||
* the refcount.
|
||||
*
|
||||
* @return the argument passed into the function
|
||||
*/
|
||||
struct oeffis *
|
||||
oeffis_ref(struct oeffis *oeffis);
|
||||
|
||||
/**
|
||||
* Decrease the refcount of this struct by one. When the refcount reaches
|
||||
* zero, the context disconnects from DBus and all allocated resources
|
||||
* are released.
|
||||
*
|
||||
* @note A caller must keep the oeffis context alive after a successful
|
||||
* connection. Destroying the context results in the DBus connection being
|
||||
* closed and that again results in our EIS fd being invalidated by the
|
||||
* portal and/or the EIS implementation.
|
||||
*
|
||||
* @return always NULL
|
||||
*/
|
||||
struct oeffis *
|
||||
oeffis_unref(struct oeffis *oeffis);
|
||||
|
||||
/**
|
||||
* Set a custom data pointer for this context. liboeffis will not look at or
|
||||
* modify the pointer. Use oeffis_get_user_data() to retrieve a previously set
|
||||
* user data.
|
||||
*/
|
||||
void
|
||||
oeffis_set_user_data(struct oeffis *oeffis, void *user_data);
|
||||
|
||||
/**
|
||||
* Return the custom data pointer for this context. liboeffis will not look at or
|
||||
* modify the pointer. Use oeffis_set_user_data() to change the user data.
|
||||
*/
|
||||
void *
|
||||
oeffis_get_user_data(struct oeffis *oeffis);
|
||||
|
||||
/**
|
||||
* liboeffis keeps a single file descriptor for all events. This fd should be
|
||||
* monitored for events by the caller's mainloop, e.g. using select(). When
|
||||
* events are available on this fd, call oeffis_dispatch() immediately to
|
||||
* process the events.
|
||||
*/
|
||||
int
|
||||
oeffis_get_fd(struct oeffis *oeffis);
|
||||
|
||||
/**
|
||||
* Get a `dup()` of the file descriptor. This function should only be called
|
||||
* after an event of type @ref OEFFIS_EVENT_CONNECTED_TO_EIS. Otherwise,
|
||||
* this function returns -1 and errno is set to the `dup()` error.
|
||||
* If this function is called when liboeffis is not connected to EIS, the errno
|
||||
* is set to `ENODEV`.
|
||||
*
|
||||
* Repeated calls to this functions will return additional duplicated file
|
||||
* descriptors. There is no need for a well-written application to call this
|
||||
* function more than once.
|
||||
*
|
||||
* The caller is responsible for closing the returned fd.
|
||||
*
|
||||
* @return The EIS fd or -1 on failure or before the fd was retrieved.
|
||||
*/
|
||||
int
|
||||
oeffis_get_eis_fd(struct oeffis *oeffis);
|
||||
|
||||
/**
|
||||
* The bitmask of devices to request. This bitmask matches the devices
|
||||
* bitmask in the XDG RemoteDesktop portal.
|
||||
*/
|
||||
enum oeffis_device {
|
||||
OEFFIS_DEVICE_ALL_DEVICES = 0,
|
||||
OEFFIS_DEVICE_KEYBOARD = (1 << 0),
|
||||
OEFFIS_DEVICE_POINTER = (1 << 1),
|
||||
OEFFIS_DEVICE_TOUCHSCREEN = (1 << 2),
|
||||
};
|
||||
|
||||
/**
|
||||
* Connect this oeffis instance to a RemoteDesktop session with the given device
|
||||
* mask selected.
|
||||
*
|
||||
* This initiates the DBus communication, starts a RemoteDesktop session and
|
||||
* selects the devices. On success, the @ref
|
||||
* OEFFIS_EVENT_CONNECTED_TO_EIS event is created and the EIS fd can be
|
||||
* retrieved with oeffis_get_eis_fd().
|
||||
*
|
||||
* Any failure in the above process or any other DBus communication error
|
||||
* once connected, including caller bugs, result in the oeffis context being
|
||||
* disconnected and an @ref OEFFIS_EVENT_DISCONNECTED event. Once
|
||||
* disconnected, the context should be released with oeffis_unref().
|
||||
* An @ref OEFFIS_EVENT_DISCONNECTED indicates a communication error and
|
||||
* oeffis_get_error_message() is set with an appropriate error message.
|
||||
*
|
||||
* If the RemoteDesktop session is closed by the
|
||||
* compositor, an @ref OEFFIS_EVENT_CLOSED event is created and the context
|
||||
* should be released with oeffis_unref(). Unlike a disconnection, an @ref
|
||||
* OEFFIS_EVENT_CLOSED event signals intentional closure by the portal. For
|
||||
* example, this may happen as a result of user interaction to terminate the
|
||||
* RemoteDesktop session.
|
||||
*
|
||||
* @note A caller must keep the oeffis context alive after a successful
|
||||
* connection. Destroying the context results in the DBus connection being
|
||||
* closed and that again results in our EIS fd being invalidated by the
|
||||
* portal and/or the EIS implementation.
|
||||
*
|
||||
* @warning Due to the asynchronous nature of DBus and libei, it is not
|
||||
* guaranteed that an event of type @ref OEFFIS_EVENT_DISCONNECTED or @ref
|
||||
* OEFFIS_EVENT_CLOSED is received before the EIS fd becomes invalid.
|
||||
*
|
||||
* @param oeffis A new oeffis context
|
||||
* @param devices A bitmask of @ref oeffis_device
|
||||
*/
|
||||
void
|
||||
oeffis_create_session(struct oeffis *oeffis, uint32_t devices);
|
||||
|
||||
/**
|
||||
* See oeffis_create_session() but this function allows to specify the busname to connect to.
|
||||
* This function should only be used for testing.
|
||||
*/
|
||||
void
|
||||
oeffis_create_session_on_bus(struct oeffis *oeffis, const char *busname, uint32_t devices);
|
||||
|
||||
enum oeffis_event_type {
|
||||
OEFFIS_EVENT_NONE = 0, /**< No event currently available */
|
||||
OEFFIS_EVENT_CONNECTED_TO_EIS, /**< The RemoteDesktop session was created and an eis fd is available */
|
||||
OEFFIS_EVENT_CLOSED, /**< The session was closed by the compositor or portal */
|
||||
OEFFIS_EVENT_DISCONNECTED, /**< We were disconnected from the Bus due to an error */
|
||||
};
|
||||
|
||||
/**
|
||||
* Process pending events. This function must be called immediately after
|
||||
* the file descriptor returned by oeffis_get_fd() signals data is
|
||||
* available.
|
||||
*
|
||||
* After oeffis_dispatch() completes, zero or more events may be available
|
||||
* by oeffis_get_event().
|
||||
*/
|
||||
void
|
||||
oeffis_dispatch(struct oeffis *oeffis);
|
||||
|
||||
/**
|
||||
* Return the next available event, if any. If no event is currently
|
||||
* available, @ref OEFFIS_EVENT_NONE is returned.
|
||||
*
|
||||
* Calling oeffis_dispatch() does not guarantee events are available to the
|
||||
* caller. A single call oeffis_dispatch() may cause more than one event to
|
||||
* be available.
|
||||
*/
|
||||
enum oeffis_event_type
|
||||
oeffis_get_event(struct oeffis *oeffis);
|
||||
|
||||
/**
|
||||
* If the session was @ref OEFFIS_EVENT_DISCONNECTED, return the error message
|
||||
* that caused the disconnection. The returned string is owned by the oeffis context.
|
||||
*/
|
||||
const char *
|
||||
oeffis_get_error_message(struct oeffis *oeffis);
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
124
test/templates/__init__.py
Normal file
124
test/templates/__init__.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
# SPDX-License-Identifier: MIT
|
||||
#
|
||||
#
|
||||
# This file is formatted with Python Black
|
||||
|
||||
from dbusmock import DBusMockObject
|
||||
from typing import Dict, Any, NamedTuple, Optional
|
||||
from itertools import count
|
||||
from gi.repository import GLib # type: ignore
|
||||
|
||||
import dbus
|
||||
import dbus.service
|
||||
import logging
|
||||
|
||||
|
||||
ASVType = Dict[str, Any]
|
||||
|
||||
logging.basicConfig(format="%(levelname).1s|%(name)s: %(message)s", level=logging.DEBUG)
|
||||
logger = logging.getLogger("templates")
|
||||
|
||||
|
||||
class MockParams:
|
||||
"""
|
||||
Helper class for storing template parameters. The Mock object passed into
|
||||
``load()`` is shared between all templates. This makes it easy to have
|
||||
per-template parameters by calling:
|
||||
|
||||
>>> params = MockParams.get(mock, MAIN_IFACE)
|
||||
>>> params.version = 1
|
||||
|
||||
and later, inside a DBus method:
|
||||
>>> params = MockParams.get(self, MAIN_IFACE)
|
||||
>>> return params.version
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get(cls, mock, interface_name):
|
||||
params = getattr(mock, "params", {})
|
||||
try:
|
||||
return params[interface_name]
|
||||
except KeyError:
|
||||
c = cls()
|
||||
params[interface_name] = c
|
||||
mock.params = params
|
||||
return c
|
||||
|
||||
|
||||
class Response(NamedTuple):
|
||||
response: int
|
||||
results: ASVType
|
||||
|
||||
|
||||
class Request:
|
||||
_token_counter = count()
|
||||
|
||||
def __init__(
|
||||
self, bus_name: dbus.service.BusName, sender: str, options: Optional[ASVType]
|
||||
):
|
||||
options = options or {}
|
||||
sender_token = sender.removeprefix(":").replace(".", "_")
|
||||
handle_token = options.get("handle_token", next(self._token_counter))
|
||||
self.sender = sender
|
||||
self.handle = (
|
||||
f"/org/freedesktop/portal/desktop/request/{sender_token}/{handle_token}"
|
||||
)
|
||||
self.mock = DBusMockObject(
|
||||
bus_name=bus_name,
|
||||
path=self.handle,
|
||||
interface="org.freedesktop.portal.Request",
|
||||
props={},
|
||||
)
|
||||
self.mock.AddMethod("", "Close", "", "", "self.RemoveObject(self.path)")
|
||||
logger.debug(f"Request created at {self.handle}")
|
||||
|
||||
def respond(self, response: Response, delay: int = 0):
|
||||
def respond():
|
||||
logger.debug(f"Request.Response on {self.handle}: {response}")
|
||||
self.mock.EmitSignalDetailed(
|
||||
"",
|
||||
"Response",
|
||||
"ua{sv}",
|
||||
[dbus.UInt32(response.response), response.results],
|
||||
details={"destination": self.sender},
|
||||
)
|
||||
|
||||
if delay > 0:
|
||||
GLib.timeout_add(delay, respond)
|
||||
else:
|
||||
respond()
|
||||
|
||||
|
||||
class Session:
|
||||
_token_counter = count()
|
||||
|
||||
def __init__(
|
||||
self, bus_name: dbus.service.BusName, sender: str, options: Optional[ASVType]
|
||||
):
|
||||
options = options or {}
|
||||
sender_token = sender.removeprefix(":").replace(".", "_")
|
||||
handle_token = options.get("session_handle_token", next(self._token_counter))
|
||||
self.sender = sender
|
||||
self.handle = (
|
||||
f"/org/freedesktop/portal/desktop/session/{sender_token}/{handle_token}"
|
||||
)
|
||||
self.mock = DBusMockObject(
|
||||
bus_name=bus_name,
|
||||
path=self.handle,
|
||||
interface="org.freedesktop.portal.Session",
|
||||
props={},
|
||||
)
|
||||
self.mock.AddMethod("", "Close", "", "", "self.RemoveObject(self.path)")
|
||||
logger.debug(f"Session created at {self.handle}")
|
||||
|
||||
def close(self, details: ASVType, delay: int = 0):
|
||||
def respond():
|
||||
logger.debug(f"Session.Closed on {self.handle}: {details}")
|
||||
self.mock.EmitSignalDetailed(
|
||||
"", "Closed", "a{sv}", [details], destination=self.sender
|
||||
)
|
||||
|
||||
if delay > 0:
|
||||
GLib.timeout_add(delay, respond)
|
||||
else:
|
||||
respond()
|
||||
336
test/templates/remotedesktop.py
Normal file
336
test/templates/remotedesktop.py
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
# SPDX-License-Identifier: MIT
|
||||
#
|
||||
# This file is formatted with Python Black
|
||||
|
||||
from templates import Request, Response, Session, ASVType, MockParams
|
||||
from typing import Dict, List, Tuple, Iterator
|
||||
from itertools import count
|
||||
|
||||
import dbus
|
||||
import dbus.service
|
||||
import enum
|
||||
import logging
|
||||
import socket
|
||||
|
||||
logger = logging.getLogger(f"templates.{__name__}")
|
||||
|
||||
BUS_NAME = "org.freedesktop.portal.Desktop"
|
||||
MAIN_OBJ = "/org/freedesktop/portal/desktop"
|
||||
SYSTEM_BUS = False
|
||||
MAIN_IFACE = "org.freedesktop.portal.RemoteDesktop"
|
||||
|
||||
_restore_tokens = count()
|
||||
|
||||
|
||||
class RDSession(Session):
|
||||
class State(enum.IntEnum):
|
||||
CREATED = enum.auto()
|
||||
SELECTED = enum.auto()
|
||||
STARTED = enum.auto()
|
||||
CONNECTED = enum.auto()
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
try:
|
||||
return self._session_state
|
||||
except AttributeError:
|
||||
self._session_state = RDSession.State.CREATED
|
||||
return self._session_state
|
||||
|
||||
def advance_state(self):
|
||||
if self.state != RDSession.State.CONNECTED:
|
||||
self._session_state += 1
|
||||
|
||||
|
||||
def load(mock, parameters):
|
||||
logger.debug(f"loading {MAIN_IFACE} template")
|
||||
|
||||
params = MockParams.get(mock, MAIN_IFACE)
|
||||
params.delay = 500
|
||||
params.version = parameters.get("version", 2)
|
||||
params.response = parameters.get("response", 0)
|
||||
params.devices = parameters.get("devices", 0b111)
|
||||
params.sessions: Dict[str, RDSession] = {}
|
||||
|
||||
mock.AddProperties(
|
||||
MAIN_IFACE,
|
||||
dbus.Dictionary(
|
||||
{
|
||||
"version": dbus.UInt32(params.version),
|
||||
"AvailableDeviceTypes": dbus.UInt32(
|
||||
parameters.get("device-types", params.devices)
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dbus.service.method(
|
||||
MAIN_IFACE,
|
||||
sender_keyword="sender",
|
||||
in_signature="a{sv}",
|
||||
out_signature="o",
|
||||
)
|
||||
def CreateSession(self, options, sender):
|
||||
try:
|
||||
logger.debug(f"CreateSession: {options}")
|
||||
params = MockParams.get(self, MAIN_IFACE)
|
||||
request = Request(bus_name=self.bus_name, sender=sender, options=options)
|
||||
|
||||
session = RDSession(bus_name=self.bus_name, sender=sender, options=options)
|
||||
params.sessions[session.handle] = session
|
||||
|
||||
response = Response(params.response, {"session_handle": session.handle})
|
||||
|
||||
request.respond(response, delay=params.delay)
|
||||
|
||||
return request.handle
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
|
||||
|
||||
@dbus.service.method(
|
||||
MAIN_IFACE,
|
||||
sender_keyword="sender",
|
||||
in_signature="oa{sv}",
|
||||
out_signature="o",
|
||||
)
|
||||
def SelectDevices(self, session_handle, options, sender):
|
||||
try:
|
||||
logger.debug(f"SelectDevices: {session_handle} {options}")
|
||||
params = MockParams.get(self, MAIN_IFACE)
|
||||
request = Request(bus_name=self.bus_name, sender=sender, options=options)
|
||||
|
||||
try:
|
||||
session = params.sessions[session_handle]
|
||||
if session.state != RDSession.State.CREATED:
|
||||
raise dbus.exceptions.DBusException(f"Session in state {session.state}, expected CREATED",
|
||||
name="org.freedesktop.DBus.Error.AccessDenied")
|
||||
else:
|
||||
resp = params.response
|
||||
if resp == 0:
|
||||
session.advance_state()
|
||||
except KeyError:
|
||||
raise dbus.exceptions.DBusException(
|
||||
"Invalid session",
|
||||
name="org.freedesktop.DBus.Error.AccessDenied")
|
||||
|
||||
response = Response(resp, {})
|
||||
request.respond(response, delay=params.delay)
|
||||
|
||||
return request.handle
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
if isinstance(e, dbus.exceptions.DBusException):
|
||||
raise e
|
||||
|
||||
|
||||
@dbus.service.method(
|
||||
MAIN_IFACE,
|
||||
sender_keyword="sender",
|
||||
in_signature="osa{sv}",
|
||||
out_signature="o",
|
||||
)
|
||||
def Start(self, session_handle, parent_window, options, sender):
|
||||
try:
|
||||
logger.debug(f"Start: {session_handle} {options}")
|
||||
params = MockParams.get(self, MAIN_IFACE)
|
||||
request = Request(bus_name=self.bus_name, sender=sender, options=options)
|
||||
|
||||
results = {
|
||||
"devices": dbus.UInt32(params.devices),
|
||||
}
|
||||
|
||||
try:
|
||||
session = params.sessions[session_handle]
|
||||
if session.state != RDSession.State.SELECTED:
|
||||
raise dbus.exceptions.DBusException(f"Session in state {session.state}, expected SELECTED",
|
||||
name="org.freedesktop.DBus.Error.AccessDenied")
|
||||
else:
|
||||
resp = params.response
|
||||
if resp == 0:
|
||||
session.advance_state()
|
||||
except KeyError:
|
||||
raise dbus.exceptions.DBusException(
|
||||
"Invalid session",
|
||||
name="org.freedesktop.DBus.Error.AccessDenied")
|
||||
|
||||
response = Response(resp, results)
|
||||
request.respond(response, delay=params.delay)
|
||||
|
||||
return request.handle
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
if isinstance(e, dbus.exceptions.DBusException):
|
||||
raise e
|
||||
|
||||
|
||||
@dbus.service.method(
|
||||
MAIN_IFACE,
|
||||
in_signature="oa{sv}dd",
|
||||
out_signature="",
|
||||
)
|
||||
def NotifyPointerMotion(self, session_handle, options, dx, dy):
|
||||
try:
|
||||
logger.debug(f"NotifyPointerMotion: {session_handle} {options} {dx} {dy}")
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
|
||||
|
||||
@dbus.service.method(
|
||||
MAIN_IFACE,
|
||||
in_signature="oa{sv}udd",
|
||||
out_signature="",
|
||||
)
|
||||
def NotifyPointerMotionAbsolute(self, session_handle, options, stream, x, y):
|
||||
try:
|
||||
logger.debug(
|
||||
f"NotifyPointerMotionAbsolute: {session_handle} {options} {stream} {x} {y}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
|
||||
|
||||
@dbus.service.method(
|
||||
MAIN_IFACE,
|
||||
in_signature="oa{sv}iu",
|
||||
out_signature="",
|
||||
)
|
||||
def NotifyPointerButton(self, session_handle, options, button, state):
|
||||
try:
|
||||
logger.debug(
|
||||
f"NotifyPointerButton: {session_handle} {options} {button} {state}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
|
||||
|
||||
@dbus.service.method(
|
||||
MAIN_IFACE,
|
||||
in_signature="oa{sv}dd",
|
||||
out_signature="",
|
||||
)
|
||||
def NotifyPointerAxis(self, session_handle, options, dx, dy):
|
||||
try:
|
||||
logger.debug(f"NotifyPointerAxis: {session_handle} {options} {dx} {dx}")
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
|
||||
|
||||
@dbus.service.method(
|
||||
MAIN_IFACE,
|
||||
in_signature="oa{sv}ui",
|
||||
out_signature="",
|
||||
)
|
||||
def NotifyPointerAxisDiscrete(self, session_handle, options, axis, steps):
|
||||
try:
|
||||
logger.debug(
|
||||
f"NotifyPointerAxisDiscrete: {session_handle} {options} {axis} {steps}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
|
||||
|
||||
@dbus.service.method(
|
||||
MAIN_IFACE,
|
||||
in_signature="oa{sv}iu",
|
||||
out_signature="",
|
||||
)
|
||||
def NotifyKeyboardKeycode(self, session_handle, options, keycode, state):
|
||||
try:
|
||||
logger.debug(
|
||||
f"NotifyKeyboardKeycode: {session_handle} {options} {keycode} {state}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
|
||||
|
||||
@dbus.service.method(
|
||||
MAIN_IFACE,
|
||||
in_signature="oa{sv}iu",
|
||||
out_signature="",
|
||||
)
|
||||
def NotifyKeyboardKeysym(self, session_handle, options, keysym, state):
|
||||
try:
|
||||
logger.debug(
|
||||
f"NotifyKeyboardKeysym: {session_handle} {options} {keysym} {state}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
|
||||
|
||||
@dbus.service.method(
|
||||
MAIN_IFACE,
|
||||
in_signature="oa{sv}uudd",
|
||||
out_signature="",
|
||||
)
|
||||
def NotifyTouchDown(self, session_handle, options, stream, slot, x, y):
|
||||
try:
|
||||
logger.debug(
|
||||
f"NotifyTouchDown: {session_handle} {options} {stream} {slot} {x} {y}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
|
||||
|
||||
@dbus.service.method(
|
||||
MAIN_IFACE,
|
||||
in_signature="oa{sv}uudd",
|
||||
out_signature="",
|
||||
)
|
||||
def NotifyTouchMotion(self, session_handle, options, stream, slot, x, y):
|
||||
try:
|
||||
logger.debug(
|
||||
f"NotifyTouchMotion: {session_handle} {options} {stream} {slot} {x} {y}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
|
||||
|
||||
@dbus.service.method(
|
||||
MAIN_IFACE,
|
||||
in_signature="oa{sv}u",
|
||||
out_signature="",
|
||||
)
|
||||
def NotifyTouchUp(self, session_handle, options, slot):
|
||||
try:
|
||||
logger.debug(f"NotifyTouchMotion: {session_handle} {options} {slot}")
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
|
||||
|
||||
@dbus.service.method(
|
||||
MAIN_IFACE,
|
||||
in_signature="oa{sv}",
|
||||
out_signature="h",
|
||||
)
|
||||
def ConnectToEIS(self, session_handle, options):
|
||||
try:
|
||||
logger.debug(f"ConnectToEIS: {session_handle} {options}")
|
||||
|
||||
params = MockParams.get(self, MAIN_IFACE)
|
||||
try:
|
||||
session = params.sessions[session_handle]
|
||||
if session.state != RDSession.State.STARTED:
|
||||
logger.error(f"Session in state {session.state}, expected STARTED")
|
||||
raise dbus.exceptions.DBusException("Session must be started before ConnectToEIS",
|
||||
name="org.freedesktop.DBus.Error.AccesDenied")
|
||||
except KeyError:
|
||||
raise dbus.exceptions.DBusException(
|
||||
"Invalid session",
|
||||
name="org.freedesktop.DBus.Error.AccessDenied")
|
||||
|
||||
import socket
|
||||
|
||||
sockets = socket.socketpair()
|
||||
# Write some random data down so it'll break anything that actually
|
||||
# expects the socket to be a real EIS socket, plus it makes it
|
||||
# easy to check if we connected to the right EIS socket in our tests.
|
||||
sockets[0].send(b"VANILLA")
|
||||
fd = sockets[1]
|
||||
logger.debug(f"ConnectToEIS with fd {fd.fileno()}")
|
||||
return dbus.types.UnixFd(fd)
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
if isinstance(e, dbus.exceptions.DBusException):
|
||||
raise e
|
||||
392
test/test_oeffis.py
Normal file
392
test/test_oeffis.py
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
#
|
||||
# This file is formatted with Python Black
|
||||
#
|
||||
# Introduction
|
||||
# ============
|
||||
#
|
||||
# This is a Python-based test suite making use of DBusMock to test the
|
||||
# liboeffis.so C library.
|
||||
#
|
||||
# The main components are:
|
||||
# - LibOeffis: the Python class wrapping liboeffis.so via ctypes.
|
||||
# This is a manually maintained mapping, any API additions/changes must
|
||||
# updated here.
|
||||
# - Oeffis: a pythonic wrapper around LibOeffis so the tests look like Python
|
||||
# - TestOeffis: the test class for all tests that must talk to DBus
|
||||
#
|
||||
# DBusMock integration
|
||||
# ====================
|
||||
#
|
||||
# DBusMock works in that a **separate process** is started that provides a
|
||||
# DBus session bus that our tests connect to. Templates for dbusmock provide
|
||||
# the behavior of that bus. Note that the mocked bus cannot be controlled
|
||||
# from our test code, it's a separate process. Unless you set up special
|
||||
# DBus signals/methods to talk to it, which we don't.
|
||||
#
|
||||
# Any test that requires DBus looks like this:
|
||||
#
|
||||
# ```python
|
||||
# class TestOeffis():
|
||||
# ....
|
||||
# def test_foo(self):
|
||||
# params = {}
|
||||
# self.setup_daemon(params)
|
||||
# # now you can talk to the RemoteDesktop portal
|
||||
# ```
|
||||
# See the RemoteDesktop template for parameters that can be passed in.
|
||||
#
|
||||
# DBusMock templates
|
||||
# ------------------
|
||||
#
|
||||
# See the templates/ directory for the templates used by DBusMock. Templates
|
||||
# are named after the portal. Available parameters are in the `load()` function.
|
||||
#
|
||||
|
||||
|
||||
from ctypes import c_char_p, c_int, c_uint32, c_void_p
|
||||
from typing import Dict, List, Tuple, Type, Optional, TextIO
|
||||
from gi.repository import GLib # type: ignore
|
||||
from dbus.mainloop.glib import DBusGMainLoop
|
||||
|
||||
import attr
|
||||
import ctypes
|
||||
import dbus
|
||||
import dbusmock
|
||||
import fcntl
|
||||
import os
|
||||
import pytest
|
||||
import socket
|
||||
import subprocess
|
||||
|
||||
DBusGMainLoop(set_as_default=True)
|
||||
|
||||
PREFIX = "oeffis_"
|
||||
|
||||
|
||||
@attr.s
|
||||
class _Api:
|
||||
name: str = attr.ib()
|
||||
args: Tuple[Type[ctypes._SimpleCData], ...] = attr.ib()
|
||||
return_type: Optional[Type[ctypes._SimpleCData]] = attr.ib()
|
||||
|
||||
@property
|
||||
def basename(self) -> str:
|
||||
return self.name[len(PREFIX) :]
|
||||
|
||||
|
||||
@attr.s
|
||||
class _Enum:
|
||||
name: str = attr.ib()
|
||||
value: int = attr.ib()
|
||||
|
||||
@property
|
||||
def basename(self) -> str:
|
||||
return self.name[len(PREFIX) :]
|
||||
|
||||
|
||||
class LibOeffis(object):
|
||||
"""
|
||||
liboeffis.so wrapper. This is a singleton ctypes wrapper into liboeffis.so with
|
||||
minimal processing. Example:
|
||||
|
||||
>>> lib = LibOeffis.instance()
|
||||
>>> ctx = lib.oeffis_new(None)
|
||||
>>> lib.oeffis_unref(ctx)
|
||||
>>> print(lib.OEFFIS_EVENT_CLOSED)
|
||||
|
||||
In most cases you probably want to use the ``Oeffis`` class instead.
|
||||
"""
|
||||
|
||||
_lib = None
|
||||
|
||||
@staticmethod
|
||||
def _cdll():
|
||||
return ctypes.CDLL("liboeffis.so", use_errno=True)
|
||||
|
||||
@classmethod
|
||||
def _load(cls):
|
||||
cls._lib = cls._cdll()
|
||||
for api in cls._api_prototypes:
|
||||
func = getattr(cls._lib, api.name)
|
||||
func.argtypes = api.args
|
||||
func.restype = api.return_type
|
||||
setattr(cls, api.name, func)
|
||||
|
||||
for e in cls._enums:
|
||||
setattr(cls, e.name, e.value)
|
||||
|
||||
_api_prototypes: List[_Api] = [
|
||||
_Api(name="oeffis_new", args=(c_void_p,), return_type=c_void_p),
|
||||
_Api(name="oeffis_ref", args=(c_void_p,), return_type=c_void_p),
|
||||
_Api(name="oeffis_unref", args=(c_void_p,), return_type=c_void_p),
|
||||
_Api(name="oeffis_set_user_data", args=(c_void_p, c_void_p), return_type=None),
|
||||
_Api(name="oeffis_get_user_data", args=(c_void_p,), return_type=c_void_p),
|
||||
_Api(name="oeffis_get_fd", args=(c_void_p,), return_type=c_int),
|
||||
_Api(name="oeffis_get_eis_fd", args=(c_void_p,), return_type=c_int),
|
||||
_Api(name="oeffis_create_session", args=(c_void_p, c_uint32), return_type=None),
|
||||
_Api(
|
||||
name="oeffis_create_session_on_bus",
|
||||
args=(c_void_p, c_char_p, c_uint32),
|
||||
return_type=None,
|
||||
),
|
||||
_Api(name="oeffis_dispatch", args=(c_void_p,), return_type=None),
|
||||
_Api(name="oeffis_get_event", args=(c_void_p,), return_type=c_int),
|
||||
_Api(name="oeffis_get_error_message", args=(c_void_p,), return_type=c_char_p),
|
||||
]
|
||||
|
||||
_enums: List[_Enum] = [
|
||||
_Enum(name="OEFFIS_DEVICE_ALL_DEVICES", value=0),
|
||||
_Enum(name="OEFFIS_DEVICE_KEYBOARD", value=1),
|
||||
_Enum(name="OEFFIS_DEVICE_POINTER", value=2),
|
||||
_Enum(name="OEFFIS_DEVICE_TOUCHSCREEN", value=4),
|
||||
_Enum(name="OEFFIS_EVENT_NONE", value=0),
|
||||
_Enum(name="OEFFIS_EVENT_CONNECTED_TO_EIS", value=1),
|
||||
_Enum(name="OEFFIS_EVENT_CLOSED", value=2),
|
||||
_Enum(name="OEFFIS_EVENT_DISCONNECTED", value=3),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def instance(cls):
|
||||
if cls._lib is None:
|
||||
cls._load()
|
||||
return cls
|
||||
|
||||
|
||||
class Oeffis:
|
||||
"""
|
||||
Convenience wrapper to make using liboeffis a bit more pythonic.
|
||||
|
||||
>>> o = Oeffis()
|
||||
>>> fd = o.fd
|
||||
>>> o.create_session(o.DEVICE_POINTER)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, userdata=None):
|
||||
l = LibOeffis.instance()
|
||||
self.ctx = l.oeffis_new(userdata) # type: ignore
|
||||
|
||||
def wrapper(func):
|
||||
return lambda *args, **kwargs: func(self.ctx, *args, **kwargs)
|
||||
|
||||
for api in l._api_prototypes:
|
||||
# skip some APIs that are not be exposed because they don't make sense
|
||||
# to have in python.
|
||||
if api.name not in (
|
||||
"oeffis_ref",
|
||||
"oeffis_unref",
|
||||
"oeffis_get_user_data",
|
||||
"oeffis_set_user_data",
|
||||
):
|
||||
func = getattr(l, api.name)
|
||||
setattr(self, api.basename, wrapper(func))
|
||||
|
||||
for e in l._enums:
|
||||
val = getattr(l, e.name)
|
||||
setattr(self, e.basename, val)
|
||||
|
||||
@property
|
||||
def fd(self) -> TextIO:
|
||||
"""
|
||||
Return the fd we need to monitor for oeffis_dispatch()
|
||||
"""
|
||||
return os.fdopen(self.get_fd(), "rb") # type: ignore
|
||||
|
||||
@property
|
||||
def eis_fd(self) -> Optional[socket.socket]:
|
||||
"""Return the socket connecting us to the EIS implementation or None if we're not ready/disconnected"""
|
||||
fd = self.get_eis_fd() # type: ignore
|
||||
if fd != -1:
|
||||
return socket.socket(fileno=fd)
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def error_message(self) -> Optional[str]:
|
||||
return self.get_error_message() # type: ignore
|
||||
|
||||
def __del__(self):
|
||||
LibOeffis.instance().oeffis_unref(self.ctx) # type: ignore
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def liboeffis():
|
||||
return LibOeffis.instance()
|
||||
|
||||
|
||||
def test_ref_unref(liboeffis):
|
||||
o = liboeffis.oeffis_new(None)
|
||||
assert o is not None
|
||||
o2 = liboeffis.oeffis_ref(o)
|
||||
assert o2 == o
|
||||
assert liboeffis.oeffis_unref(o) is None
|
||||
assert liboeffis.oeffis_unref(o2) is None
|
||||
assert liboeffis.oeffis_unref(None) is None
|
||||
|
||||
|
||||
def test_set_user_data(liboeffis):
|
||||
o = liboeffis.oeffis_new(None)
|
||||
assert liboeffis.oeffis_get_user_data(o) is None
|
||||
liboeffis.oeffis_unref(o)
|
||||
|
||||
data = ctypes.pointer(ctypes.c_int(52))
|
||||
o = liboeffis.oeffis_new(data)
|
||||
assert o is not None
|
||||
|
||||
|
||||
def test_ctx():
|
||||
oeffis = Oeffis()
|
||||
assert oeffis.error_message is None
|
||||
|
||||
fd = oeffis.fd
|
||||
assert fd is not None
|
||||
|
||||
eisfd = oeffis.eis_fd
|
||||
assert eisfd is None
|
||||
|
||||
|
||||
def test_error_out():
|
||||
# Bus doesn't exist
|
||||
oeffis = Oeffis()
|
||||
oeffis.create_session_on_bus(b"org.freedesktop.OeffisTest", oeffis.DEVICE_POINTER)
|
||||
oeffis.dispatch()
|
||||
e = oeffis.get_event()
|
||||
assert e == oeffis.EVENT_DISCONNECTED
|
||||
assert oeffis.error_message is not None
|
||||
|
||||
|
||||
# Uncomment this to have dbus-monitor listen on the normal session address
|
||||
# rather than the test DBus. This can be useful for cases where *something*
|
||||
# messes up and tests run against the wrong bus.
|
||||
#
|
||||
# session_dbus_address = os.environ["DBUS_SESSION_BUS_ADDRESS"]
|
||||
|
||||
|
||||
def start_dbus_monitor() -> "subprocess.Process":
|
||||
import subprocess
|
||||
|
||||
env = os.environ.copy()
|
||||
try:
|
||||
env["DBUS_SESSION_BUS_ADDRESS"] = session_dbus_address
|
||||
except NameError:
|
||||
# See comment above
|
||||
pass
|
||||
|
||||
argv = ["dbus-monitor", "--session"]
|
||||
mon = subprocess.Popen(argv, env=env)
|
||||
|
||||
def stop_dbus_monitor():
|
||||
mon.terminate()
|
||||
mon.wait()
|
||||
|
||||
GLib.timeout_add(2000, stop_dbus_monitor)
|
||||
return mon
|
||||
|
||||
|
||||
class TestOeffis(dbusmock.DBusTestCase):
|
||||
"""
|
||||
Test class that sets up a mocked DBus session bus to be used by liboeffis.so.
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.PORTAL_NAME = "RemoteDesktop"
|
||||
cls.INTERFACE_NAME = f"org.freedesktop.portal.{cls.PORTAL_NAME}"
|
||||
|
||||
def setUp(self):
|
||||
self.p_mock = None
|
||||
self._mainloop = None
|
||||
self.dbus_monitor = None
|
||||
|
||||
def setup_daemon(self, params=None, extra_templates: List[Tuple[str, Dict]] = []):
|
||||
"""
|
||||
Start a DBusMock daemon in a separate process.
|
||||
|
||||
If extra_templates is specified, it is a list of tuples with the
|
||||
portal name as first value and the param dict to be passed to that
|
||||
template as second value, e.g. ("ScreenCast", {...}).
|
||||
"""
|
||||
self.start_session_bus()
|
||||
self.p_mock, self.obj_portal = self.spawn_server_template(
|
||||
template=f"templates/{self.PORTAL_NAME.lower()}.py",
|
||||
parameters=params or {},
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
|
||||
flags = fcntl.fcntl(self.p_mock.stdout, fcntl.F_GETFL)
|
||||
fcntl.fcntl(self.p_mock.stdout, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
||||
self.mock_interface = dbus.Interface(self.obj_portal, dbusmock.MOCK_IFACE)
|
||||
self.properties_interface = dbus.Interface(
|
||||
self.obj_portal, dbus.PROPERTIES_IFACE
|
||||
)
|
||||
self.portal_interface = dbus.Interface(self.obj_portal, self.INTERFACE_NAME)
|
||||
|
||||
for t, tparams in extra_templates:
|
||||
template = f"templates/{t.lower()}.py"
|
||||
self.obj_portal.AddTemplate(
|
||||
template,
|
||||
dbus.Dictionary(tparams, signature="sv"),
|
||||
dbus_interface=dbusmock.MOCK_IFACE,
|
||||
)
|
||||
|
||||
self.dbus_monitor = start_dbus_monitor()
|
||||
self._mainloop = None
|
||||
|
||||
def tearDown(self):
|
||||
if self.p_mock:
|
||||
if self.p_mock.stdout:
|
||||
out = (self.p_mock.stdout.read() or b"").decode("utf-8")
|
||||
if out:
|
||||
print(out)
|
||||
self.p_mock.stdout.close()
|
||||
self.p_mock.terminate()
|
||||
self.p_mock.wait()
|
||||
|
||||
if self.dbus_monitor:
|
||||
self.dbus_monitor.terminate()
|
||||
self.dbus_monitor.wait()
|
||||
|
||||
@property
|
||||
def mainloop(self):
|
||||
"""
|
||||
The mainloop for this test. This mainloop automatically quits after a
|
||||
fixed timeout, but only on the first run. That's usually enough for
|
||||
tests, if you need to call mainloop.run() repeatedly ensure that a
|
||||
timeout handler is set to ensure quick test case failure in case of
|
||||
error.
|
||||
"""
|
||||
if self._mainloop is None:
|
||||
|
||||
def quit():
|
||||
self._mainloop.quit() # type: ignore
|
||||
self._mainloop = None
|
||||
|
||||
self._mainloop = GLib.MainLoop()
|
||||
GLib.timeout_add(2000, quit)
|
||||
|
||||
return self._mainloop
|
||||
|
||||
def test_create_session(self):
|
||||
self.setup_daemon()
|
||||
|
||||
oeffis = Oeffis()
|
||||
oeffis.create_session(oeffis.DEVICE_POINTER | oeffis.DEVICE_KEYBOARD) # type: ignore
|
||||
oeffis.dispatch() # type: ignore
|
||||
|
||||
def _dispatch(source, condition):
|
||||
oeffis.dispatch() # type: ignore
|
||||
return True
|
||||
|
||||
GLib.io_add_watch(oeffis.fd, 0, GLib.IO_IN, _dispatch)
|
||||
|
||||
self.mainloop.run()
|
||||
|
||||
e = oeffis.get_event() # type: ignore
|
||||
assert e == oeffis.EVENT_CONNECTED_TO_EIS, oeffis.error_message # type: ignore
|
||||
|
||||
eisfd = oeffis.eis_fd
|
||||
assert eisfd is not None
|
||||
|
||||
assert eisfd.recv(64) == b"VANILLA" # that's what the template sends
|
||||
101
tools/oeffis-demo-tool.c
Normal file
101
tools/oeffis-demo-tool.c
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
/* SPDX-License-Identifier: MIT */
|
||||
/*
|
||||
* Copyright © 2020 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.
|
||||
*/
|
||||
|
||||
/* A simple tool that connects to an EIS implementation via the RemoteDesktop
|
||||
* portal using liboeffis
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <signal.h>
|
||||
#include <sys/poll.h>
|
||||
|
||||
#include "src/util-macros.h"
|
||||
#include "src/util-mem.h"
|
||||
#include "src/util-io.h"
|
||||
|
||||
#include "liboeffis.h"
|
||||
|
||||
DEFINE_UNREF_CLEANUP_FUNC(oeffis);
|
||||
|
||||
static bool stop = false;
|
||||
|
||||
static void sighandler(int signal) {
|
||||
stop = true;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
_unref_(oeffis) *oeffis = oeffis_new(NULL);
|
||||
|
||||
signal(SIGINT, sighandler);
|
||||
|
||||
struct pollfd fds = {
|
||||
.fd = oeffis_get_fd(oeffis),
|
||||
.events = POLLIN,
|
||||
.revents = 0,
|
||||
};
|
||||
|
||||
oeffis_create_session(oeffis, OEFFIS_DEVICE_POINTER|OEFFIS_DEVICE_KEYBOARD);
|
||||
|
||||
while (!stop && poll(&fds, 1, 1000) > -1) {
|
||||
oeffis_dispatch(oeffis);
|
||||
|
||||
enum oeffis_event_type e = oeffis_get_event(oeffis);
|
||||
switch (e) {
|
||||
case OEFFIS_EVENT_NONE:
|
||||
break;
|
||||
case OEFFIS_EVENT_CONNECTED_TO_EIS: {
|
||||
_cleanup_close_ int eisfd = oeffis_get_eis_fd(oeffis);
|
||||
printf("We have an eisfd: lucky number %d\n", eisfd);
|
||||
/* Note: unless we get closed/disconnected, we are
|
||||
* started now, or will be once the compositor is
|
||||
* happy with it.
|
||||
*
|
||||
* We still need to keep dispatching events though
|
||||
* to be able to receive the Session.Closed signal
|
||||
* or any disconnections. And we must keep the
|
||||
* oeffis context alive - once the context is
|
||||
* destroyed we disconnect from DBus which will
|
||||
* cause the compositor or the portal to invalidate
|
||||
* our EIS fd.
|
||||
*/
|
||||
break;
|
||||
}
|
||||
case OEFFIS_EVENT_DISCONNECTED:
|
||||
fprintf(stderr, "Disconnected: %s\n", oeffis_get_error_message(oeffis));
|
||||
stop = true;
|
||||
break;
|
||||
case OEFFIS_EVENT_CLOSED:
|
||||
printf("Closing\n");
|
||||
stop = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue