#!/usr/bin/python2

import time, sys, os, errno, logging, signal, copy, select, socket, grp, random

daily = 86400
twice_daily = 86400 / 2
hourly = 3600
bi_hourly = 7200
tri_hourly = 10800
twice_hourly = 1800
ten_minutely = 600
five_minutely = 300
maxtime = 86400
mintime = 60
jobs = {}
MAX_JOBS = 6
#earPath = '/home/mirror/merlin/merlin.sock'
earPath = '/mirror/merlin/run/merlin.sock'
cmd_buf_size = 4096

repos = {
    'debian': {
        'command': '~/bin/csc-sync-debian debian debian.mirror.rafal.ca debian',
        'interval': bi_hourly,
        'max-sync-time': maxtime,
    },
#    'debian-cdimage': {
#        'command': '~/bin/csc-sync-cdimage debian-cdimage cdimage.debian.org cdimage',
#        'interval': twice_daily,
#        'max-sync-time': maxtime,
#    },
    'ubuntu': {
        'command': '~/bin/csc-sync-debian ubuntu archive.ubuntu.com ubuntu drescher.canonical.com',
        'interval': bi_hourly,
        'max-sync-time': maxtime,
    },
    'ubuntu-ports': {
        'command': '~/bin/csc-sync-debian ubuntu-ports ports.ubuntu.com ubuntu-ports drescher.canonical.com',
        'interval': bi_hourly,
        'max-sync-time': maxtime,
    },
    'linuxmint-packages': {
        'command': '~/bin/csc-sync-debian linuxmint-packages rsync-packages.linuxmint.com packages',
        'interval': bi_hourly,
        'max-sync-time': maxtime,
    },
    'debian-multimedia': {
        'command': '~/bin/csc-sync-debian debian-multimedia www.deb-multimedia.org deb',
        'interval': bi_hourly,
        'max-sync-time': maxtime,
    },
    'debian-backports': {
        'command': '~/bin/csc-sync-debian debian-backports debian.mirror.rafal.ca debian-backports',
        'interval': bi_hourly,
        'max-sync-time': maxtime,
    },
#    'debian-volatile': {
#        'command': '~/bin/csc-sync-debian debian-volatile debian.mirror.rafal.ca debian-volatile',
#        'interval': bi_hourly,
#        'max-sync-time': maxtime,
#    },
    'debian-security': {
        'command': '~/bin/csc-sync-debian debian-security rsync.security.debian.org debian-security security-master.debian.org',
        'interval': twice_hourly,
        'max-sync-time': maxtime,
    },
    'ubuntu-releases': {
        'command': '~/bin/csc-sync-standard ubuntu-releases rsync.releases.ubuntu.com releases',
        'interval': bi_hourly,
        'max-sync-time': maxtime,
    },
    'xubuntu-releases': {
        'command': '~/bin/csc-sync-standard xubuntu-releases cdimage.ubuntu.com cdimage/xubuntu/releases/',
        'interval': bi_hourly,
        'max-sync-time': maxtime,
    },
#    'emdebian': {
#        'command': '~/bin/csc-sync-badperms emdebian www.emdebian.org debian',
#        'interval': twice_daily,
#        'max-sync-time': maxtime,
#    },
    'puppylinux': {
        'command': '~/bin/csc-sync-standard puppylinux distro.ibiblio.org puppylinux',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'CPAN': {
        'command': '~/bin/csc-sync-standard CPAN cpan-rsync.perl.org CPAN',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'CRAN': {
        'command': '~/bin/csc-sync-ssh CRAN cran.r-project.org "" cran-rsync ~/.ssh/id_cran_rsa',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'CTAN': {
        'command': '~/bin/csc-sync-standard CTAN rsync.dante.ctan.org CTAN',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
#    'openoffice': {
#        'command': '~/bin/csc-sync-standard openoffice rsync.services.openoffice.org openoffice-extended',
#        'command': '~/bin/csc-sync-standard openoffice ftp.snt.utwente.nl openoffice-extended',
#        'interval': twice_daily,
#        'max-sync-time': maxtime,
#    },
    'fedora-epel': {
        'command': '~/bin/csc-sync-standard fedora/epel mirrors.kernel.org fedora-epel && ~/bin/report_mirror >/dev/null',
        'interval': bi_hourly,
        'max-sync-time': maxtime,
    },
    'cygwin': {
        'command': '~/bin/csc-sync-standard cygwin cygwin.com cygwin-ftp',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'gnu': {
        #'command': '~/bin/csc-sync-standard gnu mirrors.ibiblio.org gnuftp/gnu/',
        'command': '~/bin/csc-sync-standard gnu ftp.gnu.org gnu',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'nongnu': {
        # 'command': '~/bin/csc-sync-standard nongnu dl.sv.gnu.org releases --ignore-errors',
        'command': '~/bin/csc-sync-standard nongnu dl.sv.gnu.org releases',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'mysql': {
        #'command': '~/bin/csc-sync-standard mysql mysql.he.net mysql',
        'command': '~/bin/csc-sync-standard mysql rsync.mirrorservice.org ftp.mysql.com',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
#	No longer syncs, and no longer really relevant
#    'mozdev': {
#        'command': '~/bin/csc-sync-standard mozdev rsync.mozdev.org mozdev',
#        'interval': twice_daily,
#        'max-sync-time': maxtime,
#    },
    'gnome': {
        'command': '~/bin/csc-sync-standard gnome master.gnome.org gnomeftp gnome',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'damnsmalllinux': {
        'command': '~/bin/csc-sync-standard damnsmalllinux ftp.heanet.ie mirrors/damnsmalllinux.org/',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'linuxmint': {
        'command': '~/bin/csc-sync-standard linuxmint pub.linuxmint.com pub',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'kernel.org-linux': {
        'command': '~/bin/csc-sync-standard kernel.org/linux rsync.kernel.org pub/linux/',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'kernel.org-software': {
        'command': '~/bin/csc-sync-standard kernel.org/software rsync.kernel.org pub/software/',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'apache': {
        'command': '~/bin/csc-sync-apache apache rsync.us.apache.org apache-dist',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'eclipse': {
        'command': '~/bin/csc-sync-standard eclipse download.eclipse.org eclipseMirror',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'kde': {
        'command': '~/bin/csc-sync-standard kde rsync.kde.org kdeftp',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'kde-applicationdata': {
        'command': '~/bin/csc-sync-standard kde-applicationdata rsync.kde.org applicationdata',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    # We are a Tier 1 arch mirror (https://bugs.archlinux.org/task/52853)
    # so our IP is important.
    'archlinux': {
        #'command': '~/bin/csc-sync-standard archlinux archlinux.mirror.rafal.ca archlinux',
        'command': '~/bin/csc-sync-archlinux archlinux',
        'interval': five_minutely,
        'max-sync-time': maxtime,
    },
    'debian-ports': {
        'command': '~/bin/csc-sync-standard debian-ports ftp.de.debian.org debian-ports',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'slackware': {
        'command': '~/bin/csc-sync-standard slackware slackware.cs.utah.edu slackware',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'debian-cd': {
        'command': '~/bin/csc-sync-debian-cd',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'x.org': {
        #'command': '~/bin/csc-sync-standard x.org xorg.freedesktop.org xorg-archive',
        #'command': '~/bin/csc-sync-standard x.org mirror.us.leaseweb.net xorg',
        'command': '~/bin/csc-sync-standard x.org rsync.mirrorservice.org ftp.x.org/pub',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'centos': {
        'command': '~/bin/csc-sync-standard centos us-msync.centos.org CentOS',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'opensuse': {
        'command': '~/bin/csc-sync-standard opensuse stage.opensuse.org opensuse-full/opensuse/ #"--exclude distribution/.timestamp_invisible"',
        'interval': bi_hourly,
        'max-sync-time': maxtime,
    },
    'FreeBSD': {
        # Has not updated since at least June 2018
        #'command': '~/bin/csc-sync-standard FreeBSD ftp10.us.freebsd.org FreeBSD',
        'command': '~/bin/csc-sync-standard FreeBSD ftp2.uk.freebsd.org ftp.freebsd.org/pub/FreeBSD/',
        #'command': '~/bin/csc-sync-standard FreeBSD ftp3.us.freebsd.org FreeBSD/',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'fedora-enchilada': {
        # 'command': '~/bin/csc-sync-standard fedora/linux mirrors.kernel.org fedora-enchilada/linux/ --ignore-errors && ~/bin/report_mirror >/dev/null',
        'command': '~/bin/csc-sync-standard fedora/linux mirrors.kernel.org fedora-enchilada/linux/ && ~/bin/report_mirror >/dev/null',
        'interval': bi_hourly,
        'max-sync-time': maxtime,
    },
    'ubuntu-ports-releases': {
        'command': '~/bin/csc-sync-standard ubuntu-ports-releases cdimage.ubuntu.com cdimage/releases/',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'gentoo-distfiles': {
        'command': '~/bin/csc-sync-gentoo',
        'interval': bi_hourly,
        'max-sync-time': maxtime,
    },
    'gentoo-portage': {
        'command': '~/bin/csc-sync-standard gentoo-portage rsync1.us.gentoo.org gentoo-portage',
        'interval': twice_hourly,
        'max-sync-time': maxtime,
    },
    # This project is no longer available for mirroring
    # https://bugzilla.mozilla.org/show_bug.cgi?id=807543
    #'mozilla.org': {
    #    'command': '~/bin/csc-sync-standard mozilla.org releases-rsync.mozilla.org mozilla-releases',
    #    'interval': twice_hourly,
    #    'max-sync-time': maxtime,
    #},
    'gutenberg': {
        'command': '~/bin/csc-sync-standard gutenberg ftp@ftp.ibiblio.org gutenberg',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'racket-installers': {
        'command': '~/bin/csc-sync-wget racket/racket-installers https://mirror.racket-lang.org/installers/ 1',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'plt-bundles': {
        'command': '~/bin/csc-sync-standard racket/plt-bundles mirror.racket-lang.org plt-bundles',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'OpenBSD': {
        'command': '~/bin/csc-sync-standard OpenBSD ftp3.usa.openbsd.org ftp',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'xiph': {
        #'command': '~/bin/csc-sync-standard xiph downloads.xiph.org xiph/releases',
        'command': '~/bin/csc-sync-standard xiph ftp.osuosl.org xiph',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
#	We currently don't have the disk space
    'netbsd': {
        'command': '~/bin/csc-sync-standard NetBSD rsync.netbsd.org NetBSD',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'netbsd-pkgsrc': {
        'command': '~/bin/csc-sync-standard pkgsrc rsync.netbsd.org pkgsrc',
        #'command': '~/bin/csc-sync-standard pkgsrc rsync3.jp.netbsd.org pub/pkgsrc/',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'macports-release': {
        'command': '~/bin/csc-sync-standard MacPorts/release rsync.macports.org macports/release/',
        'interval': bi_hourly,
        'max-sync-time': maxtime,
    },
    'macports-distfiles': {
        'command': '~/bin/csc-sync-standard MacPorts/mpdistfiles rsync.macports.org macports/distfiles/',
        'interval': bi_hourly,
        'max-sync-time': maxtime,
    },
#    'raspberrypi': {
#        'command': '~/bin/csc-sync-standard raspberrypi mirrors.rit.edu rpi',
#        'interval': twice_daily,
#        'max-sync-time': maxtime,
#    },
    'sagemath': {
        #'command': '~/bin/csc-sync-standard sage mirror.clibre.uqam.ca sage',
        'command': '~/bin/csc-sync-standard sage rsync.sagemath.org sage',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
#    'cs136': {
#        'command': '~/bin/csc-sync-ssh uw-coursewear/cs136 linux024.student.cs.uwaterloo.ca /u/cs136/mirror.uwaterloo.ca csc01 ~/.ssh/id_rsa_csc01',
#        'interval': hourly,
#        'max-sync-time': maxtime,
#    },
    'vlc': {
        'command': '~/bin/csc-sync-standard vlc rsync.videolan.org videolan-ftp',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'qtproject': {
        'command': '~/bin/csc-sync-standard qtproject master.qt.io qt-all',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'tdf': {
        'command': '~/bin/csc-sync-standard tdf rsync.documentfoundation.org tdf-pub',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'saltstack': {
        'command': '~/bin/csc-sync-s3 saltstack https://s3.repo.saltproject.io',
        'interval': daily,
        'max-sync-time': maxtime,
    },
#    'kali': {
#        'command': '~/bin/csc-sync-standard kali kali.mirror.globo.tech kali',
#        'interval': twice_daily,
#        'max-sync-time': maxtime,
#    },
#    'kali-images': {
#        'command': '~/bin/csc-sync-standard kali-images kali.mirror.globo.tech kali-images',
#        'interval': twice_daily,
#        'max-sync-time': maxtime,
#    },
    'alpine': {
        'command': '~/bin/csc-sync-standard alpine rsync.alpinelinux.org alpine',
        'interval': hourly,
        'max-sync-time': maxtime,
    },
    'raspbian': {
        'command': '~/bin/csc-sync-standard raspbian archive.raspbian.org archive',
        'interval': bi_hourly,
        'max-sync-time': maxtime,
    },
    'raspberrypi': {
        'command': '~/bin/csc-sync-standard-ipv6 raspberrypi apt-repo.raspberrypi.org archive',
        'interval': bi_hourly,
        'max-sync-time': maxtime,
    },
    'ipfire': {
        'command': '~/bin/csc-sync-standard ipfire rsync.ipfire.org full',
        'interval': hourly,
        'max-sync-time': maxtime,
    },
    'manjaro': {
        'command': '~/bin/csc-sync-standard manjaro mirrorservice.org repo.manjaro.org/repos/',
        'interval': hourly,
        'max-sync-time': maxtime,
    },
    'scientific': {
        'command': '~/bin/csc-sync-standard scientific rsync.scientificlinux.org scientific',
        'interval': bi_hourly,
        'max-sync-time': maxtime,
    },
    'mxlinux': {
        'command': '~/bin/csc-sync-standard mxlinux mirror.math.princeton.edu pub/mxlinux/',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'mxlinux-iso': {
        'command': '~/bin/csc-sync-standard mxlinux-iso mirror.math.princeton.edu pub/mxlinux-iso/',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },

    'parabola': {
        'command': '~/bin/csc-sync-standard parabola repo.parabola.nu:875 repos/',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    #'hyperbola-sources': {
    #    'command': '~/bin/csc-sync-chmod hyperbola/sources repo.hyperbola.info:52000 repo/',
    #    'interval': twice_daily,
    #    'max-sync-time': maxtime,
    #},
    #'hyperbola-stable': {
    #    'command': '~/bin/csc-sync-chmod hyperbola/gnu-plus-linux-libre/stable repo.hyperbola.info:52012 repo/',
    #    'interval': twice_daily,
    #    'max-sync-time': maxtime,
    #},
    #'hyperbola-testing': {
    #    'command': '~/bin/csc-sync-chmod hyperbola/gnu-plus-linux-libre/testing repo.hyperbola.info:52011 repo/',
    #    'interval': twice_daily,
    #    'max-sync-time': maxtime,
    #},
    'trisquel-packages': {
        'command': '~/bin/csc-sync-standard trisquel/packages rsync.trisquel.info trisquel.packages/',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'trisquel-iso': {
        'command': '~/bin/csc-sync-standard trisquel/iso rsync.trisquel.info trisquel.iso/',
        'interval': twice_daily,
        'max-sync-time': maxtime,
    },
    'almalinux': {
        'command': '~/bin/csc-sync-standard almalinux rsync.repo.almalinux.org almalinux/',
        'interval': bi_hourly,
        'max-sync-time': maxtime,
    },
    'ceph': {
        'command': '~/bin/csc-sync-ceph -q -s global -t ceph',
        'interval': tri_hourly,
        'max-sync-time': maxtime,
    },
}


def mirror_status():
    out = []
    for x in repos:
        repository = repos[x]
        last_attempt = repository['last-attempt']
        next_attempt = repository['last-attempt'] + repository['interval']
        out.append( [x, last_attempt, next_attempt] )

    out.sort(key= lambda x: x[2])

    #turn floating point time values into human readable strings
    for x in out:
        for y in (1,2):
            x[y] = time.ctime(x[y])

    out.insert(0,['Repository', 'Last Synced', 'Next Expected Sync'])

    #calculate maximum width of each column
    widths = []
    i = 0
    while True:
        try:
            column_width = max([len(row[i]) for row in out])
            widths.append(column_width + 3)
            i = i + 1
        except:
            break

    #compose table string and pad out columns
    status_string = '%s%s%s\n' % (out[0][0].ljust(widths[0]), out[0][1].ljust(widths[1]), out[0][2].ljust(widths[2]))
    for x in out[1:]:
        for y in (0,1,2):
            status_string = status_string + ('%s' % x[y].rjust(widths[y]))
        status_string = status_string + '\n'

    return status_string


def init_last_sync():

    if not os.path.isdir('stamps'):
        os.mkdir('stamps')

    now = time.time()

    for repo in repos:
        try:
            last = os.stat('stamps/%s' % repo).st_mtime
            repos[repo]['last-sync'], repos[repo]['last-attempt']  = last, last
            logging.info('repo %s last synced %d seconds ago' % (repo, now - last))
        except OSError:
            repos[repo]['last-sync'], repos[repo]['last-attempt']  = 0, 0
            logging.warning('repo %s has never been synced' % repo)


def update_last_attempt(repo, start_time):
    repos[repo]['last-attempt'] = start_time

def update_last_sync(repo, start_time, duration, exit_code):

    repos[repo]['last-sync'] = start_time
    update_last_attempt(repo, start_time)

    # touch the timestamp file
    with open('stamps/%s' % repo, 'w') as f:
        f.write('%d,%d,%d' % (start_time, duration, exit_code))
    # open('stamps/%s' % repo, 'w').close()

def handler(signum, frame):
    logging.info("Caught signal %d" % signum)

    os.remove(earPath)

    for job in jobs:
        os.kill(job, signal.SIGTERM)

    time.sleep(1)

    for job in jobs:
        os.kill(job, signal.SIGKILL)

    try:
        while True:
            pid, status = os.wait()
            logging.info("Child process %d has terminated" % pid)

    #ECHILD is thrown when there are no child processes left
    except OSError, e:
        if e.errno != errno.ECHILD:
            raise
        logging.info("All child processes terminated. Goodbye!")
        sys.exit()

def setup_logging():
    if not os.path.isdir('logs'):
        os.mkdir('logs')

    try:
        os.unlink(earPath)
    except OSError:
        pass

    if '-d' in sys.argv:
        logging.basicConfig(filename="/home/mirror/merlin/logs/merlin.log",
                            level=logging.DEBUG, format="%(asctime)-15s %(message)s")
    else:
        logging.basicConfig(level=logging.DEBUG, format="%(asctime)-15s %(message)s")

def sync(current, now):
    pid = os.fork()
    if not pid:
        try:
            logfd = os.open('logs/%s' % current, os.O_WRONLY|os.O_APPEND|os.O_CREAT, 0644)
            nulfd = os.open('/dev/null', os.O_RDONLY)
            os.dup2(nulfd, 0)
            os.dup2(logfd, 1)
            os.dup2(logfd, 2)
            os.close(logfd)
            os.close(nulfd)
            os.execv("/bin/sh", ['sh', '-c', repos[current]['command']])
        except OSError, e:
            print >>sys.stderr, 'failed to exec: %s' % e
            os.exit(1)

    #There exists a race condition that manifests if merlin is asked to terminate ( by receiving signal ) after fork() but before the jobs table is updated.
    #Normally you would mask off signals to avoid attempting shutdown in that time period, but this is not supported in the version of Python for which merlin was written.
    #TODO:
    #The Linux Programming Interface 24.5 illustrates how to synchronize parent and child using signals.  Might be applicable.

    jobs[pid] = {'name': current, 'start_time': now, 'status': 'running'}

def zfssync(current):
    pid = os.fork()
    if not pid:
        try:
            logfd = os.open('logs/zfssync-%s.log' % current, os.O_WRONLY|os.O_APPEND|os.O_CREAT, 0644)
            nulfd = os.open('/dev/null', os.O_RDONLY)
            os.dup2(nulfd, 0)
            os.dup2(logfd, 1)
            os.dup2(logfd, 2)
            os.close(logfd)
            os.close(nulfd)
            os.execv("/home/mirror/bin/zfssync", ['zfssync', current])
        except OSError, e:
            print >>sys.stderr, 'failed to exec: %s' % e
            os.exit(1)

def await_command(ear):
    if select.select([ear],[],[],1) == ([ear],[],[]):
        #handle command heard on ear socket
        s, address = ear.accept()
        cmdstring = ''
        while True:
            data = s.recv(cmd_buf_size)
            if not data:
                break
            cmdstring = cmdstring + data
        cmdstring = cmdstring.split(':',1)
        command = cmdstring[0]
        try:
            if command == 'sync':
                try:
                    arg = cmdstring[1]
                    if arg in repos:
                        if arg in (x['name'] for x in jobs.itervalues()):
                            logging.info('Cannot force sync: %s. Already syncing.' % arg)
                            s.send('Cannot force sync %s, already syncing.' % command)
                        else:
                            logging.info('Forcing sync: %s' % arg)
                            s.send('Forcing sync: %s' % arg)
                            sync(arg, time.time())
                    else:
                        logging.info('%s not tracked, cannot sync.' % arg)
                        s.send('%s not tracked, cannot sync.' % arg)
                except:
                    s.send('Could not parse sync command, forced sync fails.')
                    raise

            elif command == 'status':
                s.send(mirror_status())

            elif command == 'dump':
                s.send(str(jobs))
                s.send("\n")
                s.send(str(repos))

            else:
                logging.error('Received unrecognized command: %s' % command)
                s.send('Bad command: %s' % command)

            s.close()

        except socket.error, e:
            logging.error('Could not communicate with arthur over socket.')

def new_jobs(now):
    #To prevent repos at the 'bottom' of the dictionary from getting neglected when mirror is under unusual load (and merlin is running at MAX_JOBS)
    keys = repos.keys()
    random.shuffle(keys)
    for current in keys:
        if len(jobs) >= MAX_JOBS:
            break
        if now <= repos[current]['last-attempt'] + mintime:
            continue
        if current in (x['name'] for x in jobs.itervalues()):
            continue
        when_due = repos[current]['last-sync'] + repos[current]['interval']
        if now >= when_due:
            logging.debug("syncing %s, due for %d seconds" % (current, now - when_due))
            sync(current, now)

def handle_completed(now):
    try:
        pid, status = os.waitpid(-1, os.WNOHANG) #waitpid() likes to throw an exception when there's nothing to wait for

        #Don't know if we should be forever ignoring hung jobs if they eventually return...
        if pid != 0 and pid in jobs:
            job = jobs.pop(pid)

            if os.WIFSIGNALED(status):
                logging.error("%s %s sync terminated with signal %d" % (job['status'], job['name'], os.WTERMSIG(status)))
                update_last_attempt(job['name'], job['start_time'])
            elif os.WIFEXITED(status):
                exit_status = os.WEXITSTATUS(status)
                logging.error("%s %s sync exited with status %d " % (job['status'], job['name'], exit_status))
                update_last_attempt(job['name'], job['start_time'])
                interval = repos[job['name']]['interval']
                sync_took = int(now - job['start_time'])
                next_sync = max(0, interval - sync_took)
                update_last_sync(job['name'], job['start_time'], sync_took, exit_status)
                logging.info('%s sync complete, took %d seconds, syncs again in %d seconds'
                             % (job['name'], sync_took, next_sync))

                zfssync(job['name'])

    except OSError, e:
        if e.errno != errno.ECHILD:
            raise

def check_hung(now):
    for pid in jobs:
        #check if hung
        runtime = now - jobs[pid]['start_time']
        repo = jobs[pid]['name']
        if jobs[pid]['status'] == 'running' and runtime > repos[repo]['max-sync-time']:
            jobs[pid]['status'] = 'hung'
            logging.error("%s sync process hung, process pid %d" % (repo, pid))

def main():
    if not os.geteuid():
        print "Don't run merlin as root!"
        sys.exit(1);

    setup_logging()

    logging.info("Starting Merlin")

    signal.signal(signal.SIGTERM, handler)
    signal.signal(signal.SIGINT, handler)

    init_last_sync()


    old_umask = os.umask(0o002)
    ear = socket.socket(socket.AF_UNIX)
    ear.bind(earPath)
    ear.listen(1)
    os.umask(old_umask)
    os.chown(earPath, -1, grp.getgrnam("push").gr_gid)

    while True:
        await_command(ear)

        now = time.time()

        new_jobs(now)

        handle_completed(now)

        check_hung(now)


if __name__ == '__main__':
    main()
