Seite 1 von 1

QProcess richtig anwenden

Verfasst: Freitag 22. November 2024, 10:15
von Nobuddy
Hallo zusammen,

beschäftige mich gerade mit QProcess.

Dazu habe ich folgenden Code:

Code: Alles auswählen

import sys
from PyQt5.QtWidgets import (
	QApplication
)
from PyQt5.QtCore import (
	QProcess
)


class OpenApplication(object):

	def __init__(self, cmd=None, args=[]):
		super().__init__()

		self.p = None
		if not cmd or args == []:
			return
		self.start_process(cmd, args)
		sys.exit()

	def start_process(self, cmd, args):
		self.p = QProcess()
		self.p.readyReadStandardOutput.connect(self.handle_stdout)
		self.p.readyReadStandardError.connect(self.handle_stderr)
		self.p.stateChanged.connect(self.handle_state)
		self.p.finished.connect(self.process_finished)
		self.p.setProgram(cmd)
		self.p.setArguments(args)
		self.p.start()
		self.p.waitForStarted()
		self.p.waitForFinished()

	def handle_stderr(self):
		data = self.p.readAllStandardError()
		stderr = bytes(data).decode("utf8")

	def handle_stdout(self):
		data = self.p.readAllStandardOutput()
		stdout = bytes(data).decode("utf8")

	def handle_state(self, state):
		states = {
			QProcess.NotRunning: 'Not running',
			QProcess.Starting: 'Starting',
			QProcess.Running: 'Running',
		}
		state_name = states[state]
		print(f"State changed: {state_name}")

	def process_finished(self, exitCode, exitStatus):
		print(f'process finished: {exitCode}, {exitStatus}')
		self.p = None


def main(cmd=None, args=[]):
	app = QApplication(sys.argv)
	w = OpenApplication(cmd, args=args)
	app.exec_()

	filepath = '/path to/my_program.py'
	main(cmd='python3.12', args=[filepath])
Bei Programmen mit GUI, wird nach Beendigung des Programms, sauber mit finished beendet.

Code: Alles auswählen

State changed: Starting
State changed: Running
State changed: Not running
process finished: 0, 0
Bei Programmen ohne GUI, sieht das so aus:

Code: Alles auswählen

State changed: Starting
State changed: Running
QProcess: Destroyed while process ("python3.12") is still running.
Lässt sich da was optimieren, damit es einen sauberen Abschluss gibt?
Freue mich auch über weiteren Input dazu!

Grüße Nobuddy

Re: QProcess richtig anwenden

Verfasst: Freitag 22. November 2024, 11:20
von Nobuddy
Konnte es selbst lösen.

Code: Alles auswählen

import os
import sys
import signal
from PyQt5.QtWidgets import (
	QApplication
)
from PyQt5.QtCore import (
	QProcess
)


class OpenApplication(object):

	def __init__(self, cmd=None, args=[]):
		super().__init__()

		self.p = None
		if not cmd or args == []:
			return
		self.start_process(cmd, args)
		if self.p:
			os.kill(self.pid, signal.SIGINT)
			self.p.kill()

	def start_process(self, cmd, args):
		self.p = QProcess()
		self.p.readyReadStandardOutput.connect(self.handle_stdout)
		self.p.readyReadStandardError.connect(self.handle_stderr)
		self.p.stateChanged.connect(self.handle_state)
		self.p.finished.connect(self.process_finished)
		self.p.setProgram(cmd)
		self.p.setArguments(args)
		self.p.start()
		self.pid = self.p.processId()
		self.p.waitForStarted()
		self.p.waitForFinished()

	def handle_stderr(self):
		data = self.p.readAllStandardError()
		stderr = bytes(data).decode("utf8")

	def handle_stdout(self):
		data = self.p.readAllStandardOutput()
		stdout = bytes(data).decode("utf8")

	def handle_state(self, state):
		states = {
			QProcess.NotRunning: 'Not running',
			QProcess.Starting: 'Starting',
			QProcess.Running: 'Running',
		}
		state_name = states[state]
		print(f"State changed: {state_name}")

	def process_finished(self, exitCode, exitStatus):
		print(f'process finished: {exitCode}, {exitStatus}')
		self.p = None
		return


def main(cmd=None, args=[]):
	app = QApplication(sys.argv)
	w = OpenApplication(cmd, args=args)
	app.exec_()

if __name__ == '__main__':
	filepath = '/path to/my_proc.py'
	main(cmd='python3.12', args=[filepath])
Nun wird das sauber beendet.

Code: Alles auswählen

State changed: Starting
State changed: Running
State changed: Not running
process finished: 9, 1

Re: QProcess richtig anwenden

Verfasst: Freitag 22. November 2024, 14:26
von __blackjack__
@Nobuddy: Der `exitStatus` ist 1 also ``QProcess::CrashExit`` — das würde ich jetzt nicht als „sauber beendet“ bezeichnen.

`OpenApplication` ist so keine Klasse. Die `__init__()` macht letztlich alles und wenn die fertig ist, kann man mit dem Objekt nichts sinnvolles mehr anfangen. So eine `__init__()` sollte ein Objekt initialisieren, mit dem der Code an der Stelle wo das Objekt erstellt wurde, dann etwas mit diesem Objekt machen kann. Es wird mit dem Objekt aber nichts gemacht und es wird auch an einen total willkürlichen, nichtssagenden Namen `w` gebunden.

Die Defaultwerte machen auch keinen Sinn. Wenn man kein `cmd` hat, dann sollte man das Objekt halt gar nicht erst erstellen. Und Argumente sind nicht bei jedem Programm erforderlich, es macht also keinen Sinn die gleichzeitig mit einer leeren Liste als Defaultwert zu belegen aber dann im Code abzubrechen wenn da tatsächlich nichts übergeben wurde.

`start_process()` wartet am Ende bis der externe Prozess beendet ist, also macht das genau gar keinen Sinn an diesen beendeten Prozess noch Signale zu schicken. Weder ein SIGINT per `os`-Modul, noch ein SIGKILL per `kill()`-Methode auf dem QProcess. Wobei ein tatsächles SIGKILL das allerletzte Mittel sein sollte, weil das Betriebssystem dem Prozess dann gar keine Gelegenheit mehr gibt, selbst hinter sich aufzuräumen. Der wird einfach hart beendet.

Wenn man diese Signale entfernt, dann steht der Aufruf von `start_process()` als letztes in der `__init__()` und es gibt nicht wirklich einen Grund das in einer eigenen Methode zu haben.

`setProgram()` und `setArguments()` macht hier keinen Sinn, das kann man auch alles `start()` mitgeben.

Die Prozess-ID darf man nicht *vor* `waitForStarted()` aufrufen, denn wenn der Prozess noch nicht gestartet ist, dann hat der natürlich auch keine PID.

`waitForFinished()` wartet nicht bis der Prozess beendet ist, sondern nur 30 Sekunden sofern man nicht -1 als Argument übergibt. Letztlich macht hier blockierendes warten aber auch überhaupt gar keinen Sinn. Wenn das eine GUI-Anwendung wäre, würde die GUI einfrieren an der Stelle.

Wenn das externe Programm beendet ist, sollte man die Qt-Anwendung beenden, denn für den Benutzer wird das sonst umständlich bei einer Nicht-GUI-Anwendung.

Apropos Nicht-GUI-Anwendung: `QApplication` ist da unnötig, da reicht `QCoreApplication`.

Zwischenstand:

Code: Alles auswählen

import sys
from pathlib import Path

from PyQt5.QtCore import QCoreApplication, QProcess

STATE_TO_TEXT = {
    QProcess.NotRunning: "Not running",
    QProcess.Starting: "Starting",
    QProcess.Running: "Running",
}


def drain(method):
    _text = bytes(method()).decode("utf-8")


def on_finished(exit_code, exit_status):
    print(f"process finished: {exit_code}, {exit_status}")
    QCoreApplication.instance().quit()


def main(command, arguments=None):
    app = QCoreApplication(sys.argv)
    process = QProcess()
    process.readyReadStandardOutput.connect(
        lambda: drain(process.readAllStandardOutput)
    )
    process.readyReadStandardError.connect(
        lambda: drain(process.readAllStandardError)
    )
    process.stateChanged.connect(
        lambda state: print(f"State changed: {STATE_TO_TEXT[state]}")
    )
    process.finished.connect(on_finished)
    process.start(command, [] if arguments is None else arguments)
    app.exec_()


if __name__ == "__main__":
    main(sys.executable, [str(Path(__file__).parent / "external.py")])

Re: QProcess richtig anwenden

Verfasst: Freitag 22. November 2024, 16:09
von Nobuddy
@__blackjack__: mit Deinem Code Beispiel, konnte ich Deine Erklärungen gut nachvollziehen, Danke dafür!

Dein Code funktioniert prima mit GUI-Anwendungen.
Für Nicht-GUI-Anwendungen ist Dein Beispiel wahrscheinlich enstsprechend zu erweitern?

Habe mal statt QCoreApplication QApplication verwendet, gibt aber keine positive Veränderung bei Nicht-GUI-Anwendungen.

Jetzt kenne ich schon mal die richtige Richtung.

Danke und Grüße Nobuddy

Re: QProcess richtig anwenden

Verfasst: Samstag 23. November 2024, 09:23
von Nobuddy
Möchte mich nochmals kurz zurück melden.
Ich bin zu der Einsicht gekommen, dass es Quatsch ist Nicht-GUI-Anwendungen mit QProcess umzusetzen.
Dafür gibt es andere Möglichkeiten.

__blackjack__, Dein Code macht genau das was er soll, die GUI-Anwendung zu starten und bei Beendigung der Anwendung, den Prozess sauber zu beenden!

PS: Zu oft sucht man nach der "eierlegende Wollmilchsau" und verliert den Blick auf das Eigentliche.

Danke und Grüße Nobuddy

Re: QProcess richtig anwenden

Verfasst: Donnerstag 5. Dezember 2024, 14:25
von Nobuddy
Hallo zusammen,
habe mehrere Möglichkeiten gefunden, wobei Dein Code __blackjack__ oberste Sahne ist!

Was muss ergänzt/erweitert werden, um z.B. 2 Programme gleichzeitig laufen zu lassen?

Hier mal 2 weitere Beispiele:

Code: Alles auswählen

import sys
import time
import threading
import subprocess
from pathlib import Path
from PyQt5.QtWidgets import (
	QApplication
	)


def executeObjects(modulpath=[]):
	threads = [threading.Thread(target=run_script, args=(path,))
		for path in modulpath]
	[(thread.start(), time.sleep(1)) for thread in threads]
	[thread.join() for thread in threads]

def run_script(script_name):
	subprocess.run([sys.executable, script_name])

def openObject(modulpath=[]):
	main(modulpath=modulpath)


# file and base path
filePath = str(Path(__file__).parent)
basePath = str(Path(filePath).parent)
# main test
filepath = f'{basePath}/Woding/Woding.py'
filepath1 = f'{basePath}/Calendar/Calendar.py'
modulpath = [filepath, filepath1]
def main():
	App = QApplication(sys.argv)
	obj = executeObjects(modulpath=modulpath)
	sys.exit(App.exec())

if __name__ == '__main__':
	main()
und

Code: Alles auswählen

import os
import sys
import platform
from pathlib import Path
from PyQt5.QtWidgets import (
	QApplication
	)

# update sys path
filePath = str(Path(__file__).parent)
basePath = str(Path(filePath).parent)
try:
	from sysUpdate import sysUpdate
except ModuleNotFoundError:
	sys.path.append(basePath)
	from sysUpdate import sysUpdate
sysUpdate(__file__)


def executeModul(modulpath=[]):
	if modulpath == __file__ or modulpath == [__file__]:
		return
	# open first application
	# wait until the previous application has finished,
	# then start the next application
	[os.system(f'{sys.executable} {path}')
		for path in modulpath if os.path.isdir(os.path.dirname(path))]


# file and base path
filePath = str(Path(__file__).parent)
basePath = str(Path(filePath).parent)
# main test
filepath = f'{basePath}/Woding/Woding.py'
filepath1 = f'{basePath}/Calendar/Calendar.py'
modulpath = [filepath, filepath1]
def main():
	App = QApplication(sys.argv)
	obj = executeModul(modulpath=modulpath)
	sys.exit(App.exec())

if __name__ == '__main__':
	main()
Wie Ihr ja wisst, freue ich mich immer über Euren Input!

Grüße Nobuddy

Re: QProcess richtig anwenden

Verfasst: Donnerstag 5. Dezember 2024, 15:21
von Sirius3
@Nobuddy: eingerückt wird immer mit 4 Leerzeichen pro Ebene, keine Tabs. os.system soltle man nicht verwenden und Pfade sind keine Strings, da benutzt man keine String-Formatierung, vor allem, wenn Du schon eine Zeile davor pathlib benutzt?!?!?
List-Comprehension ist kein genereller Ersatz für Schleifen und Tuple kein Ersatz für zwei einzelne Anweisungen. Variablennamen und Funktionen schreibt man generell komplett_klein.
executeModul hat keinen Rückgabewert, warum bindest Du diesen an `obj`?
Das ganze könnte also ungefähr so aussehen:

Code: Alles auswählen

import sys
import time
import threading
import subprocess
from pathlib import Path

BASE_PATH = Path(__file__).parent.parent
MODULE_PATHS = [
    BASE_PATH / "Woding" / "Woding.py",
    BASE_PATH / "Calendar" / "Calendar.py",
]

def execute_objects(modulpaths):
    threads = [threading.Thread(target=run_script, args=(path,)) for path in modulpaths]
    for thread in threads:
        thread.start()
        time.sleep(1)
    for thread in threads:
        thread.join()


def run_script(script_name):
    subprocess.run([sys.executable, script_name])

def main():
    execute_objects(modulpath=MODULE_PATHS)


if __name__ == "__main__":
    main()
Da Du aber in den Threads nur Prozesse startest, kannst Du das ja auch gleich im Hauptprogramm tun.

Code: Alles auswählen

import sys
import subprocess
from pathlib import Path

BASE_PATH = Path(__file__).parent.parent
MODULE_PATHS = [
    BASE_PATH / "Woding" / "Woding.py",
    BASE_PATH / "Calendar" / "Calendar.py",
]


def execute_objects(modulpaths):
    processes = [
        subprocess.Popen([sys.executable, script_name])
        for script_name in modulpaths
    ]
    for process in processes:
        process.wait()


def main():
    execute_objects(modulpath=MODULE_PATHS)


if __name__ == "__main__":
    main()

Re: QProcess richtig anwenden

Verfasst: Donnerstag 5. Dezember 2024, 16:09
von Nobuddy
Hallo Sirius3,
bei mir hat das erste Programm eine längere Ladezeit, gegenüber dem Nachfolgenden.

Hatte es auch mit Popen und wait versucht.
Das Ergebnis war dass das kleinere Programm zuerst auf dem Monitor erscheint und so die Reihenfolge nicht eingehalten wird.
Das passiert auch mit Deinem Codebeispiel bei mir.

Grüße Nobuddy

Re: QProcess richtig anwenden

Verfasst: Donnerstag 5. Dezember 2024, 16:27
von Nobuddy
So funktioniert es mit Deinem Code bei mir.

Code: Alles auswählen

import sys
import time
import subprocess
from pathlib import Path

BASE_PATH = Path(__file__).parent.parent
MODULE_PATHS = [
    BASE_PATH / "Woding" / "Woding.py",
    BASE_PATH / "Calendar" / "Calendar.py",
]


def execute_objects(modulpaths=[]):
    processes = [
        (time.sleep(1), subprocess.Popen([sys.executable, script_name]))
        for script_name in modulpaths
       ]


def main():
    execute_objects(modulpaths=MODULE_PATHS)


if __name__ == "__main__":
    main()

Re: QProcess richtig anwenden

Verfasst: Donnerstag 5. Dezember 2024, 17:50
von Sirius3
Das mit den unsinnigen Tuplen hatten wir doch gerade schon.
Wenn das sleep relevant ist, brauchst Du halt wieder eine normale for-Schleife:

Code: Alles auswählen

import sys
import subprocess
import time
from pathlib import Path

BASE_PATH = Path(__file__).parent.parent
MODULE_PATHS = [
    BASE_PATH / "Woding" / "Woding.py",
    BASE_PATH / "Calendar" / "Calendar.py",
]


def execute_objects(modulpaths):
    processes = []
    for script_name in modulpaths:
        processes.append(subprocess.Popen([sys.executable, script_name]))
        time.sleep(1)
    for process in processes:
        process.wait()


def main():
    execute_objects(modulpath=MODULE_PATHS)


if __name__ == "__main__":
    main()

Re: QProcess richtig anwenden

Verfasst: Freitag 6. Dezember 2024, 14:54
von Nobuddy
Ja Tuple sind schwerer zu lesen, da ist eine for-Schleife wesentlich übersichtlicher und werde es auch so übernehmen.

Ob das mit "time.sleep(1)" immer ausreicht, muss ich noch testen.

Grüße Nobuddy

Re: QProcess richtig anwenden

Verfasst: Freitag 6. Dezember 2024, 17:26
von __blackjack__
@Nobuddy: Ein bisschen warten, und hoffen das wird ausreichen, ist bei nebenläufiger Programmierung falsch, denn irgendwann wird es dann doch wieder nicht ausreichen. Entweder man synchronisiert nebenläufigen Code ordentlich, oder man lässt es bleiben.

In einem Python-Programm andere Python-Programme auf diese Weise zu starten, ist an sich ja schon ein „code smell“.

Re: QProcess richtig anwenden

Verfasst: Sonntag 8. Dezember 2024, 12:00
von Sirius3
@Nobuddy: Was ist Dein eigentliches Ziel? Was machen die zwei Pythonprogramme, die Du ausführen möchtest und warum kannst Du die nicht in einem Prozess zusammenfassen?

Re: QProcess richtig anwenden

Verfasst: Montag 13. Januar 2025, 09:36
von Nobuddy
Hallo zusammen,
war krank und hatte daher keinen Kopf zu antworten.

@__blackjack__, gebe Dir Recht das ist ein verkorkster Code, daher habe ich den Code gelöscht.
Nebenläufigen Code ordentlich zu synchronisieren, das ist hier die Kunst.

@Sirius3, die zwei Pythonprogramme in einem Prozess zusammen zu fassen, das wäre wohl die einfachste Lösung.

Im Moment belasse ich es dabei, habe aber Euren Input abgespeichert!

Grüße und Danke
Nobuddy