Update NEWS and version.
[oweals/dinit.git] / src / dinit.cc
index 25d0c2414131e5e742be3f1642d8a4387d0b49d9..1bff5dc6b7c14edf3d8686da0ad4e740cecddefa 100644 (file)
@@ -13,6 +13,7 @@
 #include <unistd.h>
 #include <fcntl.h>
 #include <pwd.h>
+#include <termios.h>
 #ifdef __linux__
 #include <sys/prctl.h>
 #include <sys/klog.h>
@@ -29,6 +30,7 @@
 #include "dinit-log.h"
 #include "dinit-socket.h"
 #include "static-string.h"
+#include "dinit-utmp.h"
 
 #include "mconfig.h"
 
@@ -54,8 +56,9 @@ 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 open_control_socket(bool report_ro_failure = true) noexcept;
 static void close_control_socket() noexcept;
-static void wait_for_user_input() noexcept;
+static void confirm_restart_boot() noexcept;
 static void read_env_file(const char *);
 
 static void control_socket_cb(eventloop_t *loop, int fd);
@@ -68,6 +71,7 @@ static dirload_service_set *services;
 static bool am_pid_one = false;     // true if we are PID 1
 static bool am_system_init = false; // true if we are the system init process
 
+static bool did_log_boot = false;
 static bool control_socket_open = false;
 static bool external_log_open = false;
 int active_control_conns = 0;
@@ -84,10 +88,12 @@ static bool log_is_syslog = true; // if false, log is a file
 
 static const char *user_home_path = nullptr;
 
+// Set to true (when console_input_watcher is active) if console input becomes available
+static bool console_input_ready = false;
 
 // Get user home (and set user_home_path). (The return may become invalid after
 // changing the evironment (HOME variable) or using the getpwuid() function).
-const char * get_user_home()
+static const char * get_user_home()
 {
     if (user_home_path == nullptr) {
         user_home_path = getenv("HOME");
@@ -103,6 +109,7 @@ const char * get_user_home()
 
 
 namespace {
+    // Event-loop handler for a signal, which just delegates to a function (pointer).
     class callback_signal_handler : public eventloop_t::signal_watcher_impl<callback_signal_handler>
     {
         using rearm = dasynq::rearm;
@@ -129,6 +136,7 @@ namespace {
         }
     };
 
+    // Event-loop handler for when a connection is made to the control socket.
     class control_socket_watcher : public eventloop_t::fd_watcher_impl<control_socket_watcher>
     {
         using rearm = dasynq::rearm;
@@ -141,6 +149,18 @@ namespace {
         }
     };
 
+    class console_input_watcher : public eventloop_t::fd_watcher_impl<console_input_watcher>
+    {
+        using rearm = dasynq::rearm;
+
+        public:
+        rearm fd_event(eventloop_t &loop, int fd, int flags) noexcept
+        {
+            console_input_ready = true;
+            return rearm::REARM;
+        }
+    };
+
     // 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<log_flush_timer_t>
     {
@@ -162,6 +182,7 @@ namespace {
     };
 
     control_socket_watcher control_socket_io;
+    console_input_watcher console_input_io;
     log_flush_timer_t log_flush_timer;
 }
 
@@ -186,83 +207,87 @@ int dinit_main(int argc, char **argv)
     // 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], "--env-file") == 0 || strcmp(argv[i], "-e") == 0) {
-                if (++i < argc) {
-                    env_file_set = true;
-                    env_file = argv[i];
+        for (int i = 1; i < argc; i++) {
+            if (argv[i][0] == '-') {
+                // An option...
+                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 {
-                    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];
+                    }
+                    else {
+                        cerr << "dinit: '--services-dir' (-d) requires an argument" << endl;
+                        return 1;
+                    }
                 }
-            }
-            else if (strcmp(argv[i], "--services-dir") == 0 || strcmp(argv[i], "-d") == 0) {
-                if (++i < argc) {
-                    service_dir = argv[i];
+                else if (strcmp(argv[i], "--system") == 0 || strcmp(argv[i], "-s") == 0) {
+                    am_system_init = true;
                 }
-                else {
-                    cerr << "dinit: '--services-dir' (-d) requires an argument" << endl;
-                    return 1;
+                else if (strcmp(argv[i], "--user") == 0 || strcmp(argv[i], "-u") == 0) {
+                    am_system_init = false;
                 }
-            }
-            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) {
-                if (++i < argc) {
-                    control_socket_path = argv[i];
-                    control_socket_path_set = true;
+                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;
+                    }
+                    else {
+                        cerr << "dinit: '--socket-path' (-p) requires an argument" << endl;
+                        return 1;
+                    }
                 }
-                else {
-                    cerr << "dinit: '--socket-path' (-p) requires an argument" << endl;
-                    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], "--log-file") == 0 || strcmp(argv[i], "-l") == 0) {
-                if (++i < argc) {
-                    log_path = argv[i];
-                    log_is_syslog = false;
+                else if (strcmp(argv[i], "--help") == 0) {
+                    cout << "dinit, an init with dependency management\n"
+                            " --help                       display help\n"
+                            " --env-file <file>, -e <file>\n"
+                            "                              environment variable initialisation file\n"
+                            " --services-dir <dir>, -d <dir>\n"
+                            "                              set base directory for service description\n"
+                            "                              files (-d <dir>)\n"
+                            " --system, -s                 run as the system service manager\n"
+                            " --user, -u                   run as a user service manager\n"
+                            " --socket-path <path>, -p <path>\n"
+                            "                              path to control socket\n"
+                            " <service-name>               start service with name <service-name>\n";
+                    return 0;
                 }
                 else {
-                    cerr << "dinit: '--log-file' (-l) requires an argument" << endl;
-                    return 1;
+                    // unrecognized
+                    if (! am_system_init) {
+                        cerr << "dinit: Unrecognized option: " << argv[i] << endl;
+                        return 1;
+                    }
                 }
             }
-            else if (strcmp(argv[i], "--help") == 0) {
-                cout << "dinit, an init with dependency management\n"
-                        " --help                       display help\n"
-                        " --env-file <file>, -e <file>\n"
-                        "                              environment variable initialisation file\n"
-                        " --services-dir <dir>, -d <dir>\n"
-                        "                              set base directory for service description\n"
-                        "                              files (-d <dir>)\n"
-                        " --system, -s                 run as the system init process\n"
-                        " --socket-path <path>, -p <path>\n"
-                        "                              path to control socket\n"
-                        " <service-name>               start service with name <service-name>\n";
-                return 0;
-            }
             else {
-                // unrecognized
-                if (! am_system_init) {
-                    cerr << "dinit: Unrecognized option: " << argv[i] << endl;
-                    return 1;
-                }
-            }
-        }
-        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]);
-            }
+                // LILO puts "auto" on the kernel command line for unattended boots; we'll filter it.
+                if (! am_pid_one || strcmp(argv[i], "auto") != 0) {
+                    services_to_start.push_back(argv[i]);
+                }
 #else
-            services_to_start.push_back(argv[i]);
+                services_to_start.push_back(argv[i]);
 #endif
+            }
         }
-      }
     }
     
     if (am_system_init) {
@@ -346,6 +371,7 @@ int dinit_main(int argc, char **argv)
 
     sigint_watcher.add_watch(event_loop, SIGINT);
     sigterm_watcher.add_watch(event_loop, SIGTERM);
+    console_input_io.add_watch(event_loop, STDIN_FILENO, dasynq::IN_EVENTS, false);
     
     if (am_pid_one) {
         // PID 1: SIGQUIT exec's shutdown
@@ -411,10 +437,10 @@ int dinit_main(int argc, char **argv)
             // exit if user process).
         }
         catch (service_not_found &snf) {
-            log(loglevel_t::ERROR, snf.serviceName, ": Could not find service description.");
+            log(loglevel_t::ERROR, snf.service_name, ": Could not find service description.");
         }
         catch (service_load_exc &sle) {
-            log(loglevel_t::ERROR, sle.serviceName, ": ", sle.excDescription);
+            log(loglevel_t::ERROR, sle.service_name, ": ", sle.exc_description);
         }
         catch (std::bad_alloc &badalloce) {
             log(loglevel_t::ERROR, "Out of memory when trying to start service: ", svc, ".");
@@ -422,7 +448,7 @@ int dinit_main(int argc, char **argv)
         }
     }
     
-    event_loop:
+    run_event_loop:
     
     // Process events until all services have terminated.
     while (services->count_active_services() != 0) {
@@ -430,6 +456,9 @@ int dinit_main(int argc, char **argv)
     }
 
     shutdown_type_t shutdown_type = services->get_shutdown_type();
+    if (shutdown_type == shutdown_type_t::REMAIN) {
+        goto run_event_loop;
+    }
     
     if (am_pid_one) {
         log_msg_begin(loglevel_t::INFO, "No more active services.");
@@ -443,9 +472,6 @@ int dinit_main(int argc, char **argv)
         else if (shutdown_type == shutdown_type_t::POWEROFF) {
             log_msg_end(" Will power down.");
         }
-        else {
-            log_msg_end(" Re-initiating boot sequence.");
-        }
     }
     
     log_flush_timer.arm_timer_rel(event_loop, timespec{5,0}); // 5 seconds
@@ -456,19 +482,23 @@ int dinit_main(int argc, char **argv)
     close_control_socket();
     
     if (am_pid_one) {
-        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 {
-                services->start_service("boot");
-                goto event_loop; // yes, the "evil" goto
-            }
-            catch (...) {
-                // 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;
+        if (shutdown_type == shutdown_type_t::NONE) {
+            // Services all stopped but there was no shutdown issued. Inform user, wait for ack, and
+            // re-start boot sequence.
+            std::cout << "No shutdown was requested; boot failure? Will re-run boot sequence." << std::endl;
+            sync(); // Sync to minimise data loss if user elects to power off / hard reset
+            confirm_restart_boot();
+            shutdown_type = services->get_shutdown_type();
+            if (shutdown_type == shutdown_type_t::NONE) {
+                try {
+                    services->start_service("boot");
+                    goto run_event_loop; // yes, the "evil" goto
+                }
+                catch (...) {
+                    // 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.");
+                    shutdown_type = shutdown_type_t::REBOOT;
+                }
             }
         }
         
@@ -497,16 +527,17 @@ int dinit_main(int argc, char **argv)
     }
     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);
+        sigset_t sigwait_set_int;
+        sigemptyset(&sigwait_set_int);
+        sigaddset(&sigwait_set_int, SIGINT);
         raise(SIGINT);
-        sigprocmask(SIG_UNBLOCK, &sigwait_set, NULL);
+        sigprocmask(SIG_UNBLOCK, &sigwait_set_int, NULL);
     }
     
     return 0;
 }
 
+// Log a parse error when reading the environment file.
 static void log_bad_env(int linenum)
 {
     log(loglevel_t::ERROR, "invalid environment variable setting in environment file (line ", linenum, ")");
@@ -566,13 +597,31 @@ static void read_env_file(const char *env_file_path)
     }
 }
 
-// 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
+// Get user confirmation before proceeding with restarting boot sequence.
+static void confirm_restart_boot() noexcept
 {
+    // Bypass log; we want to make certain the message is seen:
     std::cout << "Press Enter to continue." << std::endl;
-    char buf[1];
-    read(STDIN_FILENO, buf, 1);
+
+    // Drain input, set non-blocking mode
+    tcflush(STDIN_FILENO, TCIFLUSH);
+    int origFlags = fcntl(STDIN_FILENO, F_GETFL);
+
+    console_input_io.set_enabled(event_loop, true);
+    do {
+        event_loop.run();
+    } while (! console_input_ready && services->get_shutdown_type() == shutdown_type_t::NONE);
+    console_input_io.set_enabled(event_loop, false);
+
+    // We either have input, or shutdown type has been set, or both.
+    if (console_input_ready) {
+        char buf[1];
+        read(STDIN_FILENO, buf, 1);  // read a single character, to make sure we wait for input
+        tcflush(STDIN_FILENO, TCIFLUSH); // discard the rest of input
+        console_input_ready = false;
+    }
+
+    fcntl(STDIN_FILENO, F_SETFL, origFlags);
 }
 
 // Callback for control socket
@@ -597,8 +646,31 @@ static void control_socket_cb(eventloop_t *loop, int sockfd)
     }
 }
 
-void open_control_socket(bool report_ro_failure) noexcept
+// Callback when the root filesystem is read/write:
+void rootfs_is_rw() noexcept
+{
+    open_control_socket(true);
+    if (! did_log_boot) {
+        did_log_boot = log_boot();
+    }
+}
+
+// Open/create the control socket, normally /dev/dinitctl, used to allow client programs to connect
+// and issue service orders and shutdown commands etc. This can safely be called multiple times;
+// once the socket has been successfully opened, further calls have no effect.
+static void open_control_socket(bool report_ro_failure) noexcept
 {
+    if (control_socket_open) {
+        struct stat stat_buf;
+        if (stat(control_socket_path, &stat_buf) != 0 && errno == ENOENT) {
+            // Looks like our control socket has disappeared from the filesystem. Close our control
+            // socket and re-create it:
+            control_socket_io.deregister(event_loop);
+            close(control_socket_io.get_watched_fd());
+            control_socket_open = false; // now re-open below
+        }
+    }
+
     if (! control_socket_open) {
         const char * saddrname = control_socket_path;
         size_t saddrname_len = strlen(saddrname);
@@ -672,6 +744,8 @@ static void close_control_socket() noexcept
         
         // Unlink the socket:
         unlink(control_socket_path);
+
+        control_socket_open = false;
     }
 }
 
@@ -734,7 +808,8 @@ void setup_external_log() noexcept
                 }
             }
             else {
-                // log failure to log? It makes more sense than first appears, because we also log to console:
+                // 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));
             }
         }