#!/usr/bin/env python3
#
# texworker.py -- background worker for flasktex
#
# This file is part of flasktex.
#
# Copyright (c) 2015, Boyuan Yang <073plan@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors
# may be used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

"""
Background worker for latex
"""
import subprocess
import os, sys
import shutil
import signal
import sqlite3
import time
import syslog
import multiprocessing
from flasktex.config import ft_getconfig

# CONFIG
DEFAULT_RENDERER = ft_getconfig("DEFAULTRENDERER")
DEFAULT_TIMEOUT = ft_getconfig("WORKERTIMEOUT")
DATABASE_NAME = ft_getconfig("DATABASENAME")
DATABASE_PATH = ft_getconfig("DATABASEPATH")
assert DEFAULT_RENDERER
assert DEFAULT_TIMEOUT
assert DATABASE_NAME
assert DATABASE_PATH

class TexWorker():
    """
    A background worker to automatically finish the work.

    Will indeed daemonize by double-fork.
    Will end-up in given seconds.
    """
    def __init__(self, rawstring, renderer=DEFAULT_RENDERER,
                 timeout=DEFAULT_TIMEOUT, db=DATABASE_NAME,
                 path=DATABASE_PATH, args=None):
        self.rawstring = rawstring # Have to be UTF-8 String.
        assert hasattr(self.rawstring, 'encode')
        self.renderer = renderer
        self.timeout = int(timeout) # XXX: Have to be integer
        self.conn_filepath = path + db
        self.popen = None
        self.workid = None
        self.result = None

        conn = sqlite3.connect(path+db)
        c = conn.cursor()
        # Write to be in waiting line
        start_time = str(time.time())
        print('start_time is now {}.'.format(start_time))
        c.execute('INSERT INTO `work` VALUES (?,?,?,?,?,?,?,?);',
                (None, None, self.rawstring, None, start_time, None,
                    'R', None))
        conn.commit()
        found = False
        for i in c.execute('SELECT `id` FROM `work` WHERE starttime=?', (start_time,)):
            found = True
            self.workid = i[0];
            print('the workid is {}.'.format(self.workid))
            break
        if not found:
            raise Exception('WORKER_NOT_FOUND_IN_DATABASE')
        try:
            conn.close()
        except Exception:
            pass
        return

    @staticmethod
    def daemonize():
        """
        Daemonize with special treatments.

        If return false, we are still in original process.
        If return true, we are in the daemon process.
        """
        syslog.syslog('Beginning to daemonize...')
        try:
            pid = os.fork()
            if pid > 0:
                return False # return to flask worker
        except OSError as e:
            syslog.syslog('OSError1!')
            raise
        os.chdir("/")
        os.setsid()
        os.umask(0)
        sys.stdout.flush()
        sys.stderr.flush()

        # Closing all opened file descriptors
        MAXFD = os.sysconf("SC_OPEN_MAX")
        for i in range(0, MAXFD):
            try:
                os.close(i)
            except:
                pass

        try:
            pid = os.fork()
            if pid > 0:
                sys.exit(0)
        except OSError as e:
            syslog.syslog('OSError2!')
            raise
        si = open("/dev/null", 'r')
        so = open("/dev/null", 'w')
        se = open("/dev/null", 'w')
        os.dup2(si.fileno(), sys.stdin.fileno())
        os.dup2(so.fileno(), sys.stdout.fileno())
        os.dup2(se.fileno(), sys.stderr.fileno())
        syslog.syslog('daemonize finished. pid is {}.'.format(os.getpid()))
        return True

    def __startup(self):
        # Switch to working dir and make a subprocess.Popen object for working
        # After that, wait for timeout.
        syslog.syslog('at the beginning of __startup().')
        os.chdir("/tmp/")
        tempdir = None
        try:
            tempdir = subprocess.check_output(['mktemp', '-d', 'flasktex.XXXXXXXXXX'], timeout=3).decode('UTF-8').strip()
        except subprocess.TimeoutExpired:
            syslog.syslog('Exception: subprocess.TimeoutExpired.')
            raise
        os.chdir("./{}/".format(tempdir))

        # Write input file as `input.tex'
        f = open('input.tex', 'wb')
        f.write(self.rawstring.encode('UTF-8'))
        f.close()
        syslog.syslog('after writing input.tex')

        # Form the Popen object, start the process, log in SQLite database
        try:
            syslog.syslog('Now renderer: {}'.format(self.renderer))
            # XXX: use xelatex by force
            self.renderer = "xelatex"
            self.popen = subprocess.Popen(['latexmk',
                '-pdflatex="{} {} %O %S"'.format(self.renderer, '-halt-on-error'),
                '-xelatex',
                'input.tex'], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            syslog.syslog('we have started subporcess.')
            try:
                conn = sqlite3.connect(self.conn_filepath)
                conn.execute('UPDATE `work` SET `status`=? WHERE `id`={};'.format(self.workid), ('R',))
                conn.commit()
                conn.close()
            except Exception as e:
                syslog.syslog('Houston, we had a problem.')
                raise
            syslog.syslog('after writing running state.')
            (stdout_data, stderr_data) = self.popen.communicate(input=None, timeout=self.timeout)
            # XXX: here reads all the data
            syslog.syslog("The stdout_data is: {}.".format(str(stdout_data)))
            syslog.syslog("The stderr_data is: {}.".format(str(stderr_data)))
        except Exception as e:
            raise

    def __cleanup(self, success=False):
        conn = sqlite3.connect(self.conn_filepath)
        c = conn.cursor()
        c.execute('BEGIN TRANSACTION;')
        if success:
            # write pdf and status into database
            try:
                f = open('input.pdf', 'rb')
            except OSError:
                raise
            c.execute('UPDATE `work` SET `output`=?, `status`=? WHERE `id`={};'.format(self.workid),
                    (f.read(), 'S'))
            f.close()
        else:
            if self.popen.returncode == None:
                # terminate the Process first
                self.popen.terminate()
                self.result = 'X' # TIME_EXCEEDED
            else:
                self.result = 'E' # ERROR_HAPPENED
            c.execute('UPDATE `work` SET `status`=? WHERE `id`={};'.format(self.workid),
                    (self.result,))

        # write log and stoptime into database
        try:
            f = open('input.log', 'r')
        except OSError:
            raise
        c.execute('UPDATE `work` SET `log`=?, `stoptime`=? WHERE `id`={};'.format(self.workid),
                (f.read(), str(time.time())))

        # close database connection
        conn.commit()
        conn.close()
        # remove the temp dir
        syslog.syslog('removing working dir...')
        cwd = os.getcwd()
        assert cwd.split('.')[0] == '/tmp/flasktex'
        os.chdir('..')
        shutil.rmtree(cwd)
        syslog.syslog('end of __cleanup(). status is {}.'.format(str(success)))
        return

    def __terminate_handler(self, signum, stackframe):
        """
        The final method to be called, then do sys.exit().

        """
        syslog.syslog('entered handler with signum of {}.'.format(signum))
        signal.alarm(0)
        syslog.syslog('after signal.')
        if self.popen == None or self.popen.returncode != 0:
            syslog.syslog('entering __cleanup, not successful.')
            self.__cleanup(success=False)
            syslog.syslog('worker exiting with num -1.')
            sys.exit(-1)
        else:
            syslog.syslog('entering __cleanup, not successful.')
            self.__cleanup(success=True)
            syslog.syslog('worker exiting with num 0.')
            sys.exit(0)

    def _do_work(self):
        """
        The uppermost method to finish TeXWork.

        * Use SIGALRM to set timeout.

          If SIGALRM is received, consider that the worker
          is taking too much time, then gracefully exit and
          record the task as failed.
        """
        syslog.syslog('entering _do_work().')
        try:
            signal.signal(signal.SIGALRM, self.__terminate_handler)
            signal.signal(signal.SIGTERM, self.__terminate_handler)
            signal.alarm(self.timeout)
        except Exception as e:
            syslog.syslog(str(e.args))
        self.__startup()
        syslog.syslog('successfully finished the work within time.')
        signal.alarm(0)
        self.__cleanup(success=True)

    def run(self): 
        """
        Start the TeXWorker in a daemonized process.

        Using the twice-fork magic.
        Calling this method will return immediately in original process.
        """
        # Daemonize and continue the work.
        if not self.daemonize():
            return
        
        # run the work.
        syslog.syslog('This is worker daemon and we will now begin the work.')
        self._do_work()

        # shall never reach.
        return 

#  vim: set ts=8 sw=4 tw=0 et :
