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:
committed by
Daan De Meyer
parent
22f1d5a625
commit
c0696f1f5d
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user