From 7f21fa255569e2fc96dd82c70e558c11cc4c40c5 Mon Sep 17 00:00:00 2001 From: Davin McCall Date: Mon, 4 Jan 2016 00:01:43 +0000 Subject: [PATCH] Implement socket activation (single, unix-family socket only) --- README | 12 +++++++ load_service.cc | 19 +++++++++++ service.cc | 83 +++++++++++++++++++++++++++++++++++++++++++++++++ service.h | 26 ++++++++++++---- 4 files changed, 134 insertions(+), 6 deletions(-) diff --git a/README b/README index 4a50c4f..5ef5256 100644 --- 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 diff --git a/load_service.cc b/load_service.cc index deac502..626f5b9 100644 --- a/load_service.cc +++ b/load_service.cc @@ -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; } diff --git a/service.cc b/service.cc index 06ec5ad..14685dd 100644 --- a/service.cc +++ b/service.cc @@ -3,10 +3,13 @@ #include #include #include +#include #include #include #include +#include +#include #include #include #include @@ -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(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 &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("LISTEN_FDS=1"))) goto failure_out; + + snprintf(nbuf, bufsz, "LISTEN_PID=%jd", static_cast(getpid())); + + if (putenv(nbuf)) goto failure_out; + } + if (! on_console) { // Re-set stdin, stdout, stderr close(0); close(1); close(2); @@ -536,6 +618,7 @@ bool ServiceRecord::start_ps_process(const std::vector &cmd, bool execvp(exec_arg_parts[0], const_cast(args)); // If we got here, the exec failed: + failure_out: int exec_status = errno; write(pipefd[1], &exec_status, sizeof(int)); exit(0); diff --git a/service.h b/service.h index ae878b5..024a380 100644 --- 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 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 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; } -- 2.25.1