aboutsummaryrefslogblamecommitdiffstats
path: root/src/kernel.c
blob: 7030a996088a30916ca695a2e7302ca1b06bea11 (plain) (tree)






























































































































































































































































































































































                                                                                        
/**
 * 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 <http://www.gnu.org/licenses/>.
 */
#include "kernel.h"
#include "state.h"
#include "util.h"

#include <libgamma.h>

#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <errno.h>
#include <fcntl.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>



/**
 * Get the pathname of the runtime file
 * 
 * @param   suffix  The suffix for the file
 * @return          The pathname of the file, `NULL` on error
 */
GCC_ONLY(__attribute__((malloc, nonnull)))
static char* get_pathname(const char* restrict suffix)
{
  const char* restrict rundir = getenv("XDG_RUNTIME_DIR");
  const char* restrict username = "";
  char* name = NULL;
  char* p;
  char* restrict rc;
  struct passwd* restrict 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
 */
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
 */
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
 */
char* get_state_pathname(void)
{
  return get_pathname(".state");
}


/**
 * Check whether a PID file is outdated
 * 
 * @param   pidpath  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
 */
GCC_ONLY(__attribute__((nonnull)))
static int is_pidfile_reusable(const char* restrict pidpath, const char* restrict 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(pidpath, 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, pidpath);
  errno = 0;
  return -1;
 fail:
  saved_errno = errno;
  free(content);
  if (fd >= 0)
    close(fd);
  errno = saved_errno;
  return -1;
}


/**
 * Create PID file
 * 
 * @param   pidpath  The pathname of the PID file
 * @return           Zero on success, -1 on error,
 *                   -2 if the service is already running
 */
int create_pidfile(char* pidpath)
{
  int fd, r, saved_errno;
  char* p;
  char* restrict token;
  
  /* Create token used to validate the service. */
  token = malloc(sizeof("COOPGAMMAD_PIDFILE_TOKEN=") + strlen(pidpath));
  if (token == NULL)
    return -1;
  sprintf(token, "COOPGAMMAD_PIDFILE_TOKEN=%s", pidpath);
  if (putenv(token))
    return -1;
  
  /* Create PID file's directory. */
  for (p = pidpath; *p == '/'; p++);
  while ((p = strchr(p, '/')))
    {
      *p = '\0';
      if (mkdir(pidpath, 0644) < 0)
	if (errno != EEXIST)
	  return -1;
      *p++ = '/';
    }
  
  /* Create PID file. */
 retry:
  fd = open(pidpath, O_CREAT | O_EXCL, 0644);
  if (fd < 0)
    {
      if (errno == EINTR)
	goto retry;
      if (errno != EEXIST)
	return -1;
      r = is_pidfile_reusable(pidpath, token);
      if (r > 0)
	{
	  unlink(pidpath);
	  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(pidpath);
  errno = saved_errno;
  return -1;
}


/**
 * Create socket and start listening
 * 
 * @param   socketpath  The pathname of the socket
 * @return              Zero on success, -1 on error
 */
int create_socket(const char* socketpath)
{
  struct sockaddr_un address;
  
  address.sun_family = AF_UNIX;
  if (strlen(socketpath) >= sizeof(address.sun_path))
    {
      errno = ENAMETOOLONG;
      return -1;
    }
  strcpy(address.sun_path, socketpath);
  unlink(socketpath);
  if ((socketfd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
    return -1;
  if (fchmod(socketfd, S_IRWXU) < 0)
    return -1;
  if (bind(socketfd, (struct sockaddr*)(&address), (socklen_t)sizeof(address)) < 0)
    return -1;
  if (listen(socketfd, SOMAXCONN) < 0)
    return -1;
  
  return 0;
}


/**
 * Close and unlink the socket
 * 
 * @param  socketpath  The pathname of the socket
 */
void close_socket(const char* socketpath)
{
  if (socketfd >= 0)
    {
      shutdown(socketfd, SHUT_RDWR);
      close(socketfd);
      unlink(socketpath);
    }
}