diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 354fd11..2f3f9b6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -232,6 +232,14 @@ build-no-libxkcommon-nodeps@fedora:32: before_script: - dnf remove -y libxkcommon-devel +build-no-portal@fedora:32: + extends: + - .fedora-build@template + before_script: + - dnf remove -y libsystemd-devel + variables: + MESON_ARGS: "-Dportal=false" + valgrind@fedora:32: extends: - .fedora-build@template diff --git a/.gitlab-ci/ci.template b/.gitlab-ci/ci.template index e3e4dcf..a85eb6f 100644 --- a/.gitlab-ci/ci.template +++ b/.gitlab-ci/ci.template @@ -253,6 +253,14 @@ build-no-libxkcommon-nodeps@{{distro.name}}:{{version}}: before_script: - dnf remove -y libxkcommon-devel +build-no-portal@{{distro.name}}:{{version}}: + extends: + - .{{distro.name}}-build@template + before_script: + - dnf remove -y libsystemd-devel + variables: + MESON_ARGS: "-Dportal=false" + valgrind@{{distro.name}}:{{version}}: extends: - .{{distro.name}}-build@template diff --git a/README.md b/README.md index 0e08e83..2282513 100644 --- a/README.md +++ b/README.md @@ -157,13 +157,43 @@ Open questions ### Flatpak integration -Where flatpak portals are in use, `libei` will communicate with -the portal and `libeis` with the portal implementation (e.g. -`xdg-desktop-portal-gdk`). The portal is reponsible for -allowing the client to connect and restrictions on the devices a client may -create. `libeis` will run in a private namespace of the compositor. +Where flatpak portals are in use, `libei` can communicate with +the portal through a custom backend. The above diagram modified for +Flatpak would be: -The portal **may** control suspending/resuming devices (in addition to the +``` + +--------------------+ + | Wayland compositor |_ + +--------------------+ \ + | libinput | libeis | \_wayland______ + +----------+---------+ \ + | [eis-0.socket] \ + /dev/input/ / \\ +-------+------------------+ + | ======>| libei | Wayland client A | + | after +-------+------------------+ + initial| handover / + connection| / initial request + | / dbus[org.freedesktop.portal.EmulatedInput] + +--------------------+ + | xdg-desktop-portal | + +--------------------+ +``` + +The current approach works so that +- the compositor starts an `libeis` socket backend at `$XDG_RUNTIME_DIR/eis-0` +- `xdg-desktop-portal` provides `org.freedesktop.portal.EmulatedInput` +- a client connects to the `xdg-desktop-portal` to request emulated input +- `xdg-desktop-portal` authenticates a client and opens the initial + connection to the `libeis` socket. It restricts the capabilities available + on that socket (e.g. sets the client name based on `app-id` using + `libreis`). +- `xdg-desktop-portal` hands over the file descriptor to the client which + can initialize a `libei` context +- from then on, `libei` and `libeis` talk directly to each other, the portal + has no further influence. + +This describes the **current** implementation. Changes to this approach are +likely, e.g. the portal **may** control suspending/resuming devices (in addition to the server). The UI for this is not yet sorted. ### Authentication diff --git a/meson.build b/meson.build index f25cfc9..d614e3f 100644 --- a/meson.build +++ b/meson.build @@ -50,12 +50,30 @@ src_libei = [ 'src/libei-fd.c', 'src/libei-proto.h', 'src/libei-proto.c', + 'src/libei-stubs.c', proto_headers, ] + +if get_option('portal') + dep_systemd = dependency('libsystemd') + config_h.set10('ENABLE_LIBEI_PORTAL', 1) + src_libei += [ + 'src/libei-portal.c', + ] +else + dep_systemd = dependency('', required: false) +endif + +deps_libei = [ + dep_libutil, + dep_protobuf, + dep_systemd, +] + lib_libei = shared_library('ei', src_libei, - dependencies: [dep_libutil, dep_protobuf], + dependencies: deps_libei, install: true ) install_headers('src/libei.h') @@ -69,6 +87,7 @@ pkgconfig.generate(lib_libei, description: 'Emulated Input client library', version: meson.project_version(), libraries: lib_libei, + variables: [ 'ei_portal=' + get_option('portal').to_string() ], ) src_libeis = [ @@ -136,6 +155,13 @@ executable('ei-demo-client', 'tools/ei-demo-client.c', dependencies: [dep_libutil, dep_libei, dep_libxkbcommon]) +if get_option('portal') + executable('eis-fake-portal', + 'tools/eis-fake-portal.c', + include_directories: 'src', + dependencies: [dep_libutil, dep_systemd]) +endif + # tests subproject('munit') @@ -166,7 +192,7 @@ test('unit-tests-ei', src_libei, include_directories: 'src', c_args: ['-D_enable_tests_'], - dependencies: [dep_unittest, dep_libutil, dep_protobuf])) + dependencies: deps_libei + [dep_unittest])) test('unit-tests-eis', executable('unit-tests-eis', diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..97deb22 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1 @@ +option('portal', type: 'boolean', value: 'true', description: 'Enable/disable org.freedesktop.portal support') diff --git a/src/libei-portal.c b/src/libei-portal.c new file mode 100644 index 0000000..7e8cd69 --- /dev/null +++ b/src/libei-portal.c @@ -0,0 +1,291 @@ +/* + * 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. + */ + +#include "config.h" + +#include +#include +#include +#include + +#include "libei.h" +#include "libei-private.h" + +#include "util-io.h" +#include "util-macros.h" +#include "util-mem.h" +#include "util-object.h" +#include "util-strings.h" + +struct ei_portal { + struct object object; + struct source *bus_source; + sd_bus *bus; + sd_bus_slot *slot; + + char *busname; +}; + +static void +ei_portal_destroy(struct ei_portal *portal) +{ + free(portal->busname); + sd_bus_unref(portal->bus); +} + +OBJECT_IMPLEMENT_CREATE(ei_portal); +static +OBJECT_IMPLEMENT_PARENT(ei_portal, ei); +static +OBJECT_IMPLEMENT_REF(ei_portal); +static +OBJECT_IMPLEMENT_UNREF(ei_portal); +#define _cleanup_ei_portal_ _cleanup_(ei_portal_cleanup) + +static void +interface_portal_destroy(struct ei *ei, void *backend) +{ + struct ei_portal *portal = backend; + ei_portal_unref(portal); +} + +static const struct ei_backend_interface interface = { + .destroy = interface_portal_destroy, +}; + +static char * +xdp_token(sd_bus *bus, char **token_out) +{ + _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] = '_'; + } + + char *token = xaprintf("ei_%d", rand()); + *token_out = token; + + return xaprintf("/org/freedesktop/portal/desktop/request/%s/%s", sender, token); +} + +static void +portal_connect(struct ei_portal *portal) +{ + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *response = NULL; + struct sd_bus *bus = portal->bus; + struct ei *ei = ei_portal_parent(portal); + int eisfd; + + int rc = sd_bus_call_method(bus, portal->busname, + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.EmulatedInput", + "Connect", + &error, + &response, + "a{sv}", 0); + + if (rc < 0) { + log_error(ei, "Failed to call method: %s\n", strerror(-rc)); + goto out; + } + + rc = sd_bus_message_read(response, "h", &eisfd); + if (rc < 0) { + log_error(ei, "Failed to extract fd: %s\n", strerror(-rc)); + goto out; + } + + /* the fd is owned by the message */ + rc = xerrno(dup(eisfd)); + if (rc < 0) { + log_error(ei, "Failed to dup fd: %s\n", strerror(-rc)); + goto out; + } else { + eisfd = rc; + int flags = fcntl(eisfd, F_GETFL, 0); + fcntl(eisfd, F_SETFL, flags | O_NONBLOCK); + } + + log_debug(ei, "Initiating ei context with fd %d from portal\n", eisfd); + + /* We're done with DBus, lets clean up */ + source_remove(portal->bus_source); + source_unref(portal->bus_source); + portal->bus = sd_bus_unref(portal->bus); + + rc = ei_set_connection(ei, eisfd); +out: + if (rc < 0) { + log_error(ei, "Failed to set the connection: %s\n", strerror(-rc)); + ei_disconnect(ei); + } +} + +static int +portal_response_received(sd_bus_message *m, void *userdata, sd_bus_error *error) +{ + struct ei_portal *portal = userdata; + struct ei *ei = ei_portal_parent(portal); + unsigned response; + + assert(m); + assert(portal); + + /* We'll only get this signal once */ + portal->slot = sd_bus_slot_unref(portal->slot); + + int rc = sd_bus_message_read(m, "u", &response); + if (rc < 0) { + log_error(ei, "Failed to read response from signal: %s\n", strerror(-rc)); + ei_disconnect(ei); + return 0; + } + + log_debug(ei, "Portal EmulateInput reponse is %d\n", response); + + if (response != 0) { + ei_disconnect(ei); + return 0; + } + + portal_connect(portal); + + return 0; +} + +static void +dbus_dispatch(struct source *source, void *data) +{ + struct ei_portal *portal = data; + + /* We need to ref the bus here, portal_connect() may remove + * portal->bus but that needs to stay valid here until the end of + * the loop. + */ + _cleanup_(sd_bus_unrefp) struct sd_bus *bus = sd_bus_ref(portal->bus); + + int rc; + do { + rc = sd_bus_process(bus, NULL); + } while (rc > 0); + + if (rc != 0) { + log_error(ei_portal_parent(portal), "dbus processing failed with %s", strerror(-rc)); + } +} + +static int +portal_init(struct ei *ei, const char *busname) +{ + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *response = NULL; + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; + _cleanup_(sd_bus_slot_unrefp) sd_bus_slot *slot = NULL; + _cleanup_ei_portal_ struct ei_portal *portal = ei_portal_create(&ei->object); + const char *path = NULL; + + int rc = sd_bus_open_user(&bus); + if (rc < 0 ) { + log_error(ei, "Failed to init dbus: %s\n", strerror(-rc)); + return -ECONNREFUSED; + } + + _cleanup_free_ char *token = NULL; + _cleanup_free_ char *handle = xdp_token(bus, &token); + + rc = sd_bus_match_signal(bus, &slot, + busname, + handle, + "org.freedesktop.portal.Request", + "Response", + portal_response_received, + portal); + if (rc < 0) { + log_error(ei, "Failed to subscribe to signal: %s\n", strerror(-rc)); + return -ECONNREFUSED; + } + + rc = sd_bus_call_method(bus, + busname, + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.EmulatedInput", + "EmulateInput", + &error, + &response, + "a{sv}", 1, + "handle_token", /* string key */ + "s", token /* variant string */ + ); + + if (rc < 0) { + log_error(ei, "Failed to call method: %s\n", strerror(-rc)); + return -ECONNREFUSED; + } + + rc = sd_bus_message_read(response, "o", &path); + if (rc < 0) { + log_error(ei, "Failed to parse response: %s\n", strerror(-rc)); + return -ECONNREFUSED; + } + + log_debug(ei, "portal Response object is %s\n", path); + + struct source *s = source_new(sd_bus_get_fd(bus), dbus_dispatch, portal); + source_never_close_fd(s); /* the bus object handles the fd */ + rc = sink_add_source(ei->sink, s); + if (rc == 0) { + portal->bus_source = source_ref(s); + portal->bus = sd_bus_ref(bus); + portal->slot = sd_bus_slot_ref(slot); + } + + portal->busname = xstrdup(busname); + + ei->backend = ei_portal_ref(portal); + ei->backend_interface = interface; + + source_unref(s); + + return 0; +} + +_public_ int +ei_setup_backend_portal(struct ei *ei) +{ + return portal_init(ei, "org.freedesktop.portal.Desktop"); +} + +_public_ int +ei_setup_backend_portal_busname(struct ei *ei, const char *busname) +{ + return portal_init(ei, busname); +} diff --git a/src/libei-private.h b/src/libei-private.h index 9996d33..07225df 100644 --- a/src/libei-private.h +++ b/src/libei-private.h @@ -113,6 +113,9 @@ ei_init(struct ei *ei); int ei_set_connection(struct ei *ei, int fd); +void +ei_disconnect(struct ei *ei); + int ei_add_device(struct ei_device *device); diff --git a/src/libei-stubs.c b/src/libei-stubs.c new file mode 100644 index 0000000..09c32c4 --- /dev/null +++ b/src/libei-stubs.c @@ -0,0 +1,38 @@ +/* + * 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. + */ + +#include "config.h" + +#include + +#include "libei.h" + +#include "util-macros.h" + +#if !ENABLE_LIBEI_PORTAL +_public_ int +ei_setup_backend_portal(struct ei *ei) +{ + return -ENOSYS; +} +#endif diff --git a/src/libei.c b/src/libei.c index 0f341b9..68401ff 100644 --- a/src/libei.c +++ b/src/libei.c @@ -66,9 +66,6 @@ OBJECT_IMPLEMENT_GETTER(ei_event, type, enum ei_event_type); _public_ OBJECT_IMPLEMENT_GETTER(ei_event, device, struct ei_device*); -static void -ei_disconnect(struct ei *ei); - static void ei_destroy(struct ei *ei) { @@ -276,7 +273,7 @@ connection_send_key(struct ei *ei, struct ei_device *device, return ei_proto_send_key(ei, device, k, is_press); } -static void +void ei_disconnect(struct ei *ei) { if (ei->state == EI_STATE_DISCONNECTED || diff --git a/src/libei.h b/src/libei.h index 4862b28..bbe0e2c 100644 --- a/src/libei.h +++ b/src/libei.h @@ -238,6 +238,17 @@ ei_unref(struct ei *ei); void ei_set_user_data(struct ei *ei, void *user_data); +/** + * Connect to the org.freedesktop.portal.Desktop portal + * + * @return 0 on success or a negative errno on failure + */ +int +ei_setup_backend_portal(struct ei *ei); + +int +ei_setup_backend_portal_busname(struct ei *ei, const char *busname); + /** * Return the custom data pointer for this context. libei will not look at or * modify the pointer. Use ei_set_user_data() to change the user data. diff --git a/src/util-sources.c b/src/util-sources.c index 7157c84..778660c 100644 --- a/src/util-sources.c +++ b/src/util-sources.c @@ -114,6 +114,12 @@ source_new(int sourcefd, source_dispatch_t dispatch, void *user_data) } +void +source_never_close_fd(struct source *s) +{ + s->close_behavior = SOURCE_CLOSE_FD_NEVER; +} + static void sink_destroy(struct sink *sink) { diff --git a/src/util-sources.h b/src/util-sources.h index aa61c66..65b2610 100644 --- a/src/util-sources.h +++ b/src/util-sources.h @@ -82,6 +82,9 @@ source_set_user_data(struct source *source, void *user_data); struct source * source_new(int fd, source_dispatch_t dispatch, void *user_data); +void +source_never_close_fd(struct source *s); + struct sink * sink_new(void); diff --git a/tools/ei-demo-client.c b/tools/ei-demo-client.c index a79398e..7584db1 100644 --- a/tools/ei-demo-client.c +++ b/tools/ei-demo-client.c @@ -135,7 +135,7 @@ static void usage(FILE *fp, const char *argv0) { fprintf(fp, - "Usage: %s [--verbose] [--socket] [--layout=us]\n" + "Usage: %s [--verbose] [--socket|--portal] [--busname=a.b.c.d] [--layout=us]\n" "\n" "Start an EI demo client. The client will connect to EIS\n" "with the chosen backend (default: socket) and emulate pointer\n" @@ -144,8 +144,11 @@ usage(FILE *fp, const char *argv0) "Options:\n" " --socket Use the socket backend. The socket path is $LIBEI_SOCKET if set, \n" " otherwise XDG_RUNTIME/eis-0\n" + " --portal Use the portal backend.\n" + " --busname Use the given busname (default: org.freedesktop.portal.Desktop)\n" " --layout Use the given XKB layout (requires libxkbcommon). Default: none\n" - " --verbose Enable debugging output\n", + " --verbose Enable debugging output\n" + "", argv0); } @@ -153,21 +156,28 @@ int main(int argc, char **argv) { enum { SOCKET, + PORTAL, } backend = SOCKET; bool verbose = false; const char *layout = NULL; + _cleanup_free_ char *busname = xstrdup("org.freedesktop.portal.Desktop"); + while (1) { enum { OPT_BACKEND_SOCKET, + OPT_BACKEND_PORTAL, + OPT_BUSNAME, OPT_LAYOUT, OPT_VERBOSE, }; static struct option long_opts[] = { {"socket", no_argument, 0, OPT_BACKEND_SOCKET}, - {"layout", required_argument, 0, OPT_LAYOUT}, - {"verbose", no_argument, 0, OPT_VERBOSE}, - {"help", no_argument, 0, 'h'}, + { "portal", no_argument, 0, OPT_BACKEND_PORTAL}, + { "busname", required_argument, 0, OPT_BUSNAME}, + { "layout", required_argument, 0, OPT_LAYOUT}, + { "verbose", no_argument, 0, OPT_VERBOSE}, + { "help", no_argument, 0, 'h'}, {NULL}, }; @@ -186,6 +196,13 @@ int main(int argc, char **argv) case OPT_BACKEND_SOCKET: backend = SOCKET; break; + case OPT_BACKEND_PORTAL: + backend = PORTAL; + break; + case OPT_BUSNAME: + free(busname); + busname = xstrdup(optarg); + break; case OPT_LAYOUT: layout = optarg; break; @@ -209,6 +226,11 @@ int main(int argc, char **argv) const char SOCKETNAME[] = "eis-0"; colorprint("connecting to %s\n", SOCKETNAME); rc = ei_setup_backend_socket(ei, getenv("LIBEI_SOCKET") ? NULL : SOCKETNAME); + } else if (backend == PORTAL) { +#if ENABLE_LIBEI_PORTAL + colorprint("connecting to %s\n", busname); + rc = ei_setup_backend_portal_busname(ei, busname); +#endif } if (rc != 0) { diff --git a/tools/eis-fake-portal.c b/tools/eis-fake-portal.c new file mode 100644 index 0000000..43c47a0 --- /dev/null +++ b/tools/eis-fake-portal.c @@ -0,0 +1,263 @@ +/* + * 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. + */ + +#include "config.h" + +#include +#include +#include + +#include "util-io.h" +#include "util-mem.h" +#include "util-logger.h" +#include "util-strings.h" + +#include + +#define _cleanup_sd_bus_ _cleanup_(sd_bus_unrefp) +#define _cleanup_sd_bus_slot_ _cleanup_(sd_bus_slot_unrefp) +#define _cleanup_sd_event_ _cleanup_(sd_event_unrefp) + +struct portal { + struct logger *logger; + char *busname; +} portal; + + +#define call(_call) do { \ + int _rc = _call; \ + if (_rc < 0) { \ + log_error(portal, "Failed with %s %s:%d\n", strerror(-_rc), __func__, __LINE__); \ + return _rc; \ + } } while(0) + +static int +request_close(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + /* We don't live long enough for this to be called */ + return 0; +} + +static const sd_bus_vtable request_vtable[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_METHOD("Close", "", "", request_close, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_SIGNAL("Response", "ua{sv}", 0), + SD_BUS_VTABLE_END, +}; + +static int +create_request_object(struct portal *portal, + sd_bus *bus, + const char *objectpath) +{ + _cleanup_sd_bus_slot_ sd_bus_slot *slot = NULL; + call(sd_bus_add_object_vtable(bus, &slot, + objectpath, + "org.freedesktop.portal.Request", + request_vtable, + NULL)); + + int response = 0; + log_debug(portal, "emitting Response %d on %s\n", response, objectpath); + return sd_bus_emit_signal(bus, + objectpath, + "org.freedesktop.portal.Request", + "Response", + "ua{sv}", + response, + 0 /* Array size */ + ); + /* note: _cleanup_ removes object immediately */ +} + +static char * +sender_token(const char *input) +{ + if (!input) + return NULL; + + char *token = strstrip(input, ":"); + for (size_t idx = 0; token[idx]; idx++) { + if (token[idx] == '.') + token[idx] = '_'; + } + + return token; +} + +static int +portal_emulate_input(sd_bus_message *m, void *userdata, + sd_bus_error *ret_error) +{ + struct portal *portal = userdata; + + call(sd_bus_message_enter_container(m, 'a', "{sv}")); + + const char *key, *handle_token; + call(sd_bus_message_read(m, "{sv}", &key, "s", &handle_token)); + /* we only have a handle token in the dict so far */ + if (!streq(key, "handle_token")) + return -EINVAL; + call(sd_bus_message_exit_container(m)); + + _cleanup_free_ char *sender = sender_token(sd_bus_message_get_sender(m)); + if (!sender) + return -ENOMEM; + + /* Send back the object path of the object we're about to create. We + * then create the object, so if that fails we have a problem but + * meh, this is for testing only .*/ + _cleanup_free_ char *objpath = xaprintf("%s/request/%s/%s", + "/org/freedesktop/portal/desktop", + sender, + handle_token); + call(sd_bus_reply_method_return(m, "o", objpath)); + + /* now create the object */ + return create_request_object(portal, sd_bus_message_get_bus(m), objpath); +} + +static int +portal_connect(sd_bus_message *m, void *userdata, + sd_bus_error *ret_error) +{ + struct portal *portal = userdata; + + const char *xdg = getenv("XDG_RUNTIME_DIR"); + if (!xdg) + return -ENOENT; + + _cleanup_free_ char *sockpath = xaprintf("%s/eis-0", xdg); + int handle = xconnect(sockpath); + log_debug(portal, "passing Handle %d\n", handle); + return sd_bus_reply_method_return(m, "h", handle); +} + +static const sd_bus_vtable portal_vtable[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_METHOD("EmulateInput", "a{sv}", "o", portal_emulate_input, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("Connect", "a{sv}", "h", portal_connect, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_VTABLE_END, +}; + +static int +run(struct portal *portal) +{ + _cleanup_sd_bus_ sd_bus *bus = NULL; + _cleanup_sd_bus_slot_ sd_bus_slot *slot = NULL; + int rc = sd_bus_open_user(&bus); + if (rc < 0) + return rc; + + rc = sd_bus_add_object_vtable(bus, &slot, + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.EmulatedInput", + portal_vtable, + portal); + if (rc < 0) + return rc; + + log_debug(portal, "Portal object at: /org/freedesktop/portal/desktop\n"); + + rc = sd_bus_request_name(bus, portal->busname, 0); + if (rc < 0) + return rc; + + log_debug(portal, "Portal DBus name: %s\n", portal->busname); + + _cleanup_sd_event_ struct sd_event *event = NULL; + rc = sd_event_default(&event); + if (rc < 0) + return rc; + + rc = sd_event_set_watchdog(event, true); + if (rc < 0) + return rc; + + rc = sd_bus_attach_event(bus, event, 0); + if (rc < 0) + return rc; + + return sd_event_loop(event); +} + +static void +usage(FILE *fp, const char *argv0) +{ + fprintf(fp, + "Usage: %s [--busname=a.b.c.d]\n" + "\n" + "Emulates an XDG Desktop portal for the org.freedesktop.portal.EmulatedInput interface\n" + "\n" + "Options:\n" + " --busname use the given busname instead of the default org.freedesktop.libei.Desktop\n" + "", + basename(argv0)); +} + +int +main(int argc, char **argv) +{ + _cleanup_free_ char *busname = xstrdup("org.freedesktop.portal.Desktop"); + + while (1) { + enum opts { + OPT_BUSNAME, + }; + static struct option long_opts[] = { + { "busname", required_argument, 0, OPT_BUSNAME}, + { "help", no_argument, 0, 'h'}, + { NULL}, + }; + + int optind = 0; + int c = getopt_long(argc, argv, "h", long_opts, &optind); + if (c == -1) + break; + + switch(c) { + case 'h': + usage(stdout, argv[0]); + return EXIT_SUCCESS; + case OPT_BUSNAME: + free(busname); + busname = xstrdup(optarg); + break; + default: + usage(stderr, argv[0]); + return EXIT_FAILURE; + } + } + + portal.busname = steal(&busname); + portal.logger = logger_new("portal", NULL); + logger_set_priority(portal.logger, LOGGER_DEBUG); + + int rc = run(&portal); + if (rc < 0) + fprintf(stderr, "Failed to start fake portal: %s\n", strerror(-rc)); + + logger_unref(portal.logger); + free(portal.busname); + return rc == 0; +}