Ich sehe ein sehr seltsames Verhalten, bei dem sich Haskells bracket
Funktion je nach Verwendung stack run
oder stack test
Verwendung unterschiedlich verhält .
Betrachten Sie den folgenden Code, in dem zwei verschachtelte Klammern zum Erstellen und Bereinigen von Docker-Containern verwendet werden:
module Main where
import Control.Concurrent
import Control.Exception
import System.Process
main :: IO ()
main = do
bracket (callProcess "docker" ["run", "-d", "--name", "container1", "registry:2"])
(\() -> do
putStrLn "Outer release"
callProcess "docker" ["rm", "-f", "container1"]
putStrLn "Done with outer release"
)
(\() -> do
bracket (callProcess "docker" ["run", "-d", "--name", "container2", "registry:2"])
(\() -> do
putStrLn "Inner release"
callProcess "docker" ["rm", "-f", "container2"]
putStrLn "Done with inner release"
)
(\() -> do
putStrLn "Inside both brackets, sleeping!"
threadDelay 300000000
)
)
Wenn ich dies mit ausführe stack run
und mit unterbreche Ctrl+C
, erhalte ich die erwartete Ausgabe:
Inside both brackets, sleeping!
^CInner release
container2
Done with inner release
Outer release
container1
Done with outer release
Und ich kann überprüfen, ob beide Docker-Container erstellt und dann entfernt wurden.
Wenn ich jedoch genau denselben Code in einen Test einfüge und ausführe stack test
, erfolgt nur (ein Teil) der ersten Bereinigung:
Inside both brackets, sleeping!
^CInner release
container2
Dies führt dazu, dass ein Docker-Container auf meinem Computer ausgeführt wird. Was ist los?
- Ich habe dafür gesorgt, dass genau dasselbe
ghc-options
an beide weitergegeben wird. - Vollständiges Demonstrations-Repo hier: https://github.com/thomasjm/bracket-issue
.stack-work
und sie direkt ausführe, tritt das Problem nicht auf. Es passiert nur, wenn man unter läuftstack test
.stack test
Startet Worker-Threads, um Tests durchzuführen. 2) Der SIGINT-Handler beendet den Haupt-Thread. 3) Haskell-Programme werden beendet, wenn der Haupt-Thread dies tut, wobei zusätzliche Threads ignoriert werden. 2 ist das Standardverhalten von SIGINT für von GHC kompilierte Programme. 3 ist, wie Threads in Haskell funktionieren. 1 ist eine vollständige Vermutung.Antworten:
Wenn Sie verwenden
stack run
, verwendet Stack effektiv einenexec
Systemaufruf, um die Steuerung auf die ausführbare Datei zu übertragen, sodass der Prozess für die neue ausführbare Datei den ausgeführten Stack-Prozess ersetzt, so als würden Sie die ausführbare Datei direkt von der Shell ausführen. So sieht der Prozessbaum ausstack run
. Beachten Sie insbesondere, dass die ausführbare Datei ein direktes untergeordnetes Element der Bash-Shell ist. Beachten Sie kritischer, dass die Vordergrundprozessgruppe (TPGID) des Terminals 17996 ist und der einzige Prozess in dieser Prozessgruppe (PGID) derbracket-test-exe
Prozess ist.Wenn Sie also Strg-C drücken, um den Prozess zu unterbrechen, der entweder unter
stack run
oder direkt von der Shell ausgeführt wird, wird das SIGINT-Signal nur an denbracket-test-exe
Prozess gesendet . Dies löst eine asynchroneUserInterrupt
Ausnahme aus. Der Wegbracket
funktioniert, wenn:Empfängt während der Verarbeitung eine asynchrone Ausnahme
body
, wird ausgeführtrelease
und löst die Ausnahme erneut aus. Bei Ihren verschachteltenbracket
Aufrufen führt dies dazu, dass der innere Körper unterbrochen, die innere Freigabe verarbeitet, die Ausnahme erneut ausgelöst wird, um den äußeren Körper zu unterbrechen, die äußere Freigabe verarbeitet und schließlich die Ausnahme erneut ausgelöst wird, um das Programm zu beenden. (Wennbracket
in Ihrermain
Funktion mehr Aktionen nach dem Äußeren folgen würden, würden diese nicht ausgeführt.)Wenn Sie dagegen
stack test
Stack verwendenwithProcessWait
, wird die ausführbare Datei als untergeordneter Prozess desstack test
Prozesses gestartet . Beachten Sie im folgenden Prozessbaum, dassbracket-test-test
es sich um einen untergeordneten Prozess von handeltstack test
. Entscheidend ist, dass die Vordergrundprozessgruppe des Terminals 18050 ist und diese Prozessgruppe sowohl denstack test
Prozess als auch denbracket-test-test
Prozess umfasst.Wenn Sie drücken Sie Strg-C im Terminal wird das SIGINT Signal gesendet alle Prozesse im Bereich des Terminals Vordergrund Prozessgruppe so beide
stack test
undbracket-test-test
bekommen das Signal.bracket-test-test
startet die Verarbeitung des Signals und führt die Finalisierer wie oben beschrieben aus. Hier gibt es jedoch eine Rennbedingung, denn wenn siestack test
unterbrochen wird, ist sie in der MittewithProcessWait
mehr oder weniger wie folgt definiert:Wenn
bracket
es unterbrochen wird, ruft es auf,stopProcess
wodurch der untergeordnete Prozess durch Senden desSIGTERM
Signals beendet wird. ImSIGINT
Gegensatz dazu löst dies keine asynchrone Ausnahme aus. Das Kind wird nur sofort beendet, in der Regel bevor die Finalisierer ausgeführt werden können.Ich kann mir keinen besonders einfachen Weg vorstellen, dies zu umgehen. Eine Möglichkeit besteht darin, die Einrichtungen
System.Posix
zu verwenden, um den Prozess in eine eigene Prozessgruppe einzuteilen:Jetzt führt Strg-C dazu, dass SIGINT nur an den
bracket-test-test
Prozess geliefert wird. Es wird bereinigt, die ursprüngliche Vordergrundprozessgruppe wiederhergestellt, um auf den Prozess zu verweisenstack test
, und beendet. Dies führt dazu, dass der Test fehlschlägt undstack test
einfach weiterläuft.Eine Alternative wäre, zu versuchen
SIGTERM
, den untergeordneten Prozess zu handhaben und am Laufen zu halten, um eine Bereinigung durchzuführen, selbst wenn derstack test
Prozess beendet wurde. Dies ist etwas hässlich, da der Prozess im Hintergrund aufgeräumt wird, während Sie die Shell-Eingabeaufforderung betrachten.quelle
stack test
Prozesse mit derdelegate_ctlc
Option vonSystem.Process
(oder ähnlichem) zu starten .