Repository: learningequality/kolibri
Issue: network discovery error on Windows 10: JSON object must be str, not 'bytes'


### Observed behavior

On startup, observed unhandled JSON errors when running Kolibri in foreground in Windows

### Expected behavior

No unhandled error

### User-facing consequences

* Might prevent network discovery?
* Unhandled exceptions will cause confusion while debugging

### Errors and logs


```
C:\\Users\\IEUser>kolibri start --foreground
[37mINFO     Option RUN_MODE in section [Deployment] being overridden by environment variable KOLIBRI_RUN_MODE[0m
[37mINFO     Running Kolibri with the following settings: kolibri.deployment.default.settings.base[0m
[37mINFO     Running Kolibri[0m
[37mINFO     Kolibri running on:
[0m
        http://10.0.2.15:8080/

[37mINFO     Starting Kolibri 0.13.1[0m
[37mINFO     ENGINE Listening for SIGINT.[0m
[37mINFO     ENGINE Listening for SIGTERM.[0m
[37mINFO     ENGINE Bus STARTING[0m
[37mINFO     Started new JOBCHECKER thread ID#0d0388159e174e9093a7ed176eb108f8[0m
[37mINFO     Started new JOBCHECKER thread ID#a2fe0081d5044727baeb1ba17e90d57d[0m
[37mINFO     Started new SCHEDULECHECKER thread ID#3d24b5cbdb144373b7518c4530b22d52[0m
[37mINFO     Registering ourselves to zeroconf network with id 'e6bba31a6ecc89563edecea04f4dad57'...[0m
Exception in thread zeroconf-ServiceBrowser_Kolibri._sub._http._tcp.local.:
Traceback (most recent call last):
  File "C:\\Python34\\lib\\threading.py", line 920, in _bootstrap_inner
    self.run()
  File "C:\\Python34\\lib\\site-packages\\kolibri\\dist\\zeroconf.py", line 1443, in run
    handler(self.zc)
  File "C:\\Python34\\lib\\site-packages\\kolibri\\dist\\zeroconf.py", line 1384, in <lambda>
    state_change=state_change,
  File "C:\\Python34\\lib\\site-packages\\kolibri\\dist\\zeroconf.py", line 1280, in fire
    h(**kwargs)
  File "C:\\Python34\\lib\\site-packages\\kolibri\\dist\\zeroconf.py", line 1353, in on_change
    listener.add_service(*args)
  File "C:\\Python34\\lib\\site-packages\\kolibri\\core\\discovery\\utils\\network\\search.py", line 137, in add_service
    bytes.decode(key): json.loads(val) for (key, val) in info.properties.items()
  File "C:\\Python34\\lib\\site-packages\\kolibri\\core\\discovery\\utils\\network\\search.py", line 137, in <dictcomp>
    bytes.decode(key): json.loads(val) for (key, val) in info.properties.items()
  File "C:\\Python34\\lib\\json\\__init__.py", line 312, in loads
    s.__class__.__name__))
TypeError: the JSON object must be str, not 'bytes'

[37mINFO     ENGINE Serving on http://0.0.0.0:8080[0m
[37mINFO     ENGINE Bus STARTED[0m
[37mINFO     Ping succeeded! (response: {'messages': [], 'id': 648632})[0m
```

### Steps to reproduce

* Installed Kolibri on a win10 VM
* Shut it down via the GUI
* Opened a command promp
* Ran `kolibri start --foreground`

### Context

0.13.1



Changed files:
kolibri/core/discovery/test/test_network_search.py, kolibri/core/discovery/utils/network/search.py

diff --git a/kolibri/core/discovery/test/test_network_search.py b/kolibri/core/discovery/test/test_network_search.py
index fe0e385385..db2ac27071 100644
--- a/kolibri/core/discovery/test/test_network_search.py
+++ b/kolibri/core/discovery/test/test_network_search.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 import socket
 import threading
 
@@ -14,6 +15,7 @@ from ..utils.network.search import initialize_zeroconf_listener
 from ..utils.network.search import KolibriZeroconfService
 from ..utils.network.search import LOCAL_DOMAIN
 from ..utils.network.search import NonUniqueNameException
+from ..utils.network.search import parse_device_info
 from ..utils.network.search import register_zeroconf_service
 from ..utils.network.search import SERVICE_TYPE
 from ..utils.network.search import unregister_zeroconf_service
@@ -119,6 +121,16 @@ class TestNetworkSearch(TransactionTestCase):
         initialize_zeroconf_listener()
         assert ZEROCONF_STATE["listener"] is not None
 
+    def test_call_parse_device_info_value_bytes(self, *mocks):
+        info_mock = mock.MagicMock()
+        info_mock.properties = MOCK_PROPERTIES.copy()
+        info_mock.properties[b"operating_system"] = '"كوليبري"'.encode("utf-8")
+        try:
+            device_info = parse_device_info(info_mock)
+        except TypeError:
+            self.fail("Failed to parse info with bytes values")
+        self.assertEqual(device_info["operating_system"], "كوليبري")
+
     def test_register_zeroconf_service(self, mock_db, get_device_mock):
         assert len(get_peer_instances()) == 0
         initialize_zeroconf_listener()
diff --git a/kolibri/core/discovery/utils/network/search.py b/kolibri/core/discovery/utils/network/search.py
index 13c7caefbc..c0a00e20da 100644
--- a/kolibri/core/discovery/utils/network/search.py
+++ b/kolibri/core/discovery/utils/network/search.py
@@ -101,6 +101,15 @@ class KolibriZeroconfService(object):
             self.unregister()
 
 
+def parse_device_info(info):
+    obj = {}
+    for key, val in info.properties.items():
+        if isinstance(val, bytes):
+            val = val.decode("utf-8")
+        obj[bytes.decode(key)] = json.loads(val)
+    return obj
+
+
 class KolibriZeroconfListener(object):
 
     instances = {}
@@ -133,9 +142,7 @@ class KolibriZeroconfListener(object):
             "self": is_self,
         }
 
-        device_info = {
-            bytes.decode(key): json.loads(val) for (key, val) in info.properties.items()
-        }
+        device_info = parse_device_info(info)
 
         instance.update(device_info)
         self.instances[id] = instance