11{-# LANGUAGE CPP, ForeignFunctionInterface #-}
2+ {-# LANGUAGE BangPatterns #-}
23{-# LANGUAGE InterruptibleFFI #-}
34module System.Process.Windows
45 ( mkProcessHandle
@@ -22,6 +23,7 @@ module System.Process.Windows
2223import System.Process.Common
2324import Control.Concurrent
2425import Control.Exception
26+ import Control.Monad
2527import Data.Bits
2628import Foreign.C
2729import Foreign.Marshal
@@ -65,14 +67,14 @@ throwErrnoIfBadPHandle = throwErrnoIfNull
6567
6668-- On Windows, we have to close this HANDLE when it is no longer required,
6769-- hence we add a finalizer to it
68- mkProcessHandle :: PHANDLE -> PHANDLE -> IO ProcessHandle
69- mkProcessHandle h job = do
70+ mkProcessHandle :: PHANDLE -> Bool -> PHANDLE -> IO ProcessHandle
71+ mkProcessHandle h ignore_signals job = do
7072 m <- if job == nullPtr
7173 then newMVar (OpenHandle h)
7274 else newMVar (OpenExtHandle h job)
7375 _ <- mkWeakMVar m (processHandleFinaliser m)
7476 l <- newMVar ()
75- return (ProcessHandle m False l)
77+ return (ProcessHandle m ignore_signals l)
7678
7779processHandleFinaliser :: MVar ProcessHandle__ -> IO ()
7880processHandleFinaliser m =
@@ -114,7 +116,6 @@ createProcess_Internal_mio fun def@CreateProcess{
114116 std_err = mb_stderr,
115117 close_fds = mb_close_fds,
116118 create_group = mb_create_group,
117- delegate_ctlc = _ignored,
118119 detach_console = mb_detach_console,
119120 create_new_console = mb_create_new_console,
120121 new_session = mb_new_session,
@@ -166,7 +167,7 @@ createProcess_Internal_wrapper _fun CreateProcess{
166167 cmdspec = cmdsp,
167168 cwd = mb_cwd,
168169 env = mb_env,
169- delegate_ctlc = _ignored }
170+ delegate_ctlc = ignore_signals }
170171 action
171172 = do
172173 let lenPtr = sizeOf (undefined :: WordPtr )
@@ -183,8 +184,15 @@ createProcess_Internal_wrapper _fun CreateProcess{
183184 (proc_handle, hndStdInput, hndStdOutput, hndStdError)
184185 <- action pfdStdInput pfdStdOutput pfdStdError hJob pEnv pWorkDir pcmdline
185186
187+ -- If we have successfully created the process then check if we have to
188+ -- detach from the console. I'm not sure why the posix version changes
189+ -- the state right before creating the child process, but doing so here
190+ -- means the first child also inherits this
191+ when ignore_signals $
192+ startDelegateControlC
193+
186194 phJob <- peek hJob
187- ph <- mkProcessHandle proc_handle phJob
195+ ph <- mkProcessHandle proc_handle ignore_signals phJob
188196 return ProcRetHandles { hStdInput = hndStdInput
189197 , hStdOutput = hndStdOutput
190198 , hStdError = hndStdError
@@ -203,7 +211,6 @@ createProcess_Internal_winio fun def@CreateProcess{
203211 std_err = mb_stderr,
204212 close_fds = mb_close_fds,
205213 create_group = mb_create_group,
206- delegate_ctlc = _ignored,
207214 detach_console = mb_detach_console,
208215 create_new_console = mb_create_new_console,
209216 new_session = mb_new_session,
@@ -260,18 +267,71 @@ createProcess_Internal_winio fun def@CreateProcess{
260267runInteractiveProcess_lock :: MVar ()
261268runInteractiveProcess_lock = unsafePerformIO $ newMVar ()
262269
263- -- The following functions are always present in the export list. For
264- -- compatibility with the non-Windows code, we provide the same functions with
265- -- matching type signatures, but implemented as no-ops. For details, see:
266- -- <https://github.com/haskell/process/pull/21>
270+ -- ----------------------------------------------------------------------------
271+ -- Delegated control-C handling on Windows
272+
273+ -- See https://learn.microsoft.com/en-us/windows/console/setconsolectrlhandler
274+ --
275+ -- While running an interactive console process like ghci or a shell, we want
276+ -- to let that process handle Ctl-C keyboard interrupts how it sees fit.
277+ -- So that means we need to ignore the CTRL_C_EVENT/CTRL_BREAK_EVENT Windows
278+ -- events while we're running such programs.
279+ --
280+ -- If we run multiple programs like this concurrently then we have to be
281+ -- careful to avoid messing up the signal handlers. We keep a count and only
282+ -- restore when the last one has finished.
283+ --
284+ -- To do this we have to use SetConsoleCtrlHandler which masks the events for
285+ -- the current process and any child it creates from that point.
286+ --
287+ -- In this case we can't use FreeConsole/AttachConsole since those destroy
288+ -- the signal handler stack for the application when called. This means we'd
289+ -- have to recreate them and process doesn't know what's there.
290+
291+ {-# NOINLINE runInteractiveProcess_delegate_ctlc #-}
292+ runInteractiveProcess_delegate_ctlc :: MVar (Maybe (Int ))
293+ runInteractiveProcess_delegate_ctlc = unsafePerformIO $ newMVar Nothing
294+
267295startDelegateControlC :: IO ()
268- startDelegateControlC = return ()
296+ startDelegateControlC =
297+ modifyMVar_ runInteractiveProcess_delegate_ctlc $ \ delegating -> do
298+ case delegating of
299+ Nothing -> do
300+ -- We're going to ignore ^C in the parent while there are any
301+ -- processes using ^C delegation.
302+ --
303+ -- If another thread runs another process without using
304+ -- delegation while we're doing this then it will inherit the
305+ -- ignore ^C status.
306+ _ <- c_setConsoleCtrlHandler nullPtr True
307+ return (Just 1 )
308+
309+ Just count -> do
310+ -- If we're already doing it, just increment the count
311+ let ! count' = count + 1
312+ return (Just count')
313+
314+ stopDelegateControlC :: IO ()
315+ stopDelegateControlC =
316+ modifyMVar_ runInteractiveProcess_delegate_ctlc $ \ delegating -> do
317+ case delegating of
318+ Just 1 -> do
319+ -- Last process, so restore the old signal handlers
320+ _ <- c_setConsoleCtrlHandler nullPtr False
321+ return Nothing
322+
323+ Just count -> do
324+ -- Not the last, just decrement the count
325+ let ! count' = count - 1
326+ return (Just count')
327+
328+ Nothing -> return Nothing -- should be impossible
269329
270330endDelegateControlC :: ExitCode -> IO ()
271- endDelegateControlC _ = return ()
331+ -- I don't think there's a standard exit code for program interruptions
332+ -- on Windows, so I'll just ignore it for now.
333+ endDelegateControlC _ = stopDelegateControlC
272334
273- stopDelegateControlC :: IO ()
274- stopDelegateControlC = return ()
275335
276336-- End no-op functions
277337
@@ -308,6 +368,12 @@ foreign import WINDOWS_CCONV unsafe "TerminateJobObject"
308368 -> CUInt
309369 -> IO Bool
310370
371+ foreign import WINDOWS_CCONV unsafe " SetConsoleCtrlHandler"
372+ c_setConsoleCtrlHandler
373+ :: Ptr ()
374+ -> Bool
375+ -> IO Bool
376+
311377foreign import ccall interruptible " waitForJobCompletion" -- NB. safe - can block
312378 c_waitForJobCompletion
313379 :: PHANDLE
0 commit comments