mirror of
https://github.com/mistergibson/gxg-web-client.git
synced 2026-08-15 11:34:29 -07:00
3609 lines
89 KiB
Ruby
3609 lines
89 KiB
Ruby
class IO
|
||
include Enumerable
|
||
|
||
def self.fnmatch(pattern, path, flags)
|
||
Rubinius.primitive :io_fnmatch
|
||
raise PrimitiveFailure, "IO#fnmatch primitive failed"
|
||
end
|
||
|
||
def socket_recv(bytes, flags, type)
|
||
Rubinius.primitive :io_socket_read
|
||
raise PrimitiveFailure, "io_socket_read failed"
|
||
end
|
||
|
||
module TransferIO
|
||
def send_io(io)
|
||
Rubinius.primitive :io_send_io
|
||
raise PrimitiveFailure, "IO#send_io failed"
|
||
end
|
||
|
||
def recv_fd
|
||
Rubinius.primitive :io_recv_fd
|
||
raise PrimitiveFailure, "IO#recv_fd failed"
|
||
end
|
||
end
|
||
|
||
module WaitReadable; end
|
||
module WaitWritable; end
|
||
|
||
class EAGAINWaitReadable < Errno::EAGAIN
|
||
include ::IO::WaitReadable
|
||
end
|
||
|
||
class EAGAINWaitWritable < Errno::EAGAIN
|
||
include ::IO::WaitWritable
|
||
end
|
||
|
||
class EWOULDBLOCKWaitReadable < Errno::EAGAIN
|
||
include WaitReadable
|
||
end
|
||
|
||
class EWOULDBLOCKWaitWritable < Errno::EAGAIN
|
||
include WaitWritable
|
||
end
|
||
|
||
class EINPROGRESSWaitReadable < Errno::EINPROGRESS
|
||
include WaitReadable
|
||
end
|
||
|
||
class EINPROGRESSWaitWritable < Errno::EINPROGRESS
|
||
include WaitWritable
|
||
end
|
||
|
||
class FileDescriptor
|
||
class RIOStream
|
||
def self.close(io, allow_exception)
|
||
Rubinius.primitive :rio_close
|
||
raise PrimitiveFailure, "IO::FileDescriptor::RIOStream.close primitive failed"
|
||
end
|
||
end
|
||
|
||
attr_reader :offset
|
||
|
||
def self.choose_type(fd, io)
|
||
stat = File::Stat.fstat(fd)
|
||
|
||
case stat.ftype
|
||
when "file"
|
||
BufferedFileDescriptor.new(fd, stat, io)
|
||
when "fifo", "characterSpecial"
|
||
FIFOFileDescriptor.new(fd, stat, io)
|
||
when "socket"
|
||
SocketFileDescriptor.new(fd, stat, io)
|
||
when "directory"
|
||
DirectoryFileDescriptor.new(fd, stat, io)
|
||
when "blockSpecial"
|
||
raise "cannot make block special"
|
||
when "link"
|
||
raise "cannot make link"
|
||
else
|
||
BufferedFileDescriptor.new(fd, stat, io)
|
||
end
|
||
end
|
||
|
||
def self.open_with_mode(path, mode, perm)
|
||
fd = open_with_cloexec(path, mode, perm)
|
||
|
||
if fd < 0
|
||
Errno.handle("failed to open file")
|
||
end
|
||
|
||
return fd
|
||
end
|
||
|
||
def self.open_with_cloexec(path, mode, perm)
|
||
if O_CLOEXEC
|
||
fd = FFI::Platform::POSIX.open(path, mode | O_CLOEXEC, perm)
|
||
update_max_fd(fd)
|
||
else
|
||
fd = FFI::Platform::POSIX.open(path, mode, perm)
|
||
new_open_fd(fd)
|
||
end
|
||
|
||
return fd
|
||
end
|
||
|
||
def self.new_open_fd(new_fd)
|
||
if new_fd > 2
|
||
flags = FFI::Platform::POSIX.fcntl(new_fd, F_GETFD, 0)
|
||
Errno.handle("fcntl(2) failed") if FFI.call_failed?(flags)
|
||
flags = FFI::Platform::POSIX.fcntl(new_fd, F_SETFD, flags | FD_CLOEXEC)
|
||
Errno.handle("fcntl(2) failed") if FFI.call_failed?(flags)
|
||
end
|
||
|
||
update_max_fd(new_fd)
|
||
end
|
||
|
||
def self.pagesize
|
||
@pagesize ||= FFI::Platform::POSIX.getpagesize
|
||
end
|
||
|
||
def self.truncate(name, offset)
|
||
raise RangeError, "bignum too big to convert into `long'" if offset.kind_of?(Bignum)
|
||
|
||
status = FFI::Platform::POSIX.truncate(name, offset)
|
||
Errno.handle("truncate(2) failed") if FFI.call_failed?(status)
|
||
return status
|
||
end
|
||
|
||
def self.update_max_fd(new_fd)
|
||
@@max_descriptors.get_and_set(new_fd)
|
||
end
|
||
|
||
def self.max_fd
|
||
@@max_descriptors.get
|
||
end
|
||
|
||
def self.get_flags(fd)
|
||
if IO::F_GETFL
|
||
if FFI.call_failed?(flags = FFI::Platform::POSIX.fcntl(fd, IO::F_GETFL, 0))
|
||
Errno.handle("fcntl(2) failed")
|
||
end
|
||
else
|
||
flags = 0
|
||
end
|
||
flags
|
||
end
|
||
|
||
def self.clear_flag(flag, fd)
|
||
flags = get_flags(fd)
|
||
if (flags & flag) == 0
|
||
flags &= ~flag
|
||
if FFI.call_failed?(flags = FFI::Platform::POSIX.fcntl(fd, IO::F_SETFL, flags))
|
||
Errno.handle("fcntl(2) failed")
|
||
end
|
||
end
|
||
end
|
||
|
||
def self.set_flag(flag, fd)
|
||
flags = get_flags(fd)
|
||
if (flags & flag) == 0
|
||
flags |= flag
|
||
if FFI.call_failed?(flags = FFI::Platform::POSIX.fcntl(fd, IO::F_SETFL, flags))
|
||
Errno.handle("fcntl(2) failed")
|
||
end
|
||
end
|
||
end
|
||
|
||
def self.get_empty_8bit_buffer
|
||
"".force_encoding(Encoding::ASCII_8BIT)
|
||
end
|
||
|
||
def initialize(fd, stat, io)
|
||
@descriptor, @stat, @io = fd, stat, io
|
||
acc_mode = FileDescriptor.get_flags(@descriptor)
|
||
|
||
if acc_mode < 0
|
||
# Assume it's closed.
|
||
if Errno.eql?(Errno::EBADF::Errno)
|
||
@descriptor = -1
|
||
end
|
||
|
||
@mode = nil
|
||
else
|
||
@mode = acc_mode
|
||
end
|
||
|
||
@sync = true
|
||
@autoclose = false
|
||
reset_positioning(@stat)
|
||
|
||
# Don't bother to add finalization for stdio
|
||
if @descriptor >= 3
|
||
# Sometimes a FileDescriptor class is replaced (see IO#reopen) so we need to be
|
||
# careful we don't finalize that descriptor. Probably need a way to cancel
|
||
# the finalization when we are transferring an FD from one instance to another.
|
||
ObjectSpace.define_finalizer(self)
|
||
end
|
||
end
|
||
|
||
private :initialize
|
||
|
||
def autoclose=(bool)
|
||
@autoclose = bool
|
||
end
|
||
|
||
def descriptor
|
||
@descriptor
|
||
end
|
||
|
||
def descriptor=(value)
|
||
@descriptor = value
|
||
end
|
||
|
||
def mode
|
||
@mode
|
||
end
|
||
|
||
def mode=(value)
|
||
@mode = value
|
||
end
|
||
|
||
def sync
|
||
@sync
|
||
end
|
||
|
||
def sync=(value)
|
||
@sync = value
|
||
end
|
||
|
||
CLONG_OVERFLOW = 1 << 64
|
||
|
||
def lseek(offset, whence=SEEK_SET)
|
||
ensure_open
|
||
|
||
# FIXME: check +amount+ to make sure it isn't too large
|
||
raise RangeError if offset > CLONG_OVERFLOW
|
||
|
||
position = FFI::Platform::POSIX.lseek(descriptor, offset, whence)
|
||
|
||
if FFI.call_failed?(position)
|
||
Errno.handle("seek failed: fd: #{descriptor}, offset: #{offset}, from: #{whence}")
|
||
end
|
||
|
||
@offset = position
|
||
determine_eof
|
||
|
||
return position
|
||
end
|
||
|
||
def read(length, output_string=nil)
|
||
length ||= FileDescriptor.pagesize
|
||
|
||
while true
|
||
ensure_open
|
||
|
||
storage = FFI::MemoryPointer.new(length)
|
||
raise IOError, "read(2) failed to malloc a buffer for read length #{length}" if storage.null?
|
||
bytes_read = read_into_storage(length, storage)
|
||
|
||
if bytes_read == 0
|
||
@eof = true if length > 0
|
||
return nil
|
||
else
|
||
break
|
||
end
|
||
end
|
||
|
||
if output_string
|
||
output_string.replace(storage.read_string(bytes_read))
|
||
else
|
||
output_string = storage.read_string(bytes_read).force_encoding(Encoding::ASCII_8BIT)
|
||
end
|
||
|
||
@offset += bytes_read
|
||
determine_eof
|
||
|
||
return output_string
|
||
end
|
||
|
||
def read_into_storage(count, storage)
|
||
while true
|
||
bytes_read = FFI::Platform::POSIX.read(descriptor, storage, count)
|
||
|
||
if FFI.call_failed?(bytes_read)
|
||
errno = Errno.errno
|
||
|
||
if errno == Errno::EAGAIN::Errno || errno == Errno::EINTR::Errno
|
||
ensure_open
|
||
next
|
||
else
|
||
Errno.handle "read(2) failed"
|
||
end
|
||
else
|
||
break
|
||
end
|
||
end
|
||
|
||
# before returning verify file hasn't been closed in another thread
|
||
ensure_open
|
||
|
||
return bytes_read
|
||
end
|
||
private :read_into_storage
|
||
|
||
def write(str)
|
||
buf_size = str.bytesize
|
||
left = buf_size
|
||
|
||
buffer = FFI::MemoryPointer.new(left)
|
||
buffer.write_string(str)
|
||
error = false
|
||
|
||
while left > 0
|
||
bytes_written = FFI::Platform::POSIX.write(@descriptor, buffer, left)
|
||
|
||
if FFI.call_failed?(bytes_written)
|
||
errno = Errno.errno
|
||
if errno == Errno::EINTR::Errno || errno == Errno::EAGAIN::Errno
|
||
# do a #select and wait for descriptor to become writable
|
||
if blocking?
|
||
Select.wait_for_writable(@descriptor)
|
||
next
|
||
else
|
||
break
|
||
end
|
||
end
|
||
|
||
error = true
|
||
break
|
||
end
|
||
|
||
break if error
|
||
|
||
left -= bytes_written
|
||
buffer += bytes_written
|
||
@offset += bytes_written
|
||
|
||
if @offset > @total_size
|
||
@total_size = @offset
|
||
@eof = false # only a failed read can set EOF!
|
||
end
|
||
end
|
||
|
||
Errno.handle("write failed") if error
|
||
|
||
return(buf_size - left)
|
||
end
|
||
|
||
def close
|
||
ensure_open
|
||
fd = @descriptor
|
||
|
||
if fd != -1
|
||
# return early if this handle was promoted to a stream by a C-ext
|
||
return if RIOStream.close(@io, true)
|
||
ret_code = FFI::Platform::POSIX.close(fd)
|
||
|
||
if FFI.call_failed?(ret_code)
|
||
Errno.handle("close failed")
|
||
elsif ret_code == 0
|
||
# no op
|
||
else
|
||
raise IOError, "::close(): Unknown error on fd #{fd}"
|
||
end
|
||
end
|
||
|
||
@descriptor = -1
|
||
|
||
return nil
|
||
end
|
||
|
||
def determine_eof
|
||
if @offset >= @total_size
|
||
@eof = true
|
||
@total_size += (@total_size - @offset)
|
||
@total_size = @offset if @offset > @total_size
|
||
else
|
||
@eof = false
|
||
end
|
||
end
|
||
private :determine_eof
|
||
|
||
def eof?
|
||
@eof
|
||
end
|
||
|
||
# This is NOT the same as close().
|
||
def shutdown(how)
|
||
ensure_open
|
||
fd = descriptor
|
||
|
||
if how != IO::SHUT_RD && how != IO::SHUT_WR && how != IO::SHUT_RDWR
|
||
raise ArgumentError, "::shutdown(): Invalid `how` #{how} for fd #{fd}"
|
||
end
|
||
|
||
ret_code = FFI::Platform::POSIX.shutdown(fd, how)
|
||
|
||
if FFI.call_failed?(ret_code)
|
||
Errno.handle("shutdown(2) failed")
|
||
elsif ret_code == 0
|
||
if how == IO::SHUT_RDWR
|
||
close
|
||
self.descriptor = -2
|
||
end
|
||
else
|
||
Errno.handle("::shutdown(): Unknown error on fd #{fd}")
|
||
end
|
||
|
||
return how
|
||
end
|
||
|
||
def ensure_open(fd=nil)
|
||
fd ||= descriptor
|
||
|
||
if fd.nil?
|
||
raise IOError, "uninitialized stream"
|
||
elsif fd == -1
|
||
raise IOError, "closed stream"
|
||
elsif fd == -2
|
||
raise IOError, "shutdown stream"
|
||
end
|
||
return nil
|
||
end
|
||
|
||
def force_read_only
|
||
@mode = (@mode & ~IO::O_ACCMODE ) | O_RDONLY
|
||
end
|
||
|
||
def force_write_only
|
||
@mode = (@mode & ~IO::O_ACCMODE) | O_WRONLY
|
||
end
|
||
|
||
def read_only?
|
||
(@mode & O_ACCMODE) == O_RDONLY
|
||
end
|
||
|
||
def write_only?
|
||
(@mode & O_ACCMODE) == O_WRONLY
|
||
end
|
||
|
||
def read_write?
|
||
(@mode & O_ACCMODE) == O_RDWR
|
||
end
|
||
|
||
def reopen(other_fd)
|
||
current_fd = @descriptor
|
||
|
||
if FFI.call_failed?(FFI::Platform::POSIX.dup2(other_fd, current_fd))
|
||
Errno.handle("reopen")
|
||
end
|
||
|
||
set_mode
|
||
reset_positioning
|
||
|
||
return true
|
||
end
|
||
|
||
def reopen_path(path, mode)
|
||
current_fd = @descriptor
|
||
other_fd = FileDescriptor.open_with_cloexec(path, mode, 0666)
|
||
|
||
Errno.handle("could not reopen path \"#{path}\"") if other_fd < 0
|
||
|
||
if FFI.call_failed?(FFI::Platform::POSIX.dup2(other_fd, current_fd))
|
||
if Errno.eql?(Errno::EBADF::Errno)
|
||
# means current_fd is closed, so set ourselves to use the new fd and continue
|
||
@descriptor = other_fd
|
||
else
|
||
FFI::Platform::POSIX.close(other_fd) if other_fd > 0
|
||
Errno.handle("could not reopen path \"#{path}\"")
|
||
end
|
||
else
|
||
FFI::Platform::POSIX.close(other_fd)
|
||
end
|
||
|
||
set_mode
|
||
reset_positioning
|
||
|
||
return true
|
||
end
|
||
|
||
def reset_positioning(stat=nil)
|
||
# Discover final size of file so we can set EOF properly
|
||
stat = File::Stat.fstat(@descriptor) unless stat
|
||
@total_size = stat.size
|
||
|
||
# We may have reopened a file descriptor that went from "file" to a different
|
||
# ftype which doesn't allow seeking, so catch it here.
|
||
if stat.ftype == "file"
|
||
seek_positioning
|
||
else
|
||
@offset = 0
|
||
end
|
||
|
||
determine_eof
|
||
end
|
||
|
||
def seek_positioning
|
||
@offset = lseek(0, SEEK_CUR) # find current position if we are reopening!
|
||
end
|
||
private :seek_positioning
|
||
|
||
def set_mode
|
||
@mode = FileDescriptor.get_flags(@descriptor)
|
||
end
|
||
|
||
def blocking?
|
||
(FileDescriptor.get_flags(@descriptor) & O_NONBLOCK) == 0
|
||
end
|
||
|
||
def clear_nonblock
|
||
flags = FileDescriptor.get_flags(@descriptor)
|
||
FileDescriptor.clear_flag(O_NONBLOCK, @descriptor)
|
||
end
|
||
|
||
def set_nonblock
|
||
flags = FileDescriptor.get_flags(@descriptor)
|
||
FileDescriptor.set_flag(O_NONBLOCK, @descriptor)
|
||
end
|
||
|
||
def ftruncate(offset)
|
||
ensure_open
|
||
|
||
# FIXME: fail if +offset+ is too large, see C++ code
|
||
|
||
status = FFI::Platform::POSIX.ftruncate(descriptor, offset)
|
||
Errno.handle("ftruncate(2) failed") if FFI.call_failed?(status)
|
||
return status
|
||
end
|
||
|
||
##
|
||
# Returns true if ios is associated with a terminal device (tty), false otherwise.
|
||
#
|
||
# File.new("testfile").isatty #=> false
|
||
# File.new("/dev/tty").isatty #=> true
|
||
def tty?
|
||
ensure_open
|
||
FFI::Platform::POSIX.isatty(@descriptor) == 1
|
||
end
|
||
|
||
def ttyname
|
||
# Need to protect this call with a lock since the OS copies this string information
|
||
# to an internal static object and returns a pointer to that object. Future calls
|
||
# to #ttyname modify that same object. Therefore, we need to protect this from
|
||
# race conditions.
|
||
Rubinius.lock(self)
|
||
name = FFI::Platform::POSIX.ttyname(descriptor)
|
||
if name
|
||
name
|
||
else
|
||
Errno.handle("ttyname(3) failed")
|
||
nil
|
||
end
|
||
ensure
|
||
Rubinius.unlock(self)
|
||
end
|
||
|
||
def inspect
|
||
stat = File::Stat.fstat(@descriptor)
|
||
"fd [#{descriptor}], mode [#{@mode}], total_size [#{@total_size}], offset [#{@offset}], eof [#{@eof}], stat.size [#{stat.size}], written? [#{@written}]"
|
||
end
|
||
|
||
def cancel_finalizer
|
||
ObjectSpace.undefine_finalizer(self)
|
||
end
|
||
|
||
def __finalize__
|
||
return if @descriptor.nil? || @descriptor == -1
|
||
|
||
fd = @descriptor
|
||
|
||
# Should flush a write buffer here if one exists. Current
|
||
# implementation does not buffer writes internally, so this
|
||
# is a no-op.
|
||
|
||
# Do not close stdin/out/err (0, 1, and 2)
|
||
if @descriptor > 3
|
||
@descriptor = -1
|
||
|
||
# Take care of any IO cleanup for the C API here. An IO may
|
||
# have been promoted to a low-level RIO struct using #fdopen,
|
||
# so we MUST use #fclose to clost it.
|
||
return if RIOStream.close(@io, false)
|
||
|
||
# Ignore any return code... don't care if it fails
|
||
FFI::Platform::POSIX.close(fd) if @autoclose
|
||
end
|
||
end
|
||
end # class FileDescriptor
|
||
|
||
class BufferedFileDescriptor < FileDescriptor
|
||
|
||
def buffer_reset
|
||
@unget_buffer.clear
|
||
end
|
||
private :buffer_reset
|
||
|
||
def read(length, output_string=nil)
|
||
length ||= FileDescriptor.pagesize
|
||
|
||
# Preferentially read from the buffer and then from the underlying
|
||
# FileDescriptor.
|
||
# FIXME: make this logic clearer.
|
||
if length > @unget_buffer.size
|
||
@offset += @unget_buffer.size
|
||
length -= @unget_buffer.size
|
||
|
||
str = @unget_buffer.inject(self.class.get_empty_8bit_buffer) { |sum, val| val.chr + sum }
|
||
str2 = super(length, output_string)
|
||
|
||
if str.size == 0 && str2.nil?
|
||
determine_eof
|
||
return nil
|
||
elsif str2
|
||
str += str2
|
||
end
|
||
buffer_reset
|
||
elsif length == @unget_buffer.size
|
||
@offset += length
|
||
length -= @unget_buffer.size
|
||
|
||
str = @unget_buffer.inject(self.class.get_empty_8bit_buffer) { |sum, val| val.chr + sum }
|
||
buffer_reset
|
||
else
|
||
@offset += @unget_buffer.size
|
||
str = self.class.get_empty_8bit_buffer
|
||
|
||
length.times do
|
||
str << @unget_buffer.pop
|
||
end
|
||
end
|
||
|
||
if output_string
|
||
output_string.replace(str)
|
||
else
|
||
output_string = str.force_encoding(Encoding::ASCII_8BIT)
|
||
end
|
||
|
||
determine_eof
|
||
return output_string
|
||
end
|
||
|
||
def read_only_buffer(length)
|
||
unless @unget_buffer.empty?
|
||
if length >= @unget_buffer.size
|
||
@offset += @unget_buffer.size
|
||
length -= @unget_buffer.size
|
||
|
||
str = @unget_buffer.inject(self.class.get_empty_8bit_buffer) { |sum, val| val.chr + sum }
|
||
buffer_reset
|
||
[str, length]
|
||
else
|
||
@offset += @unget_buffer.size
|
||
str = self.class.get_empty_8bit_buffer
|
||
|
||
length.times do
|
||
str << @unget_buffer.pop
|
||
end
|
||
[str, 0]
|
||
end
|
||
else
|
||
[nil, length]
|
||
end
|
||
end
|
||
|
||
def seek(bytes, whence)
|
||
# @offset may not match actual file pointer if there were calls to #unget.
|
||
if whence == SEEK_CUR
|
||
# adjust the number of bytes to seek based on how far ahead we are with the buffer
|
||
bytes -= @unget_buffer.size
|
||
end
|
||
|
||
buffer_reset
|
||
lseek(bytes, whence)
|
||
end
|
||
|
||
def sysread(byte_count)
|
||
raise_if_buffering
|
||
read(byte_count)
|
||
end
|
||
|
||
def sysseek(bytes, whence)
|
||
raise_if_buffering
|
||
lseek(bytes, whence)
|
||
end
|
||
|
||
def eof?
|
||
super && @unget_buffer.empty?
|
||
end
|
||
|
||
def flush
|
||
#@unget_buffer.clear
|
||
end
|
||
|
||
def raise_if_buffering
|
||
raise IOError unless @unget_buffer.empty?
|
||
end
|
||
|
||
def reset_positioning(*args)
|
||
@unget_buffer = []
|
||
super
|
||
end
|
||
|
||
def write_nonblock(str)
|
||
buffer_reset
|
||
set_nonblock
|
||
|
||
buf_size = str.bytesize
|
||
left = buf_size
|
||
|
||
if left > 0
|
||
buffer = FFI::MemoryPointer.new(left)
|
||
buffer.write_string(str)
|
||
|
||
if FFI.call_failed?(bytes_written = FFI::Platform::POSIX.write(@descriptor, buffer, left))
|
||
clear_nonblock
|
||
Errno.raise_waitwritable("write_nonblock")
|
||
end
|
||
|
||
left -= bytes_written
|
||
buffer += bytes_written
|
||
@offset += bytes_written
|
||
end
|
||
|
||
return(buf_size - left)
|
||
end
|
||
|
||
def unget(byte)
|
||
@offset -= 1
|
||
@unget_buffer << byte
|
||
end
|
||
end # class BufferedFileDescriptor
|
||
|
||
class FIFOFileDescriptor < BufferedFileDescriptor
|
||
|
||
def self.connect_pipe_fds
|
||
fds = FFI::MemoryPointer.new(:int, 2)
|
||
|
||
Errno.handle("creating pipe failed") if FFI.call_failed?(FFI::Platform::POSIX.pipe(fds))
|
||
fd0, fd1 = fds.read_array_of_int(2)
|
||
|
||
FileDescriptor.new_open_fd(fd0)
|
||
FileDescriptor.new_open_fd(fd1)
|
||
|
||
return [fd0, fd1]
|
||
end
|
||
|
||
def initialize(fd, stat, io, mode=nil)
|
||
super(fd, stat, self)
|
||
@mode = mode if mode
|
||
@eof = false # force to false
|
||
end
|
||
|
||
private :initialize
|
||
|
||
def determine_eof
|
||
if @offset >= @total_size
|
||
@eof = true
|
||
|
||
# No seeking allowed on a pipe, so its size is always its offset
|
||
@total_size = @offset
|
||
end
|
||
end
|
||
|
||
def eof?
|
||
# The only way to confirm we are EOF with a pipe is to try to read from
|
||
# it. If we fail, then we are EOF. If we succeed, then unget the byte
|
||
# so that EOF is false (i.e. more to read).
|
||
str = read(1)
|
||
unget(str) if str
|
||
super
|
||
end
|
||
|
||
def seek_positioning
|
||
# no seeking allowed for pipes
|
||
@offset = 0
|
||
end
|
||
|
||
def sysseek(offset, whence=SEEK_SET)
|
||
# lseek does not work with pipes.
|
||
raise Errno::ESPIPE
|
||
end
|
||
end # class FIFOFileDescriptor
|
||
|
||
class DirectoryFileDescriptor < BufferedFileDescriptor
|
||
end # class DirectoryFileDescriptor
|
||
|
||
class SocketFileDescriptor < FIFOFileDescriptor
|
||
def initialize(fd, stat, io)
|
||
super
|
||
|
||
@mode &= O_ACCMODE
|
||
@sync = true
|
||
end
|
||
|
||
private :initialize
|
||
|
||
def force_read_write
|
||
@mode &= ~(O_RDONLY | O_WRONLY)
|
||
@mode |= O_RDWR
|
||
end
|
||
end # class SocketFileDescriptor
|
||
|
||
# Encapsulates all of the logic necessary for handling #select.
|
||
class Select
|
||
#eval(Rubinius::Config['rbx.platform.timeval.class'])
|
||
|
||
class FDSet
|
||
def self.new
|
||
Rubinius.primitive :fdset_allocate
|
||
raise PrimitiveFailure, "FDSet.allocate failed"
|
||
end
|
||
|
||
def zero
|
||
Rubinius.primitive :fdset_zero
|
||
raise PrimitiveFailure, "FDSet.zero failed"
|
||
end
|
||
|
||
def set(descriptor)
|
||
Rubinius.primitive :fdset_set
|
||
raise PrimitiveFailure, "FDSet.set failed"
|
||
end
|
||
|
||
def set?(descriptor)
|
||
Rubinius.primitive :fdset_is_set
|
||
raise PrimitiveFailure, "FDSet.set? failed"
|
||
end
|
||
|
||
def to_set
|
||
Rubinius.primitive :fdset_to_set
|
||
raise PrimitiveFailure, "FDSet.to_set failed"
|
||
end
|
||
end
|
||
|
||
def self.fd_set_from_array(array)
|
||
highest = -1
|
||
fd_set = FDSet.new
|
||
fd_set.zero
|
||
|
||
array.each do |io|
|
||
io = io[1] if io.is_a?(Array)
|
||
descriptor = io.descriptor
|
||
|
||
if descriptor >= FD_SETSIZE
|
||
raise IOError
|
||
end
|
||
|
||
if descriptor >= 0
|
||
highest = descriptor > highest ? descriptor : highest
|
||
fd_set.set(descriptor)
|
||
end
|
||
end
|
||
|
||
return [fd_set, highest]
|
||
end
|
||
|
||
def self.collect_set_fds(array, fd_set)
|
||
return [] unless fd_set
|
||
array.map do |io|
|
||
key, io = if io.is_a?(Array)
|
||
[io[0], io[1]]
|
||
else
|
||
[io, io]
|
||
end
|
||
|
||
if fd_set.set?(io.descriptor) || io.descriptor < 0
|
||
key
|
||
else
|
||
nil
|
||
end
|
||
end.compact
|
||
end
|
||
|
||
def self.timer_add(time1, time2, result)
|
||
result[:tv_sec] = time1[:tv_sec] + time2[:tv_sec]
|
||
result[:tv_usec] = time1[:tv_usec] + time2[:tv_usec]
|
||
|
||
if result[:tv_usec] >= 1_000_000
|
||
result[:tv_sec] += 1
|
||
result[:tv_usec] -= 1_000_000
|
||
end
|
||
end
|
||
|
||
def self.timer_sub(time1, time2, result)
|
||
result[:tv_sec] = time1[:tv_sec] - time2[:tv_sec]
|
||
result[:tv_usec] = time1[:tv_usec] - time2[:tv_usec]
|
||
|
||
if result[:tv_usec] < 0
|
||
result[:tv_sec] -= 1
|
||
result[:tv_usec] += 1_000_000
|
||
end
|
||
end
|
||
|
||
def self.make_timeval_timeout(timeout)
|
||
if timeout
|
||
limit = Rubinius::FFI::Platform::POSIX::TimeVal.new
|
||
future = Rubinius::FFI::Platform::POSIX::TimeVal.new
|
||
|
||
limit[:tv_sec] = (timeout / 1_000_000.0).to_i
|
||
limit[:tv_usec] = (timeout % 1_000_000.0).to_i
|
||
|
||
# Get current time to be used if select is interrupted and we have to recalculate the sleep time
|
||
if FFI.call_failed?(FFI::Platform::POSIX.gettimeofday(future, nil))
|
||
Errno.handle("gettimeofday(2) failed")
|
||
end
|
||
|
||
timer_add(future, limit, future)
|
||
end
|
||
|
||
[limit, future]
|
||
end
|
||
|
||
def self.reset_timeval_timeout(time_limit, future)
|
||
now = Rubinius::FFI::Platform::POSIX::TimeVal.new
|
||
|
||
if FFI.call_failed?(FFI::Platform::POSIX.gettimeofday(now, nil))
|
||
Errno.handle("gettimeofday(2) failed")
|
||
end
|
||
|
||
timer_sub(future, now, time_limit)
|
||
end
|
||
|
||
def self.readable_events(read_fd)
|
||
fd_set = FDSet.new
|
||
fd_set.zero
|
||
fd_set.set(read_fd)
|
||
|
||
# sets fields to zero by default
|
||
timer = Rubinius::FFI::Platform::POSIX::TimeVal.new
|
||
|
||
FFI::Platform::POSIX.select(read_fd + 1, fd_set.to_set, nil, nil, timer)
|
||
end
|
||
|
||
def self.wait_for_writable(fd)
|
||
fd_set = FDSet.new
|
||
fd_set.zero
|
||
fd_set.set(fd)
|
||
|
||
FFI::Platform::POSIX.select(fd + 1, nil, fd_set.to_set, nil, nil)
|
||
end
|
||
|
||
def self.select(readables, writables, errorables, timeout)
|
||
read_set, highest_read_fd = readables.nil? ? [nil, nil] : fd_set_from_array(readables)
|
||
write_set, highest_write_fd = writables.nil? ? [nil, nil] : fd_set_from_array(writables)
|
||
error_set, highest_err_fd = errorables.nil? ? [nil, nil] : fd_set_from_array(errorables)
|
||
max_fd = [highest_read_fd, highest_write_fd, highest_err_fd].compact.max || -1
|
||
|
||
time_limit, future = make_timeval_timeout(timeout)
|
||
|
||
events = 0
|
||
loop do
|
||
events = FFI::Platform::POSIX.select(max_fd + 1,
|
||
(read_set ? read_set.to_set : nil),
|
||
(write_set ? write_set.to_set : nil),
|
||
(error_set ? error_set.to_set : nil),
|
||
time_limit)
|
||
|
||
if FFI.call_failed?(events)
|
||
|
||
if Errno::EAGAIN::Errno == Errno.errno || Errno::EINTR::Errno == Errno.errno
|
||
# return nil if async_interruption?
|
||
time_limit = reset_timeval_timeout(time_limit, future)
|
||
continue
|
||
end
|
||
|
||
Errno.handle("select(2) failed")
|
||
end
|
||
|
||
break
|
||
end
|
||
|
||
return nil if events.zero?
|
||
|
||
output_fds = []
|
||
output_fds << collect_set_fds(readables, read_set)
|
||
output_fds << collect_set_fds(writables, write_set)
|
||
output_fds << collect_set_fds(errorables, error_set)
|
||
return output_fds
|
||
end
|
||
|
||
def self.validate_and_convert_argument(objects)
|
||
if objects
|
||
raise TypeError, "Argument must be an Array" unless objects.respond_to?(:to_ary)
|
||
objects =
|
||
objects.to_ary.map do |obj|
|
||
if obj.kind_of? IO
|
||
raise IOError, "closed stream" if obj.closed?
|
||
obj
|
||
else
|
||
raise TypeError unless obj.respond_to?(:to_io)
|
||
io = obj.to_io
|
||
raise TypeError unless io
|
||
raise IOError, "closed stream" if io.closed?
|
||
[obj, io]
|
||
end
|
||
end
|
||
end
|
||
|
||
objects
|
||
end
|
||
end # class Select
|
||
|
||
attr_accessor :external
|
||
attr_accessor :internal
|
||
# intended to only be used by IO.setup to associate a new FileDescriptor object with instance of IO
|
||
attr_accessor :fd
|
||
|
||
def self.binread(file, length=nil, offset=0)
|
||
raise ArgumentError, "Negative length #{length} given" if !length.nil? && length < 0
|
||
|
||
File.open(file, "r", :encoding => "ascii-8bit:-") do |f|
|
||
f.seek(offset)
|
||
f.read(length)
|
||
end
|
||
end
|
||
|
||
def self.binwrite(file, string, *args)
|
||
offset, opts = args
|
||
opts ||= {}
|
||
if offset.is_a?(Hash)
|
||
offset, opts = nil, offset
|
||
end
|
||
|
||
mode, binary, external, internal, autoclose, encoding_options = IO.normalize_options(nil, opts)
|
||
unless mode
|
||
mode = File::CREAT | File::RDWR | File::BINARY
|
||
mode |= File::TRUNC unless offset
|
||
end
|
||
File.open(file, mode, encoding_options.merge(:encoding => (external || "ASCII-8BIT"))) do |f|
|
||
f.seek(offset || 0)
|
||
f.write(string)
|
||
end
|
||
end
|
||
|
||
class StreamCopier
|
||
def initialize(from, to, length, offset)
|
||
@length = length
|
||
@offset = offset
|
||
|
||
@from_io, @from = to_io(from, "rb")
|
||
@to_io, @to = to_io(to, "wb")
|
||
|
||
@method = read_method @from
|
||
end
|
||
|
||
private :initialize
|
||
|
||
def to_io(obj, mode)
|
||
if obj.kind_of? IO
|
||
flag = true
|
||
io = obj
|
||
else
|
||
flag = false
|
||
|
||
if obj.kind_of? String
|
||
io = File.open obj, mode
|
||
elsif obj.respond_to? :to_path
|
||
path = Rubinius::Type.coerce_to obj, String, :to_path
|
||
io = File.open path, mode
|
||
else
|
||
io = obj
|
||
end
|
||
end
|
||
|
||
return flag, io
|
||
end
|
||
|
||
def read_method(obj)
|
||
if obj.respond_to? :readpartial
|
||
:readpartial
|
||
else
|
||
:read
|
||
end
|
||
end
|
||
|
||
def run
|
||
@from.ensure_open_and_readable if @from.kind_of? IO
|
||
@to.ensure_open_and_writable if @to.kind_of? IO
|
||
|
||
if @offset
|
||
if @from_io && !@from.pipe?
|
||
saved_pos = @from.pos
|
||
else
|
||
saved_pos = 0
|
||
end
|
||
|
||
@from.seek @offset, IO::SEEK_CUR
|
||
end
|
||
|
||
size = @length ? @length : 16384
|
||
bytes = 0
|
||
|
||
begin
|
||
while data = @from.__send__(@method, size, "")
|
||
@to.write data
|
||
bytes += data.bytesize
|
||
|
||
break if @length && bytes >= @length
|
||
end
|
||
rescue EOFError
|
||
# done reading
|
||
end
|
||
|
||
@to.flush if @to.kind_of? IO
|
||
return bytes
|
||
ensure
|
||
if @from_io
|
||
@from.pos = saved_pos if @offset
|
||
else
|
||
@from.close if @from.kind_of? IO
|
||
end
|
||
|
||
@to.close if @to.kind_of? IO unless @to_io
|
||
end
|
||
end # class StreamCopier
|
||
|
||
def self.copy_stream(from, to, max_length=nil, offset=nil)
|
||
StreamCopier.new(from, to, max_length, offset).run
|
||
end
|
||
|
||
def self.foreach(name, separator=undefined, limit=undefined, options=undefined)
|
||
return to_enum(:foreach, name, separator, limit, options) unless block_given?
|
||
|
||
name = Rubinius::Type.coerce_to_path name
|
||
|
||
case separator
|
||
when Fixnum
|
||
options = limit
|
||
limit = separator
|
||
separator = $/
|
||
when undefined
|
||
separator = $/
|
||
when Hash
|
||
options = separator
|
||
separator = $/
|
||
when nil
|
||
# do nothing
|
||
else
|
||
separator = StringValue(separator)
|
||
end
|
||
|
||
case limit
|
||
when Fixnum, nil
|
||
# do nothing
|
||
when undefined
|
||
limit = nil
|
||
when Hash
|
||
if undefined.equal? options
|
||
options = limit
|
||
limit = nil
|
||
else
|
||
raise TypeError, "can't convert Hash into Integer"
|
||
end
|
||
else
|
||
value = limit
|
||
limit = Rubinius::Type.try_convert limit, Fixnum, :to_int
|
||
|
||
unless limit
|
||
options = Rubinius::Type.coerce_to value, Hash, :to_hash
|
||
end
|
||
end
|
||
|
||
case options
|
||
when Hash
|
||
# do nothing
|
||
when undefined, nil
|
||
options = { }
|
||
else
|
||
options = Rubinius::Type.coerce_to options, Hash, :to_hash
|
||
end
|
||
|
||
if name[0] == ?|
|
||
io = IO.popen(name[1..-1], "r")
|
||
return nil unless io
|
||
else
|
||
options[:mode] = "r" unless options.key? :mode
|
||
io = File.open(name, options)
|
||
end
|
||
|
||
begin
|
||
while line = io.gets(separator, limit)
|
||
yield line
|
||
end
|
||
ensure
|
||
$_ = nil
|
||
io.close
|
||
end
|
||
|
||
return nil
|
||
end
|
||
|
||
def self.readlines(name, separator=undefined, limit=undefined, options=undefined)
|
||
lines = []
|
||
saved_line = $_
|
||
foreach(name, separator, limit, options) { |l| lines << l }
|
||
|
||
$_ = saved_line
|
||
lines
|
||
end
|
||
|
||
def self.read_encode(io, str, encoding_options)
|
||
internal = io.internal_encoding
|
||
external = io.external_encoding || Encoding.default_external
|
||
|
||
if external.equal? Encoding::ASCII_8BIT
|
||
str.force_encoding external
|
||
elsif internal and external
|
||
ec = Encoding::Converter.new(external, internal, (encoding_options || {}))
|
||
ec.convert str
|
||
else
|
||
str.force_encoding external
|
||
end
|
||
end
|
||
|
||
def self.write(file, string, *args)
|
||
if args.size > 2
|
||
raise ArgumentError, "wrong number of arguments (#{args.size + 2} for 2..3)"
|
||
end
|
||
|
||
offset, opts = args
|
||
opts ||= {}
|
||
if offset.is_a?(Hash)
|
||
offset, opts = nil, offset
|
||
end
|
||
|
||
mode, binary, external, internal, autoclose, encoding_options = IO.normalize_options(nil, opts)
|
||
unless mode
|
||
mode = File::CREAT | File::WRONLY
|
||
mode |= File::TRUNC unless offset
|
||
end
|
||
|
||
open_args = opts[:open_args] || [mode, :encoding => (external || "ASCII-8BIT")]
|
||
open_args[1].merge!(encoding_options) if open_args[1].kind_of?(Hash)
|
||
File.open(file, *open_args) do |f|
|
||
f.seek(offset) if offset
|
||
f.write(string)
|
||
end
|
||
end
|
||
|
||
def self.for_fd(fd, mode=undefined, options=undefined)
|
||
new fd, mode, options
|
||
end
|
||
|
||
def self.read(name, length_or_options=undefined, offset=0, options=nil)
|
||
offset = 0 if offset.nil?
|
||
name = Rubinius::Type.coerce_to_path name
|
||
mode = "r"
|
||
|
||
if undefined.equal? length_or_options
|
||
length = undefined
|
||
elsif Rubinius::Type.object_kind_of? length_or_options, Hash
|
||
length = undefined
|
||
offset = 0
|
||
options = length_or_options
|
||
elsif length_or_options
|
||
offset = Rubinius::Type.coerce_to(offset || 0, Fixnum, :to_int)
|
||
raise Errno::EINVAL, "offset must not be negative" if offset < 0
|
||
|
||
length = Rubinius::Type.coerce_to(length_or_options, Fixnum, :to_int)
|
||
raise ArgumentError, "length must not be negative" if length < 0
|
||
else
|
||
length = undefined
|
||
end
|
||
|
||
if options
|
||
mode = options.delete(:mode) || "r"
|
||
end
|
||
|
||
# Detect pipe mode
|
||
if name[0] == ?|
|
||
io = IO.popen(name[1..-1], "r")
|
||
return nil unless io # child process
|
||
else
|
||
io = File.new(name, mode, options)
|
||
end
|
||
|
||
str = nil
|
||
begin
|
||
io.seek(offset) unless offset == 0
|
||
if undefined.equal?(length)
|
||
str = io.read
|
||
else
|
||
str = io.read length
|
||
end
|
||
ensure
|
||
io.close
|
||
end
|
||
|
||
return str
|
||
end
|
||
|
||
def self.try_convert(obj)
|
||
Rubinius::Type.try_convert obj, IO, :to_io
|
||
end
|
||
|
||
def self.normalize_options(mode, options)
|
||
mode = nil if undefined.equal?(mode)
|
||
autoclose = true
|
||
encoding_options = {}
|
||
|
||
if undefined.equal?(options)
|
||
options = Rubinius::Type.try_convert(mode, Hash, :to_hash)
|
||
mode = nil if options
|
||
elsif !options.nil?
|
||
options = Rubinius::Type.try_convert(options, Hash, :to_hash)
|
||
raise ArgumentError, "wrong number of arguments (3 for 1..2)" unless options
|
||
end
|
||
|
||
if mode
|
||
mode = (Rubinius::Type.try_convert(mode, Integer, :to_int) or
|
||
Rubinius::Type.coerce_to(mode, String, :to_str))
|
||
end
|
||
|
||
if options
|
||
if optmode = options[:mode]
|
||
optmode = (Rubinius::Type.try_convert(optmode, Integer, :to_int) or
|
||
Rubinius::Type.coerce_to(optmode, String, :to_str))
|
||
end
|
||
|
||
if mode
|
||
raise ArgumentError, "mode specified twice" if optmode
|
||
else
|
||
mode = optmode
|
||
end
|
||
|
||
autoclose = !!options[:autoclose] if options.key?(:autoclose)
|
||
end
|
||
|
||
if mode.kind_of?(String)
|
||
mode, external, internal = mode.split(":")
|
||
raise ArgumentError, "invalid access mode" unless mode
|
||
|
||
binary = true if mode[1] === ?b
|
||
binary = false if mode[1] === ?t
|
||
elsif mode
|
||
binary = true if (mode & BINARY) != 0
|
||
end
|
||
|
||
if options
|
||
if options[:textmode] and options[:binmode]
|
||
raise ArgumentError, "both textmode and binmode specified"
|
||
end
|
||
|
||
if binary.nil?
|
||
binary = options[:binmode]
|
||
elsif options.key?(:textmode) or options.key?(:binmode)
|
||
raise ArgumentError, "text/binary mode specified twice"
|
||
end
|
||
|
||
if !external and !internal
|
||
external = options[:external_encoding]
|
||
internal = options[:internal_encoding]
|
||
elsif options[:external_encoding] or options[:internal_encoding] or options[:encoding]
|
||
raise ArgumentError, "encoding specified twice"
|
||
end
|
||
|
||
if !external and !internal
|
||
encoding = options[:encoding]
|
||
|
||
if encoding.kind_of? Encoding
|
||
external = encoding
|
||
elsif !encoding.nil?
|
||
encoding = StringValue(encoding)
|
||
external, internal = encoding.split(':')
|
||
end
|
||
end
|
||
|
||
[
|
||
:invalid, :undef, :replace, :newline, :universal_newline, :crlf_newline,
|
||
:cr_newline, :xml
|
||
].each do |options_key|
|
||
encoding_options[options_key] = options[options_key] if options.has_key?(options_key)
|
||
end
|
||
end
|
||
|
||
[mode, binary, external, internal, autoclose, encoding_options]
|
||
end
|
||
|
||
def self.open(*args)
|
||
io = new(*args)
|
||
|
||
return io unless block_given?
|
||
|
||
begin
|
||
yield io
|
||
ensure
|
||
begin
|
||
io.close unless io.closed?
|
||
rescue StandardError
|
||
# nothing, just swallow them.
|
||
end
|
||
end
|
||
end
|
||
|
||
def self.parse_mode(mode)
|
||
return mode if Rubinius::Type.object_kind_of? mode, Integer
|
||
|
||
mode = StringValue(mode)
|
||
|
||
ret = 0
|
||
|
||
case mode[0]
|
||
when ?r
|
||
ret |= RDONLY
|
||
when ?w
|
||
ret |= WRONLY | CREAT | TRUNC
|
||
when ?a
|
||
ret |= WRONLY | CREAT | APPEND
|
||
else
|
||
raise ArgumentError, "invalid mode -- #{mode}"
|
||
end
|
||
|
||
return ret if mode.length == 1
|
||
|
||
case mode[1]
|
||
when ?+
|
||
ret &= ~(RDONLY | WRONLY)
|
||
ret |= RDWR
|
||
when ?b
|
||
ret |= BINARY
|
||
when ?t
|
||
ret &= ~BINARY
|
||
when ?:
|
||
warn("encoding options not supported in 1.8")
|
||
return ret
|
||
else
|
||
raise ArgumentError, "invalid mode -- #{mode}"
|
||
end
|
||
|
||
return ret if mode.length == 2
|
||
|
||
case mode[2]
|
||
when ?+
|
||
ret &= ~(RDONLY | WRONLY)
|
||
ret |= RDWR
|
||
when ?b
|
||
ret |= BINARY
|
||
when ?t
|
||
ret &= ~BINARY
|
||
when ?:
|
||
warn("encoding options not supported in 1.8")
|
||
return ret
|
||
else
|
||
raise ArgumentError, "invalid mode -- #{mode}"
|
||
end
|
||
|
||
ret
|
||
end
|
||
|
||
def self.pipe(external=nil, internal=nil, options=nil)
|
||
# The use of #allocate is to make sure we create an IO obj. Would be so much
|
||
# cleaner to just do a PipeIO class as a subclass, but that would not be
|
||
# backward compatible. <sigh>
|
||
fd0, fd1 = FIFOFileDescriptor.connect_pipe_fds
|
||
lhs = allocate
|
||
lhs.send(:new_pipe, fd0, external, internal, options, FileDescriptor::O_RDONLY, true)
|
||
rhs = allocate
|
||
rhs.send(:new_pipe, fd1, nil, nil, nil, FileDescriptor::O_WRONLY)
|
||
|
||
if block_given?
|
||
begin
|
||
yield lhs, rhs
|
||
ensure
|
||
lhs.close unless lhs.closed?
|
||
rhs.close unless rhs.closed?
|
||
end
|
||
else
|
||
[lhs, rhs]
|
||
end
|
||
end
|
||
|
||
def self.popen(*args)
|
||
if env = Rubinius::Type.try_convert(args.first, Hash, :to_hash)
|
||
args.shift
|
||
end
|
||
|
||
if io_options = Rubinius::Type.try_convert(args.last, Hash, :to_hash)
|
||
args.pop
|
||
end
|
||
|
||
if args.size > 2
|
||
raise ArgumentError, "#{__method__}: given #{args.size}, expected 1..2"
|
||
end
|
||
|
||
cmd, mode = args
|
||
mode ||= "r"
|
||
|
||
if cmd.kind_of? Array
|
||
if sub_env = Rubinius::Type.try_convert(cmd.first, Hash, :to_hash)
|
||
env = sub_env unless env
|
||
cmd.shift
|
||
end
|
||
|
||
if exec_options = Rubinius::Type.try_convert(cmd.last, Hash, :to_hash)
|
||
cmd.pop
|
||
end
|
||
end
|
||
|
||
mode, binary, external, internal, autoclose, encoding_options =
|
||
IO.normalize_options(mode, io_options || {})
|
||
mode_int = parse_mode mode
|
||
|
||
readable = false
|
||
writable = false
|
||
|
||
if mode_int & IO::RDWR != 0
|
||
readable = true
|
||
writable = true
|
||
elsif mode_int & IO::WRONLY != 0
|
||
writable = true
|
||
else # IO::RDONLY
|
||
readable = true
|
||
end
|
||
|
||
pa_read, ch_write = pipe if readable
|
||
ch_read, pa_write = pipe if writable
|
||
|
||
# We only need the Bidirectional pipe if we're reading and writing.
|
||
# If we're only doing one, we can just return the IO object for
|
||
# the proper half.
|
||
if readable and writable
|
||
# Transmogrify pa_read into a BidirectionalPipe object,
|
||
# and then tell it abou it's pid and pa_write
|
||
|
||
Rubinius::Unsafe.set_class pa_read, IO::BidirectionalPipe
|
||
|
||
pipe = pa_read
|
||
pipe.set_pipe_info(pa_write)
|
||
elsif readable
|
||
pipe = pa_read
|
||
elsif writable
|
||
pipe = pa_write
|
||
else
|
||
raise ArgumentError, "IO is neither readable nor writable"
|
||
end
|
||
|
||
pipe.binmode if binary
|
||
pipe.set_encoding(external || Encoding.default_external, internal)
|
||
|
||
if cmd == "-"
|
||
pid = Rubinius::Mirror::Process.fork
|
||
|
||
if !pid
|
||
# Child
|
||
begin
|
||
if readable
|
||
pa_read.close
|
||
STDOUT.reopen ch_write
|
||
end
|
||
|
||
if writable
|
||
pa_write.close
|
||
STDIN.reopen ch_read
|
||
end
|
||
|
||
if block_given?
|
||
yield nil
|
||
exit! 0
|
||
else
|
||
return nil
|
||
end
|
||
rescue => e
|
||
exit! 0
|
||
end
|
||
end
|
||
else
|
||
options = {}
|
||
options[:in] = ch_read.fileno if ch_read
|
||
options[:out] = ch_write.fileno if ch_write
|
||
|
||
if io_options
|
||
io_options.delete_if do |key, _|
|
||
[:mode, :external_encoding, :internal_encoding,
|
||
:encoding, :textmode, :binmode, :autoclose
|
||
].include? key
|
||
end
|
||
|
||
options.merge! io_options
|
||
end
|
||
|
||
if exec_options
|
||
options.merge! exec_options
|
||
end
|
||
|
||
pid = Rubinius::Mirror::Process.spawn(env || {}, *cmd, options)
|
||
end
|
||
|
||
pipe.pid = pid
|
||
|
||
ch_write.close if readable
|
||
ch_read.close if writable
|
||
|
||
return pipe unless block_given?
|
||
|
||
begin
|
||
yield pipe
|
||
ensure
|
||
pipe.close unless pipe.closed?
|
||
end
|
||
end
|
||
|
||
#
|
||
# +select+ examines the IO object Arrays that are passed in
|
||
# as +readables+, +writables+, and +errorables+ to see if any
|
||
# of their descriptors are ready for reading, are ready for
|
||
# writing, or have an exceptions pending respectively. An IO
|
||
# may appear in more than one of the sets. Any of the three
|
||
# sets may be +nil+ if you are not interested in those events.
|
||
#
|
||
# If +timeout+ is not nil, it specifies the number of seconds
|
||
# to wait for events (maximum.) The number may be fractional,
|
||
# conceptually up to a microsecond resolution.
|
||
#
|
||
# A +timeout+ of 0 indicates that each descriptor should be
|
||
# checked once only, effectively polling the sets.
|
||
#
|
||
# Leaving the +timeout+ to +nil+ causes +select+ to block
|
||
# infinitely until an event transpires.
|
||
#
|
||
# If the timeout expires without events, +nil+ is returned.
|
||
# Otherwise, an [readable, writable, errors] Array of Arrays
|
||
# is returned, only, with the IO objects that have events.
|
||
#
|
||
# @compatibility MRI 1.8 and 1.9 require the +readables+ Array,
|
||
# Rubinius does not.
|
||
#
|
||
def self.select(readables=nil, writables=nil, errorables=nil, timeout=nil)
|
||
if timeout
|
||
unless Rubinius::Type.object_kind_of? timeout, Numeric
|
||
raise TypeError, "Timeout must be numeric"
|
||
end
|
||
|
||
raise ArgumentError, 'timeout must be positive' if timeout < 0
|
||
|
||
# Microseconds, rounded down
|
||
timeout = Integer(timeout * 1_000_000)
|
||
end
|
||
|
||
readables = IO::Select.validate_and_convert_argument(readables)
|
||
writables = IO::Select.validate_and_convert_argument(writables)
|
||
errorables = IO::Select.validate_and_convert_argument(errorables)
|
||
|
||
IO::Select.select(readables, writables, errorables, timeout)
|
||
end
|
||
|
||
##
|
||
# Opens the given path, returning the underlying file descriptor as a Fixnum.
|
||
# IO.sysopen("testfile") #=> 3
|
||
def self.sysopen(path, mode = nil, perm = nil)
|
||
path = Rubinius::Type.coerce_to_path path
|
||
mode = parse_mode(mode || "r")
|
||
perm ||= 0666
|
||
|
||
FileDescriptor.open_with_mode path, mode, perm
|
||
end
|
||
|
||
#
|
||
# Internally associate +io+ with the given descriptor.
|
||
#
|
||
# The +mode+ will be checked and set as the current mode if
|
||
# the underlying descriptor allows it.
|
||
#
|
||
# The +sync+ attribute will also be set.
|
||
#
|
||
def self.setup(io, fd, mode=nil, sync=false)
|
||
cur_mode = FileDescriptor.get_flags(fd)
|
||
Errno.handle if cur_mode < 0
|
||
|
||
cur_mode &= ACCMODE
|
||
|
||
if mode
|
||
mode = parse_mode(mode)
|
||
mode &= ACCMODE
|
||
|
||
if (cur_mode == RDONLY or cur_mode == WRONLY) and mode != cur_mode
|
||
raise Errno::EINVAL, "Invalid new mode for existing descriptor #{fd}"
|
||
end
|
||
end
|
||
|
||
# Check the given +io+ for a valid fd instance first. If it has one, cancel
|
||
# the existing finalizer since we are about to allocate a new fd instance.
|
||
if fd_obj = io.instance_variable_get(:@fd)
|
||
fd_obj.cancel_finalizer
|
||
end
|
||
|
||
fd_obj = FileDescriptor.choose_type(fd, io)
|
||
io.instance_variable_set(:@fd, fd_obj)
|
||
raise "FD could not be allocated for fd [#{fd}]" unless fd_obj
|
||
raise "No descriptor set for fd [#{fd}]" unless fd_obj.descriptor
|
||
|
||
io.mode = mode || cur_mode
|
||
io.sync = !!sync
|
||
io.lineno = 0 if io.lineno.nil?
|
||
|
||
# FIXME - re-enable this somehow. Right now this breaks kernel/delta/io.rb when it
|
||
# redefines STDIN/STDOUT/STDERR from the IO.open call. The new IO code has already
|
||
# loaded so we can no longer access the object that STDIN/STDOUT/STDERR points to
|
||
# via Ruby code, so the following code blows up.
|
||
# if STDOUT.respond_to?(:fileno) and not STDOUT.closed?
|
||
# io.sync ||= STDOUT.fileno == fd
|
||
# end
|
||
#
|
||
# if STDERR.respond_to?(:fileno) and not STDERR.closed?
|
||
# io.sync ||= STDERR.fileno == fd
|
||
# end
|
||
end
|
||
|
||
#
|
||
# Create a new IO associated with the given fd.
|
||
#
|
||
def initialize(fd, mode=undefined, options=undefined)
|
||
if block_given?
|
||
warn 'IO::new() does not take block; use IO::open() instead'
|
||
end
|
||
|
||
mode, binary, external, internal, @autoclose, @encoding_options =
|
||
IO.normalize_options(mode, options)
|
||
|
||
fd = Rubinius::Type.coerce_to fd, Integer, :to_int
|
||
autoclose = @autoclose
|
||
IO.setup self, fd, mode
|
||
|
||
binmode if binary
|
||
set_encoding external, internal
|
||
|
||
if @external && !external
|
||
@external = nil
|
||
end
|
||
|
||
if @internal
|
||
if Encoding.default_external == Encoding.default_internal or
|
||
(@external || Encoding.default_external) == Encoding::ASCII_8BIT
|
||
@internal = nil
|
||
end
|
||
elsif !mode_read_only?
|
||
if Encoding.default_external != Encoding.default_internal
|
||
@internal = Encoding.default_internal
|
||
end
|
||
end
|
||
|
||
unless @external
|
||
if @binmode
|
||
@external = Encoding::ASCII_8BIT
|
||
elsif @internal or Encoding.default_internal
|
||
@external = Encoding.default_external
|
||
end
|
||
end
|
||
|
||
@pipe = false # FIXME
|
||
end
|
||
|
||
private :initialize
|
||
|
||
##
|
||
# Obtains a new duplicate descriptor for the current one.
|
||
def initialize_copy(original_io) # :nodoc:
|
||
# Make a complete copy of the +original_io+ object including
|
||
# the mode, binmode, path, position, lineno, and a new FD.
|
||
dest_io = self
|
||
|
||
fd = FFI::Platform::POSIX.dup(original_io.descriptor)
|
||
|
||
# The system makes a shallow copy of all ivars, so this copy has
|
||
# the same @fd as the original. That shallow copy is really only
|
||
# relevant for primitive values (Fixnum, String, etc) and not
|
||
# our own objects. Instantiate a new @fd.
|
||
@fd = FileDescriptor.choose_type(fd, dest_io)
|
||
dest_io.mode = original_io.mode
|
||
dest_io.sync = original_io.sync
|
||
dest_io.binmode if original_io.binmode?
|
||
dest_io.autoclose = original_io.autoclose
|
||
|
||
dest_io
|
||
end
|
||
private :initialize_copy
|
||
|
||
def new_pipe(fd, external, internal, options, mode, do_encoding=false)
|
||
@fd = FIFOFileDescriptor.new(fd, nil, self, mode)
|
||
@lineno = 0
|
||
@pipe = true
|
||
|
||
# Why do we only set encoding for the "left hand side" pipe? Why not both?
|
||
if do_encoding
|
||
set_encoding((external || Encoding.default_external), (internal || Encoding.default_internal), options)
|
||
end
|
||
end
|
||
private :new_pipe
|
||
|
||
def super_inspect
|
||
"<IO:#{object_id}> \n#{@fd.inspect}"
|
||
end
|
||
|
||
# alias_method :prim_write, :write
|
||
# alias_method :prim_close, :close
|
||
# alias_method :prim_read, :read
|
||
|
||
def descriptor
|
||
@fd.descriptor if @fd
|
||
end
|
||
|
||
def descriptor=(value)
|
||
@fd.descriptor = value if @fd
|
||
end
|
||
|
||
def mode
|
||
@fd.mode if @fd
|
||
end
|
||
|
||
def mode=(value)
|
||
@fd.mode = value if @fd
|
||
end
|
||
|
||
def sync
|
||
@fd.sync if @fd
|
||
end
|
||
|
||
def sync=(value)
|
||
@fd.sync = value if @fd
|
||
end
|
||
|
||
def advise(advice, offset = 0, len = 0)
|
||
raise IOError, "stream is closed" if closed?
|
||
raise TypeError, "advice must be a Symbol" unless advice.kind_of?(Symbol)
|
||
|
||
if offset.kind_of?(Bignum) || len.kind_of?(Bignum)
|
||
raise RangeError, "bignum too big to convert into `long'"
|
||
end
|
||
|
||
unless [:normal, :sequential, :random, :noreuse, :dontneed, :willneed].include? advice
|
||
raise NotImplementedError, advice.inspect
|
||
end
|
||
|
||
advice = case advice
|
||
when :normal; POSIX_FADV_NORMAL
|
||
when :sequential; POSIX_FADV_SEQUENTIAL
|
||
when :random; POSIX_FADV_RANDOM
|
||
when :willneed; POSIX_FADV_WILLNEED
|
||
when :dontneed; POSIX_FADV_DONTNEED
|
||
when :noreuse; POSIX_FADV_NOREUSE
|
||
end
|
||
|
||
offset = Rubinius::Type.coerce_to offset, Integer, :to_int
|
||
len = Rubinius::Type.coerce_to len, Integer, :to_int
|
||
|
||
begin
|
||
if FFI.call_failed?(FFI::Platform::POSIX.posix_fadvise(descriptor, offset, len, advice))
|
||
Errno.handle("posix_fadvise(2) failed")
|
||
end
|
||
rescue NotImplementedError
|
||
# MRI thinks platforms that don't support #advise should silently fail.
|
||
# See https://bugs.ruby-lang.org/issues/11806
|
||
nil
|
||
end
|
||
|
||
nil
|
||
end
|
||
|
||
def autoclose
|
||
@autoclose
|
||
end
|
||
|
||
def autoclose?
|
||
@autoclose
|
||
end
|
||
|
||
def autoclose=(bool)
|
||
@fd.autoclose = bool
|
||
@autoclose = !!bool
|
||
end
|
||
|
||
def binmode
|
||
ensure_open
|
||
|
||
@binmode = true
|
||
@external = Encoding::BINARY
|
||
@internal = nil
|
||
|
||
# HACK what to do?
|
||
self
|
||
end
|
||
|
||
def binmode?
|
||
!@binmode.nil?
|
||
end
|
||
|
||
def close_on_exec=(value)
|
||
if value
|
||
fcntl(F_SETFD, fcntl(F_GETFD) | FD_CLOEXEC)
|
||
else
|
||
fcntl(F_SETFD, fcntl(F_GETFD) & ~FD_CLOEXEC)
|
||
end
|
||
nil
|
||
end
|
||
|
||
def close_on_exec?
|
||
(fcntl(F_GETFD) & FD_CLOEXEC) != 0
|
||
end
|
||
|
||
def <<(obj)
|
||
write(obj.to_s)
|
||
return self
|
||
end
|
||
|
||
##
|
||
# Closes the read end of a duplex I/O stream (i.e., one
|
||
# that contains both a read and a write stream, such as
|
||
# a pipe). Will raise an IOError if the stream is not duplexed.
|
||
#
|
||
# f = IO.popen("/bin/sh","r+")
|
||
# f.close_read
|
||
# f.readlines
|
||
# produces:
|
||
#
|
||
# prog.rb:3:in `readlines': not opened for reading (IOError)
|
||
# from prog.rb:3
|
||
def close_read
|
||
return if invalid_descriptor?
|
||
|
||
if mode_write_only? || mode_read_write?
|
||
raise IOError, 'closing non-duplex IO for reading'
|
||
end
|
||
|
||
@fd.close unless closed?
|
||
end
|
||
|
||
##
|
||
# Closes the write end of a duplex I/O stream (i.e., one
|
||
# that contains both a read and a write stream, such as
|
||
# a pipe). Will raise an IOError if the stream is not duplexed.
|
||
#
|
||
# f = IO.popen("/bin/sh","r+")
|
||
# f.close_write
|
||
# f.print "nowhere"
|
||
# produces:
|
||
#
|
||
# prog.rb:3:in `write': not opened for writing (IOError)
|
||
# from prog.rb:3:in `print'
|
||
# from prog.rb:3
|
||
def close_write
|
||
return if invalid_descriptor?
|
||
|
||
if mode_read_only? || mode_read_write?
|
||
raise IOError, 'closing non-duplex IO for writing'
|
||
end
|
||
|
||
@fd.close unless closed?
|
||
end
|
||
|
||
##
|
||
# Returns true if ios is completely closed (for duplex
|
||
# streams, both reader and writer), false otherwise.
|
||
#
|
||
# f = File.new("testfile")
|
||
# f.close #=> nil
|
||
# f.closed? #=> true
|
||
# f = IO.popen("/bin/sh","r+")
|
||
# f.close_write #=> nil
|
||
# f.closed? #=> false
|
||
# f.close_read #=> nil
|
||
# f.closed? #=> true
|
||
def closed?
|
||
invalid_descriptor?
|
||
end
|
||
|
||
def dup
|
||
ensure_open
|
||
super # calls #initialize_copy
|
||
end
|
||
|
||
# Argument matrix for IO#gets and IO#each:
|
||
#
|
||
# separator / limit | nil | >= 0 | < 0
|
||
# ===================+=====+======+=====
|
||
# String (nonempty) | A | B | C
|
||
# +-----+------+-----
|
||
# "" | D | E | F
|
||
# +-----+------+-----
|
||
# nil | G | H | I
|
||
#
|
||
|
||
class EachReader
|
||
READ_SIZE = 512 # bytes
|
||
|
||
def initialize(io, separator, limit, encoding_options)
|
||
@io = io
|
||
@separator = separator ? separator.force_encoding("ASCII-8BIT") : separator
|
||
@limit = limit
|
||
@encoding_options = encoding_options
|
||
@skip = nil
|
||
end
|
||
|
||
private :initialize
|
||
|
||
def each(&block)
|
||
if @separator
|
||
if @separator.empty?
|
||
@separator = "\n\n"
|
||
@skip = "\n"
|
||
end
|
||
|
||
if @limit
|
||
read_to_separator_with_limit(&block)
|
||
else
|
||
read_to_separator(&block)
|
||
end
|
||
else
|
||
if @limit
|
||
read_to_limit(&block)
|
||
else
|
||
read_all(&block)
|
||
end
|
||
end
|
||
end
|
||
|
||
def read_and_yield_count_chars(str, buffer, byte_count, &block)
|
||
str << buffer.slice!(0, byte_count)
|
||
|
||
if @limit
|
||
# Always read to char boundary because the +limit+ may have cut a multi-byte
|
||
# character in the middle. Returning such a string would have an invalid encoding.
|
||
if buffer.size < PEEK_AHEAD_LIMIT
|
||
# Use nonblocking read since existing +buffer+ might already have a valid encoding
|
||
# and we don't want to block forever if no more data is coming
|
||
result = @io.get_empty_8bit_buffer
|
||
|
||
# save and restore encodings around the read operation so that we ensure ASCII_8BIT
|
||
internal, external = @io.internal, @io.external
|
||
@io.external = Encoding::ASCII_8BIT
|
||
(@io.read_nonblock(PEEK_AHEAD_LIMIT, result, exception: false) || @io.get_empty_8bit_buffer)
|
||
@io.internal, @io.external = internal, external
|
||
|
||
buffer << result
|
||
end
|
||
str, bytes_read = read_to_char_boundary(@io, str, buffer)
|
||
else
|
||
# We are confident that our +str+ ends on a char boundary
|
||
str = IO.read_encode(@io, str, @encoding_options)
|
||
end
|
||
|
||
str.taint
|
||
$. = @io.increment_lineno
|
||
skip_contiguous_chars(buffer)
|
||
|
||
# Unused bytes/chars should be saved for the next read. Since the block that we yield to
|
||
# may +return+ we don't want to drop the bytes that are stored in +buffer+. To save,
|
||
# unget them so the next read will fetch them again. This might be expensive and could
|
||
# potentially use a little tuning. Maybe use an +unread(bytes)+ method which just moves
|
||
# a pointer around. Think about this for the mmap stuff.
|
||
@io.ungetc(buffer)
|
||
buffer.clear
|
||
|
||
yield str
|
||
end
|
||
|
||
def read_and_yield_entire_string(str, &block)
|
||
str = IO.read_encode(@io, str, @encoding_options)
|
||
str.taint
|
||
$. = @io.increment_lineno
|
||
yield str
|
||
end
|
||
|
||
# method A, D
|
||
def read_to_separator(&block)
|
||
str = @io.get_empty_8bit_buffer
|
||
buffer = @io.get_empty_8bit_buffer
|
||
separator_size = @separator.bytesize
|
||
|
||
begin
|
||
if buffer.size == 0
|
||
buffer = @io.read(READ_SIZE)
|
||
end
|
||
|
||
break unless buffer.size > 0
|
||
|
||
if count = buffer.index(@separator)
|
||
# #index returns a 0-based location but we want a length (so +1) and it should include
|
||
# the pattern/separator which may be >1. therefore, add the separator size.
|
||
count += separator_size
|
||
|
||
read_and_yield_count_chars(str, buffer, count, &block)
|
||
str = @io.get_empty_8bit_buffer
|
||
else
|
||
str << buffer
|
||
buffer.clear
|
||
end
|
||
end until buffer.size == 0 && @io.eof?
|
||
|
||
str << buffer
|
||
|
||
unless str.empty?
|
||
read_and_yield_entire_string(str, &block)
|
||
end
|
||
end
|
||
|
||
# method B, E
|
||
|
||
def read_to_separator_with_limit(&block)
|
||
str = @io.get_empty_8bit_buffer
|
||
buffer = @io.get_empty_8bit_buffer
|
||
separator_size = @separator.bytesize
|
||
|
||
#TODO: implement ignoring encoding with negative limit
|
||
wanted = limit = @limit.abs
|
||
|
||
begin
|
||
if buffer.size == 0
|
||
buffer = @io.read(READ_SIZE)
|
||
end
|
||
|
||
break unless buffer && buffer.size > 0
|
||
|
||
if count = buffer.index(@separator)
|
||
# #index returns a 0-based location but we want a length (so +1) and it should include
|
||
# the pattern/separator which may be >1. therefore, add the separator size.
|
||
count += separator_size
|
||
count = count < wanted ? count : wanted
|
||
read_and_yield_count_chars(str, buffer, count, &block)
|
||
|
||
str = @io.get_empty_8bit_buffer
|
||
else
|
||
if wanted < buffer.size
|
||
read_and_yield_count_chars(str, buffer, wanted, &block)
|
||
str = @io.get_empty_8bit_buffer
|
||
else
|
||
str << buffer
|
||
wanted -= buffer.size
|
||
buffer.clear
|
||
end
|
||
end
|
||
end until buffer.size == 0 && @io.eof?
|
||
|
||
unless str.empty?
|
||
read_and_yield_entire_string(str, &block)
|
||
end
|
||
end
|
||
|
||
# Method G
|
||
def read_all(&block)
|
||
str = @io.get_empty_8bit_buffer
|
||
|
||
begin
|
||
str << @io.read
|
||
end until @io.eof?
|
||
|
||
unless str.empty?
|
||
read_and_yield_entire_string(str, &block)
|
||
end
|
||
end
|
||
|
||
# Method H
|
||
# Only ever called if separator or $/ is forced to nil
|
||
def read_to_limit(&block)
|
||
str = @io.get_empty_8bit_buffer
|
||
wanted = limit = @limit.abs
|
||
|
||
begin
|
||
str << @io.read(wanted)
|
||
read_and_yield_count_chars(str, @io.get_empty_8bit_buffer, str.bytesize, &block)
|
||
str = @io.get_empty_8bit_buffer
|
||
end until @io.eof?
|
||
|
||
unless str.empty?
|
||
read_and_yield_entire_string(str, &block)
|
||
end
|
||
end
|
||
|
||
# Utility methods
|
||
|
||
def try_to_force_encoding(io, str)
|
||
str.force_encoding(io.external_encoding || Encoding.default_external)
|
||
|
||
IO.read_encode(io, str, @encoding_options)
|
||
end
|
||
|
||
PEEK_AHEAD_LIMIT = 16
|
||
|
||
def read_to_char_boundary(io, str, buffer)
|
||
str.force_encoding(io.external_encoding || Encoding.default_external)
|
||
return [IO.read_encode(io, str, @encoding_options), 0] if str.valid_encoding?
|
||
|
||
peek_ahead = 0
|
||
while buffer.size > 0 and peek_ahead < PEEK_AHEAD_LIMIT
|
||
str.force_encoding Encoding::ASCII_8BIT
|
||
substring = buffer.slice!(0, 1)
|
||
str << substring
|
||
peek_ahead += 1
|
||
|
||
str.force_encoding(io.external_encoding || Encoding.default_external)
|
||
if str.valid_encoding?
|
||
return [IO.read_encode(io, str, @encoding_options), peek_ahead]
|
||
end
|
||
end
|
||
|
||
[IO.read_encode(io, str, @encoding_options), peek_ahead]
|
||
end
|
||
|
||
# Advances the buffer index past any number of contiguous
|
||
# characters == +skip+ and throws away that data. For
|
||
# example, if +skip+ is ?\n and the buffer contents are
|
||
# "\n\n\nAbc...", the buffer will discard all chars
|
||
# up to 'A'.
|
||
def skip_contiguous_chars(buffer)
|
||
return 0 unless @skip
|
||
|
||
skip_count = 0
|
||
skip_count += 1 while buffer[skip_count] == @skip
|
||
if skip_count > 0
|
||
slice = buffer.slice!(0, skip_count)
|
||
slice.bytesize
|
||
else
|
||
0
|
||
end
|
||
end
|
||
end
|
||
|
||
def increment_lineno
|
||
@lineno += 1
|
||
end
|
||
|
||
##
|
||
# Return a string describing this IO object.
|
||
def inspect
|
||
if @fd
|
||
if @fd.descriptor != -1
|
||
"#<#{self.class}:fd #{@fd.descriptor}>"
|
||
else
|
||
"#<#{self.class}:(closed)>"
|
||
end
|
||
else
|
||
"#<#{self.class}:fd nil>"
|
||
end
|
||
end
|
||
|
||
def lines(*args, &block)
|
||
if block_given?
|
||
each_line(*args, &block)
|
||
else
|
||
to_enum :each_line, *args
|
||
end
|
||
end
|
||
|
||
def each(sep_or_limit=$/, limit=nil, &block)
|
||
return to_enum(:each, sep_or_limit, limit) unless block_given?
|
||
|
||
ensure_open_and_readable
|
||
|
||
if limit
|
||
limit = Rubinius::Type.coerce_to limit, Integer, :to_int
|
||
sep = sep_or_limit ? StringValue(sep_or_limit) : nil
|
||
else
|
||
case sep_or_limit
|
||
when String
|
||
sep = sep_or_limit
|
||
when nil
|
||
sep = nil
|
||
else
|
||
unless sep = Rubinius::Type.check_convert_type(sep_or_limit, String, :to_str)
|
||
sep = $/
|
||
limit = Rubinius::Type.coerce_to sep_or_limit, Integer, :to_int
|
||
end
|
||
end
|
||
end
|
||
|
||
raise ArgumentError, "limit of 0 is invalid" if limit && limit.zero?
|
||
return if eof?
|
||
|
||
EachReader.new(self, sep, limit, @encoding_options).each(&block)
|
||
|
||
self
|
||
end
|
||
|
||
def each_line(sep_or_limit=$/, limit=nil, &block)
|
||
if limit
|
||
limit = Rubinius::Type.coerce_to limit, Integer, :to_int
|
||
sep = sep_or_limit ? StringValue(sep_or_limit) : nil
|
||
raise ArgumentError, "invalid limit: 0 for each_line" if limit.zero?
|
||
else
|
||
case sep_or_limit
|
||
when String
|
||
sep = sep_or_limit
|
||
when nil
|
||
sep = nil
|
||
else
|
||
unless sep = Rubinius::Type.check_convert_type(sep_or_limit, String, :to_str)
|
||
sep = $/
|
||
limit = Rubinius::Type.coerce_to sep_or_limit, Integer, :to_int
|
||
raise ArgumentError, "invalid limit: 0 for each_line" if limit.zero?
|
||
sep_or_limit = sep
|
||
end
|
||
end
|
||
end
|
||
|
||
each(sep_or_limit, limit, &block)
|
||
end
|
||
|
||
def each_byte
|
||
return to_enum(:each_byte) unless block_given?
|
||
|
||
yield getbyte until eof?
|
||
|
||
self
|
||
end
|
||
|
||
alias_method :bytes, :each_byte
|
||
|
||
def each_char
|
||
return to_enum :each_char unless block_given?
|
||
ensure_open_and_readable
|
||
|
||
while char = getc
|
||
yield char
|
||
end
|
||
|
||
self
|
||
end
|
||
|
||
alias_method :chars, :each_char
|
||
|
||
def each_codepoint
|
||
return to_enum :each_codepoint unless block_given?
|
||
ensure_open_and_readable
|
||
|
||
while char = getc
|
||
yield char.ord
|
||
end
|
||
|
||
self
|
||
end
|
||
|
||
alias_method :codepoints, :each_codepoint
|
||
|
||
|
||
##
|
||
# Set the pipe so it is at the end of the file
|
||
def eof!
|
||
@eof = true
|
||
end
|
||
|
||
##
|
||
# Returns true if ios is at end of file that means
|
||
# there are no more data to read. The stream must be
|
||
# opened for reading or an IOError will be raised.
|
||
#
|
||
# f = File.new("testfile")
|
||
# dummy = f.readlines
|
||
# f.eof #=> true
|
||
# If ios is a stream such as pipe or socket, IO#eof?
|
||
# blocks until the other end sends some data or closes it.
|
||
#
|
||
# r, w = IO.pipe
|
||
# Thread.new { sleep 1; w.close }
|
||
# r.eof? #=> true after 1 second blocking
|
||
#
|
||
# r, w = IO.pipe
|
||
# Thread.new { sleep 1; w.puts "a" }
|
||
# r.eof? #=> false after 1 second blocking
|
||
#
|
||
# r, w = IO.pipe
|
||
# r.eof? # blocks forever
|
||
#
|
||
# Note that IO#eof? reads data to a input buffer.
|
||
# So IO#sysread doesn't work with IO#eof?.
|
||
def eof?
|
||
ensure_open_and_readable
|
||
@fd.eof?
|
||
end
|
||
|
||
alias_method :eof, :eof?
|
||
|
||
def ensure_open_and_readable
|
||
ensure_open
|
||
raise IOError, "not opened for reading" if mode_write_only?
|
||
end
|
||
|
||
def ensure_open_and_writable
|
||
ensure_open
|
||
raise IOError, "not opened for writing" if mode_read_only?
|
||
end
|
||
|
||
def external_encoding
|
||
return @external if @external
|
||
return Encoding.default_external if mode_read_only?
|
||
end
|
||
|
||
##
|
||
# Provides a mechanism for issuing low-level commands to
|
||
# control or query file-oriented I/O streams. Arguments
|
||
# and results are platform dependent. If arg is a number,
|
||
# its value is passed directly. If it is a string, it is
|
||
# interpreted as a binary sequence of bytes (Array#pack
|
||
# might be a useful way to build this string). On Unix
|
||
# platforms, see fcntl(2) for details. Not implemented on all platforms.
|
||
def fcntl(command, arg=0)
|
||
ensure_open
|
||
|
||
if !arg
|
||
arg = 0
|
||
elsif arg == true
|
||
arg = 1
|
||
elsif arg.kind_of? String
|
||
raise NotImplementedError, "cannot handle String"
|
||
else
|
||
arg = Rubinius::Type.coerce_to arg, Fixnum, :to_int
|
||
end
|
||
|
||
command = Rubinius::Type.coerce_to command, Fixnum, :to_int
|
||
FFI::Platform::POSIX.fcntl descriptor, command, arg
|
||
end
|
||
|
||
def internal_encoding
|
||
@internal
|
||
end
|
||
|
||
##
|
||
# Provides a mechanism for issuing low-level commands to
|
||
# control or query file-oriented I/O streams. Arguments
|
||
# and results are platform dependent. If arg is a number,
|
||
# its value is passed directly. If it is a string, it is
|
||
# interpreted as a binary sequence of bytes (Array#pack
|
||
# might be a useful way to build this string). On Unix
|
||
# platforms, see fcntl(2) for details. Not implemented on all platforms.
|
||
def ioctl(command, arg=0)
|
||
ensure_open
|
||
|
||
if !arg
|
||
real_arg = 0
|
||
elsif arg == true
|
||
real_arg = 1
|
||
elsif arg.kind_of? String
|
||
# This could be faster.
|
||
buffer_size = arg.bytesize
|
||
# On BSD and Linux, we could read the buffer size out of the ioctl value.
|
||
# Most Linux ioctl codes predate the convention, so a fallback like this
|
||
# is still necessary.
|
||
buffer_size = 4096 if buffer_size < 4096
|
||
buffer = FFI::MemoryPointer.new buffer_size
|
||
buffer.write_string arg, arg.bytesize
|
||
real_arg = buffer.address
|
||
else
|
||
real_arg = Rubinius::Type.coerce_to arg, Fixnum, :to_int
|
||
end
|
||
|
||
command = Rubinius::Type.coerce_to command, Fixnum, :to_int
|
||
ret = FFI::Platform::POSIX.ioctl descriptor, command, real_arg
|
||
Errno.handle if ret < 0
|
||
if arg.kind_of?(String)
|
||
arg.replace buffer.read_string(buffer_size)
|
||
buffer.free
|
||
end
|
||
ret
|
||
end
|
||
|
||
##
|
||
# Returns an integer representing the numeric file descriptor for ios.
|
||
#
|
||
# $stdin.fileno #=> 0
|
||
# $stdout.fileno #=> 1
|
||
def fileno
|
||
ensure_open
|
||
return @fd.descriptor
|
||
end
|
||
|
||
alias_method :to_i, :fileno
|
||
|
||
##
|
||
# Flushes any buffered data within ios to the underlying
|
||
# operating system (note that this is Ruby internal
|
||
# buffering only; the OS may buffer the data as well).
|
||
#
|
||
# $stdout.print "no newline"
|
||
# $stdout.flush
|
||
# produces:
|
||
#
|
||
# no newline
|
||
def flush
|
||
ensure_open
|
||
#@fd.reset_positioning
|
||
return self
|
||
end
|
||
|
||
def force_read_only
|
||
@fd.force_read_only
|
||
end
|
||
|
||
def force_write_only
|
||
@fd.force_write_only
|
||
end
|
||
|
||
##
|
||
# Immediately writes all buffered data in ios to disk. Returns
|
||
# nil if the underlying operating system does not support fsync(2).
|
||
# Note that fsync differs from using IO#sync=. The latter ensures
|
||
# that data is flushed from Ruby's buffers, but does not guarantee
|
||
# that the underlying operating system actually writes it to disk.
|
||
def fsync
|
||
flush
|
||
|
||
err = FFI::Platform::POSIX.fsync descriptor
|
||
|
||
Errno.handle 'fsync(2)' if err < 0
|
||
|
||
return err
|
||
end
|
||
|
||
def getbyte
|
||
ensure_open
|
||
byte = read(1)
|
||
return(byte ? byte.ord : nil)
|
||
end
|
||
|
||
##
|
||
# Gets the next 8-bit byte (0..255) from ios.
|
||
# Returns nil if called at end of file.
|
||
#
|
||
# f = File.new("testfile")
|
||
# f.getc #=> 84
|
||
# f.getc #=> 104
|
||
def getc
|
||
ensure_open
|
||
return if eof?
|
||
|
||
char = get_empty_8bit_buffer
|
||
begin
|
||
char.force_encoding Encoding::ASCII_8BIT
|
||
char << read(1)
|
||
|
||
char.force_encoding(self.external_encoding || Encoding.default_external)
|
||
if char.chr_at(0)
|
||
return IO.read_encode(self, char, @encoding_options)
|
||
end
|
||
end until eof?
|
||
|
||
return nil
|
||
end
|
||
|
||
def gets(sep_or_limit=$/, limit=nil)
|
||
each sep_or_limit, limit do |line|
|
||
$_ = line if line
|
||
return line
|
||
end
|
||
|
||
return nil
|
||
end
|
||
|
||
##
|
||
# Returns the current line number in ios. The
|
||
# stream must be opened for reading. lineno
|
||
# counts the number of times gets is called,
|
||
# rather than the number of newlines encountered.
|
||
# The two values will differ if gets is called with
|
||
# a separator other than newline. See also the $. variable.
|
||
#
|
||
# f = File.new("testfile")
|
||
# f.lineno #=> 0
|
||
# f.gets #=> "This is line one\n"
|
||
# f.lineno #=> 1
|
||
# f.gets #=> "This is line two\n"
|
||
# f.lineno #=> 2
|
||
def lineno
|
||
ensure_open
|
||
|
||
return @lineno
|
||
end
|
||
|
||
##
|
||
# Manually sets the current line number to the
|
||
# given value. $. is updated only on the next read.
|
||
#
|
||
# f = File.new("testfile")
|
||
# f.gets #=> "This is line one\n"
|
||
# $. #=> 1
|
||
# f.lineno = 1000
|
||
# f.lineno #=> 1000
|
||
# $. # lineno of last read #=> 1
|
||
# f.gets #=> "This is line two\n"
|
||
# $. # lineno of last read #=> 1001
|
||
def lineno=(line_number)
|
||
ensure_open
|
||
|
||
raise TypeError if line_number.nil?
|
||
|
||
@lineno = Integer(line_number)
|
||
end
|
||
|
||
def mode_read_only?
|
||
@fd.read_only?
|
||
end
|
||
private :mode_read_only?
|
||
|
||
def mode_read_write?
|
||
@fd.read_write?
|
||
end
|
||
private :mode_read_write?
|
||
|
||
def mode_write_only?
|
||
@fd.write_only?
|
||
end
|
||
private :mode_write_only?
|
||
|
||
##
|
||
# FIXME
|
||
# Returns the process ID of a child process
|
||
# associated with ios. This will be set by IO::popen.
|
||
#
|
||
# pipe = IO.popen("-")
|
||
# if pipe
|
||
# $stderr.puts "In parent, child pid is #{pipe.pid}"
|
||
# else
|
||
# $stderr.puts "In child, pid is #{$$}"
|
||
# end
|
||
# produces:
|
||
#
|
||
# In child, pid is 26209
|
||
# In parent, child pid is 26209
|
||
def pid
|
||
raise IOError, 'closed stream' if closed?
|
||
@pid
|
||
end
|
||
|
||
attr_writer :pid
|
||
|
||
def pipe=(v)
|
||
@pipe = !!v
|
||
end
|
||
|
||
def pipe?
|
||
@pipe
|
||
end
|
||
|
||
##
|
||
#
|
||
def pos
|
||
ensure_open
|
||
@fd.offset
|
||
end
|
||
|
||
alias_method :tell, :pos
|
||
|
||
##
|
||
# Seeks to the given position (in bytes) in ios.
|
||
#
|
||
# f = File.new("testfile")
|
||
# f.pos = 17
|
||
# f.gets #=> "This is line two\n"
|
||
def pos=(offset)
|
||
seek offset, SEEK_SET
|
||
end
|
||
|
||
##
|
||
# Writes each given argument.to_s to the stream or $_ (the result of last
|
||
# IO#gets) if called without arguments. Appends $\.to_s to output. Returns
|
||
# nil.
|
||
def print(*args)
|
||
if args.empty?
|
||
write $_.to_s
|
||
else
|
||
args.each { |o| write o.to_s }
|
||
end
|
||
|
||
write $\.to_s
|
||
nil
|
||
end
|
||
|
||
##
|
||
# Formats and writes to ios, converting parameters under
|
||
# control of the format string. See Kernel#sprintf for details.
|
||
def printf(fmt, *args)
|
||
fmt = StringValue(fmt)
|
||
write ::Rubinius::Sprinter.get(fmt).call(*args)
|
||
end
|
||
|
||
##
|
||
# If obj is Numeric, write the character whose code is obj,
|
||
# otherwise write the first character of the string
|
||
# representation of obj to ios.
|
||
#
|
||
# $stdout.putc "A"
|
||
# $stdout.putc 65
|
||
# produces:
|
||
#
|
||
# AA
|
||
def putc(obj)
|
||
if Rubinius::Type.object_kind_of? obj, String
|
||
write obj.substring(0, 1)
|
||
else
|
||
byte = Rubinius::Type.coerce_to(obj, Integer, :to_int) & 0xff
|
||
write byte.chr
|
||
end
|
||
|
||
return obj
|
||
end
|
||
|
||
##
|
||
# Writes the given objects to ios as with IO#print.
|
||
# Writes a record separator (typically a newline)
|
||
# after any that do not already end with a newline
|
||
# sequence. If called with an array argument, writes
|
||
# each element on a new line. If called without arguments,
|
||
# outputs a single record separator.
|
||
#
|
||
# $stdout.puts("this", "is", "a", "test")
|
||
# produces:
|
||
#
|
||
# this
|
||
# is
|
||
# a
|
||
# test
|
||
def puts(*args)
|
||
if args.empty?
|
||
write DEFAULT_RECORD_SEPARATOR
|
||
else
|
||
args.each do |arg|
|
||
if arg.equal? nil
|
||
str = ""
|
||
elsif arg.kind_of? String
|
||
str = arg
|
||
elsif Thread.guarding? arg
|
||
str = "[...]"
|
||
else
|
||
Thread.recursion_guard arg do
|
||
begin
|
||
arg.to_ary.each { |a| puts a }
|
||
rescue NoMethodError
|
||
unless (str = arg.to_s).kind_of? String
|
||
str = "#<#{arg.class}:0x#{arg.object_id.to_s(16)}>"
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
if str
|
||
write str
|
||
write DEFAULT_RECORD_SEPARATOR unless str.suffix?(DEFAULT_RECORD_SEPARATOR)
|
||
end
|
||
end
|
||
end
|
||
|
||
nil
|
||
end
|
||
|
||
def read(length=nil, buffer=nil)
|
||
ensure_open_and_readable
|
||
buffer = StringValue(buffer) if buffer
|
||
|
||
unless length
|
||
str = IO.read_encode(self, read_all, @encoding_options)
|
||
return str unless buffer
|
||
|
||
return buffer.replace(str)
|
||
end
|
||
|
||
str = ""
|
||
emulate_blocking_read do
|
||
read_nonblock(length, str)
|
||
end
|
||
|
||
if str.empty? && length > 0
|
||
str = nil
|
||
end
|
||
|
||
if str
|
||
if buffer
|
||
buffer.replace str.force_encoding(buffer.encoding)
|
||
else
|
||
str.force_encoding Encoding::ASCII_8BIT
|
||
end
|
||
else
|
||
buffer.clear if buffer
|
||
nil
|
||
end
|
||
end
|
||
|
||
##
|
||
# Reads all input until +#eof?+ is true. Returns the input read.
|
||
# If the buffer is already exhausted, returns +""+.
|
||
def read_all
|
||
str = ""
|
||
begin
|
||
buffer = ""
|
||
emulate_blocking_read do
|
||
read_nonblock(FileDescriptor.pagesize, buffer)
|
||
end
|
||
str << buffer
|
||
end until eof?
|
||
|
||
str
|
||
end
|
||
|
||
private :read_all
|
||
|
||
def read_if_available(bytes)
|
||
return "" if bytes.zero?
|
||
buffer, bytes = @fd.read_only_buffer(bytes)
|
||
|
||
events = IO::Select.readable_events(descriptor)
|
||
if events == 0 && !buffer
|
||
Errno.raise_waitreadable("no data ready")
|
||
return ""
|
||
elsif events < 0 && !buffer
|
||
Errno.handle("read(2) failed")
|
||
return ""
|
||
elsif events == 0 && buffer
|
||
# we were able to read from the buffer but no more data is waiting
|
||
return buffer
|
||
end
|
||
|
||
# if we get here then we have data to read from the descriptor
|
||
str = ""
|
||
bytes_read = @fd.read(bytes, str)
|
||
|
||
if bytes_read.nil?
|
||
# there's a chance the read could fail even when we have data read from the buffer
|
||
# to return to caller
|
||
return (nil || buffer)
|
||
else
|
||
# combine what we read from the buffer with what we read from the descriptor
|
||
buffer = buffer.to_s + str
|
||
return buffer
|
||
end
|
||
end
|
||
# defined in bootstrap, used here.
|
||
private :read_if_available
|
||
|
||
##
|
||
# Reads at most maxlen bytes from ios using read(2) system
|
||
# call after O_NONBLOCK is set for the underlying file descriptor.
|
||
#
|
||
# If the optional outbuf argument is present, it must reference
|
||
# a String, which will receive the data.
|
||
#
|
||
# read_nonblock just calls read(2). It causes all errors read(2)
|
||
# causes: EAGAIN, EINTR, etc. The caller should care such errors.
|
||
#
|
||
# read_nonblock causes EOFError on EOF.
|
||
#
|
||
# If the read buffer is not empty, read_nonblock reads from the
|
||
# buffer like readpartial. In this case, read(2) is not called.
|
||
def read_nonblock(size, buffer=nil, exception: true)
|
||
raise ArgumentError, "illegal read size" if size < 0
|
||
ensure_open
|
||
|
||
buffer = StringValue buffer if buffer
|
||
|
||
unless @fd.blocking?
|
||
@fd.set_nonblock
|
||
nonblock_reset = true
|
||
end
|
||
|
||
begin
|
||
str = read_if_available(size)
|
||
rescue EAGAINWaitReadable => exc
|
||
raise exc if exception
|
||
|
||
return :wait_readable
|
||
end
|
||
|
||
if str
|
||
if buffer
|
||
buffer.replace(str)
|
||
IO.read_encode(self, buffer, @encoding_options)
|
||
else
|
||
IO.read_encode(self, str, @encoding_options)
|
||
end
|
||
else
|
||
raise EOFError, "stream closed" if exception
|
||
end
|
||
ensure
|
||
@fd.clear_nonblock if nonblock_reset
|
||
end
|
||
|
||
##
|
||
# Reads a character as with IO#getc, but raises an EOFError on end of file.
|
||
def readchar
|
||
char = getc
|
||
raise EOFError, 'end of file reached' unless char
|
||
char
|
||
end
|
||
|
||
def readbyte
|
||
byte = getbyte
|
||
raise EOFError, "end of file reached" unless byte
|
||
#raise EOFError, "end of file" unless bytes # bytes/each_byte is deprecated, FIXME - is this line necessary?
|
||
byte
|
||
end
|
||
|
||
##
|
||
# Reads a line as with IO#gets, but raises an EOFError on end of file.
|
||
def readline(sep=$/)
|
||
out = gets(sep)
|
||
raise EOFError, "end of file" unless out
|
||
return out
|
||
end
|
||
|
||
##
|
||
# Reads all of the lines in ios, and returns them in an array.
|
||
# Lines are separated by the optional sep_string. If sep_string
|
||
# is nil, the rest of the stream is returned as a single record.
|
||
# The stream must be opened for reading or an IOError will be raised.
|
||
#
|
||
# f = File.new("testfile")
|
||
# f.readlines[0] #=> "This is line one\n"
|
||
def readlines(sep=$/)
|
||
sep = StringValue sep if sep
|
||
|
||
old_line = $_
|
||
ary = Array.new
|
||
while line = gets(sep)
|
||
ary << line
|
||
end
|
||
$_ = old_line
|
||
|
||
ary
|
||
end
|
||
|
||
##
|
||
# Reads at most maxlen bytes from the I/O stream. It blocks
|
||
# only if ios has no data immediately available. It doesn‘t
|
||
# block if some data available. If the optional outbuf argument
|
||
# is present, it must reference a String, which will receive the
|
||
# data. It raises EOFError on end of file.
|
||
#
|
||
# readpartial is designed for streams such as pipe, socket, tty,
|
||
# etc. It blocks only when no data immediately available. This
|
||
# means that it blocks only when following all conditions hold.
|
||
#
|
||
# the buffer in the IO object is empty.
|
||
# the content of the stream is empty.
|
||
# the stream is not reached to EOF.
|
||
# When readpartial blocks, it waits data or EOF on the stream.
|
||
# If some data is reached, readpartial returns with the data.
|
||
# If EOF is reached, readpartial raises EOFError.
|
||
#
|
||
# When readpartial doesn‘t blocks, it returns or raises immediately.
|
||
# If the buffer is not empty, it returns the data in the buffer.
|
||
# Otherwise if the stream has some content, it returns the data in
|
||
# the stream. Otherwise if the stream is reached to EOF, it raises EOFError.
|
||
#
|
||
# r, w = IO.pipe # buffer pipe content
|
||
# w << "abc" # "" "abc".
|
||
# r.readpartial(4096) #=> "abc" "" ""
|
||
# r.readpartial(4096) # blocks because buffer and pipe is empty.
|
||
#
|
||
# r, w = IO.pipe # buffer pipe content
|
||
# w << "abc" # "" "abc"
|
||
# w.close # "" "abc" EOF
|
||
# r.readpartial(4096) #=> "abc" "" EOF
|
||
# r.readpartial(4096) # raises EOFError
|
||
#
|
||
# r, w = IO.pipe # buffer pipe content
|
||
# w << "abc\ndef\n" # "" "abc\ndef\n"
|
||
# r.gets #=> "abc\n" "def\n" ""
|
||
# w << "ghi\n" # "def\n" "ghi\n"
|
||
# r.readpartial(4096) #=> "def\n" "" "ghi\n"
|
||
# r.readpartial(4096) #=> "ghi\n" "" ""
|
||
# Note that readpartial behaves similar to sysread. The differences are:
|
||
#
|
||
# If the buffer is not empty, read from the buffer instead
|
||
# of "sysread for buffered IO (IOError)".
|
||
# It doesn‘t cause Errno::EAGAIN and Errno::EINTR. When readpartial
|
||
# meets EAGAIN and EINTR by read system call, readpartial retry the system call.
|
||
# The later means that readpartial is nonblocking-flag insensitive. It
|
||
# blocks on the situation IO#sysread causes Errno::EAGAIN as if the fd is blocking mode.
|
||
def readpartial(size, buffer=nil)
|
||
raise ArgumentError, 'negative string size' unless size >= 0
|
||
ensure_open
|
||
|
||
if buffer
|
||
buffer = StringValue(buffer)
|
||
|
||
buffer.shorten! buffer.bytesize
|
||
|
||
return buffer if size == 0
|
||
|
||
data = nil
|
||
begin
|
||
data = read_nonblock(size)
|
||
rescue IO::WaitReadable
|
||
IO.select([self])
|
||
retry
|
||
rescue IO::WaitWritable
|
||
IO.select(nil, [self])
|
||
retry
|
||
end
|
||
|
||
buffer.replace(data)
|
||
|
||
return buffer
|
||
else
|
||
return "" if size == 0
|
||
|
||
data = nil
|
||
begin
|
||
data = read_nonblock(size)
|
||
rescue IO::WaitReadable
|
||
IO.select([self])
|
||
retry
|
||
rescue IO::WaitWritable
|
||
IO.select(nil, [self])
|
||
retry
|
||
end
|
||
|
||
return data
|
||
end
|
||
end
|
||
|
||
##
|
||
# Reassociates ios with the I/O stream given in other_IO or to
|
||
# a new stream opened on path. This may dynamically change the
|
||
# actual class of this stream.
|
||
#
|
||
# f1 = File.new("testfile")
|
||
# f2 = File.new("testfile")
|
||
# f2.readlines[0] #=> "This is line one\n"
|
||
# f2.reopen(f1) #=> #<File:testfile>
|
||
# f2.readlines[0] #=> "This is line one\n"
|
||
def reopen(other, mode=undefined)
|
||
if other.respond_to?(:to_io)
|
||
flush
|
||
|
||
if other.kind_of? IO
|
||
io = other
|
||
else
|
||
io = other.to_io
|
||
unless io.kind_of? IO
|
||
raise TypeError, "#to_io must return an instance of IO"
|
||
end
|
||
end
|
||
|
||
# Note: this is the whole reason that FileDescriptor#ensure_open takes an argument.
|
||
#
|
||
ensure_open(io.descriptor)
|
||
# io.reset_buffering
|
||
|
||
@fd.reopen(io.descriptor)
|
||
|
||
# When reopening we may be going from a Pipe to a File or vice versa. Let the
|
||
# system figure out the proper FD class.
|
||
@fd.cancel_finalizer # cancel soon-to-be-overwritten instance's finalizer
|
||
@fd = FileDescriptor.choose_type(descriptor, self)
|
||
Rubinius::Unsafe.set_class self, io.class
|
||
if io.respond_to?(:path)
|
||
@path = io.path
|
||
end
|
||
|
||
seek(other.pos, SEEK_SET) rescue Errno::ESPIPE
|
||
else
|
||
flush unless closed?
|
||
|
||
# If a mode isn't passed in, use the mode that the IO is already in.
|
||
if undefined.equal? mode
|
||
mode = @fd.mode
|
||
# If this IO was already opened for writing, we should
|
||
# create the target file if it doesn't already exist.
|
||
if mode_read_write? || mode_write_only?
|
||
mode |= CREAT
|
||
end
|
||
else
|
||
mode = IO.parse_mode(mode)
|
||
end
|
||
|
||
reopen_path Rubinius::Type.coerce_to_path(other), mode
|
||
|
||
unless closed?
|
||
seek(0, SEEK_SET) rescue Errno::ESPIPE
|
||
end
|
||
end
|
||
|
||
self
|
||
end
|
||
|
||
def reopen_path(path, mode)
|
||
status = @fd.reopen_path(path, mode)
|
||
@fd.cancel_finalizer
|
||
@fd = FileDescriptor.choose_type(descriptor, self)
|
||
return status
|
||
end
|
||
|
||
##
|
||
# Internal method used to reset the state of the buffer, including the
|
||
# physical position in the stream.
|
||
def reset_buffering
|
||
# ##@ibuffer.unseek! self
|
||
end
|
||
|
||
##
|
||
# Positions ios to the beginning of input, resetting lineno to zero.
|
||
#
|
||
# f = File.new("testfile")
|
||
# f.readline #=> "This is line one\n"
|
||
# f.rewind #=> 0
|
||
# f.lineno #=> 0
|
||
# f.readline #=> "This is line one\n"
|
||
def rewind
|
||
seek 0
|
||
@lineno = 0
|
||
return 0
|
||
end
|
||
|
||
##
|
||
# Seeks to a given offset +amount+ in the stream according to the value of whence:
|
||
#
|
||
# IO::SEEK_CUR | Seeks to _amount_ plus current position
|
||
# --------------+----------------------------------------------------
|
||
# IO::SEEK_END | Seeks to _amount_ plus end of stream (you probably
|
||
# | want a negative value for _amount_)
|
||
# --------------+----------------------------------------------------
|
||
# IO::SEEK_SET | Seeks to the absolute location given by _amount_
|
||
# Example:
|
||
#
|
||
# f = File.new("testfile")
|
||
# f.seek(-13, IO::SEEK_END) #=> 0
|
||
# f.readline #=> "And so on...\n"
|
||
def seek(amount, whence=SEEK_SET)
|
||
flush
|
||
|
||
@eof = false
|
||
|
||
@fd.seek Integer(amount), whence
|
||
|
||
return 0
|
||
end
|
||
|
||
def set_encoding(external, internal=nil, options=undefined)
|
||
case external
|
||
when Encoding
|
||
@external = external
|
||
when String
|
||
@external = nil
|
||
when nil
|
||
if mode_read_only? || @external
|
||
@external = nil
|
||
else
|
||
@external = Encoding.default_external
|
||
end
|
||
else
|
||
@external = nil
|
||
external = StringValue(external)
|
||
end
|
||
|
||
if @external.nil? and not external.nil?
|
||
if index = external.index(":")
|
||
internal = external[index+1..-1]
|
||
external = external[0, index]
|
||
end
|
||
|
||
if external[3] == ?|
|
||
if encoding = strip_bom
|
||
external = encoding
|
||
else
|
||
external = external[4..-1]
|
||
end
|
||
end
|
||
|
||
@external = Encoding.find external
|
||
end
|
||
|
||
unless undefined.equal? options
|
||
# TODO: set the encoding options on the IO instance
|
||
if options and not options.kind_of? Hash
|
||
@encoding_options = Rubinius::Type.coerce_to options, Hash, :to_hash
|
||
end
|
||
end
|
||
|
||
case internal
|
||
when Encoding
|
||
@internal = nil if @external == internal
|
||
when String
|
||
# do nothing
|
||
when nil
|
||
internal = Encoding.default_internal
|
||
else
|
||
internal = StringValue(internal)
|
||
end
|
||
|
||
if internal.kind_of? String
|
||
return self if internal == "-"
|
||
internal = Encoding.find internal
|
||
end
|
||
|
||
@internal = internal unless internal && @external == internal
|
||
|
||
self
|
||
end
|
||
|
||
def read_bom_byte
|
||
read_ios, _, _ = IO.select [self], nil, nil, 0.1
|
||
return getbyte if read_ios
|
||
end
|
||
|
||
def strip_bom
|
||
return unless File::Stat.fstat(descriptor).file?
|
||
|
||
case b1 = getbyte
|
||
when 0x00
|
||
b2 = getbyte
|
||
if b2 == 0x00
|
||
b3 = getbyte
|
||
if b3 == 0xFE
|
||
b4 = getbyte
|
||
if b4 == 0xFF
|
||
return "UTF-32BE"
|
||
end
|
||
ungetbyte b4
|
||
end
|
||
ungetbyte b3
|
||
end
|
||
ungetbyte b2
|
||
|
||
when 0xFF
|
||
b2 = getbyte
|
||
if b2 == 0xFE
|
||
b3 = getbyte
|
||
if b3 == 0x00
|
||
b4 = getbyte
|
||
if b4 == 0x00
|
||
return "UTF-32LE"
|
||
end
|
||
ungetbyte b4
|
||
else
|
||
ungetbyte b3
|
||
return "UTF-16LE"
|
||
end
|
||
ungetbyte b3
|
||
end
|
||
ungetbyte b2
|
||
|
||
when 0xFE
|
||
b2 = getbyte
|
||
if b2 == 0xFF
|
||
return "UTF-16BE"
|
||
end
|
||
ungetbyte b2
|
||
|
||
when 0xEF
|
||
b2 = getbyte
|
||
if b2 == 0xBB
|
||
b3 = getbyte
|
||
if b3 == 0xBF
|
||
return "UTF-8"
|
||
end
|
||
ungetbyte b3
|
||
end
|
||
ungetbyt b2 # FIXME: syntax error waiting to happen!
|
||
end
|
||
|
||
ungetbyte b1
|
||
nil
|
||
end
|
||
|
||
##
|
||
# Returns status information for ios as an object of type File::Stat.
|
||
#
|
||
# f = File.new("testfile")
|
||
# s = f.stat
|
||
# "%o" % s.mode #=> "100644"
|
||
# s.blksize #=> 4096
|
||
# s.atime #=> Wed Apr 09 08:53:54 CDT 2003
|
||
def stat
|
||
ensure_open
|
||
|
||
File::Stat.fstat descriptor
|
||
end
|
||
|
||
##
|
||
# Returns the current "sync mode" of ios. When sync mode is true,
|
||
# all output is immediately flushed to the underlying operating
|
||
# system and is not buffered by Ruby internally. See also IO#fsync.
|
||
#
|
||
# f = File.new("testfile")
|
||
# f.sync #=> false
|
||
def sync
|
||
ensure_open
|
||
@sync == true
|
||
end
|
||
|
||
##
|
||
# Sets the "sync mode" to true or false. When sync mode is true,
|
||
# all output is immediately flushed to the underlying operating
|
||
# system and is not buffered internally. Returns the new state.
|
||
# See also IO#fsync.
|
||
def sync=(v)
|
||
ensure_open
|
||
@sync = !!v
|
||
end
|
||
|
||
##
|
||
# Reads integer bytes from ios using a low-level read and returns
|
||
# them as a string. Do not mix with other methods that read from
|
||
# ios or you may get unpredictable results. Raises SystemCallError
|
||
# on error and EOFError at end of file.
|
||
#
|
||
# f = File.new("testfile")
|
||
# f.sysread(16) #=> "This is line one"
|
||
#
|
||
def sysread(number_of_bytes, buffer=undefined)
|
||
str = @fd.sysread number_of_bytes
|
||
raise EOFError if str.nil?
|
||
|
||
unless undefined.equal? buffer
|
||
StringValue(buffer).replace str
|
||
end
|
||
|
||
str
|
||
end
|
||
|
||
##
|
||
# Seeks to a given offset in the stream according to the value
|
||
# of whence (see IO#seek for values of whence). Returns the new offset into the file.
|
||
#
|
||
# f = File.new("testfile")
|
||
# f.sysseek(-13, IO::SEEK_END) #=> 53
|
||
# f.sysread(10) #=> "And so on."
|
||
def sysseek(amount, whence=SEEK_SET)
|
||
ensure_open
|
||
amount = Integer(amount)
|
||
|
||
@fd.sysseek amount, whence
|
||
end
|
||
|
||
def to_io
|
||
self
|
||
end
|
||
|
||
def ftruncate(offset)
|
||
@fd.ftruncate offset
|
||
end
|
||
|
||
def truncate(name, offset)
|
||
@fd.truncate name, offset
|
||
end
|
||
|
||
##
|
||
# Returns true if ios is associated with a terminal device (tty), false otherwise.
|
||
#
|
||
# File.new("testfile").isatty #=> false
|
||
# File.new("/dev/tty").isatty #=> true
|
||
def tty?
|
||
@fd.tty?
|
||
end
|
||
|
||
alias_method :isatty, :tty?
|
||
|
||
def ttyname
|
||
@fd.ttyname
|
||
end
|
||
|
||
def syswrite(data)
|
||
data = String data
|
||
return 0 if data.bytesize == 0
|
||
|
||
ensure_open_and_writable
|
||
# @ibuffer.unseek!(self) unless @sync
|
||
|
||
@fd.write(data)
|
||
end
|
||
|
||
def ungetbyte(obj)
|
||
ensure_open
|
||
|
||
case obj
|
||
when String
|
||
str = obj
|
||
when Integer
|
||
@fd.unget(obj & 0xff)
|
||
return
|
||
when nil
|
||
return
|
||
else
|
||
str = StringValue(obj)
|
||
end
|
||
|
||
str.bytes.reverse_each { |byte| @fd.unget byte }
|
||
|
||
nil
|
||
end
|
||
|
||
def ungetc(obj)
|
||
ensure_open
|
||
|
||
case obj
|
||
when String
|
||
str = obj
|
||
when Integer
|
||
@fd.unget(obj)
|
||
return
|
||
when nil
|
||
return
|
||
else
|
||
str = StringValue(obj)
|
||
end
|
||
|
||
str.bytes.reverse_each { |byte| @fd.unget byte }
|
||
|
||
nil
|
||
end
|
||
|
||
def write(data)
|
||
data = String data
|
||
return 0 if data.bytesize == 0
|
||
|
||
ensure_open_and_writable
|
||
|
||
if !binmode? && external_encoding &&
|
||
external_encoding != data.encoding &&
|
||
external_encoding != Encoding::ASCII_8BIT
|
||
unless data.ascii_only? && external_encoding.ascii_compatible?
|
||
data.encode!(external_encoding)
|
||
end
|
||
end
|
||
data.encode!(@encoding_options) unless (@encoding_options || {}).empty?
|
||
|
||
# if @sync
|
||
@fd.write(data)
|
||
# else
|
||
# @ibuffer.unseek! self
|
||
# bytes_to_write = data.bytesize
|
||
#
|
||
# while bytes_to_write > 0
|
||
# bytes_to_write -= @ibuffer.unshift(data, data.bytesize - bytes_to_write)
|
||
# @ibuffer.empty_to self if @ibuffer.full? or sync
|
||
# end
|
||
# end
|
||
|
||
data.bytesize
|
||
end
|
||
|
||
def write_nonblock(data, exception: true)
|
||
ensure_open_and_writable
|
||
|
||
data = String data
|
||
return 0 if data.bytesize == 0
|
||
|
||
@fd.write_nonblock(data)
|
||
rescue EAGAINWaitWritable => exc
|
||
raise exc if exception
|
||
|
||
return :wait_writable
|
||
end
|
||
|
||
def close
|
||
return if closed?
|
||
begin
|
||
flush
|
||
ensure
|
||
@fd.close
|
||
end
|
||
|
||
if @pid and @pid != 0
|
||
begin
|
||
Process.wait @pid
|
||
rescue Errno::ECHILD
|
||
# If the child already exited
|
||
end
|
||
@pid = nil
|
||
end
|
||
|
||
return nil
|
||
end
|
||
|
||
def ensure_open(fd=nil)
|
||
raise IOError, "uninitialized stream" unless @fd
|
||
@fd.ensure_open(fd)
|
||
end
|
||
|
||
def ensure_open_and_readable
|
||
ensure_open
|
||
raise IOError, "not opened for reading" if @fd.write_only?
|
||
end
|
||
|
||
def ensure_open_and_writable
|
||
ensure_open
|
||
raise IOError, "not opened for writing" if @fd.read_only?
|
||
end
|
||
|
||
def get_empty_8bit_buffer
|
||
FileDescriptor.get_empty_8bit_buffer
|
||
end
|
||
|
||
def invalid_descriptor?
|
||
descriptor == -1 || descriptor.nil?
|
||
end
|
||
private :invalid_descriptor?
|
||
|
||
def emulate_blocking_read
|
||
# Simple wrapper intended to wrap a call to #read_nonblock.
|
||
# Loops forever while waiting for data to become available
|
||
# just like a blocking read, but avoids blocking on the
|
||
# low-level read(2) call. Allows us to catch situations
|
||
# where another thread has closed the FD.
|
||
begin
|
||
yield
|
||
rescue IO::EAGAINWaitReadable
|
||
sleep 0.10
|
||
retry
|
||
rescue EOFError
|
||
end
|
||
end
|
||
private :emulate_blocking_read
|
||
end
|
||
|
||
##
|
||
# Implements the pipe returned by IO::pipe.
|
||
|
||
class IO::BidirectionalPipe < IO
|
||
|
||
def set_pipe_info(write)
|
||
@write = write
|
||
@sync = true
|
||
end
|
||
|
||
##
|
||
# Closes ios and flushes any pending writes to the
|
||
# operating system. The stream is unavailable for
|
||
# any further data operations; an IOError is raised
|
||
# if such an attempt is made. I/O streams are
|
||
# automatically closed when they are claimed by
|
||
# the garbage collector.
|
||
#
|
||
# If ios is opened by IO.popen, close sets $?.
|
||
def close
|
||
@write.close unless @write.closed?
|
||
|
||
super unless closed?
|
||
|
||
nil
|
||
end
|
||
|
||
def closed?
|
||
super and @write.closed?
|
||
end
|
||
|
||
def close_read
|
||
super
|
||
end
|
||
|
||
def close_write
|
||
return if @write.closed?
|
||
|
||
@write.close
|
||
end
|
||
|
||
# Expand these out rather than using some metaprogramming because it's a fixed
|
||
# set and it's faster to have them as normal methods because then InlineCaches
|
||
# work right.
|
||
#
|
||
def <<(obj)
|
||
@write << obj
|
||
end
|
||
|
||
def print(*args)
|
||
@write.print(*args)
|
||
end
|
||
|
||
def printf(fmt, *args)
|
||
@write.printf(fmt, *args)
|
||
end
|
||
|
||
def putc(obj)
|
||
@write.putc(obj)
|
||
end
|
||
|
||
def puts(*args)
|
||
@write.puts(*args)
|
||
end
|
||
|
||
def syswrite(data)
|
||
@write.syswrite(data)
|
||
end
|
||
|
||
def write(data)
|
||
@write.write(data)
|
||
end
|
||
|
||
def write_nonblock(data)
|
||
@write.write_nonblock(data)
|
||
end
|
||
|
||
end
|
||
|
||
module Rubinius
|
||
class IOUtility
|
||
# Redefine STDIN, STDOUT & STDERR to use the new IO class. It reopened and redefined
|
||
# all methods used in the bootstrap step. Secondly, update the $std* globals to point
|
||
# to the new objects.
|
||
|
||
def self.redefine_io(fd, mode)
|
||
# Note that we use IO.open instead of IO.reopen. The reason is that we reopened the
|
||
# IO class in common/io.rb and overwrote a lot of the methods that were defined in
|
||
# bootstrap/io.rb. So if we try to use IO.reopen on the original IO object, it won't
|
||
# be able to reach those original methods anymore. So, we just pass in the file
|
||
# descriptor integer directly and wrap it up in a new object. The original object
|
||
# will probably get garbage collected but we don't set a finalizer for FDs 0-2 which
|
||
# correspond to STDIN, STDOUT and STDERR so we don't need to worry that they'll get
|
||
# closed out from under us.
|
||
# Hopefully we can find a cleaner way to do this in the future, but for now it's a
|
||
# bit ugly.
|
||
new_io = IO.open(fd)
|
||
new_io.sync = true
|
||
|
||
if mode == :read_only
|
||
new_io.force_read_only
|
||
elsif mode == :write_only
|
||
new_io.force_write_only
|
||
end
|
||
|
||
return new_io
|
||
end
|
||
end
|
||
end
|