Skip to content

Commit

Permalink
Implement systemd-style readiness notification via NOTIFY_SOCKET
Browse files Browse the repository at this point in the history
The service may specify this with ready-notification=socket:path.
This will result in dinit creating an abstract datagram socket
and perform reads on it. As soon as the READY=1 datagram arrives,
the readiness notification is signaled.
  • Loading branch information
q66 committed Oct 22, 2024
1 parent e7ad5b1 commit 2db9710
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 52 deletions.
117 changes: 76 additions & 41 deletions src/baseproc-service.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ void base_process_service::do_smooth_recovery() noexcept

bool base_process_service::bring_up() noexcept
{
if (!open_socket()) {
if (!open_socket() || !open_ready_socket()) {
becoming_inactive();
return false;
}

Expand Down Expand Up @@ -152,7 +153,7 @@ bool base_process_service::start_ps_process(const std::vector<const char *> &cmd

int control_socket[2] = {-1, -1};
int notify_pipe[2] = {-1, -1};
bool have_notify = !notification_var.empty() || force_notification_fd != -1;
bool have_notify = !notification_var.empty() || ready_socket_fd >= 0 || force_notification_fd != -1;
ready_notify_watcher * rwatcher = have_notify ? get_ready_watcher() : nullptr;
bool ready_watcher_registered = false;

Expand All @@ -175,7 +176,7 @@ bool base_process_service::start_ps_process(const std::vector<const char *> &cmd
}
}

if (have_notify) {
if (have_notify && ready_socket_fd < 0) {
// Create a notification pipe:
if (bp_sys::pipe2(notify_pipe, 0) != 0) {
log(loglevel_t::ERROR, get_name(), ": can't create notification pipe: ", strerror(errno));
Expand All @@ -185,10 +186,14 @@ bool base_process_service::start_ps_process(const std::vector<const char *> &cmd
// Set the read side as close-on-exec:
int fdflags = bp_sys::fcntl(notify_pipe[0], F_GETFD);
bp_sys::fcntl(notify_pipe[0], F_SETFD, fdflags | FD_CLOEXEC);
} else if (have_notify) {
notification_var = "NOTIFY_SOCKET=" + ready_socket_path;
}

if (have_notify) {
// add, but don't yet enable, readiness watcher:
try {
rwatcher->add_watch(event_loop, notify_pipe[0], dasynq::IN_EVENTS, false);
rwatcher->add_watch(event_loop, ready_socket_fd >= 0 ? ready_socket_fd : notify_pipe[0], dasynq::IN_EVENTS, false);
ready_watcher_registered = true;
}
catch (std::exception &exc) {
Expand Down Expand Up @@ -251,7 +256,7 @@ bool base_process_service::start_ps_process(const std::vector<const char *> &cmd
run_params.unmask_sigint = onstart_flags.unmask_intr;
run_params.csfd = control_socket[1];
run_params.socket_fd = socket_fd;
run_params.notify_fd = notify_pipe[1];
run_params.notify_fd = ready_socket_fd >= 0 ? ready_socket_fd : notify_pipe[1];
run_params.force_notify_fd = force_notification_fd;
run_params.notify_var = notification_var.c_str();
run_params.env_file = env_file.c_str();
Expand Down Expand Up @@ -469,86 +474,116 @@ void base_process_service::becoming_inactive() noexcept
close(socket_fd);
socket_fd = -1;
}
}

bool base_process_service::open_socket() noexcept
{
if (socket_path.empty() || socket_fd != -1) {
// No socket, or already open
return true;
if (ready_socket_fd != -1) {
close(ready_socket_fd);
ready_socket_fd = -1;
}
free(ready_socket_name);
ready_socket_name = nullptr;
}

const char * saddrname = socket_path.c_str();

static int open_sock(const char *path, const std::string &svcname, int type,
uid_t uid, gid_t gid, int perms, struct sockaddr_un *&name) noexcept {
// Check the specified socket path
struct stat stat_buf;
if (stat(saddrname, &stat_buf) == 0) {
if (stat(path, &stat_buf) == 0) {
if ((stat_buf.st_mode & S_IFSOCK) == 0) {
// Not a socket
log(loglevel_t::ERROR, get_name(), ": activation socket file exists (and is not a socket)");
return false;
log(loglevel_t::ERROR, svcname, ": socket file exists (and is not a socket)");
return -1;
}
}
else if (errno != ENOENT) {
// Other error
log(loglevel_t::ERROR, get_name(), ": error checking activation socket: ", strerror(errno));
return false;
log(loglevel_t::ERROR, svcname, ": error checking socket: ", strerror(errno));
return -1;
}

// Remove stale socket file (if it exists).
// We won't test the return from unlink - if it fails other than due to ENOENT, we should get an
// error when we try to create the socket anyway.
unlink(saddrname);
unlink(path);

uint sockaddr_size = offsetof(struct sockaddr_un, sun_path) + socket_path.length() + 1;
struct sockaddr_un * name = static_cast<sockaddr_un *>(malloc(sockaddr_size));
uint sockaddr_size = offsetof(struct sockaddr_un, sun_path) + strlen(path) + 1;
name = static_cast<sockaddr_un *>(malloc(sockaddr_size));
if (name == nullptr) {
log(loglevel_t::ERROR, get_name(), ": opening activation socket: out of memory");
return false;
log(loglevel_t::ERROR, svcname, ": opening socket: out of memory");
return -1;
}

name->sun_family = AF_UNIX;
strcpy(name->sun_path, saddrname);
strcpy(name->sun_path, path);

int sockfd = dinit_socket(AF_UNIX, SOCK_STREAM, 0, SOCK_NONBLOCK | SOCK_CLOEXEC);
int sockfd = dinit_socket(AF_UNIX, type, 0, SOCK_NONBLOCK | SOCK_CLOEXEC);
if (sockfd == -1) {
log(loglevel_t::ERROR, get_name(), ": error creating activation socket: ", strerror(errno));
log(loglevel_t::ERROR, svcname, ": error creating socket: ", strerror(errno));
free(name);
return false;
return -1;
}

if (bind(sockfd, (struct sockaddr *) name, sockaddr_size) == -1) {
log(loglevel_t::ERROR, get_name(), ": error binding activation socket: ", strerror(errno));
log(loglevel_t::ERROR, svcname, ": error binding socket: ", strerror(errno));
close(sockfd);
free(name);
return false;
return -1;
}

free(name);

// POSIX (1003.1, 2013) says that fchown and fchmod don't necessarily work on sockets. We have to
// use chown and chmod instead.
if (chown(saddrname, socket_uid, socket_gid)) {
log(loglevel_t::ERROR, get_name(), ": error setting activation socket owner/group: ",
if (chown(path, uid, gid)) {
log(loglevel_t::ERROR, svcname, ": error setting socket owner/group: ",
strerror(errno));
close(sockfd);
return false;
return -1;
}

if (chmod(saddrname, socket_perms) == -1) {
log(loglevel_t::ERROR, get_name(), ": Error setting activation socket permissions: ",
if (chmod(path, perms) == -1) {
log(loglevel_t::ERROR, svcname, ": Error setting socket permissions: ",
strerror(errno));
close(sockfd);
return false;
return -1;
}

if (listen(sockfd, 128) == -1) { // 128 "seems reasonable".
log(loglevel_t::ERROR, ": error listening on activation socket: ", strerror(errno));
if (type != SOCK_DGRAM && listen(sockfd, 128) == -1) { // 128 "seems reasonable".
log(loglevel_t::ERROR, ": error listening on socket: ", strerror(errno));
close(sockfd);
return -1;
}

return sockfd;
}

bool base_process_service::open_socket() noexcept
{
if (socket_path.empty() || socket_fd != -1) {
// No socket, or already open
return true;
}

struct sockaddr_un *name = nullptr;
socket_fd = open_sock(socket_path.c_str(), get_name(), SOCK_STREAM, socket_uid,
socket_gid, socket_perms, name);
free(name);

return socket_fd >= 0;
}

bool base_process_service::open_ready_socket() noexcept
{
if (ready_socket_path.empty() || ready_socket_fd != -1) {
// No socket, or already open
return true;
}

ready_socket_fd = open_sock(ready_socket_path.c_str(), get_name(), SOCK_DGRAM,
ready_socket_uid, ready_socket_gid, ready_socket_perms, ready_socket_name);

if (ready_socket_fd < 0) {
free(ready_socket_name);
ready_socket_name = nullptr;
return false;
}

socket_fd = sockfd;
return true;
}

Expand Down
40 changes: 38 additions & 2 deletions src/includes/load-service.h
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ enum class setting_id_t {
LOGFILE_GID, LOG_TYPE, LOG_BUFFER_SIZE, CONSUMER_OF, RESTART, SMOOTH_RECOVERY, OPTIONS,
LOAD_OPTIONS, TERM_SIGNAL, TERMSIGNAL /* deprecated */, RESTART_LIMIT_INTERVAL, RESTART_DELAY,
RESTART_LIMIT_COUNT, STOP_TIMEOUT, START_TIMEOUT, RUN_AS, CHAIN_TO, READY_NOTIFICATION,
INITTAB_ID, INITTAB_LINE,
READY_SOCKET_PERMISSIONS, READY_SOCKET_UID, READY_SOCKET_GID, INITTAB_ID, INITTAB_LINE,
// Prefixed with SETTING_ to avoid name collision with system macros:
SETTING_RLIMIT_NOFILE, SETTING_RLIMIT_CORE, SETTING_RLIMIT_DATA, SETTING_RLIMIT_ADDRSPACE,
// Possibly unsupported depending on platform/build options:
Expand Down Expand Up @@ -1282,12 +1282,17 @@ class service_settings_wrapper
auto_restart_mode auto_restart = auto_restart_mode::DEFAULT_AUTO_RESTART;
bool smooth_recovery = false;
string socket_path;
string ready_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_uid_gid = -1; // primary group of socket user if known
gid_t socket_gid = -1;
int ready_socket_perms = 0600;
uid_t ready_socket_uid = -1;
gid_t ready_socket_uid_gid = -1;
gid_t ready_socket_gid = -1;
// Restart limit interval / count; default is 10 seconds, 3 restarts:
timespec restart_interval = { .tv_sec = 10, .tv_nsec = 0 };
int max_restarts = 3;
Expand Down Expand Up @@ -1362,6 +1367,9 @@ class service_settings_wrapper
if (!socket_path.empty()) {
report_lint("'socket-listen' specified, but ignored for the specified (or default) service type'.");
}
if (!ready_socket_path.empty()) {
report_lint("'ready-notification' specified, but ignored for the specified (or default) service type'.");
}
#if USE_UTMPX
if (inittab_id[0] != 0 || inittab_line[0] != 0) {
report_lint("'inittab_line' or 'inittab_id' specified, but ignored for the specified (or default) service type.");
Expand Down Expand Up @@ -1395,7 +1403,7 @@ class service_settings_wrapper
report_error("process ID file ('pid-file') not specified for bgprocess service.");
}

if (readiness_fd != -1 || !readiness_var.empty()) {
if (readiness_fd != -1 || !ready_socket_path.empty() || !readiness_var.empty()) {
report_error("readiness notification ('ready-notification') is not supported "
"for bgprocess services.");
}
Expand All @@ -1421,6 +1429,7 @@ class service_settings_wrapper
};

do_resolve("socket-listen", socket_path);
do_resolve("ready-notification", ready_socket_path);
do_resolve("logfile", logfile);
do_resolve("working-dir", working_dir);
do_resolve("pid-file", pid_file);
Expand All @@ -1429,6 +1438,7 @@ class service_settings_wrapper
// If socket_gid hasn't been explicitly set, but the socket_uid was specified as a name (and
// we therefore recovered the primary group), use the primary group of the specified user.
if (socket_gid == (gid_t)-1) socket_gid = socket_uid_gid;
if (ready_socket_gid == (gid_t)-1) ready_socket_gid = ready_socket_uid_gid;
// Also for logfile_uid/gid, we reset logfile ownership to dinit process uid/gid if uid/gid
// wasn't specified by service
if (logfile_uid == (uid_t) -1) logfile_uid = getuid();
Expand Down Expand Up @@ -1872,12 +1882,38 @@ void process_service_line(settings_wrapper &settings, const char *name, const ch
"ready-notification", input_pos);
}
}
else if (starts_with(notify_setting, "socket:")) {
settings.ready_socket_path = notify_setting.substr(7 /* len 'socket:' */);
if (settings.ready_socket_path.empty()) {
throw service_description_exc(name, "invalid readiness socket path",
"ready-notification", input_pos);
}
}
else {
throw service_description_exc(name, "unrecognised setting: " + notify_setting,
"ready-notification", input_pos);
}
break;
}
case setting_id_t::READY_SOCKET_PERMISSIONS:
{
string sock_perm_str = read_setting_value(input_pos, i, end, nullptr);
settings.ready_socket_perms = parse_perms(input_pos, sock_perm_str, name, "ready-socket-permissions");
break;
}
case setting_id_t::READY_SOCKET_UID:
{
string sock_uid_s = read_setting_value(input_pos, i, end, nullptr);
settings.ready_socket_uid = parse_uid_param(input_pos, sock_uid_s, name, "ready-socket-uid",
&settings.ready_socket_uid_gid);
break;
}
case setting_id_t::READY_SOCKET_GID:
{
string sock_gid_s = read_setting_value(input_pos, i, end, nullptr);
settings.ready_socket_gid = parse_gid_param(input_pos, sock_gid_s, "ready-socket-gid", name);
break;
}
case setting_id_t::INITTAB_ID:
{
string inittab_setting = read_setting_value(input_pos, i, end, nullptr);
Expand Down
5 changes: 5 additions & 0 deletions src/includes/proc-service.h
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ class base_process_service : public service_record
int notification_fd = -1; // If readiness notification is via fd
int log_output_fd = -1; // If logging via buffer/pipe, write end of the pipe
int log_input_fd = -1; // If logging via buffer/pipe, read end of the pipe
int ready_socket_fd = -1; // For socket ready notification, this is the file descriptor for the socket.
struct sockaddr_un *ready_socket_name = nullptr; // Since ready socket is UDP, we need this for recvfrom().

// Only one of waiting_restart_timer and waiting_stopstart_timer should be set at any time.
// They indicate that the process timer is armed (and why).
Expand Down Expand Up @@ -298,6 +300,9 @@ class base_process_service : public service_record
// Open the activation socket, return false on failure
bool open_socket() noexcept;

// Open the readiness socket, return false on failure
bool open_ready_socket() noexcept;

// Get the readiness notification watcher for this service, if it has one; may return nullptr.
virtual ready_notify_watcher *get_ready_watcher() noexcept
{
Expand Down
14 changes: 14 additions & 0 deletions src/includes/service.h
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,11 @@ class service_record
uid_t socket_uid = -1; // socket user id or -1
gid_t socket_gid = -1; // socket group id or -1

string ready_socket_path; // path to the socket for ready-notification
int ready_socket_perms = 0;
uid_t ready_socket_uid = -1;
gid_t ready_socket_gid = -1;

stopped_reason_t stop_reason = stopped_reason_t::NORMAL; // reason why stopped

string start_on_completion; // service to start when this one completes
Expand Down Expand Up @@ -601,6 +606,15 @@ class service_record
this->socket_gid = socket_gid;
}

void set_ready_socket_details(string &&socket_path, int socket_perms, uid_t socket_uid, uid_t socket_gid)
noexcept
{
this->ready_socket_path = std::move(socket_path);
this->ready_socket_perms = socket_perms;
this->ready_socket_uid = socket_uid;
this->ready_socket_gid = socket_gid;
}

// Set the service that this one "chains" to. When this service completes, the named service is started.
void set_chain_to(string &&chain_to) noexcept
{
Expand Down
2 changes: 2 additions & 0 deletions src/load-service.cc
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,8 @@ service_record * dirload_service_set::load_reload_service(const char *fullname,
rval->set_flags(settings.onstart_flags);
rval->set_socket_details(std::move(settings.socket_path), settings.socket_perms,
settings.socket_uid, settings.socket_gid);
rval->set_ready_socket_details(std::move(settings.ready_socket_path),
settings.ready_socket_perms, settings.ready_socket_uid, settings.ready_socket_gid);
rval->set_chain_to(std::move(settings.chain_to_name));
rval->set_environment(std::move(srv_env));

Expand Down
Loading

0 comments on commit 2db9710

Please sign in to comment.