# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import contextlib
import os
import multiprocessing
import signal
import sys

from chromite.lib import cros_build_lib
from chromite.lib import locking


class EnforcedCleanupSection(cros_build_lib.MasterPidContextManager):

  """Context manager used to ensure that a section of cleanup code is run

  This is designed such that a child splits off, ensuring that even if the
  parent is sigkilled, the section marked *will* be run.  This is implemented
  via a ProcessLock shared between parent, and a process split off to
  survive any sigkills/hard crashes in the parent.

  The usage of this is basically in a pseudo-transactional manner:

  >>> with EnforcedCleanupSection() as critical:
  ...   with other_handler:
  ...     try:
  ...       with critical.ForkWatchdog():
  ...         # Everything past here doesn't run during enforced cleanup
  ...         # ... normal code ...
  ...     finally:
  ...       pass # This is guaranteed to run.
  ...    # The __exit__ for other_handler is guaranteed to run.
  ...  # Anything from this point forward will only be run by the invoking
  ...  # process. If cleanup enforcement had to occur, any code from this
  ...  # point forward won't be run.
  >>>
  """
  def __init__(self):
    cros_build_lib.MasterPidContextManager.__init__(self)
    self._lock = locking.ProcessLock(verbose=False)
    self._forked = False
    self._is_child = False
    self._watchdog_alive = False
    self._read_pipe, self._write_pipe = multiprocessing.Pipe(duplex=False)

  @contextlib.contextmanager
  def ForkWatchdog(self):
    if self._forked:
      raise RuntimeError("ForkWatchdog was invoked twice for %s" % (self,))
    self._lock.write_lock()

    pid = os.fork()
    self._forked = True

    if pid:
      # Parent; nothing further to do here.
      self._watchdog_alive = True
      try:
        yield
      finally:
        self._KillWatchdog()
      return

    # Get ourselves a new process group; note that we do not reparent
    # to init.
    os.setsid()

    # Since we share stdin/stdout/whatever, suppress sigint should we somehow
    # become the foreground process in the session group.
    # pylint: disable=W0212
    signal.signal(signal.SIGINT, signal.SIG_IGN)
    # Child code.  We lose the lock via lockf/fork semantics.
    self._is_child = True
    try:
      self._lock.write_lock()
    except BaseException as e:
      print >> sys.stderr, ("EnforcedCleanupSection %s excepted(%r) attempting "
                            "to take the write lock; hard exiting." % (self, e))
      sys.stderr.flush()
      # We have no way of knowing the state of the parent if this locking
      # fails- failure means a code bug.  Specifically, we don't know if
      # cleanup code was run, thus just flat out bail.
      os._exit(1)

    # Check if the parent exited cleanly; if so, we don't need to do anything.
    if self._read_pipe.poll() and self._read_pipe.recv_bytes():
      for handle in (sys.__stdin__, sys.__stdout__, sys.__stderr__):
        try:
          handle.flush()
        except EnvironmentError:
          pass
      os._exit(0)

    # Allow masterpid context managers to run in this case, since we're
    # explicitly designed for this cleanup.
    cros_build_lib.MasterPidContextManager.ALTERNATE_MASTER_PID = os.getpid()

    raise RuntimeError("Parent exited uncleanly; forcing cleanup code to run.")

  def _enter(self):
    self._lock.write_lock()
    return self

  def _KillWatchdog(self):
    """Kill the child watchdog cleanly."""
    if self._watchdog_alive:
      self._write_pipe.send_bytes('\n')
      self._lock.unlock()
      self._lock.close()

  def _exit(self, _exc, _exc_type, _tb):
    if self._is_child:
      # All cleanup code that would've run, has ran.
      # Hard exit to bypass any further code execution.
      # pylint: disable=W0212
      os._exit(0)
    self._KillWatchdog()
