#!/usr/bin/python
# -*- coding: utf-8 -*-
# vim: set expandtab:tabstop=4:softtabstop=4:shiftwidth=4:nowrap
# $Id$

from fcntl import lockf, LOCK_EX, LOCK_UN
from errno import ENOENT
import logging
import argparse
import json
import re
import os
import sys
from os import chdir, fork, kill, mkdir, setsid
from os.path import abspath
from signal import signal, SIGTERM
from subprocess import Popen
from sys import argv, exit, stdout, stderr
import tempfile

root = '/var/lib/direct-async-run'
jobs_dir = root + '/jobs'

USAGE = '''%(prog)s -- запустить команду асинхронно. 

Примеры:
%(prog)s start sleep 600 # запустить асинхронно sleep 600, выведет на stdout номер задания
%(prog)s get-status 13 # вывести состояние задания с номером 13, например "running, pid <pid>" или "exited, code <код возврата>"; номер задания -- из вывода direct-async-run start
%(prog)s show-out 13 # вывести stdout команды, запущенной в задании 13
%(prog)s show-err 13 # вывести stderr команды, запущенной в задании 13
'''

def run():
    parser = argparse.ArgumentParser(usage=USAGE)
    parser.add_argument("-j", "--json", action="store_true",
            dest='json', help="вывод в формате json")
    parser.add_argument("action", nargs='?', type=str, action='store',
            help="доступное действие: start/supervise/get-status/show-out/show-err")
    #parser.add_argument("args", nargs='*', type=str, action='store',
    #        help="список аргументов для действия: start [command], get-status <id_task>")
    parser.add_argument("--run-shell", action="store_true",
            dest='shell', help="запустить процесс в subprocess shell=True")
    opts, args = parser.parse_known_args()

    if opts.action is None:
        parser.print_help()
        sys.exit(1)

    script_path = abspath(__file__)
    action = opts.action
    if opts.action == 'start':
        cmd = args

        lockfile = open(jobs_dir + '/lock', 'w+')
        lockf(lockfile, LOCK_EX)
        max_job_id = max([int(d) for d in os.listdir(jobs_dir) if re.match(r'^[0-9]+$', d)] or [0])
        new_job_id = max_job_id + 1
        lockf(lockfile, LOCK_UN)

        job_dir = jobs_dir + '/' + str(new_job_id)
        mkdir(job_dir)
        chdir(job_dir)
        open('cmdline', 'w+').write('\0'.join(cmd))

        sv_pid = os.fork()
        if sv_pid != 0:
            if opts.json:
                run_status = { 
                        "hashid":   str(new_job_id),
                        "msg":      "start direct-async-run {0}".format(opts.action) 
                        }
                print json.dumps(run_status)
            else:
                print new_job_id
            exit(0)
        else:
            os.setsid()
            shell = '--run-shell' if opts.shell else ""
            Popen([script_path, 'supervise'] + [str(new_job_id)] + [shell],
                stdin=open('/dev/null', 'r'),
                stdout=open('/dev/null', 'w'),
                stderr=open('supervise.log', 'w+')
            )
    elif opts.action == 'supervise':
        # supervise не предполагается запускать вручную
        # нет проверки на то, что это задание уже выполнилось/ещё выполняется
        job_id = args[0]
        job_dir = jobs_dir + '/' + job_id
        chdir(job_dir)

        logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
        cmd = open('cmdline', 'r').read().strip('\0')
        logging.info('start')
        cmd_stdout = open('out', 'w+')
        cmd_stderr = open('err', 'w+')
        if opts.shell:
            cmd = open('cmdline', 'r').read().replace('\0', ' ')
            p = Popen(cmd, shell=True, stdout=cmd_stdout, stderr=cmd_stderr)
        else:
            cmd = open('cmdline', 'r').read().split('\0')
            p = Popen(cmd, stdout=cmd_stdout, stderr=cmd_stderr)
        open('pid', 'w+').write(str(p.pid))

        def propagate_signal(signum, frame):
            logging.info('received signal {}, sending it to child'.format(signum))
            kill(p.pid, signum)
        for s in [SIGTERM]:
            signal(s, propagate_signal)

        exit_code = p.wait()
        open('exit_code', 'w+').write(str(exit_code))
        logging.info('finish')
    elif opts.action == 'get-status':
        job_id = args[0]
        job_dir = jobs_dir + '/' + job_id
        try:
            chdir(job_dir)
        except OSError as e:
            if e.errno == ENOENT:
                if opts.json:
                    run_status = { 
                            "status":   "FAILED",
                            "message":  "not found job {0}".format(job_id) 
                            }
                    return
                else:
                    exit('no job ' + job_id)
            else:
                raise
        if os.path.exists('exit_code'):
            exit_code = open('exit_code', 'r').read().strip()
            if opts.json:
                if int(exit_code) == 0:
                    status = "SUCCESS"
                    message = "finish task. Run from read log: {1} show-out {0}".format(job_id, script_path)
                else:
                    status = "FAILED"
                    message = "error task. Run from read log: {1} show-err {0}".format(job_id, script_path)
                run_status = { 
                        "status":   status,
                        "message":  message
                        }
                print json.dumps(run_status)
            else:
                print 'exited, code ' + exit_code
        elif os.path.exists('pid'):
            pid = open('pid', 'r').read().strip()
            if opts.json:
                run_status = { 
                        "status":   "PROCESS",
                        "message":  "running, pid={0}".format(pid) 
                        }
                print json.dumps(run_status)
            else:
                print 'running, pid ' + pid
        else:
            if opts.json:
                run_status = {
                    "status":   "FAILED",
                    "message":  "problem with start script"
                }
                print json.dumps(run_status)
            else:
                exit("problem with start script")
        # если успели вызывать до запуска команды, получим исключение
    elif re.match(r'^show-(out|err)$', opts.action):
        job_id = args[0]
        job_dir = jobs_dir + '/' + job_id
        try:
            chdir(job_dir)
        except OSError as e:
            if e.errno == ENOENT:
                exit('no job ' + job_id)
            else:
                raise
        filename = re.match('^show-(out|err)$', opts.action).group(1)
        f = open(filename, 'r')
        for line in f.readlines():
            stdout.write(line)
    else:
        if opts.json:
            run_status = { 
                    "status":   "FAILED",
                    "message":  "unknown action {0}".format(opts.action) 
                    }
            print json.dumps(run_status)
        else:
            exit('unknown action {0}'.format(opts.action))

if __name__ == '__main__':
    run()

