mirror of
https://github.com/troglobit/finit.git
synced 2026-10-28 12:30:44 +00:00
Apply a PAM session to run/task/sysv/services Finit starts, pam_limits
above all, so a service running as a given user picks up that user's
limits the way a login does.
Add a new `pam` setting for the new block format (only), like the
per-service directories, naming a file in /etc/pam.d:
service weston {
user = "weston"
pam = "weston-autologin"
command = "/usr/bin/weston --continue-without-input"
}
pam_close_session() has to be called by a process still holding the
handle, and the handle does not survive exec(). Hence the keeper: it
holds the handle, drops to the service's credentials, and waits for a
parent-death signal before closing the session. Same shape as
systemd's (sd-pam), for the same reason, and one per fork, so the
script hooks open and close their own.
The keeper closes the descriptors it inherited from Finit and only
those. Closing everything would also take out what pam_open_session()
opened for itself, a keyring fd or a lock file, and leave the modules
to close a session with those pulled out from under them. Closing
nothing, as (sd-pam) does, would leave it holding the write end of the
notify pipe for the service's whole lifetime and starve notify = "s6"
services of their ready signal. So the fds open before pam_start()
are snapshotted and exactly those are closed, while the ones PAM opens
after are marked close-on-exec so the daemon does not inherit them
either.
A refused value, a denied account stack, an uninstalled pam.d file,
and a build without PAM support all keep the service from starting
rather than running it with the stacks skipped: one that quietly loses
pam_limits and its private /tmp, with nothing said. Capabilities a
module like pam_cap.so granted are merged into the IAB Finit applies
instead of being replaced by it, which only helps a service that also
sets capabilities, the other arm being a plain setuid() with nothing
left to restore once permitted is empty.
The test sysroot gains pam_permit.so, pam_deny.so and pam_limits.so,
which ldd cannot see, libpam dlopen()s them, and the test skips when
the host has none to stage. The negative cases pin the exit status
rather than only asserting crashed, which serv reports for any early
exit, so a bad command or an unwritable pidfile cannot pass for a
rejected session.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2787 lines
72 KiB
C
2787 lines
72 KiB
C
/* 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 <troglobit@gmail.com>
|
|
*
|
|
* 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 <stdarg.h>
|
|
#include <string.h>
|
|
#include <sys/inotify.h>
|
|
#include <sys/resource.h>
|
|
#ifdef _LIBITE_LITE
|
|
# include <libite/lite.h>
|
|
# include <libite/queue.h> /* BSD sys/queue.h API */
|
|
#else
|
|
# include <lite/lite.h>
|
|
# include <lite/queue.h> /* BSD sys/queue.h API */
|
|
#endif
|
|
#include <confuse.h>
|
|
#include <signal.h>
|
|
#include <time.h>
|
|
#include <glob.h>
|
|
|
|
#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.<size>.max, rdma.<dev>.*, 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 */
|
|
/*
|
|
* A list, but libconfuse takes a bare string for one as well, so
|
|
* the common `command = "prog args"` is unaffected. See
|
|
* svc_command().
|
|
*/
|
|
CFG_STR_LIST("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 ("pam", NULL, CFGF_NODEFAULT),
|
|
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_LIST("provides", 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_BOOL ("passenv", cfg_false, CFGF_NODEFAULT),
|
|
CFG_STR_LIST("command", NULL, CFGF_NODEFAULT), /* candidates, see svc_command() */
|
|
CFG_BOOL ("notty", cfg_false, CFGF_NODEFAULT),
|
|
CFG_BOOL ("rescue", cfg_false, CFGF_NODEFAULT),
|
|
CFG_END()
|
|
};
|
|
|
|
/*
|
|
* Flags for the sections whose title is a service identity.
|
|
* libconfuse merges two sections sharing a title, silently, which for
|
|
* these would fold two declarations into one service, so duplicates
|
|
* are rejected instead.
|
|
*/
|
|
#define SVC_SEC_FLAGS (CFGF_MULTI | CFGF_TITLE | CFGF_NO_TITLE_DUPES)
|
|
|
|
static cfg_opt_t conf_opts[] = {
|
|
CFG_SEC ("service", svc_opts, SVC_SEC_FLAGS),
|
|
CFG_SEC ("task", svc_opts, SVC_SEC_FLAGS),
|
|
CFG_SEC ("run", svc_opts, SVC_SEC_FLAGS),
|
|
CFG_SEC ("sysv", svc_opts, SVC_SEC_FLAGS),
|
|
CFG_SEC ("tty", tty_opts, SVC_SEC_FLAGS),
|
|
/* cgroup titles name a group, not a service, so merging is fine */
|
|
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 (=> <boot/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);
|
|
}
|
|
|
|
/*
|
|
* Escape backslash and double quote so a runtime string can sit in a
|
|
* double-quoted .conf value. Truncates to fit @buf, always returns a
|
|
* terminated string.
|
|
*/
|
|
char *conf_escape(const char *str, char *buf, size_t len)
|
|
{
|
|
size_t i = 0;
|
|
|
|
while (*str && i + 2 < len) {
|
|
if (*str == '"' || *str == '\\')
|
|
buf[i++] = '\\';
|
|
buf[i++] = *str++;
|
|
}
|
|
buf[i] = 0;
|
|
|
|
return buf;
|
|
}
|
|
|
|
/*
|
|
* Generates /run/finit/system/<file> for Finit's built-in services,
|
|
* which must probe the running system before registering. @fmt 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 <hard|soft> 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: <!~pid/foo,~pid/bar> 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;
|
|
}
|
|
|
|
/*
|
|
* Pick the command for a service. Several may be listed, in which
|
|
* case they are candidates for the same service and the first one
|
|
* whose binary resolves wins:
|
|
*
|
|
* command = { "/lib/systemd/systemd-udevd", "udevd" }
|
|
*
|
|
* The line-based format could only spell this as one stanza per
|
|
* candidate, relying on nowarn to skip the ones that were not
|
|
* installed. A candidate not being there is not an error here, so
|
|
* only the winner is looked up again by service_register().
|
|
*
|
|
* A leading '-' still says a missing binary is expected, cf. systemd
|
|
* ExecStart=-.
|
|
*
|
|
* Returns NULL when the section has no command at all, which is an
|
|
* error for a service but not for a tty, so the caller decides.
|
|
*/
|
|
static const char *svc_command(cfg_t *sec, int *nowarn)
|
|
{
|
|
const char *cand = NULL;
|
|
unsigned int i, num;
|
|
|
|
num = cfg_size(sec, "command");
|
|
if (!num)
|
|
return NULL;
|
|
|
|
for (i = 0; i < num; i++) {
|
|
cand = cfg_getnstr(sec, "command", i);
|
|
*nowarn = cand[0] == '-';
|
|
|
|
/* which() skips any arguments to the command */
|
|
if (whichp(&cand[*nowarn]))
|
|
break;
|
|
}
|
|
|
|
/* the winner, or the last candidate when none of them resolve */
|
|
return &cand[*nowarn];
|
|
}
|
|
|
|
/*
|
|
* 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;
|
|
}
|
|
|
|
/*
|
|
* provides names conditions this service asserts on top of its own
|
|
* pid/<ident>, so blocks with distinct titles can supply one barrier
|
|
* to everything downstream. Like the per-service directories it has
|
|
* no legacy token and is stored on the registered svc.
|
|
*
|
|
* A claim on a condition that is already owned is dropped, with the
|
|
* service itself left alone: the overlap is a configuration bug, and
|
|
* an init system is more useful degraded than refusing to boot.
|
|
*/
|
|
static void provides_translate(cfg_t *sec, svc_t *svc, char *file)
|
|
{
|
|
unsigned int i, num;
|
|
|
|
num = cfg_size(sec, "provides");
|
|
for (i = 0; i < num; i++) {
|
|
const char *cond = cfg_getnstr(sec, "provides", i);
|
|
svc_t *owner;
|
|
|
|
if (!strchr(cond, '/')) {
|
|
logit(LOG_ERR, "%s: %s: provides '%s' is not a condition,"
|
|
" it needs a namespace, e.g. usr/%s, ignoring",
|
|
file, cfg_title(sec), cond, cond);
|
|
continue;
|
|
}
|
|
|
|
if (strlen(cond) >= MAX_COND_LEN) {
|
|
logit(LOG_ERR, "%s: %s: provides '%s' is too long,"
|
|
" ignoring", file, cfg_title(sec), cond);
|
|
continue;
|
|
}
|
|
|
|
if (svc->num_provides >= MAX_NUM_PROVIDES) {
|
|
logit(LOG_WARNING, "%s: %s: too many provides, max %d,"
|
|
" ignoring '%s'", file, cfg_title(sec),
|
|
MAX_NUM_PROVIDES, cond);
|
|
break;
|
|
}
|
|
|
|
owner = svc_cond_owner(cond, svc);
|
|
if (owner) {
|
|
logit(LOG_WARNING, "%s: %s: provides condition <%s>"
|
|
" already registered by service %s, ignoring",
|
|
file, cfg_title(sec), cond,
|
|
svc_ident(owner, NULL, 0));
|
|
continue;
|
|
}
|
|
|
|
strlcpy(svc->provides[svc->num_provides++], cond, MAX_COND_LEN);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Also block format only. The value names a file in /etc/pam.d, so a
|
|
* path is refused rather than followed.
|
|
*
|
|
* A bad value is stored anyway, or as much of it as fits, and
|
|
* service_start() refuses to start the service. Dropping it here
|
|
* instead would start the service with no session at all, which is the
|
|
* one outcome the service asking for a session cannot live with.
|
|
*/
|
|
static void pam_translate(cfg_t *sec, svc_t *svc, char *file)
|
|
{
|
|
const char *str;
|
|
|
|
str = sec_getstr(sec, "pam", NULL);
|
|
if (!str || !str[0])
|
|
return;
|
|
|
|
if (strlcpy(svc->pam, str, sizeof(svc->pam)) >= sizeof(svc->pam)) {
|
|
logit(LOG_ERR, "%s: %s: pam '%s' is too long, not starting",
|
|
file, cfg_title(sec), str);
|
|
return;
|
|
}
|
|
|
|
if (strchr(str, '/') || strstr(str, "..")) {
|
|
logit(LOG_ERR, "%s: %s: pam '%s' names a file in /etc/pam.d,"
|
|
" not a path, not starting", file, cfg_title(sec), str);
|
|
return;
|
|
}
|
|
|
|
if (svc->forking)
|
|
logit(LOG_WARNING, "%s: %s: pam with type = forking closes the"
|
|
" session when the initial process exits",
|
|
file, cfg_title(sec));
|
|
}
|
|
|
|
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 = svc_command(sec, &nowarn);
|
|
if (!cmd) {
|
|
logit(LOG_ERR, "%s: section '%s' missing command, skipping",
|
|
file, cfg_title(sec));
|
|
return;
|
|
}
|
|
|
|
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);
|
|
provides_translate(sec, svc, file);
|
|
pam_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;
|
|
int nowarn = 0;
|
|
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 = svc_command(sec, &nowarn);
|
|
if (nowarn)
|
|
addtok(line, sizeof(line), "nowarn");
|
|
|
|
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");
|
|
|
|
/*
|
|
* passenv is handed to the built-in getty as -p, which it turns
|
|
* into login -p. An external getty gets its arguments from
|
|
* command, so there is nowhere to put it.
|
|
*/
|
|
if (sec_getbool(sec, "passenv", NULL)) {
|
|
if (dev || !cmd)
|
|
addtok(line, sizeof(line), "passenv");
|
|
else
|
|
logit(LOG_WARNING, "%s: %s: passenv applies to the"
|
|
" built-in getty, pass -p in command instead,"
|
|
" ignoring", file, cfg_title(sec));
|
|
}
|
|
|
|
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_opt_t *opt;
|
|
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);
|
|
|
|
/*
|
|
* The verdict must stay syntactic. A duplicate section title is
|
|
* a semantic error in a file that is unmistakably block format,
|
|
* and answering 'no' here would hand it to the legacy parser,
|
|
* burying the real message under its errors. cfg_init() copies
|
|
* the option array, so this only relaxes the probe.
|
|
*/
|
|
for (opt = cfg->opts; opt && opt->name; opt++)
|
|
opt->flags &= ~CFGF_NO_TITLE_DUPES;
|
|
|
|
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();
|
|
svc_provides_reset();
|
|
|
|
/*
|
|
* 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 /run/finit/system, /lib/finit/system,
|
|
* and /etc/finit.d/. The runtime directory holds files Finit
|
|
* generates for its built-in services (keventd, watchdogd,
|
|
* runparts), read first so later files can reference them in
|
|
* if: statements. A same-named file in a later directory
|
|
* overrides an earlier one, similar to how tmpfiles.d(5) work,
|
|
* so /etc/finit.d, always read last, is how users override any
|
|
* built-in or bundled configuration.
|
|
*
|
|
* The .conf files (and their run/task/service stanzas) are
|
|
* parsed and started in order. Each directory is sorted
|
|
* alphanumerically before being appended to the overall order,
|
|
* recorded in /run/finit/conf.order at bootstrap. See
|
|
* doc/config/files.md for an example.
|
|
*/
|
|
glob_append(&gl, 0, "%s/*.conf", FINIT_RUNPATH_);
|
|
glob_append(&gl, 1, "%s/*.conf", FINIT_SYSPATH_);
|
|
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.
|
|
* In passive mode (-p) alongside the hotplug plugin, keventd
|
|
* only monitors power supply events (sys/pwr/ac condition).
|
|
*/
|
|
if (whichp(FINIT_EXECPATH_ "/keventd"))
|
|
conf_save_service(SVC_TYPE_SERVICE, "keventd", "keventd.conf",
|
|
"\tdescription = \"kernel event daemon\"\n"
|
|
"\trunlevel = \"S0123456789\"\n"
|
|
"\tnotify = \"pid\"\n"
|
|
"\tcgroup init {}\n"
|
|
"\tcommand = \"" FINIT_EXECPATH_ "/keventd"
|
|
#ifdef HAVE_HOTPLUG_PLUGIN
|
|
" -p"
|
|
#else
|
|
" -c"
|
|
#endif
|
|
"\"\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 };
|
|
char dir[256];
|
|
|
|
if (debug)
|
|
strlcat(args, "-d ", sizeof(args));
|
|
if (runparts_progress)
|
|
strlcat(args, "-p ", sizeof(args));
|
|
if (runparts_sysv)
|
|
strlcat(args, "-s ", sizeof(args));
|
|
|
|
conf_escape(runparts, dir, sizeof(dir));
|
|
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",
|
|
dir, _PATH_RUNPARTS, args, dir);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Local Variables:
|
|
* indent-tabs-mode: t
|
|
* c-file-style: "linux"
|
|
* End:
|
|
*/
|