Skip to content

Commit b396f5d

Browse files
fallensbourdeauducq
authored andcommittedFeb 11, 2015
pc_rpc: support for method listing, frontend: artiq_rpctool, +fixes by SB
1 parent 6d11da3 commit b396f5d

File tree

7 files changed

+217
-43
lines changed

7 files changed

+217
-43
lines changed
 

Diff for: ‎artiq/frontend/artiq_ctlid.py

-30
This file was deleted.

Diff for: ‎artiq/frontend/artiq_rpctool.py

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import textwrap
5+
import sys
6+
7+
from artiq.protocols.pc_rpc import Client
8+
9+
10+
def get_argparser():
11+
parser = argparse.ArgumentParser(
12+
description="ARTIQ RPC tool")
13+
parser.add_argument("server",
14+
help="hostname or IP of the controller to connect to")
15+
parser.add_argument("port", type=int,
16+
help="TCP port to use to connect to the controller")
17+
subparsers = parser.add_subparsers(dest="action")
18+
subparsers.required = True
19+
subparsers.add_parser("list-targets", help="list existing targets")
20+
parser_list_methods = subparsers.add_parser("list-methods",
21+
help="list target's methods")
22+
parser_list_methods.add_argument("-t", "--target", help="target name")
23+
parser_call = subparsers.add_parser("call", help="call a target's method")
24+
parser_call.add_argument("-t", "--target", help="target name")
25+
parser_call.add_argument("method", help="method name")
26+
parser_call.add_argument("args", nargs=argparse.REMAINDER,
27+
help="arguments")
28+
return parser
29+
30+
31+
def list_targets(target_names, id_parameters):
32+
print("Target(s): " + ", ".join(target_names))
33+
if id_parameters is not None:
34+
print("Parameters: " + id_parameters)
35+
36+
37+
def list_methods(remote):
38+
methods = remote.get_rpc_method_list()
39+
for name, (argspec, docstring) in sorted(methods.items()):
40+
args = ""
41+
for arg in argspec["args"]:
42+
args += arg
43+
if argspec["defaults"] is not None:
44+
kword_index = len(argspec["defaults"]) - len(argspec["args"])\
45+
+ argspec["args"].index(arg)
46+
if kword_index >= 0:
47+
if argspec["defaults"][kword_index] == Ellipsis:
48+
args += "=..."
49+
else:
50+
args += "={}".format(argspec["defaults"][kword_index])
51+
if argspec["args"].index(arg) < len(argspec["args"]) - 1:
52+
args += ", "
53+
if argspec["varargs"] is not None:
54+
args += ", *{}".format(argspec["varargs"])
55+
elif len(argspec["kwonlyargs"]) > 0:
56+
args += ", *"
57+
for kwonlyarg in argspec["kwonlyargs"]:
58+
args += ", {}".format(kwonlyarg)
59+
if kwonlyarg in argspec["kwonlydefaults"]:
60+
if argspec["kwonlydefaults"][kwonlyarg] == Ellipsis:
61+
args += "=..."
62+
else:
63+
args += "={}".format(argspec["kwonlydefaults"][kwonlyarg])
64+
if argspec["varkw"] is not None:
65+
args += ", **{}".format(argspec["varkw"])
66+
print("{}({})".format(name, args))
67+
if docstring is not None:
68+
print(textwrap.indent(docstring, " "))
69+
print()
70+
71+
72+
def call_method(remote, method_name, args):
73+
method = getattr(remote, method_name)
74+
if args != []:
75+
args = eval(" ".join(args))
76+
try:
77+
iter(args)
78+
except TypeError:
79+
# not iterable
80+
ret = method(args)
81+
else:
82+
# iterable
83+
ret = method(*args)
84+
else:
85+
ret = method()
86+
if ret is not None:
87+
print("{}".format(ret))
88+
89+
90+
def main():
91+
args = get_argparser().parse_args()
92+
93+
remote = Client(args.server, args.port, None)
94+
95+
targets, id_parameters = remote.get_rpc_id()
96+
97+
if args.action != "list-targets":
98+
# If no target specified and remote has only one, then use this one.
99+
# Exit otherwise.
100+
if len(targets) > 1 and args.target is None:
101+
print("Remote server has several targets, please supply one with "
102+
"-t")
103+
sys.exit(1)
104+
elif args.target is None:
105+
args.target = targets[0]
106+
remote.select_rpc_target(args.target)
107+
108+
if args.action == "list-targets":
109+
list_targets(targets, id_parameters)
110+
elif args.action == "list-methods":
111+
list_methods(remote)
112+
elif args.action == "call":
113+
call_method(remote, args.method, args.args)
114+
else:
115+
print("Unrecognized action: {}".format(args.action))
116+
117+
if __name__ == "__main__":
118+
main()

Diff for: ‎artiq/protocols/pc_rpc.py

+29-6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import threading
1919
import time
2020
import logging
21+
import inspect
2122

2223
from artiq.protocols import pyon
2324
from artiq.protocols.asyncio_server import AsyncioServer as _AsyncioServer
@@ -127,9 +128,8 @@ def __recv(self):
127128
buf += more.decode()
128129
return pyon.decode(buf)
129130

130-
def __do_rpc(self, name, args, kwargs):
131-
obj = {"action": "call", "name": name, "args": args, "kwargs": kwargs}
132-
self.__send(obj)
131+
def __do_action(self, action):
132+
self.__send(action)
133133

134134
obj = self.__recv()
135135
if obj["status"] == "ok":
@@ -139,6 +139,14 @@ def __do_rpc(self, name, args, kwargs):
139139
else:
140140
raise ValueError
141141

142+
def __do_rpc(self, name, args, kwargs):
143+
obj = {"action": "call", "name": name, "args": args, "kwargs": kwargs}
144+
return self.__do_action(obj)
145+
146+
def get_rpc_method_list(self):
147+
obj = {"action": "get_rpc_method_list"}
148+
return self.__do_action(obj)
149+
142150
def __getattr__(self, name):
143151
def proxy(*args, **kwargs):
144152
return self.__do_rpc(name, args, kwargs)
@@ -397,9 +405,24 @@ def _handle_connection_cr(self, reader, writer):
397405
break
398406
obj = pyon.decode(line.decode())
399407
try:
400-
method = getattr(target, obj["name"])
401-
ret = method(*obj["args"], **obj["kwargs"])
402-
obj = {"status": "ok", "ret": ret}
408+
if obj["action"] == "get_rpc_method_list":
409+
members = inspect.getmembers(target, inspect.ismethod)
410+
methods = {}
411+
for name, method in members:
412+
if name.startswith("_"):
413+
continue
414+
method = getattr(target, name)
415+
argspec = inspect.getfullargspec(method)
416+
methods[name] = (dict(argspec.__dict__),
417+
inspect.getdoc(method))
418+
obj = {"status": "ok", "ret": methods}
419+
elif obj["action"] == "call":
420+
method = getattr(target, obj["name"])
421+
ret = method(*obj["args"], **obj["kwargs"])
422+
obj = {"status": "ok", "ret": ret}
423+
else:
424+
raise ValueError("Unknown action: {}"
425+
.format(obj["action"]))
403426
except Exception:
404427
obj = {"status": "failed",
405428
"message": traceback.format_exc()}

Diff for: ‎conda/artiq/meta.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ build:
1111
string: dev
1212
entry_points:
1313
- artiq_client = artiq.frontend.artiq_client:main
14-
- artiq_ctlid = artiq.frontend.artiq_ctlid:main
14+
- artiq_rpctool = artiq.frontend.artiq_rpctool:main
1515
- artiq_gui = artiq.frontend.artiq_gui:main # [not win]
1616
- artiq_master = artiq.frontend.artiq_master:main
1717
- artiq_run = artiq.frontend.artiq_run:main

Diff for: ‎doc/manual/utilities.rst

+66-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,72 @@ Local running tool
88
:ref: artiq.frontend.artiq_run.get_argparser
99
:prog: artiq_run
1010

11-
Controller identification tool
11+
Remote Procedure Call tool
1212
------------------------------
1313

1414
.. argparse::
15-
:ref: artiq.frontend.artiq_ctlid.get_argparser
16-
:prog: artiq_ctlid
15+
:ref: artiq.frontend.artiq_rpctool.get_argparser
16+
:prog: artiq_rpctool
17+
18+
This tool is the preferred way of handling simple ARTIQ controllers.
19+
Instead of writing a client for very simple cases you can just use this tool
20+
in order to call remote functions of an ARTIQ controller.
21+
22+
* Listing existing targets
23+
24+
The ``list-targets`` sub-command will print to standard output the
25+
target list of the remote server::
26+
27+
$ artiq_rpctool.py hostname port list-targets
28+
29+
* Listing callable functions
30+
31+
The ``list-methods`` sub-command will print to standard output a sorted
32+
list of the functions you can call on the remote server's target.
33+
34+
The list will contain function names, signatures (arguments) and
35+
docstrings.
36+
37+
If the server has only one target, you can do::
38+
39+
$ artiq_rpctool.py hostname port list-methods
40+
41+
Otherwise you need to specify the target, using the ``-t target``
42+
option::
43+
44+
$ artiq_rpctool.py hostname port list-methods -t target_name
45+
46+
* Remotely calling a function
47+
48+
The ``call`` sub-command will call a function on the specified remote
49+
server's target, passing the specified arguments.
50+
Like with the previous sub-command, you only need to provide the target
51+
name (with ``-t target``) if the server hosts several targets.
52+
53+
The following example will call the ``set_attenuation`` method of the
54+
Lda controller with the argument ``5``::
55+
56+
$ artiq_rpctool.py ::1 3253 call -t lda set_attenuation 5
57+
58+
In general, to call a function named ``f`` with N arguments named
59+
respectively ``x1, x2, ..., xN``.
60+
61+
You must pass them as a Python iterable object::
62+
63+
$ artiq_rpctool.py hostname port call -t target f '(x1, x2, ..., xN)'
64+
65+
You can use Python syntax to compute arguments as they will be passed
66+
to the ``eval()`` primitive::
67+
68+
$ artiq_rpctool.py hostname port call -t target f '(x*3+5 for x in range(8))'
69+
$ artiq_rpctool.py hostname port call -t target f 'range(5)'
70+
71+
If you only need one argument, you don't need to pass an iterable, a
72+
single value is accepted.
73+
74+
If the called function has a return value, it will get printed to
75+
the standard output if the value is not None like in the standard
76+
python interactive console::
77+
78+
$ artiq_rpctool.py ::1 3253 call get_attenuation
79+
5.0 dB

Diff for: ‎doc/manual/writing_a_driver.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ and verify that you can connect to the TCP port: ::
5151

5252
: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.
5353

54-
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: ::
54+
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: ::
5555

56-
$ artiq_ctlid.py ::1 3249
56+
$ artiq_rpctool.py ::1 3249 list-targets
5757
Target(s): hello
5858

5959
The client

Diff for: ‎setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
]
1111
scripts = [
1212
"artiq_client=artiq.frontend.artiq_client:main",
13-
"artiq_ctlid=artiq.frontend.artiq_ctlid:main",
13+
"artiq_rpctool=artiq.frontend.artiq_rpctool:main",
1414
"artiq_ctlmgr=artiq.frontend.artiq_ctlmgr:main",
1515
"artiq_master=artiq.frontend.artiq_master:main",
1616
"artiq_run=artiq.frontend.artiq_run:main",

0 commit comments

Comments
 (0)
Please sign in to comment.