Justcurious
Justcurious

Reputation: 2064

How to make a python script stopable from another script?

TL;DR: If you have a program that should run for an undetermined amount of time, how do you code something to stop it when the user decide it is time? (Without KeyboardInterrupt or killing the task)

--

I've recently posted this question: How to make my code stopable? (Not killing/interrupting) The answers did address my question, but from a termination/interruption point of view, and that's not really what I wanted. (Although, my question didn't made that clear) So, I'm rephrasing it.

I created a generic script for example purposes. So I have this class, that gathers data from a generic API and write the data into a csv. The code is started by typing python main.py on a terminal window.

import time,csv

import GenericAPI

class GenericDataCollector:
    def __init__(self):
        self.generic_api = GenericAPI()
        self.loop_control = True

    def collect_data(self):
        while self.loop_control: #Can this var be changed from outside of the class? (Maybe one solution)
            data = self.generic_api.fetch_data() #Returns a JSON with some data
            self.write_on_csv(data)
            time.sleep(1)

    def write_on_csv(self, data):
        with open('file.csv','wt') as f:
            writer = csv.writer(f)
            writer.writerow(data)

def run():
    obj = GenericDataCollector()
    obj.collect_data()

if __name__ == "__main__":
    run()

The script is supposed to run forever OR until I command it to stop. I know I can just KeyboardInterrupt (Ctrl+C) or abruptly kill the task. That isn't what I'm looking for. I want a "soft" way to tell the script it's time to stop, not only because interruption can be unpredictable, but it's also a harsh way to stop.

If that script was running on a docker container (for example) you wouldn't be able to Ctrl+C unless you happen to be in the terminal/bash inside the docker.

Or another situation: If that script was made for a customer, I don't think it's ok to tell the customer, just use Ctrl+C/kill the task to stop it. Definitely counterintuitive, especially if it's a non tech person.

I'm looking for way to code another script (assuming that's a possible solution) that would change to False the attribute obj.loop_control, finishing the loop once it's completed. Something that could be run by typing on a (different) terminal python stop_script.py.

It doesn't, necessarily, needs to be this way. Other solutions are also acceptable, as long it doesn't involve KeyboardInterrupt or Killing tasks. If I could use a method inside the class, that would be great, as long I can call it from another terminal/script.

Is there a way to do this?

If you have a program that should run for an undetermined amount of time, how do you code something to stop it when the user decide it is time?

Upvotes: 3

Views: 807

Answers (2)

ivissani
ivissani

Reputation: 2664

In general, there are two main ways of doing this (as far as I can see). The first one would be to make your script check some condition that can be modified from outside (like the existence or the content of some file/socket). Or as @Green Cloak Guy stated, using pipes which is one form of interprocess communication.

The second one would be to use the built in mechanism for interprocess communication called signals that exists in every OS where python runs. When the user presses Ctrl+C the terminal sends a specific signal to the process in the foreground. But you can send the same (or another) signal programmatically (i.e. from another script).

Reading the answers to your other question I would say that what is missing to address this one is a way to send the appropriate signal to your already running process. Essentially this can be done by using the os.kill() function. Note that although the function is called 'kill' it can send any signal (not only SIGKILL).

In order for this to work you need to have the process id of the running process. A commonly used way of knowing this is making your script save its process id when it launches into a file stored in a common location. To get the current process id you can use the os.getpid() function.

So summarizing I'd say that the steps to achieve what you want would be:

  1. Modify your current script to store its process id (obtainable by using os.getpid()) into a file in a common location, for example /tmp/myscript.pid. Note that if you want your script to be protable you will need to address this in a way that works in non-unix like OSs like Windows.
  2. Choose one signal (typically SIGINT or SIGSTOP or SIGTERM) and modify your script to register a custom handler using signal.signal() that addresses the graceful termination of your script.
  3. Create another (note that it could be the same script with some command line paramater) script that reads the process id from the known file (aka /tmp/myscript.pid) and sends the chosen signal to that process using os.kill().

Note that an advantage of using signals to achieve this instead of an external way (files, pipes, etc.) is that the user can still press Ctrl+C (if you chose SIGINT) and that will produce the same behavior as the 'stop script' would.

Upvotes: 4

Green Cloak Guy
Green Cloak Guy

Reputation: 24691

What you're really looking for is any way to send a signal from one program to another, independent, program. One way to do this would be to use an inter-process pipe. Python has a module for this (which does, admittedly, seem to require a POSIX-compliant shell, but most major operating systems should provide that).

What you'll have to do is agree on a filepath beforehand between your running-program (let's say main.py) and your stopping-program (let's say stop.sh). Then you might make the main program run until someone inputs something to that pipe:

import pipes
...
t = pipes.Template()
# create a pipe in the first place
t.open("/tmp/pipefile", "w")
# create a lasting pipe to read from that
pipefile = t.open("/tmp/pipefile", "r")
...

And now, inside your program, change your loop condition to "as long as there's no input from this file - unless someone writes something to it, .read() will return an empty string:

while not pipefile.read():
    # do stuff

To stop it, you put another file or script or something that will write to that file. This is easiest to do with a shell script:

#!/usr/bin/env sh
echo STOP >> /tmp/pipefile

which, if you're containerizing this, you could put in /usr/bin and name it stop, give it at least 0111 permissions, and tell your user "to stop the program, just do docker exec containername stop".

(using >> instead of > is important because we just want to append to the pipe, not to overwrite it).


Proof of concept on my python console:

>>> import pipes
>>> t = pipes.Template()
>>> t.open("/tmp/file1", "w")
<_io.TextIOWrapper name='/tmp/file1' mode='w' encoding='UTF-8'>
>>> pipefile = t.open("/tmp/file1", "r")
>>> i = 0
>>> while not pipefile.read():
...     i += 1
... 

At this point I go to a different terminal tab and do

$ echo "Stop" >> /tmp/file1

then I go back to my python tab, and the while loop is no longer executing, so I can check what happened to i while I was gone.

>>> print(i)
1704312

Upvotes: 3

Related Questions