#!/usr/bin/env python2
from __future__ import absolute_import, print_function

import os, sys, time, types, subprocess, signal

import dbus
import logging
import logging.handlers

# cgitb is in python-misc and requires python-pkgutil and python-pydoc
# It is very usefull for analyzing exceptions.
# import cgitb

# who am i
myscript = os.path.basename(__file__)

# Log formating class
class flog:
    # priority strings to be used
    # with the __init__ function
    priorities = {
        'debug':    logging.DEBUG,
        'info':     logging.INFO,
        'warn':     logging.WARNING,
        'warning':  logging.WARNING,
        'error':    logging.ERROR,
        'critical': logging.CRITICAL,
        }
    
    def __init__(self,myscript,facility,priority):
        """
        Initialize for logging

        :param myscript:    The name of the python program script
        :param facility:    The syslog facility, such as daemon or user
        :param priority:    The minimum priority to be printed to the log
        :returns: Nothing
        :raises TBD:        logging class errors.
        """
        name_len = str(len(myscript))
        self.myscript = myscript
        self.log = logging.getLogger(myscript)
        self.handler = logging.handlers.SysLogHandler(address=('/dev/log'),facility=facility)
        self.default_fmt = ' %(levelname)-9s %(name)-' + name_len + 's %(message)s'
        self.verbose_fmt1 = ' %(levelname)-9s %(name)-' + name_len + 's %(threadName)-14s '
        self.verbose_fmt2 = ' %(message)s'
        formatter = logging.Formatter(self.default_fmt)
        self.handler.setFormatter(formatter)
        self.log.setLevel(self.priorities[priority]) # Minimum infolevel to log
        self.log.addHandler(self.handler)
        self.handler.createLock()

    def __default(self,func,*args):
        self.handler.acquire()
        formatter = logging.Formatter(self.default_fmt)
        self.handler.setFormatter(formatter)
        func(*args)
        self.handler.release()

    def setThreshold(self,threshold):
        """
        Change the syslog priority threshold
        
        :param priority:    Character string corresponding to the threshold
        """
        self.handler.acquire()
        self.log.setLevel(self.priorities[threshold]) # Minimum infolevel to log
        self.handler.release()

    def critical(self,*args):
        """
        Prints a variable argument list at critical priority
        
        :returns: logging result
        """
        self.__default(self.log.critical,*args)

    def error(self,*args):
        """
        Prints a variable argument list at error priority
            
        :returns: logging result
        """
        self.__default(self.log.error,*args)

    def warning(self,*args):
        """
        Prints a variable argument list at warning priority
        
        :returns: logging result
        """
        self.__default(self.log.warning,*args)

    #  Python has no notice level!

    def info(self,*args):
        """
        Prints a variable argument list at info priority
        
        :returns: logging result
        """
        self.__default(self.log.info,*args)

    def debug(self,*args):
        """
        Prints a variable argument list at debug priority

        Printing debug includes function name and line
        number.
        
        :returns: logging result
        """
        caller_frame = sys._getframe().f_back
        callerfunc = caller_frame.f_code.co_name + '@' + str(caller_frame.f_lineno);
        callerfunc = callerfunc.ljust(16)
        self.handler.acquire()
        log = logging.getLogger(self.myscript)
        formatter = logging.Formatter(self.verbose_fmt1+callerfunc+self.verbose_fmt2)
        self.handler.setFormatter(formatter)
        log.debug(*args)
        self.handler.release()

# End of log handler

### ~bluezutils.py

iface_base = 'org.bluez'
iface_dev = '{}.Device1'.format(iface_base)
iface_adapter = '{}.Adapter1'.format(iface_base)
iface_props = 'org.freedesktop.DBus.Properties'
global lg

# Will this write to syslog?????
class BTError(Exception): pass

def get_bus():
	bus = getattr(get_bus, 'cached_obj', None)
	if not bus: bus = get_bus.cached_obj = dbus.SystemBus()
	return bus

def get_manager():
	manager = getattr(get_manager, 'cached_obj', None)
	if not manager:
            try:
		manager = get_manager.cached_obj = dbus.Interface(
			get_bus().get_object(iface_base, '/'),
			'org.freedesktop.DBus.ObjectManager' )
            except dbus.exceptions.DBusException as e:
                s=""
                try:
                    s = e.__dict__["_dbus_error_name"]
                except KeyError:
                    pass
                unk = 'org.freedesktop.DBus.Error.ServiceUnknown'
                cexit = 'org.freedesktop.DBus.Error.Spawn.ChildExited'
                if (s == unk) or (s == cexit):
                    msg = 'Is bluetoothd running? Bluetooth tree missing from DBUS'
                    lg.error(msg)
                    print(msg)
                    exit(1)
                else:
                    raise
	return manager

def prop_get(obj, k, iface=None):
	if iface is None: iface = obj.dbus_interface
	return obj.Get(iface, k, dbus_interface=iface_props)
def prop_set(obj, k, v, iface=None):
	if iface is None: iface = obj.dbus_interface
	return obj.Set(iface, k, v, dbus_interface=iface_props)

msg=''
def find_adapter(pattern=None):
        try:
            adapter = find_adapter_in_objects(get_manager().GetManagedObjects(), pattern)
        
        # DBusException
        # Original code:
        #    template = "An exception of type {0} occurred. Arguments:\n{1!r}"
        #    message = template.format(type(ex).__name__, ex.args)
        #    print message
        # dbus.exceptions.DBusException:
        # org.freedesktop.DBus.Error.AccessDenied:
        except dbus.exceptions.DBusException as e:
            s=""
            try:
                s = e.__dict__["_dbus_error_name"]
            except KeyError:
                pass

            if "org.freedesktop.DBus.Error.AccessDenied" == s:
                rot="You do not have sufficient privilege to run bt-pan"
                print(rot)
                lg.error(rot)
                exit(1)
            else:
                raise
  	return adapter

def find_adapter_in_objects(objects, pattern=None):
	bus, obj = get_bus(), None
	for path, ifaces in objects.iteritems():
		adapter = ifaces.get(iface_adapter)
		if adapter is None: continue
		if not pattern or pattern == adapter['Address'] or path.endswith(pattern):
			obj = bus.get_object(iface_base, path)
			yield dbus.Interface(obj, iface_adapter)
	if obj is None:
                msg = 'Bluetooth adapter not found'
		lg.error(msg)
		raise BTError(msg)

def find_device(device_address, adapter_pattern=None):
	return find_device_in_objects(get_manager().GetManagedObjects(), device_address, adapter_pattern)

def find_device_in_objects(objects, device_address, adapter_pattern=None):
	bus = get_bus()
	path_prefix = ''
	if adapter_pattern:
		if not isinstance(adapter_pattern, types.StringTypes): adapter = adapter_pattern
		else: adapter = find_adapter_in_objects(objects, adapter_pattern)
		path_prefix = adapter.object_path
	for path, ifaces in objects.iteritems():
		device = ifaces.get(iface_dev)
		if device is None: continue
		if device['Address'] == device_address and path.startswith(path_prefix):
			obj = bus.get_object(iface_base, path)
			return dbus.Interface(obj, iface_dev)
        msg = 'Bluetooth device not found'
	lg.error(msg)
	raise BTError(msg)

pidPath = ""

def writePidFile(device):
    global pidPath
    pid = str(os.getpid())
    pidPath = '/run/bt-pan.' + device + '.pid'
    f = open(pidPath, 'w')
    f.write(pid)
    f.close()

def hexdump(string):
    return ":".join("{:02x}".format(ord(c)) for c in string)

### bt-pan

def main(args=None):
	import argparse
	global lg

	# Set up logging initially info and above	
	lg = flog(myscript,'daemon','info')

        # cgitb.enable(format='text')
	
	parser = argparse.ArgumentParser(
		description='BlueZ bluetooth PAN network server/client.')

	parser.add_argument('-i', '--device', metavar='local-addr/pattern',
		help='Local device address/pattern to use (if not default).')
	parser.add_argument('-a', '--device-all', action='store_true',
		help='Use all local hci devices, not just default one.'
			' Only valid with "server" mode, mutually exclusive with --device option.')
	parser.add_argument('-u', '--uuid',
		metavar='uuid_or_shortcut', default='nap',
		help='Service UUID to use. Can be either full UUID'
			' or one of the shortcuts: gn, panu, nap. Default: %(default)s.')
	parser.add_argument('--systemd', action='store_true',
		help='Use systemd service'
			' notification/watchdog mechanisms in daemon modes, if available.')
	parser.add_argument('--debug',
		action='store_true', help='Verbose operation mode.')

	cmds = parser.add_subparsers( dest='call',
		title='Supported operations (have their own suboptions as well)' )

	cmd = cmds.add_parser('server', help='Run infinitely as a NAP network server.')
	cmd.add_argument('iface_name',
		help='Bridge interface name to which each link will be added by bluez.'
			' It must be created and configured before starting the server.')

	cmd = cmds.add_parser('client', help='Connect to a PAN network.')
	cmd.add_argument('remote_addr', help='Remote device address to connect to.')
	cmd.add_argument('-d', '--disconnect', action='store_true',
		help='Instead of connecting (default action), disconnect'
			' (if connected) and exit. Overrides all other options for this command.')
	cmd.add_argument('-w', '--wait', action='store_true',
		help='Go into an endless wait-loop after connection, terminating it on exit.')
	cmd.add_argument('-c', '--if-not-connected', action='store_true',
		help='Dont raise error if connection is already established.')
	cmd.add_argument('-r', '--reconnect', action='store_true',
		help='Force reconnection if some connection is already established.')

	opts = parser.parse_args()

        if opts.debug:
            lg.setThreshold('debug')

	if not opts.device_all: devs = [next(iter(find_adapter(opts.device)))]
	else:
		if opts.call != 'server':
			parser.error('--device-all option is only valid with "server" mode.')
		devs = list(find_adapter())
	devs = dict((prop_get(dev, 'Address'), dev) for dev in devs)
	for dev_addr, dev in devs.viewitems():
		prop_set(dev, 'Powered', True)
		lg.debug('Using local device (addr: %s): %s', dev_addr, dev.object_path)

	wait_iter_noop = 3600
	if opts.systemd:
		from systemd import daemon
		def wait_iter():
			if not wait_iter.sd_ready:
				daemon.notify('READY=1')
				daemon.notify('STATUS=Running in {} mode...'.format(opts.call))
				wait_iter.sd_ready = True
			time.sleep(wait_iter.timeout)
			if wait_iter.sd_wdt: daemon.notify('WATCHDOG=1')
		wd_pid, wd_usec = (os.environ.get(k) for k in ['WATCHDOG_PID', 'WATCHDOG_USEC'])
		if wd_pid and wd_pid.isdigit() and int(wd_pid) == os.getpid():
			wd_interval = float(wd_usec) / 2e6 # half of interval in seconds
			assert wd_interval > 0, wd_interval
		else: wd_interval = None
		if wd_interval:
			lg.debug('Initializing systemd watchdog pinger with interval: %ss', wd_interval)
			wait_iter.sd_wdt, wait_iter.timeout = True, min(wd_interval, wait_iter_noop)
		else: wait_iter.sd_wdt, wait_iter.timeout = False, wait_iter_noop
		wait_iter.sd_ready = False
	else: wait_iter = lambda: time.sleep(wait_iter_noop)
	signal.signal(signal.SIGTERM, lambda sig,frm: sys.exit(0))


	if opts.call == 'server':
                inm = opts.iface_name
		brctl = subprocess.Popen(
			['brctl', 'show', inm],
			stdout=open(os.devnull, 'wb'), stderr=subprocess.PIPE )
		brctl_stderr = brctl.stderr.read()
                writePidFile(opts.iface_name)
		if brctl.wait() or brctl_stderr:
			lg.error('brctl check failed for interface (missing?): {}'.format(inm)) 
			lg.error('Bridge interface must be added and configured before starting server, e.g. with:')
			lg.error('  brctl addbr {}'.format(inm))
			lg.error('  brctl setfd {} 0'.format(inm))
			lg.error('  brctl stp {} off'.format(inm))
			lg.error('  ip addr add 10.101.225.84/24 dev {}'.format(inm))
			lg.error('  ip link set {} up'.format(inm))
			return 1

		servers = list()
		try:
			for dev_addr, dev in devs.viewitems():
				server = dbus.Interface(dev, 'org.bluez.NetworkServer1')
				server.Unregister(opts.uuid) # in case already registered
				server.Register(opts.uuid, opts.iface_name)
				servers.append(server)
				lg.debug( 'Registered uuid %r with'
					' bridge/dev: %s / %s', opts.uuid, opts.iface_name, dev_addr )
			while True: wait_iter()
		except KeyboardInterrupt: pass
		finally:
			if servers:
				for server in servers: server.Unregister(opts.uuid)
				lg.debug('Unregistered server uuids')


	elif opts.call == 'client':
		dev_remote = find_device(opts.remote_addr, devs.values()[0])
		lg.debug( 'Using remote device (addr: %s): %s',
			prop_get(dev_remote, 'Address'), dev_remote.object_path )
		try: dev_remote.ConnectProfile(opts.uuid)
		except: pass # no idea why it fails sometimes, but still creates dbus interface
		net = dbus.Interface(dev_remote, 'org.bluez.Network1')

		if opts.disconnect:
			try: net.Disconnect()
			except dbus.exceptions.DBusException as err:
				if err.get_dbus_name() != 'org.bluez.Error.Failed': raise
				connected = prop_get(net, 'Connected')
				if connected: raise
			lg.debug(
				'Disconnected from network'
					' (dev_remote: %s, addr: %s) uuid %r, by explicit command',
				dev_remote.object_path, prop_get(dev_remote, 'Address'), opts.uuid )
			return

		for n in xrange(2):
			try: iface = net.Connect(opts.uuid)
			except dbus.exceptions.DBusException as err:
				if err.get_dbus_name() != 'org.bluez.Error.Failed': raise
				connected = prop_get(net, 'Connected')
				if not connected: raise
				if opts.reconnect:
					lg.debug( 'Detected pre-established connection'
						' (iface: %s), reconnecting', prop_get(net, 'Interface') )
					net.Disconnect()
					continue
				if not opts.if_not_connected: raise
			else: break
		lg.debug(
			'Connected to network (dev_remote: %s, addr: %s) uuid %r with iface: %s',
			dev_remote.object_path, prop_get(dev_remote, 'Address'), opts.uuid, iface )

		if opts.wait:
			try:
				while True: wait_iter()
			except KeyboardInterrupt: pass
			finally:
				net.Disconnect()
				lg.debug('Disconnected from network')


	else: raise ValueError(opts.call)
        global pidPath
	try:
        	os.remove(pidPath)
        except OSError:
		pass
	lg.debug('Finished')

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