Commands with the 'async' key will be registered as async type (see related commit), and will allow a synchronous (in scope callback) or asynchronous return (out-of-scope when ready, in idle etc) by keeping the given QmpReturn and calling qmp_return function later.
Ex: { 'command': 'foo-async, 'data': {'arg': 'str'}, 'returns': 'Foo', 'async': true } generates the following marshaller: void qmp_marshal_foo_async(QDict *args, QmpReturn *qret) { Error *err = NULL; Visitor *v; q_obj_foo_async_arg arg = {0}; v = qmp_input_visitor_new(QOBJECT(args), true); visit_start_struct(v, NULL, NULL, 0, &err); if (err) { goto out; } visit_type_q_obj_foo_async_arg_members(v, &arg, &err); if (!err) { visit_check_struct(v, &err); } visit_end_struct(v, NULL); if (err) { goto out; } qmp_foo_async(arg.arg, qret); out: if (err) { qmp_return_error(qret, err); } visit_free(v); v = qapi_dealloc_visitor_new(); visit_start_struct(v, NULL, NULL, 0, NULL); visit_type_q_obj_foo_async_arg_members(v, &arg, NULL); visit_end_struct(v, NULL); visit_free(v); } and a return helper: void qmp_foo_async_return(QmpReturn *qret, Foo *ret_in) { Error *err = NULL; QObject *ret_out = NULL; qmp_marshal_output_Foo(ret_in, &ret_out, &err); if (err) { qmp_return_error(qret, err); } else { qmp_return(qret, ret_out); } } The dispatched function may call the return helper within the calling scope or delay the return. To return an error, it can call qmp_return_error() directly instead. Signed-off-by: Marc-André Lureau <marcandre.lur...@redhat.com> --- scripts/qapi/commands.py | 151 ++++++++++++++++++++---- scripts/qapi/common.py | 15 ++- scripts/qapi/doc.py | 3 +- scripts/qapi/introspect.py | 3 +- tests/test-qmp-cmds.c | 60 ++++++++++ tests/qapi-schema/qapi-schema-test.json | 5 + tests/qapi-schema/qapi-schema-test.out | 8 ++ tests/qapi-schema/test-qapi.py | 8 +- 8 files changed, 218 insertions(+), 35 deletions(-) diff --git a/scripts/qapi/commands.py b/scripts/qapi/commands.py index 6d66bf6aa3..74a7ec112c 100644 --- a/scripts/qapi/commands.py +++ b/scripts/qapi/commands.py @@ -16,18 +16,36 @@ See the COPYING file in the top-level directory. from qapi.common import * -def gen_command_decl(name, arg_type, boxed, ret_type): - return mcgen(''' -%(c_type)s qmp_%(c_name)s(%(params)s); +def gen_command_decl(name, arg_type, boxed, ret_type, success_response, async): + if async: + extra = "QmpReturn *qret" + else: + extra = 'Error **errp' + + if async: + ret = mcgen(''' +void qmp_%(name)s(%(params)s); ''', - c_type=(ret_type and ret_type.c_type()) or 'void', - c_name=c_name(name), - params=build_params(arg_type, boxed, 'Error **errp')) + name=c_name(name), + params=build_params(arg_type, boxed, extra)) + if success_response: + ret += mcgen(''' +void qmp_%(name)s_return(QmpReturn *qret%(c_type)s); +''', + c_type=(", " + ret_type.c_type() if ret_type else ""), + name=c_name(name)) + return ret + else: + return mcgen(''' +%(c_type)s qmp_%(c_name)s(%(params)s); +''', + c_type=(ret_type and ret_type.c_type()) or 'void', + c_name=c_name(name), + params=build_params(arg_type, boxed, extra)) -def gen_call(name, arg_type, boxed, ret_type): - ret = '' +def gen_argstr(arg_type, boxed): argstr = '' if boxed: assert arg_type and not arg_type.is_empty() @@ -39,6 +57,13 @@ def gen_call(name, arg_type, boxed, ret_type): argstr += 'arg.has_%s, ' % c_name(memb.name) argstr += 'arg.%s, ' % c_name(memb.name) + return argstr + + +def gen_call(name, arg_type, boxed, ret_type): + ret = '' + + argstr = gen_argstr(arg_type, boxed) lhs = '' if ret_type: lhs = 'retval = ' @@ -60,6 +85,50 @@ def gen_call(name, arg_type, boxed, ret_type): return ret +def gen_async_call(name, arg_type, boxed): + argstr = gen_argstr(arg_type, boxed) + + push_indent() + ret = mcgen(''' + +qmp_%(c_name)s(%(args)sqret); +''', + c_name=c_name(name), args=argstr) + + pop_indent() + return ret + + +def gen_async_return(name, ret_type): + if ret_type: + return mcgen(''' + +void qmp_%(c_name)s_return(QmpReturn *qret, %(ret_type)s ret_in) +{ + Error *err = NULL; + QObject *ret_out = NULL; + + qmp_marshal_output_%(ret_c_name)s(ret_in, &ret_out, &err); + + if (err) { + qmp_return_error(qret, err); + } else { + qmp_return(qret, ret_out); + } +} +''', + c_name=c_name(name), + ret_type=ret_type.c_type(), ret_c_name=ret_type.c_name()) + else: + return mcgen(''' + +void qmp_%(c_name)s_return(QmpReturn *qret) +{ + qmp_return(qret, QOBJECT(qdict_new())); +} +''', + c_name=c_name(name)) + def gen_marshal_output(ret_type): return mcgen(''' @@ -83,19 +152,22 @@ static void qmp_marshal_output_%(c_name)s(%(c_type)s ret_in, QObject **ret_out, c_type=ret_type.c_type(), c_name=ret_type.c_name()) -def build_marshal_proto(name): - return ('void qmp_marshal_%s(QDict *args, QObject **ret, Error **errp)' - % c_name(name)) +def build_marshal_proto(name, async): + if async: + tmpl = 'void qmp_marshal_%s(QDict *args, QmpReturn *qret)' + else: + tmpl = 'void qmp_marshal_%s(QDict *args, QObject **ret, Error **errp)' + return tmpl % c_name(name) -def gen_marshal_decl(name): +def gen_marshal_decl(name, async): return mcgen(''' %(proto)s; ''', - proto=build_marshal_proto(name)) + proto=build_marshal_proto(name, async)) -def gen_marshal(name, arg_type, boxed, ret_type): +def gen_marshal(name, arg_type, boxed, ret_type, async): have_args = arg_type and not arg_type.is_empty() ret = mcgen(''' @@ -104,9 +176,9 @@ def gen_marshal(name, arg_type, boxed, ret_type): { Error *err = NULL; ''', - proto=build_marshal_proto(name)) + proto=build_marshal_proto(name, async)) - if ret_type: + if ret_type and not async: ret += mcgen(''' %(c_type)s retval; ''', @@ -153,12 +225,28 @@ def gen_marshal(name, arg_type, boxed, ret_type): } ''') - ret += gen_call(name, arg_type, boxed, ret_type) + if async: + ret += gen_async_call(name, arg_type, boxed) + else: + ret += gen_call(name, arg_type, boxed, ret_type) ret += mcgen(''' out: +''') + + if async: + ret += mcgen(''' + if (err) { + qmp_return_error(qret, err); + } +''') + else: + ret += mcgen(''' error_propagate(errp, err); +''') + + ret += mcgen(''' visit_free(v); ''') @@ -193,7 +281,8 @@ out: return ret -def gen_register_command(name, success_response, allow_oob, allow_preconfig): +def gen_register_command(name, success_response, allow_oob, allow_preconfig, + async): options = [] if not success_response: @@ -202,17 +291,24 @@ def gen_register_command(name, success_response, allow_oob, allow_preconfig): options += ['QCO_ALLOW_OOB'] if allow_preconfig: options += ['QCO_ALLOW_PRECONFIG'] + if async: + options += ['QCO_ASYNC'] if not options: options = ['QCO_NO_OPTIONS'] options = " | ".join(options) + if async: + regfn = 'qmp_register_async_command' + else: + regfn = 'qmp_register_command' + ret = mcgen(''' - qmp_register_command(cmds, "%(name)s", + %(regfn)s(cmds, "%(name)s", qmp_marshal_%(c_name)s, %(opts)s); ''', - name=name, c_name=c_name(name), + regfn=regfn, name=name, c_name=c_name(name), opts=options) return ret @@ -278,7 +374,8 @@ void %(c_prefix)sqmp_init_marshal(QmpCommandList *cmds); genc.add(gen_registry(self._regy.get_content(), self._prefix)) def visit_command(self, name, info, ifcond, arg_type, ret_type, gen, - success_response, boxed, allow_oob, allow_preconfig): + success_response, boxed, allow_oob, allow_preconfig, + async): if not gen: return # FIXME: If T is a user-defined type, the user is responsible @@ -292,11 +389,15 @@ void %(c_prefix)sqmp_init_marshal(QmpCommandList *cmds); self._genh, self._genc, self._regy): self._genc.add(gen_marshal_output(ret_type)) with ifcontext(ifcond, self._genh, self._genc, self._regy): - self._genh.add(gen_command_decl(name, arg_type, boxed, ret_type)) - self._genh.add(gen_marshal_decl(name)) - self._genc.add(gen_marshal(name, arg_type, boxed, ret_type)) + self._genh.add(gen_command_decl(name, arg_type, boxed, ret_type, + success_response, async)) + self._genh.add(gen_marshal_decl(name, async)) + self._genc.add(gen_marshal(name, arg_type, boxed, ret_type, async)) + if async and success_response: + self._genc.add(gen_async_return(name, ret_type)) self._regy.add(gen_register_command(name, success_response, - allow_oob, allow_preconfig)) + allow_oob, allow_preconfig, + async)) def gen_commands(schema, output_dir, prefix): diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py index f07869ec73..1bca5fc150 100644 --- a/scripts/qapi/common.py +++ b/scripts/qapi/common.py @@ -993,7 +993,7 @@ def check_exprs(exprs): meta = 'command' check_keys(expr_elem, 'command', [], ['data', 'returns', 'gen', 'success-response', - 'boxed', 'allow-oob', 'allow-preconfig', 'if']) + 'boxed', 'allow-oob', 'allow-preconfig', 'if', 'async']) normalize_members(expr.get('data')) elif 'event' in expr: meta = 'event' @@ -1136,7 +1136,8 @@ class QAPISchemaVisitor(object): pass def visit_command(self, name, info, ifcond, arg_type, ret_type, gen, - success_response, boxed, allow_oob, allow_preconfig): + success_response, boxed, allow_oob, allow_preconfig, + async): pass def visit_event(self, name, info, ifcond, arg_type, boxed): @@ -1533,7 +1534,8 @@ class QAPISchemaAlternateType(QAPISchemaType): class QAPISchemaCommand(QAPISchemaEntity): def __init__(self, name, info, doc, ifcond, arg_type, ret_type, - gen, success_response, boxed, allow_oob, allow_preconfig): + gen, success_response, boxed, allow_oob, allow_preconfig, + async): QAPISchemaEntity.__init__(self, name, info, doc, ifcond) assert not arg_type or isinstance(arg_type, str) assert not ret_type or isinstance(ret_type, str) @@ -1546,6 +1548,7 @@ class QAPISchemaCommand(QAPISchemaEntity): self.boxed = boxed self.allow_oob = allow_oob self.allow_preconfig = allow_preconfig + self.async = async def check(self, schema): QAPISchemaEntity.check(self, schema) @@ -1572,7 +1575,7 @@ class QAPISchemaCommand(QAPISchemaEntity): self.arg_type, self.ret_type, self.gen, self.success_response, self.boxed, self.allow_oob, - self.allow_preconfig) + self.allow_preconfig, self.async) class QAPISchemaEvent(QAPISchemaEntity): @@ -1820,6 +1823,7 @@ class QAPISchema(object): allow_oob = expr.get('allow-oob', False) allow_preconfig = expr.get('allow-preconfig', False) ifcond = expr.get('if') + async = expr.get('async', False) if isinstance(data, OrderedDict): data = self._make_implicit_object_type( name, info, doc, ifcond, 'arg', self._make_members(data, info)) @@ -1828,7 +1832,8 @@ class QAPISchema(object): rets = self._make_array_type(rets[0], info) self._def_entity(QAPISchemaCommand(name, info, doc, ifcond, data, rets, gen, success_response, - boxed, allow_oob, allow_preconfig)) + boxed, allow_oob, allow_preconfig, + async)) def _def_event(self, expr, info, doc): name = expr['event'] diff --git a/scripts/qapi/doc.py b/scripts/qapi/doc.py index 5c8c136899..0f51a7bff3 100755 --- a/scripts/qapi/doc.py +++ b/scripts/qapi/doc.py @@ -236,7 +236,8 @@ class QAPISchemaGenDocVisitor(qapi.common.QAPISchemaVisitor): body=texi_entity(doc, 'Members', ifcond))) def visit_command(self, name, info, ifcond, arg_type, ret_type, gen, - success_response, boxed, allow_oob, allow_preconfig): + success_response, boxed, allow_oob, allow_preconfig, + async): doc = self.cur_doc if boxed: body = texi_body(doc) diff --git a/scripts/qapi/introspect.py b/scripts/qapi/introspect.py index f7f2ca07e4..7f3ea88c72 100644 --- a/scripts/qapi/introspect.py +++ b/scripts/qapi/introspect.py @@ -202,7 +202,8 @@ const QLitObject %(c_name)s = %(c_string)s; for m in variants.variants]}, ifcond) def visit_command(self, name, info, ifcond, arg_type, ret_type, gen, - success_response, boxed, allow_oob, allow_preconfig): + success_response, boxed, allow_oob, allow_preconfig, + async): arg_type = arg_type or self._schema.the_empty_object_type ret_type = ret_type or self._schema.the_empty_object_type obj = {'arg-type': self._use_type(arg_type), diff --git a/tests/test-qmp-cmds.c b/tests/test-qmp-cmds.c index c4593552e3..58cd09ab08 100644 --- a/tests/test-qmp-cmds.c +++ b/tests/test-qmp-cmds.c @@ -36,6 +36,28 @@ void qmp_cmd_success_response(Error **errp) { } +static gboolean cmd_async_idle(gpointer user_data) +{ + QmpReturn *qret = user_data; + + qmp_cmd_async_return(qret, g_new0(Empty2, 1)); + + return G_SOURCE_REMOVE; +} + +void qmp_cmd_async(const char *filename, QmpReturn *qret) +{ + g_idle_add(cmd_async_idle, qret); +} + +void qmp_cmd_success_response_async(const char *filename, QmpReturn *qret) +{ + Error *err = NULL; + + error_setg(&err, "no response, but error ok"); + qmp_return_error(qret, err); +} + Empty2 *qmp_user_def_cmd0(Error **errp) { return g_new0(Empty2, 1); @@ -360,6 +382,43 @@ static void test_qmp_return_orderly(void) qobject_unref(dict); } +typedef struct QmpReturnAsync { + QmpSession session; + GMainLoop *loop; +} QmpReturnAsync; + +static void dispatch_return_async(QmpSession *session, QDict *resp) +{ + QmpReturnAsync *a = container_of(session, QmpReturnAsync, session); + + g_main_loop_quit(a->loop); + g_main_loop_unref(a->loop); + a->loop = NULL; +} + +static void test_qmp_return_async(void) +{ + QmpReturnAsync a = { 0, }; + QDict *args = qdict_new(); + QDict *req = qdict_new(); + + a.loop = g_main_loop_new(NULL, TRUE); + qmp_session_init(&a.session, &qmp_commands, + NULL, dispatch_return_async); + + qdict_put_str(args, "filename", "test-filename"); + qdict_put_str(req, "execute", "cmd-async"); + qdict_put(req, "arguments", args); + qmp_dispatch(&a.session, QOBJECT(req), false); + g_assert(a.loop); + + g_main_loop_run(a.loop); + g_assert(!a.loop); + + qmp_session_destroy(&a.session); + qobject_unref(req); +} + int main(int argc, char **argv) { g_test_init(&argc, &argv, NULL); @@ -373,6 +432,7 @@ int main(int argc, char **argv) g_test_add_func("/qmp/dealloc_types", test_dealloc_types); g_test_add_func("/qmp/dealloc_partial", test_dealloc_partial); g_test_add_func("/qmp/return_orderly", test_qmp_return_orderly); + g_test_add_func("/qmp/return_async", test_qmp_return_async); test_qmp_init_marshal(&qmp_commands); g_test_run(); diff --git a/tests/qapi-schema/qapi-schema-test.json b/tests/qapi-schema/qapi-schema-test.json index 0952c68734..d10ce39215 100644 --- a/tests/qapi-schema/qapi-schema-test.json +++ b/tests/qapi-schema/qapi-schema-test.json @@ -143,6 +143,11 @@ { 'command': 'cmd-success-response', 'data': {}, 'success-response': false } +{ 'command': 'cmd-async', 'data': {'filename': 'str'}, + 'returns': 'Empty2', 'async': true } +{ 'command': 'cmd-success-response-async', 'data': {'filename': 'str'}, + 'async': true, 'success-response': false} + # Returning a non-dictionary requires a name from the whitelist { 'command': 'guest-get-time', 'data': {'a': 'int', '*b': 'int' }, 'returns': 'int' } diff --git a/tests/qapi-schema/qapi-schema-test.out b/tests/qapi-schema/qapi-schema-test.out index 77fb1e1aa9..d652ebf75d 100644 --- a/tests/qapi-schema/qapi-schema-test.out +++ b/tests/qapi-schema/qapi-schema-test.out @@ -208,6 +208,14 @@ command user_def_cmd2 q_obj_user_def_cmd2-arg -> UserDefTwo gen=True success_response=True boxed=False oob=False preconfig=False command cmd-success-response None -> None gen=True success_response=False boxed=False oob=False preconfig=False +object q_obj_cmd-async-arg + member filename: str optional=False +command cmd-async q_obj_cmd-async-arg -> Empty2 + gen=True success_response=True boxed=False oob=False preconfig=False async=True +object q_obj_cmd-success-response-async-arg + member filename: str optional=False +command cmd-success-response-async q_obj_cmd-success-response-async-arg -> None + gen=True success_response=False boxed=False oob=False preconfig=False async=True object q_obj_guest-get-time-arg member a: int optional=False member b: int optional=True diff --git a/tests/qapi-schema/test-qapi.py b/tests/qapi-schema/test-qapi.py index d21fca01fc..0d0cef479c 100644 --- a/tests/qapi-schema/test-qapi.py +++ b/tests/qapi-schema/test-qapi.py @@ -55,12 +55,14 @@ class QAPISchemaTestVisitor(QAPISchemaVisitor): self._print_if(ifcond) def visit_command(self, name, info, ifcond, arg_type, ret_type, gen, - success_response, boxed, allow_oob, allow_preconfig): + success_response, boxed, allow_oob, allow_preconfig, + async): print('command %s %s -> %s' % (name, arg_type and arg_type.name, ret_type and ret_type.name)) - print(' gen=%s success_response=%s boxed=%s oob=%s preconfig=%s' - % (gen, success_response, boxed, allow_oob, allow_preconfig)) + print(' gen=%s success_response=%s boxed=%s oob=%s preconfig=%s%s' + % (gen, success_response, boxed, allow_oob, allow_preconfig, + ' async=True' if async else '')) self._print_if(ifcond) def visit_event(self, name, info, ifcond, arg_type, boxed): -- 2.21.0.196.g041f5ea1cf