Skip to content

Commit 84ca292

Browse files
committed
Implement systemd-style readiness notification via NOTIFY_SOCKET
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.
1 parent e7ad5b1 commit 84ca292

7 files changed

Lines changed: 169 additions & 51 deletions

File tree

src/baseproc-service.cc

Lines changed: 73 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ void base_process_service::do_smooth_recovery() noexcept
3030

3131
bool base_process_service::bring_up() noexcept
3232
{
33-
if (!open_socket()) {
33+
if (!open_socket() || !open_ready_socket()) {
34+
becoming_inactive();
3435
return false;
3536
}
3637

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

178-
if (have_notify) {
179+
if (have_notify && ready_socket_fd < 0) {
179180
// Create a notification pipe:
180181
if (bp_sys::pipe2(notify_pipe, 0) != 0) {
181182
log(loglevel_t::ERROR, get_name(), ": can't create notification pipe: ", strerror(errno));
@@ -185,10 +186,12 @@ bool base_process_service::start_ps_process(const std::vector<const char *> &cmd
185186
// Set the read side as close-on-exec:
186187
int fdflags = bp_sys::fcntl(notify_pipe[0], F_GETFD);
187188
bp_sys::fcntl(notify_pipe[0], F_SETFD, fdflags | FD_CLOEXEC);
189+
}
188190

191+
if (have_notify) {
189192
// add, but don't yet enable, readiness watcher:
190193
try {
191-
rwatcher->add_watch(event_loop, notify_pipe[0], dasynq::IN_EVENTS, false);
194+
rwatcher->add_watch(event_loop, ready_socket_fd >= 0 ? ready_socket_fd : notify_pipe[0], dasynq::IN_EVENTS, false);
192195
ready_watcher_registered = true;
193196
}
194197
catch (std::exception &exc) {
@@ -251,7 +254,7 @@ bool base_process_service::start_ps_process(const std::vector<const char *> &cmd
251254
run_params.unmask_sigint = onstart_flags.unmask_intr;
252255
run_params.csfd = control_socket[1];
253256
run_params.socket_fd = socket_fd;
254-
run_params.notify_fd = notify_pipe[1];
257+
run_params.notify_fd = ready_socket_fd >= 0 ? ready_socket_fd : notify_pipe[1];
255258
run_params.force_notify_fd = force_notification_fd;
256259
run_params.notify_var = notification_var.c_str();
257260
run_params.env_file = env_file.c_str();
@@ -469,86 +472,116 @@ void base_process_service::becoming_inactive() noexcept
469472
close(socket_fd);
470473
socket_fd = -1;
471474
}
472-
}
473-
474-
bool base_process_service::open_socket() noexcept
475-
{
476-
if (socket_path.empty() || socket_fd != -1) {
477-
// No socket, or already open
478-
return true;
475+
if (ready_socket_fd != -1) {
476+
close(ready_socket_fd);
477+
ready_socket_fd = -1;
479478
}
479+
free(ready_socket_name);
480+
ready_socket_name = nullptr;
481+
}
480482

481-
const char * saddrname = socket_path.c_str();
482-
483+
static int open_sock(const char *path, const std::string &svcname, int type,
484+
uid_t uid, gid_t gid, int perms, struct sockaddr_un *&name) noexcept {
483485
// Check the specified socket path
484486
struct stat stat_buf;
485-
if (stat(saddrname, &stat_buf) == 0) {
487+
if (stat(path, &stat_buf) == 0) {
486488
if ((stat_buf.st_mode & S_IFSOCK) == 0) {
487489
// Not a socket
488-
log(loglevel_t::ERROR, get_name(), ": activation socket file exists (and is not a socket)");
489-
return false;
490+
log(loglevel_t::ERROR, svcname, ": socket file exists (and is not a socket)");
491+
return -1;
490492
}
491493
}
492494
else if (errno != ENOENT) {
493495
// Other error
494-
log(loglevel_t::ERROR, get_name(), ": error checking activation socket: ", strerror(errno));
495-
return false;
496+
log(loglevel_t::ERROR, svcname, ": error checking socket: ", strerror(errno));
497+
return -1;
496498
}
497499

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

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

510512
name->sun_family = AF_UNIX;
511-
strcpy(name->sun_path, saddrname);
513+
strcpy(name->sun_path, path);
512514

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

520522
if (bind(sockfd, (struct sockaddr *) name, sockaddr_size) == -1) {
521-
log(loglevel_t::ERROR, get_name(), ": error binding activation socket: ", strerror(errno));
523+
log(loglevel_t::ERROR, svcname, ": error binding socket: ", strerror(errno));
522524
close(sockfd);
523525
free(name);
524-
return false;
526+
return -1;
525527
}
526528

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

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

545-
if (listen(sockfd, 128) == -1) { // 128 "seems reasonable".
546-
log(loglevel_t::ERROR, ": error listening on activation socket: ", strerror(errno));
545+
if (type != SOCK_DGRAM && listen(sockfd, 128) == -1) { // 128 "seems reasonable".
546+
log(loglevel_t::ERROR, ": error listening on socket: ", strerror(errno));
547547
close(sockfd);
548+
return -1;
549+
}
550+
551+
return sockfd;
552+
}
553+
554+
bool base_process_service::open_socket() noexcept
555+
{
556+
if (socket_path.empty() || socket_fd != -1) {
557+
// No socket, or already open
558+
return true;
559+
}
560+
561+
struct sockaddr_un *name = nullptr;
562+
socket_fd = open_sock(socket_path.c_str(), get_name(), SOCK_STREAM, socket_uid,
563+
socket_gid, socket_perms, name);
564+
free(name);
565+
566+
return socket_fd >= 0;
567+
}
568+
569+
bool base_process_service::open_ready_socket() noexcept
570+
{
571+
if (ready_socket_path.empty() || ready_socket_fd != -1) {
572+
// No socket, or already open
573+
return true;
574+
}
575+
576+
ready_socket_fd = open_sock(ready_socket_path.c_str(), get_name(), SOCK_DGRAM,
577+
ready_socket_uid, ready_socket_gid, ready_socket_perms, ready_socket_name);
578+
579+
if (ready_socket_fd < 0) {
580+
free(ready_socket_name);
581+
ready_socket_name = nullptr;
548582
return false;
549583
}
550584

551-
socket_fd = sockfd;
552585
return true;
553586
}
554587

src/includes/load-service.h

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ enum class setting_id_t {
225225
LOGFILE_GID, LOG_TYPE, LOG_BUFFER_SIZE, CONSUMER_OF, RESTART, SMOOTH_RECOVERY, OPTIONS,
226226
LOAD_OPTIONS, TERM_SIGNAL, TERMSIGNAL /* deprecated */, RESTART_LIMIT_INTERVAL, RESTART_DELAY,
227227
RESTART_LIMIT_COUNT, STOP_TIMEOUT, START_TIMEOUT, RUN_AS, CHAIN_TO, READY_NOTIFICATION,
228-
INITTAB_ID, INITTAB_LINE,
228+
READY_SOCKET_PERMISSIONS, READY_SOCKET_UID, READY_SOCKET_GID, INITTAB_ID, INITTAB_LINE,
229229
// Prefixed with SETTING_ to avoid name collision with system macros:
230230
SETTING_RLIMIT_NOFILE, SETTING_RLIMIT_CORE, SETTING_RLIMIT_DATA, SETTING_RLIMIT_ADDRSPACE,
231231
// Possibly unsupported depending on platform/build options:
@@ -1282,12 +1282,17 @@ class service_settings_wrapper
12821282
auto_restart_mode auto_restart = auto_restart_mode::DEFAULT_AUTO_RESTART;
12831283
bool smooth_recovery = false;
12841284
string socket_path;
1285+
string ready_socket_path;
12851286
int socket_perms = 0666;
12861287
// Note: Posix allows that uid_t and gid_t may be unsigned types, but eg chown uses -1 as an
12871288
// invalid value, so it's safe to assume that we can do the same:
12881289
uid_t socket_uid = -1;
12891290
gid_t socket_uid_gid = -1; // primary group of socket user if known
12901291
gid_t socket_gid = -1;
1292+
int ready_socket_perms = 0600;
1293+
uid_t ready_socket_uid = -1;
1294+
gid_t ready_socket_uid_gid = -1;
1295+
gid_t ready_socket_gid = -1;
12911296
// Restart limit interval / count; default is 10 seconds, 3 restarts:
12921297
timespec restart_interval = { .tv_sec = 10, .tv_nsec = 0 };
12931298
int max_restarts = 3;
@@ -1362,6 +1367,9 @@ class service_settings_wrapper
13621367
if (!socket_path.empty()) {
13631368
report_lint("'socket-listen' specified, but ignored for the specified (or default) service type'.");
13641369
}
1370+
if (!ready_socket_path.empty()) {
1371+
report_lint("'ready-notification' specified, but ignored for the specified (or default) service type'.");
1372+
}
13651373
#if USE_UTMPX
13661374
if (inittab_id[0] != 0 || inittab_line[0] != 0) {
13671375
report_lint("'inittab_line' or 'inittab_id' specified, but ignored for the specified (or default) service type.");
@@ -1395,7 +1403,7 @@ class service_settings_wrapper
13951403
report_error("process ID file ('pid-file') not specified for bgprocess service.");
13961404
}
13971405

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

14231431
do_resolve("socket-listen", socket_path);
1432+
do_resolve("ready-notification", ready_socket_path);
14241433
do_resolve("logfile", logfile);
14251434
do_resolve("working-dir", working_dir);
14261435
do_resolve("pid-file", pid_file);
@@ -1429,6 +1438,7 @@ class service_settings_wrapper
14291438
// If socket_gid hasn't been explicitly set, but the socket_uid was specified as a name (and
14301439
// we therefore recovered the primary group), use the primary group of the specified user.
14311440
if (socket_gid == (gid_t)-1) socket_gid = socket_uid_gid;
1441+
if (ready_socket_gid == (gid_t)-1) ready_socket_gid = ready_socket_uid_gid;
14321442
// Also for logfile_uid/gid, we reset logfile ownership to dinit process uid/gid if uid/gid
14331443
// wasn't specified by service
14341444
if (logfile_uid == (uid_t) -1) logfile_uid = getuid();
@@ -1872,12 +1882,38 @@ void process_service_line(settings_wrapper &settings, const char *name, const ch
18721882
"ready-notification", input_pos);
18731883
}
18741884
}
1885+
else if (starts_with(notify_setting, "socket:")) {
1886+
settings.ready_socket_path = notify_setting.substr(7 /* len 'socket:' */);
1887+
if (settings.ready_socket_path.empty()) {
1888+
throw service_description_exc(name, "invalid readiness socket path",
1889+
"ready-notification", input_pos);
1890+
}
1891+
}
18751892
else {
18761893
throw service_description_exc(name, "unrecognised setting: " + notify_setting,
18771894
"ready-notification", input_pos);
18781895
}
18791896
break;
18801897
}
1898+
case setting_id_t::READY_SOCKET_PERMISSIONS:
1899+
{
1900+
string sock_perm_str = read_setting_value(input_pos, i, end, nullptr);
1901+
settings.ready_socket_perms = parse_perms(input_pos, sock_perm_str, name, "ready-socket-permissions");
1902+
break;
1903+
}
1904+
case setting_id_t::READY_SOCKET_UID:
1905+
{
1906+
string sock_uid_s = read_setting_value(input_pos, i, end, nullptr);
1907+
settings.ready_socket_uid = parse_uid_param(input_pos, sock_uid_s, name, "ready-socket-uid",
1908+
&settings.ready_socket_uid_gid);
1909+
break;
1910+
}
1911+
case setting_id_t::READY_SOCKET_GID:
1912+
{
1913+
string sock_gid_s = read_setting_value(input_pos, i, end, nullptr);
1914+
settings.ready_socket_gid = parse_gid_param(input_pos, sock_gid_s, "ready-socket-gid", name);
1915+
break;
1916+
}
18811917
case setting_id_t::INITTAB_ID:
18821918
{
18831919
string inittab_setting = read_setting_value(input_pos, i, end, nullptr);

src/includes/proc-service.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,13 @@ class base_process_service : public service_record
228228
int log_output_fd = -1; // If logging via buffer/pipe, write end of the pipe
229229
int log_input_fd = -1; // If logging via buffer/pipe, read end of the pipe
230230

231+
string ready_socket_path; // path to the socket for ready-notification
232+
int ready_socket_perms = 0; // socket permissions ("mode")
233+
uid_t ready_socket_uid = -1; // socket user id or -1
234+
gid_t ready_socket_gid = -1; // socket group id or -1
235+
int ready_socket_fd = -1; // For socket ready notification, this is the file descriptor for the socket.
236+
struct sockaddr_un *ready_socket_name = nullptr; // Since ready socket is UDP, we need this for recvfrom().
237+
231238
// Only one of waiting_restart_timer and waiting_stopstart_timer should be set at any time.
232239
// They indicate that the process timer is armed (and why).
233240
bool waiting_restart_timer : 1;
@@ -298,6 +305,9 @@ class base_process_service : public service_record
298305
// Open the activation socket, return false on failure
299306
bool open_socket() noexcept;
300307

308+
// Open the readiness socket, return false on failure
309+
bool open_ready_socket() noexcept;
310+
301311
// Get the readiness notification watcher for this service, if it has one; may return nullptr.
302312
virtual ready_notify_watcher *get_ready_watcher() noexcept
303313
{
@@ -518,6 +528,20 @@ class base_process_service : public service_record
518528
notification_var = std::move(varname);
519529
}
520530

531+
void set_ready_socket_details(string &&socket_path, int socket_perms, uid_t socket_uid, uid_t socket_gid)
532+
noexcept
533+
{
534+
ready_socket_path = std::move(socket_path);
535+
ready_socket_perms = socket_perms;
536+
ready_socket_uid = socket_uid;
537+
ready_socket_gid = socket_gid;
538+
if (ready_socket_path.length() > 0) {
539+
// we want to expose this in the environment
540+
// and this also simplifies the logic elsewhere
541+
set_notification_var("NOTIFY_SOCKET=" + ready_socket_path);
542+
}
543+
}
544+
521545
// The restart/stop timer expired.
522546
void timer_expired() noexcept;
523547

src/load-service.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,8 @@ service_record * dirload_service_set::load_reload_service(const char *fullname,
742742
rvalps->set_run_as_uid_gid(settings.run_as_uid, settings.run_as_gid);
743743
rvalps->set_notification_fd(settings.readiness_fd);
744744
rvalps->set_notification_var(std::move(settings.readiness_var));
745+
rvalps->set_ready_socket_details(std::move(settings.ready_socket_path),
746+
settings.ready_socket_perms, settings.ready_socket_uid, settings.ready_socket_gid);
745747
rvalps->set_logfile_details(std::move(settings.logfile), settings.logfile_perms,
746748
settings.logfile_uid, settings.logfile_gid);
747749
rvalps->set_log_buf_max(settings.max_log_buffer_sz);

src/proc-service.cc

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,20 @@ rearm ready_notify_watcher::fd_event(eventloop_t &, int fd, int flags) noexcept
175175
{
176176
char buf[128];
177177
if (service->get_state() == service_state_t::STARTING) {
178-
// can we actually read anything from the notification pipe?
179-
int r = bp_sys::read(fd, buf, sizeof(buf));
178+
// can we actually read anything from the notification pipe/socket?
179+
ssize_t r;
180+
if (fd != service->ready_socket_fd) {
181+
r = bp_sys::read(fd, buf, sizeof(buf));
182+
}
183+
else {
184+
socklen_t alen = service->ready_socket_path.length() + sizeof(sa_family_t);
185+
r = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr *)service->ready_socket_name, &alen);
186+
if (r > 0 && (r != strlen("READY=1") || memcmp(buf, "READY=1", strlen("READY=1")))) {
187+
/* ignore datagram */
188+
errno = EAGAIN;
189+
r = -1;
190+
}
191+
}
180192
if (r > 0) {
181193
if (service->waiting_stopstart_timer) {
182194
service->process_timer.stop_timer(event_loop);
@@ -195,7 +207,7 @@ rearm ready_notify_watcher::fd_event(eventloop_t &, int fd, int flags) noexcept
195207
}
196208
service->services->process_queues();
197209
}
198-
else {
210+
else if (fd != service->ready_socket_fd) {
199211
// Just keep consuming data from the pipe:
200212
int r = bp_sys::read(fd, buf, sizeof(buf));
201213
if (r == 0) {
@@ -204,6 +216,10 @@ rearm ready_notify_watcher::fd_event(eventloop_t &, int fd, int flags) noexcept
204216
service->notification_fd = -1;
205217
return rearm::DISARM;
206218
}
219+
} else {
220+
// Just consume the datagram
221+
socklen_t alen = service->ready_socket_path.length() + sizeof(sa_family_t);
222+
recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr *)service->ready_socket_name, &alen);
207223
}
208224

209225
return rearm::REARM;

0 commit comments

Comments
 (0)