208 lines
5.6 KiB
Python
208 lines
5.6 KiB
Python
|
|
import abc
|
|||
|
|
import time
|
|||
|
|
import shutil
|
|||
|
|
import psutil
|
|||
|
|
import datetime
|
|||
|
|
import threading
|
|||
|
|
import subprocess
|
|||
|
|
from ..hands import *
|
|||
|
|
|
|||
|
|
|
|||
|
|
class BaseService(object):
|
|||
|
|
|
|||
|
|
def __init__(self, **kwargs):
|
|||
|
|
self.name = kwargs['name']
|
|||
|
|
self._process = None
|
|||
|
|
self.STOP_TIMEOUT = 10
|
|||
|
|
self.max_retry = 0
|
|||
|
|
self.retry = 3
|
|||
|
|
self.LOG_KEEP_DAYS = 7
|
|||
|
|
self.EXIT_EVENT = threading.Event()
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
@abc.abstractmethod
|
|||
|
|
def cmd(self):
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
@abc.abstractmethod
|
|||
|
|
def cwd(self):
|
|||
|
|
return ''
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def is_running(self):
|
|||
|
|
if self.pid == 0:
|
|||
|
|
return False
|
|||
|
|
try:
|
|||
|
|
os.kill(self.pid, 0)
|
|||
|
|
except (OSError, ProcessLookupError):
|
|||
|
|
return False
|
|||
|
|
else:
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
def show_status(self):
|
|||
|
|
if self.is_running:
|
|||
|
|
msg = f'{self.name} is running: {self.pid}.'
|
|||
|
|
else:
|
|||
|
|
msg = f'{self.name} is stopped.'
|
|||
|
|
if DEBUG:
|
|||
|
|
msg = '\033[31m{} is stopped.\033[0m\nYou can manual start it to find the error: \n' \
|
|||
|
|
' $ cd {}\n' \
|
|||
|
|
' $ {}'.format(self.name, self.cwd, ' '.join(self.cmd))
|
|||
|
|
|
|||
|
|
print(msg)
|
|||
|
|
|
|||
|
|
# -- log --
|
|||
|
|
@property
|
|||
|
|
def log_filename(self):
|
|||
|
|
return f'{self.name}.log'
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def log_filepath(self):
|
|||
|
|
return os.path.join(LOG_DIR, self.log_filename)
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def log_file(self):
|
|||
|
|
return open(self.log_filepath, 'a')
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def log_dir(self):
|
|||
|
|
return os.path.dirname(self.log_filepath)
|
|||
|
|
# -- end log --
|
|||
|
|
|
|||
|
|
# -- pid --
|
|||
|
|
@property
|
|||
|
|
def pid_filepath(self):
|
|||
|
|
return os.path.join(TMP_DIR, f'{self.name}.pid')
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def pid(self):
|
|||
|
|
if not os.path.isfile(self.pid_filepath):
|
|||
|
|
return 0
|
|||
|
|
with open(self.pid_filepath) as f:
|
|||
|
|
try:
|
|||
|
|
pid = int(f.read().strip())
|
|||
|
|
except ValueError:
|
|||
|
|
pid = 0
|
|||
|
|
return pid
|
|||
|
|
|
|||
|
|
def write_pid(self):
|
|||
|
|
with open(self.pid_filepath, 'w') as f:
|
|||
|
|
f.write(str(self.process.pid))
|
|||
|
|
|
|||
|
|
def remove_pid(self):
|
|||
|
|
if os.path.isfile(self.pid_filepath):
|
|||
|
|
os.unlink(self.pid_filepath)
|
|||
|
|
# -- end pid --
|
|||
|
|
|
|||
|
|
# -- process --
|
|||
|
|
@property
|
|||
|
|
def process(self):
|
|||
|
|
if not self._process:
|
|||
|
|
try:
|
|||
|
|
self._process = psutil.Process(self.pid)
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
return self._process
|
|||
|
|
|
|||
|
|
# -- end process --
|
|||
|
|
|
|||
|
|
# -- action --
|
|||
|
|
def open_subprocess(self):
|
|||
|
|
kwargs = {'cwd': self.cwd, 'stderr': self.log_file, 'stdout': self.log_file}
|
|||
|
|
self._process = subprocess.Popen(self.cmd, **kwargs)
|
|||
|
|
|
|||
|
|
def start(self):
|
|||
|
|
if self.is_running:
|
|||
|
|
self.show_status()
|
|||
|
|
return
|
|||
|
|
self.remove_pid()
|
|||
|
|
self.open_subprocess()
|
|||
|
|
self.write_pid()
|
|||
|
|
self.start_other()
|
|||
|
|
|
|||
|
|
def start_other(self):
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def stop(self, force=False):
|
|||
|
|
if not self.is_running:
|
|||
|
|
self.show_status()
|
|||
|
|
# self.remove_pid()
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
print(f'Stop service: {self.name}', end='')
|
|||
|
|
sig = 9 if force else 15
|
|||
|
|
os.kill(self.pid, sig)
|
|||
|
|
|
|||
|
|
if self.process is None:
|
|||
|
|
print("\033[31m No process found\033[0m")
|
|||
|
|
return
|
|||
|
|
try:
|
|||
|
|
self.process.wait(1)
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
for i in range(self.STOP_TIMEOUT):
|
|||
|
|
if i == self.STOP_TIMEOUT - 1:
|
|||
|
|
print("\033[31m Error\033[0m")
|
|||
|
|
if not self.is_running:
|
|||
|
|
print("\033[32m Ok\033[0m")
|
|||
|
|
self.remove_pid()
|
|||
|
|
break
|
|||
|
|
else:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
def watch(self):
|
|||
|
|
self._check()
|
|||
|
|
if not self.is_running:
|
|||
|
|
self._restart()
|
|||
|
|
self._rotate_log()
|
|||
|
|
|
|||
|
|
def _check(self):
|
|||
|
|
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|||
|
|
print(f"{now} Check service status: {self.name} -> ", end='')
|
|||
|
|
if self.process:
|
|||
|
|
try:
|
|||
|
|
self.process.wait(1) # 不wait,子进程可能无法回收
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
if self.is_running:
|
|||
|
|
print(f'running at {self.pid}')
|
|||
|
|
else:
|
|||
|
|
print(f'stopped at {self.pid}')
|
|||
|
|
|
|||
|
|
def _restart(self):
|
|||
|
|
if self.retry > self.max_retry:
|
|||
|
|
logging.info("Service start failed, exit: {}".format(self.name))
|
|||
|
|
self.EXIT_EVENT.set()
|
|||
|
|
return
|
|||
|
|
self.retry += 1
|
|||
|
|
logging.info(f'> Find {self.name} stopped, retry {self.retry}, {self.pid}')
|
|||
|
|
self.start()
|
|||
|
|
|
|||
|
|
def _rotate_log(self):
|
|||
|
|
now = datetime.datetime.now()
|
|||
|
|
_time = now.strftime('%H:%M')
|
|||
|
|
if _time != '23:59':
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
backup_date = now.strftime('%Y-%m-%d')
|
|||
|
|
backup_log_dir = os.path.join(self.log_dir, backup_date)
|
|||
|
|
if not os.path.exists(backup_log_dir):
|
|||
|
|
os.mkdir(backup_log_dir)
|
|||
|
|
|
|||
|
|
backup_log_path = os.path.join(backup_log_dir, self.log_filename)
|
|||
|
|
if os.path.isfile(self.log_filepath) and not os.path.isfile(backup_log_path):
|
|||
|
|
logging.info(f'Rotate log file: {self.log_filepath} => {backup_log_path}')
|
|||
|
|
shutil.copy(self.log_filepath, backup_log_path)
|
|||
|
|
with open(self.log_filepath, 'w') as f:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
to_delete_date = now - datetime.timedelta(days=self.LOG_KEEP_DAYS)
|
|||
|
|
to_delete_dir = os.path.join(LOG_DIR, to_delete_date.strftime('%Y-%m-%d'))
|
|||
|
|
if os.path.exists(to_delete_dir):
|
|||
|
|
logging.info(f'Remove old log: {to_delete_dir}')
|
|||
|
|
shutil.rmtree(to_delete_dir, ignore_errors=True)
|
|||
|
|
# -- end action --
|