+++ /dev/null
--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
+++ /dev/null
-// 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;
+++ /dev/null
-#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<ShutdownType>(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<char> rp_buf;
- rp_buf.reserve(7);
- rp_buf.push_back(DINIT_RP_SERVICERECORD);
- rp_buf.push_back(static_cast<char>(record->getState()));
- for (int i = 0; i < (int) sizeof(handle); i++) {
- rp_buf.push_back(*(((char *) &handle) + i));
- }
- rp_buf.push_back(static_cast<char>(record->getTargetState()));
- if (! queuePacket(std::move(rp_buf))) return;
- }
- else {
- std::vector<char> 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<handle_t>::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<char> &&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<char> & 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--;
-}
+++ /dev/null
-#ifndef DINIT_CONTROL_H
-#define DINIT_CONTROL_H
-
-#include <list>
-#include <vector>
-#include <unordered_map>
-#include <limits>
-
-#include <unistd.h>
-#include <ev++.h>
-
-#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 <typename T> using list = std::list<T>;
- template <typename T> using vector = std::vector<T>;
-
- // A mapping between service records and their associated numerical identifier used
- // in communction
- using handle_t = uint32_t;
- std::unordered_multimap<ServiceRecord *, handle_t> serviceKeyMap;
- std::unordered_map<handle_t, ServiceRecord *> keyServiceMap;
-
- // Buffer for outgoing packets. Each outgoing back is represented as a vector<char>.
- list<vector<char>> 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<char> &&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<char> 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<char>(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
+++ /dev/null
-#ifndef CPBUFFER_H
-#define CPBUFFER_H
-
-#include <cstring>
-
-// 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
+++ /dev/null
-#include <iostream>
-#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;
- }
-}
+++ /dev/null
-#ifndef DINIT_LOG_H
-#define DINIT_LOG_H
-
-// Logging for Dinit
-
-#include <string>
-#include <cstdio>
-#include <climits>
-
-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 <typename A> static inline void logParts(A a) noexcept
- {
- logMsgEnd(a);
- }
-
- template <typename A, typename ...B> static inline void logParts(A a, B... b) noexcept
- {
- logMsgPart(a);
- logParts(b...);
- }
-}
-
-// Variadic 'log' method.
-template <typename A, typename ...B> static inline void log(LogLevel lvl, A a, B ...b) noexcept
-{
- logMsgBegin(lvl, a);
- dinit_log::logParts(b...);
-}
-
-#endif
+++ /dev/null
-#include <cstdio>
-#include <cstddef>
-#include <cstring>
-#include <string>
-#include <iostream>
-#include <system_error>
-
-#include <sys/types.h>
-#include <sys/socket.h>
-#include <sys/un.h>
-#include <unistd.h>
-#include <pwd.h>
-
-#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 << " <service-name> : 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<ServiceState>(rbuffer[1]);
- target_state = static_cast<ServiceState>(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<ServiceEvent>(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;
-}
+++ /dev/null
-#include <iostream>
-#include <list>
-#include <cstring>
-#include <csignal>
-#include <cstddef>
-#include <cstdlib>
-
-#include <sys/types.h>
-#include <sys/stat.h>
-#include <sys/un.h>
-#include <sys/socket.h>
-#include <unistd.h>
-#include <fcntl.h>
-#include <pwd.h>
-
-#include "service.h"
-#include "ev++.h"
-#include "control.h"
-#include "dinit-log.h"
-
-#ifdef __linux__
-#include <sys/klog.h>
-#include <sys/reboot.h>
-#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<const char *> 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 <dir>, -d <dir> : set base directory for service description files (-d <dir>)" << endl;
- cout << " <service-name> : start service with name <service-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<const char *>::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<sockaddr_un *>(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();
-}
+++ /dev/null
-#!/bin/sh
-# "halt" command actually executes the more useful "power off".
-shutdown -p "$@"
+++ /dev/null
-#include <algorithm>
-#include <string>
-#include <fstream>
-#include <locale>
-#include <iostream>
-#include <limits>
-
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <pwd.h>
-#include <grp.h>
-
-#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<char> & facet = use_facet<ctype<char> >(locale::classic());
-
- string rval;
- // Allow alphabetical characters, and dash (-) in setting name
- while (i != end && (*i == '-' || facet.is(ctype<char>::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 <int,int> 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<std::pair<unsigned,unsigned>> * 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<uid_t>::max() <= (uintmax_t)std::numeric_limits<unsigned long long>::max(), "uid_t is too large");
- unsigned long long v = std::stoull(param, &ind, 0);
- if (v > static_cast<unsigned long long>(std::numeric_limits<uid_t>::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<unsigned long long>(std::numeric_limits<gid_t>::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<pair<unsigned,unsigned>> command_offsets;
- string stop_command;
- list<pair<unsigned,unsigned>> stop_command_offsets;
- string pid_file;
-
- ServiceType service_type = ServiceType::PROCESS;
- std::list<ServiceRecord *> depends_on;
- std::list<ServiceRecord *> 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<std::pair<unsigned,unsigned>> 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;
- }
-}
+++ /dev/null
-#!/bin/sh
-shutdown -r "$@"
+++ /dev/null
-#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
+++ /dev/null
-#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
+++ /dev/null
-#include <cstring>
-#include <cerrno>
-#include <sstream>
-#include <iterator>
-#include <memory>
-#include <cstddef>
-
-#include <sys/types.h>
-#include <sys/stat.h>
-#include <sys/ioctl.h>
-#include <sys/un.h>
-#include <sys/socket.h>
-#include <fcntl.h>
-#include <unistd.h>
-#include <termios.h>
-
-#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<ServiceRecord *> & records,
- const char *name) noexcept
-{
- using std::list;
- list<ServiceRecord *>::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<sockaddr_un *>(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<const char *> &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<char *>("LISTEN_FDS=1"))) goto failure_out;
-
- snprintf(nbuf, bufsz, "LISTEN_PID=%jd", static_cast<intmax_t>(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<char **>(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--;
-}
+++ /dev/null
-#ifndef SERVICE_H
-#define SERVICE_H
-
-#include <string>
-#include <list>
-#include <vector>
-#include <csignal>
-#include <unordered_set>
-#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<const char *> separate_args(std::string &s, std::list<std::pair<unsigned,unsigned>> &arg_indices)
-{
- std::vector<const char *> 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<const char *> exec_arg_parts; /* pointer to each argument/part of the program_name */
-
- string stop_command; /* storage for stop program/script and arguments */
- std::vector<const char *> 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<ServiceRecord *> sr_list;
- typedef sr_list::iterator sr_iter;
-
- // list of soft dependencies
- typedef std::list<ServiceDep> softdep_list;
-
- // list of soft dependents
- typedef std::list<ServiceDep *> 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<ServiceListener *> 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<const char *> &args, bool on_console) noexcept;
-
- // Callback from libev when a child process dies
- static void process_child_callback(struct ev_loop *loop, struct ev_child *w,
- 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<std::pair<unsigned,unsigned>> &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<std::pair<unsigned,unsigned>> &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<ServiceRecord *> 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<ServiceRecord *>::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
+++ /dev/null
-// #include <netinet/in.h>
-#include <cstddef>
-#include <cstdio>
-#include <csignal>
-#include <unistd.h>
-#include <cstring>
-#include <string>
-#include <iostream>
-
-#include <sys/reboot.h>
-#include <sys/types.h>
-#include <sys/socket.h>
-#include <sys/un.h>
-#include <sys/wait.h>
-#include <sys/stat.h>
-#include <fcntl.h>
-
-#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<char>(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);
- }
-}
--- /dev/null
+-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
--- /dev/null
+// 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;
--- /dev/null
+#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<ShutdownType>(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<char> rp_buf;
+ rp_buf.reserve(7);
+ rp_buf.push_back(DINIT_RP_SERVICERECORD);
+ rp_buf.push_back(static_cast<char>(record->getState()));
+ for (int i = 0; i < (int) sizeof(handle); i++) {
+ rp_buf.push_back(*(((char *) &handle) + i));
+ }
+ rp_buf.push_back(static_cast<char>(record->getTargetState()));
+ if (! queuePacket(std::move(rp_buf))) return;
+ }
+ else {
+ std::vector<char> 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<handle_t>::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<char> &&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<char> & 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--;
+}
--- /dev/null
+#ifndef DINIT_CONTROL_H
+#define DINIT_CONTROL_H
+
+#include <list>
+#include <vector>
+#include <unordered_map>
+#include <limits>
+
+#include <unistd.h>
+#include <ev++.h>
+
+#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 <typename T> using list = std::list<T>;
+ template <typename T> using vector = std::vector<T>;
+
+ // A mapping between service records and their associated numerical identifier used
+ // in communction
+ using handle_t = uint32_t;
+ std::unordered_multimap<ServiceRecord *, handle_t> serviceKeyMap;
+ std::unordered_map<handle_t, ServiceRecord *> keyServiceMap;
+
+ // Buffer for outgoing packets. Each outgoing back is represented as a vector<char>.
+ list<vector<char>> 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<char> &&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<char> 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<char>(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
--- /dev/null
+#ifndef CPBUFFER_H
+#define CPBUFFER_H
+
+#include <cstring>
+
+// 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
--- /dev/null
+#include <iostream>
+#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;
+ }
+}
--- /dev/null
+#ifndef DINIT_LOG_H
+#define DINIT_LOG_H
+
+// Logging for Dinit
+
+#include <string>
+#include <cstdio>
+#include <climits>
+
+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 <typename A> static inline void logParts(A a) noexcept
+ {
+ logMsgEnd(a);
+ }
+
+ template <typename A, typename ...B> static inline void logParts(A a, B... b) noexcept
+ {
+ logMsgPart(a);
+ logParts(b...);
+ }
+}
+
+// Variadic 'log' method.
+template <typename A, typename ...B> static inline void log(LogLevel lvl, A a, B ...b) noexcept
+{
+ logMsgBegin(lvl, a);
+ dinit_log::logParts(b...);
+}
+
+#endif
--- /dev/null
+#include <cstdio>
+#include <cstddef>
+#include <cstring>
+#include <string>
+#include <iostream>
+#include <system_error>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+#include <pwd.h>
+
+#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 << " <service-name> : 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<ServiceState>(rbuffer[1]);
+ target_state = static_cast<ServiceState>(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<ServiceEvent>(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;
+}
--- /dev/null
+#include <iostream>
+#include <list>
+#include <cstring>
+#include <csignal>
+#include <cstddef>
+#include <cstdlib>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/un.h>
+#include <sys/socket.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <pwd.h>
+
+#include "service.h"
+#include "ev++.h"
+#include "control.h"
+#include "dinit-log.h"
+
+#ifdef __linux__
+#include <sys/klog.h>
+#include <sys/reboot.h>
+#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<const char *> 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 <dir>, -d <dir> : set base directory for service description files (-d <dir>)" << endl;
+ cout << " <service-name> : start service with name <service-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<const char *>::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<sockaddr_un *>(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();
+}
--- /dev/null
+#!/bin/sh
+# "halt" command actually executes the more useful "power off".
+shutdown -p "$@"
--- /dev/null
+#include <algorithm>
+#include <string>
+#include <fstream>
+#include <locale>
+#include <iostream>
+#include <limits>
+
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <pwd.h>
+#include <grp.h>
+
+#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<char> & facet = use_facet<ctype<char> >(locale::classic());
+
+ string rval;
+ // Allow alphabetical characters, and dash (-) in setting name
+ while (i != end && (*i == '-' || facet.is(ctype<char>::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 <int,int> 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<std::pair<unsigned,unsigned>> * 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<uid_t>::max() <= (uintmax_t)std::numeric_limits<unsigned long long>::max(), "uid_t is too large");
+ unsigned long long v = std::stoull(param, &ind, 0);
+ if (v > static_cast<unsigned long long>(std::numeric_limits<uid_t>::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<unsigned long long>(std::numeric_limits<gid_t>::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<pair<unsigned,unsigned>> command_offsets;
+ string stop_command;
+ list<pair<unsigned,unsigned>> stop_command_offsets;
+ string pid_file;
+
+ ServiceType service_type = ServiceType::PROCESS;
+ std::list<ServiceRecord *> depends_on;
+ std::list<ServiceRecord *> 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<std::pair<unsigned,unsigned>> 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;
+ }
+}
--- /dev/null
+#!/bin/sh
+shutdown -r "$@"
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#include <cstring>
+#include <cerrno>
+#include <sstream>
+#include <iterator>
+#include <memory>
+#include <cstddef>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/ioctl.h>
+#include <sys/un.h>
+#include <sys/socket.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <termios.h>
+
+#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<ServiceRecord *> & records,
+ const char *name) noexcept
+{
+ using std::list;
+ list<ServiceRecord *>::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<sockaddr_un *>(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<const char *> &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<char *>("LISTEN_FDS=1"))) goto failure_out;
+
+ snprintf(nbuf, bufsz, "LISTEN_PID=%jd", static_cast<intmax_t>(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<char **>(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--;
+}
--- /dev/null
+#ifndef SERVICE_H
+#define SERVICE_H
+
+#include <string>
+#include <list>
+#include <vector>
+#include <csignal>
+#include <unordered_set>
+#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<const char *> separate_args(std::string &s, std::list<std::pair<unsigned,unsigned>> &arg_indices)
+{
+ std::vector<const char *> 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<const char *> exec_arg_parts; /* pointer to each argument/part of the program_name */
+
+ string stop_command; /* storage for stop program/script and arguments */
+ std::vector<const char *> 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<ServiceRecord *> sr_list;
+ typedef sr_list::iterator sr_iter;
+
+ // list of soft dependencies
+ typedef std::list<ServiceDep> softdep_list;
+
+ // list of soft dependents
+ typedef std::list<ServiceDep *> 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<ServiceListener *> 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<const char *> &args, bool on_console) noexcept;
+
+ // Callback from libev when a child process dies
+ static void process_child_callback(struct ev_loop *loop, struct ev_child *w,
+ 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<std::pair<unsigned,unsigned>> &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<std::pair<unsigned,unsigned>> &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<ServiceRecord *> 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<ServiceRecord *>::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
--- /dev/null
+// #include <netinet/in.h>
+#include <cstddef>
+#include <cstdio>
+#include <csignal>
+#include <unistd.h>
+#include <cstring>
+#include <string>
+#include <iostream>
+
+#include <sys/reboot.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <sys/wait.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#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<char>(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);
+ }
+}