├── grma ├── server │ ├── __init__.py │ └── base.py ├── __init__.py ├── worker.py ├── utils.py ├── config.py ├── app.py ├── pidfile.py └── mayue.py ├── images └── logo.png ├── .gitignore ├── setup.py └── README.md /grma/server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biubiu/grma/master/images/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | *.pyc 3 | *.swp 4 | *.iml 5 | *.egg-info 6 | *.egg 7 | *.patch 8 | .#* 9 | dist 10 | build 11 | tests/.cache/* 12 | .cache 13 | venv 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /grma/server/base.py: -------------------------------------------------------------------------------- 1 | class ServerBase(object): 2 | """All gRPC server class should base on""" 3 | 4 | def __repr__(self): 5 | return '<%s>' % self.__class__.__name__ 6 | 7 | def start(self): 8 | raise NotImplementedError() 9 | 10 | def bind(self, host, port, private_key_path='', certificate_chain_path=''): 11 | raise NotImplementedError() 12 | 13 | def stop(self, grace=0): 14 | raise NotImplementedError() 15 | -------------------------------------------------------------------------------- /grma/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | 3 | version_info = (0, 0, 3) 4 | __version__ = '.'.join([str(i) for i in version_info]) 5 | __logo__ = ''' 6 | ************************************ 7 | 8 | ██████╗ ██████╗ ███╗ ███╗ █████╗ 9 | ██╔════╝ ██╔══██╗████╗ ████║██╔══██╗ 10 | ██║ ███╗██████╔╝██╔████╔██║███████║ 11 | ██║ ██║██╔══██╗██║╚██╔╝██║██╔══██║ 12 | ╚██████╔╝██║ ██║██║ ╚═╝ ██║██║ ██║ 13 | ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ 14 | 15 | fire in the hole 16 | 17 | ************************************ 18 | ''' 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from grma import __version__ 2 | 3 | from setuptools import setup, find_packages 4 | 5 | setup( 6 | name='grma', 7 | version=__version__, 8 | 9 | description='Simple gRPC Python manager', 10 | author='GuoJing', 11 | author_email='soundbbg@gmail.com', 12 | license='MIT', 13 | url='https://github.com/qiajigou/grma', 14 | zip_safe=False, 15 | packages=find_packages(exclude=['examples', 'tests']), 16 | include_package_data=True, 17 | entry_points=""" 18 | [console_scripts] 19 | grma=grma.app:run 20 | """, 21 | install_requires=[ 22 | 'setproctitle==1.1.10' 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /grma/worker.py: -------------------------------------------------------------------------------- 1 | import os 2 | import utils 3 | import signal 4 | 5 | 6 | class Worker(object): 7 | def __init__(self, pid, server, args): 8 | self.server = server 9 | self.args = args 10 | self.master_pid = pid 11 | self.init_signals() 12 | 13 | def run(self): 14 | pid = os.getpid() 15 | print '[OK] Worker running with pid: {pid}'.format(pid=pid) 16 | utils.setproctitle('grma worker pid={pid}'.format(pid=pid)) 17 | self.server.start() 18 | 19 | def stop(self): 20 | self.server.stop(self.args.grace) 21 | 22 | def init_signals(self): 23 | signal.signal(signal.SIGQUIT, self.handle_quit) 24 | signal.signal(signal.SIGTERM, self.handle_exit) 25 | signal.signal(signal.SIGINT, self.handle_quit) 26 | 27 | def _stop(self): 28 | self.stop() 29 | self.kill_worker(self.master_pid, signal.SIGTERM) 30 | 31 | def handle_quit(self, sig, frame): 32 | self._stop() 33 | 34 | def handle_exit(self, sig, frame): 35 | self._stop() 36 | 37 | def kill_worker(self, pid, sig): 38 | try: 39 | os.kill(pid, sig) 40 | except OSError: 41 | pass 42 | -------------------------------------------------------------------------------- /grma/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | try: 4 | from os import closerange 5 | except ImportError: 6 | def closerange(fd_low, fd_high): 7 | for fd in range(fd_low, fd_high): 8 | try: 9 | os.close(fd) 10 | except OSError: 11 | pass 12 | 13 | try: 14 | from setproctitle import setproctitle 15 | except ImportError: 16 | def setproctitle(title): 17 | return 18 | 19 | 20 | def getcwd(): 21 | try: 22 | pwd = os.stat(os.environ['PWD']) 23 | cwd = os.stat(os.getcwd()) 24 | if pwd.st_ino == cwd.st_ino and pwd.st_dev == cwd.st_dev: 25 | cwd = os.environ['PWD'] 26 | else: 27 | cwd = os.getcwd() 28 | except: 29 | cwd = os.getcwd() 30 | return cwd 31 | 32 | 33 | def daemonize(): 34 | if os.fork(): 35 | os._exit(0) 36 | os.setsid() 37 | 38 | if os.fork(): 39 | os._exit(0) 40 | 41 | os.umask(0o22) 42 | 43 | closerange(0, 3) 44 | 45 | redir = getattr(os, 'devnull', '/dev/null') 46 | fd_null = os.open(redir, os.O_RDWR) 47 | 48 | if fd_null != 0: 49 | os.dup2(fd_null, 0) 50 | 51 | os.dup2(fd_null, 1) 52 | os.dup2(fd_null, 2) 53 | -------------------------------------------------------------------------------- /grma/config.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | 4 | class Config(object): 5 | def parser(self): 6 | parser = argparse.ArgumentParser(description='A simple gunicorn like ' 7 | 'gRPC server management tool') 8 | parser.add_argument('--host', type=str, 9 | default='0.0.0.0', 10 | help='an string for gRPC Server host') 11 | parser.add_argument('--port', type=int, 12 | default=60051, 13 | help='an integer for gRPC Server port') 14 | parser.add_argument('--private', type=str, default='', 15 | help='a string of private key path') 16 | parser.add_argument('--certificate', type=str, default='', 17 | help='a string of private certificate key path') 18 | parser.add_argument('--cls', type=str, required=True, 19 | help='a string of gRPC server module ' 20 | '[app:server]') 21 | parser.add_argument('--num', type=int, default=1, 22 | help='a int of worker number') 23 | parser.add_argument('--pid', type=str, 24 | help='pid file for grma') 25 | parser.add_argument('--daemon', type=int, default=0, 26 | help='run as daemon') 27 | parser.add_argument('--grace', type=int, default=3, 28 | help='timeout for graceful shutdown') 29 | return parser 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # Overview 6 | 7 | A simple gunicorn like gRPC management server. 8 | 9 | # How to used 10 | 11 | inherit `ServerBase` to create your own `JediServer` Class: 12 | 13 | ```python 14 | from grma.server.base import ServerBase 15 | 16 | class JediServer(ServerBase): 17 | """Your gRPC server class""" 18 | 19 | def start(self): 20 | pass 21 | 22 | def bind(self, host, port, private_key_path='', certificate_chain_path=''): 23 | pass 24 | 25 | def stop(self, grace=3): 26 | pass 27 | 28 | app = JediServer() 29 | ``` 30 | 31 | Launching should be simple: 32 | 33 | run grma --port=50051 --cls=app:app --num=8 --daemon=1 34 | 35 | 36 | Get more from help 37 | 38 | 39 | ``` 40 | usage: grma [-h] [--host HOST] [--port PORT] [--private PRIVATE] 41 | [--certificate CERTIFICATE] --cls CLS [--num NUM] [--pid PID] 42 | [--daemon DAEMON] 43 | 44 | A simple gunicorn like gRPC server management tool 45 | 46 | optional arguments: 47 | -h, --help show this help message and exit 48 | --host HOST an string for gRPC Server host 49 | --port PORT an integer for gRPC Server port 50 | --private PRIVATE a string of private key path 51 | --certificate CERTIFICATE 52 | a string of private certificate key path 53 | --cls CLS a string of gRPC server module [app:server] 54 | --num NUM a int of worker number 55 | --pid PID pid file for grma 56 | --daemon DAEMON run as daemon 57 | ``` 58 | 59 | # TODO 60 | 61 | Lots to do... 62 | -------------------------------------------------------------------------------- /grma/app.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import utils 3 | import importlib 4 | 5 | from config import Config 6 | from mayue import Mayue 7 | 8 | 9 | class Application(object): 10 | def __init__(self): 11 | self.cfg = None 12 | self.args = None 13 | self.server = None 14 | self.init_path() 15 | self.init_config() 16 | self.load_config() 17 | self.load_class() 18 | 19 | def __repr__(self): 20 | return '' 21 | 22 | def run(self): 23 | if self.server: 24 | Mayue(self).run() 25 | 26 | def init_path(self): 27 | path = utils.getcwd() 28 | sys.path.insert(0, path) 29 | 30 | def init_config(self): 31 | self.cfg = Config() 32 | 33 | def load_config(self): 34 | parser = self.cfg.parser() 35 | args = parser.parse_args() 36 | self.args = args 37 | 38 | def load_class(self): 39 | try: 40 | kls = self.args.cls 41 | module, var = kls.split(':') 42 | i = importlib.import_module(module) 43 | c = i.__dict__.get(var) 44 | if c: 45 | try: 46 | if getattr(c, 'start') and getattr(c, 'stop'): 47 | self.server = c 48 | except AttributeError: 49 | msg = '''--cls={cls} have no [start] or [stop] method: 50 | 51 | exp: 52 | 53 | class App(object): 54 | def __init__(self): 55 | pass 56 | 57 | def start(self): 58 | # start the gRPC server 59 | 60 | def stop(self): 61 | # stop the gRPC server 62 | ''' 63 | print msg 64 | return False 65 | else: 66 | return False 67 | except Exception, e: 68 | print e 69 | return False 70 | 71 | 72 | def run(): 73 | Application().run() 74 | 75 | 76 | if __name__ == '__main__': 77 | run() 78 | -------------------------------------------------------------------------------- /grma/pidfile.py: -------------------------------------------------------------------------------- 1 | # original code from gunicorn pidfile 2 | # has some little change 3 | 4 | import errno 5 | import os 6 | import tempfile 7 | 8 | 9 | class Pidfile(object): 10 | def __init__(self, fname): 11 | self.fname = fname 12 | self.pid = None 13 | 14 | def create(self, pid): 15 | oldpid = self.validate() 16 | if oldpid: 17 | if oldpid == os.getpid(): 18 | return 19 | msg = ('Already running on PID {oldpid} ' 20 | '(or pid file {fname} is stale)') 21 | raise RuntimeError(msg.format(oldpid=oldpid, fname=self.fname)) 22 | raise RuntimeError(msg % (oldpid, self.fname)) 23 | 24 | self.pid = pid 25 | 26 | # Write pidfile 27 | fdir = os.path.dirname(self.fname) 28 | if fdir and not os.path.isdir(fdir): 29 | msg = '{fdir} does not exitst, cant create pidfile'.format( 30 | fdir=fdir) 31 | raise RuntimeError(msg) 32 | fd, fname = tempfile.mkstemp(dir=fdir) 33 | os.write(fd, ('{pid}\n'.format(pid=self.pid)).encode('utf-8')) 34 | if self.fname: 35 | os.rename(fname, self.fname) 36 | else: 37 | self.fname = fname 38 | os.close(fd) 39 | os.chmod(self.fname, 420) 40 | 41 | def rename(self, path): 42 | self.unlink() 43 | self.fname = path 44 | self.create(self.pid) 45 | 46 | def unlink(self): 47 | try: 48 | with open(self.fname, 'r') as f: 49 | pid1 = int(f.read() or 0) 50 | 51 | if pid1 == self.pid: 52 | os.unlink(self.fname) 53 | except: 54 | pass 55 | 56 | def validate(self): 57 | if not self.fname: 58 | return 59 | try: 60 | with open(self.fname, 'r') as f: 61 | try: 62 | wpid = int(f.read()) 63 | except ValueError: 64 | return 65 | 66 | try: 67 | os.kill(wpid, 0) 68 | return wpid 69 | except OSError as e: 70 | if e.args[0] == errno.ESRCH: 71 | return 72 | raise 73 | except IOError as e: 74 | if e.args[0] == errno.ENOENT: 75 | return 76 | raise 77 | -------------------------------------------------------------------------------- /grma/mayue.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import utils 4 | import signal 5 | 6 | from time import sleep 7 | 8 | from grma import __version__, __logo__ 9 | from worker import Worker 10 | from pidfile import Pidfile 11 | 12 | 13 | class Mayue(object): 14 | ctx = dict() 15 | workers = dict() 16 | 17 | def __init__(self, app): 18 | self.app = app 19 | self.pid = None 20 | self.pidfile = None 21 | self.try_to_stop = False 22 | 23 | args = sys.argv[:] 24 | args.insert(0, sys.executable) 25 | cwd = utils.getcwd() 26 | 27 | self.ctx = dict(args=args, cwd=cwd, exectable=sys.executable) 28 | 29 | def spawn_worker(self): 30 | sleep(0.1) 31 | worker = Worker(self.pid, self.app.server, self.app.args) 32 | 33 | pid = os.fork() 34 | 35 | if pid != 0: 36 | # parent process 37 | self.workers[pid] = worker 38 | return pid 39 | 40 | # child process 41 | try: 42 | worker.run() 43 | sys.exit(0) 44 | except Exception as e: 45 | print e 46 | finally: 47 | worker.stop(self.app.args.grace) 48 | 49 | def stop_workers(self): 50 | for pid, worker in self.workers.items(): 51 | worker.stop() 52 | del self.workers[pid] 53 | self.kill_worker(pid, signal.SIGKILL) 54 | 55 | def kill_worker(self, pid, sig): 56 | try: 57 | os.kill(pid, sig) 58 | except OSError: 59 | pass 60 | 61 | def clean(self): 62 | self.stop_workers() 63 | if self.pidfile is not None: 64 | self.pidfile.unlink() 65 | 66 | def run(self): 67 | print __logo__ 68 | 69 | print '[OK] Running grma {version}'.format(version=__version__) 70 | 71 | print '-' * 10 + ' CONFIG ' + '-' * 10 72 | 73 | cf = dict() 74 | for arg in vars(self.app.args): 75 | cf[arg] = getattr(self.app.args, arg) 76 | 77 | for k, v in cf.items(): 78 | msg = '{key}\t{value}'.format(key=k, value=v) 79 | print msg 80 | 81 | print '-' * 28 82 | 83 | if self.app.args.daemon: 84 | utils.daemonize() 85 | 86 | self.pid = os.getpid() 87 | 88 | self.app.server.bind( 89 | self.app.args.host, self.app.args.port, 90 | self.app.args.private, self.app.args.certificate 91 | ) 92 | 93 | print '[OK] Master running pid: {pid}'.format(pid=self.pid) 94 | utils.setproctitle('grma master pid={pid}'.format(pid=self.pid)) 95 | 96 | for i in range(self.app.args.num): 97 | self.spawn_worker() 98 | 99 | if self.app.args.pid: 100 | self.pidfile = Pidfile(self.app.args.pid) 101 | self.pidfile.create(self.pid) 102 | 103 | self.init_signals() 104 | 105 | while True: 106 | try: 107 | sleep(1) 108 | if self.try_to_stop: 109 | break 110 | except: 111 | self.clean() 112 | break 113 | # gRPC master server should close first 114 | self.kill_worker(self.pid, signal.SIGKILL) 115 | 116 | def init_signals(self): 117 | signal.signal(signal.SIGINT, self.handle_exit) 118 | signal.signal(signal.SIGQUIT, self.handle_exit) 119 | signal.signal(signal.SIGTERM, self.handle_exit) 120 | signal.signal(signal.SIGCHLD, signal.SIG_IGN) 121 | 122 | def handle_exit(self, sig, frame): 123 | self.clean() 124 | self.try_to_stop = True 125 | --------------------------------------------------------------------------------