Skip to content

Commit

Permalink
Port Process to win32
Browse files Browse the repository at this point in the history
  • Loading branch information
RX14 committed Sep 15, 2018
1 parent caf8afe commit ed14ad5
Show file tree
Hide file tree
Showing 17 changed files with 677 additions and 244 deletions.
88 changes: 68 additions & 20 deletions spec/std/process_spec.cr
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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$/)

Expand Down
11 changes: 11 additions & 0 deletions 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 %}
151 changes: 151 additions & 0 deletions 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
5 changes: 3 additions & 2 deletions src/crystal/system/win32/file.cr
Expand Up @@ -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.
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/crystal/system/win32/file_descriptor.cr
Expand Up @@ -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

Expand Down

0 comments on commit ed14ad5

Please sign in to comment.