Repository: learningequality/kolibri

Issue:
Kolibri fails to handle errors during startup and instead, itself errors
## Observed behavior
When making changes to the zeroconf code that resulted in an error, a further error occurred during the handling of said error.

## Errors and logs
```
ERROR    Error in 'START_ERROR' listener <bound method ProcessBus.START_ERROR of <magicbus.process.ProcessBus object at 0x7fda2bd12390>>
Traceback (most recent call last):
  File "/home/richard/.virtualenvs/kolibri/lib/python2.7/site-packages/magicbus/base.py", line 273, in publish
    result = listener(*args, **kwargs)
  File "/home/richard/.virtualenvs/kolibri/lib/python2.7/site-packages/magicbus/process.py", line 104, in START_ERROR
    self.transition('EXITED')
  File "/home/richard/.virtualenvs/kolibri/lib/python2.7/site-packages/magicbus/base.py", line 186, in transition
    output.append(self._transition(next_state))
  File "/home/richard/.virtualenvs/kolibri/lib/python2.7/site-packages/magicbus/base.py", line 223, in _transition
    self._transition(self.errors[newstate], *sys.exc_info())
  File "/home/richard/.virtualenvs/kolibri/lib/python2.7/site-packages/magicbus/base.py", line 215, in _transition
    return self.publish(newstate, *args, **kwargs)
  File "/home/richard/.virtualenvs/kolibri/lib/python2.7/site-packages/magicbus/base.py", line 291, in publish
    raise exc
ChannelFailures: TypeError('set_pid_file() takes exactly 2 arguments (5 given)',)
```

## Expected behavior
Errors should be handled and not result in further errors during handling.

This seems to be a result of binding the `set_pid_file` method of `PIDPlugin` to every status in the `magicbus` status map. This includes the `START_ERROR`, `EXIT_ERROR` etc states - which have a different callback signature of the exception traceback. 

To fix this, we should only bind the function to handle the states that just set a status.


Discussion:

Resolving patch:
diff --git a/kolibri/utils/server.py b/kolibri/utils/server.py
--- a/kolibri/utils/server.py
+++ b/kolibri/utils/server.py
@@ -277,17 +277,14 @@ def SERVING(self, port):
         register_zeroconf_service(port=port or self.port)
 
     def STOP(self):
-        from kolibri.core.tasks.main import scheduler
-
-        scheduler.shutdown_scheduler()
-        if self.workers is not None:
-            for worker in self.workers:
-                worker.shutdown()
         from kolibri.core.discovery.utils.network.search import (
             unregister_zeroconf_service,
         )
 
         unregister_zeroconf_service()
+        from kolibri.core.tasks.main import scheduler
+
+        scheduler.shutdown_scheduler()
 
         if self.workers is not None:
             for worker in self.workers:
@@ -324,7 +321,7 @@ def __init__(self, bus, port, zip_port, pid_file=PID_FILE):
             handler.priority = 10
             self.bus.subscribe(bus_status, handler)
 
-    def set_pid_file(self, status):
+    def set_pid_file(self, status, *args):
         """
         Writes a PID file in the format Kolibri parses
 
@@ -386,9 +383,9 @@ def __init__(self, bus):
 
         self.handlers.update(
             {
-                "SIGINT": self.handle_SIGTERM,
-                "CTRL_C_EVENT": self.handle_SIGTERM,
-                "CTRL_BREAK_EVENT": self.handle_SIGTERM,
+                "SIGINT": self.handle_SIGINT,
+                "CTRL_C_EVENT": self.handle_SIGINT,
+                "CTRL_BREAK_EVENT": self.handle_SIGINT,
             }
         )
 
@@ -405,6 +402,11 @@ def subscribe(self):
     def ENTER(self):
         self.process_pid = os.getpid()
 
+    def handle_SIGINT(self):
+        """Transition to the EXITED state."""
+        self.bus.log("Keyboard interrupt caught. Exiting.")
+        self.bus.transition("EXITED")
+
 
 class ProcessControlPlugin(Monitor):
     def __init__(self, bus):


Test patch:
diff --git a/kolibri/utils/tests/test_server.py b/kolibri/utils/tests/test_server.py
--- a/kolibri/utils/tests/test_server.py
+++ b/kolibri/utils/tests/test_server.py
@@ -178,9 +178,8 @@ def test_services_shutdown_on_stop(self, unregister_zeroconf_service):
 
             # Do we shutdown workers correctly?
             for mock_worker in services_plugin.workers:
-                assert mock_worker.shutdown.call_count == 2
+                assert mock_worker.shutdown.call_count == 1
                 assert mock_worker.mock_calls == [
-                    mock.call.shutdown(),
                     mock.call.shutdown(wait=True),
                 ]