Repository: ipython/ipython

Issue:
Security file is not removed after shutdown by Ctrl+C or kill -INT
I am using IPython 0.12 (Ubuntu 11.10, Python 2.7.2+).
This is how to reproduce. I just open one new notebook after starting the server.

```
% ipython profile create test
[ProfileCreate] Generating default config file: u'/home/takafumi/.config/ipython/p
rofile_test/ipython_config.py'
[ProfileCreate] Generating default config file: u'/home/takafumi/.config/ipython/p
rofile_test/ipython_qtconsole_config.py'
[ProfileCreate] Generating default config file: u'/home/takafumi/.config/ipython/p
rofile_test/ipython_notebook_config.py'
% ipython notebook --profile=test
[NotebookApp] Using existing profile dir: u'/home/takafumi/.config/ipython/profile_test'
[NotebookApp] The IPython Notebook is running at: http://127.0.0.1:8888
[NotebookApp] Use Control-C to stop this server and shut down all kernels.
Created new window in existing browser session.
[NotebookApp] Using MathJax from CDN
[NotebookApp] Kernel started: 5946f2c5-4a2f-434f-8962-ef379d7969a0
[NotebookApp] Connecting to: tcp://127.0.0.1:42572
[NotebookApp] Connecting to: tcp://127.0.0.1:39724
[NotebookApp] Connecting to: tcp://127.0.0.1:54142
[IPKernelApp] To connect another client to this kernel, use:
[IPKernelApp] --existing kernel-5946f2c5-4a2f-434f-8962-ef379d7969a0.json --profile test
^C
% ls ~/.config/ipython/profile_test/security/
kernel-5946f2c5-4a2f-434f-8962-ef379d7969a0.json
```

BTW, is it safe to do `rm ~/.config/ipython/profile_*/security/*` when there is no IPython instance running?



Discussion:
OK, I found that KeyboardInterrupt is treated in the master branch.  But it does not treat shutdown by kill, right?

> OK, I found that KeyboardInterrupt is treated in the master branch. But it does not treat shutdown by kill, right?

Yes, master does a much better job cleaning up after itself, but that does not include kill (that's kind of the point of Kill - you can't handle it).

> is it safe to do rm `~/.config/ipython/profile_*/security/*` when there is no IPython instance running?

Yes, this is always safe.  In fact, I've been meaning to add a simple command `ipython profile clean`, which would remove everything in the log/security/pid directories, because they can grow.

Cleaning command sounds nice!

You can handle kill by doing something with the signal module: http://danielkaes.wordpress.com/2009/06/04/how-to-catch-kill-events-with-python/
I suggest to handle SIGTERM.

TERM is not KILL - you cannot catch KILL, but we might as well catch TERM as well as INT.

Sorry, I mean kill command (without option, resulting in TERM).  Yes, it would be much better if IPython can handle it.


Resolving patch:
diff --git a/IPython/frontend/html/notebook/notebookapp.py b/IPython/frontend/html/notebook/notebookapp.py
--- a/IPython/frontend/html/notebook/notebookapp.py
+++ b/IPython/frontend/html/notebook/notebookapp.py
@@ -20,10 +20,13 @@
 import errno
 import logging
 import os
+import re
+import select
 import signal
 import socket
 import sys
 import threading
+import time
 import webbrowser
 
 # Third party
@@ -449,11 +452,73 @@ def init_webapp(self):
                 self.port = port
                 break
     
+    def init_signal(self):
+        # FIXME: remove this check when pyzmq dependency is >= 2.1.11
+        # safely extract zmq version info:
+        try:
+            zmq_v = zmq.pyzmq_version_info()
+        except AttributeError:
+            zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
+            if 'dev' in zmq.__version__:
+                zmq_v.append(999)
+            zmq_v = tuple(zmq_v)
+        if zmq_v >= (2,1,9):
+            # This won't work with 2.1.7 and
+            # 2.1.9-10 will log ugly 'Interrupted system call' messages,
+            # but it will work
+            signal.signal(signal.SIGINT, self._handle_sigint)
+        signal.signal(signal.SIGTERM, self._signal_stop)
+    
+    def _handle_sigint(self, sig, frame):
+        """SIGINT handler spawns confirmation dialog"""
+        # register more forceful signal handler for ^C^C case
+        signal.signal(signal.SIGINT, self._signal_stop)
+        # request confirmation dialog in bg thread, to avoid
+        # blocking the App
+        thread = threading.Thread(target=self._confirm_exit)
+        thread.daemon = True
+        thread.start()
+    
+    def _restore_sigint_handler(self):
+        """callback for restoring original SIGINT handler"""
+        signal.signal(signal.SIGINT, self._handle_sigint)
+    
+    def _confirm_exit(self):
+        """confirm shutdown on ^C
+        
+        A second ^C, or answering 'y' within 5s will cause shutdown,
+        otherwise original SIGINT handler will be restored.
+        """
+        # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
+        time.sleep(0.1)
+        sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
+        sys.stdout.flush()
+        r,w,x = select.select([sys.stdin], [], [], 5)
+        if r:
+            line = sys.stdin.readline()
+            if line.lower().startswith('y'):
+                self.log.critical("Shutdown confirmed")
+                ioloop.IOLoop.instance().stop()
+                return
+        else:
+            print "No answer for 5s:",
+        print "resuming operation..."
+        # no answer, or answer is no:
+        # set it back to original SIGINT handler
+        # use IOLoop.add_callback because signal.signal must be called
+        # from main thread
+        ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
+    
+    def _signal_stop(self, sig, frame):
+        self.log.critical("received signal %s, stopping", sig)
+        ioloop.IOLoop.instance().stop()
+    
     @catch_config_error
     def initialize(self, argv=None):
         super(NotebookApp, self).initialize(argv)
         self.init_configurables()
         self.init_webapp()
+        self.init_signal()
 
     def cleanup_kernels(self):
         """shutdown all kernels


Test patch: