233 lines
5.7 KiB
Python
233 lines
5.7 KiB
Python
import abc
|
|
import os
|
|
from pathlib import Path
|
|
from subprocess import Popen, TimeoutExpired
|
|
from time import time, sleep
|
|
from typing import TypeVar, Generic, Optional, List
|
|
|
|
from biopro.util.cli_base import CliOptions
|
|
from biopro.util.logger import LoggerFlag
|
|
from biopro.util.socket import SocketClient
|
|
from biopro.util.sys import current_command_header, PID
|
|
|
|
PO = TypeVar('PO', bound=CliOptions)
|
|
PC = TypeVar('PC', bound=SocketClient)
|
|
|
|
|
|
class ControllerProcess(LoggerFlag, Generic[PO, PC], metaclass=abc.ABCMeta):
|
|
"""the class handle the process controlled by the :class:`ControlServer`
|
|
|
|
"""
|
|
|
|
def __init__(self,
|
|
name: str,
|
|
options: PO,
|
|
flag_disable=False):
|
|
"""
|
|
|
|
:param name: process name
|
|
:param options: server cli options
|
|
:param flag_disable: disable process
|
|
"""
|
|
super().__init__(name)
|
|
|
|
self.__flag_disable = flag_disable
|
|
self.__options: PO = options
|
|
self.__process: Optional[Popen] = None
|
|
|
|
# process start time stamp
|
|
self.__start_time: Optional[float] = None
|
|
|
|
# delay response client()
|
|
# client() return None if process have not setup until delay seconds,
|
|
# otherwise raise error
|
|
self.__delay_time = 30
|
|
|
|
@property
|
|
def options(self) -> PO:
|
|
"""
|
|
|
|
:return: server cli options
|
|
"""
|
|
return self.__options
|
|
|
|
@property
|
|
def process(self) -> Optional[Popen]:
|
|
"""
|
|
|
|
:return: process
|
|
"""
|
|
return self.__process
|
|
|
|
@property
|
|
def is_alive(self) -> bool:
|
|
return self.__process is not None and self.__process.returncode is None
|
|
|
|
@property
|
|
def return_code(self) -> Optional[int]:
|
|
"""
|
|
|
|
:return: process exit code. If process not terminated, return None. If *flag_disable* is True, return -1.
|
|
"""
|
|
p = self.__process
|
|
|
|
if p is None:
|
|
return -1
|
|
|
|
else:
|
|
return p.poll()
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def socket_file(self) -> Path:
|
|
"""
|
|
|
|
:return: socket file path
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def get_process_arguments(self) -> List[str]:
|
|
"""
|
|
|
|
:return: cli arguments
|
|
"""
|
|
pass
|
|
|
|
def start_process(self) -> bool:
|
|
"""start process.
|
|
|
|
:return: process started.
|
|
"""
|
|
if self.__flag_disable:
|
|
return False
|
|
|
|
elif self.__process is not None:
|
|
if self.__process.returncode is not None:
|
|
raise RuntimeError('process has terminated')
|
|
return True
|
|
|
|
self.log_info('start_process')
|
|
|
|
socket_file = self.socket_file
|
|
if socket_file.exists():
|
|
socket_file.unlink()
|
|
|
|
arg = self.get_process_arguments()
|
|
self.log_verbose(' '.join(arg))
|
|
|
|
env = dict(os.environ)
|
|
env['BPS_PARENT_PID'] = str(PID)
|
|
cmd = current_command_header()
|
|
|
|
cmd.extend(arg)
|
|
|
|
self.__process = process = Popen(cmd, env=env)
|
|
self.log_info('process PID :', process.pid)
|
|
|
|
if process.poll() is not None:
|
|
self.log_info('start fail, exit', process.returncode)
|
|
self.__start_time = 0
|
|
return False
|
|
|
|
else:
|
|
self.__start_time = time()
|
|
return True
|
|
|
|
def stop_process(self, external_interrupt: bool) -> Optional[int]:
|
|
"""stop process.
|
|
|
|
:param external_interrupt:
|
|
:return: exit code. If *flag_disable* is True, return None.
|
|
"""
|
|
p = self.__process
|
|
|
|
if p is None:
|
|
return None
|
|
|
|
self.log_info('stop_process')
|
|
|
|
try:
|
|
with p:
|
|
if p.poll() is not None:
|
|
return p.returncode
|
|
|
|
if not external_interrupt:
|
|
self.log_verbose('shutdown')
|
|
try:
|
|
self.shutdown()
|
|
except BaseException as e:
|
|
self.log_warn(e)
|
|
|
|
retry = 0
|
|
|
|
while retry < 3:
|
|
if p.poll() is not None:
|
|
return p.returncode
|
|
|
|
else:
|
|
sleep(1)
|
|
retry += 1
|
|
|
|
self.log_verbose('terminate')
|
|
p.terminate()
|
|
|
|
try:
|
|
p.wait(10)
|
|
except TimeoutExpired:
|
|
self.log_verbose('kill')
|
|
p.kill()
|
|
|
|
return p.returncode
|
|
|
|
finally:
|
|
exit_code = p.returncode
|
|
|
|
if exit_code is not None:
|
|
self.log_info('process exit code :', exit_code)
|
|
|
|
def restart_process(self) -> bool:
|
|
"""restart process"""
|
|
if self.__flag_disable:
|
|
return False
|
|
|
|
self.log_info('restart process')
|
|
|
|
self.stop_process(False)
|
|
|
|
sleep(1)
|
|
|
|
# unset process
|
|
self.__process = None
|
|
|
|
return self.start_process()
|
|
|
|
def client(self) -> Optional[PC]:
|
|
"""
|
|
|
|
:return: client interface. None if *flag_disable* is True, or process just started.
|
|
:raises RuntimeError: process not start or no response.
|
|
"""
|
|
if self.__flag_disable:
|
|
return None
|
|
|
|
elif self.__process is not None:
|
|
return self.new_client()
|
|
|
|
elif self.__start_time is None:
|
|
raise RuntimeError('process ' + self.log_name + ' not start')
|
|
|
|
elif time() - self.__start_time < self.__delay_time:
|
|
return None
|
|
|
|
else:
|
|
raise RuntimeError('process ' + self.log_name + ' no response')
|
|
|
|
@abc.abstractmethod
|
|
def new_client(self) -> PC:
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def shutdown(self):
|
|
pass
|