[Python 3] Cum pot declansa oprirea scriptului fara a astepta scurgerea timpului declarat in interval?

Salut din nou! Sper sa fie ultima data cand va “supar” cu probleme legate de acest script, care este o urmare al acestui thread.

In scriptul de fata am urmarit sa lansez un program pe care sa-l monitorizeze atata vreme cat programul exista. Asadar, am ajuns in momentul in care am folosit un Timer care controleaza o bucla ce scrie unui fiser si afiseaza consolei valoarea unui atribut al unui proces lansat (in cazul de fata mspaint).

Problema este ca atunci cand apas CTRL + C in consola sau daca inchid mspaint, scriptul python preia aceste evenimente doar dupa ce timpul definit in interval s-a scurs, evenimente care determina oprirea scriptului.

Spre exemplu, daca este definit timpul de 20 de secunde pentru interval, iar odata ce scriptul este pornit, daca la secunda 5 apas fie CTRL + C in consola, fie inchid mspaint, scriptul python va fi oprit doar dupa ce au trecut cele 15 secunde ramase.

Ce mi-as fi dorit este ca scriptul python sa fie oprit imediat atunci cand folosesc CTRL + C sau cand inchid mspaint. Dupa cateva ore de cautare n-am ajuns la nicio solutie, din pacate.

Scriptul poate fi folosit cu urmatoarea comanda, potrivit exemplului:
python.exe mon_tool.py -c "C:\Windows\System32\mspaint.exe" -i 20

Am folosit:
Python: 3.10.4
psutil: 5.9.0
argparse: 1.1

Acesta este codul:

# mon_tool.py

import psutil, sys, os, argparse
from subprocess import Popen
from threading import Timer


def parse_args(args):   
    parser = argparse.ArgumentParser()
    parser.add_argument("-c", "--cale", type=str, metavar=" ", required=True)
    parser.add_argument("-i", "--interval", type=float, metavar=" ", required=True)
    return parser.parse_args(args)
      
def validate(data):
    if data.interval < 0:            
        raise ValueError(f"Timpul are o valoare negativa: {data.interval}. Te rog introdu o valoare pozitiva")

def main():
    args = parse_args(sys.argv[1:])
    validate(args)

    try:
        validate(args)
    except ValueError as e:
        print(f"Some invalid data -- {e}")
        sys.exit(1)
  
    # creeaza directorul Process monitor data in directorul Documents al
    # prezentului profil Windows
    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("Fisierul data_test.txt se afla in: " + default_path)

    # lanseaza procesul provenit de la argumentul path, dupa care verfica daca
    # a fost intr-adevar lansat
    p: Popen[bytes] = Popen(args.cale)
    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)

    # creeaza si apoi sterge continutul fisierului
    with open(abs_path, 'w', newline='', encoding='utf-8') as testfile:
            testfile.write("")
             
    # bucla pentru a scrie numarul handle-urilor catre data_test.txt
    # si pentru a afisa in consola acelasi numar
    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()    

Va multumesc!

Am reusit sa gasesc o solutie pentru CTRL + C, insa, din pacate functioneaza doar pe sisteme Unix, incat signal.pause() nu exista pentru Windows.

Asa arata un exemplu de cod:

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

Incerc sa gasesc o varianta pentru Windows.

Am impresia că te-ai complicat îngrozitor. De ce ai folosit timerul în loc să faci pur și simplu o buclă infinită cu un sleep() în ea?

LE Cred că ce cauți este o soluție bazată pe asyncio, pentru că ai nevoie să rulezi două chestii care se blochează reciproc.

#!/usr/bin/python3

import asyncio

pid = None


async def start_process():
    global pid
    print("entering start_process()...")

    proc = await asyncio.create_subprocess_shell("sleep 5")
    pid = proc.pid
    await proc.communicate()


async def write_stats():
    global pid
    print("entering write_stats()...")

    while True:
        print(f"write_stats(pid={pid})")
        await asyncio.sleep(1)


async def main():
    task = asyncio.create_task(write_stats())
    await start_process()
    task.cancel()
    
try:
    asyncio.run(main())
except KeyboardInterrupt:
    pass
2 Likes

Iti multumesc foarte mult!

Motivul pentru care n-am folosit o buclă infinită cu un sleep() în ea este pentru ca sunt incepator. Google-ul mi-a dat solutia cu Timer-ul cand am cautat, si am ales-o pe asta din moment ce a functionat partial.

Pentru cei incepatori, ca mine, ca scriptul de sus sa functioneze inlocuiti:

proc = await asyncio.create_subprocess_shell("sleep 5")

cu

proc = await asyncio.create_subprocess_shell("C:\Windows\System32\mspaint.exe")

Pana sa ajung astazi sa vad solutia ta, am ajuns la o alta solutie intre timp, bineinteles, nu pe cont propriu pe deplin:

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()

Solutia ta este cu pe departe mai eleganta. :slight_smile:

Salutare! Am intampinat o problema legata de codul tau, PID-ul pe care-l obtin nu este al procesului lansat.

Spre exemplu, daca lansez mspaint, PID-ul afisat de catre cod este total diferit.

Ai/aveti o idee cum as putea sa obtin PID-ul procesului lansat (mspaint in acest caz)?

Foloseste asyncio.create_subprocess_exec in loc de asyncio.create_subprocess_shell, si pui direct “mspaint” in loc de “C:\Windows\System32\mspaint.exe”.

Dupaia compari PID-ul cu ce iti returneaza PowerShell:

Get-Process mspaint
1 Like

Iti multumesc, a functionat! :slight_smile:

Am intampinat o problema legata de asyncio.create_subprocess_exec. Spre deosebire de asyncio.create_subprocess_shell, asyncio.create_subprocess_exec opreste procesul lansat cand este apasat CTRL + C.

Este ceva ce as putea face in acest sens, incat as dori sa ca programul lansat sa continue sa ruleze, chiar daca CTRL + C este apasat?

Vezi ce efect are dacă la create_subprocess_exe adaugi flag-ul DETACHED_PROCESS.

2 Likes

Am citit postarea aceasta pentru a vedea cum sa folosesc DETACHED_PROCESS, insa nu sunt sigur daca asa ai intentionat sa o folosesc.

Acesta este codul, care, din pacate, inca opreste programul lansat, chiar daca am folosit DETACHED_PROCESS:

#!/usr/bin/python3

import asyncio

pid = None


async def start_process():
    global pid
    print("entering start_process()...")
    DETACHED_PROCESS = 0x00000008
    proc = await asyncio.create_subprocess_exec("mspaint.exe", creationflags=DETACHED_PROCESS)
    pid = proc.pid
    await proc.communicate()
    


async def write_stats():
    global pid
    print("entering write_stats()...")

    while True:
        print(f"write_stats(pid={pid})")
        await asyncio.sleep(1)


async def main():
    task = asyncio.create_task(write_stats())
    await start_process()
    task.cancel()
    
try:
    asyncio.run(main())
except KeyboardInterrupt:
    print("Ai apasat CTRL + C!")
await asyncio.create_subprocess_exec("mspaint.exe", stdin=asyncio.subprocess.PIPE, preexec_fn=None)

Pentru Linux:

preexec_fn=os.setsid

Pentru Windows:

preexec_fn=None
1 Like

Am senzația că de fapt versiunea async a lui create_subprocess_exec nu are argumentul creationflags, eu mă uitasem la ăla normal.

LE: am testat pe Linux varianta sugerată de @zhare, pare să fie ce trebuie, Ctrl+C nu omoară și procesul-copil.

2 Likes

Va multumesc amandurora! A functionat! :smiley:

Aceeasi eroare o primiti si voi la rulare pe Windows?
Din ce am experimentat, pare sa aibe legatura cu stdin=asyncio.subprocess.PIPE .

Exception ignored in: <function BaseSubprocessTransport.__del__ at 0x000001D1E1166200>
Traceback (most recent call last):
  File "C:\Users\tester\AppData\Local\Programs\Python\Python310\lib\asyncio\base_subprocess.py", line 126, in __del__
    self.close()
  File "C:\Users\tester\AppData\Local\Programs\Python\Python310\lib\asyncio\base_subprocess.py", line 104, in close  
    proto.pipe.close()
  File "C:\Users\tester\AppData\Local\Programs\Python\Python310\lib\asyncio\proactor_events.py", line 108, in close
    self._loop.call_soon(self._call_connection_lost, None)
  File "C:\Users\tester\AppData\Local\Programs\Python\Python310\lib\asyncio\base_events.py", line 750, in call_soon
    self._check_closed()
  File "C:\Users\tester\AppData\Local\Programs\Python\Python310\lib\asyncio\base_events.py", line 515, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed
Exception ignored in: <function _ProactorBasePipeTransport.__del__ at 0x000001D1E1167C70>
Traceback (most recent call last):
  File "C:\Users\tester\AppData\Local\Programs\Python\Python310\lib\asyncio\proactor_events.py", line 115, in __del__
    _warn(f"unclosed transport {self!r}", ResourceWarning, source=self)
  File "C:\Users\tester\AppData\Local\Programs\Python\Python310\lib\asyncio\proactor_events.py", line 79, in __repr__
    info.append(f'fd={self._sock.fileno()}')
  File "C:\Users\tester\AppData\Local\Programs\Python\Python310\lib\asyncio\windows_utils.py", line 102, in fileno
    raise ValueError("I/O operation on closed pipe")
ValueError: I/O operation on closed pipe

Inlocuieste

asyncio.run(main())

cu

asyncio.get_event_loop().run_until_complete(main())
1 Like

Iti multumesc din nou! A functionat! :slight_smile: