Skip to content

Commit

Permalink
Merge pull request #83 from tiagocoutinho/develop
Browse files Browse the repository at this point in the history
Prepare 6.0.1
  • Loading branch information
tiagocoutinho authored Nov 10, 2021
2 parents 63cf41f + b8fa502 commit d7c12d4
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 6 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,16 @@ 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`.

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.


Expand Down
10 changes: 7 additions & 3 deletions multivisor/multivisor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"]
Expand All @@ -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
Expand All @@ -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):
Expand Down
13 changes: 11 additions & 2 deletions multivisor/server/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
47 changes: 47 additions & 0 deletions multivisor/tests/test_multivisor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from tests.conftest import *
from tests.functions import assert_fields_in_object
import contextlib


@pytest.mark.usefixtures("supervisor_test001")
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions multivisor/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit d7c12d4

Please sign in to comment.