PageRenderTime 72ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/ivy/ivy.py

https://gitlab.com/ivybus/ivy-python
Python | 1547 lines | 1283 code | 75 blank | 189 comment | 48 complexity | 4ca80ced2d1958b4a1e35ce4771d7574 MD5 | raw file
Possible License(s): BSD-3-Clause

Large files files are truncated, but you can click here to view the full file

  1. """
  2. Using an IvyServer
  3. ------------------
  4. The following code is a typical example of use::
  5. from ivy.ivy import IvyServer
  6. class MyAgent(IvyServer):
  7. def __init__(self, agent_name):
  8. IvyServer.__init__(self,agent_name)
  9. self.start('127.255.255.255:2010')
  10. self.bind_msg(self.handle_hello, 'hello .*')
  11. self.bind_msg(self.handle_button, 'BTN ([a-fA-F0-9]+)')
  12. def handle_hello(self, agent):
  13. print('[Agent %s] GOT hello from %r'%(self.agent_name, agent))
  14. def handle_button(self, agent, btn_id):
  15. print('[Agent %s] GOT BTN button_id=%s from %r'%(self.agent_name, btn_id, agent))
  16. # let's answer!
  17. self.send_msg('BTN_ACK %s'%btn_id)
  18. a=MyAgent('007')
  19. Implementation details
  20. ----------------------
  21. An Ivy agent is made of several threads:
  22. - an :class:`IvyServer` instance
  23. - a UDP server, launched by the Ivy server, listening to incoming UDP
  24. broadcast messages
  25. - :class:`IvyTimer` objects
  26. :group Messages types: BYE, ADD_REGEXP, MSG, ERROR, DEL_REGEXP, END_REGEXP,
  27. END_INIT, START_REGEXP, START_INIT, DIRECT_MSG, DIE
  28. :group Separators: ARG_START, ARG_END
  29. :group Misc. constants: DEFAULT_IVYBUS, PROTOCOL_VERSION, IVY_SHOULD_NOT_DIE
  30. IvyApplicationConnected, IvyApplicationDisconnected, DEFAULT_TTL
  31. :group Objects and functions related to logging: ivylogger, debug, log, warn,
  32. error, ivy_loghdlr, ivy_logformatter
  33. Copyright (c) 2005-2019 Sebastien Bigaret <sbigaret@users.sourceforge.net>
  34. """
  35. import sys
  36. # The next line taken from https://pythonhosted.org/six/
  37. # Copyright (c) 2010-2016 Benjamin Peterson
  38. PY2 = sys.version_info[0] == 2
  39. import logging
  40. import os
  41. import random
  42. import re
  43. import socket
  44. import struct
  45. import threading
  46. import time
  47. import traceback
  48. import types
  49. if PY2:
  50. import SocketServer as socketserver
  51. else:
  52. import socketserver
  53. ivylogger = logging.getLogger('Ivy')
  54. if os.environ.get('IVY_LOG_TRACE'):
  55. logging.TRACE = logging.DEBUG - 1
  56. logging.addLevelName(logging.TRACE, 'TRACE')
  57. trace = lambda *args, **kw: ivylogger.log(logging.TRACE, *args, **kw)
  58. else:
  59. trace = lambda *args, **kw: None
  60. debug = ivylogger.debug
  61. info = log = ivylogger.info
  62. warn = ivylogger.warning
  63. error = ivylogger.error
  64. ivy_loghdlr = logging.StreamHandler() # stderr by default
  65. ivy_logformatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
  66. ivy_loghdlr.setFormatter(ivy_logformatter)
  67. ivylogger.addHandler(ivy_loghdlr)
  68. ##
  69. DEFAULT_IVYBUS = '127:2010'
  70. PROTOCOL_VERSION = 3
  71. # Message types. Refer to "The Ivy architecture and protocol" for details
  72. BYE = 0
  73. ADD_REGEXP = 1
  74. MSG = 2
  75. ERROR = 3
  76. DEL_REGEXP = 4
  77. # START_REGEXP and END_REGEXP are the ones declared in ivy.c
  78. # however we'll use the aliases START_INIT and END_INIT here
  79. END_REGEXP = END_INIT = 5
  80. START_REGEXP = START_INIT = 6
  81. DIRECT_MSG = 7
  82. DIE = 8
  83. PING = 9
  84. PONG = 10
  85. # Other constants
  86. ARG_START = '\002'
  87. ARG_END = '\003'
  88. # for multicast, arbitrary TTL value taken from ivysocket.c:SocketAddMember
  89. DEFAULT_TTL = 64
  90. IvyApplicationConnected = 1
  91. IvyApplicationDisconnected = 2
  92. IvyRegexpAdded = 3
  93. IvyRegexpRemoved = 4
  94. IVY_SHOULD_NOT_DIE = 'Ivy Application Should Not Die'
  95. def void_function(*arg, **kw):
  96. """A function that accepts any number of parameters and does nothing"""
  97. pass
  98. def UDP_init_and_listen(broadcast_addr, port, socket_server):
  99. """
  100. Called by an IvyServer at startup; the method is responsible for:
  101. - sending the initial UDP broadcast message,
  102. - waiting for incoming UDP broadcast messages being sent by new clients
  103. connecting on the bus. When it receives such a message, a connection
  104. is established to that client and that connection (a socket) is then
  105. passed to the IvyServer instance.
  106. :Parameters:
  107. - `broadcast_addr`: the broadcast address used on the Ivy bus
  108. - `port`: the port dedicated to the Ivy bus
  109. - `socket_server`: instance of an IvyServer handling communications
  110. for our client.
  111. """
  112. log('Starting Ivy UDP Server on %r:%r' % (broadcast_addr, port))
  113. s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  114. on = 1
  115. s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, on)
  116. if hasattr(socket, 'SO_REUSEPORT'):
  117. s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, on)
  118. s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, on)
  119. s.bind(('', port)) # '' means: INADDR_ANY
  120. # Multicast
  121. if is_multicast(broadcast_addr):
  122. debug('Broadcast address is a multicast address')
  123. ifaddr = socket.INADDR_ANY
  124. mreq = struct.pack('4sl',
  125. socket.inet_aton(broadcast_addr),
  126. socket.htonl(ifaddr))
  127. s.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
  128. s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, DEFAULT_TTL)
  129. # /Multicast
  130. msg="%li %s %s %s\n"%(PROTOCOL_VERSION,
  131. socket_server.port, socket_server.agent_id,
  132. socket_server.agent_name)
  133. if PY2:
  134. s.sendto(msg, (broadcast_addr, port))
  135. else:
  136. s.sendto(msg.encode(), (broadcast_addr, port))
  137. s.settimeout(0.1)
  138. while socket_server.isAlive():
  139. try:
  140. udp_msg, (ip, ivybus_port) = s.recvfrom(1024)
  141. except socket.timeout:
  142. continue
  143. if not PY2:
  144. udp_msg=udp_msg.decode('UTF-8')
  145. debug('UDP got: %r from: %r', udp_msg, ip)
  146. appid = appname = None
  147. try:
  148. udp_msg_l = udp_msg.split(' ')
  149. protocol_version, port_number = udp_msg_l[:2]
  150. if len(udp_msg_l) > 2:
  151. # "new" udp protocol, with id & appname
  152. appid = udp_msg_l[2]
  153. appname = ' '.join(udp_msg_l[3:]).strip('\n')
  154. debug('IP %s has id: %s and name: %s', ip, appid, appname)
  155. else:
  156. debug('Received message w/o app. id & name from %r', ip)
  157. port_number = int(port_number)
  158. protocol_version = int(protocol_version)
  159. except ValueError: # unpack error, invalid literal for int()
  160. warn('Received an invalid UDP message (%r) from :', udp_msg)
  161. if protocol_version != PROTOCOL_VERSION:
  162. error('Received a UDP broadcast msg. w/ protocol version:%s , expected: %s', protocol_version,
  163. PROTOCOL_VERSION)
  164. continue
  165. if appid == socket_server.agent_id:
  166. # this is us!
  167. debug('UDP from %r: ignored: we sent that one!', ip)
  168. continue
  169. # build a new socket and delegate its handling to the SocketServer
  170. debug('New client connected: %s:%s', ip, port_number)
  171. new_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  172. new_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, on)
  173. trace('New client %s:%s, socket %r', ip, port_number, new_socket)
  174. # Since we already have a client's name and id, lets register it
  175. # (this was previously done in IvyHandler's __init__() only)
  176. # but we want to check that we did not receive more than once a
  177. # broadcast coming from the same
  178. new_client = socket_server._get_client(ip, port_number, new_socket,
  179. appid, appname)
  180. if new_client is None:
  181. # an agent with that app-id is already registered
  182. info('UDP from %s:%s (%s): discarding message, an application w/ id=%s is already registered', ip,
  183. port_number, appname, appid)
  184. continue
  185. try:
  186. new_socket.connect((ip, port_number))
  187. except:
  188. # e.g., timeout on connect
  189. info('Client %r: failed to connect to its socket, ignoring it',
  190. new_client)
  191. debug('Client %r: failed to connect to its socket, got:%s',
  192. new_client, traceback.format_exc())
  193. socket_server.remove_client(ip, port_number,
  194. trigger_application_callback=False)
  195. else:
  196. socket_server.process_request(new_socket, (ip, port_number))
  197. log('UDP Server stopped')
  198. def is_multicast(ip):
  199. """
  200. Tells whether the specified ip is a multicast address or not
  201. :param ip: an IPv4 address in dotted-quad string format, for example
  202. 192.168.2.3
  203. """
  204. return int(ip.split('.')[0]) in range(224, 239)
  205. def decode_ivybus(ivybus=None):
  206. """
  207. Transforms the supplied string into the corresponding broadcast address
  208. and port
  209. :param ivybus: if ``None`` or empty, defaults to environment variable
  210. ``IVYBUS``
  211. :return: a tuple made of (broadcast address, port number). For example:
  212. ::
  213. >>> print decode_ivybus('192.168.12:2010')
  214. ('192.168.12.255', 2010)
  215. """
  216. if not ivybus:
  217. ivybus = os.getenv('IVYBUS', DEFAULT_IVYBUS)
  218. broadcast, port = ivybus.split(':', 1)
  219. port = int(port)
  220. broadcast = broadcast.strip('.')
  221. broadcast += '.' + '.'.join(['255', ] * (4 - len(broadcast.split('.'))))
  222. # if broadcast is multicast it had 4 elements -> previous line added a '.'
  223. broadcast = broadcast.strip('.')
  224. debug('Decoded ivybus %s:%s', broadcast, port)
  225. return broadcast, port
  226. def decode_MSG_params(params):
  227. '''Implements the special treatment of parameters in text messages
  228. (message type: MSG). The purpose here is to make sure that, when
  229. their last parameter is not ETX-terminated, they are processed the
  230. same way as in the reference ivy-c library.
  231. '''
  232. MISSING_ETX = 'Received a misformatted message: last parameter is not ETX-terminated'
  233. if ARG_END in params: # there is at least one parameter
  234. # All parameters are ETX-terminated: we remove the last ARG_END==ETX
  235. # before calling split(ARG_END)...
  236. if params[-1] == ARG_END:
  237. params = params[:-1]
  238. else:
  239. # ... However if the last ETX is missing, we pretend it was here
  240. # so that we behave exactly as ivy-c in this case.
  241. warn(MISSING_ETX)
  242. params = params.split(ARG_END)
  243. elif len(params)>0:
  244. # One parameter was transmitted but it has no trailing ARG_END/ETX:
  245. # let's conform to ivy-c behaviour and return it
  246. warn(MISSING_ETX)
  247. params = (params,)
  248. return params
  249. def decode_msg(msg):
  250. """
  251. Extracts from an ivybus message its message type, the conveyed
  252. numerical identifier and its parameters.
  253. :return: msg_type, numerical_id, parameters
  254. :except IvyMalformedMessage: if the message's type or numerical identifier are not integers
  255. """
  256. try:
  257. msg_id, _msg = msg.split(' ', 1)
  258. msg_id = int(msg_id)
  259. num_id, params = _msg.split(ARG_START, 1)
  260. num_id = int(num_id)
  261. if msg_id == MSG:
  262. params = decode_MSG_params(params)
  263. elif ARG_END in params:
  264. # Remove the trailing ARG_END before calling split()
  265. params = params[:-1].split(ARG_END)
  266. except ValueError:
  267. raise IvyMalformedMessage
  268. return msg_id, num_id, params
  269. def encode_message(msg_type, numerical_id, params=''):
  270. """
  271. params is string -> added as-is
  272. params is list -> concatenated, separated by ARG_END
  273. """
  274. msg = "%s %s" % (msg_type, numerical_id) + ARG_START
  275. _type_str=None
  276. if PY2:
  277. _type_str=basestring
  278. else:
  279. _type_str=str
  280. if isinstance(params, _type_str):
  281. msg += params
  282. else:
  283. msg += ARG_END.join(params)
  284. msg += ARG_END
  285. trace('encode_message(params: %s) -> %s' % (repr(params), repr(msg + '\n')))
  286. if PY2:
  287. msg = msg + '\n'
  288. else:
  289. msg = ( msg + '\n' ).encode()
  290. return msg
  291. class IvyProtocolError(Exception):
  292. pass
  293. class IvyMalformedMessage(Exception):
  294. pass
  295. class IvyIllegalStateError(RuntimeError):
  296. pass
  297. NOT_INITIALIZED = 0
  298. INITIALIZATION_IN_PROGRESS=1
  299. INITIALIZED=2
  300. class IvyClient:
  301. """
  302. Represents a client connected to the bus. Every callback methods
  303. registered by an agent receive an object of this type as their first
  304. parameter, so that they know which agent on the bus is the cause of the
  305. event which triggered the callback.
  306. An IvyClient is responsible for:
  307. - managing the remote agent's subscriptions,
  308. - sending messages to the remote agent.
  309. It is **not** responsible for receiving messages from the client: another
  310. object is in charge of that, namely an :class:`IvyHandler` object.
  311. The local IvyServer creates one IvyClient per agent on the bus.
  312. MT-safety
  313. ---------
  314. See the discussion in `regexps`.
  315. :Protocol-related methods: :py:func:`start_init`, :py:func:`end_init`,
  316. :py:func:`send_new_subscription`, :py:func:`remove_subscription`,
  317. :py:func:`wave_bye`
  318. :Manipulating the remote agent's subscriptions:
  319. :py:func:`add_regexp`, :py:func:`remove_regexp`
  320. :Sending messages: :py:func:`send_msg`, :py:func:`send_direct_message`,
  321. :py:func:`send_die_message`
  322. :ivar regexps: a dictionary mapping subscriptions' ids (as delivered by
  323. `add_regexp`) to the corresponding regular expressions. Precisely, it
  324. maps ids to tuples being ``(regexp_as_string, compiled_regexp)``. You
  325. shouldn't directly access or manipulate this variable, use `add_regexp`
  326. and `remove_regexp` instead; however if you really need/want to, you
  327. must acquire the `regexp_lock` before accessing/modifying it.
  328. :ivar regexp_lock: a non-reentrant lock protecting the variable
  329. `regexps`. Used by methods `add_regexp`, `remove_regexp` and `send_msg`.
  330. :type regexp_lock: `threading.Lock`
  331. """
  332. def __init__(self, ip, port, client_socket,
  333. agent_id=None, agent_name=None):
  334. self.agent_id = agent_id
  335. # agent_name will be overridden when start_init() is called
  336. # but nevermind,
  337. self.agent_name = agent_name
  338. self.ip = ip
  339. self.port = port
  340. self.socket = client_socket
  341. self.regexps = {} # maps regexp_id to (regexp_string, compiled_regexp)
  342. self.fqdn = socket.getfqdn(ip)
  343. self.status = NOT_INITIALIZED
  344. self.socket.settimeout(0.1)
  345. # regexps are modified w/ add_regexp and remove_regexp, called
  346. # by the server, while they are accessed by send_message()
  347. # called by the corresponding IvyHandler-thread --> needs to be
  348. # protected against concurrent access.
  349. self.regexps_lock = threading.Lock() # non-reentrant, faster than RLock
  350. self.ping_ts = [] # timestamps
  351. self.ping_lock = threading.Lock()
  352. def start_init(self, agent_name):
  353. """
  354. Finalizes the initialization process by setting the client's
  355. agent_name. This is a Ivy protocol requirement that an application
  356. sends its agent-name only once during the initial handshake (beginning
  357. with message of type ``START_INIT`` and ending with a type
  358. ``END_INIT``). After this method is called, we expect to receive the
  359. initial subscriptions for that client (or none); the initialization
  360. process completes after `end_init` is called.
  361. :except IvyIllegalStateError: if the method has already been called
  362. once
  363. """
  364. if self.status != NOT_INITIALIZED:
  365. raise IvyIllegalStateError
  366. self.agent_name = agent_name
  367. self.status = INITIALIZATION_IN_PROGRESS
  368. debug('Client:%r: Starting initialization', self)
  369. def end_init(self):
  370. """
  371. Should be called when the initialization process ends.
  372. :except IvyIllegalStateError: if the method has already been called
  373. (and ``self.status`` has already been set to ``INITIALIZED``)
  374. """
  375. if self.status == INITIALIZED:
  376. raise IvyIllegalStateError
  377. debug('Client:%r: Initialization ended', self)
  378. self.status = INITIALIZED
  379. def add_regexp(self, regexp_id, regexp):
  380. """
  381. :except IvyIllegalStateError: if the client has not been fully
  382. initialized yet (see :py:func:`start_init`)
  383. """
  384. if self.status not in (INITIALIZATION_IN_PROGRESS, INITIALIZED):
  385. # initialization has not begun
  386. raise IvyIllegalStateError
  387. # TODO: handle error on compile
  388. debug('Client:%r: Adding regexp id=%s: %r', self, regexp_id, regexp)
  389. self.regexps_lock.acquire()
  390. try:
  391. self.regexps[regexp_id] = (regexp, re.compile(regexp))
  392. finally:
  393. self.regexps_lock.release()
  394. def remove_regexp(self, regexp_id):
  395. """
  396. Removes a regexp
  397. :return: the regexp that has been removed
  398. :except IvyIllegalStateError: if the client has not been fully
  399. initialized yet (see :py:func:`start_init`)
  400. :except KeyError: if no such subscription exists
  401. """
  402. if self.status not in (INITIALIZATION_IN_PROGRESS, INITIALIZED):
  403. # initialization has not begun
  404. raise IvyIllegalStateError
  405. debug('Client:%r: removing regexp id=%s', self, regexp_id)
  406. regexp = None
  407. self.regexps_lock.acquire()
  408. try:
  409. regexp = self.regexps.pop(regexp_id)[0]
  410. finally:
  411. self.regexps_lock.release()
  412. return regexp
  413. def get_regexps(self):
  414. self.regexps_lock.acquire()
  415. try:
  416. return [(idx, s[0]) for idx, s in self.regexps.items()]
  417. finally:
  418. self.regexps_lock.release()
  419. def send_msg(self, msg):
  420. """
  421. Sends the message to the client. The client will receive one message
  422. for each of its subscriptions (regexps) that the message matches.
  423. :return: ``True`` if the message was actually sent to the client, that
  424. is: if there is one or more regexps matching the message in the
  425. client's subscriptions; returns ``False`` otherwise.
  426. """
  427. if self.status != INITIALIZED:
  428. return
  429. debug('Client:%r: Searching subscriptions matching msg %r', self, msg)
  430. # For each regexp that the msg matches, send the corresponding message
  431. result = False
  432. self.regexps_lock.acquire()
  433. try:
  434. for num_id, (s, r) in self.regexps.items():
  435. captures = r.match(msg)
  436. if captures:
  437. # Value for an optional group not participating in the
  438. # match defaults to the empty string
  439. captures = captures.groups(default='')
  440. # The following is needed to reproduce the very same
  441. # behaviour observed w/ the C library
  442. # (tested w/ pyhello.py and ivyprobe)
  443. if len(captures) == 0:
  444. captures = ''
  445. debug('Client:%r: msg being sent: %r (regexp: %r)',
  446. self, captures, s)
  447. self._send(MSG, num_id, captures)
  448. result = True
  449. return result
  450. finally:
  451. self.regexps_lock.release()
  452. def send_direct_message(self, num_id, msg):
  453. """
  454. Sends a direct message
  455. Note: the message will be encoded by `encode_message` with
  456. ``numerical_id=num_id`` and ``params==msg``; this means that if `msg`
  457. is not a string but a list or a tuple, the direct message will contain
  458. more than one parameter. This is an **extension** of the original Ivy
  459. design, supported by python, but if you want to inter-operate with
  460. applications using the standard Ivy API the message you send *must* be
  461. a string. See in particular in ``ivy.h``::
  462. typedef void (*MsgDirectCallback)( IvyClientPtr app, void *user_data, int id, char *msg ) ;
  463. """
  464. if self.status == INITIALIZED:
  465. debug('Client:%r: a direct message being sent: id: %r msg: %r',
  466. self, num_id, msg)
  467. self._send(DIRECT_MSG, num_id, msg)
  468. def send_die_message(self, num_id=0, msg=''):
  469. """
  470. Sends a die message
  471. """
  472. if self.status == INITIALIZED:
  473. debug('Client:%r: die msg being sent: num_id: %r msg: %r',
  474. self, num_id, msg)
  475. self._send(DIE, num_id, msg)
  476. def send_new_subscription(self, idx, regexp):
  477. """
  478. Notifies the remote agent that we (the local agent) subscribe to
  479. a new type of messages
  480. :Parameters:
  481. - `idx`: the index/id of the new subscription. It is the
  482. responsibility of the local agent to make sure that every
  483. subscription gets a unique id.
  484. - `regexp`: a regular expression. The subscription consists in
  485. receiving messages matching the regexp.
  486. """
  487. self._send(ADD_REGEXP, idx, regexp)
  488. def send_ping(self):
  489. """
  490. """
  491. self.ping_lock.acquire()
  492. try:
  493. self.ping_ts.append(time.time())
  494. self._send(PING, 0)
  495. finally:
  496. self.ping_lock.release()
  497. def get_next_ping_delta(self):
  498. """
  499. """
  500. self.ping_lock.acquire()
  501. try:
  502. if len(self.ping_ts)==0:
  503. return None
  504. ts0 = self.ping_ts.pop(0)
  505. return time.time() - ts0
  506. finally:
  507. self.ping_lock.release()
  508. def remove_subscription(self, idx):
  509. """
  510. Notifies the remote agent that we (the local agent) are not
  511. interested in a given subscription.
  512. :Parameters:
  513. - `idx`: the index/id of a subscription previously registered with
  514. `send_new_subscription`.
  515. """
  516. self._send(DEL_REGEXP, idx)
  517. def wave_bye(self, num_id=0):
  518. """Notifies the remote agent that we are about to quit"""
  519. self._send(BYE, num_id)
  520. def send_error(self, num_id, msg):
  521. """
  522. Sends an error message
  523. """
  524. self._send(ERROR, num_id, msg)
  525. def __eq__(self, client):
  526. """
  527. cf. dict[client] or dict[(ip,port)] UNNEEDED FOR THE MOMENT
  528. """
  529. if isinstance(client, IvyClient):
  530. return self.ip == client.ip and self.port == client.port
  531. _tuple_list=None
  532. if PY2:
  533. _tuple_list=(types.TupleType, types.ListType)
  534. else:
  535. _tuple_list=(tuple, list)
  536. if type(client) in _tuple_list and len(client) == 2:
  537. return self.ip == client[0] and self.port == client[1]
  538. return False
  539. def __hash__(self):
  540. """``hash((self.ip, self.port))``"""
  541. return hash((self.ip, self.port))
  542. def __repr__(self):
  543. """Returns ``'ip:port (agent_name)'``"""
  544. return '%s:%s (%s)' % (self.ip, self.port, self.agent_name)
  545. def __str__(self):
  546. """Returns ``'agent_name@FQDN'``"""
  547. return '%s@%s' % (self.agent_name, self.fqdn)
  548. def _send(self, msg_type, *params):
  549. """
  550. Internally used to send message to the remote agent through the opened
  551. socket `self.socket`. This method catches all exceptions
  552. `socket.error` and `socket.timeout` and ignores them, simply logging
  553. them at the "info" level.
  554. The errors that can occur are for example::
  555. socket.timeout: timed out
  556. socket.error: (104, 'Connection reset by peer')
  557. socket.error: (32, 'Broken pipe')
  558. They can happen after a client disconnects abruptly (because it was
  559. killed, because the network is down, etc.). We assume here that if and
  560. when an error happens here, a disconnection will be detected shortly
  561. afterwards by the server which then removes this agent from the bus.
  562. Hence, we ignore the error; please also note that not ignoring the
  563. error can have an impact on code, for example, IyServer.send_msg()
  564. does not expect that IvyClient.send() fails and if it fails, it is
  565. possible that the server does not send the message to all possible
  566. subscribers.
  567. .. note:: ``ivysocket.c:SocketSendRaw()`` also ignores error, simply
  568. logging them.
  569. """
  570. try:
  571. self.socket.send(encode_message(msg_type, *params))
  572. except (socket.timeout, socket.error) as exc:
  573. log('[ignored] Error on socket with %r: %s', self, exc)
  574. class IvyServer(socketserver.ThreadingTCPServer):
  575. """
  576. An Ivy server is responsible for receiving and handling the messages
  577. that other clients send on an Ivy bus to a given agent.
  578. An IvyServer has two important attributes: `usesDaemons` and
  579. `server_termination`.
  580. :ivar usesDaemons:
  581. whether the threads are daemonic or not. Daemonic
  582. threads do not prevent python from exiting when the main thread stop,
  583. while non-daemonic ones do. Default is False. This attribute should
  584. be set through at `__init__()` time and should not be modified
  585. afterwards.
  586. :ivar server_termination:
  587. a `threading.Event` object that is set on server shutdown. It can be
  588. used either to test whether the server has been stopped
  589. (``server_termination.isSet()``) or to wait until it is stopped
  590. (``server_termination.wait()``). Application code should not try to set
  591. the Event directly, rather it will call `stop()` to terminate the
  592. server.
  593. :ivar port: tells on which port the TCP server awaits connection
  594. MT-safety
  595. ---------
  596. All public methods (not starting with an underscore ``_``) are
  597. MT-safe
  598. :group Communication on the ivybus: :py:func:`start`, :py:func:`send_msg`, :py:func:`send_direct_message`,
  599. :py:func:`send_ready_message`, :py:func:`handle_msg`, :py:func:`stop`
  600. :group Inspecting the ivybus: :py:func:`get_clients`, :py:func:`_get_client`, :py:func:`get_client_with_name`
  601. :group Our own subscriptions: :py:func:`get_subscriptions`, :py:func:`bind_msg`, :py:func:`unbind_msg`,
  602. :py:func:`_add_subscription`, :py:func:`_remove_subscription`, :py:func:`_get_fct_for_subscription`
  603. """
  604. # Impl. note: acquiring/releasing the global lock in methods
  605. # requiring it could be done w/ a decorator instead of repeating
  606. # the acquire-try-finally-release block each time, but then we
  607. # won't be compatible w/ py < 2.4 and I do not want this
  608. def __init__(self, agent_name, ready_msg='',
  609. app_callback=void_function,
  610. die_callback=void_function,
  611. usesDaemons=False):
  612. """
  613. Builds a new IvyServer. A client only needs to call `start()` on the
  614. newly created instances to connect to the corresponding Ivy bus and to
  615. start communicating with other applications.
  616. MT-safety: both functions :py:func:`app_callback` and
  617. :py:func:`die_callback` must be prepared to be called concurrently
  618. :Parameters:
  619. - `agent_name`: the client's agent name
  620. - `ready_msg`: a message to send to clients when they connect
  621. - `app_callback`: a function called each time a client connects or
  622. disconnects. This function is called with a single parameter
  623. indicating which event occurred: `IvyApplicationConnected` or
  624. `IvyApplicationDisconnected`.
  625. - `die_callback`: called when the IvyServer receives a DIE message
  626. - `usesDaemons`: see above.
  627. .. seealso:: :py:func:`bind_msg()`, :py:func:`start()`
  628. """
  629. self._thread = None
  630. # the empty string is equivalent to INADDR_ANY
  631. socketserver.TCPServer.__init__(self, ('', 0), IvyHandler)
  632. self.port = self.socket.getsockname()[1]
  633. #self.allow_reuse_address=True
  634. # private, maps (ip,port) to IvyClient!
  635. self._clients = {}
  636. # idx -> (regexp, function), see bind_msg() for details, below
  637. self._subscriptions = {}
  638. # the next index to use within the _subscriptions map.
  639. self._next_subst_idx = 0
  640. self.agent_name = agent_name
  641. self.ready_message = ready_msg
  642. # app_callback's parameter event=CONNECTED / DISCONNECTED
  643. self.app_callback = app_callback
  644. self.die_callback = die_callback
  645. self.direct_callback = void_function
  646. self.regexp_change_callback = void_function
  647. self.pong_callback = void_function
  648. # the global_lock protects: _clients, _subscriptions
  649. # and _next_subst_idx
  650. self._global_lock = threading.RLock()
  651. self.usesDaemons = usesDaemons
  652. self.server_termination = threading.Event()
  653. self.agent_id = agent_name + time.strftime('%Y%m%d%H%M%S') + '%05i' % random.randint(0, 99999) + str(self.port)
  654. @staticmethod
  655. def run_callback(callback, callback_description,
  656. agent, *args, on_exc=None, **kw):
  657. """Runs a callback, catching any exception it may raise.
  658. :Parameters:
  659. - `callback`: the function to be called
  660. - `callback_description`: the description to use in the error message,
  661. when an exception is raised.
  662. - `agent`: the :py:class:`IvyClient` triggering the callback, which
  663. is passed as the first argument to the callback
  664. - `on_exc`: the returned value in case an exception was raised by
  665. the callback.
  666. - All other arguments are passed as-is to the callback.
  667. :return: the value returned by the callback, or `exc_none` if
  668. an exception was raised
  669. """
  670. try:
  671. return callback(agent, *args, **kw)
  672. except:
  673. error(callback_description + ': exception raised',
  674. exc_info=sys.exc_info())
  675. return on_exc
  676. def serve_forever(self):
  677. """
  678. Handle requests (calling :py:func:`handle_request()`) until doomsday... or
  679. until :py:func:`stop()` is called.
  680. This method is registered as the target method for the thread.
  681. It is also responsible for launching the UDP server in a separate
  682. thread, see :py:func:`UDP_init_and_listen` for details.
  683. You should not need to call this method, use :py:func:`start` instead.
  684. """
  685. broadcast, port = decode_ivybus(self.ivybus)
  686. l = lambda server=self: UDP_init_and_listen(broadcast, port, server)
  687. t2 = threading.Thread(target=l)
  688. t2.setDaemon(self.usesDaemons)
  689. log('Starting UDP listener')
  690. t2.start()
  691. self.socket.settimeout(0.1)
  692. while not self.server_termination.isSet():
  693. self.handle_request()
  694. log('TCP Ivy Server terminated')
  695. def start(self, ivybus=None):
  696. """
  697. Binds the server to the ivybus. The server remains connected until
  698. :py:func:`stop` is called, or until it receives and accepts a 'die' message.
  699. :except IvyIllegalStateError: if the server has already been
  700. started
  701. """
  702. if self._thread is not None:
  703. error('Cannot start: IvyServer already started')
  704. raise IvyIllegalStateError('not running')
  705. self.ivybus = ivybus
  706. log('Starting IvyServer on port %li', self.port)
  707. self.server_termination.clear()
  708. self._thread = threading.Thread(target=self.serve_forever)
  709. self._thread.setDaemon(self.usesDaemons)
  710. self._thread.start()
  711. def stop(self):
  712. """
  713. Disconnects the server from the ivybus. It also sets the
  714. :py:func:`server_termination` event.
  715. :except IvyIllegalStateError: if the server is not running started
  716. """
  717. if not self.isAlive():
  718. error('Cannot stop: not running')
  719. raise IvyIllegalStateError('not running')
  720. self._global_lock.acquire()
  721. try:
  722. for client in self._clients.values():
  723. try:
  724. client.wave_bye()
  725. except socket.error:
  726. pass
  727. finally:
  728. self._global_lock.release()
  729. self.server_termination.set()
  730. self._thread.join()
  731. self._thread = None
  732. def isAlive(self):
  733. if self._thread is None:
  734. return False
  735. return self._thread.isAlive()
  736. def get_clients(self):
  737. """
  738. Returns the list of the agent names of all connected clients
  739. :see: get_client_with_name
  740. """
  741. self._global_lock.acquire()
  742. try:
  743. return [c.agent_name for c in self._clients.values()
  744. if c.status == INITIALIZED]
  745. finally:
  746. self._global_lock.release()
  747. def _get_client(self, ip, port, client_socket=None,
  748. agent_id=None, agent_name=None):
  749. """
  750. Returns the corresponding client, and create a new one if needed.
  751. If agent_id is not None, the method checks whether a client with the
  752. same id is already registered; if it exists, the method exits by
  753. returning None.
  754. You should not need to call this, use :py:func:`get_client_with_name` instead
  755. """
  756. self._global_lock.acquire()
  757. try:
  758. # if agent_id is provided, check whether it was already registered
  759. if agent_id and agent_id in [c.agent_id for c in self._clients.values()]:
  760. return None
  761. return self._clients.setdefault((ip, port), IvyClient(ip, port, client_socket,
  762. agent_id, agent_name))
  763. finally:
  764. self._global_lock.release()
  765. def get_client_with_name(self, name):
  766. """
  767. Returns the list of the clients registered with a given agent-name
  768. :see: get_clients
  769. """
  770. clients = []
  771. self._global_lock.acquire()
  772. try:
  773. for client in self._clients.values():
  774. if client.agent_name == name:
  775. clients.append(client)
  776. return clients
  777. finally:
  778. self._global_lock.release()
  779. def handle_new_client(self, client):
  780. """
  781. Completes connection with the client
  782. TODO: maybe add a flag (while connecting) on the client, that would prevent sending msg. etc..
  783. as CNX. not confirmed
  784. """
  785. self.run_callback(self.app_callback,
  786. 'application callback (connection)',
  787. client, IvyApplicationConnected)
  788. def handle_die_message(self, msg_id, from_client=None):
  789. """ """
  790. should_die = self.run_callback(self.die_callback, 'die callback',
  791. from_client, msg_id)
  792. should_die = should_die != IVY_SHOULD_NOT_DIE
  793. log('Received a die msg from: %s with id: %s -- should die=%s',
  794. from_client or '<unknown>', msg_id, should_die)
  795. if should_die:
  796. self.stop()
  797. return should_die
  798. def handle_direct_msg(self, client, num_id, msg):
  799. """
  800. :param client:
  801. :param num_id:
  802. :param msg:
  803. :return:
  804. """
  805. log('Received a direct msg from: %s with id: %s -- %s',
  806. client or '<unknown>', num_id, msg)
  807. description = "direct message callback: num_id:%s msg:%s"%(num_id, msg)
  808. self.run_callback(self.direct_callback, description,
  809. client, num_id, msg)
  810. def handle_regexp_change(self, client, event, num_id, regexp):
  811. """
  812. """
  813. log('Regexp change: %s %s regexp %d: %s',
  814. client or '<unknown>',
  815. event == ADD_REGEXP and 'add' or 'remove',
  816. num_id, regexp)
  817. if event == ADD_REGEXP:
  818. event = IvyRegexpAdded
  819. else:
  820. event = IvyRegexpRemoved
  821. description = ( "regexp change callback ("
  822. + (ADD_REGEXP and 'add' or 'remove')
  823. + "): num_id: %s regexp:%s"%(num_id, regexp) )
  824. self.run_callback(self.regexp_change_callback, description,
  825. client, event, num_id, regexp)
  826. def handle_pong(self, client, delta):
  827. """
  828. """
  829. log('Received a pong reply from: %s with delta: %s',
  830. client or '<unknown>', delta)
  831. self.run_callback(self.pong_callback, 'pong callback', client, delta)
  832. def remove_client(self, ip, port, trigger_application_callback=True):
  833. """
  834. Removes a registered client
  835. This method is responsible for calling ``server.app_callback``
  836. :return: the removed client, or None if no such client was found
  837. .. note:: NO NETWORK CLEANUP IS DONE
  838. """
  839. self._global_lock.acquire()
  840. try:
  841. try:
  842. removed_client = self._clients[(ip, port)]
  843. except KeyError:
  844. debug('Trying to remove a non registered client %s:%s', ip, port)
  845. return None
  846. debug('Removing client %r', removed_client)
  847. del self._clients[removed_client]
  848. if trigger_application_callback:
  849. self.run_callback(self.app_callback,
  850. 'application callback (disconnection)',
  851. removed_client, IvyApplicationDisconnected)
  852. return removed_client
  853. finally:
  854. self._global_lock.release()
  855. def send_msg(self, message):
  856. """
  857. Examine the message and choose to send a message to the clients
  858. that subscribed to such a msg
  859. :return: the number of clients to which the message was sent
  860. """
  861. self._global_lock.acquire()
  862. count = 0
  863. try:
  864. for client in self._clients.values():
  865. if client.send_msg(message):
  866. count += 1
  867. finally:
  868. self._global_lock.release()
  869. return count
  870. def send_direct_message(self, agent_name, num_id, msg, stop_on_first=True):
  871. """
  872. Sends a direct message to the agent named ``agent_name``. If there
  873. is more than one agent with that name on the bus, parameter
  874. `stop_on_first` determines the behaviour.
  875. :Parameters:
  876. - `agent_name`: the name of the agent(s) to which the direct message
  877. should be sent.
  878. - `num_id`: a numerical identifier attached to the direct message
  879. - `msg`: the direct message itself
  880. - `stop_on_first`: if ``True``, the message to all agents having the
  881. same name will receive the message; if ``False`` the method exits
  882. after the first message has been sent.
  883. :return: ``True`` if at least one direct message was sent
  884. """
  885. self._global_lock.acquire()
  886. try:
  887. status = False
  888. for client in self._clients.values():
  889. if client.agent_name == agent_name:
  890. client.send_direct_message(num_id, msg)
  891. status = True
  892. if stop_on_first:
  893. return status
  894. return status
  895. finally:
  896. self._global_lock.release()
  897. def send_ready_message(self, client):
  898. """
  899. """
  900. if self.ready_message:
  901. client.send_msg(self.ready_message)
  902. def _add_subscription(self, regexp, fct):
  903. """
  904. Registers a new regexp and binds it to the supplied fct. The id
  905. assigned to the subscription and returned by method is **unique**
  906. to that subscription for the life-time of the server object: even in
  907. the case when a subscription is unregistered, its id will _never_
  908. be assigned to another subscription.
  909. :return: the unique id for that subscription
  910. """
  911. # explicit lock here: even if this method is private, it is
  912. # responsible for the uniqueness of a subscription's id, so we
  913. # prefer to lock it one time too much than taking the risk of
  914. # forgetting it (hence, the need for a reentrant lock)
  915. self._global_lock.acquire()
  916. try:
  917. idx = self._next_subst_idx
  918. self._next_subst_idx += 1
  919. self._subscriptions[idx] = (regexp, fct)
  920. return idx
  921. finally:
  922. self._global_lock.release()
  923. def _remove_subscription(self, idx):
  924. """
  925. Unregisters the corresponding regexp
  926. .. warning:: this method is not MT-safe, callers must acquire the
  927. global lock
  928. :return: the regexp that has been removed
  929. :except KeyError: if no such subscription can be found
  930. """
  931. return self._subscriptions.pop(idx)[0]
  932. def _get_fct_for_subscription(self, idx):
  933. """
  934. .. warning:: this method is not Multi-Thread-safe, callers must acquire the
  935. global lock
  936. """
  937. return self._subscriptions[int(idx)][1]
  938. def handle_msg(self, client, idx, *params):
  939. """
  940. Simply call the function bound to the subscription id `idx` with
  941. the supplied parameters.
  942. """
  943. self._global_lock.acquire()
  944. try:
  945. try:
  946. regexp, callback = self._subscriptions[int(idx)]
  947. except KeyError:
  948. # it is possible that we receive a message for a regexp that
  949. # was subscribed then unregistered
  950. warn('Asked to handle an unknown subscription: id:%r params: %r'
  951. ' --ignoring', idx, params)
  952. return
  953. self.run_callback(callback,
  954. 'callback for subscription %i (%s)'%(idx, regexp),
  955. client,
  956. *params)
  957. finally:
  958. self._global_lock.release()
  959. def get_subscriptions(self):
  960. self._global_lock.acquire()
  961. try:
  962. return [(idx, s[0]) for idx, s in self._subscriptions.items()]
  963. finally:
  964. self._global_lock.release()
  965. def bind_direct_msg(self, on_direct_msg_fct):
  966. """
  967. """
  968. self.direct_callback = on_direct_msg_fct
  969. def bind_regexp_change(self, on_regexp_change_callback):
  970. """
  971. """
  972. self.regexp_change_callback = on_regexp_change_callback
  973. def bind_pong(self, on_pong_callback):
  974. """
  975. """
  976. self.pong_callback = on_pong_callback
  977. def bind_msg(self, on_msg_fct, regexp):
  978. """
  979. Registers a new subscriptions, by binding a regexp to a function, so
  980. that this function is called whenever a message matching the regexp
  981. is received.
  982. :Parameters:
  983. - `on_msg_fct`: a function accepting as many parameters as there is
  984. groups in the regexp. For example:
  985. - the regexp ``'^hello .*'`` corresponds to a function called w/ no
  986. parameter,
  987. - ``'^hello (.*)'``: one parameter,
  988. - ``'^hello=([^ ]*) from=([^ ]*)'``: two parameters
  989. - `regexp`: (string) a regular expression
  990. :return: the binding's id, which can be used to unregister the binding
  991. with :py:func:`unbind_msg()`
  992. """
  993. self._global_lock.acquire()
  994. idx = self._add_subscription(regexp, on_msg_fct)
  995. try:
  996. for client in self._clients.values():
  997. client.send_new_subscription(idx, regexp)
  998. finally:
  999. self._global_lock.release()
  1000. return idx
  1001. def unbind_msg(self, num_id):
  1002. """
  1003. Unbinds a subscription
  1004. :param num_id: the binding's id, as returned by :py:func:`bind_msg()`
  1005. :return: the regexp corresponding to the unsubscribed binding
  1006. :except KeyError: if no such subscription can be found
  1007. """
  1008. self._global_lock.acquire()
  1009. try:
  1010. regexp = self._remove_subscription(num_id) # KeyError
  1011. # notify others that we have no interest anymore in this regexp
  1012. for client in self._clients.values():
  1013. client.remove_subscription(num_id)
  1014. finally:
  1015. self._global_lock.release()
  1016. return regexp
  1017. class IvyHandler(socketserver.StreamRequestHandler):
  1018. """
  1019. An IvyHandler is associated to one IvyClient connected to our server.
  1020. It runs into a dedicate thread as long as the remote client is connected
  1021. to us.
  1022. It is in charge of examining all messages that are received and to
  1023. take any appropriate actions.
  1024. Implementation note: the IvyServer is accessible in ``self.server``
  1025. """
  1026. def handle(self):
  1027. """
  1028. """
  1029. # self.request is the socket object
  1030. # self.server is the IvyServer
  1031. bufsize = 1024
  1032. client_socket = self.request
  1033. ip = self.client_address[0]
  1034. port = self.client_address[1]
  1035. trace('New IvyHandler for %s:%s, socket %r', ip, port, client_socket)
  1036. client = self.server._get_client(ip, port, client_socket)
  1037. debug('Got a request from ip=%s port=%s', ip, port)
  1038. # First, send our initial subscriptions
  1039. client_socket.send(encode_message(START_INIT, self.server.port,
  1040. self.server.agent_name))
  1041. for idx, subscr in self.server.get_subscriptions():
  1042. client_socket.send(encode_message(ADD_REGEXP, idx, subscr))
  1043. client_socket.send(encode_message(END_REGEXP, 0))
  1044. while self.server.isAlive():
  1045. try:
  1046. if PY2:
  1047. msgs = client_socket.recv(bufsize)
  1048. else:
  1049. msgs = client_socket.recv(bufsize).decode('UTF-8')
  1050. except socket.timeout:
  1051. trace('timeout on socket bound to client %r', client)
  1052. continue
  1053. except socket.error as e:
  1054. log('Error on socket with %r: %s', client, e)
  1055. self.server.remove_client(ip, port)
  1056. break # the server will close the TCP connection
  1057. if not msgs:
  1058. # client is not connected anymore
  1059. log('Lost connection with %r', client)
  1060. self.server.remove_client(ip, port)
  1061. break # the server will close the TCP connection
  1062. # Sometimes the message is not fully read on the first try,
  1063. # so we insist to get the final newline
  1064. if msgs[-1:] != '\n':
  1065. # w/ the following idioms (also replicated a second time below)
  1066. # we make sure that we wait until we get a message containing
  1067. # the final newline, or if the server is terminated we stop
  1068. # handling the request
  1069. while self.server.isAlive():
  1070. try:
  1071. if PY2:
  1072. _msg = client_socket.recv(bufsize)
  1073. else:
  1074. _msg = client_socket.recv(bufsize).decode('UTF-8')
  1075. break
  1076. except socket.timeout:
  1077. continue
  1078. if not self.server.isAlive():
  1079. break
  1080. msgs += _msg
  1081. while _msg[-1:] != '\n' and self.server.isAlive():
  1082. while self.server.isAlive():
  1083. try:
  1084. if PY2:
  1085. _msg = client_socket.recv(bufsize)
  1086. else:
  1087. _msg = client_socket.recv(bufsize).decode('UTF-8')
  1088. break
  1089. except socket.timeout:
  1090. continue
  1091. msgs += _msg
  1092. if not self.server.isAlive():
  1093. break
  1094. debug('Got a request from ip=%s port=%s: %r', ip, port, msgs)
  1095. msgs = msgs[:-1]
  1096. msgs = msgs.split('\n')
  1097. for msg in msgs:
  1098. keep_connection_alive = self.process_ivymessage(msg, client)
  1099. if not keep_connection_alive:
  1100. self.server.remove_client(ip, port)
  1101. break
  1102. log('Closing connection to client %r', client)
  1103. def process_ivymessage(self, msg, client):
  1104. """
  1105. Examines the message (after passing it through the :py:func:`decode_msg()`
  1106. filter) and takes the appropriate actions depending on the message
  1107. types. Please refer to the document `The Ivy Architecture and
  1108. Protocol <http://www.eei.cena.fr/products/ivy/documentation>`_ and to
  1109. the python code for further details.
  1110. :Parameters:
  1111. - `msg`: (should not include a newline at end)
  1112. :return: ``False`` if the connection should be terminated, ``True``
  1113. otherwise
  1114. """
  1115. # cf. static void Receive() in ivy.c
  1116. try:
  1117. msg_id, num_id, params = decode_msg(msg)
  1118. except IvyMalformedMessage:
  1119. warn('Received an incorrect message: %r from: %r', msg, client)
  1120. # TODO: send back an error message
  1121. return True
  1122. debug('Got: msg_id: %r, num_id: %r, params: %r',
  1123. msg_id, num_id, params)
  1124. err_msg = ''
  1125. try:
  1126. if msg_id == BYE:
  1127. # num_id: not meaningful. No parameter.
  1128. log('%s waves bye-bye: disconnecting', client)
  1129. return False
  1130. elif msg_id == ADD_REGEXP:
  1131. # num_id=id for the regexp, one parameter: the regexp
  1132. err_msg = 'Client %r was not properly initialized' % client
  1133. log('%s sending a new subscription id:%r regexp:%r ',
  1134. client, num_id, params)
  1135. client.add_regexp(num_id, params)
  1136. self.server.handle_regexp_change(client, ADD_REGEXP,
  1137. num_id, params)
  1138. # TODO: handle errors (e.g. 2 subscriptions w/ the same id)
  1139. elif msg_id == DEL_REGEXP:
  1140. # num_id=id for the regexp to removed, no par

Large files files are truncated, but you can click here to view the full file