Repository: opsdroid/opsdroid

Issue:
SIGINT (Ctrl+C) should be handled by opsdroid
When executing opsdroid in Ubuntu or in a Docker container, the following message appears:
`(Press CTRL+C to quit)`

When hitting `Ctrl+C` though, the following traceback is produced:

```
^CERROR asyncio: Task exception was never retrieved
future: <Task finished coro=<ConnectorShell.listen() done, defined at /root/.opsdroid/modules/opsdroid-modules/connector/shell/__init__.py:57> exception=ConnectionResetError('Connection lost',)>
Traceback (most recent call last):
  File "/usr/local/lib/python3.5/asyncio/tasks.py", line 240, in _step
    result = coro.send(None)
  File "/root/.opsdroid/modules/opsdroid-modules/connector/shell/__init__.py", line 63, in listen
    user_input = await async_input('', opsdroid.eventloop)
  File "/root/.opsdroid/modules/opsdroid-modules/connector/shell/__init__.py", line 37, in async_input
    await writer.drain()
  File "/usr/local/lib/python3.5/asyncio/streams.py", line 333, in drain
    yield from self._protocol._drain_helper()
  File "/usr/local/lib/python3.5/asyncio/streams.py", line 204, in _drain_helper
    raise ConnectionResetError('Connection lost')
ConnectionResetError: Connection lost
ERROR asyncio: Exception in default exception handler
Traceback (most recent call last):
  File "/usr/local/lib/python3.5/asyncio/base_events.py", line 1284, in call_exception_handler
    self.default_exception_handler(context)
  File "/usr/local/lib/python3.5/asyncio/base_events.py", line 1259, in default_exception_handler
    logger.error('\n'.join(log_lines), exc_info=exc_info)
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1314, in error
    self._log(ERROR, msg, args, **kwargs)
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1421, in _log
    self.handle(record)
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1431, in handle
    self.callHandlers(record)
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1493, in callHandlers
    hdlr.handle(record)
  File "/usr/local/lib/python3.5/logging/__init__.py", line 861, in handle
    self.emit(record)
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1053, in emit
    self.stream = self._open()
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1043, in _open
    return open(self.baseFilename, self.mode, encoding=self.encoding)
NameError: name 'open' is not defined
Exception ignored in: <bound method Task.__del__ of <Task finished coro=<ConnectorShell.listen() done, defined at /root/.opsdroid/modules/opsdroid-modules/connector/shell/__init__.py:57> exception=ConnectionResetError('Connection lost',)>>
Traceback (most recent call last):
  File "/usr/local/lib/python3.5/asyncio/tasks.py", line 93, in __del__
  File "/usr/local/lib/python3.5/asyncio/futures.py", line 234, in __del__
  File "/usr/local/lib/python3.5/asyncio/base_events.py", line 1290, in call_exception_handler
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1314, in error
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1421, in _log
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1431, in handle
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1493, in callHandlers
  File "/usr/local/lib/python3.5/logging/__init__.py", line 861, in handle
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1053, in emit
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1043, in _open
NameError: name 'open' is not defined
ERROR asyncio: Task was destroyed but it is pending!
task: <Task pending coro=<parse_crontab() running at /usr/src/app/opsdroid/parsers/crontab.py:20> wait_for=<Future pending cb=[Task._wakeup()]>>
ERROR asyncio: Exception in default exception handler
Traceback (most recent call last):
  File "/usr/local/lib/python3.5/asyncio/base_events.py", line 1284, in call_exception_handler
    self.default_exception_handler(context)
  File "/usr/local/lib/python3.5/asyncio/base_events.py", line 1259, in default_exception_handler
    logger.error('\n'.join(log_lines), exc_info=exc_info)
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1314, in error
    self._log(ERROR, msg, args, **kwargs)
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1421, in _log
    self.handle(record)
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1431, in handle
    self.callHandlers(record)
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1493, in callHandlers
    hdlr.handle(record)
  File "/usr/local/lib/python3.5/logging/__init__.py", line 861, in handle
    self.emit(record)
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1053, in emit
    self.stream = self._open()
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1043, in _open
    return open(self.baseFilename, self.mode, encoding=self.encoding)
NameError: name 'open' is not defined
Exception ignored in: <bound method Task.__del__ of <Task pending coro=<parse_crontab() running at /usr/src/app/opsdroid/parsers/crontab.py:20> wait_for=<Future pending cb=[Task._wakeup()]>>>
Traceback (most recent call last):
  File "/usr/local/lib/python3.5/asyncio/tasks.py", line 92, in __del__
  File "/usr/local/lib/python3.5/asyncio/base_events.py", line 1290, in call_exception_handler
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1314, in error
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1421, in _log
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1431, in handle
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1493, in callHandlers
  File "/usr/local/lib/python3.5/logging/__init__.py", line 861, in handle
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1053, in emit
  File "/usr/local/lib/python3.5/logging/__init__.py", line 1043, in _open
NameError: name 'open' is not defined
```

This should be handled, so that opsdroid can exit gracefully without producing any tracebacks.


Discussion:

Resolving patch:
diff --git a/opsdroid/core.py b/opsdroid/core.py
--- a/opsdroid/core.py
+++ b/opsdroid/core.py
@@ -2,6 +2,7 @@
 
 import copy
 import logging
+import signal
 import sys
 import weakref
 import asyncio
@@ -34,6 +35,8 @@ def __init__(self):
         self.connectors = []
         self.connector_tasks = []
         self.eventloop = asyncio.get_event_loop()
+        for sig in (signal.SIGINT, signal.SIGTERM):
+            self.eventloop.add_signal_handler(sig, self.stop)
         self.skills = []
         self.memory = Memory()
         self.loader = Loader(self)
@@ -100,6 +103,8 @@ def stop(self):
         for task in pending:
             task.cancel()
         self.eventloop.stop()
+        print('')  # Prints a character return for return to shell
+        _LOGGER.info("Keyboard interrupt, exiting.")
 
     def load(self):
         """Load configuration."""
@@ -123,10 +128,6 @@ def start_loop(self):
         try:
             pending = asyncio.Task.all_tasks()
             self.eventloop.run_until_complete(asyncio.gather(*pending))
-        except (KeyboardInterrupt, EOFError):
-            print('')  # Prints a character return for return to shell
-            self.stop()
-            _LOGGER.info("Keyboard interrupt, exiting.")
         except RuntimeError as error:
             if str(error) != 'Event loop is closed':
                 raise error


Test patch: