/**
* 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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "arg.h"
#include "output.h"
#include "util.h"
#include "server.h"
/**
* Number put in front of the marshalled data
* so the program an detect incompatible updates
*/
#define MARSHAL_VERSION 0
extern char* argv0_real;
extern struct output* outputs;
extern size_t outputs_n;
extern int socketfd;
extern char* pidpath;
extern char* socketpath;
extern int gerror;
extern int method;
extern char* sitename;
extern libgamma_site_state_t site;
extern libgamma_partition_state_t* partitions;
extern libgamma_crtc_state_t* crtcs;
extern volatile sig_atomic_t reexec;
extern volatile sig_atomic_t terminate;
extern volatile sig_atomic_t connection;
/**
* The name of the process
*/
char* argv0;
/**
* The real pathname of the process's binary,
* `NULL` if `argv0` is satisfactory
*/
char* argv0_real = NULL;
/**
* Array of all outputs
*/
struct output* outputs = NULL;
/**
* The nubmer of elements in `outputs`
*/
size_t outputs_n = 0;
/**
* The server socket's file descriptor
*/
int socketfd = -1;
/**
* The pathname of the PID file
*/
char* pidpath = NULL;
/**
* The pathname of the socket
*/
char* socketpath = NULL;
/**
* Error code returned by libgamma
*/
int gerror;
/**
* The adjustment method, -1 for automatic
*/
int method = -1;
/**
* The site's name, may be `NULL`
*/
char* sitename = NULL;
/**
* The libgamma site state
*/
libgamma_site_state_t site;
/**
* The libgamma partition states
*/
libgamma_partition_state_t* partitions = NULL;
/**
* The libgamma CRTC states
*/
libgamma_crtc_state_t* crtcs = NULL;
/**
* Has the process receive a signal
* telling it to re-execute?
*/
volatile sig_atomic_t reexec = 0;
/**
* Has the process receive a signal
* telling it to terminate?
*/
volatile sig_atomic_t terminate = 0;
/**
* Has the process receive a to
* disconnect from or reconnect to
* the site? 1 if disconnct, 2 if
* reconnect, 0 otherwise.
*/
volatile sig_atomic_t connection = 0;
/**
* Called when the process receives
* a signal telling it to re-execute
*
* @param signo The received signal
*/
static void sig_reexec(int signo)
{
reexec = 1;
(void) signo;
}
/**
* 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)
{
connection = signo - SIGRTMIN + 1;
}
/**
* Get the pathname of the runtime file
*
* @param suffix The suffix for the file
* @return The pathname of the file, `NULL` on error
*/
static char* get_pathname(const char* suffix)
{
const char* rundir = getenv("XDG_RUNTIME_DIR");
const char* username = "";
char* name = NULL;
char* p;
char* rc;
struct passwd* pw;
size_t n;
int saved_errno;
if (site.site)
{
name = memdup(site.site, strlen(site.site) + 1);
if (name == NULL)
goto fail;
}
else if ((name = libgamma_method_default_site(site.method)))
{
name = memdup(name, strlen(name) + 1);
if (name == NULL)
goto fail;
}
if (name != NULL)
switch (site.method)
{
case LIBGAMMA_METHOD_X_RANDR:
case LIBGAMMA_METHOD_X_VIDMODE:
if ((p = strrchr(name, ':')))
if ((p = strchr(p, '.')))
*p = '\0';
break;
default:
break;
}
if (!rundir || !*rundir)
rundir = "/tmp";
if ((pw = getpwuid(getuid())))
username = pw->pw_name ? pw->pw_name : "";
n = sizeof("/.coopgammad/~/.") + 3 * sizeof(int);
n += strlen(rundir) + strlen(username) + strlen(name) + strlen(suffix);
if (!(rc = malloc(n)))
goto fail;
sprintf(rc, "%s/.coopgammad/~%s/%i%s%s%s",
rundir, username, site.method, name ? "." : "", name ? name : "", suffix);
return rc;
fail:
saved_errno = errno;
free(name);
errno = saved_errno;
return NULL;
}
/**
* Get the pathname of the socket
*
* @return The pathname of the socket, `NULL` on error
*/
static inline char* get_socket_pathname(void)
{
return get_pathname(".socket");
}
/**
* Get the pathname of the PID file
*
* @return The pathname of the PID file, `NULL` on error
*/
static inline char* get_pidfile_pathname(void)
{
return get_pathname(".pid");
}
/**
* Get the pathname of the state file
*
* @return The pathname of the state file, `NULL` on error
*/
static inline char* get_state_pathname(void)
{
return get_pathname(".state");
}
/**
* Parse adjustment method name (or stringised number)
*
* @param arg The adjustment method name (or stringised number)
* @return The adjustment method, -1 (negative) on error
*/
static int get_method(char* arg)
{
#if LIBGAMMA_METHOD_MAX > 5
# warning libgamma has added more adjustment methods
#endif
char* p;
if (!strcmp(arg, "dummy")) return LIBGAMMA_METHOD_DUMMY;
if (!strcmp(arg, "randr")) return LIBGAMMA_METHOD_X_RANDR;
if (!strcmp(arg, "vidmode")) return LIBGAMMA_METHOD_X_VIDMODE;
if (!strcmp(arg, "drm")) return LIBGAMMA_METHOD_LINUX_DRM;
if (!strcmp(arg, "gdi")) return LIBGAMMA_METHOD_W32_GDI;
if (!strcmp(arg, "quartz")) return LIBGAMMA_METHOD_QUARTZ_CORE_GRAPHICS;
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;
}
/**
* Get the name of a CRTC
*
* @param info Information about the CRTC
* @param crtc libgamma's state for the CRTC
* @return The name of the CRTC, `NULL` on error
*/
static char* get_crtc_name(libgamma_crtc_information_t* info, libgamma_crtc_state_t* crtc)
{
if ((info->edid_error == 0) && (info->edid != NULL))
return libgamma_behex_edid(info->edid, info->edid_length);
else if ((info->connector_name_error == 0) && (info->connector_name != NULL))
{
char* name = malloc(3 * sizeof(size_t) + strlen(info->connector_name) + 2);
if (name != NULL)
sprintf(name, "%zu.%s", crtc->partition->partition, info->connector_name);
return name;
}
else
{
char* name = malloc(2 * 3 * sizeof(size_t) + 2);
if (name != NULL)
sprintf(name, "%zu.%zu", crtc->partition->partition, crtc->crtc);
return name;
}
}
/**
* Check whether a PID file is outdated
*
* @param pidfile The PID file
* @param token An environment variable (including both key and value)
* that must exist in the process if it is a coopgammad process
* @return -1: An error occurred
* 0: The service is already running
* 1: The PID file is outdated
*/
static int is_pidfile_reusable(const char* pidfile, const char* token)
{
/* PORTERS: /proc/$PID/environ is Linux specific */
char temp[sizeof("/proc//environ") + 3 * sizeof(pid_t)];
int fd = -1, saved_errno, tries = 0;
char* content = NULL;
char* p;
pid_t pid = 0;
size_t n;
#if defined(HAVE_LINUX_PROCFS)
char* end;
#else
(void) token;
#endif
/* Get PID */
retry:
fd = open(pidfile, O_RDONLY);
if (fd < 0)
return -1;
content = nread(fd, &n);
if (content == NULL)
goto fail;
close(fd), fd = -1;
if (n == 0)
{
if (++tries > 1)
goto bad;
msleep(100); /* 1 tenth of a second */
goto retry;
}
if (('0' > content[0]) || (content[0] > '9'))
goto bad;
if ((content[0] == '0') && ('0' <= content[1]) && (content[1] <= '9'))
goto bad;
for (p = content; *p; p++)
if (('0' <= *p) && (*p <= '9'))
pid = pid * 10 + (*p & 15);
else
break;
if (*p++ != '\n')
goto bad;
if (*p)
goto bad;
if ((size_t)(content - p) != n)
goto bad;
sprintf(temp, "%llu", (unsigned long long)pid);
if (strcmp(content, temp))
goto bad;
/* Validate PID */
#if defined(HAVE_LINUX_PROCFS)
sprintf(temp, "/proc/%llu/environ", (unsigned long long)pid);
fd = open(temp, O_RDONLY);
if (fd < 0)
return ((errno == ENOENT) || (errno == EACCES)) ? 1 : -1;
content = nread(fd, &n);
if (content == NULL)
goto fail;
close(fd), fd = -1;
for (end = (p = content) + n; p != end; p = strchr(p, '\0') + 1)
if (!strcmp(p, token))
return 0;
#else
if ((kill(pid, 0) == 0) || (errno == EINVAL))
return 0;
#endif
return 1;
bad:
fprintf(stderr, "%s: pid file contain invalid content: %s\n", argv0, pidfile);
errno = 0;
return -1;
fail:
saved_errno = errno;
free(content);
if (fd >= 0)
close(fd);
errno = saved_errno;
return -1;
}
/**
* Create PID file
*
* @param pidfile The pathname of the PID file
* @return Zero on success, -1 on error,
* -2 if the service is already running
*/
static int create_pidfile(char* pidfile)
{
int fd, r, saved_errno;
char* p;
char* token;
/* Create token used to validate the service. */
token = malloc(sizeof("COOPGAMMAD_PIDFILE_TOKEN=") + strlen(pidfile));
if (token == NULL)
return -1;
sprintf(token, "COOPGAMMAD_PIDFILE_TOKEN=%s", pidfile);
if (putenv(token))
return -1;
/* Create PID file's directory. */
for (p = pidfile; *p == '/'; p++);
while ((p = strchr(p, '/')))
{
*p = '\0';
if (mkdir(pidfile, 0644) < 0)
if (errno != EEXIST)
return -1;
*p++ = '/';
}
/* Create PID file. */
retry:
fd = open(pidfile, O_CREAT | O_EXCL, 0644);
if (fd < 0)
{
if (errno == EINTR)
goto retry;
if (errno != EEXIST)
return -1;
r = is_pidfile_reusable(pidfile, token);
if (r > 0)
{
unlink(pidfile);
goto retry;
}
else if (r < 0)
goto fail;
fprintf(stderr, "%s: service is already running\n", argv0);
errno = 0;
return -2;
}
/* Write PID to PID file. */
if (dprintf(fd, "%llu\n", (unsigned long long)getpid()) < 0)
goto fail;
/* Done */
if (close(fd) < 0)
if (errno != EINTR)
return -1;
return 0;
fail:
saved_errno = errno;
close(fd);
unlink(pidfile);
errno = saved_errno;
return -1;
}
/**
* Initialise the process
*
* @param full Perform a full initialisation, shall be done
* iff the state is not going to be unmarshalled
* @param preserve Preserve current gamma ramps at priority 0
* @param foreground Keep process in the foreground
* @param keep_stderr Keep stderr open
* @param query Was -q used, see `main` for description
* @return 1: Success
* 2: Normal failure
* 3: Libgamma failure
* 4: The service is already running
* Otherwise: The negative of the exit value the
* process should have and shall exit immediately
*/
static int initialise(int full, int preserve, int foreground, int keep_stderr, int query)
{
struct sockaddr_un address;
struct rlimit rlimit;
size_t i, j, n, n0;
sigset_t mask;
char* sitename_dup = NULL;
int r;
/* Zero out some memory so it can be destoried safely. */
memset(&site, 0, sizeof(site));
if (full && !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 (r = 1; r < _NSIG; r++)
signal(r, 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 1;
/* Get site */
if (sitename != NULL)
if (!(sitename_dup = memdup(sitename, strlen(sitename) + 1)))
goto fail;
if ((gerror = libgamma_site_initialise(&site, method, sitename_dup)))
goto fail_libgamma;
if (full)
{
/* Get PID file and socket pathname */
if (!(pidpath = get_pidfile_pathname()))
goto fail;
if (!(socketpath = get_socket_pathname()))
goto fail;
/* Create PID file */
if ((r = create_pidfile(pidpath)) < 0)
{
free(pidpath), pidpath = NULL;
if (r == -2)
goto already_running;
goto fail;
}
}
/* Get partitions */
if (site.partitions_available)
if (!(partitions = calloc(site.partitions_available, sizeof(*partitions))))
goto fail;
for (i = 0; i < site.partitions_available; i++)
{
if ((gerror = libgamma_partition_initialise(partitions + i, &site, i)))
goto fail_libgamma;
outputs_n += partitions[i].crtcs_available;
}
/* Get CRTC:s */
if (outputs_n)
if (!(crtcs = calloc(outputs_n, sizeof(*crtcs))))
goto fail;
for (i = 0, j = n = 0; i < site.partitions_available; i++)
for (n0 = n, n += partitions[i].crtcs_available; j < n; j++)
if ((gerror = libgamma_crtc_initialise(crtcs + j, partitions + i, j - n0)))
goto fail_libgamma;
/* Get CRTC information */
if (outputs_n)
if (!(outputs = calloc(outputs_n, sizeof(*outputs))))
goto fail;
for (i = 0; i < outputs_n; i++)
{
libgamma_crtc_information_t info;
int saved_errno;
libgamma_get_crtc_information(&info, crtcs + i,
LIBGAMMA_CRTC_INFO_EDID |
LIBGAMMA_CRTC_INFO_MACRO_RAMP |
LIBGAMMA_CRTC_INFO_GAMMA_SUPPORT |
LIBGAMMA_CRTC_INFO_CONNECTOR_NAME);
outputs[i].depth = info.gamma_depth_error ? 0 : info.gamma_depth;
outputs[i].red_size = info.gamma_size_error ? 0 : info.red_gamma_size;
outputs[i].green_size = info.gamma_size_error ? 0 : info.green_gamma_size;
outputs[i].blue_size = info.gamma_size_error ? 0 : info.blue_gamma_size;
outputs[i].supported = info.gamma_support_error ? 0 : info.gamma_support;
if (outputs[i].depth == 0 || outputs[i].red_size == 0 ||
outputs[i].green_size == 0 || outputs[i].blue_size == 0)
outputs[i].supported = 0;
outputs[i].name = get_crtc_name(&info, crtcs + i);
saved_errno = errno;
outputs[i].crtc = crtcs + i;
libgamma_crtc_information_destroy(&info);
outputs[i].ramps_size = outputs[i].red_size + outputs[i].green_size + outputs[i].blue_size;
/* outputs[i].ramps_size will be multipled by the stop-size later */
errno = saved_errno;
if (outputs[i].name == NULL)
goto fail;
}
free(crtcs), crtcs = NULL;
/* Sort outputs */
qsort(outputs, outputs_n, sizeof(*outputs), output_cmp_by_name);
/* Load current gamma ramps */
#define LOAD_RAMPS(SUFFIX, MEMBER) \
do \
{ \
libgamma_gamma_ramps##SUFFIX##_initialise(&(outputs[i].saved_ramps.MEMBER)); \
gerror = libgamma_crtc_get_gamma_ramps##SUFFIX(outputs[i].crtc, &(outputs[i].saved_ramps.MEMBER)); \
if (gerror) \
{ \
libgamma_perror(argv0, gerror); \
outputs[i].supported = LIBGAMMA_NO; \
libgamma_gamma_ramps##SUFFIX##_destroy(&(outputs[i].saved_ramps.MEMBER)); \
memset(&(outputs[i].saved_ramps.MEMBER), 0, sizeof(outputs[i].saved_ramps.MEMBER)); \
} \
} \
while (0)
for (i = 0; i < outputs_n; i++)
if (outputs[i].supported != LIBGAMMA_NO)
switch (outputs[i].depth)
{
case 8:
outputs[i].ramps_size *= sizeof(uint8_t);
LOAD_RAMPS(8, u8);
break;
case 16:
outputs[i].ramps_size *= sizeof(uint16_t);
LOAD_RAMPS(16, u16);
break;
case 32:
outputs[i].ramps_size *= sizeof(uint32_t);
LOAD_RAMPS(32, u32);
break;
default:
outputs[i].depth = 64;
/* fall through */
case 64:
outputs[i].ramps_size *= sizeof(uint64_t);
LOAD_RAMPS(64, u64);
break;
case -1:
outputs[i].ramps_size *= sizeof(float);
LOAD_RAMPS(f, f);
break;
case -2:
outputs[i].ramps_size *= sizeof(double);
LOAD_RAMPS(d, d);
break;
}
/* Preserve current gamma ramps at priority=0 if -p */
if (preserve)
for (i = 0; i < outputs_n; i++)
{
struct filter filter = {
.client = -1,
.priority = 0,
.class = NULL,
.lifespan = LIFESPAN_UNTIL_REMOVAL,
.ramps = NULL
};
outputs[i].table_filters = calloc(4, sizeof(*(outputs[i].table_filters)));
outputs[i].table_sums = calloc(4, sizeof(*(outputs[i].table_sums)));
outputs[i].table_alloc = 4;
outputs[i].table_size = 1;
filter.class = memdup(PKGNAME "::" COMMAND "::preserved", sizeof(PKGNAME "::" COMMAND "::preserved"));
if (filter.class == NULL)
goto fail;
filter.ramps = memdup(outputs[i].saved_ramps.u8.red, outputs[i].ramps_size);
if (filter.ramps == NULL)
goto fail;
outputs[i].table_filters[0] = filter;
COPY_RAMP_SIZES(&(outputs[i].table_sums[0].u8), outputs + i);
if (!gamma_ramps_unmarshal(outputs[i].table_sums, outputs[i].saved_ramps.u8.red, outputs[i].ramps_size))
goto fail;
}
if (full)
{
/* Create socket and start listening */
address.sun_family = AF_UNIX;
if (strlen(socketpath) >= sizeof(address.sun_path))
{
errno = ENAMETOOLONG;
goto fail;
}
strcpy(address.sun_path, socketpath);
unlink(socketpath);
if ((socketfd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
goto fail;
if (fchmod(socketfd, S_IRWXU) < 0)
goto fail;
if (bind(socketfd, (struct sockaddr*)(&address), (socklen_t)sizeof(address)) < 0)
goto fail;
if (listen(socketfd, SOMAXCONN) < 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, '/'))
if (!(argv0_real = realpath(argv0, NULL)))
goto fail;
/* Change directory to / to avoid blocking umounting */
if (chdir("/") < 0)
perror(argv0);
}
/* Set up signal handlers */
if (signal(SIGUSR1, sig_reexec) == SIG_ERR)
goto fail;
if (signal(SIGTERM, sig_terminate) == SIG_ERR)
goto fail;
if (signal(SIGRTMIN + 0, sig_connection) == SIG_ERR)
goto fail;
if (signal(SIGRTMIN + 1, sig_connection) == SIG_ERR)
goto fail;
/* Place in the background unless -f */
if (full && (foreground == 0))
{
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_background;
if (notify_rw[1] <= STDERR_FILENO)
if ((notify_rw[1] = dup2atleast(notify_rw[1], STDERR_FILENO + 1)) < 0)
goto fail_background;
switch ((pid = fork()))
{
case -1:
goto fail_background;
case 0:
/* Child */
close(notify_rw[0]), notify_rw[0] = -1;
if (setsid() < 0)
goto fail_background;
switch ((pid = fork()))
{
case -1:
goto fail_background;
case 0:
/* 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_background;
if (dprintf(fd, "%llu\n", (unsigned long long)getpid()) < 0)
goto fail_background;
close(fd), fd = -1;
/* Notify */
if (write(notify_rw[1], &a_byte, 1) <= 0)
goto fail_background;
close(notify_rw[1]);
break;
default:
/* Parent */
return 0;
}
break;
default:
/* Parent */
close(notify_rw[1]), notify_rw[1] = -1;
got = read(notify_rw[0], &a_byte, 1);
if (got < 0)
goto fail_background;
close(notify_rw[0]);
return -(got == 0);
}
goto done_background;
fail_background:
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;
done_background:;
}
else if (full)
{
/* 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 1;
fail:
return 2;
fail_libgamma:
return 3;
already_running:
return 4;
}
/**
* 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)
{
size_t i;
if (full && (socketfd >= 0))
{
shutdown(socketfd, SHUT_RDWR);
close(socketfd);
unlink(socketpath);
}
#define RESTORE_RAMPS(SUFFIX, MEMBER) \
do \
if (outputs[i].saved_ramps.MEMBER.red != NULL) \
{ \
gerror = libgamma_crtc_set_gamma_ramps##SUFFIX(outputs[i].crtc, outputs[i].saved_ramps.MEMBER); \
if (gerror) \
libgamma_perror(argv0, gerror); \
} \
while (0)
if (outputs != NULL)
for (i = 0; i < outputs_n; i++)
{
if (full && (outputs[i].supported != LIBGAMMA_NO))
switch (outputs[i].depth)
{
case 8:
RESTORE_RAMPS(8, u8);
break;
case 16:
RESTORE_RAMPS(16, u16);
break;
case 32:
RESTORE_RAMPS(32, u32);
break;
case 64:
RESTORE_RAMPS(64, u64);
break;
case -1:
RESTORE_RAMPS(f, f);
break;
case -2:
RESTORE_RAMPS(d, d);
break;
default:
break; /* impossible */
}
if (crtcs == NULL)
libgamma_crtc_destroy(outputs[i].crtc + i);
output_destroy(outputs + i);
}
free(outputs);
if (crtcs != NULL)
for (i = 0; i < outputs_n; i++)
libgamma_crtc_destroy(crtcs + i);
free(crtcs);
if (partitions != NULL)
for (i = 0; i < site.partitions_available; i++)
libgamma_partition_destroy(partitions + i);
free(partitions);
libgamma_site_destroy(&site);
free(socketpath);
if (full && (pidpath != NULL))
unlink(pidpath);
free(pidpath);
free(argv0_real);
free(sitename);
}
#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* buf)
{
size_t off = 0, i, n;
char* bs = buf;
if (bs != NULL)
*(int*)(bs + off) = MARSHAL_VERSION;
off += sizeof(int);
if (argv0_real == NULL)
{
if (bs != NULL)
*(bs + off) = '\0';
off += 1;
}
else
{
n = strlen(argv0_real) + 1;
if (bs != NULL)
memcpy(bs + off, argv0_real, n);
off += n;
}
if (bs != NULL)
*(size_t*)(bs + off) = outputs_n;
off += sizeof(size_t);
for (i = 0; i < outputs_n; i++)
off += output_marshal(outputs + i, bs ? bs + off : NULL);
if (bs != NULL)
*(int*)(bs + off) = socketfd;
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 += server_marshal(bs ? bs + off : NULL);
if (bs != NULL)
*(int*)(bs + off) = method;
off += sizeof(int);
if (bs != NULL)
*(int*)(bs + off) = sitename != NULL;
off += sizeof(int);
if (sitename != NULL)
{
n = strlen(sitename) + 1;
if (bs != NULL)
memcpy(bs + off, sitename, n);
off += n;
}
if (bs != NULL)
*(sig_atomic_t*)(bs + off) = connection;
off += sizeof(sig_atomic_t);
return off;
}
/**
* Unmarshal the state of the process
*
* @param buf Buffer with the marshalled data
* @return The number of marshalled bytes, 0 on error
*/
static size_t unmarshal(void* buf)
{
size_t off = 0, i, n;
char* bs = buf;
if (*(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);
if (*(bs + off))
{
off += 1;
n = strlen(bs + off) + 1;
if (!(argv0_real = memdup(bs + off, n)))
return 0;
off += n;
}
else
off += 1;
outputs_n = *(size_t*)(bs + off);
off += sizeof(size_t);
for (i = 0; i < outputs_n; i++)
{
off += n = output_unmarshal(outputs + i, bs + off);
if (n == 0)
return 0;
}
socketfd = *(int*)(bs + off);
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 = server_unmarshal(bs + off);
if (n == 0)
return 0;
method = *(int*)(bs + off);
off += sizeof(int);
if (*(int*)(bs + off))
{
off += sizeof(int);
n = strlen(bs + off) + 1;
if (!(sitename = memdup(bs + off, n)))
return 0;
off += n;
}
else
off += sizeof(int);
connection = *(sig_atomic_t*)(bs + off);
off += sizeof(sig_atomic_t);
return off;
}
#if defined(__clang__)
# pragma GCC diagnostic pop
#endif
/**
* Unmarshal the state of the process and merge with new state
*
* @param statefile The state file
* @return Zero on success, -1 on error
*/
static int unmarshal_and_merge_state(const char* statefile)
{
struct output* new_outputs = outputs;
struct output* old_outputs = NULL;
size_t new_outputs_n = outputs_n;
size_t old_outputs_n = 0;
size_t i, j, k;
void* marshalled = NULL;
int fd = -1, saved_errno;
outputs = NULL;
outputs_n = 0;
fd = open(statefile, O_RDONLY);
if (fd < 0)
goto fail;
if (!(marshalled = nread(fd, &k)))
goto fail;
close(fd), fd = -1;
unlink(statefile), statefile = NULL;
if (unmarshal(marshalled) == 0)
goto fail;
free(marshalled), marshalled = NULL;
old_outputs = outputs, outputs = new_outputs, new_outputs = NULL;
old_outputs_n = outputs_n, outputs_n = new_outputs_n, new_outputs_n = 0;
i = j = new_outputs_n = 0;
while ((i < old_outputs_n) && (j < outputs_n))
{
int cmp = strcmp(old_outputs[i].name, outputs[j].name);
if (cmp <= 0)
new_outputs_n++;
i += cmp >= 0;
j += cmp <= 0;
}
new_outputs_n += outputs_n - j;
new_outputs = calloc(new_outputs_n, sizeof(*new_outputs));
if (new_outputs == NULL)
goto fail;
i = j = k = new_outputs_n = 0;
while ((i < old_outputs_n) && (j < outputs_n))
{
int cmp = strcmp(old_outputs[i].name, outputs[j].name);
int is_same = 0;
if (cmp == 0)
is_same = (old_outputs[i].depth == outputs[j].depth &&
old_outputs[i].red_size == outputs[j].red_size &&
old_outputs[i].green_size == outputs[j].green_size &&
old_outputs[i].blue_size == outputs[j].blue_size);
if (is_same)
{
new_outputs[k] = old_outputs[i];
new_outputs[k].crtc = outputs[j].crtc;
output_destroy(outputs + j);
k++;
}
else if (cmp <= 0)
new_outputs[k++] = outputs[j];
i += cmp >= 0;
j += cmp <= 0;
}
while (j < outputs_n)
new_outputs[k++] = outputs[j++];
outputs = new_outputs;
outputs_n = new_outputs_n;
for (i = 0; i < old_outputs_n; i++)
output_destroy(old_outputs + i);
return 0;
fail:
saved_errno = errno;
if (fd >= 0)
close(fd);
free(marshalled);
for (i = 0; i < old_outputs_n; i++)
output_destroy(old_outputs + i);
free(old_outputs);
errno = saved_errno;
return -1;
}
/**
* 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* methodname = NULL;
char* p;
if (query == 1)
{
switch (method)
{
case LIBGAMMA_METHOD_DUMMY: methodname = "dummy"; break;
case LIBGAMMA_METHOD_X_RANDR: methodname = "randr"; break;
case LIBGAMMA_METHOD_X_VIDMODE: methodname = "vidmode"; break;
case LIBGAMMA_METHOD_LINUX_DRM: methodname = "drm"; break;
case LIBGAMMA_METHOD_W32_GDI: methodname = "gdi"; break;
case LIBGAMMA_METHOD_QUARTZ_CORE_GRAPHICS: methodname = "quartz"; break;
default:
if (printf("%i\n", method) < 0)
return -1;
break;
}
if (!methodname)
if (printf("%s\n", methodname) < 0)
return -1;
}
if (sitename == NULL)
{
sitename = libgamma_method_default_site(method);
if (sitename != NULL)
{
sitename = memdup(sitename, strlen(sitename) + 1);
if (sitename == NULL)
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 (close(STDOUT_FILENO) < 0)
if (errno != EINTR)
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 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, preserve = 0, foreground = 0, keep_stderr = 0, query = 0, r;
char* statefile = NULL;
char* statebuffer = 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 = EARGF(usage());
break;
default:
usage();
}
ARGEND;
if (argc > 0)
usage();
restart: /* If C had comefrom: comefrom reexec_failure; */
switch ((r = initialise(statefile == NULL, preserve, foreground, keep_stderr, query)))
{
case 1:
break;
case 2:
goto fail;
case 3:
goto fail_libgamma;
case 4:
rc = 2;
goto fail;
default:
return -r;
}
if (query)
{
if (print_method_and_site(query) < 0)
goto fail;
goto done;
}
if (statefile != NULL)
{
if (unmarshal_and_merge_state(statefile) < 0)
goto fail;
unlink(statefile), statefile = NULL;
reexec = 0; /* See `if (reexec && !terminate)` */
}
if (main_loop() < 0)
goto fail;
if (reexec && !terminate)
{
size_t buffer_size;
int fd;
/* `reexec = 0;` is done later in case of re-execute failure,
* since it determines whether `statefile` shall be freed. */
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)
abort();
fd = open(statefile, O_CREAT, S_IRUSR | S_IWUSR);
if (fd < 0)
goto fail;
if (nwrite(fd, statebuffer, buffer_size) != buffer_size)
{
perror(argv0);
close(fd);
errno = 0;
goto fail;
}
free(statebuffer), statebuffer = NULL;
if ((close(fd) < 0) && (errno != EINTR))
goto fail;
destroy(0);
execlp(argv0_real ? argv0_real : argv0, argv0, "-#", statefile, preserve ? "-p" : NULL, NULL);
perror(argv0);
fprintf(stderr, "%s: restoring state without re-executing\n", argv0);
free(argv0_real), argv0_real = NULL;
goto restart;
}
rc = 0;
done:
free(statebuffer);
if (statefile)
unlink(statefile);
if (reexec)
free(statefile);
destroy(1);
return rc;
fail:
if (errno != 0)
perror(argv0);
goto done;
fail_libgamma:
libgamma_perror(argv0, gerror);
goto done;
}