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/server/web.py
ADDED
@@ -0,0 +1,327 @@
|
|
1
|
+
import hashlib
|
2
|
+
import functools
|
3
|
+
|
4
|
+
import gevent
|
5
|
+
from blinker import signal
|
6
|
+
from gevent.monkey import patch_all
|
7
|
+
|
8
|
+
patch_all(thread=False)
|
9
|
+
|
10
|
+
import os
|
11
|
+
import logging
|
12
|
+
|
13
|
+
from gevent import queue, sleep
|
14
|
+
from gevent.pywsgi import WSGIServer
|
15
|
+
from flask import Flask, render_template, Response, request, json, jsonify, session
|
16
|
+
from werkzeug.debug import DebuggedApplication
|
17
|
+
from werkzeug.serving import run_simple
|
18
|
+
|
19
|
+
from multivisor.signals import SIGNALS
|
20
|
+
from multivisor.util import sanitize_url
|
21
|
+
from multivisor.multivisor import Multivisor
|
22
|
+
from .util import is_login_valid, login_required
|
23
|
+
|
24
|
+
|
25
|
+
log = logging.getLogger("multivisor")
|
26
|
+
|
27
|
+
app = Flask(__name__, static_folder="./dist/static", template_folder="./dist")
|
28
|
+
|
29
|
+
|
30
|
+
@app.route("/api/admin/reload")
|
31
|
+
@login_required(app)
|
32
|
+
def reload():
|
33
|
+
app.multivisor.reload()
|
34
|
+
return "OK"
|
35
|
+
|
36
|
+
|
37
|
+
@app.route("/api/refresh")
|
38
|
+
@login_required(app)
|
39
|
+
def refresh():
|
40
|
+
app.multivisor.refresh()
|
41
|
+
return jsonify(app.multivisor.safe_config)
|
42
|
+
|
43
|
+
|
44
|
+
@app.route("/api/data")
|
45
|
+
@login_required(app)
|
46
|
+
def data():
|
47
|
+
return jsonify(app.multivisor.safe_config)
|
48
|
+
|
49
|
+
|
50
|
+
@app.route("/api/config/file")
|
51
|
+
@login_required(app)
|
52
|
+
def config_file_content():
|
53
|
+
content = app.multivisor.config_file_content
|
54
|
+
return jsonify(dict(content=content))
|
55
|
+
|
56
|
+
|
57
|
+
@app.route("/api/supervisor/update", methods=["POST"])
|
58
|
+
@login_required(app)
|
59
|
+
def update_supervisor():
|
60
|
+
names = (
|
61
|
+
str.strip(supervisor) for supervisor in request.form["supervisor"].split(",")
|
62
|
+
)
|
63
|
+
app.multivisor.update_supervisors(*names)
|
64
|
+
return "OK"
|
65
|
+
|
66
|
+
|
67
|
+
@app.route("/api/supervisor/restart", methods=["POST"])
|
68
|
+
@login_required(app)
|
69
|
+
def restart_supervisor():
|
70
|
+
names = (
|
71
|
+
str.strip(supervisor) for supervisor in request.form["supervisor"].split(",")
|
72
|
+
)
|
73
|
+
app.multivisor.restart_supervisors(*names)
|
74
|
+
return "OK"
|
75
|
+
|
76
|
+
|
77
|
+
@app.route("/api/supervisor/reread", methods=["POST"])
|
78
|
+
@login_required(app)
|
79
|
+
def reread_supervisor():
|
80
|
+
names = (
|
81
|
+
str.strip(supervisor) for supervisor in request.form["supervisor"].split(",")
|
82
|
+
)
|
83
|
+
app.multivisor.reread_supervisors(*names)
|
84
|
+
return "OK"
|
85
|
+
|
86
|
+
|
87
|
+
@app.route("/api/supervisor/shutdown", methods=["POST"])
|
88
|
+
@login_required(app)
|
89
|
+
def shutdown_supervisor():
|
90
|
+
names = (
|
91
|
+
str.strip(supervisor) for supervisor in request.form["supervisor"].split(",")
|
92
|
+
)
|
93
|
+
app.multivisor.shutdown_supervisors(*names)
|
94
|
+
return "OK"
|
95
|
+
|
96
|
+
|
97
|
+
@app.route("/api/process/restart", methods=["POST"])
|
98
|
+
@login_required(app)
|
99
|
+
def restart_process():
|
100
|
+
patterns = request.form["uid"].split(",")
|
101
|
+
procs = app.multivisor.restart_processes(*patterns)
|
102
|
+
return "OK"
|
103
|
+
|
104
|
+
|
105
|
+
@app.route("/api/process/stop", methods=["POST"])
|
106
|
+
@login_required(app)
|
107
|
+
def stop_process():
|
108
|
+
patterns = request.form["uid"].split(",")
|
109
|
+
app.multivisor.stop_processes(*patterns)
|
110
|
+
return "OK"
|
111
|
+
|
112
|
+
|
113
|
+
@app.route("/api/process/list")
|
114
|
+
@login_required(app)
|
115
|
+
def list_processes():
|
116
|
+
return jsonify(tuple(app.multivisor.processes.keys()))
|
117
|
+
|
118
|
+
|
119
|
+
@app.route("/api/process/info/<uid>")
|
120
|
+
@login_required(app)
|
121
|
+
def process_info(uid):
|
122
|
+
process = app.multivisor.get_process(uid)
|
123
|
+
process.refresh()
|
124
|
+
return json.dumps(process)
|
125
|
+
|
126
|
+
|
127
|
+
@app.route("/api/supervisor/info/<uid>")
|
128
|
+
@login_required(app)
|
129
|
+
def supervisor_info(uid):
|
130
|
+
supervisor = app.multivisor.get_supervisor(uid)
|
131
|
+
supervisor.refresh()
|
132
|
+
return json.dumps(supervisor)
|
133
|
+
|
134
|
+
|
135
|
+
@app.route("/api/process/log/<stream>/tail/<uid>")
|
136
|
+
@login_required(app)
|
137
|
+
def process_log_tail(stream, uid):
|
138
|
+
sname, pname = uid.split(":", 1)
|
139
|
+
supervisor = app.multivisor.get_supervisor(sname)
|
140
|
+
server = supervisor.server
|
141
|
+
if stream == "out":
|
142
|
+
tail = server.tailProcessStdoutLog
|
143
|
+
else:
|
144
|
+
tail = server.tailProcessStderrLog
|
145
|
+
|
146
|
+
def event_stream():
|
147
|
+
i, offset, length = 0, 0, 2 ** 12
|
148
|
+
while True:
|
149
|
+
data = tail(pname, offset, length)
|
150
|
+
log, offset, overflow = data
|
151
|
+
# don't care about overflow in first log message
|
152
|
+
if overflow and i:
|
153
|
+
length = min(length * 2, 2 ** 14)
|
154
|
+
else:
|
155
|
+
data = json.dumps(dict(message=log, size=offset))
|
156
|
+
yield "data: {}\n\n".format(data)
|
157
|
+
sleep(1)
|
158
|
+
i += 1
|
159
|
+
|
160
|
+
return Response(event_stream(), mimetype="text/event-stream")
|
161
|
+
|
162
|
+
|
163
|
+
@app.route("/api/login", methods=["post"])
|
164
|
+
def login():
|
165
|
+
if not app.multivisor.use_authentication:
|
166
|
+
return "Authentication is not required"
|
167
|
+
username = request.form.get("username")
|
168
|
+
password = request.form.get("password")
|
169
|
+
if is_login_valid(app, username, password):
|
170
|
+
session["username"] = username
|
171
|
+
return json.dumps({})
|
172
|
+
else:
|
173
|
+
response_data = {"errors": {"password": "Invalid username or password"}}
|
174
|
+
return json.dumps(response_data), 400
|
175
|
+
|
176
|
+
|
177
|
+
@app.route("/api/auth", methods=["get"])
|
178
|
+
def auth():
|
179
|
+
response_data = {
|
180
|
+
"use_authentication": app.multivisor.use_authentication,
|
181
|
+
"is_authenticated": "username" in session,
|
182
|
+
}
|
183
|
+
return json.dumps(response_data)
|
184
|
+
|
185
|
+
|
186
|
+
@app.route("/api/logout", methods=["post"])
|
187
|
+
def logout():
|
188
|
+
session.clear()
|
189
|
+
return json.dumps({})
|
190
|
+
|
191
|
+
|
192
|
+
@app.route("/api/stream")
|
193
|
+
@login_required(app)
|
194
|
+
def stream():
|
195
|
+
def event_stream():
|
196
|
+
client = queue.Queue()
|
197
|
+
app.dispatcher.add_listener(client)
|
198
|
+
for event in client:
|
199
|
+
yield event
|
200
|
+
app.dispatcher.remove_listener(client)
|
201
|
+
|
202
|
+
return Response(event_stream(), mimetype="text/event-stream")
|
203
|
+
|
204
|
+
|
205
|
+
@app.route("/", defaults={"path": ""})
|
206
|
+
@app.route("/<path:path>")
|
207
|
+
def catch_all(path):
|
208
|
+
return render_template("index.html")
|
209
|
+
|
210
|
+
|
211
|
+
class Dispatcher(object):
|
212
|
+
def __init__(self):
|
213
|
+
self.clients = []
|
214
|
+
for signal_name in SIGNALS:
|
215
|
+
signal(signal_name).connect(self.on_multivisor_event)
|
216
|
+
|
217
|
+
def add_listener(self, client):
|
218
|
+
self.clients.append(client)
|
219
|
+
|
220
|
+
def remove_listener(self, client):
|
221
|
+
self.clients.remove(client)
|
222
|
+
|
223
|
+
def on_multivisor_event(self, signal, payload):
|
224
|
+
data = json.dumps(dict(payload=payload, event=signal))
|
225
|
+
event = "data: {0}\n\n".format(data)
|
226
|
+
for client in self.clients:
|
227
|
+
client.put(event)
|
228
|
+
|
229
|
+
|
230
|
+
def set_secret_key():
|
231
|
+
"""
|
232
|
+
In order to use flask sessions, secret_key must be set,
|
233
|
+
require "MULTIVISOR_SECRET_KEY" env variable only if
|
234
|
+
login and password is set in multivisor config
|
235
|
+
You can generate secret by invoking:
|
236
|
+
python -c 'import os; import binascii; print(binascii.hexlify(os.urandom(32)))'
|
237
|
+
"""
|
238
|
+
if app.multivisor.use_authentication:
|
239
|
+
secret_key = os.environ.get("MULTIVISOR_SECRET_KEY")
|
240
|
+
if not secret_key:
|
241
|
+
raise Exception(
|
242
|
+
'"MULTIVISOR_SECRET_KEY" environmental variable must be set '
|
243
|
+
"when authentication is enabled"
|
244
|
+
)
|
245
|
+
app.secret_key = secret_key
|
246
|
+
|
247
|
+
|
248
|
+
@app.errorhandler(401)
|
249
|
+
def custom_401(error):
|
250
|
+
response_data = {"message": "Authenthication is required to access this endpoint"}
|
251
|
+
return Response(
|
252
|
+
json.dumps(response_data), 401, {"content-type": "application/json"}
|
253
|
+
)
|
254
|
+
|
255
|
+
|
256
|
+
def run_with_reloader_if_debug(func):
|
257
|
+
@functools.wraps(func)
|
258
|
+
def wrapper_login_required(*args, **kwargs):
|
259
|
+
if not app.debug:
|
260
|
+
return func(*args, **kwargs)
|
261
|
+
return run_simple(func, *args, **kwargs, use_reloader=True)
|
262
|
+
|
263
|
+
return wrapper_login_required
|
264
|
+
|
265
|
+
|
266
|
+
def get_parser(args):
|
267
|
+
import argparse
|
268
|
+
|
269
|
+
parser = argparse.ArgumentParser()
|
270
|
+
parser.add_argument(
|
271
|
+
"--bind", help="[host][:port] (default: *:22000)", default="*:22000"
|
272
|
+
)
|
273
|
+
parser.add_argument(
|
274
|
+
"-c",
|
275
|
+
help="configuration file",
|
276
|
+
dest="config_file",
|
277
|
+
default="/etc/multivisor.conf",
|
278
|
+
)
|
279
|
+
parser.add_argument(
|
280
|
+
"--log-level",
|
281
|
+
help="log level",
|
282
|
+
type=str,
|
283
|
+
default="INFO",
|
284
|
+
choices=["DEBUG", "INFO", "WARN", "ERROR"],
|
285
|
+
)
|
286
|
+
return parser
|
287
|
+
|
288
|
+
|
289
|
+
@run_with_reloader_if_debug
|
290
|
+
def main(args=None):
|
291
|
+
parser = get_parser(args)
|
292
|
+
options = parser.parse_args(args)
|
293
|
+
|
294
|
+
log_level = getattr(logging, options.log_level.upper())
|
295
|
+
log_fmt = "%(levelname)s %(asctime)-15s %(name)s: %(message)s"
|
296
|
+
logging.basicConfig(level=log_level, format=log_fmt)
|
297
|
+
|
298
|
+
if not os.path.exists(options.config_file):
|
299
|
+
parser.exit(
|
300
|
+
status=2, message="configuration file does not exist. Bailing out!\n"
|
301
|
+
)
|
302
|
+
|
303
|
+
bind = sanitize_url(options.bind, host="*", port=22000)["url"]
|
304
|
+
|
305
|
+
app.dispatcher = Dispatcher()
|
306
|
+
app.multivisor = Multivisor(options)
|
307
|
+
|
308
|
+
if app.multivisor.use_authentication:
|
309
|
+
secret_key = os.environ.get("MULTIVISOR_SECRET_KEY")
|
310
|
+
if not secret_key:
|
311
|
+
raise Exception(
|
312
|
+
'"MULTIVISOR_SECRET_KEY" environmental variable must be set '
|
313
|
+
"when authentication is enabled"
|
314
|
+
)
|
315
|
+
app.secret_key = secret_key
|
316
|
+
|
317
|
+
application = DebuggedApplication(app, evalex=True) if app.debug else app
|
318
|
+
http_server = WSGIServer(bind, application=application)
|
319
|
+
logging.info("Start accepting requests")
|
320
|
+
try:
|
321
|
+
http_server.serve_forever()
|
322
|
+
except KeyboardInterrupt:
|
323
|
+
log.info("Ctrl-C pressed. Bailing out")
|
324
|
+
|
325
|
+
|
326
|
+
if __name__ == "__main__":
|
327
|
+
main()
|
multivisor/signals.py
ADDED
File without changes
|
@@ -0,0 +1,179 @@
|
|
1
|
+
import pytest
|
2
|
+
|
3
|
+
from tests.conftest import *
|
4
|
+
from tests.functions import assert_fields_in_object
|
5
|
+
import contextlib
|
6
|
+
|
7
|
+
|
8
|
+
@pytest.mark.usefixtures("supervisor_test001")
|
9
|
+
def test_supervisors_attr(multivisor_instance):
|
10
|
+
supervisors = multivisor_instance.supervisors
|
11
|
+
assert "test001" in supervisors
|
12
|
+
|
13
|
+
|
14
|
+
@pytest.mark.usefixtures("supervisor_test001")
|
15
|
+
def test_supervisor_info(multivisor_instance):
|
16
|
+
supervisor = multivisor_instance.get_supervisor("test001")
|
17
|
+
info = supervisor.read_info()
|
18
|
+
assert_fields_in_object(
|
19
|
+
[
|
20
|
+
"running",
|
21
|
+
"host",
|
22
|
+
"version",
|
23
|
+
"identification",
|
24
|
+
"name",
|
25
|
+
"url",
|
26
|
+
"supervisor_version",
|
27
|
+
"pid",
|
28
|
+
"processes",
|
29
|
+
"api_version",
|
30
|
+
],
|
31
|
+
info,
|
32
|
+
)
|
33
|
+
assert info["running"]
|
34
|
+
assert info["host"] == "localhost"
|
35
|
+
assert len(info["processes"]) == 10
|
36
|
+
assert info["name"] == "test001"
|
37
|
+
assert info["identification"] == "supervisor"
|
38
|
+
|
39
|
+
|
40
|
+
@pytest.mark.usefixtures("supervisor_test001")
|
41
|
+
def test_supervisor_info_from_bytes(multivisor_instance):
|
42
|
+
supervisor = multivisor_instance.get_supervisor("test001")
|
43
|
+
|
44
|
+
@contextlib.contextmanager
|
45
|
+
def patched_getAllProcessInfo(s):
|
46
|
+
try:
|
47
|
+
getAllProcessInfo = s.server.getAllProcessInfo
|
48
|
+
|
49
|
+
def mockedAllProcessInfo():
|
50
|
+
processesInfo = getAllProcessInfo()
|
51
|
+
for info in processesInfo:
|
52
|
+
info[b"name"] = info.pop("name").encode("ascii")
|
53
|
+
info[b"description"] = info.pop("description").encode("ascii")
|
54
|
+
return processesInfo
|
55
|
+
|
56
|
+
s.server.getAllProcessInfo = mockedAllProcessInfo
|
57
|
+
yield
|
58
|
+
finally:
|
59
|
+
s.server.getAllProcessInfo = getAllProcessInfo
|
60
|
+
|
61
|
+
# Mock getAllProcessInfo with binary data
|
62
|
+
with patched_getAllProcessInfo(supervisor):
|
63
|
+
info = supervisor.read_info()
|
64
|
+
assert_fields_in_object(
|
65
|
+
[
|
66
|
+
"running",
|
67
|
+
"host",
|
68
|
+
"version",
|
69
|
+
"identification",
|
70
|
+
"name",
|
71
|
+
"url",
|
72
|
+
"supervisor_version",
|
73
|
+
"pid",
|
74
|
+
"processes",
|
75
|
+
"api_version",
|
76
|
+
],
|
77
|
+
info,
|
78
|
+
)
|
79
|
+
assert info["running"]
|
80
|
+
assert info["host"] == "localhost"
|
81
|
+
assert len(info["processes"]) == 10
|
82
|
+
assert info["name"] == "test001"
|
83
|
+
assert info["identification"] == "supervisor"
|
84
|
+
|
85
|
+
|
86
|
+
@pytest.mark.usefixtures("supervisor_test001")
|
87
|
+
def test_processes_attr(multivisor_instance):
|
88
|
+
multivisor_instance.refresh() # processes are empty before calling this
|
89
|
+
processes = multivisor_instance.processes
|
90
|
+
assert len(processes) == 10
|
91
|
+
assert "test001:PLC:wcid00d" in processes
|
92
|
+
process = processes["test001:PLC:wcid00d"]
|
93
|
+
assert_fields_in_object(
|
94
|
+
[
|
95
|
+
"logfile",
|
96
|
+
"supervisor",
|
97
|
+
"description",
|
98
|
+
"state",
|
99
|
+
"pid",
|
100
|
+
"stderr_logfile",
|
101
|
+
"stop",
|
102
|
+
"host",
|
103
|
+
"statename",
|
104
|
+
"name",
|
105
|
+
"start",
|
106
|
+
"running",
|
107
|
+
"stdout_logfile",
|
108
|
+
"full_name",
|
109
|
+
"group",
|
110
|
+
"now",
|
111
|
+
"exitstatus",
|
112
|
+
"spawnerr",
|
113
|
+
"uid",
|
114
|
+
],
|
115
|
+
process,
|
116
|
+
)
|
117
|
+
assert process["supervisor"] == "test001"
|
118
|
+
assert process["full_name"] == "PLC:wcid00d"
|
119
|
+
assert process["name"] == "wcid00d"
|
120
|
+
assert process["uid"] == "test001:PLC:wcid00d"
|
121
|
+
assert process["group"] == "PLC"
|
122
|
+
assert "tests/log/wcid00d.log" in process["logfile"]
|
123
|
+
assert "tests/log/wcid00d.log" in process["stdout_logfile"]
|
124
|
+
assert process["stderr_logfile"] == ""
|
125
|
+
|
126
|
+
|
127
|
+
@pytest.mark.usefixtures("supervisor_test001")
|
128
|
+
def test_get_process(multivisor_instance):
|
129
|
+
multivisor_instance.refresh() # processes are empty before calling this
|
130
|
+
uid = "test001:PLC:wcid00d"
|
131
|
+
process = multivisor_instance.get_process(uid)
|
132
|
+
assert process["supervisor"] == "test001"
|
133
|
+
assert process["full_name"] == "PLC:wcid00d"
|
134
|
+
assert process["name"] == "wcid00d"
|
135
|
+
assert process["uid"] == "test001:PLC:wcid00d"
|
136
|
+
assert process["group"] == "PLC"
|
137
|
+
assert "tests/log/wcid00d.log" in process["logfile"]
|
138
|
+
assert "tests/log/wcid00d.log" in process["stdout_logfile"]
|
139
|
+
assert process["stderr_logfile"] == ""
|
140
|
+
|
141
|
+
|
142
|
+
@pytest.mark.usefixtures("supervisor_test001")
|
143
|
+
def test_use_authentication(multivisor_instance):
|
144
|
+
assert not multivisor_instance.use_authentication
|
145
|
+
|
146
|
+
|
147
|
+
@pytest.mark.usefixtures("supervisor_test001")
|
148
|
+
def test_stop_process(multivisor_instance):
|
149
|
+
multivisor_instance.refresh() # processes are empty before calling this
|
150
|
+
uid = "test001:PLC:wcid00d"
|
151
|
+
process = multivisor_instance.get_process(uid)
|
152
|
+
print(process)
|
153
|
+
index, max_retries = 0, 10
|
154
|
+
while not process["running"]: # make sure process is running
|
155
|
+
multivisor_instance.refresh()
|
156
|
+
process = multivisor_instance.get_process(uid)
|
157
|
+
sleep(0.5)
|
158
|
+
if index == max_retries:
|
159
|
+
raise AssertionError("Process {} is not running".format(uid))
|
160
|
+
index += 1
|
161
|
+
|
162
|
+
multivisor_instance.stop_processes(uid)
|
163
|
+
multivisor_instance.refresh()
|
164
|
+
process = multivisor_instance.get_process(uid)
|
165
|
+
assert not process["running"]
|
166
|
+
|
167
|
+
|
168
|
+
@pytest.mark.usefixtures("supervisor_test001")
|
169
|
+
def test_restart_process(multivisor_instance):
|
170
|
+
multivisor_instance.refresh() # processes are empty before calling this
|
171
|
+
uid = "test001:PLC:wcid00d"
|
172
|
+
process = multivisor_instance.get_process(uid)
|
173
|
+
if process["running"]:
|
174
|
+
multivisor_instance.stop_processes(uid)
|
175
|
+
|
176
|
+
multivisor_instance.restart_processes(uid)
|
177
|
+
multivisor_instance.refresh()
|
178
|
+
process = multivisor_instance.get_process(uid)
|
179
|
+
assert process["running"]
|
multivisor/util.py
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
import re
|
2
|
+
import fnmatch
|
3
|
+
|
4
|
+
try:
|
5
|
+
from collections import abc
|
6
|
+
except ImportError:
|
7
|
+
import collections as abc
|
8
|
+
|
9
|
+
import six
|
10
|
+
|
11
|
+
_PROTO_RE_STR = "(?P<protocol>\w+)\://"
|
12
|
+
_HOST_RE_STR = "?P<host>([\w\-_]+\.)*[\w\-_]+|\*"
|
13
|
+
_PORT_RE_STR = "\:(?P<port>\d{1,5})"
|
14
|
+
|
15
|
+
URL_RE = re.compile(
|
16
|
+
"({protocol})?({host})?({port})?".format(
|
17
|
+
protocol=_PROTO_RE_STR, host=_HOST_RE_STR, port=_PORT_RE_STR
|
18
|
+
)
|
19
|
+
)
|
20
|
+
|
21
|
+
|
22
|
+
def sanitize_url(url, protocol=None, host=None, port=None):
|
23
|
+
match = URL_RE.match(url)
|
24
|
+
if match is None:
|
25
|
+
raise ValueError("Invalid URL: {!r}".format(url))
|
26
|
+
pars = match.groupdict()
|
27
|
+
_protocol, _host, _port = pars["protocol"], pars["host"], pars["port"]
|
28
|
+
protocol = protocol if _protocol is None else _protocol
|
29
|
+
host = host if _host is None else _host
|
30
|
+
port = port if _port is None else _port
|
31
|
+
protocol = "" if protocol is None else (protocol + "://")
|
32
|
+
port = "" if port is None else ":" + str(port)
|
33
|
+
return dict(
|
34
|
+
url="{}{}{}".format(protocol, host, port),
|
35
|
+
protocol=protocol,
|
36
|
+
host=host,
|
37
|
+
port=port,
|
38
|
+
)
|
39
|
+
|
40
|
+
|
41
|
+
def filter_patterns(names, patterns):
|
42
|
+
patterns = [
|
43
|
+
"*:{}".format(p) if ":" not in p and "*" not in p else p for p in patterns
|
44
|
+
]
|
45
|
+
result = set()
|
46
|
+
sets = (fnmatch.filter(names, pattern) for pattern in patterns)
|
47
|
+
list(map(result.update, sets))
|
48
|
+
return result
|
49
|
+
|
50
|
+
|
51
|
+
def parse_dict(obj):
|
52
|
+
"""Returns a copy of `obj` where bytes from key/values was replaced by str"""
|
53
|
+
decoded = {}
|
54
|
+
for k, v in obj.items():
|
55
|
+
if isinstance(k, bytes):
|
56
|
+
k = k.decode("utf-8")
|
57
|
+
if isinstance(v, bytes):
|
58
|
+
v = v.decode("utf-8")
|
59
|
+
decoded[k] = v
|
60
|
+
return decoded
|
61
|
+
|
62
|
+
|
63
|
+
def parse_obj(obj):
|
64
|
+
"""Returns `obj` or a copy replacing recursively bytes by str
|
65
|
+
|
66
|
+
`obj` can be any objects, including list and dictionary"""
|
67
|
+
if isinstance(obj, bytes):
|
68
|
+
return obj.decode()
|
69
|
+
elif isinstance(obj, six.text_type):
|
70
|
+
return obj
|
71
|
+
elif isinstance(obj, abc.Mapping):
|
72
|
+
return {parse_obj(k): parse_obj(v) for k, v in obj.items()}
|
73
|
+
elif isinstance(obj, abc.Container):
|
74
|
+
return type(obj)(parse_obj(i) for i in obj)
|
75
|
+
return obj
|