예제 #1
0
 def receive_eof(self):
     connection, _ = self.listen_socket.accept()
     print(connection)
     # socket_write = socket.SocketIO(connection, 'w')
     # self.out = io.BufferedWriter(socket_write)
     socket_read = socket.SocketIO(connection, 'r')
     inp_reader = KQMLReader(io.BufferedReader(socket_read))
     # self.inp = inp_reader
     self.dispatcher = KQMLDispatcher(self, inp_reader, self.name)
     self.dispatcher.start()
예제 #2
0
    def __init__(self, argv=None, **kwargs):
        defaults = dict(host='localhost',
                        port=6200,
                        is_application=False,
                        testing=False,
                        socket=None,
                        name=None,
                        group_name=None,
                        scan_for_port=False,
                        debug=False,
                        do_register=True)
        self.dispatcher = None
        self.MAX_PORT_TRIES = 100
        self.reply_id_counter = 1

        if isinstance(argv, list):
            kwargs.update(translate_argv(argv))
        elif argv is not None:
            raise KQMLException("Unusable type for keyord argument `argv`.")

        for kw, arg in kwargs.items():
            if kw not in defaults.keys():
                raise ValueError('Unexpected keyword argument: %s' % kw)
            else:
                defaults.pop(kw)
            self.__setattr__(kw, arg)
        for kw, arg in defaults.items():
            self.__setattr__(kw, arg)

        if self.debug:
            logger.setLevel(logging.DEBUG)
        else:
            logger.setLevel(logging.INFO)

        if not self.testing:
            self.out = None
            self.inp = None
            logger.info('Using socket connection')
            conn = self.connect(self.host, self.port)
            if not conn:
                logger.error('Connection failed')
                self.exit(-1)
            assert self.inp is not None and self.out is not None, \
                "Connection formed but input (%s) and output (%s) not set." % \
                (self.inp, self.out)
        else:
            logger.info('Using stdio connection')
            self.out = io.BytesIO()
            self.inp = KQMLReader(io.BytesIO())

        self.dispatcher = KQMLDispatcher(self, self.inp, self.name)

        if self.do_register:
            self.register()
예제 #3
0
    def _listen(self):
        """Sets up input and output socket connections to our listener.

        Infinite loop while ready to connect to our listener socket. On connect
        we get the write socket as a Buffered Writer and the read socket as a
        KQML Reader (which passes through a Buffered Reader). The reader is
        then attached to a KQML Dispatcher which is subsequently started, and
        passed along to the executor (which is a thread pool manager).
        """
        LOGGER.debug('listening')
        with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
            while self.ready:
                connection, _ = self.listen_socket.accept()
                LOGGER.debug('received connection')
                socket_write = socket.SocketIO(connection, 'w')
                self.out = io.BufferedWriter(socket_write)
                socket_read = socket.SocketIO(connection, 'r')
                read_input = KQMLReader(io.BufferedReader(socket_read))
                self.dispatcher = KQMLDispatcher(self, read_input, self.name)
                executor.submit(self.dispatcher.start)
예제 #4
0
 def listen(self):
     logger.debug('listening')
     with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
         while self.ready:
             conn, addr = self.listenSoc.accept()
             logger.debug('received connection')
             sw = socket.SocketIO(conn, 'w')
             self.out = io.BufferedWriter(sw)
             sr = socket.SocketIO(conn, 'r')
             inp = KQMLReader(io.BufferedReader(sr))
             self.dispatcher = KQMLDispatcher(self, inp, self.name)
             executor.submit(self.dispatcher.start)
    def listen(self):
        """Socket server, listens for new KQML messages and dispatches them
        accordingly.

        Doesn't necessarily need threading; Could just start dispatcher and
        after it returns accept next connection. This couldn't handle loads of
        inputs while being bogged down processing. To avoid this issue we
        thread the dispatching so the functions that get called are run in a
        separate Thread. We're using ThreadPoolExecutor because sockets use io,
        io is blocking and threads allow you to not block.
        """
        with ThreadPoolExecutor(max_workers=5) as executor:
            while self.ready:
                connection, _ = self.listen_socket.accept()
                LOGGER.debug('Received connection: %s', connection)
                socket_write = SocketIO(connection, 'w')
                self.local_out = BufferedWriter(socket_write)
                socket_read = SocketIO(connection, 'r')
                read_input = KQMLReader(BufferedReader(socket_read))
                self.dispatcher = KQMLDispatcher(self, read_input, self.name)
                LOGGER.debug('Starting dispatcher: %s', self.dispatcher)
                executor.submit(self.dispatcher.start)
                self.state = 'dispatching'
예제 #6
0
    def __init__(self, argv=None, **kwargs):
        defaults = dict(host='localhost', port=6200, is_application=False,
                        testing=False, socket=None, name=None, group_name=None,
                        scan_for_port=False, debug=False)
        self.dispatcher = None
        self.MAX_PORT_TRIES = 100
        self.reply_id_counter = 1

        if isinstance(argv, list):
            kwargs.update(translate_argv(argv))
        elif argv is not None:
            raise KQMLException("Unusable type for keyord argument `argv`.")

        for kw, arg in kwargs.items():
            if kw not in defaults.keys():
                raise ValueError('Unexpected keyword argument: %s' % kw)
            else:
                defaults.pop(kw)
            self.__setattr__(kw, arg)
        for kw, arg in defaults.items():
            self.__setattr__(kw, arg)

        if self.debug:
            logger.setLevel(logging.DEBUG)
        else:
            logger.setLevel(logging.INFO)

        if not self.testing:
            self.out = None
            self.inp = None
            logger.info('Using socket connection')
            conn = self.connect(self.host, self.port)
            if not conn:
                logger.error('Connection failed')
                self.exit(-1)
            assert self.inp is not None and self.out is not None, \
                "Connection formed but input (%s) and output (%s) not set." % \
                (self.inp, self.out)
        else:
            logger.info('Using stdio connection')
            self.out = io.BytesIO()
            self.inp = KQMLReader(io.BytesIO())

        self.dispatcher = KQMLDispatcher(self, self.inp, self.name)

        self.register()
예제 #7
0
class KQMLModule(object):
    def __init__(self, argv=None, **kwargs):
        defaults = dict(host='localhost', port=6200, is_application=False,
                        testing=False, socket=None, name=None, group_name=None,
                        scan_for_port=False, debug=False)
        self.dispatcher = None
        self.MAX_PORT_TRIES = 100
        self.reply_id_counter = 1

        if isinstance(argv, list):
            kwargs.update(translate_argv(argv))
        elif argv is not None:
            raise KQMLException("Unusable type for keyord argument `argv`.")

        for kw, arg in kwargs.items():
            if kw not in defaults.keys():
                raise ValueError('Unexpected keyword argument: %s' % kw)
            else:
                defaults.pop(kw)
            self.__setattr__(kw, arg)
        for kw, arg in defaults.items():
            self.__setattr__(kw, arg)

        if self.debug:
            logger.setLevel(logging.DEBUG)
        else:
            logger.setLevel(logging.INFO)

        if not self.testing:
            self.out = None
            self.inp = None
            logger.info('Using socket connection')
            conn = self.connect(self.host, self.port)
            if not conn:
                logger.error('Connection failed')
                self.exit(-1)
            assert self.inp is not None and self.out is not None, \
                "Connection formed but input (%s) and output (%s) not set." % \
                (self.inp, self.out)
        else:
            logger.info('Using stdio connection')
            self.out = io.BytesIO()
            self.inp = KQMLReader(io.BytesIO())

        self.dispatcher = KQMLDispatcher(self, self.inp, self.name)

        self.register()

    def start(self):
        if not self.testing:
            self.dispatcher.start()

    def subscribe_request(self, req_type):
        msg = KQMLPerformative('subscribe')
        content = KQMLList('request')
        content.append('&key')
        content.set('content', KQMLList.from_string('(%s . *)' % req_type))
        msg.set('content', content)
        self.send(msg)

    def subscribe_tell(self, tell_type):
        msg = KQMLPerformative('subscribe')
        content = KQMLList('tell')
        content.append('&key')
        content.set('content', KQMLList.from_string('(%s . *)' % tell_type))
        msg.set('content', content)
        self.send(msg)

    def connect(self, host=None, startport=None):
        if host is None:
            host = self.host
        if startport is None:
            startport = self.port
        if not self.scan_for_port:
            return self.connect1(host, startport, True)
        else:
            maxtries = self.MAX_PORT_TRIES
            for port in range(startport, startport + maxtries):
                conn = self.connect1(host, port, False)
                if conn:
                    return True
            logger.error('Failed to connect to ' + host + ':' + \
                         startport + '-' + port)
            return False

    def connect1(self, host, port, verbose=True):
        try:
            self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.socket.connect((host, port))
            sw = socket.SocketIO(self.socket, 'w')
            self.out = io.BufferedWriter(sw)
            sr = socket.SocketIO(self.socket, 'r')
            self.inp = KQMLReader(io.BufferedReader(sr))
            return True
        except socket.error as e:
            if verbose:
                logger.error(e)
            return False

    def register(self):
        if self.name is not None:
            perf = KQMLPerformative('register')
            perf.set('name', self.name)
            if self.group_name is not None:
                try:
                    if self.group_name.startswith('('):
                        perf.sets('group', self.group_name)
                    else:
                        perf.set('group', self.group_name)
                except IOError:
                    logger.error('bad group name: ' + self.group_name)
            self.send(perf)

    def ready(self):
        msg = KQMLPerformative('tell')
        content = KQMLList(['module-status', 'ready'])
        msg.set('content', content)
        self.send(msg)

    def exit(self, n):
        if self.is_application:
            sys.exit(n)
        else:
            if self.dispatcher is not None:
                self.dispatcher.shutdown()
            sys.exit(n)

    def receive_eof(self):
        self.exit(0)

    def receive_message_missing_verb(self, msg):
        self.error_reply(msg, 'missing verb in performative')

    def receive_message_missing_content(self, msg):
        self.error_reply(msg, 'missing content in performative')

    def receive_ask_if(self, msg, content):
        self.error_reply(msg, 'unexpected performative: ask-if')

    def receive_ask_all(self, msg, content):
        self.error_reply(msg, 'unexpected performative: ask-all')

    def receive_ask_one(self, msg, content):
        self.error_reply(msg, 'unexpected performative: ask-one')

    def receive_stream_all(self, msg, content):
        self.error_reply(msg, 'unexpected performative: stream-all')

    def receive_tell(self, msg, content):
        logger.error('unexpected performative: tell')

    def receive_untell(self, msg, content):
        self.error_reply(msg, 'unexpected performative: untell')

    def receive_deny(self, msg, content):
        self.error_reply(msg, 'unexpected performative: deny')

    def receive_insert(self, msg, content):
        self.error_reply(msg, 'unexpected performative: insert')

    def receive_uninsert(self, msg, content):
        self.error_reply(msg, 'unexpected performative: uninsert')

    def receive_delete_one(self, msg, content):
        self.error_reply(msg, 'unexpected performative: delete-one')

    def receive_delete_all(self, msg, content):
        self.error_reply(msg, 'unexpected performative: delete-all')

    def receive_undelete(self, msg, content):
        self.error_reply(msg, 'unexpected performative: undelete')

    def receive_achieve(self, msg, content):
        self.error_reply(msg, 'unexpected performative: achieve')

    def receive_advertise(self, msg, content):
        self.error_reply(msg, 'unexpected performative: advertise')

    def receive_unadvertise(self, msg, content):
        self.error_reply(msg, 'unexpected performative: unadvertise')

    def receive_subscribe(self, msg, content):
        self.error_reply(msg, 'unexpected performative: subscribe')

    def receive_standby(self, msg, content):
        self.error_reply(msg, 'unexpected performative: standby')

    def receive_register(self, msg, content):
        self.error_reply(msg, 'unexpected performative: register')

    def receive_forward(self, msg, content):
        self.error_reply(msg, 'unexpected performative: forward')

    def receive_broadcast(self, msg, content):
        self.error_reply(msg, 'unexpected performative: broadcast')

    def receive_transport_address(self, msg, content):
        self.error_reply(msg, 'unexpected performative: transport-address')

    def receive_broker_one(self, msg, content):
        self.error_reply(msg, 'unexpected performative: broker-one')

    def receive_broker_all(self, msg, content):
        self.error_reply(msg, 'unexpected performative: broker-all')

    def receive_recommend_one(self, msg, content):
        self.error_reply(msg, 'unexpected performative: recommend-one')

    def receive_recommend_all(self, msg, content):
        self.error_reply(msg, 'unexpected performative: recommend-all')

    def receive_recruit_one(self, msg, content):
        self.error_reply(msg, 'unexpected performative: recruit-one')

    def receive_recruit_all(self, msg, content):
        self.error_reply(msg, 'unexpected performative: recruit-all')

    def receive_reply(self, msg, content):
        logger.error(msg, 'unexpected performative: reply')

    def receive_request(self, msg, content):
        self.error_reply(msg, 'unexpected performative: request')

    def receive_eos(self, msg):
        self.error_reply(msg, 'unexpected performative: eos')

    def receive_error(self, msg):
        logger.error('Error received: "%s"' % msg)

    def receive_sorry(self, msg):
        logger.error('unexpected performative: sorry')

    def receive_ready(self, msg):
        logger.error(msg, 'unexpected performative: ready')

    def receive_next(self, msg):
        self.error_reply(msg, 'unexpected performative: next')

    def receive_rest(self, msg):
        self.error_reply(msg, 'unexpected performative: rest')

    def receive_discard(self, msg):
        self.error_reply(msg, 'unexpected performative: discard')

    def receive_unregister(self, msg):
        logger.error(msg, 'unexpected performative: unregister')

    def receive_other_performative(self, msg):
        self.error_reply(msg, 'unexpected performative: ' + str(msg))

    def handle_exception(self, ex):
        logger.error(str(ex))

    def send(self, msg):
        try:
            msg.write(self.out)
        except IOError:
            logger.error('IOError during message sending')
            pass
        self.out.write(b'\n')
        self.out.flush()
        logger.debug(msg.__repr__())

    def send_with_continuation(self, msg, cont):
        reply_id_base = 'IO-'
        if self.name is not None:
            reply_id_base = self.name + '-'
        reply_id = reply_id_base + str(self.reply_id_counter)
        self.reply_id_counter += 1
        msg.append(':reply-with')
        msg.append(reply_id)
        self.dispatcher.add_reply_continuation('%s' % reply_id, cont)
        self.send(msg)

    def reply(self, msg, reply_msg):
        sender = msg.get('sender')
        if sender is not None:
            reply_msg.set('receiver', sender)
        reply_with = msg.get('reply-with')
        if reply_with is not None:
            reply_msg.set('in-reply-to', reply_with)
        self.send(reply_msg)

    def error_reply(self, msg, comment):
        reply_msg = KQMLPerformative('error')
        reply_msg.sets('comment', comment)
        self.reply(msg, reply_msg)
class CompanionsKQMLModule(KQMLModule):
    """KQMLModule override to allow for continuous back and forth communication
    from the running companions agent (facilitator) and this agent.

    Attributes:
        debug (bool): helps set the debug level for the loggers accross modules
        dispatcher (KQMLDispatcher): Dispatcher to be used (from KQMLModule),
            calls on appropriate functions based on incoming messages,
            need to keep track of it for proper shutdown
        host (str): The host of Companions (localhost or an ip address)
        listen_socket (socket): Socket object the listener will control,
            receives incoming messages from Companions
        listener (Thread): Thread running the socket listening loop, calls the
            dispatcher as well.
        listener_port (int): port number you want to host the listener on
        local_out (BufferedWriter): Connection to the listener socker server
           output, used to send messages on the listener port for Companions
           to pick up on.
        name (str): Name of this agent (module), used in registration so this
            should be set to a new name for each new agent. Currently all
            instances of this class will have the same name as they are the
            same type of agent.
        num_subs (int): The number of subscriptions that the agent has (only
            used later in Pythonian)
        out (BufferedWriter): Connection to the Companions KQML socket server,
            created from send_socket, used by send
        port (int): port number that Companions is hosted on
        ready (bool): Boolean that controls the threads looping, overwrites the
            ready function from KQMLModule
        reply_id_counter (int): From KQMLModule, used in send_with_continuation
            adds reply-with and the appropriate reply id
        send_socket (socket): Socket that will connect to Companions for
            sending messages. Need to keep track of it to properly close itself
            initializes to None and only has a socket after calling connect
        starttime (datetime): the time at which this agent started, used for
            updating running status in Companions
        state (str): the state this agent is in, used for updating running
            status in Companions
    """

    name = 'CompanionsKQMLModule'

    # pylint: disable=super-init-not-called
    #   We are rewriting the KQMLModule...
    def __init__(self,
                 host: str = 'localhost',
                 port: int = 9000,
                 listener_port: int = 8950,
                 debug: bool = False):
        """Override of KQMLModule init to add turn it into a KQML socket server

        Args:
            host (str, optional): the host location to connect to via sockets
            port (int, optional): the port on the host to connect to
            listener_port (int, optional): the port this class will host its
                KQML socket server from (the connection end that dispatches
                requests as needed)
            debug (bool, optional): Whether to set the level of the logger to
                DEBUG or INFO - silencing debug errors and only showing needed
                information.
        """
        # OUTPUTS
        assert valid_ip(host), 'Host must be local or a valid ip address'
        self.host = host
        assert valid_port(port), \
            'port must be valid port number (1024-65535)'
        self.port = port
        self.send_socket = None
        self.out = None
        # INPUTS
        assert valid_port(listener_port), \
            'listener_port must be a valid port number (1024-65535)'
        self.listener_port = listener_port
        self.dispatcher = None
        self.listen_socket = socket()
        self.listen_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
        self.listen_socket.bind(('', self.listener_port))
        self.listen_socket.listen(10)
        self.local_out = None
        self.ready = True
        self.listener = Thread(target=self.listen, args=[])
        # FROM KQMLModule
        self.reply_id_counter = 1
        # UPDATES
        self.starttime = datetime.now()
        self.state = 'idle'
        self.num_subs = 0
        # LOGGING / DEBUG
        self.debug = debug
        if self.debug:
            LOGGER.setLevel(DEBUG)
        else:
            LOGGER.setLevel(INFO)
        # REGISTER AND START LISTENING
        LOGGER.info('Starting listener (KQML socket server)...')
        self.listener.start()
        self.register()

    @classmethod
    # pylint: disable=too-many-arguments
    # We have 5 arguments plus the class to be created...
    # 4 arguments from init (keeping init clean and low on arguments),
    # 1 extra for controlling the check for companions function...
    def init_check_companions(cls,
                              host: str = None,
                              port: int = None,
                              listener_port: int = None,
                              debug: bool = None,
                              verify_port: bool = False):
        """Helper method for constructing an agent, with a special helper
        function if you are running companions on the same machine as this
        agent (judged by connecting to localhost), without overwriting the
        default values in init. When the companion is running on the same
        system as the python agent we check to see if an expected process is
        running, and if so we look for the listed port number that the process
        publishes on startup. The check for companions will prioritize
        executables before looking for something running from source
        (CompanionsMicroServer64 before CompanionsServer64, allegro for local
        development with a qrg directory installed at the root of some drive on
        the system or in the home directory) and will return the first port
        found (if multiple companions are running). If nothing is found, the
        default value is relied on. If nothing is running but an old port
        number is found, you won't connect either way as a companion isn't
        running - it just might attempt a non-default port. As such, this can
        be essentially used in place on regular init.

        Args:
            host (str, optional): the host value to pass to init, falls back to
                init defaults.
            port (int, optional): the port value to pass to init, falls back to
                init defaults.
            listener_port (int, optional): the listener_port value to pass to
                init, falls back to init defaults.
            debug (bool, optional): the debug value to pass to init, falls back
                to init defaults.
            verify_port (bool, optional): whether or not to verify the port
                number by checking the pid in the portnum.dat file (created by
                either running companions locally or in an exe) against the pid
                found on the running process where the portnum.dat file was
                found

        Returns:
            cls: instantiated cls object
        """
        kwargs = {}  # repack arguments for a non-default interrupting call
        if host:
            kwargs['host'] = host
        if port:
            kwargs['port'] = port
        if listener_port:
            kwargs['listener_port'] = listener_port
        if debug:
            kwargs['debug'] = debug
        # If no port was passed in and either no host or localhost (no host
        # would default to local)...
        if not port and (True if not host else host in LOCALHOST_DEFS):
            port = check_for_companions(verify_port)
            if port:
                kwargs['port'] = port
        return cls(**kwargs)

    @classmethod
    def parse_command_line_args(cls, argv: list = None):
        """Uses ArgumentParser to parse the args that this is called with.
        Additional benefit of searching your system for a running Companion if
        no port is specified and host is local (defaults to local). If no
        running companion is found use the default values from init. The
        additional argument -v for verify_pids will assert that the pid's match
        between what is found on the system and what is put in the file.

        Returns:
            cls: instantiated cls object

        Args:
            argv (list, optional): argument list (typically from sys.argv)
        """
        if not argv:
            argv = system_argument_list
        _, *args = argv  # ignore name of file...
        parser = ArgumentParser(description='Run Pythonian agent.')
        parser.add_argument('-u',
                            '--url',
                            type=valid_ip,
                            help='url where companions kqml server is hosted')
        parser.add_argument('-p',
                            '--port',
                            type=valid_port,
                            help='port companions kqml server is open on')
        parser.add_argument('-l',
                            '--listener_port',
                            type=valid_port,
                            help='port pythonian kqml server is open on')
        parser.add_argument('-d',
                            '--debug',
                            action='store_true',
                            help='whether or not to log debug messages')
        parser.add_argument('-v',
                            '--verify_port',
                            action='store_true',
                            help='whether or not to verify the port number by '
                            'checking the pid in the portnum.dat file '
                            '(created by either running companions '
                            'locally or in an exe) against the pid found '
                            'on the running process where the portnum.dat'
                            ' file was found')
        args = parser.parse_args(args)
        return cls.init_check_companions(host=args.url,
                                         port=args.port,
                                         listener_port=args.listener_port,
                                         debug=args.debug,
                                         verify_port=args.verify_port)

    # OUTPUT FUNCTIONS (OVERRIDES):

    # pylint: disable=arguments-differ
    def connect(self):
        """Rewrite of KQMLModule connect, only handles send_socket and output
        connections"""
        try:
            self.send_socket = socket()
            self.send_socket.connect((self.host, self.port))
            socket_write = SocketIO(self.send_socket, 'w')
            self.out = BufferedWriter(socket_write)
        except OSError as error_msg:
            LOGGER.error('Connection failed: %s', error_msg)
        # Verify that you can send messages...
        assert self.out is not None, \
            'Connection formed but output (%s) not set.' % (self.out)

    def send(self, msg: KQMLPerformative):
        """Override of send from KQMLModule, opens and closes socket around
        send for proper signaling to Companions

        Args:
            msg (KQMLPerformative): message that you are sending to Companions
        """
        self.connect()
        self.send_generic(msg, self.out)
        self.send_socket.shutdown(SHUT_RDWR)
        self.send_socket.close()
        self.send_socket = None
        self.out = None

    def send_on_local_port(self, msg: KQMLPerformative):
        """Sends a message on the local_out, i.e. sends a message on the
        listener_port. This is used for some specific functions that are not
        meant to be handled as a kqml performative.

        Args:
            msg (KQMLPerformative): message to be sent
        """
        self.send_generic(msg, self.local_out)

    def reply_on_local_port(self, msg: KQMLPerformative,
                            reply_msg: KQMLPerformative):
        """Replies to a message on the local port (listener port)

        Args:
            msg (KQMLPerformative): message to reply to
            reply_msg (KQMLPerformative): message to reply with
        """
        sender = msg.get('sender')
        if sender is not None:
            reply_msg.set('receiver', sender)
        reply_with = msg.get('reply-with')
        if reply_with is not None:
            reply_msg.set('in-reply-to', reply_with)
        self.send_on_local_port(reply_msg)

    @staticmethod
    def send_generic(msg: KQMLPerformative, out: BufferedWriter):
        """Basic send mechanism copied (more or less) from pykqml. Writes the
        msg as a string to the output buffer then flushes it.

        Args:
            msg (KQMLPerformative): Message to be sent
            out (BufferedWriter): The output to write to, needed for sending to
                Companions and sending on our own port.
        """
        LOGGER.debug('Sending: %s', msg)
        try:
            msg.write(out)
        except IOError:
            LOGGER.error('IOError during message sending')
        out.write(b'\n')
        out.flush()

    # INPUT FUNCTIONS (OVERRIDE AND ADDITION):

    def listen(self):
        """Socket server, listens for new KQML messages and dispatches them
        accordingly.

        Doesn't necessarily need threading; Could just start dispatcher and
        after it returns accept next connection. This couldn't handle loads of
        inputs while being bogged down processing. To avoid this issue we
        thread the dispatching so the functions that get called are run in a
        separate Thread. We're using ThreadPoolExecutor because sockets use io,
        io is blocking and threads allow you to not block.
        """
        with ThreadPoolExecutor(max_workers=5) as executor:
            while self.ready:
                connection, _ = self.listen_socket.accept()
                LOGGER.debug('Received connection: %s', connection)
                socket_write = SocketIO(connection, 'w')
                self.local_out = BufferedWriter(socket_write)
                socket_read = SocketIO(connection, 'r')
                read_input = KQMLReader(BufferedReader(socket_read))
                self.dispatcher = KQMLDispatcher(self, read_input, self.name)
                LOGGER.debug('Starting dispatcher: %s', self.dispatcher)
                executor.submit(self.dispatcher.start)
                self.state = 'dispatching'

    def receive_eof(self):
        """Override of KQMLModule, shuts down the dispatcher after receiving
        the end of file (eof) signal. This happens after every message...
        """
        LOGGER.debug('Closing connection on dispatcher: %s', self.dispatcher)
        self.dispatcher.shutdown()
        self.dispatcher = None
        self.state = 'idle'

    # OVERRIDES TO KQMLModule:

    def start(self):
        pass

    def connect1(self):
        pass

    def exit(self, n: int = 0):
        """Override of KQMLModule; Closes this agent, shuts down the threaded
        execution loop (by turning off the ready flag), shuts the dispatcher
        down (if running), and then joins any running threads...

        Args:
            n (int, optional): the value to pass along to sys.exit
        """
        LOGGER.info('Shutting down agent: %s', self.name)
        self.ready = False  # may need to wait for threads to stop...
        if self.dispatcher is not None:
            self.dispatcher.shutdown()
        self.listener.join()

    # COMPANIONS SPECIFIC OVERRIDES:

    def register(self):
        """Override of KQMLModule, registers this agent with Companions"""
        LOGGER.info('Registering...')
        registration = (
            f'(register :sender {self.name} :receiver facilitator :content '
            f'("socket://{self.host}:{self.listener_port}" nil nil '
            f'{self.listener_port}))')
        self.send(performative(registration))

    def receive_other_performative(self, msg: KQMLPerformative):
        """Override of KQMLModule default... ping isn't currently supported by
        pykqml so we handle other to catch ping and otherwise throw an error.

        Arguments:
            msg (KQMLPerformative): other type of performative, if ping we
                reply with a ping update otherwise error
        """
        if msg.head() == 'ping':
            LOGGER.info('Receive ping... %s', msg)
            reply_content = (
                f'(update :sender {self.name} :content (:agent {self.name} '
                f':uptime {self.uptime()} :status :OK :state {self.state} '
                f':machine {gethostname()} :subscriptions {self.num_subs}))')
            self.reply_on_local_port(msg, performative(reply_content))
        else:
            self.error_reply(msg, f'unexpected performative: {msg}')

    # Everything else (reply, error_reply, handle_exceptions,
    #   send_with_continuation, subscribe_request, subscribe_tell,
    #   and ALL the remaining receive_* functions) is fine as is

    # HELPERS:

    def uptime(self) -> str:
        """Cyc-style time since start. Using the python-dateutil library to do
        simple relative delta calculations for the uptime.

        Returns:
            str: string of the form
                     '(years months days hours minutes seconds)'
                 where years, months, days, etc are the uptime in number of
                 years, months, days, etc.
        """
        time_list = ['years', 'months', 'days', 'hours', 'minutes', 'seconds']
        diff = relativedelta(datetime.now(), self.starttime)
        time_diffs = [getattr(diff, time_period) for time_period in time_list]
        return f'({" ".join(map(str, time_diffs))})'

    def response_to_query(self, msg: KQMLPerformative,
                          content: KQMLPerformative, results: Any,
                          response_type: str):
        """Based on the response type, will create a properly formed reply
        with the results either input as patterns or bound to the arguments
        from the results. The reply is a tell which is then sent to Companions.

        Goes through the arguments and the results together to either bind a
        argument to the result or simple return the result in the place of that
        argument. The reply content is filled with these argument/result lists
        (they are listified before appending) before being added to the tell
        message and subsequently sent off to Companions.

        Arguments:
            msg (KQMLPerformative): the message being passed along to reply
            content (KQMLPerformative): query, starts with a predicate and the
                remainder is the arguments
            results (Any): The results of performing the query
            response_type (str): the given response type, if it is not given or
                is given to be pattern, the variable will be set to True,
                otherwise False
        """
        LOGGER.debug('Responding to query: %s, %s, %s', msg, content, results)
        response_type = response_type is None or response_type == ':pattern'
        reply_content = KQMLList(content.head())
        results_list = results if isinstance(results, list) else [results]
        result_index = 0
        arg_len = len(content.data[1:])
        for i, each in enumerate(content.data[1:]):
            # if argument is a variable, replace in the pattern or bind
            if str(each[0]) == '?':
                # if last argument and there's still more in results
                if i == arg_len and result_index < len(results_list) - 1:
                    pattern = results_list[result_index:]  # get remaining list
                else:
                    pattern = results_list[result_index]
                reply_with = pattern if response_type else (each, pattern)
                reply_content.append(listify(reply_with))
                result_index += 1
            # if not a variable, replace in the pattern. Ignore for bind
            elif response_type:
                reply_content.append(each)
        # no need to wrap reply_content in parens, KQMLList will do that for us
        reply_msg = f'(tell :sender {self.name} :content {reply_content})'
        self.reply(msg, performative(reply_msg))
예제 #9
0
class Pythonian(KQMLModule):
    name = 'Pythonian'

    def __init__(self, listener_port=8950, **kwargs):
        self.listener_port = listener_port
        super().__init__(name=self.name, **kwargs)
        self.starttime = datetime.now()
        self.listen_socket = socket.socket()
        self.listen_socket.bind(('', self.listener_port))
        self.listen_socket.listen(10)
        self.start()
        # self.listen()

    def register(self):
        perf_string = f'(register :sender {self.name} :receiver facilitator)'
        perf = performative(perf_string)
        socket_url = f'"socket://{self.host}:{self.listener_port}"'
        perf.set('content',
                 KQMLList([socket_url, 'nil', 'nil', self.listener_port]))
        self.send(perf)

    def receive_tell(self, msg, content):
        print(msg)
        print(content)

    def receive_eof(self):
        connection, _ = self.listen_socket.accept()
        print(connection)
        # socket_write = socket.SocketIO(connection, 'w')
        # self.out = io.BufferedWriter(socket_write)
        socket_read = socket.SocketIO(connection, 'r')
        inp_reader = KQMLReader(io.BufferedReader(socket_read))
        # self.inp = inp_reader
        self.dispatcher = KQMLDispatcher(self, inp_reader, self.name)
        self.dispatcher.start()

    # def listen(self):
    #     while True:
    #         connection, _ = self.listen_socket.accept()
    #         print(connection)
    #         socket_write = socket.SocketIO(connection, 'w')
    #         self.out = io.BufferedWriter(socket_write)
    #         socket_read = socket.SocketIO(connection, 'r')
    #         inp_reader = KQMLReader(io.BufferedReader(socket_read))
    #         # self.inp = inp_reader
    #         self.dispatcher = KQMLDispatcher(self, inp_reader, self.name)
    #         self.dispatcher.start()

    def receive_other_performative(self, msg):
        if msg.head() == 'ping':
            update_string = (
                f'(update :sender {self.name} :content (:agent {self.name} '
                f':uptime {self._uptime()} :status :OK :state idle '
                f':machine {socket.gethostname()}))')
            self.reply(msg, performative(update_string))
        else:
            self.error_reply(msg, 'unexpected performative: ' + str(msg))

    def _uptime(self):
        time_list = ['years', 'months', 'days', 'hours', 'minutes', 'seconds']
        diff = relativedelta(datetime.now(), self.starttime)
        time_diffs = [getattr(diff, time) for time in time_list]
        return f'({" ".join(map(str, time_diffs))})'
예제 #10
0
class KQMLModule(object):
    def __init__(self, argv=None, **kwargs):
        defaults = dict(host='localhost',
                        port=6200,
                        is_application=False,
                        testing=False,
                        socket=None,
                        name=None,
                        group_name=None,
                        scan_for_port=False,
                        debug=False,
                        do_register=True)
        self.dispatcher = None
        self.MAX_PORT_TRIES = 100
        self.reply_id_counter = 1

        if isinstance(argv, list):
            kwargs.update(translate_argv(argv))
        elif argv is not None:
            raise KQMLException("Unusable type for keyord argument `argv`.")

        for kw, arg in kwargs.items():
            if kw not in defaults.keys():
                raise ValueError('Unexpected keyword argument: %s' % kw)
            else:
                defaults.pop(kw)
            self.__setattr__(kw, arg)
        for kw, arg in defaults.items():
            self.__setattr__(kw, arg)

        if self.debug:
            logger.setLevel(logging.DEBUG)
        else:
            logger.setLevel(logging.INFO)

        if not self.testing:
            self.out = None
            self.inp = None
            logger.info('Using socket connection')
            conn = self.connect(self.host, self.port)
            if not conn:
                logger.error('Connection failed')
                self.exit(-1)
            assert self.inp is not None and self.out is not None, \
                "Connection formed but input (%s) and output (%s) not set." % \
                (self.inp, self.out)
        else:
            logger.info('Using stdio connection')
            self.out = io.BytesIO()
            self.inp = KQMLReader(io.BytesIO())

        self.dispatcher = KQMLDispatcher(self, self.inp, self.name)

        if self.do_register:
            self.register()

    def start(self):
        if not self.testing:
            self.dispatcher.start()

    def subscribe_request(self, req_type):
        msg = KQMLPerformative('subscribe')
        content = KQMLList('request')
        content.append('&key')
        content.set('content', KQMLList.from_string('(%s . *)' % req_type))
        msg.set('content', content)
        self.send(msg)

    def subscribe_tell(self, tell_type):
        msg = KQMLPerformative('subscribe')
        content = KQMLList('tell')
        content.append('&key')
        content.set('content', KQMLList.from_string('(%s . *)' % tell_type))
        msg.set('content', content)
        self.send(msg)

    def connect(self, host=None, startport=None):
        if host is None:
            host = self.host
        if startport is None:
            startport = self.port
        if not self.scan_for_port:
            return self.connect1(host, startport, True)
        else:
            maxtries = self.MAX_PORT_TRIES
            for port in range(startport, startport + maxtries):
                conn = self.connect1(host, port, False)
                if conn:
                    return True
            logger.error('Failed to connect to ' + host + ':' + \
                         startport + '-' + port)
            return False

    def connect1(self, host, port, verbose=True):
        try:
            self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.socket.connect((host, port))
            sw = socket.SocketIO(self.socket, 'w')
            self.out = io.BufferedWriter(sw)
            sr = socket.SocketIO(self.socket, 'r')
            self.inp = KQMLReader(io.BufferedReader(sr))
            return True
        except socket.error as e:
            if verbose:
                logger.error(e)
            return False

    def register(self):
        if self.name is not None:
            perf = KQMLPerformative('register')
            perf.set('name', self.name)
            if self.group_name is not None:
                try:
                    if self.group_name.startswith('('):
                        perf.sets('group', self.group_name)
                    else:
                        perf.set('group', self.group_name)
                except IOError:
                    logger.error('bad group name: ' + self.group_name)
            self.send(perf)

    def ready(self):
        msg = KQMLPerformative('tell')
        content = KQMLList(['module-status', 'ready'])
        msg.set('content', content)
        self.send(msg)

    def exit(self, n):
        if self.is_application:
            sys.exit(n)
        else:
            if self.dispatcher is not None:
                self.dispatcher.shutdown()
            sys.exit(n)

    def stop_waiting(self):
        raise StopWaitingSignal()

    def receive_eof(self):
        self.exit(0)

    def receive_message_missing_verb(self, msg):
        self.error_reply(msg, 'missing verb in performative')

    def receive_message_missing_content(self, msg):
        self.error_reply(msg, 'missing content in performative')

    def receive_ask_if(self, msg, content):
        self.error_reply(msg, 'unexpected performative: ask-if')

    def receive_ask_all(self, msg, content):
        self.error_reply(msg, 'unexpected performative: ask-all')

    def receive_ask_one(self, msg, content):
        self.error_reply(msg, 'unexpected performative: ask-one')

    def receive_stream_all(self, msg, content):
        self.error_reply(msg, 'unexpected performative: stream-all')

    def receive_tell(self, msg, content):
        logger.error('unexpected performative: tell')

    def receive_untell(self, msg, content):
        self.error_reply(msg, 'unexpected performative: untell')

    def receive_deny(self, msg, content):
        self.error_reply(msg, 'unexpected performative: deny')

    def receive_insert(self, msg, content):
        self.error_reply(msg, 'unexpected performative: insert')

    def receive_uninsert(self, msg, content):
        self.error_reply(msg, 'unexpected performative: uninsert')

    def receive_delete_one(self, msg, content):
        self.error_reply(msg, 'unexpected performative: delete-one')

    def receive_delete_all(self, msg, content):
        self.error_reply(msg, 'unexpected performative: delete-all')

    def receive_undelete(self, msg, content):
        self.error_reply(msg, 'unexpected performative: undelete')

    def receive_achieve(self, msg, content):
        self.error_reply(msg, 'unexpected performative: achieve')

    def receive_advertise(self, msg, content):
        self.error_reply(msg, 'unexpected performative: advertise')

    def receive_unadvertise(self, msg, content):
        self.error_reply(msg, 'unexpected performative: unadvertise')

    def receive_subscribe(self, msg, content):
        self.error_reply(msg, 'unexpected performative: subscribe')

    def receive_standby(self, msg, content):
        self.error_reply(msg, 'unexpected performative: standby')

    def receive_register(self, msg, content):
        self.error_reply(msg, 'unexpected performative: register')

    def receive_forward(self, msg, content):
        self.error_reply(msg, 'unexpected performative: forward')

    def receive_broadcast(self, msg, content):
        self.error_reply(msg, 'unexpected performative: broadcast')

    def receive_transport_address(self, msg, content):
        self.error_reply(msg, 'unexpected performative: transport-address')

    def receive_broker_one(self, msg, content):
        self.error_reply(msg, 'unexpected performative: broker-one')

    def receive_broker_all(self, msg, content):
        self.error_reply(msg, 'unexpected performative: broker-all')

    def receive_recommend_one(self, msg, content):
        self.error_reply(msg, 'unexpected performative: recommend-one')

    def receive_recommend_all(self, msg, content):
        self.error_reply(msg, 'unexpected performative: recommend-all')

    def receive_recruit_one(self, msg, content):
        self.error_reply(msg, 'unexpected performative: recruit-one')

    def receive_recruit_all(self, msg, content):
        self.error_reply(msg, 'unexpected performative: recruit-all')

    def receive_reply(self, msg, content):
        logger.error(msg, 'unexpected performative: reply')

    def receive_request(self, msg, content):
        self.error_reply(msg, 'unexpected performative: request')

    def receive_eos(self, msg):
        self.error_reply(msg, 'unexpected performative: eos')

    def receive_error(self, msg):
        logger.error('Error received: "%s"' % msg)

    def receive_sorry(self, msg):
        logger.error('unexpected performative: sorry')

    def receive_ready(self, msg):
        logger.error(msg, 'unexpected performative: ready')

    def receive_next(self, msg):
        self.error_reply(msg, 'unexpected performative: next')

    def receive_rest(self, msg):
        self.error_reply(msg, 'unexpected performative: rest')

    def receive_discard(self, msg):
        self.error_reply(msg, 'unexpected performative: discard')

    def receive_unregister(self, msg):
        logger.error(msg, 'unexpected performative: unregister')

    def receive_other_performative(self, msg):
        self.error_reply(msg, 'unexpected performative: ' + str(msg))

    def handle_exception(self, ex):
        logger.error(str(ex))

    def send(self, msg):
        try:
            msg.write(self.out)
        except IOError:
            logger.error('IOError during message sending')
            pass
        self.out.write(b'\n')
        self.out.flush()
        logger.debug(msg.__repr__())

    def send_with_continuation(self, msg, cont):
        reply_id_base = 'IO-'
        if self.name is not None:
            reply_id_base = self.name + '-'
        reply_id = reply_id_base + str(self.reply_id_counter)
        self.reply_id_counter += 1
        msg.append(':reply-with')
        msg.append(reply_id)
        self.dispatcher.add_reply_continuation('%s' % reply_id, cont)
        self.send(msg)

    def reply(self, msg, reply_msg):
        sender = msg.get('sender')
        if sender is not None:
            reply_msg.set('receiver', sender)
        reply_with = msg.get('reply-with')
        if reply_with is not None:
            reply_msg.set('in-reply-to', reply_with)
        self.send(reply_msg)

    def error_reply(self, msg, comment):
        reply_msg = KQMLPerformative('error')
        reply_msg.sets('comment', comment)
        self.reply(msg, reply_msg)
예제 #11
0
class Pythonian(KQMLModule):
    """Wrapper around the KQMLModule to create a simple Companions integration.

    KQML gives us default communications protocols that we have altered to fit
    both Lisp standards and Companion expectations. In addition, we have added
    some functions that interface with Companions both by exposing the
    Companions style communication functions and by adding helpful generic
    wrappers around those functions to make integrating even more simple.

    Extends:
        KQMLModule

    Variables:
        name {str} -- The name of the agent to register with
    """

    name = "Pythonian"

    def __init__(self, **kwargs):
        # Call the parent class' constructor which sends a registration
        # message, setting the agent's name to be recognized by the
        # Facilitator.
        if 'localPort' in kwargs:  # keyword is camelCase but,
            self.local_port = kwargs['localPort']  # variable is snake_case
            del kwargs['localPort']
        else:
            self.local_port = 8950
        if 'port' in kwargs:
            port = kwargs['port']
            del kwargs['port']
        else:
            port = 9000

        super(Pythonian, self).__init__(name=self.name, port=port, **kwargs)
        self.starttime = datetime.now()
        # tracking functions related to asks and achieves
        self.achieves = {}
        self.asks = {}
        # subscription stuff
        self.subscribers = {}  # query:[subscribe,msgs]
        self.subcribe_data_old = {}
        self.subcribe_data_new = {}
        self.polling_interval = 1
        self.poller = threading.Thread(
            target=self._poll_for_subscription_updates, args=[])
        # Finally, start the listener for incoming messages
        self.listen_socket = socket.socket()
        self.listen_socket.bind(('', self.local_port))
        self.listen_socket.listen(10)
        self.listener = threading.Thread(target=self._listen, args=[])
        # ready to go
        self.state = 'idle'
        self.ready = True
        self.poller.start()
        self.listener.start()

    ###########################################################################
    #                            Utility functions                            #
    ###########################################################################

    def add_achieve(self, name, func):
        """Adds the given function (func) to the dictionary of achieves under
        the key of the given name.

        Arguments:
            name {str} -- key in achieves dictionary
            func {function} -- value paired to key, function to be called on
                               achieve (with given name)
        """
        self.achieves[name] = func

    def achieve_on_agent(self, receiver, data):
        """Sends a KQML message to the proper receiver with the data formatted
        properly as a list.

        Arguments:
            receiver {str} -- name of the receiver
            data {[type]} -- anything
        """
        msg = KQMLPerformative('achieve')
        msg.set('sender', self.name)
        msg.set('receiver', receiver)
        msg.set('content', listify(data))
        # pylint: disable=no-member
        # pylint was unable to pick up on the host and port variables being in
        # a defaults dict which is then used to do __setattr__ with the key
        # value pairs from the dict.
        self.connect(self.host, self.port)
        self.send(msg)
        # TODO - Does this need to close after?
        # self._close_socket()

    def add_ask(self, name, func, pattern, subscribable=False):
        """Adds the given function (func) to the dictionary of asks under the
        key of the given name. If subscribable is true then we also add the
        pattern to our subscription dictionary and advertise it.

        Arguments:
            name {str} -- key in asks dictionary
            func {function} -- value paired to key, function to be called on
                               ask
            pattern {str} -- name of subscribers

        Keyword Arguments:
            subscribable {bool} -- [description] (default: {False})
        """
        self.asks[name] = func
        if subscribable:
            self.subscribers[pattern] = []
            self.advertise_subscribe(pattern)

    def advertise(self, pattern):
        """Connects to the host, then builds and sends a message to the
        facilitator with the pattern input as the content, then closes the
        connection.

        Arguments:
            pattern {[type]} -- content to be sent
        """
        # pylint: disable=no-member
        # pylint was unable to pick up on the host and port variables being in
        # a defaults dict which is then used to do __setattr__ with the key
        # value pairs from the dict.
        self.connect(self.host, self.port)
        reply_id = f'id{self.reply_id_counter}'
        self.reply_id_counter += 1
        content = KQMLPerformative('ask-all')
        content.set('receiver', self.name)
        content.set('in-reply-to', reply_id)
        content.set('content', pattern)
        msg = KQMLPerformative('advertise')
        msg.set('sender', self.name)
        msg.set('receiver', 'facilitator')
        msg.set('reply-with', reply_id)
        msg.set('content', content)
        self.send(msg)
        self._close_socket()

    def advertise_subscribe(self, pattern):
        """Connects to the host, then builds and sends a message to the
        facilitator with the pattern input as the content, then closes the
        connection.

        Arguments:
            pattern {[type]} -- content to be sent
        """
        # pylint: disable=no-member
        # pylint was unable to pick up on the host and port variables being in
        # a defaults dict which is then used to do __setattr__ with the key
        # value pairs from the dict.
        self.connect(self.host, self.port)
        reply_id = f'id{self.reply_id_counter}'
        self.reply_id_counter += 1
        content = KQMLPerformative('ask-all')
        content.set('receiver', self.name)
        content.set('in-reply-to', reply_id)
        content.set('content', pattern)
        subscribe = KQMLPerformative('subscribe')
        subscribe.set('receiver', self.name)
        subscribe.set('in-reply-to', reply_id)
        subscribe.set('content', content)
        msg = KQMLPerformative('advertise')
        msg.set('sender', self.name)
        msg.set('receiver', 'facilitator')
        msg.set('reply-with', reply_id)
        msg.set('content', subscribe)
        self.send(msg)
        self._close_socket()

    def update_query(self, query, *args):
        """Looks to see if the arguments to query have changes since last time,
        if so it will update those arguments in the subscribe_data_new dict.

        Arguments:
            query {[type]} -- string representing the query
            *args {[type]} -- anything you would input into the query
        """
        if query in self.subcribe_data_old:
            if self.subcribe_data_old[query] != args:
                LOGGER.debug("Updating %s with %s", query, args)
                self.subcribe_data_new[query] = args

    def insert_data(self, receiver, data, wm_only=False):
        """Takes the data input by the user and processes it into an insert
        message which is subsequently sent off to Companions.

        Arguments:
            receiver {str} -- name of the receiver
            data {[type]} -- anything that can be listified

        Keyword Arguments:
            wm_only {bool} -- whether or not this should only be inserted into
                              the working memory (default: {False})
        """
        msg = KQMLPerformative('insert')
        msg.set('sender', self.name)
        msg.set('receiver', receiver)
        if wm_only:
            msg.append(':wm-only?')
        msg.set('content', listify(data))
        # pylint: disable=no-member
        # pylint was unable to pick up on the host and port variables being in
        # a defaults dict which is then used to do __setattr__ with the key
        # value pairs from the dict.
        self.connect(self.host, self.port)
        self.send(msg)
        # TODO - Does this need to close after?
        # self._close_socket()

    def insert_to_microtheory(self, receiver, data, mt_name, wm_only=False):
        """Inserts a fact into the given microtheory using ist-Information

        Arguments:
            receiver {str} -- passed through to insert data
            data {[type]} -- anything that can be listified, the fact to insert
            mt_name {str} -- microtheory name

        Keyword Arguments:
            wm_only {bool} -- whether or not this fact should go to working
                              memory only or just the KB (default: {False})
        """
        new_data = '(ist-Information {} {})'.format(mt_name, data)
        self.insert_data(receiver, new_data, wm_only)

    def insert_microtheory(self, receiver, data_list, mt_name, wm_only=False):
        """Inserts a list of facts into the given microtheory

        Arguments:
            receiver {str} -- passed through to insert data
            data_list {list} -- list of facts to be added, facts can be
                                anything you can listify.
            mt_name {str} -- microtheory name

        Keyword Arguments:
            wm_only {bool} -- whether or not this fact should go to working
                              memory only or just the KB (default: {False})
        """
        for data in data_list:
            self.insert_to_microtheory(receiver, data, mt_name, wm_only)

    ###########################################################################
    #                                Overrides                                #
    ###########################################################################

    def register(self):
        """Overrides the KQMLModule default and uses Companions standards to
        send proper registration.
        """
        if self.name is not None:
            perf = KQMLPerformative('register')
            perf.set('sender', self.name)
            perf.set('receiver', 'facilitator')
            # pylint: disable=no-member
            # pylint was unable to pick up on the host variable being in a
            # defaults dict which is then used to do __setattr__ with the key
            # value pairs from the dict.
            socket_url = f'"socket://{self.host}:{self.local_port}"'
            content = KQMLList([socket_url, 'nil', 'nil', self.local_port])
            perf.set('content', content)
            self.send(perf)

    def receive_eof(self):
        """Override of the KQMLModule default which exits on eof, we instead
        just pass and do nothing on end of file.
        """
        # pylint: disable=unnecessary-pass
        # When there are no comments in this function (document string
        # included) this method passes the unnecessary-pass but fails on the
        # missing-docstring. This is a pylint bug.
        pass

    def receive_ask_one(self, msg, content):
        """Override of default ask one, creates Companions style responses.

        Gets the arguments bindings from the cdr of the content. The predicate
        (car) is then used to find the function bound to the ask predicate, and
        that function is called with the bounded argument list unpacked into
        it's own inputs. The resulting query is then passed along to the
        _response_to_query helper which will properly respond to patterns or
        bindings based on out response type.

        Arguments:
            msg {KQMLPerformative} -- pred and response type
            content {KQMLPerformative} -- arguments of the ask, passed to pred
        """
        bounded = []
        for each in content.data[1:]:
            if str(each[0]) != '?':
                bounded.append(each)
        results = self.asks[content.head()](*bounded)
        self._response_to_query(msg, content, results, msg.get('response'))

    def receive_achieve(self, msg, content):
        """Overrides the default KQMLModule receive for achieves and instead
        does basic error checking before attempting the action by calling the
        proper ask function with the arguments passed along as inputs.

        Arguments:
            msg {KQMLPerformative} -- predicate/ signifier of task
            content {KQMLPerformative} -- action task is referring to
        """
        if content.head() == 'task':
            action = content.get('action')
            if action:
                if action.head() in self.achieves:
                    try:
                        args = action.data[1:]
                        results = self.achieves[action.head()](*args)
                        LOGGER.debug("Return of achieve: %s", results)
                        reply = KQMLPerformative('tell')
                        reply.set('sender', self.name)
                        reply.set('content', listify(results))
                        self.reply(msg, reply)
                    # pylint: disable=broad-except
                    # The above try can throw KQMLBadPerformativeException,
                    # KQMLException, ValueError, and StopWaitingSignal at the
                    # least. To stay simple and more in-line with PEP8 we are
                    # logging the traceback and the user should be made aware
                    # of the fact that an error occurred via the error_reply.
                    # For these reasons the 'broad-except' is valid here.
                    except Exception:
                        LOGGER.debug(traceback.print_exc())
                        error_msg = 'An error occurred while executing: '
                        self.error_reply(msg, error_msg + action.head())
                else:
                    self.error_reply(msg, 'Unknown action: ' + action.head())
            else:
                self.error_reply(msg, 'No action for achieve task provided')
        else:
            error_msg = 'Unexpected achieve command: '
            self.error_reply(msg, error_msg + content.head())

    def receive_tell(self, msg, content):
        """Override default KQMLModule tell to simply log the content and reply
        with nothing

        Arguments:
            msg {KQMLPerformative} -- tell to be passed along in reply
            content {KQMLPerformative} -- tell from companions to be logged
        """
        LOGGER.debug('received tell: %s', content)  # lazy logging
        reply_msg = KQMLPerformative('tell')
        reply_msg.set('sender', self.name)
        reply_msg.set('content', None)
        self.reply(msg, reply_msg)

    def receive_subscribe(self, msg, content):
        """Override of KQMLModule default, expects a performative of ask-all.

        Gets the ask-all query from the message contents (the contents of
        the content variable is the query that we care about), then checks
        to see if the query head is in the dictionary of available asks and
        checks if the query string is in the dictionary of subscribers. If both
        of these are true we then append the message to the subscriber query,
        clean out any previous subscription data, and reply with a tell ok
        message.

        Arguments:
            msg {KQMLPerformative} -- performative to be passed along in reply
                                      and stored in the subscribers dictionary
            content {KQMLPerformative} -- ask-all for a query
        """
        LOGGER.debug('received subscribe: %s', content)  # lazy logging
        if content.head() == 'ask-all':
            # TODO - track msg ideas and use for filtering
            query = content.get('content')
            query_string = query.to_string()
            if query.head() in self.asks and query_string in self.subscribers:
                self.subscribers[query_string].append(msg)
                self.subcribe_data_old[query_string] = None
                self.subcribe_data_new[query_string] = None
                reply_msg = KQMLPerformative('tell')
                reply_msg.set(':sender', self.name)
                reply_msg.set('content', ':ok')
                self.reply(msg, reply_msg)

    def receive_other_performative(self, msg):
        """Override of KQMLModule default... ping isn't currently supported by
        pykqml so we handle other to catch ping and otherwise throw an error.

        Arguments:
            msg {KQMLPerformative} -- other type of performative, if ping we
                                      reply with a ping update otherwise error
        """
        if msg.head() == 'ping':
            reply_content = KQMLList([':agent', self.name])
            reply_content.append(':uptime')
            reply_content.append(self._uptime())
            # TODO - check if .set('status', ':OK') can be used here instead
            reply_content.append(':status')
            reply_content.append(':OK')
            reply_content.append(':state')
            reply_content.append('idle')
            reply_content.append(':machine')
            reply_content.append(socket.gethostname())
            reply = KQMLPerformative('update')
            reply.set('sender', self.name)
            # reply.set('receiver', msg.get('sender'))
            # reply.set('in-reply-to', msg.get('reply-with'))
            reply.set('content', reply_content)
            self.reply(msg, reply)
        else:
            self.error_reply(msg, 'unexpected performative: ' + str(msg))

    ###########################################################################
    #                                Threading                                #
    ###########################################################################

    def _poll_for_subscription_updates(self):
        """Goes through the subscription updates as they come in and properly
        respond to the query (with _response_to_query).
        """
        LOGGER.debug("Running subcription poller")
        while self.ready:
            for query, new_data in self.subcribe_data_new.items():
                if new_data is not None:
                    for msg in self.subscribers[query]:
                        ask = msg.get('content')
                        query = ask.get('content')
                        LOGGER.debug("Sending subscribe update for %s", query)
                        res_type = ask.get('response')
                        self._response_to_query(msg, query, new_data, res_type)
                        self.subcribe_data_old[query] = new_data
            for query, _ in self.subcribe_data_new.items():
                self.subcribe_data_new[query] = None
            time.sleep(self.polling_interval)

    def _listen(self):
        """Sets up input and output socket connections to our listener.

        Infinite loop while ready to connect to our listener socket. On connect
        we get the write socket as a Buffered Writer and the read socket as a
        KQML Reader (which passes through a Buffered Reader). The reader is
        then attached to a KQML Dispatcher which is subsequently started, and
        passed along to the executor (which is a thread pool manager).
        """
        LOGGER.debug('listening')
        with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
            while self.ready:
                connection, _ = self.listen_socket.accept()
                LOGGER.debug('received connection')
                socket_write = socket.SocketIO(connection, 'w')
                self.out = io.BufferedWriter(socket_write)
                socket_read = socket.SocketIO(connection, 'r')
                read_input = KQMLReader(io.BufferedReader(socket_read))
                self.dispatcher = KQMLDispatcher(self, read_input, self.name)
                executor.submit(self.dispatcher.start)

    ###########################################################################
    #                             General helpers                             #
    ###########################################################################

    def _response_to_query(self, msg, content, results, response_type):
        """Based on the response type, will create a properly formed reply
        with the results either input as patterns or bound to the arguments
        from the results. The reply is a tell which is then sent to Companions.

        Goes through the arguments and the results together to either bind a
        argument to the result or simple return the result in the place of that
        argument. The reply content is filled with these argument/result lists
        (they are listified before appending) before being added to the tell
        message and subsequently sent off to Companions.

        Arguments:
            msg {KQMLPerformative} -- the message being passed along to reply
            content {[type]} -- query, starts with a predicate and the
                                remainder is the arguments
            results {[type]} -- The results of performing the query
            response_type {[type]} -- the given response type, if it is not
                                      given or is given to be pattern, the
                                      variable will be set to True, otherwise
                                      False.
        """
        response_type = response_type is None or response_type == ':pattern'
        reply_content = KQMLList(content.head())
        results_list = results if isinstance(results, list) else [results]
        result_index = 0
        arg_len = len(content.data[1:])
        for i, each in enumerate(content.data[1:]):
            # if argument is a variable
            if str(each[0]) == '?':
                # if last argument and there's still more in results
                if i == arg_len and result_index < len(results_list)-1:
                    # get the remaining list
                    pattern = results_list[result_index:]
                    # pattern or binding...
                    reply_with = pattern if response_type else (each, pattern)
                    reply_content.append(listify(reply_with))
                else:
                    # same logic as above, just doesn't get the remaining list
                    pattern = results_list[result_index]
                    reply_with = pattern if response_type else (each, pattern)
                    reply_content.append(listify(reply_with))
                    result_index += 1
            else:
                if response_type:  # only add the arguments if this is pattern
                    reply_content.append(each)
        reply_msg = KQMLPerformative('tell')
        reply_msg.set('sender', self.name)
        reply_msg.set('content', reply_content)
        self.reply(msg, reply_msg)

    def _close_socket(self):
        """shutsdown the dispatcher and closes the socket"""
        self.dispatcher.shutdown()
        self.socket.close()

    def _uptime(self):
        """Cyc-style time since start (includes leap years)

        Returns:
            str -- string of the form
                    '(years months days hours minutes seconds)'
                   where years, months, days, etc are the uptime in number of
                   years, months, days, etc.
        """
        now = datetime.now()
        years = now.year - self.starttime.year
        # months
        if now.year == self.starttime.year:
            months = now.month - self.starttime.month
        else:
            months = 12 - self.starttime.month + now.month
        # days
        if now.month == self.starttime.month:
            days = now.day - self.starttime.day
        elif self.starttime.month in [1, 3, 5, 7, 8, 10, 12]:
            days = 31 - self.starttime.day + now.day
        elif self.starttime.month in [4, 6, 9, 11]:
            days = 30 - self.starttime.day + now.day
        else:
            days = 28 - self.starttime.day + now.day
        year_list = range(self.starttime.year, now.year+1)
        extra_days = sum(map(int, [is_leap(year) for year in year_list]))
        days += extra_days
        # hours
        if self.starttime.day == now.day:
            hours = now.hour - self.starttime.hour
        else:
            hours = 24 - self.starttime.hour + now.hour
        # minutes
        if self.starttime.hour == now.hour:
            minutes = now.minute - self.starttime.minute
        else:
            minutes = 60 - self.starttime.minute + now.minute
        # seconds
        if self.starttime.minute == now.minute:
            seconds = now.second - self.starttime.second
        else:
            seconds = 60 - self.starttime.second + now.second
        time_list = [years, months, days, hours, minutes, seconds]
        # return '({})'.format(" ".join(map(str, time_list)))
        return f'({" ".join(map(str, time_list))})'