나는 하스켈의 아주 이상한 행동을보고 있어요 bracket
함수가 다르게 행동 여부에 따라 있습니다 stack run
또는 stack test
사용됩니다.
Docker 컨테이너를 만들고 정리하는 데 두 개의 중첩 된 괄호가 사용되는 다음 코드를 고려하십시오.
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
)
)
로 이것을 실행 stack run
하고로 인터럽트 Ctrl+C
하면 예상 출력이 나타납니다.
Inside both brackets, sleeping!
^CInner release
container2
Done with inner release
Outer release
container1
Done with outer release
그리고 두 Docker 컨테이너가 모두 생성 된 다음 제거되었는지 확인할 수 있습니다.
그러나이 동일한 코드를 테스트에 붙여 넣고 실행 stack test
하면 첫 번째 정리 만 (일부) 발생합니다.
Inside both brackets, sleeping!
^CInner release
container2
결과적으로 내 컴퓨터에서 Docker 컨테이너가 계속 실행됩니다. 무슨 일이야?
- 나는 똑같은
ghc-options
것이 둘 다에 전달 되었는지 확인했습니다 . - 전체 데모 저장소 : https://github.com/thomasjm/bracket-issue
답변
을 사용 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
Stack은 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
비동기 예외는 발생하지 않습니다. 일반적으로 종료자를 실행하기 전에 자식을 즉시 종료합니다.
이 문제를 해결하는 특히 쉬운 방법은 생각할 수 없습니다. 한 가지 방법은 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
것입니다. 쉘 프롬프트를 보면서 프로세스가 백그라운드에서 정리되기 때문에 이것은 추악합니다.