#!python
"""
<Program Name>
  coordinationaffix.r2py

<Author>
  Danny Y. Huang, yh1@cs.williams.edu
  Monzur Muhammad, monzum@cs.washington.edu

<Date Started>
  March 24, 2011

<Purpose>
  Advertises the server's string representation of its affix stack. When a client
  is about to connect to this server, the coordination affix looks up the string
  representation and constructs the same affixs. At this point, both the server
  and the client have the same affixs. As their affix stacks are balanced, the
  connection is established and ready to be returned to the application.

  The advertisement key for a server is "host,port,protocol", where host is the
  hostname of the server and protocol can be either UDP or TCP. The
  advertisement value is the string representation of the CoordinationAffix's
  affix stack.

  If a zenodotus name is provided to listenforconn, this affix will also
  advertise the mapping of the name to the current IP address of the host. Any
  change in the IP address will cause the mapping to be updated, but unless used
  along with the mobility affix, the listener will not be able to accept new
  connections because it is bound to the old IP address.

  The coordination affix should always sit on top of an application's affix stack,
  so that it can see the content of the rest of the stack.

"""

dy_import_module_symbols("baseaffix.r2py")
dy_import_module_symbols("affix_exceptions.r2py")
dy_import_module_symbols("affixstackinterface.r2py")
cachedadvertise = dy_import_module("cachedadvertise.r2py")
advertisepipe = dy_import_module("advertisepipe.r2py")

# Whether we want to print debug statements.
_COORDINATION_AFFIX_DEBUG_MODE = False

zenodotus_subdomain = 'zenodotus.poly.edu'

class CoordinationAffix(BaseAffix):

  # A dictionary that maps a server socket object to the corresponding
  # advertisement handle. We need to save all the handles that are created as
  # the listener starts, so that, when the server socket's close() is called, we
  # can retrieve these handles and stop the advertisement.
  _adv_handle_dict = {}

  # Protects the dictionary above from concurrent modification.
  _adv_handle_dict_lock = createlock()


  def __init__(self, next_affix, optional_args=None):

    # If an optional argument is provided, ensure that it
    # is a string then use it as the localhost. If an 
    # argument is not provided, the we will use the local
    # ip address as the localhost.
    if optional_args:
      self.localhost = optional_args[0] 
      assert(isinstance(self.localhost, str))
    else:
      self.localhost = None
        


    # Initialize the CoordinationAffix.
    BaseAffix.__init__(self, next_affix, optional_args)


  def copy(self):
    if self.affix_context['next_affix']:
      next_affix_copy = self.affix_context['next_affix'].copy()
    else:
      next_affix_copy = None
    return CoordinationAffix(next_affix_copy, self.affix_context['optional_args'])



  def get_advertisement_string(self):
    return '(CoordinationAffix)' + self.peek().get_advertisement_string()



  # ===================================================================
  # TCP
  # ===================================================================



  def listenforconnection(self, localhost, localport):
    """
    <Purpose>
      Listens for incoming connections. Advertises the affix stack when the
      listener is ready.

    <Arguments>
      Same as the network API in repy v2.
    <Side Effects>
      See the documentation of AdvertiseObjects.

    <Exceptions>
      Same as the network API in repy v2.    

    <Returns>      
      Same as the network API in repy v2.    

    """
    if _COORDINATION_AFFIX_DEBUG_MODE:
      log(self, "Coordination is about to start listening on '%s:%d'\n" % (localhost, localport))
    # If we are listening on a zenodotus name, translate it to the local IP
    if localhost.endswith(zenodotus_subdomain):
      try:
        localip = getmyip()
      except InternetConnectivityError:
        raise AddressBindingError('Coordination Affix: Not connected to the network.')

    else:
      localip = localhost

    # Start listening so we have a complete view of the affix stack below us.
    # We try up to 5 times as we want to give some time for the zenodotus name
    # to establish, if user is using it for localhost.
    exceptions_list = []
    for i in range(5):
      try:
        tcpserversocket = self.peek().listenforconnection(localhost, localport)
        break
      except (AddressBindingError, RepyArgumentError), e:
        exceptions_list.append((self, type(e), str(e)))
        sleep(1)
    else:
      raise AddressBindingError("Unable to bind with the localhost " + 
        str(localhost) + " (IP " + str(localip) + 
        ") provided. Exceptions I saw: " + str(exceptions_list))

    if _COORDINATION_AFFIX_DEBUG_MODE:
      log(self, "CoordinationAffix started listening socket on '%s:%d'\n" % (localip, localport))

    # The underlying affix stack has been built completely. We can advertise the
    # affix stack.
    if self.localhost:
      hostname = self.localhost
    else:
      hostname = localhost

    adv_key = str(hostname) + ',' + str(localport) + ',TCP'
    adv_value = self.peek().get_advertisement_string()
    if _COORDINATION_AFFIX_DEBUG_MODE:
      log(self, 'CoordinationAffix: Advertised', adv_key, adv_value, '\n')
    adv_handle = advertisepipe.add_to_pipe(adv_key, adv_value)

    # We need to save the advertisement handle to a global dictionary, so that
    # we can keep a reference to the handle. Later on, when we stop listening,
    # we can conveniently remove this key value pair from the advertisement
    # service by using this handle.
    if isinstance(tcpserversocket, AffixTCPServerSocket):
      adv_handle_key = repr(tcpserversocket._socket)
    else:
      adv_handle_key = repr(tcpserversocket)

    self._adv_handle_dict_lock.acquire(True)
    self._adv_handle_dict[adv_handle_key] = adv_handle
    self._adv_handle_dict_lock.release()

    return AffixTCPServerSocket(tcpserversocket, self)
    
    


  def tcpserversocket_close(self, tcpserversocket):
    """
    Stops listening and stops advertising the server.

    """
    close_result = self.peek().tcpserversocket_close(tcpserversocket)

    # Now that we have closed the listener without any exceptions, we can safely
    # stop the advertisement.
    self._stop_advertisement(tcpserversocket)

    return close_result




  def openconnection(self, desthost, destport, localip, localport, timeout):
    """
    <Purpose>
      Looks up the string representation of the server's affix stack. Constructs
      the same affix stack and connects to the server. If the server's affix stack
      is not found or invalid, connects without using affixs for backward
      compatibility.

    <Arguments>
      Same as the network API in repy v2.

    <Side Effects>
      See the documentation of AdvertiseObjects.

    <Exceptions>
      Same as the network API in repy v2.    

    <Returns>      
      Same as the network API in repy v2.    

    """
    # Keep track of how much time we have spent to avoid exceeding the timeout.
    start_time = getruntime()


    # If we are provided a zenodotus name, we will convert it to an IP
    # address.
    if desthost.endswith(zenodotus_subdomain):
      # If the destinationa address ends with zenodotus,
      # use the vaue we added to the advertisepipe before.
      try:
        destip = cachedadvertise.lookup(desthost, timeout=0)[0]
      except (NetworkAddressError, IndexError):
        # We catch an indexerror as there is a possibility that
        # the advertise lookup returns an empty list.
        # In this situation we just set the destination IP as 
        # the hostname. Note that Affixes attempts to do a 
        # name lookup before it does a listenforconnection or
        # openconnection.
        destip = desthost
    else:
      destip = desthost


    # Look up the server's affix stack under this key.
    server_key = str(desthost) + ',' + str(destport) + ',TCP'
    lookup_result_list = cachedadvertise.lookup(server_key)

    if _COORDINATION_AFFIX_DEBUG_MODE:
      log(self, "Openconnection from", localip, localport, "to", server_key, " via " + str(lookup_result_list) + "\n")

    # Reverse the results as the latest affix advertisement always comes last.
    lookup_result_list.reverse()
    # We remember all the affixs that fail to connect, so that we can piggyback
    # this error report along with TimeoutError.
    failed_affix_str = ''

    # Find the localhost of the node.
    localhost = self.localhost

    # We go through all the possible affix stack strings advertised. The list
    # will most likely contain only one value, but it is possible that a affix
    # may advertise two different values under the same key because its affix
    # stack had somehow changed.
    for server_affix_stack_str in lookup_result_list:
      # Build my affix stack and store it in the affix_context.
      self.affix_context['next_affix'] = AffixStack(server_affix_stack_str, localhost).affix_stack_context['top_affix']
      if _COORDINATION_AFFIX_DEBUG_MODE:
        log(self, 'CoordinationAffix: Trying affix stack for openconnection: ', self.peek(), '\n')
      # Check if we have exceeded the time limit.
      time_taken = getruntime() - start_time
      if time_taken > timeout:
        raise TimeoutError("CoordinationAffix: We have spent too much time looking up the server's affix stack.")

      # Since we spent some time contacting the advertisement service, we have
      # less time for the actual openconn.
      if _COORDINATION_AFFIX_DEBUG_MODE:
        log(self, "Using affix string: " + self.peek().get_advertisement_string() + "to connect to '" +
            destip + ":" + str(destport) + "'\n")

      try:
        sock = self.peek().openconnection(desthost, destport, localip, localport, timeout - time_taken)
        return AffixSocket(sock, self.copy())

      # We ignore the error here, as we may have been contacting a legacy
      # server. Later, we're going to retry without affixs.
      except (ConnectionRefusedError, TimeoutError), err:
        # If we receive either the connection refused error or Timeouterror
        # we store the error message.
        failed_affix_str += server_affix_stack_str + '[' + str(err) + ']; '
        failed_affix_str += "Error while connecting to '%s:%d' from '%s:%d'." % (desthost, destport, localip, localport)

    # It is possible we're contacting a legacy server that doesn't use affixs,
    # because either it doesn't advertise itself, or none of the advertised affix
    # stacks works. Thus, we contact it without affixs.
    time_taken = getruntime() - start_time
    if time_taken < timeout or not lookup_result_list:
      empty_affix_stack = AffixStackInterface()
      sock = empty_affix_stack.openconnection(destip, destport, localip, localport, timeout - time_taken)
      return AffixSocket(sock, self.copy())

    # Piggyback the failure information on the TimeoutError.
    else:
      raise TimeoutError("CoordinationAffix: Failed to connect with affixs - " + failed_affix_str)


  # ===================================================================
  # UDP
  # ===================================================================


  def listenformessage(self, localhost, localport):
    """
    <Purpose>
      Listens for incoming connections. Advertises the affix stack when the
      listener is ready.

    <Arguments>
      Same as the network API in repy v2.

    <Side Effects>
      See the documentation of AdvertiseObjects.

    <Exceptions>
      Same as the network API in repy v2.    

    <Returns>      
      Same as the network API in repy v2.    

    """
    # If we are listening on a zenodotus name, first translate it to the local
    # IP and add the zenodotus name -> IP mapping to the advertisepipe.
    if localhost.endswith(zenodotus_subdomain):
      try:
        localip = getmyip()
      except InternetConnectivityError:
        raise AddressBindingError('Coordination Affix: Not connected to the network.')
      adv_handle = advertisepipe.add_to_pipe(localhost, localip)
      if _COORDINATION_AFFIX_DEBUG_MODE:
        log(self, 'CoordinationAffix: Added', localhost, localip, 
          'to advertisepipe.\n')

      # We need to save the advertisement handle to a global dictionary, so that
      # we can keep a reference to the handle. Later on, when we stop listening,
      # we can conveniently remove this key value pair from the advertisement
      # service by using this handle.
      if isinstance(tcpserversocket, AffixTCPServerSocket):
        adv_handle_key = repr(tcpserversocket._socket)
      else:
        adv_handle_key = repr(tcpserversocket)

      self._adv_handle_dict_lock.acquire(True)
      self._adv_handle_dict[adv_handle_key] = adv_handle
      self._adv_handle_dict_lock.release()

    else:
      localip = localhost

    if _COORDINATION_AFFIX_DEBUG_MODE:
      log(self, "Coordination is about to start UDP server socket on '%s:%d'\n" % (localhost, localport))

    # Start listening so we have a complete view of the affix stack below us.
    udpserversocket = self.peek().listenformessage(localip, localport)

    # The underlying affix stack has been built completely. We can advertise the
    # affix stack.
    adv_key = str(localip) + ',' + str(localport) + ',UDP'
    adv_value = self.peek().get_advertisement_string()
    adv_handle = advertisepipe.add_to_pipe(adv_key, adv_value)
 

    if _COORDINATION_AFFIX_DEBUG_MODE:
      log(self, 'CoordinationAffix: Advertised', adv_key, adv_value, '\n')

    # We need to save the advertisement handle to a global dictionary, so that
    # we can keep a reference to the handle. Later on, when we stop listening,
    # we can conveniently remove this key value pair from the advertisement
    # service by using this handle.
    self._adv_handle_dict_lock.acquire(True)
    self._adv_handle_dict[repr(udpserversocket)] = adv_handle
    self._adv_handle_dict_lock.release()

    return AffixUDPServerSocket(udpserversocket, self)



  def udpserversocket_close(self, udpserversocket):
    """
    Stops listening and stops advertising the server.

    """
    close_result = self.peek().udpserversocket_close(udpserversocket)

    # Now that we have closed the listener without any exceptions, we can safely
    # stop the advertisement.
    self._stop_advertisement(udpserversocket)

    return close_result



  def sendmessage(self, destip, destport, message, localip, localport):
    """
    <Purpose>
      Looks up the string representation of the server's affix stack. Constructs
      the same affix stack and connects to the server. If the server's affix stack
      is not found, connects without using affixs for backward compatibility.

    <Arguments>
      Same as the network API in repy v2.

    <Side Effects>
      See the documentation of AdvertiseObjects.

    <Exceptions>
      Same as the network API in repy v2.    

    <Returns>      
      Same as the network API in repy v2.    

    """
    # Key under which we look up the server's affix stack
    server_key = str(destip) + ',' + str(destport) + ',UDP'

    # Now we obtain the server's affix stack string representation from which we
    # will construct the client's affix stack. 
    lookup_result_list = cachedadvertise.lookup(server_key)


    if lookup_result_list:

      # We asusme that the last result in the list returned contains the most
      # up-to-date server affix stack string. We build a new affix stack based on it.
      server_affix_stack_str = lookup_result_list.pop()
      localhost = self.localhost
      self.affix_context['affix_stack'] = AffixStack(server_affix_stack_str, localhost)
      
      # Send the message using the new affix stack.
      return self.peek().sendmessage(destip, destport, message, localip, localport)

    else:

      # It is possible we're contacting a legacy server that doesn't use
      # affixs. Thus, we contact it without affixs.
      return sendmessage(destip, destport, message, localip, localport)




  def _stop_advertisement(self, serversocket):
    """
    Stops advertising on the supplied serversocket, which can be either a
    UDPServerSocket or TCPServerSocket. Helper method for the close methods of
    both sockets.

    """
    # Retrieve the advertisement handle associated with the current socket and
    # stop the advertisement.
    try:

      self._adv_handle_dict_lock.acquire(True)
      adv_handle = self._adv_handle_dict.pop(repr(serversocket))

    # Cannot find the handle, so we must have removed it when the close() method
    # was previously invoked. We quietly ignore the error.
    except KeyError:
      pass

    # Stop advertising both our affix stack and the zenodotus name -> IP mapping.
    else:
      advertisepipe.remove_from_pipe(adv_handle)

    finally:
      self._adv_handle_dict_lock.release()

