Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prepare 6.0.1 #83

Merged
merged 6 commits into from
Nov 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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