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) + ')' class Account(metaclass=API):
    ''' Accounts settle all sorts of stuff.
    
    TODO: move GolixCore into account. That way, everything to do with
    the private keys stays within the account, and is never directly
    accessed outside of it.
    '''
    _identity = immortal_property('__identity')
    _user_id = immortal_property('__user_id')
    
    _golcore = weak_property('__golcore')
    _ghidproxy = weak_property('__ghidproxy')
    _privateer = weak_property('__privateer')
    _oracle = weak_property('__oracle')
    _rolodex = weak_property('__rolodex')
    _dispatch = weak_property('__dispatch')
    _percore = weak_property('__percore')
    _librarian = weak_property('__librarian')
    _salmonator = weak_property('__salmonator')
    
    @public_api
    def __init__(self, user_id, root_secret, *args, hgxcore, **kwargs):
        ''' Gets everything ready for account bootstrapping.
        
        +   user_id explicitly passed with None means create a new
            Account.
        +   identity explicitly passed with None means load an existing
            account.
        +   user_id XOR identity must be passed.
        '''
        super().__init__(*args, **kwargs)
        
        if isinstance(user_id, Ghid):
            logger.info('Logging in with existing user ID: ' + str(user_id))
            self._identity = None
            self._user_id = user_id
        
        # This is used exclusively for testing.
        elif isinstance(user_id, FirstParty):
            logger.info('Creating a new account.')
            self._identity = user_id
            self._user_id = None
        
        else:
            raise TypeError('user_id must be an instance of Ghid or None.')
            
        self._golcore = hgxcore.golcore
        self._ghidproxy = hgxcore.ghidproxy
        self._privateer = hgxcore.privateer
        self._oracle = hgxcore.oracle
        self._rolodex = hgxcore.rolodex
        self._dispatch = hgxcore.dispatch
        self._percore = hgxcore.percore
        self._librarian = hgxcore.librarian
        self._salmonator = hgxcore.salmonator
        
        self._root_secret = root_secret
        
    @__init__.fixture
    def __init__(self, identity, *args, **kwargs):
        ''' Lulz just ignore errytang and skip calling super!
        '''
        self._identity = identity
        
        self.privateer_persistent = {}
        self.privateer_quarantine = {}
        
        self.rolodex_pending = {}
        self.rolodex_outstanding = SetMap()
        
        self.dispatch_tokens = set()
        self.dispatch_startup = {}
        self.dispatch_private = {}
        self.dispatch_incoming = set()
        self.dispatch_orphan_acks = SetMap()
        self.dispatch_orphan_naks = SetMap()
        
    @property
    def _fingerprint(self):
        ''' Get our fingerprint.
        '''
        return self._identity.ghid
        
    async def _inject_gao(self, gao):
        ''' Bypass the normal oracle get_object, new_object process and
        create the object directly.
        '''
        await self._salmonator.register(gao.ghid)
        await self._salmonator.attempt_pull(gao.ghid, quiet=True)
        gao._ctx = asyncio.Event()
        gao._ctx.set()
        self._oracle._lookup[gao.ghid] = gao
            
    async def bootstrap(self):
        ''' Used for account creation, to initialize the root node with
        its resource directory.
        '''
        # We need to pre-allocate the privateer stuff so we can bootstrap it
        # before pulling the root node when reloading.
        self.privateer_persistent = GAODict(
            ghid = None,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian
        )
        self.privateer_quarantine = GAODict(
            ghid = None,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian
        )
        
        # Privateer can be bootstrapped with or without pulling. Even though it
        # won't work *right* before pulling, it won't work AT ALL until it's
        # bootstrapped, so bootstrap first and pull later.
        logger.info('Bootstrapping privateer.')
        self._privateer.bootstrap(self)
        
        if self._user_id is not None:
            logger.info('Loading the root node.')
            root_node = GAO(
                ghid = self._user_id,
                dynamic = True,
                author = None,
                legroom = 7,
                state = b'you pass butter',
                golcore = self._golcore,
                ghidproxy = self._ghidproxy,
                privateer = self._privateer,
                percore = self._percore,
                librarian = self._librarian,
                master_secret = self._root_secret
            )
            # And now, remove the root secret from the parent namespace. This
            # will make the root_node GAO the only live reference to the root
            # secret from within the Account.
            del self._root_secret
            await root_node._pull()
                
            # TODO: convert all of this into a smartyparser (after rewriting
            # smartyparse, that is)
            
            # The last 64 bytes of the state is a SHA512 checksum for the whole
            # rest of the state
            password_validator = root_node.state[-64:]
            password_comparator = root_node.state[:-64]
            
            # This comparison is timing-insensitive: if attempting to brute
            # force a password, incorrect password attempts cannot extract any
            # information from the timing of the comparison.
            if sha512(password_comparator).digest() != password_validator:
                logger.critical('Incorrect password.')
                raise ValueError('Incorrect password.')
            
            # Identity
            identity_ghid = Ghid.from_bytes(
                root_node.state[0: 65])     # 65 bytes
            identity_master = Secret.from_bytes(
                root_node.state[65: 118])   # 53 bytes
            
            # Privateer
            privateer_persistent_ghid = Ghid.from_bytes(
                root_node.state[118: 183])
            privateer_persistent_master = Secret.from_bytes(
                root_node.state[183: 236])
            
            privateer_quarantine_ghid = Ghid.from_bytes(
                root_node.state[236: 301])
            privateer_quarantine_master = Secret.from_bytes(
                root_node.state[301: 354])
            
            # Rolodex stuff
            rolodex_pending_ghid = Ghid.from_bytes(
                root_node.state[354: 419])
            rolodex_pending_master = Secret.from_bytes(
                root_node.state[419: 472])
            
            rolodex_outstanding_ghid = Ghid.from_bytes(
                root_node.state[472: 537])
            rolodex_outstanding_master = Secret.from_bytes(
                root_node.state[537: 590])
            
            # Dispatch stuff
            dispatch_tokens_ghid = Ghid.from_bytes(
                root_node.state[590: 655])
            dispatch_tokens_master = Secret.from_bytes(
                root_node.state[655: 708])
            
            dispatch_startup_ghid = Ghid.from_bytes(
                root_node.state[708: 773])
            dispatch_startup_master = Secret.from_bytes(
                root_node.state[773: 826])
            
            dispatch_private_ghid = Ghid.from_bytes(
                root_node.state[826: 891])
            dispatch_private_master = Secret.from_bytes(
                root_node.state[891: 944])
            
            dispatch_incoming_ghid = Ghid.from_bytes(
                root_node.state[944: 1009])
            dispatch_incoming_master = Secret.from_bytes(
                root_node.state[1009: 1062])
            
            dispatch_orphan_acks_ghid = Ghid.from_bytes(
                root_node.state[1062: 1127])
            dispatch_orphan_acks_master = Secret.from_bytes(
                root_node.state[1127: 1180])
            
            dispatch_orphan_naks_ghid = Ghid.from_bytes(
                root_node.state[1180: 1245])
            dispatch_orphan_naks_master = Secret.from_bytes(
                root_node.state[1245: 1298])
        
        else:
            # We need an identity at to golcore before we can do anything
            logger.info('Bootstrapping golcore.')
            self._golcore.bootstrap(self)
            
            # Note that we first need to push our identity secondparty.
            packed = self._identity.second_party.packed
            gidc = _GidcLite.from_golix(GIDC.unpack(packed))
            logger.info('Publishing public keys.')
            # We haven't bootstrapped salmonator yet, so don't remote the keys
            await self._percore.direct_ingest(gidc, packed, remotable=False)
            
            # Now we need to declare the root note. It needs some kind of
            # useless, garbage data as well, lest it error out because it can't
            # encrypt nothingness.
            root_node = GAO(
                ghid = None,
                dynamic = True,
                author = None,
                legroom = 7,
                state = b'you pass butter',
                golcore = self._golcore,
                ghidproxy = self._ghidproxy,
                privateer = self._privateer,
                percore = self._percore,
                librarian = self._librarian,
                master_secret = self._root_secret
            )
            # And now, remove the root secret from the parent namespace. This
            # will make the root_node GAO the only live reference to the root
            # secret from within the Account.
            del self._root_secret
            
            identity_ghid = None
            identity_master = self._identity.new_secret()
            
            privateer_persistent_ghid = None
            privateer_persistent_master = self._identity.new_secret()
            
            privateer_quarantine_ghid = None
            privateer_quarantine_master = self._identity.new_secret()
            
            # Rolodex stuff
            rolodex_pending_ghid = None
            rolodex_pending_master = self._identity.new_secret()
            
            rolodex_outstanding_ghid = None
            rolodex_outstanding_master = self._identity.new_secret()
            
            # Dispatch stuff
            dispatch_tokens_ghid = None
            dispatch_tokens_master = self._identity.new_secret()
            
            dispatch_startup_ghid = None
            dispatch_startup_master = self._identity.new_secret()
            
            dispatch_private_ghid = None
            dispatch_private_master = self._identity.new_secret()
            
            dispatch_incoming_ghid = None
            dispatch_incoming_master = self._identity.new_secret()
            
            dispatch_orphan_acks_ghid = None
            dispatch_orphan_acks_master = self._identity.new_secret()
            
            dispatch_orphan_naks_ghid = None
            dispatch_orphan_naks_master = self._identity.new_secret()
        
        # Allocate the identity container
        #######################################################################
        # This stores the private keys
        identity_container = GAODict(
            ghid = identity_ghid,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian,
            master_secret = identity_master
        )
        
        # Jiggery-pokery for the privateer restoration
        #######################################################################
        # This stores persistent secrets
        self.privateer_persistent.ghid = privateer_persistent_ghid
        self.privateer_persistent._inject_msec(privateer_persistent_master)
        
        self.privateer_quarantine.ghid = privateer_quarantine_ghid
        self.privateer_quarantine._inject_msec(privateer_quarantine_master)
        
        # Save/load the identity container and bootstrap golcore, privateer
        #######################################################################
        
        # Load existing account
        if self._user_id is not None:
            logger.info('Loading identity.')
            await identity_container._pull()
            self._identity = FirstParty._from_serialized(
                identity_container.state
            )
            logger.info('Bootstrapping golcore.')
            self._golcore.bootstrap(self)
            logger.info('Bootstrapping salmonator.')
            await self._salmonator.bootstrap(self)
            
            logger.info('Loading persistent keystore.')
            await self.privateer_persistent._pull()

            logger.info('Loading quarantined keystore.')
            await self.privateer_quarantine._pull()
            
        # Save new account
        else:
            # First, bootstrap salmonator, so that we don't get errors as an
            # unknown author
            await self._salmonator.bootstrap(self)
            # Initializing is needed to prevent losing the first frame while
            # the secret ratchet initializes
            logger.info('Allocating the root node.')
            await root_node._push()
            # Don't forget to do this before creating the actual identity
            # frame, or we'll lose the first frame (and therefore, everything
            # we care about)
            logger.info('Allocating identity container.')
            await identity_container._push(force=True)
            logger.info('Saving identity.')
            identity_container.update(self._identity._serialize())
            await identity_container._push(force=True)
            
            # Because these use a master secret, they need to be initialized,
            # or the first frame will be unrecoverable.
            logger.info('Allocating persistent keystore.')
            await self.privateer_persistent._push(force=True)
            await self.privateer_persistent._push(force=True)
            
            logger.info('Allocating quarantined keystore.')
            await self.privateer_quarantine._push(force=True)
            await self.privateer_quarantine._push(force=True)
            
        # Establish the rest of the above at the various tracking agencies
        logger.info('Reticulating keystores.')
        await self._inject_gao(self.privateer_persistent)
        await self._inject_gao(self.privateer_quarantine)
        # We don't need to do this with the secondary manifest (unless we're
        # planning on adding things to it while already running, which would
        # imply an ad-hoc, on-the-fly upgrade process)
        
        # Rolodex gaos:
        self.rolodex_pending = GAODict(
            ghid = rolodex_pending_ghid,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian,
            master_secret = rolodex_pending_master
        )
        self.rolodex_outstanding = GAOSetMap(
            ghid = rolodex_outstanding_ghid,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian,
            master_secret = rolodex_outstanding_master
        )
        
        # Dispatch gaos:
        self.dispatch_tokens = GAOSet(
            ghid = dispatch_tokens_ghid,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian,
            master_secret = dispatch_tokens_master
        )
        self.dispatch_startup = GAODict(
            ghid = dispatch_startup_ghid,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian,
            master_secret = dispatch_startup_master
        )
        self.dispatch_private = GAODict(
            ghid = dispatch_private_ghid,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian,
            master_secret = dispatch_private_master
        )
        self.dispatch_incoming = GAOSet(
            ghid = dispatch_incoming_ghid,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian,
            master_secret = dispatch_incoming_master
        )
        self.dispatch_orphan_acks = GAOSetMap(
            ghid = dispatch_orphan_acks_ghid,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian,
            master_secret = dispatch_orphan_acks_master
        )
        self.dispatch_orphan_naks = GAOSetMap(
            ghid = dispatch_orphan_naks_ghid,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian,
            master_secret = dispatch_orphan_naks_master
        )
        
        # These need not have the actual objects pulled yet
        self._rolodex.bootstrap(self)
        self._dispatch.bootstrap(self)
        
        if self._user_id is not None:
            logger.info('Restoring sharing subsystem.')
            await self.rolodex_pending._pull()
            await self.rolodex_outstanding._pull()
            
            logger.info('Restoring object dispatch.')
            await self.dispatch_tokens._pull()
            await self.dispatch_startup._pull()
            await self.dispatch_private._pull()
            await self.dispatch_incoming._pull()
            await self.dispatch_orphan_acks._pull()
            await self.dispatch_orphan_naks._pull()
        
        else:
            logger.info('Building sharing subsystem.')
            await self.rolodex_pending._push(force=True)
            await self.rolodex_pending._push(force=True)
            await self.rolodex_outstanding._push(force=True)
            await self.rolodex_outstanding._push(force=True)
            
            logger.info('Building object dispatch.')
            await self.dispatch_tokens._push(force=True)
            await self.dispatch_tokens._push(force=True)
            await self.dispatch_startup._push(force=True)
            await self.dispatch_startup._push(force=True)
            await self.dispatch_private._push(force=True)
            await self.dispatch_private._push(force=True)
            await self.dispatch_incoming._push(force=True)
            await self.dispatch_incoming._push(force=True)
            await self.dispatch_orphan_acks._push(force=True)
            await self.dispatch_orphan_acks._push(force=True)
            await self.dispatch_orphan_naks._push(force=True)
            await self.dispatch_orphan_naks._push(force=True)
            
            self._user_id = root_node.ghid
        
            logger.info('Building root node...')
            # Generate secure-random-length, pseudorandom-content padding
            logger.info('    Generating noisy padding.')
            # Note that we don't actually need CSRNG for the padding, just the
            # padding length, since the whole thing is encrypted. We could just
            # as easily fill it with zeros, but by filling it with pseudorandom
            # noise, we can remove a recognizable pattern and therefore slighly
            # hinder brute force attacks against the password.
            # While we COULD use CSRNG despite all that, entropy is a limited
            # resource, and I'd rather conserve it as much as possible.
            padding_seed = int.from_bytes(os.urandom(2), byteorder='big')
            padding_min_size = 1024
            padding_clip_mask = 0b0001111111111111
            # Clip the seed to an upper range of 13 bits, of 8191, for a
            # maximum padding length of 8191 + 1024 = 9215 bytes
            padding_len = padding_min_size + (padding_seed & padding_clip_mask)
            padding_int = random.getrandbits(padding_len * 8)
            padding = padding_int.to_bytes(length=padding_len, byteorder='big')
            
            logger.info('    Serializing primary manifest.')
            root_node.state = (bytes(identity_container.ghid) +
                               bytes(identity_master) +
                               bytes(self.privateer_persistent.ghid) +
                               bytes(privateer_persistent_master) +
                               bytes(self.privateer_quarantine.ghid) +
                               bytes(privateer_quarantine_master) +
                               bytes(self.rolodex_pending.ghid) +
                               bytes(rolodex_pending_master) +
                               bytes(self.rolodex_outstanding.ghid) +
                               bytes(rolodex_outstanding_master) +
                               bytes(self.dispatch_tokens.ghid) +
                               bytes(dispatch_tokens_master) +
                               bytes(self.dispatch_startup.ghid) +
                               bytes(dispatch_startup_master) +
                               bytes(self.dispatch_private.ghid) +
                               bytes(dispatch_private_master) +
                               bytes(self.dispatch_incoming.ghid) +
                               bytes(dispatch_incoming_master) +
                               bytes(self.dispatch_orphan_acks.ghid) +
                               bytes(dispatch_orphan_acks_master) +
                               bytes(self.dispatch_orphan_naks.ghid) +
                               bytes(dispatch_orphan_naks_master) +
                               padding)
            
            # We'll use this upon future logins to verify password correctness
            logger.info('    Generating validator and comparator.')
            # Just append a SHA512 checksum to the whole thing
            password_validator = sha512(root_node.state).digest()
            root_node.state += password_validator
            
            logger.info('Saving root node.')
            await root_node._push()
        
        #######################################################################
        #######################################################################
        # ROOT NODE CREATION (PRIMARY BOOTSTRAP) COMPLETE!
        #######################################################################
        #######################################################################
        
        logger.info('Reticulating sharing subsystem.')
        await self._inject_gao(self.rolodex_pending)
        await self._inject_gao(self.rolodex_outstanding)
        
        logger.info('Reticulating object dispatch.')
        await self._inject_gao(self.dispatch_tokens)
        await self._inject_gao(self.dispatch_startup)
        await self._inject_gao(self.dispatch_private)
        await self._inject_gao(self.dispatch_incoming)
        await self._inject_gao(self.dispatch_orphan_acks)
        await self._inject_gao(self.dispatch_orphan_naks)
        
        logger.info('Account login successful.')
    
    @fixture_noop
    @public_api
    async def flush(self):
        ''' Push changes to any modified account components.
        '''
        tasks = {
            make_background_future(self.privateer_persistent.push()),
            make_background_future(self.privateer_quarantine.push()),
            make_background_future(self.rolodex_pending.push()),
            make_background_future(self.rolodex_outstanding.push()),
            make_background_future(self.dispatch_tokens.push()),
            make_background_future(self.dispatch_startup.push()),
            make_background_future(self.dispatch_private.push()),
            make_background_future(self.dispatch_incoming.push()),
            make_background_future(self.dispatch_orphan_acks.push()),
            make_background_future(self.dispatch_orphan_naks.push())
        }
        await asyncio.wait(fs=tasks, return_when=asyncio.ALL_COMPLETED) class _ConnectionBase(metaclass=API):
    ''' Defines common interface for all connections.
    
    TODO: modify this such that the return is ALWAYS weakly referenced,
    but still hashable, etc. Within that proxy, wrap all ReferenceErrors
    with ConnectionClosed errors.
    '''
    
    @public_api
    def __init__(self, *args, **kwargs):
        ''' Log the creation of the connection.
        '''
        super().__init__(*args, **kwargs)
        # Create a reference to ourselves so that we can manage our own
        # lifetime through self.terminate()
        self._ref = self
        # If termination gets any more complicated, add this in so we can
        # register self.terminate in an atexit call.
        # ALL_CONNECTIONS.add(self)
        
        # Memoize our repr as the hex repr of our id(self)
        self._repr = '<' + type(self).__name__ + ' ' + hex(id(self)) + '>'
        # Memoize a urlsafe base64 string of our id(self) for str
        self._str = str(
            # Change the bytes int to base64
            base64.urlsafe_b64encode(
                # Convert our id(self) to a bytes integer equal to the maximum
                # length of ID on a 64-bit system
                id(self).to_bytes(byteorder='big', length=8)
            ),
            # And then re-cast as a native str
            'utf-8'
        )
        # And log our creation.
        logger.info('CONN ' + str(self) + ' CREATED.')
        
    @__init__.fixture
    def __init__(self, msg_iterator=None, *args, **kwargs):
        ''' Add in an isalive flag and an iterator to simulate message
        reciept.
        '''
        super(_ConnectionBase.__fixture__, self).__init__(*args, **kwargs)
        self._isalive = True
        self._msg_iterator = msg_iterator
        
    def terminate(self):
        ''' Remove our circular reference, which (assuming all other
        refs were weak) will result in our garbage collection.
        Idempotent.
        '''
        # Note: if this gets any more complicated, then we should register an
        # atext cleanup to call terminate. For now, just let GC remove it.
        try:
            del self._ref
            
        except AttributeError:
            logger.debug(
                'Attempted to terminate connection twice. Check for other ' +
                'strong references to the connection.'
            )
            
    def __bool__(self):
        ''' Return True if the connection is still active and False
        otherwise.
        '''
        return hasattr(self, '_ref')
        
    def __repr__(self):
        ''' Make a slightly nicer version of repr, since connections are
        ephemeral and we cannot reproduce them anyways.
        '''
        # Example: <_AutoresponderSession 0x52b2978>
        return self._repr
        
    def __str__(self):
        ''' Make an even more concise version of str, just giving the
        id, as a constant-length base64 string.
        '''
        return self._str
            
    @classmethod
    def desc_str(cls, *args, **kwargs):
        ''' Override this to explain where the connection is supposed to
        go, for use in debug scenarios.
        
        *args and **kwargs should match those used in cls.new()
        '''
        return cls.__name__ + '(*' + str(args) + ', **' + str(kwargs) + ')'
        
    async def listener(self, receiver):
        ''' Once a connection has been created, call this to listen for
        a message until the connection is terminated. Put it in a loop
        to listen forever.
        '''
        try:
            msg = await self.recv()
        
        except ConnectionClosed:
            logger.info(
                'CONN ' + str(self) + ' closed at listener.'
            )
            raise
            
        else:
            logger.debug('CONN ' + str(self) + ' message received.')
            
            try:
                # When we pass to the receiver, make sure we give them a strong
                # reference, so hashing and stuff continues to work.
                await receiver(self, msg)
                
            except asyncio.CancelledError:
                logger.debug('CONN ' + str(self) + ' receive cancelled.')
                raise
            
            except Exception:
                logger.error(
                    'CONN ' + str(self) + ' Listener receiver ' +
                    'raised w/ traceback:\n' + ''.join(traceback.format_exc())
                )
                raise
                
    async def listen_forever(self, receiver):
        ''' Listens until the connection terminates.
        '''
        # Wait until terminate is called.
        try:
            while self:
                # Juuust in case things go south, add this to help
                # cancellation.
                await asyncio.sleep(0)
                await self.listener(receiver)
        
        # Catch this so we don't log a huge traceback on it.
        except ConnectionClosed as exc:
            logger.debug('CONN ' + str(self) + ' close message: ' + str(exc))
        
    @classmethod
    @fixture_api
    async def serve_forever(cls, msg_handler, *args, **kwargs):
        ''' Starts a server for this kind of connection. Should handle
        its own return, and be cancellable via task cancellation.
        '''
        
    @classmethod
    @fixture_api
    async def new(cls, *args, **kwargs):
        ''' Creates and returns a new connection. Intended to be called
        by clients; servers may call __init__ directly.
        '''
        return cls(*args, **kwargs)
    
    @fixture_api
    async def close(self):
        ''' Closes the existing connection, performing any necessary
        cleanup.
        
        Close MUST be idempotent.
        '''
        self._isalive = False
    
    @fixture_api
    async def send(self, msg):
        ''' Does whatever is needed to send a message.
        '''
        # Noop fixture.
    
    @fixture_api
    async def recv(self):
        ''' Waits for first available message and returns it.
        '''
        # Floating point [0.0, 1.0]
        scalar = random.random()
        # Wait somewhere between 0 and .1 seconds
        delay = scalar * .1
        
        if self._msg_iterator is None:
            # Figure out how long to make the message -- somewhere between
            # 32 bytes and a kilobibite, inclusive
            length = random.randrange(32, 1024)
            # Make the message itself
            msg = bytes([random.randint(0, 255) for i in range(length)])
        else:
            msg = next(self._msg_iterator)
            
        if self._isalive:
            # Wait the delay and then return
            await asyncio.sleep(delay)
            return msg
        
        else:
            raise ConnectionClosed() class Oracle(metaclass=API):
    ''' Source for total internal truth and state tracking of objects.
    
    Maintains <ghid>: <obj> lookup. Used by dispatchers to track obj
    state. Might eventually be used by AgentBase. Just a quick way to
    store and retrieve any objects based on an associated ghid.
    '''
    # These are actually used by the oracle itself
    _salmonator = weak_property('__salmonator')
    
    # These are only here to pass along to GAOs
    _golcore = weak_property('__golcore')
    _ghidproxy = weak_property('__ghidproxy')
    _privateer = weak_property('__privateer')
    _percore = weak_property('__percore')
    _librarian = weak_property('__librarian')
    
    def __init__(self, *args, **kwargs):
        ''' Sets up internal tracking.
        '''
        super().__init__(*args, **kwargs)
        self._lookup = {}
        
    @fixture_api
    def RESET(self):
        ''' Simply re-call init.
        '''
        self._lookup.clear()
        
    def assemble(self, golcore, ghidproxy, privateer, percore, librarian,
                 salmonator):
        # Chicken, meet egg.
        self._golcore = golcore
        self._ghidproxy = ghidproxy
        self._privateer = privateer
        self._percore = percore
        self._librarian = librarian
        self._salmonator = salmonator
        
    @fixture_api
    def add_object(self, ghid, obj):
        ''' Add an object to the fixture.
        '''
        self._lookup[ghid] = obj
            
    @public_api
    async def get_object(self, gaoclass, ghid, *args, **kwargs):
        ''' Get an object.
        '''
        if ghid in self._lookup:
            # TODO: this is bad, because we're suppresing any potential argsig
            # problems. We should fix the abstraction so that it doesn't break
            # assumptions like that.
            obj = self._lookup[ghid]
            logger.debug(
                'GAO ' + str(ghid) + ' already exists in Oracle memory as: ' +
                str(type(obj))
            )
            
            if not isinstance(obj, gaoclass):
                raise TypeError(
                    'Object has already been resolved, and is not the '
                    'correct GAO class.'
                )
            else:
                logger.debug(
                    'GAO ' + str(ghid) + ' acceptable for ' + str(gaoclass)
                )
                
            await obj._ctx.wait()
            if ghid not in self._lookup:
                raise RuntimeError('Contentious delete while getting object.')
        
        else:
            logger.info(
                'GAO ' + str(ghid) +
                ' not currently in Oracle memory. Attempting load as: ' +
                str(gaoclass)
            )
            
            # First create the actual GAO. We do not need to have the ghid
            # downloaded to do this -- object creation is just making a Python
            # object locally.
            obj = gaoclass(
                ghid,
                None,   # dynamic
                None,   # author
                7,      # legroom (will be overwritten by pull)
                *args,
                golcore = self._golcore,
                ghidproxy = self._ghidproxy,
                privateer = self._privateer,
                percore = self._percore,
                librarian = self._librarian,
                **kwargs
            )
            
            # TODO: fix leaky abstraction
            obj._ctx = asyncio.Event()
            
            # Always do this first to make sure we have the most recent version
            # in all subsequent calls, without a race condition.
            self._lookup[ghid] = obj
            
            try:
                # Now immediately subscribe to the object upstream, so that there
                # is no race condition getting updates
                await self._salmonator.register(ghid)
                # Add deregister as a finalizer, but don't call it atexit. TODO:
                # fix leaky abstraction
                finalizer = weakref.finalize(obj, self._salmonator._deregister,
                                             ghid)
                finalizer.atexit = False
                # Explicitly pull the object from salmonator to ensure we have
                # the newest version, and that it is available locally in
                # librarian if also available anywhere else. Note that
                # salmonator handles modal switching for dynamic/static. It
                # will also (by default, with skip_refresh=False) pull in any
                # updates that have accumulated in the meantime.
                await self._salmonator.attempt_pull(ghid, quiet=True)
                
                # Now actually fetch the object. This may KeyError if the
                # ghid is still unknown.
                await obj._pull()
            
            # Got an exception? Revert the lookup and reraise
            except Exception:
                del self._lookup[ghid]
                raise
                
            # We have to release other waiters regardless
            finally:
                # Finally, let future callers unblock
                obj._ctx.set()
            
        return obj
        
    @get_object.fixture
    async def get_object(self, gaoclass, ghid, *args, **kwargs):
        ''' Do the easy thing and just pull it out of lookup.
        '''
        # We should also check argsig while we're at it
        signature = inspect.Signature.from_callable(gaoclass)
        signature.bind(
            ghid,
            None,   # dynamic
            None,   # author
            7,      # legroom (will be overwritten by pull)
            *args,
            golcore = self,     # It's not going to be used and may not exist
            ghidproxy = self,   # It's not going to be used and may not exist
            privateer = self,   # It's not going to be used and may not exist
            percore = self,     # It's not going to be used and may not exist
            librarian = self,   # It's not going to be used and may not exist
            **kwargs
        )
            
        return self._lookup[ghid]
        
    @public_api
    async def new_object(self, gaoclass, dynamic, legroom, *args, **kwargs):
        ''' Creates a new object and returns it. Passes all *kwargs to
        the declared gao_class. Requires a zeroth state, and calls push
        internally.
        '''
        obj = gaoclass(
            None,                   # ghid
            dynamic,
            self._golcore.whoami,   # author
            legroom,
            *args,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian,
            **kwargs
        )
        await obj._push()
        
        # This is used strictly by oracle. TODO: fix leaky abstraction
        obj._ctx = asyncio.Event()
        obj._ctx.set()
        # Do this before registering with salmonator, in case the latter errors
        self._lookup[obj.ghid] = obj
        
        # Finally, register to receive any concurrent updates from other
        # simultaneous sessions, and then return the object
        await self._salmonator.register(obj.ghid)
        # Add deregister as a finalizer, but don't call it atexit. TODO: fix
        # leaky abstraction
        finalizer = weakref.finalize(obj, self._salmonator._deregister,
                                     obj.ghid)
        finalizer.atexit = False
        
        return obj
            
    @new_object.fixture
    async def new_object(self, gaoclass, dynamic, legroom, *args, **kwargs):
        ''' Relies upon add_object, but otherwise just pops something
        from the lookup.
        '''
        # We should also check argsig while we're at it
        signature = inspect.Signature.from_callable(gaoclass)
        signature.bind(
            None,               # ghid
            dynamic,
            None,               # author (normally assigned through whoami)
            legroom,
            *args,
            golcore = self,     # It's not going to be used and may not exist
            ghidproxy = self,   # It's not going to be used and may not exist
            privateer = self,   # It's not going to be used and may not exist
            percore = self,     # It's not going to be used and may not exist
            librarian = self,   # It's not going to be used and may not exist
            **kwargs
        )
        
        ghid, obj = self._lookup.popitem()
        self._lookup[ghid] = obj
        return obj
        
    def forget(self, ghid):
        ''' Removes the object from the cache. Next time an application
        wants it, it will need to be acquired from persisters.
        
        Indempotent; will not raise KeyError if called more than once.
        '''
        try:
            del self._lookup[ghid]
        except KeyError:
            logger.debug(str(ghid) + ' unknown to oracle.')
            
    def __contains__(self, ghid):
        ''' Checks for the ghid in cache (but does not check for global
        availability; that would require checking the persister for its
        existence and the privateer for access).
        '''
        return ghid in self._lookup class IPCServerProtocol(_IPCSerializer, metaclass=RequestResponseAPI,
                        error_codes=ERROR_CODES, default_version=b'\x00\x00'):
    ''' Defines the protocol for IPC, with handlers specific to servers.
    '''
    _dispatch = weak_property('__dispatch')
    _oracle = weak_property('__oracle')
    _golcore = weak_property('__golcore')
    _rolodex = weak_property('__rolodex')
    _salmonator = weak_property('__salmonator')
        
    @fixture_api
    def __init__(self, whoami, *args, **kwargs):
        super(IPCServerProtocol.__fixture__, self).__init__(*args, **kwargs)
        self._whoami = whoami
        self.shares = collections.deque()
        self.updates = collections.deque()
        self.deletes = collections.deque()
        self.share_successes = collections.deque()
        self.share_failures = collections.deque()
        
    def assemble(self, golcore, oracle, dispatch, rolodex, salmonator):
        # Chicken, egg, etc.
        self._golcore = golcore
        self._oracle = oracle
        self._dispatch = dispatch
        self._rolodex = rolodex
        self._salmonator = salmonator
        
    @request(b'+T')
    async def set_token(self, connection, token=None):
        ''' Register an existing token or get a new token, or notify an
        app of its existing token.
        '''
        # On the server side, this will only be implemented once actual
        # application launching is available.
        raise NotImplementedError()
        
    @set_token.request_handler
    async def set_token(self, connection, body):
        ''' Handles token-setting requests.
        '''
        token = AppToken(body)
        
        # Getting a new token.
        if token == AppToken.null():
            token = await self._dispatch.start_application(connection)
            logger.info(''.join((
                'CONN ', str(connection), ' generating new token: ', str(token)
            )))
        
        # Setting an existing token, but the connection already exists.
        elif self._dispatch.token_lookup(connection):
            raise IPCError(
                'Attempt to reregister a new concurrent token for an ' +
                'existing connection. Each app may use only one token.'
            )
            
        # Setting an existing token, but the token already exists.
        elif self._dispatch.connection_lookup(token) is not None:
            raise IPCError(
                'Attempt to reregister a new concurrent connection for ' +
                'an existing token. Each app may only use one connection.'
            )
        
        # Setting an existing token, with valid state.
        else:
            logger.info(''.join((
                'CONN ', str(connection), ' registering existing token: ',
                str(token)
            )))
            await self._dispatch.start_application(connection, token)
        
        return token
        
    @request(b'+A')
    async def register_api(self, connection):
        ''' Registers the application as supporting an API. Client only.
        '''
        raise NotImplementedError()
        
    @register_api.request_handler
    async def register_api(self, connection, body):
        ''' Handles API registration requests. Server only.
        '''
        api_id = ApiID.from_bytes(body)
        await self._dispatch.add_api(connection, api_id)
        
        return b'\x01'
        
    @request(b'-A')
    async def deregister_api(self, connection):
        ''' Removes any existing registration for the app supporting an
        API. Client only.
        '''
        raise NotImplementedError()
        
    @deregister_api.request_handler
    async def deregister_api(self, connection, body):
        ''' Handles API deregistration requests. Server only.
        '''
        api_id = ApiID.from_bytes(body)
        await self._dispatch.remove_api(connection, api_id)
        
        return b'\x01'
        
    @public_api
    @request(b'?I')
    async def whoami(self, connection):
        ''' Get the current hypergolix fingerprint, or notify an app of
        the current hypergolix fingerprint.
        '''
        # On the server side, this will only be implemented once actual
        # application launching is available.
        raise NotImplementedError()
        
    @whoami.request_handler
    async def whoami(self, connection, body):
        ''' Handles whoami requests.
        '''
        ghid = self._golcore.whoami
        return bytes(ghid)
        
    @request(b'>$')
    async def get_startup_obj(self, connection):
        ''' Request a startup object, or notify an app of its declared
        startup object.
        '''
        token = self._dispatch.token_lookup(connection)
        ghid = await self._dispatch.get_startup_obj(token)
        
        if ghid is not None:
            return bytes(ghid)
        else:
            return b''
        
    @get_startup_obj.request_handler
    async def get_startup_obj(self, connection, body):
        ''' Handles requests for startup objects.
        '''
        token = self._dispatch.token_lookup(connection)
        ghid = await self._dispatch.get_startup_obj(token)
        
        if ghid is not None:
            return bytes(ghid)
        else:
            return b''
        
    @request(b'+$')
    async def register_startup_obj(self, connection, ghid):
        ''' Register a startup object. Client only.
        '''
        raise NotImplementedError()
        
    @register_startup_obj.request_handler
    async def register_startup_obj(self, connection, body):
        ''' Handles startup object registration. Server only.
        '''
        ghid = Ghid.from_bytes(body)
        await self._dispatch.register_startup(connection, ghid)
        return b'\x01'
        
    @request(b'-$')
    async def deregister_startup_obj(self, connection):
        ''' Register a startup object. Client only.
        '''
        raise NotImplementedError()
        
    @deregister_startup_obj.request_handler
    async def deregister_startup_obj(self, connection, body):
        ''' Handles startup object registration. Server only.
        '''
        await self._dispatch.deregister_startup(connection)
        return b'\x01'
        
    @request(b'>O')
    async def get_obj(self, connection):
        ''' Get an object with the specified address. Client only.
        '''
        raise NotImplementedError()
        
    @get_obj.request_handler
    async def get_obj(self, connection, body):
        ''' Handles requests for an object. Server only.
        '''
        ghid = Ghid.from_bytes(body)
        obj = await self._oracle.get_object(
            gaoclass = _Dispatchable,
            ghid = ghid,
            api_id = None,  # Let _pull() apply this.
            state = None,   # Let _pull() apply this.
            dispatch = self._dispatch,
            ipc_protocol = self,
            account = self._dispatch._account
        )
        
        await self._dispatch.track_object(connection, obj.ghid)
            
        if isinstance(obj.state, Ghid):
            is_link = True
            state = bytes(obj.state)
        else:
            is_link = False
            state = obj.state
            
        # For now, anyways.
        # Note: need to add some kind of handling for legroom.
        _legroom = None
        
        # Not a big fan of how this works, seems inelegant to me
        private = bool(self._dispatch.private_parent_lookup(obj.ghid))
        
        return self._pack_object_def(
            obj.ghid,
            obj.author,
            state,
            is_link,
            obj.api_id,
            private,
            obj.dynamic,
            _legroom
        )
        
    @request(b'+O')
    async def new_obj(self, connection):
        ''' 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.
        '''
        # Not currently supported.
        raise NotImplementedError()
        
    @new_obj.request_handler
    async def new_obj(self, connection, body):
        ''' Handles requests for new objects.
        '''
        (address,    # Unused and set to None.
         author,     # Unused and set to None.
         state,
         is_link,
         api_id,
         private,
         dynamic,
         legroom) = self._unpack_object_def(body)
        
        if is_link:
            raise NotImplementedError('Linked objects are not yet supported.')
            state = Ghid.from_bytes(state)
        
        obj = await self._oracle.new_object(
            gaoclass = _Dispatchable,
            dispatch = self._dispatch,
            ipc_protocol = self,
            state = state,
            dynamic = dynamic,
            legroom = legroom,
            api_id = api_id,
            account = self._dispatch._account
        )
            
        # Add the endpoint as a listener.
        await self._dispatch.register_object(connection, obj.ghid, private)
        await self._dispatch.track_object(connection, obj.ghid)
        
        return bytes(obj.ghid)
    
    @public_api
    @request(b'!O')
    async def update_obj(self, connection, ghid):
        ''' Update an object or notify an app of an incoming update.
        '''
        try:
            obj = await self._oracle.get_object(
                gaoclass = _Dispatchable,
                ghid = ghid,
                api_id = None,  # Let _pull() apply this.
                state = None,   # Let _pull() apply this.
                dispatch = self._dispatch,
                ipc_protocol = self,
                account = self._dispatch._account
            )
            
        # No CancelledError catch necessary because we're re-raising any exc
            
        except Exception:
            # At some point we'll need some kind of proper handling for this.
            logger.error(
                'Failed to retrieve object at ' + str(ghid) + '\n' +
                ''.join(traceback.format_exc())
            )
            raise
            
        else:
            return self._pack_object_def(
                obj.ghid,
                obj.author,
                obj.state,
                False,      # is_link (currently unsupported)
                obj.api_id,
                None,       # private
                obj.dynamic,
                None        # legroom
            )
    
    @update_obj.fixture
    async def update_obj(self, connection, ghid):
        ''' Manual no-op fixture, courtesy of descriptors not being
        callable or whatever.
        '''
        self.updates.append((connection, ghid))
    
    @update_obj.request_handler
    async def update_obj(self, connection, body):
        ''' Handles update object requests.
        '''
        logger.debug('Handling update request from ' + str(connection))
        (ghid,
         author,    # Unused and set to None.
         state,
         is_link,
         api_id,    # Unused and set to None.
         private,   # TODO: use this.
         dynamic,   # Unused and set to None.
         legroom   # TODO: use this.
         ) = self._unpack_object_def(body)
        
        if is_link:
            raise NotImplementedError('Linked objects are not yet supported.')
            state = Ghid.from_bytes(state)
            
        obj = await self._oracle.get_object(
            gaoclass = _Dispatchable,
            ghid = ghid,
            api_id = None,  # Let _pull() apply this.
            state = None,   # Let _pull() apply this.
            dispatch = self._dispatch,
            ipc_protocol = self,
            account = self._dispatch._account
        )
        obj.state = state
        
        # Converting a private object to a public one
        if self._dispatch.private_parent_lookup(ghid):
            if not private:
                await self._dispatch.make_public(ghid)
        
        else:
            if private:
                raise CatOuttaBagError(
                    'Once made public, objects cannot be made private. They ' +
                    'have already been distributed to other applications.'
                )
            
        await obj.push()
        
        # Schedule an update in the background.
        make_background_future(
            self._dispatch.distribute_update(
                obj.ghid,
                skip_conn = connection
            )
        )
        
        return b'\x01'
        
    @request(b'~O')
    async def sync_obj(self, connection):
        ''' Manually force Hypergolix to check an object for updates.
        Client only.
        '''
        raise NotImplementedError()
        
    @sync_obj.request_handler
    async def sync_obj(self, connection, body):
        ''' Handles manual syncing requests. Server only.
        '''
        ghid = Ghid.from_bytes(body)
        await self._salmonator.attempt_pull(ghid)
        return b'\x01'
    
    @public_api
    @request(b'@O')
    async def share_obj(self, connection, ghid, origin, api_id):
        ''' Request an object share or notify an app of an incoming
        share.
        '''
        if origin is None:
            origin = self._golcore.whoami
        
        return bytes(ghid) + bytes(origin) + bytes(api_id)
        
    @share_obj.fixture
    async def share_obj(self, connection, ghid, origin, api_id):
        ''' Manual no-op fixture, courtesy of descriptors not being
        callable or whatever.
        '''
        self.shares.append(
            _ShareLog(connection, ghid, origin, api_id)
        )
        
    @share_obj.request_handler
    async def share_obj(self, connection, body):
        ''' Handles object share requests.
        '''
        ghid = Ghid.from_bytes(body[0:65])
        recipient = Ghid.from_bytes(body[65:130])
        
        # Instead of forbidding unregistered apps from sharing objects,
        # go for it, but document that you will never be notified of a
        # share success or failure without an app token.
        requesting_token = self._dispatch.token_lookup(connection)
        if requesting_token is None:
            logger.info(
                'CONN ' + str(connection) + ' is sharing ' + str(ghid) +
                ' with ' + str(recipient) + ' without defining an app ' +
                'token, and therefore cannot be notified of share success ' +
                'or failure.'
            )
            
        await self._rolodex.share_object(ghid, recipient, requesting_token)
        return b'\x01'
    
    @public_api
    @request(b'^S')
    async def notify_share_success(self, connection, ghid, recipient):
        ''' Notify app of successful share. Server only.
        '''
        return bytes(ghid) + bytes(recipient)
    
    @notify_share_success.fixture
    async def notify_share_success(self, connection, ghid, recipient):
        ''' Manual no-op fixture, courtesy of descriptors not being
        callable or whatever.
        '''
        self.share_successes.append((connection, ghid, recipient))
    
    @notify_share_success.request_handler
    async def notify_share_success(self, connection, body):
        ''' Handles app notifications for successful shares. Client
        only.
        '''
        raise NotImplementedError()
    
    @public_api
    @request(b'^F')
    async def notify_share_failure(self, connection, ghid, recipient):
        ''' Notify app of unsuccessful share. Server only.
        '''
        return bytes(ghid) + bytes(recipient)
        
    @notify_share_failure.fixture
    async def notify_share_failure(self, connection, ghid, recipient):
        ''' Manual no-op fixture, courtesy of descriptors not being
        callable or whatever.
        '''
        self.share_failures.append((connection, ghid, recipient))
    
    @notify_share_failure.request_handler
    async def notify_share_failure(self, connection, body):
        ''' Handles app notifications for unsuccessful shares. Client
        only.
        '''
        raise NotImplementedError()
        
    @request(b'*O')
    async def freeze_obj(self, connection):
        ''' 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.
        '''
        # Not currently supported.
        raise NotImplementedError()
        
    @freeze_obj.request_handler
    async def freeze_obj(self, connection, body):
        ''' Handles object freezing requests.
        '''
        ghid = Ghid.from_bytes(body)
        obj = await self._oracle.get_object(
            gaoclass = _Dispatchable,
            ghid = ghid,
            api_id = None,  # Let _pull() apply this.
            state = None,   # Let _pull() apply this.
            dispatch = self._dispatch,
            ipc_protocol = self,
            account = self._dispatch._account
        )
        frozen_address = await obj.freeze()
        
        return bytes(frozen_address)
        
    @request(b'#O')
    async def hold_obj(self, connection):
        ''' Creates a new static binding for the object, or notifies an
        app of a static binding created by a concurrent instance of the
        app.
        '''
        # Not currently supported.
        raise NotImplementedError()
        
    @hold_obj.request_handler
    async def hold_obj(self, connection, body):
        ''' Handles object holding requests.
        '''
        ghid = Ghid.from_bytes(body)
        obj = await self._oracle.get_object(
            gaoclass = _Dispatchable,
            ghid = ghid,
            api_id = None,  # Let _pull() apply this.
            state = None,   # Let _pull() apply this.
            dispatch = self._dispatch,
            ipc_protocol = self,
            account = self._dispatch._account
        )
        await obj.hold()
        return b'\x01'
        
    @request(b'-O')
    async def discard_obj(self, connection):
        ''' Stop listening to object updates. Client only.
        '''
        raise NotImplementedError()
        
    @discard_obj.request_handler
    async def discard_obj(self, connection, body):
        ''' Handles object discarding requests. Server only.
        '''
        ghid = Ghid.from_bytes(body)
        await self._dispatch.untrack_object(connection, ghid)
        return b'\x01'
    
    @public_api
    @request(b'XO')
    async def delete_obj(self, connection, ghid):
        ''' Request an object deletion or notify an app of an incoming
        deletion.
        '''
        if not isinstance(ghid, Ghid):
            raise TypeError('ghid must be type Ghid or similar.')
        
        return bytes(ghid)
    
    @delete_obj.fixture
    async def delete_obj(self, connection, ghid):
        ''' Manual no-op fixture, courtesy of descriptors not being
        callable or whatever.
        '''
        self.deletes.append((connection, ghid))
    
    @delete_obj.request_handler
    async def delete_obj(self, connection, body):
        ''' Handles object deletion requests.
        '''
        ghid = Ghid.from_bytes(body)
        obj = await self._oracle.get_object(
            gaoclass = _Dispatchable,
            ghid = ghid,
            api_id = None,  # Let _pull() apply this.
            state = None,   # Let _pull() apply this.
            dispatch = self._dispatch,
            ipc_protocol = self,
            account = self._dispatch._account
        )
        await self._dispatch.untrack_object(connection, ghid)
        # TODO: shift to a dispatch.delete_object method that can ignore this
        # connection when the intevitable "deleted that object!" warning comes
        # back
        await obj.delete()
        return b'\x01' class Dispatcher(metaclass=API):
    ''' The Dispatcher decides which objects should be delivered where.
    This is decided through either:
    
    1. An API identifier (schema) dictating general compatibility as a
        dispatchable
    2. Declaring an object to be private, at which point the Dispatcher
        will internally (though distributedly) maintain that object to
        be exclusively available to an app token.
        
    Objects, once declared as non-private, cannot be retroactively
    privatized. That cat has officially left the bag/building/rodeo.
    However, a private object can be made non-private at a later time,
    provided it has defined an API ID.
    
    Private objects do not track their owning app tokens internally.
    Instead, this is managed through a Dispatcher-internal GAO. As such,
    even a bug resulting in a leaked private object will not result in a
    leaked app token.
    
    Ideally, the dispatcher will eventually also enforce whatever user
    restrictions on sharing are desired BETWEEN INSTALLED APPLICATIONS.
    Sharing restrictions between external parties are within the purview
    of the Rolodex.
    
    TODO: support notification mechanism to push new objects to other
    concurrent hypergolix instances. See note in ipc.ipccore.send_object
    
    TODO: move most of the misc stuff into dedicated new_object and
    get_object methods within dispatch, which (for the most part) just
    pass on straight to the oracle, but add all of the extra tracking
    stuff (and so on) internally.
    '''
    _account = weak_property('__account')
    _ipc_protocol = weak_property('__ipc_protocol')
    _oracle = weak_property('__oracle')
    
    @public_api
    def __init__(self, *args, **kwargs):
        ''' Yup yup yup yup yup yup yup
        '''
        super().__init__(*args, **kwargs)
        
        # Temporarily set distributed state to None.
        # Lookup for all known tokens: set(<tokens>)
        self._all_known_tokens = None
        # Lookup (dict-like) for <app token>: <startup ghid>
        self._startup_by_token = None
        # Lookup (dict-like) for <obj ghid>: <private owner>
        self._private_by_ghid = None
        # Distributed lock for adding app tokens
        self._token_lock = None
        
        # Set of incoming shared ghids that had no conn
        # set(<ghid, sender tuples>)
        self._orphan_incoming_shares = None
        # Setmap-like lookup for share acks that had no conn
        # <app token>: set(<ghids>)
        self._orphan_share_acks = None
        # Setmap-like lookup for share naks that had no conn
        # <app token>: set(<ghids>)
        self._orphan_share_naks = None
        
        # Lookup <app token>: <connection/session/conn>
        self._conn_from_token = weakref.WeakValueDictionary()
        # Reverse lookup <connection/session/conn>: <app token>
        self._token_from_conn = weakref.WeakKeyDictionary()
        
        # Lookup <api ID>: set(<connection/session/conn>)
        self._conns_from_api = WeakSetMap()
        
        # This lookup directly tracks who has a copy of the object
        # Lookup <object ghid>: set(<connection/session/conn>)
        self._update_listeners = WeakSetMap()
        # This lookup preserves a strong reference to the dispatchable **as the
        # key**, allowing the object to be GC'd from local memory only when no
        # connections remain that have it
        self._obj_binding = WeakSetMap()
        
    @__init__.fixture
    def __init__(self, *args, **kwargs):
        ''' Create a dispatch fixture.
        '''
        super(Dispatcher.__fixture__, self).__init__(*args, **kwargs)
        
        self._all_known_tokens = set()
        # Lookup (dict-like) for <app token>: <startup ghid>
        self._startup_by_token = {}
        # Lookup (dict-like) for <obj ghid>: <private owner>
        self._private_by_ghid = {}
        # Distributed lock for adding app tokens
        self._token_lock = NoContext()
        
        # Lookup <api ID>: set(<connection/session/conn>)
        self._conns_from_api = WeakSetMap()
        
        # Lookup <app token>: <connection/session/conn>
        self._conn_from_token = weakref.WeakValueDictionary()
        # Reverse lookup <connection/session/conn>: <app token>
        self._token_from_conn = weakref.WeakKeyDictionary()
        
    @fixture_api
    def RESET(self):
        ''' Reset the fixture to a pristine state.
        '''
        self._all_known_tokens.clear()
        # Lookup (dict-like) for <app token>: <startup ghid>
        self._startup_by_token.clear()
        # Lookup (dict-like) for <obj ghid>: <private owner>
        self._private_by_ghid.clear()
        # Lookup <api ID>: set(<connection/session/conn>)
        self._conns_from_api.clear_all()
        
        # Lookup <app token>: <connection/session/conn>
        self._conn_from_token.clear()
        # Reverse lookup <connection/session/conn>: <app token>
        self._token_from_conn.clear()
        
    def assemble(self, oracle, ipc_protocol):
        self._oracle = oracle
        self._ipc_protocol = ipc_protocol
        
    @public_api
    def bootstrap(self, account):
        ''' Initialize distributed state.
        '''
        self._account = account
        
        # Now init distributed state.
            
        # Lookup for all known tokens: set(<tokens>)
        self._all_known_tokens = account.dispatch_tokens
        # Lookup (set-map-like) for <app token>: set(<startup ghids>)
        self._startup_by_token = account.dispatch_startup
        # Lookup (dict-like) for <obj ghid>: <private owner>
        self._private_by_ghid = account.dispatch_private
        
        # These need to be distributed but aren't yet. TODO!
        # Distributed lock for adding app tokens
        self._token_lock = NoContext()
        
        # Set of incoming shared ghids that had no conn
        # set(<ghid, sender tuples>)
        self._orphan_incoming_shares = account.dispatch_incoming
        # Setmap-like lookup for share acks that had no conn
        # <app token>: set(<ghid, recipient tuples>)
        self._orphan_share_acks = account.dispatch_orphan_acks
        # Setmap-like lookup for share naks that had no conn
        # <app token>: set(<ghid, recipient tuples>)
        self._orphan_share_naks = account.dispatch_orphan_naks
        
        # All known tokens must already contain a key for the null token, which
        # is used by dispatch to indicate a lack of a token in an exchange. Do
        # this as LBYL to avoid false positive _mutated flags in the GAO.
        if AppToken.null() not in self._all_known_tokens:
            self._all_known_tokens.add(AppToken.null())
            
    @public_api
    def token_lookup(self, connection):
        ''' Return the token associated with the connection, or None if
        there is no currently defined token.
        '''
        try:
            return self._token_from_conn[connection]
            
        except KeyError as exc:
            return None
            
    @public_api
    def connection_lookup(self, token):
        ''' Returns the current connection associated with the token, or
        None if there is no currently available connection.
        '''
        try:
            return self._conn_from_token[token]
            
        except KeyError as exc:
            return None
    
    @public_api
    def private_parent_lookup(self, ghid):
        ''' Returns the app_token parent for the passed ghid, if (and
        only if) it's private. Otherwise, returns None.
        '''
        try:
            return self._private_by_ghid[ghid]
        except KeyError:
            return None
    
    @public_api
    async def make_public(self, ghid):
        ''' Makes a private object public.
        '''
        try:
            del self._private_by_ghid[ghid]
            
        except KeyError as exc:
            raise ValueError(
                'Obj w/ passed ghid is unknown or already public: ' + str(ghid)
            ) from exc
            
        else:
            await self._account.flush()
            
    @make_public.fixture
    async def make_public(self, ghid):
        # Bypass all validation
        del self._private_by_ghid[ghid]
        
    @public_api
    async def add_api(self, connection, api_id):
        ''' Register the connection as currently tracking the api_id.
        '''
        self._conns_from_api.add(api_id, connection)
        await self._account.flush()
        
    @add_api.fixture
    async def add_api(self, connection, api_id):
        # Bypass account flushing
        self._conns_from_api.add(api_id, connection)
        
    @public_api
    async def remove_api(self, connection, api_id):
        ''' Remove a connection's registration for the api_id. Happens
        automatically when connections are GC'd.
        '''
        self._conns_from_api.discard(api_id, connection)
        await self._account.flush()
        
    @remove_api.fixture
    async def remove_api(self, connection, api_id):
        # Bypass account flushing
        self._conns_from_api.discard(api_id, connection)
        
    def _make_new_token(self):
        token = None
        # Birthday paradox be damned; we can actually *enforce* uniqueness
        while token is None or token in self._all_known_tokens:
            token = AppToken(os.urandom(AppToken.TOKEN_LEN))
        return token
        
    @public_api
    async def start_application(self, connection, token=None):
        ''' Ensures that an application is known to the dispatcher.
        
        Currently just checks within all known tokens. In the future,
        this be responsible for starting the application (unless we move
        that responsibility elsewhere), and then sending all of the
        startup objects to the application.
        '''
        if connection in self._token_from_conn:
            raise ExistantAppError('Token already registered for connection.')
            
        elif token in self._conn_from_token:
            raise ExistantAppError(
                'Token already started at different connection.'
            )
            
        elif token is None:
            # Token lock is currently a NoContext, but eventually will need to
            # be a distributed lock -- or, the token lookup needs to be made
            # into a no-contention system. Which I'm not sure it possibly can
            # be, since we're best off actually ENFORCING the "no birthday
            # collisions" for tokens.
            with self._token_lock:
                token = self._make_new_token()
                # Do this right away to prevent race condition
                self._all_known_tokens.add(token)
                await self._account.flush()
            
        elif token not in self._all_known_tokens:
            raise UnknownToken('App token unknown to dispatcher.')
            
        # TODO: should these be enclosed within an operations lock?
        self._conn_from_token[token] = connection
        self._token_from_conn[connection] = token
        
        return token
        
    @start_application.fixture
    async def start_application(self, connection, token=None):
        if token is None:
            # ONLY FOR THE FIXTURE! Do we use a pseudorandom token.
            token = AppToken.pseudorandom()
        
        # Don't emulate normal dispatcher behavior here; let everything start,
        # regardless of status re: "exists in tokens"
        self._all_known_tokens.add(token)
        self._conn_from_token[token] = connection
        self._token_from_conn[connection] = token
        
        return token
    
    @fixture_noop
    @public_api
    async def track_object(self, connection, ghid):
        ''' Registers a connection as tracking a ghid.
        
        This is necessary so that upstream updates can be properly
        dispatched to any applications with copies of the object.
        '''
        logger.debug(
            'CONN ' + str(connection) + ' tracking ' + str(ghid) + '...'
        )
        self._update_listeners.add(ghid, connection)
        obj = await self._oracle.get_object(
            gaoclass = _Dispatchable,
            ghid = ghid,
            api_id = None,  # Let _pull() apply this.
            state = None,   # Let _pull() apply this.
            dispatch = self,
            ipc_protocol = self._ipc_protocol,
            account = self._account
        )
        # This keeps the object in memory, allowing it to receive subscriptions
        # When all apps untrack the object, it gets unsubbed.
        self._obj_binding.add(obj, connection)
    
    @fixture_noop
    @public_api
    async def untrack_object(self, connection, ghid):
        ''' Remove a connection as tracking a ghid.
        
        This indicates an application no longer has a copy of the
        object, therefore silencing any updates it would otherwise have
        received.
        '''
        logger.debug(
            'CONN ' + str(connection) + ' untracking ' + str(ghid) + '...'
        )
        self._update_listeners.discard(ghid, connection)
        obj = await self._oracle.get_object(
            gaoclass = _Dispatchable,
            ghid = ghid,
            api_id = None,  # Let _pull() apply this.
            state = None,   # Let _pull() apply this.
            dispatch = self,
            ipc_protocol = self._ipc_protocol,
            account = self._account
        )
        # This keeps the object in memory, allowing it to receive subscriptions
        # When all apps untrack the object, it gets unsubbed.
        self._obj_binding.discard(obj, connection)
        
    @public_api
    async def register_object(self, connection, ghid, private):
        ''' Call this every time a new object is created to register it
        with the dispatcher, recording it as private or distributing it
        to other applications as needed.
        '''
        # If the object is private, register it as such.
        if private:
            try:
                token = self._token_from_conn[connection]
            
            except KeyError as exc:
                raise UnknownToken(
                    'Must register app token before creating private objects.'
                ) from exc
                
            else:
                logger.debug(
                    'Creating private object for ' + str(connection) +
                    '; bypassing distribution.'
                )
                self._private_by_ghid[ghid] = token
            
        # Otherwise, make sure to notify any other interested parties.
        else:
            make_background_future(
                self.distribute_share(
                    ghid,
                    origin = None,
                    skip_conn = connection
                )
            )
            
    @register_object.fixture
    async def register_object(self, connection, ghid, private):
        ''' Just don't distribute the object.
        '''
        if private:
            try:
                token = self._token_from_conn[connection]
            
            except KeyError as exc:
                raise UnknownToken(
                    'Must register app token before creating private objects.'
                ) from exc
                
            else:
                logger.debug(
                    'Creating private object for ' + str(connection) +
                    '; bypassing distribution.'
                )
                self._private_by_ghid[ghid] = token
            
    @public_api
    async def register_startup(self, connection, ghid):
        ''' Registers a ghid to be used as a startup object for token.
        '''
        try:
            token = self._token_from_conn[connection]
            
        except KeyError as exc:
            raise UnknownToken(
                'Must register app token before registering startup objects.'
            ) from exc
            
        else:
            if token in self._startup_by_token:
                raise ValueError(
                    'Startup object already defined for that application. '
                    'Deregister it before registering a new startup object.'
                )
            else:
                self._startup_by_token[token] = ghid
                await self._account.flush()
                
    @public_api
    async def deregister_startup(self, connection):
        ''' Deregisters a ghid to be used as a startup object for token.
        '''
        try:
            token = self._token_from_conn[connection]
            
        except KeyError as exc:
            raise UnknownToken(
                'Must register app token before deregistering startup objects.'
            ) from exc
            
        else:
            if token not in self._startup_by_token:
                raise ValueError(
                    'Startup object has not been defined for that application.'
                )
            else:
                del self._startup_by_token[token]
                await self._account.flush()
    
    @public_api
    async def get_startup_obj(self, token):
        ''' Returns the ghid of the declared startup object for that
        token, or None if none has been declared.
        '''
        if token not in self._all_known_tokens:
            raise UnknownToken()
        
        elif token not in self._startup_by_token:
            return None
        
        else:
            return self._startup_by_token[token]
    
    @fixture_noop
    @public_api
    async def distribute_share(self, ghid, origin, skip_conn=None):
        ''' Perform an actual share distribution.
        
        Pass an explicit None as the origin to indicate self as origin.
        '''
        callsheet = set()
        
        # Note that this is called in exactly two situations: when creating a
        # new object locally, through register_object, or when receiving a new
        # share, through rolodex. As such, this will never be called when the
        # object is private. So, don't check for a private owner.
        
        obj = await self._oracle.get_object(
            gaoclass = _Dispatchable,
            ghid = ghid,
            api_id = None,  # Let _pull() apply this.
            state = None,   # Let _pull() apply this.
            dispatch = self,
            ipc_protocol = self._ipc_protocol,
            account = self._account
        )
        
        callsheet.update(
            # Get any connections that have registered the api_id
            self._conns_from_api.get_any(obj.api_id)
        )
        callsheet.update(
            # Get any connections that have an instance of the object
            self._update_listeners.get_any(obj.ghid)
        )
            
        # Now, check to see if we have anything in the callsheet. Do it before
        # discarding, so that we know if it's actually an orphan share.
        if callsheet:
            # Discard the skipped connection, if one is defined
            callsheet.discard(skip_conn)
            str_origin = origin or 'self'
            logger.debug(
                'Distributing ' + str(ghid) + ' shared object from ' +
                str(str_origin) + ' to ' + repr(callsheet)
            )
            
            # If we still have a callsheet, distribute it.
            if callsheet:
                await self._distribute(
                    self._ipc_protocol.share_obj,    # distr_coro
                    callsheet,
                    ghid,
                    origin,
                    obj.api_id
                )
        
        # Only make an orphan_shares entry if we didn't create the object.
        elif origin is not None:
            sharelog = _ShareLog(ghid, origin)
            self._orphan_incoming_shares.add(sharelog)
            await self._account.flush()
    
    @fixture_noop
    @public_api
    async def distribute_update(self, ghid, deleted=False, skip_conn=None):
        ''' Perform an actual update distribution.
        '''
        # Get any connections that have an instance of the object
        callsheet = set()
        # Note that this call returns a frozenset, hence the copy.
        callsheet.update(self._update_listeners.get_any(ghid))
        # Skip the connection if one is passed.
        callsheet.discard(skip_conn)
        
        logger.debug(
            'Distributing {!s} update to {!r}.'.format(ghid, callsheet)
        )
        
        if deleted:
            await self._distribute(
                self._ipc_protocol.delete_obj,   # distr_coro
                callsheet,
                ghid
            )
            
        else:
            await self._distribute(
                self._ipc_protocol.update_obj,   # distr_coro
                callsheet,
                ghid
            )
            
    @fixture_noop
    @public_api
    async def distribute_share_success(self, ghid, recipient, tokens):
        ''' Notify any available connections for all passed <tokens> of
        a share failure.
        '''
        callsheet = set()
        for token in tokens:
            # Escape any keys that have gone missing during the rat race
            try:
                callsheet.add(self._conn_from_token[token])
            except KeyError:
                logger.info('No connection for token ' + str(token))
        
        # Distribute the share failure to all apps that requested its delivery
        await self._distribute(
            self._ipc_protocol.notify_share_success,   # distr_coro
            callsheet,
            ghid,
            recipient
        )
    
    @fixture_noop
    @public_api
    async def distribute_share_failure(self, ghid, recipient, tokens):
        ''' Notify any available connections for all passed <tokens> of
        a share failure.
        '''
        callsheet = set()
        for token in tokens:
            # Escape any keys that have gone missing during the rat race
            try:
                callsheet.add(self._conn_from_token[token])
            except KeyError:
                logger.info('No connection for token ' + str(token))
        
        # Distribute the share failure to all apps that requested its delivery
        await self._distribute(
            self._ipc_protocol.notify_share_failure,   # distr_coro
            callsheet,
            ghid,
            recipient
        )
                
    async def _distribute(self, distr_coro, callsheet, *args, **kwargs):
        ''' Call distr_coro with *args and **kwargs at each of the
        connections in the callsheet. Log (but don't raise) any errors.
        '''
        logger.info(
            'Dispatching ' + str(len(callsheet)) + ' calls to ' +
            str(distr_coro)
        )
        distributions = set()
        for connection in callsheet:
            # For each connection...
            distributions.add(
                # ...in parallel, schedule a single execution of the
                # distribution coroutine.
                make_background_future(distr_coro(connection, *args, **kwargs))
            )
            
        # And gather the results, logging (but not raising) any exceptions.
        # BUT, only do it if we have some to do.
        if distributions:
            try:
                await asyncio.wait(
                    fs = distributions,
                    return_when = asyncio.ALL_COMPLETED
                )
            
            except asyncio.CancelledError:
                logger.debug('Cancelling pending distributions.')
                for distr in distributions:
                    distr.cancel()
                raise class _Dispatchable(GAOCore, metaclass=API):
    ''' A dispatchable object. Note that privacy is store within the
    dispatcher itself, and not within the individual dispatchables.
    '''
    _dispatch = weak_property('__dispatch')
    _account = weak_property('__account')
    _ipc_protocol = weak_property('__ipc_protocol')
    api_id = immutable_property('_api_id')
    
    def __init__(self, *args, api_id, state, dispatch, ipc_protocol, account,
                 **kwargs):
        super().__init__(*args, **kwargs)
        
        self._dispatch = dispatch
        self._ipc_protocol = ipc_protocol
        self._account = account
        
        # These may be explicitly set to None to allow pull() to apply them.
        if state is not None:
            self.state = state
            
        if api_id is not None:
            self.api_id = api_id
            
    @public_api
    async def _push(self):
        ''' Extend GAOCore._push() to flush the account upon completion.
        '''
        await super()._push()
        await self._account.flush()
        
    @_push.fixture
    async def _push(self):
        ''' Call super() to FIXTURE super instead of dispatchable.
        '''
        await super(_Dispatchable, self)._push()
    
    @public_api
    async def _pull(self):
        ''' Extend GAOCore._pull() to notify IPC about updates.
        '''
        await super()._pull()
        make_background_future(
            self._dispatch.distribute_update(self.ghid)
        )
        
    @_pull.fixture
    async def _pull(self):
        ''' Call super() to FIXTURE super instead of dispatchable.
        '''
        await super(_Dispatchable, self)._pull()
        
    async def pack_gao(self):
        ''' Packs state into a bytes object. May be overwritten in subs
        to pack more complex objects. Should always be a staticmethod or
        classmethod.
        '''
        version = b'\x00'
        return b'hgxd' + version + bytes(self.api_id) + self.state
        
    async def unpack_gao(self, packed):
        ''' Unpacks state from a bytes object. May be overwritten in
        subs to unpack more complex objects. Should always be a
        staticmethod or classmethod.
        '''
        magic = packed[0:4]
        version = packed[4:5]
        api_id = ApiID.from_bytes(packed[5:70])
        
        if magic != b'hgxd':
            raise DispatchError('Object does not appear to be dispatchable.')
        elif version != b'\x00':
            raise DispatchError('Incompatible dispatchable version number.')
        elif getattr(self, 'api_id', None) is None:
            self.api_id = api_id
        else:
            # YES, this does need to be _api_id (with underscore!) because
            # we may need to override something existing.
            self._api_id = api_id
        # Checking for this from other people isn't really that useful. We'll
        # already error out if you try to change it locally, and if someone
        # goes to the effort of changing it elsewhere, well, more power to them
        # I guess.
        # elif api_id != self.api_id:
        #     raise DispatchError('Cannot change API ID.')
            
        self.state = packed[70:]
    
    @fixture_noop
    @public_api
    async def apply_delete(self, debinding):
        ''' Extend GAOCore.apply_delete to notify consumers of deletion.
        '''
        await super().apply_delete(debinding)
        make_background_future(
            self._dispatch.distribute_update(self.ghid, deleted=True)
        )
        
    def __eq__(self, other):
        ''' Ensure the other object is the same gao with the same state.
        Intended pretty much exclusively for use in testing.
        '''
        equal = True
        
        try:
            equal &= (self.dynamic == other.dynamic)
            equal &= (self.ghid == other.ghid)
            equal &= (self.author == other.author)
            equal &= (self.state == other.state)
            equal &= (self.api_id == other.api_id)
            
        except AttributeError:
            equal = False
        
        return equal
        
    def __hash__(self):
        ''' Dispatchable hashes should mix the hashes of the api_id, the
        author, and the ghid together, but only if all of the above are
        defined.
        '''
        defined = self.ghid is not None
        defined &= self.author is not None
        defined &= self.api_id is not None
        
        if not defined:
            raise TypeError(
                'Dispatchable objects may only be hashed after they are ' +
                'fully defined (with a ghid, an author, and an api_id.'
            )
        
        return hash(self.ghid) ^ hash(self.author) ^ hash(self.api_id) class UnknownToken(DispatchError, ValueError):
    ''' Raised when something fails with dispatch.
    '''
    pass class ExistantAppError(DispatchError):
    ''' Raised when attempting to register a second application for the
    same connection, or the same token for a different connection.
    '''
import unittest
import logging
import loopa
import random
import gc
from loopa import NoopLoop
from loopa.utils import await_coroutine_threadsafe
from queue import Empty
from golix import Ghid
from hypergolix.utils import ApiID
from hypergolix.utils import AppToken
from hypergolix.accounting import Account
from hypergolix.comms import _ConnectionBase
from hypergolix.core import Oracle
from hypergolix.ipc import IPCServerProtocol
from hypergolix.dispatch import Dispatcher
from hypergolix.dispatch import _Dispatchable
from hypergolix.exceptions import UnknownToken
from hypergolix.exceptions import ExistantAppError
from _fixtures.ghidutils import make_random_ghid
from _fixtures.identities import TEST_AGENT1
    from hypergolix import logutils
                loop = self.nooploop._loop
            )
            
        # Finally, delete the original connection, and re-register the existing
        # token under the new connection.
        del conn
        gc.collect()
        token2 = await_coroutine_threadsafe(
            coro = self.dispatch.start_application(conn2, token),
            loop = self.nooploop._loop
        )
        self.assertEqual(token, token2)
        
    def test_obj_tracking(self):
        ''' Test tracking and untracking an object for an application.
        '''
        # Create a new connection
        conn = _ConnectionBase.__fixture__()
        obj1 = _Dispatchable.__fixture__(
            ghid = make_random_ghid(),
            dynamic = True,
            author = make_random_ghid(),
            legroom = 7,
            api_id = ApiID(bytes([random.randint(0, 255) for i in range(64)])),
            state = bytes([random.randint(0, 255) for i in range(4)]),
            golcore = self.golcore,
            ghidproxy = self.ghidproxy,
            privateer = self.privateer,
            percore = self.percore,
            librarian = self.librarian,
            dispatch = self.dispatch,
            ipc_protocol = self.ipc_protocol,
            account = self.account
        )
        self.oracle.add_object(obj1.ghid, obj1)
        obj2 = _Dispatchable.__fixture__(
            ghid = make_random_ghid(),
            dynamic = True,
            author = make_random_ghid(),
            legroom = 7,
            api_id = ApiID(bytes([random.randint(0, 255) for i in range(64)])),
            state = bytes([random.randint(0, 255) for i in range(4)]),
            golcore = self.golcore,
            ghidproxy = self.ghidproxy,
            privateer = self.privateer,
            percore = self.percore,
            librarian = self.librarian,
            dispatch = self.dispatch,
            ipc_protocol = self.ipc_protocol,
            account = self.account
        )
        self.oracle.add_object(obj2.ghid, obj2)
        
        await_coroutine_threadsafe(
            coro = self.dispatch.track_object(conn, obj1.ghid),
            loop = self.nooploop._loop
        )
        
        await_coroutine_threadsafe(
            coro = self.dispatch.untrack_object(conn, obj1.ghid),
            loop = self.nooploop._loop
        )
        await_coroutine_threadsafe(
            coro = self.dispatch.untrack_object(conn, obj2.ghid),
            loop = self.nooploop._loop
        )
        
    def test_obj_registration_nonprivate(self):
        ''' Test registering a new, non-private object.
        '''
        # Create a new connection and object fixture
        conn = _ConnectionBase.__fixture__()
        obj = _Dispatchable.__fixture__(
            ghid = make_random_ghid(),
            dynamic = True,
            author = make_random_ghid(),
            legroom = 7,
            api_id = ApiID(bytes([random.randint(0, 255) for i in range(64)])),
            state = bytes([random.randint(0, 255) for i in range(4)]),
            golcore = self.golcore,
            ghidproxy = self.ghidproxy,
            privateer = self.privateer,
            percore = self.percore,
            librarian = self.librarian,
            dispatch = self.dispatch,
            ipc_protocol = self.ipc_protocol,
            account = self.account
        )
        self.oracle.add_object(obj.ghid, obj)
        
        # Register a non-private object first
        await_coroutine_threadsafe(
            coro = self.dispatch.register_object(conn, obj.ghid, False),
            loop = self.nooploop._loop
        )
        
    def test_obj_registration_private(self):
        ''' Test registering a new, private object.
        '''
        # Create a new connection and object fixture
        conn = _ConnectionBase.__fixture__()
        obj = _Dispatchable.__fixture__(
            ghid = make_random_ghid(),
            dynamic = True,
            author = make_random_ghid(),
            legroom = 7,
            api_id = ApiID(bytes([random.randint(0, 255) for i in range(64)])),
            state = bytes([random.randint(0, 255) for i in range(4)]),
            golcore = self.golcore,
            ghidproxy = self.ghidproxy,
            privateer = self.privateer,
            percore = self.percore,
            librarian = self.librarian,
            dispatch = self.dispatch,
            ipc_protocol = self.ipc_protocol,
            account = self.account
        )
        self.oracle.add_object(obj.ghid, obj)
        
        # With no token, this should raise.

        with self.assertRaises(UnknownToken):