class RecurringTask(object): def __init__(self, interval, fn): self.interval = interval self.fn = fn self._wakeup = Event() self._stopped = Event() self._gthread = None def touch(self): """Make sure the task is executed now.""" self._wakeup.set() def start(self): self._gthread = gevent.spawn(self._run) def stop(self): self._stopped.set() self._wakeup.set() def _run(self): while not self._stopped.is_set(): self.fn() self._wakeup.wait(timeout=self.interval) self._wakeup.clear()
class GeventCursor(net.Cursor): def __init__(self, *args, **kwargs): super(GeventCursor, self).__init__(*args, **kwargs) self.new_response = Event() def __iter__(self): return self def __next__(self): return self._get_next(None) def _empty_error(self): return GeventCursorEmpty() def _extend(self, res): super(GeventCursor, self)._extend(res) self.new_response.set() self.new_response.clear() def _get_next(self, timeout): with gevent.Timeout(timeout, RqlTimeoutError()) as timeout: self._maybe_fetch_batch() while len(self.items) == 0: if self.error is not None: raise self.error self.new_response.wait() return self.items.popleft()
def __call__(self, environ, start_response): handler = self.websocket.routes.get(environ['PATH_INFO']) if not handler: return self.wsgi_app(environ, start_response) # do handshake uwsgi.websocket_handshake(environ['HTTP_SEC_WEBSOCKET_KEY'], environ.get('HTTP_ORIGIN', '')) # setup events send_event = Event() send_queue = Queue(maxsize=1) recv_event = Event() recv_queue = Queue(maxsize=1) # create websocket client client = self.client(environ, uwsgi.connection_fd(), send_event, send_queue, recv_event, recv_queue, self.websocket.timeout) # spawn handler handler = spawn(handler, client) # spawn recv listener def listener(client): ready = select([client.fd], [], [], client.timeout) recv_event.set() listening = spawn(listener, client) while True: if not client.connected: recv_queue.put(None) listening.kill() handler.join(client.timeout) return '' # wait for event to draw our attention ready = wait([handler, send_event, recv_event], None, 1) # handle send events if send_event.is_set(): try: uwsgi.websocket_send(send_queue.get()) send_event.clear() except IOError: client.connected = False # handle receive events elif recv_event.is_set(): recv_event.clear() try: recv_queue.put(uwsgi.websocket_recv_nb()) listening = spawn(listener, client) except IOError: client.connected = False # handler done, we're outta here elif handler.ready(): listening.kill() return ''
class Chat(object): def __init__(self): # at some point, may want to implement a buffer for messages # self.buffer = [] self.msg_event = Event() def index(self, request): form = models.MsgForm() msg_list = models.Msg.objects.all() return render(request, 'index.html', { 'form': form, 'msg_list': msg_list, }) def send(self, request): if request.method == 'POST': form = models.MsgForm(request.POST) if form.is_valid(): form.save() # tell everyone who's waiting on msg_event that a msg was just # posted self.msg_event.set() self.msg_event.clear() return HttpResponse(json.dumps(True), mimetype='application/json') return HttpResponse(json.dumps(False), mimetype='application/json') def update(self, request): check_time = datetime.now() # wait for next msg post self.msg_event.wait() msg_list = models.Msg.objects.filter(time_stamp__gte=check_time) return HttpResponse(serializers.serialize('xml', msg_list), mimetype='text/xml')
class Console(BaseService): """A service starting an interactive ipython session when receiving the SIGSTP signal (e.g. via keyboard shortcut CTRL-Z). """ name = 'console' def __init__(self, app): super(Console, self).__init__(app) self.interrupt = Event() gevent.signal(signal.SIGTSTP, self.interrupt.set) self.console_locals = [] def start(self): super(Console, self).start() self.console_locals = {} self.console_locals.update(self.app.services) self.console_locals['app'] = self.app def _run(self): while True: self.interrupt.wait() IPython.start_ipython(argv=['--gui', 'gevent'], user_ns=self.console_locals) self.interrupt.clear()
class TestEvent(Actor): '''**Generates a test event at the chosen interval.** This module is only available for testing purposes and has further hardly any use. Events have following format: { "header":{}, "data":"test" } Parameters: - name (str): The instance name when initiated. - interval (float): The interval in seconds between each generated event. Should have a value > 0. default: 1 Queues: - outbox: Contains the generated events. ''' def __init__(self, name, interval=1): Actor.__init__(self, name, setupbasic=False) self.createQueue("outbox") self.name = name self.interval=interval if interval == 0: self.sleep = self.doNoSleep else: self.sleep = self.doSleep self.throttle=Event() self.throttle.set() def preHook(self): spawn(self.go) def go(self): switcher = self.getContextSwitcher(100) while switcher(): self.throttle.wait() try: self.queuepool.outbox.put({"header":{},"data":"test"}) except (QueueFull, QueueLocked): self.queuepool.outbox.waitUntilPutAllowed() self.sleep(self.interval) def doSleep(self, interval): sleep(interval) def doNoSleep(self, interval): pass def enableThrottling(self): self.throttle.clear() def disableThrottling(self): self.throttle.set()
class Chat(object): def __init__(self): self.new_msg_event = Event() def write_message(self, request): if not request.user.is_authenticated() or request.method != 'POST': return HttpResponse(status=404) form = MessageForm(request.POST) output = dict(success=False) if form.is_valid(): form.save(request.user) output['success'] = True else: output['errors'] = form.get_errors() self.new_msg_event.set() self.new_msg_event.clear() return HttpResponse(json.dumps(output)) def get_messages(self, request): if not request.user.is_authenticated(): return HttpResponse(status=404) pk = int(request.GET.get('pk', 1)) messages = [{'created_at': DateFormat(el.created_at).format('H:i:s'), 'username': el.username, 'pk': el.pk, 'msg': el.msg} for el in Message.objects.filter(pk__gt=int(pk)) .order_by('-created_at')[:100]] if not messages: self.new_msg_event.wait() return HttpResponse(json.dumps(messages[::-1]))
def test(self): e = Event() waiters = [gevent.spawn(e.wait) for i in range(self.N)] gevent.sleep(0.001) e.set() e.clear() for t in waiters: t.join()
class Streamer(object): def __init__(self, data_product_id, interval=1, simple_time=False, connection=False): self.resource_registry = Container.instance.resource_registry self.pubsub_management = PubsubManagementServiceClient() self.data_product_id = data_product_id self.i = 0 self.interval = interval self.simple_time = simple_time self.finished = Event() self.g = gevent.spawn(self.run) self.connection = connection def run(self): connection = uuid4().hex while not self.finished.wait(self.interval): rdt = ParameterHelper.rdt_for_data_product(self.data_product_id) now = time.time() if self.simple_time: rdt["time"] = [self.i] else: rdt["time"] = np.array([now + 2208988800]) rdt["temp"] = self.float_range(10, 14, np.array([now])) rdt["pressure"] = self.float_range(11, 12, np.array([now])) rdt["lat"] = [41.205] rdt["lon"] = [-71.74] rdt["conductivity"] = self.float_range(3.3, 3.5, np.array([now])) rdt["driver_timestamp"] = np.array([now + 2208988800]) rdt["preferred_timestamp"] = ["driver_timestamp"] if self.connection: ParameterHelper.publish_rdt_to_data_product( self.data_product_id, rdt, connection_id=connection, connection_index=self.i ) else: ParameterHelper.publish_rdt_to_data_product(self.data_product_id, rdt) self.i += 1 def stop(self): self.finished.set() self.g.join(5) self.g.kill() def start(self): self.finished.clear() self.g = gevent.spawn(self.run) @classmethod def float_range(cls, minvar, maxvar, t): """ Produces a signal with values between minvar and maxvar at a frequency of 1/60 Hz centered at the midpoint between minvar and maxvar. This method provides a deterministic function that varies over time and is sinusoidal when graphed. """ a = (maxvar - minvar) / 2 return np.sin(np.pi * 2 * t / 60) * a + (minvar + a)
class AvailManager(object): SIM_RUNNING = False sim_thread = None def __init__(self): self.updates = [] self.event = Event() def run_sim(self, delay, size): roomids = Room.objects.filter(avail=True).values_list('id',flat=True) #print 'Room ids: %s ' % roomids ids = [] for i in range(0, len(roomids)): ids.append(roomids[i]) random.shuffle(ids) for i in range(0, len(ids)): roomg = Room.objects.filter(id=ids[i]) updateavail(Room.objects.filter(id=ids[i])) room = roomg[0] self.updates.append(RoomUpdate(room)) self.event.set() self.event.clear() if i % size == 0: sleep(delay) self.SIM_RUNNING = False def start_sim(self, delay, size=1): if self.SIM_RUNNING: kill(self.sim_thread) self.updates = [] Room.objects.all().update(avail=True) self.sim_thread = spawn(self.run_sim, delay=delay, size=size) self.SIM_RUNNING = True return HttpResponse('Started Simulation with delay %d' % delay) def stop_sim(self): if not self.SIM_RUNNING: return HttpResponse('No current simulation') kill(self.sim_thread) self.SIM_RUNNING = False return HttpResponse('Stopped simulation') def check_avail(self, timestamp): if len(self.updates) == 0 or timestamp > self.updates[0].timestamp: self.event.wait() room_ids = [] i = len(self.updates) - 1 while i >= 0: i = i - 1 update = self.updates[i] if timestamp <= update.timestamp: room_ids.append(update.room_id) else: break return {'timestamp':int(time.time()), 'rooms':room_ids}
class SphinxServer(object): '''Coordinate sphinx greenlets. manage method render sphinx pages, initialize and give control to serve, watch and render greenlets. ''' def __init__(self, c): self.c = c self.watch_ev = Event() self.render_ev = Event() def serve(self): '''Serve web requests from path as soon docs are ready. Reload remote browser when updates are rendered using websockets ''' host, port = self.c.socket.split(':') server = Webserver( os.path.join(self.c.sphinx_path, self.c.output), host, port, self.render_ev ) server.run() def watch(self): '''Watch sphinx_path signalling render when rst files change ''' with fs_event_ctx(self.c.sphinx_path, self.c.extensions) as fs_ev_iter: for event in fs_ev_iter: log.debug('filesystem event: {}'.format(event, event.ev_name)) self.watch_ev.set() def render(self): '''Render and listen for doc changes (watcher events) ''' while True: self.watch_ev.wait() # Wait for docs changes self.watch_ev.clear() with capture_streams() as streams: self.build() log.debug(streams.getvalue()) self.render_ev.set() def build(self): '''Render reStructuredText files with sphinx''' return build_main(['sphinx-build', self.c.sphinx_path, os.path.join(self.c.sphinx_path, self.c.output)]) def manage(self): '''Manage web server, watcher and sphinx docs renderer ''' with capture_streams() as streams: ret = self.build() if ret != 0: sys.exit(streams.getvalue()) log.debug(streams.getvalue()) workers = [spawn(self.serve), spawn(self.watch), spawn(self.render)] joinall(workers)
class _ResponseIterator(object): _global_counter = 0 def __init__(self, maintainOrder, preprocessor): self._currentIndex = 0 if maintainOrder else None self._preprocessor = preprocessor self._responseAdded = Event() self._responses = OrderedDict() # A request is in-flight the moment it is popped off the request queue, until it is either added to this iterator or discarded (due to being killed) # The "done" variable is necessary to handle some corner cases, such as right at the beginning before the first request is in-flight self._inflight = 0 self._done = False # Purely for debugging purposes to help differentiate the iterators # It's surprisingly frequent that new iterators will occupy exactly the same stop # in memory as a previous iterator, making id(self) not very useful. _ResponseIterator._global_counter += 1 self._counter = _ResponseIterator._global_counter def __iter__(self): return self def _add(self, bundle, requestIndex): if self._responses is not None: self._responses[requestIndex] = bundle self._inflight -= 1 #print('(Notify it.) %s [%d] %d, %s, %d, %s' % ( time(), self._counter, self._inflight, self._done, len(self._responses), bundle.request.url )) self._responseAdded.set() def next(self): while True: #print('(Loop it. ) %s [%d] %d, %s, %d' % ( time(), self._counter, self._inflight, self._done, len(self._responses) )) if self._inflight == 0 and self._done and (self._responses is None or len(self._responses) == 0): raise StopIteration found = False if self._responses is not None and len(self._responses) > 0: if self._currentIndex is None: found = True bundle = self._responses.popitem(last = False)[1] else: if self._currentIndex in self._responses: found = True bundle = self._responses.pop(self._currentIndex) self._currentIndex += 1 if found: #print('(Return it.) %s [%d] %d, %s, %d, %s' % ( time(), self._counter, self._inflight, self._done, len(self._responses), bundle.request.url )) if bundle.exception is None: return self._preprocessor.success(bundle) else: return self._preprocessor.error(bundle) else: #print('(Wait it. ) %s [%d] %d, %s, %d' % ( time(), self._counter, self._inflight, self._done, len(self._responses) )) self._responseAdded.clear() self._responseAdded.wait()
class SphinxServer(object): '''Coordinate sphinx greenlets. manage method render sphinx pages, initialize and give control to serve, watch and render greenlets.''' def __init__(self, c): self.c = c self.watch_ev = Event() self.render_ev = Event() def serve(self): '''Serve web requests from path as soon docs are ready. Reload remote browser when updates are rendered using websockets''' host, port = self.c.socket.split(':') server = Webserver(self.c.path_dest, host, port, self.render_ev) log.warning('Listening on http://%s:%s', host, port) server.run() def watch(self): '''Watch sphinx_path signalling render when rst files change''' with fs_event_ctx(self.c.sphinx_path, self.c.extensions) as fs_ev_iter: for event in fs_ev_iter: log.info('%s %s', event, event.ev_name) self.watch_ev.set() @elapsed def build(self): '''Render reStructuredText files with sphinx''' proc = Process(target=sphinx.main, args=[['sphinx-build'] + self.c.quiet + [shlex_quote(self.c.sphinx_path), shlex_quote(self.c.path_dest)]]) proc.start() proc.join() return proc.exitcode def render(self): '''Render and listen for doc changes (watcher events)''' while True: self.watch_ev.wait() # Wait for docs changes self.watch_ev.clear() if self.build() != 0: exit_msg('rendering %s. Aborting.' % self.c.sphinx_path) self.render_ev.set() def manage(self): '''Manage web server, watcher and sphinx docs renderer.''' def shutdown_handler(): log.info('Received SIGTERM signal to shut down!') killall(workers) if self.build() != 0: exit_msg('rendering %s. Aborting.' % self.c.sphinx_path) workers = [spawn(self.serve), spawn(self.watch), spawn(self.render)] signal(SIGTERM, shutdown_handler) joinall(workers) exit(0)
class Irc(object): """Handles a client connection to the IRC protocol""" def __init__(self, server, publisher, flood_prevention=1): self.server = server self.publisher = publisher self.flood_prevention = flood_prevention self._conn = Connection(server['host'], server['port'], server['ssl'], server['timeout']) # Internal connection # Timer to prevent flooding self.timer = Event() self.timer.set() # The canonical channels of IRC to subscribe / publish # Receives input to send to irc server self.sender = Queue() # Receives output to publish self.receiver = Queue() # Suscribe my output to receive data from connection self.publisher.subscribe(self.receiver, self._conn.receiver, Msg.from_msg) # Subscribe connection to send data from my input self.publisher.subscribe(self._conn.sender, self.sender, self._prevent_flood) @property def connected(self): return self._conn.connected def connect(self): if self.connected: return True self._conn.connect() if self.connected: return True else: return self._conn.state def disconnect(self): self._conn.disconnect() return self.connected def kill(self): """Completely terminate the irc connection""" self.publisher.unsubscribe(self.receiver, self._conn.receiver) self.publisher.unsubscribe(self._conn.sender, self.sender) self._conn.kill() def _prevent_flood(self, msg): """Used to prevent sending messages extremely quickly""" if self.flood_prevention > 0 and msg.cmd != 'PONG': self.timer.wait() self.timer.clear() gevent.spawn_later(self.flood_prevention, self.timer.set) return str(msg)
class Votes(object): def __init__(self): self.event = Event() def release(self): self.event.set() self.event.clear() def wait(self): self.event.wait()
class Socket(socket.Socket): """Green version of :class:`zmq.core.socket.Socket` The following four methods are overridden: * _send_message * _send_copy * _recv_message * _recv_copy To ensure that the ``zmq.NOBLOCK`` flag is set and that sending or recieving is deferred to the hub if a ``zmq.EAGAIN`` (retry) error is raised. The `__setup_events` method is triggered when the zmq.FD for the socket is marked as readable and triggers the necessary read and write events (which are waited for in the recv and send methods). """ def __init__(self, context, socket_type): super(Socket, self).__init__(context, socket_type) self.__setup_events() def __setup_events(self): self._read_ready = Event() self._write_ready = Event() try: read_event = get_hub().reactor.read_event self._state_event = read_event(self.getsockopt(zmq.FD), persist=True) self._state_event.add(None, self.__state_changed) except AttributeError: # for gevent<=0.14 compatibility from gevent.core import read_event self._state_event = read_event(self.getsockopt(zmq.FD), self.__state_changed, persist=True) def __state_changed(self, event, _evtype): events = self.getsockopt(zmq.EVENTS) if events & zmq.POLLOUT: self._write_ready.set() if events & zmq.POLLIN: self._read_ready.set() def _send_message(self, msg, flags=0): flags |= zmq.NOBLOCK while True: try: super(Socket, self)._send_message(msg, flags) return except zmq.ZMQError, e: if e.errno != zmq.EAGAIN: raise self._write_ready.clear() self._write_ready.wait()
class Condition(object): def __init__(self): self._event = Event() def broadcast(self): try: self._event.set() finally: self._event.clear() def wait(self): self._event.wait()
class Connection: def __init__( self, env, connections, handler, timeout ): self.ack_timeout = timeout self.ack_done = Event() self.ack_id = None self.connections = connections self.handler = handler self.server_event_queue = Queue() self.client_event_queue = Queue() self._id = uuid().hex self.env = deepcopydict( env ) self.env['connection'] = self spawn( self._kill_idle ) spawn\ ( self.handler , self.env , lambda: self.client_event_queue , self.server_event_queue.put ) def _check_next( self, confirm_ID ): result = self.server_event_queue.get() self.ack_id = confirm_ID self.current_result.set( (confirm_ID, result ) ) self._kill_idle() def next( self, confirm_ID, current_ID ): if confirm_ID == self.ack_id: self.ack_id = None self.ack_done.set() self.current_result = AsyncResult() spawn( self._check_next, current_ID ) result = self.current_result.get( ) return result def emit( self, event ): self.client_event_queue.put( event ) def _kill_idle( self ): self.ack_done.clear() self.ack_done.wait( timeout=self.ack_timeout ) if not self.ack_done.isSet(): self.close() def close( self ): self.client_event_queue.put( StopIteration ) self.server_event_queue.put( StopIteration ) del self.connections[ self._id ]
class Channel(object): """ Represent a channel in shaveet,a channel is something that clients can subscribe to and publish messages to. Each subscribed client get all the messages published to the channel. Each channel is identified by a name unique to that channel """ __slots__ = ('clients','messages','new_message_event','max_messages','id','name') def __init__(self,name,max_messages): self.name = name #a cache of recent messages limited by max_messages self.messages = [] #a event that fire on every new message self.new_message_event = Event() self.max_messages = max_messages self.id = 1 self.clients = set() def add_client(self,client_id): self.clients.add(client_id) self.new_message(client_id,dumps({"type":"subscribe"}),True) def remove_client(self,client_id,gc=False): self.clients.discard(client_id) self.new_message(client_id,dumps({"type":"unsubscribe","gc":gc}),True) def new_message(self,client_id,message,system=False): self.messages.append(Message(client_id,message,self.id,system)) self.id += 1 #trim list,remove old messages if len(self.messages) > self.max_messages: self.messages = self.messages[-self.max_messages:] self.new_message_event.set() self.new_message_event.clear() def get_updates(self,cursor): """ get updates for the channel,if there is no updates returns a event for notification """ if not self.messages or cursor >= self.messages[-1].id: return self.new_message_event return [message for message in self.messages if message.id > cursor] def get_active_clients(self): now = int(time())#saves call to time return [client_id for client_id in self.clients if client_id and not "admin" in client_id and client_exists(client_id) and get_client(client_id).is_active(now)] def __hash__(self): return hash(self.name)
class JoinableQueue(Queue): """A subclass of :class:`Queue` that additionally has :meth:`task_done` and :meth:`join` methods.""" def __init__(self, maxsize=None, items=None, unfinished_tasks=None): from gevent.event import Event Queue.__init__(self, maxsize, items) self.unfinished_tasks = unfinished_tasks or 0 self._cond = Event() self._cond.set() def copy(self): return type(self)(self.maxsize, self.queue, self.unfinished_tasks) def _format(self): result = Queue._format(self) if self.unfinished_tasks: result += " tasks=%s _cond=%s" % (self.unfinished_tasks, self._cond) return result def _put(self, item): Queue._put(self, item) self.unfinished_tasks += 1 self._cond.clear() def task_done(self): """Indicate that a formerly enqueued task is complete. Used by queue consumer threads. For each :meth:`get <Queue.get>` used to fetch a task, a subsequent call to :meth:`task_done` tells the queue that the processing on the task is complete. If a :meth:`join` is currently blocking, it will resume when all items have been processed (meaning that a :meth:`task_done` call was received for every item that had been :meth:`put <Queue.put>` into the queue). Raises a :exc:`ValueError` if called more times than there were items placed in the queue. """ if self.unfinished_tasks <= 0: raise ValueError("task_done() called too many times") self.unfinished_tasks -= 1 if self.unfinished_tasks == 0: self._cond.set() def join(self): """Block until all items in the queue have been gotten and processed. The count of unfinished tasks goes up whenever an item is added to the queue. The count goes down whenever a consumer thread calls :meth:`task_done` to indicate that the item was retrieved and all work on it is complete. When the count of unfinished tasks drops to zero, :meth:`join` unblocks. """ self._cond.wait()
class Observer(Queue): def __init__(self, *args, **kw): game = kw.pop('game') self.event = Event() Queue.__init__(self, *args, **kw) def reaper(): self.event.clear() self.event.wait(30) game.remove_observer(self) gevent.spawn(reaper) def get(self, *args, **kw): self.event.set() return Queue.get(self, *args, **kw)
class UpdateManager(object): def __init__(self): self.event = Event() self.messages = [] def trigger(self, message): try: self.messages.append(message); self.event.set() self.event.clear() except Exception, e: log.error("exception notifying") print e
class Signals(object): def __init__(self): self.new_user_event = Event() def signaluser(self, request): user_id = request.GET.get('id') print self.new_user_event.is_set() self.new_user_event.set() self.new_user_event.clear() return HttpResponse("New User Signaled") def receiveuser(self, request): self.new_user_event.wait() return HttpResponse("New User Received")
class Test_DataServer(unittest.TestCase): def janitor(self, *args, **kwargs): pass def setUp(self): self.heartbeat_event = Event() self.cfg = Mock() self.cfg.data_port = DATA_PORT self.q = Queue() self.orbpktsrc = MagicMock() self.orbpktsrc.subscription.return_value.__enter__.return_value = self.q self.ds = DataServer(('localhost', DATA_PORT), self.orbpktsrc.subscription, self.heartbeat_event, self.janitor) def test_DataServer(self): self.cfg.heartbeat_interval = 10 [self.q.put(('', float(n))) for n in xrange(15)] received = [] def rx(): try: self.ds.start() sock = create_connection(('127.0.0.1', DATA_PORT), timeout=2) while True: received.append(sock.recv(0x10)) finally: self.ds.stop() rxg = spawn(rx) sleep(1) rxg.kill() self.assertEquals(len(received), 15) def test_DataServer_heartbeat(self): self.cfg.heartbeat_interval = 1 received = [] def rx(): try: self.ds.start() sock = create_connection(('127.0.0.1', DATA_PORT), timeout=2) while True: received.append(sock.recv(0x10)) finally: self.ds.stop() rxg = spawn(rx) sleep(0.1) self.heartbeat_event.set() self.heartbeat_event.clear() sleep(0.1) rxg.kill() self.assertEquals(len(received), 1)
def test_data_watcher(self): client = self._get_client(handler=self._makeOne()) client.start() client.ensure_path('/some/node') ev = Event() @client.DataWatch('/some/node') def changed(d, stat): ev.set() ev.wait() ev.clear() client.set('/some/node', 'newvalue') ev.wait() client.stop()
class UpdateManager(object): def __init__(self): print "created update manager" self.event = Event() print "created event object" self.messages = [] print "inited message queue!" def trigger(self, message): try: self.messages.append(message) self.event.set() self.event.clear() except Exception, e: print "exception notifying" print e
class ChatRoom(object): def __init__(self): self.msg = [] self.event = Event() def new_message(self, message): self.msg.append(message) self.event.set() self.event.clear() def wait(self): self.event.wait() def get_messages(self): return self.msg
def test_stopping_an_actor_prevents_it_from_processing_any_more_messages(defer): class MyActor(Actor): def receive(self, _): received.set() node = DummyNode() defer(node.stop) received = Event() a = node.spawn(MyActor) a << None received.wait() received.clear() a.stop() sleep(.001) ok_(not received.is_set(), "the '_stop' message should not be receivable in the actor") with expect_one_event(DeadLetter(a, None, sender=None)): a << None
class Site(Greenlet): def __init__(self, managers): super(Site,self).__init__() self.event = Event() self.event.clear() self.managers = managers self.managers.observe(self) def update(self): self.event.set() def _run(self): logger.debug("Site started") app = Flask(__name__) @app.route('/', methods=['GET']) def index(): return render_template('index.html') @app.route('/data', methods=['GET']) def data(): now = time.time() d = {'contents':[]} apps = [value for key,value in self.managers.managers.iteritems() if value['running']] d['contents'] = apps return jsonify(d) @app.route('/updated', methods=['GET']) def updated(): changed = self.event.wait(5.0) if changed: self.event.clear() return 'changed' else: return 'timeout' @app.route('/send', methods=['POST']) def send(): s = sender.AMQPSender() req = request s.send(exchange='manage',key=request.json['key'],object=request.json['body']) return 'OK' self.new_data = False http_server = WSGIServer(('', 5000), app) http_server.serve_forever()
def pipe_socket(client, remote): def copy(a, b, finish): while not finish.is_set(): try: data = a.recv(CHUNKSIZE) if not data: break b.sendall(data) except: break finish.set() finish = Event() finish.clear() threads = [gevent.spawn(copy, client, remote, finish), gevent.spawn(copy, remote, client, finish)] [t.join() for t in threads] client.close() remote.close()
class Group(object): """Maintain a group of greenlets that are still running. Links to each item and removes it upon notification. """ greenlet_class = Greenlet def __init__(self, *args): assert len(args) <= 1, args self.greenlets = set(*args) if args: for greenlet in args[0]: greenlet.rawlink(self._discard) # each item we kill we place in dying, to avoid killing the same greenlet twice self.dying = set() self._empty_event = Event() self._empty_event.set() def __repr__(self): return '<%s at 0x%x %s>' % (self.__class__.__name__, id(self), self.greenlets) def __len__(self): return len(self.greenlets) def __contains__(self, item): return item in self.greenlets def __iter__(self): return iter(self.greenlets) def add(self, greenlet): try: rawlink = greenlet.rawlink except AttributeError: pass # non-Greenlet greenlet, like MAIN else: rawlink(self._discard) self.greenlets.add(greenlet) self._empty_event.clear() def _discard(self, greenlet): self.greenlets.discard(greenlet) self.dying.discard(greenlet) if not self.greenlets: self._empty_event.set() def discard(self, greenlet): self._discard(greenlet) try: unlink = greenlet.unlink except AttributeError: pass # non-Greenlet greenlet, like MAIN else: unlink(self._discard) def start(self, greenlet): self.add(greenlet) greenlet.start() def spawn(self, *args, **kwargs): greenlet = self.greenlet_class(*args, **kwargs) self.start(greenlet) return greenlet # def close(self): # """Prevents any more tasks from being submitted to the pool""" # self.add = RaiseException("This %s has been closed" % self.__class__.__name__) def join(self, timeout=None, raise_error=False): if raise_error: greenlets = self.greenlets.copy() self._empty_event.wait(timeout=timeout) for greenlet in greenlets: if greenlet.exception is not None: raise greenlet.exception else: self._empty_event.wait(timeout=timeout) def kill(self, exception=GreenletExit, block=True, timeout=None): timer = Timeout.start_new(timeout) try: try: while self.greenlets: for greenlet in list(self.greenlets): if greenlet not in self.dying: try: kill = greenlet.kill except AttributeError: _kill(greenlet, exception) else: kill(exception, block=False) self.dying.add(greenlet) if not block: break joinall(self.greenlets) except Timeout: ex = sys.exc_info()[1] if ex is not timer: raise finally: timer.cancel() def killone(self, greenlet, exception=GreenletExit, block=True, timeout=None): if greenlet not in self.dying and greenlet in self.greenlets: greenlet.kill(exception, block=False) self.dying.add(greenlet) if block: greenlet.join(timeout) def apply(self, func, args=None, kwds=None): """Equivalent of the apply() builtin function. It blocks till the result is ready.""" if args is None: args = () if kwds is None: kwds = {} if getcurrent() in self: return func(*args, **kwds) else: return self.spawn(func, *args, **kwds).get() def apply_cb(self, func, args=None, kwds=None, callback=None): result = self.apply(func, args, kwds) if callback is not None: Greenlet.spawn(callback, result) return result def apply_async(self, func, args=None, kwds=None, callback=None): """A variant of the apply() method which returns a Greenlet object. If callback is specified then it should be a callable which accepts a single argument. When the result becomes ready callback is applied to it (unless the call failed).""" if args is None: args = () if kwds is None: kwds = {} if self.full(): # cannot call spawn() directly because it will block return Greenlet.spawn(self.apply_cb, func, args, kwds, callback) else: greenlet = self.spawn(func, *args, **kwds) if callback is not None: greenlet.link(pass_value(callback)) return greenlet def map(self, func, iterable): return list(self.imap(func, iterable)) def map_cb(self, func, iterable, callback=None): result = self.map(func, iterable) if callback is not None: callback(result) return result def map_async(self, func, iterable, callback=None): """ A variant of the map() method which returns a Greenlet object. If callback is specified then it should be a callable which accepts a single argument. """ return Greenlet.spawn(self.map_cb, func, iterable, callback) def imap(self, func, iterable): """An equivalent of itertools.imap()""" return IMap.spawn(func, iterable, spawn=self.spawn) def imap_unordered(self, func, iterable): """The same as imap() except that the ordering of the results from the returned iterator should be considered in arbitrary order.""" return IMapUnordered.spawn(func, iterable, spawn=self.spawn) def full(self): return False def wait_available(self): pass
class UserAddressManager: """ Matrix user <-> eth address mapping and user / address reachability helper. In Raiden the smallest unit of addressability is a node with an associated Ethereum address. In Matrix it's a user. Matrix users are (at the moment) bound to a specific homeserver. Since we want to provide resiliency against unavailable homeservers a single Raiden node with a single Ethereum address can be in control over multiple Matrix users on multiple homeservers. Therefore we need to perform a many-to-one mapping of Matrix users to Ethereum addresses. Each Matrix user has a presence state (ONLINE, OFFLINE). One of the preconditions of running a Raiden node is that there can always only be one node online for a particular address at a time. That means we can synthesize the reachability of an address from the user presence states. This helper internally tracks both the user presence and address reachability for addresses that have been marked as being 'interesting' (by calling the `.add_address()` method). Additionally it provides the option of passing callbacks that will be notified when presence / reachability change. """ def __init__( self, client: GMatrixClient, get_user_callable: Callable[[Union[User, str]], User], address_reachability_changed_callback: Callable[ [Address, AddressReachability], None], user_presence_changed_callback: Optional[Callable[[User, UserPresence], None]] = None, _log_context: Optional[Dict[str, Any]] = None, ) -> None: self._client = client self._get_user = get_user_callable self._address_reachability_changed_callback = address_reachability_changed_callback self._user_presence_changed_callback = user_presence_changed_callback self._stop_event = Event() self._reset_state() self._log_context = _log_context self._log = None self._listener_id: Optional[UUID] = None def start(self) -> None: """ Start listening for presence updates. Should be called before ``.login()`` is called on the underlying client. """ assert self._listener_id is None, "UserAddressManager.start() called twice" self._stop_event.clear() self._listener_id = self._client.add_presence_listener( self._presence_listener) def stop(self) -> None: """ Stop listening on presence updates. """ assert self._listener_id is not None, "UserAddressManager.stop() called before start" self._stop_event.set() self._client.remove_presence_listener(self._listener_id) self._listener_id = None self._log = None self._reset_state() @property def known_addresses(self) -> KeysView[Address]: """ Return all addresses we keep track of """ return self._address_to_userids.keys() def is_address_known(self, address: Address) -> bool: """ Is the given ``address`` reachability being monitored? """ return address in self._address_to_userids def add_address(self, address: Address): """ Add ``address`` to the known addresses that are being observed for reachability. """ # Since _address_to_userids is a defaultdict accessing the key creates the entry _ = self._address_to_userids[address] def add_userid_for_address(self, address: Address, user_id: str): """ Add a ``user_id`` for the given ``address``. Implicitly adds the address if it was unknown before. """ self._address_to_userids[address].add(user_id) def add_userids_for_address(self, address: Address, user_ids: Iterable[str]): """ Add multiple ``user_ids`` for the given ``address``. Implicitly adds any addresses if they were unknown before. """ self._address_to_userids[address].update(user_ids) def get_userids_for_address(self, address: Address) -> Set[str]: """ Return all known user ids for the given ``address``. """ if not self.is_address_known(address): return set() return self._address_to_userids[address] def get_userid_presence(self, user_id: str) -> UserPresence: """ Return the current presence state of ``user_id``. """ return self._userid_to_presence.get(user_id, UserPresence.UNKNOWN) def get_address_reachability(self, address: Address) -> AddressReachability: """ Return the current reachability state for ``address``. """ return self._address_to_reachability.get(address, AddressReachability.UNKNOWN) def force_user_presence(self, user: User, presence: UserPresence): """ Forcibly set the ``user`` presence to ``presence``. This method is only provided to cover an edge case in our use of the Matrix protocol and should **not** generally be used. """ self._userid_to_presence[user.user_id] = presence def populate_userids_for_address(self, address: Address, force: bool = False): """ Populate known user ids for the given ``address`` from the server directory. If ``force`` is ``True`` perform the directory search even if there already are known users. """ if force or not self.get_userids_for_address(address): self.add_userids_for_address( address, (user.user_id for user in self._client.search_user_directory( to_normalized_address(address)) if self._validate_userid_signature(user)), ) def refresh_address_presence(self, address: Address): """ Update synthesized address presence state from cached user presence states. Triggers callback (if any) in case the state has changed. This method is only provided to cover an edge case in our use of the Matrix protocol and should **not** generally be used. """ composite_presence = { self._fetch_user_presence(uid) for uid in self._address_to_userids[address] } # Iterate over UserPresence in definition order (most to least online) and pick # first matching state new_presence = UserPresence.UNKNOWN for presence in UserPresence.__members__.values(): if presence in composite_presence: new_presence = presence break new_address_reachability = USER_PRESENCE_TO_ADDRESS_REACHABILITY[ new_presence] prev_addresss_reachability = self.get_address_reachability(address) if new_address_reachability == prev_addresss_reachability: # Cached address reachability matches new state, do nothing return self.log.debug( "Changing address reachability state", address=to_checksum_address(address), prev_state=prev_addresss_reachability, state=new_address_reachability, ) self._address_to_reachability[address] = new_address_reachability self._address_reachability_changed_callback(address, new_address_reachability) def _presence_listener(self, event: Dict[str, Any]): """ Update cached user presence state from Matrix presence events. Due to the possibility of nodes using accounts on multiple homeservers a composite address state is synthesised from the cached individual user presence states. """ if self._stop_event.ready(): return user_id = event["sender"] if event["type"] != "m.presence" or user_id == self._user_id: return user = self._get_user(user_id) user.displayname = event["content"].get( "displayname") or user.displayname address = self._validate_userid_signature(user) if not address: # Malformed address - skip return # not a user we've whitelisted, skip if not self.is_address_known(address): return self.add_userid_for_address(address, user_id) new_state = UserPresence(event["content"]["presence"]) if new_state == self.get_userid_presence(user_id): # Cached presence state matches, no action required return self.log.debug( "Changing user presence state", user_id=user_id, prev_state=self._userid_to_presence.get(user_id), state=new_state, ) self._userid_to_presence[user_id] = new_state self.refresh_address_presence(address) if self._user_presence_changed_callback: self._user_presence_changed_callback(user, new_state) def log_status_message(self): while not self._stop_event.ready(): addresses_uids_presence = { to_checksum_address(address): { user_id: self.get_userid_presence(user_id).value for user_id in self.get_userids_for_address(address) } for address in self.known_addresses } log.debug( "Matrix address manager status", addresses_uids_and_presence=addresses_uids_presence, current_user=self._user_id, ) self._stop_event.wait(30) def _reset_state(self): self._address_to_userids: Dict[Address, Set[str]] = defaultdict(set) self._address_to_reachability: Dict[Address, AddressReachability] = dict() self._userid_to_presence: Dict[str, UserPresence] = dict() @property def _user_id(self) -> str: user_id = getattr(self._client, "user_id", None) assert user_id, f"{self.__class__.__name__}._user_id accessed before client login" return user_id def _fetch_user_presence(self, user_id: str) -> UserPresence: if user_id not in self._userid_to_presence: try: presence = UserPresence( self._client.get_user_presence(user_id)) except MatrixRequestError: presence = UserPresence.UNKNOWN self._userid_to_presence[user_id] = presence return self._userid_to_presence[user_id] @staticmethod def _validate_userid_signature(user: User) -> Optional[Address]: return validate_userid_signature(user) @property def log(self) -> BoundLoggerLazyProxy: if not self._log: if not hasattr(self._client, "user_id"): return log self._log = log.bind( **{ "current_user": self._user_id, "node": to_checksum_address( self._user_id.split(":", 1)[0][1:]), **(self._log_context or {}), }) return self._log
def test_list_service_info(zk, test_application_name, service_holder, hub): is_reached = Event() is_reached.clear() @service_holder.tree_changed.connect_via(service_holder) def reach(sender, event): print event is_reached.set() def wait_and_reset(): is_reached.wait() is_reached.clear() base_path = '/huskar/service/%s' % test_application_name zk.create('%s/stable' % base_path, makepath=True) wait_and_reset() assert dict(service_holder.list_service_info()) == { 'overall': {}, 'stable': {} } zk.set(base_path, json.dumps({'info': {'balance_policy': 'RoundRobin'}})) wait_and_reset() assert dict(service_holder.list_service_info(['overall'])) == { 'overall': { 'balance_policy': { 'value': '"RoundRobin"' } } } zk.set('%s/stable' % base_path, json.dumps({'info': { 'dict': { "port": 8080 } }})) wait_and_reset() assert dict(service_holder.list_service_info()) == { 'overall': { 'balance_policy': { 'value': '"RoundRobin"' } }, 'stable': { 'dict': { 'value': '{"port": 8080}' } } } assert dict(service_holder.list_service_info(['stable'])) == { 'stable': { 'dict': { 'value': '{"port": 8080}' } } } zk.create('%s/overall' % base_path, json.dumps({'info': { 'balance_policy': 'Random' }}), makepath=True) wait_and_reset() assert dict(service_holder.list_service_info()) == { 'overall': { 'balance_policy': { 'value': '"RoundRobin"' } }, 'stable': { 'dict': { 'value': '{"port": 8080}' } } } zk.set('%s/stable' % base_path, '233') wait_and_reset() assert dict(service_holder.list_service_info(['stable'])) == {'stable': {}}
class RNOService(BaseService): # required by BaseService name = 'rno' default_config = dict(eth=dict(privkey_hex='')) # RNO address, where the requests for random number should be addressed to. my_addr = None # Keeps all transactions not yet processed by loop_body tx_queue = None # Will be used to a) sign transaction and b) encrypt random number using ECIES eccx = None privkey_hex = None def __init__(self, app): super(RNOService, self).__init__(app) log.info('Initializing RNO') self.config = app.config self.interrupt = Event() self.tx_queue = Queue() # thread safe self.privkey_hex = self.config['eth']['privkey_hex'].decode('hex') self.my_addr = privtoaddr(self.privkey_hex) self.eccx = ECCx(None, self.privkey_hex) # Process the transaction queue. There is no concurrency problem here since # the Queue is thread-safe. def loop_body(self): log.debug('RNO body', my_addr=self.my_addr) while not self.tx_queue.empty(): tx = self.tx_queue.get() if tx.to == self.my_addr: self.process_tx(tx) # Transactions should be added to a queue so that 'loop_body' process that queue # To minimize code dependency and coupling, this method will be called for ALL # transactions received. # It is called in the loop of eth_service.py -> on_receive_transactions def add_transaction(self, tx): log.debug('RNO received transaction', tx=tx) # All transactions are being queued here to minimize the blocking # of caller's thread. Transactions not addressed to RNO are discarded # in loop_body. self.tx_queue.put(tx) # This method is the core of the RNO. Transactions should NOT be processed in the # add_transaction otherwise it would block the caller. def process_tx(self, tx): log.debug('process tx', tx=tx) # 2) Extract sender's pubkey from the Electrum-style signature of the tx sender_pubkey = self.sender_pubkey_from_tx(tx) enc_num = self.generate_encrypted_random_number(sender_pubkey) # 5) encrypt RN using reveal host's pubkey (eRN2) (???) # this is not specified yet # 6) create/send transaction back to tx sender self.deliver(enc_num, tx.sender) # 7) create/send transaction to reveal host. # this is not specified yet def sender_pubkey_from_tx(self, tx): encoded_signature = _encode_sig(tx.v, tx.r, tx.s) message = None # TODO: find out how to build the data (message) where the signature is applied. return recover(message, encoded_signature) def generate_encrypted_random_number(self, pubkey): # 3) generate the random number number = os.urandom(64) # 4) encrypt RN using sender's pubkey (eRN1) return self.eccx.encrypt(number, pubkey) def deliver(self, enc_num, to): # nonce = number of transactions already sent by that account head = self.app.services.chain.chain.head nonce = head.get_nonce(self.my_addr) # Took from buterin example: # https://blog.ethereum.org/2014/04/10/pyethereum-and-serpent-programming-guide/ gasprice = 10**12 # Took from buterin example: # https://blog.ethereum.org/2014/04/10/pyethereum-and-serpent-programming-guide/ startgas = 10000 value = 0 # It's just a message, don't need to send any value (TODO: confirm that info) # data is a json formatted message but has to be 'binary' unix_now = int(round(time())) payload = {} payload['when'] = unix_now payload['number'] = enc_num payload['publish_on'] = unix_now + 86400 # in 24 hours payload['published_at'] = 'http://www.example.com/foo' data = json.dumps(payload) deliver_tx = Transaction(nonce, gasprice, startgas, to, value, data) signed_deliver_tx = deliver_tx.sign(self.privkey_hex) success, output = apply_transaction(head, signed_deliver_tx) # Sends the reply back to Requester and Reveal Host def send_replies(self, number, requester_addr, reveal_host_addr, publish_at, publish_on): log.debug('RNO reply', number=number, requester_addr=requester_addr, publish_at=publish_at, publish_on=publish_on) # Generates the public address (IPFS?) that will be used by the Reveal Host def generate_public_address(self, number): address = 'some public address' log.debug('RNO pub address', address=address) return address # This will make the loop_body be executed by RNOService thread. def wakeup(self): self.interrupt.set() # @override BaseService._run (Greenlet._run) def _run(self): while True: self.interrupt.wait() self.loop_body() self.interrupt.clear()
class Console(BaseService): """A service starting an interactive ipython session when receiving the SIGSTP signal (e.g. via keyboard shortcut CTRL-Z). """ name = 'console' def __init__(self, app): super(Console, self).__init__(app) self.interrupt = Event() self.console_locals = {} if app.start_console: self.start() self.interrupt.set() else: SigINTHandler(self.interrupt) def _stop_app(self): try: self.app.stop() except gevent.GreenletExit: pass def start(self): super(Console, self).start() class Eth(object): """ convenience object to interact with the live chain """ def __init__(this, app): this.app = app this.services = app.services this.stop = app.stop this.chainservice = app.services.chain this.chain = this.chainservice.chain this.coinbase = app.services.accounts.coinbase @property def pending(this): return this.chainservice.head_candidate head_candidate = pending @property def latest(this): return this.chain.head def transact(this, to, value=0, data='', sender=None, startgas=25000, gasprice=60 * denoms.shannon): sender = normalize_address(sender or this.coinbase) to = normalize_address(to, allow_blank=True) state = State(this.head_candidate.state_root, this.chain.env) nonce = state.get_nonce(sender) tx = Transaction(nonce, gasprice, startgas, to, value, data) this.app.services.accounts.sign_tx(sender, tx) assert tx.sender == sender this.chainservice.add_transaction(tx) return tx def call(this, to, value=0, data='', sender=None, startgas=25000, gasprice=60 * denoms.shannon): sender = normalize_address(sender or this.coinbase) to = normalize_address(to, allow_blank=True) block = this.head_candidate state_root_before = block.state_root assert block.prevhash == this.chain.head_hash # rebuild block state before finalization test_state = this.chain.mk_poststate_of_blockhash( block.prevhash) initialize(test_state, block) for tx in block.transactions: success, _ = apply_transaction(test_state, tx) assert success # Need this because otherwise the Transaction.network_id # @property returns 0, which causes the tx to fail validation. class MockedTx(Transaction): network_id = None # apply transaction nonce = test_state.get_nonce(sender) tx = MockedTx(nonce, gasprice, startgas, to, value, data) tx.sender = sender try: success, output = apply_transaction(test_state, tx) except InvalidTransaction as e: log.debug("error applying tx in Eth.call", exc=e) success = False assert block.state_root == state_root_before if success: return output else: return False def find_transaction(this, tx): try: t, blk, idx = this.chain.get_transaction(tx.hash) except: return {} return dict(tx=t, block=blk, index=idx) def new_contract(this, abi, address, sender=None): return ABIContract(sender or this.coinbase, abi, address, this.call, this.transact) def block_from_rlp(this, rlp_data): from .eth_protocol import TransientBlock import rlp l = rlp.decode_lazy(rlp_data) return TransientBlock.init_from_rlp(l).to_block() try: from ethereum.tools._solidity import solc_wrapper except ImportError: solc_wrapper = None pass try: import serpent except ImportError: serpent = None pass self.console_locals = dict(eth=Eth(self.app), solidity=solc_wrapper, serpent=serpent, denoms=denoms, true=True, false=False, Eth=Eth) for k, v in list(self.app.script_globals.items()): self.console_locals[k] = v def _run(self): self.interrupt.wait() print('\n' * 2) print("Entering Console" + bc.OKGREEN) print("Tip:" + bc.OKBLUE) print( "\tuse `{}lastlog(n){}` to see n lines of log-output. [default 10] " .format(bc.HEADER, bc.OKBLUE)) print("\tuse `{}lasterr(n){}` to see n lines of stderr.".format( bc.HEADER, bc.OKBLUE)) print("\tuse `{}help(eth){}` for help on accessing the live chain.". format(bc.HEADER, bc.OKBLUE)) print("\n" + bc.ENDC) # runmultiple hack in place? if hasattr(self.console_locals['eth'].app, 'apps'): print('\n' * 2 + bc.OKGREEN) print("Hint:" + bc.OKBLUE) print(( '\tOther nodes are accessible from {}`eth.app.apps`{}').format( bc.HEADER, bc.OKBLUE)) print('\tThey where automatically assigned to:') print("\t`{}eth1{}`".format(bc.HEADER, bc.OKBLUE)) if len(self.console_locals['eth'].app.apps) > 3: print("\t {}...{}".format(bc.HEADER, bc.OKBLUE)) print("\t`{}eth{}{}`".format( bc.HEADER, len(self.console_locals['eth'].app.apps) - 1, bc.OKBLUE)) print("\n" + bc.ENDC) # automatically assign different nodes to 'eth1.', 'eth2.'' , .... Eth = self.console_locals['Eth'] for x in range(1, len(self.console_locals['eth'].app.apps)): self.console_locals['eth' + str(x)] = Eth( self.console_locals['eth'].app.apps[x]) # Remove handlers that log to stderr root = getLogger() for handler in root.handlers[:]: if isinstance(handler, StreamHandler) and handler.stream == sys.stderr: root.removeHandler(handler) stream = io.StringIO() handler = StreamHandler(stream=stream) handler.formatter = Formatter("%(levelname)s:%(name)s %(message)s") root.addHandler(handler) def lastlog(n=10, prefix=None, level=None): """Print the last `n` log lines to stdout. Use `prefix='p2p'` to filter for a specific logger. Use `level=INFO` to filter for a specific level. Level- and prefix-filtering are applied before tailing the log. """ lines = (stream.getvalue().strip().split('\n') or []) if prefix: lines = [ line for line in lines if line.split(':')[1].startswith(prefix) ] if level: lines = [line for line in lines if line.split(':')[0] == level] for line in lines[-n:]: print(line) self.console_locals['lastlog'] = lastlog err = io.StringIO() sys.stderr = err def lasterr(n=1): """Print the last `n` entries of stderr to stdout. """ for line in (err.getvalue().strip().split('\n') or [])[-n:]: print(line) self.console_locals['lasterr'] = lasterr IPython.start_ipython(argv=['--gui', 'gevent'], user_ns=self.console_locals) self.interrupt.clear() sys.exit(0)
class Session(object): """ Base class for Session objects. Provides for different backends for queueing messages for sessions. Subclasses are expected to overload the add_message and get_messages to reflect their storage system. """ # Session's timeout after 5 seconds expires = timedelta(seconds=5) def __init__(self, server, session_id=None): self.expires_at = datetime.now() + self.expires self.expired = False self.forever = False self.session_id = self.generate_uid() # Whether this was closed explictly by client vs # internally by garbage collection. self.interrupted = False # When a polling request is closed by a network error - not by # server, the session should be automatically closed. When there # is a network error - we're in an undefined state. Some messages # may have been lost, there is not much we can do about it. self.network_error = False # Async event, use rawlink to string callbacks self.timeout = Event() self.locked = Event() def generate_uid(self): """ Returns a string of the unique identifier of the session. """ return str(uuid.uuid4()) def persist(self, extension=None, forever=False): """ Bump the time to live of the session by a given amount, or forever. """ self.expired = False if forever: self.forever = True return # Slide the expirtaion time one more expiration interval # into the future if extension is None: self.expires_at = datetime.now() + self.expires else: self.expires_at = datetime.now() + extension self.forever = False def post_delete(self): pass def kill(self): self.killed = True self.expire() def expire(self): """ Manually expire a session. """ self.expired = True self.forever = False def incr_hits(self): self.hits += 1 def is_new(self): return self.hits == 0 def heartbeat(self): self.persist() self.heartbeats += 1 return self.heartbeats def add_message(self, msg): raise NotImplemented() def get_messages(self, **kwargs): raise NotImplemented() def is_locked(self): return self.locked.is_set() def is_network_error(self): return self.network_error def is_expired(self): return self.expired def is_interrupted(self): return self.interrupted def lock(self): self.locked.set() def unlock(self): self.locked.clear() def __str__(self): pass
class JobsQueue(object): def __init__(self, middleware): self.middleware = middleware self.deque = JobsDeque() self.queue = [] # Event responsible for the job queue schedule loop. # This event is set and a new job is potentially ready to run self.queue_event = Event() # Shared lock (JobSharedLock) dict self.job_locks = {} def all(self): return self.deque.all() def add(self, job): self.deque.add(job) self.queue.append(job) self.middleware.send_event('core.get_jobs', 'ADDED', id=job.id, fields=job.__encode__()) # A job has been added to the queue, let the queue scheduler run self.queue_event.set() def get_lock(self, job): """ Get a shared lock for a job """ name = job.get_lock_name() if name is None: return None lock = self.job_locks.get(name) if lock is None: lock = JobSharedLock(self, name) self.job_locks[lock.name] = lock lock.add_job(job) return lock def release_lock(self, job): lock = job.get_lock() if not lock: return # Remove job from lock list and release it so another job can use it lock.remove_job(job) lock.release() if len(lock.get_jobs()) == 0: self.job_locks.pop(lock.name) # Once a lock is released there could be another job in the queue # waiting for the same lock self.queue_event.set() def next(self): """ This is a blocking method. Returns when there is a new job ready to run. """ while True: # Awaits a new event to look for a job self.queue_event.wait() found = None for job in self.queue: lock = self.get_lock(job) # Get job in the queue if it has no lock or its not locked if lock is None or not lock.locked(): found = job if lock: job.set_lock(lock) break if found: # Unlocked job found to run self.queue.remove(found) # If there are no more jobs in the queue, clear the event if len(self.queue) == 0: self.queue_event.clear() return found else: # No jobs available to run, clear the event self.queue_event.clear() def run(self): while True: job = self.next() gevent.spawn(job.run, self)
class RaidenService(Runnable): """ A Raiden node. """ def __init__( self, chain: BlockChainService, query_start_block: BlockNumber, default_registry: TokenNetworkRegistry, default_secret_registry: SecretRegistry, default_service_registry: Optional[ServiceRegistry], default_one_to_n_address: Optional[Address], transport, raiden_event_handler, message_handler, config, discovery=None, user_deposit=None, ): super().__init__() self.tokennetworkids_to_connectionmanagers: ConnectionManagerDict = dict( ) self.targets_to_identifiers_to_statuses: StatusesDict = defaultdict( dict) self.chain: BlockChainService = chain self.default_registry = default_registry self.query_start_block = query_start_block self.default_one_to_n_address = default_one_to_n_address self.default_secret_registry = default_secret_registry self.default_service_registry = default_service_registry self.config = config self.signer: Signer = LocalSigner(self.chain.client.privkey) self.address = self.signer.address self.discovery = discovery self.transport = transport self.user_deposit = user_deposit self.blockchain_events = BlockchainEvents() self.alarm = AlarmTask(chain) self.raiden_event_handler = raiden_event_handler self.message_handler = message_handler self.stop_event = Event() self.stop_event.set() # inits as stopped self.greenlets: List[Greenlet] = list() self.snapshot_group = 0 self.contract_manager = ContractManager(config["contracts_path"]) self.database_path = config["database_path"] self.wal = None if self.database_path != ":memory:": database_dir = os.path.dirname(config["database_path"]) os.makedirs(database_dir, exist_ok=True) self.database_dir = database_dir # Two raiden processes must not write to the same database. Even # though it's possible the database itself would not be corrupt, # the node's state could. If a database was shared among multiple # nodes, the database WAL would be the union of multiple node's # WAL. During a restart a single node can't distinguish its state # changes from the others, and it would apply it all, meaning that # a node would execute the actions of itself and the others. # # Additionally the database snapshots would be corrupt, because it # would not represent the effects of applying all the state changes # in order. lock_file = os.path.join(self.database_dir, ".lock") self.db_lock = filelock.FileLock(lock_file) else: self.database_path = ":memory:" self.database_dir = None self.serialization_file = None self.db_lock = None self.event_poll_lock = gevent.lock.Semaphore() self.gas_reserve_lock = gevent.lock.Semaphore() self.payment_identifier_lock = gevent.lock.Semaphore() # Flag used to skip the processing of all Raiden events during the # startup. # # Rationale: At the startup, the latest snapshot is restored and all # state changes which are not 'part' of it are applied. The criteria to # re-apply the state changes is their 'absence' in the snapshot, /not/ # their completeness. Because these state changes are re-executed # in-order and some of their side-effects will already have been # completed, the events should be delayed until the state is # synchronized (e.g. an open channel state change, which has already # been mined). # # Incomplete events, i.e. the ones which don't have their side-effects # applied, will be executed once the blockchain state is synchronized # because of the node's queues. self.ready_to_process_events = False def start(self): """ Start the node synchronously. Raises directly if anything went wrong on startup """ assert self.stop_event.ready(), f"Node already started. node:{self!r}" self.stop_event.clear() self.greenlets = list() self.ready_to_process_events = False # set to False because of restarts if self.database_dir is not None: self.db_lock.acquire(timeout=0) assert self.db_lock.is_locked, f"Database not locked. node:{self!r}" # start the registration early to speed up the start if self.config["transport_type"] == "udp": endpoint_registration_greenlet = gevent.spawn( self.discovery.register, self.address, self.config["transport"]["udp"]["external_ip"], self.config["transport"]["udp"]["external_port"], ) self.maybe_upgrade_db() storage = sqlite.SerializedSQLiteStorage( database_path=self.database_path, serializer=serialize.JSONSerializer()) storage.update_version() storage.log_run() self.wal = wal.restore_to_state_change( transition_function=node.state_transition, storage=storage, state_change_identifier="latest", ) if self.wal.state_manager.current_state is None: log.debug("No recoverable state available, creating inital state.", node=pex(self.address)) # On first run Raiden needs to fetch all events for the payment # network, to reconstruct all token network graphs and find opened # channels last_log_block_number = self.query_start_block last_log_block_hash = self.chain.client.blockhash_from_blocknumber( last_log_block_number) state_change = ActionInitChain( pseudo_random_generator=random.Random(), block_number=last_log_block_number, block_hash=last_log_block_hash, our_address=self.chain.node_address, chain_id=self.chain.network_id, ) self.handle_and_track_state_change(state_change) payment_network = PaymentNetworkState( self.default_registry.address, [], # empty list of token network states as it's the node's startup ) state_change = ContractReceiveNewPaymentNetwork( transaction_hash=constants.EMPTY_HASH, payment_network=payment_network, block_number=last_log_block_number, block_hash=last_log_block_hash, ) self.handle_and_track_state_change(state_change) else: # The `Block` state change is dispatched only after all the events # for that given block have been processed, filters can be safely # installed starting from this position without losing events. last_log_block_number = views.block_number( self.wal.state_manager.current_state) log.debug( "Restored state from WAL", last_restored_block=last_log_block_number, node=pex(self.address), ) known_networks = views.get_payment_network_identifiers( views.state_from_raiden(self)) if known_networks and self.default_registry.address not in known_networks: configured_registry = pex(self.default_registry.address) known_registries = lpex(known_networks) raise RuntimeError( f"Token network address mismatch.\n" f"Raiden is configured to use the smart contract " f"{configured_registry}, which conflicts with the current known " f"smart contracts {known_registries}") # Restore the current snapshot group state_change_qty = self.wal.storage.count_state_changes() self.snapshot_group = state_change_qty // SNAPSHOT_STATE_CHANGES_COUNT # Install the filters using the latest confirmed from_block value, # otherwise blockchain logs can be lost. self.install_all_blockchain_filters(self.default_registry, self.default_secret_registry, last_log_block_number) # Complete the first_run of the alarm task and synchronize with the # blockchain since the last run. # # Notes about setup order: # - The filters must be polled after the node state has been primed, # otherwise the state changes won't have effect. # - The alarm must complete its first run before the transport is started, # to reject messages for closed/settled channels. self.alarm.register_callback(self._callback_new_block) self.alarm.first_run(last_log_block_number) chain_state = views.state_from_raiden(self) self._initialize_payment_statuses(chain_state) self._initialize_transactions_queues(chain_state) self._initialize_messages_queues(chain_state) self._initialize_whitelists(chain_state) self._initialize_monitoring_services_queue(chain_state) self._initialize_ready_to_processed_events() if self.config["transport_type"] == "udp": endpoint_registration_greenlet.get( ) # re-raise if exception occurred # Start the side-effects: # - React to blockchain events # - React to incoming messages # - Send pending transactions # - Send pending message self.alarm.link_exception(self.on_error) self.transport.link_exception(self.on_error) self._start_transport(chain_state) self._start_alarm_task() log.debug("Raiden Service started", node=pex(self.address)) super().start() def _run(self, *args, **kwargs): # pylint: disable=method-hidden """ Busy-wait on long-lived subtasks/greenlets, re-raise if any error occurs """ self.greenlet.name = f"RaidenService._run node:{pex(self.address)}" try: self.stop_event.wait() except gevent.GreenletExit: # killed without exception self.stop_event.set() gevent.killall([self.alarm, self.transport]) # kill children raise # re-raise to keep killed status except Exception: self.stop() raise def stop(self): """ Stop the node gracefully. Raise if any stop-time error occurred on any subtask """ if self.stop_event.ready(): # not started return # Needs to come before any greenlets joining self.stop_event.set() # Filters must be uninstalled after the alarm task has stopped. Since # the events are polled by an alarm task callback, if the filters are # uninstalled before the alarm task is fully stopped the callback # `poll_blockchain_events` will fail. # # We need a timeout to prevent an endless loop from trying to # contact the disconnected client self.transport.stop() self.alarm.stop() self.transport.join() self.alarm.join() self.blockchain_events.uninstall_all_event_listeners() # Close storage DB to release internal DB lock self.wal.storage.conn.close() if self.db_lock is not None: self.db_lock.release() log.debug("Raiden Service stopped", node=pex(self.address)) @property def confirmation_blocks(self): return self.config["blockchain"]["confirmation_blocks"] @property def privkey(self): return self.chain.client.privkey def add_pending_greenlet(self, greenlet: Greenlet): """ Ensures an error on the passed greenlet crashes self/main greenlet. """ def remove(_): self.greenlets.remove(greenlet) self.greenlets.append(greenlet) greenlet.link_exception(self.on_error) greenlet.link_value(remove) def __repr__(self): return f"<{self.__class__.__name__} node:{pex(self.address)}>" def _start_transport(self, chain_state: ChainState): """ Initialize the transport and related facilities. Note: The transport must not be started before the node has caught up with the blockchain through `AlarmTask.first_run()`. This synchronization includes the on-chain channel state and is necessary to reject new messages for closed channels. """ assert self.alarm.is_primed(), f"AlarmTask not primed. node:{self!r}" assert self.ready_to_process_events, f"Event procossing disable. node:{self!r}" self.transport.start( raiden_service=self, message_handler=self.message_handler, prev_auth_data=chain_state.last_transport_authdata, ) for neighbour in views.all_neighbour_nodes(chain_state): if neighbour != ConnectionManager.BOOTSTRAP_ADDR: self.start_health_check_for(neighbour) def _start_alarm_task(self): """Start the alarm task. Note: The alarm task must be started only when processing events is allowed, otherwise side-effects of blockchain events will be ignored. """ assert self.ready_to_process_events, f"Event procossing disable. node:{self!r}" self.alarm.start() def _initialize_ready_to_processed_events(self): assert not self.transport assert not self.alarm # This flag /must/ be set to true before the transport or the alarm task is started self.ready_to_process_events = True def get_block_number(self) -> BlockNumber: assert self.wal, f"WAL object not yet initialized. node:{self!r}" return views.block_number(self.wal.state_manager.current_state) def on_message(self, message: Message): self.message_handler.on_message(self, message) def handle_and_track_state_change(self, state_change: StateChange): """ Dispatch the state change and does not handle the exceptions. When the method is used the exceptions are tracked and re-raised in the raiden service thread. """ for greenlet in self.handle_state_change(state_change): self.add_pending_greenlet(greenlet) def handle_state_change(self, state_change: StateChange) -> List[Greenlet]: """ Dispatch the state change and return the processing threads. Use this for error reporting, failures in the returned greenlets, should be re-raised using `gevent.joinall` with `raise_error=True`. """ assert self.wal, f"WAL not restored. node:{self!r}" log.debug( "State change", node=pex(self.address), state_change=_redact_secret( serialize.JSONSerializer.serialize(state_change)), ) old_state = views.state_from_raiden(self) new_state, raiden_event_list = self.wal.log_and_dispatch(state_change) for changed_balance_proof in views.detect_balance_proof_change( old_state, new_state): update_services_from_balance_proof(self, new_state, changed_balance_proof) log.debug( "Raiden events", node=pex(self.address), raiden_events=[ _redact_secret(serialize.JSONSerializer.serialize(event)) for event in raiden_event_list ], ) greenlets: List[Greenlet] = list() if self.ready_to_process_events: for raiden_event in raiden_event_list: greenlets.append( self.handle_event(chain_state=new_state, raiden_event=raiden_event)) state_changes_count = self.wal.storage.count_state_changes() new_snapshot_group = state_changes_count // SNAPSHOT_STATE_CHANGES_COUNT if new_snapshot_group > self.snapshot_group: log.debug("Storing snapshot", snapshot_id=new_snapshot_group) self.wal.snapshot() self.snapshot_group = new_snapshot_group return greenlets def handle_event(self, chain_state: ChainState, raiden_event: RaidenEvent) -> Greenlet: """Spawn a new thread to handle a Raiden event. This will spawn a new greenlet to handle each event, which is important for two reasons: - Blockchain transactions can be queued without interfering with each other. - The calling thread is free to do more work. This is specially important for the AlarmTask thread, which will eventually cause the node to send transactions when a given Block is reached (e.g. registering a secret or settling a channel). Important: This is spawing a new greenlet for /each/ transaction. It's therefore /required/ that there is *NO* order among these. """ return gevent.spawn(self._handle_event, chain_state, raiden_event) def _handle_event(self, chain_state: ChainState, raiden_event: RaidenEvent): assert isinstance(chain_state, ChainState) assert isinstance(raiden_event, RaidenEvent) try: self.raiden_event_handler.on_raiden_event(raiden=self, chain_state=chain_state, event=raiden_event) except RaidenRecoverableError as e: log.error(str(e)) except InvalidDBData: raise except RaidenUnrecoverableError as e: log_unrecoverable = ( self.config["environment_type"] == Environment.PRODUCTION and not self.config["unrecoverable_error_should_crash"]) if log_unrecoverable: log.error(str(e)) else: raise def set_node_network_state(self, node_address: Address, network_state: str): state_change = ActionChangeNodeNetworkState(node_address, network_state) self.handle_and_track_state_change(state_change) def start_health_check_for(self, node_address: Address): """Start health checking `node_address`. This function is a noop during initialization, because health checking can be started as a side effect of some events (e.g. new channel). For these cases the healthcheck will be started by `start_neighbours_healthcheck`. """ if self.transport: self.transport.start_health_check(node_address) def _callback_new_block(self, latest_block: Dict): """Called once a new block is detected by the alarm task. Note: This should be called only once per block, otherwise there will be duplicated `Block` state changes in the log. Therefore this method should be called only once a new block is mined with the corresponding block data from the AlarmTask. """ # User facing APIs, which have on-chain side-effects, force polled the # blockchain to update the node's state. This force poll is used to # provide a consistent view to the user, e.g. a channel open call waits # for the transaction to be mined and force polled the event to update # the node's state. This pattern introduced a race with the alarm task # and the task which served the user request, because the events are # returned only once per filter. The lock below is to protect against # these races (introduced by the commit # 3686b3275ff7c0b669a6d5e2b34109c3bdf1921d) with self.event_poll_lock: latest_block_number = latest_block["number"] # Handle testing with private chains. The block number can be # smaller than confirmation_blocks confirmed_block_number = max( GENESIS_BLOCK_NUMBER, latest_block_number - self.config["blockchain"]["confirmation_blocks"], ) confirmed_block = self.chain.client.web3.eth.getBlock( confirmed_block_number) # These state changes will be procesed with a block_number which is # /larger/ than the ChainState's block_number. for event in self.blockchain_events.poll_blockchain_events( confirmed_block_number): on_blockchain_event(self, event) # On restart the Raiden node will re-create the filters with the # ethereum node. These filters will have the from_block set to the # value of the latest Block state change. To avoid missing events # the Block state change is dispatched only after all of the events # have been processed. # # This means on some corner cases a few events may be applied # twice, this will happen if the node crashed and some events have # been processed but the Block state change has not been # dispatched. state_change = Block( block_number=confirmed_block_number, gas_limit=confirmed_block["gasLimit"], block_hash=BlockHash(bytes(confirmed_block["hash"])), ) # Note: It's important to /not/ block here, because this function # can be called from the alarm task greenlet, which should not # starve. self.handle_and_track_state_change(state_change) def _initialize_transactions_queues(self, chain_state: ChainState): """Initialize the pending transaction queue from the previous run. Note: This will only send the transactions which don't have their side-effects applied. Transactions which another node may have sent already will be detected by the alarm task's first run and cleared from the queue (e.g. A monitoring service update transfer). """ assert self.alarm.is_primed(), f"AlarmTask not primed. node:{self!r}" pending_transactions = views.get_pending_transactions(chain_state) log.debug( "Processing pending transactions", num_pending_transactions=len(pending_transactions), node=pex(self.address), ) for transaction in pending_transactions: try: self.raiden_event_handler.on_raiden_event( raiden=self, chain_state=chain_state, event=transaction) except RaidenRecoverableError as e: log.error(str(e)) except InvalidDBData: raise except RaidenUnrecoverableError as e: log_unrecoverable = ( self.config["environment_type"] == Environment.PRODUCTION and not self.config["unrecoverable_error_should_crash"]) if log_unrecoverable: log.error(str(e)) else: raise def _initialize_payment_statuses(self, chain_state: ChainState): """ Re-initialize targets_to_identifiers_to_statuses. Restore the PaymentStatus for any pending payment. This is not tied to a specific protocol message but to the lifecycle of a payment, i.e. the status is re-created if a payment itself has not completed. """ with self.payment_identifier_lock: for task in chain_state.payment_mapping.secrethashes_to_task.values( ): if not isinstance(task, InitiatorTask): continue # Every transfer in the transfers_list must have the same target # and payment_identifier, so using the first transfer is # sufficient. initiator = next( iter(task.manager_state.initiator_transfers.values())) transfer = initiator.transfer transfer_description = initiator.transfer_description target = transfer.target identifier = transfer.payment_identifier balance_proof = transfer.balance_proof self.targets_to_identifiers_to_statuses[target][ identifier] = PaymentStatus( payment_identifier=identifier, amount=transfer_description.amount, token_network_identifier=TokenNetworkID( balance_proof.token_network_identifier), payment_done=AsyncResult(), ) def _initialize_messages_queues(self, chain_state: ChainState): """Initialize all the message queues with the transport. Note: All messages from the state queues must be pushed to the transport before it's started. This is necessary to avoid a race where the transport processes network messages too quickly, queueing new messages before any of the previous messages, resulting in new messages being out-of-order. The Alarm task must be started before this method is called, otherwise queues for channel closed while the node was offline won't be properly cleared. It is not bad but it is suboptimal. """ assert not self.transport, f"Transport is running. node:{self!r}" assert self.alarm.is_primed(), f"AlarmTask not primed. node:{self!r}" events_queues = views.get_all_messagequeues(chain_state) for queue_identifier, event_queue in events_queues.items(): self.start_health_check_for(queue_identifier.recipient) for event in event_queue: message = message_from_sendevent(event) self.sign(message) self.transport.send_async(queue_identifier, message) def _initialize_monitoring_services_queue(self, chain_state: ChainState): """Send the monitoring requests for all current balance proofs. Note: The node must always send the *received* balance proof to the monitoring service, *before* sending its own locked transfer forward. If the monitoring service is updated after, then the following can happen: For a transfer A-B-C where this node is B - B receives T1 from A and processes it - B forwards its T2 to C * B crashes (the monitoring service is not updated) For the above scenario, the monitoring service would not have the latest balance proof received by B from A available with the lock for T1, but C would. If the channel B-C is closed and B does not come back online in time, the funds for the lock L1 can be lost. During restarts the rationale from above has to be replicated. Because the initialization code *is not* the same as the event handler. This means the balance proof updates must be done prior to the processing of the message queues. """ msg = ( "Transport was started before the monitoring service queue was updated. " "This can lead to safety issue. node:{self!r}") assert not self.transport, msg msg = "The node state was not yet recovered, cant read balance proofs. node:{self!r}" assert self.wal, msg current_balance_proofs = views.detect_balance_proof_change( old_state=ChainState( pseudo_random_generator=chain_state.pseudo_random_generator, block_number=GENESIS_BLOCK_NUMBER, block_hash=constants.EMPTY_HASH, our_address=chain_state.our_address, chain_id=chain_state.chain_id, ), current_state=chain_state, ) for balance_proof in current_balance_proofs: update_services_from_balance_proof(self, chain_state, balance_proof) def _initialize_whitelists(self, chain_state: ChainState): """ Whitelist neighbors and mediated transfer targets on transport """ for neighbour in views.all_neighbour_nodes(chain_state): if neighbour == ConnectionManager.BOOTSTRAP_ADDR: continue self.transport.whitelist(neighbour) events_queues = views.get_all_messagequeues(chain_state) for event_queue in events_queues.values(): for event in event_queue: if isinstance(event, SendLockedTransfer): transfer = event.transfer if transfer.initiator == self.address: self.transport.whitelist(address=transfer.target) def sign(self, message: Message): """ Sign message inplace. """ if not isinstance(message, SignedMessage): raise ValueError("{} is not signable.".format(repr(message))) message.sign(self.signer) def install_all_blockchain_filters( self, token_network_registry_proxy: TokenNetworkRegistry, secret_registry_proxy: SecretRegistry, from_block: BlockNumber, ): with self.event_poll_lock: node_state = views.state_from_raiden(self) token_networks = views.get_token_network_identifiers( node_state, token_network_registry_proxy.address) self.blockchain_events.add_token_network_registry_listener( token_network_registry_proxy=token_network_registry_proxy, contract_manager=self.contract_manager, from_block=from_block, ) self.blockchain_events.add_secret_registry_listener( secret_registry_proxy=secret_registry_proxy, contract_manager=self.contract_manager, from_block=from_block, ) for token_network in token_networks: token_network_proxy = self.chain.token_network( TokenNetworkAddress(token_network)) self.blockchain_events.add_token_network_listener( token_network_proxy=token_network_proxy, contract_manager=self.contract_manager, from_block=from_block, ) def connection_manager_for_token_network( self, token_network_identifier: TokenNetworkID) -> ConnectionManager: if not is_binary_address(token_network_identifier): raise InvalidAddress("token address is not valid.") known_token_networks = views.get_token_network_identifiers( views.state_from_raiden(self), self.default_registry.address) if token_network_identifier not in known_token_networks: raise InvalidAddress("token is not registered.") manager = self.tokennetworkids_to_connectionmanagers.get( token_network_identifier) if manager is None: manager = ConnectionManager(self, token_network_identifier) self.tokennetworkids_to_connectionmanagers[ token_network_identifier] = manager return manager def mediated_transfer_async( self, token_network_identifier: TokenNetworkID, amount: PaymentAmount, target: TargetAddress, identifier: PaymentID, fee: FeeAmount = MEDIATION_FEE, secret: Secret = None, secrethash: SecretHash = None, ) -> PaymentStatus: """ Transfer `amount` between this node and `target`. This method will start an asynchronous transfer, the transfer might fail or succeed depending on a couple of factors: - Existence of a path that can be used, through the usage of direct or intermediary channels. - Network speed, making the transfer sufficiently fast so it doesn't expire. """ if secret is None: if secrethash is None: secret = random_secret() else: secret = EMPTY_SECRET payment_status = self.start_mediated_transfer_with_secret( token_network_identifier=token_network_identifier, amount=amount, fee=fee, target=target, identifier=identifier, secret=secret, secrethash=secrethash, ) return payment_status def start_mediated_transfer_with_secret( self, token_network_identifier: TokenNetworkID, amount: PaymentAmount, fee: FeeAmount, target: TargetAddress, identifier: PaymentID, secret: Secret, secrethash: SecretHash = None, ) -> PaymentStatus: if secrethash is None: secrethash = sha3(secret) elif secrethash != sha3(secret): raise InvalidSecretHash( "provided secret and secret_hash do not match.") if len(secret) != SECRET_LENGTH: raise InvalidSecret("secret of invalid length.") # We must check if the secret was registered against the latest block, # even if the block is forked away and the transaction that registers # the secret is removed from the blockchain. The rationale here is that # someone else does know the secret, regardless of the chain state, so # the node must not use it to start a payment. # # For this particular case, it's preferable to use `latest` instead of # having a specific block_hash, because it's preferable to know if the secret # was ever known, rather than having a consistent view of the blockchain. secret_registered = self.default_secret_registry.is_secret_registered( secrethash=secrethash, block_identifier="latest") if secret_registered: raise RaidenUnrecoverableError( f"Attempted to initiate a locked transfer with secrethash {pex(secrethash)}." f" That secret is already registered onchain.") self.start_health_check_for(Address(target)) if identifier is None: identifier = create_default_identifier() with self.payment_identifier_lock: payment_status = self.targets_to_identifiers_to_statuses[ target].get(identifier) if payment_status: payment_status_matches = payment_status.matches( token_network_identifier, amount) if not payment_status_matches: raise PaymentConflict( "Another payment with the same id is in flight") return payment_status payment_status = PaymentStatus( payment_identifier=identifier, amount=amount, token_network_identifier=token_network_identifier, payment_done=AsyncResult(), ) self.targets_to_identifiers_to_statuses[target][ identifier] = payment_status init_initiator_statechange = initiator_init( raiden=self, transfer_identifier=identifier, transfer_amount=amount, transfer_secret=secret, transfer_secrethash=secrethash, transfer_fee=fee, token_network_identifier=token_network_identifier, target_address=target, ) # Dispatch the state change even if there are no routes to create the # wal entry. self.handle_and_track_state_change(init_initiator_statechange) return payment_status def mediate_mediated_transfer(self, transfer: LockedTransfer): init_mediator_statechange = mediator_init(self, transfer) self.handle_and_track_state_change(init_mediator_statechange) def target_mediated_transfer(self, transfer: LockedTransfer): self.start_health_check_for(Address(transfer.initiator)) init_target_statechange = target_init(transfer) self.handle_and_track_state_change(init_target_statechange) def maybe_upgrade_db(self) -> None: manager = UpgradeManager(db_filename=self.database_path, raiden=self, web3=self.chain.client.web3) manager.run()
class JoinableQueue(Queue): """ A subclass of :class:`Queue` that additionally has :meth:`task_done` and :meth:`join` methods. """ def __init__(self, maxsize=None, items=None, unfinished_tasks=None): """ .. versionchanged:: 1.1a1 If *unfinished_tasks* is not given, then all the given *items* (if any) will be considered unfinished. """ from gevent.event import Event Queue.__init__(self, maxsize, items) self._cond = Event() self._cond.set() if unfinished_tasks: self.unfinished_tasks = unfinished_tasks elif items: self.unfinished_tasks = len(items) else: self.unfinished_tasks = 0 if self.unfinished_tasks: self._cond.clear() def copy(self): return type(self)(self.maxsize, self.queue, self.unfinished_tasks) def _format(self): result = Queue._format(self) if self.unfinished_tasks: result += ' tasks=%s _cond=%s' % (self.unfinished_tasks, self._cond) return result def _put(self, item): Queue._put(self, item) self.unfinished_tasks += 1 self._cond.clear() def task_done(self): '''Indicate that a formerly enqueued task is complete. Used by queue consumer threads. For each :meth:`get <Queue.get>` used to fetch a task, a subsequent call to :meth:`task_done` tells the queue that the processing on the task is complete. If a :meth:`join` is currently blocking, it will resume when all items have been processed (meaning that a :meth:`task_done` call was received for every item that had been :meth:`put <Queue.put>` into the queue). Raises a :exc:`ValueError` if called more times than there were items placed in the queue. ''' if self.unfinished_tasks <= 0: raise ValueError('task_done() called too many times') self.unfinished_tasks -= 1 if self.unfinished_tasks == 0: self._cond.set() def join(self, timeout=None): ''' Block until all items in the queue have been gotten and processed. The count of unfinished tasks goes up whenever an item is added to the queue. The count goes down whenever a consumer thread calls :meth:`task_done` to indicate that the item was retrieved and all work on it is complete. When the count of unfinished tasks drops to zero, :meth:`join` unblocks. :param float timeout: If not ``None``, then wait no more than this time in seconds for all tasks to finish. :return: ``True`` if all tasks have finished; if ``timeout`` was given and expired before all tasks finished, ``False``. .. versionchanged:: 1.1a1 Add the *timeout* parameter. ''' return self._cond.wait(timeout=timeout)
class HighAvailabilityAgent(SimpleResourceAgent): """Agent to manage high availability processes """ def __init__(self): SimpleResourceAgent.__init__(self) self.dashi_handler = None self.service_id = None self.policy_thread = None self.policy_event = None self._policy_loop_event = Event() def on_init(self): if not HighAvailabilityCore: msg = "HighAvailabilityCore isn't available. Use autolaunch.cfg buildout" log.error(msg) return cfg = self.CFG.get_safe("highavailability") # use default PD name as the sole PD if none are provided in config self.pds = self.CFG.get_safe("highavailability.process_dispatchers", [ProcessDispatcherService.name]) if not len(self.pds) == 1: raise Exception( "HA Service doesn't support multiple Process Dispatchers") self.process_definition_id, self.process_definition = self._get_process_definition( ) self.process_configuration = self.CFG.get_safe( "highavailability.process_configuration") aggregator_config = _get_aggregator_config(self.CFG) self.service_id, self.service_name = self._register_service() self.policy_event = Event() stored_policy = self._stored_policy if stored_policy != {}: policy_name = stored_policy.get('name') policy_parameters = stored_policy.get('parameters') self._validate_policy_name(policy_name) self.policy_name = policy_name.lower() self.policy_parameters = policy_parameters else: policy_name = self.CFG.get_safe("highavailability.policy.name") self._validate_policy_name(policy_name) self.policy_name = policy_name.lower() self.policy_parameters = self.CFG.get_safe( "highavailability.policy.parameters") self.policy_interval = self.CFG.get_safe( "highavailability.policy.interval", DEFAULT_INTERVAL) self.logprefix = "HA Agent (%s): " % self.service_name self.control = HAProcessControl(self.pds[0], self.container.resource_registry, self.service_id, self.policy_event.set, logprefix=self.logprefix) self.core = HighAvailabilityCore( cfg, self.control, self.pds, self.policy_name, process_definition_id=self.process_definition_id, parameters=self.policy_parameters, process_configuration=self.process_configuration, aggregator_config=aggregator_config, name=self.service_name) dashi_messaging = self.CFG.get_safe("highavailability.dashi_messaging", False) if dashi_messaging: dashi_name = self.CFG.get_safe("highavailability.dashi_name") if not dashi_name: raise Exception("dashi_name unknown") dashi_uri = self.CFG.get_safe("highavailability.dashi_uri") if not dashi_uri: rabbit_host = self.CFG.get_safe("server.amqp.host") rabbit_user = self.CFG.get_safe("server.amqp.username") rabbit_pass = self.CFG.get_safe("server.amqp.password") if not (rabbit_host and rabbit_user and rabbit_pass): raise Exception("cannot form dashi URI") dashi_uri = "amqp://%s:%s@%s/" % (rabbit_user, rabbit_pass, rabbit_host) dashi_exchange = self.CFG.get_safe( "highavailability.dashi_exchange") if not dashi_exchange: dashi_exchange = get_sys_name() self.dashi_handler = HADashiHandler(self, dashi_name, dashi_uri, dashi_exchange) else: self.dashi_handler = None def _get_process_definition(self): process_definition_id = self.CFG.get_safe( "highavailability.process_definition_id") process_definition_name = self.CFG.get_safe( "highavailability.process_definition_name") if process_definition_id: pd_name = self.pds[0] pd = ProcessDispatcherServiceClient(to_name=pd_name) definition = pd.read_process_definition(process_definition_id) elif process_definition_name: definitions, _ = self.container.resource_registry.find_resources( restype="ProcessDefinition", name=process_definition_name) if len(definitions) == 0: raise Exception("Process definition with name '%s' not found" % process_definition_name) elif len(definitions) > 1: raise Exception( "multiple process definitions found with name '%s'" % process_definition_name) definition = definitions[0] process_definition_id = definition._id else: raise Exception( "HA Agent requires either process definition ID or name") return process_definition_id, definition def on_start(self): if self.dashi_handler: self.dashi_handler.start() self.control.start() # override the core's list of currently managed processes. This is to support # restart of an HAAgent. self.core.set_managed_upids(self.control.get_managed_upids()) self.policy_thread = gevent.spawn(self._policy_thread_loop) # kickstart the policy once. future invocations will happen via event callbacks. self.policy_event.set() def on_quit(self): self.control.stop() self._policy_loop_event.set() self.policy_thread.join() self.policy_thread.kill(block=True, timeout=3) if self.dashi_handler: self.dashi_handler.stop() # DL: do we ever want to remove this object? #self._unregister_service() def _register_service(self): definition = self.process_definition existing_services, _ = self.container.resource_registry.find_resources( restype="Service", name=definition.name) if len(existing_services) > 0: if len(existing_services) > 1: log.warning( "There is more than one service object for %s. Using the first one" % definition.name) service_id = existing_services[0]._id else: svc_obj = Service(name=definition.name, exchange_name=definition.name) service_id, _ = self.container.resource_registry.create(svc_obj) svcdefs, _ = self.container.resource_registry.find_resources( restype="ServiceDefinition", name=definition.name) if svcdefs: try: self.container.resource_registry.create_association( service_id, "hasServiceDefinition", svcdefs[0]._id) except BadRequest: log.warn( "Failed to associate %s Service and ServiceDefinition. It probably exists.", definition.name) else: log.error("Cannot find ServiceDefinition resource for %s", definition.name) return service_id, definition.name def _unregister_service(self): if not self.service_id: log.error("No service id. Cannot unregister service") return self.container.resource_registry.delete(self.service_id, del_associations=True) def _policy_thread_loop(self): """Single thread runs policy loops, to prevent races """ while not self._policy_loop_event.wait(timeout=0.1): # wait until our event is set, up to policy_interval seconds self.policy_event.wait(self.policy_interval) if self.policy_event.is_set(): self.policy_event.clear() log.debug("%sapplying policy due to event", self.logprefix) else: # on a regular basis, we check for the current state of each process. # this is essentially a hedge against bugs in the HAAgent, or in the # ION events system that could prevent us from seeing state changes # of processes. log.debug( "%sapplying policy due to timer. Reloading process cache first.", self.logprefix) try: self.control.reload_processes() except (Exception, gevent.Timeout): log.warn( "%sFailed to reload processes from PD. Will retry later.", self.logprefix, exc_info=True) try: self._apply_policy() except (Exception, gevent.Timeout): log.warn("%sFailed to apply policy. Will retry later.", self.logprefix, exc_info=True) def _validate_policy_name(self, policy_name): if policy_name is None: msg = "HA service requires a policy name at CFG.highavailability.policy.name" raise Exception(msg) try: policy.policy_map[policy_name.lower()] except KeyError: raise Exception("HA Service doesn't support '%s' policy" % policy_name) @property def _policy_dict(self): policy_dict = { 'name': self.core.policy_type, 'parameters': self.core.policy.parameters } return policy_dict @property def _stored_policy(self): service = self.container.resource_registry.read(self.service_id) return service.policy def _apply_policy(self): self.core.apply_policy() try: new_service_state = _core_hastate_to_service_state( self.core.status()) new_policy = self._policy_dict service = self.container.resource_registry.read(self.service_id) update_service = False if service.state != new_service_state: service.state = new_service_state update_service = True if service.policy != new_policy: service.policy = new_policy update_service = True if update_service is True: self.container.resource_registry.update(service) except Exception: log.warn("%sProblem when updating Service state", self.logprefix, exc_info=True) def rcmd_reconfigure_policy(self, new_policy_params, new_policy_name=None): """Service operation: Change the parameters of the policy used for service @param new_policy_params: parameters of policy @param new_policy_name: name of policy @return: """ self.core.reconfigure_policy(new_policy_params, new_policy_name) #trigger policy thread to wake up self.policy_event.set() def rcmd_status(self): """Service operation: Get the status of the HA Service @return: {PENDING, READY, STEADY, BROKEN} """ return self.core.status() def rcmd_dump(self): dump = self.core.dump() dump['service_id'] = self.service_id return dump
class TangoChannel(ChannelObject): _tangoEventsQueue = Queue() _eventReceivers = {} _tangoEventsProcessingTimer = gevent.get_hub().loop.async_() # start Tango events processing timer _tangoEventsProcessingTimer.start(processTangoEvents) def __init__(self, name, attribute_name, tangoname=None, username=None, polling=None, timeout=10000, **kwargs): ChannelObject.__init__(self, name, username, **kwargs) self.attributeName = attribute_name self.deviceName = tangoname self.device = None self.value = Poller.NotInitializedValue self.polling = polling self.pollingTimer = None self.pollingEvents = False self.timeout = int(timeout) self.read_as_str = kwargs.get("read_as_str", False) self._device_initialized = Event() logging.getLogger("HWR").debug( "creating Tango attribute %s/%s, polling=%s, timeout=%d", self.deviceName, self.attributeName, polling, self.timeout, ) self.init_device() self.continue_init(None) """ self.init_poller = Poller.poll(self.init_device, polling_period = 3000, value_changed_callback = self.continue_init, error_callback = self.init_poll_failed, start_delay=100) """ def init_poll_failed(self, e, poller_id): self._device_initialized.clear() logging.warning( "%s/%s (%s): could not complete init. (hint: device server is not running, or has to be restarted)", self.deviceName, self.attributeName, self.name(), ) self.init_poller = self.init_poller.restart(3000) def continue_init(self, _): # self.init_poller.stop() if isinstance(self.polling, int): self.raw_device = RawDeviceProxy(self.deviceName) Poller.poll( self.poll, polling_period=self.polling, value_changed_callback=self.update, error_callback=self.pollFailed, ) else: if self.polling == "events": # try to register event try: self.pollingEvents = True # logging.getLogger("HWR").debug("subscribing to CHANGE event for %s", self.attributeName) self.device.subscribe_event( self.attributeName, PyTango.EventType.CHANGE_EVENT, self, [], True, ) # except PyTango.EventSystemFailed: # pass except Exception: logging.getLogger("HWR").exception( "could not subscribe event") self._device_initialized.set() def init_device(self): try: self.device = DeviceProxy(self.deviceName) except PyTango.DevFailed as traceback: self.imported = False last_error = traceback[-1] logging.getLogger("HWR").error("%s: %s", str(self.name()), last_error["desc"]) else: self.imported = True try: self.device.ping() except PyTango.ConnectionFailed: self.device = None raise ConnectionError else: self.device.set_timeout_millis(self.timeout) # check that the attribute exists (to avoid Abort in PyTango grrr) if not self.attributeName.lower() in [ attr.name.lower() for attr in self.device.attribute_list_query() ]: logging.getLogger("HWR").error( "no attribute %s in Tango device %s", self.attributeName, self.deviceName, ) self.device = None def push_event(self, event): # logging.getLogger("HWR").debug("%s | attr_value=%s, event.errors=%s, quality=%s", self.name(), event.attr_value, event.errors,event.attr_value is None and "N/A" or event.attr_value.quality) if (event.attr_value is None or event.err or event.attr_value.quality != PyTango.AttrQuality.ATTR_VALID): # logging.getLogger("HWR").debug("%s, receving BAD event... attr_value=%s, event.errors=%s, quality=%s", self.name(), event.attr_value, event.errors, event.attr_value is None and "N/A" or event.attr_value.quality) return else: pass # logging.getLogger("HWR").debug("%s, receiving good event", self.name()) ev = E(event) TangoChannel._eventReceivers[id(ev)] = saferef.safe_ref(self.update) TangoChannel._tangoEventsQueue.put(ev) TangoChannel._tangoEventsProcessingTimer.send() def poll(self): if self.read_as_str: value = self.raw_device.read_attribute( self.attributeName, PyTango.DeviceAttribute.ExtractAs.String).value # value = self.device.read_attribute_as_str(self.attributeName).value else: value = self.raw_device.read_attribute(self.attributeName).value self.emit("update", value) return value def pollFailed(self, e, poller_id): self.emit("update", None) """ emit_update = True if self.value is None: emit_update = False else: self.value = None try: self.init_device() except: pass poller = Poller.get_poller(poller_id) if poller is not None: poller.restart(1000) try: raise e except: logging.exception("%s: Exception happened while polling %s", self.name(), self.attributeName) if emit_update: # emit at the end => can raise exceptions in callbacks self.emit('update', None) """ def getInfo(self): self._device_initialized.wait(timeout=3) return self.device.get_attribute_config(self.attributeName) def update(self, value=Poller.NotInitializedValue): if value == Poller.NotInitializedValue: value = self.getValue() if isinstance(value, types.TupleType): value = list(value) self.value = value self.emit("update", value) def getValue(self): self._device_initialized.wait(timeout=3) if self.read_as_str: value = self.device.read_attribute( self.attributeName, PyTango.DeviceAttribute.ExtractAs.String).value else: value = self.device.read_attribute(self.attributeName).value return value def setValue(self, newValue): self.device.write_attribute(self.attributeName, newValue) # attr = PyTango.AttributeProxy(self.deviceName + "/" + self.attributeName) # a = attr.read() # a.value = newValue # attr.write(a) def isConnected(self): return self.device is not None
class HTTPServer(object): spawn = Greenlet.spawn # set to None to avoid spawning at all backlog = 5 def __init__(self, handle=None, spawn='default'): self.listeners = [] self._stopped_event = Event() self._no_connections_event = Event() self._requests = {} # maps connection -> list of requests self.http = core.http() self.http.set_gencb(self._cb_request) if handle is not None: self.handle = handle if spawn != 'default': self.spawn = spawn def start(self, socket_or_address, backlog=None): """Start accepting connections""" fileno = getattr(socket_or_address, 'fileno', None) if fileno is not None: fd = fileno() sock = socket_or_address else: sock = self.make_listener(socket_or_address, backlog=backlog) fd = sock.fileno() self.http.accept(fd) self.listeners.append(sock) self._stopped_event.clear() if self._requests: self._no_connections_event.clear() else: self._no_connections_event.set() return sock def make_listener(self, address, backlog=None): if backlog is None: backlog = self.backlog sock = socket.socket() try: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR) | 1) except socket.error: pass sock.bind(address) sock.listen(backlog) sock.setblocking(False) return sock def stop(self, timeout=0): """Shutdown the server.""" for sock in self.listeners: sock.close() self.socket = [] #2. Set "keep-alive" connections to "close" # TODO #3a. set low timeout (min(1s, timeout or 1)) on events belonging to connection (to kill long-polling connections # TODO #3. Wait until every connection is closed or timeout expires if self._requests: timer = Timeout.start_new(timeout) try: try: self._no_connections_event.wait(timeout=timeout) except Timeout, ex: if timer is not ex: raise finally: timer.cancel() #4. forcefull close all the connections # TODO #5. free http instance self.http = None #6. notify event created in serve_forever() self._stopped_event.set() def handle(self, request): request.send_reply(200, 'OK', 'It works!') def _cb_connection_close(self, connection): # make sure requests belonging to this connection cannot be accessed anymore # because they've been freed by libevent requests = self._requests.pop(connection._obj, []) for request in requests: request.detach() if not self._requests: self._no_connections_event.set() def _cb_request_processed(self, greenlet): request = greenlet._request greenlet._request = None if request: if not greenlet.successful(): self.reply_error(request) requests = self._requests.get(request.connection._obj) if requests is not None: requests.remove(request) def _cb_request(self, request): try: spawn = self.spawn request.connection.set_closecb(self) self._requests.setdefault(request.connection._obj, []).append(request) if spawn is None: self.handle(request) else: greenlet = spawn(wrap_errors(core.HttpRequestDeleted, self.handle), request) rawlink = getattr(greenlet, 'rawlink', None) if rawlink is not None: greenlet._request = request rawlink(self._cb_request_processed) except: traceback.print_exc() try: sys.stderr.write('Failed to handle request: %s\n\n' % (request, )) except: pass self.reply_error(request) def reply_error(self, request): try: if request.response == (0, None): request.send_reply(500, 'Internal Server Error', '<h1>Internal Server Error</h1>') except core.HttpRequestDeleted: pass def serve_forever(self, *args, **kwargs): stop_timeout = kwargs.pop('stop_timeout', 0) self.start(*args, **kwargs) try: self._stopped_event.wait() finally: Greenlet.spawn(self.stop, timeout=stop_timeout).join()
class Socket(object): """ Virtual Socket implementation, checks heartbeats, writes to local queues for message passing, holds the Namespace objects, dispatches de packets to the underlying namespaces. This is the abstraction on top of the different transports. It's like if you used a WebSocket only... """ STATE_CONNECTING = "CONNECTING" STATE_CONNECTED = "CONNECTED" STATE_DISCONNECTING = "DISCONNECTING" STATE_DISCONNECTED = "DISCONNECTED" GLOBAL_NS = '' """Use this to be explicit when specifying a Global Namespace (an endpoint with no name, not '/chat' or anything.""" json_loads = staticmethod(default_json_loads) json_dumps = staticmethod(default_json_dumps) def __init__(self, server, config, error_handler=None): self.server = weakref.proxy(server) self.sessid = str(random.random())[2:] self.session = {} # the session dict, for general developer usage self.client_queue = Queue() # queue for messages to client self.server_queue = Queue() # queue for messages to server self.hits = 0 self.heartbeats = 0 self.timeout = Event() self.wsgi_app_greenlet = None self.state = "NEW" self.connection_established = False self.ack_callbacks = {} self.ack_counter = 0 self.request = None self.environ = None self.namespaces = {} self.active_ns = {} # Namespace sessions that were instantiated self.jobs = [] self.error_handler = default_error_handler self.config = config if error_handler is not None: self.error_handler = error_handler def _set_namespaces(self, namespaces): """This is a mapping (dict) of the different '/namespaces' to their BaseNamespace object derivative. This is called by socketio_manage().""" self.namespaces = namespaces def _set_request(self, request): """Saves the request object for future use by the different Namespaces. This is called by socketio_manage(). """ self.request = request def _set_environ(self, environ): """Save the WSGI environ, for future use. This is called by socketio_manage(). """ self.environ = environ def _set_error_handler(self, error_handler): """Changes the default error_handler function to the one specified This is called by socketio_manage(). """ self.error_handler = error_handler def _set_json_loads(self, json_loads): """Change the default JSON decoder. This should be a callable that accepts a single string, and returns a well-formed object. """ self.json_loads = json_loads def _set_json_dumps(self, json_dumps): """Change the default JSON decoder. This should be a callable that accepts a single string, and returns a well-formed object. """ self.json_dumps = json_dumps def _get_next_msgid(self): """This retrieves the next value for the 'id' field when sending an 'event' or 'message' or 'json' that asks the remote client to 'ack' back, so that we trigger the local callback. """ self.ack_counter += 1 return self.ack_counter def _save_ack_callback(self, msgid, callback): """Keep a reference of the callback on this socket.""" if msgid in self.ack_callbacks: return False self.ack_callbacks[msgid] = callback def _pop_ack_callback(self, msgid): """Fetch the callback for a given msgid, if it exists, otherwise, return None""" if msgid not in self.ack_callbacks: return None return self.ack_callbacks.pop(msgid) def __str__(self): result = ['sessid=%r' % self.sessid] if self.state == self.STATE_CONNECTED: result.append('connected') if self.client_queue.qsize(): result.append('client_queue[%s]' % self.client_queue.qsize()) if self.server_queue.qsize(): result.append('server_queue[%s]' % self.server_queue.qsize()) if self.hits: result.append('hits=%s' % self.hits) if self.heartbeats: result.append('heartbeats=%s' % self.heartbeats) return ' '.join(result) def __getitem__(self, key): """This will get the nested Namespace using its '/chat' reference. Using this, you can go from one Namespace to the other (to emit, add ACLs, etc..) with: adminnamespace.socket['/chat'].add_acl_method('kick-ban') """ return self.active_ns[key] def __hasitem__(self, key): """Verifies if the namespace is active (was initialized)""" return key in self.active_ns @property def connected(self): """Returns whether the state is CONNECTED or not.""" return self.state == self.STATE_CONNECTED def incr_hits(self): self.hits += 1 def heartbeat(self): """This makes the heart beat for another X seconds. Call this when you get a heartbeat packet in. This clear the heartbeat disconnect timeout (resets for X seconds). """ self.timeout.set() def kill(self, detach=False): """This function must/will be called when a socket is to be completely shut down, closed by connection timeout, connection error or explicit disconnection from the client. It will call all of the Namespace's :meth:`~socketio.namespace.BaseNamespace.disconnect` methods so that you can shut-down things properly. """ # Clear out the callbacks self.ack_callbacks = {} if self.connected: self.state = self.STATE_DISCONNECTING self.server_queue.put_nowait(None) self.client_queue.put_nowait(None) if len(self.active_ns) > 0: log.debug("Calling disconnect() on %s" % self) self.disconnect() if detach: self.detach() gevent.killall(self.jobs) def detach(self): """Detach this socket from the server. This should be done in conjunction with kill(), once all the jobs are dead, detach the socket for garbage collection.""" log.debug("Removing %s from server sockets" % self) if self.sessid in self.server.sockets: self.server.sockets.pop(self.sessid) def put_server_msg(self, msg): """Writes to the server's pipe, to end up in in the Namespaces""" self.heartbeat() self.server_queue.put_nowait(msg) def put_client_msg(self, msg): """Writes to the client's pipe, to end up in the browser""" self.client_queue.put_nowait(msg) def get_client_msg(self, **kwargs): """Grab a message to send it to the browser""" return self.client_queue.get(**kwargs) def get_server_msg(self, **kwargs): """Grab a message, to process it by the server and dispatch calls """ return self.server_queue.get(**kwargs) def get_multiple_client_msgs(self, **kwargs): """Get multiple messages, in case we're going through the various XHR-polling methods, on which we can pack more than one message if the rate is high, and encode the payload for the HTTP channel.""" client_queue = self.client_queue msgs = [client_queue.get(**kwargs)] while client_queue.qsize(): msgs.append(client_queue.get()) return msgs def error(self, error_name, error_message, endpoint=None, msg_id=None, quiet=False): """Send an error to the user, using the custom or default ErrorHandler configured on the [TODO: Revise this] Socket/Handler object. :param error_name: is a simple string, for easy association on the client side :param error_message: is a human readable message, the user will eventually see :param endpoint: set this if you have a message specific to an end point :param msg_id: set this if your error is relative to a specific message :param quiet: way to make the error handler quiet. Specific to the handler. The default handler will only log, with quiet. """ handler = self.error_handler return handler( self, error_name, error_message, endpoint, msg_id, quiet) # User facing low-level function def disconnect(self, silent=False): """Calling this method will call the :meth:`~socketio.namespace.BaseNamespace.disconnect` method on all the active Namespaces that were open, killing all their jobs and sending 'disconnect' packets for each of them. Normally, the Global namespace (endpoint = '') has special meaning, as it represents the whole connection, :param silent: when True, pass on the ``silent`` flag to the Namespace :meth:`~socketio.namespace.BaseNamespace.disconnect` calls. """ for ns_name, ns in list(self.active_ns.items()): ns.recv_disconnect() def remove_namespace(self, namespace): """This removes a Namespace object from the socket. This is usually called by :meth:`~socketio.namespace.BaseNamespace.disconnect`. """ if namespace in self.active_ns: del self.active_ns[namespace] if len(self.active_ns) == 0 and self.connected: self.kill(detach=True) def send_packet(self, pkt): """Low-level interface to queue a packet on the wire (encoded as wire protocol""" self.put_client_msg(packet.encode(pkt, self.json_dumps)) def spawn(self, fn, *args, **kwargs): """Spawn a new Greenlet, attached to this Socket instance. It will be monitored by the "watcher" method """ log.debug("Spawning sub-Socket Greenlet: %s" % fn.__name__) job = gevent.spawn(fn, *args, **kwargs) self.jobs.append(job) return job def _receiver_loop(self): """This is the loop that takes messages from the queue for the server to consume, decodes them and dispatches them. It is the main loop for a socket. We join on this process before returning control to the web framework. This process is not tracked by the socket itself, it is not going to be killed by the ``gevent.killall(socket.jobs)``, so it must exit gracefully itself. """ while True: rawdata = self.get_server_msg() if not rawdata: continue # or close the connection ? try: pkt = packet.decode(rawdata, self.json_loads) except (ValueError, KeyError, Exception) as e: self.error('invalid_packet', "There was a decoding error when dealing with packet " "with event: %s... (%s)" % (rawdata[:20], e)) continue if pkt['type'] == 'heartbeat': # This is already dealth with in put_server_msg() when # any incoming raw data arrives. continue if pkt['type'] == 'disconnect' and pkt['endpoint'] == '': # On global namespace, we kill everything. self.kill(detach=True) continue endpoint = pkt['endpoint'] if endpoint not in self.namespaces: self.error("no_such_namespace", "The endpoint you tried to connect to " "doesn't exist: %s" % endpoint, endpoint=endpoint) continue elif endpoint in self.active_ns: pkt_ns = self.active_ns[endpoint] else: new_ns_class = self.namespaces[endpoint] pkt_ns = new_ns_class(self.environ, endpoint, request=self.request) # This calls initialize() on all the classes and mixins, etc.. # in the order of the MRO for cls in type(pkt_ns).__mro__: if hasattr(cls, 'initialize'): cls.initialize(pkt_ns) # use this instead of __init__, # for less confusion self.active_ns[endpoint] = pkt_ns retval = pkt_ns.process_packet(pkt) # Has the client requested an 'ack' with the reply parameters ? if pkt.get('ack') == "data" and pkt.get('id'): if type(retval) is tuple: args = list(retval) else: args = [retval] returning_ack = dict(type='ack', ackId=pkt['id'], args=args, endpoint=pkt.get('endpoint', '')) self.send_packet(returning_ack) # Now, are we still connected ? if not self.connected: self.kill(detach=True) # ?? what,s the best clean-up # when its not a # user-initiated disconnect return def _spawn_receiver_loop(self): """Spawns the reader loop. This is called internall by socketio_manage(). """ job = gevent.spawn(self._receiver_loop) self.jobs.append(job) return job def _watcher(self): """Watch out if we've been disconnected, in that case, kill all the jobs. """ while True: gevent.sleep(1.0) if not self.connected: for ns_name, ns in list(self.active_ns.items()): ns.recv_disconnect() # Killing Socket-level jobs gevent.killall(self.jobs) break def _spawn_watcher(self): """This one is not waited for with joinall(socket.jobs), as it is an external watcher, to clean up when everything is done.""" job = gevent.spawn(self._watcher) return job def _heartbeat(self): """Start the heartbeat Greenlet to check connection health.""" interval = self.config['heartbeat_interval'] while self.connected: gevent.sleep(interval) # TODO: this process could use a timeout object like the disconnect # timeout thing, and ONLY send packets when none are sent! # We would do that by calling timeout.set() for a "sending" # timeout. If we're sending 100 messages a second, there is # no need to push some heartbeats in there also. self.put_client_msg("2::") def _heartbeat_timeout(self): timeout = float(self.config['heartbeat_timeout']) while True: self.timeout.clear() gevent.sleep(0) wait_res = self.timeout.wait(timeout=timeout) if not wait_res: if self.connected: log.debug("heartbeat timed out, killing socket") self.kill(detach=True) return def _spawn_heartbeat(self): """This functions returns a list of jobs""" self.spawn(self._heartbeat) self.spawn(self._heartbeat_timeout)
class RaidenService: """ A Raiden node. """ def __init__( self, chain: BlockChainService, query_start_block: typing.BlockNumber, default_registry: TokenNetworkRegistry, default_secret_registry: SecretRegistry, private_key_bin, transport, config, discovery=None, ): if not isinstance(private_key_bin, bytes) or len(private_key_bin) != 32: raise ValueError('invalid private_key') self.tokennetworkids_to_connectionmanagers = dict() self.identifier_to_results = defaultdict(list) self.chain: BlockChainService = chain self.default_registry = default_registry self.query_start_block = query_start_block self.default_secret_registry = default_secret_registry self.config = config self.privkey = private_key_bin self.address = privatekey_to_address(private_key_bin) self.discovery = discovery self.private_key = PrivateKey(private_key_bin) self.pubkey = self.private_key.public_key.format(compressed=False) self.transport = transport self.blockchain_events = BlockchainEvents() self.alarm = AlarmTask(chain) self.shutdown_timeout = config['shutdown_timeout'] self.stop_event = Event() self.start_event = Event() self.chain.client.inject_stop_event(self.stop_event) self.wal = None self.database_path = config['database_path'] if self.database_path != ':memory:': database_dir = os.path.dirname(config['database_path']) os.makedirs(database_dir, exist_ok=True) self.database_dir = database_dir # Prevent concurrent access to the same db self.lock_file = os.path.join(self.database_dir, '.lock') self.db_lock = filelock.FileLock(self.lock_file) else: self.database_path = ':memory:' self.database_dir = None self.lock_file = None self.serialization_file = None self.db_lock = None self.event_poll_lock = gevent.lock.Semaphore() def start_async(self) -> Event: """ Start the node asynchronously. """ self.start_event.clear() self.stop_event.clear() if self.database_dir is not None: self.db_lock.acquire(timeout=0) assert self.db_lock.is_locked # start the registration early to speed up the start if self.config['transport_type'] == 'udp': endpoint_registration_greenlet = gevent.spawn( self.discovery.register, self.address, self.config['external_ip'], self.config['external_port'], ) # The database may be :memory: storage = sqlite.SQLiteStorage(self.database_path, serialize.PickleSerializer()) self.wal, unapplied_events = wal.restore_from_latest_snapshot( node.state_transition, storage, ) if self.wal.state_manager.current_state is None: block_number = self.chain.block_number() state_change = ActionInitChain( random.Random(), block_number, self.chain.network_id, ) self.wal.log_and_dispatch(state_change, block_number) payment_network = PaymentNetworkState( self.default_registry.address, [], # empty list of token network states as it's the node's startup ) state_change = ContractReceiveNewPaymentNetwork(payment_network) self.handle_state_change(state_change) # On first run Raiden needs to fetch all events for the payment # network, to reconstruct all token network graphs and find opened # channels last_log_block_number = 0 else: # The `Block` state change is dispatched only after all the events # for that given block have been processed, filters can be safely # installed starting from this position without losing events. last_log_block_number = views.block_number(self.wal.state_manager.current_state) # Install the filters using the correct from_block value, otherwise # blockchain logs can be lost. self.install_all_blockchain_filters( self.default_registry, self.default_secret_registry, last_log_block_number, ) # Complete the first_run of the alarm task and synchronize with the # blockchain since the last run. # # Notes about setup order: # - The filters must be polled after the node state has been primed, # otherwise the state changes won't have effect. # - The alarm must complete its first run before the transport is started, # to avoid rejecting messages for unknown channels. self.alarm.register_callback(self._callback_new_block) self.alarm.first_run() self.alarm.start() queueids_to_queues = views.get_all_messagequeues(views.state_from_raiden(self)) self.transport.start(self, queueids_to_queues) # Health check needs the transport layer self.start_neighbours_healthcheck() for event in unapplied_events: on_raiden_event(self, event) if self.config['transport_type'] == 'udp': def set_start_on_registration(_): self.start_event.set() endpoint_registration_greenlet.link(set_start_on_registration) else: self.start_event.set() return self.start_event def start(self) -> Event: """ Start the node. """ self.start_async().wait() def start_neighbours_healthcheck(self): for neighbour in views.all_neighbour_nodes(self.wal.state_manager.current_state): if neighbour != ConnectionManager.BOOTSTRAP_ADDR: self.start_health_check_for(neighbour) def stop(self): """ Stop the node. """ # Needs to come before any greenlets joining self.stop_event.set() self.transport.stop_and_wait() self.alarm.stop_async() wait_for = [self.alarm] wait_for.extend(getattr(self.transport, 'greenlets', [])) # We need a timeout to prevent an endless loop from trying to # contact the disconnected client gevent.wait(wait_for, timeout=self.shutdown_timeout) # Filters must be uninstalled after the alarm task has stopped. Since # the events are polled by an alarm task callback, if the filters are # uninstalled before the alarm task is fully stopped the callback # `poll_blockchain_events` will fail. # # We need a timeout to prevent an endless loop from trying to # contact the disconnected client try: with gevent.Timeout(self.shutdown_timeout): self.blockchain_events.uninstall_all_event_listeners() except (gevent.timeout.Timeout, RaidenShuttingDown): pass self.blockchain_events.reset() if self.db_lock is not None: self.db_lock.release() def __repr__(self): return '<{} {}>'.format(self.__class__.__name__, pex(self.address)) def get_block_number(self): return views.block_number(self.wal.state_manager.current_state) def handle_state_change(self, state_change, block_number=None): log.debug('STATE CHANGE', node=pex(self.address), state_change=state_change) if block_number is None: block_number = self.get_block_number() event_list = self.wal.log_and_dispatch(state_change, block_number) for event in event_list: log.debug('RAIDEN EVENT', node=pex(self.address), raiden_event=event) on_raiden_event(self, event) return event_list def set_node_network_state(self, node_address, network_state): state_change = ActionChangeNodeNetworkState(node_address, network_state) self.wal.log_and_dispatch(state_change, self.get_block_number()) def start_health_check_for(self, node_address): self.transport.start_health_check(node_address) def _callback_new_block(self, current_block_number, chain_id): """Called once a new block is detected by the alarm task. Note: This should be called only once per block, otherwise there will be duplicated `Block` state changes in the log. Therefore this method should be called only once a new block is mined with the appropriate block_number argument from the AlarmTask. """ # Raiden relies on blockchain events to update its off-chain state, # therefore some APIs /used/ to forcefully poll for events. # # This was done for APIs which have on-chain side-effects, e.g. # openning a channel, where polling the event is required to update # off-chain state to providing a consistent view to the caller, e.g. # the channel exists after the API call returns. # # That pattern introduced a race, because the events are returned only # once per filter, and this method would be called concurrently by the # API and the AlarmTask. The following lock is necessary, to ensure the # expected side-effects are properly applied (introduced by the commit # 3686b3275ff7c0b669a6d5e2b34109c3bdf1921d) with self.event_poll_lock: for event in self.blockchain_events.poll_blockchain_events(current_block_number): # These state changes will be procesed with a block_number # which is /larger/ than the ChainState's block_number. on_blockchain_event(self, event, current_block_number, chain_id) # On restart the Raiden node will re-create the filters with the # ethereum node. These filters will have the from_block set to the # value of the latest Block state change. To avoid missing events # the Block state change is dispatched only after all of the events # have been processed. # # This means on some corner cases a few events may be applied # twice, this will happen if the node crashed and some events have # been processed but the Block state change has not been # dispatched. state_change = Block(current_block_number) self.handle_state_change(state_change, current_block_number) def sign(self, message): """ Sign message inplace. """ if not isinstance(message, SignedMessage): raise ValueError('{} is not signable.'.format(repr(message))) message.sign(self.private_key) def install_all_blockchain_filters( self, token_network_registry_proxy, secret_registry_proxy, from_block, ): with self.event_poll_lock: node_state = views.state_from_raiden(self) channels = views.list_all_channelstate(node_state) token_networks = views.get_token_network_identifiers( node_state, token_network_registry_proxy.address, ) self.blockchain_events.add_token_network_registry_listener( token_network_registry_proxy, from_block, ) self.blockchain_events.add_secret_registry_listener( secret_registry_proxy, from_block, ) for token_network in token_networks: token_network_proxy = self.chain.token_network(token_network) self.blockchain_events.add_token_network_listener( token_network_proxy, from_block, ) for channel_state in channels: channel_proxy = self.chain.payment_channel( channel_state.token_network_identifier, channel_state.identifier, ) self.blockchain_events.add_payment_channel_listener( channel_proxy, from_block, ) def connection_manager_for_token_network(self, token_network_identifier): if not is_binary_address(token_network_identifier): raise InvalidAddress('token address is not valid.') known_token_networks = views.get_token_network_identifiers( views.state_from_raiden(self), self.default_registry.address, ) if token_network_identifier not in known_token_networks: raise InvalidAddress('token is not registered.') manager = self.tokennetworkids_to_connectionmanagers.get(token_network_identifier) if manager is None: manager = ConnectionManager(self, token_network_identifier) self.tokennetworkids_to_connectionmanagers[token_network_identifier] = manager return manager def leave_all_token_networks(self): state_change = ActionLeaveAllNetworks() self.wal.log_and_dispatch(state_change, self.get_block_number()) def close_and_settle(self): log.info('raiden will close and settle all channels now') self.leave_all_token_networks() connection_managers = [cm for cm in self.tokennetworkids_to_connectionmanagers.values()] if connection_managers: waiting.wait_for_settle_all_channels( self, self.alarm.sleep_time, ) def mediated_transfer_async( self, token_network_identifier, amount, target, identifier, ): """ Transfer `amount` between this node and `target`. This method will start an asyncronous transfer, the transfer might fail or succeed depending on a couple of factors: - Existence of a path that can be used, through the usage of direct or intermediary channels. - Network speed, making the transfer sufficiently fast so it doesn't expire. """ async_result = self.start_mediated_transfer( token_network_identifier, amount, target, identifier, ) return async_result def direct_transfer_async(self, token_network_identifier, amount, target, identifier): """ Do a direct transfer with target. Direct transfers are non cancellable and non expirable, since these transfers are a signed balance proof with the transferred amount incremented. Because the transfer is non cancellable, there is a level of trust with the target. After the message is sent the target is effectively paid and then it is not possible to revert. The async result will be set to False iff there is no direct channel with the target or the payer does not have balance to complete the transfer, otherwise because the transfer is non expirable the async result *will never be set to False* and if the message is sent it will hang until the target node acknowledge the message. This transfer should be used as an optimization, since only two packets are required to complete the transfer (from the payers perspective), whereas the mediated transfer requires 6 messages. """ self.start_health_check_for(target) if identifier is None: identifier = create_default_identifier() direct_transfer = ActionTransferDirect( token_network_identifier, target, identifier, amount, ) self.handle_state_change(direct_transfer) def start_mediated_transfer( self, token_network_identifier, amount, target, identifier, ): self.start_health_check_for(target) if identifier is None: identifier = create_default_identifier() assert identifier not in self.identifier_to_results async_result = AsyncResult() self.identifier_to_results[identifier].append(async_result) secret = random_secret() init_initiator_statechange = initiator_init( self, identifier, amount, secret, token_network_identifier, target, ) # TODO: implement the network timeout raiden.config['msg_timeout'] and # cancel the current transfer if it happens (issue #374) # # Dispatch the state change even if there are no routes to create the # wal entry. self.handle_state_change(init_initiator_statechange) return async_result def mediate_mediated_transfer(self, transfer: LockedTransfer): init_mediator_statechange = mediator_init(self, transfer) self.handle_state_change(init_mediator_statechange) def target_mediated_transfer(self, transfer: LockedTransfer): self.start_health_check_for(transfer.initiator) init_target_statechange = target_init(transfer) self.handle_state_change(init_target_statechange)
class Group(GroupMappingMixin): """ Maintain a group of greenlets that are still running, without limiting their number. Links to each item and removes it upon notification. Groups can be iterated to discover what greenlets they are tracking, they can be tested to see if they contain a greenlet, and they know the number (len) of greenlets they are tracking. If they are not tracking any greenlets, they are False in a boolean context. """ #: The type of Greenlet object we will :meth:`spawn`. This can be changed #: on an instance or in a subclass. greenlet_class = Greenlet def __init__(self, *args): assert len(args) <= 1, args self.greenlets = set(*args) if args: for greenlet in args[0]: greenlet.rawlink(self._discard) # each item we kill we place in dying, to avoid killing the same greenlet twice self.dying = set() self._empty_event = Event() self._empty_event.set() def __repr__(self): return '<%s at 0x%x %s>' % (self.__class__.__name__, id(self), self.greenlets) def __len__(self): """ Answer how many greenlets we are tracking. Note that if we are empty, we are False in a boolean context. """ return len(self.greenlets) def __contains__(self, item): """ Answer if we are tracking the given greenlet. """ return item in self.greenlets def __iter__(self): """ Iterate across all the greenlets we are tracking, in no particular order. """ return iter(self.greenlets) def add(self, greenlet): """ Begin tracking the greenlet. If this group is :meth:`full`, then this method may block until it is possible to track the greenlet. """ try: rawlink = greenlet.rawlink except AttributeError: pass # non-Greenlet greenlet, like MAIN else: rawlink(self._discard) self.greenlets.add(greenlet) self._empty_event.clear() def _discard(self, greenlet): self.greenlets.discard(greenlet) self.dying.discard(greenlet) if not self.greenlets: self._empty_event.set() def discard(self, greenlet): """ Stop tracking the greenlet. """ self._discard(greenlet) try: unlink = greenlet.unlink except AttributeError: pass # non-Greenlet greenlet, like MAIN else: unlink(self._discard) def start(self, greenlet): """ Start the un-started *greenlet* and add it to the collection of greenlets this group is monitoring. """ self.add(greenlet) greenlet.start() def spawn(self, *args, **kwargs): """ Begin a new greenlet with the given arguments (which are passed to the greenlet constructor) and add it to the collection of greenlets this group is monitoring. :return: The newly started greenlet. """ greenlet = self.greenlet_class(*args, **kwargs) self.start(greenlet) return greenlet # def close(self): # """Prevents any more tasks from being submitted to the pool""" # self.add = RaiseException("This %s has been closed" % self.__class__.__name__) def join(self, timeout=None, raise_error=False): """ Wait for this group to become empty *at least once*. If there are no greenlets in the group, returns immediately. .. note:: By the time the waiting code (the caller of this method) regains control, a greenlet may have been added to this group, and so this object may no longer be empty. (That is, ``group.join(); assert len(group) == 0`` is not guaranteed to hold.) This method only guarantees that the group reached a ``len`` of 0 at some point. :keyword bool raise_error: If True (*not* the default), if any greenlet that finished while the join was in progress raised an exception, that exception will be raised to the caller of this method. If multiple greenlets raised exceptions, which one gets re-raised is not determined. Only greenlets currently in the group when this method is called are guaranteed to be checked for exceptions. :return bool: A value indicating whether this group became empty. If the timeout is specified and the group did not become empty during that timeout, then this will be a false value. Otherwise it will be a true value. .. versionchanged:: 1.2a1 Add the return value. """ greenlets = list(self.greenlets) if raise_error else () result = self._empty_event.wait(timeout=timeout) for greenlet in greenlets: if greenlet.exception is not None: if hasattr(greenlet, '_raise_exception'): greenlet._raise_exception() raise greenlet.exception return result def kill(self, exception=GreenletExit, block=True, timeout=None): """ Kill all greenlets being tracked by this group. """ timer = Timeout._start_new_or_dummy(timeout) try: while self.greenlets: for greenlet in list(self.greenlets): if greenlet in self.dying: continue try: kill = greenlet.kill except AttributeError: _kill(greenlet, exception) else: kill(exception, block=False) self.dying.add(greenlet) if not block: break joinall(self.greenlets) except Timeout as ex: if ex is not timer: raise finally: timer.cancel() def killone(self, greenlet, exception=GreenletExit, block=True, timeout=None): """ If the given *greenlet* is running and being tracked by this group, kill it. """ if greenlet not in self.dying and greenlet in self.greenlets: greenlet.kill(exception, block=False) self.dying.add(greenlet) if block: greenlet.join(timeout) def full(self): """ Return a value indicating whether this group can track more greenlets. In this implementation, because there are no limits on the number of tracked greenlets, this will always return a ``False`` value. """ return False def wait_available(self, timeout=None): """ Block until it is possible to :meth:`spawn` a new greenlet. In this implementation, because there are no limits on the number of tracked greenlets, this will always return immediately. """ pass # MappingMixin methods def _apply_immediately(self): # If apply() is called from one of our own # worker greenlets, don't spawn a new one---if we're full, that # could deadlock. return getcurrent() in self def _apply_async_cb_spawn(self, callback, result): Greenlet.spawn(callback, result) def _apply_async_use_greenlet(self): # cannot call self.spawn() because it will block, so # use a fresh, untracked greenlet that when run will # (indirectly) call self.spawn() for us. return self.full()
class BaseServer(object): """ An abstract base class that implements some common functionality for the servers in gevent. :param listener: Either be an address that the server should bind on or a :class:`gevent.socket.socket` instance that is already bound (and put into listening mode in case of TCP socket). :keyword handle: If given, the request handler. The request handler can be defined in a few ways. Most commonly, subclasses will implement a ``handle`` method as an instance method. Alternatively, a function can be passed as the ``handle`` argument to the constructor. In either case, the handler can later be changed by calling :meth:`set_handle`. When the request handler returns, the socket used for the request will be closed. Therefore, the handler must not return if the socket is still in use (for example, by manually spawned greenlets). :keyword spawn: If provided, is called to create a new greenlet to run the handler. By default, :func:`gevent.spawn` is used (meaning there is no artificial limit on the number of concurrent requests). Possible values for *spawn*: - a :class:`gevent.pool.Pool` instance -- ``handle`` will be executed using :meth:`gevent.pool.Pool.spawn` only if the pool is not full. While it is full, no new connections are accepted; - :func:`gevent.spawn_raw` -- ``handle`` will be executed in a raw greenlet which has a little less overhead then :class:`gevent.Greenlet` instances spawned by default; - ``None`` -- ``handle`` will be executed right away, in the :class:`Hub` greenlet. ``handle`` cannot use any blocking functions as it would mean switching to the :class:`Hub`. - an integer -- a shortcut for ``gevent.pool.Pool(integer)`` .. versionchanged:: 1.1a1 When the *handle* function returns from processing a connection, the client socket will be closed. This resolves the non-deterministic closing of the socket, fixing ResourceWarnings under Python 3 and PyPy. """ # pylint: disable=too-many-instance-attributes,bare-except,broad-except #: the number of seconds to sleep in case there was an error in accept() call #: for consecutive errors the delay will double until it reaches max_delay #: when accept() finally succeeds the delay will be reset to min_delay again min_delay = 0.01 max_delay = 1 #: Sets the maximum number of consecutive accepts that a process may perform on #: a single wake up. High values give higher priority to high connection rates, #: while lower values give higher priority to already established connections. #: Default is 100. Note, that in case of multiple working processes on the same #: listening value, it should be set to a lower value. (pywsgi.WSGIServer sets it #: to 1 when environ["wsgi.multiprocess"] is true) max_accept = 100 _spawn = Greenlet.spawn #: the default timeout that we wait for the client connections to close in stop() stop_timeout = 1 fatal_errors = (errno.EBADF, errno.EINVAL, errno.ENOTSOCK) def __init__(self, listener, handle=None, spawn='default'): self._stop_event = Event() self._stop_event.set() self._watcher = None self._timer = None self._handle = None # XXX: FIXME: Subclasses rely on the presence or absence of the # `socket` attribute to determine whether we are open/should be opened. # Instead, have it be None. self.pool = None try: self.set_listener(listener) self.set_spawn(spawn) self.set_handle(handle) self.delay = self.min_delay self.loop = get_hub().loop if self.max_accept < 1: raise ValueError('max_accept must be positive int: %r' % (self.max_accept, )) except: self.close() raise def set_listener(self, listener): if hasattr(listener, 'accept'): if hasattr(listener, 'do_handshake'): raise TypeError( 'Expected a regular socket, not SSLSocket: %r' % (listener, )) self.family = listener.family self.address = listener.getsockname() self.socket = listener else: self.family, self.address = parse_address(listener) def set_spawn(self, spawn): if spawn == 'default': self.pool = None self._spawn = self._spawn elif hasattr(spawn, 'spawn'): self.pool = spawn self._spawn = spawn.spawn elif isinstance(spawn, integer_types): from gevent.pool import Pool self.pool = Pool(spawn) self._spawn = self.pool.spawn else: self.pool = None self._spawn = spawn if hasattr(self.pool, 'full'): self.full = self.pool.full if self.pool is not None: self.pool._semaphore.rawlink(self._start_accepting_if_started) def set_handle(self, handle): if handle is not None: self.handle = handle if hasattr(self, 'handle'): self._handle = self.handle else: raise TypeError("'handle' must be provided") def _start_accepting_if_started(self, _event=None): if self.started: self.start_accepting() def start_accepting(self): if self._watcher is None: # just stop watcher without creating a new one? self._watcher = self.loop.io(self.socket.fileno(), 1) self._watcher.start(self._do_read) def stop_accepting(self): if self._watcher is not None: self._watcher.stop() self._watcher.close() self._watcher = None if self._timer is not None: self._timer.stop() self._timer.close() self._timer = None def do_handle(self, *args): spawn = self._spawn handle = self._handle close = self.do_close try: if spawn is None: _handle_and_close_when_done(handle, close, args) else: spawn(_handle_and_close_when_done, handle, close, args) except: close(*args) raise def do_close(self, *args): pass def do_read(self): raise NotImplementedError() def _do_read(self): for _ in xrange(self.max_accept): if self.full(): self.stop_accepting() return try: args = self.do_read() self.delay = self.min_delay if not args: return except: self.loop.handle_error(self, *sys.exc_info()) ex = sys.exc_info()[1] if self.is_fatal_error(ex): self.close() sys.stderr.write('ERROR: %s failed with %s\n' % (self, str(ex) or repr(ex))) return if self.delay >= 0: self.stop_accepting() self._timer = self.loop.timer(self.delay) self._timer.start(self._start_accepting_if_started) self.delay = min(self.max_delay, self.delay * 2) break else: try: self.do_handle(*args) except: self.loop.handle_error((args[1:], self), *sys.exc_info()) if self.delay >= 0: self.stop_accepting() self._timer = self.loop.timer(self.delay) self._timer.start(self._start_accepting_if_started) self.delay = min(self.max_delay, self.delay * 2) break def full(self): # copied from self.pool # pylint: disable=method-hidden return False def __repr__(self): return '<%s at %s %s>' % (type(self).__name__, hex( id(self)), self._formatinfo()) def __str__(self): return '<%s %s>' % (type(self).__name__, self._formatinfo()) def _formatinfo(self): if hasattr(self, 'socket'): try: fileno = self.socket.fileno() except Exception as ex: fileno = str(ex) result = 'fileno=%s ' % fileno else: result = '' try: if isinstance(self.address, tuple) and len(self.address) == 2: result += 'address=%s:%s' % self.address else: result += 'address=%s' % (self.address, ) except Exception as ex: result += str(ex) or '<error>' handle = self.__dict__.get('handle') if handle is not None: fself = getattr(handle, '__self__', None) try: if fself is self: # Checks the __self__ of the handle in case it is a bound # method of self to prevent recursivly defined reprs. handle_repr = '<bound method %s.%s of self>' % ( self.__class__.__name__, handle.__name__, ) else: handle_repr = repr(handle) result += ' handle=' + handle_repr except Exception as ex: result += str(ex) or '<error>' return result @property def server_host(self): """IP address that the server is bound to (string).""" if isinstance(self.address, tuple): return self.address[0] @property def server_port(self): """Port that the server is bound to (an integer).""" if isinstance(self.address, tuple): return self.address[1] def init_socket(self): """If the user initialized the server with an address rather than socket, then this function will create a socket, bind it and put it into listening mode. It is not supposed to be called by the user, it is called by :meth:`start` before starting the accept loop.""" @property def started(self): return not self._stop_event.is_set() def start(self): """Start accepting the connections. If an address was provided in the constructor, then also create a socket, bind it and put it into the listening mode. """ self.init_socket() self._stop_event.clear() try: self.start_accepting() except: self.close() raise def close(self): """Close the listener socket and stop accepting.""" self._stop_event.set() try: self.stop_accepting() finally: try: self.socket.close() except Exception: pass finally: self.__dict__.pop('socket', None) self.__dict__.pop('handle', None) self.__dict__.pop('_handle', None) self.__dict__.pop('_spawn', None) self.__dict__.pop('full', None) if self.pool is not None: self.pool._semaphore.unlink( self._start_accepting_if_started) # If the pool's semaphore had a notifier already started, # there's a reference cycle we're a part of # (self->pool->semaphere-hub callback->semaphore) # But we can't destroy self.pool, because self.stop() # calls this method, and then wants to join self.pool() @property def closed(self): return not hasattr(self, 'socket') def stop(self, timeout=None): """ Stop accepting the connections and close the listening socket. If the server uses a pool to spawn the requests, then :meth:`stop` also waits for all the handlers to exit. If there are still handlers executing after *timeout* has expired (default 1 second, :attr:`stop_timeout`), then the currently running handlers in the pool are killed. If the server does not use a pool, then this merely stops accepting connections; any spawned greenlets that are handling requests continue running until they naturally complete. """ self.close() if timeout is None: timeout = self.stop_timeout if self.pool: self.pool.join(timeout=timeout) self.pool.kill(block=True, timeout=1) def serve_forever(self, stop_timeout=None): """Start the server if it hasn't been already started and wait until it's stopped.""" # add test that serve_forever exists on stop() if not self.started: self.start() try: self._stop_event.wait() finally: Greenlet.spawn(self.stop, timeout=stop_timeout).join() def is_fatal_error(self, ex): return isinstance(ex, _socket.error) and ex.args[0] in self.fatal_errors
class SardanaMacro(CommandObject, SardanaObject): macroStatusAttr = None INIT, STARTED, RUNNING, DONE = range(4) def __init__(self, name, macro, doorname=None, username=None, **kwargs): super(SardanaMacro, self).__init__(name, username, **kwargs) self._reply_arrived_event = Event() self.macro_format = macro self.doorname = doorname self.door = None self.init_device() self.macrostate = SardanaMacro.INIT self.doorstate = None self.t0 = 0 def init_device(self): self.door = Device(self.doorname) self.door.set_timeout_millis(10000) # # DIRTY FIX to make compatible taurus listeners and existence of Tango channels/commands # as defined in Command/Tango.py # # if self.door.__class__ == taurus.core.tango.tangodevice.TangoDevice: # dp = self.door.getHWObj() # try: # dp.subscribe_event = dp._subscribe_event # except AttributeError: # pass if self.macroStatusAttr is None: self.macroStatusAttr = self.door.getAttribute("State") self.macroStatusAttr.addListener(self.objectListener) def __call__(self, *args, **kwargs): self._reply_arrived_event.clear() self.result = None wait = kwargs.get("wait", False) if self.door is None: self.init_device() logging.getLogger("HWR").debug( "Executing sardana macro: %s" % self.macro_format ) logging.getLogger("HWR").debug( " args=%s / kwargs=%s" % (str(args), str(kwargs)) ) try: fullcmd = self.macro_format + " " + " ".join([str(a) for a in args]) except Exception: import traceback logging.getLogger("HWR").info(traceback.format_exc()) logging.getLogger("HWR").info( " - Wrong format for macro arguments. Macro is %s / args are (%s)" % (self.macro_format, str(args)) ) return try: import time self.t0 = time.time() if self.doorstate in ["ON", "ALARM"]: self.door.runMacro(fullcmd.split()) self.macrostate = SardanaMacro.STARTED self.emit("commandBeginWaitReply", (str(self.name()),)) else: logging.getLogger("HWR").error( "%s. Cannot execute. Door is not READY", str(self.name()) ) self.emit("commandFailed", (-1, self.name())) except TypeError: logging.getLogger("HWR").error( "%s. Cannot properly format macro code. Format is: %s, args are %s", str(self.name()), self.macro_format, str(args), ) self.emit("commandFailed", (-1, self.name())) except DevFailed as error_dict: logging.getLogger("HWR").error( "%s: Cannot run macro. %s", str(self.name()), error_dict ) self.emit("commandFailed", (-1, self.name())) except AttributeError as error_dict: logging.getLogger("HWR").error( "%s: MacroServer not running?, %s", str(self.name()), error_dict ) self.emit("commandFailed", (-1, self.name())) except Exception: logging.getLogger("HWR").exception( "%s: an error occured when calling Tango command %s", str(self.name()), self.macro_format, ) self.emit("commandFailed", (-1, self.name())) if wait: logging.getLogger("HWR").debug("... start waiting...") t = gevent.spawn(endOfMacro, self) t.get() logging.getLogger("HWR").debug("... end waiting...") return def update(self, event): data = event.event[2] try: if not isinstance(data, PyTango.DeviceAttribute): # Events different than a value changed on attribute. Taurus sends an event with attribute info # logging.getLogger('HWR').debug("==========. Got an event, but it is not an attribute . it is %s" % type(data)) # logging.getLogger('HWR').debug("doorstate event. type is %s" % str(type(data))) return # Handling macro state changed event doorstate = str(data.value) logging.getLogger("HWR").debug( "doorstate changed. it is %s" % str(doorstate) ) if doorstate != self.doorstate: self.doorstate = doorstate # logging.getLogger('HWR').debug("self.doorstate is %s" % self.canExecute()) self.emit("commandCanExecute", (self.canExecute(),)) if doorstate in ["ON", "ALARM"]: # logging.getLogger('HWR').debug("Macroserver ready for commands") self.emit("commandReady", ()) else: # logging.getLogger('HWR').debug("Macroserver busy ") self.emit("commandNotReady", ()) if self.macrostate == SardanaMacro.STARTED and doorstate == "RUNNING": # logging.getLogger('HWR').debug("Macro server is running") self.macrostate = SardanaMacro.RUNNING elif self.macrostate == SardanaMacro.RUNNING and ( doorstate in ["ON", "ALARM"] ): logging.getLogger("HWR").debug("Macro execution finished") self.macrostate = SardanaMacro.DONE self.result = self.door.result self.emit("commandReplyArrived", (self.result, str(self.name()))) if doorstate == "ALARM": self.emit("commandAborted", (str(self.name()),)) self._reply_arrived_event.set() elif ( self.macrostate == SardanaMacro.DONE or self.macrostate == SardanaMacro.INIT ): # already handled in the general case above pass else: logging.getLogger("HWR").debug("Macroserver state changed") self.emit("commandFailed", (-1, str(self.name()))) except ConnectionFailed: logging.getLogger("HWR").debug("Cannot connect to door %s" % self.doorname) self.emit("commandFailed", (-1, str(self.name()))) except Exception: import traceback logging.getLogger("HWR").debug( "SardanaMacro / event handling problem. Uggh. %s" % traceback.format_exc() ) self.emit("commandFailed", (-1, str(self.name()))) def abort(self): if self.door is not None: logging.getLogger("HWR").debug("SardanaMacro / aborting macro") self.door.abortMacro() # self.emit('commandReady', ()) def isConnected(self): return self.door is not None def canExecute(self): return self.door is not None and (self.doorstate in ["ON", "ALARM"])
class State(object): def __init__(self): self._eid = 0 self._event = Event() self._w = self._h = self._health = None self.size_changed_count = 0 def wait(self, eid, timeout=5): if eid < self._eid: return self._event.clear() self._event.wait(timeout) return self._eid def notify(self): self._eid += 1 self._event.set() def _update_health(self): health = True output = gsp.check_output([ 'supervisorctl', '-c', '/etc/supervisor/supervisord.conf', 'status' ]) for line in output.strip().split('\n'): if not line.startswith('web') and line.find('RUNNING') < 0: health = False break if self._health != health: self._health = health self.notify() return self._health def to_dict(self): self._update_health() state = { 'id': self._eid, 'config': { 'fixedResolution': 'RESOLUTION' in environ, 'sizeChangedCount': self.size_changed_count } } self._update_size() state.update({ 'width': self.w, 'height': self.h, }) return state def set_size(self, w, h): gsp.check_call(('sed -i \'s#' '^exec /usr/bin/Xvfb.*$' '#' 'exec /usr/bin/Xvfb :1 -screen 0 {}x{}x16' '#\' /usr/local/bin/xvfb.sh').format(w, h), shell=True) self.size_changed_count += 1 def apply_and_restart(self): gsp.check_call([ 'supervisorctl', '-c', '/etc/supervisor/supervisord.conf', 'restart', 'x:' ]) self._w = self._h = self._health = None self.notify() def switch_video(self, onoff): xenvs = { 'DISPLAY': ':1', } try: cmd = 'nofb' if onoff else 'fb' gsp.check_output(['x11vnc', '-remote', cmd], env=xenvs) except gsp.CalledProcessError as e: log.warn('failed to set x11vnc fb: ' + str(e)) def _update_size(self): if self._w is not None and self._h is not None: return xenvs = { 'DISPLAY': ':1', } try: output = gsp.check_output(['x11vnc', '-query', 'dpy_x,dpy_y'], env=xenvs).decode('utf-8') mobj = research(r'dpy_x:(\d+).*dpy_y:(\d+)', output) if mobj is not None: w, h = int(mobj.group(1)), int(mobj.group(2)) changed = False if self._w != w: changed = True self._w = w if self._h != h: changed = True self._h = h if changed: self.notify() except gsp.CalledProcessError as e: log.warn('failed to get dispaly size: ' + str(e)) def reset_size(self): self.size_changed_count = 0 @property def w(self): return self._w @property def h(self): return self._h @property def health(self): self._update_health() return self._health
class APIClient(BaseNamespace, plugintools.GreenletObject): node = ('ws.download.am', 443) change_node = True # change node if asked to, may be overridden by --testbackend for internal testing def __init__(self): plugintools.GreenletObject.__init__(self) self.io = None self.next_node = None self.greenlet = None self.lock = Semaphore() self.login_results = dict() self.connected_event = Event() self.disconnected_event = Event() self.connect_retry = 0 self.connect_retry_last_msgbox = 0 self.connect_error_dialog = None self.connection_states = None def __call__(self, socketio, path): BaseNamespace.__init__(self, socketio, path) return self def connect(self): try: if self.io: self.io.disconnect() if self.next_node is not None: node = self.next_node self.next_node = None else: node = self.node log.info('connecting to {}:{}'.format(*node)) self.io = SocketIO(node[0], node[1], self, secure=True) all_events = ["disconnect", "reconnect", "open", "close", "error", "retry", "message"] for e in all_events: self.io.on(e, getattr(self, "on_" + e)) except BaseException as e: return e def is_connected(self): return True if self.connected_event.is_set() and not self.disconnected_event.is_set() else False def wait_connected(self): self.connected_event.wait() def close(self): if self.greenlet and gevent.getcurrent() != self.greenlet: self.greenlet.kill() if self.io: io = self.io self.io = None try: io.disconnect() except: pass log.info('closed api connection') self.disconnected_event.set() def on_connect(self, *args, **kwargs): log.debug('connected') def on_disconnect(self, exception): if self.io: if exception: log.warning('disconnected with error: {}'.format(exception)) else: log.debug('disconnected') self.close() def on_reconnect(self, *args): log.warning("unhandled event: RECONNECT: {}".format(args)) def on_open(self, *args): log.warning("unhandled event: OPEN: {}".format(args)) def on_close(self, *args): log.warning("unhandled event: CLOSE: {}".format(args)) def on_error(self, reason, advice): log.warning("unhandled event: ERROR: reason: {}, advise: {}".format(reason, advice)) def on_retry(self, *args): log.warning("unhandled event: RETRY: {}".format(args)) def on_(self, *args): log.warning("unhandled event: ON_: {}".format(args)) def send_message(self, message): with self.lock: #print ">>> SEND CLIENT", message['flags'], message.get('command'), message['payload'] self.emit("message", message) def send(self, type, command=None, in_response_to=None, payload=None, channel=None, encrypt=True, emit=None): if type == 'frontend': if not self.connected_event.is_set(): return message = proto.pack_message(type, command, in_response_to, payload, channel=channel, encrypt=encrypt) self.send_message(message) def on_message(self, message): message = proto.unpack_message(message) gevent.spawn(proto.process_message, self.send_message, message) def handshake(self): """returns None on error or (node_host, node_port) """ data = {} data["name"] = base64.standard_b64encode(login.encrypt('frontend', socket.gethostname())) data["system"] = platform.system() data["machine"] = platform.machine() data["platform"] = platform.platform() data["release"] = platform.release() data["version"] = platform.version() result = AsyncResult() rid = str(id(result)) self.login_results[rid] = result key = login.generate_backend_key() from .. import patch payload = { 'id': rid, 'version': proto.VERSION, 'branch': patch.config.branch, 'commit_id': patch.core_source.version, 'l': login.get('login'), 'system': data, } message_key = proto.pack_message('backend', 'api.set_key', payload=dict(key=key), encrypt="rsa") message = proto.pack_message('backend', 'api.login', payload=payload) def send_login(): for i in xrange(7): try: if result.value is not None: return except AttributeError: return msg = 'logging in (retry {})'.format(i) if i > 0: if self.connection_state is not None: self.connection_state.put(msg) else: log.info(msg) try: self.send_message(message_key) self.send_message(message) except AttributeError: result.set([False, 'Client login error']) return secs = 5*i if secs < 10: secs = 10 gevent.sleep(secs) if result.value is None: result.set([False, 'Login timed out']) g = gevent.spawn(send_login) try: result = result.get() finally: try: del self.login_results[rid] except KeyError: pass g.kill() if not result[0]: log.error('login failed: {}'.format(result[1])) if result[1] == 'Invalid Login Credentials': self.connect_retry = 0 login.logout() return False node = result[1].rsplit(':', 1) node = str(node[0]), int(len(node) == 2 and node[1] or 443) if node != self.node and self.node != self.next_node: if not self.change_node: log.info("i was asked to switch my connection to `{}`. I refuse. (--testbackend is {})".format(node, self.node[0])) return True self.next_node = node log.info('switching to node {}:{}'.format(*self.next_node)) return False return True def connection_error(self): try: if not self.connect_error_dialog: self.connect_error_dialog = gevent.spawn(self._connection_error) finally: self.connect_retry = 0 self.connect_error_dialog = None def _connection_error(self): elements = list() elements.append(input.Float('left')) elements.append(input.Text('Failed connecting to download.am.\nPlease check your internet connection.')) elements.append(input.Text('')) elements.append(input.Text('Proxy settings')) elements.append([input.Text('Type:'), input.Select('type', ('direct', 'http', 'https', 'socks4', 'socks5'), default=proxy.proxy_enabled() and proxy.config.type or 'direct')]) elements.append([input.Text('Hostname:'), input.Input('host', value=proxy.config.host or '')]) elements.append([input.Text('Port:'), input.Input('port', value=proxy.config.port or '')]) elements.append(input.Text('')) elements.append(input.Text('Proxy authorization')) elements.append([input.Text('Username:'******'username', value=proxy.config.username or '')]) elements.append([input.Text('Password:'******'password', 'password', value=proxy.config.password or '')]) elements.append(input.Text('')) elements.append(input.Float('center')) elements.append(input.Choice('result', choices=[ dict(value='save', content='Save', ok=True), dict(value='retry', content='Retry', cancel=True), dict(value='exit', content='Exit download.am')])) try: result = input.get(elements, type='api_connect', timeout=None, ignore_api=True) except input.InputAborted: return finally: self.connect_retry_last_msgbox = time.time() if result.get('result') == 'save': if result['type'] == 'direct': result['type'] = None result['enabled'] = True del result['result'] interface.call('proxy', 'set', **result) elif result.get('result') == 'retry': return elif result.get('result') == 'exit': sys.exit(1) else: raise RuntimeError('invalid response: {}'.format(result)) def run(self): self.connect_retry = 0 error = False self.connect_error_dialog = None while True: self.close() self.connected_event.clear() login.wait() # wait until user entered login data # TODO: wait for reconnected event if error: self.connect_retry += 1 error = False else: self.connect_retry = 0 sleep_time = (self.connect_retry + 1)*3 if sleep_time > 15: sleep_time = 15 if self.connection_state is not None: self.connection_state.put('connecting') try: e = self.spawn(self.connect).get() if e is not None: raise e self.emit("hello", settings.app_uuid, "client") except (KeyboardInterrupt, SystemExit, gevent.GreenletExit): raise except BaseException as e: if self.connection_state is not None: self.connection_state.put('connection error') log.error('error connecting: {}'.format(e)) self.close() event.fire('api:connection_error') error = True gevent.sleep(sleep_time) continue finally: self.kill() if self.connection_state is not None: self.connection_state.put('logging in') try: self.spawn(self.handshake) result = self.greenlet.get() if not result: continue except (KeyboardInterrupt, SystemExit, gevent.GreenletExit): raise except BaseException as e: if self.connection_state is not None: self.connection_state.put('login error') log.error('error handshaking: {}'.format(str(e))) self.close() error = True gevent.sleep(sleep_time) continue finally: self.kill() if self.io is None or not self.io.connected: log.warning('api bootstrap done but not connected. this is strange...') if self.connection_state is not None: self.connection_state.put('connection error') self.close() error = True gevent.sleep(sleep_time) continue self.connect_retry = 0 if self.connect_error_dialog: self.connect_error_dialog.kill() self.connect_error_dialog = None if self.connection_state is not None: self.connection_state.put('sending infos') payload = interface.call('api', 'expose_all') message = proto.pack_message('frontend', 'api.expose_all', payload=payload) try: self.send_message(message) except AttributeError: log.warning('api closed unexpected') if self.connection_state is not None: self.connection_state.put('connection error') self.close() error = True gevent.sleep(sleep_time) continue self.connected_event.set() self.disconnected_event.clear() if self.connection_state is not None: self.connection_state.put('connected') event.fire('api:connected') self.disconnected_event.wait() login.hashes["backend"] = None event.fire('api:disconnected')
class BaseServer(object): """An abstract base class that implements some common functionality for the servers in gevent. *listener* can either be an address that the server should bind on or a :class:`gevent.socket.socket` instance that is already bound (and put into listening mode in case of TCP socket). *spawn*, if provided, is called to create a new greenlet to run the handler. By default, :func:`gevent.spawn` is used. Possible values for *spawn*: * a :class:`gevent.pool.Pool` instance -- *handle* will be executed using :meth:`Pool.spawn` method only if the pool is not full. While it is full, all the connection are dropped; * :func:`gevent.spawn_raw` -- *handle* will be executed in a raw greenlet which have a little less overhead then :class:`gevent.Greenlet` instances spawned by default; * ``None`` -- *handle* will be executed right away, in the :class:`Hub` greenlet. *handle* cannot use any blocking functions as it means switching to the :class:`Hub`. * an integer -- a shortcut for ``gevent.pool.Pool(integer)`` """ # the number of seconds to sleep in case there was an error in accept() call # for consecutive errors the delay will double until it reaches max_delay # when accept() finally succeeds the delay will be reset to min_delay again min_delay = 0.01 max_delay = 1 # Sets the maximum number of consecutive accepts that a process may perform on # a single wake up. High values give higher priority to high connection rates, # while lower values give higher priority to already established connections. # Default is 100. Note, that in case of multiple working processes on the same # listening value, it should be set to a lower value. (pywsgi.WSGIServer sets it # to 1 when environ["wsgi.multiprocess"] is true) max_accept = 100 _spawn = Greenlet.spawn # the default timeout that we wait for the client connections to close in stop() stop_timeout = 1 fatal_errors = (errno.EBADF, errno.EINVAL, errno.ENOTSOCK) def __init__(self, listener, handle=None, spawn='default'): self._stop_event = Event() self._stop_event.set() self._watcher = None self._timer = None self.pool = None try: self.set_listener(listener) self.set_spawn(spawn) self.set_handle(handle) self.delay = self.min_delay self.loop = gevent.get_hub().loop if self.max_accept < 1: raise ValueError('max_accept must be positive int: %r' % (self.max_accept, )) except: self.close() raise def set_listener(self, listener): if hasattr(listener, 'accept'): if hasattr(listener, 'do_handshake'): raise TypeError( 'Expected a regular socket, not SSLSocket: %r' % (listener, )) self.family = listener.family self.address = listener.getsockname() self.socket = listener else: self.family, self.address = parse_address(listener) def set_spawn(self, spawn): if spawn == 'default': self.pool = None self._spawn = self._spawn elif hasattr(spawn, 'spawn'): self.pool = spawn self._spawn = spawn.spawn elif isinstance(spawn, (int, long)): from gevent.pool import Pool self.pool = Pool(spawn) self._spawn = self.pool.spawn else: self.pool = None self._spawn = spawn if hasattr(self.pool, 'full'): self.full = self.pool.full if self.pool is not None: self.pool._semaphore.rawlink(self._start_accepting_if_started) def set_handle(self, handle): if handle is not None: self.handle = handle if hasattr(self, 'handle'): self._handle = self.handle else: raise TypeError("'handle' must be provided") def _start_accepting_if_started(self, _event=None): if self.started: self.start_accepting() def start_accepting(self): if self._watcher is None: # just stop watcher without creating a new one? self._watcher = self.loop.io(self.socket.fileno(), 1) self._watcher.start(self._do_read) def stop_accepting(self): if self._watcher is not None: self._watcher.stop() self._watcher = None if self._timer is not None: self._timer.stop() self._timer = None def do_handle(self, *args): spawn = self._spawn if spawn is None: self._handle(*args) else: spawn(self._handle, *args) def _do_read(self): for _ in xrange(self.max_accept): if self.full(): self.stop_accepting() return try: args = self.do_read() self.delay = self.min_delay if not args: return except: self.loop.handle_error(self, *sys.exc_info()) ex = sys.exc_info()[1] if self.is_fatal_error(ex): self.close() sys.stderr.write('ERROR: %s failed with %s\n' % (self, str(ex) or repr(ex))) return if self.delay >= 0: self.stop_accepting() self._timer = self.loop.timer(self.delay) self._timer.start(self._start_accepting_if_started) self.delay = min(self.max_delay, self.delay * 2) break else: try: self.do_handle(*args) except: self.loop.handle_error((args[1:], self), *sys.exc_info()) if self.delay >= 0: self.stop_accepting() self._timer = self.loop.timer(self.delay) self._timer.start(self._start_accepting_if_started) self.delay = min(self.max_delay, self.delay * 2) break def full(self): return False def __repr__(self): return '<%s at %s %s>' % (type(self).__name__, hex( id(self)), self._formatinfo()) def __str__(self): return '<%s %s>' % (type(self).__name__, self._formatinfo()) def _formatinfo(self): if hasattr(self, 'socket'): try: fileno = self.socket.fileno() except Exception: ex = sys.exc_info()[1] fileno = str(ex) result = 'fileno=%s ' % fileno else: result = '' try: if isinstance(self.address, tuple) and len(self.address) == 2: result += 'address=%s:%s' % self.address else: result += 'address=%s' % (self.address, ) except Exception: ex = sys.exc_info()[1] result += str(ex) or '<error>' try: handle = getfuncname(self.__dict__['handle']) except Exception: handle = None if handle is not None: result += ' handle=' + handle return result @property def server_host(self): """IP address that the server is bound to (string).""" if isinstance(self.address, tuple): return self.address[0] @property def server_port(self): """Port that the server is bound to (an integer).""" if isinstance(self.address, tuple): return self.address[1] def init_socket(self): """If the user initialized the server with an address rather than socket, then this function will create a socket, bind it and put it into listening mode. It is not supposed to be called by the user, it is called by :meth:`start` before starting the accept loop.""" pass @property def started(self): return not self._stop_event.is_set() def start(self): """Start accepting the connections. If an address was provided in the constructor, then also create a socket, bind it and put it into the listening mode. """ self.init_socket() self._stop_event.clear() try: self.start_accepting() except: self.kill() raise def close(self): """Close the listener socket and stop accepting.""" self._stop_event.set() try: self.stop_accepting() finally: try: self.socket.close() except Exception: pass finally: self.__dict__.pop('socket', None) self.__dict__.pop('handle', None) self.__dict__.pop('_handle', None) self.__dict__.pop('_spawn', None) self.__dict__.pop('full', None) if self.pool is not None: self.pool._semaphore.unlink( self._start_accepting_if_started) def stop(self, timeout=None): """Stop accepting the connections and close the listening socket. If the server uses a pool to spawn the requests, then :meth:`stop` also waits for all the handlers to exit. If there are still handlers executing after *timeout* has expired (default 1 second), then the currently running handlers in the pool are killed.""" self.close() if timeout is None: timeout = self.stop_timeout if self.pool: self.pool.join(timeout=timeout) self.pool.kill(block=True, timeout=1) def serve_forever(self, stop_timeout=None): """Start the server if it hasn't been already started and wait until it's stopped.""" # add test that serve_forever exists on stop() if not self.started: self.start() try: self._stop_event.wait() finally: gevent.spawn(self.stop, timeout=stop_timeout).join() def is_fatal_error(self, ex): return isinstance(ex, _socket.error) and ex[0] in self.fatal_errors
class ClientBase(object): _buffer = b'' _receivedFDs = [] _toBeSentFDs = [] _nextMsgLen = 0 _endian = '<' _firstByte = True _unix_creds = None # (pid, uid, gid) from UnixSocket credential passing authenticator = None # Class to handle DBus authentication MAX_MSG_LENGTH = 2**27 guid = None # Filled in with the GUID of the server (for client protocol) # or the username of the authenticated client (for server protocol) def __init__(self, transport): self.rx_ev = Event() self.ret_val = None self._transport = transport def connect(self, target=None): # self._transport.connect(target) # DBus specification requires that clients send a null byte upon # connection to the bus self._transport.send(b'\0') # do auth # print "authenticating" ClientAuthenticator().authenticate(self) return self def write(self, data): # print msg.rawMessage # self._transport.send(b"GET / HTTP/1.0\r\n'") # print ">>> %r" % data self._transport.send(data) def read(self): # print msg.rawMessage # self._transport.send(b"GET / HTTP/1.0\r\n'") data = self._transport.recv(1024) # print "<<< %r" % data return data def await_result(self): try: if self.rx_ev.is_set(): raise RuntimeError("already pending result") while not self.rx_ev.is_set(): data = self.read() if not data: raise Exception("ConnectionClosed") # print data self.on_data_received(data) return self.ret_val finally: self.rx_ev.clear() def on_data_received(self, data): self._buffer = self._buffer + data buffer_len = len(self._buffer) if self._nextMsgLen == 0 and buffer_len >= 16: # There would be multiple clients using different endians. # Reset endian every time. if self._buffer[:1] != b'l': self._endian = '>' else: self._endian = '<' body_len = struct.unpack(self._endian + 'I', self._buffer[4:8])[0] harr_len = struct.unpack(self._endian + 'I', self._buffer[12:16])[0] hlen = MSG_HDR_LEN + harr_len padlen = hlen % 8 and (8 - hlen % 8) or 0 self._nextMsgLen = (MSG_HDR_LEN + harr_len + padlen + body_len) if self._nextMsgLen != 0 and buffer_len >= self._nextMsgLen: raw_msg = self._buffer[:self._nextMsgLen] self._buffer = self._buffer[self._nextMsgLen:] self._nextMsgLen = 0 self.process_raw_dbus_message(raw_msg) if self._buffer: # Recursively process any other complete messages self.on_data_received(b'') def process_raw_dbus_message(self, rawMsg): """ Called when the raw bytes for a complete DBus message are received @param rawMsg: Byte-string containing the complete message @type rawMsg: C{str} """ m = message.parse_message(rawMsg, self._receivedFDs) mt = m._messageType self._receivedFDs = [] if mt == 1: self.on_method_call_received(m) elif mt == 2: self.on_method_return_received(m) elif mt == 3: self.on_error_received(m) elif mt == 4: self.on_signal_received(m) def teardown(self): self._transport.close() def on_method_call_received(self, mcall): """ Called when a DBus METHOD_CALL message is received """ raise NotImplementedError def on_method_return_received(self, mret): """ Called when a DBus METHOD_RETURN message is received """ self.rx_ev.set() self.ret_val = mret def on_error_received(self, merr): """ Called when a DBus ERROR message is received """ e = RemoteError(merr.error_name) e.message = '' e.values = [] if merr.body: if isinstance(merr.body[0], six.string_types): e.message = merr.body[0] e.values = merr.body raise e def on_signal_received(self, msig): """ Called when a DBus METHOD_CALL message is received """ # raise NotImplementedError print "signal", msig def on_connection_authenticated(self): print "authenticated!!"
class RaidenService: """ A Raiden node. """ def __init__( self, chain, default_registry, private_key_bin, transport, config, discovery=None ): if not isinstance(private_key_bin, bytes) or len(private_key_bin) != 32: raise ValueError('invalid private_key') invalid_timeout = ( config['settle_timeout'] < NETTINGCHANNEL_SETTLE_TIMEOUT_MIN or config['settle_timeout'] > NETTINGCHANNEL_SETTLE_TIMEOUT_MAX ) if invalid_timeout: raise ValueError('settle_timeout must be in range [{}, {}]'.format( NETTINGCHANNEL_SETTLE_TIMEOUT_MIN, NETTINGCHANNEL_SETTLE_TIMEOUT_MAX )) self.tokens_to_connectionmanagers = dict() self.identifier_to_results = defaultdict(list) # This is a map from a secrethash to a list of channels, the same # secrethash can be used in more than one token (for tokenswaps), a # channel should be removed from this list only when the lock is # released/withdrawn but not when the secret is registered. self.token_to_secrethash_to_channels = defaultdict(lambda: defaultdict(list)) self.chain = chain self.default_registry = default_registry self.config = config self.privkey = private_key_bin self.address = privatekey_to_address(private_key_bin) if config['transport_type'] == 'udp': endpoint_registration_event = gevent.spawn( discovery.register, self.address, config['external_ip'], config['external_port'], ) endpoint_registration_event.link_exception(endpoint_registry_exception_handler) self.private_key = PrivateKey(private_key_bin) self.pubkey = self.private_key.public_key.format(compressed=False) self.protocol = transport self.blockchain_events = BlockchainEvents() self.alarm = AlarmTask(chain) self.shutdown_timeout = config['shutdown_timeout'] self._block_number = None self.stop_event = Event() self.start_event = Event() self.chain.client.inject_stop_event(self.stop_event) self.wal = None self.database_path = config['database_path'] if self.database_path != ':memory:': database_dir = os.path.dirname(config['database_path']) os.makedirs(database_dir, exist_ok=True) self.database_dir = database_dir # Prevent concurrent acces to the same db self.lock_file = os.path.join(self.database_dir, '.lock') self.db_lock = filelock.FileLock(self.lock_file) else: self.database_path = ':memory:' self.database_dir = None self.lock_file = None self.serialization_file = None self.db_lock = None if config['transport_type'] == 'udp': # If the endpoint registration fails the node will quit, this must # finish before starting the protocol endpoint_registration_event.join() # Lock used to serialize calls to `poll_blockchain_events`, this is # important to give a consistent view of the node state. self.event_poll_lock = gevent.lock.Semaphore() self.start() def start(self): """ Start the node. """ if self.stop_event and self.stop_event.is_set(): self.stop_event.clear() if self.database_dir is not None: self.db_lock.acquire(timeout=0) assert self.db_lock.is_locked # The database may be :memory: storage = sqlite.SQLiteStorage(self.database_path, serialize.PickleSerializer()) self.wal, unapplied_events = wal.restore_from_latest_snapshot( node.state_transition, storage, ) last_log_block_number = None # First run, initialize the basic state if self.wal.state_manager.current_state is None: block_number = self.chain.block_number() state_change = ActionInitNode( random.Random(), block_number, ) self.wal.log_and_dispatch(state_change, block_number) else: # Get the last known block number after reapplying all the state changes from the log last_log_block_number = views.block_number(self.wal.state_manager.current_state) # The alarm task must be started after the snapshot is loaded or the # state is primed, the callbacks assume the node is initialized. self.alarm.start() self.alarm.register_callback(self.poll_blockchain_events) self.alarm.register_callback(self.set_block_number) self._block_number = self.chain.block_number() # Registry registration must start *after* the alarm task. This # avoids corner cases where the registry is queried in block A, a new # block B is mined, and the alarm starts polling at block C. # If last_log_block_number is None, the wal.state_manager.current_state was # None in the log, meaning we don't have any events we care about, so just # read the latest state from the network self.register_payment_network(self.default_registry.address, last_log_block_number) # Start the protocol after the registry is queried to avoid warning # about unknown channels. queueids_to_queues = views.get_all_messagequeues(views.state_from_raiden(self)) # TODO: remove the cyclic dependency between the protocol and this instance self.protocol.start(self, queueids_to_queues) # Health check needs the protocol layer self.start_neighbours_healthcheck() for event in unapplied_events: on_raiden_event(self, event) self.start_event.set() def start_neighbours_healthcheck(self): for neighbour in views.all_neighbour_nodes(self.wal.state_manager.current_state): if neighbour != ConnectionManager.BOOTSTRAP_ADDR: self.start_health_check_for(neighbour) def stop(self): """ Stop the node. """ # Needs to come before any greenlets joining self.stop_event.set() self.protocol.stop_and_wait() self.alarm.stop_async() wait_for = [self.alarm] wait_for.extend(getattr(self.protocol, 'greenlets', [])) # We need a timeout to prevent an endless loop from trying to # contact the disconnected client gevent.wait(wait_for, timeout=self.shutdown_timeout) # Filters must be uninstalled after the alarm task has stopped. Since # the events are polled by an alarm task callback, if the filters are # uninstalled before the alarm task is fully stopped the callback # `poll_blockchain_events` will fail. # # We need a timeout to prevent an endless loop from trying to # contact the disconnected client try: with gevent.Timeout(self.shutdown_timeout): self.blockchain_events.uninstall_all_event_listeners() except (gevent.timeout.Timeout, RaidenShuttingDown): pass if self.db_lock is not None: self.db_lock.release() def __repr__(self): return '<{} {}>'.format(self.__class__.__name__, pex(self.address)) def set_block_number(self, block_number): state_change = Block(block_number) self.handle_state_change(state_change, block_number) # To avoid races, only update the internal cache after all the state # tasks have been updated. self._block_number = block_number def handle_state_change(self, state_change, block_number=None): is_logging = log.isEnabledFor(logging.DEBUG) if is_logging: log.debug('STATE CHANGE', node=pex(self.address), state_change=state_change) if block_number is None: block_number = self.get_block_number() event_list = self.wal.log_and_dispatch(state_change, block_number) for event in event_list: if is_logging: log.debug('EVENT', node=pex(self.address), event=event) on_raiden_event(self, event) return event_list def set_node_network_state(self, node_address, network_state): state_change = ActionChangeNodeNetworkState(node_address, network_state) self.wal.log_and_dispatch(state_change, self.get_block_number()) def start_health_check_for(self, node_address): self.protocol.start_health_check(node_address) def get_block_number(self): return views.block_number(self.wal.state_manager.current_state) def poll_blockchain_events(self, current_block=None): # pylint: disable=unused-argument with self.event_poll_lock: for event in self.blockchain_events.poll_blockchain_events(): on_blockchain_event(self, event) def sign(self, message): """ Sign message inplace. """ if not isinstance(message, SignedMessage): raise ValueError('{} is not signable.'.format(repr(message))) message.sign(self.private_key, self.address) def register_payment_network(self, registry_address, from_block=None): proxies = get_relevant_proxies( self.chain, self.address, registry_address, ) # Install the filters first to avoid missing changes, as a consequence # some events might be applied twice. self.blockchain_events.add_proxies_listeners(proxies, from_block) token_network_list = list() for manager in proxies.channel_managers: manager_address = manager.address netting_channel_proxies = proxies.channelmanager_nettingchannels[manager_address] network = get_token_network_state_from_proxies(self, manager, netting_channel_proxies) token_network_list.append(network) payment_network = PaymentNetworkState( registry_address, token_network_list, ) state_change = ContractReceiveNewPaymentNetwork(payment_network) self.handle_state_change(state_change) def connection_manager_for_token(self, registry_address, token_address): if not isaddress(token_address): raise InvalidAddress('token address is not valid.') known_token_networks = views.get_token_network_addresses_for( self.wal.state_manager.current_state, registry_address, ) if token_address not in known_token_networks: raise InvalidAddress('token is not registered.') manager = self.tokens_to_connectionmanagers.get(token_address) if manager is None: manager = ConnectionManager(self, registry_address, token_address) self.tokens_to_connectionmanagers[token_address] = manager return manager def leave_all_token_networks(self): state_change = ActionLeaveAllNetworks() self.wal.log_and_dispatch(state_change, self.get_block_number()) def close_and_settle(self): log.info('raiden will close and settle all channels now') self.leave_all_token_networks() connection_managers = [ self.tokens_to_connectionmanagers[token_address] for token_address in self.tokens_to_connectionmanagers ] if connection_managers: waiting.wait_for_settle_all_channels( self, self.alarm.wait_time, ) def mediated_transfer_async( self, registry_address, token_address, amount, target, identifier, ): """ Transfer `amount` between this node and `target`. This method will start an asyncronous transfer, the transfer might fail or succeed depending on a couple of factors: - Existence of a path that can be used, through the usage of direct or intermediary channels. - Network speed, making the transfer sufficiently fast so it doesn't expire. """ async_result = self.start_mediated_transfer( registry_address, token_address, amount, target, identifier, ) return async_result def direct_transfer_async(self, registry_address, token_address, amount, target, identifier): """ Do a direct transfer with target. Direct transfers are non cancellable and non expirable, since these transfers are a signed balance proof with the transferred amount incremented. Because the transfer is non cancellable, there is a level of trust with the target. After the message is sent the target is effectively paid and then it is not possible to revert. The async result will be set to False iff there is no direct channel with the target or the payer does not have balance to complete the transfer, otherwise because the transfer is non expirable the async result *will never be set to False* and if the message is sent it will hang until the target node acknowledge the message. This transfer should be used as an optimization, since only two packets are required to complete the transfer (from the payers perspective), whereas the mediated transfer requires 6 messages. """ self.protocol.start_health_check(target) if identifier is None: identifier = create_default_identifier() direct_transfer = ActionTransferDirect( registry_address, token_address, target, identifier, amount, ) self.handle_state_change(direct_transfer) def start_mediated_transfer( self, registry_address, token_address, amount, target, identifier, ): self.protocol.start_health_check(target) if identifier is None: identifier = create_default_identifier() assert identifier not in self.identifier_to_results async_result = AsyncResult() self.identifier_to_results[identifier].append(async_result) secret = random_secret() init_initiator_statechange = initiator_init( self, identifier, amount, secret, registry_address, token_address, target, ) # TODO: implement the network timeout raiden.config['msg_timeout'] and # cancel the current transfer if it happens (issue #374) # # Dispatch the state change even if there are no routes to create the # wal entry. self.handle_state_change(init_initiator_statechange) return async_result def mediate_mediated_transfer(self, transfer: LockedTransfer): init_mediator_statechange = mediator_init(self, transfer) self.handle_state_change(init_mediator_statechange) def target_mediated_transfer(self, transfer: LockedTransfer): init_target_statechange = target_init(self, transfer) self.handle_state_change(init_target_statechange)
class ReplayProcess(BaseReplayProcess): ''' ReplayProcess - A process spawned for the purpose of replaying data -------------------------------------------------------------------------------- Configurations ============== process: dataset_id: "" # Dataset to be replayed delivery_format: {} # Delivery format to be replayed back (unused for now) query: start_time: 0 # Start time (index value) to be replayed end_time: 0 # End time (index value) to be replayed parameters: [] # List of parameters to form in the granule ''' process_type = 'standalone' publish_limit = 10 dataset_id = None delivery_format = {} start_time = None end_time = None stride_time = None parameters = None stream_id = '' stream_def_id = '' def __init__(self, *args, **kwargs): super(ReplayProcess, self).__init__(*args, **kwargs) self.deserializer = IonObjectDeserializer( obj_registry=get_obj_registry()) self.publishing = Event() self.play = Event() self.end = Event() def on_start(self): ''' Starts the process ''' log.info('Replay Process Started') super(ReplayProcess, self).on_start() dsm_cli = DatasetManagementServiceProcessClient(process=self) pubsub = PubsubManagementServiceProcessClient(process=self) self.dataset_id = self.CFG.get_safe('process.dataset_id', None) self.delivery_format = self.CFG.get_safe('process.delivery_format', {}) self.start_time = self.CFG.get_safe('process.query.start_time', None) self.end_time = self.CFG.get_safe('process.query.end_time', None) self.stride_time = self.CFG.get_safe('process.query.stride_time', None) self.parameters = self.CFG.get_safe('process.query.parameters', None) self.publish_limit = self.CFG.get_safe('process.query.publish_limit', 10) self.tdoa = self.CFG.get_safe('process.query.tdoa', None) self.stream_id = self.CFG.get_safe('process.publish_streams.output', '') self.stream_def = pubsub.read_stream_definition( stream_id=self.stream_id) self.stream_def_id = self.stream_def._id self.publishing.clear() self.play.set() self.end.clear() if self.dataset_id is None: raise BadRequest('dataset_id not specified') self.dataset = dsm_cli.read_dataset(self.dataset_id) self.pubsub = PubsubManagementServiceProcessClient(process=self) @classmethod def get_time_idx(cls, coverage, timeval): temporal_variable = coverage.temporal_parameter_name uom = coverage.get_parameter_context(temporal_variable).uom units = TimeUtils.ts_to_units(uom, timeval) idx = TimeUtils.get_relative_time(coverage, units) return idx @classmethod def _coverage_to_granule(cls, coverage, start_time=None, end_time=None, stride_time=None, fuzzy_stride=True, parameters=None, stream_def_id=None, tdoa=None): slice_ = slice(None) # Defaults to all values # Validations if start_time is not None: validate_is_instance(start_time, Number, 'start_time must be a number for striding.') if end_time is not None: validate_is_instance(end_time, Number, 'end_time must be a number for striding.') if stride_time is not None: validate_is_instance(stride_time, Number, 'stride_time must be a number for striding.') if tdoa is not None and isinstance(tdoa, slice): slice_ = tdoa elif stride_time is not None and not fuzzy_stride: # SLOW ugly_range = np.arange(start_time, end_time, stride_time) idx_values = [cls.get_time_idx(coverage, i) for i in ugly_range] idx_values = list( set(idx_values) ) # Removing duplicates - also mixes the order of the list!!! idx_values.sort() slice_ = [idx_values] elif not (start_time is None and end_time is None): if start_time is not None: start_time = cls.get_time_idx(coverage, start_time) if end_time is not None: end_time = cls.get_time_idx(coverage, end_time) slice_ = slice(start_time, end_time, stride_time) log.info('Slice: %s', slice_) if stream_def_id: rdt = RecordDictionaryTool(stream_definition_id=stream_def_id) else: rdt = RecordDictionaryTool( param_dictionary=coverage.parameter_dictionary) if parameters is not None: # TODO: Improve efficiency here fields = list(set(parameters).intersection(rdt.fields)) else: fields = rdt.fields if slice_.start == slice_.stop and slice_.start is not None: log.warning('Requested empty set of data. %s', slice_) return rdt # Do time first tname = coverage.temporal_parameter_name cls.map_cov_rdt(coverage, rdt, tname, slice_) for field in fields: if field == tname: continue cls.map_cov_rdt(coverage, rdt, field, slice_) return rdt @classmethod def map_cov_rdt(cls, coverage, rdt, field, slice_): log.trace('Slice is %s', slice_) try: n = coverage.get_parameter_values(field, tdoa=slice_) except ParameterFunctionException: return if n is None: rdt[field] = [n] elif isinstance(n, np.ndarray): if coverage.get_data_extents(field)[0] < coverage.num_timesteps: log.error( "Misformed coverage detected, padding with fill_value") arr_len = utils.slice_shape(slice_, (coverage.num_timesteps, ))[0] fill_arr = np.empty(arr_len - n.shape[0], dtype=n.dtype) fill_arr.fill(coverage.get_parameter_context(field).fill_value) n = np.append(n, fill_arr) elif coverage.get_data_extents(field)[0] > coverage.num_timesteps: raise CorruptionError( 'The coverage is corrupted:\n\tfield: %s\n\textents: %s\n\ttimesteps: %s' % (field, coverage.get_data_extents(field), coverage.num_timesteps)) rdt[field] = np.atleast_1d(n) else: rdt[field] = [n] def execute_retrieve(self): ''' execute_retrieve Executes a retrieval and returns the result as a value in lieu of publishing it on a stream ''' try: coverage = DatasetManagementService._get_coverage(self.dataset_id, mode='r') if coverage.num_timesteps == 0: log.info('Reading from an empty coverage') rdt = RecordDictionaryTool( param_dictionary=coverage.parameter_dictionary) else: rdt = self._coverage_to_granule(coverage=coverage, start_time=self.start_time, end_time=self.end_time, stride_time=self.stride_time, parameters=self.parameters, tdoa=self.tdoa) except: log.exception('Problems reading from the coverage') raise BadRequest('Problems reading from the coverage') finally: coverage.close(timeout=5) return rdt.to_granule() def execute_replay(self): ''' execute_replay Performs a replay and publishes the results on a stream. ''' if self.publishing.is_set(): return False gevent.spawn(self.replay) return True def replay(self): self.publishing.set( ) # Minimal state, supposed to prevent two instances of the same process from replaying on the same stream for rdt in self._replay(): if self.end.is_set(): return self.play.wait() self.output.publish(rdt.to_granule()) self.publishing.clear() return def pause(self): self.play.clear() def resume(self): self.play.set() def stop(self): self.end.set() @classmethod def get_last_values(cls, dataset_id, number_of_points, delivery_format): coverage = DatasetManagementService._get_coverage(dataset_id, mode='r') if coverage.num_timesteps < number_of_points: if coverage.num_timesteps == 0: rdt = RecordDictionaryTool( param_dictionary=coverage.parameter_dictionary) return rdt.to_granule() number_of_points = coverage.num_timesteps rdt = cls._coverage_to_granule(coverage, tdoa=slice(-number_of_points, None), stream_def_id=delivery_format) coverage.close(timeout=5) return rdt.to_granule() def _replay(self): coverage = DatasetManagementService._get_coverage(self.dataset_id, mode='r') rdt = self._coverage_to_granule(coverage=coverage, start_time=self.start_time, end_time=self.end_time, stride_time=self.stride_time, parameters=self.parameters, stream_def_id=self.stream_def_id) elements = len(rdt) for i in xrange(elements / self.publish_limit): outgoing = RecordDictionaryTool( stream_definition_id=self.stream_def_id) fields = self.parameters or outgoing.fields for field in fields: v = rdt[field] if v is not None: outgoing[field] = v[( i * self.publish_limit):((i + 1) * self.publish_limit)] yield outgoing coverage.close(timeout=5) return
class Group(GroupMappingMixin): """Maintain a group of greenlets that are still running. Links to each item and removes it upon notification. """ greenlet_class = Greenlet def __init__(self, *args): assert len(args) <= 1, args self.greenlets = set(*args) if args: for greenlet in args[0]: greenlet.rawlink(self._discard) # each item we kill we place in dying, to avoid killing the same greenlet twice self.dying = set() self._empty_event = Event() self._empty_event.set() def __repr__(self): return '<%s at 0x%x %s>' % (self.__class__.__name__, id(self), self.greenlets) def __len__(self): return len(self.greenlets) def __contains__(self, item): return item in self.greenlets def __iter__(self): return iter(self.greenlets) def add(self, greenlet): try: rawlink = greenlet.rawlink except AttributeError: pass # non-Greenlet greenlet, like MAIN else: rawlink(self._discard) self.greenlets.add(greenlet) self._empty_event.clear() def _discard(self, greenlet): self.greenlets.discard(greenlet) self.dying.discard(greenlet) if not self.greenlets: self._empty_event.set() def discard(self, greenlet): self._discard(greenlet) try: unlink = greenlet.unlink except AttributeError: pass # non-Greenlet greenlet, like MAIN else: unlink(self._discard) def start(self, greenlet): """ Start the un-started *greenlet* and add it to the collection of greenlets this group is monitoring. """ self.add(greenlet) greenlet.start() def spawn(self, *args, **kwargs): """ Begin a new greenlet with the given arguments (which are passed to the greenlet constructor) and add it to the collection of greenlets this group is monitoring. :return: The newly started greenlet. """ greenlet = self.greenlet_class(*args, **kwargs) self.start(greenlet) return greenlet # def close(self): # """Prevents any more tasks from being submitted to the pool""" # self.add = RaiseException("This %s has been closed" % self.__class__.__name__) def join(self, timeout=None, raise_error=False): if raise_error: greenlets = self.greenlets.copy() self._empty_event.wait(timeout=timeout) for greenlet in greenlets: if greenlet.exception is not None: if hasattr(greenlet, '_raise_exception'): greenlet._raise_exception() raise greenlet.exception else: self._empty_event.wait(timeout=timeout) def kill(self, exception=GreenletExit, block=True, timeout=None): timer = Timeout.start_new(timeout) try: try: while self.greenlets: for greenlet in list(self.greenlets): if greenlet not in self.dying: try: kill = greenlet.kill except AttributeError: _kill(greenlet, exception) else: kill(exception, block=False) self.dying.add(greenlet) if not block: break joinall(self.greenlets) except Timeout as ex: if ex is not timer: raise finally: timer.cancel() def killone(self, greenlet, exception=GreenletExit, block=True, timeout=None): if greenlet not in self.dying and greenlet in self.greenlets: greenlet.kill(exception, block=False) self.dying.add(greenlet) if block: greenlet.join(timeout) def full(self): return False def wait_available(self): pass # MappingMixin methods def _apply_immediately(self): # If apply() is called from one of our own # worker greenlets, don't spawn a new one return getcurrent() in self def _apply_async_cb_spawn(self, callback, result): Greenlet.spawn(callback, result) def _apply_async_use_greenlet(self): return self.full() # cannot call self.spawn() because it will block
class Console(BaseService): """A service starting an interactive ipython session when receiving the SIGSTP signal (e.g. via keyboard shortcut CTRL-Z). """ name = 'console' def __init__(self, app): super(Console, self).__init__(app) self.interrupt = Event() self.console_locals = {} if app.start_console: self.start() self.interrupt.set() else: SigINTHandler(self.interrupt) def start(self): # start console service super(Console, self).start() class Raiden(object): def __init__(self, app): self.app = app self.console_locals = dict( _raiden=Raiden(self.app), raiden=self.app.raiden, chain=self.app.raiden.chain, discovery=self.app.discovery, tools=ConsoleTools( self.app.raiden, self.app.discovery, self.app.config['settle_timeout'], self.app.config['reveal_timeout'], ), denoms=denoms, true=True, false=False, usage=print_usage, ) def _run(self): self.interrupt.wait() print('\n' * 2) print("Entering Console" + bc.OKGREEN) print("Tip:" + bc.OKBLUE) print_usage() # Remove handlers that log to stderr root = getLogger() for handler in root.handlers[:]: if isinstance(handler, StreamHandler) and handler.stream == sys.stderr: root.removeHandler(handler) stream = cStringIO.StringIO() handler = StreamHandler(stream=stream) handler.formatter = Formatter("%(levelname)s:%(name)s %(message)s") root.addHandler(handler) def lastlog(n=10, prefix=None, level=None): """Print the last `n` log lines to stdout. Use `prefix='p2p'` to filter for a specific logger. Use `level=INFO` to filter for a specific level. Level- and prefix-filtering are applied before tailing the log. """ lines = (stream.getvalue().strip().split('\n') or []) if prefix: lines = [ line for line in lines if line.split(':')[1].startswith(prefix) ] if level: lines = [line for line in lines if line.split(':')[0] == level] for line in lines[-n:]: print(line) self.console_locals['lastlog'] = lastlog err = cStringIO.StringIO() sys.stderr = err def lasterr(n=1): """Print the last `n` entries of stderr to stdout. """ for line in (err.getvalue().strip().split('\n') or [])[-n:]: print(line) self.console_locals['lasterr'] = lasterr IPython.start_ipython(argv=['--gui', 'gevent'], user_ns=self.console_locals) self.interrupt.clear() sys.exit(0)
class AceClient(object): def __init__(self, host, port, connect_timeout=5, result_timeout=10): # Receive buffer self._recvbuffer = None # Stream URL self._url = None # Ace stream socket self._socket = None # Result timeout self._resulttimeout = result_timeout # Shutting down flag self._shuttingDown = Event() # Product key self._product_key = None # Current STATUS self._status = None # Current STATE self._state = None # Current video position self._position = None # Available video position (loaded data) self._position_last = None # Buffered video pieces self._position_buf = None # Current AUTH self._auth = None self._gender = None self._age = None # Result (Created with AsyncResult() on call) self._result = AsyncResult() self._authevent = Event() # Result for getURL() self._urlresult = AsyncResult() # Result for GETCID() self._cidresult = AsyncResult() # Event for resuming from PAUSE self._resumeevent = Event() # Seekback seconds. self._seekback = 0 # Did we get START command again? For seekback. self._started_again = False self._idleSince = time.time() self._lock = threading.Condition(threading.Lock()) self._streamReaderConnection = None self._streamReaderState = None self._streamReaderQueue = deque() self._engine_version_code = 0 # Logger logger = logging.getLogger('AceClieimport tracebacknt_init') try: self._socket = telnetlib.Telnet(host, port, connect_timeout) logger.info("Successfully connected with Ace!") except Exception as e: raise AceException("Socket creation error! Ace is not running? " + repr(e)) # Spawning recvData greenlet gevent.spawn(self._recvData) gevent.sleep() def __del__(self): # Destructor just calls destroy() method self.destroy() def destroy(self): ''' AceClient Destructor ''' if self._shuttingDown.isSet(): # Already in the middle of destroying return # Logger logger = logging.getLogger("AceClient_destroy") # We should resume video to prevent read greenlet deadlock self._resumeevent.set() # And to prevent getUrl deadlock self._urlresult.set() # Trying to disconnect try: logger.debug("Destroying client...") self._shuttingDown.set() self._write(AceMessage.request.SHUTDOWN) except: # Ignore exceptions on destroy pass finally: self._shuttingDown.set() def reset(self): self._started_again = False self._idleSince = time.time() self._streamReaderState = None def _write(self, message): try: logger = logging.getLogger("AceClient_write") logger.debug(message) self._socket.write(message + "\r\n") except EOFError as e: raise AceException("Write error! " + repr(e)) def aceInit(self, gender=AceConst.SEX_MALE, age=AceConst.AGE_18_24, product_key=None, pause_delay=0, seekback=0): self._product_key = product_key self._gender = gender self._age = age # PAUSE/RESUME delay self._pausedelay = pause_delay # Seekback seconds self._seekback = seekback # Logger logger = logging.getLogger("AceClient_aceInit") # Sending HELLO self._write(AceMessage.request.HELLO) if not self._authevent.wait(self._resulttimeout): errmsg = "Authentication timeout. Wrong key?" logger.error(errmsg) raise AceException(errmsg) return if not self._auth: errmsg = "Authentication error. Wrong key?" logger.error(errmsg) raise AceException(errmsg) return logger.debug("aceInit ended") def _getResult(self): # Logger try: result = self._result.get(timeout=self._resulttimeout) if not result: raise AceException("Result not received") except gevent.Timeout: raise AceException("Timeout") return result def START(self, datatype, value): ''' Start video method ''' stream_type = 'output_format=http' if self._engine_version_code >= 3010500 and not AceConfig.vlcuse else '' self._urlresult = AsyncResult() self._write( AceMessage.request.START(datatype.upper(), value, stream_type)) self._getResult() def STOP(self): ''' Stop video method ''' if self._state is not None and self._state != '0': self._result = AsyncResult() self._write(AceMessage.request.STOP) self._getResult() def LOADASYNC(self, datatype, url): self._result = AsyncResult() self._write( AceMessage.request.LOADASYNC(datatype.upper(), 0, {'url': url})) return self._getResult() def GETCID(self, datatype, url): contentinfo = self.LOADASYNC(datatype, url) self._cidresult = AsyncResult() self._write( AceMessage.request.GETCID(contentinfo.get('checksum'), contentinfo.get('infohash'), 0, 0, 0)) cid = self._cidresult.get(True, 5) return '' if not cid or cid == '' else cid[2:] def GETCONTENTINFO(self, datatype, url): contentinfo = self.LOADASYNC(datatype, url) return contentinfo def getUrl(self, timeout=40): # Logger logger = logging.getLogger("AceClient_getURL") try: res = self._urlresult.get(timeout=timeout) return res except gevent.Timeout: errmsg = "getURL timeout!" logger.error(errmsg) raise AceException(errmsg) def startStreamReader(self, url, cid, counter): logger = logging.getLogger("StreamReader") self._streamReaderState = 1 logger.debug("Opening video stream: %s" % url) try: connection = self._streamReaderConnection = urllib2.urlopen(url) if url.endswith('.m3u8'): logger.debug("Can't stream HLS in non VLC mode: %s" % url) return if connection.getcode() != 200: logger.error("Failed to open video stream %s" % connection) return with self._lock: self._streamReaderState = 2 self._lock.notifyAll() while True: data = None clients = counter.getClients(cid) try: data = connection.read(AceConfig.readchunksize) except: break if data and clients: with self._lock: if len(self._streamReaderQueue ) == AceConfig.readcachesize: self._streamReaderQueue.popleft() self._streamReaderQueue.append(data) for c in clients: try: c.addChunk(data, 5.0) except Queue.Full: if len(clients) > 1: logger.debug("Disconnecting client: %s" % str(c)) c.destroy() elif not clients: logger.debug( "All clients disconnected - closing video stream") break else: logger.warning("No data received") break except urllib2.URLError: logger.error("Failed to open video stream") logger.error(traceback.format_exc()) except: logger.error(traceback.format_exc()) if counter.getClients(cid): logger.error("Failed to read video stream") finally: self.closeStreamReader() with self._lock: self._streamReaderState = 3 self._lock.notifyAll() counter.deleteAll(cid) def closeStreamReader(self): logger = logging.getLogger("StreamReader") c = self._streamReaderConnection if c: self._streamReaderConnection = None c.close() logger.debug("Video stream closed") self._streamReaderQueue.clear() def getPlayEvent(self, timeout=None): ''' Blocking while in PAUSE, non-blocking while in RESUME ''' return self._resumeevent.wait(timeout=timeout) def pause(self): self._write(AceMessage.request.PAUSE) def play(self): self._write(AceMessage.request.PLAY) def _recvData(self): ''' Data receiver method for greenlet ''' logger = logging.getLogger('AceClient_recvdata') while True: gevent.sleep() try: self._recvbuffer = self._socket.read_until("\r\n") self._recvbuffer = self._recvbuffer.strip() # logger.debug('<<< ' + self._recvbuffer) except: # If something happened during read, abandon reader. if not self._shuttingDown.isSet(): logger.error("Exception at socket read") self._shuttingDown.set() return if self._recvbuffer: # Parsing everything only if the string is not empty if self._recvbuffer.startswith(AceMessage.response.HELLO): # Parse HELLO if 'version_code=' in self._recvbuffer: v = self._recvbuffer.find('version_code=') self._engine_version_code = int( self._recvbuffer[v + 13:v + 20]) if 'key=' in self._recvbuffer: self._request_key_begin = self._recvbuffer.find('key=') self._request_key = \ self._recvbuffer[self._request_key_begin + 4:self._request_key_begin + 14] try: self._write( AceMessage.request.READY_key( self._request_key, self._product_key)) except urllib2.URLError as e: logger.error("Can't connect to keygen server! " + \ repr(e)) self._auth = False self._authevent.set() self._request_key = None else: self._write(AceMessage.request.READY_nokey) elif self._recvbuffer.startswith(AceMessage.response.NOTREADY): # NOTREADY logger.error("Ace is not ready. Wrong auth?") self._auth = False self._authevent.set() elif self._recvbuffer.startswith(AceMessage.response.LOADRESP): # LOADRESP _contentinfo_raw = self._recvbuffer.split()[2:] _contentinfo_raw = ' '.join(_contentinfo_raw) _contentinfo = json.loads(_contentinfo_raw) if _contentinfo.get('status') == 100: logger.error( "LOADASYNC returned error with message: %s" % _contentinfo.get('message')) self._result.set(False) else: logger.debug("Content info: %s", _contentinfo) self._result.set(_contentinfo) elif self._recvbuffer.startswith(AceMessage.response.START): # START if not self._seekback or self._started_again or not self._recvbuffer.endswith( ' stream=1'): # If seekback is disabled, we use link in first START command. # If seekback is enabled, we wait for first START command and # ignore it, then do seeback in first EVENT position command # AceStream sends us STOP and START again with new link. # We use only second link then. try: self._url = self._recvbuffer.split()[1] self._urlresult.set(self._url) self._resumeevent.set() except IndexError as e: self._url = None else: logger.debug("START received. Waiting for %s." % AceMessage.response.LIVEPOS) elif self._recvbuffer.startswith(AceMessage.response.STOP): pass elif self._recvbuffer.startswith(AceMessage.response.SHUTDOWN): logger.debug("Got SHUTDOWN from engine") self._socket.close() return elif self._recvbuffer.startswith(AceMessage.response.AUTH): try: self._auth = self._recvbuffer.split()[1] # Send USERDATA here self._write( AceMessage.request.USERDATA( self._gender, self._age)) except: pass self._authevent.set() elif self._recvbuffer.startswith( AceMessage.response.GETUSERDATA): raise AceException("You should init me first!") elif self._recvbuffer.startswith(AceMessage.response.LIVEPOS): self._position = self._recvbuffer.split() self._position_last = self._position[2].split('=')[1] self._position_buf = self._position[9].split('=')[1] self._position = self._position[4].split('=')[1] if self._seekback and not self._started_again: self._write(AceMessage.request.SEEK(str(int(self._position_last) - \ self._seekback))) logger.debug('Seeking back') self._started_again = True elif self._recvbuffer.startswith(AceMessage.response.STATE): self._state = self._recvbuffer.split()[1] elif self._recvbuffer.startswith(AceMessage.response.STATUS): self._tempstatus = self._recvbuffer.split()[1].split( ';')[0] if self._tempstatus != self._status: self._status = self._tempstatus logger.debug("STATUS changed to " + self._status) if self._status == 'main:err': logger.error(self._status + ' with message ' + self._recvbuffer.split(';')[2]) self._result.set_exception( AceException(self._status + ' with message ' + self._recvbuffer.split(';')[2])) self._urlresult.set_exception( AceException(self._status + ' with message ' + self._recvbuffer.split(';')[2])) elif self._status == 'main:starting': self._result.set(True) elif self._status == 'main:idle': self._result.set(True) elif self._recvbuffer.startswith(AceMessage.response.PAUSE): logger.debug("PAUSE event") self._resumeevent.clear() elif self._recvbuffer.startswith(AceMessage.response.RESUME): logger.debug("RESUME event") gevent.sleep(self._pausedelay) self._resumeevent.set() elif self._recvbuffer.startswith('##') or len( self._recvbuffer) == 0: self._cidresult.set(self._recvbuffer) logger.debug("CID: %s" % self._recvbuffer) def sendHeadersPT(self, client, code, headers): client.handler.send_response(code) for key, value in headers.items(): client.handler.send_header(key, value) client.handler.end_headers() def openStreamReaderPT(self, url, req_headers): logger = logging.getLogger("openStreamReaderPT") logger.debug("Opening video stream: %s" % url) logger.debug("headers: %s" % req_headers) if url.endswith('.m3u8'): logger.debug("Can't stream HLS in non VLC mode: %s" % url) return None, None, None request = urllib2.Request(url, headers=req_headers) connection = self._streamReaderConnection = urllib2.urlopen( request, timeout=120) code = connection.getcode() if code not in (200, 206): logger.error("Failed to open video stream %s" % connection) return None, None, None FORWARD_HEADERS = [ 'Content-Range', 'Connection', 'Keep-Alive', 'Content-Type', 'Accept-Ranges', 'X-Content-Duration', 'Content-Length', ] SKIP_HEADERS = ['Server', 'Date'] response_headers = {} for k in connection.info().headers: if k.split(':')[0] not in (FORWARD_HEADERS + SKIP_HEADERS): logger.debug('NEW HEADERS: %s' % k.split(':')[0]) for h in FORWARD_HEADERS: if connection.info().getheader(h): response_headers[h] = connection.info().getheader(h) # self.connection.send_header(h, connection.info().getheader(h)) logger.debug('key=%s value=%s' % (h, connection.info().getheader(h))) with self._lock: self._streamReaderState = 2 self._lock.notifyAll() return connection, code, response_headers def startStreamReaderPT(self, url, cid, counter, req_headers=None): logger = logging.getLogger("StreamReaderPT") self._streamReaderState = 1 # current_req_headers = req_headers try: while True: data = None clients = counter.getClientsPT(cid) if not req_headers == self.req_headers: self.req_headers = req_headers connection, code, resp_headers = self.openStreamReaderPT( url, req_headers) if not connection: return for c in clients: try: c.headers_sent except: self.sendHeadersPT(c, code, resp_headers) c.headers_sent = True # logger.debug("i") try: data = connection.read(AceConfig.readchunksize) except: break # logger.debug("d: %s c:%s" % (data, clients)) if data and clients: with self._lock: if len(self._streamReaderQueue ) == AceConfig.readcachesize: self._streamReaderQueue.popleft() self._streamReaderQueue.append(data) for c in clients: try: c.addChunk(data, 5.0) except Queue.Full: if len(clients) > 1: logger.debug("Disconnecting client: %s" % str(c)) c.destroy() elif not clients: logger.debug( "All clients disconnected - closing video stream") break else: logger.warning("No data received") break except urllib2.URLError: logger.error("Failed to open video stream") logger.error(traceback.format_exc()) except: logger.error(traceback.format_exc()) if counter.getClientsPT(cid): logger.error("Failed to read video stream") finally: self.closeStreamReader() with self._lock: self._streamReaderState = 3 self._lock.notifyAll() counter.deleteAll(cid)
def run_( data_path: Path, auth: str, password: Optional[str], keystore_file: str, scenario_file: Path, enable_ui: bool, password_file: str, log_file_name: str, environment: EnvironmentConfig, delete_snapshots: bool, raiden_client: Optional[str], smoketest_deployment_data=None, ) -> None: """Execute a scenario as defined in scenario definition file. (Shared code for `run` and `smoketest` command). Calls :func:`exit` when done, with the following status codes: Exit code 1x There was a problem when starting up the SP, nodes, deploying tokens or setting up services. This points at an issue in the SP and of of its components. Exit code 2x There was an error when parsing or evaluating the given scenario definition file. This may be a syntax- or logic-related issue. Exit code 3x There was an assertion error while executing the scenario. This points to an error in a `raiden` component (the client, services or contracts). """ log.info("Scenario Player version:", version_info=get_complete_spec()) password = get_password(password, password_file) account = get_account(keystore_file, password) log_buffer = None if enable_ui: log_buffer = attach_urwid_logbuffer() # Dynamically import valid Task classes from scenario_player.tasks package. collect_tasks(tasks) # Start our Services report: Dict[str, str] = {} success = Event() success.clear() try: # We need to fix the log stream early in case the UI is active scenario_runner = ScenarioRunner( account=account, auth=auth, data_path=data_path, scenario_file=scenario_file, environment=environment, success=success, smoketest_deployment_data=smoketest_deployment_data, delete_snapshots=delete_snapshots, raiden_client=raiden_client, ) if enable_ui: ui: AbstractContextManager = ScenarioUIManager( scenario_runner, log_buffer, log_file_name, success ) else: ui = nullcontext() log.info("Startup complete") with ui: scenario_runner.run_scenario() except ScenarioAssertionError as ex: log.error("Run finished", result="assertion errors") if hasattr(ex, "exit_code"): exit_code = ex.exit_code else: exit_code = 30 report.update(dict(subject=f"Assertion mismatch in {scenario_file.name}", message=str(ex))) exit(exit_code) except ScenarioError as ex: log.error("Run finished", result="scenario error", message=str(ex)) if hasattr(ex, "exit_code"): exit_code = ex.exit_code else: exit_code = 20 report.update( dict( subject=f"Invalid scenario {scenario_file.name}", message=traceback.format_exc(), ) ) exit(exit_code) except Exception as ex: log.exception("Exception while running scenario") if hasattr(ex, "exit_code"): exit_code = ex.exit_code # type: ignore # pylint: disable=no-member else: exit_code = 10 report.update( dict( subject=f"Error running scenario {scenario_file.name}", message=traceback.format_exc(), ) ) exit(exit_code) else: exit_code = 0 log.info("Run finished", result="success") report.update(dict(subject=f"Scenario successful {scenario_file.name}", message="Success")) log.info("Scenario player unwind complete") exit(exit_code)
class AceClient(object): def __init__(self, clientcounter, ace, connect_timeout=5, result_timeout=10): # Telnet socket response buffer self._recvbuffer = None # AceEngine socket self._socket = None # AceEngine read result timeout self._resulttimeout = result_timeout # AceEngine product key self._product_key = None # Result (Created with AsyncResult() on call) self._auth = AsyncResult() # Result for START URL self._url = AsyncResult() # Response time from AceEngine to get URL or DATA self._videotimeout = None # Result for CID self._cid = AsyncResult() # Result fo LOADASYNC self._loadasync = AsyncResult() # Current STATUS self._status = AsyncResult() # Current EVENT self._event = AsyncResult() # Current STATE self._state = AsyncResult() # Current AUTH self._gender = self._age = None # Seekback seconds. self._seekback = None # Did we get START command again? For seekback. self._started_again = Event() # ClientCounter self._clientcounter = clientcounter # AceConfig.ace self._ace = ace try: self._socket = Telnet(self._ace['aceHostIP'], self._ace['aceAPIport'], connect_timeout) logging.debug('Successfully connected to AceStream on %s:%s' % (self._ace['aceHostIP'], self._ace['aceAPIport'])) except: errmsg = 'The are no alive AceStream Engines found!' raise AceException(errmsg) def destroy(self): ''' AceClient Destructor ''' # Send SHUTDOWN to AceEngine try: self._write(AceMessage.request.SHUTDOWN) except: pass # Ignore exceptions on destroy finally: self._clientcounter.idleAce = None def reset(self): ''' Reset initial values ''' self._started_again.clear() self._url.set() self._loadasync.set() self._cid.set() self._status = AsyncResult() def _write(self, message): try: self._socket.write('%s\r\n' % message) logging.debug('>>> %s' % message) except gevent.socket.error: raise AceException('Error writing data to AceEngine API port') def _recvError(self): raise AceException('Error reading data from AceEngine API port') def aceInit(self, gender=AceConst.SEX_MALE, age=AceConst.AGE_25_34, product_key=None, videoseekback=0, videotimeout=30): self._gender = gender self._age = age self._product_key = product_key self._seekback = videoseekback self._videotimeout = videotimeout self._started_again.clear() # Spawning telnet data reader with recvbuffer read timeout (allowable STATE 0 (IDLE) time) gevent.spawn( wrap_errors((EOFError, gevent.socket.error), self._recvData), self._videotimeout).link_exception(lambda x: self._recvError()) self._auth = AsyncResult() self._write(AceMessage.request.HELLO) # Sending HELLOBG try: params = self._auth.get(timeout=self._resulttimeout) except gevent.Timeout as t: errmsg = 'Engine response time %s exceeded. HELLOTS not resived!' % t raise AceException(errmsg) self._auth = AsyncResult() self._write( AceMessage.request.READY(params.get('key', ''), self._product_key)) try: if self._auth.get( timeout=self._resulttimeout ) == 'NOTREADY': # Get NOTREADY instead AUTH user_auth_level errmsg = 'NOTREADY recived from AceEngine! Wrong acekey?' raise AceException(errmsg) except gevent.Timeout as t: errmsg = 'Engine response time %s exceeded. AUTH not resived!' % t raise AceException(errmsg) if int(params.get('version_code', 0)) >= 3003600: # Display download_stopped massage params_dict = {'use_stop_notifications': '1'} self._write(AceMessage.request.SETOPTIONS(params_dict)) def START(self, command, paramsdict, acestreamtype): ''' Start video method. Get url for play from AceEngine ''' paramsdict['stream_type'] = ' '.join( ['{}={}'.format(k, v) for k, v in acestreamtype.items()]) self._url = AsyncResult() self._write(AceMessage.request.START(command.upper(), paramsdict)) try: return self._url.get(timeout=self._videotimeout) except gevent.Timeout as t: errmsg = 'START URL not received! Engine response time %s exceeded' % t raise AceException(errmsg) def STOP(self): ''' Stop video method ''' self._state = AsyncResult() self._write(AceMessage.request.STOP) try: self._state.get(timeout=self._resulttimeout) except gevent.Timeout as t: errmsg = 'Engine response time %s exceeded. STATE 0 (IDLE) not resived!' % t raise AceException(errmsg) def LOADASYNC(self, command, params, sessionid='0'): self._loadasync = AsyncResult() self._write( AceMessage.request.LOADASYNC(command.upper(), sessionid, params)) try: return self._loadasync.get( timeout=self._resulttimeout) # Get _contentinfo json except gevent.Timeout as t: errmsg = 'Engine response %s time exceeded. LOADARESP not resived!' % t raise AceException(errmsg) def GETCONTENTINFO(self, command, value, sessionid='0'): paramsdict = { command: value, 'developer_id': '0', 'affiliate_id': '0', 'zone_id': '0' } return self.LOADASYNC(command, paramsdict, sessionid) def GETCID(self, command, value): contentinfo = self.GETCONTENTINFO(command, value) if contentinfo['status'] in (1, 2): paramsdict = { 'checksum': contentinfo['checksum'], 'infohash': contentinfo['infohash'], 'developer_id': '0', 'affiliate_id': '0', 'zone_id': '0' } self._cid = AsyncResult() self._write(AceMessage.request.GETCID(paramsdict)) try: return self._cid.get(timeout=self._resulttimeout)[2:] # ##CID except gevent.Timeout as t: errmsg = 'Engine response time %s exceeded. CID not resived!' % t raise AceException(errmsg) else: errmsg = 'LOADASYNC returned error with message: %s' % contentinfo[ 'message'] raise AceException(errmsg) def GETINFOHASH(self, command, value, sessionid='0', idx=0): contentinfo = self.GETCONTENTINFO(command, value, sessionid) if contentinfo['status'] in (1, 2): return contentinfo['infohash'], [ x[0] for x in contentinfo['files'] if x[1] == int(idx) ][0] elif contentinfo['status'] == 0: errmsg = 'LOADASYNC returned status 0: The transport file does not contain audio/video files' raise AceException(errmsg) else: errmsg = 'LOADASYNC returned error with message: %s' % contentinfo[ 'message'] raise AceException(errmsg) def _recvData(self, timeout=30): ''' Data receiver method for greenlet ''' while 1: # Destroy socket connection if AceEngine STATE 0 (IDLE) and we didn't read anything from socket until Nsec with gevent.Timeout(timeout, False): try: self._recvbuffer = self._socket.read_until('\r\n', None).strip() except gevent.Timeout: self.destroy() except gevent.socket.timeout: pass except: raise else: logging.debug('<<< %s' % unquote(self._recvbuffer)) # Parsing everything only if the string is not empty # HELLOTS if self._recvbuffer.startswith('HELLOTS'): #version=engine_version version_code=version_code key=request_key http_port=http_port self._auth.set({ k: v for k, v in (x.split('=') for x in self._recvbuffer.split() if '=' in x) }) # NOTREADY elif self._recvbuffer.startswith('NOTREADY'): self._auth.set('NOTREADY') # AUTH elif self._recvbuffer.startswith('AUTH'): self._auth.set( self._recvbuffer.split()[1]) # user_auth_level # START elif self._recvbuffer.startswith('START'): # url [ad=1 [interruptable=1]] [stream=1] [pos=position] params = { k: v for k, v in (x.split('=') for x in self._recvbuffer.split() if '=' in x) } if not self._seekback or self._started_again.ready( ) or params.get('stream', '') is not '1': # If seekback is disabled, we use link in first START command. # If seekback is enabled, we wait for first START command and # ignore it, then do seekback in first EVENT position command # AceStream sends us STOP and START again with new link. # We use only second link then. self._url.set( self._recvbuffer.split()[1]) # url for play # LOADRESP elif self._recvbuffer.startswith('LOADRESP'): self._loadasync.set( json.loads( unquote(''.join( self._recvbuffer.split()[2:])))) # STATE elif self._recvbuffer.startswith('STATE'): self._state.set(AceConst.STATE[self._recvbuffer.split( )[1]]) # STATE state_id -> STATE_NAME # STATUS elif self._recvbuffer.startswith('STATUS'): self._tempstatus = self._recvbuffer.split()[1] stat = [self._tempstatus.split(';')[0].split(':')[1] ] # main:???? if self._tempstatus.startswith('main:idle'): pass elif self._tempstatus.startswith('main:loading'): pass elif self._tempstatus.startswith('main:starting'): pass elif self._tempstatus.startswith('main:check'): pass elif self._tempstatus.startswith('main:err'): pass # err;error_id;error_message elif self._tempstatus.startswith('main:dl'): #dl; stat.extend( map(int, self._tempstatus.split(';')[1:])) elif self._tempstatus.startswith( 'main:wait'): #wait;time; stat.extend( map(int, self._tempstatus.split(';')[2:])) elif self._tempstatus.startswith( ('main:prebuf', 'main:buf')): #buf;progress;time; stat.extend( map(int, self._tempstatus.split(';')[3:])) if len(stat) == len(AceConst.STATUS): self._status.set({ k: v for k, v in zip(AceConst.STATUS, stat) }) # dl, wait, buf, prebuf else: self._status.set( {'status': stat[0]}) # idle, loading, starting, check # CID elif self._recvbuffer.startswith('##'): self._cid.set(self._recvbuffer) # INFO elif self._recvbuffer.startswith('INFO'): pass # EVENT elif self._recvbuffer.startswith('EVENT'): self._tempevent = self._recvbuffer.split() if self._seekback and not self._started_again.ready( ) and 'livepos' in self._tempevent: params = { k: v for k, v in (x.split('=') for x in self._tempevent if '=' in x) } self._write( AceMessage.request.LIVESEEK( int(params['last']) - self._seekback)) self._started_again.set() elif 'getuserdata' in self._tempevent: self._write( AceMessage.request.USERDATA( self._gender, self._age)) elif 'cansave' in self._tempevent: pass elif 'showurl' in self._tempevent: pass elif 'download_stopped' in self._tempevent: pass # PAUSE elif self._recvbuffer.startswith('PAUSE'): pass #self._write(AceMessage.request.EVENT('pause')) # RESUME elif self._recvbuffer.startswith('RESUME'): pass #self._write(AceMessage.request.EVENT('play')) # STOP elif self._recvbuffer.startswith('STOP'): pass #self._write(AceMessage.request.EVENT('stop')) # SHUTDOWN elif self._recvbuffer.startswith('SHUTDOWN'): self._socket.close() break finally: gevent.sleep()
class RaidenService(Runnable): """ A Raiden node. """ def __init__( self, chain: BlockChainService, query_start_block: typing.BlockNumber, default_registry: TokenNetworkRegistry, default_secret_registry: SecretRegistry, private_key_bin, transport, config, discovery=None, ): super().__init__() if not isinstance(private_key_bin, bytes) or len(private_key_bin) != 32: raise ValueError('invalid private_key') self.tokennetworkids_to_connectionmanagers = dict() self.identifier_to_results: typing.Dict[typing.PaymentID, AsyncResult, ] = dict() self.chain: BlockChainService = chain self.default_registry = default_registry self.query_start_block = query_start_block self.default_secret_registry = default_secret_registry self.config = config self.privkey = private_key_bin self.address = privatekey_to_address(private_key_bin) self.discovery = discovery self.private_key = PrivateKey(private_key_bin) self.pubkey = self.private_key.public_key.format(compressed=False) self.transport = transport self.blockchain_events = BlockchainEvents() self.alarm = AlarmTask(chain) self.stop_event = Event() self.stop_event.set() # inits as stopped self.wal = None self.snapshot_group = 0 # This flag will be used to prevent the service from processing # state changes events until we know that pending transactions # have been dispatched. self.dispatch_events_lock = Semaphore(1) self.database_path = config['database_path'] if self.database_path != ':memory:': database_dir = os.path.dirname(config['database_path']) os.makedirs(database_dir, exist_ok=True) self.database_dir = database_dir # Prevent concurrent access to the same db self.lock_file = os.path.join(self.database_dir, '.lock') self.db_lock = filelock.FileLock(self.lock_file) else: self.database_path = ':memory:' self.database_dir = None self.lock_file = None self.serialization_file = None self.db_lock = None self.event_poll_lock = gevent.lock.Semaphore() def start(self): """ Start the node synchronously. Raises directly if anything went wrong on startup """ if not self.stop_event.ready(): raise RuntimeError(f'{self!r} already started') self.stop_event.clear() if self.database_dir is not None: self.db_lock.acquire(timeout=0) assert self.db_lock.is_locked # start the registration early to speed up the start if self.config['transport_type'] == 'udp': endpoint_registration_greenlet = gevent.spawn( self.discovery.register, self.address, self.config['transport']['udp']['external_ip'], self.config['transport']['udp']['external_port'], ) # The database may be :memory: storage = sqlite.SQLiteStorage(self.database_path, serialize.JSONSerializer()) self.wal = wal.restore_from_latest_snapshot( node.state_transition, storage, ) if self.wal.state_manager.current_state is None: log.debug( 'No recoverable state available, created inital state', node=pex(self.address), ) block_number = self.chain.block_number() state_change = ActionInitChain( random.Random(), block_number, self.chain.node_address, self.chain.network_id, ) self.wal.log_and_dispatch(state_change) payment_network = PaymentNetworkState( self.default_registry.address, [], # empty list of token network states as it's the node's startup ) state_change = ContractReceiveNewPaymentNetwork( constants.NULL_HASH_BYTES, payment_network, ) self.handle_state_change(state_change) # On first run Raiden needs to fetch all events for the payment # network, to reconstruct all token network graphs and find opened # channels last_log_block_number = 0 else: # The `Block` state change is dispatched only after all the events # for that given block have been processed, filters can be safely # installed starting from this position without losing events. last_log_block_number = views.block_number( self.wal.state_manager.current_state) log.debug( 'Restored state from WAL', last_restored_block=last_log_block_number, node=pex(self.address), ) known_networks = views.get_payment_network_identifiers( views.state_from_raiden(self)) if known_networks and self.default_registry.address not in known_networks: configured_registry = pex(self.default_registry.address) known_registries = lpex(known_networks) raise RuntimeError( f'Token network address mismatch.\n' f'Raiden is configured to use the smart contract ' f'{configured_registry}, which conflicts with the current known ' f'smart contracts {known_registries}', ) # Clear ref cache & disable caching serialize.RaidenJSONDecoder.ref_cache.clear() serialize.RaidenJSONDecoder.cache_object_references = False # Restore the current snapshot group state_change_qty = self.wal.storage.count_state_changes() self.snapshot_group = state_change_qty // SNAPSHOT_STATE_CHANGES_COUNT # Install the filters using the correct from_block value, otherwise # blockchain logs can be lost. self.install_all_blockchain_filters( self.default_registry, self.default_secret_registry, last_log_block_number, ) # Complete the first_run of the alarm task and synchronize with the # blockchain since the last run. # # Notes about setup order: # - The filters must be polled after the node state has been primed, # otherwise the state changes won't have effect. # - The alarm must complete its first run before the transport is started, # to avoid rejecting messages for unknown channels. self.alarm.register_callback(self._callback_new_block) self.alarm.first_run() chain_state = views.state_from_raiden(self) # Dispatch pending transactions pending_transactions = views.get_pending_transactions(chain_state, ) log.debug( 'Processing pending transactions', num_pending_transactions=len(pending_transactions), node=pex(self.address), ) with self.dispatch_events_lock: for transaction in pending_transactions: on_raiden_event(self, transaction) self.alarm.start() queueids_to_queues = views.get_all_messagequeues(chain_state) # repopulate identifier_to_results for pending transfers for queue_messages in queueids_to_queues.values(): for message in queue_messages: if isinstance(message, SendDirectTransfer): self.identifier_to_results[ message.payment_identifier] = AsyncResult() self.transport.start(self, queueids_to_queues) # exceptions on these subtasks should crash the app and bubble up self.alarm.link_exception(self.on_error) self.transport.link_exception(self.on_error) # Health check needs the transport layer self.start_neighbours_healthcheck() if self.config['transport_type'] == 'udp': endpoint_registration_greenlet.get( ) # re-raise if exception occurred super().start() def _run(self): """ Busy-wait on long-lived subtasks/greenlets, re-raise if any error occurs """ try: self.stop_event.wait() except gevent.GreenletExit: # killed without exception self.stop_event.set() gevent.killall([self.alarm, self.transport]) # kill children raise # re-raise to keep killed status except Exception: self.stop() raise def stop(self): """ Stop the node gracefully. Raise if any stop-time error occurred on any subtask """ if self.stop_event.ready(): # not started return # Needs to come before any greenlets joining self.stop_event.set() # Filters must be uninstalled after the alarm task has stopped. Since # the events are polled by an alarm task callback, if the filters are # uninstalled before the alarm task is fully stopped the callback # `poll_blockchain_events` will fail. # # We need a timeout to prevent an endless loop from trying to # contact the disconnected client try: self.transport.stop() self.alarm.stop() self.transport.get() self.alarm.get() self.blockchain_events.uninstall_all_event_listeners() except (gevent.Timeout, RaidenShuttingDown): pass self.blockchain_events.reset() if self.db_lock is not None: self.db_lock.release() def __repr__(self): return '<{} {}>'.format(self.__class__.__name__, pex(self.address)) def start_neighbours_healthcheck(self): for neighbour in views.all_neighbour_nodes( self.wal.state_manager.current_state): if neighbour != ConnectionManager.BOOTSTRAP_ADDR: self.start_health_check_for(neighbour) def get_block_number(self): return views.block_number(self.wal.state_manager.current_state) def handle_state_change(self, state_change): log.debug('STATE CHANGE', node=pex(self.address), state_change=state_change) event_list = self.wal.log_and_dispatch(state_change) if self.dispatch_events_lock.locked(): return [] for event in event_list: log.debug('RAIDEN EVENT', node=pex(self.address), raiden_event=event) on_raiden_event(self, event) # Take a snapshot every SNAPSHOT_STATE_CHANGES_COUNT # TODO: Gather more data about storage requirements # and update the value to specify how often we need # capturing a snapshot should take place new_snapshot_group = self.wal.storage.count_state_changes( ) // SNAPSHOT_STATE_CHANGES_COUNT if new_snapshot_group > self.snapshot_group: log.debug(f'Storing snapshot: {new_snapshot_group}') self.wal.snapshot() self.snapshot_group = new_snapshot_group return event_list def set_node_network_state(self, node_address, network_state): state_change = ActionChangeNodeNetworkState(node_address, network_state) self.wal.log_and_dispatch(state_change) def start_health_check_for(self, node_address): self.transport.start_health_check(node_address) def _callback_new_block(self, current_block_number): """Called once a new block is detected by the alarm task. Note: This should be called only once per block, otherwise there will be duplicated `Block` state changes in the log. Therefore this method should be called only once a new block is mined with the appropriate block_number argument from the AlarmTask. """ # Raiden relies on blockchain events to update its off-chain state, # therefore some APIs /used/ to forcefully poll for events. # # This was done for APIs which have on-chain side-effects, e.g. # openning a channel, where polling the event is required to update # off-chain state to providing a consistent view to the caller, e.g. # the channel exists after the API call returns. # # That pattern introduced a race, because the events are returned only # once per filter, and this method would be called concurrently by the # API and the AlarmTask. The following lock is necessary, to ensure the # expected side-effects are properly applied (introduced by the commit # 3686b3275ff7c0b669a6d5e2b34109c3bdf1921d) with self.event_poll_lock: for event in self.blockchain_events.poll_blockchain_events( current_block_number): # These state changes will be procesed with a block_number # which is /larger/ than the ChainState's block_number. on_blockchain_event(self, event) # On restart the Raiden node will re-create the filters with the # ethereum node. These filters will have the from_block set to the # value of the latest Block state change. To avoid missing events # the Block state change is dispatched only after all of the events # have been processed. # # This means on some corner cases a few events may be applied # twice, this will happen if the node crashed and some events have # been processed but the Block state change has not been # dispatched. state_change = Block(current_block_number) self.handle_state_change(state_change) def sign(self, message): """ Sign message inplace. """ if not isinstance(message, SignedMessage): raise ValueError('{} is not signable.'.format(repr(message))) message.sign(self.private_key) def install_all_blockchain_filters( self, token_network_registry_proxy: TokenNetworkRegistry, secret_registry_proxy: SecretRegistry, from_block: typing.BlockNumber, ): with self.event_poll_lock: node_state = views.state_from_raiden(self) token_networks = views.get_token_network_identifiers( node_state, token_network_registry_proxy.address, ) self.blockchain_events.add_token_network_registry_listener( token_network_registry_proxy, from_block, ) self.blockchain_events.add_secret_registry_listener( secret_registry_proxy, from_block, ) for token_network in token_networks: token_network_proxy = self.chain.token_network(token_network) self.blockchain_events.add_token_network_listener( token_network_proxy, from_block, ) def connection_manager_for_token_network(self, token_network_identifier): if not is_binary_address(token_network_identifier): raise InvalidAddress('token address is not valid.') known_token_networks = views.get_token_network_identifiers( views.state_from_raiden(self), self.default_registry.address, ) if token_network_identifier not in known_token_networks: raise InvalidAddress('token is not registered.') manager = self.tokennetworkids_to_connectionmanagers.get( token_network_identifier) if manager is None: manager = ConnectionManager(self, token_network_identifier) self.tokennetworkids_to_connectionmanagers[ token_network_identifier] = manager return manager def leave_all_token_networks(self): state_change = ActionLeaveAllNetworks() self.wal.log_and_dispatch(state_change) def close_and_settle(self): log.info('raiden will close and settle all channels now') self.leave_all_token_networks() connection_managers = [ cm for cm in self.tokennetworkids_to_connectionmanagers.values() ] if connection_managers: waiting.wait_for_settle_all_channels( self, self.alarm.sleep_time, ) def mediated_transfer_async( self, token_network_identifier, amount, target, identifier, ): """ Transfer `amount` between this node and `target`. This method will start an asynchronous transfer, the transfer might fail or succeed depending on a couple of factors: - Existence of a path that can be used, through the usage of direct or intermediary channels. - Network speed, making the transfer sufficiently fast so it doesn't expire. """ async_result = self.start_mediated_transfer( token_network_identifier, amount, target, identifier, ) return async_result def direct_transfer_async(self, token_network_identifier, amount, target, identifier): """ Do a direct transfer with target. Direct transfers are non cancellable and non expirable, since these transfers are a signed balance proof with the transferred amount incremented. Because the transfer is non cancellable, there is a level of trust with the target. After the message is sent the target is effectively paid and then it is not possible to revert. The async result will be set to False iff there is no direct channel with the target or the payer does not have balance to complete the transfer, otherwise because the transfer is non expirable the async result *will never be set to False* and if the message is sent it will hang until the target node acknowledge the message. This transfer should be used as an optimization, since only two packets are required to complete the transfer (from the payers perspective), whereas the mediated transfer requires 6 messages. """ self.start_health_check_for(target) if identifier is None: identifier = create_default_identifier() direct_transfer = ActionTransferDirect( token_network_identifier, target, identifier, amount, ) async_result = AsyncResult() self.identifier_to_results[identifier] = async_result self.handle_state_change(direct_transfer) def start_mediated_transfer( self, token_network_identifier, amount, target, identifier, ): self.start_health_check_for(target) if identifier is None: identifier = create_default_identifier() if identifier in self.identifier_to_results: return self.identifier_to_results[identifier] async_result = AsyncResult() self.identifier_to_results[identifier] = async_result secret = random_secret() init_initiator_statechange = initiator_init( self, identifier, amount, secret, token_network_identifier, target, ) # Dispatch the state change even if there are no routes to create the # wal entry. self.handle_state_change(init_initiator_statechange) return async_result def mediate_mediated_transfer(self, transfer: LockedTransfer): init_mediator_statechange = mediator_init(self, transfer) self.handle_state_change(init_mediator_statechange) def target_mediated_transfer(self, transfer: LockedTransfer): self.start_health_check_for(transfer.initiator) init_target_statechange = target_init(transfer) self.handle_state_change(init_target_statechange)
class RaidenService(Runnable): """ A Raiden node. """ def __init__( self, chain: BlockChainService, query_start_block: BlockNumber, default_registry: TokenNetworkRegistry, default_secret_registry: SecretRegistry, transport, raiden_event_handler, message_handler, config, discovery=None, ): super().__init__() self.tokennetworkids_to_connectionmanagers: ConnectionManagerDict = dict( ) self.targets_to_identifiers_to_statuses: StatusesDict = defaultdict( dict) self.chain: BlockChainService = chain self.default_registry = default_registry self.query_start_block = query_start_block self.default_secret_registry = default_secret_registry self.config = config self.signer: Signer = LocalSigner(self.chain.client.privkey) self.address = self.signer.address self.discovery = discovery self.transport = transport self.blockchain_events = BlockchainEvents() self.alarm = AlarmTask(chain) self.raiden_event_handler = raiden_event_handler self.message_handler = message_handler self.stop_event = Event() self.stop_event.set() # inits as stopped self.greenlets = list() self.wal: Optional[wal.WriteAheadLog] = None self.snapshot_group = 0 # This flag will be used to prevent the service from processing # state changes events until we know that pending transactions # have been dispatched. self.dispatch_events_lock = Semaphore(1) self.contract_manager = ContractManager(config['contracts_path']) self.database_path = config['database_path'] if self.database_path != ':memory:': database_dir = os.path.dirname(config['database_path']) os.makedirs(database_dir, exist_ok=True) self.database_dir = database_dir # Two raiden processes must not write to the same database, even # though the database itself may be consistent. If more than one # nodes writes state changes to the same WAL there are no # guarantees about recovery, this happens because during recovery # the WAL replay can not be deterministic. lock_file = os.path.join(self.database_dir, '.lock') self.db_lock = filelock.FileLock(lock_file) else: self.database_path = ':memory:' self.database_dir = None self.serialization_file = None self.db_lock = None self.event_poll_lock = gevent.lock.Semaphore() self.gas_reserve_lock = gevent.lock.Semaphore() self.payment_identifier_lock = gevent.lock.Semaphore() def start(self): """ Start the node synchronously. Raises directly if anything went wrong on startup """ if not self.stop_event.ready(): raise RuntimeError(f'{self!r} already started') self.stop_event.clear() self.greenlets = list() if self.database_dir is not None: self.db_lock.acquire(timeout=0) assert self.db_lock.is_locked # start the registration early to speed up the start if self.config['transport_type'] == 'udp': endpoint_registration_greenlet = gevent.spawn( self.discovery.register, self.address, self.config['transport']['udp']['external_ip'], self.config['transport']['udp']['external_port'], ) self.maybe_upgrade_db() storage = sqlite.SerializedSQLiteStorage( database_path=self.database_path, serializer=serialize.JSONSerializer(), ) storage.log_run() self.wal = wal.restore_to_state_change( transition_function=node.state_transition, storage=storage, state_change_identifier='latest', ) if self.wal.state_manager.current_state is None: log.debug( 'No recoverable state available, created inital state', node=pex(self.address), ) # On first run Raiden needs to fetch all events for the payment # network, to reconstruct all token network graphs and find opened # channels last_log_block_number = self.query_start_block last_log_block_hash = self.chain.client.blockhash_from_blocknumber( last_log_block_number, ) state_change = ActionInitChain( pseudo_random_generator=random.Random(), block_number=last_log_block_number, block_hash=last_log_block_hash, our_address=self.chain.node_address, chain_id=self.chain.network_id, ) self.handle_and_track_state_change(state_change) payment_network = PaymentNetworkState( self.default_registry.address, [], # empty list of token network states as it's the node's startup ) state_change = ContractReceiveNewPaymentNetwork( transaction_hash=constants.EMPTY_HASH, payment_network=payment_network, block_number=last_log_block_number, block_hash=last_log_block_hash, ) self.handle_and_track_state_change(state_change) else: # The `Block` state change is dispatched only after all the events # for that given block have been processed, filters can be safely # installed starting from this position without losing events. last_log_block_number = views.block_number( self.wal.state_manager.current_state) log.debug( 'Restored state from WAL', last_restored_block=last_log_block_number, node=pex(self.address), ) known_networks = views.get_payment_network_identifiers( views.state_from_raiden(self)) if known_networks and self.default_registry.address not in known_networks: configured_registry = pex(self.default_registry.address) known_registries = lpex(known_networks) raise RuntimeError( f'Token network address mismatch.\n' f'Raiden is configured to use the smart contract ' f'{configured_registry}, which conflicts with the current known ' f'smart contracts {known_registries}', ) # Restore the current snapshot group state_change_qty = self.wal.storage.count_state_changes() self.snapshot_group = state_change_qty // SNAPSHOT_STATE_CHANGES_COUNT # Install the filters using the correct from_block value, otherwise # blockchain logs can be lost. self.install_all_blockchain_filters( self.default_registry, self.default_secret_registry, last_log_block_number, ) # Complete the first_run of the alarm task and synchronize with the # blockchain since the last run. # # Notes about setup order: # - The filters must be polled after the node state has been primed, # otherwise the state changes won't have effect. # - The alarm must complete its first run before the transport is started, # to reject messages for closed/settled channels. self.alarm.register_callback(self._callback_new_block) with self.dispatch_events_lock: self.alarm.first_run(last_log_block_number) chain_state = views.state_from_raiden(self) self._initialize_transactions_queues(chain_state) self._initialize_whitelists(chain_state) self._initialize_payment_statuses(chain_state) # send messages in queue before starting transport, # this is necessary to avoid a race where, if the transport is started # before the messages are queued, actions triggered by it can cause new # messages to be enqueued before these older ones self._initialize_messages_queues(chain_state) # before we start the transport, we need to request monitoring for all current # balance proofs. current_balance_proofs = views.detect_balance_proof_change( State(), chain_state, ) for balance_proof in current_balance_proofs: update_monitoring_service_from_balance_proof(self, balance_proof) # The transport must not ever be started before the alarm task's # `first_run()` has been, because it's this method which synchronizes the # node with the blockchain, including the channel's state (if the channel # is closed on-chain new messages must be rejected, which will not be the # case if the node is not synchronized) self.transport.start( raiden_service=self, message_handler=self.message_handler, prev_auth_data=chain_state.last_transport_authdata, ) # First run has been called above! self.alarm.start() # exceptions on these subtasks should crash the app and bubble up self.alarm.link_exception(self.on_error) self.transport.link_exception(self.on_error) # Health check needs the transport layer self.start_neighbours_healthcheck(chain_state) if self.config['transport_type'] == 'udp': endpoint_registration_greenlet.get( ) # re-raise if exception occurred log.debug('Raiden Service started', node=pex(self.address)) super().start() def _run(self, *args, **kwargs): # pylint: disable=method-hidden """ Busy-wait on long-lived subtasks/greenlets, re-raise if any error occurs """ self.greenlet.name = f'RaidenService._run node:{pex(self.address)}' try: self.stop_event.wait() except gevent.GreenletExit: # killed without exception self.stop_event.set() gevent.killall([self.alarm, self.transport]) # kill children raise # re-raise to keep killed status except Exception: self.stop() raise def stop(self): """ Stop the node gracefully. Raise if any stop-time error occurred on any subtask """ if self.stop_event.ready(): # not started return # Needs to come before any greenlets joining self.stop_event.set() # Filters must be uninstalled after the alarm task has stopped. Since # the events are polled by an alarm task callback, if the filters are # uninstalled before the alarm task is fully stopped the callback # `poll_blockchain_events` will fail. # # We need a timeout to prevent an endless loop from trying to # contact the disconnected client self.transport.stop() self.alarm.stop() self.transport.join() self.alarm.join() self.blockchain_events.uninstall_all_event_listeners() # Close storage DB to release internal DB lock self.wal.storage.conn.close() if self.db_lock is not None: self.db_lock.release() log.debug('Raiden Service stopped', node=pex(self.address)) @property def confirmation_blocks(self): return self.config['blockchain']['confirmation_blocks'] def add_pending_greenlet(self, greenlet: Greenlet): """ Ensures an error on the passed greenlet crashes self/main greenlet. """ def remove(_): self.greenlets.remove(greenlet) self.greenlets.append(greenlet) greenlet.link_exception(self.on_error) greenlet.link_value(remove) def __repr__(self): return f'<{self.__class__.__name__} node:{pex(self.address)}>' def start_neighbours_healthcheck(self, chain_state: ChainState): for neighbour in views.all_neighbour_nodes(chain_state): if neighbour != ConnectionManager.BOOTSTRAP_ADDR: self.start_health_check_for(neighbour) def get_block_number(self) -> BlockNumber: assert self.wal, 'WAL object not yet initialized.' return views.block_number(self.wal.state_manager.current_state) def on_message(self, message: Message): self.message_handler.on_message(self, message) def handle_and_track_state_change(self, state_change: StateChange): """ Dispatch the state change and does not handle the exceptions. When the method is used the exceptions are tracked and re-raised in the raiden service thread. """ for greenlet in self.handle_state_change(state_change): self.add_pending_greenlet(greenlet) def handle_state_change(self, state_change: StateChange) -> List[Greenlet]: """ Dispatch the state change and return the processing threads. Use this for error reporting, failures in the returned greenlets, should be re-raised using `gevent.joinall` with `raise_error=True`. """ assert self.wal log.debug( 'State change', node=pex(self.address), state_change=_redact_secret( serialize.JSONSerializer.serialize(state_change)), ) old_state = views.state_from_raiden(self) raiden_event_list = self.wal.log_and_dispatch(state_change) current_state = views.state_from_raiden(self) for balance_proof in views.detect_balance_proof_change( old_state, current_state): update_monitoring_service_from_balance_proof(self, balance_proof) log.debug( 'Raiden events', node=pex(self.address), raiden_events=[ _redact_secret(serialize.JSONSerializer.serialize(event)) for event in raiden_event_list ], ) greenlets: List[Greenlet] = list() if not self.dispatch_events_lock.locked(): for raiden_event in raiden_event_list: greenlets.append( self.handle_event(raiden_event=raiden_event), ) state_changes_count = self.wal.storage.count_state_changes() new_snapshot_group = (state_changes_count // SNAPSHOT_STATE_CHANGES_COUNT) if new_snapshot_group > self.snapshot_group: log.debug('Storing snapshot', snapshot_id=new_snapshot_group) self.wal.snapshot() self.snapshot_group = new_snapshot_group return greenlets def handle_event(self, raiden_event: RaidenEvent) -> Greenlet: """Spawn a new thread to handle a Raiden event. This will spawn a new greenlet to handle each event, which is important for two reasons: - Blockchain transactions can be queued without interfering with each other. - The calling thread is free to do more work. This is specially important for the AlarmTask thread, which will eventually cause the node to send transactions when a given Block is reached (e.g. registering a secret or settling a channel). Important: This is spawing a new greenlet for /each/ transaction. It's therefore /required/ that there is *NO* order among these. """ return gevent.spawn(self._handle_event, raiden_event) def _handle_event(self, raiden_event: RaidenEvent): assert isinstance(raiden_event, RaidenEvent) try: self.raiden_event_handler.on_raiden_event( raiden=self, event=raiden_event, ) except RaidenRecoverableError as e: log.error(str(e)) except InvalidDBData: raise except RaidenUnrecoverableError as e: log_unrecoverable = ( self.config['environment_type'] == Environment.PRODUCTION and not self.config['unrecoverable_error_should_crash']) if log_unrecoverable: log.error(str(e)) else: raise def set_node_network_state(self, node_address: Address, network_state: str): state_change = ActionChangeNodeNetworkState(node_address, network_state) self.handle_and_track_state_change(state_change) def start_health_check_for(self, node_address: Address): # This function is a noop during initialization. It can be called # through the alarm task while polling for new channel events. The # healthcheck will be started by self.start_neighbours_healthcheck() if self.transport: self.transport.start_health_check(node_address) def _callback_new_block(self, latest_block: Dict): """Called once a new block is detected by the alarm task. Note: This should be called only once per block, otherwise there will be duplicated `Block` state changes in the log. Therefore this method should be called only once a new block is mined with the corresponding block data from the AlarmTask. """ # User facing APIs, which have on-chain side-effects, force polled the # blockchain to update the node's state. This force poll is used to # provide a consistent view to the user, e.g. a channel open call waits # for the transaction to be mined and force polled the event to update # the node's state. This pattern introduced a race with the alarm task # and the task which served the user request, because the events are # returned only once per filter. The lock below is to protect against # these races (introduced by the commit # 3686b3275ff7c0b669a6d5e2b34109c3bdf1921d) with self.event_poll_lock: latest_block_number = latest_block['number'] confirmed_block_number = latest_block_number - self.confirmation_blocks confirmed_block = self.chain.client.web3.eth.getBlock( confirmed_block_number) # handle testing private chains confirmed_block_number = max(GENESIS_BLOCK_NUMBER, confirmed_block_number) for event in self.blockchain_events.poll_blockchain_events( confirmed_block_number): # These state changes will be procesed with a block_number # which is /larger/ than the ChainState's block_number. on_blockchain_event(self, event) # On restart the Raiden node will re-create the filters with the # ethereum node. These filters will have the from_block set to the # value of the latest Block state change. To avoid missing events # the Block state change is dispatched only after all of the events # have been processed. # # This means on some corner cases a few events may be applied # twice, this will happen if the node crashed and some events have # been processed but the Block state change has not been # dispatched. state_change = Block( block_number=confirmed_block_number, gas_limit=confirmed_block['gasLimit'], block_hash=BlockHash(bytes(confirmed_block['hash'])), ) # Note: It's important to /not/ block here, because this function # can be called from the alarm task greenlet, which should not # starve. self.handle_and_track_state_change(state_change) def _initialize_transactions_queues(self, chain_state: ChainState): pending_transactions = views.get_pending_transactions(chain_state) log.debug( 'Processing pending transactions', num_pending_transactions=len(pending_transactions), node=pex(self.address), ) with self.dispatch_events_lock: for transaction in pending_transactions: self.add_pending_greenlet( self.handle_event(raiden_event=transaction), ) def _initialize_payment_statuses(self, chain_state: ChainState): """ Re-initialize targets_to_identifiers_to_statuses. """ with self.payment_identifier_lock: for task in chain_state.payment_mapping.secrethashes_to_task.values( ): if not isinstance(task, InitiatorTask): continue # Every transfer in the transfers_list must have the same target # and payment_identifier, so using the first transfer is # sufficient. initiator = next( iter(task.manager_state.initiator_transfers.values())) transfer = initiator.transfer target = transfer.target identifier = transfer.payment_identifier balance_proof = transfer.balance_proof self.targets_to_identifiers_to_statuses[target][ identifier] = PaymentStatus( payment_identifier=identifier, amount=transfer.lock.amount, token_network_identifier=balance_proof. token_network_identifier, payment_done=AsyncResult(), ) def _initialize_messages_queues(self, chain_state: ChainState): """ Push the message queues to the transport. """ events_queues = views.get_all_messagequeues(chain_state) for queue_identifier, event_queue in events_queues.items(): self.start_health_check_for(queue_identifier.recipient) for event in event_queue: message = message_from_sendevent(event, self.address) self.sign(message) self.transport.send_async(queue_identifier, message) def _initialize_whitelists(self, chain_state: ChainState): """ Whitelist neighbors and mediated transfer targets on transport """ for neighbour in views.all_neighbour_nodes(chain_state): if neighbour == ConnectionManager.BOOTSTRAP_ADDR: continue self.transport.whitelist(neighbour) events_queues = views.get_all_messagequeues(chain_state) for event_queue in events_queues.values(): for event in event_queue: if isinstance(event, SendLockedTransfer): transfer = event.transfer if transfer.initiator == self.address: self.transport.whitelist(address=transfer.target) def sign(self, message: Message): """ Sign message inplace. """ if not isinstance(message, SignedMessage): raise ValueError('{} is not signable.'.format(repr(message))) message.sign(self.signer) def install_all_blockchain_filters( self, token_network_registry_proxy: TokenNetworkRegistry, secret_registry_proxy: SecretRegistry, from_block: BlockNumber, ): with self.event_poll_lock: node_state = views.state_from_raiden(self) token_networks = views.get_token_network_identifiers( node_state, token_network_registry_proxy.address, ) self.blockchain_events.add_token_network_registry_listener( token_network_registry_proxy=token_network_registry_proxy, contract_manager=self.contract_manager, from_block=from_block, ) self.blockchain_events.add_secret_registry_listener( secret_registry_proxy=secret_registry_proxy, contract_manager=self.contract_manager, from_block=from_block, ) for token_network in token_networks: token_network_proxy = self.chain.token_network( TokenNetworkAddress(token_network), ) self.blockchain_events.add_token_network_listener( token_network_proxy=token_network_proxy, contract_manager=self.contract_manager, from_block=from_block, ) def connection_manager_for_token_network( self, token_network_identifier: TokenNetworkID, ) -> ConnectionManager: if not is_binary_address(token_network_identifier): raise InvalidAddress('token address is not valid.') known_token_networks = views.get_token_network_identifiers( views.state_from_raiden(self), self.default_registry.address, ) if token_network_identifier not in known_token_networks: raise InvalidAddress('token is not registered.') manager = self.tokennetworkids_to_connectionmanagers.get( token_network_identifier) if manager is None: manager = ConnectionManager(self, token_network_identifier) self.tokennetworkids_to_connectionmanagers[ token_network_identifier] = manager return manager def mediated_transfer_async( self, token_network_identifier: TokenNetworkID, amount: PaymentAmount, target: TargetAddress, identifier: PaymentID, secret: Secret = None, secret_hash: SecretHash = None, ) -> PaymentStatus: """ Transfer `amount` between this node and `target`. This method will start an asynchronous transfer, the transfer might fail or succeed depending on a couple of factors: - Existence of a path that can be used, through the usage of direct or intermediary channels. - Network speed, making the transfer sufficiently fast so it doesn't expire. """ if secret is None: secret = random_secret() payment_status = self.start_mediated_transfer_with_secret( token_network_identifier, amount, target, identifier, secret, secret_hash, ) return payment_status def start_mediated_transfer_with_secret( self, token_network_identifier: TokenNetworkID, amount: PaymentAmount, target: TargetAddress, identifier: PaymentID, secret: Secret, secret_hash: SecretHash = None, ) -> PaymentStatus: if secret_hash is None: secret_hash = sha3(secret) # We must check if the secret was registered against the latest block, # even if the block is forked away and the transaction that registers # the secret is removed from the blockchain. The rationale here is that # someone else does know the secret, regardless of the chain state, so # the node must not use it to start a payment. # # For this particular case, it's preferable to use `latest` instead of # having a specific block_hash, because it's preferable to know if the secret # was ever known, rather than having a consistent view of the blockchain. secret_registered = self.default_secret_registry.check_registered( secrethash=secret_hash, block_identifier='latest', ) if secret_registered: raise RaidenUnrecoverableError( f'Attempted to initiate a locked transfer with secrethash {pex(secret_hash)}.' f' That secret is already registered onchain.', ) self.start_health_check_for(Address(target)) if identifier is None: identifier = create_default_identifier() with self.payment_identifier_lock: payment_status = self.targets_to_identifiers_to_statuses[ target].get(identifier) if payment_status: payment_status_matches = payment_status.matches( token_network_identifier, amount, ) if not payment_status_matches: raise PaymentConflict( 'Another payment with the same id is in flight', ) return payment_status payment_status = PaymentStatus( payment_identifier=identifier, amount=amount, token_network_identifier=token_network_identifier, payment_done=AsyncResult(), secret=secret, secret_hash=secret_hash, ) self.targets_to_identifiers_to_statuses[target][ identifier] = payment_status init_initiator_statechange = initiator_init( raiden=self, transfer_identifier=identifier, transfer_amount=amount, transfer_secret=secret, token_network_identifier=token_network_identifier, target_address=target, ) # Dispatch the state change even if there are no routes to create the # wal entry. self.handle_and_track_state_change(init_initiator_statechange) return payment_status def mediate_mediated_transfer(self, transfer: LockedTransfer): init_mediator_statechange = mediator_init(self, transfer) self.handle_and_track_state_change(init_mediator_statechange) def target_mediated_transfer(self, transfer: LockedTransfer): self.start_health_check_for(transfer.initiator) init_target_statechange = target_init(transfer) self.handle_and_track_state_change(init_target_statechange) def maybe_upgrade_db(self): manager = UpgradeManager(db_filename=self.database_path) manager.run()