From 51b362195adb93e7547feec5762c96993013c620 Mon Sep 17 00:00:00 2001 From: Cyril Guilloud on pcsht Date: Mon, 14 Dec 2020 19:07:40 +0100 Subject: [PATCH 1/6] Fix SUPERVISOR_STATE_CHANGE event name in doc. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 482c349..a5e356c 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ to your *supervisord.conf*: ```ini [eventlistener:multivisor-rpc] command=multivisor-rpc --bind 0:9002 -events=PROCESS_STATE,SUPERVISOR_STATE +events=PROCESS_STATE,SUPERVISOR_STATE_CHANGE ``` If no *bind* is given, it defaults to `*:9002`. From 9c6d2d020c646faa9b154552b9af439b963f6781 Mon Sep 17 00:00:00 2001 From: Cyril Guilloud on pcsht Date: Mon, 14 Dec 2020 19:07:58 +0100 Subject: [PATCH 2/6] Documentation tip to ease debugging :) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a5e356c..6b20629 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,8 @@ If no *bind* is given, it defaults to `*:9002`. You are free to choose the event listener name. As a convention we propose `multivisor-rpc`. +NB: Make sure that `multivisor-rpc` command is accessible or provide full PATH. + Repeat the above procedure for every supervisor you have running. From 726bbec0ae005bf1e10ef4f632b5ca0b53b7672b Mon Sep 17 00:00:00 2001 From: Bryan Bugyi Date: Sun, 3 Jan 2021 14:58:03 -0500 Subject: [PATCH 3/6] Fix TypeError when logging into multivisor using authentication Fixes #72 --- multivisor/server/util.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/multivisor/server/util.py b/multivisor/server/util.py index 912a22f..6858afa 100644 --- a/multivisor/server/util.py +++ b/multivisor/server/util.py @@ -27,11 +27,11 @@ def constant_time_compare(val1, val2): Taken from Django Source Code """ - val1 = hashlib.sha1(val1).hexdigest() + val1 = hashlib.sha1(_safe_encode(val1)).hexdigest() if val2.startswith("{SHA}"): # password can be specified as SHA-1 hash in config val2 = val2.split("{SHA}")[1] else: - val2 = hashlib.sha1(val2).hexdigest() + val2 = hashlib.sha1(_safe_encode(val2)).hexdigest() if len(val1) != len(val2): return False result = 0 @@ -40,6 +40,15 @@ def constant_time_compare(val1, val2): return result == 0 +def _safe_encode(data): + """Safely encode @data string to utf-8""" + try: + result = data.encode("utf-8") + except (UnicodeDecodeError, UnicodeEncodeError, AttributeError): + result = data + return result + + def login_required(app): """ Decorator to mark view as requiring being logged in From e2ec8ce89b9a6be9d4bf6ae0f638c6583a01b776 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Mon, 4 Oct 2021 18:51:37 +0200 Subject: [PATCH 4/6] Make sure messages from server are string --- multivisor/multivisor.py | 10 +++++++--- multivisor/util.py | 12 ++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/multivisor/multivisor.py b/multivisor/multivisor.py index 8f6987b..a51bd50 100644 --- a/multivisor/multivisor.py +++ b/multivisor/multivisor.py @@ -17,7 +17,7 @@ from supervisor.xmlrpc import Faults from supervisor.states import RUNNING_STATES -from .util import sanitize_url, filter_patterns, parse_obj +from .util import sanitize_url, filter_patterns, parse_dict log = logging.getLogger("multivisor") @@ -109,11 +109,12 @@ def read_info(self): info["processes"] = processes = {} procInfo = server.getAllProcessInfo() for proc in procInfo: - process = Process(self, proc) + process = Process(self, parse_dict(proc)) processes[process["uid"]] = process return info def update_info(self, info): + info = parse_dict(info) if self == info: this_p, info_p = self["processes"], info["processes"] if this_p != info_p: @@ -259,6 +260,7 @@ def handle_event(self, event): payload = event["payload"] proc_info = payload.get("process") if proc_info is not None: + proc_info = parse_dict(proc_info) old = self.update_info(proc_info) if old != self: old_state, new_state = old["statename"], self["statename"] @@ -273,7 +275,8 @@ def handle_event(self, event): def read_info(self): proc_info = dict(self.Null) try: - proc_info.update(self.server.getProcessInfo(self.full_name)) + from_serv = parse_dict(self.server.getProcessInfo(self.full_name)) + proc_info.update(from_serv) except Exception as err: self.log.warn("Failed to read info from %s: %s", self["uid"], err) return proc_info @@ -286,6 +289,7 @@ def update_info(self, proc_info): def refresh(self): proc_info = self.read_info() + proc_info = parse_dict(proc_info) self.update_info(proc_info) def start(self): diff --git a/multivisor/util.py b/multivisor/util.py index faab544..a4375c5 100644 --- a/multivisor/util.py +++ b/multivisor/util.py @@ -48,6 +48,18 @@ def filter_patterns(names, patterns): return result +def parse_dict(obj): + """Returns a copy of `obj` where bytes from key/values was replaced by str""" + decoded = {} + for k, v in obj.items(): + if isinstance(k, bytes): + k = k.decode("utf-8") + if isinstance(v, bytes): + v = v.decode("utf-8") + decoded[k] = v + return decoded + + def parse_obj(obj): if isinstance(obj, bytes): return obj.decode() From f5f1a63120ef5ceb33a9e85a62366ed8e9f70c4e Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Tue, 19 Oct 2021 17:40:38 +0200 Subject: [PATCH 5/6] Documentation --- multivisor/util.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/multivisor/util.py b/multivisor/util.py index a4375c5..8170a2c 100644 --- a/multivisor/util.py +++ b/multivisor/util.py @@ -61,6 +61,9 @@ def parse_dict(obj): def parse_obj(obj): + """Returns `obj` or a copy replacing recursively bytes by str + + `obj` can be any objects, including list and dictionary""" if isinstance(obj, bytes): return obj.decode() elif isinstance(obj, six.text_type): From b8fa5027d517ac444e87e8bf7c53ca4ba188eb6b Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Tue, 19 Oct 2021 17:40:50 +0200 Subject: [PATCH 6/6] Test binary messages --- multivisor/tests/test_multivisor.py | 47 +++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/multivisor/tests/test_multivisor.py b/multivisor/tests/test_multivisor.py index 0e3621c..a66af6e 100644 --- a/multivisor/tests/test_multivisor.py +++ b/multivisor/tests/test_multivisor.py @@ -2,6 +2,7 @@ from tests.conftest import * from tests.functions import assert_fields_in_object +import contextlib @pytest.mark.usefixtures("supervisor_test001") @@ -36,6 +37,52 @@ def test_supervisor_info(multivisor_instance): assert info["identification"] == "supervisor" +@pytest.mark.usefixtures("supervisor_test001") +def test_supervisor_info_from_bytes(multivisor_instance): + supervisor = multivisor_instance.get_supervisor("test001") + + @contextlib.contextmanager + def patched_getAllProcessInfo(s): + try: + getAllProcessInfo = s.server.getAllProcessInfo + + def mockedAllProcessInfo(): + processesInfo = getAllProcessInfo() + for info in processesInfo: + info[b"name"] = info.pop("name").encode("ascii") + info[b"description"] = info.pop("description").encode("ascii") + return processesInfo + + s.server.getAllProcessInfo = mockedAllProcessInfo + yield + finally: + s.server.getAllProcessInfo = getAllProcessInfo + + # Mock getAllProcessInfo with binary data + with patched_getAllProcessInfo(supervisor): + info = supervisor.read_info() + assert_fields_in_object( + [ + "running", + "host", + "version", + "identification", + "name", + "url", + "supervisor_version", + "pid", + "processes", + "api_version", + ], + info, + ) + assert info["running"] + assert info["host"] == "localhost" + assert len(info["processes"]) == 10 + assert info["name"] == "test001" + assert info["identification"] == "supervisor" + + @pytest.mark.usefixtures("supervisor_test001") def test_processes_attr(multivisor_instance): multivisor_instance.refresh() # processes are empty before calling this