Skip to content

Commit

Permalink
Minimal Windows support for Process (crystal-lang#9047)
Browse files Browse the repository at this point in the history
Co-authored-by: Stephanie Hobbs <[email protected]>
  • Loading branch information
2 people authored and carlhoerberg committed Apr 29, 2020
1 parent 193c077 commit 9a7d5fa
Show file tree
Hide file tree
Showing 15 changed files with 401 additions and 65 deletions.
157 changes: 108 additions & 49 deletions spec/std/process_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand All @@ -99,54 +153,59 @@ 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
output.should eq "`echo hi`\n"
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
end
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
Expand All @@ -156,38 +215,38 @@ 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
end
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
Expand All @@ -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

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

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

Expand Down
2 changes: 1 addition & 1 deletion spec/win32_std_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 5 additions & 0 deletions src/crystal/system/process.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 %}
3 changes: 3 additions & 0 deletions src/crystal/system/unix/process.cr
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ struct Crystal::System::Process
@channel = Crystal::SignalChildHandler.wait(@pid)
end

def release
end

def wait
@channel.receive
end
Expand Down
5 changes: 3 additions & 2 deletions src/crystal/system/win32/file.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/crystal/system/win32/file_descriptor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading

0 comments on commit 9a7d5fa

Please sign in to comment.