/** * Copyright © 2015, 2016 Mattias Andrée * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include "common.h" #include #include #include #include /** * The environment. */ extern char **environ; /** * Common code for `preadn` and `pwriten`. * * @param FUN `pread` or `pwrite`. */ #define PIO(FUN) \ ssize_t r, n = 0; \ int saved_errno = 0; \ sigset_t mask, oldmask; \ sigfillset(&mask); \ sigprocmask(SIG_BLOCK, &mask, &oldmask); \ while (nbyte) { \ t (r = FUN(fildes, buffer, nbyte, (off_t)offset), r < 0); \ if (r == 0) \ break; \ n += r; \ buffer += r; \ offset += (size_t)r; \ nbyte -= (size_t)r; \ } \ done: \ sigprocmask(SIG_SETMASK, &oldmask, NULL); \ errno = saved_errno; \ return n; \ fail: \ saved_errno = errno; \ n = -1; \ goto done /** * Wrapper for `pread` that reads the required amount of data. * * @param fildes See pread(3). * @param buf See pread(3). * @param nbyte See pread(3). * @param offset See pread(3). * @return See pread(3), only short if the file is shorter. */ ssize_t preadn(int fildes, void *buf, size_t nbyte, size_t offset) { char *buffer = buf; PIO(pread); } /** * Wrapper for `pwrite` that writes all specified data. * * @param fildes See pwrite(3). * @param buf See pwrite(3). * @param nbyte See pwrite(3). * @param offset See pwrite(3). * @return See pwrite(3). */ ssize_t pwriten(int fildes, const void *buf, size_t nbyte, size_t offset) { const char *buffer = buf; PIO(pwrite); } /** * Unmarshal a `NULL`-terminated string array. * * The elements are not actually copied, subpointers * to `buf` are stored in the returned list. * * @param buf The marshalled array. Must end with a NUL byte. * @param len The length of `buf`. * @param n Output parameter for the number of elements. May be `NULL` * @return The list, `NULL` on error. * * @throws Any exception specified for realloc(3). */ char ** restore_array(char *buf, size_t len, size_t *n) { char **rc = malloc((len + 1) * sizeof(char*)); char **new = NULL; size_t i = 0, e = 0; t (!rc); while (i < len) i += strlen(rc[e++] = buf + i) + 1; if (n) *n = e; rc[e++] = NULL; new = realloc(rc, e * sizeof(char*)); fail: return new ? new : rc; } /** * Create `NULL`-terminate subcopy of an list, * * @param list The list. * @param n The number of elements in the new sublist. * @return The sublist, `NULL` on error. * * @throws Any exception specified for malloc(3). */ char ** sublist(char *const *list, size_t n) { char **rc = malloc((n + 1) * sizeof(char*)); t (!rc); for (rc[n] = NULL; n--; rc[n] = list[n]); fail: return rc; } /** * Create a new open file descriptor for an already * existing file descriptor. * * @param fd The file descriptor that shall be promoted * to a new open file descriptor. * @param oflag See open(3), `O_CREAT` is not allowed. * @return 0 on success, -1 on error. */ int reopen(int fd, int oflag) { char path[sizeof("/dev/fd/") + 3 * sizeof(int)]; int r, saved_errno; sprintf(path, "/dev/fd/%i", fd); if (r = open(path, oflag), r < 0) return -1; if (DUP2_AND_CLOSE(r, fd) == -1) return S(close(r)), -1; return 0; } /** * Run a job or a hook. * * @param job The job. * @param hook The hook, `NULL` to run the job. * @return 0 on success, -1 on error, 1 if the child failed. */ int run_job_or_hook(struct job *job, const char *hook) { pid_t pid; char **args = NULL; char **argv = NULL; char **envp = NULL; size_t argsn; void *new; int status = 0, saved_errno; t (!(args = restore_array(job->payload, job->n, &argsn))); t (!(argv = sublist(args, (size_t)(job->argc)))); t (!(envp = sublist(args + job->argc, argsn - (size_t)(job->argc)))); /* Includes wdir. */ free(args), args = NULL; if (hook) { t (!(new = realloc(argv, ((size_t)(job->argc) + 3) * sizeof(*argv)))); argv = new; memmove(argv + 2, argv, ((size_t)(job->argc) + 1) * sizeof(*argv)); argv[0] = getenv("SAT_HOOK_PATH"); argv[1] = (strstr)(hook, hook); /* strstr: just to remove a warning */ } if (!(pid = fork())) { close(STATE_FILENO), close(BOOT_FILENO), close(REAL_FILENO); (void)(status = chdir(envp[0])); environ = envp + 1; execvp(*argv, argv); exit(1); } t ((pid < 0) || (waitpid(pid, &status, 0) != pid)); fail: S(free(args), free(argv), free(envp)); return status ? 1 : -!!saved_errno; } /** * Removes (and optionally runs) a job. * * @param jobno The job number, `NULL` for any job. * @param runjob Shall we run the job too? 2 if its time has expired (not forced). * @return 0 on success, -1 on error. * * @throws 0 The job is not in the queue. */ int remove_job(const char *jobno, int runjob) { char *end; char *buf = NULL; size_t no = 0, off = sizeof(size_t), n; ssize_t r; struct stat attr; struct job job; struct job *job_full = NULL; int rc = 0, saved_errno = 0; if (jobno) { no = (errno = 0, strtoul)(jobno, &end, 10); if (errno || *end || !isdigit(*jobno)) return 0; } t (flock(STATE_FILENO, LOCK_EX)); t (fstat(STATE_FILENO, &attr)); for (n = (size_t)(attr.st_size); off < n; off += sizeof(job) + job.n) { t (preadn(STATE_FILENO, &job, sizeof(job), off) < (ssize_t)sizeof(job)); if (!jobno || (job.no == no)) goto found_it; } flock(STATE_FILENO, LOCK_UN); /* Failure isn't fatal. */ return errno = 0, -1; found_it: t (!(job_full = malloc(sizeof(job) + job.n))); *job_full = job; t (preadn(STATE_FILENO, job_full->payload, job.n, off + sizeof(job)) < (ssize_t)(job.n)); n -= off + sizeof(job) + job.n; t (!(buf = malloc(n))); t (r = preadn(STATE_FILENO, buf, n, off + sizeof(job) + job.n), r < 0); t (pwriten(STATE_FILENO, buf, (size_t)r, off) < 0); t (ftruncate(STATE_FILENO, (off_t)r + (off_t)off)); free(buf), buf = NULL; fsync(STATE_FILENO); if (runjob) { run_job_or_hook(job_full, runjob == 2 ? "expired" : "forced"); rc = run_job_or_hook(job_full, NULL); saved_errno = errno; run_job_or_hook(job_full, rc ? "failure" : "success"); rc = rc == 1 ? 0 : rc; } else { run_job_or_hook(job_full, "removed"); } free(job_full); flock(STATE_FILENO, LOCK_UN); /* Unlock late so that hooks are synchronised. Failure isn't fatal. */ errno = saved_errno; return rc; fail: S(flock(STATE_FILENO, LOCK_UN), free(buf), free(job_full)); return -1; } /** * Get a `NULL`-terminated list of all queued jobs. * * @return A `NULL`-terminated list of all queued jobs. `NULL` on error. */ struct job ** get_jobs(void) { size_t off = sizeof(size_t), n, j = 0; struct stat attr; struct job **js = NULL; struct job job; int saved_errno; t (flock(STATE_FILENO, LOCK_SH)); t (fstat(STATE_FILENO, &attr)); n = (size_t)(attr.st_size); t (!(js = malloc((n / sizeof(**js) + 1) * sizeof(*js)))); while (off < n) { t (preadn(STATE_FILENO, &job, sizeof(job), off) < (ssize_t)sizeof(job)); off += sizeof(job); t (!(js[j] = malloc(sizeof(job) + job.n))); *(js[j]) = job; t (preadn(STATE_FILENO, js[j++]->payload, job.n, off) < (ssize_t)(job.n)); off += job.n; } t (flock(STATE_FILENO, LOCK_UN)); return js[j] = NULL, js; fail: saved_errno = errno; flock(STATE_FILENO, LOCK_UN); while (j--) free(js[j]); free(js); errno = saved_errno; return NULL; } /** * Duplicate a file descriptor, and * open /dev/null to the old file descriptor. * However, if `old` is 3 or greater, it will * be closed rather than /dev/null. * * @param old The old file descriptor. * @param new The new file descriptor. * @return `new`, -1 on error. */ int dup2_and_null(int old, int new) { int fd = -1, saved_errno; if (old == new) return new; t (DUP2_AND_CLOSE(old, new)); if (old >= 3) return new; t (fd = open("/dev/null", O_RDWR), fd == -1); if (fd == old) return new; t (DUP2_AND_CLOSE(fd, old)); return new; fail: return S(close(fd)), -1; } /** * Create or open the state file. * * @param open_flags Flags (the second parameter) for `open`. * @param state_path Output parameter for the state file's pathname. * May be `NULL`; * @return A file descriptor to the state file, -1 on error. * * @throws 0 `!(open_flags & O_CREAT)` and the file does not exist. */ int open_state(int open_flags, char **state_path) { const char *dir; char *path; int fd = -1, saved_errno; /* Create directory. */ dir = getenv("XDG_RUNTIME_DIR"), dir = (dir ? dir : "/run"); t (!(path = malloc(strlen(dir) * sizeof(char) + sizeof("/" PACKAGE "/state")))); stpcpy(stpcpy(path, dir), "/" PACKAGE "/state"); t (fd = open(path, open_flags, S_IRUSR | S_IWUSR), fd == -1); if (state_path) *state_path = path, path = NULL; else free(path), path = NULL; fail: S(free(path)); if (!(open_flags & O_CREAT) && ((errno == ENOENT) || (errno == ENOTDIR))) errno = 0; return fd; } /** * Let the daemon know that it may need to * update the timers, and perhaps exit. * * @param start Start the daemon if it is not running? * @param name The name of the process. * @return 0 on success, -1 on error. */ int poke_daemon(int start, const char *name) { char *path = NULL; const char *dir; pid_t pid; int fd = -1, status, saved_errno; /* Get the lock file's pathname. */ dir = getenv("XDG_RUNTIME_DIR"), dir = (dir ? dir : "/run"); t (!(path = malloc(strlen(dir) * sizeof(char) + sizeof("/" PACKAGE "/lock")))); stpcpy(stpcpy(path, dir), "/" PACKAGE "/lock"); /* Any daemon listening? */ fd = open(path, O_RDONLY); if (fd == -1) { t ((errno != ENOENT) && (errno != ENOTDIR)); } else { if (flock(fd, LOCK_SH | LOCK_NB /* and LOCK_DRY if that was ever added... */)) t (start = 0, errno != EWOULDBLOCK); else flock(fd, LOCK_UN); t (read(fd, &pid, sizeof(pid)) < (ssize_t)sizeof(pid)); close(fd), fd = -1; } free(path), path = NULL; /* Start daemon if not running, otherwise poke it. */ if (start) { switch ((pid = fork())) { case -1: goto fail; case 0: execl(BINDIR "/satd", BINDIR "/satd", NULL); perror(name); exit(1); default: t (waitpid(pid, &status, 0) != pid); t (errno = 0, status); break; } } else { t (kill(pid, SIGCHLD)); } return 0; fail: return S(close(fd), free(path)), -1; } /** * Construct the pathname for the hook script. * * @param env The environment variable to use for the beginning * of the pathname, `NULL` for the home directory. * @param suffix The rest of the pathname. * @return The pathname. * * @throws 0 The environment variable is not set, or, if `env` is * `NULL` the user is root or homeless. */ static char * hookpath(const char *env, const char *suffix) { struct passwd *pwd; const char *prefix = NULL; char *path; if (env) { prefix = getenv(env); } else if (getuid()) { pwd = getpwuid(getuid()); prefix = pwd ? pwd->pw_dir : NULL; } if (!prefix || !*prefix) return errno = 0, NULL; t (!(path = malloc((strlen(prefix) + strlen(suffix) + 1) * sizeof(char)))); stpcpy(stpcpy(path, prefix), suffix); fail: return path; } /** * Set SAT_HOOK_PATH. * * @return 0 on success, -1 on error. */ int set_hookpath(void) { #define HOOKPATH(PRE, SUF) \ t (path = path ? path : hookpath(PRE, SUF), !path && errno) char *path = NULL; int saved_errno; if (!getenv("SAT_HOOK_PATH")) { HOOKPATH("XDG_CONFIG_HOME", "/sat/hook"); HOOKPATH("HOME", "/.config/sat/hook"); HOOKPATH(NULL, "/.config/sat/hook"); t (setenv("SAT_HOOK_PATH", path ? path : "/etc/sat/hook", 1)); } return free(path), 0; fail: return S(free(path)), -1; }