|
1 | 1 | /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
2 | 2 |
|
3 | 3 | #include "sd-event.h" |
| 4 | +#include "sd-future.h" |
4 | 5 | #include "sd-json.h" |
5 | 6 |
|
6 | 7 | #include "alloc-util.h" |
@@ -226,19 +227,23 @@ static int qmp_extract_response_id(sd_json_variant *v, uint64_t *ret) { |
226 | 227 | return 1; |
227 | 228 | } |
228 | 229 |
|
229 | | -/* Returns 0 on success (ret_result = "return" value), -EIO on QMP error (reterr_desc set). */ |
230 | | -static int qmp_parse_response(sd_json_variant *v, sd_json_variant **ret_result, const char **reterr_desc) { |
| 230 | +/* Returns 0 on success (ret_result = freshly reffed "return" value), -EIO on QMP error |
| 231 | + * (ret_error_desc set to a freshly allocated string). Caller owns both outputs. */ |
| 232 | +static int qmp_parse_response(sd_json_variant *v, sd_json_variant **ret_result, char **reterr_error_desc) { |
231 | 233 | const char *desc; |
232 | 234 |
|
233 | 235 | desc = qmp_extract_error_description(v); |
234 | 236 | if (desc) { |
235 | | - if (reterr_desc) |
236 | | - *reterr_desc = desc; |
| 237 | + if (reterr_error_desc) { |
| 238 | + *reterr_error_desc = strdup(desc); |
| 239 | + if (!*reterr_error_desc) |
| 240 | + return -ENOMEM; |
| 241 | + } |
237 | 242 | return -EIO; |
238 | 243 | } |
239 | 244 |
|
240 | 245 | if (ret_result) |
241 | | - *ret_result = sd_json_variant_by_key(v, "return"); |
| 246 | + *ret_result = sd_json_variant_ref(sd_json_variant_by_key(v, "return")); |
242 | 247 | return 0; |
243 | 248 | } |
244 | 249 |
|
@@ -273,8 +278,8 @@ static int qmp_client_build_command( |
273 | 278 |
|
274 | 279 | /* Route c->current to event callback or matching async slot. Returns 1 on dispatch. */ |
275 | 280 | static int qmp_client_dispatch(QmpClient *c) { |
276 | | - sd_json_variant *result = NULL; |
277 | | - const char *desc = NULL; |
| 281 | + _cleanup_(sd_json_variant_unrefp) sd_json_variant *result = NULL; |
| 282 | + _cleanup_free_ char *desc = NULL; |
278 | 283 | uint64_t id; |
279 | 284 | int error, r; |
280 | 285 |
|
@@ -318,8 +323,8 @@ static int qmp_client_dispatch(QmpClient *c) { |
318 | 323 | } |
319 | 324 |
|
320 | 325 | /* Synchronous slot (no callback): leave c->current pinned so qmp_client_call() can |
321 | | - * pick up the reply and hand out borrowed pointers into it. The sync caller owns a |
322 | | - * ref on the slot and detects completion by observing slot->client turning NULL. */ |
| 326 | + * pick the reply up after its pump loop. The sync caller owns a ref on the slot and |
| 327 | + * detects completion by observing slot->client turning NULL. */ |
323 | 328 | if (!slot->callback) { |
324 | 329 | qmp_slot_disconnect(slot, /* unref= */ true); |
325 | 330 | return 1; |
@@ -574,6 +579,10 @@ static void qmp_client_clear(QmpClient *c) { |
574 | 579 | qmp_client_detach_event(c); |
575 | 580 | qmp_client_clear_current(c); |
576 | 581 | json_stream_done(&c->stream); |
| 582 | + /* qmp_client_handle_disconnect() above drained every entry via qmp_client_fail_pending(); |
| 583 | + * the set is borrow-only for non-floating slots, so set_free() can't safely run a |
| 584 | + * destructor over leftovers — enforce the drain invariant instead. */ |
| 585 | + assert(set_isempty(c->slots)); |
577 | 586 | c->slots = set_free(c->slots); |
578 | 587 | } |
579 | 588 |
|
@@ -745,7 +754,7 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(QmpClientArgs*, qmp_client_args_close_fds); |
745 | 754 |
|
746 | 755 | /* Shared send path for qmp_client_invoke() and qmp_client_call(). A NULL callback registers |
747 | 756 | * a "synchronous" slot: dispatch_reply leaves c->current pinned on match instead of invoking |
748 | | - * a callback, so qmp_client_call() can hand out borrowed pointers into the reply. If ret_slot |
| 757 | + * a callback, so qmp_client_call() can pick the reply up after its pump loop. If ret_slot |
749 | 758 | * is NULL the slot is allocated as floating (owned by c->slots); otherwise a reference is |
750 | 759 | * handed back to the caller. */ |
751 | 760 | static int qmp_client_send( |
@@ -810,21 +819,193 @@ int qmp_client_invoke( |
810 | 819 | return qmp_client_send(c, command, args, callback, userdata, ret_slot); |
811 | 820 | } |
812 | 821 |
|
| 822 | +typedef struct QmpFuture { |
| 823 | + QmpSlot *slot; /* owned, non-floating; NULL once disconnected */ |
| 824 | + sd_json_variant *result; |
| 825 | + char *error_desc; |
| 826 | +} QmpFuture; |
| 827 | + |
| 828 | +static void* qmp_future_alloc(void) { |
| 829 | + return new0(QmpFuture, 1); |
| 830 | +} |
| 831 | + |
| 832 | +static void qmp_future_free(sd_future *f) { |
| 833 | + QmpFuture *qf = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f))); |
| 834 | + qmp_slot_unref(qf->slot); |
| 835 | + sd_json_variant_unref(qf->result); |
| 836 | + free(qf->error_desc); |
| 837 | + free(qf); |
| 838 | +} |
| 839 | + |
| 840 | +static int qmp_future_cancel(sd_future *f) { |
| 841 | + QmpFuture *qf = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f))); |
| 842 | + |
| 843 | + /* Drop the pending slot so dispatch_reply won't try to fire our callback (and touch |
| 844 | + * freed memory) when the reply eventually arrives. */ |
| 845 | + qf->slot = qmp_slot_unref(qf->slot); |
| 846 | + return sd_future_resolve(f, -ECANCELED); |
| 847 | +} |
| 848 | + |
| 849 | +static const sd_future_ops qmp_call_future_ops = { |
| 850 | + .size = sizeof(sd_future_ops), |
| 851 | + .alloc = qmp_future_alloc, |
| 852 | + .free = qmp_future_free, |
| 853 | + .cancel = qmp_future_cancel, |
| 854 | +}; |
| 855 | + |
| 856 | +static int qmp_future_callback( |
| 857 | + QmpClient *c, |
| 858 | + sd_json_variant *result, |
| 859 | + const char *desc, |
| 860 | + int error, |
| 861 | + void *userdata) { |
| 862 | + |
| 863 | + sd_future *f = ASSERT_PTR(userdata); |
| 864 | + QmpFuture *qf = ASSERT_PTR(sd_future_get_private(f)); |
| 865 | + int r; |
| 866 | + |
| 867 | + assert(result || desc || error); |
| 868 | + |
| 869 | + if (result) |
| 870 | + qf->result = sd_json_variant_ref(result); |
| 871 | + if (desc) { |
| 872 | + qf->error_desc = strdup(desc); |
| 873 | + if (!qf->error_desc) |
| 874 | + /* No usable reply payload to surface — propagate as transport-style |
| 875 | + * failure so suspend() / sd_future_result() see the OOM. */ |
| 876 | + return sd_future_resolve(f, -ENOMEM); |
| 877 | + } |
| 878 | + |
| 879 | + /* Resolve with 0 on a success reply and -EIO on a QMP-level error (matching the synchronous |
| 880 | + * path's errno for the desc-without-ret_error_desc case), so a caller awaiting the future |
| 881 | + * learns about call failures from the resolution value alone. The reply payload itself |
| 882 | + * (result or error_desc) is always stashed on the QmpFuture so future_get_qmp_reply() can |
| 883 | + * hand the description string back on top of the bare -EIO. With no reply at all (transport |
| 884 | + * failure, disconnect), resolve with the propagated transport errno; cancellation surfaces |
| 885 | + * as -ECANCELED via qmp_future_cancel(). */ |
| 886 | + if (result) |
| 887 | + r = 0; |
| 888 | + else if (desc) |
| 889 | + r = -EIO; |
| 890 | + else |
| 891 | + r = error; |
| 892 | + |
| 893 | + return sd_future_resolve(f, r); |
| 894 | +} |
| 895 | + |
| 896 | +int qmp_client_call_future( |
| 897 | + QmpClient *c, |
| 898 | + const char *command, |
| 899 | + QmpClientArgs *args, |
| 900 | + sd_future **ret) { |
| 901 | + |
| 902 | + int r; |
| 903 | + |
| 904 | + assert(c); |
| 905 | + assert(command); |
| 906 | + assert(ret); |
| 907 | + |
| 908 | + _cleanup_(sd_future_unrefp) sd_future *f = NULL; |
| 909 | + r = sd_future_new(&qmp_call_future_ops, &f); |
| 910 | + if (r < 0) |
| 911 | + return r; |
| 912 | + |
| 913 | + QmpFuture *qf = sd_future_get_private(f); |
| 914 | + |
| 915 | + r = qmp_client_send(c, command, args, qmp_future_callback, f, &qf->slot); |
| 916 | + if (r < 0) |
| 917 | + return r; |
| 918 | + |
| 919 | + *ret = TAKE_PTR(f); |
| 920 | + return 0; |
| 921 | +} |
| 922 | + |
| 923 | +/* Extract the reply from a resolved qmp_client_call_future(). Returns 1 on success (with |
| 924 | + * *ret_result a fresh reference the caller unrefs), -EIO on a QMP-level error (with the detail |
| 925 | + * description copied into *ret_error_desc when the caller passed one to receive it), and the |
| 926 | + * future's negative resume errno when no reply landed at all (transport failure / cancellation). |
| 927 | + */ |
| 928 | +int future_get_qmp_reply(sd_future *f, sd_json_variant **ret_result, char **reterr_error_desc) { |
| 929 | + assert(f); |
| 930 | + assert(sd_future_get_ops(f) == &qmp_call_future_ops); |
| 931 | + assert(sd_future_state(f) == SD_FUTURE_RESOLVED); |
| 932 | + |
| 933 | + QmpFuture *qf = ASSERT_PTR(sd_future_get_private(f)); |
| 934 | + |
| 935 | + /* No reply at all: transport failure or cancellation — surface the future result. */ |
| 936 | + if (!qf->result && !qf->error_desc) |
| 937 | + return sd_future_result(f); |
| 938 | + |
| 939 | + if (qf->error_desc) { |
| 940 | + if (reterr_error_desc) { |
| 941 | + char *desc = strdup(qf->error_desc); |
| 942 | + if (!desc) |
| 943 | + return -ENOMEM; |
| 944 | + *reterr_error_desc = desc; |
| 945 | + } |
| 946 | + return -EIO; |
| 947 | + } |
| 948 | + |
| 949 | + if (reterr_error_desc) |
| 950 | + *reterr_error_desc = NULL; |
| 951 | + if (ret_result) |
| 952 | + *ret_result = sd_json_variant_ref(qf->result); |
| 953 | + |
| 954 | + return 1; |
| 955 | +} |
| 956 | + |
| 957 | +static int qmp_client_call_suspend( |
| 958 | + QmpClient *c, |
| 959 | + const char *command, |
| 960 | + QmpClientArgs *args, |
| 961 | + sd_json_variant **ret_result, |
| 962 | + char **ret_error_desc) { |
| 963 | + |
| 964 | + int r; |
| 965 | + |
| 966 | + assert(c); |
| 967 | + assert(command); |
| 968 | + assert(sd_fiber_is_running()); |
| 969 | + |
| 970 | + _cleanup_(sd_future_cancel_wait_unrefp) sd_future *call = NULL; |
| 971 | + r = qmp_client_call_future(c, command, args, &call); |
| 972 | + if (r < 0) |
| 973 | + return r; |
| 974 | + |
| 975 | + r = sd_fiber_suspend(); |
| 976 | + |
| 977 | + /* If the future isn't resolved, the suspend was interrupted before a reply arrived (fiber |
| 978 | + * cancelled, fiber-wide SD_FIBER_TIMEOUT scope expired, …). There's no reply to extract, |
| 979 | + * so surface the resume error directly. When the future is resolved, future_get_qmp_reply() |
| 980 | + * already encodes success (1), QMP-level error (-EIO with the desc captured if asked for), |
| 981 | + * and no-reply (negative future result) — pass it through. */ |
| 982 | + if (sd_future_state(call) != SD_FUTURE_RESOLVED) |
| 983 | + return r; |
| 984 | + |
| 985 | + return future_get_qmp_reply(call, ret_result, ret_error_desc); |
| 986 | +} |
| 987 | + |
813 | 988 | int qmp_client_call( |
814 | 989 | QmpClient *c, |
815 | 990 | const char *command, |
816 | 991 | QmpClientArgs *args, |
817 | 992 | sd_json_variant **ret_result, |
818 | | - const char **ret_error_desc) { |
| 993 | + char **reterr_error_desc) { |
819 | 994 |
|
820 | | - _cleanup_(qmp_slot_unrefp) QmpSlot *slot = NULL; |
| 995 | + _cleanup_(sd_json_variant_unrefp) sd_json_variant *result = NULL; |
| 996 | + _cleanup_free_ char *desc = NULL; |
821 | 997 | int r; |
822 | 998 |
|
823 | 999 | assert_return(c, -EINVAL); |
824 | 1000 | assert_return(command, -EINVAL); |
825 | 1001 |
|
826 | | - /* Drop any reply pinned by a previous qmp_client_call() before we pin a new one. */ |
827 | | - qmp_client_clear_current(c); |
| 1002 | + /* If we're on a fiber sharing the QMP client's event loop, use the async + suspend path so |
| 1003 | + * multiple concurrent qmp_client_call() invocations across fibers don't deadlock each other |
| 1004 | + * on the process+wait pump. */ |
| 1005 | + if (sd_fiber_is_running() && qmp_client_get_event(c) == sd_fiber_get_event()) |
| 1006 | + return qmp_client_call_suspend(c, command, args, ret_result, reterr_error_desc); |
| 1007 | + |
| 1008 | + _cleanup_(qmp_slot_unrefp) QmpSlot *slot = NULL; |
828 | 1009 |
|
829 | 1010 | /* NULL callback marks this as a synchronous slot: dispatch_reply matches on id like |
830 | 1011 | * any other slot (so stray unknown-id replies still get logged and dropped), but |
@@ -855,18 +1036,24 @@ int qmp_client_call( |
855 | 1036 | return r; |
856 | 1037 | } |
857 | 1038 |
|
858 | | - sd_json_variant *result = NULL; |
859 | | - const char *desc = NULL; |
860 | | - int error = qmp_parse_response(c->current, &result, &desc); |
| 1039 | + _cleanup_(sd_json_variant_unrefp) sd_json_variant *current = TAKE_PTR(c->current); |
| 1040 | + r = qmp_parse_response(current, &result, &desc); |
| 1041 | + if (r < 0 && r != -EIO) |
| 1042 | + return r; |
861 | 1043 |
|
862 | | - /* If caller doesn't ask for the error string, surface the error as the return code. */ |
863 | | - if (!ret_error_desc && error < 0) |
864 | | - return error; |
| 1044 | + /* QMP-level error: copy the description into *ret_error_desc when the caller asked for it, |
| 1045 | + * and surface the failure via the return value (matching qmp_client_call_suspend() / |
| 1046 | + * sd_bus_call()'s "negative on error" convention). */ |
| 1047 | + if (desc) { |
| 1048 | + if (reterr_error_desc) |
| 1049 | + *reterr_error_desc = TAKE_PTR(desc); |
| 1050 | + return -EIO; |
| 1051 | + } |
865 | 1052 |
|
866 | 1053 | if (ret_result) |
867 | | - *ret_result = result; |
868 | | - if (ret_error_desc) |
869 | | - *ret_error_desc = desc; |
| 1054 | + *ret_result = TAKE_PTR(result); |
| 1055 | + if (reterr_error_desc) |
| 1056 | + *reterr_error_desc = NULL; |
870 | 1057 |
|
871 | 1058 | return 1; |
872 | 1059 | } |
|
0 commit comments