/* Low-level service primitives and generic API for managing svc_t structures * * Copyright (c) 2008-2010 Claudio Matsuoka * Copyright (c) 2008-2025 Joachim Wiberg * * 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 "config.h" /* Generated by configure script */ #include /* isdigit() */ #include #include #include #include #include #ifdef _LIBITE_LITE # include /* BSD sys/queue.h API */ #else # include /* BSD sys/queue.h API */ #endif #include "finit.h" #include "conf.h" #include "svc.h" #include "helpers.h" #include "pid.h" #include "util.h" #include "cond.h" #include "schedule.h" /* Each svc_t needs a unique job# */ static int jobcounter = 1; static TAILQ_HEAD(, svc) svc_list = TAILQ_HEAD_INITIALIZER(svc_list); static TAILQ_HEAD(, svc) gc_list = TAILQ_HEAD_INITIALIZER(gc_list); /* * Before gc removal of svc, make sure we don't clear an active * condition of a new instance of the svc. */ static void maybe_clear_cond(svc_t *svc) { char ident[MAX_IDENT_LEN]; char buf[MAX_COND_LEN]; const char *cond; int i = 0; svc_ident(svc, ident, sizeof(ident)); while ((cond = svc_cond_nth(svc, i++, buf, sizeof(buf)))) { svc_t *owner = svc_cond_owner(cond, svc); if (owner) { dbg("Not clearing cond %s from gc svc %s, provided by new active service %s", cond, ident, svc_ident(owner, NULL, 0)); continue; } dbg("Cleaning out %s, clearing cond %s ...", ident, cond); cond_clear(cond); } } static void svc_gc(void *arg) { struct timespec now; struct wq *work = (struct wq *)arg; svc_t *svc, *next; clock_gettime(CLOCK_MONOTONIC_COARSE, &now); TAILQ_FOREACH_SAFE(svc, &gc_list, link, next) { int msec; msec = (now.tv_sec - svc->gc.tv_sec) * 1000 + (now.tv_nsec - svc->gc.tv_nsec) / 1000000; if (msec < SVC_TERM_TIMEOUT) continue; TAILQ_REMOVE(&gc_list, svc, link); maybe_clear_cond(svc); free(svc); } if (!TAILQ_EMPTY(&gc_list)) schedule_work(work); } /** * svc_new - Create a new service * @cmd: External program to call * @name: Default or custom svc name * @id: Instance id * @type: Service type, one of service, task, run * * Returns: * A pointer to a new &svc_t object, or %NULL if out of empty slots. */ svc_t *svc_new(char *cmd, char *name, char *id, int type) { svc_t *svc, *iter = NULL; int job = -1; /* Find first job n:o if registering multiple instances */ for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { if (cmd && !strcmp(svc->cmd, cmd)) { job = svc->job; break; } } if (job == -1) job = jobcounter++; svc = calloc(1, sizeof(*svc)); if (!svc) return NULL; svc->type = type; svc->job = job; if (name) strlcpy(svc->name, name, sizeof(svc->name)); if (id && id[0]) strlcpy(svc->id, id, sizeof(svc->id)); if (cmd) strlcpy(svc->cmd, cmd, sizeof(svc->cmd)); /* Default description, if missing */ strlcpy(svc->desc, svc->name, sizeof(svc->desc)); /* Default HALT signal to send */ if (svc_is_tty(svc)) svc->sighalt = SIGHUP; else svc->sighalt = SIGTERM; /* Default delay between SIGTERM and SIGKILL */ svc->killdelay = SVC_TERM_TIMEOUT; TAILQ_INSERT_TAIL(&svc_list, svc, link); return svc; } static struct wq work = { .cb = svc_gc, .delay = SVC_TERM_TIMEOUT }; /** * svc_del - Mark a service object for deletion * @svc: Pointer to an &svc_t object * * Returns: * Always succeeds, returning POSIX OK(0). */ int svc_del(svc_t *svc) { TAILQ_REMOVE(&svc_list, svc, link); TAILQ_INSERT_TAIL(&gc_list, svc, link); clock_gettime(CLOCK_MONOTONIC_COARSE, &svc->gc); schedule_work(&work); return 0; } /** * svc_validate - Check if service asserts same condition as another service * @svc: Pointer to an &svc_t object * * Logs to syslog if there is a name clash with existing services. */ void svc_validate(svc_t *svc) { char ident[MAX_IDENT_LEN]; char cond[MAX_COND_LEN]; svc_t *iter = NULL; svc_t *s; mkcond(svc, cond, sizeof(cond)); svc_ident(svc, ident, sizeof(ident)); for (s = svc_iterator(&iter, 1); s; s = svc_iterator(&iter, 0)) { char c[MAX_COND_LEN]; if (s->removed) continue; if (s == svc) continue; mkcond(s, c, sizeof(c)); if (!string_compare(cond, c)) continue; logit(LOG_WARNING, "%s (%s) asserts the same condition as %s (%s) => %s", svc->cmd, ident, s->cmd, svc_ident(s, NULL, 0), c); } } /** * svc_iterator - Naive iterator over all registered services. * @iter: Iterator, must be a valid pointer * @first: If set, get first &svc_t, otherwise get next * * Returns: * An &svc_t pointer, or %NULL when no more entries can be found. */ svc_t *svc_iterator(svc_t **iter, int first) { svc_t *svc; if (!iter) { errno = EINVAL; return NULL; } if (first) svc = TAILQ_FIRST(&svc_list); else svc = *iter; if (svc) *iter = TAILQ_NEXT(svc, link); return svc; } /** * svc_named_iterator - Iterates over all instances of a service. * @iter: Iterator, must be a valid pointer * @first: If set, get first &svc_t, otherwise get next * @cmd: Service name to look for. * * Returns: * The first matching &svc_t when %NULL is given as argument, otherwise * the next &svc_t with the same @cmd name until the end when %NULL is * returned. */ svc_t *svc_named_iterator(svc_t **iter, int first, char *cmd) { svc_t *svc; for (svc = svc_iterator(iter, first); svc; svc = svc_iterator(iter, 0)) { char *name = svc->name; if (string_compare(name, cmd)) return svc; } return NULL; } /** * svc_job_iterator - Iterates over all instances of a service. * @iter: Iterator, must be a valid pointer * @first: If set, get first &svc_t, otherwise get next * @job: Job to look for. * * Returns: * The first matching &svc_t when %NULL is given as argument, otherwise * the next &svc_t with the same @job ID until the end when %NULL is * returned. */ svc_t *svc_job_iterator(svc_t **iter, int first, int job) { svc_t *svc; for (svc = svc_iterator(iter, first); svc; svc = svc_iterator(iter, 0)) { if (svc->job == job) return svc; } return NULL; } /** * svc_foreach - Run a callback for each registered service * @cb: Callback to run for each service */ void svc_foreach(int (*cb)(svc_t *)) { svc_t *svc, *iter = NULL; if (!cb) return; for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) cb(svc); } /** * svc_foreach_type - Run a callback for each matching type * @types: Mask of service types * @cb: Callback to run for each matching type */ void svc_foreach_type(int types, int (*cb)(svc_t *)) { svc_t *svc, *iter = NULL; if (!cb) return; for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { if (!(svc->type & types)) continue; cb(svc); } } /** * svc_stop_completed - Have all stopped services been collected? * * Returns: * %NULL if all stopped services have been collected, otherwise a * pointer to the first svc_t waiting to be collected. */ svc_t *svc_stop_completed(void) { svc_t *svc, *iter = NULL; for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { if (svc->state == SVC_STOPPING_STATE && svc->pid > 1) return svc; /* Also wait for remain tasks running their post script */ if (svc_is_remain(svc) && svc->state == SVC_TEARDOWN_STATE && svc->pid > 1) return svc; } return NULL; } /** * svc_clean_completed - Have post: and cleanup: scripts finished? * * Called late in runlevel and reload transitions to check if all post: and * cleanup:scripts have completed. A removed service should end up in the * DEAD state, while a service with a post:script ends up in HALTED. * * Returns: * %NULL if post: and cleanup: scripts have run, otherwise a pointer to * the first found svc_t waiting for post: or cleanup:script. */ svc_t *svc_clean_completed(void) { svc_t *svc, *iter = NULL; for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { if (svc_enabled(svc)) continue; if (svc_is_runtask(svc) && svc->state == SVC_DONE_STATE) continue; if (svc_is_removed(svc)) { if (svc->state == SVC_DEAD_STATE) continue; /* any cleanup:script done */ } else { if (svc->state == SVC_HALTED_STATE) continue; /* any post:script done */ } return svc; } return NULL; } /** * svc_find - Find a service object by its full path name * @name: Full path name, e.g., /sbin/syslogd * @id: Optional instance id * * Returns: * A pointer to an &svc_t object, or %NULL if not found. */ svc_t *svc_find(char *name, char *id) { svc_t *svc, *iter = NULL; if (!id) id = ""; for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { if (!strcmp(svc->name, name) && !strcmp(svc->id, id)) return svc; } return NULL; } /** * svc_find_by_str - Find a service object by name:id * @str: name[:id] * * Like svc_find() but takes 'name[:id]' argument * * Returns: * A pointer to an &svc_t object, or %NULL if not found. */ svc_t *svc_find_by_str(const char *str) { char *name, *id; if (!str) return NULL; name = strdupa(str); id = strchr(name, ':'); if (id) *id++ = 0; else id = ""; return svc_find(name, id); } /** * svc_find_by_pid - Find a service object by its PID * @pid: Process ID to match * * Returns: * A pointer to an &svc_t object, or %NULL if not found. */ svc_t *svc_find_by_pid(pid_t pid) { svc_t *svc, *iter = NULL; for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { if (svc->pid == pid) return svc; } return NULL; } /** * svc_cond_nth - Walk the conditions a service owns * @svc: Service object * @i: 0 for the service's own pid/, 1.. for provided ones * @buf: Scratch for the own condition, unused for the rest * @len: Size of @buf * * Returns: * The @i'th condition, or %NULL when @i is past the last one. */ const char *svc_cond_nth(svc_t *svc, int i, char *buf, size_t len) { if (!svc || i < 0) return NULL; if (i == 0) return mkcond(svc, buf, len); if (i > svc->num_provides) return NULL; return svc->provides[i - 1]; } /** * svc_cond_owner - Find the service that owns a condition * @cond: Full condition, e.g. "pid/syslogd" * @skip: Service to leave out of the search, or %NULL * * A service's own pid/ outranks one it merely provides, that is * how Finit tracks the service itself, so every identity is checked * before any provides. * * Returns: * A pointer to an &svc_t object, or %NULL if nobody owns @cond. */ svc_t *svc_cond_owner(const char *cond, svc_t *skip) { svc_t *svc, *iter = NULL; int i; if (!cond || !cond[0]) return NULL; for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { char buf[MAX_COND_LEN]; if (svc == skip) continue; if (string_compare(mkcond(svc, buf, sizeof(buf)), cond)) return svc; } iter = NULL; for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { if (svc == skip) continue; for (i = 0; i < svc->num_provides; i++) { if (string_compare(svc->provides[i], cond)) return svc; } } return NULL; } /** * svc_find_by_cond - Find a service object by a condition it owns * @cond: Full condition, e.g. "pid/syslogd" * * Returns: * A pointer to an &svc_t object, or %NULL if not found. */ svc_t *svc_find_by_cond(const char *cond) { return svc_cond_owner(cond, NULL); } /* * Provides are re-read from the .conf files on every reload, and a * service re-registering must not lose to a claim another service has * not dropped yet, so they all go at once before parsing starts. */ void svc_provides_reset(void) { svc_t *svc, *iter = NULL; for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) svc->num_provides = 0; } /** * svc_find_by_jobid - Find a service object by its JOB:ID * @job: Job n:o * @id: Optional instance id * * Returns: * A pointer to an &svc_t object, or %NULL if not found. */ svc_t *svc_find_by_jobid(int job, char *id) { svc_t *svc, *iter = NULL; if (!id) id = ""; for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { if (svc->job == job && !strcmp(svc->id, id)) return svc; } return NULL; } svc_t *svc_find_by_tty(char *dev) { svc_t *svc, *iter = NULL; /* rescue (notty) shells have no device node */ if (!dev) return NULL; for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { if (!svc_is_tty(svc)) continue; if (!strcmp(dev, svc->dev)) return svc; } return NULL; } /** * svc_find_by_plidfile - Find a service by its PID file * @fn: PID file, can be absolute path or relative to /run * * This function is primarily used by the pidfile plugin to track the * conditions and dependencies of services. * * Note, the PID file is assumed to exist so the contents can be * compared to the the PID of a located service. There may be several * services, in many runlevels, that use the same name for the PID file. * So we won't know for sure until the PID is compared. * * Returns: * A pointer to an &svc_t object, or %NULL if not found. */ svc_t *svc_find_by_pidfile(char *fn) { svc_t *svc, *iter = NULL; pid_t pid; pid = pid_file_read(fn); if (pid > 1) { svc = svc_find_by_pid(pid); if (svc) return svc; } /* * No PID in file, or a sysv service. So it has probably forked * off a chiled that we don't know about. See if we can match * the PID file to a service instead. */ for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { int v = 0; if (svc->pidfile[0] == '!') v = 1; if (strcmp(&svc->pidfile[v], fn)) continue; return svc; } return NULL; } void svc_mark(svc_t *svc) { if (!svc || svc->protect) return; *((int *)&svc->removed) = 1; } /** * svc_mark_dynamic - Mark dynamically loaded services for deletion. * * This function traverses the list of known services, marking all that * have been loaded from /etc/finit.d/ for deletion. If a .conf file * has been removed svc_cleanup_dynamic() will stop and delete the * service. * * This function is called from sighup_cb(). */ void svc_mark_dynamic(void) { svc_t *svc, *iter = NULL; for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) svc_mark(svc); } void svc_mark_dirty(svc_t *svc) { *((int *)&svc->dirty) = 1; } void svc_mark_clean(svc_t *svc) { *((int *)&svc->dirty) = 0; *((int *)&svc->args_dirty) = 0; } void svc_enable(svc_t *svc) { *((int *)&svc->removed) = 0; } /** * svc_clean_dynamic - Stop and cleanup stale services removed from /etc/finit.d * @cb: Callback to run for each stale service * * This function traverses the list of known services to stop and clean * up all non-existing services previously marked by svc_mark_dynamic(). */ void svc_clean_dynamic(void (*cb)(svc_t *)) { svc_t *svc, *iter = NULL; for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { if (svc_is_removed(svc) && cb) cb(svc); } } /** * svc_clean_bootstrap - Remove bootstrap-only services after boot * @svc: Pointer to &svc_t object * * Returns: * %TRUE(1) if object was bootstrap-only, otherwise %FALSE(0) */ int svc_clean_bootstrap(svc_t *svc) { if (!ISOTHER(svc->runlevels, INIT_LEVEL)) { svc->pid = 0; svc_del(svc); return 1; } return 0; } /** * svc_prune_bootstrap - Prune any remaining bootstrap tasks * * This function cleans up all bootstrap tasks that have not yet been * collected, or that never even ran in bootstrap. All other instances * when svc_clean_bootstrap() is called are when a service is collected. */ void svc_prune_bootstrap(void) { svc_t *svc, *iter = NULL; for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { if (!svc->pid) svc_clean_bootstrap(svc); } } /** * svc_enabled - Should the service run? * @svc: Pointer to &svc_t object * * Returns: * 1, if the service is allowed to run in the current runlevel and the * user has not manually requested that this service should not run. 0 * otherwise. */ int svc_enabled(svc_t *svc) { if (!svc) return 0; if (!svc_in_runlevel(svc, runlevel)) { // dbg("Not in runlevel"); return 0; } if (svc_is_removed(svc) || svc_is_blocked(svc)) { // dbg("removed (%d) / blocked (%d)", svc->removed, svc->block); return 0; } if (svc_is_missing(svc)) { // dbg("missing"); return 0; } if (!svc_ifthen(0, NULL, svc->ifstmt, svc->nowarn)) { // dbg("ifthen:%s>", svc->ifstmt[0] ? svc->ifstmt : "<"); return 0; } if (svc_is_tty(svc) && bootstrap) { // dbg("is tty in bootstrap"); return 0; } return 1; } /** * svc_conflicts - Check if svc conflicts with any other running svc * @svc: * * Returns: * %TRUE(1) or %FALSE(0) */ int svc_conflicts(svc_t *svc) { char *ptr, *nm; int rc = 0; if (!svc->conflict[0]) return 0; ptr = strdup(svc->conflict); if (!ptr) return 0; nm = strtok(ptr, ","); while (nm) { svc_t *s; s = svc_find_by_str(nm); if (s) { if (s->state >= SVC_STOPPING_STATE) rc = 1; break; } nm = strtok(NULL, ","); } free(ptr); return rc; } /** * svc_ifthen - Check if: statement in declaration * @is_conf: Set when called by service_mark_unavail() * @ident: svc_t identififcation (name:id) * @stmt: if: statement from .conf file * @quiet: If non-zero, do not warn in syslog * * Returns: * %TRUE(1) yes, use this svc_t, %FALSE(0) prune */ int svc_ifthen(int is_conf, const char *ident, char *stmt, int quiet) { char stmts[MAX_IDENT_LEN]; int not = 0; svc_t *svc; if (!stmt || !stmt[0]) return 1; if (stmt[0] == '<') { enum cond_state cond; char *ptr = ++stmt; int i = 0; if (is_conf) return 1; while (ptr[i] != '>' && ptr[i] != 0) i++; ptr[i] = 0; strlcpy(stmts, stmt, sizeof(stmts)); for (stmt = strtok(stmts, ","); stmt; stmt = strtok(NULL, ","), not = 0) { if (stmt[0] == '!') { stmt++; not++; } cond = cond_get(stmt); if (not && cond == COND_ON) return 0; if (!not && cond == COND_OFF) return 0; } return 1; } if (!is_conf) return 1; strlcpy(stmts, stmt, sizeof(stmts)); for (stmt = strtok(stmts, ","); stmt; stmt = strtok(NULL, ","), not = 0) { if (stmt[0] == '!') { stmt++; not++; } svc = svc_find_by_str(stmt); if (not && svc) { if (!quiet) logit(LOG_NOTICE, "skipping %s, %s already loaded.", ident, svc_ident(svc, NULL, 0)); return 0; } if (!not && !svc) { if (!quiet) logit(LOG_NOTICE, "skipping %s, %s not available.", ident, stmt); return 0; } } return 1; } /* break up "job:id job:id job:id ..." into several "job:id" tokens */ static char *tokstr(char *str, size_t len) { static char *cur = NULL; static size_t pos = 0; char *token; if (!cur && !str) return NULL; if (str) { cur = str; pos = 0; } if (pos >= len) return NULL; token = &cur[pos]; while (pos < len) { if (isspace(cur[pos])) { cur[pos++] = 0; break; } pos++; } return token; } /* * Used by api.c (to start/stop/restart) and initctl.c (for input validation) */ int svc_parse_jobstr(char *str, size_t len, void *user_data, int (*found)(svc_t *, void *), int (not_found)(char *, char *, void *)) { char *input, *token; int result = 0; dbg("Got str:'%s'", str); input = tokstr(str, len); while (input) { char *id = NULL; svc_t *svc, *iter = NULL; char *ptr; dbg("Got token:'%s'", input); token = sanitize(input, strlen(input) + 1); if (!token) { dbg("Sanitation of token:'%s' failed", input); goto next; } ptr = strchr(token, ':'); if (isdigit(token[0])) { char *ep; long job = 0; errno = 0; job = strtol(token, &ep, 10); if ((errno == ERANGE && (job == LONG_MAX || job == LONG_MIN)) || (errno != 0 && job == 0) || (token == ep)) { result++; continue; } if (!ptr) { svc = svc_job_iterator(&iter, 1, job); if (!svc && not_found) result += not_found(NULL, token, user_data); while (svc) { if (found) result += found(svc, user_data); svc = svc_job_iterator(&iter, 0, job); } } else { *ptr++ = 0; id = ptr; svc = svc_find_by_jobid(job, id); if (!svc && not_found) result += not_found(token, id, user_data); else if (found) result += found(svc, user_data); } } else { if (!ptr) { svc = svc_named_iterator(&iter, 1, token); if (!svc && not_found) result += not_found(token, id, user_data); while (svc) { if (found) result += found(svc, user_data); svc = svc_named_iterator(&iter, 0, token); } } else { *ptr++ = 0; id = ptr; svc = svc_find(token, id); if (!svc && not_found) result += not_found(token, id, user_data); else if (found) result += found(svc, user_data); } } next: input = tokstr(NULL, len); } return result; } /** * Local Variables: * indent-tabs-mode: t * c-file-style: "linux" * End: */