
from __future__ import absolute_import

import errno
import logging
import threading

from vdsm import utils
from vdsm.common import cmdutils
from vdsm.common import commands
from vdsm.common.compat import subprocess
from vdsm.common import exception


CREATED = "created"

RUNNING = "running"

TERMINATED = "terminated"

ABORTING = "aborting"

ABORTED = "aborted"

log = logging.getLogger("storage.operation")


class Command(object):

    def __init__(self, cmd, cwd=None, nice=utils.NICENESS.HIGH,
                 ioclass=utils.IOCLASS.IDLE):
        self._cmd = cmd
        self._cwd = cwd
        self._nice = nice
        self._ioclass = ioclass
        self._lock = threading.Lock()
        self._state = CREATED
        self._proc = None

    def run(self):
        self._start_process()
        out, err = self._proc.communicate()
        self._finalize(out, err)
        return out

    def watch(self):
        self._start_process()
        err = bytearray()
        for src, data in cmdutils.receive(self._proc):
            if src == cmdutils.OUT:
                yield data
            else:
                err += data
        self._finalize(b"", err)

    def abort(self):
        with self._lock:
            if self._state == CREATED:
                log.debug("%s not started yet", self)
                self._state = ABORTED
            elif self._state == RUNNING:
                self._state = ABORTING
                log.info("Aborting %s", self)
                self._kill_process()
            elif self._state == ABORTING:
                log.info("Retrying abort %s", self)
                self._kill_process()
            elif self._state == TERMINATED:
                log.debug("%s has terminated", self)
            elif self._state == ABORTED:
                log.debug("%s was aborted", self)
            else:
                raise RuntimeError("Invalid state: %s" % self)

    def _start_process(self):
        with self._lock:
            if self._state == ABORTED:
                raise exception.ActionStopped
            if self._state != CREATED:
                raise RuntimeError("Attempt to run an operation twice")
            self._proc = commands.start(
                self._cmd,
                cwd=self._cwd,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                nice=self._nice,
                ioclass=self._ioclass)
            self._state = RUNNING

    def _finalize(self, out, err):
        rc = self._proc.returncode
        log.debug(cmdutils.retcode_log_line(rc, err))
        with self._lock:
            self._proc = None
            if self._state == ABORTING:
                self._state = ABORTED
                raise exception.ActionStopped
            elif self._state == RUNNING:
                self._state = TERMINATED
                if rc != 0:
                    raise cmdutils.Error(self._cmd, rc, out, err)
            else:
                raise RuntimeError("Invalid state: %s" % self)

    def _kill_process(self):
        if self._proc.poll() is not None:
            log.debug("%s has terminated", self)
            return
        try:
            self._proc.kill()
        except OSError as e:
            if e.errno != errno.ESRCH:
                raise
            log.debug("%s has terminated", self)

    def __repr__(self):
        return s.format(self=self, addr=id(self))
