#!/usr/bin/python3 -u

# autojack - Monitors dbus for added audio devices (hot plugged USB audio intefaces)
# on detect it does one of three things:
# makes it the jack device
# makes it a jack client (via zita-ajbridge)
# nothing
#
# Monitors acpi for headphone un/plug  and reroutes audio if needed
#
# autojack also monitors dbus for messages from studio-controls to:
# - stop jack
# - re/start jack
# - remove a USB device as jack master to allow safe device removal
# - reread ~/.config/autojackrc and apply any changes

global name_base
global control_interface_name
global configure_interface_name
global service_name
name_base = 'org.jackaudio'
control_interface_name = name_base + '.JackControl'
configure_interface_name = name_base + '.Configure'
service_name = name_base + '.service'

import os
from os.path import expanduser
import re
import time
from gi.repository import GLib
import dbus
import dbus.mainloop.glib
import subprocess
import glob
import shlex
import signal
#import configparser
import logging
import jack
import alsaaudio
import json
import copy

def get_dev_info(x):
    '''uses audio device number to make add a device structure with device
    info to the config database

        devicename
            number (-1 for configured devices not plugged in)
            usb
            internal
            firewire
            sub:
                number (not all are used) string (str(int))
                    playback bool
                    play_pid int
                    play_chan int (config)
                    capture bool
                    cap_pid int
                    cap_chan int (config)
                    rate: int (config)
                    frame: int (config)
                    nperiods: int (config)
                    hide: bool (config)
    this call adds to the config parts if they exist


    '''

    global conf_db
    global fw_exists
    fw_exists = False
    cname = ""
    sub = 0
    if os.path.exists(f"/proc/asound/card{str(x)}"):
        if os.path.isfile(f"/proc/asound/card{str(x)}/id"):
            with open(f"/proc/asound/card{str(x)}/id", "r") as card_file:
                for line in card_file:
                    # only need one line
                    cname = line.rstrip()
        else:
            cname = str(x)
    if cname in conf_db['devices']:
        conf_db['devices'][cname]['number'] = str(x)
    else:
        conf_db['devices'][cname] = {'number': str(x)}
    device = conf_db['devices'][cname]

    #if os.path.exists(f"/proc/asound/card{str(x)}/usbbus"):
    #    device['usb'] = True
    device['usb'] = os.path.exists(f"/proc/asound/card{str(x)}/usbbus")
    device['internal'] = os.path.exists(f"/proc/asound/card{str(x)}/codec#0")
    device['firewire'] = os.path.exists(f"/proc/asound/card{str(x)}/firewire")
    device['hdmi'] = False
    if cname == "HDMI" or cname == "NVidia":
        device['hdmi'] = True

    fw_exists = device['firewire']
    if not 'sub' in device:
        device['sub'] = {}

    for y in range(0, 20):

        subdevice = {}
        cap = False
        cap_pid = 0
        play = False
        play_pid = 0
        if os.path.exists(f"/proc/asound/card{str(x)}/pcm{str(y)}p"):
            play = True
            if os.path.exists(f"/proc/asound/card{str(x)}/pcm{str(y)}p/sub0"):
                with open(f"/proc/asound/card{str(x)}/pcm{str(y)}p/sub0/status", "r") as info_file:
                    for line in info_file:
                        if re.match("^owner_pid", line.rstrip()):
                            play_pid = int(line.rstrip().split(": ", 1)[1])

        if os.path.exists(f"/proc/asound/card{str(x)}/pcm{str(y)}c"):
            cap = True
            if os.path.exists(f"/proc/asound/card{str(x)}/pcm{str(y)}c/sub0"):
                with open(f"/proc/asound/card{str(x)}/pcm{str(y)}c/sub0/status", "r") as info_file:
                    for line in info_file:
                        if re.match("^owner_pid", line.rstrip()):
                            cap_pid = int(line.rstrip().split(": ", 1)[1])

        if play or cap:
            if not str(y) in device['sub']:
                device['sub'][str(y)] = {'playback': True, 'capture': True,
                        'play-chan': 0, 'cap-chan': 0,
                        'rate': conf_db['jack']['rate'], 'frame': conf_db['jack']['frame'],
                        'nperiods': conf_db['jack']['period'], 'hide': False}

            device['sub'][str(y)]['playback'] = play
            device['sub'][str(y)]['capture'] = cap
            device['sub'][str(y)]['play-pid'] = play_pid
            device['sub'][str(y)]['cap-pid'] = cap_pid
    logging.log(7, f"Device scan found: {str(cname)}")
    #logging.log(7, json.dumps(device, indent = 4))
    #logging.log(7, f"Device list: {str(device)}")


def import_device_array():
    '''creates an array of device structures as per get_dev_info(), from
    device 0 to the highest number device currently on the system. Missing
    device numbers will still save a space in the case a USB device has
    been removed and there is still a higher number so that array index
    can be the same as the device number.'''
    global conf_db
    devices = conf_db['devices']
    ndevs = 0
    for this_dev in devices:
        # we need these even for unplugged devices
        devices[this_dev]['firewire'] = False
        for this_sub in devices[this_dev]['sub']:
            devices[this_dev]['sub'][this_sub]['play-pid'] = 0
            devices[this_dev]['sub'][this_sub]['cap-pid'] = 0

    if os.path.exists("/proc/asound/cards"):
        with open("/proc/asound/cards", "r") as cards_file:
            for line in cards_file:
                # need to find lines with:space/int/space[
                # ndevs = int from above
                # last one is highest dev number
                sub = line.rstrip()[1:]
                sub2 = sub.split(" ")
                if sub2[0].isdigit():
                    ndevs = int(sub2[0])
    ndevs += 1
    for x in range(0, ndevs):
        # card loop
        get_dev_info(x)


def extra_devices():
    ''' set up all extra device as per configuration '''
    global conf_db
    global phones
    logging.debug("updating extra devices")

    if not conf_db['jack']['on']:
        return
        # no use checking anything else
    import_device_array()
    # a few loops
    for dev in conf_db['devices']:
        if int(conf_db['devices'][dev]['number']) > -1:
            usb = bool(conf_db['devices'][dev]['usb'] and conf_db['extra']['usbauto'])
            for sub in conf_db['devices'][dev]['sub']:
                fullname = f"{dev},{sub},0"
                sub_db = conf_db['devices'][dev]['sub'][sub]
                if sub_db['hide']:
                    if sub_db['cap-pid'] or sub_db['play-pid']:
                        kill_slave(fullname)
                    break
                if fullname == conf_db['jack']['dev'] or fullname == conf_db['jack']['usbdev']:
                    break
                if usb and not (sub_db['play-chan'] or sub_db['cap-chan']):
                    if not sub_db['cap-pid'] or not sub_db['play-pid']:
                        start_slave(fullname)
                    break
                if fullname == conf_db['extra']['phone-device']:
                    if phones:
                        if not sub_db['play-pid']:
                            start_slave(fullname)
                            break
                if sub_db['play-chan'] or sub_db['cap-chan']:
                    if not sub_db['cap-pid'] or not sub_db['play-pid']:
                        start_slave(fullname)
                        break
                else:
                    if sub_db['cap-pid'] or sub_db['play-pid']:
                        kill_slave(fullname)
    logging.debug("updating extra devices complete")


def import_config(caller):
    ''' sets default parmeters, then reads values from configuration file'''
    global config
    global config_path
    global config_file
    global conf_db

    logging.log(7, f"import_config called by: {caller}")

    config_path = "~/.config/autojack"
    config_file = f"{config_path}/autojack.json"

    default_device = "0,0,0"
    for adir in glob.glob("/proc/asound/card*/codec*"):
        idfile = adir.rsplit('/', 1)[0]
        with open(f"{idfile}/id", "r") as card_file:
            for line in card_file:
                # only need one line
                tmpname = line.rstrip()
            if tmpname != "HDMI" and tmpname != "NVidia":
                default_device = f"{tmpname},0,0"

    # read in autojack config file
    c_file = expanduser(config_file)
    if not os.path.isfile(c_file):
        # no config file, make one
        cp = subprocess.run(["/usr/bin/convert-studio-controls"],
            universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"convert config file: {cp.stdout.strip()}")

    # config file exists, read it in
    with open(c_file) as f:
        conf_db = json.load(f)
    #logging.log(7, json.dumps(conf_db, indent = 4))
    # print(json.dumps(conf_db, indent = 4))

    for pout in conf_db['pulse']['outputs']:
        if conf_db['pulse']['outputs'][pout]['connection'] == "monitor":
            conf_db['pulse']['outputs'][pout]['connection'] = conf_db['extra']['monitor']

    logger = logging.getLogger()
    logger.setLevel(int(conf_db['log-level']))
    logging.log(7, f"log level: {str(conf_db['log-level'])}")
    import_device_array()
    logging.log(7, json.dumps(conf_db, indent = 4))


def reconfig():
    '''reads values from configuration file and changes run to match. This tries
    to do this without stopping jack if not needed'''
    global phones
    global conf_db
    global last_master
    logging.debug("reconfigure autojack")

    old_conf_db = copy.deepcopy(conf_db)
    import_config("reconfig")
    ol_jkdb = old_conf_db['jack']
    jackdb = conf_db['jack']

    old_jack = [ol_jkdb['on'], ol_jkdb['driver'], ol_jkdb['dev'],
            ol_jkdb['rate'], ol_jkdb['frame'], ol_jkdb['period'],
            ol_jkdb['connect-mode'], ol_jkdb['chan-in'], ol_jkdb['chan-out']]
    new_jack = [jackdb['on'], jackdb['driver'], jackdb['dev'],
            jackdb['rate'], jackdb['frame'], jackdb['period'],
            jackdb['connect-mode'], jackdb['chan-in'], jackdb['chan-out']]
    if old_jack != new_jack:
        logging.debug("change requires restart")
        config_start()
        return
    if ol_jkdb['usbdev'] != jackdb['usbdev']:
        if ol_jkdb['usbdev'] == last_master:
            logging.debug("change requires restart")
            config_start()
            return
        usb_name = jackdb['usbdev'].split(',')[0]
        if jackdb['usbdev'] != 'none' and conf_db['devices'][usb_name]['number'] != -1:
            logging.debug("change requires restart")
            config_start()
            return
    if not conf_db['jack']['on']:
        return
        # no use checking anything else

    pulse_dirty = False
    if old_conf_db['pulse']['inputs'] != conf_db['pulse']['inputs']:
        logging.debug("reconfiguring pulse bridges")
        pulse_dirty = True
        # next line needs to be changed to use old_conf_db
        disconnect_pa(old_conf_db)
        cp = subprocess.run(["/usr/bin/pactl", "unload-module", "module-jack-source"],
                            universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"remove jackd_source: {cp.stdout.strip()}")
        for bridge in conf_db['pulse']['inputs']:
            # this should get changed to a global pulse bool or removed
            this_count = str(conf_db['pulse']['inputs'][bridge]['count'])
            cp = subprocess.run(["/usr/bin/pactl", "load-module", "module-jack-source", f"client_name={bridge}",
                                f"channels={this_count}", "connect=no"],
                                universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
            logging.debug(f"Load jackd_source: {cp.stdout.strip()}")

    if old_conf_db['pulse']['outputs'] != conf_db['pulse']['outputs']:
        logging.debug("reconfiguring pulse bridges")
        if not pulse_dirty:
            pulse_dirty = True
            # next line needs to be changed to use old_conf_db
            disconnect_pa(old_conf_db)
        cp = subprocess.run(["/usr/bin/pactl", "unload-module", "module-jack-sink"],
                            universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"PA unload jack-sink: {cp.stdout.strip()}")
        for bridge in conf_db['pulse']['outputs']:
        #for i, poname in enumerate(pulse_out):
            # this should get changed to a global pulse bool or removed
            this_count = str(conf_db['pulse']['outputs'][bridge]['count'])
            cp = subprocess.run(["/usr/bin/pactl", "load-module", "module-jack-sink", f"client_name={bridge}",
                            f"channels={this_count}", "connect=no"], universal_newlines=True, stdout=subprocess.PIPE,
                            stderr=subprocess.STDOUT, shell=False)
            logging.debug(f"PA load jack-sink: {cp.stdout.strip()}")

    if old_conf_db['extra']['a2j'] != conf_db['extra']['a2j']:
        logging.debug("reconfiguring MIDI bridge")
        cp = subprocess.run(["/usr/bin/killall", "-9", "a2jmidid"],
                            universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"kill old a2j: {cp.stdout.strip()}")
        if conf_db['extra']['A2J']:
            # Can't add logging for background processes. Will show up in syslog
            subprocess.Popen(["/usr/bin/a2jmidid", "-e"], shell=False).pid

    # extra devices compares what is to config and fixes things
    logging.debug("Check extra devices")
    extra_devices()

    if conf_db['extra']['phone-device'] != old_conf_db['extra']['phone-device']:
        logging.debug("reconfigure Headphones setup")
        phones = False
        phones_check()
    if conf_db['extra']['monitor'] != "system:playback_1" and not phones:
        logging.debug("Check main outputs")
        mon_dev, ldev, temp = conf_db['extra']['monitor'].split('-')
        if not conf_db['devices'][mon_dev]['sub'][ldev] and conf_db['jack']['driver']  != "alsa" or mon_dev != last_master:
            start_slave(mon_dev)
            con_dirty = True

    if pulse_dirty:
        #do this last after all bridges are created
        connect_pa()


def config_start():
    ''' Pulls configuration and force restarts the world '''
    global last_master
    global conf_db
    global fw_exists
    global phones
    global name_base
    global control_interface_name
    global configure_interface_name
    global service_name
    global jack_died

    jack_died = True

    logging.info("Running: config_start()")

    import_config("config_start")

    # if at session start we should wait a few seconds for pulse
    # to be fully running
    time.sleep(2)
    # Stop jack if running
    cp = subprocess.run(["/usr/bin/killall", "-q", "-9", "jackdbus", "jackd", "a2jmidid"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"Kill old Procs: {cp.stdout.strip()}")
    if (conf_db['jack']['driver'] == "firewire") or fw_exists:
        time.sleep(3)
        # this is a firewire device, A busreset makes for stability
        cp = subprocess.run(["/usr/bin/ffado-test", "BusReset"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"reset firewire bus: {cp.stdout.strip()}")
        time.sleep(3)
        cp = subprocess.run(["/usr/bin/killall", "-q", "ffado-dbus-server", "ffado-mixer"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Kill ffado mixer because busreset needed: {cp.stdout.strip()}")


    if not conf_db['jack']['on']:
        # restart Pulse
        cp = subprocess.run(["/usr/bin/pulseaudio", "-k"],
                    universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"JACK is {str(conf_db['jack']['on'])} restart pulse: {cp.stdout.strip()}")
        return
    logging.debug(f"JACK is {str(conf_db['jack']['on'])} start up jack")

    # Assume start of session where pulse may be fully loaded
    # get rid of anything that can automatically interfere
    cp = subprocess.run(["/usr/bin/pactl", "unload-module", "module-jackdbus-detect"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"remove jackdbus_detect: {cp.stdout.strip()}")
    cp = subprocess.run(["/usr/bin/pactl", "unload-module", "module-udev-detect"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"remove module-udev-detect: {cp.stdout.strip()}")
    cp = subprocess.run(["/usr/bin/pactl", "unload-module", "module-alsa-card"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"remove module-alsa-card: {cp.stdout.strip()}")
    if os.path.exists(f"/proc/asound/{conf_db['jack']['usbdev'].split(',')[0]}") and conf_db['jack']['usbdev'] != "":
        mdev = conf_db['jack']['usbdev']
    else:
        mdev = conf_db['jack']['dev']
        if mdev.split(',')[0] == "HDMI" or mdev.split(',')[0] == "NVidia":
            logging.info("HDMI device, setting buffer to 4096")
            conf_db['jack']['frame'] = "4096"

    if os.path.isfile(expanduser('~/.config/jack/conf.xml')):
        logging.debug("Found previous jack config removing")
        os.remove(expanduser('~/.config/jack/conf.xml'))

    bus = dbus.SessionBus()

    controller = bus.get_object(service_name, "/org/jackaudio/Controller")
    control_iface = dbus.Interface(controller, control_interface_name)
    configure_iface = dbus.Interface(controller, configure_interface_name)

    # Now start jackdbus with the configured device
    if conf_db['jack']['connect-mode'] != 'n':
        configure_iface.SetParameterValue(['engine', 'self-connect-mode'],
            dbus.Byte(ord(conf_db['jack']['connect-mode'])))

    if conf_db['jack']['driver'] == "firewire":
        cp = subprocess.run(["/usr/bin/ffado-test", "BusReset"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"reset firewire bus: {cp.stdout.strip()}")
        cp = subprocess.run(["/usr/bin/killall", "-q", "ffado-dbus-server", "ffado-mixer"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Kill ffado mixer because busreset needed: {cp.stdout.strip()}")
        time.sleep(3)

    configure_iface.SetParameterValue(['engine', 'driver'], conf_db['jack']['driver'])
    time.sleep(.2)
    if conf_db['jack']['driver'] == "alsa":
        # we use default device for FW right now
        configure_iface.SetParameterValue(['driver', 'device'], f"hw:{mdev}")
    time.sleep(.2)
    configure_iface.SetParameterValue(['driver', 'rate'], dbus.UInt32(conf_db['jack']['rate']))
    time.sleep(.2)
    configure_iface.SetParameterValue(['driver', 'period'], dbus.UInt32(conf_db['jack']['frame']))
    if conf_db['jack']['driver'] != "dummy":
        configure_iface.SetParameterValue(['driver', 'nperiods'],
            dbus.UInt32(conf_db['jack']['period']))

    else:
        configure_iface.SetParameterValue(['driver', 'capture'],
            dbus.UInt32(conf_db['jack']['chan-in']))
        configure_iface.SetParameterValue(['driver', 'playback'],
            dbus.UInt32(conf_db['jack']['chan_out']))
        configure_iface.SetParameterValue(['driver', 'monitor'], True)
    time.sleep(2)

    control_iface.StartServer()

    last_master = mdev
    # maybe check for jack up (need function?)
    time.sleep(2)

    start_jack_client()

    for bridge in conf_db['pulse']['inputs']:
        this_count = str(conf_db['pulse']['inputs'][bridge]['count'])
        cp = subprocess.run(["/usr/bin/pactl", "load-module", "module-jack-source",
                f"client_name={bridge}", f"channels={this_count}", "connect=no"],
                universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Pulseaudio: {cp.stdout.strip()}")
    for bridge in conf_db['pulse']['outputs']:
        this_count = str(conf_db['pulse']['outputs'][bridge]['count'])
        cp = subprocess.run(["/usr/bin/pactl", "load-module", "module-jack-sink",
                f"client_name={bridge}", f"channels={this_count}", "connect=no"],
                universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Pulseaudio: {cp.stdout.strip()}")

    extra_devices()

    # check if phones device exists
    ph_dev = conf_db['extra']['phone-device'].split(',')[0]
    if not ph_dev in conf_db['devices']:
        phones = False
    if phones:
        if conf_db['extra']['phone-device'] != last_master:
            start_slave(conf_db['extra']['phone-device'])
            con_dirty = True
    elif conf_db['extra']['monitor'] != "system:playback_1":
        mon_dev = conf_db['extra']['monitor'].split('-')[0]
        if not mon_dev in zdev and (conf_db['jack']['driver'] != "alsa" or mon_dev != last_master):
            start_slave(mon_dev)
            con_dirty = True

    # not sure all these delays need to be here. Was checking with old pulse.
    # this has to be last after all audio bridges are created
    time.sleep(2)
    connect_pa()

    if conf_db['extra']['a2j']:
        # Can't set up logging
        cmd = "/usr/bin/a2jmidid -e"
        subprocess.Popen(shlex.split(cmd), shell=False).pid

def start_jack_client():
    ''' Create a jack client for monitoring and changing port connections'''
    global jack_client
    global jack_died
    global jack_alive
    global con_dirty
    logging.debug("create jack client")
    jack.set_error_function(callback=jack_error)
    jack.set_info_function(callback=jack_info)
    try:
        jack_client = jack.Client('AutoJack', use_exact_name=False, no_start_server=True)
    except jack.JackError:
        logging.warning("Unable to create jack client")
        jack_died = True
        return
    jack_client.set_shutdown_callback(jackdied)
    jack_client.set_port_connect_callback(jack_con_det)
    jack_client.activate()

    jack_died = False
    jack_alive = True
    con_dirty = True
    logging.debug("jack client created")


def jackdied(state, why):
    '''gets called by jack if it is exiting, we can't clean up
    here... so tell the world jack died with a flag instead'''
    global jack_died
    jack_died = True
    logging.debug("jack died callback")


def jack_con_det(a, b, connect):
    ''' a port has had a connection check if one of the ports is monitor L/R
        if so and phones == True move to phones port'''
    global con_dirty
    global conf_db
    if connect and (phones or (conf_db['extra']['monitor'] != "system:playback_1")):
        con_dirty = True


def check_jack_status(user_data):
    '''Check if jack has died and the client needs to be
    closed.'''
    global jack_client
    global jack_alive
    global jack_died
    global jack_count
    global con_dirty
    global phone_port
    jack_count = jack_count + 1
    if jack_count == 200:
        logging.log(7, f"Jack_status: {str(jack_alive)}")
        jack_count = 0

        logfile = expanduser("~/.log/autojack.log")
        if os.path.isfile(logfile) and os.path.getsize(logfile) > 200000:
            logging.debug("Log File getting large: rotate")
            for old_idx in range(5, -1, -1):
                if old_idx:
                    if os.path.isfile(f"{logfile}.{str(old_idx)}"):
                        os.replace(f"{logfile}.{str(old_idx)}", f"{logfile}.{str(old_idx + 1)}")
                else:
                    logging.shutdown()
                    os.replace(logfile, f"{logfile}.1")
                    logging.basicConfig(filename=logfile, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
                                        level=conf_db['log-level'])
            logging.debug("Log File rotated")
        logfile = expanduser("~/.log/jack/jackdbus.log")
        if os.path.isfile(logfile) and os.path.getsize(logfile) > 200000:
            logging.debug("JACK log File getting large: rotate")
            for old_idx in range(5, -1, -1):
                if old_idx:
                    if os.path.isfile(f"{logfile}.{str(old_idx)}"):
                        os.replace(f"{logfile}.{str(old_idx)}", f"{logfile}.{str(old_idx + 1)}")
                else:
                    os.replace(logfile, f"{logfile}.1")
            logging.debug("JACK Log File rotated")

    if con_dirty and jack_alive:
        logging.debug("Connection changed")
        if phones:
            new_l = phone_port
            logging.debug(f"change it to phones port: {new_l}")
        else:
            new_l = conf_db['extra']['monitor']
        switch_outputs("system:playback_1", new_l)
        con_dirty = False

    # sent by jackdied() callback
    if jack_died:
        jack_client.deactivate()
        jack_client.close()
        jack_died = False
        jack_alive = False
        logging.debug("jack client closed and cleaned up")
    return True


def switch_outputs(left_old, left_new):
    '''finds any outputs connected to a_dev and moves them to b_dev.
    Devices are jack client names including the first port of the stereo
    pair. example: system:playback_1'''
    global jack_client
    global jack_alive
    logging.debug(f"moving connections from {str(left_old)} to {str(left_new)}")
    if not jack_alive:
        logging.debug("Jack not running")
        return
    if left_old == left_new:
        logging.debug("both ports are the same, no move done")
        return
    right_old = get_next_port(left_old)
    right_new = get_next_port(left_new)
    l_old_con = jack_client.get_all_connections(left_old)
    logging.log(8, f"old left connections {str(l_old_con)}")
    # first connect all output to new device
    for raw_p in l_old_con:
        s_port = str(raw_p).split("'")[1]
        jack_client.connect(s_port, left_new)
    r_old_con = jack_client.get_all_connections(right_old)
    logging.log(8, f"old right connections {str(r_old_con)}")
    for raw_p in r_old_con:
        s_port = str(raw_p).split("'")[1]
        jack_client.connect(s_port, right_new)
    # then disconnect old ones
    for raw_p in l_old_con:
        s_port = str(raw_p).split("'")[1]
        jack_client.disconnect(s_port, left_old)
    for raw_p in r_old_con:
        s_port = str(raw_p).split("'")[1]
        jack_client.disconnect(s_port, right_old)


def get_next_port(left_port_ask):
    ''' given a port, this returns the jack port of the next port
    or for a mono device the same port so that it is connected to both
    left and write'''
    global jack_client
    global jack_alive
    if not jack_alive:
        return ""

    right = False
    ''' For firewire backend, the ports are labeled firewire_pcm:something<num>_in or _out
        but have an alias of system:<direction>_<num>. We have to find the real port name
        for left_port '''
    left_port = jack_client.get_port_by_name(left_port_ask)
    logging.debug(f"Real left port name: {str(left_port.name)}")
    right_port = left_port.name
    if left_port.is_input:
        ports = jack_client.get_ports(f"{left_port.name.split(':')[0]}*", is_audio=True, is_input=True)
    else:
        ports = jack_client.get_ports(f"{left_port.name.split(':')[0]}*", is_audio=True, is_output=True)
    for next_port in ports:
        if next_port.name == left_port.name:
            logging.log(8, f"{str(next_port.name)} == {str(left_port.name)}")
            right = True
        elif right:
            right_port = next_port.name
            logging.log(8, f"Right port: {str(right_port)}")
            break
    return right_port


def connect_pa():
    '''connects pulse ports to the correct device ports. May have to
    use zita-ajbridge to first make the correct device available.'''
    global conf_db
    global jack_client
    global jack_alive
    if not jack_alive:
        logging.debug("Can't connect pulse, jack not running")
        return
    logging.debug("connect pulse bridges")

    for bridge in conf_db['pulse']['inputs']:
        connection = conf_db['pulse']['inputs'][bridge]['connection']
        if connection != 'none':
            prevport = ""
            nextport = connection
            pulselist = jack_client.get_ports(f"{bridge}*", is_audio=True, is_input=True)
            for pport in pulselist:
                if nextport != prevport:
                    logging.debug(f"jack connect {nextport} to {pport.name}")
                    jack_client.connect(nextport, pport)
                    prevport = nextport
                    nextport = get_next_port(prevport)

    for bridge in conf_db['pulse']['outputs']:
        connection = conf_db['pulse']['outputs'][bridge]['connection']
        if connection != 'none':
            prevport = ""
            nextport = connection
            if connection == "monitor":
                nextport = conf_db['extra']['MONITOR']
            pulselist = jack_client.get_ports(f"{bridge}*", is_audio=True, is_output=True)
            for pport in pulselist:
                if nextport != prevport:
                    logging.debug(f"jack connect {pport.name} to {nextport}")
                    jack_client.connect(pport, nextport)
                    prevport = nextport
                    nextport = get_next_port(prevport)


def disconnect_pa(our_config):
    '''disconnect Pulse ports we know we have connected.
    The pa-jack bridge is left running. Leave other connections
    alone.'''
    global jack_client
    global jack_alive
    if not jack_alive:
        logging.debug("Jack not running")
        return
    logging.debug("disconnect pulse bridges")
    p_in_db = our_config['pulse']['inputs']
    p_out_db = our_config['pulse']['outputs']
    for bridge in p_in_db:
        if p_in_db[bridge]['connection'] != 'none':
            prevport = ""
            nextport = p_in_db[bridge]['connection']
            pulselist = jack_client.get_ports(f"{bridge}*", is_audio=True, is_input=True)
            for pport in pulselist:
                if nextport != prevport:
                    logging.debug(f"jack disconnect {nextport} from {pport.name}")
                    jack_client.disconnect(nextport, pport)
                    prevport = nextport
                    nextport = get_next_port(prevport)

    for bridge in p_out_db:
        if p_out_db[bridge]['connection'] != 'none':
            prevport = ""
            if p_out_db[bridge]['connection'] == "monitor":
                nextport = our_config['extra']['monitor']
            else:
                nextport = p_out_db[bridge]['connection']
            pulselist = jack_client.get_ports(f"{bridge}*", is_audio=True, is_output=True)
            for pport in pulselist:
                if nextport != prevport:
                    logging.debug(f"jack disconnect {pport.name} from {nextport}")
                    jack_client.disconnect(pport, nextport)
                    prevport = nextport
                    nextport = get_next_port(prevport)


def msg_cb_new(*args, **kwargs):
    '''call back for udev sensing new device. checks if device is audio.
    checks if device is USB. If both are true and configuration is to
    use this device, the device is either connected with zita-ajbridge
    or becomes jack's master device'''
    global last_master
    global conf_db

    if not conf_db['jack']['on']:
        # then we don't care
        return
    if not conf_db['extra']['usbauto']:
        #do nothing
        return

    # let things settle
    time.sleep(2)

    if args[0].find("sound-card") >= 0:
        # remake database
        import_device_array()
        a_if = args[0].split("sound-card", 1)
        audio_if = a_if[1].split(".", 1)[0]
        dev_db = {}
        for dev_name in conf_db['devices']:
            #print(f"devdb number: {conf_db['devices'][dev_name]['number']} audioif: {audio_if}")
            if str(conf_db['devices'][dev_name]['number']) == audio_if:
                dev_db = conf_db['devices'][dev_name]
                # make sure device is USB and is not midi only
                if dev_db['usb'] and len(dev_db['sub']):
                    # tell gui devicelist has changed so it can up date device dropdowns
                    subprocess.run(["/usr/bin/dbus-send", "--type=signal", "/", "org.studio.control.event.usb_signal"],
                                   shell=False)
                    logging.debug(f"USB device {dev_name} has been plugged in")
                    # why are we importing config again?
                    #import_config("msg_cb_new")

                    for subn in dev_db['sub']:
                        subname = f"{dev_name},{str(subn)},0"
                        if subname == last_master:
                            # spurious indication, ignore
                            logging.debug(f" We already have that device - spurious signal, ignoring")
                            return
                        sub_db = dev_db['sub'][subn]
                        logging.debug(f" device = {subname} play:{str(sub_db['playback'])}"
                                    f" capture:{str(sub_db['capture'])}")

                        if (conf_db['jack']['driver'] == "alsa") and (conf_db['jack']['usbdev'] == subname):
                            # change_jack_master(cid, "sm")
                            logging.debug(f"Changing jack master to: {subname}")
                            config_start()
                            return
                        elif conf_db['extra']['usbauto']:
                            start_slave(subname)
                            if subname == conf_db['extra']['phone-device']:
                                phones_switch(True)


def msg_cb_removed(*args, **kwargs):
    ''' dbus call back when a USB device removal has been detected by udev '''
    global last_master
    global conf_db
    global jack_died

    if not conf_db['jack']['on']:
        # then we don't care
        return

    # let things settle
    time.sleep(.5)

    if args[0].find("sound-card") >= 0:
        # tell gui devicelist has changed
        subprocess.run(["/usr/bin/dbus-send", "--type=signal", "/", "org.studio.control.event.usb_signal"],
                       shell=False)

        a_if = args[0].split("sound-card", 1)
        audio_if = a_if[1].split(".", 1)[0]
        logging.info(f"sound card: hw:{audio_if} removed")
        if os.path.exists(f"/proc/asound/card{str(audio_if)}"):
            # Hmm, I guess it hasn't really gone, false alarm
            logging.debug(f"Device is still here - signal ignored")
            return
        dev_db = {}
        for dev_name in conf_db['devices']:
            if str(conf_db['devices'][dev_name]['number']) == str(audio_if):
                dev_db = conf_db['devices'][dev_name]
                if not dev_db['usb']:
                    # not a usb device
                    return
                for subn in dev_db['sub']:
                    subname = f"{dev_name},{str(subn)},0"
                    logging.debug(f"Subdevice {subname} unplugged")
                    if conf_db['jack']['usbdev'] == subname:
                        # must get rid of our jack client
                        jack_died = True
                        if last_master == conf_db['jack']['usbdev']:
                            kill_slave(conf_db['jack']['dev'])
                            time.sleep(1)
                            config_start()
                    elif conf_db['extra']['usbauto']:
                        if subname == conf_db['extra']['phone-device']:
                            phones_switch(False)
                        kill_slave(subname)
        import_device_array()


def get_card_rate(rtdev):
    ''' find out closest SR jack master that device handles'''
    global conf_db
    dname, l_dev, sub = rtdev.split(",", 2)
    dev_db = conf_db['devices'][dname]
    desired_rate = dev_db['sub'][l_dev]['rate']
    backup_rate = ""
    if os.path.isfile(f"/proc/asound/{dname}/pcm{l_dev}p/sub{sub}/hw_params"):
        p_file = f"/proc/asound/{dname}/pcm{l_dev}p/sub{sub}/hw_params"
        dtype = 0
    elif os.path.isfile(f"/proc/asound/{dname}/pcm{l_dev}c/sub{sub}/hw_params"):
        p_file = f"/proc/asound/{dname}/pcm{l_dev}c/sub{sub}/hw_params"
        dtype = 1
    else:
        logging.warn(f"missing info file for: {rtdev} adding client may fail.")
        return desired_rate

    adev_h = alsaaudio.PCM(type=dtype, device=f"hw:{rtdev}")

    adev_h.setrate(int(desired_rate))
    with open(p_file, "r") as card_file:
        for line in card_file:
            if 'rate:' in line:
                fnd_rate = line.split()[1]
                backup_rate = fnd_rate
    if fnd_rate == desired_rate:
        logging.debug(f"Good supports desired rate: {desired_rate}")
        adev_h.close()
        return fnd_rate
    for try_rate in ['48000', '44100', '88200', '96000', '32000']:
        adev_h.setrate(int(try_rate))
        with open(p_file, "r") as card_file:
            for line in card_file:
                if 'rate:' in line:
                    fnd_rate = line.split()[1]
                    if fnd_rate == try_rate:
                        logging.debug(f"Closest we could find: {fnd_rate}")
                        adev_h.close()
                        return fnd_rate

    if backup_rate:
        adev_h.close()
        return backup_rate
    logging.debug(f"Unable to confirm working rate, using: {desired_rate}")
    adev_h.close()
    return desired_rate


def start_slave(ldev):
    ''' takes the audio device as a parameter and starts a bridge
    from that device to jack '''
    global conf_db
    global procs

    logging.debug(f"Start slave: {ldev}")
    import_device_array()
    dname, l_dev, sub = ldev.split(",", 2)
    dev_db = conf_db['devices'][dname]
    sub_db = dev_db['sub'][l_dev]

    if sub_db['hide']:
        #don't start a slave blacklisted device
        logging.debug(f"Blacklisted: {ldev} don't start")
        return

    buff_size = sub_db['frame']
    #should do play and capture separately
    if sub_db['play-pid'] or sub_db['cap-pid']:
        logging.debug(f" Device {ldev} already bridged or in use by other application")
        return

    if dev_db['hdmi']:
        logging.info("HDMI device, setting buffer to 4096")
        buff_size = "4096"
    elif dev_db['internal'] and (buff_size < 128):
        logging.info("Internal device, minimum buffer 128, using 128")
        buff_size = "128"
    dsr = get_card_rate(ldev)
    # we found it and it seems to have this sub
    manual = False
    if sub_db['play-chan'] or sub_db['cap-chan']:
        manual = True
    if sub_db['playback']:
        if not sub_db['play-pid']:
            #this should detect if the user has manually set either in or out numbers
            channels = 0
            if manual:
                channels = sub_db['play-chan']
            elif dev_db['usb'] and not conf_db['extra']['usb-single']:
                channels = 100
            elif ldev == conf_db['extra']['phone-device']:
                channels = 2
            if channels:
                cmd = f"/usr/bin/zita-j2a -j {ldev}-out -d hw:{ldev} -r {dsr} -p {buff_size}"
                cmd = f"{cmd} -n {str(sub_db['nperiods'])} -c {str(channels)}"
                logging.debug(f"device bridging comand line: {cmd}")
                procout = subprocess.Popen(shlex.split(cmd), shell=False)
                pidout = procout.pid
                logging.debug(f" Device {ldev} out has pid: {pidout}")
                procs.append(procout)
                sub_db['play-pid'] = pidout
        else:
            logging.debug(f" Device {ldev} playback already bridged or in use by other application")
    if sub_db['capture']:
        if not sub_db['cap-pid']:
            chanels = 0
            if manual:
                channels = sub_db['cap-chan']
            elif dev_db['usb']:
                channels = 100
            if channels:
                cmd = f"/usr/bin/zita-a2j -j {ldev}-in -d hw:{ldev} -r {dsr} -p {buff_size}"
                cmd = f"{cmd} -n {str(sub_db['nperiods'])} -c {str(channels)}"
                logging.debug(f"Device input comand line: {cmd}")
                procin = subprocess.Popen(shlex.split(cmd), shell=False)
                pidin = procin.pid
                logging.debug(f" Device {ldev} in has pid: {pidin}")
                procs.append(procin)
                sub_db['cap-pid'] = pidin
        else:
            logging.debug(f" Device {ldev} capture already bridged or in use by other application")
    time.sleep(1)
    import_device_array()


def kill_slave(ldev):
    ''' takes the device as a parameter and if the device exists
    and is bridged to jack, stops the bridge '''
    global conf_db
    global procs
    dname, l_dev, sub = ldev.split(",", 2)
    logging.debug(f"{ldev} kill in progress")
    dev_db = conf_db['devices'][dname]
    if len(dev_db['sub']):
        sub_db = dev_db['sub'][l_dev]
        logging.debug(f"{dname} has {str(len(dev_db['sub']))} sub devices, want sub device {l_dev}")
        if sub_db['play-pid']:
            logging.debug(f"{ldev} found Playback bridge to kill. PID: {str(sub_db['play-pid'])}")
            for i, pr in enumerate(procs):
                if pr.pid == sub_db['play-pid']:
                    logging.debug(f"kill {str(dname)} sub: {str(l_dev)} PID: {str(sub_db['play-pid'])}")
                    pr.send_signal(signal.SIGINT)
                    try:
                        rt = pr.wait(timeout=15)
                    except subprocess.TimeoutExpired:
                        logging.debug(f"kill PID: {str(sub_db['play-pid'])} failed")
                        pr.terminate()
                        outs, errs = pr.communicate()
                    del procs[i]
        else:
            logging.debug(f"{ldev} no Playback bridge found")
        if sub_db['cap-pid']:
            logging.debug(f"{ldev} found Capture bridge to kill. PID: {str(sub_db['cap-pid'])}")
            for i, pr in enumerate(procs):
                if pr.pid == sub_db['cap-pid']:
                    logging.info(f"kill {str(dname)} sub: {str(l_dev)} PID: {str(sub_db['cap-pid'])}")
                    pr.send_signal(signal.SIGINT)
                    try:
                        rt = pr.wait(timeout=15)
                    except subprocess.TimeoutExpired:
                        logging.debug(f"kill PID: {str(sub_db['cap-pid'])} failed")
                        try:
                            os.kill(int(sub_db['cap-pid']), 9)
                        except:
                            print("")
                    del procs[i]
        else:
            logging.debug(f"{ldev} no Capture bridge found")


def ses_cb_quit(*args, **kwargs):
    ''' dbus call back when quit signal caught. This is for use in
    testing and the GUI never sends this. '''
    logging.warning("Got quit signal.")
    we_die()

def handler(signum, frame):
    ''' a handler for system signals that may be sent by the system.
        we want to trap sigint, sigkill and sigterm and do the same as
        above. '''
    logging.warning(f"Got signal number: {str(signum)} - Dying.")
    we_die()

def ses_cb_stop(*args, **kwargs):
    ''' dbus call back when stop signal caught. This stops jack. '''
    logging.info("Got stop signal.")
    cp = subprocess.run(["/usr/bin/killall", "-9", "jackdbus", "jackd", "a2jmidid"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"Kill jack and friends: {cp.stdout.strip()}")
    cp = subprocess.run(["/usr/bin/pulseaudio", "-k"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"Restart PA: {cp.stdout.strip()}")


def we_die():
    global lock_file
    cp = subprocess.run(["/usr/bin/killall", "-9", "jackdbus", "jackd", "a2jmidid"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"Kill jack and friends: {cp.stdout.strip()}")
    cp = subprocess.run(["/usr/bin/pulseaudio", "-k"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"Restart PA: {cp.stdout.strip()}")
    if os.path.isfile(lock_file):
        new_pid = str(os.getpid())
        if os.path.isfile(lock_file):
            with open(lock_file, "r") as lk_file:
                for line in lk_file:
                    # only need one line
                    old_pid = line.rstrip()
            if new_pid == old_pid:
                os.remove(lock_file)
    os._exit(0)


def ses_cb_start(*args, **kwargs):
    ''' dbus call back when (re)start signal caught'''
    logging.info("Got start signal.")
    config_start()


def ses_cb_config(*args, **kwargs):
    ''' dbus call back when config signal caught '''
    logging.info("Got config signal.")
    reconfig()


def ses_cb_tablet(*args, **kwargs):
    ''' dbus call back when tablet signal caught
        This reads the config file and sets the tablet
        buttons, pen and size/position'''
    ''' not sure what we will do with this, if anything
        tablet plans changed/on hold for now; '''
    logging.info("Got tablet signal.")


def ses_cb_ping(*args, **kwargs):
    ''' dbus call back when config signal caught '''
    logging.info("Got ping signal.")
    time.sleep(3)
    subprocess.run(["/usr/bin/dbus-send", "--type=signal", "/", "org.studio.control.event.V3_5_signal"],
                   shell=False)


def phones_switch(plugged):
    ''' Does the actual phones signal and level switching '''
    global conf_db
    global phones
    global phone_port
    global jack_client
    global jack_alive
    phones = plugged
    logging.debug(f"Changing Headphone to plugged in = {str(plugged)}")
    if (not conf_db['jack']['on']) or not jack_alive:
        # we can't do anything up to pulse (I think)
        return
    out_L = "system:playback_1"
    out_R = "system:playback_2"
    logging.debug(f"Headphone phone action = {str(conf_db['extra']['phone-action'])}")
    dname, l_dev, sub = conf_db['extra']['phone-device'].split(",", 2)
    dev_db = conf_db['devices'][dname]
    sub_db = dev_db['sub'][l_dev]

    if conf_db['extra']['phone-action'] == "switch":
        logging.debug("Switching outputs")
        if dev_db['internal']:
            spkr = ""
            logging.debug("Headphone device is internal change it's mixer settings")
            my_mixers = alsaaudio.mixers(device=f"hw:{dname}")
            if "Front" in my_mixers:
                spkr = "Front"
            elif "Speakers" in my_mixers:
                spkr = "Speakers"
            spkr_mix = alsaaudio.Mixer(control=spkr, device=f'hw:{dname}')
            hp_mix = alsaaudio.Mixer(control='Headphone', device=f'hw:{dname}')
            if plugged:
                spkr_mix.setvolume(0)
                spkr_mix.setmute(1)
                hp_mix.setvolume(100)
                hp_mix.setmute(0)
            else:
                spkr_mix.setvolume(100)
                spkr_mix.setmute(0)
                hp_mix.setvolume(0)
                hp_mix.setmute(1)
        if conf_db['extra']['phone-device'] != conf_db['jack']['dev']:
            logging.debug("Headphone device is not system")
            # set jack port names
            phone_port = f"{conf_db['extra']['phone-device']}-out:playback_1"
            logging.debug(f"Headphone jack is: {phone_port}")
            # USB device?
            if not plugged:
                port_list = jack_client.get_ports(name_pattern=f'{dname}*', is_audio=True, is_input=True, is_physical=True)
                for this_port in port_list:
                    if this_port.name == phone_port:
                        switch_outputs(phone_port, conf_db['extra']['monitor'])
                if (not sub_db['play-chan']) and (not dev_db['usb']):
                    logging.debug("Headphone device had no jack port ... kill unneeded bridge")
                    kill_slave(conf_db['extra']['phone-device'])
            else:
                if not sub_db['play-pid'] and (not dev_db['usb']):
                    logging.debug("Headphone device has no jack port ... create bridge")
                    # we have to start a zita proc
                    start_slave(conf_db['extra']['phone-device'])
                    time.sleep(1)
                port_list = jack_client.get_ports(name_pattern=f'{dname}*', is_audio=True, is_input=True, is_physical=True)
                for this_port in port_list:
                    if this_port.name == phone_port:
                        switch_outputs("system:playback_1", phone_port)

    elif conf_db['extra']['phone-action'] == "script":
        # run a script to perform this instead
        logging.debug("Use script")
        fpath = expanduser('~/.config/autojack/headphone')
        if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
            cp = subprocess.run([fpath, str(plugged)],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
            logging.debug(f"Running script: {fpath} {str(plugged)}")
    else:
        logging.debug("No action chosen")


def phones_check():
    ''' check to see if there are phones plugged in. We only do this
    on start up'''
    logging.debug("Checking for phones plugged in")
    global phones
    global conf_db
    phones = False

    import_config("phones_check")
    import_device_array()

    dname, ddev, dsub = conf_db['extra']['phone-device'].split(',')
    dev_db = conf_db['devices'][dname]
    sub_db = dev_db['sub'][ddev]
    logging.log(7, f"phonecheck: got device")
    if int(dev_db['number']) < 0:
        logging.info(f"Phones device: {dname} not present")
        return
    logging.debug(f"Checking phones device: {dname}")
    if dev_db['usb']:
        # phones device is USB
        if sub_db['playback']:
            # If USB make sure it has audio playback
            phones_switch(True)
            return
        else:
            logging.warning(f"Headphone device {conf_db['extra']['phone-device']} appears to have no outputs")
    elif dev_db['internal']:
        # this is internal we can use amixer
        logging.debug("Checking for phones with internal device")
        my_mixers = alsaaudio.mixers(device=f"hw:{dname}")
        hp_sw = ""
        if "Front" in my_mixers:
            hp_sw = "Front "
        cmd = f"/usr/bin/amixer -D hw:{dname} cget iface=CARD,name='{hp_sw}Headphone Jack'"
        logging.debug(f"amixer command: {cmd}")
        cp = subprocess.run(shlex.split(cmd), universal_newlines=True,
                stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        for line in cp.stdout.split(":"):
            logging.debug(f"amixer line: {line.strip()}")
            if line.strip() == "values=on":
                phones_switch(True)
                return
            elif line.strip() == "values=off":
                phones_switch(False)
                return
        logging.debug(f"headphone detect string: {cp.stdout.strip()}")


def ses_cb_phones(*args, **kwargs):
    ''' User has manually asked to toggle phones (un)plugged'''
    global phones
    if phones:
        phones_switch(False)
        phones = False
    else:
        phones_switch(True)
        phones = True
    logging.debug(f"Manual phones switch to: {str(phones)}")


def phones_plug(*args, **kwargs):
    ''' callback means headphones have been plugged in lets make sure
        phones are unmuted and have a level higher than -inf.
        We also may want to mute speakers '''
    global conf_db
    logging.info("Got phones plugged in signal.")
    dname, ddev, dsub = conf_db['extra']['phone-device'].split(',')
    dev_db = conf_db['devices'][dname]
    if int(dev_db['number']) > -1 and dev_db['internal']:
            phones_switch(True)
    else:
        logging.info("Ignored phone plug: not correct device")


def phones_unplug(*args, **kwargs):
    ''' callback means headphones have been unplugged in lets make sure
        phones are muted and the speakers  are unmuted and have a
        level higher than -inf.'''
    global conf_db
    logging.info("Got phones unplugged signal.")
    dname, ddev, dsub = conf_db['extra']['phone-device'].split(',')
    dev_db = conf_db['devices'][dname]
    if int(dev_db['number']) > -1 and dev_db['internal']:
        phones_switch(False)
    else:
        logging.info("Ignored phone unplug: not correct device")


def jack_error(mesg):
    ''' jack call back to deal with jack errors '''
    global jack_alive
    if jack_alive:
        if "not running" in mesg:
            logging.warning(f"Jack Message: {mesg}")


def jack_info(mesg):
    ''' jack call back to deal with jack info '''
    logging.info(f"Jack Message: {mesg}")


def main():
    ''' Autojack runs at session start and manages audio for the session.
    this is the daemon for studio-controls'''
    global last_master
    global procs
    global jack_alive
    global jack_died
    global jack_count
    global phones
    global con_dirty
    global lock_file
    phones = False
    jack_count = 0
    procs = []
    last_master = ""
    jack_alive = False
    jack_died = False
    con_dirty = False
    print("starting up")

    # Try and kill any other running instance
    config_path = expanduser("~/.config/autojack")
    if not os.path.isdir(config_path):
        os.makedirs(config_path)
    lock_file = f"{config_path}/autojack.lock"
    # if os.path.isfile(lock_file): # this can be added in when 20.04 is EOL
    print("sending quit to any old autojack")
    subprocess.run(["/usr/bin/dbus-send", "--type=signal", "/",
                "org.studio.control.event.quit_signal"], shell=False)
    time.sleep(3)
    new_pid = str(os.getpid())
    if os.path.isfile(lock_file):
        # other instance still hasn't gone maybe hung
        with open(lock_file, "r") as lk_file:
            for line in lk_file:
                # only need one line
                old_pid = line.rstrip()
        if new_pid != old_pid:
            print("old lock file found, killing old pid")
            try:
                os.kill(int(old_pid), 9)
            except:
                print("")
            time.sleep(1)
    with open(lock_file, "w") as lk_file:
        lk_file.write(new_pid)
        print("Lock file created")

    # set up logging
    logpath = expanduser("~/.log")
    # make sure the logfile directory exists
    if not os.path.exists(logpath):
        os.makedirs(logpath)
    logfile = expanduser("~/.log/autojack.log")
    logging.basicConfig(filename=logfile, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.DEBUG)
    logging.info('Autojack started: logging started')
    print("logging started")

    cp = subprocess.run(["/usr/bin/convert-studio-controls"],
        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"check config file: {cp.stdout.strip()}")

    phones_check()
    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
    signal.signal(signal.SIGHUP, handler)
    signal.signal(signal.SIGINT, handler)
    signal.signal(signal.SIGQUIT, handler)
    signal.signal(signal.SIGILL, handler)
    signal.signal(signal.SIGTRAP, handler)
    signal.signal(signal.SIGABRT, handler)
    signal.signal(signal.SIGBUS, handler)
    signal.signal(signal.SIGFPE, handler)
    #signal.signal(signal.SIGKILL, handler) unhandlable  :)
    signal.signal(signal.SIGUSR1, handler)
    signal.signal(signal.SIGSEGV, handler)
    signal.signal(signal.SIGUSR2, handler)
    signal.signal(signal.SIGPIPE, handler)
    signal.signal(signal.SIGALRM, handler)
    signal.signal(signal.SIGTERM, handler)

    system_bus = dbus.SystemBus()
    system_bus.add_signal_receiver(msg_cb_new, dbus_interface='org.freedesktop.systemd1.Manager', signal_name='UnitNew')
    system_bus.add_signal_receiver(msg_cb_removed, dbus_interface='org.freedesktop.systemd1.Manager',
                                   signal_name='UnitRemoved')
    system_bus.add_signal_receiver(phones_plug, dbus_interface='org.studio.control.event',
                                signal_name='plug_signal')
    system_bus.add_signal_receiver(phones_unplug, dbus_interface='org.studio.control.event',
                                signal_name='unplug_signal')

    user_bus = dbus.SessionBus()
    user_bus.add_signal_receiver(ses_cb_start, dbus_interface='org.studio.control.event',
                                 signal_name='start_signal')
    user_bus.add_signal_receiver(ses_cb_stop, dbus_interface='org.studio.control.event',
                                 signal_name='stop_signal')
    user_bus.add_signal_receiver(ses_cb_config, dbus_interface='org.studio.control.event',
                                 signal_name='config_signal')
    user_bus.add_signal_receiver(ses_cb_ping, dbus_interface='org.studio.control.event',
                                 signal_name='ping_signal')
    user_bus.add_signal_receiver(ses_cb_tablet, dbus_interface='org.studio.control.event',
                                 signal_name='tablet_signal')
    user_bus.add_signal_receiver(ses_cb_quit, dbus_interface='org.studio.control.event',
                                 signal_name='quit_signal')
    user_bus.add_signal_receiver(ses_cb_phones, dbus_interface='org.studio.control.event',
                                 signal_name='phones_signal')

    subprocess.run(["/usr/bin/dbus-send", "--type=signal", "/",
                    "org.studio.control.event.start_signal"], shell=False)
    subprocess.run(["/usr/bin/dbus-send", "--type=signal", "/",
                    "org.studio.control.event.V3_5_signal"], shell=False)

    timeout_id = GLib.timeout_add(500, check_jack_status, None)

    loop = GLib.MainLoop()
    loop.run()


if __name__ == '__main__':
    main()
