2

I'm using Python version 2.7.9 on Windows 8.1 Enterprise 64-bit.

(Python 2.7.9 (default, Dec 10 2014, 12:28:03) [MSC v.1500 64 bit (AMD64)] on win32)

So I am writing a python IRC bot and everything works within the script.

The issue that I am having is if I send a KeyboardInterrupt, the script in the console window shows that it is still running UNTIL the bot receives data.

Situation:

Execute script to connect to IRC server

Logs onto the server no problem

In console window, I send a CTRL + C

Console window hangs making it seem like script is running

Send bot a query message / message is sent to the channel

Console shows interrupt and exit message I designated in the exception

Shouldn't the script immediately quit once a CTRL + C is sent to the console? I do have a part in the script to make it close gracefully if I send a quit message, but this part bothers me.

Here is my code where I believe it may have issues:

def main(NETWORK, NICK, CHAN, PORT):
   flag = True
   readbuffer = ""
   global CURRENTCHANNELS
   global MAXCHANNELS

   s.connect((NETWORK,PORT))
   s.send("NICK %s\r\n" % NICK)
   s.send("USER %s %s bla :%s\r\n" % (IDENTD, NETWORK, REALNAME))

   while(flag):
       try:
           readbuffer = readbuffer + s.recv(4096)
       except KeyboardInterrupt:
           print "Interrupt received"
       finally:
            s.close()
falconspy
  • 750
  • 3
  • 8
  • 22
  • Which Windows version? As [`signal`](https://docs.python.org/2/library/signal.html#execution-of-python-signal-handlers) explains, when Python receives a signal, it just stashes it away to deal with after the current bytecode finishes. If that bytecode is a call to the C function `socket.recv`, that won't finish until there's data. Of course on most *nix platforms in most scenarios, the signal will interrupt the `recv`, so that isn't a problem, but you're not on *nix, you're on Windows. And that means digging into what Python's Windows code and Windows' WinSock2 code do on Ctrl-C here… – abarnert May 20 '15 at 22:57
  • I'm using Python version 2.7.9 on Windows. I am not doing any kind of threading. The bot just sits and waits for data from a user or channel – falconspy May 20 '15 at 22:58
  • Yes, I see "Windows", but which Windows version? (I tried to rewrite my comment to explain why that might matter.) – abarnert May 20 '15 at 22:59
  • Python 2.7.9 (default, Dec 10 2014, 12:28:03) [MSC v.1500 64 bit (AMD64)] on win32 – falconspy May 20 '15 at 22:59
  • That's which _Python_ version, not which _WIndows_ versions. – abarnert May 20 '15 at 23:00
  • Also, does Ctrl-Break work, or does it have the same problem as Ctrl-C? – abarnert May 20 '15 at 23:00
  • Woops sorry, Windows 8.1 Enterprise 64-bit. Ctrl + Break has the same problem – falconspy May 20 '15 at 23:01

1 Answers1

2

On Windows, with Python 2.x, Ctrl-C generally does not interrupt socket calls.

In some cases, Ctrl-Break works. If it does, and if that's good enough for you, you're done.

But if Ctrl-Break doesn't work, or if that isn't acceptable as a workaround, the only option is to set your own console ctrl-key handler, with [SetConsoleControlHandler]( https://msdn.microsoft.com/en-us/library/windows/desktop/ms686016%28v=vs.85%29.aspx).

There's a good discussion of this on the PyZMQ issue tracker, including a link to some sample code for working around it.

The code could be simpler if you can use the win32api module from PyWin32, but assuming you can't, I think this is the code you want:

from ctypes import WINFUNCTYPE, windll
from ctypes.wintypes import BOOL, DWORD

kernel32 = windll.LoadLibrary('kernel32')
PHANDLER_ROUTINE = WINFUNCTYPE(BOOL, DWORD)
SetConsoleCtrlHandler = kernel32.SetConsoleCtrlHandler
SetConsoleCtrlHandler.argtypes = (PHANDLER_ROUTINE, BOOL)
SetConsoleCtrlHandler.restype = BOOL
CTRL_C_EVENT = 0
CTRL_BREAK_EVENT = 1

@PHANDLER_ROUTINE
def console_handler(ctrl_type):
    if ctrl_type in (CTRL_C_EVENT, CTRL_BREAK_EVENT):
        # do something here
        return True
    return False

if __name__ == '__main__':
    if not SetConsoleCtrlHandler(console_handler, True):
        raise RuntimeError('SetConsoleCtrlHandler failed.')

The question is what to put in the # do something here. If there were an easy answer, Python would already be doing it. :)

According to the docs, HandlerRoutine functions actually run on a different thread. And, IIRC, closing the socket out from under the main thread will always cause its recv to wake up and raise an exception. (Not the one you want, but still, something you can handle.)

However, I can't find documentation to prove that—it's definitely not recommended (and WinSock2 seems to officially allow the close to fail with WSAEINPROGRESS, even if no Microsoft implementation of WinSock2 actually does that…), but then you're just trying to bail out and quit here.

So, I believe just defining and installing the handler inside main so you can write s.close() as the # do something here will work.


If you want to do this in a way that's guaranteed to be safe and effective, even with some weird WinSock2 implementation you've never heard of being, what you need to do is a bit more complicated. You need to make the recv asynchronous, and use either Windows async I/O (which is very painful from Python 2.x, unless you use a heavy-duty library like Twisted), or write cross-platform select-based code (which isn't very Windows-y, but works—as long as your use an extra socket instead of the usual Unix solution of a pipe). Like this (untested!) example:

# ctypes setup code from above

def main():
    extrasock = socket.socket(socket.SOCK_DGRAM)
    extrasock.bind(('127.0.0.1', 0))
    extrasock.setblocking(False)

    @PHANDLER_ROUTINE
    def console_handler(ctrl_type):
        if ctrl_type in (CTRL_C_EVENT, CTRL_BREAK_EVENT):
            killsock = socket.socket(socket.SOCK_DGRAM)
            killsock.sendto('DIE', extrasock.getsockname())
            killsock.close()
            return True
        return False

    if not SetConsoleCtrlHandler(console_handler, True):
        raise RuntimeError('SetConsoleCtrlHandler failed.')

    # your existing main code here, up to the try
    # except that you need s.setblocking(False) too
    try:
        r, _, _ = select.select([s, extrasock], [], [])
        if extrasock in r:
            raise KeyboardInterrupt
        if s in r:
            readbuffer = readbuffer + s.recv(4096)
    except KeyboardInterrupt:
    # rest of your code here

If this makes no sense to you, the Sockets HOWTO actually has a pretty good explanation of how to use select, and the pitfalls of using it on Windows.

abarnert
  • 354,177
  • 51
  • 601
  • 671