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

706 lines
20 KiB
Python

import abc
import os
import selectors as _selectors
import socket as _socket
import sys
import threading as _threading
from concurrent.futures import Executor, ThreadPoolExecutor as _ThreadPoolExecutor
from selectors import DefaultSelector
from threading import Thread
from time import time
from typing import Set, Type, Union, TypeVar, Tuple, Any, Optional
from biopro import NAME, VERSION, COPYRIGHT
from biopro.controller import ComplexResponse
from biopro.util.console import pp, foreground as _fc, LINE_BREAK
from biopro.util.json import JsonSerialize
from biopro.util.logger import LoggerFlag, Logger
from biopro.util.sleep import long_sleep
from biopro.util.socket import socket_send, socket_recv, Socket, SocketTimeoutError
from biopro.util.stack import *
from .dispatcher import APIDispatcher
_RUNTIME_COMPILE = False
if sys.version_info.minor < 6:
# noinspection PyUnusedLocal
def ThreadPoolExecutor(max_workers: int = 16,
thread_name_prefix = None):
return _ThreadPoolExecutor(max_workers=max_workers)
else:
ThreadPoolExecutor = _ThreadPoolExecutor
class ServerThreadClosedInterrupt(RuntimeError):
"""server thread has closed"""
__slots__ = ()
class ServerShutdownException(RuntimeError):
"""server shutdown RuntimeError as signal"""
__slots__ = ()
class ServerRestartException(RuntimeError):
"""server restart RuntimeError as signal."""
SERVER_RESTART_EXIT = 4
"""process exit code for restart server"""
SERVER_DUMMY_EXIT = 5
"""process exit code for restart in dummy mode"""
__slots__ = ('mode',)
def __init__(self, mode: int = SERVER_RESTART_EXIT):
if mode not in (4, 5):
raise ValueError()
self.mode = mode
T_ServerThread = TypeVar('T_ServerThread', bound='ServerThread')
class SocketServer(Logger, metaclass=abc.ABCMeta):
"""the server which provide the (file) socket protocol.
"""
SOCKET_ACCEPT_KEY = 'SOCKET_ACCEPT_KEY'
__slots__ = ('__socket_file', '__selector', '__socket', '__socket_listen',
'__server_name', '__dispatcher', '__log_level',
'__worker_thread', '__executor', '__response_counter',
'__close_flag', '__interrupt',
'__thread_pool_count', '__mqtt_server_ip', '__mqtt_server_port'
'__db_host', '__db_name', '__db_user', '__db_password',
'_threads')
def __init__(self,
api_class: Union[Type, Tuple[Type, ...]],
server_name: str,
socket_file: str,
socket_listen = 1,
thread_pool_count=16):
"""
:param api_class: api provide.
:param server_name: server name
:param socket_file: socket filename
:param socket_listen: number of listen socket port
:param thread_pool_count: number of thread in pool.
"""
self.__server_name = server_name
# socket relative
self.__socket_file = socket_file
self.__selector = None
self.__socket = None
self.__socket_listen = socket_listen
# method dispatcher
self.__dispatcher = APIDispatcher(api_class, self)
# general server thread
self.__worker_thread = set()
# self._executor = ThreadPoolExecutor(max_workers=8)
self._threads = []
self._event = _threading.Event()
# response thread
self.__executor = None
self.__response_counter = 0
# close flag
self.__close_flag = False
self.__interrupt = None
# worker control
self.__thread_pool_count = thread_pool_count
# mqtt control options
self.__mqtt_server_ip = '52.194.17.114'
self.__mqtt_server_port = 1883
# database constrol options
self.__db_host = '127.0.0.1'
self.__db_name = 'postgres'
self.__db_user = 'biopro'
self.__db_password = 'BioProControlBox'
@property
def server_name(self) -> str:
return self.__server_name
@property
def socket_file(self) -> str:
return self.__socket_file
@property
def socket_db_host(self) -> str:
return self.__db_host
@property
def socket_db_name(self) -> str:
return self.__db_name
@property
def socket_db_user(self) -> str:
return self.__db_user
@property
def socket_db_password(self) -> str:
return self.__db_password
@property
def socket_mqtt_port(self) -> int:
return self.__mqtt_server_port
@property
def socket_mqtt_ip(self) -> str:
return self.__mqtt_server_ip
def interrupt(self, exception: BaseException) -> None:
"""interrupt server main thread with non-rescue ``exception``.
:param exception: error be raised in the main server thread.
:return: None
"""
if not _RUNTIME_COMPILE:
self.log_info('interrupt with', exception.__class__.__name__)
self.__interrupt = exception
def is_interrupted(self) -> bool:
if self.__interrupt is not None:
if isinstance(self.__interrupt, ServerRestartException):
raise ServerRestartException(self.__interrupt.mode)
else:
return True
return self.__close_flag
@property
def is_closed(self) -> bool:
return self.__close_flag
@abc.abstractmethod
def setup(self):
"""call when server starting.
"""
pass
@abc.abstractmethod
def close(self, external_interrupt: bool):
"""call when server closing.
:param external_interrupt: interrupt from external Ctrl+C
"""
pass
def shutdown_server(self):
self.log_info('shutdown server')
self.interrupt(ServerShutdownException())
def restart_server(self):
self.log_info('restart server')
self.interrupt(ServerRestartException())
# noinspection PyBroadException
def main(self) -> int:
"""server main function.
:return: exit code.
"""
# print server information
self.log_info(LINE_BREAK)
self.log_info(NAME, VERSION, self.__server_name)
self.log_info(COPYRIGHT)
self.log_info(LINE_BREAK)
self.log_verbose('start', self.__server_name, 'process')
# initialize thread pool.
if self.__thread_pool_count > 0:
executor = ThreadPoolExecutor(thread_name_prefix=self.__server_name,
max_workers=self.__thread_pool_count)
else:
executor = None
self.__executor = executor
self.__close_flag = False
exit_code = 0
external_interrupt = False
# this function focus on error handling.
try:
self._handle_socket_open()
try:
self.setup()
self._handle_socket_routine_forever()
finally:
self._handle_socket_close()
# close response thread
self.__close_flag = True
if executor is not None:
executor.shutdown(wait=False)
except KeyboardInterrupt:
# user or system send SIGTERM signal
print("")
self.log_warn('interrupt captured')
external_interrupt = True
except ServerShutdownException:
self.log_verbose('catch server shutdown signal')
exit_code = 0
except ServerRestartException as e:
self.log_verbose('catch server restart signal')
exit_code = e.mode
except OSError:
e_type, e_value, e_stack = sys.exc_info()
if '[Errno 98] Address already in use' in str(e_value):
# socket file already in used, may has another process is using it
# or previous process forget to delete it.
self.log_error('server fatal error : process has already run')
exit_code = 1
if not _RUNTIME_COMPILE:
print_exception(self)
except:
# unknown error
if not _RUNTIME_COMPILE:
print_exception(self)
finally:
self.log_verbose('close service')
try:
self.broadcast_command('shutdown')
except:
self.log_verbose('broadcast_command shutdown error')
try:
# close server thread
# copy a list avoid worker closing change the content
# which would raise RuntimeError: Set changed size during iteration
current_worker = list(self.__worker_thread)
for worker in current_worker:
worker.close()
self.close(external_interrupt)
if external_interrupt:
# wait all thread if server shutdown by interrupt
for worker in current_worker:
worker.join(5)
for worker in current_worker:
if worker.thread.is_alive():
self.log_verbose(worker.log_name, 'still alive')
except KeyboardInterrupt:
pass
except:
if not _RUNTIME_COMPILE:
print_exception(self)
self.log_info('server', self.__server_name, 'exit', exit_code)
return exit_code
def _handle_socket_open(self):
"""open file socket. """
# create selector,
# let OS notify us when the file-object (in here, is socket) is ready to be read.
# Therefore, we can write code without being blocked by IO function.
if self.__selector is not None or self.__socket is not None:
raise RuntimeError('SocketHandler has opened')
if os.path.exists(self.__socket_file):
os.remove(self.__socket_file)
self.__selector = DefaultSelector()
self.__socket = Socket(_socket.AF_UNIX, _socket.SOCK_STREAM)
logger = self
if isinstance(logger, LoggerFlag):
logger.log_verbose('listen', self.__socket_file)
self.__socket.bind(self.__socket_file)
self.__socket.listen(self.__socket_listen)
self.__socket.setblocking(False)
# register socket in selector.
# When it is ready to read, we can get the event at selector.select()
self.__selector.register(self.__socket, _selectors.EVENT_READ, self.SOCKET_ACCEPT_KEY)
def _handle_socket_close(self):
"""close socket. """
selector = self.__selector
socket = self.__socket
if socket is not None:
socket.close()
if selector is not None:
selector.close()
if os.path.exists(self.__socket_file):
os.remove(self.__socket_file)
self.__selector = None
self.__socket = None
def _handle_socket_routine_once(self, timeout: int = 1):
"""listen socket incoming at least *timeout* second.
Once socket connected, call :func:`handle_socket_accept`.
:param timeout:
"""
selector = self.__selector
if selector is None:
raise RuntimeError('SocketHandler not open')
e = selector.select(timeout)
if self.is_interrupted():
return
for k, _ in e:
if k.data == self.SOCKET_ACCEPT_KEY:
socket = k.fileobj
cnt, _ = socket.accept()
cnt.setblocking(False)
self._handle_socket_accept(cnt)
def _handle_socket_routine_forever(self):
"""listen socket incoming forever, unit :func:`is_interrupted` not True.
Once socket connected, call :func:`handle_socket_accept`.
"""
selector = self.__selector
if selector is None:
raise RuntimeError('SocketHandler not open')
while not self.is_interrupted():
e = selector.select(1)
if self.is_interrupted():
return
for k, _ in e:
if k.data == self.SOCKET_ACCEPT_KEY:
socket = k.fileobj
cnt, _ = socket.accept()
cnt.setblocking(False)
self._handle_socket_accept(cnt)
def _handle_socket_accept(self, socket: Socket):
c = self.__response_counter
self.__response_counter += 1
# pass incoming socket to task thread.
self.submit_thread(self.__handle_socket_accept, c, socket, time())
def __handle_socket_accept(self, counter: int, socket: _socket.socket, init_time: float):
start_time = time()
wait_time = (start_time - init_time) * 1000
thread = _threading.current_thread()
# give task thread a attribute 'log_header'
# make it work like a LoggerFlag in LoggerFormatter.
# it will replace the default thread name with our 'header' in logging message.
header = pp('%04d' % counter, _fc(counter), ']')
thread.log_header = header
try:
self._handle_socket_communicate(socket)
except (ServerRestartException, ServerShutdownException) as e:
self.interrupt(e)
except BrokenPipeError:
self.log_verbose(header, 'broken pipe')
except SocketTimeoutError:
self.log_verbose(header, 'timeout')
except:
print_exception(self)
finally:
if not _RUNTIME_COMPILE:
close_time = time()
live_time = (close_time - start_time) * 1000
total_time = (close_time - init_time) * 1000
if wait_time > 1000:
wait_time = '%.1f s,' % (wait_time / 1000)
else:
wait_time = '%.2f ms,' % wait_time
if live_time > 1000:
live_time = '%.1f s,' % (live_time / 1000)
else:
live_time = '%.2f ms,' % live_time
if total_time > 1000:
total_time = '%.1f s' % (total_time / 1000)
else:
total_time = '%.2f ms' % total_time
self.log_verbose('wait', wait_time,
'live', live_time,
'total', total_time)
del thread.log_header
def _handle_socket_communicate(self, socket: Socket):
"""communicate with socket.
:param socket:
"""
try:
go = True
while not self.is_interrupted() and go:
recv = socket_recv(socket, 3)
if recv is None:
break
send, go = self._handle_socket_receive(recv)
socket_send(socket, send)
except SocketTimeoutError:
pass
finally:
socket.close()
def _handle_socket_receive(self, content: Any) -> Tuple[Any, bool]:
if not isinstance(content, tuple) or len(content) == 0:
return None, False
try:
method, argument, options = content
except ValueError:
return None, False
try:
ret = self.__dispatcher.method_dispatch(method, argument, options)
except BaseException as e:
if isinstance(self, Logger):
print_exception(self)
return RuntimeError(e.__class__.__name__, *e.args), True
else:
if isinstance(ret, ComplexResponse):
return ComplexResponse(JsonSerialize.to_json(ret.response), ret), True
else:
return JsonSerialize.to_json(ret), True
def submit_thread(self, f, *args):
"""submit task to thread pool.
:param f: callable
:param args: *f* 's arguments
"""
if self.__executor is not None:
self.__executor.submit(f, *args)
else:
# do not make exception impact main thread,
# ket its behavior as same as using executor
try:
f(*args)
except ServerShutdownException as e:
self.interrupt(e)
except ServerThreadClosedInterrupt:
pass
except:
print_exception(self)
def run_thread(self, thread: T_ServerThread) -> T_ServerThread:
"""start daemon thread.
:param thread: server thread instance
:return: *thread* itself.
"""
if thread is None:
raise TypeError('NoneType for ServerThread')
thread.log_verbose('start')
if self.__executor is not None:
self._threads.append(self.__executor.submit(self._run_thread, thread))
# Thread(target=self._run_thread,
# name=thread.log_name,
# args=(thread,),
# daemon=True).start()
return thread
def _run_thread(self, thread: 'ServerThread'):
try:
self.__worker_thread.add(thread)
thread.log_verbose('setup')
thread.setup()
if not thread._event.is_set():
thread.log_verbose('routine')
while not thread._event.is_set():
thread.routine(thread._event)
except ServerShutdownException as e:
self.interrupt(e)
except ServerThreadClosedInterrupt:
pass
except:
print_exception(self)
finally:
try:
thread.log_verbose('closing')
thread.shutdown()
except KeyboardInterrupt:
thread.log_warn('closing interrupt')
finally:
try:
self.__worker_thread.remove(thread)
except KeyError:
pass
thread.log_verbose('closed')
class ServerThread(LoggerFlag, metaclass=abc.ABCMeta):
"""server thread."""
def __init__(self, name: str, header: Union[str, int, Tuple[Any, ...]] = None):
super().__init__(name, header)
self.__thread = None
self.__closed = False
self.__condition = _threading.Condition()
self._event = _threading.Event()
@property
def thread(self) -> Optional[_threading.Thread]:
return self.__thread
def join(self, timeout: Optional[float] = None):
if self.__thread is not None:
self.__thread.join(timeout)
def setup(self) -> None:
"""call when thread is started"""
# reset close flag
self.__closed = False
# get current thread
self.__thread = _threading.current_thread()
return
@abc.abstractmethod
def routine(self, event = None) -> None:
"""routine logic"""
return
def shutdown(self) -> None:
"""call when thread is closing"""
return
def close(self) -> None:
"""close this thread"""
if not self.__closed:
self.log_verbose('close')
self.__closed = True
self._event.set()
self.notify_all()
@property
def is_startup(self):
"""
:return: is this thread setup
"""
return self.__thread is not None and not self.__closed
@property
def is_closed(self):
"""
:return: is this thread closing
"""
return self.__closed
def sleep(self, second: float, disable_interrupt=False):
"""long period sleep"""
if long_sleep(second, lambda: self.__closed):
if not disable_interrupt:
raise ServerThreadClosedInterrupt()
def wait(self, timeout: Optional[float] = None):
with self.__condition:
self.log_verbose('wait')
self.__condition.wait(timeout)
self.log_verbose('wake')
def notify_all(self):
with self.__condition:
self.log_verbose('notify')
self.__condition.notify_all()
def __str__(self):
return type(self).__name__
def __repr__(self):
return str(self)