Clarify behaviour of `close_fds` on Windows
The documentation of close_fds states that:
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).
It's quite unclear what "only works" means. Will things blow up if I set close_fds and CreatePipe? My best guess is something like "if any of std_in, std_out, std_err are not Inherit, those will be closed too".
Specifically, based on the documentation, I can't answer the question whether
(rstdin, wstdin) <- createPipe
(_,_,_,p) <- createProcess (proc "cat" []) { std_in = UseHandle rstdin, close_fds = True }
hPutStr wstdin "foo"
hClose wstdin
waitProcess p
should work on Windows. (This hangs on Linux without close_fds, (apparently) because the child inherits the open write-end, causing the pipe to stay open despite hClose.)
@Mistuke do you have any thoughts here?
It's quite unclear what "only works" means. Will things blow up if I set
close_fdsandCreatePipe? My best guess is something like "if any ofstd_in,std_out,std_errare notInherit, those will be closed too".
The answer to this is.. it depends.
So one clarification close_fds doesn't actually close anything on Windows. It only changes the state of the child process's ability to inherit any handles at all or not.
specifically, when close_fds is specified and stdin, stderr, stdout have not been redirected then we turns off handle inheritance completely. This behavior is an attempt to work around https://gitlab.haskell.org/ghc/ghc/-/issues/3231
should work on Windows. (This hangs on Linux without close_fds, (apparently) because the child inherits the open write-end, causing the pipe to stay open despite hClose.)
This is again, it depends. Point of clarification, FDs on Windows can't be inherited at all. FDs are a somewhat pseudo concept, in that the FD numbers are mapped inside each process. i.e. FD 3 in parent doesn't need to point to the same object as a child. The mapping is maintained in process and is unique for each process.
What can be inherited on Windows are HANDLE. Inside each process there's an FD <-> HANDLE table mapping if something like _open_osfhandle is called.
Now GHC on Windows has two I/O managers, and the behavior depends on the I/O manager:
So for:
(rstdin, wstdin) <- createPipe
(_,_,_,p) <- createProcess (proc "cat" []) { std_in = UseHandle rstdin, close_fds = True }
hPutStr wstdin "foo"
hClose wstdin
waitProcess p
- On MIO
No, this wouldn't hang. createPipe creates uninheritable handles by default. During createProcess for any handles explicitly redirected we create a duplicate handle with the inherit state set to true. So the child never gets to see wstdin nor rstdin (directly).
- On WinIO
Yes, this would hang because createPipe creates inheritable handles for both rstdin and wstdin and you have redirected a handle so we don't change the process handle inheritance state.
However most WinIO relies on being able to share all HANDLEs created.
The situation is not ideal... I think it would be better if createPipe took an argument to explicitly control inheritence state of HANDLEs or have a variant that explicitly creates non-inheritable Handles.
Thank you for the detailed explanation. I see how that is hard to document clearly :/
I like the general idea of providing an API where createPipe generally has "non-inheritable" handles -- my understanding that could also work be done on (some?) unix systems by setting CLOEXEC.
(I've solved my particular problem that prompted this issue by reverting to use CreatePipe instead of UseHandle -- don't actually know in how far the code works right on Windows, but I'm no longer potentially breaking things.)