diff --git a/artiq/frontend/artiq_ctlid.py b/artiq/frontend/artiq_ctlid.py deleted file mode 100755 index 4219f04a4..000000000 --- a/artiq/frontend/artiq_ctlid.py +++ /dev/null @@ -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() diff --git a/artiq/frontend/artiq_rpctool.py b/artiq/frontend/artiq_rpctool.py new file mode 100755 index 000000000..2ade18b08 --- /dev/null +++ b/artiq/frontend/artiq_rpctool.py @@ -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() diff --git a/artiq/protocols/pc_rpc.py b/artiq/protocols/pc_rpc.py index 3c49bab81..6499ff65b 100644 --- a/artiq/protocols/pc_rpc.py +++ b/artiq/protocols/pc_rpc.py @@ -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()} diff --git a/conda/artiq/meta.yaml b/conda/artiq/meta.yaml index e2d3b6d0f..e1107e24a 100644 --- a/conda/artiq/meta.yaml +++ b/conda/artiq/meta.yaml @@ -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 diff --git a/doc/manual/utilities.rst b/doc/manual/utilities.rst index cef4cfffb..c6f4d0dc7 100644 --- a/doc/manual/utilities.rst +++ b/doc/manual/utilities.rst @@ -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 diff --git a/doc/manual/writing_a_driver.rst b/doc/manual/writing_a_driver.rst index a709908aa..6aa3d4d51 100644 --- a/doc/manual/writing_a_driver.rst +++ b/doc/manual/writing_a_driver.rst @@ -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 diff --git a/setup.py b/setup.py index bf51a65ea..0afb79417 100755 --- a/setup.py +++ b/setup.py @@ -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",