Marge Bot pushed to branch master at Glasgow Haskell Compiler / GHC

Commits:

14 changed files:

Changes:

  • changelog.d/T4162
    1
    +section: compiler
    
    2
    +issues: #4162
    
    3
    +mrs: !16774
    
    4
    +synopsis:
    
    5
    +  `runGhc/runGhcT` no longer hijack process signals, old behaviour provided by
    
    6
    +  `runGhcWithSignalHandlers`.
    
    7
    +description:
    
    8
    +  Functions `runGhc/runGhcT` no longer go out of their way to redirect process
    
    9
    +  signals as exceptions to them.
    
    10
    +
    
    11
    +  The installed handlers make it so other threads are unaware of any signals,
    
    12
    +  which can cause the process to become unresponsive whenever a crash of the GHC
    
    13
    +  session(s) does not cascade into a shutdown of the process itself.
    
    14
    +
    
    15
    +  The old behaviour is available through `runGhcWithSignalHandlers` or by
    
    16
    +  calling `withSignalHandlers`. Only intended to be used from the main thread of
    
    17
    +  your process, see the documentation.

  • compiler/GHC.hs
    ... ... @@ -23,6 +23,7 @@ module GHC (
    23 23
             -- * GHC Monad
    
    24 24
             Ghc, GhcT, GhcMonad(..), HscEnv,
    
    25 25
             runGhc, runGhcT, initGhcMonad,
    
    26
    +        runGhcWithSignalHandlers,
    
    26 27
             printException,
    
    27 28
             handleSourceError,
    
    28 29
     
    
    ... ... @@ -555,10 +556,26 @@ runGhc :: Maybe FilePath -- ^ See argument to 'initGhcMonad'.
    555 556
     runGhc mb_top_dir ghc = do
    
    556 557
       ref <- newIORef (panic "empty session")
    
    557 558
       let session = Session ref
    
    558
    -  flip unGhc session $ withSignalHandlers $ do -- catch ^C
    
    559
    +  flip unGhc session $ do
    
    559 560
         initGhcMonad mb_top_dir
    
    560 561
         withCleanupSession ghc
    
    561 562
     
    
    563
    +-- | Like 'runGhc' but uses 'withSignalHandlers' to redirect selected signals to
    
    564
    +-- this thread to trigger 'withCleanupSession', and so that interactive
    
    565
    +-- evaluation receives 'UserInterrupt' exceptions.
    
    566
    +--
    
    567
    +-- __WARNING__: Only intended to be called from the __main thread__ of your
    
    568
    +-- process, see 'withSignalHandlers' documentation for more details.
    
    569
    +--
    
    570
    +-- More complex multi-threaded programs are recommended to use 'runGhc' or
    
    571
    +-- 'runGhcT', and handle signals as appropriate for their control flow.
    
    572
    +
    
    573
    +runGhcWithSignalHandlers :: Maybe FilePath  -- ^ See argument to 'initGhcMonad'.
    
    574
    +                         -> Ghc a           -- ^ The action to perform.
    
    575
    +                         -> IO a
    
    576
    +runGhcWithSignalHandlers mb_top_dir ghc = runGhc mb_top_dir $
    
    577
    +  withSignalHandlers $ ghc
    
    578
    +
    
    562 579
     -- | Run function for 'GhcT' monad transformer.
    
    563 580
     --
    
    564 581
     -- It initialises the GHC session and warnings via 'initGhcMonad'.  Each call
    
    ... ... @@ -572,7 +589,7 @@ runGhcT :: ExceptionMonad m =>
    572 589
     runGhcT mb_top_dir ghct = do
    
    573 590
       ref <- liftIO $ newIORef (panic "empty session")
    
    574 591
       let session = Session ref
    
    575
    -  flip unGhcT session $ withSignalHandlers $ do -- catch ^C
    
    592
    +  flip unGhcT session $ do
    
    576 593
         initGhcMonad mb_top_dir
    
    577 594
         withCleanupSession ghct
    
    578 595
     
    

  • compiler/GHC/Utils/Panic.hs
    ... ... @@ -234,8 +234,21 @@ signalHandlersRefCount :: MVar (Word, Maybe S.Handler)
    234 234
     signalHandlersRefCount = unsafePerformIO $ newMVar (0,Nothing)
    
    235 235
     
    
    236 236
     
    
    237
    --- | Temporarily install standard signal handlers for catching ^C, which just
    
    238
    --- throw an exception in the current thread.
    
    237
    +-- | Temporarily install signal handlers to redirect the following signals to
    
    238
    +-- the calling thread as async exceptions: @INT@, @QUIT@, @HUP@, @TERM@ on unix, and
    
    239
    +-- @CtrlC@, @Break@ on windows. The original signal handlers are restored when all
    
    240
    +-- concurrent calls of this function are exited.
    
    241
    +--
    
    242
    +-- @INT@, @QUIT@, @CtrlC@, and @Break@ are thrown as @'UserInterrupt'@.
    
    243
    +--
    
    244
    +-- @HUP@, and @TERM@ are thrown as @'Panic' <signal number>@.
    
    245
    +--
    
    246
    +-- __WARNING__: Only intended to be called from the __main thread__ of your
    
    247
    +-- process, as otherwise it can swallow signals the process should react to in
    
    248
    +-- other ways. Nested calls from a single thread are fine, but concurrent calls
    
    249
    +-- from different threads will have the exceptions delivered to only one of
    
    250
    +-- them. If that thread is not @main@, extra care is needed to e.g. actually
    
    251
    +-- terminate the process when it receives @TERM@.
    
    239 252
     withSignalHandlers :: ExceptionMonad m => m a -> m a
    
    240 253
     #if !defined(HAVE_SIGNAL_H)
    
    241 254
     -- No signal functionality exist on the host platform (e.g. on
    

  • ghc/Main.hs
    ... ... @@ -141,7 +141,7 @@ main = do
    141 141
                        ShowOptions isInteractive -> showOptions isInteractive
    
    142 142
             Right postStartupMode ->
    
    143 143
                 -- start our GHC session
    
    144
    -            GHC.runGhc mbMinusB $ do
    
    144
    +            GHC.runGhcWithSignalHandlers mbMinusB $ do
    
    145 145
     
    
    146 146
                 dflags <- GHC.getSessionDynFlags
    
    147 147
     
    

  • linters/lint-codes/LintCodes/Static.hs
    ... ... @@ -31,7 +31,7 @@ import GHC.Types.Error.Codes
    31 31
     
    
    32 32
     -- ghc (API usage)
    
    33 33
     import GHC
    
    34
    -  ( runGhc, parseDynamicFlags
    
    34
    +  ( runGhcWithSignalHandlers, parseDynamicFlags
    
    35 35
       , getSessionDynFlags, setSessionDynFlags
    
    36 36
       , getSession, getLogger
    
    37 37
       , noLoc
    
    ... ... @@ -145,7 +145,7 @@ newtype LibDir = LibDir { libDir :: FilePath }
    145 145
     -- | Look up the 'GhcDiagnosticCode' type family using the GHC API.
    
    146 146
     ghcDiagnosticCodeTyCon :: Maybe LibDir -> IO TyCon
    
    147 147
     ghcDiagnosticCodeTyCon mb_libDir =
    
    148
    -  runGhc (libDir <$> mb_libDir)
    
    148
    +  runGhcWithSignalHandlers (libDir <$> mb_libDir)
    
    149 149
     
    
    150 150
       -- STEP 1: start a GHC API session with "-package ghc"
    
    151 151
       do { dflags1 <- getSessionDynFlags
    

  • testsuite/tests/ghc-api/T4162.hs
    1
    +{-# LANGUAGE CPP #-}
    
    2
    +{-# LANGUAGE LambdaCase #-}
    
    3
    +
    
    4
    +-- | Test for #4162: 'runGhc' must not install signal handlers on behalf of its
    
    5
    +-- caller.
    
    6
    +--
    
    7
    +-- 'withSignalHandlers' redirects signals to the thread that called it. While
    
    8
    +-- 'runGhc' did that implicitly, a GHC session running in a 'forkIO' thread
    
    9
    +-- would capture signals aimed at the process as a whole and deliver them to a
    
    10
    +-- thread that may not be in a position to act on them. Here that thread sits
    
    11
    +-- under 'uninterruptibleMask', so the exception can never be delivered and the
    
    12
    +-- signal is swallowed outright.
    
    13
    +--
    
    14
    +-- The worker process parks a 'forkIO' thread inside 'runGhc' under
    
    15
    +-- 'uninterruptibleMask' and blocks its main thread waiting for that thread to
    
    16
    +-- finish. The driver process waits for the worker to report that the thread is
    
    17
    +-- inside 'runGhc', signals it, and checks that it dies promptly:
    
    18
    +--
    
    19
    +--   * on unix, SIGTERM has no handler, so the kernel kills the process;
    
    20
    +--   * on Windows, CTRL_BREAK is handled by GHC.TopHandler, which throws
    
    21
    +--     UserInterrupt at the main thread, which exits with 252.
    
    22
    +--
    
    23
    +-- Before #4162 both were swallowed by the masked 'forkIO' thread and the worker
    
    24
    +-- carried on running.
    
    25
    +module Main (main) where
    
    26
    +
    
    27
    +import Control.Concurrent
    
    28
    +import Control.Exception
    
    29
    +import Control.Monad
    
    30
    +import Control.Monad.IO.Class (liftIO)
    
    31
    +import GHC (runGhc)
    
    32
    +import System.Environment
    
    33
    +import System.Exit
    
    34
    +import System.IO
    
    35
    +import System.Process
    
    36
    +import System.Timeout (timeout)
    
    37
    +
    
    38
    +#if !defined(mingw32_HOST_OS)
    
    39
    +import System.Posix.Signals (sigKILL, sigTERM, signalProcess)
    
    40
    +#endif
    
    41
    +
    
    42
    +main :: IO ()
    
    43
    +main = getArgs >>= \case
    
    44
    +  [libdir]             -> driver libdir
    
    45
    +  [libdir, "--worker"] -> worker libdir
    
    46
    +  args                 -> die ("FAILED: unexpected arguments: " ++ show args)
    
    47
    +
    
    48
    +-- | The process under test: a GHC session pinned in a 'forkIO' thread, with the
    
    49
    +-- main thread waiting on it.
    
    50
    +worker :: FilePath -> IO ()
    
    51
    +worker libdir = do
    
    52
    +  hSetBuffering stdout LineBuffering
    
    53
    +  done <- newEmptyMVar
    
    54
    +  _ <- forkIO $
    
    55
    +         flip finally (putMVar done ()) $
    
    56
    +           uninterruptibleMask_ $
    
    57
    +             runGhc (Just libdir) $ liftIO $ do
    
    58
    +               putStrLn ready
    
    59
    +               forever (threadDelay oneSecond)
    
    60
    +  takeMVar done
    
    61
    +  -- Only reachable if the session thread died on its own; the signal was
    
    62
    +  -- supposed to take the whole process down first.
    
    63
    +  putStrLn "worker: session thread finished"
    
    64
    +
    
    65
    +-- | Wait for the worker to reach 'runGhc', signal it, and require a prompt exit.
    
    66
    +driver :: FilePath -> IO ()
    
    67
    +driver libdir = do
    
    68
    +  self <- getExecutablePath
    
    69
    +  let cp = (proc self [libdir, "--worker"])
    
    70
    +             { std_out = CreatePipe
    
    71
    +#if defined(mingw32_HOST_OS)
    
    72
    +             -- GenerateConsoleCtrlEvent can only target a process group, so the
    
    73
    +             -- worker needs one of its own.
    
    74
    +             , create_group = True
    
    75
    +#endif
    
    76
    +             }
    
    77
    +  bracket (createProcess cp) kill $ \(_, Just out, _, ph) -> do
    
    78
    +    handshake <- timeout handshakeTimeout
    
    79
    +                   (try (hGetLine out) :: IO (Either IOException String))
    
    80
    +    case handshake of
    
    81
    +      Just (Right l) | l == ready -> return ()
    
    82
    +      _ -> die ("FAILED: worker never reached runGhc: " ++ show handshake)
    
    83
    +    interruptWorker ph
    
    84
    +    waitForExit ph >>= \case
    
    85
    +      Just code | code == expectedExit -> putStrLn "worker terminated promptly"
    
    86
    +      other -> die ("FAILED: expected " ++ show expectedExit
    
    87
    +                     ++ " from the worker, got " ++ show other)
    
    88
    +
    
    89
    +-- | Deliver the signal that the worker must not be able to swallow.
    
    90
    +interruptWorker :: ProcessHandle -> IO ()
    
    91
    +#if defined(mingw32_HOST_OS)
    
    92
    +-- CTRL_BREAK_EVENT rather than CTRL_C_EVENT: GenerateConsoleCtrlEvent cannot
    
    93
    +-- deliver CTRL+C to a specific process group. Both reach the same handler, in
    
    94
    +-- GHC.TopHandler now and in withSignalHandlers before #4162.
    
    95
    +interruptWorker = interruptProcessGroupOf
    
    96
    +#else
    
    97
    +interruptWorker ph = getPid ph >>= \case
    
    98
    +  Just pid -> signalProcess sigTERM pid
    
    99
    +  Nothing  -> die "FAILED: worker exited before it could be signalled"
    
    100
    +#endif
    
    101
    +
    
    102
    +-- | How the worker is expected to die once signalled.
    
    103
    +expectedExit :: ExitCode
    
    104
    +#if defined(mingw32_HOST_OS)
    
    105
    +-- UserInterrupt reaches the top handler, which calls exitInterrupted.
    
    106
    +expectedExit = ExitFailure 252
    
    107
    +#else
    
    108
    +expectedExit = ExitFailure (negate (fromIntegral sigTERM))
    
    109
    +#endif
    
    110
    +
    
    111
    +-- | Poll for the worker's exit, giving up after 'terminationTimeout'. Polling
    
    112
    +-- rather than 'waitForProcess' so that a worker that ignored the signal fails
    
    113
    +-- the test quickly instead of hanging it until the testsuite timeout.
    
    114
    +waitForExit :: ProcessHandle -> IO (Maybe ExitCode)
    
    115
    +waitForExit ph = go (terminationTimeout `div` pollInterval)
    
    116
    +  where
    
    117
    +    go :: Int -> IO (Maybe ExitCode)
    
    118
    +    go n
    
    119
    +      | n <= 0 = return Nothing
    
    120
    +      | otherwise = getProcessExitCode ph >>= \case
    
    121
    +          Just code -> return (Just code)
    
    122
    +          Nothing   -> threadDelay pollInterval >> go (n - 1)
    
    123
    +
    
    124
    +-- | Keep a worker that ignored the signal from outliving the test and holding
    
    125
    +-- the inherited stderr open. Note that 'terminateProcess' is only SIGTERM on
    
    126
    +-- unix, which is precisely what such a worker swallows.
    
    127
    +kill :: (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) -> IO ()
    
    128
    +kill (_, _, _, ph) = do
    
    129
    +#if defined(mingw32_HOST_OS)
    
    130
    +  terminateProcess ph
    
    131
    +#else
    
    132
    +  getPid ph >>= mapM_ (signalProcess sigKILL)
    
    133
    +#endif
    
    134
    +  void (waitForProcess ph)
    
    135
    +
    
    136
    +-- | Printed by the worker from inside 'runGhc', so that by the time the driver
    
    137
    +-- sees it any handlers 'runGhc' might install are already in place.
    
    138
    +ready :: String
    
    139
    +ready = "ready"
    
    140
    +
    
    141
    +oneSecond, pollInterval, handshakeTimeout, terminationTimeout :: Int
    
    142
    +oneSecond          = 1000000
    
    143
    +pollInterval       = 100000            -- 0.1s
    
    144
    +handshakeTimeout   = 60 * oneSecond    -- starting a GHC session can be slow
    
    145
    +terminationTimeout = 10 * oneSecond

  • testsuite/tests/ghc-api/T4162.stdout
    1
    +worker terminated promptly

  • testsuite/tests/ghc-api/all.T
    ... ... @@ -91,3 +91,13 @@ test('T27240', [extra_run_opts(f'"{config.libdir}"')], compile_and_run, ['-packa
    91 91
     test('T27273', [extra_run_opts(f'"{config.libdir}"')],
    
    92 92
                    compile_and_run,
    
    93 93
                    ['-package ghc'])
    
    94
    +
    
    95
    +# runGhc must not install signal handlers on behalf of its caller, or a session
    
    96
    +# running off the main thread makes the process unresponsive to signals.
    
    97
    +test('T4162', [ extra_run_opts(f'"{config.libdir}"')
    
    98
    +              # the test re-executes itself, so it has to be a real executable
    
    99
    +              , only_ways(['normal'])
    
    100
    +              , req_process
    
    101
    +              # no subprocesses or signals
    
    102
    +              , when(arch('wasm32') or arch('javascript'), skip)
    
    103
    +              ], compile_and_run, ['-package ghc'])

  • utils/check-exact/Parsers.hs
    ... ... @@ -231,7 +231,7 @@ parseModuleEpAnnsWithCpp libdir cppOptions file = ghcWrapper libdir $ do
    231 231
     ghcWrapper :: LibDir -> GHC.Ghc a -> IO a
    
    232 232
     ghcWrapper libdir a =
    
    233 233
       GHC.defaultErrorHandler GHC.defaultFatalMessager GHC.defaultFlushOut
    
    234
    -    $ GHC.runGhc (Just libdir) a
    
    234
    +    $ GHC.runGhcWithSignalHandlers (Just libdir) a
    
    235 235
     
    
    236 236
     -- | Internal function. Exposed if you want to muck with DynFlags
    
    237 237
     -- before parsing.
    

  • utils/check-ppr/Main.hs
    ... ... @@ -81,7 +81,7 @@ testOneFile libdir fileName = do
    81 81
     
    
    82 82
     parseOneFile :: FilePath -> FilePath -> IO ParsedModule
    
    83 83
     parseOneFile libdir fileName = do
    
    84
    -       runGhc (Just libdir) $ do
    
    84
    +       runGhcWithSignalHandlers (Just libdir) $ do
    
    85 85
              dflags <- getSessionDynFlags
    
    86 86
              let dflags2 = dflags `gopt_set` Opt_KeepRawTokenStream
    
    87 87
              _ <- setSessionDynFlags dflags2
    

  • utils/count-deps/Main.hs
    ... ... @@ -51,7 +51,7 @@ printDeps libdir modName dot = do
    51 51
     calcDeps :: String -> FilePath -> IO (Map.Map ModuleName [ModuleName])
    
    52 52
     calcDeps modName libdir =
    
    53 53
       defaultErrorHandler defaultFatalMessager defaultFlushOut $ do
    
    54
    -    runGhc (Just libdir) $ do
    
    54
    +    runGhcWithSignalHandlers (Just libdir) $ do
    
    55 55
             df <- getSessionDynFlags
    
    56 56
             logger <- getLogger
    
    57 57
             (df, _, _) <- parseDynamicFlags logger df [noLoc "-package=ghc"]
    

  • utils/dump-decls/Main.hs
    ... ... @@ -32,7 +32,7 @@ main = do
    32 32
         mapM_ (run ghcRoot) pkg_names
    
    33 33
     
    
    34 34
     run :: FilePath -> String -> IO ()
    
    35
    -run root pkg_nm = runGhc (Just root) $ do
    
    35
    +run root pkg_nm = runGhcWithSignalHandlers (Just root) $ do
    
    36 36
         let args = map noLoc
    
    37 37
                 [ "-package=" ++ pkg_nm
    
    38 38
                 , "-dppr-cols=1000"
    

  • utils/haddock/haddock-api/src/Haddock.hs
    ... ... @@ -196,7 +196,7 @@ handleGhcExceptions =
    196 196
     --
    
    197 197
     -- > main = getArgs >>= haddock
    
    198 198
     haddock :: [String] -> IO ()
    
    199
    -haddock args = haddockWithGhc withGhc args
    
    199
    +haddock args = withSignalHandlers $ haddockWithGhc withGhc args
    
    200 200
     
    
    201 201
     haddockWithGhc :: (forall a. [Flag] -> Ghc a -> IO a) -> [String] -> IO ()
    
    202 202
     haddockWithGhc ghc args = handleTopExceptions $ do
    

  • utils/haddock/haddock-api/test/Haddock/Backends/Hyperlinker/ParserSpec.hs
    ... ... @@ -20,7 +20,7 @@ import Haddock.Backends.Hyperlinker.Types
    20 20
     withDynFlags :: (DynFlags -> IO ()) -> IO ()
    
    21 21
     withDynFlags cont = do
    
    22 22
       libDir <- fmap snd (getGhcDirs [])
    
    23
    -  runGhc libDir $ do
    
    23
    +  runGhcWithSignalHandlers libDir $ do
    
    24 24
         dflags <- getSessionDynFlags
    
    25 25
         liftIO $ cont dflags
    
    26 26