X-Git-Url: https://git.librecmc.org/?a=blobdiff_plain;f=src%2Fdinit.cc;h=b7fe727a007ecb7f2bae19fe303d160d9fddc2c9;hb=f50aeb7dc90cd932deecdb648962f41d44cc8b71;hp=7d3c2fd0a38fb19c00d0df481d3153fb7101a95e;hpb=41cefb9ccfeab5f506b0c21074a6850a77b70efb;p=oweals%2Fdinit.git diff --git a/src/dinit.cc b/src/dinit.cc index 7d3c2fd..b7fe727 100644 --- a/src/dinit.cc +++ b/src/dinit.cc @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -12,17 +13,24 @@ #include #include #include +#ifdef __linux__ +#include +#include +#include +#endif +#if defined(__FreeBSD__) || defined(__DragonFly__) +#include +#endif +#include "dinit.h" #include "dasynq.h" #include "service.h" #include "control.h" #include "dinit-log.h" #include "dinit-socket.h" +#include "static-string.h" -#ifdef __linux__ -#include -#include -#endif +#include "mconfig.h" /* * When running as the system init process, Dinit processes the following signals: @@ -37,23 +45,21 @@ * services even if the halt/reboot commands are unavailable for some reason. */ +using namespace cts; -using namespace dasynq; -using eventloop_t = event_loop; +using eventloop_t = dasynq::event_loop; -eventloop_t eventLoop = eventloop_t(); +eventloop_t event_loop; static void sigint_reboot_cb(eventloop_t &eloop) noexcept; static void sigquit_cb(eventloop_t &eloop) noexcept; static void sigterm_cb(eventloop_t &eloop) noexcept; static void close_control_socket() noexcept; static void wait_for_user_input() noexcept; +static void read_env_file(const char *); static void control_socket_cb(eventloop_t *loop, int fd); -void open_control_socket(bool report_ro_failure = true) noexcept; -void setup_external_log() noexcept; - // Variables @@ -67,10 +73,13 @@ int active_control_conns = 0; // Control socket path. We maintain a string (control_socket_str) in case we need // to allocate storage, but control_socket_path is the authoritative value. -static const char *control_socket_path = "/dev/dinitctl"; +static const char *control_socket_path = SYSCONTROLSOCKET; static std::string control_socket_str; -static const char *log_socket_path = "/dev/log"; +static const char *env_file_path = "/etc/dinit/environment"; + +static const char *log_path = "/dev/log"; +static bool log_is_syslog = true; // if false, log is a file static const char *user_home_path = nullptr; @@ -95,6 +104,8 @@ const char * get_user_home() namespace { class callback_signal_handler : public eventloop_t::signal_watcher_impl { + using rearm = dasynq::rearm; + public: typedef void (*cb_func_t)(eventloop_t &); @@ -119,6 +130,8 @@ namespace { class control_socket_watcher : public eventloop_t::fd_watcher_impl { + using rearm = dasynq::rearm; + public: rearm fd_event(eventloop_t &loop, int fd, int flags) noexcept { @@ -127,7 +140,28 @@ namespace { } }; + // Simple timer used to limit the amount of time waiting for the log flush to complete (at shutdown) + class log_flush_timer_t : public eventloop_t::timer_impl + { + using rearm = dasynq::rearm; + + bool expired = false; + + public: + rearm timer_expiry(eventloop_t &, int expiry_count) + { + expired = true; + return rearm::DISARM; + } + + bool has_expired() + { + return expired; + } + }; + control_socket_watcher control_socket_io; + log_flush_timer_t log_flush_timer; } int dinit_main(int argc, char **argv) @@ -136,24 +170,34 @@ int dinit_main(int argc, char **argv) am_system_init = (getpid() == 1); const char * service_dir = nullptr; + bool service_dir_dynamic = false; // service_dir dynamically allocated? + const char * env_file = nullptr; string service_dir_str; // to hold storage for above if necessary bool control_socket_path_set = false; + bool env_file_set = false; // list of services to start list services_to_start; // Arguments, if given, specify a list of services to start. - // If we are running as init (PID=1), the kernel gives us any command line - // arguments it was given but didn't recognize, including "single" (usually - // for "boot to single user mode" aka just start the shell). We can treat - // them as service names. In the worst case we can't find any of the named + // If we are running as init (PID=1), the Linux kernel gives us any command line arguments it was given + // but didn't recognize, including "single" (usually for "boot to single user mode" aka just start the + // shell). We can treat them as service names. In the worst case we can't find any of the named // services, and so we'll start the "boot" service by default. if (argc > 1) { for (int i = 1; i < argc; i++) { if (argv[i][0] == '-') { // An option... - if (strcmp(argv[i], "--services-dir") == 0 || - strcmp(argv[i], "-d") == 0) { + if (strcmp(argv[i], "--env-file") == 0 || strcmp(argv[i], "-e") == 0) { + if (++i < argc) { + env_file_set = true; + env_file = argv[i]; + } + else { + cerr << "dinit: '--env-file' (-e) requires an argument" << endl; + } + } + else if (strcmp(argv[i], "--services-dir") == 0 || strcmp(argv[i], "-d") == 0) { if (++i < argc) { service_dir = argv[i]; } @@ -162,12 +206,10 @@ int dinit_main(int argc, char **argv) return 1; } } - else if (strcmp(argv[i], "--system") == 0 || - strcmp(argv[i], "-s") == 0) { + else if (strcmp(argv[i], "--system") == 0 || strcmp(argv[i], "-s") == 0) { am_system_init = true; } - else if (strcmp(argv[i], "--socket-path") == 0 || - strcmp(argv[i], "-p") == 0) { + else if (strcmp(argv[i], "--socket-path") == 0 || strcmp(argv[i], "-p") == 0) { if (++i < argc) { control_socket_path = argv[i]; control_socket_path_set = true; @@ -177,9 +219,21 @@ int dinit_main(int argc, char **argv) return 1; } } + else if (strcmp(argv[i], "--log-file") == 0 || strcmp(argv[i], "-l") == 0) { + if (++i < argc) { + log_path = argv[i]; + log_is_syslog = false; + } + else { + cerr << "dinit: '--log-file' (-l) requires an argument" << endl; + return 1; + } + } else if (strcmp(argv[i], "--help") == 0) { cout << "dinit, an init with dependency management" << endl; cout << " --help display help" << endl; + cout << " --env-file , -e " << endl; + cout << " environment variable initialisation file" << endl; cout << " --services-dir , -d " << endl; cout << " set base directory for service description" << endl; cout << " files (-d )" << endl; @@ -203,6 +257,8 @@ int dinit_main(int argc, char **argv) if (! am_system_init || strcmp(argv[i], "auto") != 0) { services_to_start.push_back(argv[i]); } +#else + services_to_start.push_back(argv[i]); #endif } } @@ -218,8 +274,12 @@ int dinit_main(int argc, char **argv) if (onefd > 2) close(onefd); if (twofd > 2) close(twofd); + + if (! env_file_set) { + env_file = env_file_path; + } } - + /* Set up signal handlers etc */ /* SIG_CHILD is ignored by default: good */ sigset_t sigwait_set; @@ -229,7 +289,7 @@ int dinit_main(int argc, char **argv) sigaddset(&sigwait_set, SIGTERM); if (am_system_init) sigaddset(&sigwait_set, SIGQUIT); sigprocmask(SIG_BLOCK, &sigwait_set, NULL); - + // Terminal access control signals - we block these so that dinit can't be // suspended if it writes to the terminal after some other process has claimed // ownership of it. @@ -252,16 +312,20 @@ int dinit_main(int argc, char **argv) if (service_dir == nullptr && ! am_system_init) { const char * userhome = get_user_home(); if (userhome != nullptr) { - service_dir_str = get_user_home(); - service_dir_str += "/dinit.d"; - service_dir = service_dir_str.c_str(); + const char * user_home = get_user_home(); + size_t user_home_len = strlen(user_home); + size_t dinit_d_len = strlen("/dinit.d"); + size_t full_len = user_home_len + dinit_d_len + 1; + char *service_dir_w = new char[full_len]; + std::memcpy(service_dir_w, user_home, user_home_len); + std::memcpy(service_dir_w + user_home_len, "/dinit.d", dinit_d_len); + service_dir_w[full_len - 1] = 0; + + service_dir = service_dir_w; + service_dir_dynamic = true; } } - if (service_dir == nullptr) { - service_dir = "/etc/dinit.d"; - } - if (services_to_start.empty()) { services_to_start.push_back("boot"); } @@ -279,12 +343,12 @@ int dinit_main(int argc, char **argv) sigint_watcher.setCbFunc(sigterm_cb); } - sigint_watcher.add_watch(eventLoop, SIGINT); - sigterm_watcher.add_watch(eventLoop, SIGTERM); + sigint_watcher.add_watch(event_loop, SIGINT); + sigterm_watcher.add_watch(event_loop, SIGTERM); if (am_system_init) { // PID 1: SIGQUIT exec's shutdown - sigquit_watcher.add_watch(eventLoop, SIGQUIT); + sigquit_watcher.add_watch(event_loop, SIGQUIT); // As a user process, we instead just let SIGQUIT perform the default action. } @@ -298,13 +362,45 @@ int dinit_main(int argc, char **argv) // Make ctrl+alt+del combination send SIGINT to PID 1 (this process) reboot(RB_DISABLE_CAD); } + + // Mark ourselves as a subreaper. This means that if a process we start double-forks, the + // orphaned child will re-parent to us rather than to PID 1 (although that could be us too). + prctl(PR_SET_CHILD_SUBREAPER, 1); +#elif defined(__FreeBSD__) || defined(__DragonFly__) + // Documentation (man page) for this kind of sucks. PROC_REAP_ACQUIRE "acquires the reaper status for + // the current process" but does that mean the first two arguments still need valid values to be + // supplied? We'll play it safe and explicitly target our own process: + procctl(P_PID, getpid(), PROC_REAP_ACQUIRE, NULL); #endif + log_flush_timer.add_timer(event_loop, dasynq::clock_type::MONOTONIC); + + bool add_all_service_dirs = false; + if (service_dir == nullptr) { + service_dir = "/etc/dinit.d"; + add_all_service_dirs = true; + } + /* start requested services */ - services = new dirload_service_set(service_dir); + services = new dirload_service_set(service_dir, service_dir_dynamic); + if (add_all_service_dirs) { + services->add_service_dir("/usr/local/lib/dinit.d", false); + services->add_service_dir("/lib/dinit.d", false); + } - init_log(services); + init_log(services, log_is_syslog); + if (am_system_init) { + log(loglevel_t::INFO, false, "starting system"); + } + // Only try to set up the external log now if we aren't the system init. (If we are the + // system init, wait until the log service starts). + if (! am_system_init) setup_external_log(); + + if (env_file != nullptr) { + read_env_file(env_file); + } + for (auto svc : services_to_start) { try { services->start_service(svc); @@ -314,13 +410,13 @@ int dinit_main(int argc, char **argv) // exit if user process). } catch (service_not_found &snf) { - log(LogLevel::ERROR, snf.serviceName, ": Could not find service description."); + log(loglevel_t::ERROR, snf.serviceName, ": Could not find service description."); } catch (service_load_exc &sle) { - log(LogLevel::ERROR, sle.serviceName, ": ", sle.excDescription); + log(loglevel_t::ERROR, sle.serviceName, ": ", sle.excDescription); } catch (std::bad_alloc &badalloce) { - log(LogLevel::ERROR, "Out of memory when trying to start service: ", svc, "."); + log(loglevel_t::ERROR, "Out of memory when trying to start service: ", svc, "."); break; } } @@ -329,30 +425,31 @@ int dinit_main(int argc, char **argv) // Process events until all services have terminated. while (services->count_active_services() != 0) { - eventLoop.run(); + event_loop.run(); } - shutdown_type_t shutdown_type = services->getShutdownType(); + shutdown_type_t shutdown_type = services->get_shutdown_type(); if (am_system_init) { - logMsgBegin(LogLevel::INFO, "No more active services."); + log_msg_begin(loglevel_t::INFO, "No more active services."); if (shutdown_type == shutdown_type_t::REBOOT) { - logMsgEnd(" Will reboot."); + log_msg_end(" Will reboot."); } else if (shutdown_type == shutdown_type_t::HALT) { - logMsgEnd(" Will halt."); + log_msg_end(" Will halt."); } else if (shutdown_type == shutdown_type_t::POWEROFF) { - logMsgEnd(" Will power down."); + log_msg_end(" Will power down."); } else { - logMsgEnd(" Re-initiating boot sequence."); + log_msg_end(" Re-initiating boot sequence."); } } - while (! is_log_flushed()) { - eventLoop.run(); + log_flush_timer.arm_timer_rel(event_loop, timespec{5,0}); // 5 seconds + while (! is_log_flushed() && ! log_flush_timer.has_expired()) { + event_loop.run(); } close_control_socket(); @@ -368,7 +465,7 @@ int dinit_main(int argc, char **argv) } catch (...) { // Now what do we do? try to reboot, but wait for user ack to avoid boot loop. - log(LogLevel::ERROR, "Could not start 'boot' service. Will attempt reboot."); + log(loglevel_t::ERROR, "Could not start 'boot' service. Will attempt reboot."); wait_for_user_input(); shutdown_type = shutdown_type_t::REBOOT; } @@ -387,12 +484,14 @@ int dinit_main(int argc, char **argv) } // Fork and execute dinit-reboot. - execl("/sbin/shutdown", "/sbin/shutdown", "--system", cmd_arg, nullptr); - log(LogLevel::ERROR, "Could not execute /sbin/shutdown: ", strerror(errno)); + constexpr auto shutdown_exec = literal(SBINDIR) + "/shutdown"; + execl(shutdown_exec.c_str(), shutdown_exec.c_str(), "--system", cmd_arg, nullptr); + log(loglevel_t::ERROR, (literal("Could not execute ") + SBINDIR + "/shutdown: ").c_str(), + strerror(errno)); // PID 1 must not actually exit, although we should never reach this point: while (true) { - eventLoop.run(); + event_loop.run(); } } else if (shutdown_type == shutdown_type_t::REBOOT) { @@ -407,6 +506,65 @@ int dinit_main(int argc, char **argv) return 0; } +static void log_bad_env(int linenum) +{ + log(loglevel_t::ERROR, "invalid environment variable setting in environment file (line ", linenum, ")"); +} + +// Read and set environment variables from a file. +static void read_env_file(const char *env_file_path) +{ + // Note that we can't use the log in this function; it hasn't been initialised yet. + + std::ifstream env_file(env_file_path); + if (! env_file) return; + + env_file.exceptions(std::ios::badbit); + + auto &clocale = std::locale::classic(); + std::string line; + int linenum = 0; + + while (std::getline(env_file, line)) { + linenum++; + auto lpos = line.begin(); + auto lend = line.end(); + while (lpos != lend && std::isspace(*lpos, clocale)) { + ++lpos; + } + + if (lpos != lend) { + if (*lpos != '#') { + if (*lpos == '=') { + log_bad_env(linenum); + continue; + } + auto name_begin = lpos++; + // skip until '=' or whitespace: + while (lpos != lend && *lpos != '=' && ! std::isspace(*lpos, clocale)) ++lpos; + auto name_end = lpos; + // skip whitespace: + while (lpos != lend && std::isspace(*lpos, clocale)) ++lpos; + if (lpos == lend) { + log_bad_env(linenum); + continue; + } + + ++lpos; + auto val_begin = lpos; + while (lpos != lend && *lpos != '\n') ++lpos; + auto val_end = lpos; + + std::string name = line.substr(name_begin - line.begin(), name_end - name_begin); + std::string value = line.substr(val_begin - line.begin(), val_end - val_begin); + if (setenv(name.c_str(), value.c_str(), true) == -1) { + throw std::system_error(errno, std::system_category()); + } + } + } + } +} + // In exception situations we want user confirmation before proceeding (eg on critical boot failure // we wait before rebooting to avoid a reboot loop). static void wait_for_user_input() noexcept @@ -419,19 +577,20 @@ static void wait_for_user_input() noexcept // Callback for control socket static void control_socket_cb(eventloop_t *loop, int sockfd) { - // TODO limit the number of active connections. Keep a tally, and disable the - // control connection listening socket watcher if it gets high, and re-enable - // it once it falls below the maximum. + // Considered keeping a limit the number of active connections, however, there doesn't + // seem much to be gained from that. Only root can create connections and not being + // able to establish a control connection is as much a denial-of-service as is not being + // able to start a service due to lack of fd's. // Accept a connection int newfd = dinit_accept4(sockfd, nullptr, nullptr, SOCK_NONBLOCK | SOCK_CLOEXEC); if (newfd != -1) { try { - new control_conn_t(loop, services, newfd); // will delete itself when it's finished + new control_conn_t(*loop, services, newfd); // will delete itself when it's finished } catch (std::exception &exc) { - log(LogLevel::ERROR, "Accepting control connection: ", exc.what()); + log(loglevel_t::ERROR, "Accepting control connection: ", exc.what()); close(newfd); } } @@ -446,7 +605,7 @@ void open_control_socket(bool report_ro_failure) noexcept struct sockaddr_un * name = static_cast(malloc(sockaddr_size)); if (name == nullptr) { - log(LogLevel::ERROR, "Opening control socket: out of memory"); + log(loglevel_t::ERROR, "Opening control socket: out of memory"); return; } @@ -461,14 +620,14 @@ void open_control_socket(bool report_ro_failure) noexcept int sockfd = dinit_socket(AF_UNIX, SOCK_STREAM, 0, SOCK_NONBLOCK | SOCK_CLOEXEC); if (sockfd == -1) { - log(LogLevel::ERROR, "Error creating control socket: ", strerror(errno)); + log(loglevel_t::ERROR, "Error creating control socket: ", strerror(errno)); free(name); return; } if (bind(sockfd, (struct sockaddr *) name, sockaddr_size) == -1) { if (errno != EROFS || report_ro_failure) { - log(LogLevel::ERROR, "Error binding control socket: ", strerror(errno)); + log(loglevel_t::ERROR, "Error binding control socket: ", strerror(errno)); } close(sockfd); free(name); @@ -480,24 +639,24 @@ void open_control_socket(bool report_ro_failure) noexcept // No connections can be made until we listen, so it is fine to change the permissions now // (and anyway there is no way to atomically create the socket and set permissions): if (chmod(saddrname, S_IRUSR | S_IWUSR) == -1) { - log(LogLevel::ERROR, "Error setting control socket permissions: ", strerror(errno)); + log(loglevel_t::ERROR, "Error setting control socket permissions: ", strerror(errno)); close(sockfd); return; } if (listen(sockfd, 10) == -1) { - log(LogLevel::ERROR, "Error listening on control socket: ", strerror(errno)); + log(loglevel_t::ERROR, "Error listening on control socket: ", strerror(errno)); close(sockfd); return; } try { - control_socket_io.add_watch(eventLoop, sockfd, IN_EVENTS); + control_socket_io.add_watch(event_loop, sockfd, dasynq::IN_EVENTS); control_socket_open = true; } catch (std::exception &e) { - log(LogLevel::ERROR, "Could not setup I/O on control socket: ", e.what()); + log(loglevel_t::ERROR, "Could not setup I/O on control socket: ", e.what()); close(sockfd); } } @@ -507,7 +666,7 @@ static void close_control_socket() noexcept { if (control_socket_open) { int fd = control_socket_io.get_watched_fd(); - control_socket_io.deregister(eventLoop); + control_socket_io.deregister(event_loop); close(fd); // Unlink the socket: @@ -518,45 +677,66 @@ static void close_control_socket() noexcept void setup_external_log() noexcept { if (! external_log_open) { - - const char * saddrname = log_socket_path; - size_t saddrname_len = strlen(saddrname); - uint sockaddr_size = offsetof(struct sockaddr_un, sun_path) + saddrname_len + 1; - - struct sockaddr_un * name = static_cast(malloc(sockaddr_size)); - if (name == nullptr) { - log(LogLevel::ERROR, "Connecting to log socket: out of memory"); - return; - } - - name->sun_family = AF_UNIX; - memcpy(name->sun_path, saddrname, saddrname_len + 1); - - int sockfd = dinit_socket(AF_UNIX, SOCK_DGRAM, 0, SOCK_NONBLOCK | SOCK_CLOEXEC); - if (sockfd == -1) { - log(LogLevel::ERROR, "Error creating log socket: ", strerror(errno)); - free(name); - return; - } - - if (connect(sockfd, (struct sockaddr *) name, sockaddr_size) == 0 || errno == EINPROGRESS) { - // TODO for EINPROGRESS, set up a watcher so we can properly wait until - // connection is established (or fails) before we pass it to the logging subsystem. - try { - setup_main_log(sockfd); + if (log_is_syslog) { + const char * saddrname = log_path; + size_t saddrname_len = strlen(saddrname); + uint sockaddr_size = offsetof(struct sockaddr_un, sun_path) + saddrname_len + 1; + + struct sockaddr_un * name = static_cast(malloc(sockaddr_size)); + if (name == nullptr) { + log(loglevel_t::ERROR, "Connecting to log socket: out of memory"); + return; + } + + name->sun_family = AF_UNIX; + memcpy(name->sun_path, saddrname, saddrname_len + 1); + + int sockfd = dinit_socket(AF_UNIX, SOCK_DGRAM, 0, SOCK_NONBLOCK | SOCK_CLOEXEC); + if (sockfd == -1) { + log(loglevel_t::ERROR, "Error creating log socket: ", strerror(errno)); + free(name); + return; + } + + if (connect(sockfd, (struct sockaddr *) name, sockaddr_size) == 0 || errno == EINPROGRESS) { + // For EINPROGRESS, connection is still being established; however, we can select on + // the file descriptor so we will be notified when it's ready. In other words we can + // basically use it anyway. + try { + setup_main_log(sockfd); + external_log_open = true; + } + catch (std::exception &e) { + log(loglevel_t::ERROR, "Setting up log failed: ", e.what()); + close(sockfd); + } } - catch (std::exception &e) { - log(LogLevel::ERROR, "Setting up log failed: ", e.what()); + else { + // Note if connect fails, we haven't warned at all, because the syslog server might not + // have started yet. close(sockfd); } + + free(name); } else { - // Note if connect fails, we haven't warned at all, because the syslog server might not - // have started yet. - close(sockfd); + // log to file: + int log_fd = open(log_path, O_WRONLY | O_CREAT | O_APPEND | O_NONBLOCK | O_CLOEXEC, 0644); + if (log_fd >= 0) { + try { + setup_main_log(log_fd); + external_log_open = true; + } + catch (std::exception &e) { + log(loglevel_t::ERROR, "Setting up log failed: ", e.what()); + close(log_fd); + } + } + else { + // log failure to log? It makes more sense than first appears, because we also log to console: + log(loglevel_t::ERROR, "Setting up log failed: ", strerror(errno)); + } } - - free(name); } } @@ -571,8 +751,9 @@ static void sigquit_cb(eventloop_t &eloop) noexcept { // This performs an immediate shutdown, without service rollback. close_control_socket(); - execl("/sbin/shutdown", "/sbin/shutdown", "--system", (char *) 0); - log(LogLevel::ERROR, "Error executing /sbin/shutdown: ", strerror(errno)); + constexpr auto shutdown_exec = literal(SBINDIR) + "/shutdown"; + execl(shutdown_exec.c_str(), shutdown_exec.c_str(), "--system", (char *) 0); + log(loglevel_t::ERROR, literal("Error executing ") + SBINDIR + "/sbin/shutdown: ", strerror(errno)); sync(); // since a hard poweroff might be required at this point... }