return 1;
}
+ /* Synchronous slot (no callback): leave c->current pinned so qmp_client_call() can
+ * pick up the reply and hand out borrowed pointers into it. */
+ if (!pending->callback)
+ return 1;
+
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = TAKE_PTR(c->current);
error = qmp_parse_response(v, &result, &desc);
assert(c);
while ((p = set_steal_first(c->slots))) {
- r = p->callback(c, /* result= */ NULL, /* error_desc= */ NULL, error, p->userdata);
- if (r < 0)
- json_stream_log_errno(&c->stream, r, "Command callback returned error, ignoring: %m");
+ if (p->callback) {
+ r = p->callback(c, /* result= */ NULL, /* error_desc= */ NULL, error, p->userdata);
+ if (r < 0)
+ json_stream_log_errno(&c->stream, r, "Command callback returned error, ignoring: %m");
+ }
free(p);
}
}
DEFINE_TRIVIAL_CLEANUP_FUNC(QmpClientArgs*, qmp_client_args_close_fds);
-int qmp_client_invoke(
+/* Shared send path for qmp_client_invoke() and qmp_client_call(). A NULL callback registers
+ * a "synchronous" slot: dispatch_reply leaves c->current pinned on match instead of invoking
+ * a callback, so qmp_client_call() can hand out borrowed pointers into the reply. */
+static int qmp_client_send(
QmpClient *c,
const char *command,
QmpClientArgs *args,
qmp_command_callback_t callback,
- void *userdata) {
+ void *userdata,
+ uint64_t *ret_id) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *cmd = NULL;
_cleanup_free_ QmpSlot *pending = NULL;
assert(c);
assert(command);
- assert(callback);
r = qmp_client_ensure_running(c);
if (r < 0)
TAKE_PTR(pending);
TAKE_PTR(fds_owner);
+
+ if (ret_id)
+ *ret_id = id;
return 0;
}
+int qmp_client_invoke(
+ QmpClient *c,
+ const char *command,
+ QmpClientArgs *args,
+ qmp_command_callback_t callback,
+ void *userdata) {
+
+ assert(callback);
+ return qmp_client_send(c, command, args, callback, userdata, /* ret_id= */ NULL);
+}
+
+int qmp_client_call(
+ QmpClient *c,
+ const char *command,
+ QmpClientArgs *args,
+ sd_json_variant **ret_result,
+ const char **ret_error_desc) {
+
+ uint64_t id;
+ int r;
+
+ assert_return(c, -EINVAL);
+ assert_return(command, -EINVAL);
+
+ /* Drop any reply pinned by a previous qmp_client_call() before we pin a new one. */
+ qmp_client_clear_current(c);
+
+ /* NULL callback marks this as a synchronous slot: dispatch_reply matches on id like
+ * any other slot (so stray unknown-id replies still get logged and dropped), but
+ * pins c->current for us instead of invoking a callback. */
+ r = qmp_client_send(c, command, args, /* callback= */ NULL, /* userdata= */ NULL, &id);
+ if (r < 0)
+ return r;
+
+ /* Pump the loop until our sync slot fires (removed from c->slots, c->current pinned). */
+ for (;;) {
+ if (c->state == QMP_CLIENT_DISCONNECTED)
+ return -ECONNRESET;
+
+ if (!set_contains(c->slots, &(QmpSlot) { .id = id })) {
+ assert(c->current);
+ break;
+ }
+
+ r = qmp_client_process(c);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ continue;
+
+ r = qmp_client_wait(c, USEC_INFINITY);
+ if (r < 0)
+ return r;
+ }
+
+ sd_json_variant *result = NULL;
+ const char *desc = NULL;
+ int error = qmp_parse_response(c->current, &result, &desc);
+
+ /* If caller doesn't ask for the error string, surface the error as the return code. */
+ if (!ret_error_desc && error < 0)
+ return error;
+
+ if (ret_result)
+ *ret_result = result;
+ if (ret_error_desc)
+ *ret_error_desc = desc;
+
+ return 1;
+}
+
void qmp_client_bind_event(QmpClient *c, qmp_event_callback_t callback, void *userdata) {
assert(c);
c->event_callback = callback;
ASSERT_EQ(errno, EBADF);
}
+/* Reads one command, asserts its execute name, and replies with a QMP error object carrying
+ * the given description. Mirrors mock_qmp_expect_and_reply() but on the error branch. */
+static void mock_qmp_expect_and_reply_error(int fd, const char *expected_command, const char *error_desc) {
+ _cleanup_free_ char *buf = NULL;
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *cmd = NULL, *error_obj = NULL, *response = NULL;
+
+ buf = ASSERT_NOT_NULL(new(char, 4096));
+
+ ssize_t n = read(fd, buf, 4095);
+ assert_se(n > 0);
+ buf[n] = '\0';
+
+ ASSERT_OK(sd_json_parse(buf, 0, &cmd, NULL, NULL));
+
+ sd_json_variant *execute = ASSERT_NOT_NULL(sd_json_variant_by_key(cmd, "execute"));
+ ASSERT_STREQ(sd_json_variant_string(execute), expected_command);
+
+ sd_json_variant *id = ASSERT_NOT_NULL(sd_json_variant_by_key(cmd, "id"));
+
+ ASSERT_OK(sd_json_buildo(
+ &error_obj,
+ SD_JSON_BUILD_PAIR_STRING("class", "GenericError"),
+ SD_JSON_BUILD_PAIR_STRING("desc", error_desc)));
+
+ ASSERT_OK(sd_json_buildo(
+ &response,
+ SD_JSON_BUILD_PAIR("error", SD_JSON_BUILD_VARIANT(error_obj)),
+ SD_JSON_BUILD_PAIR("id", SD_JSON_BUILD_VARIANT(id))));
+
+ mock_qmp_write_json(fd, response);
+}
+
+/* Drives a small wire dance for the sync call test: greeting, capabilities, one successful
+ * command reply, and two error replies (one for the ret_error_desc path, one for the -EIO
+ * path). */
+static _noreturn_ void mock_qmp_server_call(int fd) {
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *status_return = NULL;
+
+ mock_qmp_write_literal(fd,
+ "{\"QMP\": {\"version\": {\"qemu\": {\"micro\": 0, \"minor\": 0, \"major\": 9}}, \"capabilities\": []}}");
+
+ mock_qmp_expect_and_reply(fd, "qmp_capabilities", NULL);
+
+ ASSERT_OK(sd_json_buildo(
+ &status_return,
+ SD_JSON_BUILD_PAIR_BOOLEAN("running", true),
+ SD_JSON_BUILD_PAIR_STRING("status", "running")));
+ mock_qmp_expect_and_reply(fd, "query-status", status_return);
+
+ mock_qmp_expect_and_reply_error(fd, "stop", "not running");
+ mock_qmp_expect_and_reply_error(fd, "stop", "still not running");
+
+ safe_close(fd);
+ _exit(EXIT_SUCCESS);
+}
+
+TEST(qmp_client_call) {
+ _cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
+ _cleanup_(pidref_done_sigkill_wait) PidRef pid = PIDREF_NULL;
+ int qmp_fds[2];
+ int r;
+
+ ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
+
+ r = ASSERT_OK(pidref_safe_fork("(mock-qmp-call)", FORK_DEATHSIG_SIGKILL|FORK_LOG, &pid));
+ if (r == 0) {
+ safe_close(qmp_fds[0]);
+ mock_qmp_server_call(qmp_fds[1]);
+ }
+ safe_close(qmp_fds[1]);
+
+ /* qmp_client_call() drives its own process()+wait() pump, so no event loop needed. */
+ ASSERT_OK(qmp_client_connect_fd(&client, qmp_fds[0]));
+
+ /* Successful call: borrowed result pointer is valid until the next call. */
+ sd_json_variant *result = NULL;
+ const char *error_desc = NULL;
+ ASSERT_EQ(qmp_client_call(client, "query-status", NULL, &result, &error_desc), 1);
+ ASSERT_NULL(error_desc);
+ ASSERT_NOT_NULL(result);
+
+ sd_json_variant *running = ASSERT_NOT_NULL(sd_json_variant_by_key(result, "running"));
+ ASSERT_TRUE(sd_json_variant_boolean(running));
+ sd_json_variant *status = ASSERT_NOT_NULL(sd_json_variant_by_key(result, "status"));
+ ASSERT_STREQ(sd_json_variant_string(status), "running");
+
+ /* QMP error with ret_error_desc provided: returns 1, result NULL, desc set. */
+ result = (sd_json_variant*) 0x1; /* poison to catch lack-of-write */
+ error_desc = NULL;
+ ASSERT_EQ(qmp_client_call(client, "stop", NULL, &result, &error_desc), 1);
+ ASSERT_NULL(result);
+ ASSERT_STREQ(error_desc, "not running");
+
+ /* QMP error without ret_error_desc: surfaces as -EIO. */
+ ASSERT_EQ(qmp_client_call(client, "stop", NULL, NULL, NULL), -EIO);
+}
+
+/* Server variant for the sync-call disconnect test: greets, accepts capabilities, reads one
+ * command without replying, then closes the socket so the client sees EOF mid-wait. */
+static _noreturn_ void mock_qmp_server_call_disconnect(int fd) {
+ _cleanup_free_ char *buf = NULL;
+
+ mock_qmp_write_literal(fd,
+ "{\"QMP\": {\"version\": {\"qemu\": {\"micro\": 0, \"minor\": 0, \"major\": 9}}, \"capabilities\": []}}");
+
+ mock_qmp_expect_and_reply(fd, "qmp_capabilities", NULL);
+
+ /* Consume the stop command but don't reply — just close to trigger EOF while the
+ * client is blocked in qmp_client_call()'s process+wait pump. */
+ buf = ASSERT_NOT_NULL(new(char, 4096));
+ ssize_t n = read(fd, buf, 4095);
+ assert_se(n > 0);
+
+ safe_close(fd);
+ _exit(EXIT_SUCCESS);
+}
+
+TEST(qmp_client_call_disconnect) {
+ _cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
+ _cleanup_(pidref_done_sigkill_wait) PidRef pid = PIDREF_NULL;
+ int qmp_fds[2];
+ int r;
+
+ ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
+
+ r = ASSERT_OK(pidref_safe_fork("(mock-qmp-call-disc)", FORK_DEATHSIG_SIGKILL|FORK_LOG, &pid));
+ if (r == 0) {
+ safe_close(qmp_fds[0]);
+ mock_qmp_server_call_disconnect(qmp_fds[1]);
+ }
+ safe_close(qmp_fds[1]);
+
+ ASSERT_OK(qmp_client_connect_fd(&client, qmp_fds[0]));
+
+ /* The server reads our stop command and closes without replying. qmp_client_call()
+ * is driving its own pump, so it must notice the EOF, transition to DISCONNECTED,
+ * and return a disconnect error rather than hanging. */
+ r = qmp_client_call(client, "stop", NULL, NULL, NULL);
+ ASSERT_TRUE(r < 0);
+ ASSERT_TRUE(ERRNO_IS_NEG_DISCONNECT(r));
+}
+
TEST(qmp_schema_has_member) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *schema = NULL;