From: Davin McCall Date: Tue, 5 Jan 2016 23:55:05 +0000 (+0000) Subject: Move source files int src directory X-Git-Tag: v0.01~53 X-Git-Url: https://git.librecmc.org/?a=commitdiff_plain;h=20a52bc5344dd7b2fe6e3d40a050746fc49290c4;p=oweals%2Fdinit.git Move source files int src directory --- diff --git a/Makefile b/Makefile deleted file mode 100644 index fe89883..0000000 --- a/Makefile +++ /dev/null @@ -1,32 +0,0 @@ --include mconfig - -objects = dinit.o load_service.o service.o control.o dinit-log.o dinit-start.o shutdown.o dinit-reboot.o - -dinit_objects = dinit.o load_service.o service.o control.o dinit-log.o - -all: dinit dinit-start - -shutdown-utils: shutdown - -dinit: $(dinit_objects) - $(CXX) -o dinit $(dinit_objects) -lev $(EXTRA_LIBS) - -dinit-start: dinit-start.o - $(CXX) -o dinit-start dinit-start.o $(EXTRA_LIBS) - -shutdown: shutdown.o - $(CXX) -o shutdown shutdown.o - -dinit-reboot: dinit-reboot.o - $(CXX) -o dinit-reboot dinit-reboot.o - -$(objects): %.o: %.cc service.h dinit-log.h control.h control-cmds.h - $(CXX) $(CXXOPTS) -c $< -o $@ - -#install: all - -#install.man: - -clean: - rm *.o - rm dinit diff --git a/control-cmds.h b/control-cmds.h deleted file mode 100644 index 263f61a..0000000 --- a/control-cmds.h +++ /dev/null @@ -1,61 +0,0 @@ -// Dinit control command packet types - -// Requests: - -// Query protocol version: -constexpr static int DINIT_CP_QUERYVERSION = 0; - -// Find (but don't load) a service: -constexpr static int DINIT_CP_FINDSERVICE = 1; - -// Find or load a service: -constexpr static int DINIT_CP_LOADSERVICE = 2; - -// Start or stop a service: -constexpr static int DINIT_CP_STARTSERVICE = 3; -constexpr static int DINIT_CP_STOPSERVICE = 4; - -// Shutdown: -constexpr static int DINIT_CP_SHUTDOWN = 5; - // followed by 1-byte shutdown type - - - -// Replies: - -// Reply: ACK/NAK to request -constexpr static int DINIT_RP_ACK = 50; -constexpr static int DINIT_RP_NAK = 51; - -// Request was bad (connection will be closed) -constexpr static int DINIT_RP_BADREQ = 52; - -// Connection being closed due to out-of-memory condition -constexpr static int DINIT_RP_OOM = 53; - -// Start service replies: -constexpr static int DINIT_RP_SERVICELOADERR = 54; -constexpr static int DINIT_RP_SERVICEOOM = 55; // couldn't start due to out-of-memory - -constexpr static int DINIT_RP_SSISSUED = 56; // service start/stop was issued (includes 4-byte service handle) -constexpr static int DINIT_RP_SSREDUNDANT = 57; // service was already started/stopped (or for stop, not loaded) - -// Query version response: -constexpr static int DINIT_RP_CPVERSION = 58; - -// Service record loaded/found -constexpr static int DINIT_RP_SERVICERECORD = 59; -// followed by 4-byte service handle, 1-byte service state - -// Couldn't find/load service -constexpr static int DINIT_RP_NOSERVICE = 60; - - - -// Information: - -// Service event occurred (4-byte service handle, 1 byte event code) -constexpr static int DINIT_IP_SERVICEEVENT = 100; - -// rollback completed -constexpr static int DINIT_ROLLBACK_COMPLETED = 101; diff --git a/control.cc b/control.cc deleted file mode 100644 index 22f4378..0000000 --- a/control.cc +++ /dev/null @@ -1,419 +0,0 @@ -#include "control.h" -#include "service.h" - -void ControlConn::processPacket() -{ - using std::string; - - // Note that where we call queuePacket, we must generally check the return value. If it - // returns false it has either deleted the connection or marked it for deletion; we - // shouldn't touch instance members after that point. - - int pktType = rbuf[0]; - if (pktType == DINIT_CP_QUERYVERSION) { - // Responds with: - // DINIT_RP_CVERSION, (2 byte) minimum compatible version, (2 byte) maximum compatible version - char replyBuf[] = { DINIT_RP_CPVERSION, 0, 0, 0, 0 }; - if (! queuePacket(replyBuf, 1)) return; - rbuf.consume(1); - return; - } - if (pktType == DINIT_CP_FINDSERVICE || pktType == DINIT_CP_LOADSERVICE) { - processFindLoad(pktType); - return; - } - if (pktType == DINIT_CP_STARTSERVICE || pktType == DINIT_CP_STOPSERVICE) { - processStartStop(pktType); - return; - } - else if (pktType == DINIT_CP_SHUTDOWN) { - // Shutdown/reboot - if (rbuf.get_length() < 2) { - chklen = 2; - return; - } - - auto sd_type = static_cast(rbuf[1]); - - service_set->stop_all_services(sd_type); - log_to_console = true; - char ackBuf[] = { DINIT_RP_ACK }; - if (! queuePacket(ackBuf, 1)) return; - - // Clear the packet from the buffer - rbuf.consume(2); - chklen = 0; - return; - } - else { - // Unrecognized: give error response - char outbuf[] = { DINIT_RP_BADREQ }; - if (! queuePacket(outbuf, 1)) return; - bad_conn_close = true; - ev_io_set(&iob, iob.fd, EV_WRITE); - } - return; -} - -void ControlConn::processFindLoad(int pktType) -{ - using std::string; - - constexpr int pkt_size = 4; - - if (rbuf.get_length() < pkt_size) { - chklen = pkt_size; - return; - } - - uint16_t svcSize; - rbuf.extract((char *)&svcSize, 1, 2); - chklen = svcSize + 3; // packet type + (2 byte) length + service name - if (svcSize <= 0 || chklen > 1024) { - // Queue error response / mark connection bad - char badreqRep[] = { DINIT_RP_BADREQ }; - if (! queuePacket(badreqRep, 1)) return; - bad_conn_close = true; - ev_io_set(&iob, iob.fd, EV_WRITE); - return; - } - - if (rbuf.get_length() < chklen) { - // packet not complete yet; read more - return; - } - - ServiceRecord * record = nullptr; - - string serviceName = std::move(rbuf.extract_string(3, svcSize)); - - if (pktType == DINIT_CP_LOADSERVICE) { - // LOADSERVICE - try { - record = service_set->loadService(serviceName); - } - catch (ServiceLoadExc &slexc) { - log(LogLevel::ERROR, "Could not load service ", slexc.serviceName, ": ", slexc.excDescription); - } - } - else { - // FINDSERVICE - record = service_set->findService(serviceName.c_str()); - } - - if (record != nullptr) { - // Allocate a service handle - handle_t handle = allocateServiceHandle(record); - std::vector rp_buf; - rp_buf.reserve(7); - rp_buf.push_back(DINIT_RP_SERVICERECORD); - rp_buf.push_back(static_cast(record->getState())); - for (int i = 0; i < (int) sizeof(handle); i++) { - rp_buf.push_back(*(((char *) &handle) + i)); - } - rp_buf.push_back(static_cast(record->getTargetState())); - if (! queuePacket(std::move(rp_buf))) return; - } - else { - std::vector rp_buf = { DINIT_RP_NOSERVICE }; - if (! queuePacket(std::move(rp_buf))) return; - } - - // Clear the packet from the buffer - rbuf.consume(chklen); - chklen = 0; - return; -} - -void ControlConn::processStartStop(int pktType) -{ - using std::string; - - constexpr int pkt_size = 2 + sizeof(handle_t); - - if (rbuf.get_length() < pkt_size) { - chklen = pkt_size; - return; - } - - // 1 byte: packet type - // 1 byte: pin in requested state (0 = no pin, 1 = pin) - // 4 bytes: service handle - - bool do_pin = (rbuf[1] == 1); - handle_t handle; - rbuf.extract((char *) &handle, 2, sizeof(handle)); - - ServiceRecord *service = findServiceForKey(handle); - if (service == nullptr) { - // Service handle is bad - char badreqRep[] = { DINIT_RP_BADREQ }; - if (! queuePacket(badreqRep, 1)) return; - bad_conn_close = true; - ev_io_set(&iob, iob.fd, EV_WRITE); - return; - } - else { - if (pktType == DINIT_CP_STARTSERVICE) { - if (do_pin) { - service->pinStart(); - } - else { - service->start(); - } - } - else { - if (do_pin) { - service->pinStop(); - } - else { - service->stop(); - } - } - - char ack_buf[] = { DINIT_RP_ACK }; - if (! queuePacket(ack_buf, 1)) return; - } - - // Clear the packet from the buffer - rbuf.consume(pkt_size); - chklen = 0; - return; -} - -ControlConn::handle_t ControlConn::allocateServiceHandle(ServiceRecord *record) -{ - bool is_unique = true; - handle_t largest_seen = 0; - handle_t candidate = 0; - for (auto p : keyServiceMap) { - if (p.first > largest_seen) largest_seen = p.first; - if (p.first == candidate) { - if (largest_seen == std::numeric_limits::max()) throw std::bad_alloc(); - candidate = largest_seen + 1; - } - is_unique &= (p.second != record); - } - - keyServiceMap[candidate] = record; - serviceKeyMap.insert(std::make_pair(record, candidate)); - - if (is_unique) { - record->addListener(this); - } - - return candidate; -} - - -bool ControlConn::queuePacket(const char *pkt, unsigned size) noexcept -{ - if (bad_conn_close) return false; - - bool was_empty = outbuf.empty(); - - if (was_empty) { - int wr = write(iob.fd, pkt, size); - if (wr == -1) { - if (errno == EPIPE) { - delete this; - return false; - } - if (errno != EAGAIN && errno != EWOULDBLOCK) { - // TODO log error - delete this; - return false; - } - } - else { - if ((unsigned)wr == size) { - // Ok, all written. - return true; - } - pkt += wr; - size -= wr; - } - ev_io_set(&iob, iob.fd, EV_READ | EV_WRITE); - } - - // Create a vector out of the (remaining part of the) packet: - try { - outbuf.emplace_back(pkt, pkt + size); - return true; - } - catch (std::bad_alloc &baexc) { - // Mark the connection bad, and stop reading further requests - bad_conn_close = true; - oom_close = true; - if (was_empty) { - // We can't send out-of-memory response as we already wrote as much as we - // could above. Neither can we later send the response since we have currently - // sent an incomplete packet. All we can do is close the connection. - delete this; - } - else { - ev_io_set(&iob, iob.fd, EV_WRITE); - } - return false; - } -} - - -bool ControlConn::queuePacket(std::vector &&pkt) noexcept -{ - if (bad_conn_close) return false; - - bool was_empty = outbuf.empty(); - - if (was_empty) { - outpkt_index = 0; - // We can try sending the packet immediately: - int wr = write(iob.fd, pkt.data(), pkt.size()); - if (wr == -1) { - if (errno == EPIPE) { - delete this; - return false; - } - if (errno != EAGAIN && errno != EWOULDBLOCK) { - // TODO log error - delete this; - return false; - } - } - else { - if ((unsigned)wr == pkt.size()) { - // Ok, all written. - return true; - } - outpkt_index = wr; - } - ev_io_set(&iob, iob.fd, EV_READ | EV_WRITE); - } - - try { - outbuf.emplace_back(pkt); - return true; - } - catch (std::bad_alloc &baexc) { - // Mark the connection bad, and stop reading further requests - bad_conn_close = true; - oom_close = true; - if (was_empty) { - // We can't send out-of-memory response as we already wrote as much as we - // could above. Neither can we later send the response since we have currently - // sent an incomplete packet. All we can do is close the connection. - delete this; - } - else { - ev_io_set(&iob, iob.fd, EV_WRITE); - } - return false; - } -} - -bool ControlConn::rollbackComplete() noexcept -{ - char ackBuf[2] = { DINIT_ROLLBACK_COMPLETED, 2 }; - return queuePacket(ackBuf, 2); -} - -bool ControlConn::dataReady() noexcept -{ - int fd = iob.fd; - - int r = rbuf.fill(fd); - - // Note file descriptor is non-blocking - if (r == -1) { - if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) { - // TODO log error - delete this; - return true; - } - return false; - } - - if (r == 0) { - delete this; - return true; - } - - // complete packet? - if (rbuf.get_length() >= chklen) { - try { - processPacket(); - } - catch (std::bad_alloc &baexc) { - doOomClose(); - } - } - - if (rbuf.get_length() == 1024) { - // Too big packet - // TODO log error? - // TODO error response? - bad_conn_close = true; - ev_io_set(&iob, iob.fd, EV_WRITE); - } - - return false; -} - -void ControlConn::sendData() noexcept -{ - if (outbuf.empty() && bad_conn_close) { - if (oom_close) { - // Send oom response - char oomBuf[] = { DINIT_RP_OOM }; - write(iob.fd, oomBuf, 1); - } - delete this; - return; - } - - vector & pkt = outbuf.front(); - char *data = pkt.data(); - int written = write(iob.fd, data + outpkt_index, pkt.size() - outpkt_index); - if (written == -1) { - if (errno == EPIPE) { - // read end closed - delete this; - } - else if (errno == EAGAIN || errno == EWOULDBLOCK) { - // spurious readiness notification? - } - else { - log(LogLevel::ERROR, "Error writing to control connection: ", strerror(errno)); - delete this; - } - return; - } - - outpkt_index += written; - if (outpkt_index == pkt.size()) { - // We've finished this packet, move on to the next: - outbuf.pop_front(); - outpkt_index = 0; - if (outbuf.empty() && ! oom_close) { - if (! bad_conn_close) { - ev_io_set(&iob, iob.fd, EV_READ); - } - else { - delete this; - } - } - } -} - -ControlConn::~ControlConn() noexcept -{ - close(iob.fd); - ev_io_stop(loop, &iob); - - // Clear service listeners - for (auto p : serviceKeyMap) { - p.first->removeListener(this); - } - - active_control_conns--; -} diff --git a/control.h b/control.h deleted file mode 100644 index 0bce51c..0000000 --- a/control.h +++ /dev/null @@ -1,184 +0,0 @@ -#ifndef DINIT_CONTROL_H -#define DINIT_CONTROL_H - -#include -#include -#include -#include - -#include -#include - -#include "dinit-log.h" -#include "control-cmds.h" -#include "service-listener.h" -#include "cpbuffer.h" - -// Control connection for dinit - -// TODO: Use the input buffer as a circular buffer, instead of chomping data from -// the front using a data move. - -// forward-declaration of callback: -static void control_conn_cb(struct ev_loop * loop, ev_io * w, int revents); - -class ControlConn; - -// Pointer to the control connection that is listening for rollback completion -extern ControlConn * rollback_handler_conn; - -extern int active_control_conns; - -// "packet" format: -// (1 byte) packet type -// (N bytes) additional data (service name, etc) -// for LOADSERVICE/FINDSERVICE: -// (2 bytes) service name length -// (M bytes) service name (without nul terminator) - -// Information packet: -// (1 byte) packet type, >= 100 -// (1 byte) packet length (including all fields) -// N bytes: packet data (N = (length - 2)) - -class ServiceSet; -class ServiceRecord; - -class ControlConn : private ServiceListener -{ - friend void control_conn_cb(struct ev_loop *, ev_io *, int); - - struct ev_io iob; - struct ev_loop *loop; - ServiceSet *service_set; - - bool bad_conn_close; // close when finished output? - bool oom_close; // send final 'out of memory' indicator - - // The packet length before we need to re-check if the packet is complete. - // processPacket() will not be called until the packet reaches this size. - int chklen; - - // Receive buffer - CPBuffer rbuf; - - template using list = std::list; - template using vector = std::vector; - - // A mapping between service records and their associated numerical identifier used - // in communction - using handle_t = uint32_t; - std::unordered_multimap serviceKeyMap; - std::unordered_map keyServiceMap; - - // Buffer for outgoing packets. Each outgoing back is represented as a vector. - list> outbuf; - // Current index within the first outgoing packet (all previous bytes have been sent). - unsigned outpkt_index = 0; - - // Queue a packet to be sent - // Returns: true if the packet was successfully queued, false if otherwise - // (eg if out of memory); in the latter case the connection might - // no longer be valid (iff there are no outgoing packets queued). - bool queuePacket(vector &&v) noexcept; - bool queuePacket(const char *pkt, unsigned size) noexcept; - - // Process a packet. Can cause the ControlConn to be deleted iff there are no - // outgoing packets queued. - // Throws: - // std::bad_alloc - if an out-of-memory condition prevents processing - void processPacket(); - - // Process a STARTSERVICE/STOPSERVICE packet. May throw std::bad_alloc. - void processStartStop(int pktType); - - // Process a FINDSERVICE/LOADSERVICE packet. May throw std::bad_alloc. - void processFindLoad(int pktType); - - // Notify that data is ready to be read from the socket. Returns true in cases where the - // connection was deleted with potentially pending outgoing packets. - bool dataReady() noexcept; - - void sendData() noexcept; - - // Allocate a new handle for a service; may throw std::bad_alloc - handle_t allocateServiceHandle(ServiceRecord *record); - - ServiceRecord *findServiceForKey(uint32_t key) - { - try { - return keyServiceMap.at(key); - } - catch (std::out_of_range &exc) { - return nullptr; - } - } - - // Close connection due to out-of-memory condition. - void doOomClose() - { - bad_conn_close = true; - oom_close = true; - ev_io_set(&iob, iob.fd, EV_WRITE); - } - - // Process service event broadcast. - void serviceEvent(ServiceRecord * service, ServiceEvent event) noexcept final override - { - // For each service handle corresponding to the event, send an information packet. - auto range = serviceKeyMap.equal_range(service); - auto & i = range.first; - auto & end = range.second; - try { - while (i != end) { - uint32_t key = i->second; - std::vector pkt; - constexpr int pktsize = 3 + sizeof(key); - pkt.reserve(pktsize); - pkt.push_back(DINIT_IP_SERVICEEVENT); - pkt.push_back(pktsize); - char * p = (char *) &key; - for (int j = 0; j < (int)sizeof(key); j++) { - pkt.push_back(*p++); - } - pkt.push_back(static_cast(event)); - queuePacket(std::move(pkt)); - ++i; - } - } - catch (std::bad_alloc &exc) { - doOomClose(); - } - } - - public: - ControlConn(struct ev_loop * loop, ServiceSet * service_set, int fd) : loop(loop), service_set(service_set), chklen(0) - { - ev_io_init(&iob, control_conn_cb, fd, EV_READ); - iob.data = this; - ev_io_start(loop, &iob); - - active_control_conns++; - } - - bool rollbackComplete() noexcept; - - virtual ~ControlConn() noexcept; -}; - - -static void control_conn_cb(struct ev_loop * loop, ev_io * w, int revents) -{ - ControlConn *conn = (ControlConn *) w->data; - if (revents & EV_READ) { - if (conn->dataReady()) { - // ControlConn was deleted - return; - } - } - if (revents & EV_WRITE) { - conn->sendData(); - } -} - -#endif diff --git a/cpbuffer.h b/cpbuffer.h deleted file mode 100644 index 251ef6a..0000000 --- a/cpbuffer.h +++ /dev/null @@ -1,89 +0,0 @@ -#ifndef CPBUFFER_H -#define CPBUFFER_H - -#include - -// control protocol buffer, a circular buffer with 1024-byte capacity. -class CPBuffer -{ - char buf[1024]; - int cur_idx = 0; - int length = 0; // number of elements in the buffer - - public: - int get_length() noexcept - { - return length; - } - - // fill by reading from the given fd, return positive if some was read or -1 on error. - int fill(int fd) noexcept - { - int pos = cur_idx + length; - if (pos >= 1024) pos -= 1024; - int max_count = std::min(1024 - pos, 1024 - length); - ssize_t r = read(fd, buf + cur_idx, max_count); - if (r >= 0) { - length += r; - } - return r; - } - - // fill by readin from the given fd, until at least the specified number of bytes are in - // the buffer. Return 0 if end-of-file reached before fill complete, or -1 on error. - int fillTo(int fd, int rlength) noexcept - { - while (length < rlength) { - int r = fill(fd); - if (r <= 0) return r; - } - return 1; - } - - int operator[](int idx) noexcept - { - int dest_idx = cur_idx + idx; - if (dest_idx > 1024) dest_idx -= 1024; - return buf[dest_idx]; - } - - void consume(int amount) noexcept - { - cur_idx += amount; - if (cur_idx >= 1024) cur_idx -= 1024; - length -= amount; - } - - void extract(char *dest, int index, int length) noexcept - { - index += cur_idx; - if (index >= 1024) index -= 1024; - if (index + length > 1024) { - // wrap-around copy - int half = 1024 - index; - std::memcpy(dest, buf + index, half); - std::memcpy(dest + half, buf, length - half); - } - else { - std::memcpy(dest, buf + index, length); - } - } - - // Extract string of give length from given index - // Throws: std::bad_alloc on allocation failure - std::string extract_string(int index, int length) - { - index += cur_idx; - if (index >= 1024) index -= 1024; - if (index + length > 1024) { - std::string r(buf + index, 1024 - index); - r.insert(r.end(), buf, buf + length - (1024 - index)); - return r; - } - else { - return std::string(buf + index, length); - } - } -}; - -#endif diff --git a/dinit-log.cc b/dinit-log.cc deleted file mode 100644 index 8e031e7..0000000 --- a/dinit-log.cc +++ /dev/null @@ -1,68 +0,0 @@ -#include -#include "dinit-log.h" - -LogLevel log_level = LogLevel::WARN; -bool log_to_console = true; // whether we should output log messages to console -bool log_current_line; - -// Log a message -void log(LogLevel lvl, const char *msg) noexcept -{ - if (lvl >= log_level) { - if (log_to_console) { - std::cout << "dinit: " << msg << std::endl; - } - } -} - -// Log a multi-part message beginning -void logMsgBegin(LogLevel lvl, const char *msg) noexcept -{ - log_current_line = lvl >= log_level; - if (log_current_line) { - if (log_to_console) { - std::cout << "dinit: " << msg; - } - } -} - -// Continue a multi-part log message -void logMsgPart(const char *msg) noexcept -{ - if (log_current_line) { - if (log_to_console) { - std::cout << msg; - } - } -} - -// Complete a multi-part log message -void logMsgEnd(const char *msg) noexcept -{ - if (log_current_line) { - if (log_to_console) { - std::cout << msg << std::endl; - } - } -} - -void logServiceStarted(const char *service_name) noexcept -{ - if (log_to_console) { - std::cout << "[ OK ] " << service_name << std::endl; - } -} - -void logServiceFailed(const char *service_name) noexcept -{ - if (log_to_console) { - std::cout << "[FAILED] " << service_name << std::endl; - } -} - -void logServiceStopped(const char *service_name) noexcept -{ - if (log_to_console) { - std::cout << "[STOPPED] " << service_name << std::endl; - } -} diff --git a/dinit-log.h b/dinit-log.h deleted file mode 100644 index b54664e..0000000 --- a/dinit-log.h +++ /dev/null @@ -1,112 +0,0 @@ -#ifndef DINIT_LOG_H -#define DINIT_LOG_H - -// Logging for Dinit - -#include -#include -#include - -enum class LogLevel { - DEBUG, - INFO, - WARN, - ERROR, - ZERO // log absolutely nothing -}; - -extern LogLevel log_level; -extern bool log_to_console; - -void log(LogLevel lvl, const char *msg) noexcept; -void logMsgBegin(LogLevel lvl, const char *msg) noexcept; -void logMsgPart(const char *msg) noexcept; -void logMsgEnd(const char *msg) noexcept; -void logServiceStarted(const char *service_name) noexcept; -void logServiceFailed(const char *service_name) noexcept; -void logServiceStopped(const char *service_name) noexcept; - -// Convenience methods which perform type conversion of the argument. -// There is some duplication here that could possibly be avoided, but -// it doesn't seem like a big deal. -static inline void log(LogLevel lvl, const std::string &str) noexcept -{ - log(lvl, str.c_str()); -} - -static inline void logMsgBegin(LogLevel lvl, const std::string &str) noexcept -{ - logMsgBegin(lvl, str.c_str()); -} - -static inline void logMsgBegin(LogLevel lvl, int a) noexcept -{ - constexpr int bufsz = (CHAR_BIT * sizeof(int) - 1) / 3 + 2; - char nbuf[bufsz]; - snprintf(nbuf, bufsz, "%d", a); - logMsgBegin(lvl, nbuf); -} - -static inline void logMsgPart(const std::string &str) noexcept -{ - logMsgPart(str.c_str()); -} - -static inline void logMsgPart(int a) noexcept -{ - constexpr int bufsz = (CHAR_BIT * sizeof(int) - 1) / 3 + 2; - char nbuf[bufsz]; - snprintf(nbuf, bufsz, "%d", a); - logMsgPart(nbuf); -} - -static inline void logMsgEnd(const std::string &str) noexcept -{ - logMsgEnd(str.c_str()); -} - -static inline void logMsgEnd(int a) noexcept -{ - constexpr int bufsz = (CHAR_BIT * sizeof(int) - 1) / 3 + 2; - char nbuf[bufsz]; - snprintf(nbuf, bufsz, "%d", a); - logMsgEnd(nbuf); -} - -static inline void logServiceStarted(const std::string &str) noexcept -{ - logServiceStarted(str.c_str()); -} - -static inline void logServiceFailed(const std::string &str) noexcept -{ - logServiceFailed(str.c_str()); -} - -static inline void logServiceStopped(const std::string &str) noexcept -{ - logServiceStopped(str.c_str()); -} - -// It's not intended that methods in this namespace be called directly: -namespace dinit_log { - template static inline void logParts(A a) noexcept - { - logMsgEnd(a); - } - - template static inline void logParts(A a, B... b) noexcept - { - logMsgPart(a); - logParts(b...); - } -} - -// Variadic 'log' method. -template static inline void log(LogLevel lvl, A a, B ...b) noexcept -{ - logMsgBegin(lvl, a); - dinit_log::logParts(b...); -} - -#endif diff --git a/dinit-start.cc b/dinit-start.cc deleted file mode 100644 index 3e6a9e7..0000000 --- a/dinit-start.cc +++ /dev/null @@ -1,253 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "control-cmds.h" -#include "service-constants.h" -#include "cpbuffer.h" - -// dinit-start: utility to start a dinit service - -// This utility communicates with the dinit daemon via a unix socket (/dev/initctl). - -using handle_t = uint32_t; - - -class ReadCPException -{ - public: - int errcode; - ReadCPException(int err) : errcode(err) { } -}; - -static void fillBufferTo(CPBuffer *buf, int fd, int rlength) -{ - int r = buf->fillTo(fd, rlength); - if (r == -1) { - throw ReadCPException(errno); - } - else if (r == 0) { - throw ReadCPException(0); - } -} - - -int main(int argc, char **argv) -{ - using namespace std; - - bool show_help = argc < 2; - char *service_name = nullptr; - - std::string control_socket_str; - const char * control_socket_path = nullptr; - - bool verbose = true; - bool sys_dinit = false; // communicate with system daemon - bool wait_for_service = true; - - for (int i = 1; i < argc; i++) { - if (argv[i][0] == '-') { - if (strcmp(argv[i], "--help") == 0) { - show_help = true; - break; - } - else if (strcmp(argv[i], "--no-wait") == 0) { - wait_for_service = false; - } - else if (strcmp(argv[i], "--quiet") == 0) { - verbose = false; - } - else if (strcmp(argv[i], "--system") == 0 || strcmp(argv[i], "-s") == 0) { - sys_dinit = true; - } - else { - cerr << "Unrecognized command-line parameter: " << argv[i] << endl; - return 1; - } - } - else { - // service name - service_name = argv[i]; - // TODO support multiple services (or at least give error if multiple - // services supplied) - } - } - - if (show_help) { - cout << "dinit-start: start a dinit service" << endl; - cout << " --help : show this help" << endl; - cout << " --no-wait : don't wait for service startup/shutdown to complete" << endl; - cout << " --quiet : suppress output (except errors)" << endl; - cout << " -s, --system : control system daemon instead of user daemon" << endl; - cout << " : start the named service" << endl; - return 1; - } - - - control_socket_path = "/dev/dinitctl"; - - if (! sys_dinit) { - char * userhome = getenv("HOME"); - if (userhome == nullptr) { - struct passwd * pwuid_p = getpwuid(getuid()); - if (pwuid_p != nullptr) { - userhome = pwuid_p->pw_dir; - } - } - - if (userhome != nullptr) { - control_socket_str = userhome; - control_socket_str += "/.dinitctl"; - control_socket_path = control_socket_str.c_str(); - } - else { - cerr << "Cannot locate user home directory (set HOME or check /etc/passwd file)" << endl; - return 1; - } - } - - int socknum = socket(AF_UNIX, SOCK_STREAM, 0); - if (socknum == -1) { - perror("socket"); - return 1; - } - - struct sockaddr_un * name; - uint sockaddr_size = offsetof(struct sockaddr_un, sun_path) + strlen(control_socket_path) + 1; - name = (struct sockaddr_un *) malloc(sockaddr_size); - if (name == nullptr) { - cerr << "dinit-start: out of memory" << endl; - return 1; - } - - name->sun_family = AF_UNIX; - strcpy(name->sun_path, control_socket_path); - - int connr = connect(socknum, (struct sockaddr *) name, sockaddr_size); - if (connr == -1) { - perror("connect"); - return 1; - } - - // TODO should start by querying protocol version - - // Build buffer; - uint16_t sname_len = strlen(service_name); - int bufsize = 3 + sname_len; - char * buf = new char[bufsize]; - - buf[0] = DINIT_CP_LOADSERVICE; - memcpy(buf + 1, &sname_len, 2); - memcpy(buf + 3, service_name, sname_len); - - int r = write(socknum, buf, bufsize); - // TODO make sure we write it all - delete [] buf; - if (r == -1) { - perror("write"); - return 1; - } - - // Now we expect a reply: - // NOTE: should skip over information packets. - - try { - CPBuffer rbuffer; - fillBufferTo(&rbuffer, socknum, 1); - - ServiceState state; - ServiceState target_state; - handle_t handle; - - if (rbuffer[0] == DINIT_RP_SERVICERECORD) { - fillBufferTo(&rbuffer, socknum, 2 + sizeof(handle)); - rbuffer.extract((char *) &handle, 2, sizeof(handle)); - state = static_cast(rbuffer[1]); - target_state = static_cast(rbuffer[2 + sizeof(handle)]); - rbuffer.consume(3 + sizeof(handle)); - } - else if (rbuffer[0] == DINIT_RP_NOSERVICE) { - cerr << "Failed to find/load service." << endl; - return 1; - } - else { - cerr << "Protocol error." << endl; - return 1; - } - - // Need to issue STARTSERVICE: - if (target_state != ServiceState::STARTED) { - buf = new char[2 + sizeof(handle)]; - buf[0] = DINIT_CP_STARTSERVICE; - buf[1] = 0; // don't pin - memcpy(buf + 2, &handle, sizeof(handle)); - r = write(socknum, buf, 2 + sizeof(handle)); - delete buf; - } - - if (state == ServiceState::STARTED) { - if (verbose) { - cout << "Service already started." << endl; - } - return 0; // success! - } - - if (! wait_for_service) { - return 0; - } - - // Wait until service started: - r = rbuffer.fillTo(socknum, 2); - while (r > 0) { - if (rbuffer[0] >= 100) { - int pktlen = (unsigned char) rbuffer[1]; - fillBufferTo(&rbuffer, socknum, pktlen); - - if (rbuffer[0] == DINIT_IP_SERVICEEVENT) { - handle_t ev_handle; - rbuffer.extract((char *) &ev_handle, 2, sizeof(ev_handle)); - ServiceEvent event = static_cast(rbuffer[2 + sizeof(ev_handle)]); - if (ev_handle == handle && event == ServiceEvent::STARTED) { - if (verbose) { - cout << "Service started." << endl; - } - return 0; - } - } - } - else { - // Not an information packet? - cerr << "protocol error" << endl; - return 1; - } - } - - if (r == -1) { - perror("read"); - } - else { - cerr << "protocol error (connection closed by server)" << endl; - } - return 1; - } - catch (ReadCPException &exc) { - cerr << "control socket read failure or protocol error" << endl; - return 1; - } - catch (std::bad_alloc &exc) { - cerr << "out of memory" << endl; - return 1; - } - - return 0; -} diff --git a/dinit.cc b/dinit.cc deleted file mode 100644 index 9b26b3a..0000000 --- a/dinit.cc +++ /dev/null @@ -1,429 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "service.h" -#include "ev++.h" -#include "control.h" -#include "dinit-log.h" - -#ifdef __linux__ -#include -#include -#endif - -/* - * "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). - * - * 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. - * - * 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. - */ - - -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; - -struct ev_io control_socket_io; - - -// Variables - -static ServiceSet *service_set; - -static bool am_system_init = false; // true if we are the system init process - -static bool control_socket_open = false; -int active_control_conns = 0; - -static const char *control_socket_path = "/dev/dinitctl"; -static std::string control_socket_str; - - -int main(int argc, char **argv) -{ - using namespace std; - - am_system_init = (getpid() == 1); - - if (am_system_init) { - // setup STDIN, STDOUT, STDERR so that we can use them - int onefd = open("/dev/console", O_RDONLY, 0); - dup2(onefd, 0); - int twofd = open("/dev/console", O_RDWR, 0); - dup2(twofd, 1); - dup2(twofd, 2); - } - - /* 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); */ - - // 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(SIGTSTP, SIG_IGN); - signal(SIGTTIN, SIG_IGN); - signal(SIGTTOU, SIG_IGN); - - /* list of services to start */ - list services_to_start; - - if (! am_system_init) { - char * userhome = getenv("HOME"); - if (userhome == nullptr) { - struct passwd * pwuid_p = getpwuid(getuid()); - if (pwuid_p != nullptr) { - userhome = pwuid_p->pw_dir; - } - } - - if (userhome != nullptr) { - control_socket_str = userhome; - control_socket_str += "/.dinitctl"; - control_socket_path = control_socket_str.c_str(); - } - } - - /* service directory name */ - const char * service_dir = "/etc/dinit.d"; - - // 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 - // 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) { - ++i; - if (i < argc) { - service_dir = argv[i]; - } - else { - cerr << "dinit: '--services-dir' (-d) 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 << " --services-dir , -d : set base directory for service description files (-d )" << endl; - cout << " : start service with name " << endl; - return 0; - } - else { - // unrecognized - if (! am_system_init) { - cerr << "dinit: Unrecognized option: " << argv[i] << endl; - return 1; - } - } - } - else { - // 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]); - } - } - } - } - - if (services_to_start.empty()) { - services_to_start.push_back("boot"); - } - - // Set up signal handlers - ev_signal sigint_ev_signal; - if (am_system_init) { - ev_signal_init(&sigint_ev_signal, sigint_reboot_cb, SIGINT); - } - else { - ev_signal_init(&sigint_ev_signal, sigterm_cb, SIGINT); - } - - 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); - } - - 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); - -#ifdef __linux__ - if (am_system_init) { - // Disable non-critical kernel output to console - klogctl(6 /* SYSLOG_ACTION_CONSOLE_OFF */, nullptr, 0); - // Make ctrl+alt+del combination send SIGINT to PID 1 (this process) - reboot(RB_DISABLE_CAD); - } -#endif - - /* start requested services */ - service_set = new ServiceSet(service_dir); - for (list::iterator i = services_to_start.begin(); - i != services_to_start.end(); - ++i) { - try { - service_set->startService(*i); - } - catch (ServiceNotFound &snf) { - log(LogLevel::ERROR, snf.serviceName, ": Could not find service description."); - } - catch (ServiceLoadExc &sle) { - log(LogLevel::ERROR, sle.serviceName, ": ", sle.excDescription); - } - catch (std::bad_alloc &badalloce) { - log(LogLevel::ERROR, "Out of memory when trying to start service: ", *i, "."); - } - } - - event_loop: - - // Process events until all services have terminated. - while (service_set->count_active_services() != 0) { - ev_loop(loop, EVLOOP_ONESHOT); - } - - ShutdownType shutdown_type = service_set->getShutdownType(); - - if (am_system_init) { - logMsgBegin(LogLevel::INFO, "No more active services."); - - if (shutdown_type == ShutdownType::REBOOT) { - logMsgEnd(" Will reboot."); - } - else if (shutdown_type == ShutdownType::HALT) { - logMsgEnd(" Will halt."); - } - else if (shutdown_type == ShutdownType::POWEROFF) { - logMsgEnd(" Will power down."); - } - else { - logMsgEnd(" Re-initiating boot sequence."); - } - } - - close_control_socket(ev_default_loop(EVFLAG_AUTO)); - - if (am_system_init) { - if (shutdown_type == ShutdownType::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"); - 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; - } - } - - const char * cmd_arg; - if (shutdown_type == ShutdownType::HALT) { - cmd_arg = "-h"; - } - else if (shutdown_type == ShutdownType::REBOOT) { - cmd_arg = "-r"; - } - else { - // power off. - cmd_arg = "-p"; - } - - // Fork and execute dinit-reboot. - execl("/sbin/shutdown", "/sbin/shutdown", "--system", cmd_arg, nullptr); - log(LogLevel::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); - } - } - - return 0; -} - -// Callback for control socket -static void control_socket_cb(struct ev_loop *loop, ev_io *w, int revents) -{ - // 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. - - // Accept a connection - int sockfd = w->fd; - - int newfd = 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 - } - catch (std::bad_alloc &bad_alloc_exc) { - log(LogLevel::ERROR, "Accepting control connection: Out of memory"); - close(newfd); - } - } -} - -void open_control_socket(struct ev_loop *loop) noexcept -{ - if (! control_socket_open) { - const char * saddrname = control_socket_path; - uint sockaddr_size = offsetof(struct sockaddr_un, sun_path) + strlen(saddrname) + 1; - - struct sockaddr_un * name = static_cast(malloc(sockaddr_size)); - if (name == nullptr) { - log(LogLevel::ERROR, "Opening control socket: out of memory"); - return; - } - - if (am_system_init) { - // Unlink any stale control socket file, but only if we are system init, since otherwise - // the 'stale' file may not be stale at all: - unlink(saddrname); - } - - name->sun_family = AF_UNIX; - strcpy(name->sun_path, saddrname); - - int sockfd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); - if (sockfd == -1) { - log(LogLevel::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)); - close(sockfd); - free(name); - return; - } - - free(name); - - // 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)); - close(sockfd); - return; - } - - if (listen(sockfd, 10) == -1) { - log(LogLevel::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); - } -} - -void close_control_socket(struct ev_loop *loop) noexcept -{ - if (control_socket_open) { - int fd = control_socket_io.fd; - ev_io_stop(loop, &control_socket_io); - close(fd); - - // Unlink the socket: - unlink(control_socket_path); - } -} - -/* 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) -{ - log_to_console = true; - service_set->stop_all_services(ShutdownType::REBOOT); -} - -/* handle SIGQUIT (if we are system init) */ -static void sigquit_cb(struct ev_loop *loop, ev_signal *w, int revents) -{ - // 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)); -} - -/* handle SIGTERM/SIGQUIT - stop all services (not used for system daemon) */ -static void sigterm_cb(struct ev_loop *loop, ev_signal *w, int revents) -{ - log_to_console = true; - service_set->stop_all_services(); -} diff --git a/halt b/halt deleted file mode 100755 index 4987aff..0000000 --- a/halt +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -# "halt" command actually executes the more useful "power off". -shutdown -p "$@" diff --git a/load_service.cc b/load_service.cc deleted file mode 100644 index 79972ff..0000000 --- a/load_service.cc +++ /dev/null @@ -1,507 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "service.h" - -typedef std::string string; -typedef std::string::iterator string_iterator; - -// Utility function to skip white space. Returns an iterator at the -// first non-white-space position (or at end). -static string_iterator skipws(string_iterator i, string_iterator end) -{ - using std::locale; - using std::isspace; - - while (i != end) { - if (! isspace(*i, locale::classic())) { - break; - } - ++i; - } - return i; -} - -// Read a setting name. -static string read_setting_name(string_iterator & i, string_iterator end) -{ - using std::locale; - using std::ctype; - using std::use_facet; - - const ctype & facet = use_facet >(locale::classic()); - - string rval; - // Allow alphabetical characters, and dash (-) in setting name - while (i != end && (*i == '-' || facet.is(ctype::alpha, *i))) { - rval += *i; - ++i; - } - return rval; -} - -// Read a setting value -// -// In general a setting value is a single-line string. It may contain multiple parts -// separated by white space (which is normally collapsed). A hash mark - # - denotes -// the end of the value and the beginning of a comment (it should be preceded by -// whitespace). -// -// Part of a value may be quoted using double quote marks, which prevents collapse -// of whitespace and interpretation of most special characters (the quote marks will -// not be considered part of the value). A backslash can precede a character (such -// as '#' or '"' or another backslash) to remove its special meaning. Newline -// characters are not allowed in values and cannot be quoted. -// -// This function expects the string to be in an ASCII-compatible, single byte -// encoding (the "classic" locale). -// -// Params: -// i - reference to string iterator through the line -// end - iterator at end of line -// part_positions - list of to which the position of each setting value -// part will be added as [start,end). May be null. -static string read_setting_value(string_iterator & i, string_iterator end, - std::list> * part_positions = nullptr) -{ - using std::locale; - using std::isspace; - - i = skipws(i, end); - - string rval; - bool new_part = true; - int part_start; - - while (i != end) { - char c = *i; - if (c == '\"') { - if (new_part) { - part_start = rval.length(); - new_part = false; - } - // quoted string - ++i; - while (i != end) { - c = *i; - if (c == '\"') break; - if (c == '\n') { - // TODO error here. - } - else if (c == '\\') { - // A backslash escapes the following character. - ++i; - if (i != end) { - c = *i; - if (c == '\n') { - // TODO error here. - } - rval += c; - } - } - else { - rval += c; - } - ++i; - } - if (i == end) { - // String wasn't terminated - // TODO error here - break; - } - } - else if (c == '\\') { - if (new_part) { - part_start = rval.length(); - new_part = false; - } - // A backslash escapes the next character - ++i; - if (i != end) { - rval += *i; - } - else { - // TODO error here - } - } - else if (isspace(c, locale::classic())) { - if (! new_part && part_positions != nullptr) { - part_positions->emplace_back(part_start, rval.length()); - new_part = true; - } - i = skipws(i, end); - if (i == end) break; - if (*i == '#') break; // comment - rval += ' '; // collapse ws to a single space - continue; - } - else if (c == '#') { - // hmm... comment? Probably, though they should have put a space - // before it really. TODO throw an exception, and document - // that '#' for comments must be preceded by space, and in values - // must be quoted. - break; - } - else { - if (new_part) { - part_start = rval.length(); - new_part = false; - } - rval += c; - } - ++i; - } - - // Got to end: - if (part_positions != nullptr) { - part_positions->emplace_back(part_start, rval.length()); - } - - return rval; -} - -static int signalNameToNumber(std::string &signame) -{ - if (signame == "HUP") return SIGHUP; - if (signame == "INT") return SIGINT; - if (signame == "QUIT") return SIGQUIT; - if (signame == "USR1") return SIGUSR1; - if (signame == "USR2") return SIGUSR2; - return -1; -} - -static const char * uid_err_msg = "Specified user id contains invalid numeric characters or is outside allowed range."; - -// Parse a userid parameter which may be a numeric user ID or a username. If a name, the -// userid is looked up via the system user database (getpwnam() function). In this case, -// the associated group is stored in the location specified by the group_p parameter iff -// it is not null and iff it contains the value -1. -static uid_t parse_uid_param(const std::string ¶m, const std::string &service_name, gid_t *group_p) -{ - // Could be a name or a numeric id. But we should assume numeric first, just in case - // a user manages to give themselves a username that parses as a number. - std::size_t ind = 0; - try { - // POSIX does not specify whether uid_t is an signed or unsigned, but regardless - // is is probably safe to assume that valid values are positive. We'll also assume - // that the value range fits with "unsigned long long" since it seems unlikely - // that would ever not be the case. - // - // TODO perhaps write a number parser, since even the unsigned variants of the C/C++ - // functions accept a leading minus sign... - static_assert((uintmax_t)std::numeric_limits::max() <= (uintmax_t)std::numeric_limits::max(), "uid_t is too large"); - unsigned long long v = std::stoull(param, &ind, 0); - if (v > static_cast(std::numeric_limits::max()) || ind != param.length()) { - throw ServiceDescriptionExc(service_name, uid_err_msg); - } - return v; - } - catch (std::out_of_range &exc) { - throw ServiceDescriptionExc(service_name, uid_err_msg); - } - catch (std::invalid_argument &exc) { - // Ok, so it doesn't look like a number: proceed... - } - - errno = 0; - struct passwd * pwent = getpwnam(param.c_str()); - if (pwent == nullptr) { - // Maybe an error, maybe just no entry. - if (errno == 0) { - throw new ServiceDescriptionExc(service_name, "Specified user \"" + param + "\" does not exist in system database."); - } - else { - throw new ServiceDescriptionExc(service_name, std::string("Error accessing user database: ") + strerror(errno)); - } - } - - if (group_p && *group_p != (gid_t)-1) { - *group_p = pwent->pw_gid; - } - - return pwent->pw_uid; -} - -static const char * gid_err_msg = "Specified group id contains invalid numeric characters or is outside allowed range."; - -static gid_t parse_gid_param(const std::string ¶m, const std::string &service_name) -{ - // Could be a name or a numeric id. But we should assume numeric first, just in case - // a user manages to give themselves a username that parses as a number. - std::size_t ind = 0; - try { - // POSIX does not specify whether uid_t is an signed or unsigned, but regardless - // is is probably safe to assume that valid values are positive. We'll also assume - // that the value range fits with "unsigned long long" since it seems unlikely - // that would ever not be the case. - // - // TODO perhaps write a number parser, since even the unsigned variants of the C/C++ - // functions accept a leading minus sign... - unsigned long long v = std::stoull(param, &ind, 0); - if (v > static_cast(std::numeric_limits::max()) || ind != param.length()) { - throw ServiceDescriptionExc(service_name, gid_err_msg); - } - return v; - } - catch (std::out_of_range &exc) { - throw ServiceDescriptionExc(service_name, gid_err_msg); - } - catch (std::invalid_argument &exc) { - // Ok, so it doesn't look like a number: proceed... - } - - errno = 0; - struct group * grent = getgrnam(param.c_str()); - if (grent == nullptr) { - // Maybe an error, maybe just no entry. - if (errno == 0) { - throw new ServiceDescriptionExc(service_name, "Specified group \"" + param + "\" does not exist in system database."); - } - else { - throw new ServiceDescriptionExc(service_name, std::string("Error accessing group database: ") + strerror(errno)); - } - } - - return grent->gr_gid; -} - -// Find a service record, or load it from file. If the service has -// dependencies, load those also. -// -// Might throw a ServiceLoadExc exception if a dependency cycle is found or if another -// problem occurs (I/O error, service description not found etc). Throws std::bad_alloc -// if a memory allocation failure occurs. -ServiceRecord * ServiceSet::loadServiceRecord(const char * name) -{ - using std::string; - using std::ifstream; - using std::ios; - using std::ios_base; - using std::locale; - using std::isspace; - - using std::list; - using std::pair; - - // First try and find an existing record... - ServiceRecord * rval = findService(string(name)); - if (rval != 0) { - if (rval->isDummy()) { - throw ServiceCyclicDependency(name); - } - return rval; - } - - // Couldn't find one. Have to load it. - string service_filename = service_dir; - if (*(service_filename.rbegin()) != '/') { - service_filename += '/'; - } - service_filename += name; - - string command; - list> command_offsets; - string stop_command; - list> stop_command_offsets; - string pid_file; - - ServiceType service_type = ServiceType::PROCESS; - std::list depends_on; - std::list depends_soft; - string logfile; - OnstartFlags onstart_flags; - int term_signal = -1; // additional termination signal - bool auto_restart = false; - bool smooth_recovery = false; - string socket_path; - int socket_perms = 0666; - // Note: Posix allows that uid_t and gid_t may be unsigned types, but eg chown uses -1 as an - // invalid value, so it's safe to assume that we can do the same: - uid_t socket_uid = -1; - gid_t socket_gid = -1; - - string line; - ifstream service_file; - service_file.exceptions(ios::badbit | ios::failbit); - - try { - service_file.open(service_filename.c_str(), ios::in); - } - catch (std::ios_base::failure &exc) { - throw ServiceNotFound(name); - } - - // Add a dummy service record now to prevent infinite recursion in case of cyclic dependency - rval = new ServiceRecord(this, string(name)); - records.push_back(rval); - - try { - // getline can set failbit if it reaches end-of-file, we don't want an exception in that case: - service_file.exceptions(ios::badbit); - - while (! (service_file.rdstate() & ios::eofbit)) { - getline(service_file, line); - string::iterator i = line.begin(); - string::iterator end = line.end(); - - i = skipws(i, end); - if (i != end) { - if (*i == '#') { - continue; // comment line - } - string setting = read_setting_name(i, end); - i = skipws(i, end); - if (i == end || (*i != '=' && *i != ':')) { - throw ServiceDescriptionExc(name, "Badly formed line."); - } - i = skipws(++i, end); - - if (setting == "command") { - command = read_setting_value(i, end, &command_offsets); - } - else if (setting == "socket-listen") { - socket_path = read_setting_value(i, end, nullptr); - } - else if (setting == "socket-permissions") { - string sock_perm_str = read_setting_value(i, end, nullptr); - std::size_t ind = 0; - try { - socket_perms = std::stoi(sock_perm_str, &ind, 8); - if (ind != sock_perm_str.length()) { - throw std::logic_error(""); - } - } - catch (std::logic_error &exc) { - throw ServiceDescriptionExc(name, "socket-permissions: Badly-formed or out-of-range numeric value"); - } - } - else if (setting == "socket-uid") { - string sock_uid_s = read_setting_value(i, end, nullptr); - socket_uid = parse_uid_param(sock_uid_s, name, &socket_gid); - } - else if (setting == "socket-gid") { - string sock_gid_s = read_setting_value(i, end, nullptr); - socket_gid = parse_gid_param(sock_gid_s, name); - } - else if (setting == "stop-command") { - stop_command = read_setting_value(i, end, &stop_command_offsets); - } - else if (setting == "pid-file") { - pid_file = read_setting_value(i, end); - } - else if (setting == "depends-on") { - string dependency_name = read_setting_value(i, end); - depends_on.push_back(loadServiceRecord(dependency_name.c_str())); - } - else if (setting == "waits-for") { - string dependency_name = read_setting_value(i, end); - depends_soft.push_back(loadServiceRecord(dependency_name.c_str())); - } - else if (setting == "logfile") { - logfile = read_setting_value(i, end); - } - else if (setting == "restart") { - string restart = read_setting_value(i, end); - auto_restart = (restart == "yes" || restart == "true"); - } - else if (setting == "smooth-recovery") { - string recovery = read_setting_value(i, end); - smooth_recovery = (recovery == "yes" || recovery == "true"); - } - else if (setting == "type") { - string type_str = read_setting_value(i, end); - if (type_str == "scripted") { - service_type = ServiceType::SCRIPTED; - } - else if (type_str == "process") { - service_type = ServiceType::PROCESS; - } - else if (type_str == "bgprocess") { - service_type = ServiceType::BGPROCESS; - } - else if (type_str == "internal") { - service_type = ServiceType::INTERNAL; - } - else { - throw ServiceDescriptionExc(name, "Service type must be one of: \"scripted\"," - " \"process\", \"bgprocess\" or \"internal\""); - } - } - else if (setting == "onstart") { - std::list> indices; - string onstart_cmds = read_setting_value(i, end, &indices); - for (auto indexpair : indices) { - string onstart_cmd = onstart_cmds.substr(indexpair.first, indexpair.second - indexpair.first); - if (onstart_cmd == "rw_ready") { - onstart_flags.rw_ready = true; - } - else { - throw new ServiceDescriptionExc(name, "Unknown onstart command: " + onstart_cmd); - } - } - } - else if (setting == "termsignal") { - string signame = read_setting_value(i, end, nullptr); - int signo = signalNameToNumber(signame); - if (signo == -1) { - throw new ServiceDescriptionExc(name, "Unknown/unsupported termination signal: " + signame); - } - else { - term_signal = signo; - } - } - else if (setting == "nosigterm") { - string sigtermsetting = read_setting_value(i, end); - onstart_flags.no_sigterm = (sigtermsetting == "yes" || sigtermsetting == "true"); - } - else if (setting == "runs-on-console") { - string runconsolesetting = read_setting_value(i, end); - onstart_flags.runs_on_console = (runconsolesetting == "yes" || runconsolesetting == "true"); - } - else { - throw ServiceDescriptionExc(name, "Unknown setting: " + setting); - } - } - } - - service_file.close(); - // TODO check we actually have all the settings - type, command - - // Now replace the dummy service record with a real record: - for (auto iter = records.begin(); iter != records.end(); iter++) { - if (*iter == rval) { - // We've found the dummy record - delete rval; - rval = new ServiceRecord(this, string(name), service_type, std::move(command), command_offsets, - & depends_on, & depends_soft); - rval->setStopCommand(stop_command, stop_command_offsets); - rval->setLogfile(logfile); - rval->setAutoRestart(auto_restart); - rval->setSmoothRecovery(smooth_recovery); - rval->setOnstartFlags(onstart_flags); - rval->setExtraTerminationSignal(term_signal); - rval->set_pid_file(std::move(pid_file)); - rval->set_socket_details(std::move(socket_path), socket_perms, socket_uid, socket_gid); - *iter = rval; - break; - } - } - - return rval; - } - catch (...) { - // Must remove the dummy service record. - std::remove(records.begin(), records.end(), rval); - delete rval; - throw; - } -} diff --git a/reboot b/reboot deleted file mode 100755 index c607879..0000000 --- a/reboot +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -shutdown -r "$@" diff --git a/service-constants.h b/service-constants.h deleted file mode 100644 index 8084066..0000000 --- a/service-constants.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef SERVICE_CONSTANTS_H -#define SERVICE_CONSTANTS_H - -/* Service states */ -enum class ServiceState { - STOPPED, // service is not running. - STARTING, // service is starting, and will start (or fail to start) in time. - STARTED, // service is running, - STOPPING // service script is stopping and will stop. -}; - -/* Service types */ -enum class ServiceType { - DUMMY, // Dummy service, used to detect cyclice dependencies - PROCESS, // Service runs as a process, and can be stopped by - // sending the process a signal (usually SIGTERM) - BGPROCESS, // Service runs as a process which "daemonizes" to run in the - // "background". - SCRIPTED, // Service requires an external command to start, - // and a second command to stop - INTERNAL // Internal service, runs no external process -}; - -/* Service events */ -enum class ServiceEvent { - STARTED, // Service was started (reached STARTED state) - STOPPED, // Service was stopped (reached STOPPED state) - FAILEDSTART, // Service failed to start (possibly due to dependency failing) - STARTCANCELLED, // Service was set to be started but a stop was requested - STOPCANCELLED // Service was set to be stopped but a start was requested -}; - -/* Shutdown types */ -enum class ShutdownType { - CONTINUE, // Continue normal boot sequence (used after single-user shell) - HALT, // Halt system without powering down - POWEROFF, // Power off system - REBOOT // Reboot system -}; - -#endif diff --git a/service-listener.h b/service-listener.h deleted file mode 100644 index c45c2a9..0000000 --- a/service-listener.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef SERVICE_LISTENER_H -#define SERVICE_LISTENER_H - -#include "service-constants.h" - -class ServiceRecord; - -// Interface for listening to services -class ServiceListener -{ - public: - - // An event occurred on the service being observed. - // Listeners must not be added or removed during event notification. - virtual void serviceEvent(ServiceRecord * service, ServiceEvent event) noexcept = 0; -}; - -#endif diff --git a/service.cc b/service.cc deleted file mode 100644 index 4fec13b..0000000 --- a/service.cc +++ /dev/null @@ -1,870 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "service.h" -#include "dinit-log.h" - -// from dinit.cc: -void open_control_socket(struct ev_loop *loop) noexcept; - - -// Find the requested service by name -static ServiceRecord * findService(const std::list & records, - const char *name) noexcept -{ - using std::list; - list::const_iterator i = records.begin(); - for ( ; i != records.end(); i++ ) { - if (strcmp((*i)->getServiceName(), name) == 0) { - return *i; - } - } - return (ServiceRecord *)0; -} - -ServiceRecord * ServiceSet::findService(const std::string &name) noexcept -{ - return ::findService(records, name.c_str()); -} - -void ServiceSet::startService(const char *name) -{ - using namespace std; - ServiceRecord *record = loadServiceRecord(name); - - record->start(); -} - -void ServiceSet::stopService(const std::string & name) noexcept -{ - ServiceRecord *record = findService(name); - if (record != nullptr) { - record->stop(); - } -} - -// Called when a service has actually stopped. -void ServiceRecord::stopped() noexcept -{ - if (service_type != ServiceType::SCRIPTED && service_type != ServiceType::BGPROCESS && onstart_flags.runs_on_console) { - tcsetpgrp(0, getpgrp()); - releaseConsole(); - } - - logServiceStopped(service_name); - service_state = ServiceState::STOPPED; - force_stop = false; - - // Stop any dependencies whose desired state is STOPPED: - for (sr_iter i = depends_on.begin(); i != depends_on.end(); i++) { - (*i)->dependentStopped(); - } - - service_set->service_inactive(this); - notifyListeners(ServiceEvent::STOPPED); - - if (desired_state == ServiceState::STARTED) { - // Desired state is "started". - start(); - } - else if (socket_fd != -1) { - close(socket_fd); - socket_fd = -1; - } -} - -void ServiceRecord::process_child_callback(struct ev_loop *loop, ev_child *w, int revents) noexcept -{ - ServiceRecord *sr = (ServiceRecord *) w->data; - - sr->pid = -1; - sr->exit_status = w->rstatus; - ev_child_stop(loop, w); - - // Ok, for a process service, any process death which we didn't rig - // ourselves is a bit... unexpected. Probably, the child died because - // we asked it to (sr->service_state == STOPPING). But even if - // we didn't, there's not much we can do. - - if (sr->waiting_for_execstat) { - // We still don't have an exec() status from the forked child, wait for that - // before doing any further processing. - return; - } - - sr->handle_exit_status(); -} - -void ServiceRecord::handle_exit_status() noexcept -{ - if (exit_status != 0 && service_state != ServiceState::STOPPING) { - log(LogLevel::ERROR, "Service ", service_name, " process terminated with exit code ", exit_status); - } - - if (doing_recovery) { - // (BGPROCESS only) - doing_recovery = false; - bool do_stop = false; - if (exit_status != 0) { - do_stop = true; - } - else { - // We need to re-read the PID, since it has now changed. - if (service_type == ServiceType::BGPROCESS && pid_file.length() != 0) { - if (! read_pid_file()) { - do_stop = true; - } - } - } - - if (do_stop) { - stop(); - if (auto_restart && service_set->get_auto_restart()) { - start(); - } - } - - return; - } - - if (service_type == ServiceType::PROCESS || service_type == ServiceType::BGPROCESS) { - if (service_state == ServiceState::STARTING) { - // (only applies to BGPROCESS) - if (exit_status == 0) { - started(); - } - else { - failed_to_start(); - } - } - else if (service_state == ServiceState::STOPPING) { - // TODO log non-zero rstatus? - stopped(); - } - else if (smooth_recovery && service_state == ServiceState::STARTED) { - // TODO ensure a minimum time between restarts - // TODO if we are pinned-started then we should probably check - // that dependencies have started before trying to re-start the - // service process. - doing_recovery = (service_type == ServiceType::BGPROCESS); - start_ps_process(); - return; - } - else { - forceStop(); - } - - if (auto_restart && service_set->get_auto_restart()) { - start(); - } - } - else { // SCRIPTED - if (service_state == ServiceState::STOPPING) { - if (exit_status == 0) { - stopped(); - } - else { - // ??? failed to stop! Let's log it as info: - log(LogLevel::INFO, "service ", service_name, " stop command failed with exit code ", exit_status); - // Just assume that we stopped, so that any dependencies - // can be stopped: - stopped(); - } - } - else { // STARTING - if (exit_status == 0) { - started(); - } - else { - // failed to start - log(LogLevel::ERROR, "service ", service_name, " command failed with exit code ", exit_status); - failed_to_start(); - } - } - } -} - -void ServiceRecord::process_child_status(struct ev_loop *loop, ev_io * stat_io, int revents) noexcept -{ - ServiceRecord *sr = (ServiceRecord *) stat_io->data; - sr->waiting_for_execstat = false; - - int exec_status; - int r = read(stat_io->fd, &exec_status, sizeof(int)); - close(stat_io->fd); - ev_io_stop(loop, stat_io); - - if (r != 0) { - // We read an errno code; exec() failed, and the service startup failed. - sr->pid = -1; - log(LogLevel::ERROR, sr->service_name, ": execution failed: ", strerror(exec_status)); - if (sr->service_state == ServiceState::STARTING) { - sr->failed_to_start(); - } - else if (sr->service_state == ServiceState::STOPPING) { - // Must be a scripted servce. We've logged the failure, but it's probably better - // not to leave the service in STARTED state: - sr->stopped(); - } - } - else { - // exec() succeeded. - if (sr->service_type == ServiceType::PROCESS) { - if (sr->service_state != ServiceState::STARTED) { - sr->started(); - } - } - - if (sr->pid == -1) { - // Somehow the process managed to complete before we even saw the status. - sr->handle_exit_status(); - } - } -} - -void ServiceRecord::start() noexcept -{ - if ((service_state == ServiceState::STARTING || service_state == ServiceState::STARTED) - && desired_state == ServiceState::STOPPED) { - // This service was starting, or started, but was set to be stopped. - // Cancel the stop (and continue starting/running). - notifyListeners(ServiceEvent::STOPCANCELLED); - } - - if (desired_state == ServiceState::STARTED && service_state != ServiceState::STOPPED) return; - - desired_state = ServiceState::STARTED; - - if (pinned_stopped) return; - - if (service_state != ServiceState::STOPPED) { - // We're already starting/started, or we are stopping and need to wait for - // that the complete. - if (service_state != ServiceState::STOPPING || ! can_interrupt_stop()) { - return; - } - // We're STOPPING, and that can be interrupted. Our dependencies might be STOPPING, - // but if so they are waiting (for us), so they too can be instantly returned to - // STARTING state. - } - - service_state = ServiceState::STARTING; - service_set->service_active(this); - - waiting_for_deps = true; - - // Ask dependencies to start, mark them as being waited on. - if (! startCheckDependencies(true)) { - return; - } - - // Actually start this service. - allDepsStarted(); -} - -void ServiceRecord::dependencyStarted() noexcept -{ - if (service_state != ServiceState::STARTING || ! waiting_for_deps) { - return; - } - - if (startCheckDependencies(false)) { - allDepsStarted(); - } -} - -bool ServiceRecord::startCheckDependencies(bool start_deps) noexcept -{ - bool all_deps_started = true; - - for (sr_iter i = depends_on.begin(); i != depends_on.end(); ++i) { - if ((*i)->service_state != ServiceState::STARTED) { - if (start_deps) { - all_deps_started = false; - (*i)->start(); - } - else { - return false; - } - } - } - - for (auto i = soft_deps.begin(); i != soft_deps.end(); ++i) { - ServiceRecord * to = i->getTo(); - if (start_deps) { - if (to->service_state != ServiceState::STARTED) { - to->start(); - i->waiting_on = true; - all_deps_started = false; - } - else { - i->waiting_on = false; - } - } - else if (i->waiting_on) { - if (to->service_state != ServiceState::STARTING) { - // Service has either started or is no longer starting - i->waiting_on = false; - } - else { - // We are still waiting on this service - return false; - } - } - } - - return all_deps_started; -} - -bool ServiceRecord::open_socket() noexcept -{ - if (socket_path.empty() || socket_fd != -1) { - // No socket, or already open - return true; - } - - const char * saddrname = socket_path.c_str(); - uint sockaddr_size = offsetof(struct sockaddr_un, sun_path) + socket_path.length() + 1; - - struct sockaddr_un * name = static_cast(malloc(sockaddr_size)); - if (name == nullptr) { - log(LogLevel::ERROR, service_name, ": Opening activation socket: out of memory"); - return false; - } - - // Un-link any stale socket. TODO: safety check? should at least confirm the path is a socket. - unlink(saddrname); - - name->sun_family = AF_UNIX; - strcpy(name->sun_path, saddrname); - - int sockfd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); - if (sockfd == -1) { - log(LogLevel::ERROR, service_name, ": Error creating activation socket: ", strerror(errno)); - free(name); - return false; - } - - if (bind(sockfd, (struct sockaddr *) name, sockaddr_size) == -1) { - log(LogLevel::ERROR, service_name, ": Error binding activation socket: ", strerror(errno)); - close(sockfd); - free(name); - return false; - } - - free(name); - - // POSIX (1003.1, 2013) says that fchown and fchmod don't necesarily work on sockets. We have to - // use chown and chmod instead. - if (chown(saddrname, socket_uid, socket_gid)) { - log(LogLevel::ERROR, service_name, ": Error setting activation socket owner/group: ", strerror(errno)); - close(sockfd); - return false; - } - - if (chmod(saddrname, socket_perms) == -1) { - log(LogLevel::ERROR, service_name, ": Error setting activation socket permissions: ", strerror(errno)); - close(sockfd); - return false; - } - - if (listen(sockfd, 128) == -1) { // 128 "seems reasonable". - log(LogLevel::ERROR, ": Error listening on activation socket: ", strerror(errno)); - close(sockfd); - return false; - } - - socket_fd = sockfd; - return true; -} - -void ServiceRecord::allDepsStarted(bool has_console) noexcept -{ - if (onstart_flags.runs_on_console && ! has_console) { - waiting_for_deps = true; - queueForConsole(); - return; - } - - waiting_for_deps = false; - - if (! open_socket()) { - failed_to_start(); - } - - if (service_type == ServiceType::PROCESS || service_type == ServiceType::BGPROCESS - || service_type == ServiceType::SCRIPTED) { - bool start_success = start_ps_process(); - if (! start_success) { - failed_to_start(); - } - } - else { - // "internal" service - started(); - } -} - -void ServiceRecord::acquiredConsole() noexcept -{ - if (service_state != ServiceState::STARTING) { - // We got the console but no longer want it. - releaseConsole(); - } - else if (startCheckDependencies(false)) { - log_to_console = false; - allDepsStarted(true); - } - else { - // We got the console but can't use it yet. - releaseConsole(); - } -} - -bool ServiceRecord::read_pid_file() noexcept -{ - const char *pid_file_c = pid_file.c_str(); - int fd = open(pid_file_c, O_CLOEXEC); - if (fd != -1) { - char pidbuf[21]; // just enought to hold any 64-bit integer - int r = read(fd, pidbuf, 20); - if (r > 0) { - pidbuf[r] = 0; // store nul terminator - pid = std::atoi(pidbuf); - if (kill(pid, 0) == 0) { - ev_child_init(&child_listener, process_child_callback, pid, 0); - child_listener.data = this; - ev_child_start(ev_default_loop(EVFLAG_AUTO), &child_listener); - } - else { - log(LogLevel::ERROR, service_name, ": pid read from pidfile (", pid, ") is not valid"); - pid = -1; - close(fd); - return false; - } - } - close(fd); - return true; - } - else { - log(LogLevel::ERROR, service_name, ": read pid file: ", strerror(errno)); - return false; - } -} - -void ServiceRecord::started() noexcept -{ - if (onstart_flags.runs_on_console && (service_type == ServiceType::SCRIPTED || service_type == ServiceType::BGPROCESS)) { - tcsetpgrp(0, getpgrp()); - releaseConsole(); - } - - if (service_type == ServiceType::BGPROCESS && pid_file.length() != 0) { - if (! read_pid_file()) { - failed_to_start(); - return; - } - } - - logServiceStarted(service_name); - service_state = ServiceState::STARTED; - notifyListeners(ServiceEvent::STARTED); - - if (onstart_flags.rw_ready) { - open_control_socket(ev_default_loop(EVFLAG_AUTO)); - } - - if (force_stop || desired_state == ServiceState::STOPPED) { - // We must now stop. - bool do_restart = (desired_state != ServiceState::STOPPED); - stop(); - if (do_restart) { - start(); - } - return; - } - - // Notify any dependents whose desired state is STARTED: - for (auto i = dependents.begin(); i != dependents.end(); i++) { - (*i)->dependencyStarted(); - } - for (auto i = soft_dpts.begin(); i != soft_dpts.end(); i++) { - (*i)->getFrom()->dependencyStarted(); - } -} - -void ServiceRecord::failed_to_start() -{ - if (onstart_flags.runs_on_console) { - tcsetpgrp(0, getpgrp()); - releaseConsole(); - } - - logServiceFailed(service_name); - service_state = ServiceState::STOPPED; - desired_state = ServiceState::STOPPED; - service_set->service_inactive(this); - notifyListeners(ServiceEvent::FAILEDSTART); - - // failure to start - // Cancel start of dependents: - for (sr_iter i = dependents.begin(); i != dependents.end(); i++) { - if ((*i)->service_state == ServiceState::STARTING) { - (*i)->failed_dependency(); - } - } - for (auto i = soft_dpts.begin(); i != soft_dpts.end(); i++) { - // We can send 'start', because this is only a soft dependency. - // Our startup failure means that they don't have to wait for us. - (*i)->getFrom()->dependencyStarted(); - } -} - -bool ServiceRecord::start_ps_process() noexcept -{ - return start_ps_process(exec_arg_parts, onstart_flags.runs_on_console); -} - -bool ServiceRecord::start_ps_process(const std::vector &cmd, bool on_console) noexcept -{ - // In general, you can't tell whether fork/exec is successful. We use a pipe to communicate - // success/failure from the child to the parent. The pipe is set CLOEXEC so a successful - // exec closes the pipe, and the parent sees EOF. If the exec is unsuccessful, the errno - // is written to the pipe, and the parent can read it. - - int pipefd[2]; - if (pipe2(pipefd, O_CLOEXEC)) { - // TODO log error - return false; - } - - // Set up the argument array and other data now (before fork), in case memory allocation fails. - - auto args = cmd.data(); - - const char * logfile = this->logfile.c_str(); - if (*logfile == 0) { - logfile = "/dev/null"; - } - - // TODO make sure pipefd's are not 0/1/2 (STDIN/OUT/ERR) - if they are, dup them - // until they are not. - - pid_t forkpid = fork(); - if (forkpid == -1) { - // TODO log error - close(pipefd[0]); - close(pipefd[1]); - return false; - } - - // If the console already has a session leader, presumably it is us. On the other hand - // if it has no session leader, and we don't create one, then control inputs such as - // ^C will have no effect. - bool do_set_ctty = (tcgetsid(0) == -1); - - if (forkpid == 0) { - // Child process. Must not allocate memory (or otherwise risk throwing any exception) - // from here until exit(). - ev_default_destroy(); // won't need that on this side, free up fds. - - constexpr int bufsz = ((CHAR_BIT * sizeof(pid_t) - 1) / 3 + 2) + 11; - // "LISTEN_PID=" - 11 characters - char nbuf[bufsz]; - - if (socket_fd != -1) { - dup2(socket_fd, 3); - if (socket_fd != 3) { - close(socket_fd); - } - - if (putenv(const_cast("LISTEN_FDS=1"))) goto failure_out; - - snprintf(nbuf, bufsz, "LISTEN_PID=%jd", static_cast(getpid())); - - if (putenv(nbuf)) goto failure_out; - } - - if (! on_console) { - // Re-set stdin, stdout, stderr - close(0); close(1); close(2); - - // TODO rethink this logic. If we open it at not-0, shouldn't we just dup it to 0?: - if (open("/dev/null", O_RDONLY) == 0) { - // stdin = 0. That's what we should have; proceed with opening - // stdout and stderr. - open(logfile, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR); - dup2(1, 2); - } - } - else { - // "run on console" - run as a foreground job on the terminal/console device - if (do_set_ctty) { - setsid(); - ioctl(0, TIOCSCTTY, 0); - } - setpgid(0,0); - tcsetpgrp(0, getpgrp()); - - // TODO disable suspend (^Z)? (via tcsetattr) - // (should be done before TIOCSCTTY) - } - - execvp(exec_arg_parts[0], const_cast(args)); - - // If we got here, the exec failed: - failure_out: - int exec_status = errno; - write(pipefd[1], &exec_status, sizeof(int)); - exit(0); - } - else { - // Parent process - close(pipefd[1]); // close the 'other end' fd - - pid = forkpid; - - // Listen for status - ev_io_init(&child_status_listener, process_child_status, pipefd[0], EV_READ); - child_status_listener.data = this; - ev_io_start(ev_default_loop(EVFLAG_AUTO), &child_status_listener); - - // Add a process listener so we can detect when the - // service stops - ev_child_init(&child_listener, process_child_callback, pid, 0); - child_listener.data = this; - ev_child_start(ev_default_loop(EVFLAG_AUTO), &child_listener); - waiting_for_execstat = true; - return true; - } -} - -// Mark this and all dependent services as force-stopped. -void ServiceRecord::forceStop() noexcept -{ - if (service_state != ServiceState::STOPPED) { - force_stop = true; - for (sr_iter i = dependents.begin(); i != dependents.end(); i++) { - (*i)->forceStop(); - } - stop(); - - // We don't want to force stop soft dependencies, however. - } -} - -// A dependency of this service failed to start. -// Only called when state == STARTING. -void ServiceRecord::failed_dependency() -{ - desired_state = ServiceState::STOPPED; - - // Presumably, we were starting. So now we're not. - service_state = ServiceState::STOPPED; - service_set->service_inactive(this); - logServiceFailed(service_name); - - // Notify dependents of this service also - for (auto i = dependents.begin(); i != dependents.end(); i++) { - if ((*i)->service_state == ServiceState::STARTING) { - (*i)->failed_dependency(); - } - } - for (auto i = soft_dpts.begin(); i != soft_dpts.end(); i++) { - // It's a soft dependency, so send them 'started' rather than - // 'failed dep'. - (*i)->getFrom()->dependencyStarted(); - } -} - -void ServiceRecord::dependentStopped() noexcept -{ - if (service_state == ServiceState::STOPPING) { - // Check the other dependents before we stop. - if (stopCheckDependents()) { - allDepsStopped(); - } - } -} - -void ServiceRecord::stop() noexcept -{ - if ((service_state == ServiceState::STOPPING || service_state == ServiceState::STOPPED) - && desired_state == ServiceState::STARTED) { - // The service *was* stopped/stopping, but it was going to restart. - // Now, we'll cancel the restart. - notifyListeners(ServiceEvent::STARTCANCELLED); - } - - if (desired_state == ServiceState::STOPPED && service_state != ServiceState::STARTED) return; - - desired_state = ServiceState::STOPPED; - - if (pinned_started) return; - - if (service_state != ServiceState::STARTED) { - if (service_state == ServiceState::STARTING) { - if (! can_interrupt_start()) { - // Well this is awkward: we're going to have to continue - // starting, but we don't want any dependents to think that - // they are still waiting to start. - // Make sure they remain stopped: - stopDependents(); - return; - } - - // Reaching this point, we have can_interrupt_start() == true. So, - // we can stop. Dependents might be starting, but they must be - // waiting on us, so they should also be immediately stoppable. - // Fall through to below. - } - else { - // If we're starting we need to wait for that to complete. - // If we're already stopping/stopped there's nothing to do. - return; - } - } - - service_state = ServiceState::STOPPING; - waiting_for_deps = true; - - // If we get here, we are in STARTED state; stop all dependents. - if (stopDependents()) { - allDepsStopped(); - } -} - -bool ServiceRecord::stopCheckDependents() noexcept -{ - bool all_deps_stopped = true; - for (sr_iter i = dependents.begin(); i != dependents.end(); ++i) { - if ((*i)->service_state != ServiceState::STOPPED) { - all_deps_stopped = false; - break; - } - } - - return all_deps_stopped; -} - -bool ServiceRecord::stopDependents() noexcept -{ - bool all_deps_stopped = true; - for (sr_iter i = dependents.begin(); i != dependents.end(); ++i) { - if ((*i)->service_state != ServiceState::STOPPED) { - all_deps_stopped = false; - (*i)->stop(); - } - } - - return all_deps_stopped; -} - -// Dependency stopped or is stopping; we must stop too. -void ServiceRecord::allDepsStopped() -{ - waiting_for_deps = false; - if (service_type == ServiceType::PROCESS || service_type == ServiceType::BGPROCESS) { - if (pid != -1) { - // The process is still kicking on - must actually kill it. - if (! onstart_flags.no_sigterm) { - kill(pid, SIGTERM); - } - if (term_signal != -1) { - kill(pid, term_signal); - } - // Now we wait; the rest is done in process_child_callback - } - else { - // The process is already dead. - stopped(); - } - } - else if (service_type == ServiceType::SCRIPTED) { - // Scripted service. - if (stop_command.length() == 0) { - stopped(); - } - else if (! start_ps_process(stop_arg_parts, false)) { - // Couldn't execute stop script, but there's not much we can do: - stopped(); - } - } - else { - stopped(); - } -} - -void ServiceRecord::pinStart() noexcept -{ - start(); - pinned_started = true; -} - -void ServiceRecord::pinStop() noexcept -{ - stop(); - pinned_stopped = true; -} - -void ServiceRecord::unpin() noexcept -{ - if (pinned_started) { - pinned_started = false; - if (desired_state == ServiceState::STOPPED) { - stop(); - } - } - if (pinned_stopped) { - pinned_stopped = false; - if (desired_state == ServiceState::STARTED) { - start(); - } - } -} - -void ServiceRecord::queueForConsole() noexcept -{ - next_for_console = nullptr; - auto tail = service_set->consoleQueueTail(this); - if (tail == nullptr) { - acquiredConsole(); - } - else { - tail->next_for_console = this; - } -} - -void ServiceRecord::releaseConsole() noexcept -{ - log_to_console = true; - if (next_for_console != nullptr) { - next_for_console->acquiredConsole(); - } - else { - service_set->consoleQueueTail(nullptr); - } -} - -void ServiceSet::service_active(ServiceRecord *sr) noexcept -{ - active_services++; -} - -void ServiceSet::service_inactive(ServiceRecord *sr) noexcept -{ - active_services--; -} diff --git a/service.h b/service.h deleted file mode 100644 index cbeab72..0000000 --- a/service.h +++ /dev/null @@ -1,547 +0,0 @@ -#ifndef SERVICE_H -#define SERVICE_H - -#include -#include -#include -#include -#include -#include "ev.h" -#include "control.h" -#include "service-listener.h" -#include "service-constants.h" - -/* - * Possible 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 - * from leaving that state until it is unpinned. - * - * The total state is a combination of the two, current and desired: - * STOPPED/STOPPED : stopped and will remain stopped - * STOPPED/STARTED : stopped (pinned), must be unpinned to start - * STARTING/STARTED : starting, but not yet started. Dependencies may also be starting. - * STARTING/STOPPED : as above, but the service will be stopped again as soon as it has - * completed startup. - * STARTED/STARTED : running and will continue running. - * STARTED/STOPPED : started (pinned), must be unpinned to stop - * STOPPING/STOPPED : stopping and will stop. Dependents may be stopping. - * STOPPING/STARTED : as above, but the service will be re-started again once it stops. - * - * A scripted service is in the STARTING/STOPPING states during the script execution. - * 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. - */ - -struct OnstartFlags { - bool rw_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), - no_sigterm(false), runs_on_console(false) - { - } -}; - -// Exception while loading a service -class ServiceLoadExc -{ - public: - std::string serviceName; - const char *excDescription; - - protected: - ServiceLoadExc(std::string serviceName) noexcept - : serviceName(serviceName) - { - } -}; - -class ServiceNotFound : public ServiceLoadExc -{ - public: - ServiceNotFound(std::string serviceName) noexcept - : ServiceLoadExc(serviceName) - { - excDescription = "Service description not found."; - } -}; - -class ServiceCyclicDependency : public ServiceLoadExc -{ - public: - ServiceCyclicDependency(std::string serviceName) noexcept - : ServiceLoadExc(serviceName) - { - excDescription = "Has cyclic dependency."; - } -}; - -class ServiceDescriptionExc : public ServiceLoadExc -{ - public: - std::string extraInfo; - - ServiceDescriptionExc(std::string serviceName, std::string extraInfo) noexcept - : ServiceLoadExc(serviceName), extraInfo(extraInfo) - { - excDescription = extraInfo.c_str(); - } -}; - -class ServiceRecord; // forward declaration -class ServiceSet; // forward declaration - -/* Service dependency record */ -class ServiceDep -{ - ServiceRecord * from; - ServiceRecord * to; - - public: - /* Whether the 'from' service is waiting for the 'to' service to start */ - bool waiting_on; - - ServiceDep(ServiceRecord * from, ServiceRecord * to) noexcept : from(from), to(to), waiting_on(false) - { } - - ServiceRecord * getFrom() noexcept - { - return from; - } - - ServiceRecord * getTo() noexcept - { - return to; - } -}; - -// Given a string and a list of pairs of (start,end) indices for each argument in that string, -// store a null terminator for the argument. Return a `char *` vector containing the beginning -// of each argument and a trailing nullptr. (The returned array is invalidated if the string is later modified). -static std::vector separate_args(std::string &s, std::list> &arg_indices) -{ - std::vector r; - r.reserve(arg_indices.size() + 1); - - // First store nul terminator for each part: - for (auto index_pair : arg_indices) { - if (index_pair.second < s.length()) { - s[index_pair.second] = 0; - } - } - - // Now we can get the C string (c_str) and store offsets into it: - const char * cstr = s.c_str(); - for (auto index_pair : arg_indices) { - r.push_back(cstr + index_pair.first); - } - r.push_back(nullptr); - return r; -} - - -class ServiceRecord -{ - typedef std::string string; - - string service_name; - ServiceType service_type; /* ServiceType::DUMMY, PROCESS, SCRIPTED, INTERNAL */ - ServiceState service_state = ServiceState::STOPPED; /* ServiceState::STOPPED, STARTING, STARTED, STOPPING */ - ServiceState desired_state = ServiceState::STOPPED; /* ServiceState::STOPPED / STARTED */ - - string program_name; /* storage for program/script and arguments */ - std::vector exec_arg_parts; /* pointer to each argument/part of the program_name */ - - string stop_command; /* storage for stop program/script and arguments */ - std::vector stop_arg_parts; /* pointer to each argument/part of the stop_command */ - - string pid_file; - - OnstartFlags onstart_flags; - - string logfile; // log file name, empty string specifies /dev/null - bool auto_restart : 1; // whether to restart this (process) if it dies unexpectedly - bool smooth_recovery : 1; // whether the service process can restart without bringing down service - - bool pinned_stopped : 1; - bool pinned_started : 1; - bool waiting_for_deps : 1; // if STARTING, whether we are waiting for dependencies (inc console) to start - bool waiting_for_execstat : 1; // if we are waiting for exec status after fork() - bool doing_recovery : 1; // if we are currently recovering a BGPROCESS (restarting process, while - // holding STARTED service state) - - typedef std::list sr_list; - typedef sr_list::iterator sr_iter; - - // list of soft dependencies - typedef std::list softdep_list; - - // list of soft dependents - typedef std::list softdpt_list; - - sr_list depends_on; // services this one depends on - sr_list dependents; // services depending on this one - softdep_list soft_deps; // services this one depends on via a soft dependency - softdpt_list soft_dpts; // services depending on this one via a soft dependency - - // unsigned wait_count; /* if we are waiting for dependents/dependencies to - // start/stop, this is how many we're waiting for */ - - 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 listeners; - - // Process services: - bool force_stop; // true if the service must actually stop. This is the - // case if for example the process dies; the service, - // and all its dependencies, MUST be stopped. - - int term_signal = -1; // signal to use for process termination - - string socket_path; // path to the socket for socket-activation service - int socket_perms; // socket permissions ("mode") - uid_t socket_uid = -1; // socket user id or -1 - gid_t socket_gid = -1; // sockget group id or -1 - - // Implementation details - - pid_t pid = -1; // PID of the process. If state is STARTING or STOPPING, - // this is PID of the service script; otherwise it is the - // PID of the process itself (process service). - 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; - - // All dependents have stopped. - void allDepsStopped(); - - // Service has actually stopped (includes having all dependents - // reaching STOPPED state). - void stopped() noexcept; - - // Service has successfully started - void started() noexcept; - - // Service failed to start - void failed_to_start(); - - // A dependency of this service failed to start. - void failed_dependency(); - - // For process services, start the process, return true on success - bool start_ps_process() noexcept; - bool start_ps_process(const std::vector &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, - int revents) noexcept; - - static void process_child_status(struct ev_loop *loop, ev_io * stat_io, - int revents) noexcept; - - void handle_exit_status() noexcept; - - // A dependency has reached STARTED state - void dependencyStarted() noexcept; - - void allDepsStarted(bool haveConsole = false) noexcept; - - // Read the pid-file, return false on failure - bool read_pid_file() noexcept; - - // Open the activation socket, return false on failure - bool open_socket() noexcept; - - // Check whether dependencies have started, and optionally ask them to start - bool startCheckDependencies(bool do_start) noexcept; - - // Whether a STARTING service can immediately transition to STOPPED (as opposed to - // having to wait for it reach STARTED and then go through STOPPING). - bool can_interrupt_start() noexcept - { - return waiting_for_deps; - } - - // Whether a STOPPING service can immediately transition to STARTED. - bool can_interrupt_stop() noexcept - { - return waiting_for_deps && ! force_stop; - } - - // A dependent has reached STOPPED state - void dependentStopped() noexcept; - - // check if all dependents have stopped - bool stopCheckDependents() 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 notifyListeners(ServiceEvent event) noexcept - { - for (auto l : listeners) { - l->serviceEvent(this, event); - } - } - - // 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; - - public: - - ServiceRecord(ServiceSet *set, string name) - : 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), force_stop(false) - { - service_set = set; - service_name = name; - service_type = ServiceType::DUMMY; - } - - ServiceRecord(ServiceSet *set, string name, ServiceType service_type, string &&command, std::list> &command_offsets, - sr_list * pdepends_on, sr_list * pdepends_soft) - : ServiceRecord(set, name) - { - service_set = set; - service_name = name; - this->service_type = service_type; - this->depends_on = std::move(*pdepends_on); - - program_name = command; - exec_arg_parts = separate_args(program_name, command_offsets); - - for (sr_iter i = depends_on.begin(); i != depends_on.end(); ++i) { - (*i)->dependents.push_back(this); - } - - // Soft dependencies - auto b_iter = soft_deps.end(); - for (sr_iter i = pdepends_soft->begin(); i != pdepends_soft->end(); ++i) { - b_iter = soft_deps.emplace(b_iter, this, *i); - (*i)->soft_dpts.push_back(&(*b_iter)); - ++b_iter; - } - } - - // TODO write a destructor - - // Set the stop command and arguments (may throw std::bad_alloc) - void setStopCommand(std::string command, std::list> &stop_command_offsets) - { - stop_command = command; - stop_arg_parts = separate_args(stop_command, stop_command_offsets); - } - - // Get the current service state. - ServiceState getState() noexcept - { - return service_state; - } - - // Get the target (aka desired) state. - ServiceState getTargetState() noexcept - { - return desired_state; - } - - // Set logfile, should be done before service is started - void setLogfile(string logfile) - { - this->logfile = logfile; - } - - // Set whether this service should automatically restart when it dies - void setAutoRestart(bool auto_restart) noexcept - { - this->auto_restart = auto_restart; - } - - void setSmoothRecovery(bool smooth_recovery) noexcept - { - this->smooth_recovery = smooth_recovery; - } - - // Set "on start" flags (commands) - void setOnstartFlags(OnstartFlags flags) noexcept - { - this->onstart_flags = flags; - } - - // Set an additional signal (other than SIGTERM) to be used to terminate the process - void setExtraTerminationSignal(int signo) noexcept - { - this->term_signal = signo; - } - - void set_pid_file(string &&pid_file) noexcept - { - this->pid_file = pid_file; - } - - void set_socket_details(string &&socket_path, int socket_perms, uid_t socket_uid, uid_t socket_gid) noexcept - { - this->socket_path = socket_path; - this->socket_perms = socket_perms; - this->socket_uid = socket_uid; - this->socket_gid = socket_gid; - } - - const char *getServiceName() const noexcept { return service_name.c_str(); } - ServiceState getState() const noexcept { return service_state; } - - void start() noexcept; // start the service - void stop() noexcept; // stop the service - - void pinStart() noexcept; // start the service and pin it - void pinStop() noexcept; // stop the service and pin it - void unpin() noexcept; // unpin the service - - bool isDummy() noexcept - { - return service_type == ServiceType::DUMMY; - } - - // Add a listener. A listener must only be added once. May throw std::bad_alloc. - void addListener(ServiceListener * listener) - { - listeners.insert(listener); - } - - // Remove a listener. - void removeListener(ServiceListener * listener) noexcept - { - listeners.erase(listener); - } -}; - - -class ServiceSet -{ - int active_services; - std::list records; - const char *service_dir; // directory containing service descriptions - bool restart_enabled; // whether automatic restart is enabled (allowed) - - ShutdownType shutdown_type = ShutdownType::CONTINUE; // Shutdown type, if stopping - - ServiceRecord * console_queue_tail = nullptr; // last record in console queue - - // Private methods - - // Load a service description, and dependencies, if there is no existing - // record for the given name. - // Throws: - // ServiceLoadException (or subclass) on problem with service description - // std::bad_alloc on out-of-memory condition - ServiceRecord *loadServiceRecord(const char *name); - - // Public - - public: - ServiceSet(const char *service_dir) - { - this->service_dir = service_dir; - active_services = 0; - restart_enabled = true; - } - - // Start the service with the given name. The named service will begin - // transition to the 'started' state. - // - // Throws a ServiceLoadException (or subclass) if the service description - // cannot be loaded or is invalid; - // Throws std::bad_alloc if out of memory. - void startService(const char *name); - - // Locate an existing service record. - ServiceRecord *findService(const std::string &name) noexcept; - - // Find a loaded service record, or load it if it is not loaded. - // Throws: - // ServiceLoadException (or subclass) on problem with service description - // std::bad_alloc on out-of-memory condition - ServiceRecord *loadService(const std::string &name) - { - ServiceRecord *record = findService(name); - if (record == nullptr) { - record = loadServiceRecord(name.c_str()); - } - return record; - } - - // Stop the service with the given name. The named service will begin - // transition to the 'stopped' state. - void stopService(const std::string &name) noexcept; - - // Set the console queue tail (returns previous tail) - ServiceRecord * consoleQueueTail(ServiceRecord * newTail) noexcept - { - auto prev_tail = console_queue_tail; - console_queue_tail = newTail; - return prev_tail; - } - - // 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; - - // Notification from service that it is inactive (STOPPED) - // Only to be called on the transition from active to inactive. - void service_inactive(ServiceRecord *) noexcept; - - // Find out how many services are active (starting, running or stopping, - // but not stopped). - int count_active_services() noexcept - { - return active_services; - } - - void stop_all_services(ShutdownType type = ShutdownType::HALT) noexcept - { - restart_enabled = false; - shutdown_type = type; - for (std::list::iterator i = records.begin(); i != records.end(); ++i) { - (*i)->stop(); - (*i)->unpin(); - } - } - - void set_auto_restart(bool restart) noexcept - { - restart_enabled = restart; - } - - bool get_auto_restart() noexcept - { - return restart_enabled; - } - - ShutdownType getShutdownType() noexcept - { - return shutdown_type; - } -}; - -#endif diff --git a/shutdown.cc b/shutdown.cc deleted file mode 100644 index 1d46ab2..0000000 --- a/shutdown.cc +++ /dev/null @@ -1,197 +0,0 @@ -// #include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "control-cmds.h" -#include "service-constants.h" - -// shutdown: shut down the system -// This utility communicates with the dinit daemon via a unix socket (/dev/initctl). - -void do_system_shutdown(ShutdownType shutdown_type); -static void unmount_disks(); -static void swap_off(); - -int main(int argc, char **argv) -{ - using namespace std; - - bool show_help = false; - bool sys_shutdown = false; - - auto shutdown_type = ShutdownType::POWEROFF; - - for (int i = 1; i < argc; i++) { - if (argv[i][0] == '-') { - if (strcmp(argv[i], "--help") == 0) { - show_help = true; - break; - } - - if (strcmp(argv[i], "--system") == 0) { - sys_shutdown = true; - } - else if (strcmp(argv[i], "-r") == 0) { - shutdown_type = ShutdownType::REBOOT; - } - else if (strcmp(argv[i], "-h") == 0) { - shutdown_type = ShutdownType::HALT; - } - else if (strcmp(argv[i], "-p") == 0) { - shutdown_type = ShutdownType::POWEROFF; - } - else { - cerr << "Unrecognized command-line parameter: " << argv[i] << endl; - return 1; - } - } - else { - // time argument? TODO - show_help = true; - } - } - - if (show_help) { - cout << "dinit-shutdown : shutdown the system" << endl; - cout << " --help : show this help" << endl; - cout << " -r : reboot" << endl; - cout << " -h : halt system" << endl; - cout << " -p : power down (default)" << endl; - cout << " --system : perform shutdown immediately, instead of issuing shutdown" << endl; - cout << " command to the init program. Not recommended for use" << endl; - cout << " by users." << endl; - return 1; - } - - if (sys_shutdown) { - do_system_shutdown(shutdown_type); - return 0; - } - - int socknum = socket(AF_UNIX, SOCK_STREAM, 0); - if (socknum == -1) { - perror("socket"); - return 1; - } - - const char *naddr = "/dev/dinitctl"; - - struct sockaddr_un name; - name.sun_family = AF_UNIX; - strcpy(name.sun_path, naddr); - int sunlen = offsetof(struct sockaddr_un, sun_path) + strlen(naddr) + 1; // family, (string), nul - - int connr = connect(socknum, (struct sockaddr *) &name, sunlen); - if (connr == -1) { - perror("connect"); - return 1; - } - - // Build buffer; - //uint16_t sname_len = strlen(service_name); - int bufsize = 2; - char * buf = new char[bufsize]; - - buf[0] = DINIT_CP_SHUTDOWN; - buf[1] = static_cast(shutdown_type); - - cout << "Issuing shutdown command..." << endl; // DAV - - // TODO make sure to write the whole buffer - int r = write(socknum, buf, bufsize); - if (r == -1) { - perror("write"); - } - - // Wait for ACK/NACK - r = read(socknum, buf, 1); - // TODO: check result - - return 0; -} - -void do_system_shutdown(ShutdownType shutdown_type) -{ - using namespace std; - - int reboot_type = 0; - if (shutdown_type == ShutdownType::REBOOT) reboot_type = RB_AUTOBOOT; - else if (shutdown_type == ShutdownType::POWEROFF) reboot_type = RB_POWER_OFF; - else reboot_type = RB_HALT_SYSTEM; - - // Write to console rather than any terminal, since we lose the terminal it seems: - close(STDOUT_FILENO); - int consfd = open("/dev/console", O_WRONLY); - if (consfd != STDOUT_FILENO) { - dup2(consfd, STDOUT_FILENO); - } - - cout << "Sending TERM/KILL to all processes..." << endl; // DAV - - // Send TERM/KILL to all (remaining) processes - kill(-1, SIGTERM); - sleep(1); - kill(-1, SIGKILL); - - // cout << "Sending QUIT to init..." << endl; // DAV - - // Tell init to exec reboot: - // TODO what if it's not PID=1? probably should have dinit pass us its PID - // kill(1, SIGQUIT); - - // TODO can we wait somehow for above to work? - // maybe have a pipe/socket and we read from our end... - - // TODO: close all ancillary file descriptors. - - // perform shutdown - cout << "Turning off swap..." << endl; - swap_off(); - cout << "Unmounting disks..." << endl; - unmount_disks(); - sync(); - - cout << "Issuing shutdown via kernel..." << endl; - reboot(reboot_type); -} - -static void unmount_disks() -{ - pid_t chpid = fork(); - if (chpid == 0) { - // umount -a -r - // -a : all filesystems (except proc) - // -r : mount readonly if can't unmount - execl("/bin/umount", "/bin/umount", "-a", "-r", nullptr); - } - else if (chpid > 0) { - int status; - waitpid(chpid, &status, 0); - } -} - -static void swap_off() -{ - pid_t chpid = fork(); - if (chpid == 0) { - // swapoff -a - execl("/sbin/swapoff", "/sbin/swapoff", "-a", nullptr); - } - else if (chpid > 0) { - int status; - waitpid(chpid, &status, 0); - } -} diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..24bb78d --- /dev/null +++ b/src/Makefile @@ -0,0 +1,32 @@ +-include ../mconfig + +objects = dinit.o load_service.o service.o control.o dinit-log.o dinit-start.o shutdown.o dinit-reboot.o + +dinit_objects = dinit.o load_service.o service.o control.o dinit-log.o + +all: dinit dinit-start + +shutdown-utils: shutdown + +dinit: $(dinit_objects) + $(CXX) -o dinit $(dinit_objects) -lev $(EXTRA_LIBS) + +dinit-start: dinit-start.o + $(CXX) -o dinit-start dinit-start.o $(EXTRA_LIBS) + +shutdown: shutdown.o + $(CXX) -o shutdown shutdown.o + +dinit-reboot: dinit-reboot.o + $(CXX) -o dinit-reboot dinit-reboot.o + +$(objects): %.o: %.cc service.h dinit-log.h control.h control-cmds.h + $(CXX) $(CXXOPTS) -c $< -o $@ + +#install: all + +#install.man: + +clean: + rm *.o + rm dinit diff --git a/src/control-cmds.h b/src/control-cmds.h new file mode 100644 index 0000000..263f61a --- /dev/null +++ b/src/control-cmds.h @@ -0,0 +1,61 @@ +// Dinit control command packet types + +// Requests: + +// Query protocol version: +constexpr static int DINIT_CP_QUERYVERSION = 0; + +// Find (but don't load) a service: +constexpr static int DINIT_CP_FINDSERVICE = 1; + +// Find or load a service: +constexpr static int DINIT_CP_LOADSERVICE = 2; + +// Start or stop a service: +constexpr static int DINIT_CP_STARTSERVICE = 3; +constexpr static int DINIT_CP_STOPSERVICE = 4; + +// Shutdown: +constexpr static int DINIT_CP_SHUTDOWN = 5; + // followed by 1-byte shutdown type + + + +// Replies: + +// Reply: ACK/NAK to request +constexpr static int DINIT_RP_ACK = 50; +constexpr static int DINIT_RP_NAK = 51; + +// Request was bad (connection will be closed) +constexpr static int DINIT_RP_BADREQ = 52; + +// Connection being closed due to out-of-memory condition +constexpr static int DINIT_RP_OOM = 53; + +// Start service replies: +constexpr static int DINIT_RP_SERVICELOADERR = 54; +constexpr static int DINIT_RP_SERVICEOOM = 55; // couldn't start due to out-of-memory + +constexpr static int DINIT_RP_SSISSUED = 56; // service start/stop was issued (includes 4-byte service handle) +constexpr static int DINIT_RP_SSREDUNDANT = 57; // service was already started/stopped (or for stop, not loaded) + +// Query version response: +constexpr static int DINIT_RP_CPVERSION = 58; + +// Service record loaded/found +constexpr static int DINIT_RP_SERVICERECORD = 59; +// followed by 4-byte service handle, 1-byte service state + +// Couldn't find/load service +constexpr static int DINIT_RP_NOSERVICE = 60; + + + +// Information: + +// Service event occurred (4-byte service handle, 1 byte event code) +constexpr static int DINIT_IP_SERVICEEVENT = 100; + +// rollback completed +constexpr static int DINIT_ROLLBACK_COMPLETED = 101; diff --git a/src/control.cc b/src/control.cc new file mode 100644 index 0000000..22f4378 --- /dev/null +++ b/src/control.cc @@ -0,0 +1,419 @@ +#include "control.h" +#include "service.h" + +void ControlConn::processPacket() +{ + using std::string; + + // Note that where we call queuePacket, we must generally check the return value. If it + // returns false it has either deleted the connection or marked it for deletion; we + // shouldn't touch instance members after that point. + + int pktType = rbuf[0]; + if (pktType == DINIT_CP_QUERYVERSION) { + // Responds with: + // DINIT_RP_CVERSION, (2 byte) minimum compatible version, (2 byte) maximum compatible version + char replyBuf[] = { DINIT_RP_CPVERSION, 0, 0, 0, 0 }; + if (! queuePacket(replyBuf, 1)) return; + rbuf.consume(1); + return; + } + if (pktType == DINIT_CP_FINDSERVICE || pktType == DINIT_CP_LOADSERVICE) { + processFindLoad(pktType); + return; + } + if (pktType == DINIT_CP_STARTSERVICE || pktType == DINIT_CP_STOPSERVICE) { + processStartStop(pktType); + return; + } + else if (pktType == DINIT_CP_SHUTDOWN) { + // Shutdown/reboot + if (rbuf.get_length() < 2) { + chklen = 2; + return; + } + + auto sd_type = static_cast(rbuf[1]); + + service_set->stop_all_services(sd_type); + log_to_console = true; + char ackBuf[] = { DINIT_RP_ACK }; + if (! queuePacket(ackBuf, 1)) return; + + // Clear the packet from the buffer + rbuf.consume(2); + chklen = 0; + return; + } + else { + // Unrecognized: give error response + char outbuf[] = { DINIT_RP_BADREQ }; + if (! queuePacket(outbuf, 1)) return; + bad_conn_close = true; + ev_io_set(&iob, iob.fd, EV_WRITE); + } + return; +} + +void ControlConn::processFindLoad(int pktType) +{ + using std::string; + + constexpr int pkt_size = 4; + + if (rbuf.get_length() < pkt_size) { + chklen = pkt_size; + return; + } + + uint16_t svcSize; + rbuf.extract((char *)&svcSize, 1, 2); + chklen = svcSize + 3; // packet type + (2 byte) length + service name + if (svcSize <= 0 || chklen > 1024) { + // Queue error response / mark connection bad + char badreqRep[] = { DINIT_RP_BADREQ }; + if (! queuePacket(badreqRep, 1)) return; + bad_conn_close = true; + ev_io_set(&iob, iob.fd, EV_WRITE); + return; + } + + if (rbuf.get_length() < chklen) { + // packet not complete yet; read more + return; + } + + ServiceRecord * record = nullptr; + + string serviceName = std::move(rbuf.extract_string(3, svcSize)); + + if (pktType == DINIT_CP_LOADSERVICE) { + // LOADSERVICE + try { + record = service_set->loadService(serviceName); + } + catch (ServiceLoadExc &slexc) { + log(LogLevel::ERROR, "Could not load service ", slexc.serviceName, ": ", slexc.excDescription); + } + } + else { + // FINDSERVICE + record = service_set->findService(serviceName.c_str()); + } + + if (record != nullptr) { + // Allocate a service handle + handle_t handle = allocateServiceHandle(record); + std::vector rp_buf; + rp_buf.reserve(7); + rp_buf.push_back(DINIT_RP_SERVICERECORD); + rp_buf.push_back(static_cast(record->getState())); + for (int i = 0; i < (int) sizeof(handle); i++) { + rp_buf.push_back(*(((char *) &handle) + i)); + } + rp_buf.push_back(static_cast(record->getTargetState())); + if (! queuePacket(std::move(rp_buf))) return; + } + else { + std::vector rp_buf = { DINIT_RP_NOSERVICE }; + if (! queuePacket(std::move(rp_buf))) return; + } + + // Clear the packet from the buffer + rbuf.consume(chklen); + chklen = 0; + return; +} + +void ControlConn::processStartStop(int pktType) +{ + using std::string; + + constexpr int pkt_size = 2 + sizeof(handle_t); + + if (rbuf.get_length() < pkt_size) { + chklen = pkt_size; + return; + } + + // 1 byte: packet type + // 1 byte: pin in requested state (0 = no pin, 1 = pin) + // 4 bytes: service handle + + bool do_pin = (rbuf[1] == 1); + handle_t handle; + rbuf.extract((char *) &handle, 2, sizeof(handle)); + + ServiceRecord *service = findServiceForKey(handle); + if (service == nullptr) { + // Service handle is bad + char badreqRep[] = { DINIT_RP_BADREQ }; + if (! queuePacket(badreqRep, 1)) return; + bad_conn_close = true; + ev_io_set(&iob, iob.fd, EV_WRITE); + return; + } + else { + if (pktType == DINIT_CP_STARTSERVICE) { + if (do_pin) { + service->pinStart(); + } + else { + service->start(); + } + } + else { + if (do_pin) { + service->pinStop(); + } + else { + service->stop(); + } + } + + char ack_buf[] = { DINIT_RP_ACK }; + if (! queuePacket(ack_buf, 1)) return; + } + + // Clear the packet from the buffer + rbuf.consume(pkt_size); + chklen = 0; + return; +} + +ControlConn::handle_t ControlConn::allocateServiceHandle(ServiceRecord *record) +{ + bool is_unique = true; + handle_t largest_seen = 0; + handle_t candidate = 0; + for (auto p : keyServiceMap) { + if (p.first > largest_seen) largest_seen = p.first; + if (p.first == candidate) { + if (largest_seen == std::numeric_limits::max()) throw std::bad_alloc(); + candidate = largest_seen + 1; + } + is_unique &= (p.second != record); + } + + keyServiceMap[candidate] = record; + serviceKeyMap.insert(std::make_pair(record, candidate)); + + if (is_unique) { + record->addListener(this); + } + + return candidate; +} + + +bool ControlConn::queuePacket(const char *pkt, unsigned size) noexcept +{ + if (bad_conn_close) return false; + + bool was_empty = outbuf.empty(); + + if (was_empty) { + int wr = write(iob.fd, pkt, size); + if (wr == -1) { + if (errno == EPIPE) { + delete this; + return false; + } + if (errno != EAGAIN && errno != EWOULDBLOCK) { + // TODO log error + delete this; + return false; + } + } + else { + if ((unsigned)wr == size) { + // Ok, all written. + return true; + } + pkt += wr; + size -= wr; + } + ev_io_set(&iob, iob.fd, EV_READ | EV_WRITE); + } + + // Create a vector out of the (remaining part of the) packet: + try { + outbuf.emplace_back(pkt, pkt + size); + return true; + } + catch (std::bad_alloc &baexc) { + // Mark the connection bad, and stop reading further requests + bad_conn_close = true; + oom_close = true; + if (was_empty) { + // We can't send out-of-memory response as we already wrote as much as we + // could above. Neither can we later send the response since we have currently + // sent an incomplete packet. All we can do is close the connection. + delete this; + } + else { + ev_io_set(&iob, iob.fd, EV_WRITE); + } + return false; + } +} + + +bool ControlConn::queuePacket(std::vector &&pkt) noexcept +{ + if (bad_conn_close) return false; + + bool was_empty = outbuf.empty(); + + if (was_empty) { + outpkt_index = 0; + // We can try sending the packet immediately: + int wr = write(iob.fd, pkt.data(), pkt.size()); + if (wr == -1) { + if (errno == EPIPE) { + delete this; + return false; + } + if (errno != EAGAIN && errno != EWOULDBLOCK) { + // TODO log error + delete this; + return false; + } + } + else { + if ((unsigned)wr == pkt.size()) { + // Ok, all written. + return true; + } + outpkt_index = wr; + } + ev_io_set(&iob, iob.fd, EV_READ | EV_WRITE); + } + + try { + outbuf.emplace_back(pkt); + return true; + } + catch (std::bad_alloc &baexc) { + // Mark the connection bad, and stop reading further requests + bad_conn_close = true; + oom_close = true; + if (was_empty) { + // We can't send out-of-memory response as we already wrote as much as we + // could above. Neither can we later send the response since we have currently + // sent an incomplete packet. All we can do is close the connection. + delete this; + } + else { + ev_io_set(&iob, iob.fd, EV_WRITE); + } + return false; + } +} + +bool ControlConn::rollbackComplete() noexcept +{ + char ackBuf[2] = { DINIT_ROLLBACK_COMPLETED, 2 }; + return queuePacket(ackBuf, 2); +} + +bool ControlConn::dataReady() noexcept +{ + int fd = iob.fd; + + int r = rbuf.fill(fd); + + // Note file descriptor is non-blocking + if (r == -1) { + if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) { + // TODO log error + delete this; + return true; + } + return false; + } + + if (r == 0) { + delete this; + return true; + } + + // complete packet? + if (rbuf.get_length() >= chklen) { + try { + processPacket(); + } + catch (std::bad_alloc &baexc) { + doOomClose(); + } + } + + if (rbuf.get_length() == 1024) { + // Too big packet + // TODO log error? + // TODO error response? + bad_conn_close = true; + ev_io_set(&iob, iob.fd, EV_WRITE); + } + + return false; +} + +void ControlConn::sendData() noexcept +{ + if (outbuf.empty() && bad_conn_close) { + if (oom_close) { + // Send oom response + char oomBuf[] = { DINIT_RP_OOM }; + write(iob.fd, oomBuf, 1); + } + delete this; + return; + } + + vector & pkt = outbuf.front(); + char *data = pkt.data(); + int written = write(iob.fd, data + outpkt_index, pkt.size() - outpkt_index); + if (written == -1) { + if (errno == EPIPE) { + // read end closed + delete this; + } + else if (errno == EAGAIN || errno == EWOULDBLOCK) { + // spurious readiness notification? + } + else { + log(LogLevel::ERROR, "Error writing to control connection: ", strerror(errno)); + delete this; + } + return; + } + + outpkt_index += written; + if (outpkt_index == pkt.size()) { + // We've finished this packet, move on to the next: + outbuf.pop_front(); + outpkt_index = 0; + if (outbuf.empty() && ! oom_close) { + if (! bad_conn_close) { + ev_io_set(&iob, iob.fd, EV_READ); + } + else { + delete this; + } + } + } +} + +ControlConn::~ControlConn() noexcept +{ + close(iob.fd); + ev_io_stop(loop, &iob); + + // Clear service listeners + for (auto p : serviceKeyMap) { + p.first->removeListener(this); + } + + active_control_conns--; +} diff --git a/src/control.h b/src/control.h new file mode 100644 index 0000000..0bce51c --- /dev/null +++ b/src/control.h @@ -0,0 +1,184 @@ +#ifndef DINIT_CONTROL_H +#define DINIT_CONTROL_H + +#include +#include +#include +#include + +#include +#include + +#include "dinit-log.h" +#include "control-cmds.h" +#include "service-listener.h" +#include "cpbuffer.h" + +// Control connection for dinit + +// TODO: Use the input buffer as a circular buffer, instead of chomping data from +// the front using a data move. + +// forward-declaration of callback: +static void control_conn_cb(struct ev_loop * loop, ev_io * w, int revents); + +class ControlConn; + +// Pointer to the control connection that is listening for rollback completion +extern ControlConn * rollback_handler_conn; + +extern int active_control_conns; + +// "packet" format: +// (1 byte) packet type +// (N bytes) additional data (service name, etc) +// for LOADSERVICE/FINDSERVICE: +// (2 bytes) service name length +// (M bytes) service name (without nul terminator) + +// Information packet: +// (1 byte) packet type, >= 100 +// (1 byte) packet length (including all fields) +// N bytes: packet data (N = (length - 2)) + +class ServiceSet; +class ServiceRecord; + +class ControlConn : private ServiceListener +{ + friend void control_conn_cb(struct ev_loop *, ev_io *, int); + + struct ev_io iob; + struct ev_loop *loop; + ServiceSet *service_set; + + bool bad_conn_close; // close when finished output? + bool oom_close; // send final 'out of memory' indicator + + // The packet length before we need to re-check if the packet is complete. + // processPacket() will not be called until the packet reaches this size. + int chklen; + + // Receive buffer + CPBuffer rbuf; + + template using list = std::list; + template using vector = std::vector; + + // A mapping between service records and their associated numerical identifier used + // in communction + using handle_t = uint32_t; + std::unordered_multimap serviceKeyMap; + std::unordered_map keyServiceMap; + + // Buffer for outgoing packets. Each outgoing back is represented as a vector. + list> outbuf; + // Current index within the first outgoing packet (all previous bytes have been sent). + unsigned outpkt_index = 0; + + // Queue a packet to be sent + // Returns: true if the packet was successfully queued, false if otherwise + // (eg if out of memory); in the latter case the connection might + // no longer be valid (iff there are no outgoing packets queued). + bool queuePacket(vector &&v) noexcept; + bool queuePacket(const char *pkt, unsigned size) noexcept; + + // Process a packet. Can cause the ControlConn to be deleted iff there are no + // outgoing packets queued. + // Throws: + // std::bad_alloc - if an out-of-memory condition prevents processing + void processPacket(); + + // Process a STARTSERVICE/STOPSERVICE packet. May throw std::bad_alloc. + void processStartStop(int pktType); + + // Process a FINDSERVICE/LOADSERVICE packet. May throw std::bad_alloc. + void processFindLoad(int pktType); + + // Notify that data is ready to be read from the socket. Returns true in cases where the + // connection was deleted with potentially pending outgoing packets. + bool dataReady() noexcept; + + void sendData() noexcept; + + // Allocate a new handle for a service; may throw std::bad_alloc + handle_t allocateServiceHandle(ServiceRecord *record); + + ServiceRecord *findServiceForKey(uint32_t key) + { + try { + return keyServiceMap.at(key); + } + catch (std::out_of_range &exc) { + return nullptr; + } + } + + // Close connection due to out-of-memory condition. + void doOomClose() + { + bad_conn_close = true; + oom_close = true; + ev_io_set(&iob, iob.fd, EV_WRITE); + } + + // Process service event broadcast. + void serviceEvent(ServiceRecord * service, ServiceEvent event) noexcept final override + { + // For each service handle corresponding to the event, send an information packet. + auto range = serviceKeyMap.equal_range(service); + auto & i = range.first; + auto & end = range.second; + try { + while (i != end) { + uint32_t key = i->second; + std::vector pkt; + constexpr int pktsize = 3 + sizeof(key); + pkt.reserve(pktsize); + pkt.push_back(DINIT_IP_SERVICEEVENT); + pkt.push_back(pktsize); + char * p = (char *) &key; + for (int j = 0; j < (int)sizeof(key); j++) { + pkt.push_back(*p++); + } + pkt.push_back(static_cast(event)); + queuePacket(std::move(pkt)); + ++i; + } + } + catch (std::bad_alloc &exc) { + doOomClose(); + } + } + + public: + ControlConn(struct ev_loop * loop, ServiceSet * service_set, int fd) : loop(loop), service_set(service_set), chklen(0) + { + ev_io_init(&iob, control_conn_cb, fd, EV_READ); + iob.data = this; + ev_io_start(loop, &iob); + + active_control_conns++; + } + + bool rollbackComplete() noexcept; + + virtual ~ControlConn() noexcept; +}; + + +static void control_conn_cb(struct ev_loop * loop, ev_io * w, int revents) +{ + ControlConn *conn = (ControlConn *) w->data; + if (revents & EV_READ) { + if (conn->dataReady()) { + // ControlConn was deleted + return; + } + } + if (revents & EV_WRITE) { + conn->sendData(); + } +} + +#endif diff --git a/src/cpbuffer.h b/src/cpbuffer.h new file mode 100644 index 0000000..251ef6a --- /dev/null +++ b/src/cpbuffer.h @@ -0,0 +1,89 @@ +#ifndef CPBUFFER_H +#define CPBUFFER_H + +#include + +// control protocol buffer, a circular buffer with 1024-byte capacity. +class CPBuffer +{ + char buf[1024]; + int cur_idx = 0; + int length = 0; // number of elements in the buffer + + public: + int get_length() noexcept + { + return length; + } + + // fill by reading from the given fd, return positive if some was read or -1 on error. + int fill(int fd) noexcept + { + int pos = cur_idx + length; + if (pos >= 1024) pos -= 1024; + int max_count = std::min(1024 - pos, 1024 - length); + ssize_t r = read(fd, buf + cur_idx, max_count); + if (r >= 0) { + length += r; + } + return r; + } + + // fill by readin from the given fd, until at least the specified number of bytes are in + // the buffer. Return 0 if end-of-file reached before fill complete, or -1 on error. + int fillTo(int fd, int rlength) noexcept + { + while (length < rlength) { + int r = fill(fd); + if (r <= 0) return r; + } + return 1; + } + + int operator[](int idx) noexcept + { + int dest_idx = cur_idx + idx; + if (dest_idx > 1024) dest_idx -= 1024; + return buf[dest_idx]; + } + + void consume(int amount) noexcept + { + cur_idx += amount; + if (cur_idx >= 1024) cur_idx -= 1024; + length -= amount; + } + + void extract(char *dest, int index, int length) noexcept + { + index += cur_idx; + if (index >= 1024) index -= 1024; + if (index + length > 1024) { + // wrap-around copy + int half = 1024 - index; + std::memcpy(dest, buf + index, half); + std::memcpy(dest + half, buf, length - half); + } + else { + std::memcpy(dest, buf + index, length); + } + } + + // Extract string of give length from given index + // Throws: std::bad_alloc on allocation failure + std::string extract_string(int index, int length) + { + index += cur_idx; + if (index >= 1024) index -= 1024; + if (index + length > 1024) { + std::string r(buf + index, 1024 - index); + r.insert(r.end(), buf, buf + length - (1024 - index)); + return r; + } + else { + return std::string(buf + index, length); + } + } +}; + +#endif diff --git a/src/dinit-log.cc b/src/dinit-log.cc new file mode 100644 index 0000000..8e031e7 --- /dev/null +++ b/src/dinit-log.cc @@ -0,0 +1,68 @@ +#include +#include "dinit-log.h" + +LogLevel log_level = LogLevel::WARN; +bool log_to_console = true; // whether we should output log messages to console +bool log_current_line; + +// Log a message +void log(LogLevel lvl, const char *msg) noexcept +{ + if (lvl >= log_level) { + if (log_to_console) { + std::cout << "dinit: " << msg << std::endl; + } + } +} + +// Log a multi-part message beginning +void logMsgBegin(LogLevel lvl, const char *msg) noexcept +{ + log_current_line = lvl >= log_level; + if (log_current_line) { + if (log_to_console) { + std::cout << "dinit: " << msg; + } + } +} + +// Continue a multi-part log message +void logMsgPart(const char *msg) noexcept +{ + if (log_current_line) { + if (log_to_console) { + std::cout << msg; + } + } +} + +// Complete a multi-part log message +void logMsgEnd(const char *msg) noexcept +{ + if (log_current_line) { + if (log_to_console) { + std::cout << msg << std::endl; + } + } +} + +void logServiceStarted(const char *service_name) noexcept +{ + if (log_to_console) { + std::cout << "[ OK ] " << service_name << std::endl; + } +} + +void logServiceFailed(const char *service_name) noexcept +{ + if (log_to_console) { + std::cout << "[FAILED] " << service_name << std::endl; + } +} + +void logServiceStopped(const char *service_name) noexcept +{ + if (log_to_console) { + std::cout << "[STOPPED] " << service_name << std::endl; + } +} diff --git a/src/dinit-log.h b/src/dinit-log.h new file mode 100644 index 0000000..b54664e --- /dev/null +++ b/src/dinit-log.h @@ -0,0 +1,112 @@ +#ifndef DINIT_LOG_H +#define DINIT_LOG_H + +// Logging for Dinit + +#include +#include +#include + +enum class LogLevel { + DEBUG, + INFO, + WARN, + ERROR, + ZERO // log absolutely nothing +}; + +extern LogLevel log_level; +extern bool log_to_console; + +void log(LogLevel lvl, const char *msg) noexcept; +void logMsgBegin(LogLevel lvl, const char *msg) noexcept; +void logMsgPart(const char *msg) noexcept; +void logMsgEnd(const char *msg) noexcept; +void logServiceStarted(const char *service_name) noexcept; +void logServiceFailed(const char *service_name) noexcept; +void logServiceStopped(const char *service_name) noexcept; + +// Convenience methods which perform type conversion of the argument. +// There is some duplication here that could possibly be avoided, but +// it doesn't seem like a big deal. +static inline void log(LogLevel lvl, const std::string &str) noexcept +{ + log(lvl, str.c_str()); +} + +static inline void logMsgBegin(LogLevel lvl, const std::string &str) noexcept +{ + logMsgBegin(lvl, str.c_str()); +} + +static inline void logMsgBegin(LogLevel lvl, int a) noexcept +{ + constexpr int bufsz = (CHAR_BIT * sizeof(int) - 1) / 3 + 2; + char nbuf[bufsz]; + snprintf(nbuf, bufsz, "%d", a); + logMsgBegin(lvl, nbuf); +} + +static inline void logMsgPart(const std::string &str) noexcept +{ + logMsgPart(str.c_str()); +} + +static inline void logMsgPart(int a) noexcept +{ + constexpr int bufsz = (CHAR_BIT * sizeof(int) - 1) / 3 + 2; + char nbuf[bufsz]; + snprintf(nbuf, bufsz, "%d", a); + logMsgPart(nbuf); +} + +static inline void logMsgEnd(const std::string &str) noexcept +{ + logMsgEnd(str.c_str()); +} + +static inline void logMsgEnd(int a) noexcept +{ + constexpr int bufsz = (CHAR_BIT * sizeof(int) - 1) / 3 + 2; + char nbuf[bufsz]; + snprintf(nbuf, bufsz, "%d", a); + logMsgEnd(nbuf); +} + +static inline void logServiceStarted(const std::string &str) noexcept +{ + logServiceStarted(str.c_str()); +} + +static inline void logServiceFailed(const std::string &str) noexcept +{ + logServiceFailed(str.c_str()); +} + +static inline void logServiceStopped(const std::string &str) noexcept +{ + logServiceStopped(str.c_str()); +} + +// It's not intended that methods in this namespace be called directly: +namespace dinit_log { + template static inline void logParts(A a) noexcept + { + logMsgEnd(a); + } + + template static inline void logParts(A a, B... b) noexcept + { + logMsgPart(a); + logParts(b...); + } +} + +// Variadic 'log' method. +template static inline void log(LogLevel lvl, A a, B ...b) noexcept +{ + logMsgBegin(lvl, a); + dinit_log::logParts(b...); +} + +#endif diff --git a/src/dinit-start.cc b/src/dinit-start.cc new file mode 100644 index 0000000..3e6a9e7 --- /dev/null +++ b/src/dinit-start.cc @@ -0,0 +1,253 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "control-cmds.h" +#include "service-constants.h" +#include "cpbuffer.h" + +// dinit-start: utility to start a dinit service + +// This utility communicates with the dinit daemon via a unix socket (/dev/initctl). + +using handle_t = uint32_t; + + +class ReadCPException +{ + public: + int errcode; + ReadCPException(int err) : errcode(err) { } +}; + +static void fillBufferTo(CPBuffer *buf, int fd, int rlength) +{ + int r = buf->fillTo(fd, rlength); + if (r == -1) { + throw ReadCPException(errno); + } + else if (r == 0) { + throw ReadCPException(0); + } +} + + +int main(int argc, char **argv) +{ + using namespace std; + + bool show_help = argc < 2; + char *service_name = nullptr; + + std::string control_socket_str; + const char * control_socket_path = nullptr; + + bool verbose = true; + bool sys_dinit = false; // communicate with system daemon + bool wait_for_service = true; + + for (int i = 1; i < argc; i++) { + if (argv[i][0] == '-') { + if (strcmp(argv[i], "--help") == 0) { + show_help = true; + break; + } + else if (strcmp(argv[i], "--no-wait") == 0) { + wait_for_service = false; + } + else if (strcmp(argv[i], "--quiet") == 0) { + verbose = false; + } + else if (strcmp(argv[i], "--system") == 0 || strcmp(argv[i], "-s") == 0) { + sys_dinit = true; + } + else { + cerr << "Unrecognized command-line parameter: " << argv[i] << endl; + return 1; + } + } + else { + // service name + service_name = argv[i]; + // TODO support multiple services (or at least give error if multiple + // services supplied) + } + } + + if (show_help) { + cout << "dinit-start: start a dinit service" << endl; + cout << " --help : show this help" << endl; + cout << " --no-wait : don't wait for service startup/shutdown to complete" << endl; + cout << " --quiet : suppress output (except errors)" << endl; + cout << " -s, --system : control system daemon instead of user daemon" << endl; + cout << " : start the named service" << endl; + return 1; + } + + + control_socket_path = "/dev/dinitctl"; + + if (! sys_dinit) { + char * userhome = getenv("HOME"); + if (userhome == nullptr) { + struct passwd * pwuid_p = getpwuid(getuid()); + if (pwuid_p != nullptr) { + userhome = pwuid_p->pw_dir; + } + } + + if (userhome != nullptr) { + control_socket_str = userhome; + control_socket_str += "/.dinitctl"; + control_socket_path = control_socket_str.c_str(); + } + else { + cerr << "Cannot locate user home directory (set HOME or check /etc/passwd file)" << endl; + return 1; + } + } + + int socknum = socket(AF_UNIX, SOCK_STREAM, 0); + if (socknum == -1) { + perror("socket"); + return 1; + } + + struct sockaddr_un * name; + uint sockaddr_size = offsetof(struct sockaddr_un, sun_path) + strlen(control_socket_path) + 1; + name = (struct sockaddr_un *) malloc(sockaddr_size); + if (name == nullptr) { + cerr << "dinit-start: out of memory" << endl; + return 1; + } + + name->sun_family = AF_UNIX; + strcpy(name->sun_path, control_socket_path); + + int connr = connect(socknum, (struct sockaddr *) name, sockaddr_size); + if (connr == -1) { + perror("connect"); + return 1; + } + + // TODO should start by querying protocol version + + // Build buffer; + uint16_t sname_len = strlen(service_name); + int bufsize = 3 + sname_len; + char * buf = new char[bufsize]; + + buf[0] = DINIT_CP_LOADSERVICE; + memcpy(buf + 1, &sname_len, 2); + memcpy(buf + 3, service_name, sname_len); + + int r = write(socknum, buf, bufsize); + // TODO make sure we write it all + delete [] buf; + if (r == -1) { + perror("write"); + return 1; + } + + // Now we expect a reply: + // NOTE: should skip over information packets. + + try { + CPBuffer rbuffer; + fillBufferTo(&rbuffer, socknum, 1); + + ServiceState state; + ServiceState target_state; + handle_t handle; + + if (rbuffer[0] == DINIT_RP_SERVICERECORD) { + fillBufferTo(&rbuffer, socknum, 2 + sizeof(handle)); + rbuffer.extract((char *) &handle, 2, sizeof(handle)); + state = static_cast(rbuffer[1]); + target_state = static_cast(rbuffer[2 + sizeof(handle)]); + rbuffer.consume(3 + sizeof(handle)); + } + else if (rbuffer[0] == DINIT_RP_NOSERVICE) { + cerr << "Failed to find/load service." << endl; + return 1; + } + else { + cerr << "Protocol error." << endl; + return 1; + } + + // Need to issue STARTSERVICE: + if (target_state != ServiceState::STARTED) { + buf = new char[2 + sizeof(handle)]; + buf[0] = DINIT_CP_STARTSERVICE; + buf[1] = 0; // don't pin + memcpy(buf + 2, &handle, sizeof(handle)); + r = write(socknum, buf, 2 + sizeof(handle)); + delete buf; + } + + if (state == ServiceState::STARTED) { + if (verbose) { + cout << "Service already started." << endl; + } + return 0; // success! + } + + if (! wait_for_service) { + return 0; + } + + // Wait until service started: + r = rbuffer.fillTo(socknum, 2); + while (r > 0) { + if (rbuffer[0] >= 100) { + int pktlen = (unsigned char) rbuffer[1]; + fillBufferTo(&rbuffer, socknum, pktlen); + + if (rbuffer[0] == DINIT_IP_SERVICEEVENT) { + handle_t ev_handle; + rbuffer.extract((char *) &ev_handle, 2, sizeof(ev_handle)); + ServiceEvent event = static_cast(rbuffer[2 + sizeof(ev_handle)]); + if (ev_handle == handle && event == ServiceEvent::STARTED) { + if (verbose) { + cout << "Service started." << endl; + } + return 0; + } + } + } + else { + // Not an information packet? + cerr << "protocol error" << endl; + return 1; + } + } + + if (r == -1) { + perror("read"); + } + else { + cerr << "protocol error (connection closed by server)" << endl; + } + return 1; + } + catch (ReadCPException &exc) { + cerr << "control socket read failure or protocol error" << endl; + return 1; + } + catch (std::bad_alloc &exc) { + cerr << "out of memory" << endl; + return 1; + } + + return 0; +} diff --git a/src/dinit.cc b/src/dinit.cc new file mode 100644 index 0000000..9b26b3a --- /dev/null +++ b/src/dinit.cc @@ -0,0 +1,429 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "service.h" +#include "ev++.h" +#include "control.h" +#include "dinit-log.h" + +#ifdef __linux__ +#include +#include +#endif + +/* + * "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). + * + * 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. + * + * 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. + */ + + +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; + +struct ev_io control_socket_io; + + +// Variables + +static ServiceSet *service_set; + +static bool am_system_init = false; // true if we are the system init process + +static bool control_socket_open = false; +int active_control_conns = 0; + +static const char *control_socket_path = "/dev/dinitctl"; +static std::string control_socket_str; + + +int main(int argc, char **argv) +{ + using namespace std; + + am_system_init = (getpid() == 1); + + if (am_system_init) { + // setup STDIN, STDOUT, STDERR so that we can use them + int onefd = open("/dev/console", O_RDONLY, 0); + dup2(onefd, 0); + int twofd = open("/dev/console", O_RDWR, 0); + dup2(twofd, 1); + dup2(twofd, 2); + } + + /* 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); */ + + // 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(SIGTSTP, SIG_IGN); + signal(SIGTTIN, SIG_IGN); + signal(SIGTTOU, SIG_IGN); + + /* list of services to start */ + list services_to_start; + + if (! am_system_init) { + char * userhome = getenv("HOME"); + if (userhome == nullptr) { + struct passwd * pwuid_p = getpwuid(getuid()); + if (pwuid_p != nullptr) { + userhome = pwuid_p->pw_dir; + } + } + + if (userhome != nullptr) { + control_socket_str = userhome; + control_socket_str += "/.dinitctl"; + control_socket_path = control_socket_str.c_str(); + } + } + + /* service directory name */ + const char * service_dir = "/etc/dinit.d"; + + // 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 + // 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) { + ++i; + if (i < argc) { + service_dir = argv[i]; + } + else { + cerr << "dinit: '--services-dir' (-d) 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 << " --services-dir , -d : set base directory for service description files (-d )" << endl; + cout << " : start service with name " << endl; + return 0; + } + else { + // unrecognized + if (! am_system_init) { + cerr << "dinit: Unrecognized option: " << argv[i] << endl; + return 1; + } + } + } + else { + // 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]); + } + } + } + } + + if (services_to_start.empty()) { + services_to_start.push_back("boot"); + } + + // Set up signal handlers + ev_signal sigint_ev_signal; + if (am_system_init) { + ev_signal_init(&sigint_ev_signal, sigint_reboot_cb, SIGINT); + } + else { + ev_signal_init(&sigint_ev_signal, sigterm_cb, SIGINT); + } + + 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); + } + + 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); + +#ifdef __linux__ + if (am_system_init) { + // Disable non-critical kernel output to console + klogctl(6 /* SYSLOG_ACTION_CONSOLE_OFF */, nullptr, 0); + // Make ctrl+alt+del combination send SIGINT to PID 1 (this process) + reboot(RB_DISABLE_CAD); + } +#endif + + /* start requested services */ + service_set = new ServiceSet(service_dir); + for (list::iterator i = services_to_start.begin(); + i != services_to_start.end(); + ++i) { + try { + service_set->startService(*i); + } + catch (ServiceNotFound &snf) { + log(LogLevel::ERROR, snf.serviceName, ": Could not find service description."); + } + catch (ServiceLoadExc &sle) { + log(LogLevel::ERROR, sle.serviceName, ": ", sle.excDescription); + } + catch (std::bad_alloc &badalloce) { + log(LogLevel::ERROR, "Out of memory when trying to start service: ", *i, "."); + } + } + + event_loop: + + // Process events until all services have terminated. + while (service_set->count_active_services() != 0) { + ev_loop(loop, EVLOOP_ONESHOT); + } + + ShutdownType shutdown_type = service_set->getShutdownType(); + + if (am_system_init) { + logMsgBegin(LogLevel::INFO, "No more active services."); + + if (shutdown_type == ShutdownType::REBOOT) { + logMsgEnd(" Will reboot."); + } + else if (shutdown_type == ShutdownType::HALT) { + logMsgEnd(" Will halt."); + } + else if (shutdown_type == ShutdownType::POWEROFF) { + logMsgEnd(" Will power down."); + } + else { + logMsgEnd(" Re-initiating boot sequence."); + } + } + + close_control_socket(ev_default_loop(EVFLAG_AUTO)); + + if (am_system_init) { + if (shutdown_type == ShutdownType::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"); + 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; + } + } + + const char * cmd_arg; + if (shutdown_type == ShutdownType::HALT) { + cmd_arg = "-h"; + } + else if (shutdown_type == ShutdownType::REBOOT) { + cmd_arg = "-r"; + } + else { + // power off. + cmd_arg = "-p"; + } + + // Fork and execute dinit-reboot. + execl("/sbin/shutdown", "/sbin/shutdown", "--system", cmd_arg, nullptr); + log(LogLevel::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); + } + } + + return 0; +} + +// Callback for control socket +static void control_socket_cb(struct ev_loop *loop, ev_io *w, int revents) +{ + // 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. + + // Accept a connection + int sockfd = w->fd; + + int newfd = 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 + } + catch (std::bad_alloc &bad_alloc_exc) { + log(LogLevel::ERROR, "Accepting control connection: Out of memory"); + close(newfd); + } + } +} + +void open_control_socket(struct ev_loop *loop) noexcept +{ + if (! control_socket_open) { + const char * saddrname = control_socket_path; + uint sockaddr_size = offsetof(struct sockaddr_un, sun_path) + strlen(saddrname) + 1; + + struct sockaddr_un * name = static_cast(malloc(sockaddr_size)); + if (name == nullptr) { + log(LogLevel::ERROR, "Opening control socket: out of memory"); + return; + } + + if (am_system_init) { + // Unlink any stale control socket file, but only if we are system init, since otherwise + // the 'stale' file may not be stale at all: + unlink(saddrname); + } + + name->sun_family = AF_UNIX; + strcpy(name->sun_path, saddrname); + + int sockfd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (sockfd == -1) { + log(LogLevel::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)); + close(sockfd); + free(name); + return; + } + + free(name); + + // 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)); + close(sockfd); + return; + } + + if (listen(sockfd, 10) == -1) { + log(LogLevel::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); + } +} + +void close_control_socket(struct ev_loop *loop) noexcept +{ + if (control_socket_open) { + int fd = control_socket_io.fd; + ev_io_stop(loop, &control_socket_io); + close(fd); + + // Unlink the socket: + unlink(control_socket_path); + } +} + +/* 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) +{ + log_to_console = true; + service_set->stop_all_services(ShutdownType::REBOOT); +} + +/* handle SIGQUIT (if we are system init) */ +static void sigquit_cb(struct ev_loop *loop, ev_signal *w, int revents) +{ + // 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)); +} + +/* handle SIGTERM/SIGQUIT - stop all services (not used for system daemon) */ +static void sigterm_cb(struct ev_loop *loop, ev_signal *w, int revents) +{ + log_to_console = true; + service_set->stop_all_services(); +} diff --git a/src/halt b/src/halt new file mode 100755 index 0000000..4987aff --- /dev/null +++ b/src/halt @@ -0,0 +1,3 @@ +#!/bin/sh +# "halt" command actually executes the more useful "power off". +shutdown -p "$@" diff --git a/src/load_service.cc b/src/load_service.cc new file mode 100644 index 0000000..79972ff --- /dev/null +++ b/src/load_service.cc @@ -0,0 +1,507 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "service.h" + +typedef std::string string; +typedef std::string::iterator string_iterator; + +// Utility function to skip white space. Returns an iterator at the +// first non-white-space position (or at end). +static string_iterator skipws(string_iterator i, string_iterator end) +{ + using std::locale; + using std::isspace; + + while (i != end) { + if (! isspace(*i, locale::classic())) { + break; + } + ++i; + } + return i; +} + +// Read a setting name. +static string read_setting_name(string_iterator & i, string_iterator end) +{ + using std::locale; + using std::ctype; + using std::use_facet; + + const ctype & facet = use_facet >(locale::classic()); + + string rval; + // Allow alphabetical characters, and dash (-) in setting name + while (i != end && (*i == '-' || facet.is(ctype::alpha, *i))) { + rval += *i; + ++i; + } + return rval; +} + +// Read a setting value +// +// In general a setting value is a single-line string. It may contain multiple parts +// separated by white space (which is normally collapsed). A hash mark - # - denotes +// the end of the value and the beginning of a comment (it should be preceded by +// whitespace). +// +// Part of a value may be quoted using double quote marks, which prevents collapse +// of whitespace and interpretation of most special characters (the quote marks will +// not be considered part of the value). A backslash can precede a character (such +// as '#' or '"' or another backslash) to remove its special meaning. Newline +// characters are not allowed in values and cannot be quoted. +// +// This function expects the string to be in an ASCII-compatible, single byte +// encoding (the "classic" locale). +// +// Params: +// i - reference to string iterator through the line +// end - iterator at end of line +// part_positions - list of to which the position of each setting value +// part will be added as [start,end). May be null. +static string read_setting_value(string_iterator & i, string_iterator end, + std::list> * part_positions = nullptr) +{ + using std::locale; + using std::isspace; + + i = skipws(i, end); + + string rval; + bool new_part = true; + int part_start; + + while (i != end) { + char c = *i; + if (c == '\"') { + if (new_part) { + part_start = rval.length(); + new_part = false; + } + // quoted string + ++i; + while (i != end) { + c = *i; + if (c == '\"') break; + if (c == '\n') { + // TODO error here. + } + else if (c == '\\') { + // A backslash escapes the following character. + ++i; + if (i != end) { + c = *i; + if (c == '\n') { + // TODO error here. + } + rval += c; + } + } + else { + rval += c; + } + ++i; + } + if (i == end) { + // String wasn't terminated + // TODO error here + break; + } + } + else if (c == '\\') { + if (new_part) { + part_start = rval.length(); + new_part = false; + } + // A backslash escapes the next character + ++i; + if (i != end) { + rval += *i; + } + else { + // TODO error here + } + } + else if (isspace(c, locale::classic())) { + if (! new_part && part_positions != nullptr) { + part_positions->emplace_back(part_start, rval.length()); + new_part = true; + } + i = skipws(i, end); + if (i == end) break; + if (*i == '#') break; // comment + rval += ' '; // collapse ws to a single space + continue; + } + else if (c == '#') { + // hmm... comment? Probably, though they should have put a space + // before it really. TODO throw an exception, and document + // that '#' for comments must be preceded by space, and in values + // must be quoted. + break; + } + else { + if (new_part) { + part_start = rval.length(); + new_part = false; + } + rval += c; + } + ++i; + } + + // Got to end: + if (part_positions != nullptr) { + part_positions->emplace_back(part_start, rval.length()); + } + + return rval; +} + +static int signalNameToNumber(std::string &signame) +{ + if (signame == "HUP") return SIGHUP; + if (signame == "INT") return SIGINT; + if (signame == "QUIT") return SIGQUIT; + if (signame == "USR1") return SIGUSR1; + if (signame == "USR2") return SIGUSR2; + return -1; +} + +static const char * uid_err_msg = "Specified user id contains invalid numeric characters or is outside allowed range."; + +// Parse a userid parameter which may be a numeric user ID or a username. If a name, the +// userid is looked up via the system user database (getpwnam() function). In this case, +// the associated group is stored in the location specified by the group_p parameter iff +// it is not null and iff it contains the value -1. +static uid_t parse_uid_param(const std::string ¶m, const std::string &service_name, gid_t *group_p) +{ + // Could be a name or a numeric id. But we should assume numeric first, just in case + // a user manages to give themselves a username that parses as a number. + std::size_t ind = 0; + try { + // POSIX does not specify whether uid_t is an signed or unsigned, but regardless + // is is probably safe to assume that valid values are positive. We'll also assume + // that the value range fits with "unsigned long long" since it seems unlikely + // that would ever not be the case. + // + // TODO perhaps write a number parser, since even the unsigned variants of the C/C++ + // functions accept a leading minus sign... + static_assert((uintmax_t)std::numeric_limits::max() <= (uintmax_t)std::numeric_limits::max(), "uid_t is too large"); + unsigned long long v = std::stoull(param, &ind, 0); + if (v > static_cast(std::numeric_limits::max()) || ind != param.length()) { + throw ServiceDescriptionExc(service_name, uid_err_msg); + } + return v; + } + catch (std::out_of_range &exc) { + throw ServiceDescriptionExc(service_name, uid_err_msg); + } + catch (std::invalid_argument &exc) { + // Ok, so it doesn't look like a number: proceed... + } + + errno = 0; + struct passwd * pwent = getpwnam(param.c_str()); + if (pwent == nullptr) { + // Maybe an error, maybe just no entry. + if (errno == 0) { + throw new ServiceDescriptionExc(service_name, "Specified user \"" + param + "\" does not exist in system database."); + } + else { + throw new ServiceDescriptionExc(service_name, std::string("Error accessing user database: ") + strerror(errno)); + } + } + + if (group_p && *group_p != (gid_t)-1) { + *group_p = pwent->pw_gid; + } + + return pwent->pw_uid; +} + +static const char * gid_err_msg = "Specified group id contains invalid numeric characters or is outside allowed range."; + +static gid_t parse_gid_param(const std::string ¶m, const std::string &service_name) +{ + // Could be a name or a numeric id. But we should assume numeric first, just in case + // a user manages to give themselves a username that parses as a number. + std::size_t ind = 0; + try { + // POSIX does not specify whether uid_t is an signed or unsigned, but regardless + // is is probably safe to assume that valid values are positive. We'll also assume + // that the value range fits with "unsigned long long" since it seems unlikely + // that would ever not be the case. + // + // TODO perhaps write a number parser, since even the unsigned variants of the C/C++ + // functions accept a leading minus sign... + unsigned long long v = std::stoull(param, &ind, 0); + if (v > static_cast(std::numeric_limits::max()) || ind != param.length()) { + throw ServiceDescriptionExc(service_name, gid_err_msg); + } + return v; + } + catch (std::out_of_range &exc) { + throw ServiceDescriptionExc(service_name, gid_err_msg); + } + catch (std::invalid_argument &exc) { + // Ok, so it doesn't look like a number: proceed... + } + + errno = 0; + struct group * grent = getgrnam(param.c_str()); + if (grent == nullptr) { + // Maybe an error, maybe just no entry. + if (errno == 0) { + throw new ServiceDescriptionExc(service_name, "Specified group \"" + param + "\" does not exist in system database."); + } + else { + throw new ServiceDescriptionExc(service_name, std::string("Error accessing group database: ") + strerror(errno)); + } + } + + return grent->gr_gid; +} + +// Find a service record, or load it from file. If the service has +// dependencies, load those also. +// +// Might throw a ServiceLoadExc exception if a dependency cycle is found or if another +// problem occurs (I/O error, service description not found etc). Throws std::bad_alloc +// if a memory allocation failure occurs. +ServiceRecord * ServiceSet::loadServiceRecord(const char * name) +{ + using std::string; + using std::ifstream; + using std::ios; + using std::ios_base; + using std::locale; + using std::isspace; + + using std::list; + using std::pair; + + // First try and find an existing record... + ServiceRecord * rval = findService(string(name)); + if (rval != 0) { + if (rval->isDummy()) { + throw ServiceCyclicDependency(name); + } + return rval; + } + + // Couldn't find one. Have to load it. + string service_filename = service_dir; + if (*(service_filename.rbegin()) != '/') { + service_filename += '/'; + } + service_filename += name; + + string command; + list> command_offsets; + string stop_command; + list> stop_command_offsets; + string pid_file; + + ServiceType service_type = ServiceType::PROCESS; + std::list depends_on; + std::list depends_soft; + string logfile; + OnstartFlags onstart_flags; + int term_signal = -1; // additional termination signal + bool auto_restart = false; + bool smooth_recovery = false; + string socket_path; + int socket_perms = 0666; + // Note: Posix allows that uid_t and gid_t may be unsigned types, but eg chown uses -1 as an + // invalid value, so it's safe to assume that we can do the same: + uid_t socket_uid = -1; + gid_t socket_gid = -1; + + string line; + ifstream service_file; + service_file.exceptions(ios::badbit | ios::failbit); + + try { + service_file.open(service_filename.c_str(), ios::in); + } + catch (std::ios_base::failure &exc) { + throw ServiceNotFound(name); + } + + // Add a dummy service record now to prevent infinite recursion in case of cyclic dependency + rval = new ServiceRecord(this, string(name)); + records.push_back(rval); + + try { + // getline can set failbit if it reaches end-of-file, we don't want an exception in that case: + service_file.exceptions(ios::badbit); + + while (! (service_file.rdstate() & ios::eofbit)) { + getline(service_file, line); + string::iterator i = line.begin(); + string::iterator end = line.end(); + + i = skipws(i, end); + if (i != end) { + if (*i == '#') { + continue; // comment line + } + string setting = read_setting_name(i, end); + i = skipws(i, end); + if (i == end || (*i != '=' && *i != ':')) { + throw ServiceDescriptionExc(name, "Badly formed line."); + } + i = skipws(++i, end); + + if (setting == "command") { + command = read_setting_value(i, end, &command_offsets); + } + else if (setting == "socket-listen") { + socket_path = read_setting_value(i, end, nullptr); + } + else if (setting == "socket-permissions") { + string sock_perm_str = read_setting_value(i, end, nullptr); + std::size_t ind = 0; + try { + socket_perms = std::stoi(sock_perm_str, &ind, 8); + if (ind != sock_perm_str.length()) { + throw std::logic_error(""); + } + } + catch (std::logic_error &exc) { + throw ServiceDescriptionExc(name, "socket-permissions: Badly-formed or out-of-range numeric value"); + } + } + else if (setting == "socket-uid") { + string sock_uid_s = read_setting_value(i, end, nullptr); + socket_uid = parse_uid_param(sock_uid_s, name, &socket_gid); + } + else if (setting == "socket-gid") { + string sock_gid_s = read_setting_value(i, end, nullptr); + socket_gid = parse_gid_param(sock_gid_s, name); + } + else if (setting == "stop-command") { + stop_command = read_setting_value(i, end, &stop_command_offsets); + } + else if (setting == "pid-file") { + pid_file = read_setting_value(i, end); + } + else if (setting == "depends-on") { + string dependency_name = read_setting_value(i, end); + depends_on.push_back(loadServiceRecord(dependency_name.c_str())); + } + else if (setting == "waits-for") { + string dependency_name = read_setting_value(i, end); + depends_soft.push_back(loadServiceRecord(dependency_name.c_str())); + } + else if (setting == "logfile") { + logfile = read_setting_value(i, end); + } + else if (setting == "restart") { + string restart = read_setting_value(i, end); + auto_restart = (restart == "yes" || restart == "true"); + } + else if (setting == "smooth-recovery") { + string recovery = read_setting_value(i, end); + smooth_recovery = (recovery == "yes" || recovery == "true"); + } + else if (setting == "type") { + string type_str = read_setting_value(i, end); + if (type_str == "scripted") { + service_type = ServiceType::SCRIPTED; + } + else if (type_str == "process") { + service_type = ServiceType::PROCESS; + } + else if (type_str == "bgprocess") { + service_type = ServiceType::BGPROCESS; + } + else if (type_str == "internal") { + service_type = ServiceType::INTERNAL; + } + else { + throw ServiceDescriptionExc(name, "Service type must be one of: \"scripted\"," + " \"process\", \"bgprocess\" or \"internal\""); + } + } + else if (setting == "onstart") { + std::list> indices; + string onstart_cmds = read_setting_value(i, end, &indices); + for (auto indexpair : indices) { + string onstart_cmd = onstart_cmds.substr(indexpair.first, indexpair.second - indexpair.first); + if (onstart_cmd == "rw_ready") { + onstart_flags.rw_ready = true; + } + else { + throw new ServiceDescriptionExc(name, "Unknown onstart command: " + onstart_cmd); + } + } + } + else if (setting == "termsignal") { + string signame = read_setting_value(i, end, nullptr); + int signo = signalNameToNumber(signame); + if (signo == -1) { + throw new ServiceDescriptionExc(name, "Unknown/unsupported termination signal: " + signame); + } + else { + term_signal = signo; + } + } + else if (setting == "nosigterm") { + string sigtermsetting = read_setting_value(i, end); + onstart_flags.no_sigterm = (sigtermsetting == "yes" || sigtermsetting == "true"); + } + else if (setting == "runs-on-console") { + string runconsolesetting = read_setting_value(i, end); + onstart_flags.runs_on_console = (runconsolesetting == "yes" || runconsolesetting == "true"); + } + else { + throw ServiceDescriptionExc(name, "Unknown setting: " + setting); + } + } + } + + service_file.close(); + // TODO check we actually have all the settings - type, command + + // Now replace the dummy service record with a real record: + for (auto iter = records.begin(); iter != records.end(); iter++) { + if (*iter == rval) { + // We've found the dummy record + delete rval; + rval = new ServiceRecord(this, string(name), service_type, std::move(command), command_offsets, + & depends_on, & depends_soft); + rval->setStopCommand(stop_command, stop_command_offsets); + rval->setLogfile(logfile); + rval->setAutoRestart(auto_restart); + rval->setSmoothRecovery(smooth_recovery); + rval->setOnstartFlags(onstart_flags); + rval->setExtraTerminationSignal(term_signal); + rval->set_pid_file(std::move(pid_file)); + rval->set_socket_details(std::move(socket_path), socket_perms, socket_uid, socket_gid); + *iter = rval; + break; + } + } + + return rval; + } + catch (...) { + // Must remove the dummy service record. + std::remove(records.begin(), records.end(), rval); + delete rval; + throw; + } +} diff --git a/src/reboot b/src/reboot new file mode 100755 index 0000000..c607879 --- /dev/null +++ b/src/reboot @@ -0,0 +1,2 @@ +#!/bin/sh +shutdown -r "$@" diff --git a/src/service-constants.h b/src/service-constants.h new file mode 100644 index 0000000..8084066 --- /dev/null +++ b/src/service-constants.h @@ -0,0 +1,41 @@ +#ifndef SERVICE_CONSTANTS_H +#define SERVICE_CONSTANTS_H + +/* Service states */ +enum class ServiceState { + STOPPED, // service is not running. + STARTING, // service is starting, and will start (or fail to start) in time. + STARTED, // service is running, + STOPPING // service script is stopping and will stop. +}; + +/* Service types */ +enum class ServiceType { + DUMMY, // Dummy service, used to detect cyclice dependencies + PROCESS, // Service runs as a process, and can be stopped by + // sending the process a signal (usually SIGTERM) + BGPROCESS, // Service runs as a process which "daemonizes" to run in the + // "background". + SCRIPTED, // Service requires an external command to start, + // and a second command to stop + INTERNAL // Internal service, runs no external process +}; + +/* Service events */ +enum class ServiceEvent { + STARTED, // Service was started (reached STARTED state) + STOPPED, // Service was stopped (reached STOPPED state) + FAILEDSTART, // Service failed to start (possibly due to dependency failing) + STARTCANCELLED, // Service was set to be started but a stop was requested + STOPCANCELLED // Service was set to be stopped but a start was requested +}; + +/* Shutdown types */ +enum class ShutdownType { + CONTINUE, // Continue normal boot sequence (used after single-user shell) + HALT, // Halt system without powering down + POWEROFF, // Power off system + REBOOT // Reboot system +}; + +#endif diff --git a/src/service-listener.h b/src/service-listener.h new file mode 100644 index 0000000..c45c2a9 --- /dev/null +++ b/src/service-listener.h @@ -0,0 +1,18 @@ +#ifndef SERVICE_LISTENER_H +#define SERVICE_LISTENER_H + +#include "service-constants.h" + +class ServiceRecord; + +// Interface for listening to services +class ServiceListener +{ + public: + + // An event occurred on the service being observed. + // Listeners must not be added or removed during event notification. + virtual void serviceEvent(ServiceRecord * service, ServiceEvent event) noexcept = 0; +}; + +#endif diff --git a/src/service.cc b/src/service.cc new file mode 100644 index 0000000..4fec13b --- /dev/null +++ b/src/service.cc @@ -0,0 +1,870 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "service.h" +#include "dinit-log.h" + +// from dinit.cc: +void open_control_socket(struct ev_loop *loop) noexcept; + + +// Find the requested service by name +static ServiceRecord * findService(const std::list & records, + const char *name) noexcept +{ + using std::list; + list::const_iterator i = records.begin(); + for ( ; i != records.end(); i++ ) { + if (strcmp((*i)->getServiceName(), name) == 0) { + return *i; + } + } + return (ServiceRecord *)0; +} + +ServiceRecord * ServiceSet::findService(const std::string &name) noexcept +{ + return ::findService(records, name.c_str()); +} + +void ServiceSet::startService(const char *name) +{ + using namespace std; + ServiceRecord *record = loadServiceRecord(name); + + record->start(); +} + +void ServiceSet::stopService(const std::string & name) noexcept +{ + ServiceRecord *record = findService(name); + if (record != nullptr) { + record->stop(); + } +} + +// Called when a service has actually stopped. +void ServiceRecord::stopped() noexcept +{ + if (service_type != ServiceType::SCRIPTED && service_type != ServiceType::BGPROCESS && onstart_flags.runs_on_console) { + tcsetpgrp(0, getpgrp()); + releaseConsole(); + } + + logServiceStopped(service_name); + service_state = ServiceState::STOPPED; + force_stop = false; + + // Stop any dependencies whose desired state is STOPPED: + for (sr_iter i = depends_on.begin(); i != depends_on.end(); i++) { + (*i)->dependentStopped(); + } + + service_set->service_inactive(this); + notifyListeners(ServiceEvent::STOPPED); + + if (desired_state == ServiceState::STARTED) { + // Desired state is "started". + start(); + } + else if (socket_fd != -1) { + close(socket_fd); + socket_fd = -1; + } +} + +void ServiceRecord::process_child_callback(struct ev_loop *loop, ev_child *w, int revents) noexcept +{ + ServiceRecord *sr = (ServiceRecord *) w->data; + + sr->pid = -1; + sr->exit_status = w->rstatus; + ev_child_stop(loop, w); + + // Ok, for a process service, any process death which we didn't rig + // ourselves is a bit... unexpected. Probably, the child died because + // we asked it to (sr->service_state == STOPPING). But even if + // we didn't, there's not much we can do. + + if (sr->waiting_for_execstat) { + // We still don't have an exec() status from the forked child, wait for that + // before doing any further processing. + return; + } + + sr->handle_exit_status(); +} + +void ServiceRecord::handle_exit_status() noexcept +{ + if (exit_status != 0 && service_state != ServiceState::STOPPING) { + log(LogLevel::ERROR, "Service ", service_name, " process terminated with exit code ", exit_status); + } + + if (doing_recovery) { + // (BGPROCESS only) + doing_recovery = false; + bool do_stop = false; + if (exit_status != 0) { + do_stop = true; + } + else { + // We need to re-read the PID, since it has now changed. + if (service_type == ServiceType::BGPROCESS && pid_file.length() != 0) { + if (! read_pid_file()) { + do_stop = true; + } + } + } + + if (do_stop) { + stop(); + if (auto_restart && service_set->get_auto_restart()) { + start(); + } + } + + return; + } + + if (service_type == ServiceType::PROCESS || service_type == ServiceType::BGPROCESS) { + if (service_state == ServiceState::STARTING) { + // (only applies to BGPROCESS) + if (exit_status == 0) { + started(); + } + else { + failed_to_start(); + } + } + else if (service_state == ServiceState::STOPPING) { + // TODO log non-zero rstatus? + stopped(); + } + else if (smooth_recovery && service_state == ServiceState::STARTED) { + // TODO ensure a minimum time between restarts + // TODO if we are pinned-started then we should probably check + // that dependencies have started before trying to re-start the + // service process. + doing_recovery = (service_type == ServiceType::BGPROCESS); + start_ps_process(); + return; + } + else { + forceStop(); + } + + if (auto_restart && service_set->get_auto_restart()) { + start(); + } + } + else { // SCRIPTED + if (service_state == ServiceState::STOPPING) { + if (exit_status == 0) { + stopped(); + } + else { + // ??? failed to stop! Let's log it as info: + log(LogLevel::INFO, "service ", service_name, " stop command failed with exit code ", exit_status); + // Just assume that we stopped, so that any dependencies + // can be stopped: + stopped(); + } + } + else { // STARTING + if (exit_status == 0) { + started(); + } + else { + // failed to start + log(LogLevel::ERROR, "service ", service_name, " command failed with exit code ", exit_status); + failed_to_start(); + } + } + } +} + +void ServiceRecord::process_child_status(struct ev_loop *loop, ev_io * stat_io, int revents) noexcept +{ + ServiceRecord *sr = (ServiceRecord *) stat_io->data; + sr->waiting_for_execstat = false; + + int exec_status; + int r = read(stat_io->fd, &exec_status, sizeof(int)); + close(stat_io->fd); + ev_io_stop(loop, stat_io); + + if (r != 0) { + // We read an errno code; exec() failed, and the service startup failed. + sr->pid = -1; + log(LogLevel::ERROR, sr->service_name, ": execution failed: ", strerror(exec_status)); + if (sr->service_state == ServiceState::STARTING) { + sr->failed_to_start(); + } + else if (sr->service_state == ServiceState::STOPPING) { + // Must be a scripted servce. We've logged the failure, but it's probably better + // not to leave the service in STARTED state: + sr->stopped(); + } + } + else { + // exec() succeeded. + if (sr->service_type == ServiceType::PROCESS) { + if (sr->service_state != ServiceState::STARTED) { + sr->started(); + } + } + + if (sr->pid == -1) { + // Somehow the process managed to complete before we even saw the status. + sr->handle_exit_status(); + } + } +} + +void ServiceRecord::start() noexcept +{ + if ((service_state == ServiceState::STARTING || service_state == ServiceState::STARTED) + && desired_state == ServiceState::STOPPED) { + // This service was starting, or started, but was set to be stopped. + // Cancel the stop (and continue starting/running). + notifyListeners(ServiceEvent::STOPCANCELLED); + } + + if (desired_state == ServiceState::STARTED && service_state != ServiceState::STOPPED) return; + + desired_state = ServiceState::STARTED; + + if (pinned_stopped) return; + + if (service_state != ServiceState::STOPPED) { + // We're already starting/started, or we are stopping and need to wait for + // that the complete. + if (service_state != ServiceState::STOPPING || ! can_interrupt_stop()) { + return; + } + // We're STOPPING, and that can be interrupted. Our dependencies might be STOPPING, + // but if so they are waiting (for us), so they too can be instantly returned to + // STARTING state. + } + + service_state = ServiceState::STARTING; + service_set->service_active(this); + + waiting_for_deps = true; + + // Ask dependencies to start, mark them as being waited on. + if (! startCheckDependencies(true)) { + return; + } + + // Actually start this service. + allDepsStarted(); +} + +void ServiceRecord::dependencyStarted() noexcept +{ + if (service_state != ServiceState::STARTING || ! waiting_for_deps) { + return; + } + + if (startCheckDependencies(false)) { + allDepsStarted(); + } +} + +bool ServiceRecord::startCheckDependencies(bool start_deps) noexcept +{ + bool all_deps_started = true; + + for (sr_iter i = depends_on.begin(); i != depends_on.end(); ++i) { + if ((*i)->service_state != ServiceState::STARTED) { + if (start_deps) { + all_deps_started = false; + (*i)->start(); + } + else { + return false; + } + } + } + + for (auto i = soft_deps.begin(); i != soft_deps.end(); ++i) { + ServiceRecord * to = i->getTo(); + if (start_deps) { + if (to->service_state != ServiceState::STARTED) { + to->start(); + i->waiting_on = true; + all_deps_started = false; + } + else { + i->waiting_on = false; + } + } + else if (i->waiting_on) { + if (to->service_state != ServiceState::STARTING) { + // Service has either started or is no longer starting + i->waiting_on = false; + } + else { + // We are still waiting on this service + return false; + } + } + } + + return all_deps_started; +} + +bool ServiceRecord::open_socket() noexcept +{ + if (socket_path.empty() || socket_fd != -1) { + // No socket, or already open + return true; + } + + const char * saddrname = socket_path.c_str(); + uint sockaddr_size = offsetof(struct sockaddr_un, sun_path) + socket_path.length() + 1; + + struct sockaddr_un * name = static_cast(malloc(sockaddr_size)); + if (name == nullptr) { + log(LogLevel::ERROR, service_name, ": Opening activation socket: out of memory"); + return false; + } + + // Un-link any stale socket. TODO: safety check? should at least confirm the path is a socket. + unlink(saddrname); + + name->sun_family = AF_UNIX; + strcpy(name->sun_path, saddrname); + + int sockfd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (sockfd == -1) { + log(LogLevel::ERROR, service_name, ": Error creating activation socket: ", strerror(errno)); + free(name); + return false; + } + + if (bind(sockfd, (struct sockaddr *) name, sockaddr_size) == -1) { + log(LogLevel::ERROR, service_name, ": Error binding activation socket: ", strerror(errno)); + close(sockfd); + free(name); + return false; + } + + free(name); + + // POSIX (1003.1, 2013) says that fchown and fchmod don't necesarily work on sockets. We have to + // use chown and chmod instead. + if (chown(saddrname, socket_uid, socket_gid)) { + log(LogLevel::ERROR, service_name, ": Error setting activation socket owner/group: ", strerror(errno)); + close(sockfd); + return false; + } + + if (chmod(saddrname, socket_perms) == -1) { + log(LogLevel::ERROR, service_name, ": Error setting activation socket permissions: ", strerror(errno)); + close(sockfd); + return false; + } + + if (listen(sockfd, 128) == -1) { // 128 "seems reasonable". + log(LogLevel::ERROR, ": Error listening on activation socket: ", strerror(errno)); + close(sockfd); + return false; + } + + socket_fd = sockfd; + return true; +} + +void ServiceRecord::allDepsStarted(bool has_console) noexcept +{ + if (onstart_flags.runs_on_console && ! has_console) { + waiting_for_deps = true; + queueForConsole(); + return; + } + + waiting_for_deps = false; + + if (! open_socket()) { + failed_to_start(); + } + + if (service_type == ServiceType::PROCESS || service_type == ServiceType::BGPROCESS + || service_type == ServiceType::SCRIPTED) { + bool start_success = start_ps_process(); + if (! start_success) { + failed_to_start(); + } + } + else { + // "internal" service + started(); + } +} + +void ServiceRecord::acquiredConsole() noexcept +{ + if (service_state != ServiceState::STARTING) { + // We got the console but no longer want it. + releaseConsole(); + } + else if (startCheckDependencies(false)) { + log_to_console = false; + allDepsStarted(true); + } + else { + // We got the console but can't use it yet. + releaseConsole(); + } +} + +bool ServiceRecord::read_pid_file() noexcept +{ + const char *pid_file_c = pid_file.c_str(); + int fd = open(pid_file_c, O_CLOEXEC); + if (fd != -1) { + char pidbuf[21]; // just enought to hold any 64-bit integer + int r = read(fd, pidbuf, 20); + if (r > 0) { + pidbuf[r] = 0; // store nul terminator + pid = std::atoi(pidbuf); + if (kill(pid, 0) == 0) { + ev_child_init(&child_listener, process_child_callback, pid, 0); + child_listener.data = this; + ev_child_start(ev_default_loop(EVFLAG_AUTO), &child_listener); + } + else { + log(LogLevel::ERROR, service_name, ": pid read from pidfile (", pid, ") is not valid"); + pid = -1; + close(fd); + return false; + } + } + close(fd); + return true; + } + else { + log(LogLevel::ERROR, service_name, ": read pid file: ", strerror(errno)); + return false; + } +} + +void ServiceRecord::started() noexcept +{ + if (onstart_flags.runs_on_console && (service_type == ServiceType::SCRIPTED || service_type == ServiceType::BGPROCESS)) { + tcsetpgrp(0, getpgrp()); + releaseConsole(); + } + + if (service_type == ServiceType::BGPROCESS && pid_file.length() != 0) { + if (! read_pid_file()) { + failed_to_start(); + return; + } + } + + logServiceStarted(service_name); + service_state = ServiceState::STARTED; + notifyListeners(ServiceEvent::STARTED); + + if (onstart_flags.rw_ready) { + open_control_socket(ev_default_loop(EVFLAG_AUTO)); + } + + if (force_stop || desired_state == ServiceState::STOPPED) { + // We must now stop. + bool do_restart = (desired_state != ServiceState::STOPPED); + stop(); + if (do_restart) { + start(); + } + return; + } + + // Notify any dependents whose desired state is STARTED: + for (auto i = dependents.begin(); i != dependents.end(); i++) { + (*i)->dependencyStarted(); + } + for (auto i = soft_dpts.begin(); i != soft_dpts.end(); i++) { + (*i)->getFrom()->dependencyStarted(); + } +} + +void ServiceRecord::failed_to_start() +{ + if (onstart_flags.runs_on_console) { + tcsetpgrp(0, getpgrp()); + releaseConsole(); + } + + logServiceFailed(service_name); + service_state = ServiceState::STOPPED; + desired_state = ServiceState::STOPPED; + service_set->service_inactive(this); + notifyListeners(ServiceEvent::FAILEDSTART); + + // failure to start + // Cancel start of dependents: + for (sr_iter i = dependents.begin(); i != dependents.end(); i++) { + if ((*i)->service_state == ServiceState::STARTING) { + (*i)->failed_dependency(); + } + } + for (auto i = soft_dpts.begin(); i != soft_dpts.end(); i++) { + // We can send 'start', because this is only a soft dependency. + // Our startup failure means that they don't have to wait for us. + (*i)->getFrom()->dependencyStarted(); + } +} + +bool ServiceRecord::start_ps_process() noexcept +{ + return start_ps_process(exec_arg_parts, onstart_flags.runs_on_console); +} + +bool ServiceRecord::start_ps_process(const std::vector &cmd, bool on_console) noexcept +{ + // In general, you can't tell whether fork/exec is successful. We use a pipe to communicate + // success/failure from the child to the parent. The pipe is set CLOEXEC so a successful + // exec closes the pipe, and the parent sees EOF. If the exec is unsuccessful, the errno + // is written to the pipe, and the parent can read it. + + int pipefd[2]; + if (pipe2(pipefd, O_CLOEXEC)) { + // TODO log error + return false; + } + + // Set up the argument array and other data now (before fork), in case memory allocation fails. + + auto args = cmd.data(); + + const char * logfile = this->logfile.c_str(); + if (*logfile == 0) { + logfile = "/dev/null"; + } + + // TODO make sure pipefd's are not 0/1/2 (STDIN/OUT/ERR) - if they are, dup them + // until they are not. + + pid_t forkpid = fork(); + if (forkpid == -1) { + // TODO log error + close(pipefd[0]); + close(pipefd[1]); + return false; + } + + // If the console already has a session leader, presumably it is us. On the other hand + // if it has no session leader, and we don't create one, then control inputs such as + // ^C will have no effect. + bool do_set_ctty = (tcgetsid(0) == -1); + + if (forkpid == 0) { + // Child process. Must not allocate memory (or otherwise risk throwing any exception) + // from here until exit(). + ev_default_destroy(); // won't need that on this side, free up fds. + + constexpr int bufsz = ((CHAR_BIT * sizeof(pid_t) - 1) / 3 + 2) + 11; + // "LISTEN_PID=" - 11 characters + char nbuf[bufsz]; + + if (socket_fd != -1) { + dup2(socket_fd, 3); + if (socket_fd != 3) { + close(socket_fd); + } + + if (putenv(const_cast("LISTEN_FDS=1"))) goto failure_out; + + snprintf(nbuf, bufsz, "LISTEN_PID=%jd", static_cast(getpid())); + + if (putenv(nbuf)) goto failure_out; + } + + if (! on_console) { + // Re-set stdin, stdout, stderr + close(0); close(1); close(2); + + // TODO rethink this logic. If we open it at not-0, shouldn't we just dup it to 0?: + if (open("/dev/null", O_RDONLY) == 0) { + // stdin = 0. That's what we should have; proceed with opening + // stdout and stderr. + open(logfile, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR); + dup2(1, 2); + } + } + else { + // "run on console" - run as a foreground job on the terminal/console device + if (do_set_ctty) { + setsid(); + ioctl(0, TIOCSCTTY, 0); + } + setpgid(0,0); + tcsetpgrp(0, getpgrp()); + + // TODO disable suspend (^Z)? (via tcsetattr) + // (should be done before TIOCSCTTY) + } + + execvp(exec_arg_parts[0], const_cast(args)); + + // If we got here, the exec failed: + failure_out: + int exec_status = errno; + write(pipefd[1], &exec_status, sizeof(int)); + exit(0); + } + else { + // Parent process + close(pipefd[1]); // close the 'other end' fd + + pid = forkpid; + + // Listen for status + ev_io_init(&child_status_listener, process_child_status, pipefd[0], EV_READ); + child_status_listener.data = this; + ev_io_start(ev_default_loop(EVFLAG_AUTO), &child_status_listener); + + // Add a process listener so we can detect when the + // service stops + ev_child_init(&child_listener, process_child_callback, pid, 0); + child_listener.data = this; + ev_child_start(ev_default_loop(EVFLAG_AUTO), &child_listener); + waiting_for_execstat = true; + return true; + } +} + +// Mark this and all dependent services as force-stopped. +void ServiceRecord::forceStop() noexcept +{ + if (service_state != ServiceState::STOPPED) { + force_stop = true; + for (sr_iter i = dependents.begin(); i != dependents.end(); i++) { + (*i)->forceStop(); + } + stop(); + + // We don't want to force stop soft dependencies, however. + } +} + +// A dependency of this service failed to start. +// Only called when state == STARTING. +void ServiceRecord::failed_dependency() +{ + desired_state = ServiceState::STOPPED; + + // Presumably, we were starting. So now we're not. + service_state = ServiceState::STOPPED; + service_set->service_inactive(this); + logServiceFailed(service_name); + + // Notify dependents of this service also + for (auto i = dependents.begin(); i != dependents.end(); i++) { + if ((*i)->service_state == ServiceState::STARTING) { + (*i)->failed_dependency(); + } + } + for (auto i = soft_dpts.begin(); i != soft_dpts.end(); i++) { + // It's a soft dependency, so send them 'started' rather than + // 'failed dep'. + (*i)->getFrom()->dependencyStarted(); + } +} + +void ServiceRecord::dependentStopped() noexcept +{ + if (service_state == ServiceState::STOPPING) { + // Check the other dependents before we stop. + if (stopCheckDependents()) { + allDepsStopped(); + } + } +} + +void ServiceRecord::stop() noexcept +{ + if ((service_state == ServiceState::STOPPING || service_state == ServiceState::STOPPED) + && desired_state == ServiceState::STARTED) { + // The service *was* stopped/stopping, but it was going to restart. + // Now, we'll cancel the restart. + notifyListeners(ServiceEvent::STARTCANCELLED); + } + + if (desired_state == ServiceState::STOPPED && service_state != ServiceState::STARTED) return; + + desired_state = ServiceState::STOPPED; + + if (pinned_started) return; + + if (service_state != ServiceState::STARTED) { + if (service_state == ServiceState::STARTING) { + if (! can_interrupt_start()) { + // Well this is awkward: we're going to have to continue + // starting, but we don't want any dependents to think that + // they are still waiting to start. + // Make sure they remain stopped: + stopDependents(); + return; + } + + // Reaching this point, we have can_interrupt_start() == true. So, + // we can stop. Dependents might be starting, but they must be + // waiting on us, so they should also be immediately stoppable. + // Fall through to below. + } + else { + // If we're starting we need to wait for that to complete. + // If we're already stopping/stopped there's nothing to do. + return; + } + } + + service_state = ServiceState::STOPPING; + waiting_for_deps = true; + + // If we get here, we are in STARTED state; stop all dependents. + if (stopDependents()) { + allDepsStopped(); + } +} + +bool ServiceRecord::stopCheckDependents() noexcept +{ + bool all_deps_stopped = true; + for (sr_iter i = dependents.begin(); i != dependents.end(); ++i) { + if ((*i)->service_state != ServiceState::STOPPED) { + all_deps_stopped = false; + break; + } + } + + return all_deps_stopped; +} + +bool ServiceRecord::stopDependents() noexcept +{ + bool all_deps_stopped = true; + for (sr_iter i = dependents.begin(); i != dependents.end(); ++i) { + if ((*i)->service_state != ServiceState::STOPPED) { + all_deps_stopped = false; + (*i)->stop(); + } + } + + return all_deps_stopped; +} + +// Dependency stopped or is stopping; we must stop too. +void ServiceRecord::allDepsStopped() +{ + waiting_for_deps = false; + if (service_type == ServiceType::PROCESS || service_type == ServiceType::BGPROCESS) { + if (pid != -1) { + // The process is still kicking on - must actually kill it. + if (! onstart_flags.no_sigterm) { + kill(pid, SIGTERM); + } + if (term_signal != -1) { + kill(pid, term_signal); + } + // Now we wait; the rest is done in process_child_callback + } + else { + // The process is already dead. + stopped(); + } + } + else if (service_type == ServiceType::SCRIPTED) { + // Scripted service. + if (stop_command.length() == 0) { + stopped(); + } + else if (! start_ps_process(stop_arg_parts, false)) { + // Couldn't execute stop script, but there's not much we can do: + stopped(); + } + } + else { + stopped(); + } +} + +void ServiceRecord::pinStart() noexcept +{ + start(); + pinned_started = true; +} + +void ServiceRecord::pinStop() noexcept +{ + stop(); + pinned_stopped = true; +} + +void ServiceRecord::unpin() noexcept +{ + if (pinned_started) { + pinned_started = false; + if (desired_state == ServiceState::STOPPED) { + stop(); + } + } + if (pinned_stopped) { + pinned_stopped = false; + if (desired_state == ServiceState::STARTED) { + start(); + } + } +} + +void ServiceRecord::queueForConsole() noexcept +{ + next_for_console = nullptr; + auto tail = service_set->consoleQueueTail(this); + if (tail == nullptr) { + acquiredConsole(); + } + else { + tail->next_for_console = this; + } +} + +void ServiceRecord::releaseConsole() noexcept +{ + log_to_console = true; + if (next_for_console != nullptr) { + next_for_console->acquiredConsole(); + } + else { + service_set->consoleQueueTail(nullptr); + } +} + +void ServiceSet::service_active(ServiceRecord *sr) noexcept +{ + active_services++; +} + +void ServiceSet::service_inactive(ServiceRecord *sr) noexcept +{ + active_services--; +} diff --git a/src/service.h b/src/service.h new file mode 100644 index 0000000..cbeab72 --- /dev/null +++ b/src/service.h @@ -0,0 +1,547 @@ +#ifndef SERVICE_H +#define SERVICE_H + +#include +#include +#include +#include +#include +#include "ev.h" +#include "control.h" +#include "service-listener.h" +#include "service-constants.h" + +/* + * Possible 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 + * from leaving that state until it is unpinned. + * + * The total state is a combination of the two, current and desired: + * STOPPED/STOPPED : stopped and will remain stopped + * STOPPED/STARTED : stopped (pinned), must be unpinned to start + * STARTING/STARTED : starting, but not yet started. Dependencies may also be starting. + * STARTING/STOPPED : as above, but the service will be stopped again as soon as it has + * completed startup. + * STARTED/STARTED : running and will continue running. + * STARTED/STOPPED : started (pinned), must be unpinned to stop + * STOPPING/STOPPED : stopping and will stop. Dependents may be stopping. + * STOPPING/STARTED : as above, but the service will be re-started again once it stops. + * + * A scripted service is in the STARTING/STOPPING states during the script execution. + * 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. + */ + +struct OnstartFlags { + bool rw_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), + no_sigterm(false), runs_on_console(false) + { + } +}; + +// Exception while loading a service +class ServiceLoadExc +{ + public: + std::string serviceName; + const char *excDescription; + + protected: + ServiceLoadExc(std::string serviceName) noexcept + : serviceName(serviceName) + { + } +}; + +class ServiceNotFound : public ServiceLoadExc +{ + public: + ServiceNotFound(std::string serviceName) noexcept + : ServiceLoadExc(serviceName) + { + excDescription = "Service description not found."; + } +}; + +class ServiceCyclicDependency : public ServiceLoadExc +{ + public: + ServiceCyclicDependency(std::string serviceName) noexcept + : ServiceLoadExc(serviceName) + { + excDescription = "Has cyclic dependency."; + } +}; + +class ServiceDescriptionExc : public ServiceLoadExc +{ + public: + std::string extraInfo; + + ServiceDescriptionExc(std::string serviceName, std::string extraInfo) noexcept + : ServiceLoadExc(serviceName), extraInfo(extraInfo) + { + excDescription = extraInfo.c_str(); + } +}; + +class ServiceRecord; // forward declaration +class ServiceSet; // forward declaration + +/* Service dependency record */ +class ServiceDep +{ + ServiceRecord * from; + ServiceRecord * to; + + public: + /* Whether the 'from' service is waiting for the 'to' service to start */ + bool waiting_on; + + ServiceDep(ServiceRecord * from, ServiceRecord * to) noexcept : from(from), to(to), waiting_on(false) + { } + + ServiceRecord * getFrom() noexcept + { + return from; + } + + ServiceRecord * getTo() noexcept + { + return to; + } +}; + +// Given a string and a list of pairs of (start,end) indices for each argument in that string, +// store a null terminator for the argument. Return a `char *` vector containing the beginning +// of each argument and a trailing nullptr. (The returned array is invalidated if the string is later modified). +static std::vector separate_args(std::string &s, std::list> &arg_indices) +{ + std::vector r; + r.reserve(arg_indices.size() + 1); + + // First store nul terminator for each part: + for (auto index_pair : arg_indices) { + if (index_pair.second < s.length()) { + s[index_pair.second] = 0; + } + } + + // Now we can get the C string (c_str) and store offsets into it: + const char * cstr = s.c_str(); + for (auto index_pair : arg_indices) { + r.push_back(cstr + index_pair.first); + } + r.push_back(nullptr); + return r; +} + + +class ServiceRecord +{ + typedef std::string string; + + string service_name; + ServiceType service_type; /* ServiceType::DUMMY, PROCESS, SCRIPTED, INTERNAL */ + ServiceState service_state = ServiceState::STOPPED; /* ServiceState::STOPPED, STARTING, STARTED, STOPPING */ + ServiceState desired_state = ServiceState::STOPPED; /* ServiceState::STOPPED / STARTED */ + + string program_name; /* storage for program/script and arguments */ + std::vector exec_arg_parts; /* pointer to each argument/part of the program_name */ + + string stop_command; /* storage for stop program/script and arguments */ + std::vector stop_arg_parts; /* pointer to each argument/part of the stop_command */ + + string pid_file; + + OnstartFlags onstart_flags; + + string logfile; // log file name, empty string specifies /dev/null + bool auto_restart : 1; // whether to restart this (process) if it dies unexpectedly + bool smooth_recovery : 1; // whether the service process can restart without bringing down service + + bool pinned_stopped : 1; + bool pinned_started : 1; + bool waiting_for_deps : 1; // if STARTING, whether we are waiting for dependencies (inc console) to start + bool waiting_for_execstat : 1; // if we are waiting for exec status after fork() + bool doing_recovery : 1; // if we are currently recovering a BGPROCESS (restarting process, while + // holding STARTED service state) + + typedef std::list sr_list; + typedef sr_list::iterator sr_iter; + + // list of soft dependencies + typedef std::list softdep_list; + + // list of soft dependents + typedef std::list softdpt_list; + + sr_list depends_on; // services this one depends on + sr_list dependents; // services depending on this one + softdep_list soft_deps; // services this one depends on via a soft dependency + softdpt_list soft_dpts; // services depending on this one via a soft dependency + + // unsigned wait_count; /* if we are waiting for dependents/dependencies to + // start/stop, this is how many we're waiting for */ + + 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 listeners; + + // Process services: + bool force_stop; // true if the service must actually stop. This is the + // case if for example the process dies; the service, + // and all its dependencies, MUST be stopped. + + int term_signal = -1; // signal to use for process termination + + string socket_path; // path to the socket for socket-activation service + int socket_perms; // socket permissions ("mode") + uid_t socket_uid = -1; // socket user id or -1 + gid_t socket_gid = -1; // sockget group id or -1 + + // Implementation details + + pid_t pid = -1; // PID of the process. If state is STARTING or STOPPING, + // this is PID of the service script; otherwise it is the + // PID of the process itself (process service). + 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; + + // All dependents have stopped. + void allDepsStopped(); + + // Service has actually stopped (includes having all dependents + // reaching STOPPED state). + void stopped() noexcept; + + // Service has successfully started + void started() noexcept; + + // Service failed to start + void failed_to_start(); + + // A dependency of this service failed to start. + void failed_dependency(); + + // For process services, start the process, return true on success + bool start_ps_process() noexcept; + bool start_ps_process(const std::vector &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, + int revents) noexcept; + + static void process_child_status(struct ev_loop *loop, ev_io * stat_io, + int revents) noexcept; + + void handle_exit_status() noexcept; + + // A dependency has reached STARTED state + void dependencyStarted() noexcept; + + void allDepsStarted(bool haveConsole = false) noexcept; + + // Read the pid-file, return false on failure + bool read_pid_file() noexcept; + + // Open the activation socket, return false on failure + bool open_socket() noexcept; + + // Check whether dependencies have started, and optionally ask them to start + bool startCheckDependencies(bool do_start) noexcept; + + // Whether a STARTING service can immediately transition to STOPPED (as opposed to + // having to wait for it reach STARTED and then go through STOPPING). + bool can_interrupt_start() noexcept + { + return waiting_for_deps; + } + + // Whether a STOPPING service can immediately transition to STARTED. + bool can_interrupt_stop() noexcept + { + return waiting_for_deps && ! force_stop; + } + + // A dependent has reached STOPPED state + void dependentStopped() noexcept; + + // check if all dependents have stopped + bool stopCheckDependents() 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 notifyListeners(ServiceEvent event) noexcept + { + for (auto l : listeners) { + l->serviceEvent(this, event); + } + } + + // 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; + + public: + + ServiceRecord(ServiceSet *set, string name) + : 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), force_stop(false) + { + service_set = set; + service_name = name; + service_type = ServiceType::DUMMY; + } + + ServiceRecord(ServiceSet *set, string name, ServiceType service_type, string &&command, std::list> &command_offsets, + sr_list * pdepends_on, sr_list * pdepends_soft) + : ServiceRecord(set, name) + { + service_set = set; + service_name = name; + this->service_type = service_type; + this->depends_on = std::move(*pdepends_on); + + program_name = command; + exec_arg_parts = separate_args(program_name, command_offsets); + + for (sr_iter i = depends_on.begin(); i != depends_on.end(); ++i) { + (*i)->dependents.push_back(this); + } + + // Soft dependencies + auto b_iter = soft_deps.end(); + for (sr_iter i = pdepends_soft->begin(); i != pdepends_soft->end(); ++i) { + b_iter = soft_deps.emplace(b_iter, this, *i); + (*i)->soft_dpts.push_back(&(*b_iter)); + ++b_iter; + } + } + + // TODO write a destructor + + // Set the stop command and arguments (may throw std::bad_alloc) + void setStopCommand(std::string command, std::list> &stop_command_offsets) + { + stop_command = command; + stop_arg_parts = separate_args(stop_command, stop_command_offsets); + } + + // Get the current service state. + ServiceState getState() noexcept + { + return service_state; + } + + // Get the target (aka desired) state. + ServiceState getTargetState() noexcept + { + return desired_state; + } + + // Set logfile, should be done before service is started + void setLogfile(string logfile) + { + this->logfile = logfile; + } + + // Set whether this service should automatically restart when it dies + void setAutoRestart(bool auto_restart) noexcept + { + this->auto_restart = auto_restart; + } + + void setSmoothRecovery(bool smooth_recovery) noexcept + { + this->smooth_recovery = smooth_recovery; + } + + // Set "on start" flags (commands) + void setOnstartFlags(OnstartFlags flags) noexcept + { + this->onstart_flags = flags; + } + + // Set an additional signal (other than SIGTERM) to be used to terminate the process + void setExtraTerminationSignal(int signo) noexcept + { + this->term_signal = signo; + } + + void set_pid_file(string &&pid_file) noexcept + { + this->pid_file = pid_file; + } + + void set_socket_details(string &&socket_path, int socket_perms, uid_t socket_uid, uid_t socket_gid) noexcept + { + this->socket_path = socket_path; + this->socket_perms = socket_perms; + this->socket_uid = socket_uid; + this->socket_gid = socket_gid; + } + + const char *getServiceName() const noexcept { return service_name.c_str(); } + ServiceState getState() const noexcept { return service_state; } + + void start() noexcept; // start the service + void stop() noexcept; // stop the service + + void pinStart() noexcept; // start the service and pin it + void pinStop() noexcept; // stop the service and pin it + void unpin() noexcept; // unpin the service + + bool isDummy() noexcept + { + return service_type == ServiceType::DUMMY; + } + + // Add a listener. A listener must only be added once. May throw std::bad_alloc. + void addListener(ServiceListener * listener) + { + listeners.insert(listener); + } + + // Remove a listener. + void removeListener(ServiceListener * listener) noexcept + { + listeners.erase(listener); + } +}; + + +class ServiceSet +{ + int active_services; + std::list records; + const char *service_dir; // directory containing service descriptions + bool restart_enabled; // whether automatic restart is enabled (allowed) + + ShutdownType shutdown_type = ShutdownType::CONTINUE; // Shutdown type, if stopping + + ServiceRecord * console_queue_tail = nullptr; // last record in console queue + + // Private methods + + // Load a service description, and dependencies, if there is no existing + // record for the given name. + // Throws: + // ServiceLoadException (or subclass) on problem with service description + // std::bad_alloc on out-of-memory condition + ServiceRecord *loadServiceRecord(const char *name); + + // Public + + public: + ServiceSet(const char *service_dir) + { + this->service_dir = service_dir; + active_services = 0; + restart_enabled = true; + } + + // Start the service with the given name. The named service will begin + // transition to the 'started' state. + // + // Throws a ServiceLoadException (or subclass) if the service description + // cannot be loaded or is invalid; + // Throws std::bad_alloc if out of memory. + void startService(const char *name); + + // Locate an existing service record. + ServiceRecord *findService(const std::string &name) noexcept; + + // Find a loaded service record, or load it if it is not loaded. + // Throws: + // ServiceLoadException (or subclass) on problem with service description + // std::bad_alloc on out-of-memory condition + ServiceRecord *loadService(const std::string &name) + { + ServiceRecord *record = findService(name); + if (record == nullptr) { + record = loadServiceRecord(name.c_str()); + } + return record; + } + + // Stop the service with the given name. The named service will begin + // transition to the 'stopped' state. + void stopService(const std::string &name) noexcept; + + // Set the console queue tail (returns previous tail) + ServiceRecord * consoleQueueTail(ServiceRecord * newTail) noexcept + { + auto prev_tail = console_queue_tail; + console_queue_tail = newTail; + return prev_tail; + } + + // 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; + + // Notification from service that it is inactive (STOPPED) + // Only to be called on the transition from active to inactive. + void service_inactive(ServiceRecord *) noexcept; + + // Find out how many services are active (starting, running or stopping, + // but not stopped). + int count_active_services() noexcept + { + return active_services; + } + + void stop_all_services(ShutdownType type = ShutdownType::HALT) noexcept + { + restart_enabled = false; + shutdown_type = type; + for (std::list::iterator i = records.begin(); i != records.end(); ++i) { + (*i)->stop(); + (*i)->unpin(); + } + } + + void set_auto_restart(bool restart) noexcept + { + restart_enabled = restart; + } + + bool get_auto_restart() noexcept + { + return restart_enabled; + } + + ShutdownType getShutdownType() noexcept + { + return shutdown_type; + } +}; + +#endif diff --git a/src/shutdown.cc b/src/shutdown.cc new file mode 100644 index 0000000..1d46ab2 --- /dev/null +++ b/src/shutdown.cc @@ -0,0 +1,197 @@ +// #include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "control-cmds.h" +#include "service-constants.h" + +// shutdown: shut down the system +// This utility communicates with the dinit daemon via a unix socket (/dev/initctl). + +void do_system_shutdown(ShutdownType shutdown_type); +static void unmount_disks(); +static void swap_off(); + +int main(int argc, char **argv) +{ + using namespace std; + + bool show_help = false; + bool sys_shutdown = false; + + auto shutdown_type = ShutdownType::POWEROFF; + + for (int i = 1; i < argc; i++) { + if (argv[i][0] == '-') { + if (strcmp(argv[i], "--help") == 0) { + show_help = true; + break; + } + + if (strcmp(argv[i], "--system") == 0) { + sys_shutdown = true; + } + else if (strcmp(argv[i], "-r") == 0) { + shutdown_type = ShutdownType::REBOOT; + } + else if (strcmp(argv[i], "-h") == 0) { + shutdown_type = ShutdownType::HALT; + } + else if (strcmp(argv[i], "-p") == 0) { + shutdown_type = ShutdownType::POWEROFF; + } + else { + cerr << "Unrecognized command-line parameter: " << argv[i] << endl; + return 1; + } + } + else { + // time argument? TODO + show_help = true; + } + } + + if (show_help) { + cout << "dinit-shutdown : shutdown the system" << endl; + cout << " --help : show this help" << endl; + cout << " -r : reboot" << endl; + cout << " -h : halt system" << endl; + cout << " -p : power down (default)" << endl; + cout << " --system : perform shutdown immediately, instead of issuing shutdown" << endl; + cout << " command to the init program. Not recommended for use" << endl; + cout << " by users." << endl; + return 1; + } + + if (sys_shutdown) { + do_system_shutdown(shutdown_type); + return 0; + } + + int socknum = socket(AF_UNIX, SOCK_STREAM, 0); + if (socknum == -1) { + perror("socket"); + return 1; + } + + const char *naddr = "/dev/dinitctl"; + + struct sockaddr_un name; + name.sun_family = AF_UNIX; + strcpy(name.sun_path, naddr); + int sunlen = offsetof(struct sockaddr_un, sun_path) + strlen(naddr) + 1; // family, (string), nul + + int connr = connect(socknum, (struct sockaddr *) &name, sunlen); + if (connr == -1) { + perror("connect"); + return 1; + } + + // Build buffer; + //uint16_t sname_len = strlen(service_name); + int bufsize = 2; + char * buf = new char[bufsize]; + + buf[0] = DINIT_CP_SHUTDOWN; + buf[1] = static_cast(shutdown_type); + + cout << "Issuing shutdown command..." << endl; // DAV + + // TODO make sure to write the whole buffer + int r = write(socknum, buf, bufsize); + if (r == -1) { + perror("write"); + } + + // Wait for ACK/NACK + r = read(socknum, buf, 1); + // TODO: check result + + return 0; +} + +void do_system_shutdown(ShutdownType shutdown_type) +{ + using namespace std; + + int reboot_type = 0; + if (shutdown_type == ShutdownType::REBOOT) reboot_type = RB_AUTOBOOT; + else if (shutdown_type == ShutdownType::POWEROFF) reboot_type = RB_POWER_OFF; + else reboot_type = RB_HALT_SYSTEM; + + // Write to console rather than any terminal, since we lose the terminal it seems: + close(STDOUT_FILENO); + int consfd = open("/dev/console", O_WRONLY); + if (consfd != STDOUT_FILENO) { + dup2(consfd, STDOUT_FILENO); + } + + cout << "Sending TERM/KILL to all processes..." << endl; // DAV + + // Send TERM/KILL to all (remaining) processes + kill(-1, SIGTERM); + sleep(1); + kill(-1, SIGKILL); + + // cout << "Sending QUIT to init..." << endl; // DAV + + // Tell init to exec reboot: + // TODO what if it's not PID=1? probably should have dinit pass us its PID + // kill(1, SIGQUIT); + + // TODO can we wait somehow for above to work? + // maybe have a pipe/socket and we read from our end... + + // TODO: close all ancillary file descriptors. + + // perform shutdown + cout << "Turning off swap..." << endl; + swap_off(); + cout << "Unmounting disks..." << endl; + unmount_disks(); + sync(); + + cout << "Issuing shutdown via kernel..." << endl; + reboot(reboot_type); +} + +static void unmount_disks() +{ + pid_t chpid = fork(); + if (chpid == 0) { + // umount -a -r + // -a : all filesystems (except proc) + // -r : mount readonly if can't unmount + execl("/bin/umount", "/bin/umount", "-a", "-r", nullptr); + } + else if (chpid > 0) { + int status; + waitpid(chpid, &status, 0); + } +} + +static void swap_off() +{ + pid_t chpid = fork(); + if (chpid == 0) { + // swapoff -a + execl("/sbin/swapoff", "/sbin/swapoff", "-a", nullptr); + } + else if (chpid > 0) { + int status; + waitpid(chpid, &status, 0); + } +}