Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Process.exec stream redirection on Windows #14986

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 43 additions & 16 deletions src/crystal/system/win32/process.cr
Original file line number Diff line number Diff line change
Expand Up @@ -326,9 +326,9 @@ struct Crystal::System::Process
end

private def self.try_replace(command_args, env, clear_env, input, output, error, chdir)
reopen_io(input, ORIGINAL_STDIN)
reopen_io(output, ORIGINAL_STDOUT)
reopen_io(error, ORIGINAL_STDERR)
old_input_fd = reopen_io(input, ORIGINAL_STDIN)
old_output_fd = reopen_io(output, ORIGINAL_STDOUT)
old_error_fd = reopen_io(error, ORIGINAL_STDERR)

ENV.clear if clear_env
env.try &.each do |key, val|
Expand All @@ -351,11 +351,18 @@ struct Crystal::System::Process
argv << Pointer(LibC::WCHAR).null

LibC._wexecvp(command, argv)

# exec failed; restore the original C runtime file descriptors
errno = Errno.value
LibC._dup2(old_input_fd, 0)
LibC._dup2(old_output_fd, 1)
LibC._dup2(old_error_fd, 2)
errno
end

def self.replace(command_args, env, clear_env, input, output, error, chdir) : NoReturn
try_replace(command_args, env, clear_env, input, output, error, chdir)
raise_exception_from_errno(command_args.is_a?(String) ? command_args : command_args[0])
errno = try_replace(command_args, env, clear_env, input, output, error, chdir)
raise_exception_from_errno(command_args.is_a?(String) ? command_args : command_args[0], errno)
end

private def self.raise_exception_from_errno(command, errno = Errno.value)
Expand All @@ -367,21 +374,41 @@ struct Crystal::System::Process
end
end

# Replaces the C standard streams' file descriptors, not Win32's, since
# `try_replace` uses the C `LibC._wexecvp` and only cares about the former.
# Returns a duplicate of the original file descriptor
private def self.reopen_io(src_io : IO::FileDescriptor, dst_io : IO::FileDescriptor)
src_io = to_real_fd(src_io)
unless src_io.system_blocking?
raise IO::Error.new("Non-blocking streams are not supported in `Process.exec`", target: src_io)
end

dst_io.reopen(src_io)
dst_io.blocking = true
dst_io.close_on_exec = false
end
src_fd =
case src_io
when STDIN then 0
when STDOUT then 1
when STDERR then 2
else
LibC._open_osfhandle(src_io.windows_handle, 0)
end

private def self.to_real_fd(fd : IO::FileDescriptor)
case fd
when STDIN then ORIGINAL_STDIN
when STDOUT then ORIGINAL_STDOUT
when STDERR then ORIGINAL_STDERR
else fd
dst_fd =
case dst_io
when ORIGINAL_STDIN then 0
when ORIGINAL_STDOUT then 1
when ORIGINAL_STDERR then 2
else
raise "BUG: Invalid destination IO"
end

return src_fd if dst_fd == src_fd

orig_src_fd = LibC._dup(src_fd)

if LibC._dup2(src_fd, dst_fd) == -1
raise IO::Error.from_errno("Failed to replace C file descriptor", target: dst_io)
end

orig_src_fd
end

def self.chroot(path)
Expand Down
5 changes: 3 additions & 2 deletions src/lib_c/x86_64-windows-msvc/c/io.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ require "c/stdint"

lib LibC
fun _wexecvp(cmdname : WCHAR*, argv : WCHAR**) : IntPtrT
fun _open_osfhandle(osfhandle : HANDLE, flags : LibC::Int) : LibC::Int
fun _dup(fd : Int) : Int
fun _dup2(fd1 : Int, fd2 : Int) : Int

# unused
fun _open_osfhandle(osfhandle : HANDLE, flags : LibC::Int) : LibC::Int
fun _get_osfhandle(fd : Int) : IntPtrT
fun _close(fd : Int) : Int
fun _dup2(fd1 : Int, fd2 : Int) : Int
fun _isatty(fd : Int) : Int
fun _write(fd : Int, buffer : UInt8*, count : UInt) : Int
fun _read(fd : Int, buffer : UInt8*, count : UInt) : Int
Expand Down
Loading