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

Add Crystal::System::Process to split out system-specific implementations #9035

Merged
merged 7 commits into from
Apr 13, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
74 changes: 74 additions & 0 deletions src/crystal/system/process.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# :nodoc:
struct Crystal::System::Process
oprypin marked this conversation as resolved.
Show resolved Hide resolved
# Implementation-dependent "conceptual" data types (they only need to match across arg types and return types):
# * ProcessInformation: The system-dependent value with enough information to keep track of a running process.
# Could be the PID or similar.
# * Args: The system-native way to specify an executable with its command line arguments.
# Could be an array of strings or a single string.

# Creates a structure representing a running process based on its ID.
# def self.new(pi : ProcessInformation)

# Returns the PID of the running process.
# def pid : Int

# Waits until the process finishes and returns its status code
# def wait : Int

# Whether the process is still registered in the system.
# def exists? : Bool

# Asks this process to terminate gracefully.
# def terminate

# Terminates the current process immediately.
# def self.exit(status : Int)

# Returns the process identifier of the current process.
# def self.pid : Int

# Returns the process group identifier of the current process.
# def self.pgid : Int

# Returns the process group identifier of the process identified by *pid*.
# def self.pgid(pid) : Int

# Returns the process identifier of the parent process of the current process.
# def self.ppid : Int

# Sends a *signal* to the processes identified by the given *pids*.
# def self.signal(pid : Int, signal : Int)

# Whether the process identified by *pid* is still registered in the system.
# def self.exists?(pid : Int) : Bool

# Measures CPU times.
# def self.times : ::Process::Tms

# Duplicates the current process.
# def self.fork : ProcessInformation

# Launches a child process with the command + args.
# def self.spawn(command_args : Args, env : Env?, clear_env : Bool, input : Stdio, output : Stdio, error : Stdio, chdir : String?) : ProcessInformation

# Replaces the current process with a new one.
# def self.replace(command_args : Args, env : Env?, clear_env : Bool, input : Stdio, output : Stdio, error : Stdio, chdir : String?) : NoReturn

# Converts a command and array of arguments to the system-specific representation.
# def self.prepare_args(command : String, args : Enumerable(String)?, shell : Bool) : Args

# Changes the root directory for the current process.
# def self.chroot(path : String)
end

module Crystal::System
ORIGINAL_STDIN = IO::FileDescriptor.new(0, blocking: true)
ORIGINAL_STDOUT = IO::FileDescriptor.new(1, blocking: true)
ORIGINAL_STDERR = IO::FileDescriptor.new(2, blocking: true)
end

{% if flag?(:unix) %}
require "./unix/process"
{% else %}
{% raise "No Crystal::System::Process implementation available" %}
{% end %}
227 changes: 227 additions & 0 deletions src/crystal/system/unix/process.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
require "c/signal"
require "c/stdlib"
require "c/sys/resource"
require "c/unistd"

struct Crystal::System::Process
getter pid : LibC::PidT

def initialize(@pid : LibC::PidT)
@channel = Crystal::SignalChildHandler.wait(@pid)
end

def wait
@channel.receive
end

def exists?
[email protected]? && Crystal::System::Process.exists?(@pid)
end

def terminate
Crystal::System::Process.signal(@pid, LibC::SIGTERM)
end

def self.exit(status)
LibC.exit(status)
end

def self.pid
LibC.getpid
end

def self.pgid
ret = LibC.getpgid(0)
raise RuntimeError.from_errno("getpgid") if ret < 0
ret
end

def self.pgid(pid)
# Disallow users from depending on ppid(0) instead of `pgid`
raise RuntimeError.from_errno("getpgid", Errno::EINVAL) if pid == 0

ret = LibC.getpgid(pid)
raise RuntimeError.from_errno("getpgid") if ret < 0
ret
end

def self.ppid
LibC.getppid
end

def self.signal(pid, signal)
ret = LibC.kill(pid, signal)
raise RuntimeError.from_errno("kill") if ret < 0
end

def self.exists?(pid)
ret = LibC.kill(pid, 0)
if ret == 0
true
else
return false if Errno.value == Errno::ESRCH
raise RuntimeError.from_errno("kill")
end
end

def self.times
LibC.getrusage(LibC::RUSAGE_SELF, out usage)
LibC.getrusage(LibC::RUSAGE_CHILDREN, out child)

::Process::Tms.new(
usage.ru_utime.tv_sec.to_f64 + usage.ru_utime.tv_usec.to_f64 / 1e6,
usage.ru_stime.tv_sec.to_f64 + usage.ru_stime.tv_usec.to_f64 / 1e6,
child.ru_utime.tv_sec.to_f64 + child.ru_utime.tv_usec.to_f64 / 1e6,
child.ru_stime.tv_sec.to_f64 + child.ru_stime.tv_usec.to_f64 / 1e6,
)
end

def self.fork(*, will_exec = false)
newmask = uninitialized LibC::SigsetT
oldmask = uninitialized LibC::SigsetT

LibC.sigfillset(pointerof(newmask))
ret = LibC.pthread_sigmask(LibC::SIG_SETMASK, pointerof(newmask), pointerof(oldmask))
raise RuntimeError.from_errno("Failed to disable signals") unless ret == 0

case pid = LibC.fork
when 0
# child:
pid = nil
if will_exec
# reset signal handlers, then sigmask (inherited on exec):
Crystal::Signal.after_fork_before_exec
LibC.sigemptyset(pointerof(newmask))
LibC.pthread_sigmask(LibC::SIG_SETMASK, pointerof(newmask), nil)
else
{% unless flag?(:preview_mt) %}
::Process.after_fork_child_callbacks.each(&.call)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't after_fork_child be moved to Crystal::System, too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know (like, actually could be conceptually the wrong thing to do) and it's certainly not a requirement to make things work.

{% end %}
LibC.pthread_sigmask(LibC::SIG_SETMASK, pointerof(oldmask), nil)
end
when -1
# error:
errno = Errno.value
LibC.pthread_sigmask(LibC::SIG_SETMASK, pointerof(oldmask), nil)
raise RuntimeError.from_errno("fork", errno)
else
# parent:
LibC.pthread_sigmask(LibC::SIG_SETMASK, pointerof(oldmask), nil)
end

pid
end

def self.spawn(command_args, env, clear_env, input, output, error, chdir)
reader_pipe, writer_pipe = IO.pipe

pid = self.fork(will_exec: true)
if !pid
begin
reader_pipe.close
writer_pipe.close_on_exec = true
self.replace(command_args, env, clear_env, input, output, error, chdir)
rescue ex
writer_pipe.write_bytes(ex.message.try(&.bytesize) || 0)
writer_pipe << ex.message
writer_pipe.close
ensure
LibC._exit 127
end
end

writer_pipe.close
bytes = uninitialized UInt8[4]
if reader_pipe.read(bytes.to_slice) == 4
message_size = IO::ByteFormat::SystemEndian.decode(Int32, bytes.to_slice)
if message_size > 0
message = String.build(message_size) { |io| IO.copy(reader_pipe, io, message_size) }
end
reader_pipe.close
raise RuntimeError.new("Error executing process: #{message}")
end
reader_pipe.close

pid
end

def self.prepare_args(command : String, args : Enumerable(String)?, shell : Bool) : Array(String)
if shell
command = %(#{command} "${@}") unless command.includes?(' ')
shell_args = ["/bin/sh", "-c", command, "--"]

if args
unless command.includes?(%("${@}"))
raise ArgumentError.new(%(can't specify arguments in both, command and args without including "${@}" into your command))
end

{% if flag?(:freebsd) %}
shell_args << ""
{% end %}

shell_args.concat(args)
end

shell_args
else
command_args = [command]
command_args.concat(args) if args
command_args
end
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

command = command_args[0]
argv = command_args.map &.check_no_null_byte.to_unsafe
argv << Pointer(UInt8).null

LibC.execvp(command, argv)
raise RuntimeError.from_errno
end

private def self.reopen_io(src_io : IO::FileDescriptor, dst_io : IO::FileDescriptor)
src_io = to_real_fd(src_io)

dst_io.reopen(src_io)
dst_io.blocking = true
dst_io.close_on_exec = false
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
end
end

def self.chroot(path)
path.check_no_null_byte
if LibC.chroot(path) != 0
raise RuntimeError.from_errno("Failed to chroot")
end

if LibC.chdir("/") != 0
errno = RuntimeError.from_errno("chdir after chroot failed")
errno.callstack = CallStack.new
errno.inspect_with_backtrace(STDERR)
abort("Unresolvable state, exiting...")
end
end
end
Loading