Skip to content

Commit 383181f

Browse files
authored
Merge pull request #278 from Mistuke/wip/implement-delegate-ctrl-windows
Windows: Implement delegate_ctlc on Windows
2 parents 4655553 + 11b3ea9 commit 383181f

File tree

4 files changed

+82
-18
lines changed

4 files changed

+82
-18
lines changed

System/Process/Common.hs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,6 @@ data CreateProcess = CreateProcess{
9191
close_fds :: Bool, -- ^ Close all file descriptors except stdin, stdout and stderr in the new process (on Windows, only works if std_in, std_out, and std_err are all Inherit). This implementation will call close on every fd from 3 to the maximum of open files, which can be slow for high maximum of open files.
9292
create_group :: Bool, -- ^ Create a new process group
9393
delegate_ctlc:: Bool, -- ^ Delegate control-C handling. Use this for interactive console processes to let them handle control-C themselves (see below for details).
94-
--
95-
-- On Windows this has no effect.
9694
--
9795
-- @since 1.2.0.0
9896
detach_console :: Bool, -- ^ Use the windows DETACHED_PROCESS flag when creating the process; does nothing on other platforms.

System/Process/Internals.hs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,6 @@ runGenProcess_
171171
-> Maybe CLong -- ^ handler for SIGINT
172172
-> Maybe CLong -- ^ handler for SIGQUIT
173173
-> IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
174-
-- On Windows, setting delegate_ctlc has no impact
175174
runGenProcess_ fun c (Just sig) (Just sig') | isDefaultSignal sig && sig == sig'
176175
= createProcess_ fun c { delegate_ctlc = True }
177176
runGenProcess_ fun c _ _ = createProcess_ fun c

System/Process/Windows.hsc

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{-# LANGUAGE CPP, ForeignFunctionInterface #-}
2+
{-# LANGUAGE BangPatterns #-}
23
{-# LANGUAGE InterruptibleFFI #-}
34
module System.Process.Windows
45
( mkProcessHandle
@@ -22,6 +23,7 @@ module System.Process.Windows
2223
import System.Process.Common
2324
import Control.Concurrent
2425
import Control.Exception
26+
import Control.Monad
2527
import Data.Bits
2628
import Foreign.C
2729
import 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

7779
processHandleFinaliser :: MVar ProcessHandle__ -> IO ()
7880
processHandleFinaliser 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{
260267
runInteractiveProcess_lock :: MVar ()
261268
runInteractiveProcess_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+
267295
startDelegateControlC :: 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

270330
endDelegateControlC :: 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+
311377
foreign import ccall interruptible "waitForJobCompletion" -- NB. safe - can block
312378
c_waitForJobCompletion
313379
:: PHANDLE

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## unreleased
44

55
* Fix deadlock when waiting for process completion and process jobs [#273](https://github.com/haskell/process/issues/273)
6+
* Support delegate_ctlc on Windows. [#278](https://github.com/haskell/process/pull/278)
67

78
## 1.6.17.0 *February 2023*
89

0 commit comments

Comments
 (0)