From a543930951f921bf27fe560f589d12fbe59ace4f Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Wed, 12 Jun 2019 13:21:19 -0700 Subject: [PATCH 01/19] Update _kill_pid to leverage os.kill for Windows as well (Py2.7+) (also means that we start with a soft interrupt and only hard-kill later if needed, as we do on Linux) --- kolibri/utils/system.py | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/kolibri/utils/system.py b/kolibri/utils/system.py index b3fc697265b..1178c9cf719 100644 --- a/kolibri/utils/system.py +++ b/kolibri/utils/system.py @@ -16,8 +16,11 @@ from __future__ import print_function from __future__ import unicode_literals +import logging import os +import signal import sys +import time import six from django.db import connections @@ -25,6 +28,8 @@ from .conf import KOLIBRI_HOME from kolibri.utils.android import on_android +logger = logging.getLogger(__name__) + def _posix_pid_exists(pid): """Check whether PID exists in the current process table.""" @@ -41,18 +46,33 @@ def _posix_pid_exists(pid): return True -def _posix_kill_pid(pid): - """Kill a PID by sending a posix signal""" - import signal + +def _kill_pid(pid, softkill_signal_number): + """Kill a PID by sending a signal, starting with a softer one and then escalating as needed""" try: - os.kill(pid, signal.SIGTERM) + logger.debug("Attempting to soft kill process with pid %d..." % pid) + os.kill(pid, softkill_signal_number) + logger.debug("Soft kill signal sent without error.") # process does not exist except OSError: + logger.debug("Soft kill signal could not be sent (OSError); process may not exist?") return - # process didn't exit cleanly, make one last effort to kill it + # give some time for the process to clean itself up gracefully bfore we force anything + i = 0 + while pid_exists(pid) and i < 10: + time.sleep(0.5) + i += 1 + # if process didn't exit cleanly, make one last effort to kill it if pid_exists(pid): + logger.debug("Process wth pid %s still exists after soft kill signal; attempting a SIGKILL." % pid) os.kill(pid, signal.SIGKILL) + logger.debug("SIGKILL signal sent without error.") + + +def _posix_kill_pid(pid): + """Kill a PID by sending a posix-specific soft-kill signal""" + _kill_pid(pid, signal.SIGTERM) def _windows_pid_exists(pid): @@ -70,15 +90,8 @@ def _windows_pid_exists(pid): def _windows_kill_pid(pid): - """Kill the proces using pywin32 and pid""" - import ctypes - - PROCESS_TERMINATE = 1 - handle = ctypes.windll.kernel32.OpenProcess( - PROCESS_TERMINATE, False, pid - ) # @UndefinedVariable - ctypes.windll.kernel32.TerminateProcess(handle, -1) # @UndefinedVariable - ctypes.windll.kernel32.CloseHandle(handle) # @UndefinedVariable + """Kill a PID by sending a windows-specific soft-kill signal""" + _kill_pid(pid, signal.CTRL_C_EVENT) buffering = int(six.PY3) # No unbuffered text I/O on Python 3 (#20815). From 2c3c0cf40e149ffc9111fdedf4ed53e8054d5141 Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Wed, 12 Jun 2019 13:22:57 -0700 Subject: [PATCH 02/19] Utils for broadcasting/scanning zeroconf Kolibri service for discovery --- .../discovery/test/test_network_search.py | 162 ++++++++++++++++ .../core/discovery/utils/network/search.py | 174 ++++++++++++++++++ requirements/base.txt | 1 + 3 files changed, 337 insertions(+) create mode 100644 kolibri/core/discovery/test/test_network_search.py create mode 100644 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 new file mode 100644 index 00000000000..39ad443b96d --- /dev/null +++ b/kolibri/core/discovery/test/test_network_search.py @@ -0,0 +1,162 @@ +import mock +import socket + +from django.test import TestCase +from zeroconf import BadTypeInNameException +from zeroconf import service_type_name +from zeroconf import Zeroconf +from zeroconf import ServiceInfo + +from .helpers import mock_request +from ..utils.network.search import SERVICE_TYPE +from ..utils.network.search import LOCAL_DOMAIN +from ..utils.network.search import _id_from_name +from ..utils.network.search import register_zeroconf_service +from ..utils.network.search import unregister_zeroconf_service +from ..utils.network.search import get_available_instances +from ..utils.network.search import ZEROCONF_STATE +from ..utils.network.search import initialize_zeroconf_listener +from ..utils.network.search import KolibriZeroconfService +from ..utils.network.search import NonUniqueNameException + +MOCK_INTERFACE_IP = "111.222.111.222" +MOCK_PORT = 555 +MOCK_ID = "abba" + + +class MockServiceBrowser(object): + def __init__(self, zc, type_, handlers=None, listener=None): + assert handlers or listener, "You need to specify at least one handler" + if not type_.endswith(service_type_name(type_)): + raise BadTypeInNameException + self.zc = zc + self.type = type_ + + def cancel(self): + self.zc.remove_listener(self) + + +class MockZeroconf(Zeroconf): + def __init__(self, *args, **kwargs): + self.browsers = {} + self.services = {} + + def get_service_info(self, type_, name, timeout=3000): + id = _id_from_name(name) + info = ServiceInfo( + SERVICE_TYPE, + name=".".join([id, SERVICE_TYPE]), + server=".".join([id, LOCAL_DOMAIN, ""]), + address=socket.inet_aton(MOCK_INTERFACE_IP), + port=MOCK_PORT, + properties={"facilities": "[]", "channels": "[]"}, + ) + return info + + def add_service_listener(self, type_, listener): + self.remove_service_listener(listener) + self.browsers[listener] = MockServiceBrowser(self, type_, listener) + for info in self.services.values(): + listener.add_service(self, info.type, info.name) + + def register_service(self, info, ttl=60, allow_name_change=False): + self.check_service(info, allow_name_change) + self.services[info.name.lower()] = info + for listener in self.browsers: + listener.add_service(self, info.type, info.name) + + def unregister_service(self, info): + for listener in self.browsers: + listener.remove_service(self, info.type, info.name) + + def check_service(self, info, allow_name_change): + service_name = service_type_name(info.name) + if not info.type.endswith(service_name): + raise BadTypeInNameException + + instance_name = info.name[: -len(service_name) - 1] + next_instance_number = 2 + + # check for a name conflict + while info.name.lower() in self.services: + + if not allow_name_change: + raise NonUniqueNameException + + # change the name and look for a conflict + info.name = "%s-%s.%s" % (instance_name, next_instance_number, info.type) + next_instance_number += 1 + service_type_name(info.name) + + +@mock.patch( + "kolibri.core.discovery.utils.network.search._is_port_open", lambda *a, **kw: True +) +@mock.patch("kolibri.core.discovery.utils.network.search.Zeroconf", MockZeroconf) +@mock.patch( + "kolibri.core.discovery.utils.network.search.get_all_addresses", + lambda: [MOCK_INTERFACE_IP], +) +class TestNetworkSearch(TestCase): + def test_initialize_zeroconf_listener(self): + assert ZEROCONF_STATE["listener"] is None + initialize_zeroconf_listener() + assert ZEROCONF_STATE["listener"] is not None + + def test_register_zeroconf_service(self): + assert len(get_available_instances()) == 0 + initialize_zeroconf_listener() + register_zeroconf_service(MOCK_PORT, MOCK_ID) + assert get_available_instances() == [ + { + "id": MOCK_ID, + "ip": MOCK_INTERFACE_IP, + "local": True, + "self": True, + "port": MOCK_PORT, + "host": ".".join([MOCK_ID, LOCAL_DOMAIN]), + "data": {"facilities": [], "channels": []}, + } + ] + register_zeroconf_service(MOCK_PORT, MOCK_ID) + unregister_zeroconf_service() + assert len(get_available_instances()) == 0 + + def test_naming_conflict(self): + assert not ZEROCONF_STATE["listener"] + service1 = KolibriZeroconfService(id=MOCK_ID, port=MOCK_PORT) + service1.register() + assert len(get_available_instances()) == 1 + service2 = KolibriZeroconfService(id=MOCK_ID, port=MOCK_PORT) + service2.register() + assert len(get_available_instances()) == 2 + assert service1.id + "-2" == service2.id + service1.unregister() + service2.unregister() + + def test_irreconcilable_naming_conflict(self): + services = [KolibriZeroconfService(id=MOCK_ID, port=MOCK_PORT).register()] + for i in range(110): + services.append( + KolibriZeroconfService( + id="-".join([MOCK_ID, str(i)]), port=MOCK_PORT + ).register() + ) + with self.assertRaises(NonUniqueNameException): + KolibriZeroconfService(id=MOCK_ID, port=MOCK_PORT).register() + for service in services: + service.unregister() + + def test_excluding_local(self): + initialize_zeroconf_listener() + register_zeroconf_service(MOCK_PORT, MOCK_ID) + assert len(get_available_instances()) == 1 + assert len(get_available_instances(include_local=False)) == 0 + unregister_zeroconf_service() + + def tearDown(self): + unregister_zeroconf_service() + ZEROCONF_STATE["zeroconf"] = None + ZEROCONF_STATE["listener"] = None + ZEROCONF_STATE["service"] = None + super(TestNetworkSearch, self).tearDown() diff --git a/kolibri/core/discovery/utils/network/search.py b/kolibri/core/discovery/utils/network/search.py new file mode 100644 index 00000000000..bde58fc4fd1 --- /dev/null +++ b/kolibri/core/discovery/utils/network/search.py @@ -0,0 +1,174 @@ +import atexit +import json +import logging +import socket +import time +from contextlib import closing + +from zeroconf import get_all_addresses +from zeroconf import NonUniqueNameException +from zeroconf import ServiceInfo +from zeroconf import USE_IP_OF_OUTGOING_INTERFACE +from zeroconf import Zeroconf + +from kolibri.core.auth.models import Facility +from kolibri.core.content.models import ChannelMetadata + +logger = logging.getLogger(__name__) + +SERVICE_TYPE = "_kolibri._http._tcp.local." +LOCAL_DOMAIN = "kolibri.local" + +ZEROCONF_STATE = {"zeroconf": None, "listener": None, "service": None} + + +def _id_from_name(name): + assert name.endswith(SERVICE_TYPE), ( + "Invalid service name; must end with '%s'" % SERVICE_TYPE + ) + return name.replace(SERVICE_TYPE, "").strip(".") + + +def _is_port_open(host, port, timeout=1): + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: + sock.settimeout(timeout) + return sock.connect_ex((host, port)) == 0 + + +class KolibriZeroconfService(object): + + info = None + + def __init__(self, id, port=8080, data={}): + self.id = id + self.port = port + self.data = {key: json.dumps(val) for (key, val) in data.items()} + atexit.register(self.cleanup) + + def register(self): + + if not ZEROCONF_STATE["zeroconf"]: + initialize_zeroconf_listener() + + assert self.info is None, "Service is already registered!" + + i = 1 + id = self.id + + while not self.info: + + # attempt to create an mDNS service and register it on the network + try: + info = ServiceInfo( + SERVICE_TYPE, + name=".".join([id, SERVICE_TYPE]), + server=".".join([id, LOCAL_DOMAIN, ""]), + address=USE_IP_OF_OUTGOING_INTERFACE, + port=self.port, + properties=self.data, + ) + + ZEROCONF_STATE["zeroconf"].register_service(info, ttl=60) + + self.info = info + + except NonUniqueNameException: + # if there's a name conflict, append incrementing integer until no conflict + i += 1 + id = "%s-%d" % (self.id, i) + + if i > 100: + raise NonUniqueNameException() + + self.id = id + + return self + + def unregister(self): + + assert self.info is not None, "Service is not registered!" + + ZEROCONF_STATE["zeroconf"].unregister_service(self.info) + + self.info = None + + def cleanup(self, *args, **kwargs): + + if self.info and ZEROCONF_STATE["zeroconf"]: + self.unregister() + + +class KolibriZeroconfListener(object): + + instances = {} + + def add_service(self, zeroconf, type, name): + info = zeroconf.get_service_info(type, name) + id = _id_from_name(name) + ip = socket.inet_ntoa(info.address) + self.instances[id] = { + "id": id, + "ip": ip, + "local": ip in get_all_addresses(), + "port": info.port, + "host": info.server.strip("."), + "data": {key: json.loads(val) for (key, val) in info.properties.items()}, + } + logger.info( + "Kolibri instance '%s' joined zeroconf network; service info: %s\n" + % (id, self.instances[id]) + ) + + def remove_service(self, zeroconf, type, name): + id = _id_from_name(name) + logger.info("\nKolibri instance '%s' has left the zeroconf network.\n" % (id,)) + if id in self.instances: + del self.instances[id] + + +def get_available_instances(timeout=2, include_local=True): + """Retrieve a list of dicts with information about the discovered Kolibri instances on the local network, + filtering out those that can't be accessed at the specified port (via attempting to open a socket).""" + if not ZEROCONF_STATE["listener"]: + initialize_zeroconf_listener() + time.sleep(3) + instances = [] + for instance in ZEROCONF_STATE["listener"].instances.values(): + if instance["local"] and not include_local: + continue + if not _is_port_open(instance["ip"], instance["port"], timeout=timeout): + continue + instance["self"] = ( + ZEROCONF_STATE["service"] and ZEROCONF_STATE["service"].id == instance["id"] + ) + instances.append(instance) + return instances + + +def register_zeroconf_service(port, id): + if ZEROCONF_STATE["service"] is not None: + unregister_zeroconf_service() + logger.info("Registering ourselves to zeroconf network with id '%s'..." % id) + data = { + "facilities": list(Facility.objects.values("id", "dataset_id", "name")), + "channels": list( + ChannelMetadata.objects.filter(root__available=True).values("id", "name") + ), + } + ZEROCONF_STATE["service"] = KolibriZeroconfService(id=id, port=port, data=data) + ZEROCONF_STATE["service"].register() + + +def unregister_zeroconf_service(): + logger.info("Unregistering ourselves from zeroconf network...") + if ZEROCONF_STATE["service"] is not None: + ZEROCONF_STATE["service"].cleanup() + ZEROCONF_STATE["service"] = None + + +def initialize_zeroconf_listener(): + ZEROCONF_STATE["zeroconf"] = Zeroconf() + ZEROCONF_STATE["listener"] = KolibriZeroconfListener() + ZEROCONF_STATE["zeroconf"].add_service_listener( + SERVICE_TYPE, ZEROCONF_STATE["listener"] + ) diff --git a/requirements/base.txt b/requirements/base.txt index e608b1888c2..eb860ad63f0 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -33,3 +33,4 @@ sentry-sdk==0.7.9 django-redis-cache==2.0.0 redis==3.2.1 html5lib==1.0.1 +-e git://github.com/learningequality/python-zeroconf.git@8d0e70029a925862e965519c9d42b4016d694d5c#egg=zeroconf \ No newline at end of file From 2922c0f858ef1acde3bf4205dc365c2558c42acf Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Wed, 12 Jun 2019 13:33:14 -0700 Subject: [PATCH 03/19] Pass the currently specified port through into the services functions. --- kolibri/utils/cli.py | 9 +++++---- kolibri/utils/server.py | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/kolibri/utils/cli.py b/kolibri/utils/cli.py index a99fdea82a2..d34bfc57afe 100644 --- a/kolibri/utils/cli.py +++ b/kolibri/utils/cli.py @@ -420,7 +420,7 @@ def status(): } -def services(daemon=True): +def services(port, daemon=True): """ Start the kolibri background services. @@ -446,7 +446,7 @@ def services(daemon=True): become_daemon(**kwargs) - server.services() + server.services(port=port) def setup_logging(debug=False): @@ -657,8 +657,9 @@ def main(args=None): # noqa: max-complexity=13 debug = arguments["--debug"] + port = _get_port(arguments["--port"]) + if arguments["start"]: - port = _get_port(arguments["--port"]) if OPTIONS["Server"]["CHERRYPY_START"]: check_other_kolibri_running(port) @@ -733,7 +734,7 @@ def main(args=None): # noqa: max-complexity=13 "Impossible to create file lock to communicate starting process" ) - services(daemon=daemon) + services(port=port, daemon=daemon) return if arguments["language"] and arguments["setdefault"]: diff --git a/kolibri/utils/server.py b/kolibri/utils/server.py index 15b9036d0a1..ac0948dff73 100644 --- a/kolibri/utils/server.py +++ b/kolibri/utils/server.py @@ -73,7 +73,7 @@ def __init__(self, status_code): super(NotRunning, self).__init__() -def run_services(): +def run_services(port): # start the pingback thread PingbackThread.start_command() @@ -100,7 +100,7 @@ def start(port=8080, run_cherrypy=True): :param: port: Port number (default: 8080) """ - run_services() + run_services(port=port) # Write the new PID _write_pid_file(PID_FILE, port=port) @@ -125,12 +125,12 @@ def rm_pid_file(): block() -def services(): +def services(port): """ Runs the background services. """ - run_services() + run_services(port=port) # Write the new PID _write_pid_file(PID_FILE) From e923e500e252b4fcfbb62850d054c60d0e0a00c8 Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Wed, 12 Jun 2019 13:36:49 -0700 Subject: [PATCH 04/19] Register the Kolibri zeroconf service so it will be discoverable --- kolibri/utils/server.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/kolibri/utils/server.py b/kolibri/utils/server.py index ac0948dff73..f8c34066c74 100644 --- a/kolibri/utils/server.py +++ b/kolibri/utils/server.py @@ -92,6 +92,12 @@ def run_services(port): initialize_worker() + # Register the Kolibri zeroconf service so it will be discoverable on the network + from morango.models import InstanceIDModel + from kolibri.core.discovery.utils.network.search import register_zeroconf_service + instance, _ = InstanceIDModel.get_or_create_current_instance() + register_zeroconf_service(port=port, id=instance.id[:4]) + def start(port=8080, run_cherrypy=True): """ From 7d40f1e06b1e254d713d26690448ee2d75d72f47 Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Wed, 12 Jun 2019 13:38:11 -0700 Subject: [PATCH 05/19] Gracefully clean up and unregister Kolibri zeroconf service on shutdown --- kolibri/utils/cli.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/kolibri/utils/cli.py b/kolibri/utils/cli.py index d34bfc57afe..b4c62bd888b 100644 --- a/kolibri/utils/cli.py +++ b/kolibri/utils/cli.py @@ -644,6 +644,13 @@ def _get_port(port): return int(port) if port else OPTIONS["Deployment"]["HTTP_PORT"] +def _cleanup_before_quitting(signum, frame): + from kolibri.core.discovery.utils.network.search import unregister_zeroconf_service + unregister_zeroconf_service() + signal.signal(signum, signal.SIG_DFL) + os.kill(os.getpid(), signum) + + def main(args=None): # noqa: max-complexity=13 """ Kolibri's main function. Parses arguments and calls utility functions. @@ -651,7 +658,8 @@ def main(args=None): # noqa: max-complexity=13 to use main() for integration tests in order to test the argument API. """ - signal.signal(signal.SIGINT, signal.SIG_DFL) + signal.signal(signal.SIGINT, _cleanup_before_quitting) + signal.signal(signal.SIGTERM, _cleanup_before_quitting) arguments, django_args = parse_args(args) From ffe47c7508f7ed4a1a20896663cc34c5906cb2df Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Wed, 12 Jun 2019 13:39:33 -0700 Subject: [PATCH 06/19] Add network search API endpoint for enumerating found Kolibri instances --- kolibri/core/discovery/api.py | 10 ++++++++++ kolibri/core/discovery/api_urls.py | 2 ++ 2 files changed, 12 insertions(+) diff --git a/kolibri/core/discovery/api.py b/kolibri/core/discovery/api.py index 1a8389a92af..1bf722a8e4a 100644 --- a/kolibri/core/discovery/api.py +++ b/kolibri/core/discovery/api.py @@ -1,11 +1,21 @@ from rest_framework import viewsets +from rest_framework.response import Response from .models import NetworkLocation from .serializers import NetworkLocationSerializer from kolibri.core.content.permissions import CanManageContent +from kolibri.core.device.permissions import UserHasAnyDevicePermissions +from kolibri.core.discovery.utils.network.search import get_available_instances class NetworkLocationViewSet(viewsets.ModelViewSet): permission_classes = (CanManageContent,) serializer_class = NetworkLocationSerializer queryset = NetworkLocation.objects.all() + + +class NetworkSearchViewSet(viewsets.ViewSet): + permission_classes = (UserHasAnyDevicePermissions,) + + def list(self, request): + return Response(get_available_instances()) diff --git a/kolibri/core/discovery/api_urls.py b/kolibri/core/discovery/api_urls.py index 1a844a7c84b..c962f75da12 100644 --- a/kolibri/core/discovery/api_urls.py +++ b/kolibri/core/discovery/api_urls.py @@ -1,9 +1,11 @@ from rest_framework import routers from .api import NetworkLocationViewSet +from .api import NetworkSearchViewSet router = routers.SimpleRouter() router.register(r"networklocation", NetworkLocationViewSet, base_name="networklocation") +router.register(r"networksearch", NetworkSearchViewSet, base_name="networksearch") urlpatterns = router.urls From bb2d08a0b3cb5752cbf0d3e48ffd3909894f2652 Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Wed, 12 Jun 2019 15:23:50 -0700 Subject: [PATCH 07/19] Run black and flake8 on files that still needed it to pass CI --- .../core/discovery/test/test_network_search.py | 15 +++++++-------- kolibri/utils/cli.py | 1 + kolibri/utils/server.py | 1 + kolibri/utils/system.py | 11 +++++++---- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/kolibri/core/discovery/test/test_network_search.py b/kolibri/core/discovery/test/test_network_search.py index 39ad443b96d..edb20f9edc3 100644 --- a/kolibri/core/discovery/test/test_network_search.py +++ b/kolibri/core/discovery/test/test_network_search.py @@ -1,23 +1,22 @@ -import mock import socket +import mock from django.test import TestCase from zeroconf import BadTypeInNameException from zeroconf import service_type_name -from zeroconf import Zeroconf from zeroconf import ServiceInfo +from zeroconf import Zeroconf -from .helpers import mock_request -from ..utils.network.search import SERVICE_TYPE -from ..utils.network.search import LOCAL_DOMAIN from ..utils.network.search import _id_from_name -from ..utils.network.search import register_zeroconf_service -from ..utils.network.search import unregister_zeroconf_service from ..utils.network.search import get_available_instances -from ..utils.network.search import ZEROCONF_STATE 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 register_zeroconf_service +from ..utils.network.search import SERVICE_TYPE +from ..utils.network.search import unregister_zeroconf_service +from ..utils.network.search import ZEROCONF_STATE MOCK_INTERFACE_IP = "111.222.111.222" MOCK_PORT = 555 diff --git a/kolibri/utils/cli.py b/kolibri/utils/cli.py index b4c62bd888b..3cf192abd81 100644 --- a/kolibri/utils/cli.py +++ b/kolibri/utils/cli.py @@ -646,6 +646,7 @@ def _get_port(port): def _cleanup_before_quitting(signum, frame): from kolibri.core.discovery.utils.network.search import unregister_zeroconf_service + unregister_zeroconf_service() signal.signal(signum, signal.SIG_DFL) os.kill(os.getpid(), signum) diff --git a/kolibri/utils/server.py b/kolibri/utils/server.py index f8c34066c74..5eefd7d6745 100644 --- a/kolibri/utils/server.py +++ b/kolibri/utils/server.py @@ -95,6 +95,7 @@ def run_services(port): # Register the Kolibri zeroconf service so it will be discoverable on the network from morango.models import InstanceIDModel from kolibri.core.discovery.utils.network.search import register_zeroconf_service + instance, _ = InstanceIDModel.get_or_create_current_instance() register_zeroconf_service(port=port, id=instance.id[:4]) diff --git a/kolibri/utils/system.py b/kolibri/utils/system.py index 1178c9cf719..f4bf85f909c 100644 --- a/kolibri/utils/system.py +++ b/kolibri/utils/system.py @@ -46,17 +46,17 @@ def _posix_pid_exists(pid): return True - def _kill_pid(pid, softkill_signal_number): """Kill a PID by sending a signal, starting with a softer one and then escalating as needed""" - try: logger.debug("Attempting to soft kill process with pid %d..." % pid) os.kill(pid, softkill_signal_number) logger.debug("Soft kill signal sent without error.") # process does not exist except OSError: - logger.debug("Soft kill signal could not be sent (OSError); process may not exist?") + logger.debug( + "Soft kill signal could not be sent (OSError); process may not exist?" + ) return # give some time for the process to clean itself up gracefully bfore we force anything i = 0 @@ -65,7 +65,10 @@ def _kill_pid(pid, softkill_signal_number): i += 1 # if process didn't exit cleanly, make one last effort to kill it if pid_exists(pid): - logger.debug("Process wth pid %s still exists after soft kill signal; attempting a SIGKILL." % pid) + logger.debug( + "Process wth pid %s still exists after soft kill signal; attempting a SIGKILL." + % pid + ) os.kill(pid, signal.SIGKILL) logger.debug("SIGKILL signal sent without error.") From 6648666fd8c56b44dc0f1e284683651dbcbf24a2 Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Wed, 12 Jun 2019 15:59:07 -0700 Subject: [PATCH 08/19] Fix zeroconf github URL in requirements.txt --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index eb860ad63f0..6b801c058ba 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -33,4 +33,4 @@ sentry-sdk==0.7.9 django-redis-cache==2.0.0 redis==3.2.1 html5lib==1.0.1 --e git://github.com/learningequality/python-zeroconf.git@8d0e70029a925862e965519c9d42b4016d694d5c#egg=zeroconf \ No newline at end of file +git+https://github.com/learningequality/python-zeroconf.git@8d0e70029a925862e965519c9d42b4016d694d5c#egg=zeroconf From a75293ddfd2ed690f00725fe6c7da6feb681fc7f Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Wed, 12 Jun 2019 16:31:29 -0700 Subject: [PATCH 09/19] Remove .egg-info directories from kolibri/dist as they cause errors --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 9dae3e9f25f..01a77936b33 100644 --- a/Makefile +++ b/Makefile @@ -132,6 +132,7 @@ staticdeps: git checkout -- kolibri/dist # restore __init__.py pip install -t kolibri/dist -r "requirements.txt" rm -rf kolibri/dist/*.dist-info # pip installs from PyPI will complain if we have more than one dist-info directory. + rm -rf kolibri/dist/*.egg-info rm -r kolibri/dist/man kolibri/dist/bin || true # remove the two folders introduced by pip 10 # Remove unnecessary python2-syntax'ed file # https://github.com/learningequality/kolibri/issues/3152 @@ -145,6 +146,7 @@ staticdeps-cext: pip install -t kolibri/dist/cext -r "requirements/cext_noarch.txt" --no-deps rm -rf kolibri/dist/*.dist-info # pip installs from PyPI will complain if we have more than one dist-info directory. rm -rf kolibri/dist/cext/*.dist-info # pip installs from PyPI will complain if we have more than one dist-info directory. + rm -rf kolibri/dist/*.egg-info make test-namespaced-packages writeversion: From 8ae6bbcf1e3285291d0c044d6306816065ca54f0 Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Wed, 12 Jun 2019 18:50:08 -0700 Subject: [PATCH 10/19] Update to Morango 0.4.6 --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index 6b801c058ba..d74a3f12c55 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -20,7 +20,7 @@ metafone==0.5 le-utils==0.1.15 kolibri_exercise_perseus_plugin==1.1.8 jsonfield==2.0.2 -morango==0.4.5 +morango==0.4.6 requests-toolbelt==0.8.0 clint==0.5.1 tzlocal==1.5.1 From df7523b45f42fa221455dfacf6954320f5d3f03f Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Sat, 15 Jun 2019 14:30:24 -0700 Subject: [PATCH 11/19] Gracefully handle server being started within a thread (e.g. Android) --- kolibri/utils/cli.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/kolibri/utils/cli.py b/kolibri/utils/cli.py index 3cf192abd81..9274df0a337 100644 --- a/kolibri/utils/cli.py +++ b/kolibri/utils/cli.py @@ -659,8 +659,12 @@ def main(args=None): # noqa: max-complexity=13 to use main() for integration tests in order to test the argument API. """ - signal.signal(signal.SIGINT, _cleanup_before_quitting) - signal.signal(signal.SIGTERM, _cleanup_before_quitting) + try: + signal.signal(signal.SIGINT, _cleanup_before_quitting) + signal.signal(signal.SIGTERM, _cleanup_before_quitting) + logger.info("Added signal handlers for cleaning up on exit...") + except ValueError: + logger.warn("Error adding signal handlers for cleaning up on exit...") arguments, django_args = parse_args(args) From 2881db32a69a6b566deaf13fda7a29d254d3cec8 Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Mon, 17 Jun 2019 01:46:26 -0700 Subject: [PATCH 12/19] Initial prototype frontend code for zeroconf device discovery. --- .../core/discovery/utils/network/search.py | 1 + .../assets/src/apiResources.js | 4 + .../SearchAddressForm.vue | 176 ++++++++++++++++++ .../SelectAddressForm.vue | 27 ++- .../SelectNetworkAddressModal/api.js | 23 ++- .../SelectNetworkAddressModal/index.vue | 12 ++ 6 files changed, 235 insertions(+), 8 deletions(-) create mode 100644 kolibri/plugins/device_management/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SearchAddressForm.vue diff --git a/kolibri/core/discovery/utils/network/search.py b/kolibri/core/discovery/utils/network/search.py index bde58fc4fd1..fb1170dcc86 100644 --- a/kolibri/core/discovery/utils/network/search.py +++ b/kolibri/core/discovery/utils/network/search.py @@ -113,6 +113,7 @@ def add_service(self, zeroconf, type, name): "port": info.port, "host": info.server.strip("."), "data": {key: json.loads(val) for (key, val) in info.properties.items()}, + "base_url": "http://{ip}:{port}/".format(ip=ip, port=info.port), } logger.info( "Kolibri instance '%s' joined zeroconf network; service info: %s\n" diff --git a/kolibri/plugins/device_management/assets/src/apiResources.js b/kolibri/plugins/device_management/assets/src/apiResources.js index edb11c99fe6..3d470959161 100644 --- a/kolibri/plugins/device_management/assets/src/apiResources.js +++ b/kolibri/plugins/device_management/assets/src/apiResources.js @@ -3,3 +3,7 @@ import { Resource } from 'kolibri.lib.apiResource'; export const NetworkLocationResource = new Resource({ name: 'networklocation', }); + +export const NetworkSearchResource = new Resource({ + name: 'networksearch', +}); diff --git a/kolibri/plugins/device_management/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SearchAddressForm.vue b/kolibri/plugins/device_management/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SearchAddressForm.vue new file mode 100644 index 00000000000..36a72a4a73f --- /dev/null +++ b/kolibri/plugins/device_management/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SearchAddressForm.vue @@ -0,0 +1,176 @@ + + + + + + + diff --git a/kolibri/plugins/device_management/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SelectAddressForm.vue b/kolibri/plugins/device_management/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SelectAddressForm.vue index e14f65391f4..a290dbcb0b6 100644 --- a/kolibri/plugins/device_management/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SelectAddressForm.vue +++ b/kolibri/plugins/device_management/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SelectAddressForm.vue @@ -24,13 +24,25 @@ /> - + @@ -24,10 +30,12 @@ import { availableChannelsPageLink, selectContentPageLink } from '../manageContentLinks'; import AddAddressForm from './AddAddressForm'; import SelectAddressForm from './SelectAddressForm'; + import SearchAddressForm from './SearchAddressForm'; const Stages = { ADD_ADDRESS: 'ADD_ADDRESS', SELECT_ADDRESS: 'SELECT_ADDRESS', + SEARCH_ADDRESS: 'SEARCH_ADDRESS', }; export default { @@ -35,6 +43,7 @@ components: { AddAddressForm, SelectAddressForm, + SearchAddressForm, }, data() { return { @@ -54,6 +63,9 @@ goToAddAddress() { this.stage = Stages.ADD_ADDRESS; }, + goToSearchAddress() { + this.stage = Stages.SEARCH_ADDRESS; + }, goToSelectAddress() { this.stage = Stages.SELECT_ADDRESS; }, From 6dec95cd844130a97f9cb0df2a5855fe641e4fe8 Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Mon, 17 Jun 2019 10:09:42 -0700 Subject: [PATCH 13/19] Add base_url to network search results expectations for tests --- kolibri/core/discovery/test/test_network_search.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kolibri/core/discovery/test/test_network_search.py b/kolibri/core/discovery/test/test_network_search.py index edb20f9edc3..27ee4f71ddf 100644 --- a/kolibri/core/discovery/test/test_network_search.py +++ b/kolibri/core/discovery/test/test_network_search.py @@ -115,6 +115,9 @@ def test_register_zeroconf_service(self): "port": MOCK_PORT, "host": ".".join([MOCK_ID, LOCAL_DOMAIN]), "data": {"facilities": [], "channels": []}, + "base_url": "http://{ip}:{port}/".format( + ip=MOCK_INTERFACE_IP, port=MOCK_PORT + ), } ] register_zeroconf_service(MOCK_PORT, MOCK_ID) From 7c07682a965c0ddbf7f1a93a88b74d3cbd02c1a0 Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Mon, 17 Jun 2019 10:54:33 -0700 Subject: [PATCH 14/19] Put facility name in radio item for device list --- .../SelectNetworkAddressModal/SearchAddressForm.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/kolibri/plugins/device_management/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SearchAddressForm.vue b/kolibri/plugins/device_management/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SearchAddressForm.vue index 36a72a4a73f..0040c859fa8 100644 --- a/kolibri/plugins/device_management/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SearchAddressForm.vue +++ b/kolibri/plugins/device_management/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SearchAddressForm.vue @@ -20,7 +20,7 @@ v-model="selectedDeviceId" class="radio-button" :value="d.id" - :label="d.host" + :label="formatDeviceName(d)" :description="d.base_url" :disabled="d.disabled" /> @@ -111,6 +111,9 @@ this.stopPolling(); }, methods: { + formatDeviceName(device) { + return device.host + ' (' + device.data.facilities[0].name + ')'; + }, handleSubmit() { this.attemptingToConnect = true; const device = find(this.devices, { id: this.selectedDeviceId }); From 30e13a882a87bbd0480223d023cb61996a1d9769 Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Thu, 20 Jun 2019 10:57:39 -0700 Subject: [PATCH 15/19] Update zeroconf to 0.19.3 from PyPI --- kolibri/core/discovery/utils/network/search.py | 2 +- requirements/base.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kolibri/core/discovery/utils/network/search.py b/kolibri/core/discovery/utils/network/search.py index fb1170dcc86..b07f8aff3d2 100644 --- a/kolibri/core/discovery/utils/network/search.py +++ b/kolibri/core/discovery/utils/network/search.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) -SERVICE_TYPE = "_kolibri._http._tcp.local." +SERVICE_TYPE = "Kolibri._sub._http._tcp.local." LOCAL_DOMAIN = "kolibri.local" ZEROCONF_STATE = {"zeroconf": None, "listener": None, "service": None} diff --git a/requirements/base.txt b/requirements/base.txt index d74a3f12c55..9325272f374 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -33,4 +33,4 @@ sentry-sdk==0.7.9 django-redis-cache==2.0.0 redis==3.2.1 html5lib==1.0.1 -git+https://github.com/learningequality/python-zeroconf.git@8d0e70029a925862e965519c9d42b4016d694d5c#egg=zeroconf +zeroconf-py2compat==0.19.3 From 52eed3495b49edc368adcf28c6bd812e88382b08 Mon Sep 17 00:00:00 2001 From: Micah Fitch Date: Wed, 13 Nov 2019 17:47:47 -0600 Subject: [PATCH 16/19] Rough implementation of latest peer importing designs. --- .../core/discovery/utils/network/search.py | 1 + .../SearchAddressForm.vue | 179 ------------------ .../SelectAddressForm.vue | 110 +++++++---- .../__test__/SelectAddressForm.spec.js | 2 +- .../SelectNetworkAddressModal/index.vue | 8 - kolibri/utils/server.py | 2 +- 6 files changed, 80 insertions(+), 222 deletions(-) delete mode 100644 kolibri/plugins/device/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SearchAddressForm.vue diff --git a/kolibri/core/discovery/utils/network/search.py b/kolibri/core/discovery/utils/network/search.py index b07f8aff3d2..a9d3277d144 100644 --- a/kolibri/core/discovery/utils/network/search.py +++ b/kolibri/core/discovery/utils/network/search.py @@ -143,6 +143,7 @@ def get_available_instances(timeout=2, include_local=True): ZEROCONF_STATE["service"] and ZEROCONF_STATE["service"].id == instance["id"] ) instances.append(instance) + logger.info(str(instances)) return instances diff --git a/kolibri/plugins/device/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SearchAddressForm.vue b/kolibri/plugins/device/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SearchAddressForm.vue deleted file mode 100644 index cea7e72ffa9..00000000000 --- a/kolibri/plugins/device/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SearchAddressForm.vue +++ /dev/null @@ -1,179 +0,0 @@ - - - - - - - diff --git a/kolibri/plugins/device/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SelectAddressForm.vue b/kolibri/plugins/device/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SelectAddressForm.vue index fbea50fb610..43808db8f58 100644 --- a/kolibri/plugins/device/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SelectAddressForm.vue +++ b/kolibri/plugins/device/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SelectAddressForm.vue @@ -20,31 +20,19 @@ v-if="requestsFailed" appearance="basic-link" :text="$tr('refreshAddressesButtonLabel')" - @click="refreshAddressList" + @click="refreshSavedAddressList" /> - - - @@ -74,7 +83,7 @@ import find from 'lodash/find'; import UiAlert from 'keen-ui/src/UiAlert'; import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings'; - import { deleteAddress, fetchAddresses } from './api'; + import { deleteAddress, fetchAddresses, fetchDevices } from './api'; const Stages = { FETCHING_ADDRESSES: 'FETCHING_ADDRESSES', @@ -94,7 +103,8 @@ props: {}, data() { return { - addresses: [], + savedAddresses: [], + discoveredAddresses: [], selectedAddressId: '', showUiAlerts: false, stage: '', @@ -104,6 +114,9 @@ computed: { ...mapGetters('manageContent/wizard', ['isImportingMore']), ...mapState('manageContent/wizard', ['transferredChannel']), + addresses() { + return this.savedAddresses.concat(this.discoveredAddresses); + }, submitDisabled() { return ( this.selectedAddressId === '' || @@ -154,7 +167,7 @@ }, }, beforeMount() { - return this.refreshAddressList(); + return this.refreshSavedAddressList(); }, mounted() { // Wait a little bit of time before showing UI alerts so there is no flash @@ -162,14 +175,19 @@ setTimeout(() => { this.showUiAlerts = true; }, 100); + + this.startDiscoveryPolling(); + }, + destroyed() { + this.stopDiscoveryPolling(); }, methods: { - refreshAddressList() { + refreshSavedAddressList() { this.stage = this.Stages.FETCHING_ADDRESSES; - this.addresses = []; + this.savedAddresses = []; return fetchAddresses(this.isImportingMore ? this.transferredChannel.id : '') .then(addresses => { - this.addresses = addresses; + this.savedAddresses = addresses; this.resetSelectedAddress(); this.stage = this.Stages.FETCHING_SUCCESSFUL; }) @@ -185,12 +203,12 @@ this.selectedAddressId = ''; } }, - removeAddress(id) { + removeSavedAddress(id) { this.stage = this.Stages.DELETING_ADDRESS; return deleteAddress(id) .then(() => { - this.addresses = this.addresses.filter(a => a.id !== id); - this.resetSelectedAddress(this.addresses); + this.savedAddresses = this.savedAddresses.filter(a => a.id !== id); + this.resetSelectedAddress(this.savedAddresses); this.stage = this.Stages.DELETING_SUCCESSFUL; this.$emit('removed_address'); }) @@ -198,9 +216,34 @@ this.stage = this.Stages.DELETING_FAILED; }); }, + + discoverPeers() { + this.$parent.$emit('started_peer_discovery'); + return fetchDevices(this.isImportingMore ? this.transferredChannel.id : '') + .then(devices => { + this.$parent.$emit('finished_peer_discovery'); + this.devices = devices; + }) + .catch(() => { + this.$parent.$emit('peer_discovery_failed'); + }); + }, + + startDiscoveryPolling() { + if (!this.intervalId) { + this.intervalId = setInterval(this.discoverPeers, 5000); + } + }, + + stopDiscoveryPolling() { + if (this.intervalId) { + this.intervalId = clearInterval(this.intervalId); + } + }, + handleSubmit() { if (this.selectedAddressId) { - this.$emit('submit', find(this.addresses, { id: this.selectedAddressId })); + this.$emit('submit', find(this.savedAddresses, { id: this.selectedAddressId })); } }, }, @@ -211,9 +254,10 @@ forgetAddressButtonLabel: 'Forget', header: 'Select network address', newAddressButtonLabel: 'Add new address', - searchAddressButtonLabel: 'Search for devices', noAddressText: 'There are no addresses yet', refreshAddressesButtonLabel: 'Refresh addresses', + peerDeviceName: 'Local Kolibri ({ identifier })', + searchingText: 'Searching...', }, }; diff --git a/kolibri/plugins/device/assets/src/views/ManageContentPage/SelectNetworkAddressModal/__test__/SelectAddressForm.spec.js b/kolibri/plugins/device/assets/src/views/ManageContentPage/SelectNetworkAddressModal/__test__/SelectAddressForm.spec.js index f3585d20da3..8180c9961b6 100644 --- a/kolibri/plugins/device/assets/src/views/ManageContentPage/SelectNetworkAddressModal/__test__/SelectAddressForm.spec.js +++ b/kolibri/plugins/device/assets/src/views/ManageContentPage/SelectNetworkAddressModal/__test__/SelectAddressForm.spec.js @@ -101,7 +101,7 @@ describe('SelectAddressForm', () => { it('clicking "forget" next to an address triggers a forgetting action', async () => { const { wrapper } = makeWrapper(); await wrapper.vm.$nextTick(); - await wrapper.vm.removeAddress(); + await wrapper.vm.removeSavedAddress(); expect(wrapper.emitted().removed_address).toHaveLength(1); }); diff --git a/kolibri/plugins/device/assets/src/views/ManageContentPage/SelectNetworkAddressModal/index.vue b/kolibri/plugins/device/assets/src/views/ManageContentPage/SelectNetworkAddressModal/index.vue index 827af61217c..4ff7bb706e4 100644 --- a/kolibri/plugins/device/assets/src/views/ManageContentPage/SelectNetworkAddressModal/index.vue +++ b/kolibri/plugins/device/assets/src/views/ManageContentPage/SelectNetworkAddressModal/index.vue @@ -14,11 +14,6 @@ @cancel="goToSelectAddress" @added_address="handleAddedAddress" /> - @@ -30,12 +25,10 @@ import { availableChannelsPageLink, selectContentPageLink } from '../manageContentLinks'; import AddAddressForm from './AddAddressForm'; import SelectAddressForm from './SelectAddressForm'; - import SearchAddressForm from './SearchAddressForm'; const Stages = { ADD_ADDRESS: 'ADD_ADDRESS', SELECT_ADDRESS: 'SELECT_ADDRESS', - SEARCH_ADDRESS: 'SEARCH_ADDRESS', }; export default { @@ -43,7 +36,6 @@ components: { AddAddressForm, SelectAddressForm, - SearchAddressForm, }, data() { return { diff --git a/kolibri/utils/server.py b/kolibri/utils/server.py index 62a7288c336..c5d75000bad 100644 --- a/kolibri/utils/server.py +++ b/kolibri/utils/server.py @@ -138,7 +138,7 @@ def start(port=8080, run_cherrypy=True): block() -def services(port): +def services(port=8080): """ Runs the background services. """ From a40c1fcab79b2cd8d6d899337122fa5b84cca1ad Mon Sep 17 00:00:00 2001 From: Micah Fitch Date: Wed, 13 Nov 2019 18:45:19 -0600 Subject: [PATCH 17/19] reset changes made to utils/system.py --- kolibri/utils/system.py | 46 ++++++++++++++--------------------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/kolibri/utils/system.py b/kolibri/utils/system.py index dc143b9a7fe..6cfa02f89d4 100644 --- a/kolibri/utils/system.py +++ b/kolibri/utils/system.py @@ -16,11 +16,8 @@ from __future__ import print_function from __future__ import unicode_literals -import logging import os -import signal import sys -import time import six from django.db import connections @@ -28,8 +25,6 @@ from .conf import KOLIBRI_HOME from kolibri.utils.android import on_android -logger = logging.getLogger(__name__) - def _posix_pid_exists(pid): """Check whether PID exists in the current process table.""" @@ -46,36 +41,18 @@ def _posix_pid_exists(pid): return True -def _kill_pid(pid, softkill_signal_number): - """Kill a PID by sending a signal, starting with a softer one and then escalating as needed""" +def _posix_kill_pid(pid): + """Kill a PID by sending a posix signal""" + import signal + try: - logger.debug("Attempting to soft kill process with pid %d..." % pid) - os.kill(pid, softkill_signal_number) - logger.debug("Soft kill signal sent without error.") + os.kill(pid, signal.SIGTERM) # process does not exist except OSError: - logger.debug( - "Soft kill signal could not be sent (OSError); process may not exist?" - ) return - # give some time for the process to clean itself up gracefully bfore we force anything - i = 0 - while pid_exists(pid) and i < 10: - time.sleep(0.5) - i += 1 - # if process didn't exit cleanly, make one last effort to kill it + # process didn't exit cleanly, make one last effort to kill it if pid_exists(pid): - logger.debug( - "Process wth pid %s still exists after soft kill signal; attempting a SIGKILL." - % pid - ) os.kill(pid, signal.SIGKILL) - logger.debug("SIGKILL signal sent without error.") - - -def _posix_kill_pid(pid): - """Kill a PID by sending a posix-specific soft-kill signal""" - _kill_pid(pid, signal.SIGTERM) def _windows_pid_exists(pid): @@ -93,8 +70,15 @@ def _windows_pid_exists(pid): def _windows_kill_pid(pid): - """Kill a PID by sending a windows-specific soft-kill signal""" - _kill_pid(pid, signal.CTRL_C_EVENT) + """Kill the proces using pywin32 and pid""" + import ctypes + + PROCESS_TERMINATE = 1 + handle = ctypes.windll.kernel32.OpenProcess( + PROCESS_TERMINATE, False, pid + ) # @UndefinedVariable + ctypes.windll.kernel32.TerminateProcess(handle, -1) # @UndefinedVariable + ctypes.windll.kernel32.CloseHandle(handle) # @UndefinedVariable buffering = int(six.PY3) # No unbuffered text I/O on Python 3 (#20815). From ae7bb70ba852dae072eb9f7f82fa16b7be8952db Mon Sep 17 00:00:00 2001 From: Micah Fitch Date: Wed, 13 Nov 2019 18:46:23 -0600 Subject: [PATCH 18/19] remove debugging line --- kolibri/core/discovery/utils/network/search.py | 1 - 1 file changed, 1 deletion(-) diff --git a/kolibri/core/discovery/utils/network/search.py b/kolibri/core/discovery/utils/network/search.py index a9d3277d144..b07f8aff3d2 100644 --- a/kolibri/core/discovery/utils/network/search.py +++ b/kolibri/core/discovery/utils/network/search.py @@ -143,7 +143,6 @@ def get_available_instances(timeout=2, include_local=True): ZEROCONF_STATE["service"] and ZEROCONF_STATE["service"].id == instance["id"] ) instances.append(instance) - logger.info(str(instances)) return instances From 9e13f66d619c3174225f362a8d94e7948d86e6c6 Mon Sep 17 00:00:00 2001 From: Micah Date: Thu, 14 Nov 2019 09:24:53 -0600 Subject: [PATCH 19/19] Fixed typo --- .../SelectNetworkAddressModal/SelectAddressForm.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kolibri/plugins/device/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SelectAddressForm.vue b/kolibri/plugins/device/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SelectAddressForm.vue index 43808db8f58..a7f1db2f67c 100644 --- a/kolibri/plugins/device/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SelectAddressForm.vue +++ b/kolibri/plugins/device/assets/src/views/ManageContentPage/SelectNetworkAddressModal/SelectAddressForm.vue @@ -257,7 +257,7 @@ noAddressText: 'There are no addresses yet', refreshAddressesButtonLabel: 'Refresh addresses', peerDeviceName: 'Local Kolibri ({ identifier })', - searchingText: 'Searching...', + searchingText: 'Searching', }, };