sd-varlink: Introduce varlink_set_sentinel()

Streaming methods which are not used as a continuous subscription but
instead only send a series of objects all end up with the same workaround
to be able to figure out when to send sd_varlink_reply() or sd_varlink_notify().
Let's generalize this in sd-varlink itself.

Let's introduce the concept of a sentinel, which is a reply that will be sent
by sd-varlink if no other reply was queued by a method callback. The sentinel
is configured with varlink_set_sentinel(). If a sentinel is configured,
sd_varlink_reply() can be used more than once in streaming methods to queue
multiple values to stream to the client. The last queued reply is not sent
until the callback finishes. When the callback finishes, the last reply is
sent without "continues: more". If no reply was queued, the sentinel is sent.

This always using only sd_varlink_reply() in such streaming methods and
leaves sd_varlink_notify() available solely for continuous subscription
streaming methods, where we never use sd_varlink_reply() and instead disconnect
when the server exits.
This commit is contained in:
Daan De Meyer
2026-02-11 14:24:03 +01:00
committed by Daan De Meyer
parent 22f1d5a625
commit c0696f1f5d
5 changed files with 455 additions and 19 deletions
+311
View File
@@ -441,4 +441,315 @@ TEST(invalid_parameter) {
ASSERT_OK(sd_event_loop(e));
}
static int method_with_error_sentinel(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
/* Set an error sentinel and return without sending a reply. The sentinel error should be sent automatically. */
ASSERT_OK(varlink_set_sentinel(link, "io.test.SentinelError"));
return 0;
}
static int reply_sentinel_error(sd_varlink *link, sd_json_variant *parameters, const char *error_id, sd_varlink_reply_flags_t flags, void *userdata) {
ASSERT_STREQ(error_id, "io.test.SentinelError");
ASSERT_OK(sd_event_exit(sd_varlink_get_event(link), EXIT_SUCCESS));
return 0;
}
TEST(sentinel_error) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_default(&e));
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL;
ASSERT_OK(sd_varlink_server_new(&s, 0));
ASSERT_OK(sd_varlink_server_attach_event(s, e, 0));
ASSERT_OK(sd_varlink_server_bind_method(s, "io.test.ErrorSentinel", method_with_error_sentinel));
int connfd[2];
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, 0, connfd));
ASSERT_OK(sd_varlink_server_add_connection(s, connfd[0], /* ret= */ NULL));
_cleanup_(sd_varlink_unrefp) sd_varlink *c = NULL;
ASSERT_OK(sd_varlink_connect_fd(&c, connfd[1]));
ASSERT_OK(sd_varlink_attach_event(c, e, 0));
ASSERT_OK(sd_varlink_bind_reply(c, reply_sentinel_error));
ASSERT_OK(sd_varlink_invoke(c, "io.test.ErrorSentinel", /* parameters= */ NULL));
ASSERT_OK(sd_event_loop(e));
}
static int method_with_empty_sentinel(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
/* Set an empty sentinel and return without sending a reply. An empty reply should be sent automatically. */
ASSERT_OK(varlink_set_sentinel(link, /* error_id= */ NULL));
return 0;
}
static int reply_sentinel_empty(sd_varlink *link, sd_json_variant *parameters, const char *error_id, sd_varlink_reply_flags_t flags, void *userdata) {
ASSERT_NULL(error_id);
ASSERT_TRUE(sd_json_variant_is_blank_object(parameters));
ASSERT_OK(sd_event_exit(sd_varlink_get_event(link), EXIT_SUCCESS));
return 0;
}
TEST(sentinel_empty) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_default(&e));
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL;
ASSERT_OK(sd_varlink_server_new(&s, 0));
ASSERT_OK(sd_varlink_server_attach_event(s, e, 0));
ASSERT_OK(sd_varlink_server_bind_method(s, "io.test.EmptySentinel", method_with_empty_sentinel));
int connfd[2];
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, 0, connfd));
ASSERT_OK(sd_varlink_server_add_connection(s, connfd[0], /* ret= */ NULL));
_cleanup_(sd_varlink_unrefp) sd_varlink *c = NULL;
ASSERT_OK(sd_varlink_connect_fd(&c, connfd[1]));
ASSERT_OK(sd_varlink_attach_event(c, e, 0));
ASSERT_OK(sd_varlink_bind_reply(c, reply_sentinel_empty));
ASSERT_OK(sd_varlink_invoke(c, "io.test.EmptySentinel", /* parameters= */ NULL));
ASSERT_OK(sd_event_loop(e));
}
static int method_with_sentinel_but_reply(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
/* Set a sentinel but also send a reply. The sentinel should not be used. */
ASSERT_OK(varlink_set_sentinel(link, "io.test.SentinelError"));
return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_STRING("result", "explicit-reply"));
}
static int reply_sentinel_explicit(sd_varlink *link, sd_json_variant *parameters, const char *error_id, sd_varlink_reply_flags_t flags, void *userdata) {
ASSERT_NULL(error_id);
ASSERT_STREQ(sd_json_variant_string(sd_json_variant_by_key(parameters, "result")), "explicit-reply");
ASSERT_OK(sd_event_exit(sd_varlink_get_event(link), EXIT_SUCCESS));
return 0;
}
TEST(sentinel_with_explicit_reply) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_default(&e));
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL;
ASSERT_OK(sd_varlink_server_new(&s, 0));
ASSERT_OK(sd_varlink_server_attach_event(s, e, 0));
ASSERT_OK(sd_varlink_server_bind_method(s, "io.test.SentinelButReply", method_with_sentinel_but_reply));
int connfd[2];
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, 0, connfd));
ASSERT_OK(sd_varlink_server_add_connection(s, connfd[0], /* ret= */ NULL));
_cleanup_(sd_varlink_unrefp) sd_varlink *c = NULL;
ASSERT_OK(sd_varlink_connect_fd(&c, connfd[1]));
ASSERT_OK(sd_varlink_attach_event(c, e, 0));
ASSERT_OK(sd_varlink_bind_reply(c, reply_sentinel_explicit));
ASSERT_OK(sd_varlink_invoke(c, "io.test.SentinelButReply", /* parameters= */ NULL));
ASSERT_OK(sd_event_loop(e));
}
static int method_with_oneway_sentinel(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
/* The method was called oneway, so varlink_set_sentinel() should be a no-op and the server should
* transition back to idle without sending any reply. */
ASSERT_TRUE(FLAGS_SET(flags, SD_VARLINK_METHOD_ONEWAY));
ASSERT_OK(varlink_set_sentinel(link, "io.test.SentinelError"));
return 0;
}
static int method_oneway_sentinel_pong(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_STRING("result", "pong"));
}
static int reply_oneway_sentinel_pong(sd_varlink *link, sd_json_variant *parameters, const char *error_id, sd_varlink_reply_flags_t flags, void *userdata) {
/* If we get here, it means the oneway sentinel call didn't break the connection and the server
* properly handled a subsequent regular method call. */
ASSERT_NULL(error_id);
ASSERT_STREQ(sd_json_variant_string(sd_json_variant_by_key(parameters, "result")), "pong");
ASSERT_OK(sd_event_exit(sd_varlink_get_event(link), EXIT_SUCCESS));
return 0;
}
TEST(sentinel_oneway) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_default(&e));
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL;
ASSERT_OK(sd_varlink_server_new(&s, 0));
ASSERT_OK(sd_varlink_server_attach_event(s, e, 0));
ASSERT_OK(sd_varlink_server_bind_method(s, "io.test.OnewaySentinel", method_with_oneway_sentinel));
ASSERT_OK(sd_varlink_server_bind_method(s, "io.test.Pong", method_oneway_sentinel_pong));
int connfd[2];
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, 0, connfd));
ASSERT_OK(sd_varlink_server_add_connection(s, connfd[0], /* ret= */ NULL));
_cleanup_(sd_varlink_unrefp) sd_varlink *c = NULL;
ASSERT_OK(sd_varlink_connect_fd(&c, connfd[1]));
ASSERT_OK(sd_varlink_attach_event(c, e, 0));
/* Send a oneway call with a sentinel — the sentinel should be silently ignored. */
ASSERT_OK(sd_varlink_send(c, "io.test.OnewaySentinel", /* parameters= */ NULL));
/* Follow up with a regular call to verify the server is still functional. */
ASSERT_OK(sd_varlink_bind_reply(c, reply_oneway_sentinel_pong));
ASSERT_OK(sd_varlink_invoke(c, "io.test.Pong", /* parameters= */ NULL));
ASSERT_OK(sd_event_loop(e));
}
static int method_with_fd_sentinel(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
_cleanup_close_ int fd1 = -EBADF, fd2 = -EBADF;
ASSERT_TRUE(FLAGS_SET(flags, SD_VARLINK_METHOD_MORE));
/* Set a sentinel so sd_varlink_reply() defers sending: each reply and its pushed fds are captured in
* the queue, and the last one is sent as the final reply when the callback returns. */
ASSERT_OK(varlink_set_sentinel(link, /* error_id= */ NULL));
/* First reply: push one fd with "alpha" content */
ASSERT_OK(fd1 = memfd_new_and_seal_string("data", "alpha"));
ASSERT_OK_EQ(sd_varlink_push_fd(link, fd1), 0);
TAKE_FD(fd1);
ASSERT_OK(sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_INTEGER("index", 0)));
/* Second reply: push one fd with "beta" content */
ASSERT_OK(fd2 = memfd_new_and_seal_string("data", "beta"));
ASSERT_OK_EQ(sd_varlink_push_fd(link, fd2), 0);
TAKE_FD(fd2);
ASSERT_OK(sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_INTEGER("index", 1)));
return 0;
}
static int reply_sentinel_fd(sd_varlink *link, sd_json_variant *parameters, const char *error_id, sd_varlink_reply_flags_t flags, void *userdata) {
int *state = ASSERT_PTR(sd_varlink_get_userdata(link));
if (*state == 0) {
/* First reply: should carry "continues" flag and fd with "alpha" */
ASSERT_NULL(error_id);
ASSERT_TRUE(FLAGS_SET(flags, SD_VARLINK_REPLY_CONTINUES));
ASSERT_EQ(sd_json_variant_integer(sd_json_variant_by_key(parameters, "index")), 0);
int fd;
ASSERT_OK(fd = sd_varlink_peek_fd(link, 0));
test_fd(fd, "alpha", STRLEN("alpha"));
(*state)++;
} else if (*state == 1) {
/* Second (final) reply: no "continues" flag, fd with "beta" */
ASSERT_NULL(error_id);
ASSERT_FALSE(FLAGS_SET(flags, SD_VARLINK_REPLY_CONTINUES));
ASSERT_EQ(sd_json_variant_integer(sd_json_variant_by_key(parameters, "index")), 1);
int fd;
ASSERT_OK(fd = sd_varlink_peek_fd(link, 0));
test_fd(fd, "beta", STRLEN("beta"));
ASSERT_OK(sd_event_exit(sd_varlink_get_event(link), EXIT_SUCCESS));
} else
assert_not_reached();
return 0;
}
TEST(sentinel_with_fds) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_default(&e));
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL;
ASSERT_OK(sd_varlink_server_new(&s, SD_VARLINK_SERVER_ALLOW_FD_PASSING_INPUT|SD_VARLINK_SERVER_ALLOW_FD_PASSING_OUTPUT));
ASSERT_OK(sd_varlink_server_attach_event(s, e, 0));
ASSERT_OK(sd_varlink_server_bind_method(s, "io.test.FDSentinel", method_with_fd_sentinel));
int connfd[2];
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, 0, connfd));
ASSERT_OK(sd_varlink_server_add_connection(s, connfd[0], /* ret= */ NULL));
_cleanup_(sd_varlink_unrefp) sd_varlink *c = NULL;
ASSERT_OK(sd_varlink_connect_fd(&c, connfd[1]));
ASSERT_OK(sd_varlink_set_allow_fd_passing_input(c, true));
ASSERT_OK(sd_varlink_set_allow_fd_passing_output(c, true));
ASSERT_OK(sd_varlink_attach_event(c, e, 0));
int state = 0;
sd_varlink_set_userdata(c, &state);
ASSERT_OK(sd_varlink_bind_reply(c, reply_sentinel_fd));
ASSERT_OK(sd_varlink_observe(c, "io.test.FDSentinel", /* parameters= */ NULL));
ASSERT_OK(sd_event_loop(e));
}
static int method_with_notify_then_error(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
/* Send a notify first, then return an error. The notify should be received before the error. */
ASSERT_OK(sd_varlink_notifybo(link, SD_JSON_BUILD_PAIR_STRING("status", "in-progress")));
return sd_varlink_error(link, "io.test.OperationFailed", /* parameters= */ NULL);
}
static int reply_notify_then_error(sd_varlink *link, sd_json_variant *parameters, const char *error_id, sd_varlink_reply_flags_t flags, void *userdata) {
int *state = ASSERT_PTR(sd_varlink_get_userdata(link));
if (*state == 0) {
/* First callback: should be the notify (no error, has "more" flag) */
ASSERT_NULL(error_id);
ASSERT_TRUE(FLAGS_SET(flags, SD_VARLINK_REPLY_CONTINUES));
ASSERT_STREQ(sd_json_variant_string(sd_json_variant_by_key(parameters, "status")), "in-progress");
(*state)++;
} else if (*state == 1) {
/* Second callback: should be the error */
ASSERT_STREQ(error_id, "io.test.OperationFailed");
ASSERT_FALSE(FLAGS_SET(flags, SD_VARLINK_REPLY_CONTINUES));
ASSERT_OK(sd_event_exit(sd_varlink_get_event(link), EXIT_SUCCESS));
} else
assert_not_reached();
return 0;
}
TEST(notify_then_error) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_default(&e));
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL;
ASSERT_OK(sd_varlink_server_new(&s, 0));
ASSERT_OK(sd_varlink_server_attach_event(s, e, 0));
ASSERT_OK(sd_varlink_server_bind_method(s, "io.test.NotifyThenError", method_with_notify_then_error));
int connfd[2];
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, 0, connfd));
ASSERT_OK(sd_varlink_server_add_connection(s, connfd[0], /* ret= */ NULL));
_cleanup_(sd_varlink_unrefp) sd_varlink *c = NULL;
ASSERT_OK(sd_varlink_connect_fd(&c, connfd[1]));
ASSERT_OK(sd_varlink_attach_event(c, e, 0));
int state = 0;
sd_varlink_set_userdata(c, &state);
ASSERT_OK(sd_varlink_bind_reply(c, reply_notify_then_error));
ASSERT_OK(sd_varlink_observe(c, "io.test.NotifyThenError", /* parameters= */ NULL));
ASSERT_OK(sd_event_loop(e));
}
DEFINE_TEST_MAIN(LOG_DEBUG);