Hauptprogramm stoppt nicht trotz Stoppsignal

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Mondscheinsonate
User
Beiträge: 1
Registriert: Montag 2. Dezember 2024, 17:42

Hallo zusammen,

ich habe ein lange dauerndes Programm geschrieben, das u.a. Screenshots macht. Deshalb wäre es wichtig, dass das Programm stoppt, falls das niedriger-Akkustand-Fenster kommt:

Code: Alles auswählen

# main_script.py

import time
import threading
from queue import Queue, Empty
from threading import Event
import random
from concurrent.futures import ThreadPoolExecutor
from battery_monitor import monitor_low_battery_window, stop_event

directory = "C:/Users/User/pictures"

def process_files_with_work_stealing(
    main_queue: Queue,
    steal_queue: Queue,
    stop_signal: Event
) -> None:
    """Processes files using a work-stealing approach, distributing tasks between queues."""
    while not stop_signal.is_set():
        task = None

        try:
            task = main_queue.get_nowait()
        except Empty:
            # If the main queue is empty, try to steal a task from the other queue
            try:
                task = steal_queue.get_nowait()
            except Empty:
                # Both queues are empty, briefly wait before retrying
                time.sleep(0.1)
                continue

        # Process file...
        pass
    
def main() -> None:
    queue_a = Queue()
    queue_b = Queue()
    stop_signal = Event()

    try:
        with ThreadPoolExecutor(max_workers=2) as executor:
            for folder in directory.iterdir():
                if folder.is_dir():
                    for file in folder.glob("*.png"):
                        if file.is_file():
                            if random.random() < 0.5:
                                queue_a.put(file)
                            else:
                                queue_b.put(file)

                    executor.submit(process_files_with_work_stealing, queue_a, queue_b, stop_signal)
                    executor.submit(process_files_with_work_stealing, queue_b, queue_a, stop_signal)

            # Wait until all files are processed (queues are empty)
            while not queue_a.empty() or not queue_b.empty():
                time.sleep(1)

            stop_signal.set()
    except KeyboardInterrupt:
        print("\nProgram interrupted by the user.")
    finally:
        stop_event.set()  # Signals the monitor thread to exit
        monitor_thread.join()  # Waits for the thread to be closed
        print("Main thread completed. Monitor thread joined.")

if __name__ == '__main__':
    monitor_thread = threading.Thread(target=monitor_low_battery_window)
    monitor_thread.start()

    try:
        main()
    except KeyboardInterrupt:
        print("\nMain script interrupted and stopped.")
        stop_event.set()  # Stop the monitoring thread
        monitor_thread.join()
Hier ist das Hilfsprogramm, das ich dafür geschrieben habe. Wenn es allein ausgeführt wird, dann klappt es. Wenn ich das Hauptprogramm laufen lasse, dann kommt nach dem Aufploppen des niedriger-Akkustand-Fensters in der Konsole zwar die Mitteilung darüber. Das Hauptprogramm läuft aber normal(?) weiter:

Code: Alles auswählen

# battery_monitor.py

import time
import pyautogui
import cv2
import numpy as np
from colored_print import Print
import threading

stop_event = threading.Event()

def monitor_low_battery_window() -> None:
    """Monitors the screen for a low battery warning and signals the program to exit."""
    print("Monitor thread started.")
    while not stop_event.is_set():
        if check_low_battery_window(region=(900, 720, 720, 200)):  # (left, top, width, height)
            Print.critical("Low battery warning detected. Exiting program...")
            stop_event.set()
            break
        time.sleep(1)
    print("Monitor thread exiting.")

def check_low_battery_window(region: tuple) -> bool:
    """Checks for a low battery warning window in the specified region of the screen."""
    screenshot = pyautogui.screenshot()
    screenshot = cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR)

    if region:
        left, top, width, height = region
        screenshot = screenshot[top:top + height, left:left + width]

    template = cv2.imread("low_battery_window.png")
    if template is None:
        Print.error("Low battery template image not found.")
        return False

    result = cv2.matchTemplate(screenshot, template, cv2.TM_CCOEFF_NORMED)
    threshold = 0.8
    locations = np.where(result >= threshold)

    return locations[0].size > 0

if __name__ == '__main__':
    try:
        print("Starting battery monitor test...")
        monitor_thread = threading.Thread(target=monitor_low_battery_window)
        monitor_thread.start()

        while not stop_event.is_set():
            time.sleep(0.5)

        print("Test completed. Stopping monitor thread.")

        monitor_thread.join()
        print("Monitor thread stopped successfully.")
    except KeyboardInterrupt:
        print("\nTest interrupted by user.")
        stop_event.set()
        monitor_thread.join()
Ich freue mich über jede Hilfe.
Sirius3
User
Beiträge: 18270
Registriert: Sonntag 21. Oktober 2012, 17:20

globale Variablen sind schlecht, vor allem bei Threading. Zudem sollten Namen aussagekräftig sein, `stop_event` und `stop_signal` sein beides Events und heißen sehr ähnlich, da kann es schnell zu Verwechslungen kommen. Es ist schlecht, dass es außerhalb der main-Funktion noch zusätzlichen Code gibt, der eigentlich in main gehört.
Konstanten schreibt man KOMPLETT_GROSS.
Statt mit mehreren Queues und Signalen zu arbeiten, würde man alles in eine Priority-Queue packen, wobei das mit der Priorisierung bei Deinem Aufbau gar nicht nötig wäre.
`glob` kann auch in Unterverzeichnissen suchen.
Das mit dem Thread-Executor ist komisch. Warum erzeugst Du ständig neue process_files_with_work_stealing-Tasks, obwohl die ersten zwei noch gar nicht fertig sind?
Wenn Du zwei Threads hast, die die Queue abarbeiten sollen, dann brauchst Du keine zwei Queues, denn für das gemeinsame abarbeiten sind ja Queues da.
In check_low_battery_window suchst Du erst nach Werten wo result >= threshold ist, dann suchst Du alle Indizes dieser Werte und dann prüfst Du, ob es mehr als einen Index gibt. Statt dessen könnte man mit any auch prüfen, ob es irgendeinen Wert >= threshold gibt.
Ungetestet:

Code: Alles auswählen

import threading
import time
from pathlib import Path
from queue import Queue, ShutDown

import cv2
import numpy as np
import pyautogui
from colored_print import Print

MAX_WORKERS = 2
PICTURES_DIRECTORY = "C:/Users/User/pictures"
LOW_BATTERY_THRESHOLD = 0.8
LOW_BATTERY_IMAGE = "low_battery_window.png"

def process_files_with_work_stealing(file_queue):
    """Processes files using a work-stealing approach, distributing tasks between queues."""
    try:
        while True:
            task = file_queue.get()

            # Process file...
            ...
            file_queue.task_done()
    except ShutDown:
        pass


def check_low_battery_window(template, region):
    """Checks for a low battery warning window in the specified region of the screen."""
    screenshot = pyautogui.screenshot()
    screenshot = cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR)

    if region:
        left, top, width, height = region
        screenshot = screenshot[top : top + height, left : left + width]

    result = cv2.matchTemplate(screenshot, template, cv2.TM_CCOEFF_NORMED)
    return np.any(result >= LOW_BATTERY_THRESHOLD)


def monitor_low_battery_window(stop_event, file_queue):
    """Monitors the screen for a low battery warning and signals the program to exit."""
    template = cv2.imread(LOW_BATTERY_IMAGE)
    if template is None:
        Print.error("Low battery template image not found.")
        return

    print("Monitor thread started.")
    while not stop_event.is_set():
        if check_low_battery_window(
            template, region=(900, 720, 720, 200)
        ):  # (left, top, width, height)
            Print.critical("Low battery warning detected. Exiting program...")
            stop_event.set()
            file_queue.shutdown(immediate=True)
            break
        time.sleep(1)
    print("Monitor thread exiting.")


def main():
    file_queue = Queue()
    stop_event = threading.Event()

    monitor_thread = threading.Thread(
        target=monitor_low_battery_window, daemon=True, args=(stop_event, file_queue)
    )
    monitor_thread.start()

    try:
        process_threads = []
        for _ in range(MAX_WORKERS):
            process_thread = threading.Thread(
                target=process_files_with_work_stealing, daemon=True, args=(file_queue,)
            )
            process_thread.start()
            process_threads.append(process_thread)

        for path in Path(PICTURES_DIRECTORY).glob("*/*.png"):
            if path.is_file():
                file_queue.put(path)
        file_queue.join()
    except KeyboardInterrupt:
        print("\nProgram interrupted by the user.")
    finally:
        file_queue.shutdown(immediate=True)
        stop_event.set()  # Signals the monitor thread to exit
        monitor_thread.join()  # Waits for the thread to be closed
        for process_thread in process_threads:
            process_thread.join()
        print("Main thread completed. Monitor thread joined.")


if __name__ == "__main__":
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 14047
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Zusätzliche Anmerkungen: Das Umwandeln von BGR nach RGB könnte man vielleicht nach dem ausschneiden eines Bildausschnitts machen, dann müssen weniger Daten umgewandelt werden. Oder man schaut wie die Batterieanzeige an ihre Daten kommt — da muss es doch eine System-API für geben die ohne Bildanalyse auskommt‽

Ich würde `process_files_with_work_stealing()` auch das `stop_event` mitgeben und auf `Shutdown` verzichten. Dann kann man das auch mit allen anderen Python-Versionen laufen lassen ausser der allerneuesten. Das Event gibt's sowieso und so ein superzugewinn ist diese Ausnahme hier nicht wirklich.

Das `time.sleep()` im Monitoring-Thread würde ich durch `wait()` auf dem Event mit einem Timeout ersetzen. Dann wird da nicht bis zur Wartezeit gewartet wenn das Event gesetzt wurde.

Reicht die Parallelität mit Threads aus? Vielleicht wäre `concurrent.futures` mit Prozessen ja besser. Da hätte man auch das Problem mit dem selbst verwalten eines Pools nicht.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Benutzeravatar
grubenfox
User
Beiträge: 612
Registriert: Freitag 2. Dezember 2022, 15:49

__blackjack__ hat geschrieben: Mittwoch 4. Dezember 2024, 12:44 Oder man schaut wie die Batterieanzeige an ihre Daten kommt — da muss es doch eine System-API für geben die ohne Bildanalyse auskommt‽
da könnte es was von psutils geben: https://psutil.readthedocs.io/en/latest ... rs_battery
Antworten