├── 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 |
--------------------------------------------------------------------------------