pc_rpc: support for method listing, frontend: artiq_rpctool, +fixes by SB

This commit is contained in:
Yann Sionneau 2015-02-12 00:38:20 +08:00 committed by Sebastien Bourdeauducq
parent 6d11da3887
commit b396f5dd43
7 changed files with 217 additions and 43 deletions

View File

@ -1,30 +0,0 @@
#!/usr/bin/env python3
import argparse
from artiq.protocols.pc_rpc import Client
def get_argparser():
parser = argparse.ArgumentParser(
description="ARTIQ controller identification tool")
parser.add_argument("server",
help="hostname or IP of the controller to connect to")
parser.add_argument("port", type=int,
help="TCP port to use to connect to the controller")
return parser
def main():
args = get_argparser().parse_args()
remote = Client(args.server, args.port, None)
try:
target_names, id_parameters = remote.get_rpc_id()
finally:
remote.close_rpc()
print("Target(s): " + ", ".join(target_names))
if id_parameters is not None:
print("Parameters: " + id_parameters)
if __name__ == "__main__":
main()

118
artiq/frontend/artiq_rpctool.py Executable file
View File

@ -0,0 +1,118 @@
#!/usr/bin/env python3
import argparse
import textwrap
import sys
from artiq.protocols.pc_rpc import Client
def get_argparser():
parser = argparse.ArgumentParser(
description="ARTIQ RPC tool")
parser.add_argument("server",
help="hostname or IP of the controller to connect to")
parser.add_argument("port", type=int,
help="TCP port to use to connect to the controller")
subparsers = parser.add_subparsers(dest="action")
subparsers.required = True
subparsers.add_parser("list-targets", help="list existing targets")
parser_list_methods = subparsers.add_parser("list-methods",
help="list target's methods")
parser_list_methods.add_argument("-t", "--target", help="target name")
parser_call = subparsers.add_parser("call", help="call a target's method")
parser_call.add_argument("-t", "--target", help="target name")
parser_call.add_argument("method", help="method name")
parser_call.add_argument("args", nargs=argparse.REMAINDER,
help="arguments")
return parser
def list_targets(target_names, id_parameters):
print("Target(s): " + ", ".join(target_names))
if id_parameters is not None:
print("Parameters: " + id_parameters)
def list_methods(remote):
methods = remote.get_rpc_method_list()
for name, (argspec, docstring) in sorted(methods.items()):
args = ""
for arg in argspec["args"]:
args += arg
if argspec["defaults"] is not None:
kword_index = len(argspec["defaults"]) - len(argspec["args"])\
+ argspec["args"].index(arg)
if kword_index >= 0:
if argspec["defaults"][kword_index] == Ellipsis:
args += "=..."
else:
args += "={}".format(argspec["defaults"][kword_index])
if argspec["args"].index(arg) < len(argspec["args"]) - 1:
args += ", "
if argspec["varargs"] is not None:
args += ", *{}".format(argspec["varargs"])
elif len(argspec["kwonlyargs"]) > 0:
args += ", *"
for kwonlyarg in argspec["kwonlyargs"]:
args += ", {}".format(kwonlyarg)
if kwonlyarg in argspec["kwonlydefaults"]:
if argspec["kwonlydefaults"][kwonlyarg] == Ellipsis:
args += "=..."
else:
args += "={}".format(argspec["kwonlydefaults"][kwonlyarg])
if argspec["varkw"] is not None:
args += ", **{}".format(argspec["varkw"])
print("{}({})".format(name, args))
if docstring is not None:
print(textwrap.indent(docstring, " "))
print()
def call_method(remote, method_name, args):
method = getattr(remote, method_name)
if args != []:
args = eval(" ".join(args))
try:
iter(args)
except TypeError:
# not iterable
ret = method(args)
else:
# iterable
ret = method(*args)
else:
ret = method()
if ret is not None:
print("{}".format(ret))
def main():
args = get_argparser().parse_args()
remote = Client(args.server, args.port, None)
targets, id_parameters = remote.get_rpc_id()
if args.action != "list-targets":
# If no target specified and remote has only one, then use this one.
# Exit otherwise.
if len(targets) > 1 and args.target is None:
print("Remote server has several targets, please supply one with "
"-t")
sys.exit(1)
elif args.target is None:
args.target = targets[0]
remote.select_rpc_target(args.target)
if args.action == "list-targets":
list_targets(targets, id_parameters)
elif args.action == "list-methods":
list_methods(remote)
elif args.action == "call":
call_method(remote, args.method, args.args)
else:
print("Unrecognized action: {}".format(args.action))
if __name__ == "__main__":
main()

View File

@ -18,6 +18,7 @@ import traceback
import threading
import time
import logging
import inspect
from artiq.protocols import pyon
from artiq.protocols.asyncio_server import AsyncioServer as _AsyncioServer
@ -127,9 +128,8 @@ class Client:
buf += more.decode()
return pyon.decode(buf)
def __do_rpc(self, name, args, kwargs):
obj = {"action": "call", "name": name, "args": args, "kwargs": kwargs}
self.__send(obj)
def __do_action(self, action):
self.__send(action)
obj = self.__recv()
if obj["status"] == "ok":
@ -139,6 +139,14 @@ class Client:
else:
raise ValueError
def __do_rpc(self, name, args, kwargs):
obj = {"action": "call", "name": name, "args": args, "kwargs": kwargs}
return self.__do_action(obj)
def get_rpc_method_list(self):
obj = {"action": "get_rpc_method_list"}
return self.__do_action(obj)
def __getattr__(self, name):
def proxy(*args, **kwargs):
return self.__do_rpc(name, args, kwargs)
@ -397,9 +405,24 @@ class Server(_AsyncioServer):
break
obj = pyon.decode(line.decode())
try:
method = getattr(target, obj["name"])
ret = method(*obj["args"], **obj["kwargs"])
obj = {"status": "ok", "ret": ret}
if obj["action"] == "get_rpc_method_list":
members = inspect.getmembers(target, inspect.ismethod)
methods = {}
for name, method in members:
if name.startswith("_"):
continue
method = getattr(target, name)
argspec = inspect.getfullargspec(method)
methods[name] = (dict(argspec.__dict__),
inspect.getdoc(method))
obj = {"status": "ok", "ret": methods}
elif obj["action"] == "call":
method = getattr(target, obj["name"])
ret = method(*obj["args"], **obj["kwargs"])
obj = {"status": "ok", "ret": ret}
else:
raise ValueError("Unknown action: {}"
.format(obj["action"]))
except Exception:
obj = {"status": "failed",
"message": traceback.format_exc()}

View File

@ -11,7 +11,7 @@ build:
string: dev
entry_points:
- artiq_client = artiq.frontend.artiq_client:main
- artiq_ctlid = artiq.frontend.artiq_ctlid:main
- artiq_rpctool = artiq.frontend.artiq_rpctool:main
- artiq_gui = artiq.frontend.artiq_gui:main # [not win]
- artiq_master = artiq.frontend.artiq_master:main
- artiq_run = artiq.frontend.artiq_run:main

View File

@ -8,9 +8,72 @@ Local running tool
:ref: artiq.frontend.artiq_run.get_argparser
:prog: artiq_run
Controller identification tool
Remote Procedure Call tool
------------------------------
.. argparse::
:ref: artiq.frontend.artiq_ctlid.get_argparser
:prog: artiq_ctlid
:ref: artiq.frontend.artiq_rpctool.get_argparser
:prog: artiq_rpctool
This tool is the preferred way of handling simple ARTIQ controllers.
Instead of writing a client for very simple cases you can just use this tool
in order to call remote functions of an ARTIQ controller.
* Listing existing targets
The ``list-targets`` sub-command will print to standard output the
target list of the remote server::
$ artiq_rpctool.py hostname port list-targets
* Listing callable functions
The ``list-methods`` sub-command will print to standard output a sorted
list of the functions you can call on the remote server's target.
The list will contain function names, signatures (arguments) and
docstrings.
If the server has only one target, you can do::
$ artiq_rpctool.py hostname port list-methods
Otherwise you need to specify the target, using the ``-t target``
option::
$ artiq_rpctool.py hostname port list-methods -t target_name
* Remotely calling a function
The ``call`` sub-command will call a function on the specified remote
server's target, passing the specified arguments.
Like with the previous sub-command, you only need to provide the target
name (with ``-t target``) if the server hosts several targets.
The following example will call the ``set_attenuation`` method of the
Lda controller with the argument ``5``::
$ artiq_rpctool.py ::1 3253 call -t lda set_attenuation 5
In general, to call a function named ``f`` with N arguments named
respectively ``x1, x2, ..., xN``.
You must pass them as a Python iterable object::
$ artiq_rpctool.py hostname port call -t target f '(x1, x2, ..., xN)'
You can use Python syntax to compute arguments as they will be passed
to the ``eval()`` primitive::
$ artiq_rpctool.py hostname port call -t target f '(x*3+5 for x in range(8))'
$ artiq_rpctool.py hostname port call -t target f 'range(5)'
If you only need one argument, you don't need to pass an iterable, a
single value is accepted.
If the called function has a return value, it will get printed to
the standard output if the value is not None like in the standard
python interactive console::
$ artiq_rpctool.py ::1 3253 call get_attenuation
5.0 dB

View File

@ -51,9 +51,9 @@ and verify that you can connect to the TCP port: ::
:tip: Use the key combination Ctrl-AltGr-9 to get the ``telnet>`` prompt, and enter ``close`` to quit Telnet. Quit the controller with Ctrl-C.
Also verify that a target (service) named "hello" (as passed in the first argument to ``simple_server_loop``) exists using the ``artiq_ctlid.py`` program from the ARTIQ front-end tools: ::
Also verify that a target (service) named "hello" (as passed in the first argument to ``simple_server_loop``) exists using the ``artiq_rpctool.py`` program from the ARTIQ front-end tools: ::
$ artiq_ctlid.py ::1 3249
$ artiq_rpctool.py ::1 3249 list-targets
Target(s): hello
The client

View File

@ -10,7 +10,7 @@ requirements = [
]
scripts = [
"artiq_client=artiq.frontend.artiq_client:main",
"artiq_ctlid=artiq.frontend.artiq_ctlid:main",
"artiq_rpctool=artiq.frontend.artiq_rpctool:main",
"artiq_ctlmgr=artiq.frontend.artiq_ctlmgr:main",
"artiq_master=artiq.frontend.artiq_master:main",
"artiq_run=artiq.frontend.artiq_run:main",