diff --git a/README.md b/README.md index 482c349..6b20629 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`. @@ -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. 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/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 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 diff --git a/multivisor/util.py b/multivisor/util.py index faab544..8170a2c 100644 --- a/multivisor/util.py +++ b/multivisor/util.py @@ -48,7 +48,22 @@ 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): + """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):