multivisor 6.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- multivisor/__init__.py +0 -0
- multivisor/client/__init__.py +0 -0
- multivisor/client/cli.py +29 -0
- multivisor/client/http.py +93 -0
- multivisor/client/repl.py +244 -0
- multivisor/client/util.py +58 -0
- multivisor/multivisor.py +470 -0
- multivisor/rpc.py +232 -0
- multivisor/server/__init__.py +0 -0
- multivisor/server/dist/index.html +1 -0
- multivisor/server/dist/static/css/app.2aff3580a128440bba89b94112292cb1.css +6 -0
- multivisor/server/dist/static/css/app.2aff3580a128440bba89b94112292cb1.css.map +1 -0
- multivisor/server/dist/static/js/app.52791f915c2f060b9cb1.js +2 -0
- multivisor/server/dist/static/js/app.52791f915c2f060b9cb1.js.map +1 -0
- multivisor/server/dist/static/js/manifest.2ae2e69a05c33dfc65f8.js +2 -0
- multivisor/server/dist/static/js/manifest.2ae2e69a05c33dfc65f8.js.map +1 -0
- multivisor/server/dist/static/js/vendor.1d02877727062a41e9fb.js +1319 -0
- multivisor/server/dist/static/js/vendor.1d02877727062a41e9fb.js.map +1 -0
- multivisor/server/rpc.py +156 -0
- multivisor/server/tests/__init__.py +0 -0
- multivisor/server/tests/conftest.py +1 -0
- multivisor/server/tests/test_web.py +194 -0
- multivisor/server/util.py +70 -0
- multivisor/server/web.py +327 -0
- multivisor/signals.py +5 -0
- multivisor/tests/__init__.py +0 -0
- multivisor/tests/test_multivisor.py +179 -0
- multivisor/util.py +75 -0
- multivisor-6.0.2.dist-info/METADATA +375 -0
- multivisor-6.0.2.dist-info/RECORD +34 -0
- multivisor-6.0.2.dist-info/WHEEL +5 -0
- multivisor-6.0.2.dist-info/entry_points.txt +4 -0
- multivisor-6.0.2.dist-info/licenses/LICENSE +674 -0
- multivisor-6.0.2.dist-info/top_level.txt +1 -0
multivisor/__init__.py
ADDED
File without changes
|
File without changes
|
multivisor/client/cli.py
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
import argparse
|
2
|
+
|
3
|
+
import gevent.monkey
|
4
|
+
|
5
|
+
gevent.monkey.patch_all(thread=False)
|
6
|
+
|
7
|
+
from .. import util
|
8
|
+
from . import repl, http
|
9
|
+
|
10
|
+
|
11
|
+
def parse_args(args=None):
|
12
|
+
parser = argparse.ArgumentParser()
|
13
|
+
parser.add_argument(
|
14
|
+
"--url", help="[http://]<host>[:<22000>]", default="localhost:22000"
|
15
|
+
)
|
16
|
+
return parser.parse_args(args)
|
17
|
+
|
18
|
+
|
19
|
+
def main(args=None):
|
20
|
+
options = parse_args(args)
|
21
|
+
url = util.sanitize_url(options.url, protocol="http", port=22000)["url"]
|
22
|
+
multivisor = http.Multivisor(url)
|
23
|
+
cli = repl.Repl(multivisor)
|
24
|
+
gevent.spawn(multivisor.run)
|
25
|
+
cli.run()
|
26
|
+
|
27
|
+
|
28
|
+
if __name__ == "__main__":
|
29
|
+
main()
|
@@ -0,0 +1,93 @@
|
|
1
|
+
import json
|
2
|
+
|
3
|
+
import requests
|
4
|
+
from blinker import signal
|
5
|
+
|
6
|
+
|
7
|
+
class Multivisor(object):
|
8
|
+
def __init__(self, url):
|
9
|
+
self.url = url
|
10
|
+
self._status = None
|
11
|
+
self.notifications = []
|
12
|
+
|
13
|
+
def stop_processes(self, *names):
|
14
|
+
return self.post("/api/process/stop", data=dict(uid=[",".join(names)]))
|
15
|
+
|
16
|
+
def restart_processes(self, *names):
|
17
|
+
return self.post("/api/process/restart", data=dict(uid=[",".join(names)]))
|
18
|
+
|
19
|
+
@property
|
20
|
+
def status(self):
|
21
|
+
if self._status is None:
|
22
|
+
self._status = self.get_status()
|
23
|
+
return self._status
|
24
|
+
|
25
|
+
@staticmethod
|
26
|
+
def _update_status_stats(status):
|
27
|
+
supervisors, processes = status["supervisors"], status["processes"]
|
28
|
+
s_stats = dict(
|
29
|
+
running=sum((s["running"] for s in status["supervisors"].values())),
|
30
|
+
total=len(supervisors),
|
31
|
+
)
|
32
|
+
s_stats["stopped"] = s_stats["total"] - s_stats["running"]
|
33
|
+
p_stats = dict(
|
34
|
+
running=sum((p["running"] for p in status["processes"].values())),
|
35
|
+
total=len(processes),
|
36
|
+
)
|
37
|
+
p_stats["stopped"] = p_stats["total"] - p_stats["running"]
|
38
|
+
stats = dict(supervisors=s_stats, processes=p_stats)
|
39
|
+
status["stats"] = stats
|
40
|
+
return stats
|
41
|
+
|
42
|
+
def get_status(self):
|
43
|
+
status = self.get("/api/data").json()
|
44
|
+
# reorganize status per process
|
45
|
+
status["processes"] = processes = {}
|
46
|
+
for supervisor in status["supervisors"].values():
|
47
|
+
processes.update(supervisor["processes"])
|
48
|
+
self._update_status_stats(status)
|
49
|
+
return status
|
50
|
+
|
51
|
+
def refresh_status(self):
|
52
|
+
self._status = None
|
53
|
+
return self.status
|
54
|
+
|
55
|
+
def get(self, url, params=None, **kwargs):
|
56
|
+
result = requests.get(self.url + url, params=params, **kwargs)
|
57
|
+
result.raise_for_status()
|
58
|
+
return result
|
59
|
+
|
60
|
+
def post(self, url, data=None, json=None, **kwargs):
|
61
|
+
result = requests.post(self.url + url, data=data, json=json, **kwargs)
|
62
|
+
result.raise_for_status()
|
63
|
+
return result
|
64
|
+
|
65
|
+
def __getitem__(self, item):
|
66
|
+
return self.get(item).json()
|
67
|
+
|
68
|
+
def __setitem__(self, item, value):
|
69
|
+
self.post(item, data=value)
|
70
|
+
|
71
|
+
def events(self):
|
72
|
+
stream = self.get("/api/stream", stream=True)
|
73
|
+
for line in stream.iter_lines():
|
74
|
+
if line:
|
75
|
+
line = line.decode("utf-8")
|
76
|
+
if line.startswith("data:"):
|
77
|
+
line = line[5:]
|
78
|
+
try:
|
79
|
+
yield json.loads(line)
|
80
|
+
except ValueError:
|
81
|
+
print("error", line)
|
82
|
+
|
83
|
+
def run(self):
|
84
|
+
for event in self.events():
|
85
|
+
status = self.status
|
86
|
+
name, payload = event["event"], event["payload"]
|
87
|
+
if name == "process_changed":
|
88
|
+
status["processes"][payload["uid"]].update(payload)
|
89
|
+
self._update_status_stats(status)
|
90
|
+
elif name == "notification":
|
91
|
+
self.notifications.append(payload)
|
92
|
+
event_signal = signal(name)
|
93
|
+
event_signal.send(name, payload=payload)
|
@@ -0,0 +1,244 @@
|
|
1
|
+
import fnmatch
|
2
|
+
import functools
|
3
|
+
|
4
|
+
import maya
|
5
|
+
from blinker import signal
|
6
|
+
from prompt_toolkit import PromptSession, HTML, print_formatted_text
|
7
|
+
from prompt_toolkit.application import run_in_terminal
|
8
|
+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
9
|
+
from prompt_toolkit.completion import WordCompleter
|
10
|
+
from prompt_toolkit.history import InMemoryHistory
|
11
|
+
from prompt_toolkit.key_binding import KeyBindings
|
12
|
+
from prompt_toolkit.styles import Style
|
13
|
+
from prompt_toolkit.validation import ValidationError
|
14
|
+
|
15
|
+
from multivisor.signals import SIGNALS
|
16
|
+
from . import util
|
17
|
+
|
18
|
+
STYLE = Style.from_dict(
|
19
|
+
{
|
20
|
+
"stopped": "ansired",
|
21
|
+
"starting": "ansiblue",
|
22
|
+
"running": "ansigreen",
|
23
|
+
"backoff": "orange",
|
24
|
+
"stopping": "ansiblue",
|
25
|
+
"exited": "ansired bold",
|
26
|
+
"fatal": "violet",
|
27
|
+
"unknown": "grey",
|
28
|
+
}
|
29
|
+
)
|
30
|
+
|
31
|
+
NOTIF_STYLE = {"DEBUG": "grey", "INFO": "blue", "WARNING": "orange", "ERROR": "red"}
|
32
|
+
|
33
|
+
|
34
|
+
def process_description(process):
|
35
|
+
# TODO
|
36
|
+
status = process["statename"]
|
37
|
+
if status == "FATAL":
|
38
|
+
return process["description"]
|
39
|
+
elif process["running"]:
|
40
|
+
start = maya.MayaDT(process["start"])
|
41
|
+
desc = "pid {pid}, started {start} ({delta})".format(
|
42
|
+
pid=process["pid"], start=start.rfc2822(), delta=start.slang_time()
|
43
|
+
)
|
44
|
+
else:
|
45
|
+
stop = maya.MayaDT(process["stop"])
|
46
|
+
desc = "stopped on {stop} ({delta} ago)".format(
|
47
|
+
stop=stop.rfc2822(), delta=stop.slang_time()
|
48
|
+
)
|
49
|
+
return desc
|
50
|
+
|
51
|
+
|
52
|
+
def process_status(process, max_puid_len=10, group_by="group"):
|
53
|
+
state = process["statename"]
|
54
|
+
uid = u"{{uid:{}}}".format(max_puid_len).format(uid=process["uid"])
|
55
|
+
desc = process_description(process)
|
56
|
+
text = u"{p}{uid} <{lstate}>{state:8}</{lstate}> {description}".format(
|
57
|
+
p=("" if group_by in (None, "process") else " "),
|
58
|
+
uid=uid,
|
59
|
+
state=state,
|
60
|
+
lstate=state.lower(),
|
61
|
+
description=desc,
|
62
|
+
)
|
63
|
+
return HTML(text)
|
64
|
+
|
65
|
+
|
66
|
+
def processes_status(status, group_by="group", filter="*"):
|
67
|
+
filt = lambda p: fnmatch.fnmatch(p["uid"], filter)
|
68
|
+
return util.processes_status(
|
69
|
+
status, group_by=group_by, process_filter=filt, process_status=process_status
|
70
|
+
)
|
71
|
+
|
72
|
+
|
73
|
+
def print_processes_status(status, *args):
|
74
|
+
kwargs = {}
|
75
|
+
if args:
|
76
|
+
kwargs["filter"] = args[0]
|
77
|
+
for text in processes_status(status, **kwargs):
|
78
|
+
print_formatted_text(text, style=STYLE)
|
79
|
+
|
80
|
+
|
81
|
+
def cmd(f=None, name=None):
|
82
|
+
if f is None:
|
83
|
+
return functools.partial(cmd, name=name)
|
84
|
+
name = name or f.__name__
|
85
|
+
f.__cmd__ = name.decode() if isinstance(name, bytes) else name
|
86
|
+
return f
|
87
|
+
|
88
|
+
|
89
|
+
class Commands(object):
|
90
|
+
def __init__(self, multivisor):
|
91
|
+
self.multivisor = multivisor
|
92
|
+
|
93
|
+
@cmd(name="refresh-status")
|
94
|
+
def refresh_status(self):
|
95
|
+
"""
|
96
|
+
refresh Refresh status (eq of Ctrl+F5 in browser)
|
97
|
+
"""
|
98
|
+
print_processes_status(self.multivisor.refresh_status())
|
99
|
+
|
100
|
+
@cmd
|
101
|
+
def status(self, *args):
|
102
|
+
"""
|
103
|
+
status Status of all processes
|
104
|
+
status <pattern> Status of a pattern of processes
|
105
|
+
"""
|
106
|
+
print_processes_status(self.multivisor.status, *args)
|
107
|
+
|
108
|
+
@cmd
|
109
|
+
def restart(self, *args):
|
110
|
+
"""
|
111
|
+
restart <pattern> <pattern>* Restart a list of process patterns
|
112
|
+
"""
|
113
|
+
if not args:
|
114
|
+
raise ValidationError(message="Need at least one process")
|
115
|
+
self.multivisor.restart_processes(*args)
|
116
|
+
|
117
|
+
@cmd
|
118
|
+
def stop(self, *args):
|
119
|
+
"""
|
120
|
+
stop <pattern> <pattern>* Stop a list of process patterns
|
121
|
+
"""
|
122
|
+
if not args:
|
123
|
+
raise ValidationError(message="Need at least one process")
|
124
|
+
self.multivisor.stop_processes(*args)
|
125
|
+
|
126
|
+
@cmd
|
127
|
+
def help(self, *args):
|
128
|
+
"""
|
129
|
+
Available commands (type help <topic>):
|
130
|
+
=======================================
|
131
|
+
|
132
|
+
{cmds}
|
133
|
+
"""
|
134
|
+
if not args:
|
135
|
+
args = ("help",)
|
136
|
+
cmd = self.get_command(args[0])
|
137
|
+
cmds = " ".join(self.get_commands())
|
138
|
+
raw_text = cmd.__doc__.format(cmds=cmds)
|
139
|
+
text = "\n".join(map(str.strip, raw_text.split("\n")))
|
140
|
+
print_formatted_text(text)
|
141
|
+
|
142
|
+
@classmethod
|
143
|
+
def get_commands(cls):
|
144
|
+
result = {}
|
145
|
+
for name in dir(cls):
|
146
|
+
member = getattr(cls, name)
|
147
|
+
cmd = getattr(member, "__cmd__", None)
|
148
|
+
if cmd:
|
149
|
+
result[cmd] = name
|
150
|
+
return result
|
151
|
+
|
152
|
+
def get_command(self, name):
|
153
|
+
method_name = self.get_commands().get(name)
|
154
|
+
if method_name is None:
|
155
|
+
raise ValidationError(message="Unknown command '{}'".format(name))
|
156
|
+
return getattr(self, method_name)
|
157
|
+
|
158
|
+
|
159
|
+
def Prompt(**kwargs):
|
160
|
+
history = InMemoryHistory()
|
161
|
+
auto_suggest = AutoSuggestFromHistory()
|
162
|
+
prmpt = u"multivisor> "
|
163
|
+
return PromptSession(prmpt, history=history, auto_suggest=auto_suggest, **kwargs)
|
164
|
+
|
165
|
+
|
166
|
+
class Repl(object):
|
167
|
+
|
168
|
+
keys = KeyBindings()
|
169
|
+
|
170
|
+
def __init__(self, multivisor):
|
171
|
+
self.multivisor = multivisor
|
172
|
+
self.commands = Commands(multivisor)
|
173
|
+
status = self.multivisor.status
|
174
|
+
words = list(status["processes"].keys())
|
175
|
+
words.extend(self.commands.get_commands())
|
176
|
+
completer = WordCompleter(words)
|
177
|
+
self.session = Prompt(
|
178
|
+
completer=completer, bottom_toolbar=self.toolbar, key_bindings=self.keys
|
179
|
+
)
|
180
|
+
self.session.app.commands = self.commands
|
181
|
+
self.__update_toolbar()
|
182
|
+
for signal_name in SIGNALS:
|
183
|
+
signal(signal_name).connect(self.__update_toolbar)
|
184
|
+
|
185
|
+
def __update_toolbar(self, *args, **kwargs):
|
186
|
+
status = self.multivisor.status
|
187
|
+
stats = status["stats"]
|
188
|
+
s_stats, p_stats = stats["supervisors"], stats["processes"]
|
189
|
+
notifications = self.multivisor.notifications
|
190
|
+
if notifications:
|
191
|
+
notif = notifications[-1]
|
192
|
+
else:
|
193
|
+
notif = dict(level="INFO", message="Welcome to multivisor CLI")
|
194
|
+
html = (
|
195
|
+
u"{name} | Supervisors: {s[total]} ("
|
196
|
+
u'<b><style bg="green">{s[running]}</style></b>/'
|
197
|
+
u'<b><style bg="red">{s[stopped]}</style></b>) '
|
198
|
+
u"| Processes: {p[total]} ("
|
199
|
+
u'<b><style bg="green">{p[running]}</style></b>/'
|
200
|
+
u'<b><style bg="red">{p[stopped]}</style></b>) '
|
201
|
+
u'| <style bg="{notif_color}">{notif_msg}</style>'.format(
|
202
|
+
name=status["name"],
|
203
|
+
s=s_stats,
|
204
|
+
p=p_stats,
|
205
|
+
notif_color=NOTIF_STYLE[notif["level"]],
|
206
|
+
notif_msg=notif["message"],
|
207
|
+
)
|
208
|
+
)
|
209
|
+
self.__toolbar = HTML(html)
|
210
|
+
self.session.app.invalidate()
|
211
|
+
|
212
|
+
@keys.add(u"f5")
|
213
|
+
def __on_refresh(self):
|
214
|
+
run_in_terminal(self.app.commands.refresh_status())
|
215
|
+
|
216
|
+
def parse_command_line(self, text):
|
217
|
+
args = text.split()
|
218
|
+
cmd = self.commands.get_command(args[0])
|
219
|
+
return cmd, args[1:]
|
220
|
+
|
221
|
+
def run_command_line(self, text):
|
222
|
+
try:
|
223
|
+
cmd, args = self.parse_command_line(text)
|
224
|
+
cmd(*args)
|
225
|
+
except KeyboardInterrupt:
|
226
|
+
raise
|
227
|
+
except Exception as err:
|
228
|
+
print_formatted_text(HTML(u"<red>Error:</red> {}".format(err)))
|
229
|
+
|
230
|
+
def toolbar(self):
|
231
|
+
return self.__toolbar
|
232
|
+
|
233
|
+
def run(self):
|
234
|
+
self.commands.status()
|
235
|
+
while True:
|
236
|
+
try:
|
237
|
+
text = self.session.prompt()
|
238
|
+
if not text:
|
239
|
+
continue
|
240
|
+
self.run_command_line(text)
|
241
|
+
except KeyboardInterrupt:
|
242
|
+
continue
|
243
|
+
except EOFError:
|
244
|
+
break
|
@@ -0,0 +1,58 @@
|
|
1
|
+
import collections
|
2
|
+
|
3
|
+
|
4
|
+
def group_processes_status_by(processes, group_by="group", process_filter=None):
|
5
|
+
result = collections.defaultdict(lambda: dict(processes={}))
|
6
|
+
if process_filter is None:
|
7
|
+
process_filter = lambda p: True
|
8
|
+
for uid, process in processes.items():
|
9
|
+
if not process_filter(process):
|
10
|
+
continue
|
11
|
+
name = process[group_by]
|
12
|
+
order = result[name]
|
13
|
+
order["name"] = name
|
14
|
+
order["processes"][uid] = process
|
15
|
+
return result
|
16
|
+
|
17
|
+
|
18
|
+
def default_process_status(process, max_puid_len=10, group_by="group"):
|
19
|
+
nuid = "{{uid:{}}}".format(max_puid_len).format(uid=process["uid"])
|
20
|
+
if group_by in (None, "process"):
|
21
|
+
template = "{nuid} {statename:8} {description}"
|
22
|
+
else:
|
23
|
+
template = " {nuid} {statename:8} {description}"
|
24
|
+
return template.format(nuid=nuid, **process)
|
25
|
+
|
26
|
+
|
27
|
+
def processes_status(
|
28
|
+
status,
|
29
|
+
group_by="process",
|
30
|
+
process_filter=None,
|
31
|
+
process_status=default_process_status,
|
32
|
+
):
|
33
|
+
processes = status["processes"]
|
34
|
+
if processes:
|
35
|
+
puid_len = max(map(len, processes))
|
36
|
+
else:
|
37
|
+
puid_len = 8
|
38
|
+
result = []
|
39
|
+
if process_filter is None:
|
40
|
+
process_filter = lambda p: True
|
41
|
+
if group_by in (None, "process"):
|
42
|
+
for puid in sorted(processes):
|
43
|
+
process = processes[puid]
|
44
|
+
if process_filter(process):
|
45
|
+
result.append(
|
46
|
+
process_status(process, max_puid_len=puid_len, group_by=group_by)
|
47
|
+
)
|
48
|
+
else:
|
49
|
+
grouped = group_processes_status_by(
|
50
|
+
processes, group_by=group_by, process_filter=process_filter
|
51
|
+
)
|
52
|
+
for name in sorted(grouped):
|
53
|
+
result.append(name + ":")
|
54
|
+
for process in grouped[name]["processes"].values():
|
55
|
+
result.append(
|
56
|
+
process_status(process, max_puid_len=puid_len, group_by=group_by)
|
57
|
+
)
|
58
|
+
return result
|