diff --git a/.gitignore b/.gitignore index 908c563f41..061b4af9c3 100644 --- a/.gitignore +++ b/.gitignore @@ -101,6 +101,7 @@ /systemd-socket-proxyd /systemd-sysctl /systemd-system-update-generator +/systemd-sysv-generator /systemd-timedated /systemd-timesyncd /systemd-tmpfiles diff --git a/Makefile.am b/Makefile.am index 8d5082fca6..ee4ced390d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -549,6 +549,7 @@ nodist_systemunit_DATA += \ units/halt-local.service systemgenerator_PROGRAMS += \ + systemd-sysv-generator \ systemd-rc-local-generator endif @@ -1914,6 +1915,15 @@ INSTALL_EXEC_HOOKS += dbus1-generator-install-hook UNINSTALL_EXEC_HOOKS += dbus1-generator-uninstall-hook endif +# ------------------------------------------------------------------------------ +systemd_sysv_generator_SOURCES = \ + src/sysv-generator/sysv-generator.c + +systemd_sysv_generator_LDADD = \ + libsystemd-core.la \ + libsystemd-label.la \ + libsystemd-shared.la + # ------------------------------------------------------------------------------ systemd_rc_local_generator_SOURCES = \ src/rc-local-generator/rc-local-generator.c diff --git a/src/sysv-generator/Makefile b/src/sysv-generator/Makefile new file mode 100644 index 0000000000..530e5e919a --- /dev/null +++ b/src/sysv-generator/Makefile @@ -0,0 +1 @@ +../Makefile diff --git a/src/sysv-generator/sysv-generator.c b/src/sysv-generator/sysv-generator.c new file mode 100644 index 0000000000..0b8d8f73dd --- /dev/null +++ b/src/sysv-generator/sysv-generator.c @@ -0,0 +1,919 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/*** + This file is part of systemd. + + Copyright 2014 Thomas H.P. Andersen + Copyright 2010 Lennart Poettering + Copyright 2011 Michal Schmidt + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see . +***/ + +#include +#include +#include + +#include "util.h" +#include "mkdir.h" +#include "strv.h" +#include "path-util.h" +#include "path-lookup.h" +#include "log.h" +#include "strv.h" +#include "unit.h" +#include "unit-name.h" +#include "special.h" +#include "exit-status.h" +#include "def.h" +#include "env-util.h" +#include "fileio.h" +#include "hashmap.h" + +typedef enum RunlevelType { + RUNLEVEL_UP, + RUNLEVEL_DOWN +} RunlevelType; + +static const struct { + const char *path; + const char *target; + const RunlevelType type; +} rcnd_table[] = { + /* Standard SysV runlevels for start-up */ + { "rc1.d", SPECIAL_RESCUE_TARGET, RUNLEVEL_UP }, + { "rc2.d", SPECIAL_RUNLEVEL2_TARGET, RUNLEVEL_UP }, + { "rc3.d", SPECIAL_RUNLEVEL3_TARGET, RUNLEVEL_UP }, + { "rc4.d", SPECIAL_RUNLEVEL4_TARGET, RUNLEVEL_UP }, + { "rc5.d", SPECIAL_RUNLEVEL5_TARGET, RUNLEVEL_UP }, + + /* Standard SysV runlevels for shutdown */ + { "rc0.d", SPECIAL_POWEROFF_TARGET, RUNLEVEL_DOWN }, + { "rc6.d", SPECIAL_REBOOT_TARGET, RUNLEVEL_DOWN } + + /* Note that the order here matters, as we read the + directories in this order, and we want to make sure that + sysv_start_priority is known when we first load the + unit. And that value we only know from S links. Hence + UP must be read before DOWN */ +}; + +typedef struct SysvStub { + char *name; + char *path; + char *description; + int sysv_start_priority; + char *pid_file; + char **before; + char **after; + char **wants; + char **conflicts; + bool has_lsb; + bool reload; +} SysvStub; + +const char *arg_dest = "/tmp"; + +static int add_symlink(const char *service, const char *where) { + _cleanup_free_ char *from = NULL, *to = NULL; + int r; + + assert(service); + assert(where); + + from = strjoin(arg_dest, "/", service, NULL); + if (!from) + return log_oom(); + + to = strjoin(arg_dest, "/", where, ".wants/", service, NULL); + if (!to) + return log_oom(); + + mkdir_parents_label(to, 0755); + + r = symlink(from, to); + if (r < 0) { + if (errno == EEXIST) + return 0; + return -errno; + } + + return 1; +} + +static int generate_unit_file(SysvStub *s) { + char *unit; + char **p; + _cleanup_fclose_ FILE *f = NULL; + _cleanup_free_ char *before = NULL; + _cleanup_free_ char *after = NULL; + _cleanup_free_ char *conflicts = NULL; + int r; + + before = strv_join(s->before, " "); + if (!before) + return log_oom(); + + after = strv_join(s->after, " "); + if (!after) + return log_oom(); + + conflicts = strv_join(s->conflicts, " "); + if (!conflicts) + return log_oom(); + + unit = strjoin(arg_dest, "/", s->name, NULL); + if (!unit) + return log_oom(); + + f = fopen(unit, "wxe"); + if (!f) { + log_error("Failed to create unit file %s: %m", unit); + return -errno; + } + + fprintf(f, + "# Automatically generated by systemd-sysv-generator\n\n" + "[Unit]\n" + "SourcePath=%s\n" + "Description=%s\n", + s->path, s->description); + + if (!isempty(before)) + fprintf(f, "Before=%s\n", before); + if (!isempty(after)) + fprintf(f, "After=%s\n", after); + if (!isempty(conflicts)) + fprintf(f, "Conflicts=%s\n", conflicts); + + fprintf(f, + "\n[Service]\n" + "Type=forking\n" + "Restart=no\n" + "TimeoutSec=5min\n" + "IgnoreSIGPIPE=no\n" + "KillMode=process\n" + "GuessMainPID=no\n" + "RemainAfterExit=%s\n", + yes_no(!s->pid_file)); + + if (s->sysv_start_priority > 0) + fprintf(f, "SysVStartPriority=%d\n", s->sysv_start_priority); + + if (s->pid_file) + fprintf(f, "PidFile=%s\n", s->pid_file); + + fprintf(f, + "ExecStart=%s start\n" + "ExecStop=%s stop\n", + s->path, s->path); + + if (s->reload) + fprintf(f, "ExecReload=%s reload\n", s->path); + + STRV_FOREACH(p, s->wants) { + r = add_symlink(s->name, *p); + if (r < 0) + log_error_unit(s->name, "Failed to create 'Wants' symlink to %s: %s", *p, strerror(-r)); + } + + return 0; +} + +static bool usage_contains_reload(const char *line) { + return (strcasestr(line, "{reload|") || + strcasestr(line, "{reload}") || + strcasestr(line, "{reload\"") || + strcasestr(line, "|reload|") || + strcasestr(line, "|reload}") || + strcasestr(line, "|reload\"")); +} + +static char *sysv_translate_name(const char *name) { + char *r; + + r = new(char, strlen(name) + strlen(".service") + 1); + if (!r) + return NULL; + + if (endswith(name, ".sh")) + /* Drop .sh suffix */ + strcpy(stpcpy(r, name) - 3, ".service"); + else + /* Normal init script name */ + strcpy(stpcpy(r, name), ".service"); + + return r; +} + +static int sysv_translate_facility(const char *name, const char *filename, char **_r) { + + /* We silently ignore the $ prefix here. According to the LSB + * spec it simply indicates whether something is a + * standardized name or a distribution-specific one. Since we + * just follow what already exists and do not introduce new + * uses or names we don't care who introduced a new name. */ + + static const char * const table[] = { + /* LSB defined facilities */ + "local_fs", NULL, + "network", SPECIAL_NETWORK_ONLINE_TARGET, + "named", SPECIAL_NSS_LOOKUP_TARGET, + "portmap", SPECIAL_RPCBIND_TARGET, + "remote_fs", SPECIAL_REMOTE_FS_TARGET, + "syslog", NULL, + "time", SPECIAL_TIME_SYNC_TARGET, + }; + + unsigned i; + char *r; + const char *n; + + assert(name); + assert(_r); + + n = *name == '$' ? name + 1 : name; + + for (i = 0; i < ELEMENTSOF(table); i += 2) { + + if (!streq(table[i], n)) + continue; + + if (!table[i+1]) + return 0; + + r = strdup(table[i+1]); + if (!r) + return log_oom(); + + goto finish; + } + + /* If we don't know this name, fallback heuristics to figure + * out whether something is a target or a service alias. */ + + if (*name == '$') { + if (!unit_prefix_is_valid(n)) + return -EINVAL; + + /* Facilities starting with $ are most likely targets */ + r = unit_name_build(n, NULL, ".target"); + } else if (filename && streq(name, filename)) + /* Names equaling the file name of the services are redundant */ + return 0; + else + /* Everything else we assume to be normal service names */ + r = sysv_translate_name(n); + + if (!r) + return -ENOMEM; + +finish: + *_r = r; + + return 1; +} + +static int load_sysv(SysvStub *s) { + _cleanup_fclose_ FILE *f; + unsigned line = 0; + int r; + enum { + NORMAL, + DESCRIPTION, + LSB, + LSB_DESCRIPTION, + USAGE_CONTINUATION + } state = NORMAL; + _cleanup_free_ char *short_description = NULL, *long_description = NULL, *chkconfig_description = NULL; + char *description; + bool supports_reload = false; + + assert(s); + + f = fopen(s->path, "re"); + if (!f) + return errno == ENOENT ? 0 : -errno; + + while (!feof(f)) { + char l[LINE_MAX], *t; + + if (!fgets(l, sizeof(l), f)) { + if (feof(f)) + break; + + log_error_unit(s->name, + "Failed to read configuration file '%s': %m", + s->path); + return -errno; + } + + line++; + + t = strstrip(l); + if (*t != '#') { + /* Try to figure out whether this init script supports + * the reload operation. This heuristic looks for + * "Usage" lines which include the reload option. */ + if ( state == USAGE_CONTINUATION || + (state == NORMAL && strcasestr(t, "usage"))) { + if (usage_contains_reload(t)) { + supports_reload = true; + state = NORMAL; + } else if (t[strlen(t)-1] == '\\') + state = USAGE_CONTINUATION; + else + state = NORMAL; + } + + continue; + } + + if (state == NORMAL && streq(t, "### BEGIN INIT INFO")) { + state = LSB; + s->has_lsb = true; + continue; + } + + if ((state == LSB_DESCRIPTION || state == LSB) && streq(t, "### END INIT INFO")) { + state = NORMAL; + continue; + } + + t++; + t += strspn(t, WHITESPACE); + + if (state == NORMAL) { + + /* Try to parse Red Hat style description */ + + if (startswith_no_case(t, "description:")) { + + size_t k = strlen(t); + char *d; + const char *j; + + if (t[k-1] == '\\') { + state = DESCRIPTION; + t[k-1] = 0; + } + + j = strstrip(t+12); + if (j && *j) { + d = strdup(j); + if (!d) + return -ENOMEM; + } else + d = NULL; + + free(chkconfig_description); + chkconfig_description = d; + + } else if (startswith_no_case(t, "pidfile:")) { + + char *fn; + + state = NORMAL; + + fn = strstrip(t+8); + if (!path_is_absolute(fn)) { + log_error_unit(s->name, + "[%s:%u] PID file not absolute. Ignoring.", + s->path, line); + continue; + } + + fn = strdup(fn); + if (!fn) + return -ENOMEM; + + free(s->pid_file); + s->pid_file = fn; + } + + } else if (state == DESCRIPTION) { + + /* Try to parse Red Hat style description + * continuation */ + + size_t k = strlen(t); + char *j; + + if (t[k-1] == '\\') + t[k-1] = 0; + else + state = NORMAL; + + j = strstrip(t); + if (j && *j) { + char *d = NULL; + + if (chkconfig_description) + d = strjoin(chkconfig_description, " ", j, NULL); + else + d = strdup(j); + + if (!d) + return -ENOMEM; + + free(chkconfig_description); + chkconfig_description = d; + } + + } else if (state == LSB || state == LSB_DESCRIPTION) { + + if (startswith_no_case(t, "Provides:")) { + char *i, *w; + size_t z; + + state = LSB; + + FOREACH_WORD_QUOTED(w, z, t+9, i) { + _cleanup_free_ char *n = NULL, *m = NULL; + + n = strndup(w, z); + if (!n) + return -ENOMEM; + + r = sysv_translate_facility(n, basename(s->path), &m); + + if (r < 0) + return r; + + if (r == 0) + continue; + + if (unit_name_to_type(m) != UNIT_SERVICE) { + /* NB: SysV targets + * which are provided + * by a service are + * pulled in by the + * services, as an + * indication that the + * generic service is + * now available. This + * is strictly + * one-way. The + * targets do NOT pull + * in the SysV + * services! */ + r = strv_extend(&s->before, m); + if (r < 0) + return log_oom(); + r = strv_extend(&s->wants, m); + if (r < 0) + return log_oom(); + } + + if (r < 0) + log_error_unit(s->name, + "[%s:%u] Failed to add LSB Provides name %s, ignoring: %s", + s->path, line, m, strerror(-r)); + } + + } else if (startswith_no_case(t, "Required-Start:") || + startswith_no_case(t, "Should-Start:") || + startswith_no_case(t, "X-Start-Before:") || + startswith_no_case(t, "X-Start-After:")) { + char *i, *w; + size_t z; + + state = LSB; + + FOREACH_WORD_QUOTED(w, z, strchr(t, ':')+1, i) { + _cleanup_free_ char *n = NULL, *m = NULL; + bool is_before; + + n = strndup(w, z); + if (!n) + return -ENOMEM; + + r = sysv_translate_facility(n, basename(s->path), &m); + if (r < 0) { + log_error_unit(s->name, + "[%s:%u] Failed to translate LSB dependency %s, ignoring: %s", + s->path, line, n, strerror(-r)); + continue; + } + + if (r == 0) + continue; + + is_before = startswith_no_case(t, "X-Start-Before:"); + + if (streq(m, SPECIAL_NETWORK_ONLINE_TARGET) && !is_before) { + /* the network-online target is special, as it needs to be actively pulled in */ + r = strv_extend(&s->after, m); + if (r < 0) + return log_oom(); + r = strv_extend(&s->wants, m); + if (r < 0) + return log_oom(); + } + else { + if (is_before) { + r = strv_extend(&s->before, m); + if (r < 0) + return log_oom(); + } + else { + r = strv_extend(&s->after, m); + if (r < 0) + return log_oom(); + } + } + + if (r < 0) + log_error_unit(s->name, + "[%s:%u] Failed to add dependency on %s, ignoring: %s", + s->path, line, m, strerror(-r)); + } + + } else if (startswith_no_case(t, "Description:")) { + char *d, *j; + + state = LSB_DESCRIPTION; + + j = strstrip(t+12); + if (j && *j) { + d = strdup(j); + if (!d) + return -ENOMEM; + } else + d = NULL; + + free(long_description); + long_description = d; + + } else if (startswith_no_case(t, "Short-Description:")) { + char *d, *j; + + state = LSB; + + j = strstrip(t+18); + if (j && *j) { + d = strdup(j); + if (!d) + return -ENOMEM; + } else + d = NULL; + + free(short_description); + short_description = d; + + } else if (state == LSB_DESCRIPTION) { + + if (startswith(l, "#\t") || startswith(l, "# ")) { + char *j; + + j = strstrip(t); + if (j && *j) { + char *d = NULL; + + if (long_description) + d = strjoin(long_description, " ", t, NULL); + else + d = strdup(j); + + if (!d) + return -ENOMEM; + + free(long_description); + long_description = d; + } + + } else + state = LSB; + } + } + } + + s->reload = supports_reload; + + /* We use the long description only if + * no short description is set. */ + + if (short_description) + description = short_description; + else if (chkconfig_description) + description = chkconfig_description; + else if (long_description) + description = long_description; + else + description = NULL; + + if (description) { + char *d; + + d = strappend(s->has_lsb ? "LSB: " : "SYSV: ", description); + if (!d) + return -ENOMEM; + + s->description = d; + } + + return 0; +} + +static int fix_order(SysvStub *s, Hashmap *all_services) { + SysvStub *other; + Iterator j; + int r; + + assert(s); + + if (s->sysv_start_priority < 0) + return 0; + + HASHMAP_FOREACH(other, all_services, j) { + if (s == other) + continue; + + if (other->sysv_start_priority < 0) + continue; + + /* If both units have modern headers we don't care + * about the priorities */ + if (s->has_lsb && other->has_lsb) + continue; + + if (other->sysv_start_priority < s->sysv_start_priority) { + r = strv_extend(&s->after, other->name); + if (r < 0) + return log_oom(); + } + else if (other->sysv_start_priority > s->sysv_start_priority) { + r = strv_extend(&s->before, other->name); + if (r < 0) + return log_oom(); + } + else + continue; + + /* FIXME: Maybe we should compare the name here lexicographically? */ + } + + return 0; +} + +static int enumerate_sysv(LookupPaths lp, Hashmap *all_services) { + char **path; + + STRV_FOREACH(path, lp.sysvinit_path) { + _cleanup_closedir_ DIR *d = NULL; + struct dirent *de; + + d = opendir(*path); + if (!d) { + if (errno != ENOENT) + log_warning("opendir(%s) failed: %m", *path); + continue; + } + + while ((de = readdir(d))) { + SysvStub *service; + struct stat st; + _cleanup_free_ char *fpath = NULL, *name = NULL; + int r; + + if (ignore_file(de->d_name)) + continue; + + fpath = strjoin(*path, "/", de->d_name, NULL); + if (!fpath) + return log_oom(); + + if (stat(fpath, &st) < 0) + continue; + + if (!(st.st_mode & S_IXUSR)) + continue; + + name = sysv_translate_name(de->d_name); + if (!name) + return log_oom(); + + if (hashmap_contains(all_services, name)) + continue; + + service = new0(SysvStub, 1); + if (!service) + return log_oom(); + + service->sysv_start_priority = -1; + service->name = name; + service->path = fpath; + + r = hashmap_put(all_services, service->name, service); + if (r < 0) + return log_oom(); + + name = fpath = NULL; + } + } + + return 0; +} + +static int set_dependencies_from_rcnd(LookupPaths lp, Hashmap *all_services) { + char **p; + unsigned i; + _cleanup_closedir_ DIR *d = NULL; + _cleanup_free_ char *path = NULL, *fpath = NULL, *name = NULL; + SysvStub *service; + Iterator j; + Set *runlevel_services[ELEMENTSOF(rcnd_table)] = {}; + _cleanup_set_free_ Set *shutdown_services = NULL; + int r = 0; + + STRV_FOREACH(p, lp.sysvrcnd_path) + for (i = 0; i < ELEMENTSOF(rcnd_table); i ++) { + struct dirent *de; + + free(path); + path = strjoin(*p, "/", rcnd_table[i].path, NULL); + if (!path) + return -ENOMEM; + + if (d) + closedir(d); + + d = opendir(path); + if (!d) { + if (errno != ENOENT) + log_warning("opendir(%s) failed: %m", path); + + continue; + } + + while ((de = readdir(d))) { + int a, b; + + if (ignore_file(de->d_name)) + continue; + + if (de->d_name[0] != 'S' && de->d_name[0] != 'K') + continue; + + if (strlen(de->d_name) < 4) + continue; + + a = undecchar(de->d_name[1]); + b = undecchar(de->d_name[2]); + + if (a < 0 || b < 0) + continue; + + free(fpath); + fpath = strjoin(*p, "/", de->d_name, NULL); + if (!fpath) { + r = -ENOMEM; + goto finish; + } + + name = sysv_translate_name(de->d_name + 3); + if (!name) { + r = log_oom(); + goto finish; + } + + if (hashmap_contains(all_services, name)) + service = hashmap_get(all_services, name); + else { + log_warning("Could not find init scirpt for %s", name); + continue; + } + + if (de->d_name[0] == 'S') { + + if (rcnd_table[i].type == RUNLEVEL_UP) { + service->sysv_start_priority = + MAX(a*10 + b, service->sysv_start_priority); + } + + r = set_ensure_allocated(&runlevel_services[i], + trivial_hash_func, trivial_compare_func); + if (r < 0) + goto finish; + + r = set_put(runlevel_services[i], service); + if (r < 0) + goto finish; + + } else if (de->d_name[0] == 'K' && + (rcnd_table[i].type == RUNLEVEL_DOWN)) { + + r = set_ensure_allocated(&shutdown_services, + trivial_hash_func, trivial_compare_func); + if (r < 0) + goto finish; + + r = set_put(shutdown_services, service); + if (r < 0) + goto finish; + } + } + } + + + for (i = 0; i < ELEMENTSOF(rcnd_table); i ++) + SET_FOREACH(service, runlevel_services[i], j) { + r = strv_extend(&service->before, rcnd_table[i].target); + if (r < 0) + return log_oom(); + r = strv_extend(&service->wants, rcnd_table[i].target); + if (r < 0) + return log_oom(); + } + + SET_FOREACH(service, shutdown_services, j) { + r = strv_extend(&service->before, SPECIAL_SHUTDOWN_TARGET); + if (r < 0) + return log_oom(); + r = strv_extend(&service->conflicts, SPECIAL_SHUTDOWN_TARGET); + if (r < 0) + return log_oom(); + } + + r = 0; + +finish: + + for (i = 0; i < ELEMENTSOF(rcnd_table); i++) + set_free(runlevel_services[i]); + + return r; +} + +int main(int argc, char *argv[]) { + int r, q; + LookupPaths lp; + Hashmap *all_services; + SysvStub *service; + Iterator j; + + if (argc > 1 && argc != 4) { + log_error("This program takes three or no arguments."); + return EXIT_FAILURE; + } + + if (argc > 1) + arg_dest = argv[3]; + + log_set_target(LOG_TARGET_SAFE); + log_parse_environment(); + log_open(); + + umask(0022); + + r = lookup_paths_init(&lp, SYSTEMD_SYSTEM, true, NULL, NULL, NULL, NULL); + if (r < 0) { + log_error("Failed to find lookup paths."); + return EXIT_FAILURE; + } + + all_services = hashmap_new(string_hash_func, string_compare_func); + if (!all_services) { + log_oom(); + return EXIT_FAILURE; + } + + r = enumerate_sysv(lp, all_services); + if (r < 0) { + log_error("Failed to generate units for all init scripts."); + return EXIT_FAILURE; + } + + r = set_dependencies_from_rcnd(lp, all_services); + if (r < 0) { + log_error("Failed to read runlevels from rcnd links."); + return EXIT_FAILURE; + } + + HASHMAP_FOREACH(service, all_services, j) { + q = load_sysv(service); + if (q < 0) + continue; + + q = fix_order(service, all_services); + if (q < 0) + continue; + + q = generate_unit_file(service); + if (q < 0) + continue; + } + + return EXIT_SUCCESS; +}