Zamykanie aplikacji PyQt
- python, subconvert, qt, pl
- finished
Wsparcie dla Qt4.8 kończy się za pół roku, więc czas najwyższy przesiąść się w starszych aplikacjach na nowszą wersję. Portowanie Subconverta do PyQt5 nie nastręczyło większych kłopotów poza jednym, drobnym crashem, którego w Qt 4.8 nie doświadczyłem…
Jedną z ważnych funkcji każdej aplikacji jest sposób, w jaki się ona zamyka.
Poza niektórymi wyjątkami (kill -9
, crashe) tzw. graceful shutdown powinien
m.in. zapisywać stan aplikacji, zwalniać zasoby itp. Obsługę wszystkich
scenariuszy, które jest w stanie obsłużyć Python zapewnia użycie modułów
atexit
i signal
. Dzięki nim jesteśmy dostarczyć jednolitą implementację
kończenia działania aplikacji po prostu definiując funkcję obsługującą cleanup.
No dobrze, napiszmy zatem prostą aplikację, która zilustruje problem:
def interruptHandler(signum, frame):
sys.exit(1) # wywoła atexit
def cleanup(w):
print("cleanup")
w.cleanup()
class Window(QMainWindow):
def __init__(self):
super(Window, self).__init__()
self.widget = QWidget(self)
def cleanup(self):
print("Window::cleanup()")
self.widget.close()
def main():
app = QApplication(sys.argv)
gui = Window()
# Rejestracja funkcji, która wywoła się z końcem aplikacji
atexit.register(cleanup, gui)
# Pozwala interpreterowi uruchomić się co 500 ms,
# dzięki czemu możemy przechwycić nadchodzące sygnały
# (w przeciwnym wypadku Qt będzie je blokować).
timer = QTimer()
timer.start(500)
timer.timeout.connect(lambda: None)
gui.show()
sys.exit(app.exec_())
# Obsługa SIGINT (ctrl-c)
signal.signal(signal.SIGINT, interruptHandler)
main()
Uruchomienie powyższego programu spowoduje wyświetlenie się pustego okna.
Zamknąć je możemy dwojako: wysyłając mu sygnał np. SIGINT (większość terminali
wysyła go po wciśnięciu sekwencji ctrl-c) lub pozwalając to zrobić managerowi
okien (alt-f4, kliknięcie przycisku “X”, itp). Moglibyśmy również dodać przycisk
zamykający, który po naciśnięciu wywoływałby qApp.quit()
.
Jaka jest różnica? Otóż pierwszy sposób (ctrl-c) zadziała bezbłędnie, a drugi wysypie aplikację.
Dlaczego tak się dzieje?
Jeśli uruchomimy naszą aplikację poprzez GDB1, dowiemy się, że aplikacja
wykrzacza się w metodzie
QWidgetPrivate::close_helper
.
Okazuje się, że obiekt typu QCoreApplication
(po którym dziedziczy
QApplication
) przestał już istnieć, więc wywołanie na nim operacji
maybeQuit
2 w oczywisty sposób spowoduje błąd. W jaki sposób Qt otrzymuje
ten obiekt? Otóż QCoreApplication
jest globalnym “czymś w rodzaju
singletona3”, więc jest on dostępny z każdego miejsca aplikacji. Deweloperzy
Qt stworzyli nawet specjalne makro: qApp
, które generalnie zwraca coś w stylu
static_cast<QApplication*>(QCoreApplication::instance())
. Specjalnie je
zapisali camel casem, aby przeciętnemu programiście nie ciążyło na sumieniu
używanie makr.
Dlaczego jednak nasz obiekt aplikacji raz jest usuwany, a raz nie? Oczywiście
powodem jest dualny sposób zamykania aplikacji. Jeśli chodzi o wysłanie sygnału
przerwania SIGINT, to musimy się odwołać do modułu signal
. Moduł ten nie jest
wywoływany w niskopoziomowym kodzie C. Zamiast tego Python ustawia odpowiednią
flagę, która pozwala na wywołanie interrupt handlera w dowolnym późniejszym
momencie czasu, np. w następnej instrukcji bytecode’u. Dzięki temu długie
operacje napisane w C (np. sprawdzanie wyrażeń regularnych) mogą nieprzerwane
zakończyć swoje działanie. Z naszej perspektywy powoduje to “wciśnięcie”
wywołania funkcji interruptHandler
gdzieś w środek naszego kodu,
najprawdopodobniej gdzieś między dwa obroty głównej pętli programu (event-loop
Qt). W takim wypadku oczywiście QCoreApplication
ciągle żyje, gdyż właśnie
wykonuje swój kod.
Natomiast “ładne” zamknięcie programu spowoduje wywołanie funkcji
QCoreApplication::exit(0)
. Metoda ta spowoduje wyjście z metody
QCoreApplication::exec
ze statusem 0, który zostanie przekazany jako argument
do wywołania funkcji sys.exit
. Co robi sys.exit
? Funkcja ta również nie jest
zaimplementowana bardzo niskopoziomowo - po prostu rzuca ona wyjątek
SystemExit
4. Z naszej perspektywy nie jest on obsługiwany na żadnym etapie
naszego programu, a zatem interpreter będzie zakres po zakresie “zwijał stos”
(tak to nazwijmy, dla uproszczenia) - najpierw wyjdzie z funkcji main
, a
następnie z zakresu globalnego. Garbage Collector zauważy wówczas, że nasz
obiekt QApplication
nie jest już używany, więc go usunie. Pozostawi natomiast
obiekt Window
wraz ze wszystkimi dziećmi, ponieważ ciągle znajduje się do
niego referencja w funkcji zarejestrowanej przez moduł atexit
. Jego
implementacja przewiduje wywołanie wszystkich zarejestrowanych funkcji (w
kolejności odwrotnej do rejestracji - LIFO) dopiero przy wychodzeniu z
interpretera. I tak dochodzimy właśnie do miejsca, w którym jeden obiekt
przestał istnieć, a drugi nadal istnieje.
Jak temu zapobiec?
Najprościej oczywiście utworzyć obiekt globalny QApplication
. Jednak my nie
chcemy tego robić, więc stworzymy sobie ładny wrapper, który postawi naszą
aplikację na nogi, a gdy trzeba, wykona cleanup:
class Application:
def __init__(self):
self.app = QApplication(sys.argv)
self.gui = Window()
def run(self):
timer = QTimer()
timer.start(500)
timer.timeout.connect(lambda: None)
self.gui.show()
return self.app.exec_()
def cleanup(self):
self.gui.cleanup()
def main():
app = Application()
atexit.register(cleanup, app)
sys.exit(app.run())
Dzięki takiemu podejściu mamy pewność, że żadne zasoby nie zostaną przedwcześnie usunięte (przed usunięciem naszego obiektu aplikacji). Co więcej, zdefiniowaliśmy w ten sposób jednolity interfejs umożliwiający tworzenie oddzielnych klas np. dla aplikacji okienkowych i konsolowych, które będą jednakowo traktowane z perspektywy uruchamiania naszego programu:
def main():
args = parseArgs(sys.argv)
app = CliApplication() if args.startCli else GuiApplication()
atexit.register(cleanup, app)
sys.exit(app.run())
-
Jeśli chcemy debugować kod C i C++ wywoływany przez interpreter, musimy mieć zainstalowane symbole debugowe dla Pythona oraz kodu, który chcemy debugować (w naszym przypadku Qt). Wówczas możemy uruchomić program przy użyciu komendy
gdb --args python3 myapp.py
. Interpreter Pythona pozostawia również po wszelkich wywrotkach coredumpy:gdb python3 core
. ↩ -
Naprawdę doskonała nazwa metody. ↩
-
QCoreApplication
nie zapewnia swojej unikalności - jedynie w dokumentacji Qt jest stwierdzenie, że powinna być utworzona tylko jedna instancja tego obiektu. ↩ -
Oprócz tego, że
SystemExit
może zostać złapany w dowolnym momencie, to aby poprawnie wyjść z aplikacjisys.exit
musi być wywołana z poziomu głównego wątku aplikacji. Na otarcie łez wspomnę, że w celu minimalizacji przypadkowego złapania tego wyjątkuSystemExit
dziedziczy poBaseException
, a nieException
. ↩