diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index a397d7f9bceb..784340ac29bc 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -3,54 +3,108 @@ 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"}} + {% else %} + {"/bin/cat", [] of String} + {% end %} +end + +private def standing_command + {% if flag?(:win32) %} + {"cmd.exe"} + {% else %} + {"yes"} + {% end %} +end + +private def newline + {% if flag?(:win32) %} + "\r\n" + {% else %} + "\n" + {% 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 it "raises if command could not be executed" do - expect_raises(RuntimeError, "Error executing process: No such file or directory") do + expect_raises(RuntimeError, "Error executing process:") do Process.new("foobarbaz", ["foo"]) end 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) + command, args = {% if flag?(:win32) %} + {"cmd.exe", {"/c", "dir"}} + {% else %} + {"/bin/ls", [] 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") + value.should eq("hello#{newline}") end - it "sends input in IO" do - value = Process.run("/bin/cat", input: IO::Memory.new("hello")) do |proc| + 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 @@ -59,31 +113,31 @@ describe Process do it "sends output to IO" do output = IO::Memory.new - Process.run("/bin/sh", {"-c", "echo hello"}, output: output) - output.to_s.should eq("hello\n") + Process.run(*shell_command("echo hello"), output: output) + output.to_s.should eq("hello#{newline}") end it "sends error to IO" do error = IO::Memory.new - Process.run("/bin/sh", {"-c", "echo hello 1>&2"}, error: error) - error.to_s.should eq("hello\n") + Process.run(*shell_command("1>&2 echo hello"), error: error) + error.to_s.should eq("hello#{newline}") end it "controls process in block" do - value = Process.run("/bin/cat") do |proc| - proc.input.print "hello" + value = Process.run(*stdin_to_stdout_command, error: :inherit) do |proc| + proc.input.puts "hello" proc.input.close proc.output.gets_to_end end - value.should eq("hello") + value.should eq("hello#{newline}") end it "closes ios after block" do - Process.run("/bin/cat") { } + Process.run(*stdin_to_stdout_command) { } $?.exit_code.should eq(0) end - it "chroot raises when unprivileged" do + pending_win32 "chroot raises when unprivileged" do status, output = build_and_run <<-'CODE' begin Process.chroot("/usr") @@ -99,31 +153,36 @@ describe Process do it "sets working directory" do parent = File.dirname(Dir.current) - value = Process.run("pwd", shell: true, chdir: parent, output: Process::Redirect::Pipe) do |proc| + command = {% if flag?(:win32) %} + "cmd.exe /c echo %cd%" + {% else %} + "pwd" + {% end %} + value = Process.run(command, shell: true, chdir: parent, output: Process::Redirect::Pipe) do |proc| proc.output.gets_to_end end - value.should eq "#{parent}\n" + value.should eq "#{parent}#{newline}" end - it "disallows passing arguments to nowhere" do + pending_win32 "disallows passing arguments to nowhere" do expect_raises ArgumentError, /args.+@/ do Process.run("foo bar", {"baz"}, shell: true) end end - it "looks up programs in the $PATH with a shell" do - proc = Process.run("uname", {"-a"}, shell: true, output: Process::Redirect::Close) + pending_win32 "looks up programs in the $PATH with a shell" do + proc = Process.run(*exit_code_command(0), shell: true, output: Process::Redirect::Close) proc.exit_code.should eq(0) end - it "allows passing huge argument lists to a shell" do + pending_win32 "allows passing huge argument lists to a shell" do proc = Process.new(%(echo "${@}"), {"a", "b"}, shell: true, output: Process::Redirect::Pipe) output = proc.output.gets_to_end proc.wait output.should eq "a b\n" end - it "does not run shell code in the argument list" do + pending_win32 "does not run shell code in the argument list" do proc = Process.new("echo", {"`echo hi`"}, shell: true, output: Process::Redirect::Pipe) output = proc.output.gets_to_end proc.wait @@ -131,14 +190,14 @@ describe Process do end describe "environ" do - it "clears the environment" do + pending_win32 "clears the environment" do value = Process.run("env", clear_env: true) do |proc| proc.output.gets_to_end end value.should eq("") end - it "sets an environment variable" do + pending_win32 "sets an environment variable" do env = {"FOO" => "bar"} value = Process.run("env", clear_env: true, env: env) do |proc| proc.output.gets_to_end @@ -146,7 +205,7 @@ describe Process do value.should eq("FOO=bar\n") end - it "deletes an environment variable" do + pending_win32 "deletes an environment variable" do env = {"HOME" => nil} value = Process.run("env | egrep '^HOME='", env: env, shell: true) do |proc| proc.output.gets_to_end @@ -156,30 +215,30 @@ describe Process do end describe "signal" do - it "kills a process" do - process = Process.new("yes") + pending_win32 "kills a process" do + process = Process.new(*standing_command) process.signal(Signal::KILL).should be_nil end - it "kills many process" do - process1 = Process.new("yes") - process2 = Process.new("yes") + pending_win32 "kills many process" do + process1 = Process.new(*standing_command) + process2 = Process.new(*standing_command) process1.signal(Signal::KILL).should be_nil process2.signal(Signal::KILL).should be_nil end end - it "gets the pgid of a process id" do - process = Process.new("yes") + pending_win32 "gets the pgid of a process id" do + process = Process.new(*standing_command) Process.pgid(process.pid).should be_a(Int64) process.signal(Signal::KILL) Process.pgid.should eq(Process.pgid(Process.pid)) end - it "can link processes together" do + pending_win32 "can link processes together" do buffer = IO::Memory.new - Process.run("/bin/cat") do |cat| - Process.run("/bin/cat", input: cat.output, output: buffer) do + Process.run(*stdin_to_stdout_command) do |cat| + Process.run(*stdin_to_stdout_command, input: cat.output, output: buffer) do 1000.times { cat.input.puts "line" } cat.close end @@ -187,7 +246,7 @@ describe Process do buffer.to_s.lines.size.should eq(1000) end - {% unless flag?(:preview_mt) %} + {% unless flag?(:preview_mt) || flag?(:win32) %} it "executes the new process with exec" do with_tempfile("crystal-spec-exec") do |path| File.exists?(path).should be_false @@ -202,14 +261,14 @@ describe Process do end {% end %} - it "checks for existence" do + pending_win32 "checks for existence" do # We can't reliably check whether it ever returns false, since we can't predict # how PIDs are used by the system, a new process might be spawned in between # reaping the one we would spawn and checking for it, using the now available # pid. Process.exists?(Process.ppid).should be_true - process = Process.new("yes") + process = Process.new(*standing_command) process.exists?.should be_true process.terminated?.should be_false @@ -224,8 +283,8 @@ describe Process do process.terminated?.should be_true end - it "terminates the process" do - process = Process.new("yes") + pending_win32 "terminates the process" do + process = Process.new(*standing_command) process.exists?.should be_true process.terminated?.should be_false @@ -243,16 +302,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/spec/win32_std_spec.cr b/spec/win32_std_spec.cr index 5c01fae0d87c..b50b2fef8b43 100644 --- a/spec/win32_std_spec.cr +++ b/spec/win32_std_spec.cr @@ -152,7 +152,7 @@ require "./std/path_spec.cr" require "./std/pointer_spec.cr" require "./std/pp_spec.cr" require "./std/pretty_print_spec.cr" -# require "./std/process_spec.cr" (failed codegen) +require "./std/process_spec.cr" require "./std/proc_spec.cr" require "./std/raise_spec.cr" require "./std/random/isaac_spec.cr" diff --git a/src/crystal/system/process.cr b/src/crystal/system/process.cr index 2dda1b26afc1..fca85f7a6c7e 100644 --- a/src/crystal/system/process.cr +++ b/src/crystal/system/process.cr @@ -9,6 +9,9 @@ struct Crystal::System::Process # Creates a structure representing a running process based on its ID. # def self.new(pi : ProcessInformation) + # Releases any resources acquired by this structure. + # def release + # Returns the PID of the running process. # def pid : Int @@ -69,6 +72,8 @@ 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 index 018ecd7cf3f3..a06e89e95f0b 100644 --- a/src/crystal/system/unix/process.cr +++ b/src/crystal/system/unix/process.cr @@ -10,6 +10,9 @@ struct Crystal::System::Process @channel = Crystal::SignalChildHandler.wait(@pid) end + def release + end + def wait @channel.receive end diff --git a/src/crystal/system/win32/file.cr b/src/crystal/system/win32/file.cr index c35ba823a99a..0536757de362 100644 --- a/src/crystal/system/win32/file.cr +++ b/src/crystal/system/win32/file.cr @@ -8,7 +8,7 @@ require "c/winbase" 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. @@ -29,7 +29,8 @@ module Crystal::System::File def self.mktemp(prefix : String, suffix : String?, dir : String) : {LibC::Int, String} path = "#{tempdir}\\#{prefix}.#{::Random::Secure.hex}#{suffix}" - 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 ::File::Error.from_errno("Error creating temporary file", file: path) end diff --git a/src/crystal/system/win32/file_descriptor.cr b/src/crystal/system/win32/file_descriptor.cr index e00d10200067..12d85d6c6054 100644 --- a/src/crystal/system/win32/file_descriptor.cr +++ b/src/crystal/system/win32/file_descriptor.cr @@ -135,7 +135,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 IO::Error.from_errno("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..e9c4788191f9 --- /dev/null +++ b/src/crystal/system/win32/process.cr @@ -0,0 +1,190 @@ +require "c/processthreadsapi" + +struct Crystal::System::Process + getter pid : LibC::DWORD + @thread_id : LibC::DWORD + @process_handle : LibC::HANDLE + + def initialize(process_info) + @pid = process_info.dwProcessId + @thread_id = process_info.dwThreadId + @process_handle = process_info.hProcess + end + + def release + close_handle(@process_handle) + end + + def wait + if LibC.WaitForSingleObject(@process_handle, LibC::INFINITE) != 0 + raise RuntimeError.from_winerror("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(@process_handle, out exit_code) == 0 + raise RuntimeError.from_winerror("GetExitCodeProcess") + end + if exit_code == LibC::STILL_ACTIVE + raise "BUG: process still active" + end + exit_code + end + + def exists? + Crystal::System::Process.exists?(@pid) + end + + def terminate + raise NotImplementedError.new("Process.kill") + end + + def self.exit(status) + LibC.exit(status) + end + + def self.pid + LibC.GetCurrentProcessId + end + + def self.pgid + raise NotImplementedError.new("Process.pgid") + end + + def self.pgid(pid) + raise NotImplementedError.new("Process.pgid") + end + + def self.ppid + raise NotImplementedError.new("Process.ppid") + end + + def self.signal(pid, signal) + raise NotImplementedError.new("Process.signal") + end + + def self.exists?(pid) + handle = LibC.OpenProcess(LibC::PROCESS_QUERY_INFORMATION, 0, pid) + return false if handle.nil? + begin + if LibC.GetExitCodeProcess(handle, out exit_code) == 0 + raise RuntimeError.from_winerror("GetExitCodeProcess") + end + exit_code == LibC::STILL_ACTIVE + ensure + close_handle(handle) + end + end + + def self.times + raise NotImplementedError.new("Process.times") + end + + def self.fork + raise NotImplementedError.new("Process.fork") + end + + private def self.handle_from_io(io : IO::FileDescriptor, parent_io) + ret = LibC._get_osfhandle(io.fd) + raise RuntimeError.from_winerror("_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 RuntimeError.from_winerror("DuplicateHandle") + end + + new_handle + end + + def self.spawn(command_args, env, clear_env, input, output, error, chdir) + if env || clear_env + raise NotImplementedError.new("Process.new with env or clear_env options") + end + + 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, command_args.check_no_null_byte.to_utf16, nil, nil, true, 0, + nil, chdir.try &.check_no_null_byte.to_utf16, + pointerof(startup_info), pointerof(process_info) + ) == 0 + raise RuntimeError.from_winerror("Error executing process") + end + + close_handle(process_info.hThread) + + close_handle(startup_info.hStdInput) + close_handle(startup_info.hStdOutput) + close_handle(startup_info.hStdError) + + process_info + end + + def self.prepare_args(command : String, args : Enumerable(String)?, shell : Bool) : String + if shell + if args + raise NotImplementedError.new("Process with args and shell: true is not supported on Windows") + end + command + else + command_args = [command] + command_args.concat(args) if args + String.build { |io| args_to_string(command_args, io) } + end + end + + private def self.args_to_string(args, io : IO) + args.join(' ', io) do |arg| + quotes = arg.empty? || arg.includes?(' ') || arg.includes?('\t') + + 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 + + def self.replace(command_args, env, clear_env, input, output, error, chdir) : NoReturn + raise NotImplementedError.new("Process.exec") + end + + def self.chroot(path) + raise NotImplementedError.new("Process.chroot") + end +end + +private def close_handle(handle) + if LibC.CloseHandle(handle) == 0 + raise RuntimeError.from_winerror("CloseHandle") + 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 index b252b70d307a..0db352c0b622 100644 --- a/src/lib_c/x86_64-windows-msvc/c/processthreadsapi.cr +++ b/src/lib_c/x86_64-windows-msvc/c/processthreadsapi.cr @@ -1,5 +1,44 @@ require "./basetsd" lib LibC + struct PROCESS_INFORMATION + hProcess : HANDLE + hThread : HANDLE + dwProcessId : DWORD + dwThreadId : DWORD + end + + 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 : BYTE* + hStdInput : HANDLE + hStdOutput : HANDLE + hStdError : HANDLE + end + fun GetCurrentThreadStackLimits(lowLimit : ULONG_PTR*, highLimit : ULONG_PTR*) : Void + fun GetCurrentProcess : HANDLE + 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 : SECURITY_ATTRIBUTES*, lpThreadAttributes : SECURITY_ATTRIBUTES*, + bInheritHandles : BOOL, dwCreationFlags : DWORD, + lpEnvironment : Void*, lpCurrentDirectory : LPWSTR, + lpStartupInfo : STARTUPINFOW*, lpProcessInformation : PROCESS_INFORMATION*) : BOOL + + PROCESS_QUERY_INFORMATION = 0x0400 end 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..23804d0f3aaf 100644 --- a/src/lib_c/x86_64-windows-msvc/c/synchapi.cr +++ b/src/lib_c/x86_64-windows-msvc/c/synchapi.cr @@ -1,5 +1,7 @@ +require "c/basetsd" 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 1694e8e77692..2c0b4188a7e0 100644 --- a/src/lib_c/x86_64-windows-msvc/c/winbase.cr +++ b/src/lib_c/x86_64-windows-msvc/c/winbase.cr @@ -91,6 +91,16 @@ lib LibC 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 + MOVEFILE_REPLACE_EXISTING = 0x1_u32 MOVEFILE_COPY_ALLOWED = 0x2_u32 MOVEFILE_DELAY_UNTIL_REBOOT = 0x4_u32 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 9a73b784b9eb..85bf8e85a0d7 100644 --- a/src/lib_c/x86_64-windows-msvc/c/winnt.cr +++ b/src/lib_c/x86_64-windows-msvc/c/winnt.cr @@ -19,4 +19,10 @@ lib LibC # Memory protection constants PAGE_READWRITE = 0x04 + + 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 2c8e0910e129..810e27b395fc 100644 --- a/src/prelude.cr +++ b/src/prelude.cr @@ -66,7 +66,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 f10ec37ad7c9..566e1fae5755 100644 --- a/src/process.cr +++ b/src/process.cr @@ -63,12 +63,16 @@ class Process # Returns a `Tms` for the current process. For the children times, only those # of terminated children are returned. + # + # Available only on Unix-like operating systems. def self.times : Tms Crystal::System::Process.times end # Runs the given block inside a new process and # returns a `Process` representing the new child process. + # + # Available only on Unix-like operating systems. def self.fork : Process if process = fork process @@ -89,6 +93,8 @@ class Process # Duplicates the current process. # Returns a `Process` representing the new child process in the current process # and `nil` inside the new child process. + # + # Available only on Unix-like operating systems. def self.fork : Process? {% raise("Process fork is unsupported with multithread mode") if flag?(:preview_mt) %} @@ -145,6 +151,8 @@ class Process end # Replaces the current process with a new one. This function never returns. + # + # Available only on Unix-like operating systems. def self.exec(command : String, args = nil, env : Env = nil, clear_env : Bool = false, shell : Bool = false, input : ExecStdio = Redirect::Inherit, output : ExecStdio = Redirect::Inherit, error : ExecStdio = Redirect::Inherit, chdir : String? = nil) command_args = Crystal::System::Process.prepare_args(command, args, shell) @@ -197,6 +205,17 @@ class Process # To wait for it to finish, invoke `wait`. # # By default the process is configured without input, output or error. + # + # If *shell* is false, the *command* is the path to the executable to run, + # along with a list of *args*. + # + # If *shell* is true, the *command* should be the full command line + # including space-separated args. + # * On POSIX this uses `/bin/sh` to process the command string. *args* are + # also passed to the shell, and you need to include the string `"${@}"` in + # the *command* to safely insert them there. + # * On Windows this is implemented by passing the string as-is to the + # process, and passing *args* is not supported. 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_args = Crystal::System::Process.prepare_args(command, args, shell) @@ -300,11 +319,12 @@ class Process !exists? end - # Closes any pipes to the child process. + # Closes any system resources (e.g. pipes) held for the child process. def close close_io @input close_io @output close_io @error + @process_info.release end # Asks this process to terminate gracefully @@ -354,6 +374,8 @@ class Process # Changes the root directory and the current working directory for the current # process. # + # Available only on Unix-like operating systems. + # # Security: `chroot` on its own is not an effective means of mitigation. At minimum # the process needs to also drop privileges as soon as feasible after the `chroot`. # Changes to the directory hierarchy or file descriptors passed via `recvmsg(2)` from @@ -416,11 +438,15 @@ def `(command) : String end # See also: `Process.fork` +# +# Available only on Unix-like operating systems. def fork Process.fork { yield } end # See also: `Process.fork` +# +# Available only on Unix-like operating systems. def fork Process.fork end diff --git a/src/windows_stubs.cr b/src/windows_stubs.cr index 0120535aaff5..c8ebcb3a12d6 100644 --- a/src/windows_stubs.cr +++ b/src/windows_stubs.cr @@ -60,16 +60,6 @@ abstract class IO end end -class Process - def self.exit(status = 0) - LibC.exit(status) - end - - def self.pid - 1 - end -end - class Mutex enum Protection Checked @@ -96,6 +86,10 @@ class Mutex end end +enum Signal + KILL = 0 +end + def sleep(seconds : Number) sleep(seconds.seconds) end