diff --git a/service-script-guide.md b/service-script-guide.md index 68ee7a684..22fb561e7 100644 --- a/service-script-guide.md +++ b/service-script-guide.md @@ -509,3 +509,9 @@ This is a sensible default for daemons that are happy with `0.0.0.0`, but lets the user specify something else, like `rc_need="net.wan"` if he needs it. The burden is on the user to determine the appropriate service whenever he changes the daemon's configuration file. + +# User services + +User services are ran as the user that starts them. A few things to note +then, are that command_user shouldn't be set, runtime data (such as pidfiles +and sockets) should be placed in XDG_RUNTIME_DIR. diff --git a/sh/openrc-run.sh.in b/sh/openrc-run.sh.in index caf78f12e..1dfdc559f 100644 --- a/sh/openrc-run.sh.in +++ b/sh/openrc-run.sh.in @@ -57,7 +57,7 @@ sourcex "@LIBEXECDIR@/sh/functions.sh" sourcex "@LIBEXECDIR@/sh/rc-functions.sh" case $RC_SYS in PREFIX|SYSTEMD-NSPAWN) ;; - *) sourcex -e "@LIBEXECDIR@/sh/rc-cgroup.sh";; + *) yesno "$RC_USER_SERVICES" || sourcex -e "@LIBEXECDIR@/sh/rc-cgroup.sh";; esac # Support LiveCD foo diff --git a/src/librc/librc-depend.c b/src/librc/librc-depend.c index 72903c04d..4cd23b541 100644 --- a/src/librc/librc-depend.c +++ b/src/librc/librc-depend.c @@ -758,6 +758,12 @@ rc_deptree_update_needed(time_t *newest, char *file) newer |= !deep_mtime_check(path, true, &mtime, file); free(path); + if (rc_is_user()) { + xasprintf(&path, "%s/rc.conf", rc_usrconfdir()); + newer |= !deep_mtime_check(path, true, &mtime, file); + free(path); + } + /* Some init scripts dependencies change depending on config files * outside of baselayout, like syslog-ng, so we check those too. */ xasprintf(&depconfig, "%s/depconfig", service_dir); diff --git a/src/librc/librc-misc.c b/src/librc/librc-misc.c index ab96f66eb..b1b9da46d 100644 --- a/src/librc/librc-misc.c +++ b/src/librc/librc-misc.c @@ -301,7 +301,7 @@ static RC_STRINGLIST *rc_config_kcl(RC_STRINGLIST *config) return config; } -static RC_STRINGLIST * rc_config_directory(RC_STRINGLIST *config, char *dir) +static RC_STRINGLIST * rc_config_directory(RC_STRINGLIST *config, const char *dir) { DIR *dp; struct dirent *d; @@ -383,28 +383,47 @@ _free_rc_conf(void) rc_stringlist_free(rc_conf); } +static void +rc_conf_append(const char *file) +{ + RC_STRINGLIST *conf = rc_config_load(file); + TAILQ_CONCAT(rc_conf, conf, entries); + rc_stringlist_free(conf); +} + char * rc_conf_value(const char *setting) { const char *sysconfdir = rc_sysconfdir(); + const char *usrconfdir = rc_usrconfdir(); RC_STRING *s; char *conf; if (rc_conf) return rc_config_value(rc_conf, setting); - xasprintf(&conf, "%s/%s", sysconfdir, "rc.conf"); - rc_conf = rc_config_load(conf); + rc_conf = rc_stringlist_new(); atexit(_free_rc_conf); + /* Load user configurations first, as they should override + * system wide configs. */ + if (usrconfdir) { + xasprintf(&conf, "%s/%s", usrconfdir, "rc.conf"); + rc_conf_append(conf); + free(conf); + + xasprintf(&conf, "%s/%s", usrconfdir, "rc.conf.d"); + rc_conf = rc_config_directory(rc_conf, conf); + free(conf); + } + + xasprintf(&conf, "%s/%s", sysconfdir, "rc.conf"); + rc_conf_append(sysconfdir); free(conf); /* Support old configs. */ - if (exists(RC_CONF_OLD)) { - RC_STRINGLIST *old_conf = rc_config_load(RC_CONF_OLD); - TAILQ_CONCAT(rc_conf, old_conf, entries); - rc_stringlist_free(old_conf); - } + if (exists(RC_CONF_OLD)) + rc_conf_append(RC_CONF_OLD); xasprintf(&conf, "%s/%s", sysconfdir, "rc.conf.d"); rc_conf = rc_config_directory(rc_conf, conf); diff --git a/src/librc/librc.c b/src/librc/librc.c index f8a2ed39e..fb6069119 100644 --- a/src/librc/librc.c +++ b/src/librc/librc.c @@ -35,6 +35,7 @@ #include "librc.h" #include "misc.h" #include "rc.h" +#include "einfo.h" #ifdef __FreeBSD__ # include #endif @@ -567,30 +568,106 @@ static const char * const scriptdirs[] = { #ifdef RC_PKG_PREFIX RC_PKG_PREFIX "/etc", #endif + NULL, /* slot for userconf dir */ NULL }; +static struct { + bool set; + char *svcdir; + char *usrconfdir; + char *runleveldir; + char *scriptdirs[ARRAY_SIZE(scriptdirs)]; +} rc_dirs; + +static void +free_rc_dirs(void) +{ + free(rc_dirs.runleveldir); + rc_dirs.runleveldir = NULL; + free(rc_dirs.svcdir); + rc_dirs.svcdir = NULL; + for (size_t i = 0; rc_dirs.scriptdirs[i]; i++) + free(rc_dirs.scriptdirs[i]); +} + +static bool is_user = false; + +bool +rc_is_user(void) +{ + return is_user; +} + +void +rc_set_user(void) +{ + char *env; + if (is_user) + return; + + is_user = true; + rc_dirs.set = true; + setenv("RC_USER_SERVICES", "yes", true); + + if ((env = getenv("XDG_CONFIG_HOME"))) + xasprintf(&rc_dirs.scriptdirs[0], "%s/openrc", env); + else if ((env = getenv("HOME"))) + xasprintf(&rc_dirs.scriptdirs[0], "%s/.config/openrc", env); + else + eerrorx("XDG_CONFIG_HOME and HOME unset"); + + rc_dirs.usrconfdir = rc_dirs.scriptdirs[0]; + + for (size_t i = 0; scriptdirs[i]; i++) + xasprintf(&rc_dirs.scriptdirs[i + 1], "%s/user.d", scriptdirs[i]); + + xasprintf(&rc_dirs.runleveldir, "%s/runlevels", rc_dirs.scriptdirs[0]); + + if (!(env = getenv("XDG_RUNTIME_DIR"))) + eerrorx("XDG_RUNTIME_DIR unset."); /* FIXME: fallback to something else? */ + xasprintf(&rc_dirs.svcdir, "%s/openrc", env); + atexit(free_rc_dirs); +} + const char * const * rc_scriptdirs(void) { + if (rc_dirs.set) + return (const char * const *) rc_dirs.scriptdirs; return scriptdirs; } const char * rc_sysconfdir(void) { + if (is_user) + return RC_SYSCONFDIR "/user.d"; return RC_SYSCONFDIR; } +const char * +rc_usrconfdir(void) +{ + if (rc_dirs.set) + return rc_dirs.usrconfdir; + + return NULL; +} + const char * rc_runleveldir(void) { + if (rc_dirs.set) + return rc_dirs.runleveldir; return RC_RUNLEVELDIR; } const char * rc_svcdir(void) { + if (rc_dirs.set) + return rc_dirs.svcdir; return RC_SVCDIR; } diff --git a/src/librc/meson.build b/src/librc/meson.build index c17588266..67c081fcd 100644 --- a/src/librc/meson.build +++ b/src/librc/meson.build @@ -22,6 +22,7 @@ librc = library('rc', librc_sources, dependencies: kvm_dep, include_directories : [incdir, einfo_incdir], link_depends : 'rc.map', + link_with : libeinfo, version : librc_version, install : true, install_dir : libdir) diff --git a/src/librc/rc.h.in b/src/librc/rc.h.in index a9c456e5d..81fe660b8 100644 --- a/src/librc/rc.h.in +++ b/src/librc/rc.h.in @@ -119,6 +119,12 @@ typedef TAILQ_HEAD(rc_stringlist, rc_string) RC_STRINGLIST; #define RC_LEVEL_SINGLE "single" #define RC_LEVEL_SHUTDOWN "shutdown" +/*! Set user-wide paths for the rc_*dir functions. */ +void rc_set_user(void); + +/*! @return true if rc_set_user was called, false otherwise. */ +bool rc_is_user(void); + /*! Sets the root prefix for all librc operations * it should be called before any other operation * @param root path to prefix */ @@ -134,6 +140,12 @@ const char * const *rc_scriptdirs(void); * @return Path to the system configuration directory */ const char *rc_sysconfdir(void); +/*! The user configuration directory is where rc.conf + * will be located for user-mode openrc. It is meant to + * override the system wide configs where applicable. + * @return Path to the user configuration directory, or NULL if in system mode */ +const char *rc_usrconfdir(void); + /*! @return Path to runlevel directory */ const char *rc_runleveldir(void); diff --git a/src/mark_service/mark_service.c b/src/mark_service/mark_service.c index e9239c04d..5011a35dd 100644 --- a/src/mark_service/mark_service.c +++ b/src/mark_service/mark_service.c @@ -45,6 +45,9 @@ int main(int argc, char **argv) if (service == NULL || *service == '\0') eerrorx("%s: no service specified", applet); + if (rc_yesno(getenv("RC_USER_SERVICES"))) + rc_set_user(); + if (!strncmp(applet, "mark_", 5) && (bit = lookup_service_state(applet + 5))) ok = rc_service_mark(service, bit); diff --git a/src/openrc-run/openrc-run.c b/src/openrc-run/openrc-run.c index 748e1d8f9..78ad264f4 100644 --- a/src/openrc-run/openrc-run.c +++ b/src/openrc-run/openrc-run.c @@ -1099,6 +1099,7 @@ int main(int argc, char **argv) char *path = NULL; char *lnk = NULL; char *dir, *save = NULL, *saveLnk = NULL; + const char *workingdir = "/"; char *pidstr = NULL; size_t l = 0, ll; const char *file; @@ -1118,7 +1119,8 @@ int main(int argc, char **argv) exit(EXIT_FAILURE); } - atexit(cleanup); + if (rc_yesno(getenv("RC_USER_SERVICES"))) + rc_set_user(); /* We need to work out the real full path to our service. * This works fine, provided that we ONLY allow multiplexed services @@ -1160,10 +1162,54 @@ int main(int argc, char **argv) if (argc < 3) usage(EXIT_FAILURE); - /* Change dir to / to ensure all init scripts don't use stuff in pwd */ - if (chdir("/") == -1) + /* Ok, we are ready to go, so setup selinux if applicable */ + selinux_setup(argv); + + deps = true; + + /* Punt the first arg as its our service name */ + argc--; + argv++; + + /* Right then, parse any options there may be */ + while ((opt = getopt_long(argc, argv, getoptstring, + longopts, (int *)0)) != -1) + switch (opt) { + case 'd': + setenv("RC_DEBUG", "YES", 1); + break; + case 'l': + exclusive_fd = atoi(optarg); + fcntl(exclusive_fd, F_SETFD, + fcntl(exclusive_fd, F_GETFD, 0) | FD_CLOEXEC); + break; + case 's': + if (!(rc_service_state(service) & RC_SERVICE_STARTED)) + exit(EXIT_FAILURE); + break; + case 'S': + if (!(rc_service_state(service) & RC_SERVICE_STOPPED)) + exit(EXIT_FAILURE); + break; + case 'D': + deps = false; + break; + case 'Z': + dry_run = true; + break; + case_RC_COMMON_GETOPT + } + + /* Change dir to / to ensure all init scripts don't use stuff in pwd + * For user services, change to the user's HOME instead. */ + if (rc_is_user() && !(workingdir = getenv("HOME"))) + eerrorx("HOME is unset."); + + if (chdir(workingdir) == -1) eerror("chdir: %s", strerror(errno)); + atexit(cleanup); + if ((runlevel = xstrdup(getenv("RC_RUNLEVEL"))) == NULL) { env_filter(); env_config(); @@ -1207,44 +1253,6 @@ int main(int argc, char **argv) eprefix(prefix); } - /* Ok, we are ready to go, so setup selinux if applicable */ - selinux_setup(argv); - - deps = true; - - /* Punt the first arg as its our service name */ - argc--; - argv++; - - /* Right then, parse any options there may be */ - while ((opt = getopt_long(argc, argv, getoptstring, - longopts, (int *)0)) != -1) - switch (opt) { - case 'd': - setenv("RC_DEBUG", "YES", 1); - break; - case 'l': - exclusive_fd = atoi(optarg); - fcntl(exclusive_fd, F_SETFD, - fcntl(exclusive_fd, F_GETFD, 0) | FD_CLOEXEC); - break; - case 's': - if (!(rc_service_state(service) & RC_SERVICE_STARTED)) - exit(EXIT_FAILURE); - break; - case 'S': - if (!(rc_service_state(service) & RC_SERVICE_STOPPED)) - exit(EXIT_FAILURE); - break; - case 'D': - deps = false; - break; - case 'Z': - dry_run = true; - break; - case_RC_COMMON_GETOPT - } - if (rc_yesno(getenv("RC_NODEPS"))) deps = false; diff --git a/src/openrc/rc.c b/src/openrc/rc.c index bbe337b35..241906d49 100644 --- a/src/openrc/rc.c +++ b/src/openrc/rc.c @@ -772,7 +772,7 @@ int main(int argc, char **argv) RC_STRING *service; bool going_down = false; int depoptions = RC_DEP_STRICT | RC_DEP_TRACE; - const char *svcdir = rc_svcdir(); + const char *svcdir; char *rc_starting, *rc_stopping; char *deptree_skewed; char *krunlevel = NULL; @@ -796,7 +796,6 @@ int main(int argc, char **argv) applet = basename_c(argv[0]); LIST_INIT(&service_pids); LIST_INIT(&free_these_pids); - atexit(cleanup); if (!applet) eerrorx("arguments required"); @@ -807,11 +806,6 @@ int main(int argc, char **argv) if (chdir("/") == -1) eerror("chdir: %s", strerror(errno)); - /* Ensure our environment is pure - * Also, add our configuration to it */ - env_filter(); - env_config(); - /* complain about old configuration settings if they exist */ if (exists(RC_CONF_OLD)) { ewarn("%s still exists on your system and should be removed.", RC_CONF_OLD); @@ -859,6 +853,13 @@ int main(int argc, char **argv) } } + /* Ensure our environment is pure + * Also, add our configuration to it */ + env_filter(); + env_config(); + + svcdir = rc_svcdir(); + newlevel = argv[optind++]; /* To make life easier, we only have the shutdown runlevel as * nothing really needs to know that we're rebooting. @@ -870,6 +871,8 @@ int main(int argc, char **argv) } } + atexit(cleanup); + /* Enable logging */ setenv("EINFO_LOG", "openrc", 1); diff --git a/src/service/service.c b/src/service/service.c index 2545a0229..6444d6802 100644 --- a/src/service/service.c +++ b/src/service/service.c @@ -40,6 +40,9 @@ int main(int argc, char **argv) if (service == NULL || *service == '\0') eerrorx("%s: no service specified", applet); + if (rc_yesno(getenv("RC_USER_SERVICES"))) + rc_set_user(); + state = rc_service_state(service); bit = lookup_service_state(applet); if (bit) { diff --git a/src/shared/_usage.h b/src/shared/_usage.h index 91b956e06..c66b792e7 100644 --- a/src/shared/_usage.h +++ b/src/shared/_usage.h @@ -12,8 +12,9 @@ #include #include +#include "librc.h" -#define getoptstring_COMMON "ChqVv" +#define getoptstring_COMMON "ChqVvU" #define longopts_COMMON \ { "help", 0, NULL, 'h'}, \ @@ -21,6 +22,7 @@ { "version", 0, NULL, 'V'}, \ { "verbose", 0, NULL, 'v'}, \ { "quiet", 0, NULL, 'q'}, \ + { "user", 0, NULL, 'U'}, \ { NULL, 0, NULL, 0 } #define longopts_help_COMMON \ @@ -28,13 +30,15 @@ "Disable color output", \ "Display software version", \ "Run verbosely", \ - "Run quietly (repeat to suppress errors)" + "Run quietly (repeat to suppress errors)", \ + "Run in user mode" #define case_RC_COMMON_getopt_case_C setenv ("EINFO_COLOR", "NO", 1); #define case_RC_COMMON_getopt_case_h usage (EXIT_SUCCESS); #define case_RC_COMMON_getopt_case_V if (argc == 2) show_version(); #define case_RC_COMMON_getopt_case_v setenv ("EINFO_VERBOSE", "YES", 1); #define case_RC_COMMON_getopt_case_q set_quiet_options(); +#define case_RC_COMMON_getopt_case_U rc_set_user(); #define case_RC_COMMON_getopt_default usage (EXIT_FAILURE); #define case_RC_COMMON_GETOPT \ @@ -43,6 +47,7 @@ case 'V': case_RC_COMMON_getopt_case_V; break; \ case 'v': case_RC_COMMON_getopt_case_v; break; \ case 'q': case_RC_COMMON_getopt_case_q; break; \ + case 'U': case_RC_COMMON_getopt_case_U; break; \ default: case_RC_COMMON_getopt_default; break; extern const char *applet; diff --git a/src/shared/misc.c b/src/shared/misc.c index c5d387567..0e7f4e3bf 100644 --- a/src/shared/misc.c +++ b/src/shared/misc.c @@ -64,6 +64,8 @@ static const char *const env_whitelist[] = { "RC_DEBUG", "RC_NODEPS", "LANG", "LC_MESSAGES", "TERM", "EINFO_COLOR", "EINFO_VERBOSE", + "RC_USER_SERVICES", "HOME", + "XDG_RUNTIME_DIR", "XDG_CONFIG_HOME", NULL }; @@ -93,6 +95,15 @@ env_filter(void) profile = rc_config_load(profile_path); free(profile_path); + if (rc_is_user()) { + RC_STRINGLIST *usrprofile; + xasprintf(&profile_path, "%s/profile.env", rc_usrconfdir()); + usrprofile = rc_config_load(profile_path); + free(profile_path); + TAILQ_CONCAT(profile, usrprofile, entries); + rc_stringlist_free(usrprofile); + } + /* Copy the env and work from this so we can manipulate it safely */ env_list = rc_stringlist_new(); while (environ && environ[i]) { diff --git a/src/start-stop-daemon/start-stop-daemon.c b/src/start-stop-daemon/start-stop-daemon.c index 5868dd377..d40042a6e 100644 --- a/src/start-stop-daemon/start-stop-daemon.c +++ b/src/start-stop-daemon/start-stop-daemon.c @@ -361,6 +361,9 @@ int main(int argc, char **argv) signal_setup(SIGQUIT, handle_signal); signal_setup(SIGTERM, handle_signal); + if (rc_yesno(getenv("RC_USER_SERVICES"))) + rc_set_user(); + openlog(applet, LOG_PID, LOG_DAEMON); if ((tmp = getenv("SSD_NICELEVEL"))) diff --git a/src/supervise-daemon/supervise-daemon.c b/src/supervise-daemon/supervise-daemon.c index ed60f5e46..f7d19a532 100644 --- a/src/supervise-daemon/supervise-daemon.c +++ b/src/supervise-daemon/supervise-daemon.c @@ -823,6 +823,8 @@ int main(int argc, char **argv) svcname = getenv("RC_SVCNAME"); if (!svcname) eerrorx("%s: The RC_SVCNAME environment variable is not set", applet); + if (rc_yesno(getenv("RC_USER_SERVICES"))) + rc_set_user(); openlog(applet, LOG_PID, LOG_DAEMON); if (argc <= 1 || strcmp(argv[1], svcname)) @@ -1085,7 +1087,7 @@ int main(int argc, char **argv) umask(numask); if (!pidfile) - xasprintf(&pidfile, "/var/run/supervise-%s.pid", svcname); + xasprintf(&pidfile, "%s/supervise-%s.pid", rc_is_user() ? getenv("XDG_RUNTIME_DIR") : "/var/run", svcname); xasprintf(&fifopath, "%s/supervise-%s.ctl", rc_svcdir(), svcname); if (mkfifo(fifopath, 0600) == -1 && errno != EEXIST) eerrorx("%s: unable to create control fifo: %s", diff --git a/src/value/value.c b/src/value/value.c index e5190379d..c9620cf0c 100644 --- a/src/value/value.c +++ b/src/value/value.c @@ -33,6 +33,9 @@ int main(int argc, char **argv) if (service == NULL) eerrorx("%s: no service specified", applet); + if (rc_yesno(getenv("RC_USER_SERVICES"))) + rc_set_user(); + if (argc < 2 || !argv[1] || *argv[1] == '\0') eerrorx("%s: no option specified", applet); diff --git a/user-guide.md b/user-guide.md index 8547b893b..a60081402 100644 --- a/user-guide.md +++ b/user-guide.md @@ -176,3 +176,21 @@ OpenRC has wrappers for many common output tasks in libeinfo. This allows to print colour-coded status notices and other things. To make the output consistent the bundled service scripts all use ebegin/eend to print nice messages. + +# User services + +OpenRC supports managing services for users. + +The init scripts are loaded from /etc/user.d/init.d, and +${XDG_CONFIG_HOME}/openrc/init.d with ~/.config should XDG_CONFIG_HOME be unset. + +Configurations in ~/.config/openrc/conf.d should overwrite /etc/user.d/conf.d, +and similarly options set in ~/.config/openrc/rc.conf overrides /etc/rc.conf. + +Runlevels are kept in ~/.config/openrc/runlevels. + +`openrc` and all `rc-*` tools provie a --user/-U flag to operate with user-mode +services and runlevels. + +The XDG_RUNTIME_DIR variable must be set before calling openrc --user, as it's +used to store state for openrc itself and the services it runs.