0

In this script I was looking to launch a given program and monitor it as long as the program exists. Thus, I reached the point where I got to use the threading's module Timer method for controlling a loop that writes to a file and prints out to the console a specific stat of the launched process (for this case, mspaint).

The problem arises when I'm hitting CTRL + C in the console or when I close mspaint, with the script capturing any of the 2 events only after the time defined for the interval has completely ran out. These events make the script stop.

For example, if a 20 seconds time is set for the interval, once the script has started, if at second 5 I either hit CTRL + C or close mspaint, the script will stop only after the remaining 15 seconds will have passed.

I would like for the script to stop right away when I either hit CTRL + C or close mspaint (or any other process launched through this script).

The script can be used with the following command, according to the example: python.exe mon_tool.py -p "C:\Windows\System32\mspaint.exe" -i 20

I'd really appreciate if you could come up with a working example.

I had used python 3.10.4 and psutil 5.9.0 .

This is the code:

# mon_tool.py

import psutil, sys, os, argparse
from subprocess import Popen
from threading import Timer
debug = False


def parse_args(args):   
    parser = argparse.ArgumentParser()
    parser.add_argument("-p", "--path", type=str, required=True)
    parser.add_argument("-i", "--interval", type=float, required=True)
    return parser.parse_args(args)

def exceptionHandler(exception_type, exception, traceback, debug_hook=sys.excepthook):
    '''Print user friendly error messages normally, full traceback if DEBUG on.
       Adapted from http://stackoverflow.com/questions/27674602/hide-traceback-unless-a-debug-flag-is-set
    '''
    if debug:
        print('\n*** Error:')
        debug_hook(exception_type, exception, traceback)
    else:
        print("%s: %s" % (exception_type.__name__, exception))
sys.excepthook = exceptionHandler
      
def validate(data):
    try:
        if data.interval < 0:            
            raise ValueError
    except ValueError:        
        raise ValueError(f"Time has a negative value: {data.interval}. Please use a positive value")
def main():
    args = parse_args(sys.argv[1:])
    validate(args)


    # creates the "Process monitor data" folder in the "Documents" folder
    # of the current Windows profile
    default_path: str = f"{os.path.expanduser('~')}\\Documents\Process monitor data"
    if not os.path.exists(default_path):
        os.makedirs(default_path)  

    abs_path: str = f'{default_path}\data_test.txt'

    print("data_test.txt can be found in: " + default_path)


    # launches the provided process for the path argument, and
    # it checks if the process was indeed launched
    p: Popen[bytes] = Popen(args.path)
    PID = p.pid    
    isProcess: bool = True
    while isProcess:
        for proc in psutil.process_iter():
            if(proc.pid == PID):
                isProcess = False

    process_stats = psutil.Process(PID)

    # creates the data_test.txt and it erases its content
    with open(abs_path, 'w', newline='', encoding='utf-8') as testfile:
            testfile.write("")
             

    # loop for writing the handles count to data_test.txt, and
    # for printing out the handles count to the console
    def process_monitor_loop():      
        with open(abs_path, 'a', newline='', encoding='utf-8') as testfile:
            testfile.write(f"{process_stats.num_handles()}\n")
            print(process_stats.num_handles())
        Timer(args.interval, process_monitor_loop).start() 
    process_monitor_loop()
                      

if __name__ == '__main__':
    main()

Thank you!

rd51
  • 252
  • 4
  • 11

3 Answers3

1

You can try registering a signal handler for SIGINT, that way whenever the user presses Ctrl+C you can have a custom handler to clean all of your dependencies, like the interval, and exit gracefully. See this for a simple implementation.

Agus Neira
  • 435
  • 4
  • 9
  • Thank you! I got so far this working example: https://pastebin.com/6dQwThLZ , where the loop runs every 10 seconds. But, unfortunately, this solution only works for Unix systems. I'll keep looking for a windows solution. – rd51 May 31 '22 at 12:18
  • 1
    Maybe add a second handler for CTRL_C_EVENT? [Check this link](https://docs.python.org/3/library/signal.html#signal.CTRL_C_EVENT). I think it wouldn't be necessary to check the OS, the uncompatible signal would just not trigger. – Agus Neira May 31 '22 at 12:29
1

I think you could use python-worker (link) for the alternatives

import time
from datetime import datetime
from worker import worker, enableKeyboardInterrupt

# make sure to execute this before running the worker to enable keyboard interrupt
enableKeyboardInterrupt()


# your codes
...


# block lines with periodic check
def block_next_lines(duration):
    t0 = time.time()
    while time.time() - t0 <= duration:
        time.sleep(0.05) # to reduce resource consumption


def main():
    # your codes
    ...

    @worker(keyboard_interrupt=True)
    def process_monitor_loop():
        while True:
            print("hii", datetime.now().isoformat())
            block_next_lines(3)
    
    return process_monitor_loop()

if __name__ == '__main__':
    main_worker = main()
    main_worker.wait()

here your process_monitor_loop will be able to stop even if it's not exactly 20 sec of interval

danangjoyoo
  • 350
  • 1
  • 6
  • Thanks! Can you please check out this code that I had modified based on your suggestion: https://pastebin.com/ThWtgx4H , maybe it's something that I did wrong? I got to run the code, but I still can't stop the script right away. – rd51 May 31 '22 at 15:28
  • 1
    ahh thanks for the feedback, I've updated my code above right away. Can you please check it? – danangjoyoo Jun 01 '22 at 00:28
  • Thanks for the code! Unfortunately it still doesn't work :( I recorded a video to see how it works for me, using a simplified version: https://imgur.com/a/YwMPuJu . I hit CTRL + C in the console at 07:49:43 and the script only stopped at 07:50:12, taking it 29 seconds to stop. The time I used for the interval was 20 seconds. Also, "worker" shows with a squiggly line where it says "from worker", saying "Import "worker" could not be resolved", could that be the problem? I get the same kind of error with the "psutil" module, but it still works. – rd51 Jun 01 '22 at 05:05
  • 1
    btw I just achieved the goal that you want by declaring the `@worker` and `process_main_loop()` outside the `main()`. I just updated my code. Please check it, I hope you got the result that I got. For the `from worker import worker` resolve issue, are you already installed on your interpreter Sir? Its fine in my side. – danangjoyoo Jun 01 '22 at 15:03
  • Thank you! It still doesn't work :( Yes, I have installed the worker module using "pip install python-worker" and its version is 1.10.1 . I had also tested the code on a different computer, but it still doesn't work. I'm using Windows 10, more specifically: 101.0.1210.53 (Official build) (64-bit) . The other computer is on the latest updates of Windows, unlike mine. What operating system are you using? – rd51 Jun 01 '22 at 17:40
  • 1
    I'm using windows 10. `python-worker==1.10.1` , `python 3.8.10`. What python version do you have? – danangjoyoo Jun 02 '22 at 00:17
  • I'm using python 3.10.4 . But now I have tried the script with 3.8.10, as well, but it still doesn't work. I really have no idea what I could do next. I ran the script within Visual Studio Code terminal and then I ran it in cmd as Administrator, but still nothing. – rd51 Jun 02 '22 at 06:09
  • Would you mind recording a very short video to see how it works for you, using this script: https://pastebin.com/LBygz7LP? – rd51 Jun 02 '22 at 06:21
  • 1
    Hey I've just added `main_worker.wait()` at the code above and you can see it in here https://imgur.com/a/B5uGT9p, it works perfectly fine. Btw thanks for giving me some insight from this thread. The previous code didn't work because we are running out of lines after `main()` and the interpreter is already exiting the main thread, thats why we can't stop it. – danangjoyoo Jun 02 '22 at 14:01
  • 1
    and you have to change it into `while loop` because we if you want to do recursive scenario (like before), you have to add a state checker to block the interpreter from exiting. – danangjoyoo Jun 02 '22 at 14:05
  • Thank you very much! This time it has worked for me! You are awesome! :D Today I was really busy and I didn't have time to test this with my real code. In case any problems come up during testing, can I let you know? – rd51 Jun 02 '22 at 20:22
  • Also, can I use your code below to post it as an answer in case I figure out how to implement immediate script stopping once the launched program stopped, I already go an idea? Don't worry, your solution will still have the mark :P – rd51 Jun 02 '22 at 20:27
  • 1
    why you have to repost it ? – danangjoyoo Jun 02 '22 at 23:17
  • I wanted to add my solution on top of your solution as a separate answer to this thread so that if a beginner programmer like me come across this thread it will be much easier for them to understand what we have done. They will see your solution as dealing with part 1 of the problem, where you have solved the CTRL + C problem. While my solution, part 2, deals with checking every second if the launched program exists. – rd51 Jun 03 '22 at 09:10
  • I implemented my solution over here: https://pastebin.com/U6tBgEDd , where, as I described above, the sub_process function checks every second if mspaint exists, if it doesn't exist, it will stop the script. Now, a new problem came up, if I hit CTRL + C, only the thread created by you will stop, while mine keeps running. Can you please see how I can stop both threads when hitting CTRL + C? – rd51 Jun 03 '22 at 09:17
  • 1
    ohh okay no probs – danangjoyoo Jun 03 '22 at 10:37
  • I solved the problem I had above. One last thing, could you tell me please if there's a better way of getting the value of "w.is_alive" than what I used on lines 61 and 62 for this code: https://pastebin.com/aeedxgX7? – rd51 Jun 03 '22 at 12:44
  • 1
    I think it's already the fastest execution for checking `is_alive` – danangjoyoo Jun 03 '22 at 23:12
  • 1
    you can do `if not all([w.is_alive for w in ThreadWorkerManager.allWorkers.values()])` but this code will check the whole registered worker – danangjoyoo Jun 03 '22 at 23:13
  • Thank you! Then I'll stick to ```is_alive```. – rd51 Jun 04 '22 at 18:36
0

This is the solution for the second part of the problem, which checks if the launched process exists. If it doesn't exist, it stops the script.

This solution comes on top of the solution, for the first part of the problem, provided above by @danangjoyoo, which deals with stopping the script when CTRL + C is used.

Thank you very much once again, @danangjoyoo! :)

This is the code for the second part of the problem:

import time, psutil, sys, os
from datetime import datetime
from worker import worker, enableKeyboardInterrupt, abort_all_thread, ThreadWorkerManager
from threading import Timer

# make sure to execute this before running the worker to enable keyboard interrupt
enableKeyboardInterrupt()


# block lines with periodic check
def block_next_lines(duration):
    t0 = time.time()
    while time.time() - t0 <= duration:
        time.sleep(0.05) # to reduce resource consumption


def main():

    # launches mspaint, gets its PID and checks if it was indeed launched
    path = f"C:\Windows\System32\mspaint.exe"
    p = psutil.Popen(path)
    PID = p.pid    
    isProcess: bool = True
    while isProcess:
        for proc in psutil.process_iter():
            if(proc.pid == PID):
                isProcess = False

    interval = 5
    global counter
    counter = 0

    #allows for sub_process to run only once
    global run_sub_process_once
    run_sub_process_once = 1

    @worker(keyboard_interrupt=True)
    def process_monitor_loop():
        while True:
            print("hii", datetime.now().isoformat())


            def sub_proccess():
                '''
                Checks every second if the launched process still exists.
                If the process doesn't exist anymore, the script will be stopped.
                '''

                print("Process online:", psutil.pid_exists(PID))
                t = Timer(1, sub_proccess)
                t.start()
                global counter
                counter += 1 
                print(counter)

                # Checks if the worker thread is alive.
                # If it is not alive, it will kill the thread spawned by sub_process
                # hence, stopping the script.
                for _, key in enumerate(ThreadWorkerManager.allWorkers):
                    w = ThreadWorkerManager.allWorkers[key]
                    if not w.is_alive:
                        t.cancel()

                if not psutil.pid_exists(PID):
                    abort_all_thread()
                    t.cancel()

            global run_sub_process_once
            if run_sub_process_once:
                run_sub_process_once = 0
                sub_proccess()

            block_next_lines(interval)
    
    return process_monitor_loop()

if __name__ == '__main__':
    main_worker = main()
    main_worker.wait()

Also, I have to note that @danangjoyoo's solution comes as an alternative to signal.pause() for Windows. This only deals with CTRL + C problem part. signal.pause() works only for Unix systems. This is how it was supposed for its usage, for my case, in case it were a Unix system:

import signal, sys
from threading import Timer
 
def main():
    def signal_handler(sig, frame):
        print('\nYou pressed Ctrl+C!')
        sys.exit(0)
 
    signal.signal(signal.SIGINT, signal_handler)
    print('Press Ctrl+C')
    def process_monitor_loop():      
        try:
            print("hi")
        except KeyboardInterrupt:    
            signal.pause()
        Timer(10, process_monitor_loop).start() 
    process_monitor_loop()
 
 
if __name__ == '__main__':
    main()

The code above is based on this.

rd51
  • 252
  • 4
  • 11