diff options
Diffstat (limited to 'coopgammad.c')
-rw-r--r-- | coopgammad.c | 875 |
1 files changed, 875 insertions, 0 deletions
diff --git a/coopgammad.c b/coopgammad.c new file mode 100644 index 0000000..50ba53b --- /dev/null +++ b/coopgammad.c @@ -0,0 +1,875 @@ +/* See LICENSE file for copyright and license details. */ +#include "arg.h" +#include "util.h" +#include "state.h" +#include "servers-master.h" +#include "servers-kernel.h" +#include "servers-crtc.h" +#include "servers-gamma.h" +#include "servers-coopgamma.h" + +#include <sys/resource.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + + +/** + * Number put in front of the marshalled data + * so the program an detect incompatible updates + */ +#define MARSHAL_VERSION 0 + + +#ifndef GCC_ONLY +# if defined(__GNUC__) && !defined(__clang__) +# define GCC_ONLY(...) __VA_ARGS__ +# else +# define GCC_ONLY(...) /* nothing */ +# endif +#endif + + +/** + * Lists all function recognised adjustment methods, + * will call macro X with the code for the each + * adjustment method as the first argument and + * corresponding name as the second argument + */ +#define LIST_ADJUSTMENT_METHODS\ + X(LIBGAMMA_METHOD_DUMMY, "dummy")\ + X(LIBGAMMA_METHOD_X_RANDR, "randr")\ + X(LIBGAMMA_METHOD_X_VIDMODE, "vidmode")\ + X(LIBGAMMA_METHOD_LINUX_DRM, "drm")\ + X(LIBGAMMA_METHOD_W32_GDI, "gdi")\ + X(LIBGAMMA_METHOD_QUARTZ_CORE_GRAPHICS, "quartz") + + +/** + * Used by initialisation functions as their return type. If a + * value not listed here is returned by such function, it is the + * exit value the process shall exit with as soon as possible. + */ +enum init_status { + /** + * Initialisation was successful + */ + INIT_SUCCESS = -1, + + /** + * Initialisation failed + */ + INIT_FAILURE = -2, + + /** + * Server is already running + */ + INIT_RUNNING = -3, +}; + + +/** + * The pathname of the PID file + */ +extern char *restrict pidpath; +char *restrict pidpath = NULL; + +/** + * The pathname of the socket + */ +extern char *restrict socketpath; +char *restrict socketpath = NULL; + + + +/** + * Called when the process receives + * a signal telling it to re-execute + * + * @param signo The received signal + */ +static void +sig_reexec(int signo) +{ + int saved_errno = errno; + reexec = 1; + signal(signo, sig_reexec); + errno = saved_errno; +} + + +/** + * Called when the process receives + * a signal telling it to terminate + * + * @param signo The received signal + */ +static void +sig_terminate(int signo) +{ + terminate = 1; + (void) signo; +} + + +/** + * Called when the process receives + * a signal telling it to disconnect + * from or reconnect to the site + * + * @param signo The received signal + */ +static void +sig_connection(int signo) +{ + int saved_errno = errno; + connection = signo - SIGRTMIN + 1; + signal(signo, sig_connection); + errno = saved_errno; +} + + +/** + * Called when the process receives + * a signal telling it to dump its + * state to stderr + * + * @param signo The received signal + */ +static void +sig_info(int signo) +{ + int saved_errno = errno; + char *env; + signal(signo, sig_info); + env = getenv("COOPGAMMAD_PIDFILE_TOKEN"); + fprintf(stderr, "PID file token: %s\n", env ? env : "(null)"); + fprintf(stderr, "PID file: %s\n", pidpath ? pidpath : "(null)"); + fprintf(stderr, "Socket path: %s\n", socketpath ? socketpath : "(null)"); + state_dump(); + errno = saved_errno; +} + + +/** + * Parse adjustment method name (or stringised number) + * + * @param arg The adjustment method name (or stringised number) + * @return The adjustment method, -1 (negative) on error + */ +GCC_ONLY(__attribute__((__nonnull__))) +static int +get_method(const char *restrict arg) +{ +#if LIBGAMMA_METHOD_MAX > 5 +# warning libgamma has added more adjustment methods +#endif + + const char *restrict p; + +#define X(C, N) if (!strcmp(arg, N)) return C; + LIST_ADJUSTMENT_METHODS; +#undef X + + if (!*arg || (/* avoid overflow: */ strlen(arg) > 4)) + goto bad; + for (p = arg; *p; p++) + if ('0' > *p || *p > '9') + goto bad; + + return atoi(arg); + +bad: + fprintf(stderr, "%s: unrecognised adjustment method name: %s\n", argv0, arg); + errno = 0; + return -1; +} + + +/** + * Set up signal handlers + * + * @return Zero on success, -1 on error + */ +static int +set_up_signals(void) +{ + if (signal(SIGUSR1, sig_reexec) == SIG_ERR || + signal(SIGUSR2, sig_info) == SIG_ERR || +#if defined(SIGINFO) + signal(SIGINFO, sig_info) == SIG_ERR || +#endif + signal(SIGTERM, sig_terminate) == SIG_ERR || + signal(SIGRTMIN + 0, sig_connection) == SIG_ERR || + signal(SIGRTMIN + 1, sig_connection) == SIG_ERR) + return -1; + return 0; +} + + +/** + * Fork the process to the background + * + * @param keep_stderr Keep stderr open? + * @return An `enum init_status` value or an exit value + */ +static enum init_status +daemonise(int keep_stderr) +{ + pid_t pid; + int fd = -1, saved_errno; + int notify_rw[2] = {-1, -1}; + char a_byte = 0; + ssize_t got; + + if (pipe(notify_rw) < 0) + goto fail; + if (notify_rw[0] <= STDERR_FILENO) + if ((notify_rw[0] = fcntl(notify_rw[0], F_DUPFD, STDERR_FILENO + 1)) < 0) + goto fail; + if (notify_rw[1] <= STDERR_FILENO) + if ((notify_rw[1] = fcntl(notify_rw[1], F_DUPFD, STDERR_FILENO + 1)) < 0) + goto fail; + + if ((pid = fork()) < 0) + goto fail; + if (pid > 0) { + /* Original process (parent): */ + waitpid(pid, NULL, 0); + close(notify_rw[1]), notify_rw[1] = -1; + got = read(notify_rw[0], &a_byte, 1); + if (got < 0) + goto fail; + close(notify_rw[0]); + errno = 0; + return got == 0 ? INIT_FAILURE : (enum init_status)0; + } + + /* Intermediary process (child): */ + close(notify_rw[0]), notify_rw[0] = -1; + if (setsid() < 0) + goto fail; + if ((pid = fork()) < 0) + goto fail; + if (pid > 0) { + /* Intermediary process (parent): */ + return (enum init_status)0; + } + + /* Daemon process (child): */ + + /* Replace std* with /dev/null */ + fd = open("/dev/null", O_RDWR); + if (fd < 0 || + dup2(fd, STDIN_FILENO) < 0 || + dup2(fd, STDOUT_FILENO) < 0 || + (keep_stderr && dup2(fd, STDERR_FILENO) < 0)) + goto fail; + if (fd > STDERR_FILENO) + close(fd); + fd = -1; + + /* Update PID file */ + fd = open(pidpath, O_WRONLY); + if (fd < 0) + goto fail; + if (dprintf(fd, "%llu\n", (unsigned long long)getpid()) < 0) + goto fail; + close(fd); + fd = -1; + + /* Notify */ + if (write(notify_rw[1], &a_byte, 1) <= 0) + goto fail; + close(notify_rw[1]); + + return INIT_SUCCESS; +fail: + saved_errno = errno; + if (fd >= 0) + close(fd); + if (notify_rw[0] >= 0) + close(notify_rw[0]); + if (notify_rw[1] >= 0) + close(notify_rw[1]); + errno = saved_errno; + return INIT_FAILURE; +} + + +/** + * Initialise the process + * + * @param foreground Keep process in the foreground + * @param keep_stderr Keep stderr open + * @param query Was -q used, see `main` for description + * @return An `enum init_status` value or an exit value + */ +static enum init_status +initialise(int foreground, int keep_stderr, int query) +{ + struct rlimit rlimit; + size_t i, n; + sigset_t mask; + int s; + enum init_status r; + + /* Zero out some memory so it can be destroyed safely. */ + memset(&site, 0, sizeof(site)); + + if (!query) { + /* Close all file descriptors above stderr */ + if (getrlimit(RLIMIT_NOFILE, &rlimit) || rlimit.rlim_cur == RLIM_INFINITY) + n = 4 << 10; + else + n = (size_t)(rlimit.rlim_cur); + for (i = STDERR_FILENO + 1; i < n; i++) + close((int)i); + + /* Set umask, reset signal handlers, and reset signal mask */ + umask(0); + for (s = 1; s < _NSIG; s++) + signal(s, SIG_DFL); + if (sigfillset(&mask)) + perror(argv0); + else + sigprocmask(SIG_UNBLOCK, &mask, NULL); + } + + /* Get method */ + if (method < 0 && libgamma_list_methods(&method, 1, 0) < 1) + return fprintf(stderr, "%s: no adjustment method available\n", argv0), -1; + + /* Go no further if we are just interested in the adjustment method and site */ + if (query) + return INIT_SUCCESS; + + /* Get site */ + if (initialise_site() < 0) + goto fail; + + /* Get PID file and socket pathname */ + if (!(pidpath = get_pidfile_pathname()) || + !(socketpath = get_socket_pathname())) + goto fail; + + /* Create PID file */ + if ((r = create_pidfile(pidpath)) < 0) { + free(pidpath); + pidpath = NULL; + if (r == -2) + return INIT_RUNNING; + goto fail; + } + + /* Get partitions and CRTC:s */ + if (initialise_crtcs() < 0) + goto fail; + + /* Get CRTC information */ + if (outputs_n && !(outputs = calloc(outputs_n, sizeof(*outputs)))) + goto fail; + if (initialise_gamma_info() < 0) + goto fail; + + /* Sort outputs */ + qsort(outputs, outputs_n, sizeof(*outputs), output_cmp_by_name); + + /* Load current gamma ramps */ + store_gamma(); + + /* Preserve current gamma ramps at priority=0 if -p */ + if (preserve && preserve_gamma() < 0) + goto fail; + + /* Create socket and start listening */ + if (create_socket(socketpath) < 0) + goto fail; + + /* Get the real pathname of the process's binary, in case + * it is relative, so we can re-execute without problem. */ + if (*argv0 != '/' && strchr(argv0, '/') && !(argv0_real = realpath(argv0, NULL))) + goto fail; + + /* Change directory to / to avoid blocking umounting */ + if (chdir("/") < 0) + perror(argv0); + + /* Set up signal handlers */ + if (set_up_signals() < 0) + goto fail; + + /* Place in the background unless -f */ + if (!foreground) { + return daemonise(keep_stderr); + } else { + /* Signal the spawner that the service is ready */ + close(STDOUT_FILENO); + /* Avoid potential catastrophes that would occur if a library + * that is being used was so mindless as to write to stdout. */ + if (dup2(STDERR_FILENO, STDOUT_FILENO) < 0) + perror(argv0); + } + + return INIT_SUCCESS; +fail: + return INIT_FAILURE; +} + + +/** + * Deinitialise the process + * + * @param full Perform a full deinitialisation, shall be + * done iff the process is going to re-execute + */ +static void +destroy(int full) +{ + if (full) { + disconnect_all(); + close_socket(socketpath); + free(argv0_real); + if (outputs && connected) + restore_gamma(); + } + state_destroy(); + free(socketpath); + if (full && pidpath) + unlink(pidpath); + free(pidpath); +} + + +#if defined(__clang__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wcast-align" +#endif + + +/** + * Marshal the state of the process + * + * @param buf Output buffer for the marshalled data, + * `NULL` to only measure how large the + * buffer needs to be + * @return The number of marshalled bytes + */ +static size_t +marshal(void *restrict buf) +{ + size_t off = 0, n; + char *restrict bs = buf; + + if (bs) + *(int *)&bs[off] = MARSHAL_VERSION; + off += sizeof(int); + + n = strlen(pidpath) + 1; + if (bs) + memcpy(&bs[off], pidpath, n); + off += n; + + n = strlen(socketpath) + 1; + if (bs) + memcpy(&bs[off], socketpath, n); + off += n; + + off += state_marshal(bs ? &bs[off] : NULL); + + return off; +} + + +/** + * Unmarshal the state of the process + * + * @param buf Buffer with the marshalled data + * @return The number of marshalled bytes, 0 on error + */ +GCC_ONLY(__attribute__((__nonnull__))) +static size_t +unmarshal(const void *restrict buf) +{ + size_t off = 0, n; + const char *restrict bs = buf; + + if (*(const int *)&bs[off] != MARSHAL_VERSION) { + fprintf(stderr, "%s: re-executing to incompatible version, sorry about that\n", argv0); + errno = 0; + return 0; + } + off += sizeof(int); + + n = strlen(&bs[off]) + 1; + if (!(pidpath = memdup(&bs[off], n))) + return 0; + off += n; + + n = strlen(&bs[off]) + 1; + if (!(socketpath = memdup(&bs[off], n))) + return 0; + off += n; + + off += n = state_unmarshal(&bs[off]); + if (!n) + return 0; + + return off; +} + + +#if defined(__clang__) +# pragma GCC diagnostic pop +#endif + + +/** + * Do minimal initialisation, unmarshal the state of + * the process and merge with new state + * + * @param statefile The state file + * @return Zero on success, -1 on error + */ +GCC_ONLY(__attribute__((__nonnull__))) +static int +restore_state(const char *restrict statefile) +{ + void *marshalled = NULL; + int fd = -1, saved_errno; + size_t r, n; + + if (set_up_signals() < 0) + goto fail; + + fd = open(statefile, O_RDONLY); + if (fd < 0) + goto fail; + + if (!(marshalled = nread(fd, &n))) + goto fail; + close(fd); + fd = -1; + unlink(statefile); + statefile = NULL; + + r = unmarshal(marshalled); + if (!r) + goto fail; + if (r != n) { + fprintf(stderr, "%s: unmarshalled state file was %s than the unmarshalled state: read %zu of %zu bytes\n", + argv0, n > r ? "larger" : "smaller", r, n); + errno = 0; + goto fail; + } + free(marshalled); + marshalled = NULL; + + if (connected) { + connected = 0; + if (reconnect() < 0) + goto fail; + } + + return 0; +fail: + saved_errno = errno; + if (fd >= 0) + close(fd); + free(marshalled); + errno = saved_errno; + return -1; +} + + +/** + * Reexecute the server + * + * Returns only on failure + * + * @return Pathname of file where the state is stored, + * `NULL` if the state is in tact + */ +static char * +reexecute(void) +{ + char *statefile = NULL; + char *statebuffer = NULL; + size_t buffer_size; + int fd = -1, saved_errno; + + statefile = get_state_pathname(); + if (!statefile) + goto fail; + + buffer_size = marshal(NULL); + statebuffer = malloc(buffer_size); + if (!statebuffer) + goto fail; + if (marshal(statebuffer) != buffer_size) { + fprintf(stderr, "%s: internal error\n", argv0); + errno = 0; + goto fail; + } + + fd = open(statefile, O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR); + if (fd < 0) + goto fail; + + if (nwrite(fd, statebuffer, buffer_size) != buffer_size) + goto fail; + free(statebuffer); + statebuffer = NULL; + + if (close(fd) < 0) { + fd = -1; + if (errno != EINTR) + goto fail; + } + fd = -1; + + destroy(0); + + execlp(argv0_real ? argv0_real : argv0, argv0, "- ", statefile, NULL); + free(argv0_real); + argv0_real = NULL; + return statefile; + +fail: + saved_errno = errno; + free(statebuffer); + if (fd >= 0) + close(fd); + if (statefile != NULL) { + unlink(statefile); + free(statefile); + } + errno = saved_errno; + return NULL; +} + + +/** + * Print the response for the -q option + * + * @param query See -q for `main`, must be atleast 1 + * @return Zero on success, -1 on error + */ +static int +print_method_and_site(int query) +{ + const char *restrict methodname = NULL; + char *p; + + if (query == 1) { + switch (method) { +#define X(C, N) case C: methodname = N; break; + LIST_ADJUSTMENT_METHODS; +#undef X + default: + if (printf("%i\n", method) < 0) + return -1; + break; + } + if (methodname) + if (printf("%s\n", methodname) < 0) + return -1; + } + + if (!sitename) + if ((sitename = libgamma_method_default_site(method))) + if (!(sitename = memdup(sitename, strlen(sitename) + 1))) + return -1; + + if (sitename) { + switch (method) { + case LIBGAMMA_METHOD_X_RANDR: + case LIBGAMMA_METHOD_X_VIDMODE: + if ((p = strrchr(sitename, ':'))) + if ((p = strchr(p, '.'))) + *p = '\0'; + break; + default: + break; + } + } + + if (sitename && query == 1) + if (printf("%s\n", sitename) < 0) + return -1; + + if (query == 2) { + site.method = method; + site.site = sitename; + sitename = NULL; + socketpath = get_socket_pathname(); + if (!socketpath) + return -1; + if (printf("%s\n", socketpath) < 0) + return -1; + } + + if (fflush(stdout)) + return -1; + return 0; +} + + +/** + * Print usage information and exit + */ +#if defined(__GNU__) || defined(__clang__) +__attribute__((__noreturn__)) +#endif +static void +usage(void) +{ + fprintf(stderr, "usage: %s [-m method] [-s site] [-fkpq]\n", argv0); + exit(1); +} + + +#if defined(__clang__) +# pragma GCC diagnostic ignored "-Wdocumentation-unknown-command" +#endif + + +/** + * Must not be started without stdin, stdout, or stderr (may be /dev/null) + * + * argv[0] must refer to the real command name or pathname, + * otherwise, re-execute will not work + * + * The process closes stdout when the socket has been created + * + * @signal SIGUSR1 Re-execute to updated process image + * @signal SIGUSR2 Dump the state of the process to standard error + * @signal SIGINFO Ditto + * @signal SIGTERM Terminate the process gracefully + * @signal SIGRTMIN+0 Disconnect from the site + * @signal SiGRTMIN+1 Reconnect to the site + * + * @param argc The number of elements in `argv` + * @param argv Command line arguments. Recognised options: + * -s SITENAME + * The site to which to connect + * -m METHOD + * Adjustment method name or adjustment method number + * -p + * Preserve current gamma ramps at priority 0 + * -f + * Do not fork the process into the background + * -k + * Keep stderr open + * -q + * Print the select (possiblity default) adjustment + * method on the first line in stdout, and the + * selected (possibility defasult) site on the second + * line in stdout, and exit. If the site name is `NULL`, + * the second line is omitted. This is indented to + * be used by clients to figure out to which instance + * of the service it should connect. Use twice to + * simply ge the socket pathname, an a terminating LF. + * By combining -q and -m you can enumerate the name + * of all recognised adjustment method, start from 0 + * and work up until a numerical adjustment method is + * returned. + * @return 0: Successful + * 1: An error occurred + * 2: Already running + */ +int +main(int argc, char *argv[]) +{ + int rc = 1, foreground = 0, keep_stderr = 0, query = 0, r; + char *statefile = NULL, *statefile_ = NULL; + + ARGBEGIN { + case 's': + sitename = EARGF(usage()); + /* To simplify re-exec: */ + sitename = memdup(sitename, strlen(sitename) + 1); + if (!sitename) + goto fail; + break; + case 'm': + method = get_method(EARGF(usage())); + if (method < 0) + goto fail; + break; + case 'p': preserve = 1; break; + case 'f': foreground = 1; break; + case 'k': keep_stderr = 1; break; + case 'q': query = 1 + !!query; break; + case ' ': /* Internal, do not document */ + statefile = statefile_ = EARGF(usage()); + break; + default: + usage(); + } + ARGEND; + + if (argc > 0) + usage(); + +restart: + if (!statefile) { + switch ((r = initialise(foreground, keep_stderr, query))) { + case INIT_SUCCESS: break; + case INIT_RUNNING: rc = 2; /* fall through */ + case INIT_FAILURE: goto fail; + default: return r; + } + } else if (restore_state(statefile) < 0) { + goto fail; + } else { + if (statefile != statefile_) + free(statefile); + unlink(statefile); + statefile = NULL; + } + + if (query) { + if (print_method_and_site(query) < 0) + goto fail; + goto done; + } + +reenter_loop: + if (main_loop() < 0) + goto fail; + + if (reexec && !terminate) { + statefile = reexecute(); + if (statefile) { + perror(argv0); + fprintf(stderr, "%s: restoring state without re-executing\n", argv0); + reexec = 0; + goto restart; + } + perror(argv0); + fprintf(stderr, "%s: continuing without re-executing\n", argv0); + reexec = 0; + goto reenter_loop; + } + +done: + rc = 0; +deinit: + if (statefile) + unlink(statefile); + if (reexec) + free(statefile); + destroy(1); + return rc; + +fail: + if (errno) + perror(argv0); + goto deinit; +} |