diff --git a/daemon/Makefile.am b/daemon/Makefile.am index df10ce3..133e956 100644 --- a/daemon/Makefile.am +++ b/daemon/Makefile.am @@ -12,11 +12,14 @@ x52d_SOURCES = \ daemon/x52d_config_dump.c \ daemon/x52d_config.c \ daemon/x52d_device.c \ + daemon/x52d_client.c \ daemon/x52d_clock.c \ daemon/x52d_mouse.c \ + daemon/x52d_notify.c \ daemon/x52d_led.c \ daemon/x52d_command.c \ - daemon/x52d_comm_internal.c + daemon/x52d_comm_internal.c \ + daemon/x52d_comm_client.c x52d_CFLAGS = \ -I $(top_srcdir) \ @@ -83,6 +86,7 @@ EXTRA_DIST += \ daemon/daemon.dox \ daemon/protocol.dox \ daemon/x52d.service.in \ + daemon/x52d_client.h \ daemon/x52d_clock.h \ daemon/x52d_config.def \ daemon/x52d_config.h \ @@ -90,6 +94,7 @@ EXTRA_DIST += \ daemon/x52d_device.h \ daemon/x52d_io.h \ daemon/x52d_mouse.h \ + daemon/x52d_notify.h \ daemon/x52d_command.h \ daemon/x52dcomm.h \ daemon/x52dcomm-internal.h \ diff --git a/daemon/x52ctl.c b/daemon/x52ctl.c index c2b0c60..a90e107 100644 --- a/daemon/x52ctl.c +++ b/daemon/x52ctl.c @@ -46,6 +46,7 @@ string "quit", or terminates input by using Ctrl+D. #include #include #include +#include #include "x52d_const.h" #include "x52dcomm.h" @@ -65,19 +66,13 @@ static int send_command(int sock_fd, int argc, char **argv) int rc; char buffer[1024]; int buflen; - int i; - memset(buffer, 0, sizeof(buffer)); - buflen = 0; - for (i = 0; i < argc; i++) { - int arglen = strlen(argv[i]) + 1; - if ((size_t)(buflen + arglen) >= sizeof(buffer)) { + buflen = x52d_format_command(argc, (const char **)argv, buffer, sizeof(buffer)); + if (buflen < 0) { + if (errno == E2BIG) { fprintf(stderr, _("Argument length too long\n")); - return -1; } - - memcpy(&buffer[buflen], argv[i], arglen); - buflen += arglen; + return -1; } rc = x52d_send_command(sock_fd, buffer, buflen, sizeof(buffer)); diff --git a/daemon/x52d_client.c b/daemon/x52d_client.c new file mode 100644 index 0000000..02f22ef --- /dev/null +++ b/daemon/x52d_client.c @@ -0,0 +1,146 @@ +/* + * Saitek X52 Pro MFD & LED driver - Client handling + * + * Copyright (C) 2022 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#include +#include +#include +#include + +#include "pinelog.h" +#include "x52d_client.h" +#include "x52dcomm-internal.h" + +void x52d_client_init(int client_fd[X52D_MAX_CLIENTS]) +{ + for (int i = 0; i < X52D_MAX_CLIENTS; i++) { + client_fd[i] = INVALID_CLIENT; + } +} + +bool x52d_client_register(int client_fd[X52D_MAX_CLIENTS], int sock_fd) +{ + int fd; + int i; + + fd = accept(sock_fd, NULL, NULL); + if (fd < 0) { + PINELOG_ERROR(_("Error accepting client connection on socket fd %d: %s"), + sock_fd, strerror(errno)); + return false; + } + + if (x52d_set_socket_nonblocking(fd) < 0) { + PINELOG_ERROR(_("Error marking client fd %d as nonblocking: %s"), + fd, strerror(errno)); + goto error; + } + + for (i = 0; i < X52D_MAX_CLIENTS; i++) { + if (client_fd[i] == INVALID_CLIENT) { + PINELOG_TRACE("Accepted client %d on socket %d, slot %d", fd, sock_fd, i); + client_fd[i] = fd; + return true; + } + } + + /* + * At this point, we've looped through the entirity of client_fd, but + * have not registered an empty slot. We need to close the socket and + * tell the caller that we haven't been able to register the client. + */ + PINELOG_TRACE("Maximum connections reached, closing socket %d", fd); +error: + close(fd); + + return false; +} + +bool x52d_client_deregister(int client_fd[X52D_MAX_CLIENTS], int fd) +{ + bool deregistered = false; + + for (int i = 0; i < X52D_MAX_CLIENTS; i++) { + if (client_fd[i] == fd) { + client_fd[i] = INVALID_CLIENT; + deregistered = true; + close(fd); + + PINELOG_TRACE("Disconnected client %d from socket", fd); + break; + } + } + + return deregistered; +} + +bool x52d_client_error(int client_fd[X52D_MAX_CLIENTS], int fd) +{ + int error; + socklen_t errlen = sizeof(error); + + getsockopt(fd, SOL_SOCKET, SO_ERROR, (void *)&error, &errlen); + PINELOG_ERROR(_("Error when polling socket: FD %d, error %d, len %lu"), + fd, error, (unsigned long int)errlen); + return x52d_client_deregister(client_fd, fd); +} + +int x52d_client_poll(int client_fd[X52D_MAX_CLIENTS], struct pollfd pfd[MAX_CONN], int listen_fd) +{ + int pfd_count; + int rc; + + memset(pfd, 0, sizeof(*pfd) * MAX_CONN); + + pfd_count = 1; + pfd[0].fd = listen_fd; + pfd[0].events = POLLIN | POLLERR; + for (int i = 0; i < X52D_MAX_CLIENTS; i++) { + if (client_fd[i] != INVALID_CLIENT) { + pfd[pfd_count].fd = client_fd[i]; + pfd[pfd_count].events = POLLIN | POLLERR | POLLHUP; + pfd_count++; + } + } + + PINELOG_TRACE("Polling %d file descriptors", pfd_count); + +retry_poll: + rc = poll(pfd, pfd_count, -1); + if (rc < 0) { + if (errno == EINTR) { + goto retry_poll; + } + PINELOG_ERROR(_("Error %d when polling %d descriptors: %s"), + errno, pfd_count, strerror(errno)); + } else if (rc == 0) { + PINELOG_INFO(_("Timed out when polling")); + } + + return rc; +} + +void x52d_client_handle(int client_fd[X52D_MAX_CLIENTS], struct pollfd *pfd, int listen_fd, x52d_poll_handler handler) +{ + for (int i = 0; i < MAX_CONN; i++) { + if (pfd[i].revents & POLLHUP) { + /* Remote hungup */ + x52d_client_deregister(client_fd, pfd[i].fd); + } else if (pfd[i].revents & POLLERR) { + /* Error reading from the socket */ + x52d_client_error(client_fd, pfd[i].fd); + } else if (pfd[i].revents & POLLIN) { + if (pfd[i].fd == listen_fd) { + x52d_client_register(client_fd, listen_fd); + } else { + if (handler != NULL) { + handler(pfd[i].fd); + } + } + } + } +} diff --git a/daemon/x52d_client.h b/daemon/x52d_client.h new file mode 100644 index 0000000..96b7b47 --- /dev/null +++ b/daemon/x52d_client.h @@ -0,0 +1,30 @@ +/* + * Saitek X52 Pro MFD & LED driver - Client handling + * + * Copyright (C) 2022 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#ifndef X52D_CLIENT_H +#define X52D_CLIENT_H + +#include +#include + +#include "x52d_const.h" + +#define MAX_CONN (X52D_MAX_CLIENTS + 1) + +#define INVALID_CLIENT -1 + +typedef void (*x52d_poll_handler)(int); + +void x52d_client_init(int client_fd[X52D_MAX_CLIENTS]); +bool x52d_client_register(int client_fd[X52D_MAX_CLIENTS], int sock_fd); +bool x52d_client_deregister(int client_fd[X52D_MAX_CLIENTS], int fd); +bool x52d_client_error(int client_fd[X52D_MAX_CLIENTS], int fd); +int x52d_client_poll(int client_fd[X52D_MAX_CLIENTS], struct pollfd pfd[MAX_CONN], int listen_fd); +void x52d_client_handle(int client_fd[X52D_MAX_CLIENTS], struct pollfd *pfd, int listen_fd, x52d_poll_handler handler); + +#endif //!defined X52D_CLIENT_H diff --git a/daemon/x52d_comm_client.c b/daemon/x52d_comm_client.c index 3c40a73..0a40c18 100644 --- a/daemon/x52d_comm_client.c +++ b/daemon/x52d_comm_client.c @@ -18,19 +18,11 @@ #include "x52dcomm.h" #include "x52dcomm-internal.h" -int x52d_dial_command(const char *sock_path) +static int _setup_socket(struct sockaddr_un *remote, int len) { int sock; - int len; - struct sockaddr_un remote; int saved_errno; - len = x52d_setup_command_sock(sock_path, &remote); - if (len < 0) { - /* Error when setting up sockaddr */ - return -1; - } - /* Create a socket */ sock = socket(AF_UNIX, SOCK_STREAM, 0); if (sock == -1) { @@ -39,7 +31,7 @@ int x52d_dial_command(const char *sock_path) } /* Connect to the socket */ - if (connect(sock, (struct sockaddr *)&remote, (socklen_t)len) == -1) { + if (connect(sock, (struct sockaddr *)remote, (socklen_t)len) == -1) { /* Failure connecting to the socket. Cleanup */ saved_errno = errno; /* close may modify errno, so we save it prior to the call */ @@ -51,6 +43,60 @@ int x52d_dial_command(const char *sock_path) return sock; } +int x52d_dial_command(const char *sock_path) +{ + int len; + struct sockaddr_un remote; + + len = x52d_setup_command_sock(sock_path, &remote); + if (len < 0) { + /* Error when setting up sockaddr */ + return -1; + } + + return _setup_socket(&remote, len); +} + +int x52d_dial_notify(const char *sock_path) +{ + int len; + struct sockaddr_un remote; + + len = x52d_setup_notify_sock(sock_path, &remote); + if (len < 0) { + /* Error when setting up sockaddr */ + return -1; + } + + return _setup_socket(&remote, len); +} + +int x52d_format_command(int argc, const char **argv, char *buffer, size_t buflen) +{ + int msglen; + int i; + + if (argc == 0 || argv == NULL || buffer == NULL || buflen < X52D_BUFSZ) { + errno = EINVAL; + return -1; + } + + memset(buffer, 0, buflen); + msglen = 0; + for (i = 0; i < argc; i++) { + int arglen = strlen(argv[i]) + 1; + if ((size_t)(msglen + arglen) >= buflen) { + errno = E2BIG; + return -1; + } + + memcpy(&buffer[msglen], argv[i], arglen); + msglen += arglen; + } + + return msglen; +} + int x52d_send_command(int sock_fd, char *buffer, size_t bufin, size_t bufout) { int rc; @@ -94,3 +140,38 @@ int x52d_send_command(int sock_fd, char *buffer, size_t bufin, size_t bufout) return rc; } + +int x52d_recv_notification(int sock_fd, x52d_notify_callback_fn callback) +{ + int rc; + char buffer[X52D_BUFSZ]; + int argc; + char *argv[X52D_BUFSZ]; + + if (callback == NULL) { + errno = EINVAL; + return -1; + } + + /* Wait till we get a response */ + for (;;) { + rc = recv(sock_fd, buffer, sizeof(buffer), 0); + if (rc < 0) { + // Error + if (errno == EINTR) { + // System call interrupted due to signal. Try again + continue; + } else { + // Failed. Return early + return -1; + } + } + + break; + } + + /* Split into individual arguments */ + x52d_split_args(&argc, argv, buffer, rc); + + return callback(argc, argv); +} diff --git a/daemon/x52d_comm_internal.c b/daemon/x52d_comm_internal.c index 15ecbc2..8b6c6ca 100644 --- a/daemon/x52d_comm_internal.c +++ b/daemon/x52d_comm_internal.c @@ -10,6 +10,8 @@ #include #include +#include +#include #include "x52dcomm-internal.h" #include "x52d_const.h" @@ -23,7 +25,16 @@ const char * x52d_command_sock_path(const char *sock_path) return sock_path; } -int x52d_setup_command_sock(const char *sock_path, struct sockaddr_un *remote) +const char * x52d_notify_sock_path(const char *sock_path) +{ + if (sock_path == NULL) { + sock_path = X52D_SOCK_NOTIFY; + } + + return sock_path; +} + +static int _setup_sockaddr(struct sockaddr_un *remote, const char *sock_path) { int len; if (remote == NULL) { @@ -31,8 +42,6 @@ int x52d_setup_command_sock(const char *sock_path, struct sockaddr_un *remote) return -1; } - sock_path = x52d_command_sock_path(sock_path); - len = strlen(sock_path); if ((size_t)len >= sizeof(remote->sun_path)) { /* Socket path will not fit inside sun_path */ @@ -49,3 +58,54 @@ int x52d_setup_command_sock(const char *sock_path, struct sockaddr_un *remote) return len; } + +int x52d_setup_command_sock(const char *sock_path, struct sockaddr_un *remote) +{ + return _setup_sockaddr(remote, x52d_command_sock_path(sock_path)); +} + +int x52d_setup_notify_sock(const char *sock_path, struct sockaddr_un *remote) +{ + return _setup_sockaddr(remote, x52d_notify_sock_path(sock_path)); +} + +int x52d_set_socket_nonblocking(int sock_fd) +{ + int flags; + + /* Mark the socket as non-blocking */ + flags = fcntl(sock_fd, F_GETFL); + if (flags < 0) { + goto sock_failure; + } + if (fcntl(sock_fd, F_SETFL, flags | O_NONBLOCK) < 0) { + goto sock_failure; + } + + return 0; + +sock_failure: + close(sock_fd); + return -1; +} + +void x52d_split_args(int *argc, char **argv, char *buffer, int buflen) +{ + int i = 0; + + while (i < buflen) { + if (buffer[i]) { + argv[*argc] = buffer + i; + (*argc)++; + for (; i < buflen && buffer[i]; i++); + // At this point, buffer[i] = '\0' + // Skip to the next character. + i++; + } else { + // We should never reach here, unless we have two NULs in a row + argv[*argc] = buffer + i; + (*argc)++; + i++; + } + } +} diff --git a/daemon/x52d_command.c b/daemon/x52d_command.c index b34cc7e..cea2242 100644 --- a/daemon/x52d_command.c +++ b/daemon/x52d_command.c @@ -24,104 +24,15 @@ #include "x52d_const.h" #include "x52d_command.h" #include "x52d_config.h" +#include "x52d_client.h" #include "x52dcomm-internal.h" -#define MAX_CONN (X52D_MAX_CLIENTS + 1) - -#define INVALID_CLIENT -1 - static int client_fd[X52D_MAX_CLIENTS]; -static int active_clients; static pthread_t command_thr; static int command_sock_fd; static const char *command_sock; -static void register_client(int sock_fd) -{ - int fd; - - if (active_clients >= X52D_MAX_CLIENTS) { - /* Ignore the incoming connection */ - return; - } - - fd = accept(sock_fd, NULL, NULL); - if (fd < 0) { - PINELOG_ERROR(_("Error accepting client connection on command socket: %s"), - strerror(errno)); - return; - } - - PINELOG_TRACE("Accepted client %d on command socket", fd); - - for (int i = 0; i < X52D_MAX_CLIENTS; i++) { - if (client_fd[i] == INVALID_CLIENT) { - client_fd[i] = fd; - active_clients++; - break; - } - } -} - -static void deregister_client(int fd) -{ - for (int i = 0; i < X52D_MAX_CLIENTS; i++) { - if (client_fd[i] == fd) { - client_fd[i] = INVALID_CLIENT; - active_clients--; - close(fd); - - PINELOG_TRACE("Disconnected client %d from command socket", fd); - break; - } - } - -} - -static void client_error(int fd) -{ - int error; - socklen_t errlen = sizeof(error); - - getsockopt(fd, SOL_SOCKET, SO_ERROR, (void *)&error, &errlen); - PINELOG_ERROR(_("Error when polling command socket: FD %d, error %d, len %lu"), - fd, error, (unsigned long int)errlen); - deregister_client(fd); -} - -static int poll_clients(int sock_fd, struct pollfd *pfd) -{ - int pfd_count; - int rc; - - memset(pfd, 0, sizeof(*pfd) * MAX_CONN); - - pfd_count = 1; - pfd[0].fd = sock_fd; - pfd[0].events = POLLIN | POLLERR; - for (int i = 0; i < X52D_MAX_CLIENTS; i++) { - if (client_fd[i] != INVALID_CLIENT) { - pfd[pfd_count].fd = client_fd[i]; - pfd[pfd_count].events = POLLIN | POLLERR | POLLHUP; - pfd_count++; - } - } - - PINELOG_TRACE("Polling %d file descriptors", pfd_count); - rc = poll(pfd, pfd_count, -1); - if (rc < 0) { - if (errno != EINTR) { - PINELOG_ERROR(_("Error when polling for command: %s"), strerror(errno)); - return -1; - } - } else if (rc == 0) { - PINELOG_INFO(_("Timed out when polling for command")); - } - - return rc; -} - #if defined __has_attribute # if __has_attribute(format) __attribute((format(printf, 4, 5))) @@ -131,7 +42,7 @@ static void response_formatted(char *buffer, int *buflen, const char *type, const char *fmt, ...) { va_list ap; - char response[1024]; + char response[X52D_BUFSZ]; int resplen; int typelen; @@ -152,7 +63,7 @@ static void response_formatted(char *buffer, int *buflen, const char *type, static void response_strings(char *buffer, int *buflen, const char *type, int count, ...) { va_list ap; - char response[1024]; + char response[X52D_BUFSZ]; int resplen; int arglen; int i; @@ -351,6 +262,7 @@ static void cmd_logging(char *buffer, int *buflen, int argc, char **argv) [X52D_MOD_LED] = "led", [X52D_MOD_MOUSE] = "mouse", [X52D_MOD_COMMAND] = "command", + [X52D_MOD_CLIENT] = "client", }; // This corresponds to the levels in pinelog @@ -430,24 +342,9 @@ static void cmd_logging(char *buffer, int *buflen, int argc, char **argv) static void command_parser(char *buffer, int *buflen) { int argc = 0; - char *argv[1024] = { 0 }; - int i = 0; + char *argv[X52D_BUFSZ] = { 0 }; - while (i < *buflen) { - if (buffer[i]) { - argv[argc] = buffer + i; - argc++; - for (; i < *buflen && buffer[i]; i++); - // At this point, buffer[i] = '\0' - // Skip to the next character. - i++; - } else { - // We should never reach here, unless we have two NULs in a row - argv[argc] = buffer + i; - argc++; - i++; - } - } + x52d_split_args(&argc, argv, buffer, *buflen); MATCH(0, "config") { cmd_config(buffer, buflen, argc, argv); @@ -458,50 +355,41 @@ static void command_parser(char *buffer, int *buflen) } } +static void client_handler(int fd) +{ + char buffer[X52D_BUFSZ] = { 0 }; + int sent; + int rc; + + rc = recv(fd, buffer, sizeof(buffer), 0); + if (rc < 0) { + PINELOG_ERROR(_("Error reading from client %d: %s"), + fd, strerror(errno)); + return; + } + + // Parse and handle command. + command_parser(buffer, &rc); + + PINELOG_TRACE("Sending %d bytes in response '%s'", rc, buffer); + sent = send(fd, buffer, rc, 0); + if (sent != rc) { + PINELOG_ERROR(_("Short write to client %d; expected %d bytes, wrote %d bytes"), + fd, rc, sent); + } +} + int x52d_command_loop(int sock_fd) { struct pollfd pfd[MAX_CONN]; int rc; - int i; - rc = poll_clients(sock_fd, pfd); + rc = x52d_client_poll(client_fd, pfd, sock_fd); if (rc <= 0) { return -1; } - for (i = 0; i < MAX_CONN; i++) { - if (pfd[i].revents & POLLHUP) { - /* Remote hungup */ - deregister_client(pfd[i].fd); - } else if (pfd[i].revents & POLLERR) { - /* Error reading from the socket */ - client_error(pfd[i].fd); - } else if (pfd[i].revents & POLLIN) { - if (pfd[i].fd == sock_fd) { - register_client(sock_fd); - } else { - char buffer[1024] = { 0 }; - int sent; - - rc = recv(pfd[i].fd, buffer, sizeof(buffer), 0); - if (rc < 0) { - PINELOG_ERROR(_("Error reading from client %d: %s"), - pfd[i].fd, strerror(errno)); - continue; - } - - // Parse and handle command. - command_parser(buffer, &rc); - - PINELOG_TRACE("Sending %d bytes in response '%s'", rc, buffer); - sent = send(pfd[i].fd, buffer, rc, 0); - if (sent != rc) { - PINELOG_ERROR(_("Short write to client %d; expected %d bytes, wrote %d bytes"), - pfd[i].fd, rc, sent); - } - } - } - } + x52d_client_handle(client_fd, pfd, sock_fd, client_handler); return 0; } @@ -525,11 +413,7 @@ int x52d_command_init(const char *sock_path) struct sockaddr_un local; int flags; - for (int i = 0; i < X52D_MAX_CLIENTS; i++) { - client_fd[i] = INVALID_CLIENT; - } - - active_clients = 0; + x52d_client_init(client_fd); command_sock = sock_path; command_sock_fd = -1; diff --git a/daemon/x52d_const.h b/daemon/x52d_const.h index 12e3a28..5a24e6b 100644 --- a/daemon/x52d_const.h +++ b/daemon/x52d_const.h @@ -18,6 +18,7 @@ #define X52D_PID_FILE RUNDIR "/" X52D_APP_NAME ".pid" #define X52D_SOCK_COMMAND RUNDIR "/" X52D_APP_NAME ".cmd" +#define X52D_SOCK_NOTIFY RUNDIR "/" X52D_APP_NAME ".notify" #include "gettext.h" #define N_(x) gettext_noop(x) @@ -33,6 +34,8 @@ enum { X52D_MOD_LED, X52D_MOD_MOUSE, X52D_MOD_COMMAND, + X52D_MOD_CLIENT, + X52D_MOD_NOTIFY, X52D_MOD_MAX }; diff --git a/daemon/x52d_main.c b/daemon/x52d_main.c index ec543ae..a9a0449 100644 --- a/daemon/x52d_main.c +++ b/daemon/x52d_main.c @@ -23,6 +23,7 @@ #include "x52d_io.h" #include "x52d_mouse.h" #include "x52d_command.h" +#include "x52d_notify.h" #include "x52dcomm-internal.h" #include "x52dcomm.h" #include "pinelog.h" @@ -90,7 +91,8 @@ static void usage(int exit_code) _("Usage: %s [-f] [-v] [-q]\n" "\t[-l log-file] [-o override]\n" "\t[-c config-file] [-p pid-file]\n" - "\t[-s command-socket-path]\n"), + "\t[-s command-socket-path]\n" + "\t[-b notify-socket-path]\n"), X52D_APP_NAME); exit(exit_code); } @@ -205,6 +207,7 @@ int main(int argc, char **argv) char *conf_file = NULL; const char *pid_file = NULL; const char *command_sock = NULL; + const char *notify_sock = NULL; int opt; int rc; sigset_t sigblockset; @@ -231,8 +234,9 @@ int main(int argc, char **argv) * -l path to log file * -p path to PID file (only used if running in background) * -s path to command socket + * -b path to notify socket */ - while ((opt = getopt(argc, argv, "fvql:o:c:p:s:h")) != -1) { + while ((opt = getopt(argc, argv, "fvql:o:c:p:s:b:h")) != -1) { switch (opt) { case 'f': foreground = true; @@ -277,6 +281,10 @@ int main(int argc, char **argv) command_sock = optarg; break; + case 'b': + notify_sock = optarg; + break; + case 'h': usage(EXIT_SUCCESS); break; @@ -314,6 +322,7 @@ int main(int argc, char **argv) if (x52d_command_init(command_sock) < 0) { goto cleanup; } + x52d_notify_init(notify_sock); #if defined(HAVE_EVDEV) x52d_io_init(); x52d_mouse_evdev_init(); @@ -355,6 +364,7 @@ cleanup: x52d_clock_exit(); x52d_dev_exit(); x52d_command_exit(); + x52d_notify_exit(); #if defined(HAVE_EVDEV) x52d_mouse_evdev_exit(); x52d_io_exit(); diff --git a/daemon/x52d_notify.c b/daemon/x52d_notify.c new file mode 100644 index 0000000..1ab4433 --- /dev/null +++ b/daemon/x52d_notify.c @@ -0,0 +1,220 @@ +/* + * Saitek X52 Pro MFD & LED driver - Notification manager + * + * Copyright (C) 2022 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#include +#include +#include +#include + +#define PINELOG_MODULE X52D_MOD_NOTIFY +#include "pinelog.h" +#include "x52d_const.h" +#include "x52d_notify.h" +#include "x52d_client.h" +#include "x52dcomm.h" +#include "x52dcomm-internal.h" + +static pthread_t notify_thr; +static pthread_t notify_listen; +static pthread_mutex_t notify_mutex = PTHREAD_MUTEX_INITIALIZER; + +static int notify_pipe[2]; +static int notify_sock; + +static int client_fd[X52D_MAX_CLIENTS]; + +/* Bind and listen to the notify socket */ +static int listen_notify(const char *notify_sock_path) +{ + int sock_fd; + int len; + struct sockaddr_un local; + + len = x52d_setup_notify_sock(notify_sock_path, &local); + if (len < 0) { + return -1; + } + + sock_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (sock_fd < 0) { + /* Failure creating the socket. Abort early */ + PINELOG_ERROR(_("Error creating notification socket: %s"), strerror(errno)); + return -1; + } + + if (x52d_set_socket_nonblocking(sock_fd) < 0) { + PINELOG_ERROR(_("Error marking notification socket as nonblocking: %s"), strerror(errno)); + return -1; + } + + /* Cleanup any existing socket */ + unlink(local.sun_path); + if (bind(sock_fd, (struct sockaddr *)&local, (socklen_t)len) < 0) { + /* Failure binding socket */ + PINELOG_ERROR(_("Error binding to notification socket: %s"), strerror(errno)); + goto listen_failure; + } + + if (listen(sock_fd, X52D_MAX_CLIENTS) < 0) { + PINELOG_ERROR(_("Error listening on notification socket: %s"), strerror(errno)); + goto listen_failure; + } + + return sock_fd; + +listen_failure: + unlink(local.sun_path); + close(sock_fd); + return -1; +} + +static void * x52_notify_thr(void * param) +{ + char buffer[X52D_BUFSZ]; + uint16_t bufsiz; + int rc; + + for (;;) { +read_pipe_size: + rc = read(notify_pipe[0], &bufsiz, sizeof(bufsiz)); + if (rc < 0) { + if (errno == EINTR) { + goto read_pipe_size; + } else { + PINELOG_ERROR(_("Error %d reading from pipe: %s"), + errno, strerror(errno)); + } + } + + if (rc < 0) { + // Error condition, try again + continue; + } + +read_pipe_data: + rc = read(notify_pipe[0], buffer, bufsiz); + if (rc < 0) { + if (errno == EINTR) { + goto read_pipe_data; + } else { + PINELOG_ERROR(_("Error %d reading from pipe: %s"), + errno, strerror(errno)); + } + } + + if (rc < 0) { + continue; + } + + for (int i = 0; i < X52D_MAX_CLIENTS; i++) { + // Broadcast to every connected client + if (client_fd[i] != INVALID_CLIENT) { +write_client_notification: + rc = write(client_fd[i], buffer, bufsiz); + if (rc < 0 && errno == EINTR) { + goto write_client_notification; + } + } + } + } + + return NULL; +} + +void x52d_notify_send(int argc, const char **argv) +{ + char buffer[X52D_BUFSZ + sizeof(uint16_t)]; + uint16_t bufsiz; + uint16_t written; + int rc; + + bufsiz = (uint16_t)x52d_format_command(argc, argv, buffer + sizeof(uint16_t), X52D_BUFSZ); + memcpy(buffer, &bufsiz, sizeof(bufsiz)); + + pthread_mutex_lock(¬ify_mutex); + written = 0; + while (written < bufsiz) { + rc = write(notify_pipe[1], buffer + written, bufsiz - written); + if (rc < 0) { + if (errno == EINTR) { + continue; + } + PINELOG_ERROR(_("Error %d writing notification pipe: %s"), + errno, strerror(errno)); + } else { + written += rc; + } + } + pthread_mutex_unlock(¬ify_mutex); +} + +static void client_handler(int fd) +{ + char buffer[X52D_BUFSZ] = { 0 }; + int rc; + + rc = recv(fd, buffer, sizeof(buffer), 0); + PINELOG_TRACE("Received and discarded %d bytes from notification client %d", rc, fd); +} + +static void * x52_notify_loop(void * param) +{ + struct pollfd pfd[MAX_CONN]; + int rc; + + for (;;) { + rc = x52d_client_poll(client_fd, pfd, notify_sock); + if (rc <= 0) { + continue; + } + + x52d_client_handle(client_fd, pfd, notify_sock, client_handler); + } + + return NULL; +} + +void x52d_notify_init(const char *notify_sock_path) +{ + int rc; + + PINELOG_TRACE("Initializing notification manager"); + x52d_client_init(client_fd); + + PINELOG_TRACE("Creating notifications pipe"); + rc = pipe(notify_pipe); + if (rc != 0) { + PINELOG_FATAL(_("Error %d creating notification pipe: %s"), + errno, strerror(errno)); + } + + PINELOG_TRACE("Opening notification listener socket"); + notify_sock = listen_notify(notify_sock_path); + + rc = pthread_create(¬ify_thr, NULL, x52_notify_thr, NULL); + if (rc != 0) { + PINELOG_FATAL(_("Error %d initializing notify thread: %s"), + rc, strerror(rc)); + } + + rc = pthread_create(¬ify_listen, NULL, x52_notify_loop, NULL); + if (rc != 0) { + PINELOG_FATAL(_("Error %d initializing notify listener: %s"), + rc, strerror(rc)); + } +} + +void x52d_notify_exit(void) +{ + close(notify_pipe[0]); + close(notify_pipe[1]); + close(notify_sock); + + pthread_cancel(notify_thr); + pthread_cancel(notify_listen); +} diff --git a/daemon/x52d_notify.h b/daemon/x52d_notify.h new file mode 100644 index 0000000..1adf8f6 --- /dev/null +++ b/daemon/x52d_notify.h @@ -0,0 +1,17 @@ +/* + * Saitek X52 Pro MFD & LED driver - Notification manager + * + * Copyright (C) 2022 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#ifndef X52D_NOTIFY_H +#define X52D_NOTIFY_H + +void x52d_notify_init(const char *notify_sock_path); +void x52d_notify_exit(void); +void x52d_notify_send(int argc, const char **argv); + +#endif // !defined X52D_NOTIFY_H + diff --git a/daemon/x52dcomm-internal.h b/daemon/x52dcomm-internal.h index cd5c061..bce932f 100644 --- a/daemon/x52dcomm-internal.h +++ b/daemon/x52dcomm-internal.h @@ -13,7 +13,13 @@ #include #include +#define X52D_BUFSZ 1024 + const char *x52d_command_sock_path(const char *sock_path); int x52d_setup_command_sock(const char *sock_path, struct sockaddr_un *remote); +const char *x52d_notify_sock_path(const char *sock_path); +int x52d_setup_notify_sock(const char *sock_path, struct sockaddr_un *remote); +int x52d_set_socket_nonblocking(int sock_fd); +void x52d_split_args(int *argc, char **argv, char *buffer, int buflen); #endif // !defined X52DCOMM_INTERNAL_H diff --git a/daemon/x52dcomm.h b/daemon/x52dcomm.h index 612fe23..e2a6ee4 100644 --- a/daemon/x52dcomm.h +++ b/daemon/x52dcomm.h @@ -35,7 +35,7 @@ extern "C" { */ /** - * @brief Open a connection to the daemon. + * @brief Open a connection to the daemon command socket. * * This method opens a socket connection to the daemon command socket. This * socket allows the client to issue commands and retrieve data. The \p sock_path @@ -54,6 +54,46 @@ extern "C" { */ int x52d_dial_command(const char *sock_path); +/** + * @brief Open a connection to the daemon notify socket. + * + * This method opens a socket connection to the daemon notify socket. This + * socket allows the client to receive notifications from the daemon. Thej + * \p sock_path parameter may be NULL, in which case, it will use the default + * socket path. + * + * The client will need to use the returned descriptor to communicate with the + * daemon using \ref x52d_recv_notification. Once finished, the client may use + * the \c close(2) method to close the file descriptor. + * + * @param[in] sock_path Path to the daemon command socket. + * + * @returns Non-negative socket file descriptor on success. + * @returns -1 on failure, and set \c errno accordingly. + * + * @exception E2BIG returned if the passed socket path is too big + */ +int x52d_dial_notify(const char *sock_path); + +/** + * @brief Format a series of command strings into a buffer + * + * The client sends the command and parameters as a series of NUL terminated + * strings. This function concatenates the commands into a single buffer that + * can be passed to \ref x52d_send_command. + * + * \p buffer should be at least 1024 bytes long. + * + * @param[in] argc Number of arguments to fit in the buffer + * @param[in] argv Pointer to an array of arguments. + * @param[out] buffer Buffer to store the formatted command + * @param[in] buflen Length of the buffer + * + * @returns number of bytes in the formatted command + * @returns -1 on an error condition, and \c errno is set accordingly. + */ +int x52d_format_command(int argc, const char **argv, char *buffer, size_t buflen); + /** * @brief Send a command to the daemon and retrieve the response. * @@ -83,6 +123,32 @@ int x52d_dial_command(const char *sock_path); */ int x52d_send_command(int sock_fd, char *buffer, size_t bufin, size_t bufout); +/** + * @brief Notification callback function type + */ +typedef int (* x52d_notify_callback_fn)(int argc, char **argv); + +/** + * @brief Receive a notification from the daemon + * + * This function blocks until it receives a notification from the daemon. Once + * it receives a notification successfully, it will call the callback function + * with the arguments as string pointers. It will return the return value of + * the callback function, if it was called. + * + * This is a blocking function and will not return until either a notification + * is received from the server, or an exception condition occurs. + * + * @param[in] sock_fd Socket descriptor returned from + * \ref x52d_dial_notify + * + * @param[in] callback Pointer to the callback function + * + * @returns return code of the callback function on success + * @returns -1 on an error condition, and \c errno is set accordingly. + */ +int x52d_recv_notification(int sock_fd, x52d_notify_callback_fn callback); + /** @} */ #ifdef __cplusplus } diff --git a/po/libx52.pot b/po/libx52.pot index 356c9a7..dd8bd88 100644 --- a/po/libx52.pot +++ b/po/libx52.pot @@ -665,51 +665,51 @@ msgstr "" msgid "Timed out when polling for command" msgstr "" -#: daemon/x52d_command.c:488 +#: daemon/x52d_command.c:473 #, c-format msgid "Error reading from client %d: %s" msgstr "" -#: daemon/x52d_command.c:499 +#: daemon/x52d_command.c:484 #, c-format msgid "Short write to client %d; expected %d bytes, wrote %d bytes" msgstr "" -#: daemon/x52d_command.c:513 +#: daemon/x52d_command.c:498 #, c-format msgid "Error %d during command loop: %s" msgstr "" -#: daemon/x52d_command.c:545 +#: daemon/x52d_command.c:530 #, c-format msgid "Error creating command socket: %s" msgstr "" -#: daemon/x52d_command.c:552 +#: daemon/x52d_command.c:537 #, c-format msgid "Error getting command socket flags: %s" msgstr "" -#: daemon/x52d_command.c:556 +#: daemon/x52d_command.c:541 #, c-format msgid "Error setting command socket flags: %s" msgstr "" -#: daemon/x52d_command.c:564 +#: daemon/x52d_command.c:549 #, c-format msgid "Error binding to command socket: %s" msgstr "" -#: daemon/x52d_command.c:569 +#: daemon/x52d_command.c:554 #, c-format msgid "Error listening on command socket: %s" msgstr "" -#: daemon/x52d_command.c:579 +#: daemon/x52d_command.c:564 msgid "Starting command processing thread" msgstr "" -#: daemon/x52d_command.c:597 +#: daemon/x52d_command.c:582 msgid "Shutting down command processing thread" msgstr "" diff --git a/po/xx_PL.po b/po/xx_PL.po index cb9d7a9..2438d5e 100644 --- a/po/xx_PL.po +++ b/po/xx_PL.po @@ -716,52 +716,52 @@ msgstr "Erroray enwhay ollingpay orfay ommandcay: %s" msgid "Timed out when polling for command" msgstr "Imedtay outay enwhay ollingpay orfay ommandcay" -#: daemon/x52d_command.c:488 +#: daemon/x52d_command.c:473 #, c-format msgid "Error reading from client %d: %s" msgstr "Erroray eadingray omfray ientclay %d: %s" -#: daemon/x52d_command.c:499 +#: daemon/x52d_command.c:484 #, c-format msgid "Short write to client %d; expected %d bytes, wrote %d bytes" msgstr "" "Ortshay itewray otay ientclay %d; expecteday %d ytesbay, otewray %d ytesbay" -#: daemon/x52d_command.c:513 +#: daemon/x52d_command.c:498 #, c-format msgid "Error %d during command loop: %s" msgstr "Erroray %d uringday ommandcay ooplay: %s" -#: daemon/x52d_command.c:545 +#: daemon/x52d_command.c:530 #, c-format msgid "Error creating command socket: %s" msgstr "Erroray eatingcray ommandcay ocketsay: %s" -#: daemon/x52d_command.c:552 +#: daemon/x52d_command.c:537 #, c-format msgid "Error getting command socket flags: %s" msgstr "Erroray ettinggay ommandcay ocketsay agsflay: %s" -#: daemon/x52d_command.c:556 +#: daemon/x52d_command.c:541 #, c-format msgid "Error setting command socket flags: %s" msgstr "Erroray ettingsay ommandcay ocketsay agsflay: %s" -#: daemon/x52d_command.c:564 +#: daemon/x52d_command.c:549 #, c-format msgid "Error binding to command socket: %s" msgstr "Erroray indingbay otay ommandcay ocketsay: %s" -#: daemon/x52d_command.c:569 +#: daemon/x52d_command.c:554 #, c-format msgid "Error listening on command socket: %s" msgstr "Erroray isteninglay onay ommandcay ocketsay: %s" -#: daemon/x52d_command.c:579 +#: daemon/x52d_command.c:564 msgid "Starting command processing thread" msgstr "Artingstay ommandcay ocessingpray eadthray" -#: daemon/x52d_command.c:597 +#: daemon/x52d_command.c:582 msgid "Shutting down command processing thread" msgstr "Uttingshay ownday ommandcay ocessingpray eadthray"