 #!/usr/bin/env python1.5
# =============================================================
# wsdd - Web Service Discovery Daemon
# Python 1.5.2 / NetBSD 1.4 compatibility port
#
# Original code: wsdd (c) Steffen Christgau, 2017-2025
# Port: Python 3.1 -> Python 1.5.2 (for NetBSD 1.4)
#
# Major Python 1.5.2 constraints and workarounds:
#  - no import X as Y             -> import X; Y = X
#  - no super()                   -> ParentClass.__init__(self)
#  - no property()                -> call get_xxx() methods directly
#  - no staticmethod()            -> use module-level helpers
#  - no any() / all()             -> use explicit loops
#  - no @property decorator       -> same as above
#  - no f-strings / .format()     -> use % formatting
#  - no with statement            -> try/finally + explicit acquire/release
#  - no except X as e             -> except X, e
#  - no print() function          -> print statement
#  - no ternary X if C else Y     -> use if/else statements
#  - no dict/set comprehensions   -> use loops
#  - no bytes type                -> use str for byte strings
#  - no collections.deque         -> use list
#  - no argparse/optparse         -> use getopt
#  - no uuid module               -> local implementation (md5 based)
#  - no http.server               -> BaseHTTPServer module
#  - no urllib.request            -> use httplib
#  - no urllib.parse              -> use urlparse module
#  - no xml.etree.ElementTree     -> local xmllib-based implementation
#  - no ctypes                    -> use fcntl + ioctl
#  - no platform module           -> use os.uname()
#  - no logging module            -> local lightweight logger
#  - no metaclass syntax          -> call setup from __init__
#  - some socket constants missing -> fill in manually
# =============================================================

import sys
import os
import signal
import socket
import struct
import select
import time
import random
import re
import getopt
import traceback
import string
import md5

# -------------------------------------------------------------------
# thread module (Python 1.5.2 provides 'thread')
# -------------------------------------------------------------------
try:
    import thread
    _thread_mod = thread
    _HAS_THREAD = 1
except ImportError:
    _thread_mod = None
    _HAS_THREAD = 0

# -------------------------------------------------------------------
# Python 1.5.2 standard modules
# -------------------------------------------------------------------
import SocketServer
import urllib
import urlparse
import httplib
import xmllib

# -------------------------------------------------------------------
# OS detection (no platform module)
# -------------------------------------------------------------------
def _get_system():
    try:
        return os.uname()[0]
    except Exception:
        return 'Unknown'

_SYSTEM = _get_system()

# -------------------------------------------------------------------
# Socket constant fallbacks
# -------------------------------------------------------------------
if not hasattr(socket, 'AF_ROUTE'):
    socket.AF_ROUTE = 17
if not hasattr(socket, 'AF_LINK'):
    socket.AF_LINK = 18
if not hasattr(socket, 'AF_UNSPEC'):
    socket.AF_UNSPEC = 0
if not hasattr(socket, 'AF_INET6'):
    socket.AF_INET6 = 24
if not hasattr(socket, 'IPPROTO_IPV6'):
    socket.IPPROTO_IPV6 = 41
if not hasattr(socket, 'IPV6_JOIN_GROUP'):
    socket.IPV6_JOIN_GROUP = 12
if not hasattr(socket, 'IPV6_V6ONLY'):
    socket.IPV6_V6ONLY = 27
if not hasattr(socket, 'IPV6_MULTICAST_LOOP'):
    socket.IPV6_MULTICAST_LOOP = 19
if not hasattr(socket, 'IPV6_MULTICAST_HOPS'):
    socket.IPV6_MULTICAST_HOPS = 18
if not hasattr(socket, 'IPV6_MULTICAST_IF'):
    socket.IPV6_MULTICAST_IF = 17
if not hasattr(socket, 'IP_ADD_MEMBERSHIP'):
    socket.IP_ADD_MEMBERSHIP = 12
if not hasattr(socket, 'IP_MULTICAST_IF'):
    socket.IP_MULTICAST_IF = 9
if not hasattr(socket, 'IP_MULTICAST_TTL'):
    socket.IP_MULTICAST_TTL = 10
if not hasattr(socket, 'IP_MULTICAST_LOOP'):
    socket.IP_MULTICAST_LOOP = 11
if not hasattr(socket, 'IPPROTO_IP'):
    socket.IPPROTO_IP = 0
if not hasattr(socket, 'SOL_SOCKET'):
    socket.SOL_SOCKET = 0xffff
if not hasattr(socket, 'SO_REUSEADDR'):
    socket.SO_REUSEADDR = 4
if not hasattr(socket, 'AF_UNIX'):
    socket.AF_UNIX = 1
if not hasattr(socket, 'SOCK_RAW'):
    socket.SOCK_RAW = 3

# inet_pton / inet_ntop replacements
def _inet_aton_impl(addr):
    """Replacement for socket.inet_aton when missing."""
    parts = string.split(addr, '.')
    if len(parts) != 4:
        raise ValueError('illegal IP address: %s' % addr)
    return struct.pack('BBBB',
        string.atoi(parts[0]), string.atoi(parts[1]),
        string.atoi(parts[2]), string.atoi(parts[3]))

def _inet_ntoa_impl(packed):
    """Replacement for socket.inet_ntoa."""
    a, b, c, d = struct.unpack('BBBB', packed)
    return '%d.%d.%d.%d' % (a, b, c, d)

if not hasattr(socket, 'inet_aton') or 1:
    # inet_aton may be broken on NetBSD 1.4 / Python 1.5.2
    try:
        socket.inet_aton('1.2.3.4')  # test
    except Exception:
        socket.inet_aton = _inet_aton_impl
        socket.inet_ntoa = _inet_ntoa_impl

if not hasattr(socket, 'inet_pton'):
    def _inet_pton(af, addr):
        if af == socket.AF_INET:
            return _inet_aton_impl(addr)
        raise os.error('inet_pton: AF_INET6 not supported on this platform')
    socket.inet_pton = _inet_pton

if not hasattr(socket, 'inet_ntop'):
    def _inet_ntop(af, packed):
        if af == socket.AF_INET:
            return _inet_ntoa_impl(packed)
        raise os.error('inet_ntop: AF_INET6 not supported on this platform')
    socket.inet_ntop = _inet_ntop

# if_nametoindex implemented via ioctl(SIOCGIFINDEX)
if not hasattr(socket, 'if_nametoindex'):
    try:
        import fcntl
        _SIOCGIFINDEX = 0xC0206909  # NetBSD/FreeBSD

        def _if_nametoindex(name):
            try:
                s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
                ifreq = struct.pack('16si', name, 0)
                res = fcntl.ioctl(s.fileno(), _SIOCGIFINDEX, ifreq)
                s.close()
                _r = struct.unpack('i', res[16:20])
                idx = int(_r[0])
                return idx
            except Exception:
                pass
            # Fallback: parse ifconfig output and infer the index
            try:
                fd = os.popen('/sbin/ifconfig -a 2>/dev/null', 'r')
                data = fd.read()
                fd.close()
                idx = 1
                for line in string.split(data, '\n'):
                    m = re.match(r'^(\w+\d*):', line)
                    if m:
                        if m.group(1) == name:
                            return idx
                        idx = idx + 1
            except Exception:
                pass
            raise os.error('Unknown interface: %s' % name)

        socket.if_nametoindex = _if_nametoindex
    except ImportError:
        def _if_nametoindex(name):
            raise os.error('if_nametoindex not supported')
        socket.if_nametoindex = _if_nametoindex

if not hasattr(socket, 'if_indextoname'):
    try:
        import fcntl
        _SIOCGIFNAME = 0xC0206910  # NetBSD/FreeBSD

        def _if_indextoname(idx):
            try:
                s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
                ifreq = struct.pack('i16s', idx, '\x00' * 16)
                res = fcntl.ioctl(s.fileno(), _SIOCGIFNAME, ifreq)
                s.close()
                name = string.split(res[4:20], '\x00')[0]
                return name
            except Exception:
                pass
            raise os.error('Unknown interface index: %d' % idx)

        socket.if_indextoname = _if_indextoname
    except ImportError:
        def _if_indextoname(idx):
            raise os.error('if_indextoname not supported')
        socket.if_indextoname = _if_indextoname

# -------------------------------------------------------------------
# Lightweight logger (replacement for logging module)
# -------------------------------------------------------------------
_LOG_WARNING = 0
_LOG_INFO = 1
_LOG_DEBUG = 2

class _Logger:
    def __init__(self, name):
        self.name = name
        self.level = _LOG_WARNING
        self.short_fmt = 0

    def _log(self, level_str, msg):
        try:
            if self.short_fmt:
                line = '%s: %s' % (level_str, msg)
            else:
                t = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))
                line = '%s:%s %s(pid %d): %s' % (t, self.name, level_str, os.getpid(), msg)
            sys.stderr.write(line + '\n')
            sys.stderr.flush()
        except Exception:
            pass

    def debug(self, msg):
        if self.level >= _LOG_DEBUG:
            self._log('DEBUG', str(msg))

    def info(self, msg):
        if self.level >= _LOG_INFO:
            self._log('INFO', str(msg))

    def warning(self, msg):
        self._log('WARNING', str(msg))

    def error(self, msg):
        self._log('ERROR', str(msg))

    def exception(self, msg):
        self._log('ERROR', str(msg))
        traceback.print_exc(file=sys.stderr)


class _LoggingModule:
    WARNING = _LOG_WARNING
    INFO = _LOG_INFO
    DEBUG = _LOG_DEBUG

    def __init__(self):
        self._loggers = {}
        self._root_level = _LOG_WARNING
        self._short_fmt = 0

    def getLogger(self, name):
        if not self._loggers.has_key(name):
            lg = _Logger(name)
            lg.level = self._root_level
            lg.short_fmt = self._short_fmt
            self._loggers[name] = lg
        return self._loggers[name]

    def basicConfig(self, level=_LOG_WARNING, format=None):
        self._root_level = level
        if format and string.find(format, '%(levelname)s') >= 0 and string.find(format, '%(asctime)s') < 0:
            self._short_fmt = 1
        for lg in self._loggers.values():
            lg.level = level
            lg.short_fmt = self._short_fmt


logging = _LoggingModule()

# -------------------------------------------------------------------
# UUID implementation (local md5-based replacement)
# -------------------------------------------------------------------
# -------------------------------------------------------------------
# UUID implementation
# Internal representation: 16-byte str (_bytes)
# Python 1.5.2 cannot format long values with '%x'
# Avoid large integer formatting and use struct + md5 only
# -------------------------------------------------------------------
def _long_to_bytes16(n):
    """Convert long integer n to a big-endian 16-byte str."""
    result = ['\x00'] * 16
    for i in range(15, -1, -1):
        result[i] = chr(int(n & 0xffL))
        n = n >> 8
    return string.join(result, '')

def _bytes16_to_hex(b):
    """Convert a 16-byte str to a 32-char lowercase hex str."""
    h = ''
    for ch in b:
        v = ord(ch)
        h = h + '0123456789abcdef'[v >> 4] + '0123456789abcdef'[v & 0xf]
    return h

def _hex_to_bytes16(h):
    """Convert a 32-char hex str to a 16-byte str."""
    result = ''
    for i in range(0, 32, 2):
        result = result + chr(string.atoi(h[i:i+2], 16))
    return result


class UUID:
    def __init__(self, hex_str=None):
        if hex_str is not None:
            h = hex_str
            for ch in ('-', '{', '}'):
                i = 0
                out = ''
                while i < len(h):
                    if h[i] != ch:
                        out = out + h[i]
                    i = i + 1
                h = out
            if len(h) != 32:
                raise ValueError('UUID: invalid hex string: %s' % hex_str)
            self._bytes = _hex_to_bytes16(h)
        else:
            self._bytes = '\x00' * 16

    def __str__(self):
        h = _bytes16_to_hex(self._bytes)
        return '%s-%s-%s-%s-%s' % (h[0:8], h[8:12], h[12:16], h[16:20], h[20:32])

    def __repr__(self):
        return "UUID('%s')" % str(self)

    def __cmp__(self, other):
        if hasattr(other, "_bytes"):
            return cmp(self._bytes, other._bytes)
        return 1

    def __ne__(self, other):
        return not self.__eq__(other)



    def get_urn(self):
        return 'urn:uuid:' + str(self)


def _uuid_getattr(self, name):
    if name == 'urn':
        d = self.__dict__
        if d.has_key('_bytes'):
            return 'urn:uuid:' + str(self)
    raise AttributeError(name)

UUID.__getattr__ = _uuid_getattr


def _make_uuid_from_bytes(b):
    u = UUID()
    u._bytes = b
    return u


# 16-byte representation of NAMESPACE_DNS (6ba7b810-9dad-11d1-80b4-00c04fd430c8)
_NAMESPACE_DNS_BYTES = _hex_to_bytes16('6ba7b8109dad11d180b400c04fd430c8')


class _UUIDModule:
    NAMESPACE_DNS = _make_uuid_from_bytes(_NAMESPACE_DNS_BYTES)

    def uuid1(self):
        # Simple random UUID (roughly v4)
        # uuid1-style timestamp math is fragile on Python 1.5.2
        # so generate a random UUID instead
        b = ''
        for i in range(16):
            b = b + chr(random.randint(0, 255))
        # version=4, variant=RFC4122
        b4 = (ord(b[6]) & 0x0f) | 0x40
        b8 = (ord(b[8]) & 0x3f) | 0x80
        b = b[:6] + chr(b4) + b[7:8] + chr(b8) + b[9:]
        return _make_uuid_from_bytes(b)

    def uuid5(self, namespace, name):
        # UUID v3 equivalent (using md5)
        m = md5.new()
        m.update(namespace._bytes)
        m.update(name)
        digest = m.digest()   # 16 bytes
        # Set version=3 and variant=RFC4122 bits
        b = list(digest)
        b[6] = chr((ord(b[6]) & 0x0f) | 0x30)   # version 3
        b[8] = chr((ord(b[8]) & 0x3f) | 0x80)   # variant
        return _make_uuid_from_bytes(string.join(b, ''))

    def UUID(self, hex_str):
        return UUID(hex_str)


uuid = _UUIDModule()

# -------------------------------------------------------------------
# Lightweight XML implementation (xmllib-based ElementTree compatibility)
# -------------------------------------------------------------------
class _Element:
    def __init__(self, tag, attrib=None):
        self.tag = tag
        self.attrib = {}
        if attrib:
            self.attrib.update(attrib)
        self.text = None
        self.tail = None
        self._children = []

    def append(self, child):
        self._children.append(child)

    def find(self, path):
        return _elem_find(self, path)

    def findall(self, path):
        return _elem_findall(self, path)

    def findtext(self, path, default=None):
        el = self.find(path)
        if el is not None and el.text is not None:
            return el.text
        return default

    def get(self, key, default=None):
        return self.attrib.get(key, default)

    def __iter__(self):
        return iter(self._children)

    def __len__(self):
        return len(self._children)


class _ParseError(Exception):
    pass


def _split_xpath(path):
    parts = []
    depth = 0
    brace = 0   # whether we are inside {}
    cur = ''
    for ch in path:
        if ch == '{':
            brace = 1
            cur = cur + ch
        elif ch == '}':
            brace = 0
            cur = cur + ch
        elif ch == '[':
            depth = depth + 1
            cur = cur + ch
        elif ch == ']':
            depth = depth - 1
            cur = cur + ch
        elif ch == '/' and depth == 0 and brace == 0:
            if cur:
                parts.append(cur)
            cur = ''
        else:
            cur = cur + ch
    if cur:
        parts.append(cur)
    return parts


def _match_children(node, part):
    attr_filter = None
    m = re.match(r'^([^\[]+)\[@([^=]+)="([^"]*)"\]$', part)
    if m:
        part = m.group(1)
        attr_filter = (m.group(2), m.group(3))

    if part == '*':
        results = list(node._children)
    else:
        results = []
        for child in node._children:
            if child.tag == part:
                results.append(child)

    if attr_filter is not None:
        k, v = attr_filter
        filtered = []
        for c in results:
            if c.attrib.get(k) == v:
                filtered.append(c)
        results = filtered
    return results


def _elem_find(root, path):
    if not path:
        return root
    if path[:2] == './':
        path = path[2:]
    parts = _split_xpath(path)
    node = root
    for part in parts:
        candidates = _match_children(node, part)
        if not candidates:
            return None
        node = candidates[0]
    return node


def _elem_findall(root, path):
    if not path:
        return [root]
    if path[:2] == './':
        path = path[2:]
    parts = _split_xpath(path)
    nodes = [root]
    for i in range(len(parts)):
        new_nodes = []
        part = parts[i]
        if i == len(parts) - 1:
            for n in nodes:
                new_nodes.extend(_match_children(n, part))
        else:
            for n in nodes:
                cands = _match_children(n, part)
                if cands:
                    new_nodes.append(cands[0])
        nodes = new_nodes
    return nodes


class _XMLParser(xmllib.XMLParser):
    def __init__(self):
        xmllib.XMLParser.__init__(self)
        self._root = None
        self._stack = []
        self._ns_map = {}

    def unknown_starttag(self, tag, attrs):
        clean_attrs = {}
        for k, v in attrs.items():
            if k == 'xmlns':
                self._ns_map[''] = v
            elif k[:6] == 'xmlns:':
                prefix = k[6:]
                self._ns_map[prefix] = v
            else:
                clean_attrs[k] = v

        expanded_tag = self._expand(tag)
        expanded_attrs = {}
        for k, v in clean_attrs.items():
            expanded_attrs[self._expand(k)] = v

        el = _Element(expanded_tag, expanded_attrs)
        if self._stack:
            self._stack[-1].append(el)
        else:
            self._root = el
        self._stack.append(el)

    def unknown_endtag(self, tag):
        if self._stack:
            self._stack.pop()

    def handle_data(self, data):
        if self._stack:
            el = self._stack[-1]
            if el.text is None:
                el.text = data
            else:
                el.text = el.text + data

    def _expand(self, tag):
        # xmllib passes namespace tags as 'ns_uri local' (space separated)
        sp = string.find(tag, ' ')
        if sp >= 0:
            # 'http://... LocalName' -> '{http://...}LocalName'
            return '{%s}%s' % (tag[:sp], tag[sp+1:])
        if string.find(tag, ':') >= 0 and tag[:1] != '{':
            colon = string.index(tag, ':')
            prefix = tag[:colon]
            local = tag[colon+1:]
            if self._ns_map.has_key(prefix):
                return '{%s}%s' % (self._ns_map[prefix], local)
        return tag


def _fromstring(text):
    p = _XMLParser()
    try:
        p.feed(text)
        p.close()
    except xmllib.Error, e:
        raise _ParseError(str(e))
    if p._root is None:
        raise _ParseError('No root element found')
    return p._root


def _xml_escape(text):
    text = string.replace(text, '&', '&amp;')
    text = string.replace(text, '<', '&lt;')
    text = string.replace(text, '>', '&gt;')
    text = string.replace(text, '"', '&quot;')
    return text


def _serialize(el, out):
    attr_str = ''
    for k, v in el.attrib.items():
        attr_str = attr_str + ' %s="%s"' % (k, _xml_escape(v))
    if el._children or el.text:
        out.append('<%s%s>' % (el.tag, attr_str))
        if el.text:
            out.append(_xml_escape(el.text))
        for child in el._children:
            _serialize(child, out)
            if child.tail:
                out.append(_xml_escape(child.tail))
        out.append('</%s>' % el.tag)
    else:
        out.append('<%s%s />' % (el.tag, attr_str))


def _tostring(el):
    out = []
    _serialize(el, out)
    return string.join(out, '')


def _SubElement(parent, tag, attrib=None):
    el = _Element(tag, attrib)
    parent.append(el)
    return el


def ETfromString(text):
    return _fromstring(text)

# -------------------------------------------------------------------
# threading shim (wrapper around thread module)
# -------------------------------------------------------------------
class _Lock:
    def __init__(self):
        if _HAS_THREAD:
            self._lock = _thread_mod.allocate_lock()
        else:
            self._lock = None

    def acquire(self, blocking=1):
        if self._lock is not None:
            return self._lock.acquire(blocking)
        return 1

    def release(self):
        if self._lock is not None:
            self._lock.release()

    def locked(self):
        if self._lock is not None:
            return self._lock.locked()
        return 0


class _Event:
    def __init__(self):
        self._flag = 0
        self._lock = _Lock()

    def set(self):
        self._lock.acquire()
        self._flag = 1
        self._lock.release()

    def clear(self):
        self._lock.acquire()
        self._flag = 0
        self._lock.release()

    def is_set(self):
        return self._flag

    def wait(self, timeout=None):
        if timeout is not None:
            deadline = time.time() + timeout
            while not self._flag:
                if time.time() >= deadline:
                    break
                time.sleep(0.05)
        else:
            while not self._flag:
                time.sleep(0.05)


class _DummyThread:
    name = 'MainThread'
    ident = 0
    daemon = 0


class _Thread:
    def __init__(self, target=None, args=(), kwargs=None, daemon=0):
        self._target = target
        self._args = args
        self._kwargs = kwargs or {}
        self.daemon = daemon
        self._alive = 0

    def start(self):
        if self._target:
            self._alive = 1
            if _HAS_THREAD:
                try:
                    _thread_mod.start_new_thread(self._run, ())
                    return
                except Exception:
                    pass
            self._run()

    def _run(self):
        try:
            apply(self._target, self._args, self._kwargs)
        except Exception:
            pass
        self._alive = 0

    def join(self, timeout=None):
        if timeout is not None:
            deadline = time.time() + timeout
            while self._alive and time.time() < deadline:
                time.sleep(0.05)
        else:
            while self._alive:
                time.sleep(0.05)

    def is_alive(self):
        return self._alive


class _Threading:
    Lock = _Lock
    Event = _Event
    Thread = _Thread
    _main = _DummyThread()

    def current_thread(self):
        return self._main


threading = _Threading()

# -------------------------------------------------------------------
# Thread-based event loop
# select.select is avoided because selectmodule.so may segfault on Python 1.5.2
# Each reader gets a dedicated thread blocked in recv/read
# -------------------------------------------------------------------
class _FakeTask:
    def __init__(self, fn):
        self._done = 0
        try:
            if callable(fn):
                fn()
        except Exception:
            pass
        self._done = 1

    def done(self):
        return self._done

    def add_done_callback(self, cb):
        if self._done:
            cb(self)


# NetBSD i386 ioctl constants
_FIOSETOWN = 0x8004667c
_FIOASYNC  = 0x8004667d

# Global SIGIO notification pipe (safe to write from the signal handler)
_sigio_pipe_r = -1
_sigio_pipe_w = -1

def _sigio_handler_global(sig, frame):
    """SIGIO signal handler: only write one byte to the pipe."""
    global _sigio_pipe_w
    if _sigio_pipe_w >= 0:
        try:
            os.write(_sigio_pipe_w, '\x00')
        except Exception:
            pass


class _FakeEventLoop:
    """SIGIO + pipe based event loop.
    Receive SIGIO notifications through a global pipe.
    The main loop waits in blocking os.read.
    Avoid Python object work inside the signal handler for safety.
    """

    def __init__(self):
        global _sigio_pipe_r, _sigio_pipe_w
        self._readers  = {}    # key -> (callback, args)
        self._timers   = []    # [(next_time, interval, callback, args)]
        self._stop     = 0
        self._running  = 0
        # Create the pipe used for SIGIO notifications
        if _sigio_pipe_r < 0:
            _sigio_pipe_r, _sigio_pipe_w = os.pipe()
        # Install the SIGIO handler before enabling FIOASYNC
        signal.signal(signal.SIGIO, _sigio_handler_global)

    def _setup_sigio(self, key):
        """Enable SIGIO notifications for a socket."""
        try:
            import fcntl
            if hasattr(key, 'fileno'):
                fd = key.fileno()
            else:
                fd = key
            fcntl.ioctl(fd, _FIOSETOWN, struct.pack('i', os.getpid()))
            fcntl.ioctl(fd, _FIOASYNC,  struct.pack('i', 1))
        except Exception:
            pass

    def add_reader(self, key, callback, *args):
        self._readers[key] = (callback, args)
        self._setup_sigio(key)

    def remove_reader(self, key):
        if self._readers.has_key(key):
            del self._readers[key]

    def add_timer(self, interval, callback, *args):
        next_time = time.time() + interval
        self._timers.append([next_time, interval, callback, args])

    def create_task(self, fn):
        return _FakeTask(fn)

    def add_signal_handler(self, sig, callback):
        def _sh(s, f, _cb=callback):
            _cb()
        signal.signal(sig, _sh)

    def is_running(self):
        return self._running

    def run_forever(self):
        global _sigio_pipe_r, _sigio_pipe_w
        self._running = 1
        self._stop    = 0

        # SIGIO flag set by the signal handler
        # Poll using a time.sleep loop
        _sigio_flag = [0]

        def _sigio_h(sig, frame, _f=_sigio_flag):
            _f[0] = _f[0] + 1
            # Write to the pipe to wake os.read (kept as a fallback)
            global _sigio_pipe_w
            if _sigio_pipe_w >= 0:
                try:
                    os.write(_sigio_pipe_w, '\x00')
                except Exception:
                    pass

        signal.signal(signal.SIGIO, _sigio_h)
        # Enable SIGIO for existing readers
        for _key in self._readers.keys():
            self._setup_sigio(_key)

        _FIONREAD = 0x4004667f
        _last_timer = time.time()
        while not self._stop:
            # Sleep for 10 ms (may be interrupted by SIGIO)
            try:
                time.sleep(0.01)
            except Exception:
                pass

            # Timer check (once per second)
            _now = time.time()
            if _now - _last_timer >= 1.0:
                _last_timer = _now
                for _t in self._timers:
                    if _now >= _t[0]:
                        _t[0] = _now + _t[1]
                        try:
                            apply(_t[2], _t[3])
                        except Exception:
                            pass

            # Poll all sockets with FIONREAD
            try:
                import fcntl
                _has_fcntl = 1
            except Exception:
                _has_fcntl = 0

            for _key, (_cb, _cb_args) in self._readers.items():
                if hasattr(_key, 'recvfrom'):
                    # UDP socket
                    _nb = 1
                    if _has_fcntl:
                        try:
                            _buf = struct.pack('i', 0)
                            _res = fcntl.ioctl(_key.fileno(), _FIONREAD, _buf)
                            _nb = int(struct.unpack('i', _res)[0])
                        except Exception:
                            _nb = 0
                    if _nb > 0:
                        try:
                            msg, raw_addr = _key.recvfrom(32767)
                            apply(_cb, _cb_args + (msg, raw_addr))
                        except Exception:
                            pass

        self._running = 0

    def run_until_complete(self, fn):
        if callable(fn):
            fn()

    def stop(self):
        self._stop = 1
        # Write to the pipe to unblock os.read
        global _sigio_pipe_w
        try:
            os.write(_sigio_pipe_w, '\x00')
        except Exception:
            pass

    def set_debug(self, val):
        pass

    def close(self):
        self.stop()


# wsdd_proto.py
# WSD protocol constants
# -------------------------------------------------------------------
WSA_URI = 'http://schemas.xmlsoap.org/ws/2004/08/addressing'
WSD_URI = 'http://schemas.xmlsoap.org/ws/2005/04/discovery'
WSDP_URI = 'http://schemas.xmlsoap.org/ws/2006/02/devprof'

namespaces = {
    'soap': 'http://www.w3.org/2003/05/soap-envelope',
    'wsa': WSA_URI,
    'wsd': WSD_URI,
    'wsx': 'http://schemas.xmlsoap.org/ws/2004/09/mex',
    'wsdp': WSDP_URI,
    'pnpx': 'http://schemas.microsoft.com/windows/pnpx/2005/10',
    'pub': 'http://schemas.microsoft.com/windows/pub/2005/07',
}

def _ns(xpath):
    def _repl(m, _ns=namespaces):
        p = m.group(1)
        l = m.group(2)
        if _ns.has_key(p):
            return '{%s}%s' % (_ns[p], l)
        return m.group(0)
    return re.sub(r'(\w+):(\w+|\*)', _repl, xpath)


WSDD_VERSION = '0.9'
WSD_MAX_KNOWN_MESSAGES = 10

WSD_PROBE         = WSD_URI + '/Probe'
WSD_PROBE_MATCH   = WSD_URI + '/ProbeMatches'
WSD_RESOLVE       = WSD_URI + '/Resolve'
WSD_RESOLVE_MATCH = WSD_URI + '/ResolveMatches'
WSD_HELLO         = WSD_URI + '/Hello'
WSD_BYE           = WSD_URI + '/Bye'
WSD_GET           = 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Get'
WSD_GET_RESPONSE  = 'http://schemas.xmlsoap.org/ws/2004/09/transfer/GetResponse'

WSD_TYPE_DEVICE        = 'wsdp:Device'
PUB_COMPUTER           = 'pub:Computer'
WSD_TYPE_DEVICE_COMPUTER = '%s %s' % (WSD_TYPE_DEVICE, PUB_COMPUTER)

WSD_MCAST_GRP_V4 = '239.255.255.250'
WSD_MCAST_GRP_V6 = 'ff02::c'

WSA_ANON      = WSA_URI + '/role/anonymous'
WSA_DISCOVERY = 'urn:schemas-xmlsoap-org:ws:2005:04:discovery'

MIME_TYPE_SOAP_XML = 'application/soap+xml'

WSD_UDP_PORT  = 3702
WSD_HTTP_PORT = 5357
WSD_MAX_LEN   = 32767

WSDD_LISTEN_PORT = 5359

MULTICAST_UDP_REPEAT = 4
UNICAST_UDP_REPEAT   = 2
UDP_MIN_DELAY        = 50
UDP_MAX_DELAY        = 250
UDP_UPPER_DELAY      = 500

PROBE_TIMEOUT          = 4
MAX_STARTUP_PROBE_DELAY = 3

wsd_instance_id = int(time.time())
wsd_sequence_id = uuid.uuid1().urn

args   = None
logger = None
_known_messages = []

# -------------------------------------------------------------------
# Network abstraction classes
# property() is unavailable, so callers use get_xxx() methods directly
# -------------------------------------------------------------------
class NetworkInterface:
    def __init__(self, name, scope, index):
        self._name = name
        self._scope = scope
        if index is not None:
            self._index = index
        else:
            self._index = socket.if_nametoindex(self._name)

    def get_name(self):  return self._name
    def get_scope(self): return self._scope
    def get_index(self): return self._index

    def __str__(self):
        return self._name

    def __cmp__(self, other):
        if hasattr(other, 'get_name'):
            return cmp(self._name, other.get_name())
        return cmp(self._name, str(other))


class NetworkAddress:
    def __init__(self, family, raw, interface):
        self._family = family
        _is_str = (type(raw) == type(''))
        if _is_str and (len(raw) == 4 or len(raw) == 16):
            self._raw_address = raw
        elif _is_str:
            self._raw_address = socket.inet_pton(family, string.split(raw, '%')[0])
        else:
            self._raw_address = str(raw)
        self._interface = interface
        self._address_str = socket.inet_ntop(self._family, self._raw_address)

    def get_address_str(self):    return self._address_str
    def get_family(self):          return self._family
    def get_interface(self):       return self._interface
    def get_raw(self):             return self._raw_address

    def get_is_multicastable(self):
        return (
            (self._family == socket.AF_INET and ord(self._raw_address[0]) != 127) or
            (self._family == socket.AF_INET6 and self._raw_address[0:2] == '\xfe\x80')
        )

    def __cmp__(self, other):
        if hasattr(other, '_address_str') and hasattr(other, '_family'):
            if self._family != other._family:
                return cmp(self._family, other._family)
            return cmp(self._address_str, other._address_str)
        return 1

    def get_transport_str(self):
        if self._family == socket.AF_INET:
            return self._address_str
        return '[%s]' % self._address_str

    # Keep compatibility with attribute-style access via __getattr__.
    # On Python 1.5.2, reading self._xxx inside __getattr__ can recurse
    # if the attribute is missing, so read self.__dict__ directly.
    def __getattr__(self, name):
        d = self.__dict__
        if name == 'address_str':
            if d.has_key('_address_str'): return d['_address_str']
        elif name == 'family':
            if d.has_key('_family'): return d['_family']
        elif name == 'interface':
            if d.has_key('_interface'): return d['_interface']
        elif name == 'raw':
            if d.has_key('_raw_address'): return d['_raw_address']
        elif name == 'is_multicastable':
            if d.has_key('_family') and d.has_key('_raw_address'):
                return self.get_is_multicastable()
        elif name == 'transport_str':
            if d.has_key('_family') and d.has_key('_address_str'):
                return self.get_transport_str()
        raise AttributeError(name)

    def __str__(self):
        return '%s%%%s' % (self._address_str, self._interface.get_name())

    def __cmp__(self, other):
        if not hasattr(other, 'get_family'):
            return 1
        return (self._family == other.get_family() and
                self._raw_address == other.get_raw() and
                self._interface == other.get_interface())


class UdpAddress(NetworkAddress):
    def __init__(self, family, transport_address, interface):
        if family != socket.AF_INET and family != socket.AF_INET6:
            raise RuntimeError('Unsupported address family: %d' % family)
        self._transport_address = transport_address
        self._port = transport_address[1]
        NetworkAddress.__init__(self, family, transport_address[0], interface)

    def get_transport_address(self): return self._transport_address
    def get_port(self):              return self._port

    def __getattr__(self, name):
        d = self.__dict__
        if name == 'transport_address':
            if d.has_key('_transport_address'): return d['_transport_address']
        elif name == 'port':
            if d.has_key('_port'): return d['_port']
        return NetworkAddress.__getattr__(self, name)

    def __cmp__(self, other):
        if not hasattr(other, 'get_transport_address'):
            return 1
        return cmp(self._transport_address, other.get_transport_address())


class INetworkPacketHandler:
    def handle_packet(self, msg, udp_src_address=None):
        pass


class MulticastHandler:
    def __init__(self, address, aio_loop):
        self.address = address
        self.recv_socket    = socket.socket(self.address.get_family(), socket.SOCK_DGRAM)
        self.recv_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.mc_send_socket = socket.socket(self.address.get_family(), socket.SOCK_DGRAM)
        self.uc_send_socket = socket.socket(self.address.get_family(), socket.SOCK_DGRAM)
        self.uc_send_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.message_handlers = {}
        self.aio_loop = aio_loop

        if self.address.get_family() == socket.AF_INET:
            self.init_v4()
        elif self.address.get_family() == socket.AF_INET6:
            self.init_v6()

        logger.info('joined multicast group %s on %s' % (
            str(self.multicast_address.get_transport_address()), str(self.address)))
        logger.debug('transport address on %s is %s' % (
            self.address.get_interface().get_name(), self.address.get_transport_str()))
        logger.debug('will listen for HTTP on %s' % str(self.listen_address))

        self.aio_loop.add_reader(self.recv_socket, self.read_socket, self.recv_socket)
        self.aio_loop.add_reader(self.mc_send_socket, self.read_socket, self.mc_send_socket)
        self.aio_loop.add_reader(self.uc_send_socket, self.read_socket, self.uc_send_socket)

    def cleanup(self):
        self.aio_loop.remove_reader(self.recv_socket)
        self.aio_loop.remove_reader(self.mc_send_socket)
        self.aio_loop.remove_reader(self.uc_send_socket)
        self.recv_socket.close()
        self.mc_send_socket.close()
        self.uc_send_socket.close()

    def handles_address(self, address):
        try:
            return (self.address.get_address_str() == address.get_address_str() and
                    self.address.get_family() == address.get_family())
        except Exception:
            return self.address == address

    def init_v6(self):
        idx = self.address.get_interface().get_index()
        raw_mc_addr = (WSD_MCAST_GRP_V6, WSD_UDP_PORT, 0x575C, idx)
        self.multicast_address = UdpAddress(
            self.address.get_family(), raw_mc_addr, self.address.get_interface())
        mreq = (socket.inet_pton(self.address.get_family(), WSD_MCAST_GRP_V6) +
                struct.pack('@I', idx))
        self.recv_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
        self.recv_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
        try:
            self.recv_socket.bind((WSD_MCAST_GRP_V6, WSD_UDP_PORT, 0, idx))
        except os.error:
            self.recv_socket.bind(('::', 0, 0, idx))
        self.uc_send_socket.bind((self.address.get_address_str(), WSD_UDP_PORT, 0, idx))
        self.mc_send_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 0)
        self.mc_send_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, args.hoplimit)
        self.mc_send_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, idx)
        try:
            self.mc_send_socket.bind((self.address.get_address_str(), args.source_port, 0, idx))
        except os.error:
            logger.error('specified port %d already in use for %s' % (
            args.source_port, self.address.get_address_str()))
        self.listen_address = (self.address.get_address_str(), WSD_HTTP_PORT, 0, idx)

    def init_v4(self):
        idx = self.address.get_interface().get_index()
        raw_mc_addr = (WSD_MCAST_GRP_V4, WSD_UDP_PORT)
        self.multicast_address = UdpAddress(
            self.address.get_family(), raw_mc_addr, self.address.get_interface())
        mc_raw = socket.inet_pton(self.address.get_family(), WSD_MCAST_GRP_V4)
        # BSD: ip_mreq = mc_addr(4) + intf_addr(4)
        mreq = mc_raw + self.address.get_raw()
        self.recv_socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
        try:
            self.recv_socket.bind((WSD_MCAST_GRP_V4, WSD_UDP_PORT))
        except os.error:
            self.recv_socket.bind(('', WSD_UDP_PORT))
        self.uc_send_socket.bind((self.address.get_address_str(), WSD_UDP_PORT))
        # BSD: IP_MULTICAST_IF expects only an in_addr (4B)
        self.mc_send_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, self.address.get_raw())
        self.mc_send_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, struct.pack('B', 0))
        self.mc_send_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, struct.pack('B', args.hoplimit))
        try:
            self.mc_send_socket.bind((self.address.get_address_str(), args.source_port))
        except os.error:
            logger.error('specified port %d already in use for %s' % (
            args.source_port, self.address.get_address_str()))
        self.listen_address = (self.address.get_address_str(), WSD_HTTP_PORT)

    def add_handler(self, sock, handler):
        if self.message_handlers.has_key(sock):
            self.message_handlers[sock].append(handler)
        else:
            self.message_handlers[sock] = [handler]

    def remove_handler(self, sock, handler):
        if self.message_handlers.has_key(sock):
            if handler in self.message_handlers[sock]:
                self.message_handlers[sock].remove(handler)

    def read_socket(self, key, msg=None, raw_address=None):
        if msg is None:
            return
        try:
            _kfd = key.fileno()
        except Exception:
            return
        if _kfd == self.uc_send_socket.fileno():
            s = self.uc_send_socket
        elif _kfd == self.mc_send_socket.fileno():
            s = self.mc_send_socket
        elif _kfd == self.recv_socket.fileno():
            s = self.recv_socket
        else:
            return
        address = UdpAddress(
            self.address.get_family(), raw_address, self.address.get_interface())
        if self.message_handlers.has_key(s):
            for handler in self.message_handlers[s]:
                handler.handle_packet(msg, address)

    def send(self, msg, addr):
        _transport = addr.get_transport_address()
        _mc_transport = self.multicast_address.get_transport_address()
        if _transport == _mc_transport:
            self.mc_send_socket.sendto(msg, _transport)
        else:
            self.uc_send_socket.sendto(msg, _transport)


# -------------------------------------------------------------------
# WSD message handler
# -------------------------------------------------------------------
class WSDMessageHandler(INetworkPacketHandler):
    def __init__(self):
        self.handlers = {}
        self.pending_tasks = []

    def cleanup(self):
        pass

    def add_endpoint_reference(self, parent, endpoint=None):
        epr = _SubElement(parent, 'wsa:EndpointReference')
        address = _SubElement(epr, 'wsa:Address')
        if endpoint is None:
            address.text = args.uuid.urn
        else:
            address.text = endpoint

    def add_metadata_version(self, parent):
        _SubElement(parent, 'wsd:MetadataVersion').text = '2'

    def add_types(self, parent):
        _SubElement(parent, 'wsd:Types').text = WSD_TYPE_DEVICE_COMPUTER

    def add_xaddr(self, parent, transport_addr):
        if transport_addr:
            _SubElement(parent, 'wsd:XAddrs').text = (
                'http://%s:%d/%s' % (transport_addr, WSD_HTTP_PORT, str(args.uuid)))

    def build_message(self, to_addr, action_str, request_header, response):
        tree, msg_id = self.build_message_tree(to_addr, action_str, request_header, response)
        retval = self.xml_to_str(tree)
        logger.debug('constructed xml for WSD message: %s' % retval)
        return retval

    def build_message_tree(self, to_addr, action_str, request_header, body):
        root = _Element('soap:Envelope')
        header = _SubElement(root, 'soap:Header')
        _SubElement(header, 'wsa:To').text = to_addr
        _SubElement(header, 'wsa:Action').text = action_str
        msg_id_el = _SubElement(header, 'wsa:MessageID')
        msg_id_el.text = uuid.uuid1().urn

        if request_header is not None:
            req_msg_id = request_header.find(_ns('./wsa:MessageID'))
            if req_msg_id is not None:
                _SubElement(header, 'wsa:RelatesTo').text = req_msg_id.text

        self.add_header_elements(header, action_str)

        body_root = _SubElement(root, 'soap:Body')
        if body is not None:
            body_root.append(body)

        for prefix, uri in namespaces.items():
            root.attrib['xmlns:' + prefix] = uri

        return root, msg_id_el.text

    def add_header_elements(self, header, extra):
        pass

    def handle_message(self, msg, src=None):
        try:
            tree = ETfromString(msg)
        except _ParseError:
            return None

        header = tree.find(_ns('./soap:Header'))
        if header is None:
            return None

        msg_id_tag = header.find(_ns('./wsa:MessageID'))
        if msg_id_tag is None:
            return None

        msg_id = str(msg_id_tag.text)
        if self.is_duplicated_msg(msg_id):
            logger.debug('known message (%s): dropping it' % msg_id)
            return None

        action_tag = header.find(_ns('./wsa:Action'))
        if action_tag is None:
            return None

        action = str(action_tag.text)
        slash = string.rfind(action, '/')
        if slash >= 0:
            action_method = action[slash+1:]
        else:
            action_method = action

        if src:
            logger.info('%s:%s(%s) - - "%s %s UDP" - -' % (
                src.get_transport_str(), str(src.get_port()),
                str(src.get_interface()), action_method, msg_id))
        else:
            logger.debug('processing WSD %s message (%s)' % (action_method, msg_id))

        body = tree.find(_ns('./soap:Body'))
        if body is None:
            return None

        logger.debug('incoming message content is %s' % msg)
        if self.handlers.has_key(action):
            retval = self.handlers[action](header, body)
            if retval is not None:
                response, response_type = retval
                return self.build_message(WSA_ANON, response_type, header, response)
        else:
            logger.debug('unhandled action %s/%s' % (action, msg_id))

        return None

    def is_duplicated_msg(self, msg_id):
        global _known_messages
        if msg_id in _known_messages:
            return 1
        _known_messages.append(msg_id)
        if len(_known_messages) > WSD_MAX_KNOWN_MESSAGES:
            _known_messages = _known_messages[-WSD_MAX_KNOWN_MESSAGES:]
        return 0

    def xml_to_str(self, xml):
        return '<?xml version="1.0" encoding="utf-8"?>' + _tostring(xml)


class WSDUDPMessageHandler(WSDMessageHandler):
    def __init__(self, mch):
        WSDMessageHandler.__init__(self)
        self.mch = mch
        self.tearing_down = 0

    def teardown(self):
        self.tearing_down = 1

    def send_datagram(self, msg, dst):
        try:
            self.mch.send(msg, dst)
        except Exception, e:
            logger.error('error while sending packet on %s: %s' % (
                str(self.mch.address.get_interface()), str(e)))

    def enqueue_datagram(self, msg, address, msg_type=None):
        if msg_type:
            logger.info('scheduling %s message via %s to %s' % (
                str(msg_type), str(address.get_interface()), str(address)))

        _m = msg
        _a = address
        _self = self

        def _sched(_m=_m, _a=_a, _self=_self):
            _self.schedule_datagram(_m, _a)

        task = _self.mch.aio_loop.create_task(_sched)
        if self.tearing_down:
            self.pending_tasks.append(task)

    def schedule_datagram(self, msg, address):
        self.send_datagram(msg, address)
        try:
            _is_mc = (address.get_transport_address() == self.mch.multicast_address.get_transport_address())
        except Exception:
            _is_mc = 0
        if _is_mc:
            msg_count = MULTICAST_UDP_REPEAT
        else:
            msg_count = UNICAST_UDP_REPEAT
        delta = random.randint(UDP_MIN_DELAY, UDP_MAX_DELAY)
        # Repeated WSD sends are optional.
        # On NetBSD 1.4, time.sleep may be interrupted by SIGIO and crash,
        # so send only once.


class WSDDiscoveredDevice:
    instances = {}

    def __init__(self, xml_str, xaddr, interface):
        self.last_seen = 0.0
        self.addresses = {}
        self.props = {}
        self.display_name = ''
        self.types = {}
        self.update(xml_str, xaddr, interface)

    def update(self, xml_str, xaddr, interface):
        try:
            tree = ETfromString(xml_str)
        except _ParseError:
            return None
        sections = tree.findall(_ns('soap:Body/wsx:Metadata/wsx:MetadataSection'))
        for section in sections:
            dialect = section.attrib.get('Dialect', '')
            if dialect == WSDP_URI + '/ThisDevice':
                self.extract_wsdp_props(section, dialect)
            elif dialect == WSDP_URI + '/ThisModel':
                self.extract_wsdp_props(section, dialect)
            elif dialect == WSDP_URI + '/Relationship':
                host_sec = section.find(_ns('wsdp:Relationship/wsdp:Host'))
                if host_sec is not None:
                    self.extract_host_props(host_sec)
            else:
                logger.debug('unknown metadata dialect (%s)' % dialect)

        parsed = urlparse.urlparse(xaddr)
        netloc = parsed[1]
        colon = string.rfind(netloc, ':')
        if colon >= 0:
            addr = netloc[:colon]
        else:
            addr = netloc

        report = 1
        iname = interface.get_name()
        if not self.addresses.has_key(iname):
            self.addresses[iname] = {addr: 1}
        else:
            if not self.addresses[iname].has_key(addr):
                self.addresses[iname][addr] = 1
            else:
                report = 0

        self.last_seen = time.time()
        if self.props.has_key('DisplayName') and self.props.has_key('BelongsTo') and report:
            self.display_name = self.props['DisplayName']
            logger.info('discovered %s in %s on %s' % (
        self.display_name, self.props['BelongsTo'], addr))
        elif self.props.has_key('FriendlyName') and report:
            self.display_name = self.props['FriendlyName']
            logger.info('discovered %s on %s' % (self.display_name, addr))

        logger.debug(str(self.props))

    def extract_wsdp_props(self, root, dialect):
        slash = string.rfind(dialect, '/')
        if slash >= 0:
            propsRoot = dialect[slash+1:]
        else:
            propsRoot = dialect
        nodes = root.findall(_ns('./wsdp:%s/*' % propsRoot))
        ns_prefix = '{%s}' % WSDP_URI
        for node in nodes:
            if node.tag[:len(ns_prefix)] == ns_prefix:
                tag_name = node.tag[len(ns_prefix):]
        self.props[tag_name] = str(node.text)

    def extract_host_props(self, root):
        types_text = root.findtext(_ns('wsdp:Types'), '')
        self.types = {}
        for t in string.split(types_text, ' '):
            self.types[t] = 1
        if not self.types.has_key(PUB_COMPUTER):
            return
        comp = root.findtext(_ns(PUB_COMPUTER), '')
        slash = string.find(comp, '/')
        if slash >= 0:
            self.props['DisplayName'] = comp[:slash]
            self.props['BelongsTo'] = comp[slash+1:]
        else:
            self.props['DisplayName'] = comp
            self.props['BelongsTo'] = ''


class WSDClient(WSDUDPMessageHandler):
    instances = []

    def __init__(self, mch):
        WSDUDPMessageHandler.__init__(self, mch)
        WSDClient.instances.append(self)
        self.mch.add_handler(self.mch.mc_send_socket, self)
        self.mch.add_handler(self.mch.recv_socket, self)
        self.probes = {}
        self.handlers[WSD_HELLO]        = self.handle_hello
        self.handlers[WSD_BYE]          = self.handle_bye
        self.handlers[WSD_PROBE_MATCH]  = self.handle_probe_match
        self.handlers[WSD_RESOLVE_MATCH]= self.handle_resolve_match
        time.sleep(random.randint(0, MAX_STARTUP_PROBE_DELAY))
        self.send_probe()

    def cleanup(self):
        WSDMessageHandler.cleanup(self)
        if self in WSDClient.instances:
            WSDClient.instances.remove(self)
        self.mch.remove_handler(self.mch.mc_send_socket, self)
        self.mch.remove_handler(self.mch.recv_socket, self)

    def send_probe(self):
        self.remove_outdated_probes()
        probe = _Element('wsd:Probe')
        _SubElement(probe, 'wsd:Types').text = WSD_TYPE_DEVICE
        xml, i = self.build_message_tree(WSA_DISCOVERY, WSD_PROBE, None, probe)
        self.enqueue_datagram(self.xml_to_str(xml), self.mch.multicast_address, msg_type='Probe')
        self.probes[i] = time.time()

    def teardown(self):
        WSDUDPMessageHandler.teardown(self)
        self.remove_outdated_probes()

    def handle_packet(self, msg, src):
        self.handle_message(msg, src)

    def _extract_xaddr(self, xaddrs):
        for addr in string.split(string.strip(xaddrs)):
            if self.mch.address.get_family() == socket.AF_INET6 and string.find(addr, '//[fe80::') >= 0:
                return addr
            elif self.mch.address.get_family() == socket.AF_INET:
                return addr
        return None

    def handle_hello(self, header, body):
        endpoint, xaddrs = self.extract_endpoint_metadata(body, 'wsd:Hello')
        if not xaddrs:
            logger.info('Hello without XAddrs, sending resolve')
            self.enqueue_datagram(
        self.build_resolve_message(str(endpoint)), self.mch.multicast_address)
            return None
        xaddr = self._extract_xaddr(xaddrs)
        if xaddr is None:
            return None
        logger.info('Hello from %s on %s' % (str(endpoint), xaddr))
        self.perform_metadata_exchange(endpoint, xaddr)
        return None

    def handle_bye(self, header, body):
        endpoint, _ = self.extract_endpoint_metadata(body, 'wsd:Bye')
        parsed = urlparse.urlparse(str(endpoint))
        device_uri = urlparse.urlunparse(parsed)
        if WSDDiscoveredDevice.instances.has_key(device_uri):
            del WSDDiscoveredDevice.instances[device_uri]
        return None

    def handle_probe_match(self, header, body):
        rel_msg = header.findtext(_ns('wsa:RelatesTo'), None)
        if not self.probes.has_key(rel_msg):
            logger.debug('unknown probe %s' % str(rel_msg))
            return None
        endpoint, xaddrs = self.extract_endpoint_metadata(
            body, 'wsd:ProbeMatches/wsd:ProbeMatch')
        if not xaddrs:
            logger.debug('probe match without XAddrs, sending resolve')
            self.enqueue_datagram(
        self.build_resolve_message(str(endpoint)), self.mch.multicast_address)
            return None
        xaddr = self._extract_xaddr(xaddrs)
        if xaddr is None:
            return None
        logger.debug('probe match for %s on %s' % (str(endpoint), xaddr))
        self.perform_metadata_exchange(endpoint, xaddr)
        return None

    def build_resolve_message(self, endpoint):
        resolve = _Element('wsd:Resolve')
        self.add_endpoint_reference(resolve, endpoint)
        return self.build_message(WSA_DISCOVERY, WSD_RESOLVE, None, resolve)

    def handle_resolve_match(self, header, body):
        endpoint, xaddrs = self.extract_endpoint_metadata(
            body, 'wsd:ResolveMatches/wsd:ResolveMatch')
        if not endpoint or not xaddrs:
            logger.debug('resolve match without endpoint/xaddr')
            return None
        xaddr = self._extract_xaddr(xaddrs)
        if xaddr is None:
            return None
        logger.debug('resolve match for %s on %s' % (str(endpoint), xaddr))
        self.perform_metadata_exchange(endpoint, xaddr)
        return None

    def extract_endpoint_metadata(self, body, prefix):
        pref = prefix + '/'
        endpoint = body.findtext(_ns(pref + 'wsa:EndpointReference/wsa:Address'))
        xaddrs   = body.findtext(_ns(pref + 'wsd:XAddrs'))
        return endpoint, xaddrs

    def perform_metadata_exchange(self, endpoint, xaddr):
        if not (xaddr[:7] == 'http://' or xaddr[:8] == 'https://'):
            logger.debug('invalid XAddr: %s' % xaddr)
            return

        host = None
        url = xaddr
        if self.mch.address.get_family() == socket.AF_INET6:
            b1 = string.find(url, '[')
            b2 = string.find(url, ']')
            if b1 >= 0 and b2 >= 0:
                host = '[%s]' % url[b1+1:b2]
                url = url[:b2] + ('%%%s' % str(self.mch.address.get_interface())) + url[b2:]

        body = self.build_getmetadata_message(endpoint)
        try:
            parsed = urlparse.urlparse(url)
            netloc = parsed[1]
            path = parsed[2] or '/'
            colon = string.rfind(netloc, ':')
            if colon >= 0:
                http_host = netloc[:colon]
                http_port = int(netloc[colon+1:])
            else:
                http_host = netloc
                http_port = 80
            conn = httplib.HTTPConnection(http_host, http_port)
            headers = {
                'Content-Type': 'application/soap+xml',
                'User-Agent': 'wsdd',
            }
            if host is not None:
                headers['Host'] = host
            conn.request('POST', path, body, headers)
            resp = conn.getresponse()
            data = resp.read()
            conn.close()
            self.handle_metadata(data, endpoint, xaddr)
        except Exception, e:
            logger.warning('could not fetch metadata from %s: %s' % (url, str(e)))

    def build_getmetadata_message(self, endpoint):
        tree, _ = self.build_message_tree(endpoint, WSD_GET, None, None)
        return self.xml_to_str(tree)

    def handle_metadata(self, meta, endpoint, xaddr):
        parsed = urlparse.urlparse(str(endpoint))
        device_uri = urlparse.urlunparse(parsed)
        if WSDDiscoveredDevice.instances.has_key(device_uri):
            WSDDiscoveredDevice.instances[device_uri].update(
                meta, xaddr, self.mch.address.get_interface())
        else:
            WSDDiscoveredDevice.instances[device_uri] = WSDDiscoveredDevice(
                meta, xaddr, self.mch.address.get_interface())

    def remove_outdated_probes(self):
        cut = time.time() - PROBE_TIMEOUT * 2
        new_probes = {}
        for k, v in self.probes.items():
            if v > cut:
                new_probes[k] = v
        self.probes = new_probes

    def add_header_elements(self, header, extra):
        if extra == WSD_GET:
            reply_to = _SubElement(header, 'wsa:ReplyTo')
            _SubElement(reply_to, 'wsa:Address').text = WSA_ANON
            wsa_from = _SubElement(header, 'wsa:From')
            _SubElement(wsa_from, 'wsa:Address').text = args.uuid.urn



class WSDHost(WSDUDPMessageHandler):
    message_number = 0
    instances = []

    def __init__(self, mch):
        WSDUDPMessageHandler.__init__(self, mch)
        WSDHost.instances.append(self)
        self.mch.add_handler(self.mch.recv_socket, self)
        self.handlers[WSD_PROBE]   = self.handle_probe
        self.handlers[WSD_RESOLVE] = self.handle_resolve
        self.send_hello()

    def cleanup(self):
        WSDMessageHandler.cleanup(self)
        if self in WSDHost.instances:
            WSDHost.instances.remove(self)

    def teardown(self):
        WSDUDPMessageHandler.teardown(self)
        self.send_bye()

    def handle_packet(self, msg, src):
        reply = self.handle_message(msg, src)
        if reply:
            self.enqueue_datagram(reply, src)

    def send_hello(self):
        hello = _Element('wsd:Hello')
        self.add_endpoint_reference(hello)
        self.add_xaddr(hello, self.mch.address.get_transport_str())
        self.add_metadata_version(hello)
        msg = self.build_message(WSA_DISCOVERY, WSD_HELLO, None, hello)
        self.enqueue_datagram(msg, self.mch.multicast_address, msg_type='Hello')

    def send_bye(self):
        bye = _Element('wsd:Bye')
        self.add_endpoint_reference(bye)
        msg = self.build_message(WSA_DISCOVERY, WSD_BYE, None, bye)
        self.enqueue_datagram(msg, self.mch.multicast_address, msg_type='Bye')

    def handle_probe(self, header, body):
        probe = body.find(_ns('./wsd:Probe'))
        if probe is None:
            return None
        scopes = probe.find(_ns('./wsd:Scopes'))
        if scopes:
            logger.debug('scopes unsupported but probed')
            return None
        types_elem = probe.find(_ns('./wsd:Types'))
        if types_elem is None:
            logger.debug('Probe message lacks wsd:Types element. Ignored.')
            return None
        if types_elem.text != WSD_TYPE_DEVICE:
            logger.debug('unknown discovery type (%s) for probe' % str(types_elem.text))
            return None
        matches = _Element('wsd:ProbeMatches')
        match = _SubElement(matches, 'wsd:ProbeMatch')
        self.add_endpoint_reference(match)
        self.add_types(match)
        self.add_xaddr(match, self.mch.address.get_transport_str())
        self.add_metadata_version(match)
        return matches, WSD_PROBE_MATCH

    def handle_resolve(self, header, body):
        resolve = body.find(_ns('./wsd:Resolve'))
        if resolve is None:
            return None
        addr = resolve.find(_ns('./wsa:EndpointReference/wsa:Address'))
        if addr is None:
            logger.debug('invalid resolve request: missing endpoint address')
            return None
        if addr.text != args.uuid.urn:
            logger.debug('invalid resolve: address mismatch')
            return None
        matches = _Element('wsd:ResolveMatches')
        match = _SubElement(matches, 'wsd:ResolveMatch')
        self.add_endpoint_reference(match)
        self.add_types(match)
        self.add_xaddr(match, self.mch.address.get_transport_str())
        self.add_metadata_version(match)
        return matches, WSD_RESOLVE_MATCH

    def add_header_elements(self, header, extra):
        _SubElement(header, 'wsd:AppSequence', {
            'InstanceId': str(wsd_instance_id),
            'SequenceId': wsd_sequence_id,
            'MessageNumber': str(WSDHost.message_number),
        })
        WSDHost.message_number = WSDHost.message_number + 1


class WSDHttpMessageHandler(WSDMessageHandler):
    def __init__(self):
        WSDMessageHandler.__init__(self)
        self.handlers[WSD_GET] = self.handle_get

    def handle_get(self, header, body):
        metadata = _Element('wsx:Metadata')

        sec1 = _SubElement(metadata, 'wsx:MetadataSection',
                           {'Dialect': WSDP_URI + '/ThisDevice'})
        device = _SubElement(sec1, 'wsdp:ThisDevice')
        _SubElement(device, 'wsdp:FriendlyName').text = 'WSD Device %s' % args.hostname
        _SubElement(device, 'wsdp:FirmwareVersion').text = '1.0'
        _SubElement(device, 'wsdp:SerialNumber').text = '1'

        sec2 = _SubElement(metadata, 'wsx:MetadataSection',
                           {'Dialect': WSDP_URI + '/ThisModel'})
        model = _SubElement(sec2, 'wsdp:ThisModel')
        _SubElement(model, 'wsdp:Manufacturer').text = 'wsdd'
        _SubElement(model, 'wsdp:ModelName').text = 'wsdd'
        _SubElement(model, 'pnpx:DeviceCategory').text = 'Computers'

        sec3 = _SubElement(metadata, 'wsx:MetadataSection',
                           {'Dialect': WSDP_URI + '/Relationship'})
        rel  = _SubElement(sec3, 'wsdp:Relationship', {'Type': WSDP_URI + '/host'})
        host = _SubElement(rel, 'wsdp:Host')
        self.add_endpoint_reference(host)
        _SubElement(host, 'wsdp:Types').text = PUB_COMPUTER
        _SubElement(host, 'wsdp:ServiceId').text = args.uuid.urn

        if args.domain:
            value = args.domain
            if args.preserve_case:
                dh = args.hostname
            else:
                dh = string.lower(args.hostname)
            comp = '%s/Domain:%s' % (dh, value)
        else:
            value = string.upper(args.workgroup)
            if args.preserve_case:
                dh = args.hostname
            else:
                dh = string.upper(args.hostname)
            comp = '%s/Workgroup:%s' % (dh, value)

        _SubElement(host, PUB_COMPUTER).text = comp

        return metadata, WSD_GET_RESPONSE


# -------------------------------------------------------------------
# BaseHTTPServer replacement (to avoid BaseHTTPServer module crashes)
# Implements SocketServer.TCPServer + minimal HTTP parsing only
# -------------------------------------------------------------------

class _HTTPServer(SocketServer.TCPServer):
    address_family = socket.AF_INET

    def server_bind(self):
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # SocketServer.TCPServer.server_bind() may call gethostbyaddr()
        # which can segfault on NetBSD 1.4, so bind directly here.
        self.socket.bind(self.server_address)
        host, port = self.socket.getsockname()[:2]
        if not host or host == '0.0.0.0':
            # Avoid gethostname()/gethostbyname(); use /bin/hostname instead
            try:
                _fd = os.popen('/bin/hostname 2>/dev/null', 'r')
                host = string.strip(_fd.read())
                _fd.close()
                if not host:
                    host = 'localhost'
            except Exception:
                host = 'localhost'
        self.server_name = host
        self.server_port = port

    def server_activate(self):
        self.socket.listen(5)

    def server_close(self):
        try:
            self.socket.close()
        except Exception:
            pass

    def _build_http_metadata_response(self, relates_to):
        xml = '<?xml version="1.0" encoding="utf-8"?>'
        xml = xml + '<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:wsx="http://schemas.xmlsoap.org/ws/2004/09/mex" xmlns:wsdp="http://schemas.xmlsoap.org/ws/2006/02/devprof" xmlns:pub="http://schemas.microsoft.com/windows/pub/2005/07" xmlns:pnpx="http://schemas.microsoft.com/windows/pnpx/2005/10">'
        xml = xml + '<soap:Header><wsa:To>' + WSA_ANON + '</wsa:To><wsa:Action>' + WSD_GET_RESPONSE + '</wsa:Action><wsa:MessageID>' + uuid.uuid1().urn + '</wsa:MessageID>'
        if relates_to:
            xml = xml + '<wsa:RelatesTo>' + relates_to + '</wsa:RelatesTo>'
        xml = xml + '</soap:Header>'
        xml = xml + '<soap:Body><wsx:Metadata>'
        xml = xml + '<wsx:MetadataSection Dialect="' + WSDP_URI + '/ThisDevice"><wsdp:ThisDevice><wsdp:FriendlyName>WSD Device ' + args.hostname + '</wsdp:FriendlyName><wsdp:FirmwareVersion>1.0</wsdp:FirmwareVersion><wsdp:SerialNumber>1</wsdp:SerialNumber></wsdp:ThisDevice></wsx:MetadataSection>'
        xml = xml + '<wsx:MetadataSection Dialect="' + WSDP_URI + '/ThisModel"><wsdp:ThisModel><wsdp:Manufacturer>wsdd</wsdp:Manufacturer><wsdp:ModelName>wsdd</wsdp:ModelName><pnpx:DeviceCategory>Computers</pnpx:DeviceCategory></wsdp:ThisModel></wsx:MetadataSection>'
        xml = xml + '<wsx:MetadataSection Dialect="' + WSDP_URI + '/Relationship"><wsdp:Relationship Type="' + WSDP_URI + '/host"><wsdp:Host><wsa:EndpointReference><wsa:Address>' + args.uuid.urn + '</wsa:Address></wsa:EndpointReference><wsdp:Types>' + PUB_COMPUTER + '</wsdp:Types><wsdp:ServiceId>' + args.uuid.urn + '</wsdp:ServiceId>'
        if args.domain:
            comp = args.hostname + '/Domain:' + args.domain
        else:
            comp = string.upper(args.hostname) + '/Workgroup:' + string.upper(args.workgroup)
        xml = xml + '<pub:Computer>' + comp + '</pub:Computer></wsdp:Host></wsdp:Relationship></wsx:MetadataSection>'
        xml = xml + '</wsx:Metadata></soap:Body></soap:Envelope>'
        return 'HTTP/1.1 200 OK\r\nContent-Type: application/soap+xml\r\nContent-Length: ' + str(len(xml)) + '\r\nConnection: close\r\n\r\n' + xml

    def _extract_http_request_message_id(self, body):
        try:
            tree = ETfromString(body)
            header = tree.find(_ns('./soap:Header'))
            if header is None:
                return None
            msg_id_tag = header.find(_ns('./wsa:MessageID'))
            if msg_id_tag is None:
                return None
            return str(msg_id_tag.text)
        except Exception:
            return None

    def _send_all(self, conn, data):
        sent = 0
        total = len(data)
        while sent < total:
            n = conn.send(data[sent:])
            if not n:
                raise RuntimeError('short send')
            sent = sent + n
        return sent

    def _handle_http_child_request(self, conn, resp):
        try:
            conn.settimeout(2.0)
        except Exception:
            pass
        req = ''
        while string.find(req, '\r\n\r\n') < 0:
            chunk = conn.recv(4096)
            if not chunk:
                break
            req = req + chunk
            if len(req) > 65535:
                break
        sep = string.find(req, '\r\n\r\n')
        if sep < 0:
            self._send_all(conn, 'HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n')
            return
        head = req[:sep]
        body = req[sep + 4:]
        hdrs = string.split(head, '\r\n')
        relates_to = self._extract_http_request_message_id(body)
        resp = self._build_http_metadata_response(relates_to)
        logger.debug('http child sending %d bytes to metadata client' % len(resp))
        self._send_all(conn, resp)

    def fileno(self):
        return self.socket.fileno()


class _HTTPRequestHandler:
    def __init__(self, conn, addr, server):
        self.connection = conn
        self.client_address = addr
        self.server = server
        try:
            self._f = conn.makefile('r')
            self._handle()
        except Exception:
            pass

    def _handle(self):
        line = self._f.readline()
        if not line:
            return
        if line[-2:] == '\r\n':
            line = line[:-2]
        elif line[-1:] == '\n':
            line = line[:-1]
        self.requestline = line
        parts = string.split(line)
        if len(parts) == 3:
            self.command, self.path, self.request_version = parts
        elif len(parts) == 2:
            self.command, self.path = parts
            self.request_version = 'HTTP/0.9'
        else:
            self._send_error(400, 'Bad request')
            return
        self.headers = {}
        while 1:
            hline = self._f.readline()
            if not hline or hline in ('\r\n', '\n'):
                break
            if hline[-2:] == '\r\n':
                hline = hline[:-2]
            elif hline[-1:] == '\n':
                hline = hline[:-1]
            colon = string.find(hline, ':')
            if colon >= 0:
                key = string.lower(string.strip(hline[:colon]))
                val = string.strip(hline[colon+1:])
                self.headers[key] = val
        mname = 'do_' + self.command
        if hasattr(self, mname):
            getattr(self, mname)()
        else:
            self._send_error(501, 'Not implemented')

    def address_string(self):
        return str(self.client_address[0])

    def getheader(self, name, default=None):
        return self.headers.get(string.lower(name), default)

    def send_response(self, code, message=None):
        _msgs = {200: 'OK', 400: 'Bad Request', 404: 'Not Found',
                 500: 'Internal Server Error', 501: 'Not Implemented'}
        if message is None:
            message = _msgs.get(code, 'Unknown')
        self.connection.send('HTTP/1.0 %d %s\r\n' % (code, message))
        self.connection.send('Server: wsdd\r\n')

    def send_header(self, key, value):
        self.connection.send('%s: %s\r\n' % (key, value))

    def end_headers(self):
        self.connection.send('\r\n')

    def send_error(self, code, message=None):
        _msgs = {400: 'Bad Request', 404: 'Not Found',
                 500: 'Internal Server Error', 501: 'Not Implemented'}
        if message is None:
            message = _msgs.get(code, 'Error')
        self.send_response(code, message)
        self.send_header('Content-Type', 'text/plain')
        self.end_headers()
        self.connection.send('%d %s\r\n' % (code, message))

    _send_error = send_error

    def address_string(self):
        return str(self.client_address[0])

    def log_message(self, fmt, *args):
        pass


class WSDHttpServer(_HTTPServer):
    def __init__(self, mch, aio_loop):
        # address_family must be set before TCPServer.__init__ creates the socket
        _HTTPServer.address_family = mch.address.get_family()
        self.mch = mch
        self.aio_loop = aio_loop
        self.wsd_handler = WSDHttpMessageHandler()
        self.registered = 0
        _HTTPServer.__init__(self, mch.listen_address, WSDHttpRequestHandler)

    def server_bind(self):
        if self.mch.address.get_family() == socket.AF_INET6:
            self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
        _HTTPServer.server_bind(self)

    def server_activate(self):
        _HTTPServer.server_activate(self)
        self.registered = 1

    def start_http_child(self):
        # child HTTP accept loop
        try:
            signal.signal(signal.SIGCHLD, signal.SIG_IGN)
            _resp = None
        except Exception:
            _resp = None
        pid = os.fork()
        if pid == 0:
            try:
                signal.signal(signal.SIGIO, signal.SIG_DFL)
                signal.signal(signal.SIGALRM, signal.SIG_DFL)
            except Exception:
                pass
            while 1:
                try:
                    conn, addr = self.socket.accept()
                    try:
                        self._handle_http_child_request(conn, _resp)
                    except Exception:
                        try:
                            self._send_all(conn, 'HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n')
                        except Exception:
                            pass
                    try:
                        conn.close()
                    except Exception:
                        pass
                except Exception:
                    pass
            os._exit(0)

    def _poll_accept(self):
        pass  # unused

    def server_close(self):
        if self.registered:
            self.aio_loop.remove_reader(self.socket)
        _HTTPServer.server_close(self)

    def handle_request(self):
        # _pending_conns stores accepted sockets from the _poll_accept thread
        if not hasattr(self.socket, '_pending_conns'):
            return
        if not self.socket._pending_conns:
            return
        request, client_address = self.socket._pending_conns.pop(0)
        if self.verify_request(request, client_address):
            try:
                self.process_request(request, client_address)
            except Exception:
                self.handle_error(request, client_address)


class WSDHttpRequestHandler(_HTTPRequestHandler):
    def log_message(self, fmt, *a):
        logger.info('%s - - ' % self.address_string() + fmt % a)

    def do_POST(self):
        if self.path != '/' + str(args.uuid):
            self.send_error(404)
            return
        ct = self.getheader('content-type')
        if ct is None or ct[:len(MIME_TYPE_SOAP_XML)] != MIME_TYPE_SOAP_XML:
            self.send_error(400, 'Invalid Content-Type')
            return
        content_length = int(self.getheader('content-length') or '0')
        body = self._f.read(content_length)
        response = self.server.wsd_handler.handle_message(body)
        if response:
            self.send_response(200)
            self.send_header('Content-Type', MIME_TYPE_SOAP_XML)
            self.end_headers()
            self.connection.send(response)
        else:
            self.send_error(400)


# -------------------------------------------------------------------
# API server
# -------------------------------------------------------------------
class ApiServer:
    def __init__(self, aio_loop, listen_address, address_monitor):
        self.clients = []
        self.address_monitor = address_monitor
        self._server_socket = None
        self._stop_event = threading.Event()
        self._thread = _Thread(target=self._serve, args=(listen_address,))
        self._thread.daemon = 1
        self._thread.start()

    def _make_server_socket(self, listen_address):
        if hasattr(listen_address, "fileno") and hasattr(listen_address, "accept"):
            return listen_address
        s = str(listen_address)
        if s.isdigit():
            srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            srv.bind(('localhost', int(s)))
            srv.listen(5)
            return srv
        else:
            if os.path.exists(s):
                os.unlink(s)
            srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
            srv.bind(s)
            srv.listen(5)
            return srv

    def _serve(self, listen_address):
        try:
            self._server_socket = self._make_server_socket(listen_address)
        except Exception, e:
            logger.error('ApiServer: could not create socket: %s' % str(e))
            return
        # NetBSD 1.4: avoid select.select because selectmodule.so may segfault
        # Use non-blocking accept plus a polling loop instead
        _fcntl = None
        _O_NONBLOCK = 0x0004  # NetBSD O_NONBLOCK (FNDELAY)
        try:
            import fcntl
            _fcntl = fcntl
            try:
                import FCNTL
                _O_NONBLOCK = FCNTL.O_NONBLOCK
            except ImportError:
                pass  # keep using the literal constant 0x0004
        except ImportError:
            pass
        try:
            if _fcntl is not None:
                _fd = self._server_socket.fileno()
                _flags = _fcntl.fcntl(_fd, _fcntl.F_GETFL, 0)
                _fcntl.fcntl(_fd, _fcntl.F_SETFL, _flags | _O_NONBLOCK)
        except Exception:
            pass
        _abort = 0
        while not self._stop_event.is_set() and not _abort:
            try:
                time.sleep(0.5)
            except Exception:
                pass
            if self._stop_event.is_set():
                break
            try:
                conn, addr = self._server_socket.accept()
                t = _Thread(target=self.on_connect, args=(conn,))
                t.daemon = 1
                t.start()
            except socket.error:
                # EAGAIN/EWOULDBLOCK: no data available, normal case
                pass
            except Exception, e:
                if not self._stop_event.is_set():
                    logger.warning('ApiServer accept error: %s' % str(e))
                _abort = 1

    def on_connect(self, conn):
        self.clients.append(conn)
        f = conn.makefile('r')
        try:
            while 1:
                line = f.readline()
                if not line:
                    break
                _cmd_ok = 1
                try:
                    self.handle_command(string.strip(line), conn)
                except Exception, e:
                    logger.warning('exception in API client: %s' % str(e))
                    _cmd_ok = 0
                if not _cmd_ok:
                    break
        except Exception:
            pass
        if conn in self.clients:
            self.clients.remove(conn)
        try:
            conn.close()
        except Exception:
            pass

    def handle_command(self, cmd, conn):
        pass

    def cleanup(self):
        self._stop_event.set()
        for client in self.clients:
            try:
                client.close()
            except Exception:
                pass
        if self._server_socket:
            try:
                self._server_socket.close()
            except Exception:
                pass


# -------------------------------------------------------------------
# Base class for network address monitors
# -------------------------------------------------------------------
class NetworkAddressMonitor:
    instance = None

    def __init__(self, aio_loop):
        if NetworkAddressMonitor.instance is not None:
            raise RuntimeError('Instance of NetworkAddressMonitor already created')
        NetworkAddressMonitor.instance = self
        self.interfaces = {}
        self.aio_loop = aio_loop
        self.mchs = []
        self.http_servers = []
        self.teardown_tasks = []
        self.active = 0
        # Metaclass replacement: call enumerate() at the end of __init__
        if not args.no_autostart:
            self.enumerate()

    def enumerate(self):
        if self.active:
            return
        self.active = 1
        self.do_enumerate()

    def do_enumerate(self):
        pass

    def handle_change(self):
        pass

    def add_interface(self, interface):
        idx = interface.get_index()
        if not self.interfaces.has_key(idx):
            self.interfaces[idx] = interface
        return self.interfaces[idx]

    def get_handled_address_families(self):
        if not self.active:
            return {}
        if args.ipv4only:
            return {socket.AF_INET: 1}
        if args.ipv6only:
            return {socket.AF_INET6: 1}
        return {socket.AF_INET: 1, socket.AF_INET6: 1}

    def is_address_handled(self, address):
        if not self.active:
            return 0
        if not address.get_is_multicastable():
            return 0
        if args.interface:
            name = address.get_interface().get_name()
            addr_str = address.get_address_str()
            if name not in args.interface and addr_str not in args.interface:
                return 0
        return 1

    def handle_new_address(self, address):
        logger.debug('new address %s' % str(address))
        if not self.is_address_handled(address):
            logger.debug('ignoring address on %s' % str(address.get_interface()))
            return
        _addr_str = address.get_address_str()
        _addr_fam = address.get_family()
        for mch in self.mchs:
            try:
                if (mch.address.get_address_str() == _addr_str and
                        mch.address.get_family() == _addr_fam):
                    return
            except Exception:
                pass
        logger.debug('handling traffic for %s' % str(address))
        mch = MulticastHandler(address, self.aio_loop)
        self.mchs.append(mch)
        if not args.no_host:
            WSDHost(mch)
            if not args.no_http:
                _hs = WSDHttpServer(mch, self.aio_loop)
                self.http_servers.append(_hs)
                _hs.start_http_child()
        if args.discovery:
            WSDClient(mch)

    def handle_deleted_address(self, address):
        logger.info('deleted address %s' % str(address))
        if not self.is_address_handled(address):
            return
        mch = self.get_mch_by_address(address)
        if mch is None:
            return
        for c in WSDClient.instances[:]:
            if c.mch == mch:
                c.cleanup()
                break
        for h in WSDHost.instances[:]:
            if h.mch == mch:
                h.cleanup()
                break
        for s in self.http_servers[:]:
            if s.mch == mch:
                s.server_close()
                self.http_servers.remove(s)
                break
        mch.cleanup()
        if mch in self.mchs:
            self.mchs.remove(mch)

    def teardown(self):
        if not self.active:
            return
        self.active = 0
        if self.teardown_tasks:
            return
        for h in WSDHost.instances[:]:
            h.teardown()
            h.cleanup()
            self.teardown_tasks.extend(h.pending_tasks)
        for c in WSDClient.instances[:]:
            c.teardown()
            c.cleanup()
            self.teardown_tasks.extend(c.pending_tasks)
        for s in self.http_servers[:]:
            s.server_close()
        self.http_servers = []
        if not self.teardown_tasks:
            return
        if not self.aio_loop.is_running():
            for t in self.teardown_tasks:
                if hasattr(t, '_thread'):
                    t._thread.join(timeout=5)
        else:
            for t in self.teardown_tasks:
                t.add_done_callback(self.mch_teardown)

    def mch_teardown(self, task):
        all_done = 1
        for t in self.teardown_tasks:
            if not t.done():
                all_done = 0
                break
        if not all_done:
            return
        self.teardown_tasks = []
        for mch in self.mchs:
            mch.cleanup()
        self.mchs = []

    def cleanup(self):
        self.teardown()

    def get_mch_by_address(self, address):
        for retval in self.mchs:
            if retval.handles_address(address):
                return retval
        return None


# -------------------------------------------------------------------
# BSD ifconfig polling address monitor (for NetBSD 1.4)
# AF_ROUTE raw sockets are avoided because they may segfault on Python 1.5.2
# -------------------------------------------------------------------
class RouteSocketAddressMonitor(NetworkAddressMonitor):
    POLL_INTERVAL = 5.0   # seconds

    def __init__(self, aio_loop):
        self.intf_blacklist = []
        NetworkAddressMonitor.__init__(self, aio_loop)
        # Poll periodically on a timer
        self.aio_loop.add_timer(self.POLL_INTERVAL, self._poll_tick)
        # Run immediately on the first pass
        self._poll_tick()

    def _kick(self):
        # Timer-based monitor; nothing to do here
        pass

    def _poll_tick(self):
        self._enumerate_via_ifconfig()
        self._schedule_next()

    def _schedule_next(self):
        pass  # not needed because the timer keeps calling it
        try:
            signal.signal(signal.SIGCHLD, signal.SIG_IGN)
        except Exception:
            pass

    def do_enumerate(self):
        NetworkAddressMonitor.do_enumerate(self)
        self._enumerate_via_ifconfig()

    def _enumerate_via_ifconfig(self):
        try:
            fd = os.popen('/sbin/ifconfig -a 2>/dev/null', 'r')
            data = fd.read()
            fd.close()
        except Exception, e:
            logger.error('ifconfig failed: %s' % str(e))
            return

        handled = self.get_handled_address_families()
        current_iface = None
        idx = 0

        for line in string.split(data, '\n'):
            m = re.match(r'^(\w+\d*):', line)
            if m:
                current_iface = m.group(1)
                idx = idx + 1
                intf = NetworkInterface(current_iface, idx, idx)
                self.add_interface(intf)
                if re.search(r'LOOPBACK', line, re.IGNORECASE):
                    if current_iface not in self.intf_blacklist:
                        self.intf_blacklist.append(current_iface)
                continue

            if current_iface is None or current_iface in self.intf_blacklist:
                continue

            intf = None
            for iface_obj in self.interfaces.values():
                if iface_obj.get_name() == current_iface:
                    intf = iface_obj
                    break
            if intf is None:
                continue

            if handled.has_key(socket.AF_INET):
                m = re.match(r'\s+inet\s+(\d+\.\d+\.\d+\.\d+)', line)
                if m:
                    try:
                        addr = NetworkAddress(socket.AF_INET, m.group(1), intf)
                        self.handle_new_address(addr)
                    except Exception, e:
                        logger.debug('IPv4 addr parse error: %s' % str(e))
                    continue

            if handled.has_key(socket.AF_INET6):
                m = re.match(r'\s+inet6\s+([0-9a-fA-F:]+)', line)
                if m:
                    addr_str = string.split(m.group(1), '%')[0]
                    try:
                        addr = NetworkAddress(socket.AF_INET6, addr_str, intf)
                        self.handle_new_address(addr)
                    except Exception, e:
                        logger.debug('IPv6 addr parse error: %s' % str(e))

    def handle_change(self):
        pass

    def cleanup(self):
        NetworkAddressMonitor.cleanup(self)


# -------------------------------------------------------------------
# Argument parsing (using getopt)
# -------------------------------------------------------------------
class _Args:
    pass


def parse_args():
    global args, logger

    shortopts = 'i:H:U:vd:n:w:At46spPc:u:Dl:oV'
    longopts = [
        'interface=', 'hoplimit=', 'uuid=', 'verbose', 'domain=',
        'hostname=', 'workgroup=', 'no-autostart', 'no-http',
        'ipv4only', 'ipv6only', 'shortlog', 'preserve-case',
        'chroot=', 'user=', 'discovery', 'listen=', 'no-host', 'version',
        'metadata-timeout=', 'source-port=',
    ]

    try:
        opts, rest = getopt.getopt(sys.argv[1:], shortopts, longopts)
    except getopt.error, e:
        sys.stderr.write('wsdd: %s\n' % str(e))
        sys.exit(1)
    a = _Args()
    a.interface        = []
    a.hoplimit         = 1
    a.uuid             = None
    a.verbose          = 0
    a.domain           = None
    # socket.gethostname() may call gethostbyname() internally.
    # Use os.popen instead because that path may segfault on NetBSD 1.4.
    try:
        _hn_fd = os.popen('/bin/hostname 2>/dev/null', 'r')
        _hn = string.strip(_hn_fd.read())
        _hn_fd.close()
        if not _hn:
            _hn = 'localhost'
    except Exception:
        _hn = 'localhost'
    a.hostname         = string.split(_hn, '.')[0]
    a.workgroup        = 'WORKGROUP'
    a.no_autostart     = 0
    a.no_http          = 0
    a.ipv4only         = 0
    a.ipv6only         = 0
    a.shortlog         = 0
    a.preserve_case    = 0
    a.chroot           = None
    a.user             = None
    a.discovery        = 0
    a.listen           = None
    a.no_host          = 0
    a.version          = 0
    a.metadata_timeout = 2.0
    a.source_port      = 0

    for opt, val in opts:
        if opt in ('-i', '--interface'):
            a.interface.append(val)
        elif opt in ('-H', '--hoplimit'):
            a.hoplimit = int(val)
        elif opt in ('-U', '--uuid'):
            a.uuid = val
        elif opt in ('-v', '--verbose'):
            a.verbose = a.verbose + 1
        elif opt in ('-d', '--domain'):
            a.domain = val
        elif opt in ('-n', '--hostname'):
            a.hostname = val
        elif opt in ('-w', '--workgroup'):
            a.workgroup = val
        elif opt in ('-A', '--no-autostart'):
            a.no_autostart = 1
        elif opt in ('-t', '--no-http'):
            a.no_http = 1
        elif opt in ('-4', '--ipv4only'):
            a.ipv4only = 1
        elif opt in ('-6', '--ipv6only'):
            a.ipv6only = 1
        elif opt in ('-s', '--shortlog'):
            a.shortlog = 1
        elif opt in ('-p', '--preserve-case'):
            a.preserve_case = 1
        elif opt in ('-c', '--chroot'):
            a.chroot = val
        elif opt in ('-u', '--user'):
            a.user = val
        elif opt in ('-D', '--discovery'):
            a.discovery = 1
        elif opt in ('-l', '--listen'):
            a.listen = val
        elif opt in ('-o', '--no-host'):
            a.no_host = 1
        elif opt in ('-V', '--version'):
            a.version = 1
        elif opt == '--metadata-timeout':
            a.metadata_timeout = float(val)
        elif opt == '--source-port':
            a.source_port = int(val)

    args = a

    if a.version:
        print 'wsdd - Web Service Discovery Daemon, v%s' % WSDD_VERSION
        sys.exit(0)

    if a.verbose == 1:
        log_level = logging.INFO
    elif a.verbose > 1:
        log_level = logging.DEBUG
    else:
        log_level = logging.WARNING

    if a.shortlog:
        fmt = '%(levelname)s: %(message)s'
    else:
        fmt = '%(asctime)s:%(name)s %(levelname)s(pid %(process)d): %(message)s'
    logging.basicConfig(level=log_level, format=fmt)
    logger = logging.getLogger('wsdd')

    if not a.interface:
        logger.warning('no interface given, using all interfaces')

    if not a.uuid:
        def read_uuid_file(fn):
            try:
                f = open(fn, 'r')
                s = string.strip(f.readline())
                f.close()
                return UUID(s)
            except Exception:
                return None
        a.uuid = (read_uuid_file('/etc/machine-id') or
                  read_uuid_file('/etc/hostid') or
                  uuid.uuid1())
        logger.info('using pre-defined UUID %s' % str(a.uuid))
    else:
        a.uuid = UUID(a.uuid)
        logger.info('user-supplied device UUID is %s' % str(a.uuid))


def chroot_dir(root):
    try:
        import encodings.idna
    except ImportError:
        pass
    try:
        os.chroot(root)
        os.chdir('/')
        logger.info('chrooted successfully to %s' % root)
        return 1
    except Exception, e:
        logger.error('could not chroot to %s: %s' % (root, str(e)))
        return 0


def _lookup_passwd_ids(user):
    try:
        import pwd
        pw = pwd.getpwnam(user)
        try:
            return (int(pw.pw_uid), int(pw.pw_gid))
        except AttributeError:
            pass
        try:
            return (int(pw[2]), int(pw[3]))
        except Exception:
            pass
        try:
            pwt = tuple(pw)
            return (int(pwt[2]), int(pwt[3]))
        except Exception:
            pass
    except Exception:
        pass

    try:
        f = open('/etc/passwd', 'r')
        try:
            for line in f.readlines():
                line = string.strip(line)
                if not line or line[0] == '#':
                    continue
                fields = string.split(line, ':')
                if len(fields) >= 4 and fields[0] == user:
                    return (int(fields[2]), int(fields[3]))
        finally:
            f.close()
    except Exception:
        pass

    raise KeyError('user not found: %s' % user)


def _lookup_group_id(group):
    try:
        import grp
        gr = grp.getgrnam(group)
        try:
            return int(gr.gr_gid)
        except AttributeError:
            pass
        try:
            return int(gr[2])
        except Exception:
            pass
        try:
            grt = tuple(gr)
            return int(grt[2])
        except Exception:
            pass
    except Exception:
        pass

    try:
        f = open('/etc/group', 'r')
        try:
            for line in f.readlines():
                line = string.strip(line)
                if not line or line[0] == '#':
                    continue
                fields = string.split(line, ':')
                if len(fields) >= 3 and fields[0] == group:
                    return int(fields[2])
        finally:
            f.close()
    except Exception:
        pass

    raise KeyError('group not found: %s' % group)


def get_ids_from_userspec(user_spec):
    uid = None
    gid = None
    try:
        slash = string.find(user_spec, ':')
        if slash >= 0:
            user  = user_spec[:slash]
            group = user_spec[slash+1:]
        else:
            user  = user_spec
            group = ''
        if user:
            uid, gid = _lookup_passwd_ids(user)
        if group:
            gid = _lookup_group_id(group)
    except Exception, e:
        raise RuntimeError('could not get uid/gid for %s: %s' % (user_spec, str(e)))
    return (uid, gid)


def drop_privileges(uid, gid):
    try:
        if gid is not None:
            os.setgid(gid)
            if hasattr(os, 'setegid'):
                try:
                    os.setegid(gid)
                except Exception, e:
                    logger.debug('setegid unavailable/failed: %s' % str(e))
            logger.debug('switched gid to %s' % str(gid))
        if uid is not None:
            os.setuid(uid)
            if hasattr(os, 'seteuid'):
                try:
                    os.seteuid(uid)
                except Exception, e:
                    logger.debug('seteuid unavailable/failed: %s' % str(e))
            logger.debug('switched uid to %s' % str(uid))
        logger.info('running as %s (%s:%s)' % (str(args.user), str(uid), str(gid)))
    except Exception, e:
        logger.error('dropping privileges failed: %s' % str(e))
        return 0
    return 1


def create_address_monitor(system, aio_loop):
    if system in ('FreeBSD', 'Darwin', 'OpenBSD', 'NetBSD', 'DragonFly'):
        return RouteSocketAddressMonitor(aio_loop)
    else:
        raise NotImplementedError('unsupported OS: ' + system)


def sigterm_handler():
    logger.info('received termination/interrupt signal, tearing down')
    sys.exit(0)


def main():
    global logger, args

    parse_args()

    if args.ipv4only and args.ipv6only:
        logger.error('Listening to no IP address family.')
        return 4

    aio_loop = _FakeEventLoop()
    nm = create_address_monitor(_SYSTEM, aio_loop)

    api_server = None
    if args.listen:
        api_server = ApiServer(aio_loop, args.listen, nm)

    ids = None
    if args.user is not None:
        ids = get_ids_from_userspec(args.user)

    if args.chroot is not None:
        if not chroot_dir(args.chroot):
            return 2

    if ids is not None:
        if not drop_privileges(ids[0], ids[1]):
            return 3

    if args.chroot and (os.getuid() == 0 or os.getgid() == 0):
        logger.warning('chrooted but running as root, consider -u option')

    aio_loop.add_signal_handler(signal.SIGINT, sigterm_handler)
    aio_loop.add_signal_handler(signal.SIGTERM, sigterm_handler)

    try:
        aio_loop.run_forever()
    except SystemExit:
        logger.info('shutting down gracefully...')
        if api_server is not None:
            api_server.cleanup()
        nm.cleanup()
        aio_loop.stop()
    except KeyboardInterrupt:
        logger.info('shutting down gracefully...')
        if api_server is not None:
            api_server.cleanup()
        nm.cleanup()
        aio_loop.stop()
    except Exception:
        logger.exception('error in main loop')

    logger.info('Done.')
    return 0


if __name__ == '__main__':
    sys.exit(main())

