From ed14ad5ee4b0422200a18f63350ab83d90153f61 Mon Sep 17 00:00:00 2001 From: RX14 Date: Thu, 25 Jan 2018 16:34:27 +0000 Subject: [PATCH] Port Process to win32 --- spec/std/process_spec.cr | 88 ++++- src/crystal/system/process.cr | 11 + src/crystal/system/unix/process.cr | 151 ++++++++ src/crystal/system/win32/file.cr | 5 +- src/crystal/system/win32/file_descriptor.cr | 2 +- src/crystal/system/win32/process.cr | 163 ++++++++ src/kernel.cr | 22 +- .../c/processthreadsapi.cr | 43 +++ src/lib_c/x86_64-windows-msvc/c/stdio.cr | 8 + src/lib_c/x86_64-windows-msvc/c/stdlib.cr | 1 + src/lib_c/x86_64-windows-msvc/c/synchapi.cr | 1 + src/lib_c/x86_64-windows-msvc/c/winbase.cr | 10 + src/lib_c/x86_64-windows-msvc/c/winnt.cr | 6 + src/prelude.cr | 2 +- src/process.cr | 349 ++++++++---------- src/process/status.cr | 55 ++- src/windows_stubs.cr | 4 + 17 files changed, 677 insertions(+), 244 deletions(-) create mode 100644 src/crystal/system/process.cr create mode 100644 src/crystal/system/unix/process.cr create mode 100644 src/crystal/system/win32/process.cr create mode 100644 src/lib_c/x86_64-windows-msvc/c/processthreadsapi.cr diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index 0ddd045f6b5c..dc89e0255d40 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -3,14 +3,46 @@ require "process" require "./spec_helper" require "../spec_helper" +private def exit_code_command(code) + if {{flag?(:win32)}} + {"cmd.exe", {"/c", "exit #{code}"}} + else + case code + when 0 + {"true", [] of String} + when 1 + {"false", [] of String} + else + {"/bin/sh", {"-c", "exit #{code}"}} + end + end +end + +private def shell_command(command) + if {{flag?(:win32)}} + {"cmd.exe", {"/c", command}} + else + {"/bin/sh", {"-c", command}} + end +end + +private def stdin_to_stdout_command + if {{flag?(:win32)}} + # {"powershell.exe", {"-C", "$Input"}} + {"C:\\Crystal\\cat.exe", [] of String} + else + {"/bin/cat", [] of String} + end +end + describe Process do it "runs true" do - process = Process.new("true") + process = Process.new(*exit_code_command(0)) process.wait.exit_code.should eq(0) end it "runs false" do - process = Process.new("false") + process = Process.new(*exit_code_command(1)) process.wait.exit_code.should eq(1) end @@ -21,56 +53,72 @@ describe Process do end it "run waits for the process" do - Process.run("true").exit_code.should eq(0) + Process.run(*exit_code_command(0)).exit_code.should eq(0) end it "runs true in block" do - Process.run("true") { } + Process.run(*exit_code_command(0)) { } $?.exit_code.should eq(0) end it "receives arguments in array" do - Process.run("/bin/sh", ["-c", "exit 123"]).exit_code.should eq(123) + command, args = exit_code_command(123) + Process.run(command, args.to_a).exit_code.should eq(123) end it "receives arguments in tuple" do - Process.run("/bin/sh", {"-c", "exit 123"}).exit_code.should eq(123) + command, args = exit_code_command(123) + Process.run(command, args.as(Tuple)).exit_code.should eq(123) end it "redirects output to /dev/null" do # This doesn't test anything but no output should be seen while running tests - Process.run("/bin/ls", output: Process::Redirect::Close).exit_code.should eq(0) + if {{flag?(:win32)}} + command = "cmd.exe" + args = {"/c", "dir"} + else + command = "/bin/ls" + args = [] of String + end + Process.run(command, args, output: Process::Redirect::Close).exit_code.should eq(0) end it "gets output" do - value = Process.run("/bin/sh", {"-c", "echo hello"}) do |proc| + value = Process.run(*shell_command("echo hello")) do |proc| proc.output.gets_to_end end - value.should eq("hello\n") + if {{flag?(:win32)}} + value.should eq("hello\r\n") + else + value.should eq("hello\n") + end end - it "sends input in IO" do - value = Process.run("/bin/cat", input: IO::Memory.new("hello")) do |proc| + # TODO: reenable after implementing fibers/asyncio + pending_win32 "sends input in IO" do + value = Process.run(*stdin_to_stdout_command, input: IO::Memory.new("hello")) do |proc| proc.input?.should be_nil proc.output.gets_to_end end value.should eq("hello") end - it "sends output to IO" do + # TODO: reenable after implementing fibers/asyncio + pending_win32 "sends output to IO" do output = IO::Memory.new - Process.run("/bin/sh", {"-c", "echo hello"}, output: output) + Process.run(*shell_command("echo hello"), output: output) output.to_s.should eq("hello\n") end - it "sends error to IO" do + # TODO: reenable after implementing fibers/asyncio + pending_win32 "sends error to IO" do error = IO::Memory.new - Process.run("/bin/sh", {"-c", "echo hello 1>&2"}, error: error) + Process.run(*shell_command("echo hello 1>&2"), error: error) error.to_s.should eq("hello\n") end it "controls process in block" do - value = Process.run("/bin/cat") do |proc| + value = Process.run(*stdin_to_stdout_command, error: :inherit) do |proc| proc.input.print "hello" proc.input.close proc.output.gets_to_end @@ -79,7 +127,7 @@ describe Process do end it "closes ios after block" do - Process.run("/bin/cat") { } + Process.run(*stdin_to_stdout_command) { } $?.exit_code.should eq(0) end @@ -234,16 +282,16 @@ describe Process do pwd = Process::INITIAL_PWD crystal_path = File.join(pwd, "bin", "crystal") - it "resolves absolute executable" do + pending_win32 "resolves absolute executable" do Process.find_executable(File.join(pwd, "bin", "crystal")).should eq(crystal_path) end - it "resolves relative executable" do + pending_win32 "resolves relative executable" do Process.find_executable(File.join("bin", "crystal")).should eq(crystal_path) Process.find_executable(File.join("..", File.basename(pwd), "bin", "crystal")).should eq(crystal_path) end - it "searches within PATH" do + pending_win32 "searches within PATH" do (path = Process.find_executable("ls")).should_not be_nil path.not_nil!.should match(/#{File::SEPARATOR}ls$/) diff --git a/src/crystal/system/process.cr b/src/crystal/system/process.cr new file mode 100644 index 000000000000..f791d962f780 --- /dev/null +++ b/src/crystal/system/process.cr @@ -0,0 +1,11 @@ +# :nodoc: +module Crystal::System::Process +end + +{% if flag?(:unix) %} + require "./unix/process" +{% elsif flag?(:win32) %} + require "./win32/process" +{% else %} + {% raise "No Crystal::System::Process implementation available" %} +{% end %} diff --git a/src/crystal/system/unix/process.cr b/src/crystal/system/unix/process.cr new file mode 100644 index 000000000000..eaa3887b3aae --- /dev/null +++ b/src/crystal/system/unix/process.cr @@ -0,0 +1,151 @@ +require "c/signal" +require "c/stdlib" +require "c/sys/times" +require "c/sys/wait" +require "c/unistd" + +module Crystal::System::Process + def self.exit(status) + LibC.exit(status) + end + + def self.pid + LibC.getpid + end + + def self.parent_pid + LibC.getppid + end + + def self.process_gid + ret = LibC.getpgid(0) + raise Errno.new("getpgid") if ret < 0 + ret + end + + def self.process_gid(pid) + # Disallow users from depending on ppid(0) instead of `process_gid` + raise Errno.new("getpgid", Errno::EINVAL) if pid == 0 + + ret = LibC.getpgid(pid) + raise Errno.new("getpgid") if ret < 0 + ret + end + + def self.kill(pid, signal) + ret = LibC.kill(pid, signal) + raise Errno.new("kill") if ret < 0 + end + + def self.exists?(pid) + if LibC.kill(pid, 0) == 0 + true + elsif Errno.value == Errno::ESRCH + false + else + raise Errno.new("kill") + end + end + + def self.fork + case pid = LibC.fork + when -1 + raise Errno.new("fork") + when 0 + nil + else + pid + end + end + + def self.spawn(command, args, env, clear_env, input, output, error, chdir) : Int32 + reader_pipe, writer_pipe = IO.pipe + + if pid = self.fork + writer_pipe.close + bytes = uninitialized UInt8[4] + if reader_pipe.read(bytes.to_slice) == 4 + errno = IO::ByteFormat::SystemEndian.decode(Int32, bytes.to_slice) + message_size = reader_pipe.read_bytes(Int32) + if message_size > 0 + message = String.build(message_size) { |io| IO.copy(reader_pipe, io, message_size) } + end + reader_pipe.close + raise Errno.new(message, errno) + end + reader_pipe.close + + pid + else + begin + reader_pipe.close + writer_pipe.close_on_exec = true + self.replace(command, args, env, clear_env, input, output, error, chdir) + rescue ex : Errno + writer_pipe.write_bytes(ex.errno) + writer_pipe.write_bytes(ex.message.try(&.bytesize) || 0) + writer_pipe << ex.message + writer_pipe.close + rescue ex + ex.inspect_with_backtrace STDERR + ensure + LibC._exit 127 + end + end + end + + private def self.to_real_fd(fd : IO::FileDescriptor) + case fd + when STDIN + ORIGINAL_STDIN + when STDOUT + ORIGINAL_STDOUT + when STDERR + ORIGINAL_STDERR + else + fd + end + end + + private def self.reopen_io(src_io : IO::FileDescriptor, dst_io : IO::FileDescriptor) + src_io = to_real_fd(src_io) + + if src_io.closed? + dst_io.close + return + end + dst_io.reopen(src_io) if src_io.fd != dst_io.fd + dst_io.blocking = true + dst_io.close_on_exec = false + end + + def self.replace(command, args, env, clear_env, input, output, error, chdir) : NoReturn + reopen_io(input, ORIGINAL_STDIN) + reopen_io(output, ORIGINAL_STDOUT) + reopen_io(error, ORIGINAL_STDERR) + + ENV.clear if clear_env + env.try &.each do |key, val| + if val + ENV[key] = val + else + ENV.delete key + end + end + + ::Dir.cd(chdir) if chdir + + argv = [command.check_no_null_byte.to_unsafe] + args.try &.each do |arg| + argv << arg.check_no_null_byte.to_unsafe + end + argv << Pointer(UInt8).null + + LibC.execvp(command, argv) + raise Errno.new("execvp") + end + + def self.wait(pid) + Event::SignalChildHandler.instance.waitpid(pid) + end +end diff --git a/src/crystal/system/win32/file.cr b/src/crystal/system/win32/file.cr index f50d2e4ea940..764ee50fa349 100644 --- a/src/crystal/system/win32/file.cr +++ b/src/crystal/system/win32/file.cr @@ -7,7 +7,7 @@ require "c/sys/stat" module Crystal::System::File def self.open(filename : String, mode : String, perm : Int32 | ::File::Permissions) : LibC::Int perm = ::File::Permissions.new(perm) if perm.is_a? Int32 - oflag = open_flag(mode) | LibC::O_BINARY + oflag = open_flag(mode) | LibC::O_BINARY | LibC::O_NOINHERIT # Only the owner writable bit is used, since windows only supports # the read only attribute. @@ -28,7 +28,8 @@ module Crystal::System::File def self.mktemp(name : String, extension : String?) : {LibC::Int, String} path = "#{tempdir}\\#{name}.#{::Random::Secure.hex}#{extension}" - fd = LibC._wopen(to_windows_path(path), LibC::O_RDWR | LibC::O_CREAT | LibC::O_EXCL | LibC::O_BINARY, ::File::DEFAULT_CREATE_PERMISSIONS) + mode = LibC::O_RDWR | LibC::O_CREAT | LibC::O_EXCL | LibC::O_BINARY | LibC::O_NOINHERIT + fd = LibC._wopen(to_windows_path(path), mode, ::File::DEFAULT_CREATE_PERMISSIONS) if fd == -1 raise Errno.new("Error creating temporary file at #{path.inspect}") end diff --git a/src/crystal/system/win32/file_descriptor.cr b/src/crystal/system/win32/file_descriptor.cr index 95a35f743803..1d964a8f12a3 100644 --- a/src/crystal/system/win32/file_descriptor.cr +++ b/src/crystal/system/win32/file_descriptor.cr @@ -120,7 +120,7 @@ module Crystal::System::FileDescriptor def self.pipe(read_blocking, write_blocking) pipe_fds = uninitialized StaticArray(LibC::Int, 2) - if LibC._pipe(pipe_fds, 8192, LibC::O_BINARY) != 0 + if LibC._pipe(pipe_fds, 8192, LibC::O_BINARY | LibC::O_NOINHERIT) != 0 raise Errno.new("Could not create pipe") end diff --git a/src/crystal/system/win32/process.cr b/src/crystal/system/win32/process.cr new file mode 100644 index 000000000000..4dd3906d9cce --- /dev/null +++ b/src/crystal/system/win32/process.cr @@ -0,0 +1,163 @@ +require "c/processthreadsapi" + +module Crystal::System::Process + def self.exit(status) + LibC.exit(status) + end + + def self.pid + LibC.GetCurrentProcessId + end + + def self.parent_pid + # TODO: Implement this using CreateToolhelp32Snapshot + raise NotImplementedError.new("Process.ppid") + end + + def self.process_gid + raise NotImplementedError.new("Process.pgid") + end + + def self.process_gid(pid) + raise NotImplementedError.new("Process.pgid") + end + + def self.fork + raise NotImplementedError.new("Process.fork") + end + + private def self.args_to_string(command : String, args, io : IO) + command_args = Array(String).new((args.try(&.size) || 0) + 1) + command_args << command + args.try &.each do |arg| + command_args << arg + end + + first_arg = true + command_args.join(' ', io) do |arg| + quotes = first_arg || arg.size == 0 || arg.includes?(' ') || arg.includes?('\t') + first_arg = false + + io << '"' if quotes + + slashes = 0 + arg.each_char do |c| + case c + when '\\' + slashes += 1 + when '"' + (slashes + 1).times { io << '\\' } + slashes = 0 + else + slashes = 0 + end + + io << c + end + + if quotes + slashes.times { io << '\\' } + io << '"' + end + end + end + + private def self.handle_from_io(io : IO::FileDescriptor, parent_io) + ret = LibC._get_osfhandle(io.fd) + raise Errno.new("_get_osfhandle") if ret == -1 + source_handle = LibC::HANDLE.new(ret) + + cur_proc = LibC.GetCurrentProcess + if LibC.DuplicateHandle(cur_proc, source_handle, cur_proc, out new_handle, 0, true, LibC::DUPLICATE_SAME_ACCESS) == 0 + raise WinError.new("DuplicateHandle") + end + + new_handle + end + + def self.spawn(command : String, args : Enumerable(String)?, + env : ::Process::Env, clear_env : Bool, + input, output, error, + chdir : String?) : UInt32 + raise NotImplementedError.new("Process.new with env or clear_env options") if env || clear_env + raise NotImplementedError.new("Process.new with chdir set") if chdir + + args = String.build { |io| args_to_string(command, args, io) } + + puts + puts args + + command = to_windows_string(command) + args = to_windows_string(args) + + startup_info = LibC::STARTUPINFOW.new + startup_info.cb = sizeof(LibC::STARTUPINFOW) + startup_info.dwFlags = LibC::STARTF_USESTDHANDLES + + startup_info.hStdInput = handle_from_io(input, STDIN) + startup_info.hStdOutput = handle_from_io(output, STDOUT) + startup_info.hStdError = handle_from_io(error, STDERR) + + process_info = LibC::PROCESS_INFORMATION.new + + if LibC.CreateProcessW( + nil, args, nil, nil, true, 0, nil, nil, + pointerof(startup_info), pointerof(process_info) + ) == 0 + raise WinError.new("CreateProcess") + end + + close_handle(process_info.hProcess) + close_handle(process_info.hThread) + + close_handle(startup_info.hStdInput) + close_handle(startup_info.hStdOutput) + close_handle(startup_info.hStdError) + + process_info.dwProcessId + end + + private def self.close_handle(handle) : Nil + if LibC.CloseHandle(handle) == 0 + raise WinError.new("CloseHandle") + end + end + + def self.replace(command, argv, env, clear_env, input, output, error, chdir) : NoReturn + raise NotImplementedError.new("Process.exec") + end + + def self.wait(pid) + handle = LibC.OpenProcess(LibC::SYNCHRONIZE | LibC::PROCESS_QUERY_LIMITED_INFORMATION, false, pid) + raise WinError.new("OpenProcess") if handle == 0 + + if LibC.WaitForSingleObject(handle, LibC::INFINITE) != 0 + raise WinError.new("WaitForSingleObject") + end + + # WaitForSingleObject returns immediately once ExitProcess is called in the child, but + # the process still has yet to be destructed by the OS and have it's memory unmapped. + # Since the semantics on unix are that the resources of a process have been released once + # waitpid returns, we wait 5 milliseconds to attempt to replicate this behaviour. + sleep 5.milliseconds + + if LibC.GetExitCodeProcess(handle, out exit_code) != 0 + if exit_code == LibC::STILL_ACTIVE + raise "BUG: process still active" + else + exit_code + end + else + raise WinError.new("GetExitCodeProcess") + end + end + + def self.kill(pid, signal) + raise NotImplementedError.new("Process.kill with signals other than Signal::KILL") unless signal == 9 + raise NotImplementedError.new("Process.kill") + end + + private def self.to_windows_string(string : String) : LibC::LPWSTR + string.check_no_null_byte.to_utf16.to_unsafe + end +end diff --git a/src/kernel.cr b/src/kernel.cr index 91b96e9d2e00..2446f1e9d70d 100644 --- a/src/kernel.cr +++ b/src/kernel.cr @@ -8,6 +8,10 @@ STDIN = IO::FileDescriptor.from_stdio(0) STDOUT = IO::FileDescriptor.from_stdio(1).tap { |f| f.flush_on_newline = true } STDERR = IO::FileDescriptor.from_stdio(2).tap { |f| f.flush_on_newline = true } + + ORIGINAL_STDIN = IO::FileDescriptor.new(0, blocking: true) + ORIGINAL_STDOUT = IO::FileDescriptor.new(1, blocking: true).tap { |f| f.flush_on_newline = true } + ORIGINAL_STDERR = IO::FileDescriptor.new(2, blocking: true).tap { |f| f.flush_on_newline = true } {% end %} PROGRAM_NAME = String.new(ARGV_UNSAFE.value) @@ -456,14 +460,20 @@ def abort(message, status = 1) : NoReturn end class Process + @@after_fork_child_callbacks : Array(-> Nil)? + # Hooks are defined here due to load order problems. def self.after_fork_child_callbacks - @@after_fork_child_callbacks ||= [ - ->Scheduler.after_fork, - ->Crystal::Signal.after_fork, - ->Crystal::SignalChildHandler.after_fork, - ->Random::DEFAULT.new_seed, - ] of -> Nil + {% begin %} + @@after_fork_child_callbacks ||= [ + {% unless flag?(:win32) %} + ->Scheduler.after_fork, + ->Crystal::Signal.after_fork, + ->Crystal::SignalChildHandler.after_fork, + {% end %} + ->Random::DEFAULT.new_seed, + ] of -> Nil + {% end %} end end diff --git a/src/lib_c/x86_64-windows-msvc/c/processthreadsapi.cr b/src/lib_c/x86_64-windows-msvc/c/processthreadsapi.cr new file mode 100644 index 000000000000..c757a0b8a87b --- /dev/null +++ b/src/lib_c/x86_64-windows-msvc/c/processthreadsapi.cr @@ -0,0 +1,43 @@ +require "c/int_safe" + +lib LibC + fun GetProcessId(process : HANDLE) : DWORD + fun GetCurrentProcessId : DWORD + fun OpenProcess(dwDesiredAccess : DWORD, bInheritHandle : BOOL, dwProcessId : DWORD) : HANDLE + fun GetExitCodeProcess(hProcess : HANDLE, lpExitCode : DWORD*) : BOOL + fun CreateProcessW(lpApplicationName : LPWSTR, lpCommandLine : LPWSTR, + lpProcessAttributes : Void*, lpThreadAttributes : Void*, + bInheritHandles : BOOL, dwCreationFlags : DWORD, + lpEnvironment : Void*, lpCurrentDirectory : LPWSTR, + lpStartupInfo : STARTUPINFOW*, lpProcessInformation : PROCESS_INFORMATION*) : BOOL + + struct STARTUPINFOW + cb : DWORD + lpReserved : LPWSTR + lpDesktop : LPWSTR + lpTitle : LPWSTR + dwX : DWORD + dwY : DWORD + dwXSize : DWORD + dwYSize : DWORD + dwXCountChars : DWORD + dwYCountChars : DWORD + dwFillAttribute : DWORD + dwFlags : DWORD + wShowWindow : WORD + cbReserved2 : WORD + lpReserved2 : Void* + hStdInput : HANDLE + hStdOutput : HANDLE + hStdError : HANDLE + end + + struct PROCESS_INFORMATION + hProcess : HANDLE + hThread : HANDLE + dwProcessId : DWORD + dwThreadId : DWORD + end + + fun GetCurrentProcess : HANDLE +end diff --git a/src/lib_c/x86_64-windows-msvc/c/stdio.cr b/src/lib_c/x86_64-windows-msvc/c/stdio.cr index b9c57ae4133e..ad392e5d5341 100644 --- a/src/lib_c/x86_64-windows-msvc/c/stdio.cr +++ b/src/lib_c/x86_64-windows-msvc/c/stdio.cr @@ -5,6 +5,14 @@ lib LibC fun rename(old : Char*, new : Char*) : Int fun vsnprintf(str : Char*, size : SizeT, format : Char*, ap : VaList) : Int fun snprintf = __crystal_snprintf(str : Char*, size : SizeT, format : Char*, ...) : Int + + P_WAIT = 0 + P_NOWAIT = 1 + P_OVERLAY = 2 + P_NOWAITO = 3 + P_DETACH = 4 + + fun _wspawnvp(mode : Int, cmdname : WCHAR*, argv : WCHAR**) : HANDLE end fun __crystal_snprintf(str : LibC::Char*, size : LibC::SizeT, format : LibC::Char*, ...) : LibC::Int diff --git a/src/lib_c/x86_64-windows-msvc/c/stdlib.cr b/src/lib_c/x86_64-windows-msvc/c/stdlib.cr index a0d482c68a70..0e892d2e0999 100644 --- a/src/lib_c/x86_64-windows-msvc/c/stdlib.cr +++ b/src/lib_c/x86_64-windows-msvc/c/stdlib.cr @@ -9,6 +9,7 @@ lib LibC fun atof(nptr : Char*) : Double fun div(numer : Int, denom : Int) : DivT fun exit(status : Int) : NoReturn + fun _exit(status : Int) : NoReturn fun free(ptr : Void*) : Void fun malloc(size : SizeT) : Void* fun putenv(string : Char*) : Int diff --git a/src/lib_c/x86_64-windows-msvc/c/synchapi.cr b/src/lib_c/x86_64-windows-msvc/c/synchapi.cr index 3145aaa621f6..2269a6465319 100644 --- a/src/lib_c/x86_64-windows-msvc/c/synchapi.cr +++ b/src/lib_c/x86_64-windows-msvc/c/synchapi.cr @@ -2,4 +2,5 @@ require "c/int_safe" lib LibC fun Sleep(dwMilliseconds : DWORD) + fun WaitForSingleObject(hHandle : HANDLE, dwMilliseconds : DWORD) : DWORD end diff --git a/src/lib_c/x86_64-windows-msvc/c/winbase.cr b/src/lib_c/x86_64-windows-msvc/c/winbase.cr index 6fb4c3620bf0..e4b73e76f717 100644 --- a/src/lib_c/x86_64-windows-msvc/c/winbase.cr +++ b/src/lib_c/x86_64-windows-msvc/c/winbase.cr @@ -90,4 +90,14 @@ lib LibC fun GetEnvironmentStringsW : LPWCH fun FreeEnvironmentStringsW(lpszEnvironmentBlock : LPWCH) : BOOL fun SetEnvironmentVariableW(lpName : LPWSTR, lpValue : LPWSTR) : BOOL + + INFINITE = 0xFFFFFFFF + + STILL_ACTIVE = 0x103 + + STARTF_USESTDHANDLES = 0x00000100 + + fun DuplicateHandle(hSourceProcessHandle : HANDLE, hSourceHandle : HANDLE, + hTargetProcessHandle : HANDLE, lpTargetHandle : HANDLE*, + dwDesiredAccess : DWORD, bInheritHandle : BOOL, dwOptions : DWORD) : BOOL end diff --git a/src/lib_c/x86_64-windows-msvc/c/winnt.cr b/src/lib_c/x86_64-windows-msvc/c/winnt.cr index 87c0e2722349..0fc86d9f2186 100644 --- a/src/lib_c/x86_64-windows-msvc/c/winnt.cr +++ b/src/lib_c/x86_64-windows-msvc/c/winnt.cr @@ -16,4 +16,10 @@ lib LibC FILE_ATTRIBUTE_REPARSE_POINT = 0x400 FILE_READ_ATTRIBUTES = 0x80 + + PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 + SYNCHRONIZE = 0x00100000 + + DUPLICATE_CLOSE_SOURCE = 0x00000001 + DUPLICATE_SAME_ACCESS = 0x00000002 end diff --git a/src/prelude.cr b/src/prelude.cr index 453cf3bbb0f8..e233349c9598 100644 --- a/src/prelude.cr +++ b/src/prelude.cr @@ -62,7 +62,7 @@ require "pointer" require "pretty_print" require "primitives" require "proc" -no_win require "process" +require "process" require "raise" require "random" require "range" diff --git a/src/process.cr b/src/process.cr index 9692df78ef93..bd9f0570aa93 100644 --- a/src/process.cr +++ b/src/process.cr @@ -1,7 +1,4 @@ -require "c/signal" -require "c/stdlib" -require "c/sys/times" -require "c/unistd" +require "crystal/system/process" class Process # Terminate the current process immediately. All open files, pipes and sockets @@ -10,51 +7,41 @@ class Process # # *status* is the exit status of the current process. def self.exit(status = 0) - LibC.exit(status) + Crystal::System::Process.exit(status) end # Returns the process identifier of the current process. - def self.pid : LibC::PidT - LibC.getpid + def self.pid + Crystal::System::Process.pid end # Returns the process group identifier of the current process. - def self.pgid : LibC::PidT - pgid(0) + def self.pgid + Crystal::System::Process.process_gid end # Returns the process group identifier of the process identified by *pid*. - def self.pgid(pid : Int32) : LibC::PidT - ret = LibC.getpgid(pid) - raise Errno.new("getpgid") if ret < 0 - ret + def self.pgid(pid : Int) + Crystal::System::Process.process_gid(pid) end # Returns the process identifier of the parent process of the current process. - def self.ppid : LibC::PidT - LibC.getppid + def self.ppid + Crystal::System::Process.parent_pid end # Sends a *signal* to the processes identified by the given *pids*. - def self.kill(signal : Signal, *pids : Int) + def self.kill(signal : Signal, *pids : Int) : Nil pids.each do |pid| - ret = LibC.kill(pid, signal.value) - raise Errno.new("kill") if ret < 0 + Crystal::System::Process.kill(pid, signal.value) end - nil end # Returns `true` if the process identified by *pid* is valid for # a currently registered process, `false` otherwise. Note that this # returns `true` for a process in the zombie or similar state. def self.exists?(pid : Int) - ret = LibC.kill(pid, 0) - if ret == 0 - true - else - return false if Errno.value == Errno::ESRCH - raise Errno.new("kill") - end + Crystal::System::Process.exists?(pid) end # A struct representing the CPU current times of the process, @@ -76,56 +63,35 @@ class Process # Runs the given block inside a new process and # returns a `Process` representing the new child process. - def self.fork - pid = fork_internal do - with self yield self - end - new pid - end - - # Duplicates the current process. - # Returns a `Process` representing the new child process in the current process - # and `nil` inside the new child process. - def self.fork : self? - if pid = fork_internal - new pid + def self.fork : Process + if process = fork + process else - nil - end - end - - # :nodoc: - protected def self.fork_internal(run_hooks : Bool = true, &block) - pid = self.fork_internal(run_hooks) - - unless pid begin yield LibC._exit 0 rescue ex ex.inspect_with_backtrace STDERR STDERR.flush + LibC._exit 1 ensure LibC._exit 254 # not reached end end - - pid end - # *run_hooks* should ALWAYS be `true` unless `exec` is used immediately after fork. - # Channels, `IO` and other will not work reliably if *run_hooks* is `false`. - protected def self.fork_internal(run_hooks : Bool = true) - pid = LibC.fork - case pid - when 0 - pid = nil - Process.after_fork_child_callbacks.each(&.call) if run_hooks - when -1 - raise Errno.new("fork") + # Duplicates the current process. + # Returns a `Process` representing the new child process in the current process + # and `nil` inside the new child process. + def self.fork : Process? + if pid = Crystal::System::Process.fork + new(pid) + else + Process.after_fork_child_callbacks.each(&.call) + + nil end - pid end # How to redirect the standard input, output and error IO of a process. @@ -143,6 +109,7 @@ class Process # The standard `IO` configuration of a process. alias Stdio = Redirect | IO + alias ExecStdio = Redirect | IO::FileDescriptor alias Env = Nil | Hash(String, Nil) | Hash(String, String?) | Hash(String, String) # Executes a process and waits for it to complete. @@ -181,14 +148,34 @@ class Process # * `true`: inherit from parent # * `IO`: use the given `IO` def self.exec(command : String, args = nil, env : Env = nil, clear_env : Bool = false, shell : Bool = false, - input : Stdio = Redirect::Inherit, output : Stdio = Redirect::Inherit, error : Stdio = Redirect::Inherit, chdir : String? = nil) - command, argv = prepare_argv(command, args, shell) - unless exec_internal(command, argv, env, clear_env, input, output, error, chdir) - raise Errno.new("execvp") + input : ExecStdio = Redirect::Inherit, output : ExecStdio = Redirect::Inherit, error : ExecStdio = Redirect::Inherit, chdir : String? = nil) + command, args = prepare_args(command, args, shell) + input = io_for_exec(input, STDIN) + output = io_for_exec(output, STDOUT) + error = io_for_exec(error, STDERR) + Crystal::System::Process.replace(command, args, env, clear_env, input, output, error, chdir) + end + + private def self.io_for_exec(stdio : ExecStdio, for dst_io : IO::FileDescriptor) : IO::FileDescriptor + case stdio + when IO::FileDescriptor + stdio + when Redirect::Pipe + raise "Cannot use Process::Redirect::Pipe for Process.exec" + when Redirect::Inherit + dst_io + when Redirect::Close + if dst_io == STDIN + File.open(File::DEVNULL, "r") + else + File.open(File::DEVNULL, "w") + end + else + raise "BUG: impossible type in ExecStdio #{stdio.class}" end end - getter pid : Int32 + getter pid # A pipe to this process's input. Raises if a pipe wasn't asked when creating the process. getter! input : IO::FileDescriptor @@ -199,7 +186,11 @@ class Process # A pipe to this process's error. Raises if a pipe wasn't asked when creating the process. getter! error : IO::FileDescriptor - @waitpid : Channel::Buffered(Int32) + {% unless flag?(:win32) %} + @waitpid : Channel::Buffered(Int32) + @channel : Channel(Exception?)? + @wait_count = 0 + {% end %} # Creates a process, executes it, but doesn't wait for it to complete. # @@ -208,89 +199,44 @@ class Process # By default the process is configured without input, output or error. def initialize(command : String, args = nil, env : Env = nil, clear_env : Bool = false, shell : Bool = false, input : Stdio = Redirect::Close, output : Stdio = Redirect::Close, error : Stdio = Redirect::Close, chdir : String? = nil) - command, argv = Process.prepare_argv(command, args, shell) + command, args = Process.prepare_args(command, args, shell) - @wait_count = 0 - - if needs_pipe?(input) - fork_input, process_input = IO.pipe(read_blocking: true) - if input.is_a?(IO) - @wait_count += 1 - spawn { copy_io(input, process_input, channel, close_dst: true) } - else - @input = process_input - end - end + fork_input = stdio_to_fd(input, for: STDIN) + fork_output = stdio_to_fd(output, for: STDOUT) + fork_error = stdio_to_fd(error, for: STDERR) - if needs_pipe?(output) - process_output, fork_output = IO.pipe(write_blocking: true) - if output.is_a?(IO) - @wait_count += 1 - spawn { copy_io(process_output, output, channel, close_src: true) } - else - @output = process_output - end - end + @pid = Crystal::System::Process.spawn(command, args, env, clear_env, fork_input, fork_output, fork_error, chdir) - if needs_pipe?(error) - process_error, fork_error = IO.pipe(write_blocking: true) - if error.is_a?(IO) - @wait_count += 1 - spawn { copy_io(process_error, error, channel, close_src: true) } - else - @error = process_error - end - end + {% unless flag?(:win32) %} + {{ @type }} + @waitpid = Crystal::SignalChildHandler.wait(pid) + {% end %} - reader_pipe, writer_pipe = IO.pipe + # p! "closing", + # input, fork_input, fork_input == input || fork_input == STDIN, + # output, fork_output, fork_output == output || fork_output == STDOUT, + # error, fork_error, fork_error == error || fork_error == STDERR - @pid = Process.fork_internal(run_hooks: false) do - begin - reader_pipe.close - writer_pipe.close_on_exec = true - unless Process.exec_internal( - command, - argv, - env, - clear_env, - fork_input || input, - fork_output || output, - fork_error || error, - chdir - ) - writer_pipe.write_bytes(Errno.value) - writer_pipe.close - end - rescue ex - ex.inspect_with_backtrace STDERR - ensure - LibC._exit 127 - end - end - - writer_pipe.close - bytes = uninitialized UInt8[4] - if reader_pipe.read(bytes.to_slice) == 4 - reader_pipe.close - errno = IO::ByteFormat::SystemEndian.decode(Int32, bytes.to_slice) - raise Errno.new("execvp", errno) - end - reader_pipe.close - - @waitpid = Crystal::SignalChildHandler.wait(pid) - - fork_input.try &.close - fork_output.try &.close - fork_error.try &.close + fork_input.close unless fork_input == input || fork_input == STDIN + fork_output.close unless fork_output == output || fork_output == STDOUT + fork_error.close unless fork_error == error || fork_error == STDERR end private def initialize(@pid) - @waitpid = Crystal::SignalChildHandler.wait(pid) - @wait_count = 0 + {% unless flag?(:win32) %} + {{ @type }} + @waitpid = Crystal::SignalChildHandler.wait(pid) + {% end %} end + {% if flag?(:win32) %} + private DEFAULT_SIGNAL = Signal::KILL + {% else %} + private DEFAULT_SIGNAL = Signal::TERM + {% end %} + # See also: `Process.kill` - def kill(sig = Signal::TERM) + def kill(sig = DEFAULT_SIGNAL) Process.kill sig, @pid end @@ -298,13 +244,17 @@ class Process def wait : Process::Status close_io @input # only closed when a pipe was created but not managed by copy_io - @wait_count.times do - ex = channel.receive - raise ex if ex - end - @wait_count = 0 + {% if flag?(:win32) %} + Process::Status.new(Crystal::System::Process.wait(pid)) + {% else %} + @wait_count.times do + ex = channel.receive + raise ex if ex + end + @wait_count = 0 - Process::Status.new(@waitpid.receive) + Process::Status.new(@waitpid.receive) + {% end %} ensure close end @@ -317,7 +267,11 @@ class Process # Whether this process is already terminated. def terminated? - @waitpid.closed? || !Process.exists?(@pid) + {% if flag?(:win32) %} + !Process.exists?(@pid) + {% else %} + @waitpid.closed? || !Process.exists?(@pid) + {% end %} end # Closes any pipes to the child process. @@ -328,7 +282,7 @@ class Process end # :nodoc: - protected def self.prepare_argv(command, args, shell) + protected def self.prepare_args(command, args, shell) if shell command = %(#{command} "${@}") unless command.includes?(' ') shell_args = ["-c", command, "--"] @@ -349,21 +303,61 @@ class Process args = shell_args end - argv = [command.to_unsafe] - args.try &.each do |arg| - argv << arg.to_unsafe + {command, args} + end + + {% unless flag?(:win32) %} + private def channel + @channel ||= Channel(Exception?).new end - argv << Pointer(UInt8).null + {% end %} - {command, argv} - end + private def stdio_to_fd(stdio : Stdio, for dst_io : IO::FileDescriptor) : IO::FileDescriptor + case stdio + when IO::FileDescriptor + stdio + when IO + {% if flag?(:win32) %} + raise NotImplementedError.new("Process.new with input as a generic IO") + {% else %} + if dst_io == STDIN + fork_io, process_io = IO.pipe(read_blocking: true) + + @wait_count += 1 + spawn { copy_io(stdio, process_io, channel, close_dst: true) } + else + process_io, fork_io = IO.pipe(write_blocking: true) + + @wait_count += 1 + spawn { copy_io(process_io, stdio, channel, close_src: true) } + end - private def channel - @channel ||= Channel(Exception?).new - end + fork_io + {% end %} + when Redirect::Pipe + case dst_io + when STDIN + fork_io, @input = IO.pipe(read_blocking: true) + when STDOUT + @output, fork_io = IO.pipe(write_blocking: true) + when STDERR + @error, fork_io = IO.pipe(write_blocking: true) + else + raise "BUG: unknown destination io #{dst_io}" + end - private def needs_pipe?(io) - (io == Redirect::Pipe) || (io.is_a?(IO) && !io.is_a?(IO::FileDescriptor)) + fork_io + when Redirect::Inherit + dst_io + when Redirect::Close + if dst_io == STDIN + File.open(File::DEVNULL, "r") + else + File.open(File::DEVNULL, "w") + end + else + raise "BUG: impossible type in stdio #{stdio.class}" + end end private def copy_io(src, dst, channel, close_src = false, close_dst = false) @@ -389,45 +383,6 @@ class Process end end - # :nodoc: - protected def self.exec_internal(command : String, argv, env, clear_env, input, output, error, chdir) - # Reopen handles if the child is being redirected - reopen_io(input, IO::FileDescriptor.new(0, blocking: true), "r") - reopen_io(output, IO::FileDescriptor.new(1, blocking: true), "w") - reopen_io(error, IO::FileDescriptor.new(2, blocking: true), "w") - - ENV.clear if clear_env - env.try &.each do |key, val| - if val - ENV[key] = val - else - ENV.delete key - end - end - - Dir.cd(chdir) if chdir - - LibC.execvp(command, argv) != -1 - end - - private def self.reopen_io(src_io, dst_io, mode) - case src_io - when IO::FileDescriptor - src_io.blocking = true - dst_io.reopen(src_io) - when Redirect::Inherit - dst_io.blocking = true - when Redirect::Close - File.open("/dev/null", mode) do |file| - dst_io.reopen(file) - end - else - raise "BUG: unknown object type #{src_io}" - end - - dst_io.close_on_exec = false - end - private def close_io(io) io.close if io end diff --git a/src/process/status.cr b/src/process/status.cr index f593c0400652..9ca5e10a2de8 100644 --- a/src/process/status.cr +++ b/src/process/status.cr @@ -1,36 +1,57 @@ -# The status of a terminated process. +# The status of a terminated process. Returned by `Process#wait` class Process::Status - getter exit_status : Int32 - - def initialize(@exit_status : Int32) - end + {% if flag?(:win32) %} + # :nodoc: + def initialize(@exit_status : UInt32) + end + {% else %} + # :nodoc: + def initialize(@exit_status : Int32) + end + {% end %} # Returns `true` if the process was terminated by a signal. - def signal_exit? - # define __WIFSIGNALED(status) (((signed char) (((status) & 0x7f) + 1) >> 1) > 0) - ((LibC::SChar.new(@exit_status & 0x7f) + 1) >> 1) > 0 + def signal_exit? : Bool + {% if flag?(:win32) %} + false + {% else %} + # define __WIFSIGNALED(status) (((signed char) (((status) & 0x7f) + 1) >> 1) > 0) + ((LibC::SChar.new(@exit_status & 0x7f) + 1) >> 1) > 0 + {% end %} end # Returns `true` if the process terminated normally. - def normal_exit? - # define __WIFEXITED(status) (__WTERMSIG(status) == 0) - signal_code == 0 + def normal_exit? : Bool + {% if flag?(:win32) %} + true + {% else %} + # define __WIFEXITED(status) (__WTERMSIG(status) == 0) + signal_code == 0 + {% end %} end # If `signal_exit?` is `true`, returns the *Signal* the process # received and didn't handle. Will raise if `signal_exit?` is `false`. - def exit_signal - Signal.from_value(signal_code) + def exit_signal : Signal + {% if flag?(:win32) %} + raise NotImplementedError.new("Process::Status#exit_signal") + {% else %} + Signal.from_value(signal_code) + {% end %} end # If `normal_exit?` is `true`, returns the exit code of the process. - def exit_code - # define __WEXITSTATUS(status) (((status) & 0xff00) >> 8) - (@exit_status & 0xff00) >> 8 + def exit_code : Int32 + {% if flag?(:win32) %} + @exit_status.to_i32 + {% else %} + # define __WEXITSTATUS(status) (((status) & 0xff00) >> 8) + (@exit_status & 0xff00) >> 8 + {% end %} end # Returns `true` if the process exited normally with an exit code of `0`. - def success? + def success? : Bool normal_exit? && exit_code == 0 end diff --git a/src/windows_stubs.cr b/src/windows_stubs.cr index 9b598149c413..29ba311e8fd4 100644 --- a/src/windows_stubs.cr +++ b/src/windows_stubs.cr @@ -66,6 +66,10 @@ class Process end end +enum Signal + KILL = 0 +end + def sleep(seconds : Number) sleep(seconds.seconds) end