/** * coopgammad -- Cooperative gamma server * Copyright (C) 2016 Mattias Andrée (maandree@kth.se) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #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 #include #include #include #include #include #include #include #include /** * 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] = dup2atleast(notify_rw[0], STDERR_FILENO + 1)) < 0) goto fail; if (notify_rw[1] <= STDERR_FILENO) if ((notify_rw[1] = dup2atleast(notify_rw[1], 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) goto fail; #define xdup2(s, d) do if (s != d) { close(d); if (dup2(s, d) < 0) goto fail; } while (0) xdup2(fd, STDIN_FILENO); xdup2(fd, STDOUT_FILENO); if (keep_stderr) xdup2(fd, STDERR_FILENO); 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 destoried 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 == 0) 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 != NULL) && connected) restore_gamma(); } state_destroy(); free(socketpath); if (full && (pidpath != NULL)) 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 != NULL) *(int*)(bs + off) = MARSHAL_VERSION; off += sizeof(int); n = strlen(pidpath) + 1; if (bs != NULL) memcpy(bs + off, pidpath, n); off += n; n = strlen(socketpath) + 1; if (bs != NULL) 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 == 0) 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 == 0) 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 == NULL) goto fail; buffer_size = marshal(NULL); statebuffer = malloc(buffer_size); if (statebuffer == NULL) 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, errno != EINTR)) goto fail; fd = -1; destroy(0); #if !defined(USE_VALGRIND) execlp(argv0_real ? argv0_real : argv0, argv0, "- ", statefile, NULL); #else execlp("valgrind", "valgrind", "--leak-check=full", argv0_real ? argv0_real : argv0, "- ", statefile, NULL); #endif saved_errno = errno; free(argv0_real), argv0_real = NULL; errno = saved_errno; 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 != NULL) if (printf("%s\n", methodname) < 0) return -1; } if (sitename == NULL) if ((sitename = libgamma_method_default_site(method))) if (!(sitename = memdup(sitename, strlen(sitename) + 1))) return -1; if (sitename != NULL) 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 != NULL) && (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 == NULL) 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) { printf("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; char* statefile_ = NULL; ARGBEGIN { case 's': sitename = EARGF(usage()); /* To simplify re-exec: */ sitename = memdup(sitename, strlen(sitename) + 1); if (sitename == NULL) 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 == NULL) 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) { if ((statefile = reexecute())) { 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 != 0) perror(argv0); goto deinit; }