err(-1, "Could not set O_NONBLOCK");
}
+/*
+ * Given a message and a message type, create the corresponding header, merge it
+ * with the message and append it to the given client's output buffer.
+ *
+ */
+static void append_payload(ipc_client *client, uint32_t message_type, const char *payload) {
+ const size_t size = strlen(payload);
+ const i3_ipc_header_t header = {
+ .magic = {'i', '3', '-', 'i', 'p', 'c'},
+ .size = size,
+ .type = message_type};
+ const size_t header_size = sizeof(i3_ipc_header_t);
+ const size_t message_size = header_size + size;
+
+ client->buffer = srealloc(client->buffer, client->buffer_size + message_size);
+ memcpy(client->buffer + client->buffer_size, ((void *)&header), header_size);
+ memcpy(client->buffer + client->buffer_size + header_size, payload, size);
+ client->buffer_size += message_size;
+}
+
static void free_ipc_client(ipc_client *client) {
close(client->fd);
- for (int i = 0; i < client->num_events; i++){
+
+ ev_io_stop(main_loop, client->callback);
+ FREE(client->callback);
+ if (client->timeout) {
+ ev_timer_stop(main_loop, client->timeout);
+ FREE(client->timeout);
+ }
+
+ free(client->buffer);
+
+ for (int i = 0; i < client->num_events; i++) {
free(client->events[i]);
}
free(client->events);
free(client);
}
+static void ipc_client_timeout(EV_P_ ev_timer *w, int revents);
+static void ipc_socket_writeable_cb(EV_P_ struct ev_io *w, int revents);
+
+static ev_tstamp kill_timeout = 10.0;
+
+void ipc_set_kill_timeout(ev_tstamp new) {
+ kill_timeout = new;
+}
+
+/*
+ * Try to write the contents of the pending buffer to the client's subscription
+ * socket. Will set, reset or clear the timeout and io callbacks depending on
+ * the result of the write operation.
+ *
+ */
+static void ipc_push_pending(ipc_client *client) {
+ const ssize_t result = writeall_nonblock(client->fd, client->buffer, client->buffer_size);
+ if (result < 0) {
+ return;
+ }
+
+ if ((size_t)result == client->buffer_size) {
+ /* Everything was written successfully: clear the timer and stop the io
+ * callback. */
+ FREE(client->buffer);
+ client->buffer_size = 0;
+ if (client->timeout) {
+ ev_timer_stop(main_loop, client->timeout);
+ FREE(client->timeout);
+ }
+ ev_io_stop(main_loop, client->callback);
+ return;
+ }
+
+ /* Otherwise, make sure that the io callback is enabled and create a new
+ * timer if needed. */
+ ev_io_start(main_loop, client->callback);
+
+ if (!client->timeout) {
+ struct ev_timer *timeout = scalloc(1, sizeof(struct ev_timer));
+ ev_timer_init(timeout, ipc_client_timeout, kill_timeout, 0.);
+ timeout->data = client;
+ client->timeout = timeout;
+ ev_set_priority(timeout, EV_MINPRI);
+ ev_timer_start(main_loop, client->timeout);
+ } else if (result > 0) {
+ /* Keep the old timeout when nothing is written. Otherwise, we would
+ * keep a dead connection by continuously renewing its timeouts. */
+ ev_timer_stop(main_loop, client->timeout);
+ ev_timer_set(client->timeout, kill_timeout, 0.0);
+ ev_timer_start(main_loop, client->timeout);
+ }
+ if (result == 0) {
+ return;
+ }
+
+ /* Shift the buffer to the left and reduce the allocated space. */
+ client->buffer_size -= (size_t)result;
+ memmove(client->buffer, client->buffer + result, client->buffer_size);
+ client->buffer = srealloc(client->buffer, client->buffer_size);
+}
+
/*
* Sends the specified event to all IPC clients which are currently connected
* and subscribed to this kind of event.
if (!interested)
continue;
- ipc_send_message(current->fd, strlen(payload), message_type, (const uint8_t *)payload);
+ const bool push_now = (current->buffer_size == 0);
+ append_payload(current, message_type, payload);
+ if (push_now) {
+ ipc_push_pending(current);
+ }
}
}
FREE(message);
}
+static void ipc_client_timeout(EV_P_ ev_timer *w, int revents) {
+ /* No need to be polite and check for writeability, the other callback would
+ * have been called by now. */
+ ipc_client *client = (ipc_client *)w->data;
+
+ char *cmdline = NULL;
+#if defined(__linux__) && defined(SO_PEERCRED)
+ struct ucred peercred;
+ socklen_t so_len = sizeof(peercred);
+ if (getsockopt(client->fd, SOL_SOCKET, SO_PEERCRED, &peercred, &so_len) != 0) {
+ goto end;
+ }
+ char *exepath;
+ sasprintf(&exepath, "/proc/%d/cmdline", peercred.pid);
+
+ int fd = open(exepath, O_RDONLY);
+ free(exepath);
+ if (fd == -1) {
+ goto end;
+ }
+ char buf[512] = {'\0'}; /* cut off cmdline for the error message. */
+ const ssize_t n = read(fd, buf, sizeof(buf));
+ close(fd);
+ if (n < 0) {
+ goto end;
+ }
+ for (char *walk = buf; walk < buf + n - 1; walk++) {
+ if (*walk == '\0') {
+ *walk = ' ';
+ }
+ }
+ cmdline = buf;
+#endif
+
+end:
+ if (cmdline) {
+ ELOG("client %p with pid %d and cmdline '%s' on fd %d timed out, killing\n", client, peercred.pid, cmdline, client->fd);
+ } else {
+ ELOG("client %p on fd %d timed out, killing\n", client, client->fd);
+ }
+
+ free_ipc_client(client);
+}
+
+static void ipc_socket_writeable_cb(EV_P_ ev_io *w, int revents) {
+ DLOG("fd %d writeable\n", w->fd);
+ ipc_client *client = (ipc_client *)w->data;
+
+ /* If this callback is called then there should be a corresponding active
+ * timer. */
+ assert(client->timeout != NULL);
+ ipc_push_pending(client);
+}
+
/*
* Handler for activity on the listening socket, meaning that a new client
* has just connected and we should accept() him. Sets up the event handler
ev_io_init(package, ipc_receive_message, client, EV_READ);
ev_io_start(EV_A_ package);
+ ipc_client *new = scalloc(1, sizeof(ipc_client));
+
+ package = scalloc(1, sizeof(struct ev_io));
+ package->data = new;
+ ev_io_init(package, ipc_socket_writeable_cb, client, EV_WRITE);
+
DLOG("IPC: new client connected on fd %d\n", w->fd);
- ipc_client *new = scalloc(1, sizeof(ipc_client));
new->fd = client;
+ new->callback = package;
TAILQ_INSERT_TAIL(&all_clients, new, clients);
}
--- /dev/null
+#!perl
+# vim:ts=4:sw=4:expandtab
+#
+# Please read the following documents before working on tests:
+# • https://build.i3wm.org/docs/testsuite.html
+# (or docs/testsuite)
+#
+# • https://build.i3wm.org/docs/lib-i3test.html
+# (alternatively: perldoc ./testcases/lib/i3test.pm)
+#
+# • https://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)
+#
+# Test that i3 will not hang if a connected client stops reading from its
+# subscription socket and that the client is killed after a delay.
+# Ticket: #2999
+# Bug still in: 4.15-180-g715cea61
+use i3test i3_config => <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+# Set the timeout to 500ms to reduce the duration of this test.
+ipc_kill_timeout 500
+EOT
+
+# Manually connect to i3 so that we can choose to not read events
+use IO::Socket::UNIX;
+my $sock = IO::Socket::UNIX->new(Peer => get_socket_path());
+my $magic = "i3-ipc";
+my $payload = '["workspace"]';
+my $message = $magic . pack("LL", length($payload), 2) . $payload;
+print $sock $message;
+
+# Constantly switch between 2 workspaces to generate events.
+fresh_workspace;
+open_window;
+fresh_workspace;
+open_window;
+
+eval {
+ local $SIG{ALRM} = sub { die "Timeout\n" };
+ # 500 is an arbitrarily large number to make sure that the socket becomes
+ # non-writeable.
+ for (my $i = 0; $i < 500; $i++) {
+ alarm 1;
+ cmd 'workspace back_and_forth';
+ alarm 0;
+ }
+};
+ok(!$@, 'i3 didn\'t hang');
+
+# Wait for connection timeout
+sleep 1;
+
+use IO::Select;
+my $s = IO::Select->new($sock);
+my $reached_eof = 0;
+while ($s->can_read(0.05)) {
+ if (read($sock, my $buffer, 100) == 0) {
+ $reached_eof = 1;
+ last;
+ }
+}
+ok($reached_eof, 'socket connection closed');
+
+close $sock;
+done_testing;