Mini Shell

Direktori : /opt/bakmgr/lib/python3.6/site-packages/bakmgr/api/restic/
Upload File :
Current File : //opt/bakmgr/lib/python3.6/site-packages/bakmgr/api/restic/procs.py

"""Classes for restic subprocesses"""
import logging
import threading
import os
import time
import signal
import shlex
from typing import TYPE_CHECKING, Union, List
from subprocess import Popen, PIPE, CalledProcessError, CompletedProcess
import psutil
from .errors import ResticError

if TYPE_CHECKING:
    from .base import Restic

# since the processes we're pausing use network connections, they need to be
# intermittently resumed and stopped again to keep their sessions alive
SLEEP_SECS = 8  # how long to pause a process when load is high
RUN_SECS = 1  # how long to run before pausing again
POLL_INTERVAL = 1  # how often to check load and pausing logic


def join_cmd(split_command):
    """Return a shell-escaped string from *split_command*."""
    # copied from Python3.9's shlex.join
    return ' '.join(shlex.quote(arg) for arg in split_command)


class ProcMon(threading.Thread):
    """Thread that monitors server load and pauses/resumes a subprocess"""

    def __init__(self, proc: Popen, limit: float):
        super().__init__(target=self._mainloop, daemon=True)
        self.limit = limit
        self.proc = proc
        # last_time is the time last_state was changed
        self.last_time = time.time()
        # last_state holds the last state change made to the process.
        # This is not necessarily the current state.
        self.last_state = psutil.STATUS_RUNNING
        self.start()

    def _mainloop(self) -> None:
        """Runs and monitors load until the process ends"""
        while self.proc.returncode is None:
            time.sleep(POLL_INTERVAL)
            try:
                self._poll_load()
            except (OSError, psutil.NoSuchProcess):
                return  # process ended

    def _change_state(self, state: str, signum: int) -> None:
        """Send a signal to the process and update last_state/last_time"""
        self.proc.send_signal(signum)
        self.last_state = state
        self.last_time = time.time()

    def _poll_load(self) -> None:
        """Repeatedly runs to pause/resume the process"""
        # if load is not too high
        if self.limit > os.getloadavg()[0]:
            # if the process is currently paused
            if psutil.Process(self.proc.pid).status() == psutil.STATUS_STOPPED:
                self._change_state(psutil.STATUS_RUNNING, signal.SIGCONT)
            return
        # if we reach here, load is high
        secs = time.time() - self.last_time  # since last state change
        # if we recently sent kill -STOP
        if self.last_state == psutil.STATUS_STOPPED:
            # if it's been sleeping as long as allowed
            if secs >= SLEEP_SECS:
                self._change_state(psutil.STATUS_RUNNING, signal.SIGCONT)
                return
            # It can sleep longer. If not already sleeping (sometimes D-state
            # eats SIGSTOP) then send SIGSTOP again, but don't update last_time
            if psutil.Process(self.proc.pid).status() != psutil.STATUS_STOPPED:
                self.proc.send_signal(signal.SIGSTOP)
        elif secs >= RUN_SECS:
            # self.last_state == psutil.STATUS_RUNNING and it's been running
            # at high load as long as allowed
            self._change_state(psutil.STATUS_STOPPED, signal.SIGSTOP)


class ResticProc(Popen):
    """subprocess.Popen subclass for use running restic"""

    def __init__(
        self,
        args,
        *,
        restic: 'Restic',
        limit: int,
        encoding='utf-8',
        stdout=PIPE,
        stderr=PIPE,
        **kwargs,
    ):
        super().__init__(
            args,
            stdout=stdout,
            stderr=stderr,
            shell=False,
            encoding=encoding,
            **kwargs,
        )
        logging.debug('%d: %s', self.pid, join_cmd(args))
        self.restic = restic
        self.mon = ProcMon(self, limit) if limit else None

    def complete(
        self,
        *,
        check: bool,
        ok_codes: Union[List[int], None] = None,
        timeout: Union[float, None] = None,
    ) -> CompletedProcess:
        """Wait for the process to complete

        Args:
            check (bool): if True, raise ResticError on error status codes
            ok_codes (list[int], optional): defines exit codes considered
                suceessful. Defaults to [0]
            timeout (float, optional): if set, kill the process and raise an
                error if it takes longer than this in seconds. Defaults to None

        Raises:
            ResticError: if check=True and exit code was not in ok_codes
            subprocess.TimeoutExpired: if a timeout was set and exceeded

        Returns:
            subprocess.CompletedProcess: process result
        """
        if ok_codes is None:
            ok_codes = [0]
        try:
            stdout, stderr = self.communicate(timeout=timeout)
        except:  # Includes KeyboardInterrupt; communicate handled that
            self.kill()
            self.wait()
            raise
        # communicate() already called self.wait()
        if check and self.returncode not in ok_codes:
            exc = CalledProcessError(
                self.returncode, self.args, output=stdout, stderr=stderr
            )
            raise ResticError(exc, self.restic)
        return CompletedProcess(self.args, self.returncode, stdout, stderr)

Zerion Mini Shell 1.0