text: #!/usr/bin/env python
  # -- Content-Encoding: UTF-8 --
  """
  Multicast sockets utility module
  
  :author: Thomas Calmant
  :license: Apache Software License 2.0
  
  ..
  
      Copyright 2014 isandlaTech
  
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
      You may obtain a copy of the License at
  
          http://www.apache.org/licenses/LICENSE-2.0
  
      Unless required by applicable law or agreed to in writing, software
      distributed under the License is distributed on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      See the License for the specific language governing permissions and
      limitations under the License.
  """
  
  # Documentation strings format
  __docformat__ = "restructuredtext en"
  
  # Boot module version
  __version__ = "1.0.0"
  
  # ------------------------------------------------------------------------------
  
  # Pelix utilities
  from pelix.utilities import to_bytes
  
  # Standard library
  import logging
  import os
  import socket
  import struct
  
  # ------------------------------------------------------------------------------
  
  _logger = logging.getLogger(__name__)
  
  # ------------------------------------------------------------------------------
  
  if os.name == "nt":
      # Windows Specific code
      def pton(family, address):
          """
          Calls inet_pton
  
          :param family: Socket family
          :param address: A string address
          :return: The binary form of the given address
          """
          if family == socket.AF_INET:
              return socket.inet_aton(address)
  
          elif family == socket.AF_INET6:
              # Do it using WinSocks
              import ctypes
              winsock = ctypes.windll.ws2_32
  
              # Prepare structure
              class sockaddr_in6(ctypes.Structure):
                  """
                  Definition of the C structure sockaddr_in6
                  """
                  # pylint: disable=C0103
                  _fields_ = [("sin6_family", ctypes.c_short),
                              ("sin6_port", ctypes.c_ushort),
                              ("sin6_flowinfo", ctypes.c_ulong),
                              ("sin6_addr", ctypes.c_ubyte * 16),
                              ("sin6_scope_id", ctypes.c_ulong)]
  
              # Prepare pointers
              addr_ptr = ctypes.c_char_p(to_bytes(address))
  
              out_address = sockaddr_in6()
              size = len(sockaddr_in6)
              size_ptr = ctypes.pointer(size)
  
              # Second call
              winsock.WSAStringToAddressA(addr_ptr, family, 0,
                                          out_address, size_ptr)
  
              # Convert the array...
              bin_addr = 0
              for part in out_address.sin6_addr:
                  bin_addr = bin_addr * 16 + part
  
              return bin_addr
  
          else:
              raise ValueError("Unhandled socket family: {0}".format(family))
  
  else:
      # Other systems
      def pton(family, address):
          """
          Calls inet_pton
  
          :param family: Socket family
          :param address: A string address
          :return: The binary form of the given address
          """
          return socket.inet_pton(family, address)
  
  # ------------------------------------------------------------------------------
  
  
  def make_mreq(family, address):
      """
      Makes a mreq structure object for the given address and socket family.
  
      :param family: A socket family (AF_INET or AF_INET6)
      :param address: A multicast address (group)
      :raise ValueError: Invalid family or address
      """
      if not address:
          raise ValueError("Empty address")
  
      # Convert the address to a binary form
      group_bin = pton(family, address)
  
      if family == socket.AF_INET:
          # IPv4
          # struct ip_mreq
          # {
          #     struct in_addr imr_multiaddr; /* IP multicast address of group */
          #     struct in_addr imr_interface; /* local IP address of interface */
          # };
          # "=I" : Native order, standard size unsigned int
          return group_bin + struct.pack("=I", socket.INADDR_ANY)
  
      elif family == socket.AF_INET6:
          # IPv6
          # struct ipv6_mreq {
          #    struct in6_addr ipv6mr_multiaddr;
          #    unsigned int    ipv6mr_interface;
          # };
          # "@I" : Native order, native size unsigned int
          return group_bin + struct.pack("@I", 0)
  
      raise ValueError("Unknown family {0}".format(family))
  
  # ------------------------------------------------------------------------------
  
  
  def create_multicast_socket(address, port, join=True):
      """
      Creates a multicast socket according to the given address and port.
      Handles both IPv4 and IPv6 addresses.
  
      :param address: Multicast address/group
      :param port: Socket port
      :param join: If False, the socket is not bound and does not join the
                   multicast group (creates a simple UDP socket)
      :return: A tuple (socket, listening address)
      :raise ValueError: Invalid address or port
      """
      # Get the information about a datagram (UDP) socket, of any family
      try:
          addrs_info = socket.getaddrinfo(address, port, socket.AF_UNSPEC,
                                          socket.SOCK_DGRAM)
      except socket.gaierror:
          raise ValueError("Error retrieving address information ({0}, {1})"
                           .format(address, port))
  
      if len(addrs_info) > 1:
          _logger.debug("More than one address information found. "
                        "Using the first one.")
  
      # Get the first entry : (family, socktype, proto, canonname, sockaddr)
      addr_info = addrs_info[0]
  
      # Only accept IPv4/v6 addresses
      if addr_info[0] not in (socket.AF_INET, socket.AF_INET6):
          # Unhandled address family
          raise ValueError("Unhandled socket family : %d" % (addr_info[0]))
  
      # Prepare the socket
      sock = socket.socket(addr_info[0], socket.SOCK_DGRAM)
  
      if join:
          # Reuse address
          sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
          try:
              # Special case for MacOS
              # pylint: disable=no-member
              sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
          except AttributeError:
  
              pass
  
          # Bind the socket
          if sock.family == socket.AF_INET:
              # IPv4 binding
              sock.bind(('0.0.0.0', port))
  
          else:
              # IPv6 Binding
              sock.bind(('::', port))
  
          # Prepare the mreq structure to join the group
          # addrinfo[4] = (addr,port)
          mreq = make_mreq(sock.family, addr_info[4][0])
  
          # Join the group
          if sock.family == socket.AF_INET:
              # IPv4
              sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
  
              # Allow multicast packets to get back on this host
              sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1)
  
          else:
              # IPv6
              sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
  
              # Allow multicast packets to get back on this host
              sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 1)
  
      return sock, addr_info[4][0]
  
  
  def close_multicast_socket(sock, address):
      """
      Cleans up the given multicast socket.
      Unregisters it of the multicast group.
  
      Parameters should be the result of create_multicast_socket
  
      :param sock: A multicast socket
      :param address: The multicast address used by the socket
      """
      if sock is None:
          return
  
      if address:
          # Prepare the mreq structure to join the group
          mreq = make_mreq(sock.family, address)
  
          # Quit group
          if sock.family == socket.AF_INET:
              # IPv4
              sock.setsockopt(socket.IPPROTO_IP, socket.IP_DROP_MEMBERSHIP, mreq)
  
          elif sock.family == socket.AF_INET6:
              # IPv6
              sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_LEAVE_GROUP, mreq)
  
      # Close the socket
      sock.close()
  