qemu-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [Qemu-devel] [PATCH v3 5/9] QAPI: allow to specify valid runstates p


From: Dr. David Alan Gilbert
Subject: Re: [Qemu-devel] [PATCH v3 5/9] QAPI: allow to specify valid runstates per command
Date: Wed, 7 Mar 2018 14:16:50 +0000
User-agent: Mutt/1.9.2 (2017-12-15)

* Igor Mammedov (address@hidden) wrote:
> Add optional 'runstates' parameter in QAPI command definition,
> which will permit to specify RunState variations in which
> a command could be exectuted via QMP monitor.
> 
> For compatibility reasons, commands, that don't use
> 'runstates' explicitly, are not permited to run in
> preconfig state but allowed in all other states.
> 
> New option will be used to allow commands, which are
> prepared/need to run this early, to run in preconfig state.
> It will include query-hotpluggable-cpus and new set-numa-node
> commands. Other commands that should be able to run in
> preconfig state, should be ammeded to not expect machine
> in initialized state.
> 
> Signed-off-by: Igor Mammedov <address@hidden>
> ---
>  include/qapi/qmp/dispatch.h             |  5 +++-
>  monitor.c                               | 28 +++++++++++++++++---
>  qapi-schema.json                        | 12 +++++++--
>  qapi/qmp-dispatch.c                     | 39 ++++++++++++++++++++++++++++
>  qapi/qmp-registry.c                     |  4 ++-
>  qapi/run-state.json                     |  6 ++++-
>  scripts/qapi-commands.py                | 46 
> ++++++++++++++++++++++++++-------
>  scripts/qapi-introspect.py              |  2 +-
>  scripts/qapi.py                         | 15 +++++++----
>  scripts/qapi2texi.py                    |  2 +-
>  tests/qapi-schema/doc-good.out          |  4 +--
>  tests/qapi-schema/ident-with-escape.out |  2 +-
>  tests/qapi-schema/indented-expr.out     |  4 +--
>  tests/qapi-schema/qapi-schema-test.out  | 18 ++++++-------
>  tests/qapi-schema/test-qapi.py          |  6 ++---
>  15 files changed, 151 insertions(+), 42 deletions(-)
> 
> diff --git a/include/qapi/qmp/dispatch.h b/include/qapi/qmp/dispatch.h
> index 1e694b5..f821781 100644
> --- a/include/qapi/qmp/dispatch.h
> +++ b/include/qapi/qmp/dispatch.h
> @@ -15,6 +15,7 @@
>  #define QAPI_QMP_DISPATCH_H
>  
>  #include "qemu/queue.h"
> +#include "qapi-types.h"
>  
>  typedef void (QmpCommandFunc)(QDict *, QObject **, Error **);
>  
> @@ -31,12 +32,14 @@ typedef struct QmpCommand
>      QmpCommandOptions options;
>      QTAILQ_ENTRY(QmpCommand) node;
>      bool enabled;
> +    const RunState *valid_runstates;
>  } QmpCommand;
>  
>  typedef QTAILQ_HEAD(QmpCommandList, QmpCommand) QmpCommandList;
>  
>  void qmp_register_command(QmpCommandList *cmds, const char *name,
> -                          QmpCommandFunc *fn, QmpCommandOptions options);
> +                          QmpCommandFunc *fn, QmpCommandOptions options,
> +                          const RunState valid_runstates[]);
>  void qmp_unregister_command(QmpCommandList *cmds, const char *name);
>  QmpCommand *qmp_find_command(QmpCommandList *cmds, const char *name);
>  QObject *qmp_dispatch(QmpCommandList *cmds, QObject *request);
> diff --git a/monitor.c b/monitor.c
> index fcb5386..2edc96c 100644
> --- a/monitor.c
> +++ b/monitor.c
> @@ -224,6 +224,25 @@ static mon_cmd_t mon_cmds[];
>  static mon_cmd_t info_cmds[];
>  
>  QmpCommandList qmp_commands, qmp_cap_negotiation_commands;
> +const RunState cap_negotiation_valid_runstates[] = {
> +    RUN_STATE_DEBUG,
> +    RUN_STATE_INMIGRATE,
> +    RUN_STATE_INTERNAL_ERROR,
> +    RUN_STATE_IO_ERROR,
> +    RUN_STATE_PAUSED,
> +    RUN_STATE_POSTMIGRATE,
> +    RUN_STATE_PRELAUNCH,
> +    RUN_STATE_FINISH_MIGRATE,
> +    RUN_STATE_RESTORE_VM,
> +    RUN_STATE_RUNNING,
> +    RUN_STATE_SAVE_VM,
> +    RUN_STATE_SHUTDOWN,
> +    RUN_STATE_SUSPENDED,
> +    RUN_STATE_WATCHDOG,
> +    RUN_STATE_GUEST_PANICKED,
> +    RUN_STATE_COLO,
> +    RUN_STATE_PRECONFIG,
> +};

It's a shame this is such a manual copy.

While you're taking a big hammer to HMP in the preconfig case,
it's worth noting this more specific code is only protecting QMP
commands.


It's a shame in all the uses below we can't be working with bitmasks
of run-state's; it would feel a lot easier to have a default and mask
out or or in extra states.

Dave

>  Monitor *cur_mon;
>  
> @@ -1016,17 +1035,18 @@ void monitor_init_qmp_commands(void)
>  
>      qmp_register_command(&qmp_commands, "query-qmp-schema",
>                           qmp_query_qmp_schema,
> -                         QCO_NO_OPTIONS);
> +                         QCO_NO_OPTIONS, NULL);
>      qmp_register_command(&qmp_commands, "device_add", qmp_device_add,
> -                         QCO_NO_OPTIONS);
> +                         QCO_NO_OPTIONS, NULL);
>      qmp_register_command(&qmp_commands, "netdev_add", qmp_netdev_add,
> -                         QCO_NO_OPTIONS);
> +                         QCO_NO_OPTIONS, NULL);
>  
>      qmp_unregister_commands_hack();
>  
>      QTAILQ_INIT(&qmp_cap_negotiation_commands);
>      qmp_register_command(&qmp_cap_negotiation_commands, "qmp_capabilities",
> -                         qmp_marshal_qmp_capabilities, QCO_NO_OPTIONS);
> +                         qmp_marshal_qmp_capabilities, QCO_NO_OPTIONS,
> +                         cap_negotiation_valid_runstates);
>  }
>  
>  void qmp_qmp_capabilities(Error **errp)
> diff --git a/qapi-schema.json b/qapi-schema.json
> index 0262b9f..ee1053d 100644
> --- a/qapi-schema.json
> +++ b/qapi-schema.json
> @@ -219,7 +219,11 @@
>  # Note: This example has been shortened as the real response is too long.
>  #
>  ##
> -{ 'command': 'query-commands', 'returns': ['CommandInfo'] }
> +{ 'command': 'query-commands', 'returns': ['CommandInfo'],
> +  'runstates': [ 'debug', 'inmigrate', 'internal-error', 'io-error', 
> 'paused',
> +                 'postmigrate', 'prelaunch', 'finish-migrate', 'restore-vm',
> +                 'running', 'save-vm', 'shutdown', 'suspended', 'watchdog',
> +                 'guest-panicked', 'colo', 'preconfig' ] }

That's going to get to be a pain to update as we add more states.

>  ##
>  # @LostTickPolicy:
> @@ -1146,7 +1150,11 @@
>  # <- { "return": {} }
>  #
>  ##
> -{ 'command': 'cont' }
> +{ 'command': 'cont',
> +  'runstates': [ 'debug', 'inmigrate', 'internal-error', 'io-error', 
> 'paused',
> +                 'postmigrate', 'prelaunch', 'finish-migrate', 'restore-vm',
> +                 'running', 'save-vm', 'shutdown', 'suspended', 'watchdog',
> +                 'guest-panicked', 'colo', 'preconfig' ] }
>  
>  ##
>  # @system_wakeup:
> diff --git a/qapi/qmp-dispatch.c b/qapi/qmp-dispatch.c
> index e31ac4b..26cba19 100644
> --- a/qapi/qmp-dispatch.c
> +++ b/qapi/qmp-dispatch.c
> @@ -17,6 +17,7 @@
>  #include "qapi/qmp/json-parser.h"
>  #include "qapi/qmp/qdict.h"
>  #include "qapi/qmp/qjson.h"
> +#include "sysemu/sysemu.h"
>  
>  static QDict *qmp_dispatch_check_obj(const QObject *request, Error **errp)
>  {
> @@ -65,6 +66,40 @@ static QDict *qmp_dispatch_check_obj(const QObject 
> *request, Error **errp)
>      return dict;
>  }
>  
> +static bool is_cmd_permited(const QmpCommand *cmd, Error **errp)
> +{
> +    int i;
> +    char *list = NULL;
> +
> +    /* Old commands that don't have explicit runstate in schema
> +     * are permited to run except of at PRECONFIG stage
> +     */
> +    if (!cmd->valid_runstates) {
> +        if (runstate_check(RUN_STATE_PRECONFIG)) {
> +            const char *state = RunState_str(RUN_STATE_PRECONFIG);
> +            error_setg(errp, "The command '%s' isn't valid in '%s'",
> +                       cmd->name, state);
> +            return false;
> +        }
> +        return true;
> +    }
> +
> +    for (i = 0; cmd->valid_runstates[i] != RUN_STATE__MAX; i++) {
> +        if (runstate_check(cmd->valid_runstates[i])) {
> +            return true;
> +        }
> +    }
> +
> +    for (i = 0; cmd->valid_runstates[i] != RUN_STATE__MAX; i++) {
> +        const char *state = RunState_str(cmd->valid_runstates[i]);
> +        list = g_strjoin(", ", state, list, NULL);
> +    }
> +    error_setg(errp, "The command '%s' is valid only in '%s'", cmd->name, 
> list);
> +    g_free(list);
> +
> +    return false;
> +}
> +
>  static QObject *do_qmp_dispatch(QmpCommandList *cmds, QObject *request,
>                                  Error **errp)
>  {
> @@ -92,6 +127,10 @@ static QObject *do_qmp_dispatch(QmpCommandList *cmds, 
> QObject *request,
>          return NULL;
>      }
>  
> +    if (!is_cmd_permited(cmd, errp)) {
> +        return NULL;
> +    }
> +
>      if (!qdict_haskey(dict, "arguments")) {
>          args = qdict_new();
>      } else {
> diff --git a/qapi/qmp-registry.c b/qapi/qmp-registry.c
> index 5af484c..59a5b85 100644
> --- a/qapi/qmp-registry.c
> +++ b/qapi/qmp-registry.c
> @@ -16,7 +16,8 @@
>  #include "qapi/qmp/dispatch.h"
>  
>  void qmp_register_command(QmpCommandList *cmds, const char *name,
> -                          QmpCommandFunc *fn, QmpCommandOptions options)
> +                          QmpCommandFunc *fn, QmpCommandOptions options,
> +                          const RunState valid_runstates[])
>  {
>      QmpCommand *cmd = g_malloc0(sizeof(*cmd));
>  
> @@ -24,6 +25,7 @@ void qmp_register_command(QmpCommandList *cmds, const char 
> *name,
>      cmd->fn = fn;
>      cmd->enabled = true;
>      cmd->options = options;
> +    cmd->valid_runstates = valid_runstates;
>      QTAILQ_INSERT_TAIL(cmds, cmd, node);
>  }
>  
> diff --git a/qapi/run-state.json b/qapi/run-state.json
> index d24a4e8..1c1c9b8 100644
> --- a/qapi/run-state.json
> +++ b/qapi/run-state.json
> @@ -92,7 +92,11 @@
>  #                  "status": "running" } }
>  #
>  ##
> -{ 'command': 'query-status', 'returns': 'StatusInfo' }
> +{ 'command': 'query-status', 'returns': 'StatusInfo',
> +  'runstates': [ 'debug', 'inmigrate', 'internal-error', 'io-error', 
> 'paused',
> +                 'postmigrate', 'prelaunch', 'finish-migrate', 'restore-vm',
> +                 'running', 'save-vm', 'shutdown', 'suspended', 'watchdog',
> +                 'guest-panicked', 'colo', 'preconfig' ] }
>  
>  ##
>  # @SHUTDOWN:
> diff --git a/scripts/qapi-commands.py b/scripts/qapi-commands.py
> index f89d748..659e167 100644
> --- a/scripts/qapi-commands.py
> +++ b/scripts/qapi-commands.py
> @@ -192,17 +192,45 @@ out:
>      return ret
>  
>  
> -def gen_register_command(name, success_response):
> +def gen_register_command(name, success_response, runstates):
>      options = 'QCO_NO_OPTIONS'
>      if not success_response:
>          options = 'QCO_NO_SUCCESS_RESP'
>  
> -    ret = mcgen('''
> -    qmp_register_command(cmds, "%(name)s",
> -                         qmp_marshal_%(c_name)s, %(opts)s);
> -''',
> -                name=name, c_name=c_name(name),
> -                opts=options)
> +    if runstates is None:
> +        ret = mcgen('''
> +        qmp_register_command(cmds, "%(name)s",
> +                             qmp_marshal_%(c_name)s, %(opts)s,
> +                             NULL);
> +        ''',
> +                     name=name, c_name=c_name(name),
> +                     opts=options)
> +    else:
> +        ret = mcgen('''
> +        static const RunState qmp_valid_states_%(c_name)s[] = {
> +'''
> +                   , c_name=c_name(name))
> +
> +        for value in runstates:
> +            ret += mcgen('''
> +                    %(c_enum)s,
> +'''
> +                         , c_enum=c_enum_const('RunState', value))
> +
> +        ret += mcgen('''
> +                    %(c_enum)s,
> +                };
> +'''
> +                     , c_enum=c_enum_const('RunState', "_MAX"))
> +
> +        ret += mcgen('''
> +                qmp_register_command(cmds, "%(name)s",
> +                                     qmp_marshal_%(c_name)s, %(opts)s,
> +                                     qmp_valid_states_%(c_name)s);
> +        ''',
> +                     name=name, c_name=c_name(name),
> +                     opts=options)
> +
>      return ret
>  
>  
> @@ -241,7 +269,7 @@ class QAPISchemaGenCommandVisitor(QAPISchemaVisitor):
>          self._visited_ret_types = None
>  
>      def visit_command(self, name, info, arg_type, ret_type,
> -                      gen, success_response, boxed):
> +                      gen, success_response, boxed, runstates):
>          if not gen:
>              return
>          self.decl += gen_command_decl(name, arg_type, boxed, ret_type)
> @@ -250,7 +278,7 @@ class QAPISchemaGenCommandVisitor(QAPISchemaVisitor):
>              self.defn += gen_marshal_output(ret_type)
>          self.decl += gen_marshal_decl(name)
>          self.defn += gen_marshal(name, arg_type, boxed, ret_type)
> -        self._regy += gen_register_command(name, success_response)
> +        self._regy += gen_register_command(name, success_response, runstates)
>  
>  
>  (input_file, output_dir, do_c, do_h, prefix, opts) = parse_command_line()
> diff --git a/scripts/qapi-introspect.py b/scripts/qapi-introspect.py
> index 032bcea..ede86ac 100644
> --- a/scripts/qapi-introspect.py
> +++ b/scripts/qapi-introspect.py
> @@ -154,7 +154,7 @@ const char %(c_name)s[] = %(c_string)s;
>                                      for m in variants.variants]})
>  
>      def visit_command(self, name, info, arg_type, ret_type,
> -                      gen, success_response, boxed):
> +                      gen, success_response, boxed, runstates):
>          arg_type = arg_type or self._schema.the_empty_object_type
>          ret_type = ret_type or self._schema.the_empty_object_type
>          self._gen_json(name, 'command',
> diff --git a/scripts/qapi.py b/scripts/qapi.py
> index 58f995b..5c5037e 100644
> --- a/scripts/qapi.py
> +++ b/scripts/qapi.py
> @@ -919,7 +919,8 @@ def check_exprs(exprs):
>          elif 'command' in expr:
>              meta = 'command'
>              check_keys(expr_elem, 'command', [],
> -                       ['data', 'returns', 'gen', 'success-response', 
> 'boxed'])
> +                       ['data', 'returns', 'gen', 'success-response', 
> 'boxed',
> +                        'runstates'])
>          elif 'event' in expr:
>              meta = 'event'
>              check_keys(expr_elem, 'event', [], ['data', 'boxed'])
> @@ -1030,7 +1031,7 @@ class QAPISchemaVisitor(object):
>          pass
>  
>      def visit_command(self, name, info, arg_type, ret_type,
> -                      gen, success_response, boxed):
> +                      gen, success_response, boxed, runstates):
>          pass
>  
>      def visit_event(self, name, info, arg_type, boxed):
> @@ -1397,7 +1398,7 @@ class QAPISchemaAlternateType(QAPISchemaType):
>  
>  class QAPISchemaCommand(QAPISchemaEntity):
>      def __init__(self, name, info, doc, arg_type, ret_type,
> -                 gen, success_response, boxed):
> +                 gen, success_response, boxed, runstates):
>          QAPISchemaEntity.__init__(self, name, info, doc)
>          assert not arg_type or isinstance(arg_type, str)
>          assert not ret_type or isinstance(ret_type, str)
> @@ -1408,6 +1409,7 @@ class QAPISchemaCommand(QAPISchemaEntity):
>          self.gen = gen
>          self.success_response = success_response
>          self.boxed = boxed
> +        self.runstates = runstates
>  
>      def check(self, schema):
>          if self._arg_type_name:
> @@ -1431,7 +1433,8 @@ class QAPISchemaCommand(QAPISchemaEntity):
>      def visit(self, visitor):
>          visitor.visit_command(self.name, self.info,
>                                self.arg_type, self.ret_type,
> -                              self.gen, self.success_response, self.boxed)
> +                              self.gen, self.success_response, self.boxed,
> +                              self.runstates)
>  
>  
>  class QAPISchemaEvent(QAPISchemaEntity):
> @@ -1639,6 +1642,7 @@ class QAPISchema(object):
>          gen = expr.get('gen', True)
>          success_response = expr.get('success-response', True)
>          boxed = expr.get('boxed', False)
> +        runstates = expr.get('runstates')
>          if isinstance(data, OrderedDict):
>              data = self._make_implicit_object_type(
>                  name, info, doc, 'arg', self._make_members(data, info))
> @@ -1646,7 +1650,8 @@ class QAPISchema(object):
>              assert len(rets) == 1
>              rets = self._make_array_type(rets[0], info)
>          self._def_entity(QAPISchemaCommand(name, info, doc, data, rets,
> -                                           gen, success_response, boxed))
> +                                           gen, success_response, boxed,
> +                                           runstates))
>  
>      def _def_event(self, expr, info, doc):
>          name = expr['event']
> diff --git a/scripts/qapi2texi.py b/scripts/qapi2texi.py
> index bf1c57b..d28594c 100755
> --- a/scripts/qapi2texi.py
> +++ b/scripts/qapi2texi.py
> @@ -227,7 +227,7 @@ class QAPISchemaGenDocVisitor(qapi.QAPISchemaVisitor):
>                               body=texi_entity(doc, 'Members'))
>  
>      def visit_command(self, name, info, arg_type, ret_type,
> -                      gen, success_response, boxed):
> +                      gen, success_response, boxed, runstates):
>          doc = self.cur_doc
>          if boxed:
>              body = texi_body(doc)
> diff --git a/tests/qapi-schema/doc-good.out b/tests/qapi-schema/doc-good.out
> index 1d2c250..a47d6f6 100644
> --- a/tests/qapi-schema/doc-good.out
> +++ b/tests/qapi-schema/doc-good.out
> @@ -18,9 +18,9 @@ object Variant1
>      member var1: str optional=False
>  object Variant2
>  command cmd q_obj_cmd-arg -> Object
> -   gen=True success_response=True boxed=False
> +   gen=True success_response=True boxed=False, runstates=None
>  command cmd-boxed Object -> None
> -   gen=True success_response=True boxed=True
> +   gen=True success_response=True boxed=True, runstates=None
>  object q_empty
>  object q_obj_Variant1-wrapper
>      member data: Variant1 optional=False
> diff --git a/tests/qapi-schema/ident-with-escape.out 
> b/tests/qapi-schema/ident-with-escape.out
> index b5637cb..d9a272b 100644
> --- a/tests/qapi-schema/ident-with-escape.out
> +++ b/tests/qapi-schema/ident-with-escape.out
> @@ -1,7 +1,7 @@
>  enum QType ['none', 'qnull', 'qnum', 'qstring', 'qdict', 'qlist', 'qbool']
>      prefix QTYPE
>  command fooA q_obj_fooA-arg -> None
> -   gen=True success_response=True boxed=False
> +   gen=True success_response=True boxed=False, runstates=None
>  object q_empty
>  object q_obj_fooA-arg
>      member bar1: str optional=False
> diff --git a/tests/qapi-schema/indented-expr.out 
> b/tests/qapi-schema/indented-expr.out
> index 586795f..4b128d5 100644
> --- a/tests/qapi-schema/indented-expr.out
> +++ b/tests/qapi-schema/indented-expr.out
> @@ -1,7 +1,7 @@
>  enum QType ['none', 'qnull', 'qnum', 'qstring', 'qdict', 'qlist', 'qbool']
>      prefix QTYPE
>  command eins None -> None
> -   gen=True success_response=True boxed=False
> +   gen=True success_response=True boxed=False, runstates=None
>  object q_empty
>  command zwei None -> None
> -   gen=True success_response=True boxed=False
> +   gen=True success_response=True boxed=False, runstates=None
> diff --git a/tests/qapi-schema/qapi-schema-test.out 
> b/tests/qapi-schema/qapi-schema-test.out
> index 3b1e908..787c228 100644
> --- a/tests/qapi-schema/qapi-schema-test.out
> +++ b/tests/qapi-schema/qapi-schema-test.out
> @@ -153,15 +153,15 @@ object __org.qemu_x-Union2
>      tag __org.qemu_x-member1
>      case __org.qemu_x-value: __org.qemu_x-Struct2
>  command __org.qemu_x-command q_obj___org.qemu_x-command-arg -> 
> __org.qemu_x-Union1
> -   gen=True success_response=True boxed=False
> +   gen=True success_response=True boxed=False, runstates=None
>  command boxed-struct UserDefZero -> None
> -   gen=True success_response=True boxed=True
> +   gen=True success_response=True boxed=True, runstates=None
>  command boxed-union UserDefNativeListUnion -> None
> -   gen=True success_response=True boxed=True
> +   gen=True success_response=True boxed=True, runstates=None
>  command guest-get-time q_obj_guest-get-time-arg -> int
> -   gen=True success_response=True boxed=False
> +   gen=True success_response=True boxed=False, runstates=None
>  command guest-sync q_obj_guest-sync-arg -> any
> -   gen=True success_response=True boxed=False
> +   gen=True success_response=True boxed=False, runstates=None
>  object q_empty
>  object q_obj_EVENT_C-arg
>      member a: int optional=True
> @@ -222,10 +222,10 @@ object q_obj_user_def_cmd2-arg
>      member ud1a: UserDefOne optional=False
>      member ud1b: UserDefOne optional=True
>  command user_def_cmd None -> None
> -   gen=True success_response=True boxed=False
> +   gen=True success_response=True boxed=False, runstates=None
>  command user_def_cmd0 Empty2 -> Empty2
> -   gen=True success_response=True boxed=False
> +   gen=True success_response=True boxed=False, runstates=None
>  command user_def_cmd1 q_obj_user_def_cmd1-arg -> None
> -   gen=True success_response=True boxed=False
> +   gen=True success_response=True boxed=False, runstates=None
>  command user_def_cmd2 q_obj_user_def_cmd2-arg -> UserDefTwo
> -   gen=True success_response=True boxed=False
> +   gen=True success_response=True boxed=False, runstates=None
> diff --git a/tests/qapi-schema/test-qapi.py b/tests/qapi-schema/test-qapi.py
> index ac43d34..958576d 100644
> --- a/tests/qapi-schema/test-qapi.py
> +++ b/tests/qapi-schema/test-qapi.py
> @@ -37,11 +37,11 @@ class QAPISchemaTestVisitor(QAPISchemaVisitor):
>          self._print_variants(variants)
>  
>      def visit_command(self, name, info, arg_type, ret_type,
> -                      gen, success_response, boxed):
> +                      gen, success_response, boxed, runstates):
>          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' % \
> -              (gen, success_response, boxed))
> +        print('   gen=%s success_response=%s boxed=%s, runstates=%s' % \
> +              (gen, success_response, boxed, runstates))
>  
>      def visit_event(self, name, info, arg_type, boxed):
>          print('event %s %s' % (name, arg_type and arg_type.name))
> -- 
> 2.7.4
> 
> 
--
Dr. David Alan Gilbert / address@hidden / Manchester, UK



reply via email to

[Prev in Thread] Current Thread [Next in Thread]