#!/usr/bin/python
#
# Priority scheduler for build/test jobs
#
# Copyright (c) 2013-2020 Andreas Gustafsson.  All rights reserved.
# Please refer to the file COPYRIGHT for detailed copyright information.

from __future__ import print_function

import itertools
import time
import datetime
import signal
import optparse
import os
import sys

from bracket import *
from utils import mkdir_p

pid_file = os.path.join(config['db_dir'], 'schedule.pid')

# fg_jobs and bgjobs are list of tuples,
# each tuple contains the directory and script name.

def parse_job_string(job_string):
    return [s.split(':') for s in job_string.split(',')]

fg_jobs = parse_job_string(config_get('foreground_jobs', '.:notify'))
bg_jobs = parse_job_string(config_get('background_jobs', ''))

# Check that the jobs are in the expected tuple format

def check_jobs(jobs):
   for j in jobs:
       if len(j) != 2:
          raise RuntimeError("malformed job: " + str(j))

check_jobs(fg_jobs)
check_jobs(bg_jobs)

# If at least this many seconds have passed since the end of the last
# repo update, initiate a new one
time_limit = 600

got_sighup = False

def sighup_handler(signum, frame):
    global got_sighup
    print("SIGHUP received, will exit at first convenience")
    sys.stdout.flush()
    got_sighup = True

signal.signal(signal.SIGHUP, sighup_handler)

def sigint_handler(signum, frame):
    print("SIGINT received, exiting")
    sys.exit(0)

signal.signal(signal.SIGINT, sigint_handler)

fg_cycle = itertools.cycle(fg_jobs)
bg_cycle = itertools.cycle(bg_jobs)

def run_job(job):
    dir, command = job
    now = datetime.now()
    logfile = os.path.join(config['logdir'],
        'schedule', now.strftime("%Y"), now.strftime("%Y%m%d-%H%M") +
        "-" + os.path.basename(dir) + "-" + command + ".log")
    mkdir_p(os.path.dirname(logfile))
    run('cd %s && bracket %s --test-in-background --update-repo=0 >%s 2>&1' % (dir, command, logfile))

def sighup_check():
    global got_sighup
    if got_sighup:
        os.unlink(pid_file)
        print("exiting")
        sys.exit(0)

# Returns true iff we took time
def run_next_job(job_cycle, cycle_len):
   t0 = time.time()
   for i in range(cycle_len):
       run_job(next(job_cycle))
       t1 = time.time()
       dt = time_limit - (t1 - t0)
       dt = t1 - t0
       print("jobs have now taken %i seconds" % dt)
       if dt > time_limit:
           return True
   return False

def idle():
    print("sleeping for %.0f seconds before updating repo" % time_limit)
    sys.stdout.flush()
    time.sleep(time_limit)

def is_already_running():
    try:
        pid = int(read_file(pid_file, 't'))
        os.kill(pid, 0)
    except:
        return False
    return True

def schedule_run():
    if is_already_running():
        print("already running", file=sys.stderr)
        sys.exit(0)

    write_file(pid_file, str(os.getpid()) + '\n', 't')
    while True:
       sighup_check()
       try:
           update_repository()
       except:
           print("warning: repository update failed")
           # continue anyway, we can probably do useful refining
           # work while waiting for repo access to be restored

       # If update_repository() threw an exception, we may
       # still be holding a reference to its local variables
       # via the current exception context, which will prevent
       # of from properly releasing the repository lock.
       # Deal with this by clearing the exception context.
       if sys.version_info[0] < 3:
           sys.exc_clear()

       sighup_check()

       run_next_job(fg_cycle, len(fg_jobs)) or run_next_job(bg_cycle, len(bg_jobs)) or idle()

def schedule_stop():
    pid = int(read_file(pid_file, 't'))
    os.kill(pid, signal.SIGHUP)

def schedule_main(argv1):
    parser = optparse.OptionParser()
    parser.add_option("--stop",
                      help="ask currently running scheduler to stop",
                      action="store_true")
    (options, args) = parser.parse_args(argv1)
    if options.stop:
        schedule_stop()
    else:
        schedule_run()
