class API(type):
    ''' Metaclass for defining an interfaceable API.
    '''
    
    def __new__(mcls, clsname, bases, namespace, *args, _short_circuit=False,
                **kwargs):
        ''' Modify the existing namespace:
        1.  remove any @fixture_api methods.
        2.  extract any @public_api.fixture methods
        3.  extract any @public_api.interface methods
        4.  create cls.__fixture__ class object
        5.  create cls.__interface__ class object
        '''
        # Dead-end for fixtures and interfaces
        if _short_circuit:
            cls = super().__new__(mcls, clsname, bases, namespace, *args,
                                  **kwargs)
        
        # For the parent/public class...
        else:
            public_name = clsname
            fixture_name = clsname + 'Fixture'
            interface_name = clsname + 'Interface'
                
            public_namespace = {}
            fixture_namespace = {}
            interface_namespace = {}
        
            # No need to modify bases, either for the actual type or the
            # fixture/interface
            
            # Iterate over the entire defined namespace.
            for name, obj in namespace.items():
                is_fixture = getattr(obj, '__is_fixture__', False)
                is_interface = getattr(obj, '__is_interface__', False)
                
                # If the .fixture magic attr was defined, use it; else, just
                # fall back to inheritance
                fixture = getattr(obj, '__fixture__', None)
                interface = getattr(obj, '__interface__', None)
                
                # __is_fixture__ get sent only to the fixture.
                if is_fixture:
                    fixture_namespace[name] = obj
                    
                elif is_interface:
                    interface_namespace[name] = obj
                
                # All other objects pass via inheritance.
                else:
                    public_namespace[name] = obj
                
                # If we have an actual fixture defined, then it supercedes
                # public_namespace
                if fixture is not None:
                    fixture_namespace[name] = fixture
                    
                # Same goes with the interface.
                if interface is not None:
                    interface_namespace[name] = interface
            
            # Create the class
            cls = super().__new__(
                mcls,
                public_name,
                bases,
                public_namespace,
                *args,
                **kwargs
            )
            
            # Figure out the fixture inheritance tree. Fixtures always take
            # precedent over parents, but also always inherit them.
            fixture_bases = (cls, *(
                base.__fixture__ if hasattr(base, '__fixture__') else base
                for base in bases
            ))
            interface_bases = (cls, *(
                base.__interface__ if hasattr(base, '__interface__') else base
                for base in bases
            ))
            
            # NOTE:
            # See Python docs on super() with zero args: "the compiler fills in
            # the necessary details to correctly retrieve the class being
            # defined". Therefore, this is necessary when using fixture_bases:
            # super(Class, self).__init__(*args, **kwargs)
            
            # Now add in the types for both the fixture and the interface.
            # Reuse same bases for both, but use the REAL metaclass for it.
            # TODO: make the fixture inherit from the cls above, so that it can
            # have access to the original methods using super()
            cls.__fixture__ = mcls(
                fixture_name,
                fixture_bases,
                fixture_namespace,
                *args,
                _short_circuit = True,
                **kwargs
            )
            cls.__interface__ = mcls(
                interface_name,
                interface_bases,
                interface_namespace,
                *args,
                _short_circuit = True,
                **kwargs
            )
        
        # And don't forget to return the final cls object.
        return cls
        
    def __init__(cls, *args, _short_circuit=False, **kwargs):
        # Just strip out _short_circuit
        super().__init__(*args, **kwargs) def public_api(func):
    ''' Decorator to automatically mark the object as the normal thing.
    '''
    
    def fixture_closure(fixture_func, public_func=func):
        ''' Defines the decorator for @method.fixture.
        '''
        # This is the actual __fixture__ method, to be defined via decorator
        public_func.__fixture__ = fixture_func
        return public_func
    
    def interface_closure(interface_func, public_func=func):
        ''' Defines the decorator for @method.fixture.
        '''
        # This is the actual __interface method, to be defined via decorator
        public_func.__interface__ = interface_func
        return public_func
        
    func.fixture = fixture_closure
    func.interface = interface_closure
    
    # This denotes that it is an API
    func.__is_api__ = True
    
    return func def fixture_api(func):
    ''' Decorator to mark the method as a fixture-only object.
    '''
    ''' Decorator to automatically mark the object as the normal thing.
    '''
    
    # Huh, well, this is easy.
    func.__is_fixture__ = True
    return func class SetMap:
    ''' Combines a mapping with a set. Threadsafe.
    '''
    
    def __init__(self):
        ''' Create a lookup!
        
        Currently does not support pre-population during __init__().
        '''
        self._mapping = {}
        self._lock = threading.RLock()
    
    def __getitem__(self, key):
        ''' Pass-through to the core lookup. Will return a frozenset.
        Raises keyerror if missing.
        '''
        with self._lock:
            return frozenset(self._mapping[key])
        
    def get_any(self, key):
        ''' Pass-through to the core lookup. Will return a frozenset.
        Will never raise a keyerror; if key not in self, returns empty
        frozenset.
        '''
        with self._lock:
            try:
                return frozenset(self._mapping[key])
            except KeyError:
                return frozenset()
                
    def pop_any(self, key):
        ''' Unlike other methods, pop_any returns the actual set,
        instead of a frozenset copy.
        '''
        with self._lock:
            try:
                return self._mapping.pop(key)
            except KeyError:
                return set()
        
    def __contains__(self, key):
        ''' Check to see if the key exists.
        '''
        with self._lock:
            return key in self._mapping
        
    def contains_within(self, key, value):
        ''' Check to see if the key exists, AND the value exists at key.
        '''
        with self._lock:
            try:
                return value in self._mapping[key]
            except KeyError:
                return False
        
    def add(self, key, value):
        ''' Adds the value to the set at key. Creates a new set there if
        none already exists.
        '''
        with self._lock:
            try:
                self._mapping[key].add(value)
            except KeyError:
                self._mapping[key] = {value}
                
    def update(self, key, value):
        ''' Updates the key with the value. Value must support being
        passed to set.update(), and the set constructor.
        '''
        with self._lock:
            try:
                self._mapping[key].update(value)
            except KeyError:
                self._mapping[key] = set(value)
                
    def update_all(self, other):
        ''' Updates all keys in other to self.
        '''
        with self._lock:
            for key in other:
                try:
                    self._mapping[key].update(other[key])
                except KeyError:
                    self._mapping[key] = set(other[key])
            
    def _remove_if_empty(self, key):
        ''' Removes a key entirely if it no longer has any values. Will
        suppress KeyError if the key is not found.
        '''
        try:
            if len(self._mapping[key]) == 0:
                del self._mapping[key]
        except KeyError:
            pass
        
    def remove(self, key, value):
        ''' Removes the value from the set at key. Will raise KeyError
        if either the key is missing, or the value is not contained at
        the key.
        '''
        with self._lock:
            try:
                self._mapping[key].remove(value)
            finally:
                self._remove_if_empty(key)
        
    def discard(self, key, value):
        ''' Same as remove, but will never raise KeyError.
        '''
        with self._lock:
            try:
                self._mapping[key].discard(value)
            except KeyError:
                pass
            finally:
                self._remove_if_empty(key)
        
    def clear(self, key):
        ''' Clears the specified key. Raises KeyError if key is not
        found.
        '''
        with self._lock:
            del self._mapping[key]
            
    def clear_any(self, key):
        ''' Clears the specified key, if it exists. If not, suppresses
        KeyError.
        '''
        with self._lock:
            try:
                del self._mapping[key]
            except KeyError:
                pass
        
    def clear_all(self):
        ''' Clears the entire mapping.
        '''
        with self._lock:
            self._mapping.clear()
            
    def __len__(self):
        ''' Returns the length of the mapping only.
        '''
        return len(self._mapping)
            
    def __iter__(self):
        ''' Send this through to the dict, bypassing the usual route,
        because otherwise we'll have a deadlock.
        '''
        with self._lock:
            for key in self._mapping:
                yield key
            
    def __eq__(self, other):
        ''' Expand comparison to search insides.
        '''
        with self._lock, other._lock:
            # First make sure both have same mapping keys.
            if set(self._mapping) != set(other._mapping):
                return False
            
            # Okay, now check each key has identical sets
            else:
                for this_key, this_set in self._mapping.items():
                    if other._mapping[this_key] != this_set:
                        return False
                        
        # If we successfully got through all of that, they are identical.
        return True
        
    def combine(self, other):
        ''' Returns a new SetMap with the union of both.
        '''
        new = type(self)()
        # Note that the iterator will take the lock, so we don't need to.
        for key in self:
            new.update(key, self._mapping[key])
        for key in other:
            new.update(key, other._mapping[key])
        return new

    def __getstate__(self):
        ''' Ignore self._lock to support pickling. Basically boilerplate
        copy from the pickle reference docs.
        '''
        # Copy the object's state from self.__dict__ which contains
        # all our instance attributes. Always use the dict.copy()
        # method to avoid modifying the original state.
        state = self.__dict__.copy()
        # Remove the unpicklable entries.
        del state['_lock']
        return state

    def __setstate__(self, state):
        ''' Ignore self._lock to support pickling. Basically boilerplate
        copy from the pickle reference docs.
        '''
        # Restore instance attributes (i.e., filename and lineno).
        self.__dict__.update(state)
        # Restore the lock.
        self._lock = threading.RLock()
        
    def __bool__(self):
        # Pass bool straight to mapping.
        return bool(self._mapping) class WeakSetMap(SetMap):
    ''' SetMap that uses WeakerSets internally.
    '''
    
    def add(self, key, value):
        ''' Adds the value to the set at key. Creates a new set there if
        none already exists.
        '''
        with self._lock:
            try:
                self._mapping[key].add(value)
            except KeyError:
                self._mapping[key] = _KeyedWeakSet(
                    data = (value,),
                    parent = self,
                    key = key,
                )
                
    def update(self, key, value):
        ''' Updates the key with the value. Value must support being
        passed to set.update(), and the set constructor.
        '''
        with self._lock:
            try:
                self._mapping[key].update(value)
            except KeyError:
                self._mapping[key] = _KeyedWeakSet(
                    data = (item for item in value),
                    parent = self,
                    key = key,
                )
                
    def update_all(self, other):
        ''' Updates all keys in other to self.
        '''
        with self._lock:
            for key in other:
                try:
                    self._mapping[key].update(other[key])
                except KeyError:
                    self._mapping[key] = _KeyedWeakSet(
                        data = other[key],
                        parent = self,
                        key = key
                    )
                
    def __getitem__(self, key):
        ''' Resolves all of our references.
        '''
        result = super().__getitem__(key)
        return frozenset(ref() for ref in result)
        
    def get_any(self, key):
        ''' Resolve references.
        '''
        result = super().get_any(key)
        return frozenset(ref() for ref in result) class ApiID(Ghid):
    ''' Subclass Ghid in a way that makes the API ID seem like a normal
    64-byte string.
    
    Remind me again why the hell I'm subclassing ghid for this when I'm
    ending up removing just about everything that makes it a ghid?
    '''
    
    def __init__(self, address, algo=None):
        ''' Wrap the normal Ghid creation with a forced algo.
        '''
        if len(address) != 64:
            raise ValueError('Improper API ID length.')
        
        elif algo is None:
            algo = 0
            
        elif algo != 0:
            raise ValueError('Improper API ID format.')
        
        super().__init__(algo, address)
        
    def __repr__(self):
        ''' Hide that this is a ghid.
        '''
        return type(self).__name__ + '(' + repr(self.address) + ')'
        
    @property
    def algo(self):
        return self._algo
        
    @algo.setter
    def algo(self, value):
        # Currently, ensure algo is b'\x00'
        if value == 0:
            self._algo = value
        else:
            raise ValueError('Invalid address algorithm.')
            
    @property
    def address(self):
        return self._address
            
    @address.setter
    def address(self, address):
        self._address = address
        
    @classmethod
    def pseudorandom(cls):
        return super().pseudorandom(0) class AppToken(bytes):
    ''' Extend bytes to be a fixed-length thing.
    '''
    # The size, in bytes, of an app token
    TOKEN_LEN = 16
    
    def __init__(self, *args, **kwargs):
        ''' For simplicity, enforce length AFTER performing init.
        '''
        if len(self) != self.TOKEN_LEN:
            raise ValueError('Improper token length.')
    
    @classmethod
    def null(cls):
        ''' Return the null token (used for passing "no token" in IPC,
        and not a valid token for app usage.)
        '''
        return cls(cls.TOKEN_LEN)
        
    @classmethod
    def pseudorandom(cls):
        ''' Return a PSEUDOrandom (**NOT** secure) app token, for use in
        testing.
        '''
        return cls([random.randint(0, 255) for __ in range(cls.TOKEN_LEN)])
        
    def __repr__(self):
        ''' Add in the class name to repr.
        '''
        return type(self).__name__ + '(' + super().__repr__() + ')'
        
    def __str__(self, *args, **kwargs):
        ''' Add in the class name to repr.
        '''
        return type(self).__name__ + '(' + \
            super().__str__(*args, **kwargs) + ')' def _reap_wrapped_task(task):
    ''' Reap a task that was wrapped to never raise and then
    executed autonomously using ensure_future.
    '''
    task.result() class HGXLinkError(HypergolixException, RuntimeError):
    ''' This exception (or a subclass thereof) is raised for errors
    originating in the HGXLink itself.
    '''
    pass class ConnectionManager(loopa.TaskLooper, metaclass=loopa.utils.Triplicate):
    ''' Use this client-side to automatically establish (and, whenever
    necessary, re-establish) a connection with a server. This adds
    exponential backoff for every consecutive failed connection attempt.
    
    ConnectionManagers will handle listening to the connection, and will
    dispatch any incoming requests to protocol_def. They do not buffer
    the message processing; ie, they will only handle one incoming
    message at a time. They also handle closing connections.
    
    Finally, ConnectionManagers may be used to invoke any requests that
    are defined at the msg_handler, and will automatically pass them the
    current connection.
    '''
    # Minimum delay before retrying a connection, in seconds
    MIN_RETRY_DELAY = .01
    # Maximum delay before retrying a connection, in seconds (1 hour)
    MAX_RETRY_DELAY = 3600
    
    def __init__(self, connection_cls, msg_handler, conn_init=None,
                 conn_close=None, *args, autoretry=True, **kwargs):
        ''' We need to assign our connection class.
        
        conn_init, if defined, will be awaited (as a background task)
        every time the connection itself is created.
        
        conn_close, if defined, will be awaited (but not as a background
        task) every time the connection itself has terminated -- AFTER
        the closure.
        '''
        super().__init__(*args, **kwargs)
        
        self.connection_cls = connection_cls
        self.protocol_def = msg_handler
        self.conn_init = conn_init
        self.conn_close = conn_close
        self._connection = None
        self._conn_available = None
        self._conn_args = None
        self._conn_kwargs = None
        self._consecutive_attempts = 0
        # This determines whether or not we even try to reestablish the
        # connection.
        self.autoretry = autoretry
        
        # Very quick and easy way of injecting all of the handler methods into
        # self. Short of having one queue per method, we need to wrap it
        # anyways to buffer the actual method call.
        for name in msg_handler._RESPONDERS:
            async def wrap_request(*args, _method=name, **kwargs):
                ''' Pass all requests to our perform_request method.
                '''
                return (await self.perform_request(_method, args, kwargs))
            
            setattr(self, name, wrap_request)
            
    @property
    def _conn_desc(self):
        ''' Make (and cache) a description of the connection.
        '''
        conn_desc = getattr(self, '__conn_desc', None)
        
        if conn_desc is None:
            conn_desc = self.connection_cls.desc_str(*self._conn_args,
                                                     **self._conn_kwargs)
            setattr(self, '__conn_desc', conn_desc)
            
        return conn_desc
    
    async def loop_init(self, *args, **kwargs):
        ''' *args and **kwargs will be passed to the connection class.
        '''
        self._send_q = asyncio.Queue()
        self._conn_available = asyncio.Event()
        self._conn_args = args
        self._conn_kwargs = kwargs
        self._consecutive_attempts = 0
        self._connection = None
        
    async def loop_stop(self):
        ''' Reset whether or not we have a connection available.
        '''
        self._send_q = None
        self._conn_available = None
        self._conn_args = None
        self._conn_kwargs = None
        self._connection = None
        
    async def loop_run(self):
        ''' Creates a connection and listens forever.
        '''
        # Attempt to connect.
        try:
            logger.debug('Establishing connection to ' + self._conn_desc)
            # Technically this violates the idea that connections should be
            # wholly self-sustained, but we want to be able to explicitly close
            # them and pass some strong references to them later. Maybe. I
            # think, anyways. Okay, not totally sure.
            connection = await self.connection_cls.new(
                *self._conn_args,
                **self._conn_kwargs
            )
            
        # Need to catch this specifically, lest we accidentally do this forever
        except asyncio.CancelledError:
            logger.debug('Connection manager cancelled to ' + self._conn_desc)
            raise
            
        # The connection failed. Wait before reattempting it.
        except Exception as exc:
            logger.error('Failed to establish connection at ' +
                         self._conn_desc)
            logger.info('Failed connection traceback:\n' +
                        ''.join(traceback.format_exc()))
            # Do this first, because otherwise randrange errors (and also
            # otherwise it isn't technically binary exponential backoff)
            self._consecutive_attempts += 1
            backoff = random.randrange((2 ** self._consecutive_attempts) - 1)
            backoff = max(self.MIN_RETRY_DELAY, backoff)
            backoff = min(self.MAX_RETRY_DELAY, backoff)
            
            if self.autoretry:
                await asyncio.sleep(backoff)
            else:
                raise exc
            
        # We successfully connected. Awesome.
        else:
            logger.info('Connected to ' + self._conn_desc)
            # Reset the attempts counter and set that a connection is available
            self._consecutive_attempts = 0
            # See note above re: strong/weak references
            self._connection = connection
            self._conn_available.set()
            
            try:
                # If we need to do something to establish the connection, do
                # that here. We don't need to worry about race conditions with
                # task starting, because we don't cede execution flow control
                # to the loop until we hit the next await.
                if self.conn_init is not None:
                    loopa.utils.make_background_future(
                        self.conn_init(self, connection)
                    )
                
                logger.debug('Listening for messages at ' + self._conn_desc)
                
                # We don't expect clients to have a high enough message volume
                # to justify a buffer, so directly invoke the message handler
                await connection.listen_forever(receiver=self.protocol_def)
                
            # We want to log, but swallow-and-attempt-reconnect, errors in
            # connections.
            except ConnectionError:
                logger.warning('Connection errored: ' + self._conn_desc +
                               ' w/ traceback:\n' +
                               ''.join(traceback.format_exc()))
            
            # No matter what happens, when this dies we need to clean up the
            # connection and tell downstream that we cannot send anymore.
            finally:
                self._conn_available.clear()
                await connection.close()
                logger.info('Connection closed: ' + self._conn_desc)
                
                if self.conn_close is not None:
                    await self.conn_close(self, connection)
                    
    def __str__(self):
        ''' Make a better, compact representation of self.
        '''
        return type(self).__name__ + '(' + self._conn_desc + ')'
            
    async def perform_request(self, request_name, args, kwargs):
        ''' Make the given request using the protocol_def, but wait
        until a connection exists.
        '''
        # Wait for the connection to be available.
        method = getattr(self.protocol_def, request_name)
        await self.await_connection()
        return (await method(self._connection, *args, **kwargs))
    
    @property
    def has_connection(self):
        # Hmmm
        return self._conn_available.is_set()
    
    @loopa.utils.triplicated
    async def await_connection(self):
        ''' Wait for a connection to be available.
        '''
        # There is a potential race condition here with loop_init. The quick
        # and dirty solution to it is to yield control to the event loop
        # repeatedly until it appears. Also, go ahead and add in a bit of a
        # delay for good measure.
        while self._conn_available is None:
            await asyncio.sleep(.01)
            
        await self._conn_available.wait() class WSConnection(_ConnectionBase):
    ''' Bookkeeping object for a single websocket connection (client or
    server).
    
    This should definitely use slots, to save on server memory usage.
    '''
    
    def __init__(self, websocket, path=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        self.websocket = websocket
        self.path = path
    
    @classmethod
    def desc_str(cls, host, port, tls):
        ''' Override this to explain where the connection is supposed to
        go, for use in debug scenarios.
        '''
        loc = _WSLoc(host, int(port), bool(tls))
        return cls.__name__ + '(' + str(loc) + ')'
        
    @classmethod
    async def serve_forever(cls, msg_handler, host, port, tls=False):
        ''' Starts a server for this kind of connection. Should handle
        its own return, and be cancellable via task cancellation.
        '''
        async def wrapped_msg_handler(websocket, path):
            ''' We need an intermediary that will feed the conn_handler
            actual _WSConnection objects.
            '''
            self = weakref.proxy(cls(websocket, path))
            # Make sure we don't take a strong reference to the connection!
            await self.listen_forever(msg_handler)
        
        try:
            server = await websockets.server.serve(
                wrapped_msg_handler,
                host,
                port,
                max_size = 10 * (2 ** 20)    # Max incoming msg size 10 MiB
            )
            
            try:
                await server.wait_closed()
            
            except Exception:
                server.close()
                await server.wait_closed()
                raise
                
        except asyncio.CancelledError:
            # Don't log being cancelled; it's expected close behavior.
            raise
            
        except Exception as exc:
            logger.error(
                'INTERNAL SERVER ERROR. Closing server w/ traceback:\n' +
                ''.join(traceback.format_exc())
            )
            logger.debug('Error args:' + str(exc.args))
        
    @classmethod
    async def new(cls, host, port, tls):
        ''' Creates and returns a new connection. Intended to be called
        by clients; servers may call __init__ directly.
        '''
        loc = _WSLoc(host, int(port), bool(tls))
        
        # If this raises, we don't need to worry about closing, because the
        # websocket won't exist.
        if tls:
            websocket = await websockets.client.connect(
                str(loc),
                ssl = SSL_VERIFICATION_CONTEXT,
                max_size = 10 * (2 ** 20)    # Max incoming msg size 10 MiB
            )
        
        else:
            websocket = await websockets.client.connect(
                str(loc),
                max_size = 10 * (2 ** 20)    # Max incoming msg size 10 MiB
            )
        
        return cls(websocket=websocket)
        
    async def close(self):
        ''' Wraps websocket.close and calls self.terminate().
        '''
        try:
            # The close() call is idempotent, but it will raise (for example) a
            # ConnectionResetError if something went screwy. So check for
            # availability first.
            if self.websocket.open:
                await self.websocket.close()
            
        finally:
            # And force us to be GC'd
            self.terminate()
        
    async def send(self, msg):
        ''' Send from the same event loop as the websocket.
        '''
        try:
            return (await self.websocket.send(msg))
        
        # If the connection is closed, call our own close (and therefore
        # self-destruct)
        except websockets.exceptions.ConnectionClosed as exc:
            try:
                # await self.close()
                raise ConnectionClosed(
                    'code: ' + str(exc.code) + '; reason: ' + exc.reason
                ) from exc
                
            finally:
                self.terminate()
        
    async def recv(self):
        ''' Receive from the same event loop as the websocket.
        '''
        try:
            return (await self.websocket.recv())
        
        # If the connection is closed, call our own close (and therefore
        # self-destruct)
        except websockets.exceptions.ConnectionClosed as exc:
            try:
                # await self.close()
                raise ConnectionClosed(
                    'code: ' + str(exc.code) + '; reason: ' + exc.reason
                ) from exc
                
            finally:
                self.terminate() class IPCClientProtocol(_IPCSerializer, metaclass=RequestResponseAPI,
                        error_codes=ERROR_CODES, default_version=b'\x00\x00'):
    ''' Defines the protocol for IPC, with handlers specific to clients.
    '''
    _hgxlink = weak_property('__hgxlink')
        
    @fixture_api
    def __init__(self, whoami, *args, **kwargs):
        ''' Create the fixture internals.
        '''
        # This is necessary because of the fixture_bases bit.
        super(IPCClientProtocol.__fixture__, self).__init__(*args, **kwargs)
        self.whoami = whoami
        self.apis = set()
        self.token = None
        self.startup = None
        self.pending_obj = None
        self.pending_ghid = None
        self.discarded = set()
        self.updates = []
        self.syncs = []
        self.shares = SetMap()
        self.frozen = set()
        self.held = set()
        self.deleted = set()
        
    @fixture_api
    def RESET(self):
        ''' Nothing beyond just re-running __init__, reusing whoami.
        '''
        self.apis = set()
        self.token = None
        self.startup = None
        self.pending_obj = None
        self.pending_ghid = None
        self.discarded = set()
        self.updates = []
        self.syncs = []
        self.shares = SetMap()
        self.frozen = set()
        self.held = set()
        self.deleted = set()
        
    @fixture_api
    def prep_obj(self, obj):
        ''' Define the next object to be returned in any obj-based
        operations.
        '''
        self.pending_obj = (
            obj._hgx_ghid,
            obj._hgx_binder,
            obj._hgx_state,
            obj._hgx_linked,
            obj._hgx_api_id,
            obj._hgx_private,
            obj._hgx_dynamic,
            obj._hgx_legroom
        )
        
    def assemble(self, hgxlink):
        # Chicken, egg, etc.
        self._hgxlink = hgxlink
        
    @public_api
    @request(b'+T')
    async def set_token(self, connection, token):
        ''' Register an existing token or get a new token, or notify an
        app of its existing token.
        '''
        if token is None:
            return AppToken.null()
        else:
            return token
        
    @set_token.request_handler
    async def set_token(self, connection, body):
        ''' Handles token-setting requests.
        '''
        self._hgxlink.token = AppToken(body)
        
    @set_token.fixture
    async def set_token(self, token):
        ''' Fixture for setting a token (or getting a new one).
        '''
        if token is None:
            # ONLY use pseudorandom for the fixture
            token = AppToken.pseudorandom()
            
        self.token = token
        return token
        
    @set_token.response_handler
    async def set_token(self, connection, response, exc):
        ''' Converts the response into an AppToken.
        '''
        if exc is not None:
            raise exc
        else:
            return AppToken(response)
    
    @public_api
    @request(b'+A')
    async def register_api(self, connection, api_id):
        ''' Registers the application as supporting an API. Client only.
        '''
        if not isinstance(api_id, ApiID):
            raise TypeError('api_id must be ApiID.')
            
        return bytes(api_id)
        
    @register_api.request_handler
    async def register_api(self, connection, body):
        ''' Handles API registration requests. Server only.
        '''
        raise NotImplementedError()
        
    @register_api.response_handler
    async def register_api(self, connection, response, exc):
        ''' Handles responses to API registration requests. Client only.
        '''
        if exc is not None:
            raise exc
        elif response == b'\x01':
            return True
        else:
            raise IPCError('Unknown error while registering API.')
            
    @register_api.fixture
    async def register_api(self, api_id):
        ''' Fixture for registering an api.
        '''
        self.apis.add(api_id)
        
    @public_api
    @request(b'-A')
    async def deregister_api(self, connection, api_id):
        ''' Removes any existing registration for the app supporting an
        API. Client only.
        '''
        if not isinstance(api_id, ApiID):
            raise TypeError('api_id must be ApiID.')
            
        return bytes(api_id)
        
    @deregister_api.request_handler
    async def deregister_api(self, connection, body):
        ''' Handles API deregistration requests. Server only.
        '''
        raise NotImplementedError()
        
    @deregister_api.response_handler
    async def deregister_api(self, connection, response, exc):
        ''' Handles responses to API deregistration requests. Client
        only.
        '''
        if exc is not None:
            raise exc
        elif response == b'\x01':
            return True
        else:
            raise IPCError('Unknown error while deregistering API.')
            
    @deregister_api.fixture
    async def deregister_api(self, api_id):
        ''' Fixture for api removal.
        '''
        self.apis.discard(api_id)
        
    @public_api
    @request(b'?I')
    async def get_whoami(self, connection):
        ''' Get the current hypergolix fingerprint, or notify an app of
        the current hypergolix fingerprint.
        '''
        return b''
        
    @get_whoami.request_handler
    async def get_whoami(self, connection, body):
        ''' Handles whoami requests.
        '''
        ghid = Ghid.from_bytes(body)
        self._hgxlink.whoami = ghid
        return b''
        
    @get_whoami.response_handler
    async def get_whoami(self, connection, response, exc):
        ''' Handles responses to whoami requests.
        '''
        if exc is not None:
            raise exc
        else:
            ghid = Ghid.from_bytes(response)
            return ghid
            
    @get_whoami.fixture
    async def get_whoami(self, connection):
        ''' Fixture for get_whoami.
        '''
        return self.whoami
        
    @public_api
    @request(b'>$')
    async def get_startup_obj(self, connection):
        ''' Request a startup object, or notify an app of its declared
        startup object.
        '''
        return b''
        
    @get_startup_obj.request_handler
    async def get_startup_obj(self, connection, body):
        ''' Handles requests for startup objects.
        '''
        ghid = Ghid.from_bytes(body)
        self._hgxlink._startup_obj = ghid
        return b'\x01'
        
    @get_startup_obj.response_handler
    async def get_startup_obj(self, connection, response, exc):
        ''' Handle the response to retrieving startup obj ghids.
        '''
        if exc is not None:
            raise exc
        elif response == b'':
            return None
        else:
            ghid = Ghid.from_bytes(response)
            return ghid
            
    @get_startup_obj.fixture
    async def get_startup_obj(self):
        ''' Yep, go ahead and fixture that, too.
        '''
        return self.startup
        
    @public_api
    @request(b'+$')
    async def register_startup_obj(self, connection, ghid):
        ''' Register a startup object. Client only.
        '''
        return bytes(ghid)
        
    @register_startup_obj.request_handler
    async def register_startup_obj(self, connection, body):
        ''' Handles startup object registration. Server only.
        '''
        raise NotImplementedError()
        
    @register_startup_obj.fixture
    async def register_startup_obj(self, ghid):
        ''' Fixture startup obj registration and stuff.
        '''
        self.startup = ghid
        
    @public_api
    @request(b'-$')
    async def deregister_startup_obj(self, connection):
        ''' Register a startup object. Client only.
        '''
        return b''
        
    @deregister_startup_obj.request_handler
    async def deregister_startup_obj(self, connection, body):
        ''' Handles startup object registration. Server only.
        '''
        raise NotImplementedError()
        
    @deregister_startup_obj.fixture
    async def deregister_startup_obj(self):
        ''' Still more fixtures.
        '''
        self.startup = None
        
    @public_api
    @request(b'>O')
    async def get_ghid(self, connection, ghid):
        ''' Get an object with the specified address. Client only.
        '''
        return bytes(ghid)
        
    @get_ghid.request_handler
    async def get_ghid(self, connection, body):
        ''' Handles requests for an object. Server only.
        '''
        raise NotImplementedError()
        
    @get_ghid.response_handler
    async def get_ghid(self, connection, response, exc):
        ''' Handles responses to get object requests. Client only.
        '''
        if exc is not None:
            raise exc
            
        return self._unpack_object_def(response)
        
    @get_ghid.fixture
    async def get_ghid(self, ghid):
        ''' Interact with pending_obj.
        '''
        return self.pending_obj
        
    @public_api
    @request(b'+O')
    async def new_ghid(self, connection, state, api_id, dynamic, private,
                       _legroom):
        ''' Create a new object, or notify an app of a new object
        created by a concurrent instance of the app on a different
        hypergolix session.
        '''
        # If state is Ghid, it's a link.
        if isinstance(state, Ghid):
            is_link = True
        else:
            is_link = False
        
        return self._pack_object_def(
            None,               # address
            None,               # author
            state,              # state
            is_link,            # is_link
            bytes(api_id),      # api_id
            private,            # private
            dynamic,            # dynamic
            _legroom            # legroom
        )
        
    @new_ghid.request_handler
    async def new_ghid(self, connection, body):
        ''' Handles requests for new objects.
        '''
        raise NotImplementedError()
        
    @new_ghid.response_handler
    async def new_ghid(self, connection, response, exc):
        ''' Handles responses to requests for new objects.
        '''
        if exc is not None:
            raise exc
        
        else:
            return Ghid.from_bytes(response)
            
    @new_ghid.fixture
    async def new_ghid(self, state, api_id, dynamic, private, _legroom):
        ''' We just need an address.
        '''
        self.pending_obj = (
            self.pending_ghid,
            self.whoami,
            state,
            False,  # is_link
            api_id,
            private,
            _legroom
        )
        
        return self.pending_ghid
        
    @public_api
    @request(b'!O')
    async def update_ghid(self, connection, ghid, state, private, _legroom):
        ''' Update an object or notify an app of an incoming update.
        '''
        # If state is Ghid, it's a link.
        if isinstance(state, Ghid):
            is_link = True
        else:
            is_link = False
            
        return self._pack_object_def(
            ghid,       # ghid
            None,       # Author
            state,      # state
            is_link,    # is_link
            None,       # api_id
            private,    # private
            None,       # dynamic
            _legroom    # legroom
        )
        
    @update_ghid.request_handler
    async def update_ghid(self, connection, body):
        ''' Handles update object requests.
        '''
        (address,
         author,    # Will be unused and set to None
         state,
         is_link,
         api_id,    # Will be unused and set to None
         private,   # Will be unused and set to None
         dynamic,   # Will be unused and set to None
         _legroom   # Will be unused and set to None
         ) = self._unpack_object_def(body)
        
        if is_link:
            state = Ghid.from_bytes(state)
            
        await self._hgxlink._pull_state(address, state)
            
        return b'\x01'
        
    @update_ghid.response_handler
    async def update_ghid(self, connection, response, exc):
        ''' Handles responses to update object requests.
        '''
        if exc is not None:
            raise exc
            
        elif response != b'\x01':
            raise HGXLinkError('Unknown error while updating object.')
            
        else:
            return True
            
    @update_ghid.fixture
    async def update_ghid(self, ghid, state, private, _legroom):
        ''' Yarp, fixture that.
        '''
        self.updates.append(
            {ghid: (state, private, _legroom)}
        )
        
    @public_api
    @request(b'~O')
    async def sync_ghid(self, connection, ghid):
        ''' Manually force Hypergolix to check an object for updates.
        Client only.
        '''
        return bytes(ghid)
        
    @sync_ghid.request_handler
    async def sync_ghid(self, connection, body):
        ''' Handles manual syncing requests. Server only.
        '''
        raise NotImplementedError()
        
    @sync_ghid.response_handler
    async def sync_ghid(self, connection, response, exc):
        ''' Handles responses to manual syncing requests. Client only.
        '''
        if exc is not None:
            raise exc
        elif response != b'\x01':
            raise IPCError('Unknown error while updating object.')
        else:
            return True
            
    @sync_ghid.fixture
    async def sync_ghid(self, ghid):
        # Moar fixturing.
        self.syncs.append(ghid)
        
    @public_api
    @request(b'@O')
    async def share_ghid(self, connection, ghid, recipient):
        ''' Request an object share or notify an app of an incoming
        share.
        '''
        return bytes(ghid) + bytes(recipient)
        
    @share_ghid.request_handler
    async def share_ghid(self, connection, body):
        ''' Handles object share requests.
        '''
        ghid = Ghid.from_bytes(body[0:65])
        origin = Ghid.from_bytes(body[65:130])
        api_id = ApiID.from_bytes(body[130:195])
        
        await self._hgxlink.handle_share(ghid, origin, api_id)
        return b'\x01'
        
    @share_ghid.response_handler
    async def share_ghid(self, connection, response, exc):
        ''' Handles responses to object share requests.
        '''
        if exc is not None:
            raise exc
        elif response == b'\x01':
            return True
        else:
            raise IPCError('Unknown error while sharing object.')
            
    @share_ghid.fixture
    async def share_ghid(self, ghid, recipient):
        # Blahblah
        self.shares.add(ghid, recipient)
        
    @request(b'^S')
    async def share_success(self, connection):
        ''' Notify app of successful share. Server only.
        '''
        raise NotImplementedError()
        
    @share_success.request_handler
    async def share_success(self, connection, body):
        ''' Handles app notifications for successful shares. Client
        only.
        '''
        # Don't raise NotImplementedError, just because this will be called for
        # every share, and we're perfectly well aware that it isn't implemented
        # TODO: implement this.
        return b''
        
    @request(b'^F')
    async def share_failure(self, connection):
        ''' Notify app of unsuccessful share. Server only.
        '''
        raise NotImplementedError()
        
    @share_failure.request_handler
    async def share_failure(self, connection, body):
        ''' Handles app notifications for unsuccessful shares. Client
        only.
        '''
        # Don't raise NotImplementedError, just because this will be called for
        # every share, and we're perfectly well aware that it isn't implemented
        # TODO: implement this.
        return b''
        
    @public_api
    @request(b'*O')
    async def freeze_ghid(self, connection, ghid):
        ''' Creates a new static copy of the object, or notifies an app
        of a frozen copy of an existing object created by a concurrent
        instance of the app.
        '''
        return bytes(ghid)
        
    @freeze_ghid.request_handler
    async def freeze_ghid(self, connection, body):
        ''' Handles object freezing requests.
        '''
        # Not currently supported.
        raise NotImplementedError()
        
    @freeze_ghid.response_handler
    async def freeze_ghid(self, connection, response, exc):
        ''' Handles responses to object freezing requests.
        '''
        if exc is not None:
            raise exc
        
        else:
            return Ghid.from_bytes(response)
            
    @freeze_ghid.fixture
    async def freeze_ghid(self, ghid):
        # Moar fixture.
        self.frozen.add(ghid)
        return ghid
        
    @public_api
    @request(b'#O')
    async def hold_ghid(self, connection, ghid):
        ''' Creates a new static binding for the object, or notifies an
        app of a static binding created by a concurrent instance of the
        app.
        '''
        return bytes(ghid)
        
    @hold_ghid.request_handler
    async def hold_ghid(self, connection, body):
        ''' Handles object holding requests.
        '''
        # Not currently supported.
        raise NotImplementedError()
        
    @hold_ghid.response_handler
    async def hold_ghid(self, connection, response, exc):
        ''' Handles responses to object holding requests.
        '''
        if exc is not None:
            raise exc
        elif response == b'\x01':
            return True
        else:
            raise IPCError('Unknown error while holding object.')
            
    @hold_ghid.fixture
    async def hold_ghid(self, ghid):
        # Yep yep yep
        self.held.add(ghid)
        
    @public_api
    @request(b'-O')
    async def discard_ghid(self, connection, ghid):
        ''' Stop listening to object updates. Client only.
        '''
        return bytes(ghid)
        
    @discard_ghid.request_handler
    async def discard_ghid(self, connection, body):
        ''' Handles object discarding requests. Server only.
        '''
        raise NotImplementedError()
        
    @discard_ghid.response_handler
    async def discard_ghid(self, connection, response, exc):
        ''' Handles responses to object discarding requests. Client
        only.
        '''
        if exc is not None:
            raise exc
        elif response == b'\x01':
            return True
        else:
            raise IPCError('Unknown error while discarding object.')
            
    @discard_ghid.fixture
    async def discard_ghid(self, ghid):
        # Nothing special.
        self.discarded.add(ghid)
        
    @public_api
    @request(b'XO')
    async def delete_ghid(self, connection, ghid):
        ''' Request an object deletion or notify an app of an incoming
        deletion.
        '''
        return bytes(ghid)
        
    @delete_ghid.request_handler
    async def delete_ghid(self, connection, body):
        ''' Handles object deletion requests.
        '''
        ghid = Ghid.from_bytes(body)
        await self._hgxlink.handle_delete(ghid)
        return b'\x01'
        
    @delete_ghid.response_handler
    async def delete_ghid(self, connection, response, exc):
        ''' Handles responses to object deletion requests.
        '''
        if exc is not None:
            raise exc
        elif response == b'\x01':
            return True
        else:
            raise IPCError('Unknown error while deleting object.')
            
    @delete_ghid.fixture
    async def delete_ghid(self, ghid):
        # mmmmhmmm
        self.deleted.add(ghid)
import logging
import collections
import weakref
import queue
import threading
import inspect
import traceback
import asyncio
import loopa
import concurrent.futures
import functools
from golix import Ghid
from loopa.utils import await_coroutine_threadsafe
from loopa.utils import await_coroutine_loopsafe
from loopa.utils import Triplicate
from loopa.utils import triplicated
from .hypothetical import API
from .hypothetical import public_api
from .hypothetical import fixture_api
from .utils import SetMap
from .utils import WeakSetMap
from .utils import ApiID
from .utils import AppToken
from .utils import _reap_wrapped_task
from .exceptions import HGXLinkError
from .comms import ConnectionManager
from .comms import WSConnection
from .ipc import IPCClientProtocol
        self.state_lookup = {}
        self.share_lookup = {}
        self.api_lookup = {}
        self.deleted = set()
        self.obj_lookup = {}
            
    def start(self, *args, **kwargs):
        ''' Await a connection if we're running threaded-ly.
        '''
        # Explicit is needed because of the fixture
        super().start(*args, **kwargs)
        
        if self._ipc_protocol is not None and self.threaded:
            self._ipc_manager.await_connection_threadsafe()
            self._ctx.wait()
            
    async def conn_init(self, ipc_manager, connection):
        ''' Figure out whoami, set up tokens, set self._ctx, etc.
        '''
        self.whoami = await self._ipc_protocol.get_whoami(connection)
        
        if self._token is not None:
            await self._ipc_protocol.set_token(self._token)
            
        self._ctx.set()
        
    async def conn_close(self, ipc_manager, connection):
        ''' Clear whoami. Keep token, though, in case we restore the
        connection successfully. The application isn't changing, but our
        agent might.
        
        Also clear self._ctx, not that it makes much difference.
        '''
        self._whoami = None
        self._ctx.clear()
            
    @fixture_api
    def prep_obj(self, obj):
        ''' Stages an object for a get() call.
        '''
        self.obj_lookup[obj._hgx_ghid] = obj
        
    @property
    def whoami(self):
        ''' Read-only access to self._whoami with a raising wrapper if
        it is undefined.
        '''
        if self._whoami is None:
            raise HGXLinkError(
                'Whoami has not been defined. Most likely, no IPC client is ' +
                'currently available.'
            )
        else:
            return self._whoami
            
    @whoami.setter
    def whoami(self, ghid):
        ''' Set whoami, if (and only if) it has yet to be set.
        '''
        if self._whoami is None:
            self._whoami = ghid
        else:
            raise HGXLinkError(
                'Whoami has already been defined. It must be cleared before ' +
                'being re-set.'
            )
    
    @property
    def token(self):
        ''' Read-only access to the current app token.
        '''
        if self._token is None:
            raise HGXLinkError('No token available.')
        else:
            return self._token
            
    @token.setter
    def token(self, value):
        ''' Set the app token, if (and only if) it has yet to be set.
        '''
        if self._token is None:
            if isinstance(value, AppToken):
                self._token = value
            else:
                raise TypeError('Tokens must be of type ' +
                                'hypergolix.utils.AppToken')
        else:
            raise HGXLinkError(
                'Token already set. It must be cleared before being re-set.'
            )
        
    @triplicated
    async def register_token(self, token=None):
        ''' Registers the application as using a particular token, OR
        gets a new token. Returns the ghid for the startup object (if
        one has already been defined), or None. Tokens are be available
        in self.token.
        '''
        # The result of this will be the actual token.
        self.token = await self._ipc_manager.set_token(token)
        
        if token is not None:
            self._startup_obj = await self._ipc_manager.get_startup_obj()
        
        # Let applications manually request the startup object, so that they
        # can deal with casting it appropriately.
        return self._startup_obj
        
    @triplicated
    async def register_startup(self, obj):
        ''' Registers the object as the startup object.
        '''
        await self._ipc_manager.register_startup_obj(obj._hgx_ghid)
        
    @triplicated
    async def deregister_startup(self):
        ''' Inverse of the above.
        '''
        await self._ipc_manager.deregister_startup_obj()
        

    @public_api