Когда вы используете stack run
, Stack эффективно использует системный вызов exec
для передачи управления исполняемому файлу, поэтому процесс для нового исполняемого файла заменяет запущенный процесс стека, так же, как если бы вы запускали исполняемый файл непосредственно из оболочка. Вот как выглядит дерево процессов после stack run
. В частности, обратите внимание, что исполняемый файл является прямым потомком оболочки Bash. Что еще более важно, обратите внимание, что приоритетной группой процессов терминала (TPGID) является 17996, и единственный процесс в этой группе процессов (PGID) - это процесс bracket-test-exe
.
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13816 13831 13831 13831 pts/3 17996 Ss 2001 0:00 | \_ /bin/bash --noediting -i
13831 17996 17996 13831 pts/3 17996 Sl+ 2001 0:00 | | \_ .../.stack-work/.../bracket-test-exe
В результате, когда вы нажимаете Ctrl- C, чтобы прервать процесс, запущенный либо под stack run
, либо непосредственно из оболочки, сигнал SIGINT доставляется только процессу bracket-test-exe
. Это вызывает асинхронное исключение UserInterrupt
. Способ bracket
работает, когда:
bracket
acquire
(\() -> release)
(\() -> body)
получает асинхронное исключение при обработке body
, запускает release
и затем повторно вызывает исключение. С вашими вложенными вызовами bracket
это приводит к прерыванию внутреннего тела, обработке внутреннего освобождения, повторному вызову исключения для прерывания внешнего тела, обработке внешнего выпуска и, наконец, повторному вызову исключения для завершения программа. (Если бы после внешней bracket
в вашей функции main
было больше действий, они не были бы выполнены.)
С другой стороны, когда вы используете stack test
, стек использует withProcessWait
запустить исполняемый файл как дочерний процесс stack test
. Обратите внимание, что в следующем дереве процессов bracket-test-test
является дочерним процессом stack test
. Критически важно, что приоритетной группой процессов терминала является 18050, и эта группа процессов включает в себя как stack test
процесс, так и процесс bracket-test-test
.
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13816 13831 13831 13831 pts/3 18050 Ss 2001 0:00 | \_ /bin/bash --noediting -i
13831 18050 18050 13831 pts/3 18050 Sl+ 2001 0:00 | | \_ stack test
18050 18060 18050 13831 pts/3 18050 Sl+ 2001 0:00 | | \_ .../.stack-work/.../bracket-test-test
Когда вы нажимаете Ctrl- C в терминале, Сигнал SIGINT отправляется всем процессам в группе процессов переднего плана терминала, поэтому оба сигнала stack test
и bracket-test-test
получают сигнал. bracket-test-test
начнет обработку сигнала и запустит финализаторы, как описано выше. Однако здесь есть условие гонки, потому что когда stack test
прервано, оно находится в середине withProcessWait
, что определяется более или менее следующим образом:
withProcessWait config f =
bracket
(startProcess config)
stopProcess
(\p -> f p <* waitExitCode p)
, поэтому, когда его bracket
прерывается он вызывает stopProcess
, что завершает дочерний процесс, посылая ему сигнал SIGTERM
. В отличие от SIGINT
, это не вызывает асинхронного исключения. Он просто немедленно завершает работу ребенка, прежде чем он сможет завершить sh запуск любых финализаторов.
Я не могу придумать особенно простой способ обойти это. Одним из способов является использование средств System.Posix
для помещения процесса в собственную группу процессов:
main :: IO ()
main = do
-- save old terminal foreground process group
oldpgid <- getTerminalProcessGroupID (Fd 2)
-- get our PID
mypid <- getProcessID
let -- put us in our own foreground process group
handleInt = setTerminalProcessGroupID (Fd 2) mypid >> createProcessGroupFor mypid
-- restore the old foreground process gorup
releaseInt = setTerminalProcessGroupID (Fd 2) oldpgid
bracket
(handleInt >> putStrLn "acquire")
(\() -> threadDelay 1000000 >> putStrLn "release" >> releaseInt)
(\() -> putStrLn "between" >> threadDelay 60000000)
putStrLn "finished"
Теперь Ctrl- C приведет к тому, что SIGINT будет доставлен только процессу bracket-test-test
, Он очистит, восстановит исходную группу процессов переднего плана, чтобы она указала на процесс stack test
, и завершится. Это приведет к сбою теста, и stack test
просто продолжит работу.
Альтернативой может быть попытка обработать SIGTERM
и оставить работающий дочерний процесс для выполнения очистки, даже если stack test
процесс завершен. Это немного уродливо, так как процесс будет как бы очищаться в фоновом режиме, пока вы смотрите на приглашение оболочки.