def _loop(servers): """ Main event loop. Never returns. """ # pylint: disable=R0912,R0914,R0915 # Too many local variables (24/15) import logging import select import sys from x84.terminal import get_terminals, kill_session from x84.bbs.ini import CFG from x84.fail2ban import get_fail2ban_function SELECT_POLL = 0.02 # polling time is 20ms # WIN32 has no session_fds (multiprocess queues are not polled using # select), use a persistently empty set; for WIN32, sessions are always # polled for data at every loop. WIN32 = sys.platform.lower().startswith('win32') session_fds = set() log = logging.getLogger('x84.engine') if not len(servers): raise ValueError("No servers configured for event loop! (ssh, telnet)") tap_events = CFG.getboolean('session', 'tap_events') check_ban = get_fail2ban_function() locks = dict() while True: # shutdown, close & delete inactive clients, for server in servers: # bbs sessions that are no longer active on the socket # level -- send them a 'kill signal' for key, client in server.clients.items()[:]: if not client.is_active(): kill_session(client, 'socket shutdown') del server.clients[key] # on-connect negotiations that have completed or failed. # delete their thread instance from further evaluation for thread in [_thread for _thread in server.threads if _thread.stopped][:]: server.threads.remove(thread) server_fds = [server.server_socket.fileno() for server in servers] client_fds = [fd for fd in server.client_fds() for server in servers] check_r = server_fds + client_fds if not WIN32: session_fds = get_session_output_fds(servers) check_r += session_fds # We'd like to use timeout 'None', but the registration of # a new client in terminal.start_process surprises us with new # file descriptors for the session i/o. unless we loop for # additional `session_fds', a connecting client would block. ready_r, _, _ = select.select(check_r, [], [], SELECT_POLL) for fd in ready_r: # see if any new tcp connections were made server = find_server(servers, fd) if server is not None: accept(log, server, check_ban) # receive new data from tcp clients. client_recv(servers, log) terms = get_terminals() # receive new data from session terminals if WIN32 or set(session_fds) & set(ready_r): try: session_recv(locks, terms, log, tap_events) except IOError, err: # if the ipc closes while we poll, warn and continue log.warn(err) # send tcp data to clients client_send(terms, log) # send session data, poll for user-timeout and disconnect them session_send(terms)
def handle_lock(locks, tty, event, data, tap_events, log): """ handle locking event of (lock-key, (method, stale)) """ import time from x84.terminal import get_terminals method, stale = data if method == 'acquire': # this lock is already held, if event in locks: # check if lock held by an active session, holder = locks[event][1] for _sid, _tty in get_terminals(): if _sid == holder and _sid != tty.sid: # acquire the lock from a now-deceased session. log.debug('[{tty.sid}] {event} not acquired, ' 'held by active session: {holder}' .format(tty=tty, event=event, holder=holder)) break elif _sid == holder and _sid == tty.sid: # acquire the lock from ourselves! We'll allow it # (this is termed, "re-entrant locking"). log.debug('{tty.sid}] {event} is re-acquired!' .format(tty=tty, event=event)) else: # lock is held by a now-defunct session, re-acquired. log.debug('[{tty.sid}] {event} re-acquiring stale lock, ' 'previously held by session no longer active: ' '{holder}' .format(tty=tty, event=event, holder=holder)) del locks[event] # lock is not held, or release by previous block if event not in locks: # acknowledge its requirement, locks[event] = (time.time(), tty.sid) tty.master_write.send((event, True,)) if tap_events: log.debug('[{tty.sid}] {event} granted lock.' .format(tty=tty, event=event)) # lock cannot be acquired, else: holder = locks[event][1] elapsed = time.time() - locks[event][0] if (stale is not None and elapsed > stale): # caller has decreed that this lock may be acquired even if # it already held, if it has been held longer than length of # time `stale`. This is simply to prevent a global freeze # when the programmer knows the holder may fail to release, # though this is not currently used in the demonstration # system. locks[event] = (time.time(), tty.sid) tty.master_write.send((event, True,)) log.warn('[{tty.sid}] {event} re-acquiring stale lock, ' 'previously held active session {holder} after ' '{elapsed}s elapsed (stale={stale})' .format(tty=tty, event=event, holder=holder, elapsed=elapsed, stale=stale)) # signal busy with matching event, data=False else: tty.master_write.send((event, False,)) log.warn('[{tty.sid}] {event} lock rejected; already held ' 'by active session {holder} for {elapsed} seconds ' '(stale={stale})' .format(tty=tty, event=event, holder=holder, elapsed=elapsed, stale=stale)) elif method == 'release': if event not in locks: log.error('[{tty.sid}] {event} lock failed to release, ' 'not acquired.'.format(tty=tty, event=event)) else: del locks[event] if tap_events: log.debug('[{tty.sid}] {event} released lock.' .format(tty=tty, event=event))