/* Configuration frontend: file discovery, format detection, libconfuse parser * * Finit reads two configuration formats from /etc/finit.conf and the * finit.d/ hierarchy, both using the .conf file extension: * * - the new libconfuse block format, parsed here, where all new * features land, and * - the legacy one-liner format, frozen at the Finit 4.x feature * set, parsed by legacy.c * * The format is detected per file by content. Try-parse with * libconfuse first; on success the file is in the new format. On a * parse error, re-parse leniently, accepting any unknown key, to * tell the two failure modes apart: if the lenient parse succeeds * the file is in the new format but has a typo, which is reported * as-is, never fed to the legacy parser. If it fails too, no block * grammar fits and the file goes to the legacy parser. * * Each parsed block is translated to the canonical legacy one-liner * and registered through the same code paths as the legacy format, * so the two cannot drift apart. * * Copyright (c) 2012-2026 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 #include #include #include #ifdef _LIBITE_LITE # include # include /* BSD sys/queue.h API */ #else # include # include /* BSD sys/queue.h API */ #endif #include #include #include #include "finit.h" #include "cond.h" #include "conf.h" #include "devmon.h" #include "iwatch.h" #include "legacy.h" #include "private.h" #include "service.h" #include "helpers.h" #include "util.h" int runlevel = INIT_LEVEL; /* Bootstrap 'S' */ int cfglevel = RUNLEVEL; /* Fallback if no configured runlevel */ int cmdlevel = 0; /* runlevel override from cmdline */ int prevlevel = -1; int debug = 0; /* debug mode from kernel cmdline */ int rescue = 0; /* rescue mode from kernel cmdline */ int single = 0; /* single user mode from kernel cmdline */ int bootstrap = 1; /* set while bootstrapping (for TTYs) */ int kerndebug = 0; /* set if /proc/sys/kernel/printk > 7 */ int syncsec = 0; /* reboot delay */ int wdtreboot = 0; /* reboot via watchdog, default: SOC */ int readiness = SVC_NOTIFY_PID; char *finit_conf= NULL; char *finit_rcsd= NULL; char *fstab = NULL; char *sdown = NULL; char *network = NULL; char *hostname = NULL; char *osheading = NULL; char *runparts = NULL; int runparts_progress; int runparts_sysv; char cgroup_current[CGROUP_NAME_SIZE]; /* cgroup.NAME sets current cgroup for a set of services */ char cgroup_settings_current[CGROUP_SETTINGS_SIZE]; /* cgroup.system,cpu.weight:500 - cgroup settings */ int cgroup_delegate_current; /* cgroup.system,delegate - delegation flag */ struct rlimit initial_rlimit[RLIMIT_NLIMITS]; struct rlimit global_rlimit[RLIMIT_NLIMITS]; struct conf_change { TAILQ_ENTRY(conf_change) link; char *name; }; struct env_entry { TAILQ_ENTRY(env_entry) link; char *name; }; static char cfg_errmsg[256]; static struct iwatch iw_conf; static int iwatch_fd; static uev_t etcw; static TAILQ_HEAD(, env_entry) env_list = TAILQ_HEAD_INITIALIZER(env_list); static TAILQ_HEAD(, conf_change) conf_change_list = TAILQ_HEAD_INITIALIZER(conf_change_list); /* * libconfuse schema -- the new block format * * Long-form keys are canonical, short-form aliases are accepted * systematically. The translators below merge them, canonical key * wins if both are set. */ /* * Common cgroup v2 keys, declared: typed and warning-free. The open- * ended tail (hugetlb..max, rdma..*, newer kernels, ...) * is caught by CFGF_KEYSTRVAL on the section and passed through to * the kernel verbatim, like the legacy parser always did. * * XXX: Workaround for libConfuse <3.4, which logs a spurious "no such * option" per tail key, despite parsing it correctly. Harmless, * cfg_error_cb() buffers it and only a failed parse is reported. */ static cfg_opt_t cgroup_opts[] = { /* * Not cgroupfs files but arguments to parse_cgroup(): the leaf * directory to place the service in, and whether to hand the * subtree over to it. Only meaningful inside a service block. */ CFG_STR ("name", NULL, CFGF_NODEFAULT), CFG_BOOL("delegate", cfg_false, CFGF_NODEFAULT), CFG_STR("cpu.weight", NULL, CFGF_NODEFAULT), CFG_STR("cpu.weight.nice", NULL, CFGF_NODEFAULT), CFG_STR("cpu.max", NULL, CFGF_NODEFAULT), CFG_STR("cpu.max.burst", NULL, CFGF_NODEFAULT), CFG_STR("cpu.idle", NULL, CFGF_NODEFAULT), CFG_STR("cpuset.cpus", NULL, CFGF_NODEFAULT), CFG_STR("cpuset.mems", NULL, CFGF_NODEFAULT), CFG_STR("memory.min", NULL, CFGF_NODEFAULT), CFG_STR("memory.low", NULL, CFGF_NODEFAULT), CFG_STR("memory.high", NULL, CFGF_NODEFAULT), CFG_STR("memory.max", NULL, CFGF_NODEFAULT), CFG_STR("memory.swap.high", NULL, CFGF_NODEFAULT), CFG_STR("memory.swap.max", NULL, CFGF_NODEFAULT), CFG_STR("memory.oom.group", NULL, CFGF_NODEFAULT), CFG_STR("memory.zswap.max", NULL, CFGF_NODEFAULT), CFG_STR("io.weight", NULL, CFGF_NODEFAULT), CFG_STR("io.max", NULL, CFGF_NODEFAULT), CFG_STR("io.latency", NULL, CFGF_NODEFAULT), CFG_STR("pids.max", NULL, CFGF_NODEFAULT), CFG_END() }; /* * Resource limits are a closed set (kernel ABI), so the schema is * fully declared: a typo is a hard parse error. Bare resource name * sets both soft and hard limit, like the legacy two-token form. */ #define RLIMIT_OPTS(pfx) \ CFG_STR(pfx "as", NULL, CFGF_NODEFAULT), \ CFG_STR(pfx "core", NULL, CFGF_NODEFAULT), \ CFG_STR(pfx "cpu", NULL, CFGF_NODEFAULT), \ CFG_STR(pfx "data", NULL, CFGF_NODEFAULT), \ CFG_STR(pfx "fsize", NULL, CFGF_NODEFAULT), \ CFG_STR(pfx "locks", NULL, CFGF_NODEFAULT), \ CFG_STR(pfx "memlock", NULL, CFGF_NODEFAULT), \ CFG_STR(pfx "msgqueue", NULL, CFGF_NODEFAULT), \ CFG_STR(pfx "nice", NULL, CFGF_NODEFAULT), \ CFG_STR(pfx "nofile", NULL, CFGF_NODEFAULT), \ CFG_STR(pfx "nproc", NULL, CFGF_NODEFAULT), \ CFG_STR(pfx "rss", NULL, CFGF_NODEFAULT), \ CFG_STR(pfx "rtprio", NULL, CFGF_NODEFAULT), \ CFG_STR(pfx "rttime", NULL, CFGF_NODEFAULT), \ CFG_STR(pfx "sigpending", NULL, CFGF_NODEFAULT), \ CFG_STR(pfx "stack", NULL, CFGF_NODEFAULT) static cfg_opt_t rlimit_opts[] = { RLIMIT_OPTS(""), RLIMIT_OPTS("soft."), RLIMIT_OPTS("hard."), CFG_END() }; /* environment { KEY = "value" } -- global environment variables */ static cfg_opt_t env_opts[] = { CFG_END() }; /* * log { file = "/var/log/foo" ... } -- where a service's output goes. * /dev/null and /dev/console are spelled as paths, so there is one way * to say it. An empty block means syslog with defaults; `log` cannot * also be a scalar, libconfuse rejects a name declared as both. */ static cfg_opt_t svclog_opts[] = { CFG_STR("file", NULL, CFGF_NODEFAULT), CFG_STR("priority", NULL, CFGF_NODEFAULT), CFG_STR("identity", NULL, CFGF_NODEFAULT), CFG_END() }; /* log { size = 200k count = 5 } -- Finit's own log rotation */ static cfg_opt_t log_opts[] = { CFG_STR("size", NULL, CFGF_NODEFAULT), CFG_INT("count", 0, CFGF_NODEFAULT), CFG_END() }; /* a lifecycle script and the bound on how long it may run */ #define EXEC_OPTS(nm) \ CFG_STR(nm, NULL, CFGF_NODEFAULT), \ CFG_INT(nm "-timeout", 0, CFGF_NODEFAULT) /* service NAME[:ID] { ... }, shared with task/run/sysv */ static cfg_opt_t svc_opts[] = { CFG_STR ("description", NULL, CFGF_NODEFAULT), CFG_STR ("desc", NULL, CFGF_NODEFAULT), /* alias */ CFG_STR ("command", NULL, CFGF_NODEFAULT), CFG_STR ("runlevel", NULL, CFGF_NODEFAULT), CFG_STR_LIST("conditions", NULL, CFGF_NODEFAULT), CFG_STR_LIST("cond", NULL, CFGF_NODEFAULT), /* alias */ CFG_STR ("user", NULL, CFGF_NODEFAULT), CFG_STR ("group", NULL, CFGF_NODEFAULT), CFG_STR_LIST("extra-groups", NULL, CFGF_NODEFAULT), CFG_STR ("envfile", NULL, CFGF_NODEFAULT), CFG_STR ("env", NULL, CFGF_NODEFAULT), /* alias */ CFG_STR ("pidfile", NULL, CFGF_NODEFAULT), CFG_BOOL ("pidfile-create", cfg_false, CFGF_NODEFAULT), CFG_STR ("notify", NULL, CFGF_NODEFAULT), CFG_STR ("type", NULL, CFGF_NODEFAULT), CFG_BOOL ("manual-start", cfg_false, CFGF_NODEFAULT), CFG_BOOL ("manual", cfg_false, CFGF_NODEFAULT), /* alias */ CFG_BOOL ("remain-after-exit", cfg_false, CFGF_NODEFAULT), CFG_BOOL ("remain", cfg_false, CFGF_NODEFAULT), /* alias */ CFG_BOOL ("respawn", cfg_false, CFGF_NODEFAULT), CFG_STR ("restart", NULL, CFGF_NODEFAULT), CFG_INT ("restart-max", 0, CFGF_NODEFAULT), CFG_INT ("restart-sec", 0, CFGF_NODEFAULT), CFG_STR ("oncrash", NULL, CFGF_NODEFAULT), CFG_STR ("reload-signal", NULL, CFGF_NODEFAULT), CFG_BOOL ("required", cfg_true, CFGF_NODEFAULT), CFG_STR ("stop-signal", NULL, CFGF_NODEFAULT), CFG_STR ("halt", NULL, CFGF_NODEFAULT), /* alias */ CFG_INT ("stop-timeout", 0, CFGF_NODEFAULT), CFG_INT ("kill", 0, CFGF_NODEFAULT), /* alias */ /* lifecycle scripts, each with its own bound */ EXEC_OPTS("exec-start-pre"), EXEC_OPTS("exec-start-ready"), EXEC_OPTS("exec-stop"), EXEC_OPTS("exec-stop-post"), EXEC_OPTS("exec-reload"), EXEC_OPTS("exec-cleanup"), CFG_STR_LIST("capabilities", NULL, CFGF_NODEFAULT), CFG_STR_LIST("caps", NULL, CFGF_NODEFAULT), /* alias */ CFG_STR ("runtime-dir", NULL, CFGF_NODEFAULT), CFG_STR ("state-dir", NULL, CFGF_NODEFAULT), CFG_STR ("cache-dir", NULL, CFGF_NODEFAULT), CFG_STR ("logs-dir", NULL, CFGF_NODEFAULT), CFG_STR ("config-dir", NULL, CFGF_NODEFAULT), CFG_INT ("runtime-dir-mode", 0, CFGF_NODEFAULT), CFG_INT ("state-dir-mode", 0, CFGF_NODEFAULT), CFG_INT ("cache-dir-mode", 0, CFGF_NODEFAULT), CFG_INT ("logs-dir-mode", 0, CFGF_NODEFAULT), CFG_INT ("config-dir-mode", 0, CFGF_NODEFAULT), CFG_STR ("runtime-dir-preserve", NULL, CFGF_NODEFAULT), CFG_STR_LIST("conflicts", NULL, CFGF_NODEFAULT), CFG_STR ("if", NULL, CFGF_NODEFAULT), CFG_STR ("tty", NULL, CFGF_NODEFAULT), /* * MULTI is not about repetition here, it is the only way to tell * `log {}` from no log at all: libconfuse instantiates every * non-MULTI section, so cfg_size() would always answer 1. */ CFG_SEC ("log", svclog_opts, CFGF_MULTI), CFG_SEC ("cgroup", cgroup_opts, CFGF_MULTI | CFGF_TITLE | CFGF_KEYSTRVAL), CFG_SEC ("rlimit", rlimit_opts, CFGF_NONE), CFG_END() }; /* tty NAME { ... } -- all three legacy variants */ static cfg_opt_t tty_opts[] = { CFG_STR ("runlevel", NULL, CFGF_NODEFAULT), CFG_STR_LIST("conditions", NULL, CFGF_NODEFAULT), CFG_STR_LIST("cond", NULL, CFGF_NODEFAULT), /* alias */ CFG_STR ("device", NULL, CFGF_NODEFAULT), CFG_INT ("baud", 0, CFGF_NODEFAULT), CFG_STR ("term", NULL, CFGF_NODEFAULT), CFG_BOOL ("noclear", cfg_false, CFGF_NODEFAULT), CFG_BOOL ("nowait", cfg_false, CFGF_NODEFAULT), CFG_BOOL ("nologin", cfg_false, CFGF_NODEFAULT), CFG_STR ("command", NULL, CFGF_NODEFAULT), CFG_BOOL ("notty", cfg_false, CFGF_NODEFAULT), CFG_BOOL ("rescue", cfg_false, CFGF_NODEFAULT), CFG_END() }; static cfg_opt_t conf_opts[] = { CFG_SEC ("service", svc_opts, CFGF_MULTI | CFGF_TITLE), CFG_SEC ("task", svc_opts, CFGF_MULTI | CFGF_TITLE), CFG_SEC ("run", svc_opts, CFGF_MULTI | CFGF_TITLE), CFG_SEC ("sysv", svc_opts, CFGF_MULTI | CFGF_TITLE), CFG_SEC ("tty", tty_opts, CFGF_MULTI | CFGF_TITLE), CFG_SEC ("cgroup", cgroup_opts, CFGF_MULTI | CFGF_TITLE | CFGF_KEYSTRVAL), CFG_SEC ("rlimit", rlimit_opts, CFGF_NONE), CFG_SEC ("environment", env_opts, CFGF_KEYSTRVAL), CFG_SEC ("env", env_opts, CFGF_KEYSTRVAL), /* alias */ CFG_SEC ("log", log_opts, CFGF_NONE), /* static/bootstrap directives */ CFG_INT ("runlevel", 0, CFGF_NODEFAULT), CFG_STR ("hostname", NULL, CFGF_NODEFAULT), CFG_STR_LIST("modules", NULL, CFGF_NODEFAULT), CFG_STR_LIST("mod", NULL, CFGF_NODEFAULT), /* alias */ CFG_STR_LIST("mknod", NULL, CFGF_NODEFAULT), CFG_STR ("network", NULL, CFGF_NODEFAULT), CFG_STR ("rcsd", NULL, CFGF_NODEFAULT), CFG_STR ("runparts", NULL, CFGF_NODEFAULT), CFG_BOOL ("runparts-progress", cfg_false, CFGF_NODEFAULT), CFG_BOOL ("runparts-sysv", cfg_false, CFGF_NODEFAULT), CFG_STR ("readiness", NULL, CFGF_NODEFAULT), CFG_INT ("reboot-delay", 0, CFGF_NODEFAULT), CFG_BOOL ("reboot-watchdog", cfg_false, CFGF_NODEFAULT), CFG_INT ("service-interval", 0, CFGF_NODEFAULT), CFG_STR ("shutdown", NULL, CFGF_NODEFAULT), CFG_FUNC ("include", cfg_include), CFG_END() }; static void drop_changes(void); static int validate_arg(const char *arg, const char *opt) { if (!arg) { errx(1, "option %s missing argument, skipping.", opt); return 1; } return 0; } /* * finit.cond = foo (=> ) * finit.config = /path/to/etc/alt-finit.conf * finit.debug = [on,off] * finit.fstab = /path/to/etc/fstab.aternative * finit.status = [on,off] (compat finit.show_status) * finit.status_style = [old,classic,modern] */ static void parse_finit_opts(char *opt) { char *arg; arg = strchr(opt, '='); if (arg) *arg++ = 0; if (string_compare(opt, "cond")) { if (validate_arg(arg, "finit.cond")) return; cond_boot_parse(arg); return; } if (string_compare(opt, "config")) { if (validate_arg(arg, "finit.config")) return; if (finit_conf) free(finit_conf); finit_conf = strdup(arg); return; } if (string_compare(opt, "debug")) { debug = istrue(arg, 1); return; } if (string_compare(opt, "fstab")) { if (validate_arg(arg, "finit.fstab")) return; if (fstab) free(fstab); fstab = strdup(arg); return; } if (string_compare(opt, "status_style")) { if (validate_arg(arg, "finit.status_style")) return; if (string_compare(arg, "old") || string_compare(arg, "classic")) show_progress(PROGRESS_CLASSIC); else show_progress(PROGRESS_MODERN); return; } if (string_compare(opt, "status") || string_compare(opt, "show_status")) { show_progress(istrue(arg, 1) ? PROGRESS_DEFAULT : PROGRESS_SILENT); return; } } static void parse_fsck_opts(char *opt) { char *arg; arg = strchr(opt, '='); if (arg) *arg++ = 0; if (string_compare(opt, "mode")) { if (validate_arg(arg, "fsck.mode")) return; if (!strcmp(arg, "skip")) fsck_mode = NULL; if (!strcmp(arg, "auto")) fsck_mode = ""; if (!strcmp(arg, "force")) fsck_mode = "-f"; return; } if (string_compare(opt, "repair")) { if (validate_arg(arg, "fsck.repair")) return; if (!strcmp(arg, "no")) fsck_repair = "-n"; if (!strcmp(arg, "preen")) fsck_repair = "-p"; if (!strcmp(arg, "yes")) fsck_repair = "-y"; return; } } /* * When runlevel (single integer) is given on the command line, * it overrides the runlevel in finit.conf and the built-in * default (from configure). It do however have to pass the * same sanity checks. */ static int parse_runlevel(char *arg) { const char *err = NULL; char *ptr = arg; long long num; /* Sanity check the token is just digit(s) */ while (*ptr) { if (!isdigit(*ptr++)) return 0; } num = strtonum(arg, 1, 9, &err); if (err || num == 6) { dbg("Not a valid runlevel (%s), valid levels are [1-9], excluding 6, skipping.", arg); return 0; } return (int)num; } static void parse_arg(char *arg) { if (!strncmp(arg, "finit.", 6)) { parse_finit_opts(&arg[6]); return; } if (!strncmp(arg, "fsck.", 5)) { parse_fsck_opts(&arg[5]); return; } #ifdef RESCUE_MODE if (string_compare(arg, "rescue") || string_compare(arg, "recover")) { rescue = 1; return; } #endif if (string_compare(arg, "single") || string_compare(arg, "S")) { single = 1; return; } /* Put any new command line options before this line. */ cmdlevel = parse_runlevel(arg); } #ifdef KERNEL_CMDLINE /* * Parse /proc/cmdline to find args for init. Don't use this! * * Instead, rely on the kernel to give Finit its arguments as * regular argc + argv[]. Only use this if the system you run * on has a broken initramfs system that cannot forward args * to Finit properly. */ static void parse_kernel_cmdline(void) { char line[LINE_SIZE], *cmdline, *tok; FILE *fp; fp = fopen("/proc/cmdline", "r"); if (!fp) return; if (!fgets(line, sizeof(line), fp)) { fclose(fp); return; } cmdline = chomp(line); dbg("%s", cmdline); while ((tok = strtok(cmdline, " \t"))) { cmdline = NULL; parse_arg(tok); } fclose(fp); } #else #define parse_kernel_cmdline() #endif static void parse_kernel_loglevel(void) { char line[LINE_SIZE], *ptr; FILE *fp; int val; fp = fopen("/proc/sys/kernel/printk", "r"); if (!fp) return; if (!fgets(line, sizeof(line), fp)) { fclose(fp); return; } fclose(fp); ptr = chomp(line); dbg("%s", ptr); val = atoi(ptr); if (val >= 7) kerndebug = 1; } /* * Kernel gives us all non-kernel options on our cmdline */ void conf_parse_cmdline(int argc, char *argv[]) { const char *ptr; /* Set up defaults */ if ((ptr = FINIT_FSTAB)) fstab = strdup(ptr); finit_conf = strdup(FINIT_CONF); finit_rcsd = strdup(FINIT_RCSD); for (int i = 1; i < argc; i++) parse_arg(argv[i]); parse_kernel_cmdline(); parse_kernel_loglevel(); } /* * Sourced mainly by initctl and other Finit helper tools */ void conf_saverc(void) { FILE *fp; mkpath(_PATH_VARRUN "finit", 0755); fp = fopen(_PATH_VARRUN "finit/.initrc", "w"); if (!fp) { err(1, "failed creating .finitrc"); return; } fprintf(fp, "FINIT_CONF=%s\n", finit_conf); fprintf(fp, "FINIT_RCSD=%s\n", finit_rcsd); fprintf(fp, "FINIT_CGPATH=%s\n", FINIT_CGPATH); fprintf(fp, "INIT_SOCKET=%s\n", INIT_SOCKET); fprintf(fp, "INIT_MAGIC=0x%08x\n", INIT_MAGIC); fclose(fp); } /* * Called at bootstrap to log execution order (for debug) */ void conf_save_exec_order(svc_t *svc, char *cmdline, int result) { const char *fn = _PATH_VARRUN "finit/exec.order"; const char *ststr = !result ? "[ OK ]" : "[FAIL]"; static char *prepared = NULL; int first = !fexist(fn); FILE *fp; fp = fopen(fn, "a+"); if (!fp) { err(1, "failed writing to %s", fn); return; } if (first) { fprintf(fp, "# Execution order of run/task/services at bootstrap\n"); fprintf(fp, "# ST TYPE COMMAND LINE (DESC)\n"); } if (result == -1) { int len; len = snprintf(NULL, 0, " %-7s %-64s (%s)", svc_typestr(svc), cmdline, svc->desc); if (len > 0) { if (prepared) free(prepared); prepared = malloc(++len); if (!prepared) goto done; snprintf(prepared, len, " %-7s %-64s (%s)", svc_typestr(svc), cmdline, svc->desc); } } else { if (prepared) { memcpy(prepared, ststr, strlen(ststr)); fprintf(fp, "%s\n", prepared); free(prepared); prepared = NULL; } else { fprintf(fp, "%s %-7s %-64s (%s)\n", ststr, svc_typestr(svc), cmdline, svc->desc); } } done: fclose(fp); } /* * Called by plugins and similar that dynamically generate system * services. @body holds the block contents, one indented line per * setting. */ void conf_save_service(int type, const char *name, char *file, const char *fmt, ...) { char fn[256]; va_list ap; svc_t foo; FILE *fp; mkpath(FINIT_RUNPATH_, 0755); snprintf(fn, sizeof(fn), "%s/%s", FINIT_RUNPATH_, file); if (fexist(fn)) warnx("File %s already exists, overwriting.", fn); fp = fopen(fn, "w"); if (!fp) { err(1, "Failed creating %s", fn); return; } foo.type = type; fprintf(fp, "# Generated by finit:%s()\n", __func__); fprintf(fp, "%s %s {\n", svc_typestr(&foo), name); va_start(ap, fmt); vfprintf(fp, fmt, ap); va_end(ap); fprintf(fp, "}\n"); fclose(fp); } /* * Clear all environment variables read in parse_env(), they may be * removed now so let the next call to parse_env() restore them. */ void conf_reset_env(void) { struct env_entry *node, *tmp; TAILQ_FOREACH_SAFE(node, &env_list, link, tmp) { TAILQ_REMOVE(&env_list, node, link); if (node->name) { unsetenv(node->name); free(node->name); } free(node); } if (!getenv("PATH")) setenv("PATH", _PATH_STDPATH, 1); if (!getenv("SHELL")) setenv("SHELL", _PATH_BSHELL, 1); setenv("LOGNAME", "root", 1); setenv("USER", "root", 1); } /* * Sets, and makes a note of, all KEY=VALUE lines in a given .conf line * from finit.conf, or other .conf file. Note, PATH is always reset in * the conf_reset_env() function. */ void conf_set_env(char *line) { struct env_entry *node; char *key, *val; key = conf_parse_env(line, &val); if (!key) return; dbg("Global env '%s'='%s'", key, val); setenv(key, val, 1); node = malloc(sizeof(*node)); if (!node) { nomem: err(1, "Out of memory cannot track env vars"); return; } node->name = strdup(key); if (!node->name) { free(node); goto nomem; } TAILQ_INSERT_HEAD(&env_list, node, link); } /** * conf_parse_env - Parse a key=value line * @line: Line buffer without newline * @value: Whitespace trimmed value * * Used by service.c:source_env() and * * Returns: * %NULL on error, otherwise a whitespace trimmed key. */ char *conf_parse_env(char *line, char **value) { char *key, *val, *end; /* skip any leading whitespace */ key = line; while (isspace(*key)) key++; /* find end of line */ end = key; while (*end) end++; /* strip trailing whitespace */ if (end > key) { end--; while (isspace(*end)) *end-- = 0; } val = strchr(key, '='); if (!val) return NULL; *val++ = 0; /* strip leading whitespace from value */ while (isspace(*val)) val++; /* unquote value, if quoted */ unquote(&val, end); *value = val; /* find end of key */ end = key; while (*end) end++; /* strip trailing whitespace */ if (end > key) { end--; while (isspace(*end)) *end-- = 0; } /* strip any leading 'set ' */ end = key; if (!strncmp(key, "set", 3)) end += 3; /* check key, no spaces allowed */ while (*end && isspace(*end)) end++; key = end; while (*end && !isspace(*end)) end++; if (*end != 0) { warnx("'%s=%s': not a valid identifier", key, val); return NULL; /* invalid key */ } return key; } /* First form: `rlimit RESOURCE LIMIT` */ void conf_parse_rlimit(char *line, struct rlimit arr[]) { char *level, *limit, *val; int resource = -1; rlim_t cfg; level = strtok(line, " \t"); if (!level) goto error; limit = strtok(NULL, " \t"); if (!limit) goto error; val = strtok(NULL, " \t"); if (!val) { /* Second form: `rlimit RESOURCE LIMIT` */ val = limit; limit = level; level = "both"; } resource = str2rlim(limit); if (resource < 0 || resource > RLIMIT_NLIMITS) goto error; /* Official keyword from v3.1 is `unlimited`, from prlimit(1) */ if (!strcmp(val, "unlimited") || !strcmp(val, "infinity")) { cfg = RLIM_INFINITY; } else { const char *err = NULL; cfg = strtonum(val, 0, (long long)2 << 31, &err); if (err) { logit(LOG_WARNING, "rlimit: invalid %s value: %s", rlim2str(resource), val); return; } } if (!strcmp(level, "soft")) arr[resource].rlim_cur = cfg; else if (!strcmp(level, "hard")) arr[resource].rlim_max = cfg; else if (!strcmp(level, "both")) arr[resource].rlim_max = arr[resource].rlim_cur = cfg; else goto error; return; error: logit(LOG_WARNING, "rlimit: parse error"); } /* Convert optional "[!123456789S]" string into a bitmask */ int conf_parse_runlevels(const char *runlevels) { int i, not = 0, bitmask = 0; if (!runlevels) runlevels = "[234]"; i = 1; while (i) { int level; char lvl = runlevels[i++]; if (']' == lvl || 0 == lvl) break; if ('!' == lvl) { not = 1; bitmask = 0x7FE; continue; } if ('s' == lvl || 'S' == lvl) level = INIT_LEVEL; else level = lvl - '0'; if (level > INIT_LEVEL || level < 0) continue; if (not) CLRBIT(bitmask, level); else SETBIT(bitmask, level); } return bitmask; } void conf_parse_cond(svc_t *svc, char *cond) { size_t i = 0; char *ptr; char *c; if (!svc) { errx(1, "Invalid service pointer"); return; } /* By default we assume UNIX daemons support SIGHUP */ if (svc_is_daemon(svc)) svc->sighup = 1; if (!cond) { memset(svc->cond, 0, sizeof(svc->cond)); return; } /* * First character must be '!' if: * - service: SIGHUP is not supported * - run/task: Do not block bootstrap */ ptr = cond; if (ptr[i] == '!') { ptr++; if (svc_is_runtask(svc)) { /* see service_runtask_clean() */ svc->sighup = 1; svc->once = 1; } else { svc->sighup = 0; } } while (ptr[i] != '>' && ptr[i] != 0) i++; ptr[i] = 0; if (i >= sizeof(svc->cond)) { logit(LOG_WARNING, "%s: too long list of conditions: %s", svc_ident(svc, NULL, 0), ptr); return; } /* * The '~' prefix means a reload of the upstream service is * propagated to this service -- it will be reloaded (SIGHUP) * or restarted (noreload), not just paused and resumed. * Syntax: or <~pid/foo> * * Strip '~' from each condition and set the service-level * flag. This allows '~' on any condition in the list. */ svc->cond[0] = 0; for (i = 0, c = strtok(ptr, ","); c; c = strtok(NULL, ","), i++) { if (c[0] == '~') { c++; svc->flux_reload = 1; } devmon_add_cond(c); if (i) strlcat(svc->cond, ",", sizeof(svc->cond)); strlcat(svc->cond, c, sizeof(svc->cond)); } } /* * Try-parse error capture. Diagnostics are buffered, not printed: * on fallback to the legacy parser they are only debug logged. The * last message wins -- the parser stops at the first fatal error, so * the last callback before failure is the fatal one. */ static void cfg_error_cb(cfg_t *cfg, const char *fmt, va_list ap) { char msg[128]; vsnprintf(msg, sizeof(msg), fmt, ap); if (cfg && cfg->filename && cfg->line) snprintf(cfg_errmsg, sizeof(cfg_errmsg), "%s:%d: %s", cfg->filename, cfg->line, msg); else if (cfg && cfg->filename) snprintf(cfg_errmsg, sizeof(cfg_errmsg), "%s: %s", cfg->filename, msg); else strlcpy(cfg_errmsg, msg, sizeof(cfg_errmsg)); } /* discard diagnostics, the lenient try-parse only needs its verdict */ static void cfg_error_quiet(cfg_t *cfg, const char *fmt, va_list ap) { } /* * Alias helpers: canonical key wins, alias accepted. */ /* which of KEY or ALIAS the user actually set, canonical wins */ static const char *sec_key(cfg_t *sec, const char *key, const char *alias) { if (cfg_size(sec, key)) return key; if (alias && cfg_size(sec, alias)) return alias; return NULL; } static const char *sec_getstr(cfg_t *sec, const char *key, const char *alias) { key = sec_key(sec, key, alias); return key ? cfg_getstr(sec, key) : NULL; } static int sec_getint(cfg_t *sec, const char *key, const char *alias, long *val) { key = sec_key(sec, key, alias); if (!key) return 0; *val = cfg_getint(sec, key); return 1; } static char *sec_getlist(cfg_t *sec, const char *key, const char *alias, char *buf, size_t len) { unsigned int i, num; if (!cfg_size(sec, key)) { if (!alias || !cfg_size(sec, alias)) return NULL; key = alias; } buf[0] = 0; num = cfg_size(sec, key); for (i = 0; i < num; i++) { if (i) strlcat(buf, ",", len); strlcat(buf, cfg_getnstr(sec, key, i), len); } return buf; } static int sec_getbool(cfg_t *sec, const char *key, const char *alias) { key = sec_key(sec, key, alias); if (!key) return 0; return cfg_getbool(sec, key) == cfg_true; } /* * Append token to the one-liner being built, space separated. */ static void addtok(char *line, size_t len, const char *fmt, ...) { char buf[512]; va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); if (line[0]) strlcat(line, " ", len); strlcat(line, buf, len); } /* like addtok() but quotes values containing whitespace */ static void addopt(char *line, size_t len, const char *opt, const char *val) { if (strpbrk(val, " \t")) addtok(line, len, "%s\"%s\"", opt, val); else addtok(line, len, "%s%s", opt, val); } /* * cgroup NAME { key = val ... } -> "NAME" + "key:val,key:val" * Free-form (KEYSTRVAL) and declared options enumerate the same way. */ static char *cgroup_settings(cfg_t *cg, char *buf, size_t len, char *file, int svc) { cfg_opt_t *opt; buf[0] = 0; /* * delegate is a flag, so it carries no value, and a top-level * group has nothing to delegate to. */ if (cfg_size(cg, "delegate")) { if (!svc) logit(LOG_WARNING, "%s: cgroup %s: delegate only applies" " inside a service, ignoring", file, cfg_title(cg)); else if (cfg_getbool(cg, "delegate") == cfg_true) strlcat(buf, "delegate", len); } if (!svc && cfg_size(cg, "name")) logit(LOG_WARNING, "%s: cgroup %s: name only applies inside a" " service, ignoring", file, cfg_title(cg)); for (opt = cg->opts; opt && opt->name; opt++) { const char *val; if (opt->type != CFGT_STR || !cfg_opt_size(opt)) continue; val = cfg_opt_getnstr(opt, 0); if (!val) continue; if (!svc && !strcmp(opt->name, "name")) continue; if (buf[0]) strlcat(buf, ",", len); strlcat(buf, opt->name, len); strlcat(buf, ":", len); strlcat(buf, val, len); } return buf; } /* * rlimit { nofile = 1024 hard.core = unlimited } -> conf_parse_rlimit() * Bare resource name sets both soft and hard limit. */ static void rlimit_translate(cfg_t *sec, struct rlimit arr[]) { cfg_opt_t *opt; for (opt = sec->opts; opt && opt->name; opt++) { const char *name = opt->name, *level = "both"; char line[64]; if (opt->type != CFGT_STR || !cfg_opt_size(opt)) continue; if (!strncmp(name, "soft.", 5)) { level = "soft"; name += 5; } else if (!strncmp(name, "hard.", 5)) { level = "hard"; name += 5; } snprintf(line, sizeof(line), "%s %s %s", level, name, cfg_opt_getnstr(opt, 0)); conf_parse_rlimit(line, arr); } } /* * exec-NAME [+ exec-NAME-timeout] -> legacy "tok:[SEC,]script" */ static void addscript(char *line, size_t len, cfg_t *sec, const char *key, const char *tok) { char tmo[64]; const char *str; char buf[512]; str = sec_getstr(sec, key, NULL); if (!str) return; snprintf(tmo, sizeof(tmo), "%s-timeout", key); if (cfg_size(sec, tmo)) snprintf(buf, sizeof(buf), "%ld,%s", cfg_getint(sec, tmo), str); else strlcpy(buf, str, sizeof(buf)); addopt(line, len, tok, buf); } /* * log { file = "..." priority = "..." identity = "..." } -> legacy * "log" or "log:file,prio:P,tag:I". An empty block is syslog with * defaults; /dev/null and /dev/console arrive here as a file. */ static void addlog(char *line, size_t len, cfg_t *sec, char *file) { unsigned int num = cfg_size(sec, "log"); char buf[512] = ""; const char *str; cfg_t *log; if (!num) return; log = cfg_getnsec(sec, "log", num - 1); if (!log) return; if (num > 1) logit(LOG_WARNING, "%s: %s declares %u log blocks, using the last", file, cfg_title(sec), num); if ((str = sec_getstr(log, "file", NULL))) strlcpy(buf, str, sizeof(buf)); if ((str = sec_getstr(log, "priority", NULL))) { if (buf[0]) strlcat(buf, ",", sizeof(buf)); strlcat(buf, "prio:", sizeof(buf)); strlcat(buf, str, sizeof(buf)); } if ((str = sec_getstr(log, "identity", NULL))) { if (buf[0]) strlcat(buf, ",", sizeof(buf)); strlcat(buf, "tag:", sizeof(buf)); strlcat(buf, str, sizeof(buf)); } if (buf[0]) addopt(line, len, "log:", buf); else addtok(line, len, "log"); } /* * Translate service/task/run/sysv section to the canonical legacy * one-liner and register through the same path as legacy files. */ /* * if = "udevd" asks whether a service by that name is known, answered * when the .conf is read, while if = "usr/foo" tests a condition, * answered at runtime. The namespace separator tells the two apart, * so the legacy angle brackets are not needed. A statement must be * all of one kind; the two are not checked at the same time, so they * cannot be combined. */ static int if_translate(const char *str, char *buf, size_t len, char *file, const char *ident) { char tmp[MAX_IDENT_LEN]; char *op, *save; int cond = -1; if (strpbrk(str, "<>")) { logit(LOG_ERR, "%s: %s: if: angle brackets are not used in this" " format, got '%s', skipping", file, ident, str); return -1; } if (strlcpy(tmp, str, sizeof(tmp)) >= sizeof(tmp)) { logit(LOG_ERR, "%s: %s: if: '%s' is too long, skipping", file, ident, str); return -1; } for (op = strtok_r(tmp, ",", &save); op; op = strtok_r(NULL, ",", &save)) { int is_cond; if (op[0] == '!') op++; if (!op[0]) continue; is_cond = strchr(op, '/') != NULL; if (cond == -1) cond = is_cond; else if (cond != is_cond) { logit(LOG_ERR, "%s: %s: if: cannot mix a service name with a" " condition in '%s', a statement must be all of one" " kind, skipping", file, ident, str); return -1; } } if (cond == -1) { logit(LOG_ERR, "%s: %s: if: no condition or service name in '%s'," " skipping", file, ident, str); return -1; } if (cond) snprintf(buf, len, "<%s>", str); else strlcpy(buf, str, len); return 0; } /* * These have no legacy token, they are validated by service_set_dir() * and stored directly on the registered svc. Empty means unset, * service_register() has already cleared the fields. */ static void dirs_translate(cfg_t *sec, svc_t *svc, char *file) { const char *str; int i; for (i = 0; i < NUM_SVCDIRS; i++) { char key[32]; long num; str = sec_getstr(sec, svcdirs[i].key, NULL); if (str && str[0]) { if (service_set_dir(svc, &svcdirs[i], str)) logit(LOG_ERR, "%s: %s: %s '%s' %s, ignoring", file, cfg_title(sec), svcdirs[i].key, str, errno == ENAMETOOLONG ? "is too long" : "must be relative, no '..'"); } snprintf(key, sizeof(key), "%s-mode", svcdirs[i].key); if (sec_getint(sec, key, NULL, &num)) { if (num & ~07777L) logit(LOG_ERR, "%s: %s: %s %04lo is not a valid" " mode, ignoring. Modes are octal," " with the leading zero", file, cfg_title(sec), key, num); else svc->dir_mode[i] = (mode_t)num; } } if ((str = sec_getstr(sec, "runtime-dir-preserve", NULL))) { if (!strcmp(str, "no") || !strcmp(str, "false")) svc->dir_preserve = SVC_DIR_PRESERVE_NO; else if (!strcmp(str, "restart")) svc->dir_preserve = SVC_DIR_PRESERVE_RESTART; else if (!strcmp(str, "yes") || !strcmp(str, "true")) svc->dir_preserve = SVC_DIR_PRESERVE_YES; else logit(LOG_ERR, "%s: %s: runtime-dir-preserve '%s' is not" " no, restart, or yes, using no", file, cfg_title(sec), str); } } static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *file) { struct rlimit local_rlimit[RLIMIT_NLIMITS]; char line[LINE_SIZE] = ""; const char *str, *cmd, *grp; int nowarn, own, bang; unsigned int cnt; long num; char buf[512]; char nm[80]; svc_t *svc; char *id; cmd = sec_getstr(sec, "command", NULL); if (!cmd) { logit(LOG_ERR, "%s: section '%s' missing command, skipping", file, cfg_title(sec)); return; } /* a leading - tolerates a missing binary, cf. systemd ExecStart=- */ nowarn = cmd[0] == '-'; if (nowarn) cmd++; if ((str = sec_getstr(sec, "runlevel", NULL))) addtok(line, sizeof(line), "[%s]", str); /* * The ! leading a legacy condition list is not a condition at * all, and it means two unrelated things depending on the block * it sits in. Each has its own key here, valid only where the * meaning applies, and both translate back to that same !. */ bang = 0; if (type == SVC_TYPE_RUN || type == SVC_TYPE_TASK) { if (cfg_size(sec, "required") && !sec_getbool(sec, "required", NULL)) bang = 1; /* do not hold up bootstrap */ if (cfg_size(sec, "reload-signal")) logit(LOG_WARNING, "%s: %s: reload-signal does not apply" " to run or task, ignoring", file, cfg_title(sec)); } else { if ((str = sec_getstr(sec, "reload-signal", NULL))) { /* * Only SIGHUP and none survive the trip through a * legacy line, which carries the flag and not the * signal. */ if (!strcasecmp(str, "none")) bang = 1; else if (str2sig((char *)str) != SIGHUP) logit(LOG_WARNING, "%s: %s: reload-signal is SIGHUP" " or none, ignoring '%s'", file, cfg_title(sec), str); } if (cfg_size(sec, "required")) logit(LOG_WARNING, "%s: %s: required only applies to run" " or task, ignoring", file, cfg_title(sec)); } if (sec_getlist(sec, "conditions", "cond", buf, sizeof(buf))) { if (buf[0] == '!') logit(LOG_WARNING, "%s: %s: '!' in conditions is not" " supported, use reload-signal or required", file, cfg_title(sec)); addtok(line, sizeof(line), "<%s%s>", bang ? "!" : "", buf); } else if (bang) addtok(line, sizeof(line), ""); /* section title is the identity: NAME[:ID], %i in templates */ strlcpy(nm, cfg_title(sec), sizeof(nm)); id = strchr(nm, ':'); if (id) *id++ = 0; addtok(line, sizeof(line), "name:%s", nm); if (id && *id) addtok(line, sizeof(line), ":%s", id); /* @user[:group[,extra,...]] */ str = sec_getstr(sec, "user", NULL); grp = sec_getstr(sec, "group", NULL); if (sec_getlist(sec, "extra-groups", NULL, buf, sizeof(buf))) addtok(line, sizeof(line), "@%s:%s,%s", str ? str : "root", grp ? grp : "root", buf); else if (grp) addtok(line, sizeof(line), "@%s:%s", str ? str : "root", grp); else if (str) addtok(line, sizeof(line), "@%s", str); if ((str = sec_getstr(sec, "envfile", "env"))) addopt(line, sizeof(line), "env:", str); addlog(line, sizeof(line), sec, file); /* * The daemon owns its pidfile unless told otherwise, so the * legacy ! is the default here and pidfile-create drops it. A * daemon-owned default path needs no token, that is what the * pidfile plugin discovers on its own. */ str = sec_getstr(sec, "pidfile", NULL); own = sec_getbool(sec, "pidfile-create", NULL); if (str && strcmp(str, "true") && strcmp(str, "false")) addopt(line, sizeof(line), own ? "pid:" : "pid:!", str); else if (own) addtok(line, sizeof(line), "pid"); if ((str = sec_getstr(sec, "notify", NULL))) addtok(line, sizeof(line), "notify:%s", str); if ((str = sec_getstr(sec, "type", NULL))) { if (!strcmp(str, "forking")) addtok(line, sizeof(line), "type:forking"); else logit(LOG_WARNING, "%s: unknown type '%s', ignoring", file, str); } if (sec_getbool(sec, "manual-start", "manual")) addtok(line, sizeof(line), "manual:yes"); if (sec_getbool(sec, "remain-after-exit", "remain")) addtok(line, sizeof(line), "remain:yes"); if (sec_getbool(sec, "respawn", NULL)) addtok(line, sizeof(line), "respawn"); if (nowarn) addtok(line, sizeof(line), "nowarn"); /* * restart is the policy, restart-max the count. true is the * default policy and needs no token unless a count came with it. */ str = sec_getstr(sec, "restart", NULL); if (!str) str = "true"; if (!strcmp(str, "never") || !strcmp(str, "false")) addtok(line, sizeof(line), "norestart"); else if (!strcmp(str, "always")) addtok(line, sizeof(line), "restart:always"); else { if (strcmp(str, "true")) logit(LOG_WARNING, "%s: unknown restart '%s', assuming true", file, str); if (sec_getint(sec, "restart-max", NULL, &num)) addtok(line, sizeof(line), "restart:%ld", num); } if (sec_getint(sec, "restart-sec", NULL, &num)) addtok(line, sizeof(line), "restart_sec:%ld", num); if ((str = sec_getstr(sec, "oncrash", NULL))) addtok(line, sizeof(line), "oncrash:%s", str); if ((str = sec_getstr(sec, "stop-signal", "halt"))) addtok(line, sizeof(line), "halt:%s", str); if (sec_getint(sec, "stop-timeout", "kill", &num)) addtok(line, sizeof(line), "kill:%ld", num); addscript(line, sizeof(line), sec, "exec-start-pre", "pre:"); addscript(line, sizeof(line), sec, "exec-start-ready", "ready:"); addscript(line, sizeof(line), sec, "exec-stop", "stop:"); addscript(line, sizeof(line), sec, "exec-stop-post", "post:"); addscript(line, sizeof(line), sec, "exec-reload", "reload:"); addscript(line, sizeof(line), sec, "exec-cleanup", "cleanup:"); if (sec_getlist(sec, "capabilities", "caps", buf, sizeof(buf))) addtok(line, sizeof(line), "caps:%s", buf); if (sec_getlist(sec, "conflicts", NULL, buf, sizeof(buf))) addtok(line, sizeof(line), "conflict:%s", buf); if ((str = sec_getstr(sec, "if", NULL))) { char ifbuf[MAX_IDENT_LEN + 2]; if (if_translate(str, ifbuf, sizeof(ifbuf), file, cfg_title(sec))) return; addopt(line, sizeof(line), "if:", ifbuf); } if ((str = sec_getstr(sec, "tty", NULL))) addtok(line, sizeof(line), "tty:%s", str); /* * cgroup NAME {} to join group, settings become overrides. A * service joins exactly one group, so on duplicates the last * one wins, like a repeated token in a legacy one-liner. */ cnt = cfg_size(sec, "cgroup"); if (cnt) { cfg_t *cg = cfg_getnsec(sec, "cgroup", cnt - 1); if (cnt > 1) logit(LOG_WARNING, "%s: %s declares %u cgroup blocks," " using '%s'", file, cfg_title(sec), cnt, cfg_title(cg)); cgroup_settings(cg, buf, sizeof(buf), file, 1); if (buf[0]) addtok(line, sizeof(line), "cgroup.%s,%s", cfg_title(cg), buf); else addtok(line, sizeof(line), "cgroup.%s", cfg_title(cg)); } /* per-service rlimit {} on top of the file/global limits */ if (cfg_size(sec, "rlimit")) { memcpy(local_rlimit, rlimit, sizeof(local_rlimit)); rlimit_translate(cfg_getsec(sec, "rlimit"), local_rlimit); rlimit = local_rlimit; } /* the command and its arguments, verbatim */ addtok(line, sizeof(line), "%s", cmd); if ((str = sec_getstr(sec, "description", "desc"))) addtok(line, sizeof(line), "-- %s", str); dbg("translated: %s", line); svc = service_register(type, line, rlimit, file); if (!svc) return; dirs_translate(sec, svc, file); } /* * Translate tty section, covering all three legacy variants: * device (built-in getty), command (external getty), notty/rescue. */ static void tty_translate(cfg_t *sec, struct rlimit rlimit[], char *file) { char line[LINE_SIZE] = ""; const char *str, *dev, *cmd; char buf[512]; if ((str = sec_getstr(sec, "runlevel", NULL))) addtok(line, sizeof(line), "[%s]", str); if (sec_getlist(sec, "conditions", "cond", buf, sizeof(buf))) addtok(line, sizeof(line), "<%s>", buf); dev = sec_getstr(sec, "device", NULL); cmd = sec_getstr(sec, "command", NULL); if (cmd && cmd[0] == '-') { addtok(line, sizeof(line), "nowarn"); cmd++; } if (dev) { addtok(line, sizeof(line), "%s", dev); if (cfg_size(sec, "baud")) addtok(line, sizeof(line), "%ld", cfg_getint(sec, "baud")); } else if (cmd) { addtok(line, sizeof(line), "%s", cmd); } else { if (sec_getbool(sec, "notty", NULL)) addtok(line, sizeof(line), "notty"); if (sec_getbool(sec, "rescue", NULL)) addtok(line, sizeof(line), "rescue"); if (!line[0]) { logit(LOG_ERR, "%s: tty '%s' missing device, command," " or notty/rescue, skipping", file, cfg_title(sec)); return; } } if (sec_getbool(sec, "noclear", NULL)) addtok(line, sizeof(line), "noclear"); if (sec_getbool(sec, "nowait", NULL)) addtok(line, sizeof(line), "nowait"); if (sec_getbool(sec, "nologin", NULL)) addtok(line, sizeof(line), "nologin"); if (dev && (str = sec_getstr(sec, "term", NULL))) addtok(line, sizeof(line), "%s", str); dbg("translated: tty %s", line); service_register(SVC_TYPE_TTY, line, rlimit, file); } /* * environment { KEY = "val" } -> conf_set_env() */ static void env_translate(cfg_t *cfg, const char *section) { cfg_t *env = cfg_getsec(cfg, section); cfg_opt_t *opt; if (!env) return; for (opt = env->opts; opt && opt->name; opt++) { char buf[LINE_SIZE]; if (opt->type != CFGT_STR || !cfg_opt_size(opt)) continue; snprintf(buf, sizeof(buf), "%s=%s", opt->name, cfg_opt_getnstr(opt, 0)); conf_set_env(buf); } } /* * Static/bootstrap directives, same gating as the legacy parser: * most are only honored while bootstrapping (runlevel S). */ static void conf_parse_statics(cfg_t *cfg) { const char *str, *key; unsigned int i; if (BOOTSTRAP) { if ((str = sec_getstr(cfg, "hostname", NULL))) { if (hostname) free(hostname); hostname = strdup(str); } for (i = 0; i < cfg_size(cfg, "mknod"); i++) { char *dev = cfg_getnstr(cfg, "mknod", i); char cmd[CMD_SIZE]; strlcpy(cmd, "mknod ", sizeof(cmd)); strlcat(cmd, dev, sizeof(cmd)); run_interactive(cmd, "Creating device node %s", dev); } key = sec_key(cfg, "modules", "mod"); for (i = 0; key && i < cfg_size(cfg, key); i++) kmod_load(cfg_getnstr(cfg, key, i)); if ((str = sec_getstr(cfg, "network", NULL))) { if (network) free(network); network = strdup(str); } if ((str = sec_getstr(cfg, "rcsd", NULL))) { if (finit_rcsd) free(finit_rcsd); finit_rcsd = strdup(str); } if ((str = sec_getstr(cfg, "runparts", NULL))) { if (runparts) free(runparts); runparts = strdup(str); runparts_progress = sec_getbool(cfg, "runparts-progress", NULL); runparts_sysv = sec_getbool(cfg, "runparts-sysv", NULL); } if (cfg_size(cfg, "runlevel")) { long lvl = cfg_getint(cfg, "runlevel"); if (lvl < 1 || lvl > 9 || lvl == 6) cfglevel = 2; /* Fallback */ else cfglevel = (int)lvl; } if ((str = sec_getstr(cfg, "readiness", NULL))) { if (!strcmp(str, "none")) readiness = SVC_NOTIFY_NONE; } } /* * Global environment variables, environment {} or env {}. Read * on every reload, not just at bootstrap: conf_reset_env() has * just cleared them, so gating here would drop every variable on * the way into the configured runlevel. See doc/config/env.md */ env_translate(cfg, "environment"); env_translate(cfg, "env"); if (cfg_size(cfg, "log")) { cfg_t *log = cfg_getsec(cfg, "log"); int val; if ((str = sec_getstr(log, "size", NULL))) { val = strtobytes((char *)str); if (val >= 0) logfile_size_max = val; } if (cfg_size(log, "count")) { val = (int)cfg_getint(log, "count"); if (val >= 0) logfile_count_max = val; } } if ((str = sec_getstr(cfg, "shutdown", NULL))) { if (sdown) free(sdown); sdown = strdup(str); } if (cfg_size(cfg, "reboot-delay")) { long val = cfg_getint(cfg, "reboot-delay"); if (val >= 0 && val <= 60) syncsec = (int)val; } if (cfg_size(cfg, "reboot-watchdog")) wdtreboot = sec_getbool(cfg, "reboot-watchdog", NULL); if (cfg_size(cfg, "service-interval")) { long val = cfg_getint(cfg, "service-interval"); /* 0 min to 1 day, should check at least daily */ if (val >= 0 && val <= 1440) { int disabled = !service_interval; /* milliseconds for libuEv timer */ service_interval = (int)val * 1000; if (disabled) service_init(NULL); } } } /* * Sections registered in file order: cfg_t tracks the source line of * each parsed section, sort on it so run/task/service execution order * matches the order of declaration, like the legacy format. */ struct secref { unsigned int line; int type; cfg_t *sec; }; static int secref_cmp(const void *a, const void *b) { const struct secref *sa = a, *sb = b; return (int)sa->line - (int)sb->line; } static int conf_parse_cfg(cfg_t *cfg, char *file, int is_rcsd) { static const struct { const char *name; int type; } map[] = { { "service", SVC_TYPE_SERVICE }, { "task", SVC_TYPE_TASK }, { "run", SVC_TYPE_RUN }, { "sysv", SVC_TYPE_SYSV }, { "tty", SVC_TYPE_TTY }, }; struct rlimit rlimit[RLIMIT_NLIMITS]; struct secref *refs; struct rlimit *arr; unsigned int i, j, num = 0; /* Same per-file defaults as the legacy parser */ if (is_rcsd) { memcpy(rlimit, global_rlimit, sizeof(rlimit)); cgroup_current[0] = 0; arr = rlimit; } else arr = global_rlimit; conf_parse_statics(cfg); /* top-level cgroup definitions */ for (i = 0; i < cfg_size(cfg, "cgroup"); i++) { cfg_t *cg = cfg_getnsec(cfg, "cgroup", i); char buf[512]; cgroup_add((char *)cfg_title(cg), cgroup_settings(cg, buf, sizeof(buf), file, 0), 0); } /* file-scope resource limits, applies to all services in file */ if (cfg_size(cfg, "rlimit")) rlimit_translate(cfg_getsec(cfg, "rlimit"), arr); for (i = 0; i < NELEMS(map); i++) num += cfg_size(cfg, map[i].name); if (!num) return 0; refs = calloc(num, sizeof(*refs)); if (!refs) { err(1, "failed alloc in %s()", __func__); return 1; } for (i = 0, num = 0; i < NELEMS(map); i++) { for (j = 0; j < cfg_size(cfg, map[i].name); j++) { cfg_t *sec = cfg_getnsec(cfg, map[i].name, j); refs[num].line = sec->line; refs[num].type = map[i].type; refs[num].sec = sec; num++; } } qsort(refs, num, sizeof(*refs), secref_cmp); for (i = 0; i < num; i++) { if (refs[i].type == SVC_TYPE_TTY) tty_translate(refs[i].sec, arr, file); else svc_translate(refs[i].sec, refs[i].type, arr, file); } free(refs); return 0; } /* * Substitute every %i in LINE with NAME, for template instantiation. * * Very simple and crude implementation, only supports '%i' */ static char *conf_instantiate(char *line, char *name) { char *ptr, *end = strchr(line, 0); char *pos = line; size_t num = 0; if (!name[0] || !end) return line; while ((ptr = strchr(pos, '%'))) { num++; pos = ptr + 1; } ptr = realloc(line, strlen(line) + num * strlen(name) + 1); if (!ptr) return line; pos = line = ptr; while ((ptr = strchr(pos, '%'))) { if (!strncmp(ptr, "%i", 2)) { char *rest = &ptr[2]; char *next = ptr + strlen(name); memmove(next, rest, strlen(rest) + 1); memcpy(ptr, name, strlen(name)); pos += strlen(name); } else pos++; } return line; } static int conf_is_template(const char *file, char *name, size_t len) { char *ptr, *nm; size_t i = 0; /* the @ convention names the file, a directory may contain one */ ptr = strchr(basenm((char *)file), '@'); if (!ptr) return 0; /* not a template */ nm = ptr + 1; ptr = strstr(nm, ".conf"); if (!strcmp(nm, ".conf") || !ptr) return 1; /* template itself or invalid */ if (!name) return 1; while (nm < ptr && i < len - 1) name[i++] = *nm++; name[i] = 0; return 1; /* instantiated template */ } /* * Parse from FILE, or from BUF when it is set, i.e. for a template * whose %i has already been substituted. * * XXX: Workaround for libConfuse <3.4, whose cfg_parse_buf() replaces * cfg->filename with "[buf]", so every diagnostic from a template * would lose the file name. cfg_parse_fp() keeps a name the * caller has already set, so go through fmemopen() instead. With * a 3.4 floor this is just cfg_parse_buf(cfg, buf). */ static int conf_parse_any(cfg_t *cfg, char *file, char *buf) { FILE *fp; int rc; if (!buf) return cfg_parse(cfg, file); /* fmemopen() rejects a zero length on older GLIBC */ if (!buf[0]) return CFG_SUCCESS; fp = fmemopen(buf, strlen(buf), "r"); if (!fp) return CFG_FILE_ERROR; /* cfg_parse_fp() only names the stream when we have not */ cfg->filename = strdup(file); rc = cfg_parse_fp(cfg, fp); fclose(fp); return rc; } /* * Is this a new-format file, or a legacy one? * * Called only after a strict parse has already failed, to tell a * new-format file with a typo from a legacy one-liner file. The * lenient re-parse accepts any unknown key, so it succeeds on the * former and still fails on the latter -- no legacy directive can * satisfy the block grammar, they all lack the '=' or the '{'. * * Only the verdict is used, the parsed data is discarded: with * CFGF_IGNORE_UNKNOWN libconfuse drops unknown keys instead of * creating them, so a lenient tree is missing every set{} variable * and every free-form cgroup key. */ static int is_new_format(char *file, char *buf) { cfg_t *cfg; int rc; cfg = cfg_init(conf_opts, CFGF_IGNORE_UNKNOWN); if (!cfg) return 0; cfg_set_error_function(cfg, cfg_error_quiet); rc = conf_parse_any(cfg, file, buf); cfg_free(cfg); return rc == CFG_SUCCESS; } /* * Read FILE and substitute %i with NAME, for template instantiation. * Returns a malloc()'ed buffer the caller frees. */ static char *conf_read_template(char *file, char *name) { char *buf; buf = fslurp(NULL, "%s", file); if (!buf) return NULL; return conf_instantiate(buf, name); } /* * Parse one Finit .conf file, in either format. The legacy .conf * include directive routes back through this so included files are * format-detected too. */ int conf_parse_file(char *file, int is_rcsd) { char name[MAX_ID_LEN] = { 0 }; char *buf = NULL; cfg_t *cfg; int rc; /* * Template files (name@.conf, name@id.conf). A bare name@.conf * is the template itself, there is nothing to instantiate from * it. Otherwise %i is substituted over the whole file up front, * so both the format detection below and, on fallback, the * legacy parser see the finished text. */ if (conf_is_template(file, name, sizeof(name))) { if (!name[0]) { dbg("*** Skipping template file %s", file); return 0; } dbg("*** instantiating %s from %s ...", name, file); buf = conf_read_template(file, name); if (!buf) return 1; } cfg = cfg_init(conf_opts, CFGF_NONE); if (!cfg) { rc = legacy_parse_conf(file, buf, is_rcsd); goto done; } cfg_set_error_function(cfg, cfg_error_cb); rc = conf_parse_any(cfg, file, buf); if (rc == CFG_SUCCESS) { dbg("*** Parsing %s (new format)", file); rc = conf_parse_cfg(cfg, file, is_rcsd); cfg_free(cfg); goto done; } cfg_free(cfg); if (rc == CFG_FILE_ERROR) { rc = 1; /* like legacy fopen() failure */ goto done; } if (is_new_format(file, buf)) { logit(LOG_ERR, "parse error: %s", cfg_errmsg); rc = 1; goto done; } dbg("not in new format (%s), falling back to legacy parser", cfg_errmsg); rc = legacy_parse_conf(file, buf, is_rcsd); done: free(buf); return rc; } static void glob_append(glob_t *gl, int append, const char *fmt, ...) { va_list ap; size_t len; char *path; va_start(ap, fmt); len = vsnprintf(NULL, 0, fmt, ap); va_end(ap); path = alloca(++len); if (!path) { warn("failed alloca() in glob_append()"); return; } va_start(ap, fmt); vsnprintf(path, len, fmt, ap); va_end(ap); dbg("conf_reload(): glob %s ...", path); glob(path, append ? GLOB_APPEND : 0, NULL, gl); } /* * Reload /etc/finit.conf and all *.conf in /etc/finit.d/ */ int conf_reload(void) { glob_t gl; size_t i; /* Set time according to current time zone */ tzset(); dbg("Set time daylight: %d timezone: %ld tzname: %s %s", daylight, timezone, tzname[0], tzname[1]); /* Mark and sweep */ cgroup_mark_all(); svc_mark_dynamic(); conf_reset_env(); /* * Reset global rlimit to bootstrap values from conf_init(). */ memcpy(global_rlimit, initial_rlimit, sizeof(global_rlimit)); /* * When built with --disable-rescue mode many other 'if (rescue)' * code paths are #ifdeffed out. This one, and the ones in the * plugins, are not because a hook or plugin can still trigger * the alternative rescue.conf instead of finit.conf. This for * very advanced use-cases where files on /etc are generated at * bootstrap, including users and passwords, from other sources. */ if (rescue) { int rc; char line[80] = "tty [12345789] rescue"; /* If rescue.conf is missing, fall back to a root shell */ rc = conf_parse_file(RESCUE_CONF, 0); if (rc) service_register(SVC_TYPE_TTY, line, global_rlimit, NULL); print(rc, "Entering rescue mode"); goto done; } /* First, read /etc/finit.conf */ conf_parse_file(finit_conf, 0); /* Set global limits */ for (int i = 0; i < RLIMIT_NLIMITS; i++) { if (setrlimit(i, &global_rlimit[i]) == -1) logit(LOG_WARNING, "rlimit: Failed setting %s: %s", rlim2str(i), lim2str(&global_rlimit[i])); } /* * Next, read all *.conf in /lib/finit/system and /etc/finit.d/ * The system files were previously created at runtime by plugins * but are now regular files that can be overridden by files in * /etc/finit.d -- similar to how tmfiles.d(5) work. E.g., add * an override .conf, or an ignore by symlinking to /dev/null * * The .conf files (and run/task/service stanzas) are parsed and * started in order. Each directory is sorted alphanumerically * and then the result is appended to the overall order: * * /lib/finit/system/10-hotplug.conf * /lib/finit/system/90-testserv.conf * /run/finit/system/dbus.conf * /run/finit/system/tty.conf * /etc/finit.d/10-abc.conf * /etc/finit.d/20-abc.conf * /etc/finit.d/enabled/1-aaa.conf * /etc/finit.d/enabled/1-abc.conf * /etc/finit.d/enabled/2-aaa.conf */ glob_append(&gl, 0, "%s/*.conf", FINIT_SYSPATH_); glob_append(&gl, 1, "%s/*.conf", FINIT_RUNPATH_); glob_append(&gl, 1, "%s/*.conf", finit_rcsd); glob_append(&gl, 1, "%s/enabled/*.conf", finit_rcsd); if (bootstrap) { const char *fn = _PATH_VARRUN "finit/conf.order"; FILE *fp; fp = fopen(fn, "w"); if (!fp) { err(1, "failed creating %s", fn); } else { fprintf(fp, "# Evaluation & execution order of .conf files\n"); for (i = 0; i < gl.gl_pathc; i++) fprintf(fp, "%s\n", gl.gl_pathv[i]); fclose(fp); } } for (i = 0; i < gl.gl_pathc; i++) { char *path = gl.gl_pathv[i]; char *rp = NULL; struct stat st; size_t j, len; /* check for FINIT_SYSPATH_ or FINIT_RUNPATH_ overrides */ for (j = i + 1; j < gl.gl_pathc; j++) { if (strncmp(path, FINIT_SYSPATH_, strlen(FINIT_SYSPATH_)) && strncmp(path, FINIT_RUNPATH_, strlen(FINIT_RUNPATH_))) continue; if (strcmp(basenm(path), basenm(gl.gl_pathv[j]))) continue; path = NULL; /* replacement later in list, skip this */ break; } if (!path) continue; /* skip, override exists */ /* Check that it's an actual file ... beyond any symlinks */ if (lstat(path, &st)) { dbg("Skipping %s, cannot access: %s", path, strerror(errno)); continue; } /* Skip directories */ if (S_ISDIR(st.st_mode)) { dbg("Skipping directory %s", path); continue; } /* Check for dangling symlinks */ if (S_ISLNK(st.st_mode)) { rp = realpath(path, NULL); if (!rp) { logit(LOG_WARNING, "Skipping %s, dangling symlink: %s", path, strerror(errno)); continue; } } /* Check that file ends with '.conf' */ len = strlen(path); if (len < 6 || strcmp(&path[len - 5], ".conf")) dbg("Skipping %s, not a Finit .conf file ... ", path); else conf_parse_file(path, 1); if (rp) free(rp); } globfree(&gl); /* Mark any reverse deps as chenaged. */ service_update_rdeps(); /* Prune according to if:[!]ident or if:<[!]cond> */ service_mark_unavail(); /* Set up top-level cgroups */ cgroup_config(); done: /* Remove all unused top-level cgroups */ cgroup_cleanup(); /* Drop record of all .conf changes */ drop_changes(); if (bootstrap) wdog = svc_find("watchdog", "finit"); /* Override configured runlevel, user said 'S' on /proc/cmdline */ if (BOOTSTRAP && single) cfglevel = 1; /* * Set host name, from %DEFHOST, *.conf or /etc/hostname. The * latter wins, if neither exists we default to "noname" */ set_hostname(&hostname); return 0; } static struct conf_change *conf_find(char *file) { struct conf_change *node, *tmp; TAILQ_FOREACH_SAFE(node, &conf_change_list, link, tmp) { if (string_compare(node->name, file)) return node; } return NULL; } static void drop_change(struct conf_change *node) { if (!node) return; TAILQ_REMOVE(&conf_change_list, node, link); free(node->name); free(node); } static void drop_changes(void) { struct conf_change *node, *tmp; TAILQ_FOREACH_SAFE(node, &conf_change_list, link, tmp) drop_change(node); } static int conf_change_act(char *dir, char *name, uint32_t mask) { char fn[strlen(dir) + strlen(name) + 2]; struct conf_change *node; char *rp = NULL; /* Check for actual printable characters, sometimes STX */ if (name[0] && name[0] >= ' ') paste(fn, sizeof(fn), dir, name); else strlcpy(fn, dir, sizeof(fn)); dbg("path: %s mask: %08x", fn, mask); if (conf_is_template(name, NULL, 0)) { /* Skip realpath for templates */ rp = strdup(fn); } else { /* Handle disabling/removal of service */ rp = realpath(fn, NULL); if (!rp) { if (errno != ENOENT) goto fail; rp = strdup(fn); } } if (!rp) goto fail; node = conf_find(rp); if (node) { dbg("event already registered for %s ...", name); free(rp); return 0; } node = malloc(sizeof(*node)); if (!node) { free(rp); goto fail; } node->name = rp; TAILQ_INSERT_HEAD(&conf_change_list, node, link); dbg("event registered for %s, mask 0x%x", rp, mask); return 0; fail: warn("failed registering %s event mask %08x", fn, mask); return 1; } int conf_any_change(void) { if (TAILQ_EMPTY(&conf_change_list)) return 0; return 1; } int conf_changed(char *file) { int rc = 0; char *rp; if (!file) return 0; if (conf_is_template(file, NULL, 0)) rp = strdup(file); else rp = realpath(file, NULL); if (!rp) return 0; if (conf_find(rp)) rc = 1; free(rp); return rc; } static int conf_iwatch_read(int fd) { static char ev_buf[8 *(sizeof(struct inotify_event) + NAME_MAX + 1) + 1]; struct inotify_event *ev; ssize_t sz; size_t off; sz = read(fd, ev_buf, sizeof(ev_buf) - 1); if (sz <= 0) return -1; ev_buf[sz] = 0; for (off = 0; off < (size_t)sz; off += sizeof(*ev) + ev->len) { struct iwatch_path *iwp; if (off + sizeof(*ev) > (size_t)sz) break; ev = (struct inotify_event *)&ev_buf[off]; if (off + sizeof(*ev) + ev->len > (size_t)sz) break; if (!ev->mask) continue; dbg("name %s, event: 0x%08x", ev->name, ev->mask); /* Find base path for this event */ iwp = iwatch_find_by_wd(&iw_conf, ev->wd); if (!iwp) continue; if (conf_change_act(iwp->path, ev->name, ev->mask)) break; } return 0; } static void conf_cb(uev_t *w, void *arg, int events) { (void)arg; if (UEV_ERROR == events) { dbg("%s(): iwatch socket %d invalid.", __func__, w->fd); return; } if (conf_iwatch_read(w->fd)) { err(1, "invalid inotify event"); return; } #ifdef AUTO_RELOAD if (conf_any_change()) sm_reload(); #endif } void conf_flush_events(void) { while (!conf_iwatch_read(iwatch_fd)) dbg("emptying inotify queue ..."); } /* * Set up inotify watcher and load all *.conf in /etc/finit.d/ */ int conf_monitor(void) { char path[strlen(finit_rcsd) + 16]; int rc = 0; /* * If only one watcher fails, that's OK. A user may have only * one of /etc/finit.conf or /etc/finit.d in use, and may also * have or not have symlinks in place. We need to monitor for * changes to either symlink or target. */ rc |= iwatch_add(&iw_conf, finit_rcsd, IN_ONLYDIR); snprintf(path, sizeof(path), "%s/available/", finit_rcsd); rc |= iwatch_add(&iw_conf, path, IN_ONLYDIR | IN_DONT_FOLLOW); snprintf(path, sizeof(path), "%s/enabled/", finit_rcsd); rc |= iwatch_add(&iw_conf, path, IN_ONLYDIR | IN_DONT_FOLLOW); rc |= iwatch_add(&iw_conf, finit_conf, 0); /* * Systems with /etc/default, /etc/conf.d, or similar, can also * monitor changes in env files sourced by .conf files (above) * define your own with --with-sysconfig=/path/to/envfiles */ rc |= iwatch_add(&iw_conf, "/etc/default/", IN_ONLYDIR); rc |= iwatch_add(&iw_conf, "/etc/conf.d/", IN_ONLYDIR); #ifdef FINIT_SYSCONFIG rc |= iwatch_add(&iw_conf, FINIT_SYSCONFIG, IN_ONLYDIR); #endif rc |= conf_reload(); return rc; } /* * Prepare .conf parser and load /etc/finit.conf for global settings */ int conf_init(uev_ctx_t *ctx) { /* default hostname */ hostname = strdup(DEFHOST); /* * Get current global limits, which may be overridden from both * finit.conf, for Finit and its services like getty+watchdogd, * and *.conf in finit.d/, for each service(s) listed there. */ for (int i = 0; i < RLIMIT_NLIMITS; i++) { if (getrlimit(i, &initial_rlimit[i])) logit(LOG_WARNING, "rlimit: Failed reading setting %s: %s", rlim2str(i), strerror(errno)); } /* Initialize global rlimits, e.g. for built-in services */ memcpy(global_rlimit, initial_rlimit, sizeof(global_rlimit)); /* * Start built-in watchdogd as soon as possible, if enabled */ #ifdef WDT_DEVNODE if (whichp(FINIT_EXECPATH_ "/watchdogd") && fexist(WDT_DEVNODE)) { conf_save_service(SVC_TYPE_SERVICE, "watchdog:finit", "watchdogd.conf", "\tdescription = \"Finit watchdog daemon\"\n" "\trunlevel = \"S0123456789\"\n" "\tnotify = \"none\"\n" "\tcgroup init {}\n" "\tcommand = \"" FINIT_EXECPATH_ "/watchdogd\"\n"); } #endif /* * Start kernel event daemon as soon as possible, if enabled */ if (whichp(FINIT_EXECPATH_ "/keventd")) conf_save_service(SVC_TYPE_SERVICE, "keventd", "keventd.conf", "\tdescription = \"Finit kernel event daemon\"\n" "\trunlevel = \"S12345789\"\n" "\tnotify = \"none\"\n" "\tcgroup init {}\n" "\tcommand = \"" FINIT_EXECPATH_ "/keventd\"\n"); dbg("Allow plugins to register early runlevel 1 run/task/services ..."); plugin_run_hooks(HOOK_SVC_PLUGIN); /* Read global rlimits and global cgroup setup from /etc/finit.conf */ conf_parse_file(finit_conf, 0); /* prepare /etc watcher */ iwatch_fd = iwatch_init(&iw_conf); if (iwatch_fd < 0) return 1; if (uev_io_init(ctx, &etcw, conf_cb, NULL, iwatch_fd, UEV_READ)) { err(1, "Failed setting up I/O callback for /etc watcher"); close(iwatch_fd); return 1; } /* * Background startup scripts in the runparts directory, if any. */ if (runparts && fisdir(runparts) && !rescue) { char args[10] = { 0 }; if (debug) strlcat(args, "-d ", sizeof(args)); if (runparts_progress) strlcat(args, "-p ", sizeof(args)); if (runparts_sysv) strlcat(args, "-s ", sizeof(args)); conf_save_service(SVC_TYPE_TASK, "runparts", "runparts.conf", "\tdescription = \"Calling runparts %s in the background\"\n" "\trunlevel = \"S\"\n" "\tconditions = { \"int/bootstrap\" }\n" "\tnotify = \"none\"\n" "\tlog { file = \"/dev/console\" }\n" "\tcommand = \"%s %s%s\"\n", runparts, _PATH_RUNPARTS, args, runparts); } return 0; } /** * Local Variables: * indent-tabs-mode: t * c-file-style: "linux" * End: */