#include <unistd.h>
#include <fcntl.h>
#include <pwd.h>
-
-#include "service.h"
-#include "ev++.h"
-#include "control.h"
-#include "dinit-log.h"
-
#ifdef __linux__
+#include <sys/prctl.h>
#include <sys/klog.h>
#include <sys/reboot.h>
#endif
+#include "dasynq.h"
+#include "service.h"
+#include "control.h"
+#include "dinit-log.h"
+#include "dinit-socket.h"
+
/*
- * "simpleinit" from util-linux-ng package handles signals as follows:
- * SIGTSTP - spawn no more gettys (in preparation for shutdown etc).
- * In dinit terms this should probably mean "no more auto restarts"
- * (for any service). (Actually the signal acts as a toggle, if
- * respawn is disabled it will be re-enabled and init will
- * act as if SIGHUP had also been sent)
- * SIGTERM - kill spawned gettys (which are still alive)
- * Interestingly, simpleinit just sends a SIGTERM to the gettys,
- * which will not normall kill shells (eg bash ignores SIGTERM).
- * "/sbin/initctl -r" - rollback services (ran by "shutdown"/halt etc);
- * shouldn't return until all services have been stopped.
- * shutdown calls this after sending SIGTERM to processes running
- * with uid >= 100 ("mortals").
- * SIGQUIT - init will exec() shutdown. shutdown will detect that it is
- * running as pid 1 and will just loop and reap child processes.
- * This is used by shutdown so that init will not hang on to its
- * inode, allowing the filesystem to be re-mounted readonly
- * (this is only an issue if the init binary has been unlinked,
- * since it's then holding an inode which can't be maintained
- * when the filesystem is unmounted).
+ * When running as the system init process, Dinit processes the following signals:
*
- * Not sent by shutdown:
- * SIGHUP - re-read inittab and spawn any new getty entries
- * SIGINT - (ctrl+alt+del handler) - fork & exec "reboot"
- *
- * On the contrary dinit currently uses:
* SIGTERM - roll back services and then fork/exec /sbin/halt
* SIGINT - roll back services and then fork/exec /sbin/reboot
- * SIGQUIT - exec() /sbin/shutdown as per above.
+ * SIGQUIT - exec() /sbin/shutdown without rolling back services
*
- * It's an open question about whether dinit should roll back services *before*
+ * It's an open question about whether Dinit should roll back services *before*
* running halt/reboot, since those commands should prompt rollback of services
- * anyway. But it seems safe to do so.
+ * anyway. But it seems safe to do so, and it means the user can at least stop
+ * services even if the halt/reboot commands are unavailable for some reason.
*/
+using namespace dasynq;
+using eventloop_t = event_loop<null_mutex>;
+
+eventloop_t eventLoop;
-static void sigint_reboot_cb(struct ev_loop *loop, ev_signal *w, int revents);
-static void sigquit_cb(struct ev_loop *loop, ev_signal *w, int revents);
-static void sigterm_cb(struct ev_loop *loop, ev_signal *w, int revents);
-void open_control_socket(struct ev_loop *loop) noexcept;
-void close_control_socket(struct ev_loop *loop) noexcept;
+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;
-struct ev_io control_socket_io;
+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
-static ServiceSet *service_set;
+static dirload_service_set *services;
static bool am_system_init = false; // true if we are the system init process
static bool control_socket_open = false;
+static bool external_log_open = false;
int active_control_conns = 0;
// Control socket path. We maintain a string (control_socket_str) in case we need
static const char *control_socket_path = "/dev/dinitctl";
static std::string control_socket_str;
+static const char *log_socket_path = "/dev/log";
+
static const char *user_home_path = nullptr;
}
-int main(int argc, char **argv)
+namespace {
+ class callback_signal_handler : public eventloop_t::signal_watcher_impl<callback_signal_handler>
+ {
+ public:
+ typedef void (*cb_func_t)(eventloop_t &);
+
+ private:
+ cb_func_t cb_func;
+
+ public:
+ callback_signal_handler() : cb_func(nullptr) { }
+ callback_signal_handler(cb_func_t pcb_func) : cb_func(pcb_func) { }
+
+ void setCbFunc(cb_func_t cb_func)
+ {
+ this->cb_func = cb_func;
+ }
+
+ rearm received(eventloop_t &eloop, int signo, siginfo_p siginfo)
+ {
+ cb_func(eloop);
+ return rearm::REARM;
+ }
+ };
+
+ class control_socket_watcher : public eventloop_t::fd_watcher_impl<control_socket_watcher>
+ {
+ public:
+ rearm fd_event(eventloop_t &loop, int fd, int flags) noexcept
+ {
+ control_socket_cb(&loop, fd);
+ return rearm::REARM;
+ }
+ };
+
+ control_socket_watcher control_socket_io;
+}
+
+int dinit_main(int argc, char **argv)
{
using namespace std;
}
}
else {
+#ifdef __linux__
// LILO puts "auto" on the kernel command line for unattended boots; we'll filter it.
if (! am_system_init || strcmp(argv[i], "auto") != 0) {
services_to_start.push_back(argv[i]);
}
+#endif
}
}
}
if (onefd > 2) close(onefd);
if (twofd > 2) close(twofd);
}
-
+
/* Set up signal handlers etc */
/* SIG_CHILD is ignored by default: good */
- /* sigemptyset(&sigwait_set); */
- /* sigaddset(&sigwait_set, SIGCHLD); */
- /* sigaddset(&sigwait_set, SIGINT); */
- /* sigaddset(&sigwait_set, SIGTERM); */
- /* sigprocmask(SIG_BLOCK, &sigwait_set, NULL); */
-
+ sigset_t sigwait_set;
+ sigemptyset(&sigwait_set);
+ sigaddset(&sigwait_set, SIGCHLD);
+ sigaddset(&sigwait_set, SIGINT);
+ 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.
signal(SIGTTIN, SIG_IGN);
signal(SIGTTOU, SIG_IGN);
+ signal(SIGPIPE, SIG_IGN);
+
if (! am_system_init && ! control_socket_path_set) {
const char * userhome = get_user_home();
if (userhome != nullptr) {
}
// Set up signal handlers
- ev_signal sigint_ev_signal;
+ callback_signal_handler sigterm_watcher {sigterm_cb};
+ callback_signal_handler sigint_watcher;
+ callback_signal_handler sigquit_watcher;
+
if (am_system_init) {
- ev_signal_init(&sigint_ev_signal, sigint_reboot_cb, SIGINT);
+ sigint_watcher.setCbFunc(sigint_reboot_cb);
+ sigquit_watcher.setCbFunc(sigquit_cb);
}
else {
- ev_signal_init(&sigint_ev_signal, sigterm_cb, SIGINT);
+ sigint_watcher.setCbFunc(sigterm_cb);
}
+
+ sigint_watcher.add_watch(eventLoop, SIGINT);
+ sigterm_watcher.add_watch(eventLoop, SIGTERM);
- ev_signal sigquit_ev_signal;
if (am_system_init) {
// PID 1: SIGQUIT exec's shutdown
- ev_signal_init(&sigquit_ev_signal, sigquit_cb, SIGQUIT);
- }
- else {
- // Otherwise: SIGQUIT terminates dinit
- ev_signal_init(&sigquit_ev_signal, sigterm_cb, SIGQUIT);
+ sigquit_watcher.add_watch(eventLoop, SIGQUIT);
+ // As a user process, we instead just let SIGQUIT perform the default action.
}
-
- ev_signal sigterm_ev_signal;
- ev_signal_init(&sigterm_ev_signal, sigterm_cb, SIGTERM);
-
- /* Set up libev */
- struct ev_loop *loop = ev_default_loop(EVFLAG_AUTO /* | EVFLAG_SIGNALFD */);
- ev_signal_start(loop, &sigint_ev_signal);
- ev_signal_start(loop, &sigquit_ev_signal);
- ev_signal_start(loop, &sigterm_ev_signal);
// Try to open control socket (may fail due to readonly filesystem)
- open_control_socket(loop);
+ open_control_socket(false);
#ifdef __linux__
if (am_system_init) {
// 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);
#endif
/* start requested services */
- service_set = new ServiceSet(service_dir);
- for (list<const char *>::iterator i = services_to_start.begin();
- i != services_to_start.end();
- ++i) {
+ services = new dirload_service_set(service_dir);
+
+ init_log(services);
+
+ for (auto svc : services_to_start) {
try {
- service_set->startService(*i);
+ services->start_service(svc);
+ // Note in general if we fail to start a service we don't need any special error handling,
+ // since we either leave other services running or, if it was the only service, then no
+ // services will be running and we will process normally (reboot if system process,
+ // exit if user process).
}
- catch (ServiceNotFound &snf) {
- log(LogLevel::ERROR, snf.serviceName, ": Could not find service description.");
+ catch (service_not_found &snf) {
+ log(loglevel_t::ERROR, snf.serviceName, ": Could not find service description.");
}
- catch (ServiceLoadExc &sle) {
- log(LogLevel::ERROR, sle.serviceName, ": ", sle.excDescription);
+ catch (service_load_exc &sle) {
+ log(loglevel_t::ERROR, sle.serviceName, ": ", sle.excDescription);
}
catch (std::bad_alloc &badalloce) {
- log(LogLevel::ERROR, "Out of memory when trying to start service: ", *i, ".");
+ log(loglevel_t::ERROR, "Out of memory when trying to start service: ", svc, ".");
+ break;
}
}
event_loop:
// Process events until all services have terminated.
- while (service_set->count_active_services() != 0) {
- ev_loop(loop, EVLOOP_ONESHOT);
+ while (services->count_active_services() != 0) {
+ eventLoop.run();
}
- ShutdownType shutdown_type = service_set->getShutdownType();
+ shutdown_type_t shutdown_type = services->getShutdownType();
if (am_system_init) {
- logMsgBegin(LogLevel::INFO, "No more active services.");
+ log_msg_begin(loglevel_t::INFO, "No more active services.");
- if (shutdown_type == ShutdownType::REBOOT) {
- logMsgEnd(" Will reboot.");
+ if (shutdown_type == shutdown_type_t::REBOOT) {
+ log_msg_end(" Will reboot.");
}
- else if (shutdown_type == ShutdownType::HALT) {
- logMsgEnd(" Will halt.");
+ else if (shutdown_type == shutdown_type_t::HALT) {
+ log_msg_end(" Will halt.");
}
- else if (shutdown_type == ShutdownType::POWEROFF) {
- logMsgEnd(" Will power down.");
+ else if (shutdown_type == shutdown_type_t::POWEROFF) {
+ log_msg_end(" Will power down.");
}
else {
- logMsgEnd(" Re-initiating boot sequence.");
+ log_msg_end(" Re-initiating boot sequence.");
}
}
- close_control_socket(ev_default_loop(EVFLAG_AUTO));
+ while (! is_log_flushed()) {
+ eventLoop.run();
+ }
+
+ close_control_socket();
if (am_system_init) {
- if (shutdown_type == ShutdownType::CONTINUE) {
+ if (shutdown_type == shutdown_type_t::CONTINUE) {
// It could be that we started in single user mode, and the
// user has now exited the shell. We'll try and re-start the
// boot process...
try {
- service_set->startService("boot");
+ services->start_service("boot");
goto event_loop; // yes, the "evil" goto
}
catch (...) {
- // Now WTF do we do? try to reboot
- log(LogLevel::ERROR, "Could not start 'boot' service; rebooting.");
- shutdown_type = ShutdownType::REBOOT;
+ // Now what do we do? try to reboot, but wait for user ack to avoid boot loop.
+ log(loglevel_t::ERROR, "Could not start 'boot' service. Will attempt reboot.");
+ wait_for_user_input();
+ shutdown_type = shutdown_type_t::REBOOT;
}
}
const char * cmd_arg;
- if (shutdown_type == ShutdownType::HALT) {
+ if (shutdown_type == shutdown_type_t::HALT) {
cmd_arg = "-h";
}
- else if (shutdown_type == ShutdownType::REBOOT) {
+ else if (shutdown_type == shutdown_type_t::REBOOT) {
cmd_arg = "-r";
}
else {
// Fork and execute dinit-reboot.
execl("/sbin/shutdown", "/sbin/shutdown", "--system", cmd_arg, nullptr);
- log(LogLevel::ERROR, "Could not execute /sbin/shutdown: ", strerror(errno));
+ log(loglevel_t::ERROR, "Could not execute /sbin/shutdown: ", strerror(errno));
// PID 1 must not actually exit, although we should never reach this point:
while (true) {
- ev_loop(loop, EVLOOP_ONESHOT);
+ eventLoop.run();
}
}
+ else if (shutdown_type == shutdown_type_t::REBOOT) {
+ // Non-system-process. If we got SIGINT, let's die due to it:
+ sigset_t sigwait_set;
+ sigemptyset(&sigwait_set);
+ sigaddset(&sigwait_set, SIGINT);
+ raise(SIGINT);
+ sigprocmask(SIG_UNBLOCK, &sigwait_set, NULL);
+ }
return 0;
}
+// 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
+{
+ std::cout << "Press Enter to continue." << std::endl;
+ char buf[1];
+ read(STDIN_FILENO, buf, 1);
+}
+
// Callback for control socket
-static void control_socket_cb(struct ev_loop *loop, ev_io *w, int revents)
+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 sockfd = w->fd;
-
- int newfd = accept4(sockfd, nullptr, nullptr, SOCK_NONBLOCK | SOCK_CLOEXEC);
+ int newfd = dinit_accept4(sockfd, nullptr, nullptr, SOCK_NONBLOCK | SOCK_CLOEXEC);
if (newfd != -1) {
try {
- new ControlConn(loop, service_set, newfd); // will delete itself when it's finished
+ new control_conn_t(loop, services, newfd); // will delete itself when it's finished
}
- catch (std::bad_alloc &bad_alloc_exc) {
- log(LogLevel::ERROR, "Accepting control connection: Out of memory");
+ catch (std::exception &exc) {
+ log(loglevel_t::ERROR, "Accepting control connection: ", exc.what());
close(newfd);
}
}
}
-void open_control_socket(struct ev_loop *loop) noexcept
+void open_control_socket(bool report_ro_failure) noexcept
{
if (! control_socket_open) {
const char * saddrname = control_socket_path;
- uint sockaddr_size = offsetof(struct sockaddr_un, sun_path) + strlen(saddrname) + 1;
+ size_t saddrname_len = strlen(saddrname);
+ uint sockaddr_size = offsetof(struct sockaddr_un, sun_path) + saddrname_len + 1;
struct sockaddr_un * name = static_cast<sockaddr_un *>(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;
}
}
name->sun_family = AF_UNIX;
- strcpy(name->sun_path, saddrname);
+ memcpy(name->sun_path, saddrname, saddrname_len + 1);
- // OpenBSD and Linux both allow combining NONBLOCK/CLOEXEC flags with socket type, however
- // it's not actually POSIX. (TODO).
- int sockfd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
+ 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) {
- log(LogLevel::ERROR, "Error binding control socket: ", strerror(errno));
+ if (errno != EROFS || report_ro_failure) {
+ log(loglevel_t::ERROR, "Error binding control socket: ", strerror(errno));
+ }
close(sockfd);
free(name);
return;
// 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;
}
- control_socket_open = true;
- ev_io_init(&control_socket_io, control_socket_cb, sockfd, EV_READ);
- ev_io_start(loop, &control_socket_io);
+ try {
+ control_socket_io.add_watch(eventLoop, sockfd, IN_EVENTS);
+ control_socket_open = true;
+ }
+ catch (std::exception &e)
+ {
+ log(loglevel_t::ERROR, "Could not setup I/O on control socket: ", e.what());
+ close(sockfd);
+ }
}
}
-void close_control_socket(struct ev_loop *loop) noexcept
+static void close_control_socket() noexcept
{
if (control_socket_open) {
- int fd = control_socket_io.fd;
- ev_io_stop(loop, &control_socket_io);
+ int fd = control_socket_io.get_watched_fd();
+ control_socket_io.deregister(eventLoop);
close(fd);
// Unlink the socket:
}
}
-/* handle SIGINT signal (generated by kernel when ctrl+alt+del pressed) */
-static void sigint_reboot_cb(struct ev_loop *loop, ev_signal *w, int revents)
+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<sockaddr_un *>(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);
+ }
+ catch (std::exception &e) {
+ log(loglevel_t::ERROR, "Setting up log failed: ", e.what());
+ close(sockfd);
+ }
+ }
+ else {
+ // Note if connect fails, we haven't warned at all, because the syslog server might not
+ // have started yet.
+ close(sockfd);
+ }
+
+ free(name);
+ }
+}
+
+/* handle SIGINT signal (generated by Linux kernel when ctrl+alt+del pressed) */
+static void sigint_reboot_cb(eventloop_t &eloop) noexcept
{
- enable_console_log(true);
- service_set->stop_all_services(ShutdownType::REBOOT);
+ services->stop_all_services(shutdown_type_t::REBOOT);
}
/* handle SIGQUIT (if we are system init) */
-static void sigquit_cb(struct ev_loop *loop, ev_signal *w, int revents)
+static void sigquit_cb(eventloop_t &eloop) noexcept
{
- // This allows remounting the filesystem read-only if the dinit binary has been
- // unlinked. In that case the kernel holds the binary open, so that it can't be
- // properly removed.
- close_control_socket(ev_default_loop(EVFLAG_AUTO));
- execl("/sbin/shutdown", "/sbin/shutdown", (char *) 0);
- log(LogLevel::ERROR, "Error executing /sbin/shutdown: ", strerror(errno));
+ // This performs an immediate shutdown, without service rollback.
+ close_control_socket();
+ execl("/sbin/shutdown", "/sbin/shutdown", "--system", (char *) 0);
+ log(loglevel_t::ERROR, "Error executing /sbin/shutdown: ", strerror(errno));
+ sync(); // since a hard poweroff might be required at this point...
}
-/* handle SIGTERM/SIGQUIT - stop all services (not used for system daemon) */
-static void sigterm_cb(struct ev_loop *loop, ev_signal *w, int revents)
+/* handle SIGTERM/SIGQUIT(non-system-daemon) - stop all services and shut down */
+static void sigterm_cb(eventloop_t &eloop) noexcept
{
- enable_console_log(true);
- service_set->stop_all_services();
+ services->stop_all_services();
}