From 3e7a07e48d3c8e32b39c54000e8ebe0d02d47112 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sun, 20 Nov 2016 23:20:14 -0800 Subject: [PATCH] tests: add inject_randr15 (#2573) MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit This tool is similar to xtrace in usage in that it intercepts traffic to the X server. The motivating feature for writing the tool is its ability to inject prepared reply messages instead of the server’s reply. In this particular case, we’ll inject a RRGetMonitors reply to test i3’s RandR 1.5 code paths. The added testcase is a noop for now, but with the code that’s lingering in the randr15 branch, i3 does actually detect monitors as per the injected reply: 2016-11-20 21:10:05 - randr.c:__randr_query_outputs:618 - RandR 1.5 available, querying monitors 2016-11-20 21:10:05 - randr.c:__randr_query_outputs:628 - 1 RandR monitors found (timestamp 0) 2016-11-20 21:10:05 - randr.c:__randr_query_outputs:646 - name DP3, x 0, y 0, width 3840 px, height 2160 px, width 520 mm, height 290 mm, primary 1, automatic 1 This is preparation work for issue #1799 --- Makefile.am | 18 +- testcases/inject_randr1.5.c | 441 ++++++++++++++++++++++++++++++ testcases/lib/SocketActivation.pm | 12 +- testcases/lib/i3test.pm.in | 1 + testcases/t/533-randr15.t | 73 +++++ 5 files changed, 543 insertions(+), 2 deletions(-) create mode 100644 testcases/inject_randr1.5.c create mode 100644 testcases/t/533-randr15.t diff --git a/Makefile.am b/Makefile.am index c90e26c7..aea4256b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -45,7 +45,10 @@ dist_xsessions_DATA = \ noinst_LIBRARIES = libi3.a -check_PROGRAMS = test.commands_parser test.config_parser +check_PROGRAMS = \ + test.commands_parser \ + test.config_parser \ + test.inject_randr15 check_SCRIPTS = \ testcases/complete-run.pl @@ -401,6 +404,19 @@ i3_config_wizard_i3_config_wizard_SOURCES = \ i3-config-wizard/main.c \ i3-config-wizard/xcb.h +test_inject_randr15_CPPFLAGS = \ + $(AM_CPPFLAGS) + +test_inject_randr15_CFLAGS = \ + $(AM_CFLAGS) \ + $(i3_CFLAGS) + +test_inject_randr15_SOURCES = \ + testcases/inject_randr1.5.c + +test_inject_randr15_LDADD = \ + $(i3_LDADD) + test_commands_parser_CPPFLAGS = \ $(AM_CPPFLAGS) \ -DTEST_PARSER diff --git a/testcases/inject_randr1.5.c b/testcases/inject_randr1.5.c new file mode 100644 index 00000000..bd0df399 --- /dev/null +++ b/testcases/inject_randr1.5.c @@ -0,0 +1,441 @@ +/* + * vim:ts=4:sw=4:expandtab + * + * i3 - an improved dynamic tiling window manager + * © 2009 Michael Stapelberg and contributors (see also: LICENSE) + * + * inject_randr1.5.c: An X11 proxy which interprets RandR 1.5 GetMonitors + * requests and overwrites their reply with a custom reply. + * + * This tool can be refactored as necessary in order to perform the same + * purpose for other request types. The RandR 1.5 specific portions of the code + * have been marked as such to make such a refactoring easier. + * + */ +#include "all.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void uds_connection_cb(EV_P_ ev_io *w, int revents); +static void read_client_setup_request_cb(EV_P_ ev_io *w, int revents); +static void read_server_setup_reply_cb(EV_P_ ev_io *w, int revents); +static void read_client_x11_packet_cb(EV_P_ ev_io *w, int revents); +static void read_server_x11_packet_cb(EV_P_ ev_io *w, int revents); + +static char *sun_path = NULL; + +void cleanup_socket(void) { + if (sun_path != NULL) { + unlink(sun_path); + free(sun_path); + sun_path = NULL; + } +} + +/* BEGIN RandR 1.5 specific */ +static void *injected_reply = NULL; +static off_t injected_reply_len = 0; +/* END RandR 1.5 specific */ + +#define XCB_PAD(i) (-(i)&3) + +struct connstate { + /* clientw is a libev watcher for the connection which we accept()ed. */ + ev_io *clientw; + + /* serverw is a libev watcher for the connection to X11 which we initiated + * on behalf of the client. */ + ev_io *serverw; + + /* sequence is the client-side sequence number counter. In X11’s wire + * encoding, sequence counters are not included in requests, only in + * replies. */ + int sequence; + + /* BEGIN RandR 1.5 specific */ + /* sequence number of the most recent GetExtension request for RANDR */ + int getext_randr; + /* sequence number of the most recent RRGetMonitors request */ + int getmonitors; + + int randr_major_opcode; + /* END RandR 1.5 specific */ +}; + +/* + * Returns 0 on EOF + * Returns -1 on error (with errno from read() untouched) + * + */ +static size_t readall_into(void *buffer, const size_t len, int fd) { + size_t read_bytes = 0; + while (read_bytes < len) { + ssize_t n = read(fd, buffer + read_bytes, len - read_bytes); + if (n <= 0) { + return n; + } + read_bytes += (size_t)n; + } + return read_bytes; +} + +/* + * Exits the program with an error if the read failed. + * + */ +static void must_read(int n) { + if (n == -1) { + err(EXIT_FAILURE, "read()"); + } + if (n == 0) { + errx(EXIT_FAILURE, "EOF"); + } +} + +/* + * Exits the program with an error if the write failed. + * + */ +static void must_write(int n) { + if (n == -1) { + err(EXIT_FAILURE, "write()"); + } +} + +static void uds_connection_cb(EV_P_ ev_io *w, int revents) { + struct sockaddr_un addr; + socklen_t addrlen = sizeof(addr); + const int clientfd = accept(w->fd, (struct sockaddr *)&addr, &addrlen); + if (clientfd == -1) { + if (errno == EINTR) { + return; + } + err(EXIT_FAILURE, "accept()"); + } + + struct connstate *connstate = scalloc(1, sizeof(struct connstate)); + + ev_io *clientw = scalloc(1, sizeof(ev_io)); + connstate->clientw = clientw; + clientw->data = connstate; + ev_io_init(clientw, read_client_setup_request_cb, clientfd, EV_READ); + ev_io_start(EV_A_ clientw); +} + +// https://www.x.org/releases/current/doc/xproto/x11protocol.html#Encoding::Connection_Setup +static void read_client_setup_request_cb(EV_P_ ev_io *w, int revents) { + ev_io_stop(EV_A_ w); + struct connstate *connstate = (struct connstate *)w->data; + + /* Read X11 setup request in its entirety. */ + xcb_setup_request_t setup_request; + must_read(readall_into(&setup_request, sizeof(setup_request), w->fd)); + + /* Establish a connection to X11. */ + int fd = socket(AF_LOCAL, SOCK_STREAM, 0); + if (fd == -1) { + err(EXIT_FAILURE, "socket()"); + } + + char *host; + int displayp; + if (xcb_parse_display(getenv("DISPLAY"), &host, &displayp, NULL) == 0) { + errx(EXIT_FAILURE, "Could not parse DISPLAY=%s", getenv("DISPLAY")); + } + free(host); + + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_LOCAL; + snprintf(addr.sun_path, sizeof(addr.sun_path), "/tmp/.X11-unix/X%d", displayp); + if (connect(fd, (const struct sockaddr *)&addr, sizeof(struct sockaddr_un)) == -1) { + err(EXIT_FAILURE, "connect(%s)", addr.sun_path); + } + + /* Relay setup request. */ + must_write(writeall(fd, &setup_request, sizeof(setup_request))); + + if (setup_request.authorization_protocol_name_len > 0 || + setup_request.authorization_protocol_data_len > 0) { + const size_t authlen = setup_request.authorization_protocol_name_len + + XCB_PAD(setup_request.authorization_protocol_name_len) + + setup_request.authorization_protocol_data_len + + XCB_PAD(setup_request.authorization_protocol_data_len); + void *buf = smalloc(authlen); + must_read(readall_into(buf, authlen, w->fd)); + must_write(writeall(fd, buf, authlen)); + free(buf); + } + + /* Wait for a response from the X11 server. */ + ev_io *serverw = scalloc(1, sizeof(ev_io)); + connstate->serverw = serverw; + serverw->data = connstate; + ev_io_init(serverw, read_server_setup_reply_cb, fd, EV_READ); + ev_io_start(EV_A_ serverw); +} + +static void read_server_setup_reply_cb(EV_P_ ev_io *w, int revents) { + struct connstate *connstate = (struct connstate *)w->data; + xcb_setup_failed_t setup_failed; + must_read(readall_into(&setup_failed, sizeof(setup_failed), w->fd)); + + switch (setup_failed.status) { + case 0: + errx(EXIT_FAILURE, "error authenticating at the X11 server"); + + case 2: + errx(EXIT_FAILURE, "two-factor auth not implemented"); + + case 1: + must_write(writeall(connstate->clientw->fd, &setup_failed, sizeof(xcb_setup_failed_t))); + const size_t len = (setup_failed.length * 4); + void *buf = smalloc(len); + must_read(readall_into(buf, len, w->fd)); + must_write(writeall(connstate->clientw->fd, buf, len)); + free(buf); + + ev_set_cb(connstate->clientw, read_client_x11_packet_cb); + ev_set_cb(connstate->serverw, read_server_x11_packet_cb); + ev_io_start(EV_A_ connstate->clientw); + break; + + default: + errx(EXIT_FAILURE, "X11 protocol error: expected setup_failed.status in [0..2], got %d", setup_failed.status); + } +} + +// https://www.x.org/releases/current/doc/xproto/x11protocol.html#request_format +typedef struct { + uint8_t opcode; + uint8_t pad0; + uint16_t length; +} generic_x11_request_t; + +// https://www.x.org/releases/current/doc/xproto/x11protocol.html#reply_format +typedef struct { + uint8_t code; /* if 1, this is a reply. if 0, this is an error. else, an event */ + uint8_t pad0; + uint16_t sequence; + uint32_t length; +} generic_x11_reply_t; + +static void read_client_x11_packet_cb(EV_P_ ev_io *w, int revents) { + struct connstate *connstate = (struct connstate *)w->data; + + void *request = smalloc(sizeof(generic_x11_request_t)); + must_read(readall_into(request, sizeof(generic_x11_request_t), connstate->clientw->fd)); + const size_t len = (((generic_x11_request_t *)request)->length * 4); + if (len > sizeof(generic_x11_request_t)) { + request = srealloc(request, len); + must_read(readall_into(request + sizeof(generic_x11_request_t), + len - sizeof(generic_x11_request_t), + connstate->clientw->fd)); + } + + // XXX: sequence counter wrapping is not implemented, but should not be + // necessary given that this tool is scoped for test cases. + connstate->sequence++; + + /* BEGIN RandR 1.5 specific */ + const uint8_t opcode = ((generic_x11_request_t *)request)->opcode; + if (opcode == XCB_QUERY_EXTENSION) { + xcb_query_extension_request_t *req = request; + const char *name = request + sizeof(xcb_query_extension_request_t); + if (req->name_len == strlen("RANDR") && + strncmp(name, "RANDR", strlen("RANDR")) == 0) { + connstate->getext_randr = connstate->sequence; + } + } else if (opcode == connstate->randr_major_opcode) { + const uint8_t randr_opcode = ((generic_x11_request_t *)request)->pad0; + if (randr_opcode == XCB_RANDR_GET_MONITORS) { + connstate->getmonitors = connstate->sequence; + } + } + /* END RandR 1.5 specific */ + + must_write(writeall(connstate->serverw->fd, request, len)); + free(request); +} + +static void read_server_x11_packet_cb(EV_P_ ev_io *w, int revents) { + struct connstate *connstate = (struct connstate *)w->data; + // all packets from the server are at least 32 bytes in length + size_t len = 32; + void *packet = smalloc(len); + must_read(readall_into(packet, len, connstate->serverw->fd)); + switch (((generic_x11_reply_t *)packet)->code) { + case 0: // error + break; + + case 1: // reply + len += ((generic_x11_reply_t *)packet)->length * 4; + if (len > 32) { + packet = srealloc(packet, len); + must_read(readall_into(packet + 32, len - 32, connstate->serverw->fd)); + } + + /* BEGIN RandR 1.5 specific */ + const uint16_t sequence = ((generic_x11_reply_t *)packet)->sequence; + + if (sequence == connstate->getext_randr) { + xcb_query_extension_reply_t *reply = packet; + connstate->randr_major_opcode = reply->major_opcode; + } + + if (sequence == connstate->getmonitors) { + printf("RRGetMonitors reply!\n"); + xcb_randr_get_monitors_reply_t *reply = packet; + if (injected_reply != NULL) { + printf("injecting reply\n"); + ((generic_x11_reply_t *)injected_reply)->sequence = sequence; + must_write(writeall(connstate->clientw->fd, injected_reply, injected_reply_len)); + free(packet); + return; + } + } + /* END RandR 1.5 specific */ + + break; + + default: // event + break; + } + must_write(writeall(connstate->clientw->fd, packet, len)); + free(packet); +} + +static void child_cb(EV_P_ ev_child *w, int revents) { + ev_child_stop(EV_A_ w); + if (WIFEXITED(w->rstatus)) { + exit(WEXITSTATUS(w->rstatus)); + } else { + exit(WTERMSIG(w->rstatus) + 128); + } +} + +static void must_read_reply(const char *filename) { + FILE *f; + if ((f = fopen(filename, "r")) == NULL) { + err(EXIT_FAILURE, "fopen(%s)", filename); + } + struct stat stbuf; + if (fstat(fileno(f), &stbuf) != 0) { + err(EXIT_FAILURE, "fstat(%s)", filename); + } + /* BEGIN RandR 1.5 specific */ + injected_reply_len = stbuf.st_size; + injected_reply = smalloc(stbuf.st_size); + int n = fread(injected_reply, 1, stbuf.st_size, f); + /* END RandR 1.5 specific */ + if (n != stbuf.st_size) { + err(EXIT_FAILURE, "fread(%s)", filename); + } + fclose(f); +} + +int main(int argc, char *argv[]) { + static struct option long_options[] = { + {"getmonitors_reply", required_argument, 0, 0}, + {0, 0, 0, 0}, + }; + char *options_string = ""; + int opt; + int option_index = 0; + + while ((opt = getopt_long(argc, argv, options_string, long_options, &option_index)) != -1) { + switch (opt) { + case 0: + if (strcmp(long_options[option_index].name, "getmonitors_reply") == 0) { + must_read_reply(optarg); + } + break; + default: + exit(EXIT_FAILURE); + } + } + + if (optind >= argc) { + errx(EXIT_FAILURE, "syntax: %s [options] \n", argv[0]); + } + + int fd = socket(AF_LOCAL, SOCK_STREAM, 0); + if (fd == -1) { + err(EXIT_FAILURE, "socket(AF_UNIX)"); + } + + if (fcntl(fd, F_SETFD, FD_CLOEXEC)) { + warn("Could not set FD_CLOEXEC"); + } + + struct sockaddr_un addr; + memset(&addr, 0, sizeof(struct sockaddr_un)); + addr.sun_family = AF_UNIX; + int i; + bool bound = false; + for (i = 0; i < 100; i++) { + /* XXX: The path to X11 sockets differs on some platforms (e.g. Trusted + * Solaris, HPUX), but since libxcb doesn’t provide a function to + * generate the path, we’ll just have to hard-code it for now. */ + snprintf(addr.sun_path, sizeof(addr.sun_path), "/tmp/.X11-unix/X%d", i); + + if (bind(fd, (struct sockaddr *)&addr, sizeof(struct sockaddr_un)) == -1) { + warn("bind(%s)", addr.sun_path); + } else { + bound = true; + /* Let the user know bind() was successful, so that they know the + * error messages can be disregarded. */ + fprintf(stderr, "Successfuly bound to %s\n", addr.sun_path); + sun_path = sstrdup(addr.sun_path); + break; + } + } + + if (!bound) { + err(EXIT_FAILURE, "bind()"); + } + + atexit(cleanup_socket); + + /* This program will be started for each testcase which requires it, so we + * expect precisely one connection. */ + if (listen(fd, 1) == -1) { + err(EXIT_FAILURE, "listen()"); + } + + pid_t child = fork(); + if (child == -1) { + err(EXIT_FAILURE, "fork()"); + } + if (child == 0) { + char *display; + sasprintf(&display, ":%d", i); + setenv("DISPLAY", display, 1); + free(display); + + char **child_args = argv + optind; + execvp(child_args[0], child_args); + err(EXIT_FAILURE, "exec()"); + } + + struct ev_loop *loop = ev_default_loop(0); + + ev_child cw; + ev_child_init(&cw, child_cb, child, 0); + ev_child_start(loop, &cw); + + ev_io watcher; + ev_io_init(&watcher, uds_connection_cb, fd, EV_READ); + ev_io_start(loop, &watcher); + + ev_run(loop, 0); +} diff --git a/testcases/lib/SocketActivation.pm b/testcases/lib/SocketActivation.pm index 53dbb3b6..0f307eb3 100644 --- a/testcases/lib/SocketActivation.pm +++ b/testcases/lib/SocketActivation.pm @@ -88,7 +88,10 @@ sub activate_i3 { # the interactive signalhandler to make it crash immediately instead. # Also disable logging to SHM since we redirect the logs anyways. # Force Xinerama because we use Xdmx for multi-monitor tests. - my $i3cmd = q|i3 --shmlog-size=0 --disable-signalhandler --force-xinerama|; + my $i3cmd = q|i3 --shmlog-size=0 --disable-signalhandler|; + if (!defined($args{inject_randr15})) { + $i3cmd .= q| --force-xinerama|; + } if (!$args{validate_config}) { # We only set logging if i3 is actually started, but not if we only # validate the config file. This is to keep logging to a minimum as @@ -139,6 +142,13 @@ sub activate_i3 { 'sh -c "export LISTEN_PID=\$\$; ' . $cmd . '"'; } + if ($args{inject_randr15}) { + # See comment in $args{strace} branch. + $cmd = 'test.inject_randr15 --getmonitors_reply="' . + $args{inject_randr15} . '" -- ' . + 'sh -c "export LISTEN_PID=\$\$; ' . $cmd . '"'; + } + # We need to use the shell due to using output redirections. exec '/bin/sh', '-c', $cmd; diff --git a/testcases/lib/i3test.pm.in b/testcases/lib/i3test.pm.in index f9f6e821..f7e1515d 100644 --- a/testcases/lib/i3test.pm.in +++ b/testcases/lib/i3test.pm.in @@ -861,6 +861,7 @@ sub launch_with_config { cv => $cv, dont_create_temp_dir => $args{dont_create_temp_dir}, validate_config => $args{validate_config}, + inject_randr15 => $args{inject_randr15}, ); # If we called i3 with -C, we wait for it to exit and then return as diff --git a/testcases/t/533-randr15.t b/testcases/t/533-randr15.t new file mode 100644 index 00000000..f520806c --- /dev/null +++ b/testcases/t/533-randr15.t @@ -0,0 +1,73 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# +# Please read the following documents before working on tests: +# • http://build.i3wm.org/docs/testsuite.html +# (or docs/testsuite) +# +# • http://build.i3wm.org/docs/lib-i3test.html +# (alternatively: perldoc ./testcases/lib/i3test.pm) +# +# • http://build.i3wm.org/docs/ipc.html +# (or docs/ipc) +# +# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf +# (unless you are already familiar with Perl) +# +# TODO: Description of this file. +# Ticket: #999 +# Bug still in: 4.13-12-g2ff3d9d +use File::Temp qw(tempfile); +use i3test i3_autostart => 0; + +my $config = < 1); + +# Prepare a RRGetMonitors reply, see A.2.4 in +# https://cgit.freedesktop.org/xorg/proto/randrproto/tree/randrproto.txt +my $reply = pack('cxSLLLLx[LLL]', + 1, # reply + 0, # sequence (will be filled in by inject_randr15) + # 56 = length($reply) + length($monitor1) + # 32 = minimum X11 reply length + (56-32) / 4, # length in words + 0, # timestamp TODO + 1, # nmonitors + 0); # noutputs + +# Manually intern _NET_CURRENT_DESKTOP as $x->atom will not create atoms if +# they are not yet interned. +my $atom_cookie = $x->intern_atom(0, length("DP3"), "DP3"); +my $DP3 = $x->intern_atom_reply($atom_cookie->{sequence})->{atom}; + +# MONITORINFO is defined in A.1.1 in +# https://cgit.freedesktop.org/xorg/proto/randrproto/tree/randrproto.txt +my $monitor1 = pack('LccSssSSLL', + $DP3, # name (ATOM) + 1, # primary + 1, # automatic + 0, # ncrtcs + 0, # x + 0, # y + 3840, # width in pixels + 2160, # height in pixels + 520, # width in millimeters + 290); # height in millimeters + +print $outfh $reply; +print $outfh $monitor1; + +close($outfh); + +my $pid = launch_with_config($config, inject_randr15 => $outname); + +cmd 'nop'; + +exit_gracefully($pid); + +done_testing; -- 2.39.2