Skip to content

Commit c7c8ad1

Browse files
committedSep 14, 2016
pc_rpc: raise AttributeError immediately for nonexistent RPC methods. Closes #534
1 parent f010a74 commit c7c8ad1

File tree

6 files changed

+35
-12
lines changed

6 files changed

+35
-12
lines changed
 

‎RELEASE_NOTES.rst

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ Release notes
1010
ARTIQ_APPLET_EMBED. The GUI sets this enviroment variable itself and the
1111
user simply needs to remove the --embed argument.
1212
* EnvExperiment's prepare calls prepare for all its children.
13+
* Dynamic __getattr__'s returning RPC target methods are not supported anymore.
14+
Controller driver classes must define all their methods intended for RPC as
15+
members.
1316

1417

1518
2.0rc1

‎artiq/devices/ctlmgr.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ async def call(self, method, *args, **kwargs):
4141
await remote.connect_rpc(self.host, self.port, None)
4242
try:
4343
targets, _ = remote.get_rpc_id()
44-
remote.select_rpc_target(targets[0])
44+
await remote.select_rpc_target(targets[0])
4545
r = await getattr(remote, method)(*args, **kwargs)
4646
finally:
4747
remote.close_rpc()

‎artiq/protocols/fire_and_forget.py

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import threading
22
import logging
3+
import inspect
34

45

56
logger = logging.getLogger(__name__)
@@ -20,6 +21,9 @@ class FFProxy:
2021
"""
2122
def __init__(self, target):
2223
self.target = target
24+
25+
valid_methods = inspect.getmembers(target, inspect.ismethod)
26+
self._valid_methods = {m[0] for m in valid_methods}
2327
self._thread = None
2428

2529
def ff_join(self):
@@ -28,6 +32,8 @@ def ff_join(self):
2832
self._thread.join()
2933

3034
def __getattr__(self, k):
35+
if k not in self._valid_methods:
36+
raise AttributeError
3137
def run_in_thread(*args, **kwargs):
3238
if self._thread is not None and self._thread.is_alive():
3339
logger.warning("skipping fire-and-forget call to %r.%s as "

‎artiq/protocols/pc_rpc.py

+20-8
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,6 @@ class Client:
9494
in the middle of a RPC can break subsequent RPCs (from the same
9595
client).
9696
"""
97-
kernel_invariants = set()
98-
9997
def __init__(self, host, port, target_name=AutoTarget, timeout=None):
10098
self.__socket = socket.create_connection((host, port), timeout)
10199

@@ -106,6 +104,7 @@ def __init__(self, host, port, target_name=AutoTarget, timeout=None):
106104
self.__target_names = server_identification["targets"]
107105
self.__description = server_identification["description"]
108106
self.__selected_target = None
107+
self.__valid_methods = set()
109108
if target_name is not None:
110109
self.select_rpc_target(target_name)
111110
except:
@@ -118,6 +117,7 @@ def select_rpc_target(self, target_name):
118117
target_name = _validate_target_name(target_name, self.__target_names)
119118
self.__socket.sendall((target_name + "\n").encode())
120119
self.__selected_target = target_name
120+
self.__valid_methods = self.__recv()
121121

122122
def get_selected_target(self):
123123
"""Returns the selected target, or ``None`` if no target has been
@@ -173,6 +173,8 @@ def get_rpc_method_list(self):
173173
return self.__do_action(obj)
174174

175175
def __getattr__(self, name):
176+
if name not in self.__valid_methods:
177+
raise AttributeError
176178
def proxy(*args, **kwargs):
177179
return self.__do_rpc(name, args, kwargs)
178180
return proxy
@@ -187,8 +189,6 @@ class AsyncioClient:
187189
Concurrent access from different asyncio tasks is supported; all calls
188190
use a single lock.
189191
"""
190-
kernel_invariants = set()
191-
192192
def __init__(self):
193193
self.__lock = asyncio.Lock()
194194
self.__reader = None
@@ -208,19 +208,21 @@ async def connect_rpc(self, host, port, target_name):
208208
self.__target_names = server_identification["targets"]
209209
self.__description = server_identification["description"]
210210
self.__selected_target = None
211+
self.__valid_methods = set()
211212
if target_name is not None:
212-
self.select_rpc_target(target_name)
213+
await self.select_rpc_target(target_name)
213214
except:
214215
self.close_rpc()
215216
raise
216217

217-
def select_rpc_target(self, target_name):
218+
async def select_rpc_target(self, target_name):
218219
"""Selects a RPC target by name. This function should be called
219220
exactly once if the connection was created with ``target_name=None``.
220221
"""
221222
target_name = _validate_target_name(target_name, self.__target_names)
222223
self.__writer.write((target_name + "\n").encode())
223224
self.__selected_target = target_name
225+
self.__valid_methods = await self.__recv()
224226

225227
def get_selected_target(self):
226228
"""Returns the selected target, or ``None`` if no target has been
@@ -273,6 +275,8 @@ async def __do_rpc(self, name, args, kwargs):
273275
self.__lock.release()
274276

275277
def __getattr__(self, name):
278+
if name not in self.__valid_methods:
279+
raise AttributeError
276280
async def proxy(*args, **kwargs):
277281
res = await self.__do_rpc(name, args, kwargs)
278282
return res
@@ -292,8 +296,6 @@ class BestEffortClient:
292296
:param retry: Amount of time to wait between retries when reconnecting
293297
in the background.
294298
"""
295-
kernel_invariants = set()
296-
297299
def __init__(self, host, port, target_name,
298300
firstcon_timeout=1.0, retry=5.0):
299301
self.__host = host
@@ -303,6 +305,7 @@ def __init__(self, host, port, target_name,
303305

304306
self.__conretry_terminate = False
305307
self.__socket = None
308+
self.__valid_methods = set()
306309
try:
307310
self.__coninit(firstcon_timeout)
308311
except:
@@ -327,6 +330,7 @@ def __coninit(self, timeout):
327330
target_name = _validate_target_name(self.__target_name,
328331
server_identification["targets"])
329332
self.__socket.sendall((target_name + "\n").encode())
333+
self.__valid_methods = self.__recv()
330334

331335
def __start_conretry(self):
332336
self.__conretry_thread = threading.Thread(target=self.__conretry)
@@ -401,6 +405,8 @@ def __do_rpc(self, name, args, kwargs):
401405
raise ValueError
402406

403407
def __getattr__(self, name):
408+
if name not in self.__valid_methods:
409+
raise AttributeError
404410
def proxy(*args, **kwargs):
405411
return self.__do_rpc(name, args, kwargs)
406412
return proxy
@@ -558,6 +564,12 @@ async def _handle_connection_cr(self, reader, writer):
558564
if callable(target):
559565
target = target()
560566

567+
valid_methods = inspect.getmembers(target, inspect.ismethod)
568+
valid_methods = {m[0] for m in valid_methods}
569+
if self.builtin_terminate:
570+
valid_methods.add("terminate")
571+
writer.write((pyon.encode(valid_methods) + "\n").encode())
572+
561573
while True:
562574
line = await reader.readline()
563575
if not line:

‎artiq/test/test_ctlmgr.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ async def get_client(self, host, port):
3333
remote = AsyncioClient()
3434
await remote.connect_rpc(host, port, None)
3535
targets, _ = remote.get_rpc_id()
36-
remote.select_rpc_target(targets[0])
36+
await remote.select_rpc_target(targets[0])
3737
self.addCleanup(remote.close_rpc)
3838
return remote
3939

‎artiq/test/test_pc_rpc.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def _blocking_echo(self, target):
4646
test_object_back = remote.async_echo(test_object)
4747
self.assertEqual(test_object, test_object_back)
4848
with self.assertRaises(AttributeError):
49-
remote.non_existing_method()
49+
remote.non_existing_method
5050
remote.terminate()
5151
finally:
5252
remote.close_rpc()
@@ -73,7 +73,7 @@ async def _asyncio_echo(self, target):
7373
test_object_back = await remote.async_echo(test_object)
7474
self.assertEqual(test_object, test_object_back)
7575
with self.assertRaises(AttributeError):
76-
await remote.non_existing_method()
76+
await remote.non_existing_method
7777
await remote.terminate()
7878
finally:
7979
remote.close_rpc()
@@ -101,6 +101,8 @@ def test_fire_and_forget(self):
101101
self.ok = False
102102
p = fire_and_forget.FFProxy(self)
103103
p._set_ok()
104+
with self.assertRaises(AttributeError):
105+
p.non_existing_method
104106
p.ff_join()
105107
self.assertTrue(self.ok)
106108

0 commit comments

Comments
 (0)
Please sign in to comment.