Implement socket activation (single, unix-family socket only)
authorDavin McCall <davmac@davmac.org>
Mon, 4 Jan 2016 00:01:43 +0000 (00:01 +0000)
committerDavin McCall <davmac@davmac.org>
Mon, 4 Jan 2016 00:11:50 +0000 (00:11 +0000)
README
load_service.cc
service.cc
service.h

diff --git a/README b/README
index 4a50c4f44a6f521970a4733014d574b8cb70d71c..5ef52567578f7521c0e0b30a6459cdc960ce701b 100644 (file)
--- a/README
+++ b/README
@@ -138,6 +138,18 @@ waits-for = (service name)
    for this service. Starting this service will automatically start
    the named service.
 
+socket-listen = (socket path)
+   Pre-open a socket for the service and pass it to the service using the
+   Systemd activation protocol. This by itself does not give so called
+   "socket activation", but does allow that any process trying to connect
+   to the specified socket will be able to do so, even before the service
+   is properly prepared to accept connections.
+
+socket-permissions = (octal permissions mask)
+   Gives the permissions for the socket specified using socket-listen.
+   Normally this will be 600 (user access only), 660 (user and group
+   access), or 666 (all users).
+
 termsignal = HUP | INT | QUIT | USR1 | USR2
    Specifies an additional signal to send to the process when requesting it
    to terminate (applies to 'process' services only). SIGTERM is always
index deac5027fa7ccab27cb3dd6a12a612dcd32c847c..626f5b96039f18f746ba0054e67ba1be045e5eb7 100644 (file)
@@ -220,6 +220,8 @@ ServiceRecord * ServiceSet::loadServiceRecord(const char * name)
     int term_signal = -1;  // additional termination signal
     bool auto_restart = false;
     bool smooth_recovery = false;
+    string socket_path;
+    int socket_perms = 0666;
     
     string line;
     ifstream service_file;
@@ -260,6 +262,22 @@ ServiceRecord * ServiceSet::loadServiceRecord(const char * name)
                 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 == "stop-command") {
                     stop_command = read_setting_value(i, end, &stop_command_offsets);
                 }
@@ -358,6 +376,7 @@ ServiceRecord * ServiceSet::loadServiceRecord(const char * name)
                 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);
                 *iter = rval;
                 break;
             }
index 06ec5ada7084f380008368d5f0ba18d5c36367bf..14685dde6661798a4820669b5ce37250e59a7f16 100644 (file)
@@ -3,10 +3,13 @@
 #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>
@@ -77,6 +80,10 @@ void ServiceRecord::stopped() noexcept
         // 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
@@ -322,6 +329,60 @@ bool ServiceRecord::startCheckDependencies(bool start_deps) noexcept
     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);
+    
+    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) {
@@ -332,6 +393,10 @@ void ServiceRecord::allDepsStarted(bool has_console) noexcept
     
     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();
@@ -508,6 +573,23 @@ bool ServiceRecord::start_ps_process(const std::vector<const char *> &cmd, bool
         // 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);
@@ -536,6 +618,7 @@ bool ServiceRecord::start_ps_process(const std::vector<const char *> &cmd, bool
         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);
index ae878b56047f53929bf4b782a3ae4db77e0291a6..024a38095efdf2cff3ef06759baf386099e9e42e 100644 (file)
--- a/service.h
+++ b/service.h
@@ -197,22 +197,27 @@ class ServiceRecord
     
     // 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.
     
-    std::unordered_set<ServiceListener *> listeners;
-
     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")
 
     // Implementation details
     
-    pid_t pid;  // 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).
+    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;
@@ -254,6 +259,9 @@ class ServiceRecord
     // 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;
     
@@ -388,6 +396,12 @@ class ServiceRecord
     {
         this->pid_file = pid_file;
     }
+    
+    void set_socket_details(string &&socket_path, int socket_perms) noexcept
+    {
+        this->socket_path = socket_path;
+        this->socket_perms = socket_perms;
+    }
 
     const char *getServiceName() const noexcept { return service_name.c_str(); }
     ServiceState getState() const noexcept { return service_state; }