# ---------------------------------------------------------------------------
#
# Copyright (c) 2005, Greg Stein
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#
# ---------------------------------------------------------------------------
#
# This code is no longer maintained "upstream", so consider this module
# a "friendly fork" and is canonical for Apache Subversion's purposes.
#
# Use of systemd's single-process mechanism and re-launching of a daemon
# can greatly simplify daemon coding/management. A possibly-svn-relevant
# example can be found at:
#   https://github.com/apache/infrastructure-svnauthz
#
# Historical locations for this module were found on svn.webdav.org,
# gstein.googlecode.com, and (most recently) gstein.svn.beanstalkapp.com.
#

import os
import signal
import sys
import time
import stat
import multiprocessing  # requires Python 2.6


# possible return values from Daemon.daemonize()
DAEMON_RUNNING = 'The daemon is running'
DAEMON_NOT_RUNNING = 'The daemon is not running'
DAEMON_COMPLETE = 'The daemon has completed its operations'
DAEMON_STARTED = 'The daemon has been started'


class Daemon(object):

  def __init__(self, logfile, pidfile):
    self.logfile = logfile
    self.pidfile = pidfile

  def foreground(self):
    "Run in the foreground."
    ### we should probably create a pidfile. other systems may try to detect
    ### the pidfile to see if this "daemon" is running.
    self.setup()
    self.run()
    ### remove the pidfile

  def daemonize_exit(self):
    try:
      result = self.daemonize()
    except (ChildFailed, DaemonFailed) as e:
      # duplicate the exit code
      sys.exit(e.code)
    except (ChildTerminatedAbnormally, ChildForkFailed,
            DaemonTerminatedAbnormally, DaemonForkFailed) as e:
      sys.stderr.write('ERROR: %s\n' % e)
      sys.exit(1)
    except ChildResumedIncorrectly:
      sys.stderr.write('ERROR: continued after receiving unknown signal.\n')
      sys.exit(1)

    if result == DAEMON_STARTED or result == DAEMON_COMPLETE:
      sys.exit(0)
    elif result == DAEMON_NOT_RUNNING:
      sys.stderr.write('ERROR: the daemon exited with a success code '
                       'without signalling its startup.\n')
      sys.exit(1)

    # in original process. daemon is up and running. we're done.

  def daemonize(self):
    ### review error situations. map to backwards compat. ??
    ### be mindful of daemonize_exit().
    ### we should try and raise ChildFailed / ChildTerminatedAbnormally.
    ### ref: older revisions. OR: remove exceptions.

    child_is_ready = multiprocessing.Event()
    child_completed = multiprocessing.Event()

    p = multiprocessing.Process(target=self._first_child,
                                args=(child_is_ready, child_completed))
    p.start()

    # Wait for the child to finish setting things up (in case we need
    # to communicate with it). It will only exit when ready.
    ### use a timeout here! (parameterized, of course)
    p.join()

    ### need to propagate errors, to adjust the return codes
    if child_completed.is_set():
      ### what was the exit status?
      return DAEMON_COMPLETE
    if child_is_ready.is_set():
      return DAEMON_RUNNING

    ### how did we get here?! the immediate child should not exit without
    ### signalling ready/complete. some kind of error.
    return DAEMON_STARTED

  def _first_child(self, child_is_ready, child_completed):
    # we're in the child.

    ### NOTE: the original design was a bit bunk. Exceptions raised from
    ### this point are within the child processes. We need to signal the
    ### errors to the parent in other ways.

    # decouple from the parent process
    os.chdir('/')
    os.umask(0)
    os.setsid()

    # remember this pid so the second child can signal it.
    thispid = os.getpid()

    # if the daemon process exits before signalling readiness, then we
    # need to see the problem. trap SIGCHLD with a SignalCatcher.
    daemon_exit = SignalCatcher(signal.SIGCHLD)

    # perform the second fork
    try:
      pid = os.fork()
    except OSError as e:
      ### this won't make it to the parent process
      raise DaemonForkFailed(e.errno, e.strerror)

    if pid > 0:
      # in the parent.


      # Wait for the child to be ready for operation.
      while True:
        # The readiness event will invariably be signalled early/first.
        # If it *doesn't* get signalled because the child has prematurely
        # exited, then we will pause 10ms before noticing the exit. The
        # pause is acceptable since that is aberrant/unexpected behavior.
        ### is there a way to break this wait() on a signal such as SIGCHLD?
        ### parameterize this wait, in case the app knows children may
        ### fail quickly?
        if child_is_ready.wait(timeout=0.010):
          # The child signalled readiness. Yay!
          break
        if daemon_exit.signalled:
          # Whoops. The child exited without signalling :-(
          break
        # Python 2.6 compat: .wait() may exit when set, but return None
        if child_is_ready.is_set():
          break
        # A simple timeout. The child is taking a while to prepare. Go
        # back and wait for readiness.

      if daemon_exit.signalled:
        # Tell the parent that the child has exited.
        ### we need to communicate the exit status, if possible.
        child_completed.set()

        # reap the daemon process, getting its exit code. bubble it up.
        cpid, status = os.waitpid(pid, 0)
        assert pid == cpid
        if os.WIFEXITED(status):
          code = os.WEXITSTATUS(status)
          if code:
            ### this won't make it to the parent process
            raise DaemonFailed(code)
          ### this return value is ignored
          return DAEMON_NOT_RUNNING

        # the daemon did not exit cleanly.
        ### this won't make it to the parent process
        raise DaemonTerminatedAbnormally(status)

      # child_is_ready got asserted. the daemon is up and running, so
      # save the pid and return success.
      if self.pidfile:
        # Be wary of symlink attacks
        try:
          os.remove(self.pidfile)
        except OSError:
          pass
        fd = os.open(self.pidfile, os.O_WRONLY | os.O_CREAT | os.O_EXCL,
                     stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
        os.write(fd, '%d\n' % pid)
        os.close(fd)

      ### this return value is ignored
      return DAEMON_STARTED

      ### old code. what to do with this? throw ChildResumedIncorrectly
      ### or just toss this and the exception.
      # some other signal popped us out of the pause. the daemon might not
      # be running.
      ### this won't make it to the parent process
      raise ChildResumedIncorrectly()

    # we're a daemon now. get rid of the final remnants of the parent:
    # restore the signal handlers and switch std* to the proper files.
    signal.signal(signal.SIGUSR1, signal.SIG_DFL)
    signal.signal(signal.SIGCHLD, signal.SIG_DFL)
    sys.stdout.flush()
    sys.stderr.flush()
    si = open('/dev/null', 'r')
    so = open(self.logfile, 'a+')
    se = open(self.logfile, 'a+', 0)  # unbuffered
    os.dup2(si.fileno(), sys.stdin.fileno())
    os.dup2(so.fileno(), sys.stdout.fileno())
    os.dup2(se.fileno(), sys.stderr.fileno())
    # note: we could not inline the open() calls. after the fileno() completed,
    # the file would be closed, making the fileno invalid. gotta hold them
    # open until now:
    si.close()
    so.close()
    se.close()

    ### TEST: don't release the parent immediately. the whole parent stack
    ###       should pause along with this sleep.
    #time.sleep(10)

    # everything is set up. call the initialization function.
    self.setup()

    ### TEST: exit before signalling.
    #sys.exit(0)
    #sys.exit(1)

    # the child is now ready for parent/anyone to communicate with it.
    child_is_ready.set()

    # start the daemon now.
    self.run()

    # The daemon is shutting down, so toss the pidfile.
    if self.pidfile:
      try:
        os.remove(self.pidfile)
      except OSError:
        pass

    ### this return value is ignored
    return DAEMON_COMPLETE

  def setup(self):
    raise NotImplementedError

  def run(self):
    raise NotImplementedError


class _Detacher(Daemon):
  def __init__(self, target, logfile='/dev/null', pidfile=None,
               args=(), kwargs={}):
    Daemon.__init__(self, logfile, pidfile)
    self.target = target
    self.args = args
    self.kwargs = kwargs

  def setup(self):
    pass

  def run(self):
    self.target(*self.args, **self.kwargs)


def run_detached(target, *args, **kwargs):
  """Simple function to run TARGET as a detached daemon.

  The additional arguments/keywords will be passed along. This function
  does not return -- sys.exit() will be called as appropriate.

  (capture SystemExit if logging/reporting is necessary)
  ### if needed, a variant of this func could be written to not exit
  """
  d = _Detacher(target, args=args, kwargs=kwargs)
  d.daemonize_exit()


class SignalCatcher(object):
  def __init__(self, signum):
    self.signalled = False
    signal.signal(signum, self.sig_handler)

  def sig_handler(self, signum, frame):
    self.signalled = True


class ChildTerminatedAbnormally(Exception):
  "The child process terminated abnormally."
  def __init__(self, status):
    Exception.__init__(self, status)
    self.status = status
  def __str__(self):
    return 'child terminated abnormally (0x%04x)' % self.status

class ChildFailed(Exception):
  "The child process exited with a failure code."
  def __init__(self, code):
    Exception.__init__(self, code)
    self.code = code
  def __str__(self):
    return 'child failed with exit code %d' % self.code

class ChildForkFailed(Exception):
  "The child process could not be forked."
  def __init__(self, errno, strerror):
    Exception.__init__(self, errno, strerror)
    self.errno = errno
    self.strerror = strerror
  def __str__(self):
    return 'child fork failed with error %d (%s)' % self.args

class ChildResumedIncorrectly(Exception):
  "The child resumed its operation incorrectly."

class DaemonTerminatedAbnormally(Exception):
  "The daemon process terminated abnormally."
  def __init__(self, status):
    Exception.__init__(self, status)
    self.status = status
  def __str__(self):
    return 'daemon terminated abnormally (0x%04x)' % self.status

class DaemonFailed(Exception):
  "The daemon process exited with a failure code."
  def __init__(self, code):
    Exception.__init__(self, code)
    self.code = code
  def __str__(self):
    return 'daemon failed with exit code %d' % self.code

class DaemonForkFailed(Exception):
  "The daemon process could not be forked."
  def __init__(self, errno, strerror):
    Exception.__init__(self, errno, strerror)
    self.errno = errno
    self.strerror = strerror
  def __str__(self):
    return 'daemon fork failed with error %d (%s)' % self.args
