Correct exit status checks (status 0 returned by wait() doesn't have to mean
[oweals/dinit.git] / src / service.h
index 0140168a7e3da9e3c6395198f2f9e9707dcb976b..eed59e621cb3094e34b33df119fa7c607bc1493f 100644 (file)
@@ -6,14 +6,20 @@
 #include <vector>
 #include <csignal>
 #include <unordered_set>
-#include "ev.h"
+
+#include "dasynq.h"
+
 #include "control.h"
 #include "service-listener.h"
 #include "service-constants.h"
 
 /*
- * Possible service states
+ * This header defines ServiceRecord, a data record maintaining information about a service,
+ * and ServiceSet, a set of interdependent service records. It also defines some associated
+ * types and exceptions.
  *
+ * Service states
+ * --------------
  * Services have both a current state and a desired state. The desired state can be
  * either STARTED or STOPPED. The current state can also be STARTING or STOPPING.
  * A service can be "pinned" in either the STARTED or STOPPED states to prevent it
  * A process service is in the STOPPING state when it has been signalled to stop, and is
  * in the STARTING state when waiting for dependencies to start or for the exec() call in
  * the forked child to complete and return a status.
+ *
+ * Aquisition/release:
+ * ------------------
+ * Each service has a dependent-count ("required_by"). This starts at 0, adds 1 if the
+ * service has explicitly been started (i.e. "start_explicit" is true), and adds 1 for
+ * each dependent service which is not STOPPED (including depdendents with a soft dependency).
+ * When required_by transitions to 0, the service is stopped (unless it is pinned). When
+ * require_by transitions from 0, the service is started (unless pinned).
+ *
+ * So, in general, the dependent-count determines the desired state (STARTED if the count
+ * is greater than 0, otherwise STOPPED). However, a service can be issued a stop-and-take
+ * down order (via `stop(true)'); this will first stop dependent services, which may restart
+ * and cancel the stop of the former service. Finally, a service can be force-stopped, which
+ * means that its stop process cannot be cancelled (though it may still be put in a desired
+ * state of STARTED, meaning it will start immediately upon stopping).
+ *
+ * Pinning
+ * -------
+ * A service may be "pinned" in either STARTED or STOPPED states (or even both). Once it
+ * reaches a pinned state, a service will not leave that state, though its desired state
+ * may still be set. (Note that pinning prevents, but never causes, state transition).
+ *
+ * The priority of the different state deciders is:
+ *  - pins
+ *  - force stop flag
+ *  - desired state (which is manipulated by require/release operations)
+ *
+ * So a forced stop cannot occur until the service is not pinned started, for instance.
  */
 
 struct OnstartFlags {
     bool rw_ready : 1;
+    bool log_ready : 1;
     
     // Not actually "onstart" commands:
     bool no_sigterm : 1;  // do not send SIGTERM
     bool runs_on_console : 1;  // run "in the foreground"
     
-    OnstartFlags() noexcept : rw_ready(false),
+    OnstartFlags() noexcept : rw_ready(false), log_ready(false),
             no_sigterm(false), runs_on_console(false)
     {
     }
@@ -146,9 +181,38 @@ static std::vector<const char *> separate_args(std::string &s, std::list<std::pa
     return r;
 }
 
+class ServiceChildWatcher : public EventLoop_t::ChildProcWatcher
+{
+    public:
+    // TODO resolve clunkiness of storing this field
+    ServiceRecord * service;
+    Rearm childStatus(EventLoop_t &eloop, pid_t child, int status) noexcept;
+    
+    ServiceChildWatcher(ServiceRecord * sr) noexcept : service(sr) { }
+};
+
+class ServiceIoWatcher : public EventLoop_t::FdWatcher
+{
+    public:
+    // TODO resolve clunkiness of storing these fields
+    int fd;
+    ServiceRecord * service;
+    Rearm fdEvent(EventLoop_t &eloop, int fd, int flags) noexcept;
+    
+    ServiceIoWatcher(ServiceRecord * sr) noexcept : service(sr) { }
+    
+    void addWatch(EventLoop_t &loop, int fd, int flags)
+    {
+        this->fd = fd;
+        EventLoop_t::FdWatcher::addWatch(loop, fd, flags);
+    }
+};
 
 class ServiceRecord
 {
+    friend class ServiceChildWatcher;
+    friend class ServiceIoWatcher;
+    
     typedef std::string string;
     
     string service_name;
@@ -199,9 +263,6 @@ class ServiceRecord
     
     ServiceSet *service_set; // the set this service belongs to
     
-    // Next service (after this one) in the queue for the console:
-    ServiceRecord *next_for_console;
-
     std::unordered_set<ServiceListener *> listeners;
     
     // Process services:
@@ -224,9 +285,22 @@ class ServiceRecord
     int exit_status; // Exit status, if the process has exited (pid == -1).
     int socket_fd = -1;  // For socket-activation services, this is the file
                          // descriptor for the socket.
-
-    ev_child child_listener;
-    ev_io child_status_listener;
+    
+    ServiceChildWatcher child_listener;
+    ServiceIoWatcher child_status_listener;
+    
+    // Data for use by ServiceSet
+    public:
+    
+    // Next service (after this one) in the queue for the console. Intended to only be used by ServiceSet class.
+    ServiceRecord *next_for_console;
+    
+    // Start/stop queues
+    ServiceRecord *next_in_start_queue = nullptr;
+    ServiceRecord *next_in_stop_queue = nullptr;
+    
+    
+    private:
     
     // All dependents have stopped.
     void allDepsStopped();
@@ -247,17 +321,14 @@ class ServiceRecord
     bool start_ps_process(const std::vector<const char *> &args, bool on_console) noexcept;
     
     // Callback from libev when a child process dies
-    static void process_child_callback(struct ev_loop *loop, struct ev_child *w,
+    static void process_child_callback(EventLoop_t *loop, ServiceChildWatcher *w,
             int revents) noexcept;
     
-    static void process_child_status(struct ev_loop *loop, ev_io * stat_io,
+    static void process_child_status(EventLoop_t *loop, ServiceIoWatcher * stat_io,
             int revents) noexcept;
     
     void handle_exit_status() noexcept;
-    
-    void do_start() noexcept;
-    void do_stop() noexcept;
-    
+
     // A dependency has reached STARTED state
     void dependencyStarted() noexcept;
     
@@ -319,9 +390,6 @@ class ServiceRecord
     // Queue to run on the console. 'acquiredConsole()' will be called when the console is available.
     void queueForConsole() noexcept;
     
-    // Console is available.
-    void acquiredConsole() noexcept;
-    
     // Release console (console must be currently held by this service)
     void releaseConsole() noexcept;
     
@@ -333,7 +401,7 @@ class ServiceRecord
         : service_state(ServiceState::STOPPED), desired_state(ServiceState::STOPPED), auto_restart(false),
             pinned_stopped(false), pinned_started(false), waiting_for_deps(false),
             waiting_for_execstat(false), doing_recovery(false),
-            start_explicit(false), force_stop(false)
+            start_explicit(false), force_stop(false), child_listener(this), child_status_listener(this)
     {
         service_set = set;
         service_name = name;
@@ -366,6 +434,15 @@ class ServiceRecord
     }
     
     // TODO write a destructor
+
+    // Called on transition of desired state from stopped to started (or unpinned stop)
+    void do_start() noexcept;
+
+    // Called on transition of desired state from started to stopped (or unpinned start)
+    void do_stop() noexcept;
+    
+    // Console is available.
+    void acquiredConsole() noexcept;
     
     // Set the stop command and arguments (may throw std::bad_alloc)
     void setStopCommand(std::string command, std::list<std::pair<unsigned,unsigned>> &stop_command_offsets)
@@ -471,7 +548,25 @@ class ServiceRecord
     }
 };
 
-
+/*
+ * A ServiceSet, as the name suggests, manages a set of services.
+ *
+ * Other than the ability to find services by name, the service set manages various queues.
+ * One is the queue for processes wishing to acquire the console. There is also a set of
+ * processes that want to start, and another set of those that want to stop. These latter
+ * two "queues" (not really queues since their order is not important) are used to prevent too
+ * much recursion and to prevent service states from "bouncing" too rapidly.
+ * 
+ * A service that wishes to stop puts itself on the stop queue; a service that wishes to start
+ * puts itself on the start queue. Any operation that potentially manipulates the queues must
+ * be folloed by a "process queues" order (processQueues method, which can be instructed to
+ * process either the start queue or the stop queue first).
+ *
+ * Note that which queue it does process first, processQueues always repeatedly processes both
+ * queues until they are empty. The process is finite because starting a service can never
+ * cause services to be added to the stop queue, unless they fail to start, which should cause
+ * them to stop semi-permanently.
+ */
 class ServiceSet
 {
     int active_services;
@@ -481,7 +576,12 @@ class ServiceSet
     
     ShutdownType shutdown_type = ShutdownType::CONTINUE;  // Shutdown type, if stopping
     
+    ServiceRecord * console_queue_head = nullptr; // first record in console queue
     ServiceRecord * console_queue_tail = nullptr; // last record in console queue
+
+    // start/stop "queue" - list of services waiting to stop/start
+    ServiceRecord * first_start_queue = nullptr;
+    ServiceRecord * first_stop_queue = nullptr;
     
     // Private methods
         
@@ -530,14 +630,84 @@ class ServiceSet
     // transition to the 'stopped' state.
     void stopService(const std::string &name) noexcept;
     
+    // Add a service record to the start queue
+    void addToStartQueue(ServiceRecord *service) noexcept
+    {
+        if (service->next_in_start_queue == nullptr && first_start_queue != service) {
+            service->next_in_start_queue = first_start_queue;
+            first_start_queue = service;
+        }
+    }
+    
+    // Add a service to the stop queue
+    void addToStopQueue(ServiceRecord *service) noexcept
+    {
+        if (service->next_in_stop_queue == nullptr && first_stop_queue != service) {
+            service->next_in_stop_queue = first_stop_queue;
+            first_stop_queue = service;
+        }
+    }
+    
+    void processQueues(bool do_start_first) noexcept
+    {
+        if (! do_start_first) {
+            while (first_stop_queue != nullptr) {
+                auto next = first_stop_queue;
+                first_stop_queue = next->next_in_stop_queue;
+                next->next_in_stop_queue = nullptr;
+                next->do_stop();
+            }
+        }
+        
+        while (first_stop_queue != nullptr || first_start_queue != nullptr) {
+            while (first_start_queue != nullptr) {
+                auto next = first_start_queue;
+                first_start_queue = next->next_in_start_queue;
+                next->next_in_start_queue = nullptr;
+                next->do_start();
+            }
+            while (first_stop_queue != nullptr) {
+                auto next = first_stop_queue;
+                first_stop_queue = next->next_in_stop_queue;
+                next->next_in_stop_queue = nullptr;
+                next->do_stop();
+            }
+        }
+    }
+    
     // Set the console queue tail (returns previous tail)
     ServiceRecord * consoleQueueTail(ServiceRecord * newTail) noexcept
     {
         auto prev_tail = console_queue_tail;
         console_queue_tail = newTail;
+        newTail->next_for_console = nullptr;
+        if (! prev_tail) {
+            console_queue_head = newTail;
+            enable_console_log(false);
+        }
+        else {
+            prev_tail->next_for_console = newTail;
+        }
         return prev_tail;
     }
     
+    // Retrieve the current console queue head and remove it from the queue
+    ServiceRecord * pullConsoleQueue() noexcept
+    {
+        auto prev_head = console_queue_head;
+        if (prev_head) {
+            prev_head->acquiredConsole();
+            console_queue_head = prev_head->next_for_console;
+            if (! console_queue_head) {
+                console_queue_tail = nullptr;
+            }
+        }
+        else {
+            enable_console_log(true);
+        }
+        return prev_head;
+    }
+    
     // Notification from service that it is active (state != STOPPED)
     // Only to be called on the transition from inactive to active.
     void service_active(ServiceRecord *) noexcept;
@@ -561,6 +731,7 @@ class ServiceSet
             (*i)->stop(false);
             (*i)->unpin();
         }
+        processQueues(false);
     }
     
     void set_auto_restart(bool restart) noexcept