Files
controller-wisetopdataserver/python/biopro/server/process.py
T
2021-12-20 14:52:55 +08:00

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