diff --git a/configure b/configure index 8fa5fd6d4f..f743145570 100755 --- a/configure +++ b/configure @@ -161,7 +161,7 @@ class Configure @vendored_libdir = File.join(root, "/vendor") # Ruby compatibility version - @ruby_version = "2.2.2" + @ruby_version = "2.3.0" @ruby_libversion = @ruby_version.split(/\./)[0..1].join.to_i @build_bin = "#{@sourcedir}/build/bin" diff --git a/core/encoding.rb b/core/encoding.rb index 526d63d164..8b6f00a589 100644 --- a/core/encoding.rb +++ b/core/encoding.rb @@ -129,6 +129,15 @@ def initialize(from, to, options=undefined) unless source_name == dest_name @convpath, @converters = TranscodingPath[source_name, dest_name] + else + # they are the same encoding so let's check for newline transcoding and override it + if new_to = options[:newline] || options[:universal_newline] || options[:cr_newline] || options[:crlf_newline] + new_to = "#{new_to}_newline" if [:cr, :crlf, :universal].include?(new_to) + + @source_encoding = Rubinius::Type.coerce_to_encoding(from) + @destination_encoding = Rubinius::Type.coerce_to_encoding(new_to) + @convpath, @converters = TranscodingPath[@source_encoding.name.upcase.to_sym, @destination_encoding.name.to_sym] + end end unless @convpath @@ -565,7 +574,7 @@ def self.find(name) enc = Rubinius::Type.try_convert_to_encoding name return enc unless undefined.equal? enc - raise ArgumentError, "unknown encoding name - #{name}" + raise ArgumentError, "unknown encoding name - #{name}, list #{list.inspect}" end def self.list diff --git a/core/errno.rb b/core/errno.rb index 30a6bea819..2de96ac769 100644 --- a/core/errno.rb +++ b/core/errno.rb @@ -9,9 +9,34 @@ module Errno # Unlike rb_sys_fail(), handle does not raise an exception if errno is 0. def self.handle(additional = nil) - err = FFI::Platform::POSIX.errno + err = errno return if err == 0 raise SystemCallError.new(additional, err) end + + def self.errno + FFI.errno + end + + def self.eql?(code) + FFI.errno == code + end + + def self.raise_waitreadable(message=nil) + raise IO::EAGAINWaitReadable, message + end + + def self.raise_waitwritable(message=nil) + raise IO::EAGAINWaitWritable, message + end + + def self.raise_eagain(message=nil) + raise_errno(message, Errno::EAGAIN::Errno) + end + + def self.raise_errno(message, errno) + raise SystemCallError.new(message, errno) + end + private :raise_errno end diff --git a/core/ffi.rb b/core/ffi.rb index 60e9ab35b2..3cc6603232 100644 --- a/core/ffi.rb +++ b/core/ffi.rb @@ -72,6 +72,10 @@ def errno FFI::Platform::POSIX.errno end + # Convenience method for determining if a function call succeeded or failed + def call_failed?(return_code) + return_code == -1 + end end # Represents a C enum. @@ -318,7 +322,7 @@ def self.layout(*spec) element_size = type_size = f.size else if @enclosing_module - type_code = @enclosing_module.find_type(f) + type_code = @enclosing_module.find_type(f) rescue nil end type_code ||= FFI.find_type(f) diff --git a/core/file.rb b/core/file.rb index 723be78f3e..30976b920d 100644 --- a/core/file.rb +++ b/core/file.rb @@ -250,7 +250,7 @@ def self.chown(owner, group, *paths) def chmod(mode) mode = Rubinius::Type.coerce_to(mode, Integer, :to_int) - n = POSIX.fchmod @descriptor, clamp_short(mode) + n = POSIX.fchmod descriptor, clamp_short(mode) Errno.handle if n == -1 n end @@ -268,7 +268,7 @@ def chown(owner, group) group = -1 end - n = POSIX.fchown @descriptor, owner, group + n = POSIX.fchown descriptor, owner, group Errno.handle if n == -1 n end @@ -1018,7 +1018,7 @@ def self.truncate(path, length) length = Rubinius::Type.coerce_to length, Integer, :to_int - prim_truncate(path, length) + FileDescriptor.truncate(path, length) end ## @@ -1189,6 +1189,7 @@ def initialize(path_or_fd, mode=undefined, perm=undefined, options=undefined) Errno.handle path if fd < 0 @path = path + super(fd, mode, options) end end @@ -1230,7 +1231,7 @@ def reopen(other, mode = 'r+') def flock(const) const = Rubinius::Type.coerce_to const, Integer, :to_int - result = POSIX.flock @descriptor, const + result = POSIX.flock descriptor, const return false if result == -1 result @@ -1241,7 +1242,7 @@ def lstat end def stat - Stat.fstat @descriptor + Stat.fstat descriptor end alias_method :to_path, :path @@ -1254,7 +1255,7 @@ def truncate(length) flush reset_buffering - prim_ftruncate(length) + @fd.ftruncate(length) end def inspect diff --git a/core/io.rb b/core/io.rb index 9cb5ad2ef6..2d41fc55f2 100644 --- a/core/io.rb +++ b/core/io.rb @@ -1,8 +1,18 @@ 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 + def send_io(io) Rubinius.primitive :io_send_io raise PrimitiveFailure, "IO#send_io failed" end @@ -40,391 +50,964 @@ class EINPROGRESSWaitWritable < Errno::EINPROGRESS include WaitWritable end - # Import platform constants + 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 - # InternalBuffer provides a sliding window into a region of bytes. - # The buffer is filled to the +used+ indicator, which is - # always less than or equal to +total+. As bytes are taken - # from the buffer, the +start+ indicator is incremented by - # the number of bytes taken. Once +start+ == +used+, the - # buffer is +empty?+ and needs to be refilled. - # - # This description should be independent of the "direction" - # in which the buffer is used. As a read buffer, +fill_from+ - # appends at +used+, but not exceeding +total+. When +used+ - # equals total, no additional bytes will be filled until the - # buffer is emptied. - # - # As a write buffer, +empty_to+ removes bytes from +start+ up - # to +used+. When +start+ equals +used+, no additional bytes - # will be emptied until the buffer is filled. - # - # IO presents a stream of input. Buffer presents buckets of - # input. IO's task is to chain the buckets so the user sees - # a stream. IO explicitly requests that the buffer be filled - # (on input) and then determines how much of the input to take - # (e.g. by looking for a separator or collecting a certain - # number of bytes). Buffer decides whether or not to go to the - # source for more data or just present what is already in the - # buffer. - class InternalBuffer - - attr_reader :total - attr_reader :start - attr_reader :used + def self.choose_type(fd, io) + stat = File::Stat.fstat(fd) - ## - # Returns +true+ if the buffer can be filled. - def empty? - @start == @used + 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 + new(fd, stat, io) + end end - ## - # Returns +true+ if the buffer is empty and cannot be filled further. - def exhausted? - @eof and empty? + 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 - ## - # A request to the buffer to have data. The buffer decides whether - # existing data is sufficient, or whether to read more data from the - # +IO+ instance. Any new data causes this method to return. - # - # Returns the number of bytes in the buffer. - def fill_from(io, skip = nil) - Rubinius.synchronize(self) do - empty_to io - discard skip if skip + 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 - return size unless empty? + 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 - reset! + 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 - if fill(io) < 0 - raise IOError, "error occurred while filling buffer (#{obj})" + 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 + - if @used == 0 - io.eof! - @eof = true + 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 - return size + @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 - def empty_to(io) - return 0 if @write_synced or empty? - @write_synced = true + def autoclose=(bool) + @autoclose = bool + end - io.prim_write(String.from_bytearray(@storage, @start, size)) - reset! + def descriptor + @descriptor + end - return size + def descriptor=(value) + @descriptor = value end - ## - # Advances the beginning-of-buffer marker past any number - # of contiguous characters == +skip+. For example, if +skip+ - # is ?\n and the buffer contents are "\n\n\nAbc...", the - # start marker will be positioned on 'A'. - def discard(skip) - while @start < @used - break unless @storage[@start] == skip - @start += 1 + 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) + + Errno.handle("seek failed") if FFI.call_failed?(position) + + @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 - ## - # Returns the number of bytes to fetch from the buffer up-to- - # and-including +pattern+. Returns +nil+ if pattern is not found. - def find(pattern, discard = nil) - if count = @storage.locate(pattern, @start, @used) - count - @start + def read_into_storage(count, storage) + while true + Thread.current.instance_variable_set(:@sleep, true) + bytes_read = FFI::Platform::POSIX.read(descriptor, storage, count) + Thread.current.instance_variable_set(:@sleep, false) + + 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 - ## - # Returns +true+ if the buffer is filled to capacity. - def full? - @total == @used + 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 inspect # :nodoc: - "#" % [ - object_id, @total, @start, @used, @storage - ] + 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 - ## - # Resets the buffer state so the buffer can be filled again. - def reset! - @start = @used = 0 - @eof = false - @write_synced = true + 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 write_synced? - @write_synced + def eof? + @eof end - def unseek!(io) - Rubinius.synchronize(self) do - # Unseek the still buffered amount - return unless write_synced? - io.prim_seek @start - @used, IO::SEEK_CUR unless empty? - reset! + # 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 - end - ## - # Returns +count+ bytes from the +start+ of the buffer as a new String. - # If +count+ is +nil+, returns all available bytes in the buffer. - def shift(count=nil) - Rubinius.synchronize(self) do - total = size - total = count if count and count < total + ret_code = FFI::Platform::POSIX.shutdown(fd, how) - str = String.from_bytearray @storage, @start, total - @start += total + 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 - str + 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 - PEEK_AHEAD_LIMIT = 16 + def force_read_only + @mode = (@mode & ~IO::O_ACCMODE ) | O_RDONLY + end - def read_to_char_boundary(io, str) - str.force_encoding(io.external_encoding || Encoding.default_external) - return IO.read_encode(io, str) if str.valid_encoding? + def force_write_only + @mode = (@mode & ~IO::O_ACCMODE) | O_WRONLY + end - peek_ahead = 0 - while size > 0 and peek_ahead < PEEK_AHEAD_LIMIT - str.force_encoding Encoding::ASCII_8BIT - str << @storage[@start] - @start += 1 - peek_ahead += 1 + def read_only? + (@mode & O_ACCMODE) == O_RDONLY + end - str.force_encoding(io.external_encoding || Encoding.default_external) - if str.valid_encoding? - return IO.read_encode io, str + 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 - IO.read_encode io, str + 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 one Fixnum as the start byte. - def getbyte(io) - return if size == 0 and fill_from(io) == 0 + # 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. - Rubinius.synchronize(self) do - byte = @storage[@start] - @start += 1 - byte + # 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 - # TODO: fix this when IO buffering is re-written. - def getchar(io) - return if size == 0 and fill_from(io) == 0 + class BufferedFileDescriptor < FileDescriptor - Rubinius.synchronize(self) do - char = "" - while size > 0 - char.force_encoding Encoding::ASCII_8BIT - char << @storage[@start] - @start += 1 + def buffer_reset + @unget_buffer.clear + end + private :buffer_reset - char.force_encoding(io.external_encoding || Encoding.default_external) - if char.chr_at(0) - return IO.read_encode io, char - end + 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("".force_encoding(Encoding::ASCII_8BIT)) { |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("".force_encoding(Encoding::ASCII_8BIT)) { |sum, val| val.chr + sum } + buffer_reset + else + @offset += @unget_buffer.size + str = "".force_encoding(Encoding::ASCII_8BIT) + + 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 - ## - # Prepends the byte +chr+ to the internal buffer, so that future - # reads will return it. - def put_back(chr) - # A simple case, which is common and can be done efficiently - if @start > 0 - @start -= 1 - @storage[@start] = chr + 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("".force_encoding(Encoding::ASCII_8BIT)) { |sum, val| val.chr + sum } + buffer_reset + [str, length] + else + @offset += @unget_buffer.size + str = "".force_encoding(Encoding::ASCII_8BIT) + + length.times do + str << @unget_buffer.pop + end + [str, 0] + end else - @storage = @storage.prepend(chr.chr) - @start = 0 - @total = @storage.size - @used += 1 + [nil, length] end end - ## - # Returns the number of bytes available in the buffer. - def size - @used - @start + 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 - ## - # Returns the number of bytes of capacity remaining in the buffer. - # This is the number of additional bytes that can be added to the - # buffer before it is full. - def unused - @total - @used + def sysread(byte_count) + raise_if_buffering + read(byte_count) end - end - class InternalBuffer - def self.allocate - Rubinius.primitive :iobuffer_allocate - raise PrimitiveFailure, "IO::Buffer.allocate primitive failed" + def sysseek(bytes, whence) + raise_if_buffering + lseek(bytes, whence) end - ## - # Returns the number of bytes that could be written to the buffer. - # If the number is less then the expected, then we need to +empty_to+ - # the IO, and +unshift+ again beginning at +start_pos+. - def unshift(str, start_pos) - Rubinius.primitive :iobuffer_unshift - raise PrimitiveFailure, "IO::Buffer#unshift primitive failed" + def eof? + super && @unget_buffer.empty? + end + + def flush + #@unget_buffer.clear end - def fill(io) - Rubinius.primitive :iobuffer_fill + 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 - unless io.kind_of? IO - return fill(io.to_io) + left -= bytes_written + buffer += bytes_written + @offset += bytes_written end - raise PrimitiveFailure, "IOBuffer#fill primitive failed" + return(buf_size - left) end - end - def self.allocate - Rubinius.primitive :io_allocate - raise PrimitiveFailure, "IO.allocate primitive failed" - end + def unget(byte) + @offset -= 1 + @unget_buffer << byte + end + end # class BufferedFileDescriptor - def self.open_with_mode(path, mode, perm) - Rubinius.primitive :io_open - raise PrimitiveFailure, "IO.open_with_mode primitive failed" - end + class FIFOFileDescriptor < BufferedFileDescriptor - def self.connect_pipe(lhs, rhs) - Rubinius.primitive :io_connect_pipe - raise PrimitiveFailure, "IO.connect_pipe primitive failed" - end + def self.connect_pipe_fds + fds = FFI::MemoryPointer.new(:int, 2) - def self.select_primitive(readables, writables, errorables, timeout) - Rubinius.primitive :io_select - raise IOError, "Unable to select on IO set (descriptor too big?)" - end + Errno.handle("creating pipe failed") if FFI.call_failed?(FFI::Platform::POSIX.pipe(fds)) + fd0, fd1 = fds.read_array_of_int(2) - def self.fnmatch(pattern, path, flags) - Rubinius.primitive :io_fnmatch - raise PrimitiveFailure, "IO#fnmatch primitive failed" - end + FileDescriptor.new_open_fd(fd0) + FileDescriptor.new_open_fd(fd1) - # Instance primitive bindings + return [fd0, fd1] + end - def ensure_open - Rubinius.primitive :io_ensure_open - raise PrimitiveFailure, "IO#ensure_open primitive failed" - end + def initialize(fd, stat, io, mode=nil) + super(fd, stat, self) + @mode = mode if mode + @eof = false # force to false + end - def read_primitive(number_of_bytes) - Rubinius.primitive :io_sysread - raise PrimitiveFailure, "IO::sysread primitive failed" - end + def determine_eof + if @offset >= @total_size + @eof = true - def write(str) - Rubinius.primitive :io_write - raise PrimitiveFailure, "IO#write primitive failed" - end + # No seeking allowed on a pipe, so its size is always its offset + @total_size = @offset + end + end - def read_if_available(size) - Rubinius.primitive :io_read_if_available - raise PrimitiveFailure, "IO#read_if_available primitive failed" - 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 raw_write(str) - Rubinius.primitive :io_write_nonblock - raise PrimitiveFailure, "IO#write_nonblock primitive failed" - end + def seek_positioning + # no seeking allowed for pipes + @offset = 0 + end - def reopen_io(other) - Rubinius.primitive :io_reopen - raise ArgumentError, "IO#prim_reopen only accepts an IO object" - 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 - def reopen_path(string, mode) - Rubinius.primitive :io_reopen_path + class SocketFileDescriptor < FIFOFileDescriptor + def initialize(fd, stat, io) + super - if mode.kind_of? Bignum - raise ArgumentError, "Bignum too big for mode" + @mode &= O_ACCMODE + @sync = true end - reopen_path StringValue(string), Integer(mode) - end + def force_read_write + @mode &= ~(O_RDONLY | O_WRONLY) + @mode |= O_RDWR + end + end # class SocketFileDescriptor - def prim_seek(amount, whence) - Rubinius.primitive :io_seek - raise RangeError, "#{amount} is too big" - end + # Encapsulates all of the logic necessary for handling #select. + class Select + #eval(Rubinius::Config['rbx.platform.timeval.class']) - def self.prim_truncate(name, offset) - Rubinius.primitive :io_truncate - raise RangeError, "#{offset} is too big" - end + class FDSet + def self.new + Rubinius.primitive :fdset_allocate + raise PrimitiveFailure, "FDSet.allocate failed" + end - def prim_ftruncate(offset) - Rubinius.primitive :io_ftruncate - raise RangeError, "#{amount} is too big" - end + def zero + Rubinius.primitive :fdset_zero + raise PrimitiveFailure, "FDSet.zero failed" + end - def query(which) - Rubinius.primitive :io_query - raise PrimitiveFailure, "IO#query primitive failed" - end + def set(descriptor) + Rubinius.primitive :fdset_set + raise PrimitiveFailure, "FDSet.set failed" + end - def reopen(other) - reopen_io other - end + def set?(descriptor) + Rubinius.primitive :fdset_is_set + raise PrimitiveFailure, "FDSet.set? failed" + end - def tty? - query :tty? - end + def to_set + Rubinius.primitive :fdset_to_set + raise PrimitiveFailure, "FDSet.to_set failed" + end + end - def ttyname - query :ttyname - end + def self.fd_set_from_array(array) + highest = -1 + fd_set = FDSet.new + fd_set.zero - def close - Rubinius.primitive :io_close - raise PrimitiveFailure, "IO#close primitive failed" - end + array.each do |io| + io = io[1] if io.is_a?(Array) + descriptor = io.descriptor - # - # Close read and/or write stream of a full-duplex descriptor. - # - # @todo More documentation. Much more. --rue - # - def shutdown(how) - Rubinius.primitive :io_shutdown - raise PrimitiveFailure, "IO#shutdown primitive failed" - end + 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 = Timeval_t.new + future = Timeval_t.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 = Timeval_t.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) + + unless const_defined?(:Timeval_t) + # This is a complete hack. + Select.class_eval(Rubinius::Config['rbx.platform.timeval.class']) + end + + timer = Timeval_t.new # sets fields to zero by default + + 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 + + unless const_defined?(:Timeval_t) + # This is a complete hack. + Select.class_eval(Rubinius::Config['rbx.platform.timeval.class']) + end + + time_limit, future = make_timeval_timeout(timeout) + + events = 0 + loop do + Thread.current.instance_variable_set(:@sleep, true) + + 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) + + Thread.current.instance_variable_set(:@sleep, false) + + 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 - def socket_recv(bytes, flags, type) - Rubinius.primitive :io_socket_read - raise PrimitiveFailure, "io_socket_read failed" - end + objects + end + end # class Select - attr_accessor :descriptor attr_accessor :external attr_accessor :internal - attr_accessor :mode + # 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 @@ -442,12 +1025,12 @@ def self.binwrite(file, string, *args) offset, opts = nil, offset end - mode, binary, external, internal, autoclose = IO.normalize_options(nil, opts) + 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 => (external || "ASCII-8BIT")) do |f| + File.open(file, mode, encoding_options.merge(:encoding => (external || "ASCII-8BIT"))) do |f| f.seek(offset || 0) f.write(string) end @@ -531,7 +1114,7 @@ def run @to.close if @to.kind_of? IO unless @to_io end - end + end # class StreamCopier def self.copy_stream(from, to, max_length=nil, offset=nil) StreamCopier.new(from, to, max_length, offset).run @@ -588,8 +1171,6 @@ def self.foreach(name, separator=undefined, limit=undefined, options=undefined) options = Rubinius::Type.coerce_to options, Hash, :to_hash end - saved_line = $_ - if name[0] == ?| io = IO.popen(name[1..-1], "r") return nil unless io @@ -603,7 +1184,7 @@ def self.foreach(name, separator=undefined, limit=undefined, options=undefined) yield line end ensure - $_ = saved_line + $_ = nil io.close end @@ -612,19 +1193,21 @@ def self.foreach(name, separator=undefined, limit=undefined, options=undefined) 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) + 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 + ec = Encoding::Converter.new(external, internal, (encoding_options || {})) ec.convert str else str.force_encoding external @@ -642,13 +1225,14 @@ def self.write(file, string, *args) offset, opts = nil, offset end - mode, binary, external, internal, autoclose = IO.normalize_options(nil, opts) + 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.merge!(encoding_options) File.open(file, *open_args) do |f| f.seek(offset) if offset f.write(string) @@ -695,7 +1279,6 @@ def self.read(name, length_or_options=undefined, offset=0, options=nil) str = nil begin io.seek(offset) unless offset == 0 - if undefined.equal?(length) str = io.read else @@ -715,6 +1298,7 @@ def self.try_convert(obj) 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) @@ -782,9 +1366,16 @@ def self.normalize_options(mode, options) 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] + [mode, binary, external, internal, autoclose, encoding_options] end def self.open(*args) @@ -825,14 +1416,14 @@ def self.parse_mode(mode) case mode[1] when ?+ - ret &= ~(RDONLY | WRONLY) + ret &= ~(RDONLY | WRONLY) ret |= RDWR when ?b ret |= BINARY when ?t ret &= ~BINARY when ?: - warn("encoding options not supported in 1.8") + warn("encoding options not supported in 1.8") return ret else raise ArgumentError, "invalid mode -- #{mode}" @@ -842,14 +1433,14 @@ def self.parse_mode(mode) case mode[2] when ?+ - ret &= ~(RDONLY | WRONLY) + ret &= ~(RDONLY | WRONLY) ret |= RDWR when ?b ret |= BINARY when ?t ret &= ~BINARY when ?: - warn("encoding options not supported in 1.8") + warn("encoding options not supported in 1.8") return ret else raise ArgumentError, "invalid mode -- #{mode}" @@ -859,19 +1450,14 @@ def self.parse_mode(mode) 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. + fd0, fd1 = FIFOFileDescriptor.connect_pipe_fds lhs = allocate + lhs.send(:new_pipe, fd0, external, internal, options, FileDescriptor::O_RDONLY, true) rhs = allocate - - connect_pipe(lhs, rhs) - - lhs.set_encoding external || Encoding.default_external, - internal || Encoding.default_internal, options - - lhs.sync = true - rhs.sync = true - - lhs.pipe = true - rhs.pipe = true + rhs.send(:new_pipe, fd1, nil, nil, nil, FileDescriptor::O_WRONLY) if block_given? begin @@ -912,7 +1498,7 @@ def self.popen(*args) end end - mode, binary, external, internal, autoclose = + mode, binary, external, internal, autoclose, encoding_options = IO.normalize_options(mode, io_options || {}) mode_int = parse_mode mode @@ -975,7 +1561,7 @@ def self.popen(*args) else return nil end - rescue + rescue => e exit! 0 end end @@ -987,8 +1573,8 @@ def self.popen(*args) if io_options io_options.delete_if do |key, _| [:mode, :external_encoding, :internal_encoding, - :encoding, :textmode, :binmode, :autoclose - ].include? key + :encoding, :textmode, :binmode, :autoclose + ].include? key end options.merge! io_options @@ -1052,50 +1638,11 @@ def self.select(readables=nil, writables=nil, errorables=nil, timeout=nil) timeout = Integer(timeout * 1_000_000) end - if readables - readables = - Rubinius::Type.coerce_to(readables, Array, :to_ary).map do |obj| - if obj.kind_of? IO - raise IOError, "closed stream" if obj.closed? - return [[obj],[],[]] unless obj.buffer_empty? - obj - else - io = Rubinius::Type.coerce_to(obj, IO, :to_io) - raise IOError, "closed stream" if io.closed? - [obj, io] - end - end - end - - if writables - writables = - Rubinius::Type.coerce_to(writables, Array, :to_ary).map do |obj| - if obj.kind_of? IO - raise IOError, "closed stream" if obj.closed? - obj - else - io = Rubinius::Type.coerce_to(obj, IO, :to_io) - raise IOError, "closed stream" if io.closed? - [obj, io] - end - end - end - - if errorables - errorables = - Rubinius::Type.coerce_to(errorables, Array, :to_ary).map do |obj| - if obj.kind_of? IO - raise IOError, "closed stream" if obj.closed? - obj - else - io = Rubinius::Type.coerce_to(obj, IO, :to_io) - raise IOError, "closed stream" if io.closed? - [obj, io] - end - end - 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_primitive(readables, writables, errorables, timeout) + IO::Select.select(readables, writables, errorables, timeout) end ## @@ -1106,7 +1653,7 @@ def self.sysopen(path, mode = nil, perm = nil) mode = parse_mode(mode || "r") perm ||= 0666 - open_with_mode path, mode, perm + FileDescriptor.open_with_mode path, mode, perm end # @@ -1118,7 +1665,7 @@ def self.sysopen(path, mode = nil, perm = nil) # The +sync+ attribute will also be set. # def self.setup(io, fd, mode=nil, sync=false) - cur_mode = FFI::Platform::POSIX.fcntl(fd, F_GETFL, 0) + cur_mode = FileDescriptor.get_flags(fd) Errno.handle if cur_mode < 0 cur_mode &= ACCMODE @@ -1132,17 +1679,31 @@ def self.setup(io, fd, mode=nil, sync=false) end end - io.descriptor = fd + # 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 - 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 + # 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 # @@ -1153,9 +1714,13 @@ def initialize(fd, mode=undefined, options=undefined) warn 'IO::new() does not take block; use IO::open() instead' end - mode, binary, external, internal, @autoclose = IO.normalize_options(mode, options) + mode, binary, external, internal, @autoclose, @encoding_options = + IO.normalize_options(mode, options) - IO.setup self, Rubinius::Type.coerce_to(fd, Integer, :to_int), mode + fd = Rubinius::Type.coerce_to fd, Integer, :to_int + autoclose = @autoclose + IO.setup self, fd, mode + @lineno = 0 binmode if binary set_encoding external, internal @@ -1166,7 +1731,7 @@ def initialize(fd, mode=undefined, options=undefined) if @internal if Encoding.default_external == Encoding.default_internal or - (@external || Encoding.default_external) == Encoding::ASCII_8BIT + (@external || Encoding.default_external) == Encoding::ASCII_8BIT @internal = nil end elsif !mode_read_only? @@ -1183,21 +1748,77 @@ def initialize(fd, mode=undefined, options=undefined) end end - @pipe = false + @pipe = false # FIXME end private :initialize ## # Obtains a new duplicate descriptor for the current one. - def initialize_copy(original) # :nodoc: - @descriptor = FFI::Platform::POSIX.dup(@descriptor) - end + 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 - alias_method :prim_write, :write - alias_method :prim_close, :close + 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 + " \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? @@ -1208,22 +1829,45 @@ def advise(advice, offset = 0, len = 0) end unless [:normal, :sequential, :random, :noreuse, :dontneed, :willneed].include? advice - raise NotImplementedError, "Unsupported advice: #{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 - Rubinius.primitive :io_advise + 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=(autoclose) - @autoclose = !!autoclose + def autoclose=(bool) + @fd.autoclose = bool + @autoclose = !!bool end def binmode @@ -1240,10 +1884,6 @@ def binmode def binmode? !@binmode.nil? end - # Used to find out if there is buffered data available. - def buffer_empty? - @ibuffer.empty? - end def close_on_exec=(value) if value @@ -1276,12 +1916,13 @@ def <<(obj) # prog.rb:3:in `readlines': not opened for reading (IOError) # from prog.rb:3 def close_read - return if closed? + return if invalid_descriptor? if mode_write_only? || mode_read_write? raise IOError, 'closing non-duplex IO for reading' end - close + + @fd.close unless closed? end ## @@ -1298,12 +1939,13 @@ def close_read # from prog.rb:3:in `print' # from prog.rb:3 def close_write - return if closed? + return if invalid_descriptor? if mode_read_only? || mode_read_write? raise IOError, 'closing non-duplex IO for writing' end - close + + @fd.close unless closed? end ## @@ -1319,12 +1961,12 @@ def close_write # f.close_read #=> nil # f.closed? #=> true def closed? - @descriptor == -1 + invalid_descriptor? end def dup ensure_open - super + super # calls #initialize_copy end # Argument matrix for IO#gets and IO#each: @@ -1339,11 +1981,13 @@ def dup # class EachReader - def initialize(io, buffer, separator, limit) + READ_SIZE = 512 # bytes + + def initialize(io, separator, limit, encoding_options) @io = io - @buffer = buffer - @separator = separator + @separator = separator ? separator.force_encoding("ASCII-8BIT") : separator @limit = limit + @encoding_options = encoding_options @skip = nil end @@ -1351,7 +1995,7 @@ def each(&block) if @separator if @separator.empty? @separator = "\n\n" - @skip = 10 + @skip = "\n" end if @limit @@ -1368,139 +2012,190 @@ def each(&block) end end - # method A, D - def read_to_separator - str = "" + def read_and_yield_count_chars(str, buffer, byte_count, &block) + str << buffer.slice!(0, byte_count) - until @buffer.exhausted? - available = @buffer.fill_from @io, @skip - break unless available > 0 + 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. + buffer += (@io.read(PEEK_AHEAD_LIMIT) || '') if buffer.size < PEEK_AHEAD_LIMIT + 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 - if count = @buffer.find(@separator) - str << @buffer.shift(count) + str.taint + $. = @io.increment_lineno + skip_contiguous_chars(buffer) - str = IO.read_encode(@io, str) - str.taint + # 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 - $. = @io.increment_lineno - @buffer.discard @skip if @skip + def read_and_yield_entire_string(str, &block) + str = IO.read_encode(@io, str, @encoding_options) + str.taint + $. = @io.increment_lineno + yield str + end - yield str + # method A, D + def read_to_separator(&block) + str = "".force_encoding(Encoding::ASCII_8BIT) + buffer = "".force_encoding(Encoding::ASCII_8BIT) + separator_size = @separator.bytesize - str = "" + 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 = "".force_encoding(Encoding::ASCII_8BIT) else - str << @buffer.shift + str << buffer + buffer.clear end - end + end until buffer.size == 0 && @io.eof? + + str << buffer - str << @buffer.shift unless str.empty? - str = IO.read_encode(@io, str) - str.taint - $. = @io.increment_lineno - yield str + read_and_yield_entire_string(str, &block) end end # method B, E - def read_to_separator_with_limit - str = "" + + def read_to_separator_with_limit(&block) + str = "".force_encoding(Encoding::ASCII_8BIT) + buffer = "".force_encoding(Encoding::ASCII_8BIT) + separator_size = @separator.bytesize #TODO: implement ignoring encoding with negative limit wanted = limit = @limit.abs - until @buffer.exhausted? - available = @buffer.fill_from @io, @skip - break unless available > 0 - - if count = @buffer.find(@separator) - bytes = count < wanted ? count : wanted - str << @buffer.shift(bytes) - - str = IO.read_encode(@io, str) - str.taint + begin + if buffer.size == 0 + buffer = @io.read(READ_SIZE) + end - $. = @io.increment_lineno - @buffer.discard @skip if @skip + break unless buffer && buffer.size > 0 - yield str + 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 = "" - wanted = limit + str = "".force_encoding(Encoding::ASCII_8BIT) else - if wanted < available - str << @buffer.shift(wanted) - - str = @buffer.read_to_char_boundary(@io, str) - str.taint - - $. = @io.increment_lineno - @buffer.discard @skip if @skip - - yield str - - str = "" - wanted = limit + if wanted < buffer.size + read_and_yield_count_chars(str, buffer, wanted, &block) + str = "".force_encoding(Encoding::ASCII_8BIT) else - str << @buffer.shift - wanted -= available + str << buffer + wanted -= buffer.size + buffer.clear end end - end + end until buffer.size == 0 && @io.eof? unless str.empty? - str = IO.read_encode(@io, str) - str.taint - $. = @io.increment_lineno - yield str + read_and_yield_entire_string(str, &block) end end # Method G - def read_all - str = "" - until @buffer.exhausted? - @buffer.fill_from @io - str << @buffer.shift - end + def read_all(&block) + str = "".force_encoding(Encoding::ASCII_8BIT) + + begin + str << @io.read + end until @io.eof? unless str.empty? - str = IO.read_encode(@io, str) - str.taint - $. = @io.increment_lineno - yield str + read_and_yield_entire_string(str, &block) end end # Method H - def read_to_limit - str = "" + def read_to_limit(&block) + str = "".force_encoding(Encoding::ASCII_8BIT) wanted = limit = @limit.abs - until @buffer.exhausted? - available = @buffer.fill_from @io - if wanted < available - str << @buffer.shift(wanted) + begin + str << @io.read(wanted) + read_and_yield_count_chars(str, '', str.bytesize, &block) + str = "".force_encoding(Encoding::ASCII_8BIT) + 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) - str = @buffer.read_to_char_boundary(@io, str) - str.taint + IO.read_encode(io, str, @encoding_options) + end - $. = @io.increment_lineno - yield str + PEEK_AHEAD_LIMIT = 16 - str = "" - wanted = limit - else - str << @buffer.shift - wanted -= available + 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 - unless str.empty? - str = IO.read_encode(@io, str) - str.taint - $. = @io.increment_lineno - yield str + [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 @@ -1512,10 +2207,14 @@ def increment_lineno ## # Return a string describing this IO object. def inspect - if @descriptor != -1 - "#<#{self.class}:fd #{@descriptor}>" + if @fd + if @fd.descriptor != -1 + "#<#{self.class}:fd #{@fd.descriptor}>" + else + "#<#{self.class}:(closed)>" + end else - "#<#{self.class}:(closed)" + "#<#{self.class}:fd nil>" end end @@ -1550,14 +2249,35 @@ def each(sep_or_limit=$/, limit=nil, &block) end raise ArgumentError, "limit of 0 is invalid" if limit && limit.zero? - return if @ibuffer.exhausted? + return if eof? - EachReader.new(self, @ibuffer, sep, limit).each(&block) + EachReader.new(self, sep, limit, @encoding_options).each(&block) self end - alias_method :each_line, :each + 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? + end + end + end + + each(sep_or_limit, limit, &block) + end def each_byte return to_enum(:each_byte) unless block_given? @@ -1628,8 +2348,7 @@ def eof! # So IO#sysread doesn't work with IO#eof?. def eof? ensure_open_and_readable - @ibuffer.fill_from self unless @ibuffer.exhausted? - @eof and @ibuffer.exhausted? + @fd.eof? end alias_method :eof, :eof? @@ -1724,7 +2443,7 @@ def ioctl(command, arg=0) # $stdout.fileno #=> 1 def fileno ensure_open - @descriptor + return @fd.descriptor end alias_method :to_i, :fileno @@ -1741,19 +2460,17 @@ def fileno # no newline def flush ensure_open - @ibuffer.empty_to self - self + #@fd.reset_positioning + return self end def force_read_only - @mode = (@mode & ~ACCMODE ) | RDONLY + @fd.force_read_only end - private :force_read_only def force_write_only - @mode = (@mode & ~ACCMODE) | WRONLY + @fd.force_write_only end - private :force_write_only ## # Immediately writes all buffered data in ios to disk. Returns @@ -1764,17 +2481,17 @@ def force_write_only def fsync flush - err = FFI::Platform::POSIX.fsync @descriptor + err = FFI::Platform::POSIX.fsync descriptor Errno.handle 'fsync(2)' if err < 0 - err + return err end def getbyte ensure_open - - return @ibuffer.getbyte(self) + byte = read(1) + return(byte ? byte.ord : nil) end ## @@ -1786,8 +2503,20 @@ def getbyte # f.getc #=> 104 def getc ensure_open + return if eof? + + char = "" + 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 @ibuffer.getchar(self) + return nil end def gets(sep_or_limit=$/, limit=nil) @@ -1796,7 +2525,7 @@ def gets(sep_or_limit=$/, limit=nil) return line end - nil + return nil end ## @@ -1816,7 +2545,7 @@ def gets(sep_or_limit=$/, limit=nil) def lineno ensure_open - @lineno + return @lineno end ## @@ -1840,17 +2569,17 @@ def lineno=(line_number) end def mode_read_only? - (@mode & ACCMODE) == RDONLY + @fd.read_only? end private :mode_read_only? def mode_read_write? - (@mode & ACCMODE) == RDWR + @fd.read_write? end private :mode_read_write? def mode_write_only? - (@mode & ACCMODE) == WRONLY + @fd.write_only? end private :mode_write_only? @@ -1887,10 +2616,8 @@ def pipe? ## # def pos - flush - @ibuffer.unseek! self - - prim_seek 0, SEEK_CUR + ensure_open + @fd.offset end alias_method :tell, :pos @@ -2002,27 +2729,19 @@ def read(length=nil, buffer=nil) buffer = StringValue(buffer) if buffer unless length - str = IO.read_encode self, read_all + str = IO.read_encode(self, read_all, @encoding_options) return str unless buffer return buffer.replace(str) end - if @ibuffer.exhausted? - buffer.clear if buffer - return nil - end - str = "" - needed = length - while needed > 0 and not @ibuffer.exhausted? - available = @ibuffer.fill_from self - - count = available > needed ? needed : available - str << @ibuffer.shift(count) - str = nil if str.empty? + emulate_blocking_read do + read_nonblock(length, str) + end - needed -= count + if str.empty? && length > 0 + str = nil end if str @@ -2042,16 +2761,49 @@ def read(length=nil, buffer=nil) # If the buffer is already exhausted, returns +""+. def read_all str = "" - until @ibuffer.exhausted? - @ibuffer.fill_from self - str << @ibuffer.shift - end + 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 @@ -2075,8 +2827,9 @@ def read_nonblock(size, buffer=nil, exception: true) buffer = StringValue buffer if buffer - if @ibuffer.size > 0 - return @ibuffer.shift(size) + unless @fd.blocking? + @fd.set_nonblock + nonblock_reset = true end begin @@ -2088,11 +2841,17 @@ def read_nonblock(size, buffer=nil, exception: true) end if str - buffer.replace(str) if buffer - return 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 ## @@ -2106,7 +2865,7 @@ def readchar def readbyte byte = getbyte raise EOFError, "end of file reached" unless byte - raise EOFError, "end of file" unless bytes + #raise EOFError, "end of file" unless bytes # bytes/each_byte is deprecated, FIXME - is this line necessary? byte end @@ -2198,10 +2957,15 @@ def readpartial(size, buffer=nil) return buffer if size == 0 - if @ibuffer.size > 0 - data = @ibuffer.shift(size) - else - data = sysread(size) + 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) @@ -2210,11 +2974,18 @@ def readpartial(size, buffer=nil) else return "" if size == 0 - if @ibuffer.size > 0 - return @ibuffer.shift(size) + data = nil + begin + data = read_nonblock(size) + rescue IO::WaitReadable + IO.select([self]) + retry + rescue IO::WaitWritable + IO.select(nil, [self]) + retry end - return sysread(size) + return data end end @@ -2241,20 +3012,29 @@ def reopen(other, mode=undefined) end end - io.ensure_open - io.reset_buffering + # Note: this is the whole reason that FileDescriptor#ensure_open takes an argument. + # + ensure_open(io.descriptor) + # io.reset_buffering - reopen_io io + @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 = @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? @@ -2265,17 +3045,27 @@ def reopen(other, mode=undefined) end reopen_path Rubinius::Type.coerce_to_path(other), mode - seek 0, SEEK_SET + + 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 + # ##@ibuffer.unseek! self end ## @@ -2309,10 +3099,9 @@ def rewind def seek(amount, whence=SEEK_SET) flush - @ibuffer.unseek! self @eof = false - prim_seek Integer(amount), whence + @fd.seek Integer(amount), whence return 0 end @@ -2354,7 +3143,7 @@ def set_encoding(external, internal=nil, options=undefined) unless undefined.equal? options # TODO: set the encoding options on the IO instance if options and not options.kind_of? Hash - options = Rubinius::Type.coerce_to options, Hash, :to_hash + @encoding_options = Rubinius::Type.coerce_to options, Hash, :to_hash end end @@ -2385,7 +3174,7 @@ def read_bom_byte end def strip_bom - return unless File::Stat.fstat(@descriptor).file? + return unless File::Stat.fstat(descriptor).file? case b1 = getbyte when 0x00 @@ -2437,7 +3226,7 @@ def strip_bom end ungetbyte b3 end - ungetbyt b2 + ungetbyt b2 # FIXME: syntax error waiting to happen! end ungetbyte b1 @@ -2455,7 +3244,7 @@ def strip_bom def stat ensure_open - File::Stat.fstat @descriptor + File::Stat.fstat descriptor end ## @@ -2489,13 +3278,8 @@ def sync=(v) # f = File.new("testfile") # f.sysread(16) #=> "This is line one" # - # @todo Improve reading into provided buffer. - # def sysread(number_of_bytes, buffer=undefined) - flush - raise IOError unless @ibuffer.empty? - - str = read_primitive number_of_bytes + str = @fd.sysread number_of_bytes raise EOFError if str.nil? unless undefined.equal? buffer @@ -2514,41 +3298,46 @@ def sysread(number_of_bytes, buffer=undefined) # f.sysread(10) #=> "And so on." def sysseek(amount, whence=SEEK_SET) ensure_open - if @ibuffer.write_synced? - raise IOError unless @ibuffer.empty? - else - warn 'sysseek for buffered IO' - end - amount = Integer(amount) - prim_seek amount, whence + @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? - ensure_open - FFI::Platform::POSIX.isatty(@descriptor) == 1 + @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 + # @ibuffer.unseek!(self) unless @sync - prim_write(data) + @fd.write(data) end def ungetbyte(obj) @@ -2558,7 +3347,7 @@ def ungetbyte(obj) when String str = obj when Integer - @ibuffer.put_back(obj & 0xff) + @fd.unget(obj & 0xff) return when nil return @@ -2566,7 +3355,7 @@ def ungetbyte(obj) str = StringValue(obj) end - str.bytes.reverse_each { |byte| @ibuffer.put_back byte } + str.bytes.reverse_each { |byte| @fd.unget byte } nil end @@ -2578,7 +3367,7 @@ def ungetc(obj) when String str = obj when Integer - @ibuffer.put_back(obj) + @fd.unget(obj) return when nil return @@ -2586,7 +3375,7 @@ def ungetc(obj) str = StringValue(obj) end - str.bytes.reverse_each { |b| @ibuffer.put_back b } + str.bytes.reverse_each { |byte| @fd.unget byte } nil end @@ -2598,24 +3387,25 @@ def write(data) ensure_open_and_writable if !binmode? && external_encoding && - external_encoding != data.encoding && - external_encoding != Encoding::ASCII_8BIT + 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 - prim_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 + # 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 @@ -2626,9 +3416,7 @@ def write_nonblock(data, exception: true) data = String data return 0 if data.bytesize == 0 - @ibuffer.unseek!(self) unless @sync - - raw_write(data) + @fd.write_nonblock(data) rescue EAGAINWaitWritable => exc raise exc if exception @@ -2640,7 +3428,7 @@ def close begin flush ensure - prim_close + @fd.close end if @pid and @pid != 0 @@ -2655,6 +3443,41 @@ def close 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 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 ## @@ -2689,7 +3512,7 @@ def closed? end def close_read - close + super end def close_write @@ -2735,3 +3558,34 @@ def 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 diff --git a/core/pointer.rb b/core/pointer.rb index 1499b8f1c5..f2bffdce80 100644 --- a/core/pointer.rb +++ b/core/pointer.rb @@ -315,6 +315,8 @@ def self.new(type, count=nil, clear=true) total = size end + return NULL if total < 0 + ptr = malloc total ptr.total = total ptr.type_size = size diff --git a/core/stat.rb b/core/stat.rb index 6c0605d795..c1e2bef905 100644 --- a/core/stat.rb +++ b/core/stat.rb @@ -16,7 +16,7 @@ def self.stat(path) def self.fstat(fd) stat = allocate result = Rubinius.privately { stat.fsetup fd } - Errno.handle "file descriptor #{descriptor}" unless result == 0 + Errno.handle "file descriptor #{fd}" unless result == 0 stat end diff --git a/core/string.rb b/core/string.rb index 4a3f187908..a1e08dc8d6 100644 --- a/core/string.rb +++ b/core/string.rb @@ -1122,9 +1122,18 @@ def encode!(to=undefined, from=undefined, options=undefined) raise ArgumentError, "unexpected value for xml option: #{xml.inspect}" end - if options[:universal_newline] - gsub!(/\r\n|\r/, "\r\n" => "\n", "\r" => "\n") - end + if new_to = options[:universal_newline] || options[:crlf_newline] || options[:cr_newline] || options[:newline] + STDERR.puts "String.encode!, new_to #{new_to.inspect}, from_enc #{from_enc.inspect}, to_enc #{to_enc.inspect}" + #raise ArgumentError, "unexpected value, from: #{from_enc.inspect}, to: #{to_enc.inspect}, options: #{options.inspect}" + ec = Encoding::Converter.new(from_enc, (to_enc || from_enc), options) + dest = "" + status = ec.primitive_convert self.dup, dest, nil, nil, ec.options + raise ec.last_error unless status == :finished + replace dest + end +# if options[:universal_newline] +# gsub!(/\r\n|\r/, "\r\n" => "\n", "\r" => "\n") +# end end self diff --git a/core/zed.rb b/core/zed.rb index 4ecad959ea..fdc8aff957 100644 --- a/core/zed.rb +++ b/core/zed.rb @@ -595,15 +595,23 @@ module FFI # Converts an unsigned short add_typedef TYPE_USHORT, :ushort + add_typedef TYPE_USHORT, :mode_t + add_typedef TYPE_USHORT, :nlink_t # Converts an int add_typedef TYPE_INT, :int + add_typedef TYPE_INT, :dev_t + add_typedef TYPE_INT, :blksize_t + add_typedef TYPE_INT, :time_t # Converts an unsigned int add_typedef TYPE_UINT, :uint + add_typedef TYPE_UINT, :uid_t + add_typedef TYPE_UINT, :gid_t # Converts a long add_typedef TYPE_LONG, :long + add_typedef TYPE_LONG, :ssize_t # Converts an unsigned long add_typedef TYPE_ULONG, :ulong @@ -613,9 +621,12 @@ module FFI # Converts a long long add_typedef TYPE_LL, :long_long + add_typedef TYPE_LL, :blkcnt_t + add_typedef TYPE_LL, :off_t # Converts an unsigned long long add_typedef TYPE_ULL, :ulong_long + add_typedef TYPE_ULL, :ino64_t # Converts a float add_typedef TYPE_FLOAT, :float @@ -855,14 +866,32 @@ module POSIX attach_function :chroot, [:string], :int # File/IO - attach_function :fcntl, [:int, :int, :long], :int - attach_function :ioctl, [:int, :ulong, :long], :int - attach_function :fsync, [:int], :int - attach_function :dup, [:int], :int - attach_function :dup2, [:int, :int], :int + attach_function :fcntl, [:int, :int, :long], :int + attach_function :ioctl, [:int, :ulong, :long], :int + attach_function :fsync, [:int], :int + attach_function :dup, [:int], :int + attach_function :dup2, [:int, :int], :int + attach_function :open, [:string, :int, :mode_t], :int + attach_function :close, [:int], :int + attach_function :lseek, [:int, :off_t, :int], :off_t + attach_function :read, [:int, :pointer, :size_t], :ssize_t + attach_function :ftruncate, [:int, :off_t], :int + attach_function :truncate, [:string, :off_t], :int + attach_function :write, [:int, :pointer, :size_t], :ssize_t + attach_function :select, [:int, :pointer, :pointer, :pointer, :pointer], :int + + # Other I/O + attach_function :pipe, [:pointer], :int + attach_function :mmap, [:pointer, :size_t, :int, :int, :int, :off_t], :pointer + attach_function :msync, [:pointer, :size_t, :int], :int + attach_function :munmap, [:pointer, :size_t], :int + attach_function :getpagesize, [], :int + attach_function :shutdown, [:int, :int], :int + attach_function :posix_fadvise, [:int, :off_t, :off_t, :int], :int # inspecting attach_function :isatty, [:int], :int + attach_function :ttyname, [:int], :string # locking attach_function :flock, [:int, :int], :int @@ -920,6 +949,9 @@ module POSIX attach_function :major, 'ffi_major', [:dev_t], :dev_t attach_function :minor, 'ffi_minor', [:dev_t], :dev_t + # time + attach_function :gettimeofday, [:pointer, :pointer], :int + #-- # Internal class for accessing timevals #++ @@ -1074,10 +1106,13 @@ module Constants # O_ACCMODE is /undocumented/ for fcntl() on some platforms ACCMODE = Rubinius::Config['rbx.platform.fcntl.O_ACCMODE'] + O_ACCMODE = Rubinius::Config['rbx.platform.fcntl.O_ACCMODE'] F_GETFD = Rubinius::Config['rbx.platform.fcntl.F_GETFD'] F_SETFD = Rubinius::Config['rbx.platform.fcntl.F_SETFD'] FD_CLOEXEC = Rubinius::Config['rbx.platform.fcntl.FD_CLOEXEC'] + O_CLOEXEC = Rubinius::Config['rbx.platform.file.O_CLOEXEC'] + O_NONBLOCK = Rubinius::Config['rbx.platform.file.O_NONBLOCK'] RDONLY = Rubinius::Config['rbx.platform.file.O_RDONLY'] WRONLY = Rubinius::Config['rbx.platform.file.O_WRONLY'] @@ -1159,6 +1194,28 @@ class IO SEEK_SET = Rubinius::Config['rbx.platform.io.SEEK_SET'] SEEK_CUR = Rubinius::Config['rbx.platform.io.SEEK_CUR'] SEEK_END = Rubinius::Config['rbx.platform.io.SEEK_END'] + + # Not available on all platforms, so these constants may be nil + POSIX_FADV_NORMAL = Rubinius::Config['rbx.platform.advise.POSIX_FADV_NORMAL'] + POSIX_FADV_SEQUENTIAL = Rubinius::Config['rbx.platform.advise.POSIX_FADV_SEQUENTIAL'] + POSIX_FADV_RANDOM = Rubinius::Config['rbx.platform.advise.POSIX_FADV_RANDOM'] + POSIX_FADV_WILLNEED = Rubinius::Config['rbx.platform.advise.POSIX_FADV_WILLNEED'] + POSIX_FADV_DONTNEED = Rubinius::Config['rbx.platform.advise.POSIX_FADV_DONTNEED'] + POSIX_FADV_NOREUSE = Rubinius::Config['rbx.platform.advise.POSIX_FADV_NOREUSE'] + + class FileDescriptor + @@max_descriptors = Rubinius::AtomicReference.new(2) + + include File::Constants + + O_RDONLY = Rubinius::Config['rbx.platform.file.O_RDONLY'] + O_WRONLY = Rubinius::Config['rbx.platform.file.O_WRONLY'] + O_RDWR = Rubinius::Config['rbx.platform.file.O_RDWR'] + end + + class Select + FD_SETSIZE = Rubinius::Config['rbx.platform.select.FD_SETSIZE'] + end end class NilClass @@ -1599,6 +1656,15 @@ class Rlimit < FFI::Struct Rubinius::Globals.set_hook(:$?) { Thread.current[:$?] } end + +STDIN = Rubinius::IOUtility.redefine_io(0, :read_only) +STDOUT = Rubinius::IOUtility.redefine_io(1, :write_only) +STDERR = Rubinius::IOUtility.redefine_io(2, :write_only) + +Rubinius::Globals.set!(:$stdin, STDIN) +Rubinius::Globals.set!(:$stdout, STDOUT) +Rubinius::Globals.set!(:$stderr, STDERR) + module Rubinius begin is_tty = STDIN.tty? diff --git a/library/ffi/generators/structures.rb b/library/ffi/generators/structures.rb index 902b2d9f06..0c3dee7a1d 100644 --- a/library/ffi/generators/structures.rb +++ b/library/ffi/generators/structures.rb @@ -159,6 +159,14 @@ def write_config(io) @fields.each { |field| io.puts field.to_config(@name) } end + def write_class(io) + layout = generate_layout.gsub(/\n/, '').squeeze(' ') + struct_name = @struct_name.split(' ').last.capitalize + "_t" + struct_class = "class #{struct_name} < FFI::Struct; #{layout}; end" + + io.puts "rbx.platform.#{@name}.class = #{struct_class}" + end + def generate_layout buf = "" diff --git a/machine/builtin/io.cpp b/machine/builtin/io.cpp index b0f6ddd09f..1fe47bab59 100644 --- a/machine/builtin/io.cpp +++ b/machine/builtin/io.cpp @@ -11,12 +11,20 @@ #include "builtin/channel.hpp" #include "builtin/class.hpp" #include "builtin/exception.hpp" +#include "builtin/ffi_pointer.hpp" #include "builtin/fixnum.hpp" #include "builtin/io.hpp" +#include "builtin/native_method.hpp" #include "builtin/string.hpp" #include "builtin/thread.hpp" #include "capi/handle.hpp" +#include "call_frame.hpp" +#include "configuration.hpp" +#include "memory.hpp" +#include "object_utils.hpp" +#include "on_stack.hpp" +#include "ontology.hpp" #include "util/spinlock.hpp" @@ -40,829 +48,49 @@ #endif namespace rubinius { - int IO::max_descriptors_ = 2; void IO::bootstrap(STATE) { GO(io).set(state->memory()->new_class(state, G(object), "IO")); - - GO(iobuffer).set(state->memory()->new_class( - state, G(object), G(io), "InternalBuffer")); + G(io)->set_object_type(state, IOType); } IO* IO::create(STATE, int fd) { IO* io = state->memory()->new_object(state, G(io)); - io->descriptor(state, Fixnum::from(fd)); - -#ifdef RBX_WINDOWS - // TODO: Windows - int acc_mode = 0; -#else - int acc_mode = fcntl(fd, F_GETFL); -#endif - if(acc_mode < 0) { - // Assume it's closed. - if(errno == EBADF) { - io->descriptor(state, Fixnum::from(-1)); - } - io->mode(state, nil()); - } else { - io->mode(state, Fixnum::from(acc_mode)); - } - - io->ibuffer(state, IOBuffer::create(state)); - io->eof(state, cFalse); - io->lineno(state, Fixnum::from(0)); - - // Don't bother to add finalization for stdio - if(fd >= 3) { - state->memory()->extension_finalizer(state, io, - (memory::FinalizerFunction)&IO::finalize); - } - - return io; - } - - IO* IO::allocate(STATE, Object* self) { - IO* io = state->memory()->new_object(state, as(self)); - io->ibuffer(state, IOBuffer::create(state)); - - state->memory()->extension_finalizer(state, io, - (memory::FinalizerFunction)&IO::finalize); return io; } - Fixnum* IO::open(STATE, String* p, Fixnum* m, Fixnum* perm) { - char* path = strdup(p->c_str_null_safe(state)); - int mode = m->to_int(); - int permissions = perm->to_int(); - int fd = -1; - - OnStack<1> os(state, p); - - { - UnmanagedPhase unmanaged(state); - fd = open_with_cloexec(state, path, mode, permissions); - } - - free(path); - - if(fd < 0) { - Exception::raise_errno_error(state, p->c_str_null_safe(state)); - } - - return Fixnum::from(fd); - } - native_int IO::open_with_cloexec(STATE, const char* path, int mode, int permissions) { -#ifdef O_CLOEXEC - int fd = ::open(path, mode | O_CLOEXEC, permissions); - update_max_fd(state, fd); -#else - int fd = ::open(path, mode, permissions) - new_open_fd(state, fd); -#endif - - return fd; - } - - void IO::new_open_fd(STATE, native_int new_fd) { - if(new_fd > 2) { - int flags = fcntl(new_fd, F_GETFD); - if(flags == -1) Exception::raise_errno_error(state, "fcntl(2) failed"); - flags = fcntl(new_fd, F_SETFD, fcntl(new_fd, F_GETFD) | FD_CLOEXEC); - if(flags == -1) Exception::raise_errno_error(state, "fcntl(2) failed"); - } - update_max_fd(state, new_fd); - } - - void IO::update_max_fd(STATE, native_int new_fd) { - while(true) { - int max = atomic::read(&max_descriptors_); - - if(new_fd < max_descriptors_) return; - if(atomic::compare_and_swap(&max_descriptors_, max, new_fd)) return; - } - } - - namespace { - /** Utility function used by IO::select, returns highest descriptor. */ - static inline native_int fd_set_from_array(State* state, - Object* maybe_descriptors, fd_set* set) - { - if(NULL == set) { - return 0; - } - - Array* descriptors = as(maybe_descriptors); - - FD_ZERO(set); - native_int highest = -1; - - for(native_int i = 0; i < descriptors->size(); ++i) { - Object* elem = descriptors->get(state, i); - IO* io; - - if(Array* ary = try_as(elem)) { - io = as(ary->get(state, 1)); - } else { - io = as(elem); - } - - native_int descriptor = io->to_fd(); - - // 1024 is selec't limit. If we try to set a value higher, it corrupts - // memory. YAY FD_SET! - if(descriptor >= FD_SETSIZE) return -2; - highest = descriptor > highest ? descriptor : highest; - - if(descriptor >= 0) FD_SET((int_fd_t)descriptor, set); - } - - return highest; - } - - /** Utility function used by IO::select, returns Array of IOs that were set. */ - static inline Array* reject_unset_fds(State* state, - Object* maybe_originals, fd_set* set) - { - if(NULL == set) return Array::create(state, 0); - - Array* originals = as(maybe_originals); - - // A single value is the most common, so prime for that. - Array* selected = Array::create(state, 1); - - for(native_int i = 0; i < originals->size(); ++i) { - Object* elem = originals->get(state, i); - Object* key; - IO* io; - - if(Array* ary = try_as(elem)) { - key = ary->get(state, 0); - io = as(ary->get(state, 1)); - } else { - key = elem; - io = as(elem); - } - - int fd = io->to_fd(); - if(fd < 0 || FD_ISSET(fd, set)) { - selected->append(state, key); - } - } - - return selected; - } - } - - /** - * Ergh. select/FD_* is not exactly user-oriented design. - * - * @todo This is highly unoptimised since we always rebuild the FD_SETs. --rue - */ - Object* IO::select(STATE, Object* readables, Object* writables, - Object* errorables, Object* timeout) - { - // GC protection / awareness - OnStack<3> os(state, readables, writables, errorables); - - fd_set read_set; - fd_set* maybe_read_set = readables->nil_p() ? NULL : &read_set; - - fd_set write_set; - fd_set* maybe_write_set = writables->nil_p() ? NULL : &write_set; - - fd_set error_set; - fd_set* maybe_error_set = errorables->nil_p() ? NULL : &error_set; - - native_int highest = 0; - native_int candidate = 0; - - /* Build the sets, track the highest descriptor number. These handle NULLs */ - highest = fd_set_from_array(state, readables, maybe_read_set); - if(highest == -2) return Primitives::failure(); - - candidate = fd_set_from_array(state, writables, maybe_write_set); - if(candidate == -2) return Primitives::failure(); - highest = candidate > highest ? candidate : highest; - - candidate = fd_set_from_array(state, errorables, maybe_error_set); - if(candidate == -2) return Primitives::failure(); - highest = candidate > highest ? candidate : highest; - - struct timeval future; - struct timeval limit; - struct timeval* maybe_limit = NULL; - - if(!timeout->nil_p()) { - unsigned long long microseconds = as(timeout)->to_ulong_long(); - limit.tv_sec = microseconds / 1000000; - limit.tv_usec = microseconds % 1000000; - maybe_limit = &limit; - - // Get the current time to be used if select is interrupted and we - // have to recalculate the sleep time - gettimeofday(&future, NULL); - timeradd(&future, &limit, &future); - } - - native_int events; - - - /* And the main event, pun intended */ - retry: - state->vm()->interrupt_with_signal(); - state->vm()->thread()->sleep(state, cTrue); - - { - UnmanagedPhase unmanaged(state); - events = ::select((highest + 1), maybe_read_set, - maybe_write_set, - maybe_error_set, - maybe_limit); - } - - state->vm()->thread()->sleep(state, cFalse); - state->vm()->clear_waiter(); - - if(events == -1) { - if(errno == EAGAIN || errno == EINTR) { - if(state->vm()->thread_interrupted_p(state)) return NULL; - - // Recalculate the limit and go again. - if(maybe_limit) { - struct timeval now; - gettimeofday(&now, NULL); - timersub(&future, &now, &limit); - } - - goto retry; - } - - Exception::raise_errno_error(state, "select(2) failed"); - return NULL; - } - - /* Timeout expired */ - if(events == 0) return cNil; - - /* Build the results. */ - Array* output = Array::create(state, 3); - - /* These handle NULL sets. */ - output->set(state, 0, reject_unset_fds(state, readables, maybe_read_set)); - output->set(state, 1, reject_unset_fds(state, writables, maybe_write_set)); - output->set(state, 2, reject_unset_fds(state, errorables, maybe_error_set)); - - return output; - } - - -/* Instance methods */ - - Object* IO::reopen(STATE, IO* other) { - native_int cur_fd = to_fd(); - native_int other_fd = other->to_fd(); - - if(dup2(other_fd, cur_fd) == -1) { - Exception::raise_errno_error(state, "reopen"); - return NULL; - } - - set_mode(state); - if(IOBuffer* ibuf = try_as(ibuffer())) { - ibuf->reset(state); - } - - return cTrue; - } - - Object* IO::reopen_path(STATE, String* p, Fixnum* m) { - native_int cur_fd = to_fd(); - - char* path = strdup(p->c_str_null_safe(state)); - int mode = m->to_int(); - int other_fd = -1; - - IO* self = this; - OnStack<2> os(state, self, p); - - { - UnmanagedPhase unmanaged(state); - other_fd = open_with_cloexec(state, path, mode, 0666); - } - - free(path); - - if(other_fd < 0) { - Exception::raise_errno_error(state, p->c_str_null_safe(state)); - } - - if(dup2(other_fd, cur_fd) == -1) { - if(errno == EBADF) { // this means cur_fd is closed - // Just set ourselves to use the new fd and go on with life. - descriptor(state, Fixnum::from(other_fd)); + if(Class* fd_class = try_as(G(io)->get_const(state, "FileDescriptor"))) { + Tuple* args = Tuple::from(state, 3, + String::create(state, path), + Fixnum::from(mode), + Fixnum::from(permissions)); + + if(Fixnum* fd = try_as(fd_class->send(state, + state->symbol("open_with_cloexec"), + Array::from_tuple(state, args)))) + { + return fd->to_native(); } else { - if(other_fd > 0) ::close(other_fd); - Exception::raise_errno_error(state, p->c_str_null_safe(state)); + Exception::raise_runtime_error(state, "unable to open IO with cloexec"); } } else { - ::close(other_fd); - } - - self->set_mode(state); - if(IOBuffer* ibuf = try_as(self->ibuffer())) { - ibuf->reset(state); - } - - return cTrue; - } - - Object* IO::ensure_open(STATE) { - if(descriptor()->nil_p()) { - Exception::raise_io_error(state, "uninitialized stream"); - } - else if(to_fd() == -1) { - Exception::raise_io_error(state, "closed stream"); - } - else if(to_fd() == -2) { - Exception::raise_io_error(state, "shutdown stream"); - } - - return cNil; - } - - Object* IO::connect_pipe(STATE, IO* lhs, IO* rhs) { - int fds[2]; - if(pipe(fds) == -1) { - Exception::raise_errno_error(state, "creating pipe"); - } - - new_open_fd(state, fds[0]); - new_open_fd(state, fds[1]); - - lhs->descriptor(state, Fixnum::from(fds[0])); - rhs->descriptor(state, Fixnum::from(fds[1])); - - lhs->mode(state, Fixnum::from(O_RDONLY)); - rhs->mode(state, Fixnum::from(O_WRONLY)); - return cTrue; - } - - Integer* IO::seek(STATE, Integer* amount, Fixnum* whence) { - ensure_open(state); - - if(Bignum* big = try_as(amount)) { - if((size_t)mp_count_bits(big->mp_val()) > (sizeof(off_t) * 8)) { - return static_cast(Primitives::failure()); - } - } - - off_t offset = amount->to_long_long(); - off_t position = lseek(to_fd(), offset, whence->to_native()); - - if(position == -1) { - Exception::raise_errno_error(state); - } - - return Integer::from(state, position); - } - - Integer* IO::ftruncate(STATE, Fixnum* off) { - ensure_open(state); - - if(Bignum* big = try_as(off)) { - if((size_t)mp_count_bits(big->mp_val()) > (sizeof(off_t) * 8)) { - return static_cast(Primitives::failure()); - } - } - - off_t offset = off->to_long_long(); - - int status = ::ftruncate(to_fd(), offset); - if(status == -1) { - Exception::raise_errno_error(state); - } - - return Integer::from(state, status); - } - - Integer* IO::truncate(STATE, String* name, Fixnum* off) { - - if(Bignum* big = try_as(off)) { - if((size_t)mp_count_bits(big->mp_val()) > (sizeof(off_t) * 8)) { - return static_cast(Primitives::failure()); - } - } - - off_t offset = off->to_long_long(); - - int status = ::truncate(name->c_str_null_safe(state), offset); - if(status == -1) { - Exception::raise_errno_error(state); - } - - return Integer::from(state, status); - } - - /** This is NOT the same as shutdown(). */ - Object* IO::close(STATE) { - ensure_open(state); - - /** @todo Should this be just int? --rue */ - native_int desc = to_fd(); - - // Already closed, ignore. - if(desc == -1) { - return cNil; - } - - // Invalid descriptor no matter what. - descriptor(state, Fixnum::from(-1)); - - // Don't close stdin, stdout, stderr descriptors. - if(desc < 3) return cNil; - - // If there is a handle for this IO, and it's been promoted into - // a lowlevel RIO struct using fdopen, then we MUST use fclose - // to close it. - - if(capi::Handle* hdl = handle(state)) { - if(hdl->is_rio()) { - if(!hdl->rio_close()) { - Exception::raise_errno_error(state); - } - return cNil; - } - } - - switch(::close(desc)) { - case -1: - Exception::raise_errno_error(state); - break; - - case 0: - break; - - default: - std::ostringstream message; - message << "::close(): Unknown error on fd " << desc; - Exception::raise_system_call_error(state, message.str()); - } - - return cNil; - } - - /** - * This is NOT the same as close(). - * - * @todo Need to build the infrastructure to be able to only - * remove read or write waiters if a partial shutdown - * is requested. --rue - */ - Object* IO::shutdown(STATE, Fixnum* how) { - ensure_open(state); - - int which = how->to_int(); - native_int desc = to_fd(); - - if(which != SHUT_RD && which != SHUT_WR && which != SHUT_RDWR) { - std::ostringstream message; - message << "::shutdown(): Invalid `how` " << which << " for fd " << desc; - Exception::raise_argument_error(state, message.str().c_str()); - } - - switch(::shutdown(desc, which)) { - case -1: - Exception::raise_errno_error(state); - break; - - case 0: - if(which == SHUT_RDWR) { - /* Yes, it really does need to be closed still. */ - (void) close(state); - - descriptor(state, Fixnum::from(-2)); - } - - break; - - default: - std::ostringstream message; - message << "::shutdown(): Unknown error on fd " << desc; - Exception::raise_system_call_error(state, message.str()); - } - - return how; - } - - native_int IO::to_fd() { - return descriptor()->to_native(); - } - - void IO::set_mode(STATE) { -#ifdef F_GETFL - int acc_mode = fcntl(to_fd(), F_GETFL); - if(acc_mode < 0) { - Exception::raise_errno_error(state); - } -#else - int acc_mode = 0; -#endif - mode(state, Fixnum::from(acc_mode)); - } - - void IO::force_read_only(STATE) { - int m = mode()->to_native(); - mode(state, Fixnum::from((m & ~O_ACCMODE) | O_RDONLY)); - } - - void IO::force_write_only(STATE) { - int m = mode()->to_native(); - mode(state, Fixnum::from((m & ~O_ACCMODE) | O_WRONLY)); - } - - void IO::finalize(STATE, IO* io) { - if(io->descriptor()->nil_p()) return; - - native_int fd = io->descriptor()->to_native(); - - if(fd == -1) return; - - // Flush the buffer to disk if it's not write sync'd - if(IOBuffer* buf = try_as(io->ibuffer())) { - if(!CBOOL(buf->write_synced())) { - native_int start = buf->start()->to_native(); - native_int used = buf->used()->to_native(); - native_int bytes = used - start; - - if(bytes > 0 && start < buf->storage()->size()) { - fd_set fds; - FD_ZERO(&fds); - struct timeval tv = {0,0}; - - FD_SET(fd, &fds); - - // We use select(2) to prevent from blocking while - // trying to flush out the data. - - uint8_t* data = buf->storage()->raw_bytes() + start; - while(bytes > 0 && ::select(fd+1, 0, &fds, 0, &tv) > 0) { - ssize_t wrote = ::write(fd, data, bytes); - // If we couldn't write, then just bail. - if(wrote == -1) break; - data += wrote; - bytes -= wrote; - - state->vm()->metrics().system.write_bytes += wrote; - } - } - } - } - - // don't close stdin, stdout, stderr (0, 1, 2) - if(fd > STDERR_FILENO) { - io->descriptor(state, Fixnum::from(-1)); - - // If there is a handle for this IO, and it's been promoted into - // a lowlevel RIO struct using fdopen, then we MUST use fclose - // to close it. - - if(capi::Handle* hdl = io->handle(state)) { - if(hdl->is_rio()) { - hdl->rio_close(); - return; - } - } - - if(!io->autoclose()->false_p()) { - ::close(fd); - } - } - } - -#define STACK_BUF_SZ 8192 - - Object* IO::sysread(STATE, Fixnum* number_of_bytes) { - char stack_buf[STACK_BUF_SZ]; - char* malloc_buf = 0; - - char* buf = stack_buf; - - size_t count = number_of_bytes->to_ulong(); - - if(count > STACK_BUF_SZ) { - malloc_buf = (char*)malloc(count); - if(!malloc_buf) { - Exception::raise_memory_error(state); - return NULL; - } - buf = malloc_buf; - } - - ssize_t bytes_read; - native_int fd = descriptor()->to_native(); - - retry: - state->vm()->interrupt_with_signal(); - state->vm()->thread()->sleep(state, cTrue); - - { - UnmanagedPhase unmanaged(state); - bytes_read = ::read(fd, buf, count); - } - - state->vm()->thread()->sleep(state, cFalse); - state->vm()->clear_waiter(); - - if(bytes_read == -1) { - if(errno == EAGAIN || errno == EINTR) { - if(state->vm()->thread_interrupted_p(state)) { - if(malloc_buf) free(malloc_buf); - return NULL; - } - ensure_open(state); - goto retry; - } else { - Exception::raise_errno_error(state, "read(2) failed"); - } - - if(malloc_buf) free(malloc_buf); - return NULL; - } - - if(bytes_read == 0) { - if(malloc_buf) free(malloc_buf); - return cNil; - } - - state->vm()->metrics().system.read_bytes += bytes_read; - - String* str = String::create(state, buf, bytes_read); - if(malloc_buf) free(malloc_buf); - - return str; - } - - Object* IO::read_if_available(STATE, Fixnum* number_of_bytes) { - - std::size_t count = number_of_bytes->to_ulong(); - if(count == 0) return String::create(state, Fixnum::from(0)); - - fd_set set; - FD_ZERO(&set); - - native_int fd = descriptor()->to_native(); - FD_SET((int_fd_t)fd, &set); - - struct timeval tv = {0,0}; - int res = ::select(fd+1, &set, 0, 0, &tv); - - if(res == 0) { - Exception::raise_errno_wait_readable(state, EAGAIN); - return 0; - } else if(res <= 0) { - Exception::raise_errno_error(state, "read(2) failed"); - return 0; - } - - String* buffer = String::create_pinned(state, number_of_bytes); - - // There is a minor race here. If another thread is running concurrently - // and it reads from fd, then our select might say there is data, but when - // we go to read from it, we block. - // - // The problem is that twiddle the O_NONBLOCK bit has the same problem, - // so when there are concurrent IO reads, we'll need to enforce - // some kind of integrity here. - ssize_t bytes_read = ::read(fd, buffer->byte_address(), count); - - buffer->unpin(); - - if(bytes_read == -1) { - Exception::raise_errno_error(state, "read(2) failed"); + Exception::raise_runtime_error(state, "unable to access IO::FileDescriptor class"); } - - if(bytes_read == 0) return cNil; - - state->vm()->metrics().system.read_bytes += bytes_read; - - buffer->num_bytes(state, Fixnum::from(bytes_read)); - return buffer; } - Object* IO::write(STATE, String* buf) { - native_int buf_size = buf->byte_size(); - native_int data_size = as(buf->data())->size(); - native_int left = buf_size; - if(unlikely(left > data_size)) { - left = data_size; + native_int IO::descriptor(STATE) { + if(Fixnum* fd = try_as(send(state, state->symbol("descriptor")))) { + return fd->to_native(); } - uint8_t* bytes = new uint8_t[left]; - memcpy(bytes, buf->byte_address(), left); - int fd = this->to_fd(); - bool error = false; - { - UnmanagedPhase unmanaged(state); - uint8_t* cur = bytes; - while(left > 0) { - ssize_t cnt = ::write(fd, cur, left); - - if(cnt == -1) { - switch(errno) { - case EINTR: - case EAGAIN: { - // Pause before continuing - fd_set fds; - FD_ZERO(&fds); - FD_SET((int_fd_t)fd, &fds); - - ::select(fd+1, NULL, &fds, NULL, NULL); - - continue; - } - case EPIPE: - if(fd == STDOUT_FILENO || fd == STDERR_FILENO) { - left = 0; - delete[] bytes; - goto done; - } - // fall through - default: - error = true; - break; - } - } - - if(error) break; - - left -= cnt; - cur += cnt; - - state->vm()->metrics().system.write_bytes += cnt; - } - } - - delete[] bytes; - - if(error) { - Exception::raise_errno_error(state); - return NULL; - } - - done: - return Integer::from(state, buf_size - left); - } - - Object* IO::write_nonblock(STATE, String* buf) { - set_nonblock(state); - - native_int buf_size = buf->byte_size(); - native_int data_size = as(buf->data())->size(); - if(unlikely(buf_size > data_size)) { - buf_size = data_size; - } - - // We can use byte_address() here since we use an explicit size - int n = ::write(descriptor()->to_native(), buf->byte_address(), buf_size); - if(n == -1) Exception::raise_errno_wait_writable(state, errno); - - state->vm()->metrics().system.write_bytes += n; - - return Fixnum::from(n); + Exception::raise_runtime_error(state, "IO descriptor is not a Fixnum"); } - Object* IO::advise(STATE, Symbol* advice_name, Integer* offset, Integer* len) { -#ifdef HAVE_POSIX_FADVISE - int advice = 0; - - if(advice_name == state->symbol("normal")) { - advice = POSIX_FADV_NORMAL; - } else if(advice_name == state->symbol("sequential")) { - advice = POSIX_FADV_SEQUENTIAL; - } else if(advice_name == state->symbol("random")) { - advice = POSIX_FADV_RANDOM; - } else if(advice_name == state->symbol("willneed")) { - advice = POSIX_FADV_WILLNEED; - } else if(advice_name == state->symbol("dontneed")) { - advice = POSIX_FADV_DONTNEED; - } else if(advice_name == state->symbol("noreuse")) { - advice = POSIX_FADV_NOREUSE; - } else { - return Primitives::failure(); - } - - int erno = posix_fadvise(to_fd(), offset->to_long_long(), len->to_long_long(), advice); - - if(erno) { - Exception::raise_errno_error(state, "posfix_fadvise(2) failed", erno); - } - -#endif - - return cNil; + void IO::ensure_open(STATE) { + // Will raise an exception if the file descriptor is not open + send(state, state->symbol("ensure_open")); } Array* ipaddr(STATE, struct sockaddr* addr, socklen_t len) { @@ -954,7 +182,8 @@ namespace rubinius { { UnmanagedPhase unmanaged(state); - bytes_read = recvfrom(descriptor()->to_native(), + + bytes_read = recvfrom(descriptor(state), (char*)buffer->byte_address(), size, flags->to_native(), (struct sockaddr*)buf, &alen); @@ -1006,33 +235,6 @@ namespace rubinius { return ary; } - static int ttyname_lock = RBX_SPINLOCK_UNLOCKED; - Object* IO::query(STATE, Symbol* op) { - ensure_open(state); - - native_int fd = to_fd(); - - if(op == state->symbol("tty?")) { - return RBOOL(isatty(fd)); -#ifndef RBX_WINDOWS - } else if(op == state->symbol("ttyname")) { - rbx_spinlock_lock(&ttyname_lock); - char* name = ttyname(fd); - if(name) { - String* res = String::create(state, name); - rbx_spinlock_unlock(&ttyname_lock); - return res; - } else { - rbx_spinlock_unlock(&ttyname_lock); - Exception::raise_errno_error(state, "ttyname(3) failed"); - return NULL; - } -#endif - } - - return cNil; - } - // Stole/ported from 1.8.7. The system fnmatch doesn't support // a bunch of things this does (and must). @@ -1236,7 +438,7 @@ namespace rubinius { struct cmsghdr *cmsg; char cmsg_buf[cmsg_space]; - fd = io->descriptor()->to_native(); + fd = io->descriptor(state); msg.msg_name = NULL; msg.msg_namelen = 0; @@ -1261,7 +463,7 @@ namespace rubinius { int* fd_data = (int*)CMSG_DATA(cmsg); *fd_data = fd; - if(sendmsg(descriptor()->to_native(), &msg, 0) == -1) { + if(sendmsg(descriptor(state), &msg, 0) == -1) { return Primitives::failure(); } @@ -1303,7 +505,7 @@ namespace rubinius { int* fd_data = (int *)CMSG_DATA(cmsg); *fd_data = -1; - int read_fd = descriptor()->to_native(); + int read_fd = descriptor(state); int code = -1; @@ -1342,138 +544,72 @@ namespace rubinius { #endif } - void IO::set_nonblock(STATE) { -#ifdef F_GETFL - int flags = fcntl(descriptor()->to_native(), F_GETFL); - if(flags == -1) Exception::raise_errno_error(state, "fcntl(2) failed"); -#else - int flags = 0; -#endif - - if((flags & O_NONBLOCK) == 0) { - flags |= O_NONBLOCK; - flags = fcntl(descriptor()->to_native(), F_SETFL, flags); - if(flags == -1) Exception::raise_errno_error(state, "fcntl(2) failed"); - } + void FDSet::bootstrap(STATE) { + // Create a constant for FDSet under the IO::Select namespace, i.e. IO::Select::FDSet + GO(select).set(state->memory()->new_class(state, G(io), "Select")); + GO(fdset).set(state->memory()->new_class(state, G(select), "FDSet")); } -/* IOBuffer methods */ - IOBuffer* IOBuffer::create(STATE, size_t bytes) { - IOBuffer* buf = state->memory()->new_object(state, G(iobuffer)); - - buf->storage(state, ByteArray::create(state, bytes)); - buf->total(state, Fixnum::from(bytes)); - buf->used(state, Fixnum::from(0)); - buf->reset(state); - buf->write_synced(state, cTrue); - - return buf; + FDSet* FDSet::allocate(STATE, Object* self) { + FDSet* fdset = create(state); + fdset->klass(state, as(self)); + return fdset; } - IOBuffer* IOBuffer::allocate(STATE) { - return create(state); + FDSet* FDSet::create(STATE) { + FDSet* fdset = state->memory()->new_object(state, G(fdset)); + return fdset; } - Object* IOBuffer::unshift(STATE, String* str, Fixnum* start_pos) { - write_synced(state, cFalse); - native_int start_pos_native = start_pos->to_native(); - native_int str_size = str->byte_size(); - native_int data_size = as(str->data())->size(); - if(unlikely(str_size > data_size)) { - str_size = data_size; - } - native_int total_sz = str_size - start_pos_native; - native_int used_native = used()->to_native(); - native_int available_space = total()->to_native() - used_native; - - if(total_sz > available_space) { - total_sz = available_space; - } - - memcpy(storage()->raw_bytes() + used_native, str->byte_address() + start_pos_native, total_sz); - used(state, Fixnum::from(used_native + total_sz)); - - return Fixnum::from(total_sz); + Object* FDSet::zero(STATE) { + FD_ZERO((fd_set*)descriptor_set); + return cTrue; } - Object* IOBuffer::fill(STATE, IO* io) { - ssize_t bytes_read = 0; - native_int fd = io->descriptor()->to_native(); - - IOBuffer* self = this; - OnStack<1> os(state, self); + Object* FDSet::set(STATE, Fixnum* descriptor) { + native_int fd = descriptor->to_native(); - char temp_buffer[STACK_BUF_SZ] = { 0 }; - size_t count = STACK_BUF_SZ; + FD_SET((int_fd_t)fd, (fd_set*)descriptor_set); - if(self->left() < count) count = self->left(); - - retry: - state->vm()->interrupt_with_signal(); - state->vm()->thread()->sleep(state, cTrue); - - { - UnmanagedPhase unmanaged(state); - bytes_read = ::read(fd, temp_buffer, count); - } + return cTrue; + } - state->vm()->thread()->sleep(state, cFalse); - state->vm()->clear_waiter(); + Object* FDSet::is_set(STATE, Fixnum* descriptor) { + native_int fd = descriptor->to_native(); - if(bytes_read == -1) { - switch(errno) { - case ECONNRESET: - case ETIMEDOUT: - // Treat as seeing eof - bytes_read = 0; - break; - case EAGAIN: - case EINTR: - if(state->vm()->thread_interrupted_p(state)) return NULL; - io->ensure_open(state); - goto retry; - default: - Exception::raise_errno_error(state, "read(2) failed"); - return NULL; - } + if (FD_ISSET(fd, (fd_set*)descriptor_set)) { + return cTrue; } - - if(bytes_read > 0) { - // Detect if another thread has updated the buffer - // and now there isn't enough room for this data. - if(bytes_read > (ssize_t)self->left()) { - Exception::internal_error(state, "IO buffer overrun"); - return NULL; - } - memcpy(self->at_unused(), temp_buffer, bytes_read); - self->read_bytes(state, bytes_read); - state->vm()->metrics().system.read_bytes += bytes_read; + else { + return cFalse; } - - return Fixnum::from(bytes_read); } - void IOBuffer::reset(STATE) { - used(state, Fixnum::from(0)); - start(state, Fixnum::from(0)); - eof(state, cFalse); - } + Object* FDSet::to_set(STATE) { + void *ptr = (void*)&descriptor_set; - void IOBuffer::read_bytes(STATE, size_t bytes) { - used(state, Fixnum::from(used()->to_native() + bytes)); + return Pointer::create(state, ptr); } - char* IOBuffer::byte_address() { - return (char*)storage()->raw_bytes(); + void RIOStream::bootstrap(STATE) { + GO(rio_stream).set(state->memory()->new_class( + state, G(rubinius), "RIOStream")); } - size_t IOBuffer::left() { - return total()->to_native() - used()->to_native(); - } + Object* RIOStream::close(STATE, Object* io, Object* allow_exception) { + // If there is a handle for this IO, and it's been promoted into + // a lowlevel RIO struct using fdopen, then we MUST use fclose + // to close it. - char* IOBuffer::at_unused() { - char* start = (char*)storage()->raw_bytes(); - start += used()->to_native(); - return start; + if(capi::Handle* hdl = io->handle(state)) { + if(hdl->is_rio()) { + if(!hdl->rio_close() && CBOOL(allow_exception)) { + Exception::raise_errno_error(state, "failed to close RIOStream"); + } + return cTrue; + } + } + return cFalse; } -}; +} + diff --git a/machine/builtin/io.hpp b/machine/builtin/io.hpp index a6663dc627..791ad299a5 100644 --- a/machine/builtin/io.hpp +++ b/machine/builtin/io.hpp @@ -15,191 +15,103 @@ namespace rubinius { class Encoding; class IO : public Object { - static int max_descriptors_; public: const static object_type type = IOType; - attr_accessor(descriptor, Fixnum); - attr_accessor(path, String); - attr_accessor(ibuffer, Object); - attr_accessor(mode, Fixnum); - attr_accessor(eof, Object); - attr_accessor(lineno, Fixnum); - attr_accessor(sync, Object); - attr_accessor(external, Encoding); - attr_accessor(internal, Encoding); - attr_accessor(autoclose, Object); + public: + /* interface */ static void bootstrap(STATE); - static void initialize(STATE, IO* obj) { - obj->descriptor(nil()); - obj->path(nil()); - obj->ibuffer(nil()); - obj->mode(nil()); - obj->eof(cFalse); - obj->lineno(Fixnum::from(0)); - obj->sync(nil()); - obj->external(nil()); - obj->internal(nil()); - obj->autoclose(nil()); - } + static void initialize(STATE, IO* obj) { } static IO* create(STATE, int fd); + static native_int open_with_cloexec(STATE, const char* path, int mode, int permissions); - static int max_descriptors() { - return max_descriptors_; - } - - native_int to_fd(); - void set_mode(STATE); - void force_read_only(STATE); - void force_write_only(STATE); - static void finalize(STATE, IO* io); + native_int descriptor(STATE); + void ensure_open(STATE); /* Class primitives */ - // Rubinius.primitive :io_allocate - static IO* allocate(STATE, Object* self); - - // Rubinius.primitive :io_connect_pipe - static Object* connect_pipe(STATE, IO* lhs, IO* rhs); - - // Rubinius.primitive :io_open - static Fixnum* open(STATE, String* path, Fixnum* mode, Fixnum* perm); - - static native_int open_with_cloexec(STATE, const char* path, int mode, int permissions); - static void new_open_fd(STATE, native_int fd); - static void update_max_fd(STATE, native_int fd); - - /** - * Perform select() on descriptors. - * - * @todo Replace with an evented version when redoing events. --rue - */ - // Rubinius.primitive :io_select - static Object* select(STATE, Object* readables, Object* writables, Object* errorables, Object* timeout); - // Rubinius.primitive :io_fnmatch static Object* fnmatch(STATE, String* pattern, String* path, Fixnum* flags); /* Instance primitives */ - // Rubinius.primitive :io_ensure_open - Object* ensure_open(STATE); - - /** - * Directly read up to number of bytes from descriptor. - * - * Returns cNil at EOF. - */ - // Rubinius.primitive :io_sysread - Object* sysread(STATE, Fixnum* number_of_bytes); - - // Rubinius.primitive :io_read_if_available - Object* read_if_available(STATE, Fixnum* number_of_bytes); - // Rubinius.primitive :io_socket_read Object* socket_read(STATE, Fixnum* bytes, Fixnum* flags, Fixnum* type); - // Rubinius.primitive :io_seek - Integer* seek(STATE, Integer* amount, Fixnum* whence); + // Rubinius.primitive :io_send_io + Object* send_io(STATE, IO* io); - // Rubinius.primitive :io_truncate - static Integer* truncate(STATE, String* name, Fixnum* off); + // Rubinius.primitive :io_recv_fd + Object* recv_fd(STATE); - // Rubinius.primitive :io_ftruncate - Integer* ftruncate(STATE, Fixnum* off); + class Info : public TypeInfo { + public: + Info(object_type type) : TypeInfo(type) { } + void auto_mark(Object* obj, memory::ObjectMark& mark) { } + void set_field(STATE, Object* target, size_t index, Object* val) { } + Object* get_field(STATE, Object* target, size_t index) { return cNil; } + void populate_slot_locations() { } + }; - // Rubinius.primitive :io_write - Object* write(STATE, String* buf); + }; - // Rubinius.primitive :io_reopen - Object* reopen(STATE, IO* other); + class FDSet : public Object { + public: + const static object_type type = FDSetType; - // Rubinius.primitive :io_reopen_path - Object* reopen_path(STATE, String* other, Fixnum * mode); + private: + uint8_t descriptor_set[sizeof(fd_set)]; - // Rubinius.primitive :io_close - Object* close(STATE); + public: - // Rubinius.primitive :io_send_io - Object* send_io(STATE, IO* io); + static void bootstrap(STATE); - // Rubinius.primitive :io_recv_fd - Object* recv_fd(STATE); + static FDSet* create(STATE); - /** - * Shutdown a full-duplex descriptor's read and/or write stream. - * - * Careful with this, it applies to full-duplex only. - * It also shuts the stream *in all processes*, not - * just the current one. - */ - // Rubinius.primitive :io_shutdown - Object* shutdown(STATE, Fixnum* how); + // Rubinius.primitive :fdset_allocate + static FDSet* allocate(STATE, Object* self); - // Rubinius.primitive :io_query - Object* query(STATE, Symbol* op); + // Rubinius.primitive :fdset_zero + Object* zero(STATE); - // Rubinius.primitive :io_write_nonblock - Object* write_nonblock(STATE, String* buf); + // Rubinius.primitive :fdset_is_set + Object* is_set(STATE, Fixnum* descriptor); - // Rubinius.primitive :io_advise - Object* advise(STATE, Symbol* advice_name, Integer* offset, Integer* len); + // Rubinius.primitive :fdset_set + Object* set(STATE, Fixnum* descriptor); - void set_nonblock(STATE); + // Rubinius.primitive :fdset_to_set + Object* to_set(STATE); class Info : public TypeInfo { public: - BASIC_TYPEINFO(TypeInfo) + Info(object_type type) : TypeInfo(type) { } + void auto_mark(Object* obj, memory::ObjectMark& mark) { } + void set_field(STATE, Object* target, size_t index, Object* val) { } + Object* get_field(STATE, Object* target, size_t index) { return cNil; } + void populate_slot_locations() { } }; - }; -#define IOBUFFER_SIZE 32768U - - class IOBuffer : public Object { + class RIOStream : public Object { public: - const static size_t fields = 7; - const static object_type type = IOBufferType; - - attr_accessor(storage, ByteArray); - attr_accessor(total, Fixnum); - attr_accessor(used, Fixnum); - attr_accessor(start, Fixnum); - attr_accessor(eof, Object); - attr_accessor(write_synced, Object); - - static void initialize(STATE, IOBuffer* obj) { - obj->storage(nil()); - obj->total(Fixnum::from(0)); - obj->used(Fixnum::from(0)); - obj->start(Fixnum::from(0)); - obj->eof(cFalse); - obj->write_synced(cTrue); - } - - static IOBuffer* create(STATE, size_t bytes = IOBUFFER_SIZE); - // Rubinius.primitive :iobuffer_allocate - static IOBuffer* allocate(STATE); - - // Rubinius.primitive :iobuffer_unshift - Object* unshift(STATE, String* str, Fixnum* start_pos); - - // Rubinius.primitive :iobuffer_fill - Object* fill(STATE, IO* io); - - void reset(STATE); - String* drain(STATE); - char* byte_address(); - size_t left(); - char* at_unused(); - void read_bytes(STATE, size_t bytes); + const static object_type type = RIOStreamType; + + static void bootstrap(STATE); + + // Rubinius.primitive :rio_close + static Object* close(STATE, Object* io, Object* allow_exception); class Info : public TypeInfo { public: - BASIC_TYPEINFO(TypeInfo) + Info(object_type type) : TypeInfo(type) { } + void auto_mark(Object* obj, memory::ObjectMark& mark) { } + void set_field(STATE, Object* target, size_t index, Object* val) { } + Object* get_field(STATE, Object* target, size_t index) { return cNil; } + void populate_slot_locations() { } }; }; diff --git a/machine/builtin/system.cpp b/machine/builtin/system.cpp index d345486ed8..b99b3fcace 100644 --- a/machine/builtin/system.cpp +++ b/machine/builtin/system.cpp @@ -13,6 +13,7 @@ #include "builtin/location.hpp" #include "builtin/lookup_table.hpp" #include "builtin/method_table.hpp" +#include "builtin/native_method.hpp" #include "builtin/thread.hpp" #include "builtin/tuple.hpp" #include "builtin/string.hpp" @@ -346,7 +347,9 @@ namespace rubinius { } if(CBOOL(table->has_key(state, state->symbol("close_others")))) { - int max = IO::max_descriptors(); + Class* fd_class = (Class*) G(io)->get_const(state, "FileDescriptor"); + Fixnum* max_fd = (Fixnum*)fd_class->send(state, state->symbol("max_fd")); + int max = max_fd->to_native(); int flags; for(int fd = STDERR_FILENO + 1; fd < max; fd++) { @@ -362,11 +365,11 @@ namespace rubinius { native_int size = assign->size(); for(native_int i = 0; i < size; i += 4) { int from = as(assign->get(state, i))->to_native(); - int mode = as(assign->get(state, i + 2))->to_native(); - int perm = as(assign->get(state, i + 3))->to_native(); - const char* name = as(assign->get(state, i + 1))->c_str_null_safe(state); + int to = IO::open_with_cloexec(state, + as(assign->get(state, i + 1))->c_str(state), + as(assign->get(state, i + 2))->to_native(), + as(assign->get(state, i + 3))->to_native()); - int to = IO::open_with_cloexec(state, name, mode, perm); redirect_file_descriptor(from, to); } } diff --git a/machine/capi/io.cpp b/machine/capi/io.cpp index 4daad8f75a..d41c09367b 100644 --- a/machine/capi/io.cpp +++ b/machine/capi/io.cpp @@ -5,6 +5,7 @@ #include "builtin/array.hpp" #include "builtin/fixnum.hpp" #include "builtin/io.hpp" +#include "builtin/string.hpp" #include "builtin/thread.hpp" #include "memory.hpp" #include "primitives.hpp" @@ -59,12 +60,21 @@ namespace rubinius { RIO* Handle::as_rio(NativeMethodEnvironment* env) { IO* io_obj = c_as(object()); + ID id_descriptor = rb_intern("descriptor"); + ID id_mode = rb_intern("mode"); + VALUE jobj = env->get_handle(io_obj); if(type_ != cRIO) { env->shared().capi_ds_lock().lock(); if(type_ != cRIO) { - int fd = (int)io_obj->descriptor()->to_native(); + native_int fd = -1; + VALUE fileno = rb_funcall(jobj, id_descriptor, 0); + Fixnum* tmp_fd = try_as(env->get_object(fileno)); + + if(tmp_fd) { + fd = tmp_fd->to_native(); + } env->shared().capi_ds_lock().unlock(); @@ -74,7 +84,7 @@ namespace rubinius { rb_raise(rb_eIOError, "%s (%d)", err, errno); } - FILE* f = fdopen(fd, flags_modestr(io_obj->mode()->to_native())); + FILE* f = fdopen(fd, flags_modestr(try_as(env->get_object(rb_funcall(jobj, id_mode, 0)))->to_native())); if(!f) { char buf[RBX_STRERROR_BUFSIZE]; @@ -167,8 +177,7 @@ extern "C" { int rb_io_fd(VALUE io_handle) { NativeMethodEnvironment* env = NativeMethodEnvironment::get(); - IO* io = c_as(env->get_object(io_handle)); - return io->descriptor()->to_native(); + return c_as(env->get_object(rb_funcall(io_handle, rb_intern("fileno"), 0)))->to_native(); } long rb_io_fread(char* ptr, int len, FILE* f) { @@ -353,10 +362,9 @@ extern "C" { void rb_io_set_nonblock(rb_io_t* iot) { VALUE io_handle = iot->handle; - NativeMethodEnvironment* env = NativeMethodEnvironment::get(); + VALUE fd_ivar = rb_ivar_get(io_handle, rb_intern("fd")); - IO* io = c_as(env->get_object(io_handle)); - io->set_nonblock(env->state()); + rb_funcall(fd_ivar, rb_intern("set_nonblock"), 0); } VALUE rb_io_check_io(VALUE io) { @@ -371,9 +379,8 @@ extern "C" { void rb_io_check_closed(rb_io_t* iot) { VALUE io_handle = iot->handle; NativeMethodEnvironment* env = NativeMethodEnvironment::get(); - IO* io = c_as(env->get_object(io_handle)); - if(io->descriptor()->to_native() == -1) { + if(c_as(env->get_object(rb_funcall(io_handle, rb_intern("fileno"), 0)))->to_native() == -1) { rb_raise(rb_eIOError, "closed stream"); } } @@ -381,8 +388,7 @@ extern "C" { void rb_io_check_readable(rb_io_t* iot) { VALUE io_handle = iot->handle; NativeMethodEnvironment* env = NativeMethodEnvironment::get(); - IO* io = c_as(env->get_object(io_handle)); - int io_mode = io->mode()->to_native() & O_ACCMODE; + int io_mode = c_as(env->get_object(rb_funcall(io_handle, rb_intern("mode"), 0)))->to_native() & O_ACCMODE; if(!(O_RDONLY == io_mode || O_RDWR == io_mode)) { rb_raise(rb_eIOError, "not opened for reading"); } @@ -391,8 +397,7 @@ extern "C" { void rb_io_check_writable(rb_io_t* iot) { VALUE io_handle = iot->handle; NativeMethodEnvironment* env = NativeMethodEnvironment::get(); - IO* io = c_as(env->get_object(io_handle)); - int io_mode = io->mode()->to_native() & O_ACCMODE; + int io_mode = c_as(env->get_object(rb_funcall(io_handle, rb_intern("mode"), 0)))->to_native() & O_ACCMODE; if(!(O_WRONLY == io_mode || O_RDWR == io_mode)) { rb_raise(rb_eIOError, "not opened for writing"); } @@ -400,12 +405,22 @@ extern "C" { void rb_update_max_fd(int fd) { NativeMethodEnvironment* env = NativeMethodEnvironment::get(); - IO::update_max_fd(env->state(), fd); + State *state = env->state(); + Object* fd_object = G(io)->get_const(env->state(), "FileDescriptor"); + VALUE fd_class = env->get_handle(fd_object); + VALUE descriptor = env->get_handle(Fixnum::from(fd)); + + rb_funcall(fd_class, rb_intern("update_max_fd"), 1, descriptor); } void rb_fd_fix_cloexec(int fd) { NativeMethodEnvironment* env = NativeMethodEnvironment::get(); - IO::new_open_fd(env->state(), fd); + State *state = env->state(); + Object* fd_object = G(io)->get_const(env->state(), "FileDescriptor"); + VALUE fd_class = env->get_handle(fd_object); + VALUE descriptor = env->get_handle(Fixnum::from(fd)); + + rb_funcall(fd_class, rb_intern("new_open_fd"), 1, descriptor); } int rb_cloexec_open(const char *pathname, int flags, int mode) { diff --git a/machine/codegen/transcoders_extract.rb b/machine/codegen/transcoders_extract.rb index 07ffa64063..d5b6dba16b 100644 --- a/machine/codegen/transcoders_extract.rb +++ b/machine/codegen/transcoders_extract.rb @@ -13,7 +13,7 @@ def read(name) File.open definitions, "wb" do |f| re = /^static\s*const\s*rb_transcoder\s*\n rb_\w+\s*=\s*\{\s*\n - \s*"([^"]+)",\s*"([^"]+)"/mx + \s*"([^"]*)",\s*"([^"]+)"/mx Dir["#{dir}/*.c"].sort.each do |name| f.puts " // #{name}" diff --git a/machine/globals.hpp b/machine/globals.hpp index 8bc999973b..ca32df8567 100644 --- a/machine/globals.hpp +++ b/machine/globals.hpp @@ -56,7 +56,8 @@ namespace rubinius { memory::TypedRoot floatpoint, nmc, list, list_node; memory::TypedRoot channel, thread, thread_state, constantscope; memory::TypedRoot constant_table, lookup_table; - memory::TypedRoot iseq, executable, native_function, iobuffer; + memory::TypedRoot iseq, executable, native_function; + memory::TypedRoot select, fdset, rio_stream; memory::TypedRoot included_module; /* the primary symbol table */ @@ -174,7 +175,9 @@ namespace rubinius { iseq(&roots), executable(&roots), native_function(&roots), - iobuffer(&roots), + select(&roots), + fdset(&roots), + rio_stream(&roots), included_module(&roots), sym_method_missing(&roots), sym_respond_to_missing(&roots), diff --git a/machine/ontology.cpp b/machine/ontology.cpp index 0f3c8a0e6e..92d12f7e8b 100644 --- a/machine/ontology.cpp +++ b/machine/ontology.cpp @@ -260,6 +260,8 @@ namespace rubinius { Randomizer::bootstrap(state); Encoding::bootstrap(state); FSEvent::bootstrap(state); + FDSet::bootstrap(state); + RIOStream::bootstrap(state); Logger::bootstrap(state); JIT::bootstrap(state); CodeDB::bootstrap(state); @@ -335,18 +337,13 @@ namespace rubinius { } void VM::initialize_platform_data(STATE) { - // HACK test hooking up IO + /* Hook up stub IO class so we can begin bootstrapping. STDIN/OUT/ERR will be + * replaced in core/zed.rb with the pure Ruby IO objects. + */ IO* in_io = IO::create(state, STDIN_FILENO); IO* out_io = IO::create(state, STDOUT_FILENO); IO* err_io = IO::create(state, STDERR_FILENO); - out_io->sync(state, cTrue); - err_io->sync(state, cTrue); - - in_io->force_read_only(state); - out_io->force_write_only(state); - err_io->force_write_only(state); - G(object)->set_const(state, "STDIN", in_io); G(object)->set_const(state, "STDOUT", out_io); G(object)->set_const(state, "STDERR", err_io); diff --git a/machine/test/test_bignum.hpp b/machine/test/test_bignum.hpp index fc987069aa..548d8ba6fd 100644 --- a/machine/test/test_bignum.hpp +++ b/machine/test/test_bignum.hpp @@ -648,7 +648,7 @@ class TestBignum : public CxxTest::TestSuite, public VMTest { fix = neg_one->left_shift(state, width_minus1); TS_ASSERT(kind_of(fix)); - TS_ASSERT_EQUALS((0UL - 1L) << (FIXNUM_WIDTH-1), fix->to_native()); + TS_ASSERT_EQUALS((native_int)((0UL - 1L) << (FIXNUM_WIDTH-1)), fix->to_native()); Integer* max_plus1 = one->left_shift(state, width); @@ -676,7 +676,7 @@ class TestBignum : public CxxTest::TestSuite, public VMTest { fix = neg_one->right_shift(state, neg_width_minus1); TS_ASSERT(kind_of(fix)); - TS_ASSERT_EQUALS((0UL - 1L) << (FIXNUM_WIDTH-1), fix->to_native()); + TS_ASSERT_EQUALS((native_int)((0UL - 1L) << (FIXNUM_WIDTH-1)), fix->to_native()); Integer* max_plus1 = one->right_shift(state, neg_width); @@ -704,7 +704,7 @@ class TestBignum : public CxxTest::TestSuite, public VMTest { fix = as(neg_two->pow(state, width_minus1)); TS_ASSERT(kind_of(fix)); - TS_ASSERT_EQUALS((0UL - 1L) << (FIXNUM_WIDTH-1), fix->to_native()); + TS_ASSERT_EQUALS((native_int)((0UL - 1L) << (FIXNUM_WIDTH-1)), fix->to_native()); Integer* max_plus1 = as(two->pow(state, width)); @@ -719,7 +719,7 @@ class TestBignum : public CxxTest::TestSuite, public VMTest { big = as(neg_two->pow(state, Fixnum::from(FIXNUM_WIDTH+1))); TS_ASSERT(kind_of(big)); - TS_ASSERT_EQUALS((0ULL - 1LL) << (FIXNUM_WIDTH+1), as(big)->to_long_long()); + TS_ASSERT_EQUALS((long long)((0ULL - 1LL) << (FIXNUM_WIDTH+1)), as(big)->to_long_long()); } void test_equal() { diff --git a/machine/test/test_io.hpp b/machine/test/test_io.hpp deleted file mode 100644 index 645c80545f..0000000000 --- a/machine/test/test_io.hpp +++ /dev/null @@ -1,137 +0,0 @@ -#include "machine/test/test.hpp" - -#include "builtin/io.hpp" -#include "builtin/string.hpp" - -#include -#include -#include - -class TestIO : public CxxTest::TestSuite, public VMTest { -public: - - IO* io; - int fd; - - void setUp() { - create(); - fd = make_io(); - io = IO::create(state, fd); - } - - void tearDown() { - remove_io(fd); - destroy(); - } - - int make_io() { - char templ[] = "/tmp/rubinius_TestIO.XXXXXX"; - int fd = mkstemp(templ); - - if(fd == -1) { - throw std::runtime_error(strerror(errno)); - } - - unlink(templ); - - return fd; - } - - void remove_io(int fd) { - close(fd); - } - - void test_create() { - TS_ASSERT_EQUALS(fd, io->descriptor()->to_native()); - TS_ASSERT_EQUALS(Fixnum::from(0), io->lineno()); - TS_ASSERT(io->eof()->false_p()); - int acc_mode = fcntl(io->to_fd(), F_GETFL); - TS_ASSERT(acc_mode >= 0); - TS_ASSERT_EQUALS(Fixnum::from(acc_mode), io->mode()); - TS_ASSERT(kind_of(io->ibuffer())); - } - - void test_allocate() { - io = IO::allocate(state, G(io)); - TS_ASSERT(io->descriptor()->nil_p()); - TS_ASSERT_EQUALS(Fixnum::from(0), io->lineno()); - TS_ASSERT(io->eof()->false_p()); - TS_ASSERT(io->mode()->nil_p()); - TS_ASSERT(kind_of(io->ibuffer())); - } - - void test_ensure_open() { - TS_ASSERT(io->ensure_open(state)->nil_p()); - io->descriptor(state, nil()); - TS_ASSERT_THROWS_ASSERT(io->ensure_open(state), const RubyException &e, - TS_ASSERT(Exception::io_error_p(state, e.exception))); - io->descriptor(state, Fixnum::from(-1)); - TS_ASSERT_THROWS_ASSERT(io->ensure_open(state), const RubyException &e, - TS_ASSERT(Exception::io_error_p(state, e.exception))); - } - - void test_set_mode() { - io->mode(state, nil()); - TS_ASSERT(io->mode()->nil_p()); - io->set_mode(state); - int acc_mode = fcntl(io->to_fd(), F_GETFL); - TS_ASSERT(acc_mode >= 0); - TS_ASSERT_EQUALS(Fixnum::from(acc_mode), io->mode()); - } - - void test_force_read_only() { - io->force_read_only(state); - TS_ASSERT((io->mode()->to_native() & O_ACCMODE) == O_RDONLY); - } - - void test_force_write_only() { - io->force_write_only(state); - TS_ASSERT((io->mode()->to_native() & O_ACCMODE) == O_WRONLY); - } - - void test_write() { - char buf[4]; - - String* s = String::create(state, "abdc"); - io->write(state, s); - - lseek(fd, 0, SEEK_SET); - TS_ASSERT_EQUALS(::read(fd, buf, 4U), 4); - TS_ASSERT_SAME_DATA(buf, "abdc", 4); - } - - void test_query() { - TS_ASSERT_EQUALS(cNil, io->query(state, state->symbol("unknown"))); - - io->descriptor(state, Fixnum::from(-1)); - TS_ASSERT_THROWS_ASSERT(io->query(state, state->symbol("tty?")), - const RubyException &e, - TS_ASSERT(Exception::io_error_p(state, e.exception))); - } - - void test_query_tty() { - Symbol* tty_p = state->symbol("tty?"); - TS_ASSERT_EQUALS(cFalse, io->query(state, tty_p)); - } - - void test_query_ttyname() { - IO* io = as(G(object)->get_const(state, "STDOUT")); - if(isatty(io->to_fd())) { - String* tty = try_as(io->query(state, state->symbol("ttyname"))); - - // TODO: /dev/ttyxxx won't be portable to e.g. windoze - TS_ASSERT(tty); - } - } - - void test_create_buffer() { - IOBuffer* buf = IOBuffer::create(state, 10); - Fixnum* zero = Fixnum::from(0); - - TS_ASSERT_EQUALS(zero, buf->start()); - TS_ASSERT_EQUALS(zero, buf->used()); - TS_ASSERT_EQUALS(Fixnum::from(10), buf->total()); - TS_ASSERT_EQUALS(10U, buf->left()); - TS_ASSERT_EQUALS(cFalse, buf->eof()); - } -}; diff --git a/rakelib/platform.rake b/rakelib/platform.rake index 064d5ab5b5..b02972a6a9 100644 --- a/rakelib/platform.rake +++ b/rakelib/platform.rake @@ -59,12 +59,15 @@ file 'runtime/platform.conf' => deps do |task| s.field :d_name, :char_array end.write_config(f) - Rubinius::FFI::Generators::Structures.new 'timeval' do |s| + struct = Rubinius::FFI::Generators::Structures.new 'timeval' do |s| s.include "sys/time.h" s.name 'struct timeval' s.field :tv_sec, :time_t s.field :tv_usec, :suseconds_t - end.write_config(f) + end + + struct.write_config(f) + struct.write_class(f) Rubinius::FFI::Generators::Structures.new 'sockaddr_in' do |s| if BUILD_CONFIG[:windows] @@ -236,6 +239,7 @@ file 'runtime/platform.conf' => deps do |task| O_APPEND O_NONBLOCK O_SYNC + O_CLOEXEC S_IRUSR S_IWUSR S_IXUSR @@ -273,6 +277,32 @@ file 'runtime/platform.conf' => deps do |task| io_constants.each { |c| cg.const c } end.write_constants(f) + + Rubinius::FFI::Generators::Constants.new 'rbx.platform.select' do |cg| + cg.include 'sys/select.h' + + select_constants = %w[ + FD_SETSIZE + ] + + select_constants.each { |c| cg.const c } + end.write_constants(f) + + # Not available on all platforms. Try to load these constants anyway. + Rubinius::FFI::Generators::Constants.new 'rbx.platform.advise' do |cg| + cg.include 'fcntl.h' + + advise_constants = %w[ + POSIX_FADV_NORMAL + POSIX_FADV_SEQUENTIAL + POSIX_FADV_RANDOM + POSIX_FADV_WILLNEED + POSIX_FADV_DONTNEED + POSIX_FADV_NOREUSE + ] + + advise_constants.each { |c| cg.const c } + end.write_constants(f) # Only constants needed by core are added here Rubinius::FFI::Generators::Constants.new 'rbx.platform.fcntl' do |cg| diff --git a/spec/core/ffi/memorypointer/new_spec.rb b/spec/core/ffi/memorypointer/new_spec.rb index 9511a1b951..ccc9c09e38 100644 --- a/spec/core/ffi/memorypointer/new_spec.rb +++ b/spec/core/ffi/memorypointer/new_spec.rb @@ -3,4 +3,12 @@ describe "FFI::MemoryPointer.new" do it "needs to be reviewed for spec completeness" + + it "returns null when the byte size is negative" do + FFI::MemoryPointer.new(-1).should == FFI::Pointer::NULL + end + + it "returns null when the count is negative" do + FFI::MemoryPointer.new(:int, -3).should == FFI::Pointer::NULL + end end diff --git a/spec/core/io/buffer_spec.rb b/spec/core/io/buffer_spec.rb index 36466e1324..86b584c785 100644 --- a/spec/core/io/buffer_spec.rb +++ b/spec/core/io/buffer_spec.rb @@ -24,17 +24,4 @@ @io.read(4) @io.read(4).should == @contents.slice(8, 2) end - - it "doesn't confuse IO.select" do - read, write = @read, @write - - write << "data" - - IO.select([read],nil,nil,3).should == [[read],[],[]] - - read.getc - read.buffer_empty?.should be_false - - IO.select([read],nil,nil,3).should == [[read],[],[]] - end end diff --git a/spec/rbx.2.2.mspec b/spec/rbx.2.3.mspec similarity index 100% rename from spec/rbx.2.2.mspec rename to spec/rbx.2.3.mspec diff --git a/spec/ruby/core/io/advise_spec.rb b/spec/ruby/core/io/advise_spec.rb index 785133cf65..28e5d762bd 100644 --- a/spec/ruby/core/io/advise_spec.rb +++ b/spec/ruby/core/io/advise_spec.rb @@ -2,79 +2,83 @@ require File.expand_path('../../../spec_helper', __FILE__) require File.expand_path('../fixtures/classes', __FILE__) -describe "IO#advise" do - before :each do - @kcode, $KCODE = $KCODE, "utf-8" - @io = IOSpecs.io_fixture "lines.txt" - end - - after :each do - @io.close unless @io.closed? - $KCODE = @kcode - end - - it "raises a TypeError if advise is not a Symbol" do - lambda { - @io.advise("normal") - }.should raise_error(TypeError) - end - - it "raises a TypeError if offsert cannot be coerced to an Integer" do - lambda { - @io.advise(:normal, "wat") - }.should raise_error(TypeError) - end - - it "raises a TypeError if len cannot be coerced to an Integer" do - lambda { - @io.advise(:normal, 0, "wat") - }.should raise_error(TypeError) - end - - it "raises a RangeError if offset is too big" do - lambda { - @io.advise(:normal, 10 ** 32) - }.should raise_error(RangeError) - end - - it "raises a RangeError if len is too big" do - lambda { - @io.advise(:normal, 0, 10 ** 32) - }.should raise_error(RangeError) - end +with_feature :posix_fadvise do - it "raises a NotImplementedError if advise is not recognized" do - lambda{ - @io.advise(:foo) - }.should raise_error(NotImplementedError) + describe "IO#advise" do + before :each do + @kcode, $KCODE = $KCODE, "utf-8" + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + $KCODE = @kcode + end + + it "raises a TypeError if advise is not a Symbol" do + lambda { + @io.advise("normal") + }.should raise_error(TypeError) + end + + it "raises a TypeError if offsert cannot be coerced to an Integer" do + lambda { + @io.advise(:normal, "wat") + }.should raise_error(TypeError) + end + + it "raises a TypeError if len cannot be coerced to an Integer" do + lambda { + @io.advise(:normal, 0, "wat") + }.should raise_error(TypeError) + end + + it "raises a RangeError if offset is too big" do + lambda { + @io.advise(:normal, 10 ** 32) + }.should raise_error(RangeError) + end + + it "raises a RangeError if len is too big" do + lambda { + @io.advise(:normal, 0, 10 ** 32) + }.should raise_error(RangeError) + end + + it "raises a NotImplementedError if advise is not recognized" do + lambda{ + @io.advise(:foo) + }.should raise_error(NotImplementedError) + end + + it "supports the normal advice type" do + @io.advise(:normal).should be_nil + end + + it "supports the sequential advice type" do + @io.advise(:sequential).should be_nil + end + + it "supports the random advice type" do + @io.advise(:random).should be_nil + end + + it "supports the dontneed advice type" do + @io.advise(:dontneed).should be_nil + end + + it "supports the noreuse advice type" do + @io.advise(:noreuse).should be_nil + end + + it "supports the willneed advice type" do + @io.advise(:willneed).should be_nil + end + + it "raises an IOError if the stream is closed" do + @io.close + lambda { @io.advise(:normal) }.should raise_error(IOError) + end end - it "supports the normal advice type" do - @io.advise(:normal).should be_nil - end - - it "supports the sequential advice type" do - @io.advise(:sequential).should be_nil - end - - it "supports the random advice type" do - @io.advise(:random).should be_nil - end - - it "supports the dontneed advice type" do - @io.advise(:dontneed).should be_nil - end - - it "supports the noreuse advice type" do - @io.advise(:noreuse).should be_nil - end - - it "supports the willneed advice type" do - @io.advise(:willneed).should be_nil - end - - it "raises an IOError if the stream is closed" do - @io.close - lambda { @io.advise(:normal) }.should raise_error(IOError) - end -end +end \ No newline at end of file diff --git a/spec/ruby/core/io/each_line_spec.rb b/spec/ruby/core/io/each_line_spec.rb index d4d8af7902..9c7d573fb0 100644 --- a/spec/ruby/core/io/each_line_spec.rb +++ b/spec/ruby/core/io/each_line_spec.rb @@ -9,3 +9,17 @@ describe "IO#each_line" do it_behaves_like :io_each_default_separator, :each_line end + +describe "IO#each_line" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close + end + + it "raises ArgumentError when given a limit of 0" do + lambda { @io.each_line(0) }.should raise_error(ArgumentError) + end +end \ No newline at end of file diff --git a/spec/ruby/core/io/gets_spec.rb b/spec/ruby/core/io/gets_spec.rb index 77911f1123..c9762ecf1b 100644 --- a/spec/ruby/core/io/gets_spec.rb +++ b/spec/ruby/core/io/gets_spec.rb @@ -228,11 +228,11 @@ end it "reads limit bytes and extra bytes when limit is reached not at character boundary" do - [@io.gets(1), @io.gets(1)].should == ["朝", "日"] + [@io.gets(nil, 1), @io.gets(nil, 1)].should == ["朝", "日"] end it "read limit bytes and extra bytes with maximum of 16" do - @io.gets(7).should == "朝日\xE3" + "\x81\xE3" * 8 + @io.gets(nil, 7).should == "朝日\xE3" + "\x81\xE3" * 8 end after :each do diff --git a/spec/ruby/core/io/popen_spec.rb b/spec/ruby/core/io/popen_spec.rb index 10959cf030..b72d3c7093 100644 --- a/spec/ruby/core/io/popen_spec.rb +++ b/spec/ruby/core/io/popen_spec.rb @@ -21,6 +21,13 @@ lambda { @io.write('foo') }.should raise_error(IOError) end + it "sees an infinitely looping subprocess exit when read pipe is closed" do + io = IO.popen "#{RUBY_EXE} -e 'r = loop{puts \"y\"; 0} rescue 1; exit r'", 'r' + io.close + + $?.exitstatus.should_not == 0 + end + platform_is_not :windows do before :each do @fname = tmp("IO_popen_spec") diff --git a/spec/ruby/core/io/pos_spec.rb b/spec/ruby/core/io/pos_spec.rb index 300925a284..e895474581 100644 --- a/spec/ruby/core/io/pos_spec.rb +++ b/spec/ruby/core/io/pos_spec.rb @@ -6,6 +6,27 @@ it_behaves_like :io_pos, :pos end +describe "IO#pos" do + before :each do + @fname = tmp('pos-text.txt') + File.open @fname, 'w' do |f| f.write "123" end + end + + after :each do + rm_r @fname + end + + it "allows a negative value when calling #ungetc at beginning of stream" do + File.open @fname do |f| + f.pos.should == 0 + f.ungetbyte(97) + f.pos.should == -1 + f.getbyte + f.pos.should == 0 + end + end +end + describe "IO#pos=" do it_behaves_like :io_set_pos, :pos= end diff --git a/spec/ruby/core/io/read_spec.rb b/spec/ruby/core/io/read_spec.rb index db40ffc155..a63c1a6e08 100644 --- a/spec/ruby/core/io/read_spec.rb +++ b/spec/ruby/core/io/read_spec.rb @@ -278,6 +278,23 @@ it "raises IOError on closed stream" do lambda { IOSpecs.closed_io.read }.should raise_error(IOError) end + + it "raises IOError when stream is closed by another thread" do + r, w = IO.pipe + t = Thread.new do + begin + r.read(1) + rescue => e + e + end + end + + sleep(0.1) until t.stop? + r.close + t.join + t.value.should be_kind_of(IOError) + w.close + end end platform_is :windows do @@ -544,7 +561,6 @@ describe "IO#read with large data" do before :each do - # TODO: what is the significance of this mystery math? @data_size = 8096 * 2 + 1024 @data = "*" * @data_size diff --git a/spec/ruby/core/io/readlines_spec.rb b/spec/ruby/core/io/readlines_spec.rb index e0cbf8dbc4..06d7c8c72b 100644 --- a/spec/ruby/core/io/readlines_spec.rb +++ b/spec/ruby/core/io/readlines_spec.rb @@ -111,9 +111,9 @@ it "gets data from a fork when passed -" do lines = IO.readlines("|-") - if lines # parent + if !lines.empty? # parent, #readlines always returns an array lines.should == ["hello\n", "from a fork\n"] - else + elsif lines.empty? puts "hello" puts "from a fork" exit! diff --git a/spec/ruby/core/io/reopen_spec.rb b/spec/ruby/core/io/reopen_spec.rb index 2ec9bbb084..aa41d33309 100644 --- a/spec/ruby/core/io/reopen_spec.rb +++ b/spec/ruby/core/io/reopen_spec.rb @@ -21,12 +21,14 @@ it "calls #to_io to convert an object" do obj = mock("io") obj.should_receive(:to_io).and_return(@other_io) + obj.stub!(:pos).and_return(0) @io.reopen obj end it "changes the class of the instance to the class of the object returned by #to_io" do obj = mock("io") obj.should_receive(:to_io).and_return(@other_io) + obj.stub!(:pos).and_return(0) @io.reopen(obj).should be_an_instance_of(File) end @@ -191,6 +193,34 @@ end end +describe "IO#reopen with an IO at EOF" do + before :each do + @name = tmp("io_reopen.txt") + touch(@name) { |f| f.puts "a line" } + @other_name = tmp("io_reopen_other.txt") + touch(@other_name) do |f| + f.puts "Line 1" + f.puts "Line 2" + end + + @io = new_io @name, "r" + @other_io = new_io @other_name, "r" + @io.read + end + + after :each do + @io.close unless @io.closed? + @other_io.close unless @other_io.closed? + rm_r @name, @other_name + end + + it "resets the EOF status to false" do + @io.eof?.should be_true + @io.reopen @other_io + @io.eof?.should be_false + end +end + describe "IO#reopen with an IO" do before :each do @name = tmp("io_reopen.txt") diff --git a/spec/ruby/core/io/shared/write.rb b/spec/ruby/core/io/shared/write.rb index ef00252612..293bbff2d7 100644 --- a/spec/ruby/core/io/shared/write.rb +++ b/spec/ruby/core/io/shared/write.rb @@ -69,4 +69,13 @@ lambda { IOSpecs.closed_io.send(@method, "hello") }.should raise_error(IOError) end + it "does not block when descriptor is set to nonblocking mode" do + r, w = IO.pipe + flags = Rubinius::FFI::Platform::POSIX.fcntl(w.fileno, IO::F_GETFL, 0) + Rubinius::FFI::Platform::POSIX.fcntl(w.fileno, IO::F_SETFL, flags | IO::O_NONBLOCK) + + written = w.send(@method, 'a' * 1_000_000) # pick a number that will exceed buffer + written.should > 0 + end + end diff --git a/spec/ruby/core/io/shared/z_spec.rb b/spec/ruby/core/io/shared/z_spec.rb new file mode 100644 index 0000000000..3b5e3d307f --- /dev/null +++ b/spec/ruby/core/io/shared/z_spec.rb @@ -0,0 +1,237 @@ +#describe :io_readlines, :shared => true do +# it "raises TypeError if the first parameter is nil" do +# lambda { IO.send(@method, nil, &@object) }.should raise_error(TypeError) +# end +# +# it "raises an Errno::ENOENT if the file does not exist" do +# name = tmp("nonexistent.txt") +# lambda { IO.send(@method, name, &@object) }.should raise_error(Errno::ENOENT) +# end +# +# it "yields a single string with entire content when the separator is nil" do +# result = IO.send(@method, @name, nil, &@object) +# (result ? result : ScratchPad.recorded).should == [IO.read(@name)] +# end +# +# it "yields a sequence of paragraphs when the separator is an empty string" do +# result = IO.send(@method, @name, "", &@object) +# (result ? result : ScratchPad.recorded).should == IOSpecs.lines_empty_separator +# end +#end + +#describe :io_readlines_options_18, :shared => true do +# it "does not change $_" do +# $_ = "test" +# IO.send(@method, @name, &@object) +# $_.should == "test" +# end +# +# describe "when passed name" do +# it "calls #to_str to convert the name" do +# name = mock("io readlines name") +# name.should_receive(:to_str).and_return(@name) +# result = IO.send(@method, name, &@object) +# (result ? result : ScratchPad.recorded).should == IOSpecs.lines +# end +# end +# +# describe "when passed name, separator" do +# it "calls #to_str to convert the name" do +# name = mock("io readlines name") +# name.should_receive(:to_str).and_return(@name) +# result = IO.send(@method, name, &@object) +# (result ? result : ScratchPad.recorded).should == IOSpecs.lines +# end +# +# it "calls #to_str to convert the separator" do +# sep = mock("io readlines separator") +# sep.should_receive(:to_str).at_least(1).and_return(" ") +# result = IO.send(@method, @name, sep, &@object) +# (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator +# end +# end +#end + +describe :io_readlines_options_19, :shared => true do + before :each do + @filename = tmp("io readlines options") + end + + after :each do + rm_r @filename + end + +# describe "when passed name" do +# it "calls #to_path to convert the name" do +# name = mock("io name to_path") +# name.should_receive(:to_path).and_return(@name) +# IO.send(@method, name, &@object) +# end +# +# it "defaults to $/ as the separator" do +# result = IO.send(@method, @name, &@object) +# (result ? result : ScratchPad.recorded).should == IOSpecs.lines +# end +# end + + describe "when passed name, object" do +# it "calls #to_str to convert the object to a separator" do +# sep = mock("io readlines separator") +# sep.should_receive(:to_str).at_least(1).and_return(" ") +# result = IO.send(@method, @name, sep, &@object) +# (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator +# end + + describe "when the object is a Fixnum" do + before :each do + @sep = $/ + end + + after :each do + $/ = @sep + end + +# it "defaults to $/ as the separator" do +# $/ = " " +# result = IO.send(@method, @name, 10, &@object) +# (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit +# end + + it "uses the object as a limit if it is a Fixnum" do + result = IO.send(@method, @name, 10, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_limit + end + end + +# describe "when the object is a String" do +# it "uses the value as the separator" do +# result = IO.send(@method, @name, " ", &@object) +# (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator +# end +# +# it "accepts non-ASCII data as separator" do +# result = IO.send(@method, @name, "\303\250".force_encoding("utf-8"), &@object) +# (result ? result : ScratchPad.recorded).should == IOSpecs.lines_arbitrary_separator +# end +# end +# +# describe "when the object is a Hash" do +# it "uses the value as the options hash" do +# result = IO.send(@method, @name, :mode => "r", &@object) +# (result ? result : ScratchPad.recorded).should == IOSpecs.lines +# end +# end + end + +# describe "when passed name, object, object" do +# describe "when the first object is a Fixnum" do +# it "uses the second object as an options Hash" do +# lambda do +# IO.send(@method, @filename, 10, :mode => "w", &@object) +# end.should raise_error(IOError) +# end +# +# it "calls #to_hash to convert the second object to a Hash" do +# options = mock("io readlines options Hash") +# options.should_receive(:to_hash).and_return({ :mode => "w" }) +# lambda do +# IO.send(@method, @filename, 10, options, &@object) +# end.should raise_error(IOError) +# end +# end +# +# describe "when the first object is a String" do +# it "uses the second object as a limit if it is a Fixnum" do +# result = IO.send(@method, @name, " ", 10, &@object) +# (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit +# end +# +# it "calls #to_int to convert the second object" do +# limit = mock("io readlines limit") +# limit.should_receive(:to_int).at_least(1).and_return(10) +# result = IO.send(@method, @name, " ", limit, &@object) +# (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit +# end +# +# it "uses the second object as an options Hash" do +# lambda do +# IO.send(@method, @filename, " ", :mode => "w", &@object) +# end.should raise_error(IOError) +# end +# +# it "calls #to_hash to convert the second object to a Hash" do +# options = mock("io readlines options Hash") +# options.should_receive(:to_hash).and_return({ :mode => "w" }) +# lambda do +# IO.send(@method, @filename, " ", options, &@object) +# end.should raise_error(IOError) +# end +# end +# +# describe "when the first object is not a String or Fixnum" do +# it "calls #to_str to convert the object to a String" do +# sep = mock("io readlines separator") +# sep.should_receive(:to_str).at_least(1).and_return(" ") +# result = IO.send(@method, @name, sep, 10, :mode => "r", &@object) +# (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit +# end +# +# it "uses the second object as a limit if it is a Fixnum" do +# result = IO.send(@method, @name, " ", 10, :mode => "r", &@object) +# (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit +# end +# +# it "calls #to_int to convert the second object" do +# limit = mock("io readlines limit") +# limit.should_receive(:to_int).at_least(1).and_return(10) +# result = IO.send(@method, @name, " ", limit, &@object) +# (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit +# end +# +# it "uses the second object as an options Hash" do +# lambda do +# IO.send(@method, @filename, " ", :mode => "w", &@object) +# end.should raise_error(IOError) +# end +# +# it "calls #to_hash to convert the second object to a Hash" do +# options = mock("io readlines options Hash") +# options.should_receive(:to_hash).and_return({ :mode => "w" }) +# lambda do +# IO.send(@method, @filename, " ", options, &@object) +# end.should raise_error(IOError) +# end +# end +# end + +# describe "when passed name, separator, limit, options" do +# it "calls #to_path to convert the name object" do +# name = mock("io name to_path") +# name.should_receive(:to_path).and_return(@name) +# result = IO.send(@method, name, " ", 10, :mode => "r", &@object) +# (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit +# end +# +# it "calls #to_str to convert the separator object" do +# sep = mock("io readlines separator") +# sep.should_receive(:to_str).at_least(1).and_return(" ") +# result = IO.send(@method, @name, sep, 10, :mode => "r", &@object) +# (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit +# end +# +# it "calls #to_int to convert the limit argument" do +# limit = mock("io readlines limit") +# limit.should_receive(:to_int).at_least(1).and_return(10) +# result = IO.send(@method, @name, " ", limit, :mode => "r", &@object) +# (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit +# end +# +# it "calls #to_hash to convert the options object" do +# options = mock("io readlines options Hash") +# options.should_receive(:to_hash).and_return({ :mode => "w" }) +# lambda do +# IO.send(@method, @filename, " ", 10, options, &@object) +# end.should raise_error(IOError) +# end +# end +end diff --git a/spec/ruby/core/io/tty_spec.rb b/spec/ruby/core/io/tty_spec.rb index 3c1449b030..99a2ecab22 100644 --- a/spec/ruby/core/io/tty_spec.rb +++ b/spec/ruby/core/io/tty_spec.rb @@ -4,3 +4,10 @@ describe "IO#tty?" do it_behaves_like :io_tty, :tty? end + +describe "IO#ttyname" do + it "returns the name of the STDOUT tty" do + io = $stdout + io.ttyname.should =~ Regexp.new('/dev/') + end +end diff --git a/spec/ruby/core/io/ungetc_spec.rb b/spec/ruby/core/io/ungetc_spec.rb index a4a2162d5a..ce4cc9d346 100644 --- a/spec/ruby/core/io/ungetc_spec.rb +++ b/spec/ruby/core/io/ungetc_spec.rb @@ -81,18 +81,6 @@ @io.pos.should == pos - 1 end - # TODO: file MRI bug - # Another specified behavior that MRI doesn't follow: - # "Has no effect with unbuffered reads (such as IO#sysread)." - # - #it "has no effect with unbuffered reads" do - # length = File.size(@io_name) - # content = @io.sysread(length) - # @io.rewind - # @io.ungetc(100) - # @io.sysread(length).should == content - #end - it "makes subsequent unbuffered operations to raise IOError" do @io.getc @io.ungetc(100) diff --git a/spec/ruby/core/io/write_spec.rb b/spec/ruby/core/io/write_spec.rb index 1ce2229256..9131c5093a 100644 --- a/spec/ruby/core/io/write_spec.rb +++ b/spec/ruby/core/io/write_spec.rb @@ -20,9 +20,7 @@ rm_r @filename end - # TODO: impl detail? discuss this with matz. This spec is useless. - rdavis - # I agree. I've marked it not compliant on macruby, as we don't buffer input. -pthomson - not_compliant_on :macruby do + not_compliant_on :macruby, :rubinius do it "writes all of the string's bytes but buffers them" do written = @file.write("abcde") written.should == 5 diff --git a/spec/ruby/core/objectspace/define_finalizer_spec.rb b/spec/ruby/core/objectspace/define_finalizer_spec.rb index be9ae21e0c..9c92c2b364 100644 --- a/spec/ruby/core/objectspace/define_finalizer_spec.rb +++ b/spec/ruby/core/objectspace/define_finalizer_spec.rb @@ -61,6 +61,15 @@ def handler.call(obj) end # see [ruby-core:24095] with_feature :fork do + before :each do + @fname = tmp("finalizer_test.txt") + @contents = "finalized" + end + + after :each do + rm_r @fname + end + it "calls finalizer on process termination" do rd, wr = IO.pipe @@ -85,7 +94,7 @@ def handler.call(obj) end pid = Kernel::fork do rd.close obj = "Test" - handler = Proc.new { wr.write "finalized"; wr.close } + handler = Proc.new { wr.write(@contents); wr.close } ObjectSpace.define_finalizer(obj, handler) exit 0 end @@ -93,7 +102,7 @@ def handler.call(obj) end wr.close Process.waitpid pid - rd.read.should == "finalized" + rd.read.should == @contents rd.close end diff --git a/spec/ruby/core/string/encode_spec.rb b/spec/ruby/core/string/encode_spec.rb index 57d8b752ea..5699d80b51 100644 --- a/spec/ruby/core/string/encode_spec.rb +++ b/spec/ruby/core/string/encode_spec.rb @@ -60,6 +60,20 @@ "\rfoo".encode(:universal_newline => true).should == "\nfoo" end + + it "crlf_newline replaces lf with crlf" do + #"\r\nfoo".encode(crlf_newline: true).should == "\r\r\nfoo" + + "\nfoo".encode(crlf_newline: true).should == "\r\nfoo" + + "\rfoo".encode(crlf_newline: true).should == "\rfoo" + end + + it "cr_newline replaces cr with lf" do + #"\r\nfoo".encode(cr_newline: true).should == "\r\rfoo" + + "\nfoo".encode(cr_newline: true).should == "\rfoo" + end end describe "when passed to, from" do