#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)
{
}
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;
bool doing_recovery : 1; // if we are currently recovering a BGPROCESS (restarting process, while
// holding STARTED service state)
bool start_explicit : 1; // whether we are are explictly required to be started
- int started_deps = 0; // number of dependents that require this service to be started
+ int required_by = 0; // number of dependents wanting this service to be started
typedef std::list<ServiceRecord *> sr_list;
typedef sr_list::iterator sr_iter;
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:
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();
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;
-
+
// A dependency has reached STARTED state
void dependencyStarted() noexcept;
return waiting_for_deps && ! force_stop;
}
+ // Notify dependencies that we no longer need them,
+ // (if this is actually the case).
+ void notify_dependencies_stopped() noexcept;
+
// A dependent has reached STOPPED state
void dependentStopped() noexcept;
// issue a stop to all dependents, return true if they are all already stopped
bool stopDependents() noexcept;
- void forceStop() noexcept; // force-stop this service and all dependents
+ void require() noexcept;
+ void release() noexcept;
+ void release_dependencies() noexcept;
+
+ // Check if service is, fundamentally, stopped.
+ bool is_stopped() noexcept
+ {
+ return service_state == ServiceState::STOPPED
+ || (service_state == ServiceState::STARTING && waiting_for_deps);
+ }
void notifyListeners(ServiceEvent event) noexcept
{
// 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;
- bool get_start_flag(bool transitive)
- {
- return transitive ? started_deps != 0 : start_explicit;
- }
+ bool do_auto_restart() noexcept;
public:
: 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;
}
// 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)
const char *getServiceName() const noexcept { return service_name.c_str(); }
ServiceState getState() const noexcept { return service_state; }
- void start(bool transitive = false) noexcept; // start the service
- void stop() noexcept; // stop the service
- void do_start() noexcept;
- void do_stop() noexcept;
+ void start(bool activate = true) noexcept; // start the service
+ void stop(bool bring_down = true) noexcept; // stop the service
+
+ void forceStop() noexcept; // force-stop this service and all dependents
+
+ // Pin the service in "started" state (when it reaches the state)
+ void pinStart() noexcept
+ {
+ pinned_started = true;
+ }
+
+ // Pin the service in "stopped" state (when it reaches the state)
+ void pinStop() noexcept
+ {
+ pinned_stopped = true;
+ }
- void pinStart() noexcept; // start the service and pin it
- void pinStop() noexcept; // stop the service and pin it
- void unpin() noexcept; // unpin the service
+ // Remove both "started" and "stopped" pins. If the service is currently pinned
+ // in either state but would naturally be in the opposite state, it will immediately
+ // commence starting/stopping.
+ void unpin() noexcept;
bool isDummy() noexcept
{
}
};
-
+/*
+ * 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;
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
// 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;
restart_enabled = false;
shutdown_type = type;
for (std::list<ServiceRecord *>::iterator i = records.begin(); i != records.end(); ++i) {
- (*i)->stop();
+ (*i)->stop(false);
(*i)->unpin();
}
+ processQueues(false);
}
void set_auto_restart(bool restart) noexcept