Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: jruby/jruby
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 679bb6e81b62
Choose a base ref
...
head repository: jruby/jruby
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 4f8e6bde9400
Choose a head ref
  • 3 commits
  • 4 files changed
  • 1 contributor

Commits on Feb 15, 2017

  1. Copy the full SHA
    221cd42 View commit details
  2. Check for tty like MRI does.

    headius committed Feb 15, 2017
    Copy the full SHA
    bfb5eeb View commit details
  3. Copy the full SHA
    4f8e6bd View commit details
Showing with 168 additions and 57 deletions.
  1. +5 −3 core/src/main/java/org/jruby/Ruby.java
  2. +22 −1 core/src/main/java/org/jruby/util/io/FilenoUtil.java
  3. +3 −2 core/src/main/java/org/jruby/util/io/OpenFile.java
  4. +138 −51 lib/ruby/stdlib/pty.rb
8 changes: 5 additions & 3 deletions core/src/main/java/org/jruby/Ruby.java
Original file line number Diff line number Diff line change
@@ -285,6 +285,9 @@ private Ruby(RubyInstanceConfig config) {
objectSpacer = DISABLED_OBJECTSPACE;
}

posix = POSIXFactory.getPOSIX(new JRubyPOSIXHandler(this), config.isNativeEnabled());
filenoUtil = new FilenoUtil(posix);

reinitialize(false);
}

@@ -1136,7 +1139,6 @@ public boolean isClassDefined(String name) {
private void init() {
// Construct key services
loadService = config.createLoadService(this);
posix = POSIXFactory.getPOSIX(new JRubyPOSIXHandler(this), config.isNativeEnabled());
javaSupport = loadJavaSupport();

executor = new ThreadPoolExecutor(
@@ -4810,7 +4812,7 @@ private MRIRecursionGuard oldRecursionGuard() {
private final Invalidator checkpointInvalidator;
private final ThreadService threadService;

private POSIX posix;
private final POSIX posix;

private final ObjectSpace objectSpace = new ObjectSpace();

@@ -5094,7 +5096,7 @@ public void addToObjectSpace(boolean useObjectSpace, IRubyObject object) {
private final Config configBean;
private final org.jruby.management.Runtime runtimeBean;

private final FilenoUtil filenoUtil = new FilenoUtil();
private final FilenoUtil filenoUtil;

private Interpreter interpreter = new Interpreter();

23 changes: 22 additions & 1 deletion core/src/main/java/org/jruby/util/io/FilenoUtil.java
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@

import jnr.enxio.channels.NativeDeviceChannel;
import jnr.enxio.channels.NativeSocketChannel;
import jnr.posix.FileStat;
import jnr.posix.POSIX;
import jnr.unixsocket.UnixServerSocketChannel;
import jnr.unixsocket.UnixSocketChannel;

@@ -17,6 +19,10 @@
* Utilities for working with native fileno and Java structures that wrap them.
*/
public class FilenoUtil {
public FilenoUtil(POSIX posix) {
this.posix = posix;
}

public static FileDescriptor getDescriptorFromChannel(Channel channel) {
if (SEL_CH_IMPL_GET_FD != null && SEL_CH_IMPL.isInstance(channel)) {
// Pipe Source and Sink, Sockets, and other several other selectable channels
@@ -52,7 +58,21 @@ public static FileDescriptor getDescriptorFromChannel(Channel channel) {
}

public ChannelFD getWrapperFromFileno(int fileno) {
return filenoMap.get(fileno);
ChannelFD fd = filenoMap.get(fileno);

// This is a hack to get around stale ChannelFD that are closed when a descriptor is reused.
// It appears to happen for openpty, and in theory could happen for any IO call that produces
// a new descriptor.
if (fd != null && !fd.ch.isOpen() && !isFake(fileno)) {
FileStat stat = posix.allocateStat();
if (posix.fstat(fileno, stat) >= 0) {
// found ChannelFD is closed, but actual fileno is open; clear it.
filenoMap.remove(fileno);
fd = null;
}
}

return fd;
}

public void registerWrapper(int fileno, ChannelFD wrapper) {
@@ -157,6 +177,7 @@ public static int filenoFrom(FileDescriptor fd) {
public static final int FIRST_FAKE_FD = 100000;
protected final AtomicInteger internalFilenoIndex = new AtomicInteger(FIRST_FAKE_FD);
private final Map<Integer, ChannelFD> filenoMap = new ConcurrentHashMap<Integer, ChannelFD>();
private final POSIX posix;

private static final Class SEL_CH_IMPL;
private static final Method SEL_CH_IMPL_GET_FD;
5 changes: 3 additions & 2 deletions core/src/main/java/org/jruby/util/io/OpenFile.java
Original file line number Diff line number Diff line change
@@ -2393,8 +2393,9 @@ public boolean isBlocking() {

// MRI: check_tty
public void checkTTY() {
// TODO: native descriptors? Is this only used for stdio?
if (stdio_file != null) {
if (fd.realFileno != -1 && runtime.getPosix().isatty(fd.realFileno) != 0
|| stdio_file != null) {

boolean locked = lock();
try {
mode |= TTY | DUPLEX;
189 changes: 138 additions & 51 deletions lib/ruby/stdlib/pty.rb
Original file line number Diff line number Diff line change
@@ -1,69 +1,156 @@
require 'ffi'
require 'fcntl'

module PTY
private
module LibUtil
extend FFI::Library
ffi_lib FFI::Library::LIBC
# forkpty(3) is in libutil on linux and BSD, libc on MacOS
# openpty(3) is in libutil on linux and BSD, libc on MacOS
if FFI::Platform.linux? || (FFI::Platform.bsd? && !FFI::Platform.mac?)
ffi_lib 'libutil'
end
attach_function :forkpty, [ :buffer_out, :buffer_out, :buffer_in, :buffer_in ], :pid_t
attach_function :openpty, [ :buffer_out, :buffer_out, :buffer_in, :buffer_in, :buffer_in ], :int
end
module LibC
extend FFI::Library
ffi_lib FFI::Library::LIBC
attach_function :close, [ :int ], :int
attach_function :strerror, [ :int ], :string
attach_function :execv, [ :string, :buffer_in ], :int
attach_function :execvp, [ :string, :buffer_in ], :int
attach_function :dup2, [ :int, :int ], :int
attach_function :dup, [ :int ], :int
attach_function :_exit, [ :int ], :void
end
Buffer = FFI::Buffer
def self.build_args(args)
cmd = args.shift
cmd_args = args.map do |arg|
FFI::MemoryPointer.from_string(arg)

class ChildExited < RuntimeError
attr_reader :status

def initialize(status)
@status = status
end
exec_args = FFI::MemoryPointer.new(:pointer, 1 + cmd_args.length + 1)
exec_cmd = FFI::MemoryPointer.from_string(cmd)
exec_args[0].put_pointer(0, exec_cmd)
cmd_args.each_with_index do |arg, i|
exec_args[i + 1].put_pointer(0, arg)

def inspect
"<#{self.class.name}: #{status}>"
end
[ cmd, exec_args ]
end
public
def self.getpty(*args)
mfdp = Buffer.alloc_out :int
name = Buffer.alloc_out 1024
exec_cmd, exec_args = build_args(args)
pid = LibUtil.forkpty(mfdp, name, nil, nil)
#
# We want to do as little as possible in the child process, since we're running
# without any GC thread now, so test for the child case first
#
if pid == 0
LibC.execvp(exec_cmd, exec_args)
LibC._exit(1)

class << self
def spawn(*args, &block)
if args.size > 0
exec_pty(args, &block)
else
exec_pty(get_shell, &block)
end
end
raise "forkpty failed: #{LibC.strerror(FFI.errno)}" if pid < 0
masterfd = mfdp.get_int(0)
rfp = FFI::IO.for_fd(masterfd, "r")
wfp = FFI::IO.for_fd(LibC.dup(masterfd), "w")
if block_given?
retval = yield rfp, wfp, pid
begin; rfp.close; rescue Exception; end
begin; wfp.close; rescue Exception; end
alias :getpty :spawn

def open
masterfd, slavefd, slave_name = openpty

master = IO.for_fd(masterfd, IO::RDWR)
slave = File.for_fd(slavefd, IO::RDWR)

File.chmod(0600, slave_name)

slave.define_singleton_method(:path) do
slave_name
end

slave.define_singleton_method(:tty?) do
true
end

fds = [master, slave]
fds.each do |fd|
fd.sync = true

hack_close_on_exec(fd)
end

return fds unless block_given?

begin
retval = yield(fds.dup)
ensure
fds.reject(&:closed?).each(&:close)
end

retval
else
[ rfp, wfp, pid ]
end
end
def self.spawn(*args, &block)
self.getpty(*args, &block)

def check(target_pid, exception = false)
pid, status = Process.waitpid2(target_pid, Process::WNOHANG|Process::WUNTRACED)

# I sometimes see #<Process::Status: pid 0 signal 36> here.
# See Github issue #3117
if pid == target_pid && status
if exception
raise ChildExited.new(status)
else
return status
end
end
rescue SystemCallError => e
nil
end

private

def openpty
master = FFI::Buffer.alloc_out :int
slave = FFI::Buffer.alloc_out :int
name = FFI::Buffer.alloc_out 1024

result = LibUtil.openpty(master, slave, name, nil, nil)
if result != 0
raise(exception_for_errno(FFI.errno) || SystemCallError.new("errno=#{FFI.errno}"))
end

[master.get_int(0), slave.get_int(0), name.get_string(0)]
end

def exec_pty(args)
master, slave = open

read, write = IO.pipe
pid = Process.spawn(*args, in: read, out: slave, err: slave, close_others: true, pgroup: true)
[read, slave].each(&:close)

hack_close_on_exec(master)
hack_close_on_exec(write)

ret = [master, write, pid]

if block_given?
begin
retval = yield(ret.dup)
ensure
ret[0, 2].reject(&:closed?).each(&:close)
end
retval
else
ret
end
end

def get_shell
if shell = ENV['SHELL']
return shell
elsif pwent = Etc.getpwuid(Process.uid) && pwent.shell
return pwent.shell
else
"/bin/sh"
end
end

def hack_close_on_exec(fd)
# I assume this isn't actually supported for good reasons.
# but let's see how close to passing test_pty we can get.
fl = fd.fcntl(Fcntl::F_GETFL, 0)
fd.fcntl(Fcntl::F_SETFL, Fcntl::FD_CLOEXEC|fl)
fd.define_singleton_method(:close_on_exec?) do
true
end
end

def exception_for_errno(errno)
Errno.constants.each do |name|
err = Errno.const_get(name)
if err.constants.include?(:Errno) && err.const_get(:Errno) == errno
return err
end
end
SystemCallError.new("errno=#{errno}")
end
end
end