QProcess richtig anwenden

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
Nobuddy
User
Beiträge: 1019
Registriert: Montag 30. Januar 2012, 16:38

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
Nobuddy
User
Beiträge: 1019
Registriert: Montag 30. Januar 2012, 16:38

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
Benutzeravatar
__blackjack__
User
Beiträge: 13998
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@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")])
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Nobuddy
User
Beiträge: 1019
Registriert: Montag 30. Januar 2012, 16:38

@__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
Nobuddy
User
Beiträge: 1019
Registriert: Montag 30. Januar 2012, 16:38

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
Nobuddy
User
Beiträge: 1019
Registriert: Montag 30. Januar 2012, 16:38

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
Sirius3
User
Beiträge: 18250
Registriert: Sonntag 21. Oktober 2012, 17:20

@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()
Nobuddy
User
Beiträge: 1019
Registriert: Montag 30. Januar 2012, 16:38

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
Nobuddy
User
Beiträge: 1019
Registriert: Montag 30. Januar 2012, 16:38

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()
Sirius3
User
Beiträge: 18250
Registriert: Sonntag 21. Oktober 2012, 17:20

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()
Nobuddy
User
Beiträge: 1019
Registriert: Montag 30. Januar 2012, 16:38

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
Benutzeravatar
__blackjack__
User
Beiträge: 13998
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@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“.
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Sirius3
User
Beiträge: 18250
Registriert: Sonntag 21. Oktober 2012, 17:20

@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?
Nobuddy
User
Beiträge: 1019
Registriert: Montag 30. Januar 2012, 16:38

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
Antworten