Files
controller-wisetopdataserver/python/biopro/server/process.py
T

233 lines
5.7 KiB
Python
Raw Normal View History

2021-12-20 14:52:55 +08:00
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