25

I've got a script that runs an infinite loop and adds things to a database and does things that I can't just stop halfway through, so I can't just press Ctrl+C and stop it.

I want to be able to somehow stop a while loop, but let it finish it's last iteration before it stops.

Let me clarify:

My code looks something like this:

while True:
    do something
    do more things
    do more things

I want to be able to interrupt the while loop at the end, or the beginning, but not between doing things because that would be bad.

And I don't want it to ask me after every iteration if I want to continue.


Thanks for the great answers, I'm super grateful but my implementation doesn't seem to be working:

def signal_handler(signal, frame):
    global interrupted
    interrupted = True

class Crawler():
    def __init__(self):
        # not relevant

    def crawl(self):
        interrupted = False
        signal.signal(signal.SIGINT, signal_handler)
        while True:
            doing things
            more things

            if interrupted:
                print("Exiting..")
                break

When I press Ctrl+C the program just keeps going ignoring me.

sat63k
  • 333
  • 1
  • 2
  • 13
davegri
  • 2,206
  • 2
  • 26
  • 45
  • do you have any way to determine in your script that if the job has completed (last one)! If you do then use it in a condition and use a break statement – praba230890 Oct 03 '15 at 13:07
  • The job in this case is a web crawler than can keep going indefinitely. I want to be able to tell it to stop crawling but not just interrupt it in the middle of a page for example. – davegri Oct 03 '15 at 13:08
  • *let it finish it's last iteration before it stops*? How do you know if it has done the job or not? – Remi Guan Oct 03 '15 at 13:09
  • the script crawls web pages. it has a for loop inside an infinite while loop. I want to be able to stop the infinite while loop but not during an iteration of the for loop inside. – davegri Oct 03 '15 at 13:11
  • 1
    Hmm...could you add the important part of your script? This may help others understand what do you want :P – Remi Guan Oct 03 '15 at 13:14
  • 2
    The question is clear: Finish this iteration of the loop, then quit. And who thinks it's off-topic? – alexis Oct 03 '15 at 13:31

6 Answers6

26

What you need to do is catch the interrupt, set a flag saying you were interrupted but then continue working until it's time to check the flag (at the end of each loop). Because python's try-except construct will abandon the current run of the loop, you need to set up a proper signal handler; it'll handle the interrupt but then let python continue where it left off. Here's how:

import signal

import time   # For the demo only

def signal_handler(signal, frame):
    global interrupted
    interrupted = True

signal.signal(signal.SIGINT, signal_handler)


interrupted = False
while True:
    print("Working hard...")
    time.sleep(3)
    print("All done!")

    if interrupted:
        print("Gotta go")
        break

Notes:

  1. Use this from the command line. In the IDLE console, it'll trample on IDLE's own interrupt handling.

  2. A better solution would be to "block" KeyboardInterrupt for the duration of the loop, and unblock it when it's time to poll for interrupts. This is a feature of some Unix flavors but not all, hence python does not support it (see the third "General rule")

  3. The OP wants to do this inside a class. But the interrupt function is invoked by the signal handling system, with two arguments: The signal number and a pointer to the stack frame-- no place for a self argument giving access to the class object. Hence the simplest way to set a flag is to use a global variable. You can rig a pointer to the local context by using closures (i.e., define the signal handler dynamically in __init__(), but frankly I wouldn't bother unless a global is out of the question due to multi-threading or whatever.

Caveat: If your process is in the middle of a system call, handling an signal may interrupt the system call. So this may not be safe for all applications. Safer alternatives would be (a) Instead of relying on signals, use a non-blocking read at the end of each loop iteration (and type input instead of hitting ^C); (b) use threads or interprocess communication to isolate the worker from the signal handling; or (c) do the work of implementing real signal blocking, if you are on an OS that has it. All of them are OS-dependent to some extent, so I'll leave it at that.

Community
  • 1
  • 1
alexis
  • 48,685
  • 16
  • 101
  • 161
  • Ok I think I understand, but my function is running in a class so do I need to define the signal_handler as a function of the class? – davegri Oct 03 '15 at 13:30
  • Signal handlers are called outside the normal flow of control. You don't _need_ to make it a class method, and I'm not sure if it'll even work. Looking in the [signal](https://docs.python.org/3/library/signal.html) documentation... – alexis Oct 03 '15 at 13:37
  • Short answer: Just use a top-level function and a global variable. Unless you're multi-threaded etc., in which case life gets difficult... – alexis Oct 03 '15 at 13:43
  • can you look at my edit? my implementation isn't working, I edited my post to include it now. – davegri Oct 03 '15 at 13:44
  • Easy to spot: `crawl()` is setting, and then checking, a _local_ variable `interrupted` :-) It needs to be global so the interrupt handler can get to it. – alexis Oct 03 '15 at 13:51
11

the below logic will help you do this,

import signal
import sys
import time

run = True

def signal_handler(signal, frame):
    global run
    print("exiting")
    run = False

signal.signal(signal.SIGINT, signal_handler)
while run:
    print("hi")
    time.sleep(1)
    # do anything
    print("bye")

while running this, try pressing CTRL + C

Neuron
  • 5,141
  • 5
  • 38
  • 59
praba230890
  • 2,192
  • 21
  • 37
0

I hope below code would help you:

#!/bin/python

import sys
import time
import signal

def cb_sigint_handler(signum, stack):
    global is_interrupted
    print("SIGINT received")
    is_interrupted = True

if __name__ == "__main__":
    is_interrupted = False
    signal.signal(signal.SIGINT, cb_sigint_handler)
    while True:
        # do stuff here 
        print("processing...")
        time.sleep(3)
        if is_interrupted:
            print("Exiting..")
            # do clean up
            sys.exit(0)
Neuron
  • 5,141
  • 5
  • 38
  • 59
pdave
  • 1
  • 1
0

To clarify @praba230890's solution: The interrupted variable was not defined in the correct scope. It was defined in the crawl function and the handler could not reach it as a global variable, according to the definition of the handler at the root of the program.

Anthony Scemama
  • 1,563
  • 12
  • 19
0

Here is edited example of the principle above. It is the infinitive python loop in a separate thread with the safe signal ending. Also has thread-blocking sleep step - up to you to keep it, replace for asyncio implementation or remove. This function could be imported to any place in an application, runs without blocking other code (e.g. good for REDIS pusub subscription). After the SIGINT catch the thread job ends peacefully.

from typing import Callable
import time
import threading
import signal

end_job = False


def run_in_loop(job: Callable, interval_sec: int = 0.5):
    def interrupt_signal_handler(signal, frame):
        global end_job
        end_job = True

    signal.signal(signal.SIGINT, interrupt_signal_handler)

    def do_job():
        while True:
            job()
            time.sleep(interval_sec)

            if end_job:
                print("Parallel job ending...")
                break

    th = threading.Thread(target=do_job)
    th.start()
Lukas Bares
  • 349
  • 3
  • 12
0

You forgot to add global statement in crawl function. So result will be

import signal

def signal_handler(signal, frame):
    global interrupted
    interrupted = True

class Crawler():
    def __init__(self):
        ... # or pass if you don't want this to do anything. ... Is for unfinished code

    def crawl(self):
        global interrupted
        interrupted = False
        signal.signal(signal.SIGINT, signal_handler)
        while True:
            # doing things
            # more things
            if interrupted:
                print("Exiting..")
                break
UltraStudioLTD
  • 300
  • 2
  • 14