require "crystal/system/file_descriptor" require "crystal/fd_lock" # An `IO` over a file descriptor. class IO::FileDescriptor < IO include Crystal::System::FileDescriptor include IO::Buffered @volatile_fd : Atomic(Handle) @fd_lock = Crystal::FdLock.new # Returns the raw file-descriptor handle. Its type is platform-specific. # # The file-descriptor handle has been configured for the IO system # requirements. If it must be in a specific mode or have a specific set of # flags set, then they must be applied, even when when it feels redundant, # because even the same target isn't guaranteed to have the same requirements # at runtime. def fd : Handle @volatile_fd.get end # Whether or not to close the file descriptor when this object is finalized. # Disabling this is useful in order to create an IO wrapper over a file # descriptor returned from a C API that keeps ownership of the descriptor. Do # note that, if the fd is closed by its owner at any point, any IO operations # will then fail. property? close_on_finalize : Bool # The time to wait when reading before raising an `IO::TimeoutError`. property read_timeout : Time::Span? # Sets the number of seconds to wait when reading before raising an `IO::TimeoutError`. @[Deprecated("Use `#read_timeout=(Time::Span?)` instead.")] def read_timeout=(read_timeout : Number) : Number self.read_timeout = read_timeout.seconds read_timeout end # Sets the time to wait when writing before raising an `IO::TimeoutError`. property write_timeout : Time::Span? # Sets the number of seconds to wait when writing before raising an `IO::TimeoutError`. @[Deprecated("Use `#write_timeout=(Time::Span?)` instead.")] def write_timeout=(write_timeout : Number) : Number self.write_timeout = write_timeout.seconds write_timeout end {% begin %} # Creates an IO::FileDescriptor from an existing system file descriptor or # handle. # # This adopts *fd* into the IO system that will reconfigure it as per the # event loop runtime requirements. # # NOTE: On Windows, the handle should have been created with # `FILE_FLAG_OVERLAPPED`. def self.new(fd : Handle, {% if compare_versions(Crystal::VERSION, "1.5.0") >= 0 %} @[Deprecated("Use IO::FileDescriptor.set_blocking instead.")] {% end %} blocking = nil, *, close_on_finalize = true) file_descriptor = new(handle: fd, close_on_finalize: close_on_finalize) file_descriptor.system_blocking_init(blocking) unless file_descriptor.closed? file_descriptor end {% end %} # :nodoc: # # Internal constructor to wrap a system *handle*. The *blocking* arg is purely # informational. def initialize(*, handle : Handle, @close_on_finalize = true, blocking = nil) @volatile_fd = Atomic.new(handle) @closed = true # This is necessary so we can reference `self` in `system_closed?` (in case of an exception) @closed = system_closed? {% if flag?(:win32) %} @system_blocking = !!blocking {% end %} end # :nodoc: def self.from_stdio(fd : Handle) : self Crystal::System::FileDescriptor.from_stdio(fd) end # Returns whether I/O operations on this file descriptor block the current # thread. If false, operations might opt to suspend the current fiber instead. # # This might be different from the internal file descriptor. For example, when # `STDIN` is a terminal on Windows, this returns `false` since the underlying # blocking reads are done on a completely separate thread. @[Deprecated("Use IO::FileDescriptor.get_blocking instead.")] def blocking : Bool emulated = emulated_blocking? return emulated unless emulated.nil? system_blocking? end # Changes the file descriptor's mode to blocking (true) or non blocking # (false). # # WARNING: The file descriptor has been configured to behave correctly with # the event loop runtime requirements. Changing the blocking mode can cause # the event loop to misbehave, for example block the entire program when a # fiber tries to read from this file descriptor. @[Deprecated("Use IO::FileDescriptor.set_blocking instead.")] def blocking=(value : Bool) : Nil @fd_lock.reference { self.system_blocking = value } end # Returns whether the blocking mode of *fd* is blocking (true) or non blocking # (false). # # NOTE: Only implemented on UNIX targets. Raises on Windows. def self.get_blocking(fd : Handle) : Bool Crystal::System::FileDescriptor.get_blocking(fd) end # Changes the blocking mode of *fd* to be blocking (true) or non blocking # (false). # # NOTE: Only implemented on UNIX targets. Raises on Windows. def self.set_blocking(fd : Handle, value : Bool) Crystal::System::FileDescriptor.set_blocking(fd, value) end def close_on_exec? : Bool system_close_on_exec? end def close_on_exec=(value : Bool) : Bool @fd_lock.reference { self.system_close_on_exec = value } end def self.fcntl(fd, cmd, arg = 0) Crystal::System::FileDescriptor.fcntl(fd, cmd, arg) end def fcntl(cmd : Int, arg : Int = 0) : Int @fd_lock.reference { system_fcntl(cmd, arg) } end # Returns a `File::Info` object for this file descriptor, or raises # `IO::Error` in case of an error. # # Certain fields like the file size may not be updated until an explicit # flush. # # ``` # File.write("testfile", "abc") # # file = File.new("testfile", "a") # file.info.size # => 3 # file << "defgh" # file.info.size # => 3 # file.flush # file.info.size # => 8 # ``` # # Use `File.info` if the file is not open and a path to the file is available. def info : File::Info @fd_lock.reference { system_info } end # Seeks to a given *offset* (in bytes) according to the *whence* argument. # Returns `self`. # # ``` # File.write("testfile", "abc") # # file = File.new("testfile") # file.gets(3) # => "abc" # file.seek(1, IO::Seek::Set) # file.gets(2) # => "bc" # file.seek(-1, IO::Seek::Current) # file.gets(1) # => "c" # ``` def seek(offset, whence : Seek = Seek::Set) check_open flush offset -= @in_buffer_rem.size if whence.current? @fd_lock.reference { system_seek(offset, whence) } @in_buffer_rem = Bytes.empty self end # Same as `seek` but yields to the block after seeking and eventually seeks # back to the original position when the block returns. def seek(offset, whence : Seek = Seek::Set, &) original_pos = tell begin seek(offset, whence) yield ensure seek(original_pos) end end # Returns the current position (in bytes) in this `IO`. # # ``` # File.write("testfile", "hello") # # file = File.new("testfile") # file.pos # => 0 # file.gets(2) # => "he" # file.pos # => 2 # ``` protected def unbuffered_pos : Int64 check_open system_pos end # Sets the current position (in bytes) in this `IO`. # # ``` # File.write("testfile", "hello") # # file = File.new("testfile") # file.pos = 3 # file.gets_to_end # => "lo" # ``` def pos=(value) seek value value end # Flushes all data written to this File Descriptor to the disk device so that # all changed information can be retrieved even if the system # crashes or is rebooted. The call blocks until the device reports that # the transfer has completed. # To reduce disk activity the *flush_metadata* parameter can be set to false, # then the syscall *fdatasync* will be used and only data required for # subsequent data retrieval is flushed. Metadata such as modified time and # access time is not written. # # NOTE: Metadata is flushed even when *flush_metadata* is false on Windows # and DragonFly BSD. def fsync(flush_metadata : Bool = true) : Nil flush @fd_lock.reference { system_fsync(flush_metadata) } end # TODO: use fcntl/lockf instead of flock (which doesn't lock over NFS) def flock_shared(blocking = true, &) flock_shared blocking begin yield ensure flock_unlock end end # Places a shared advisory lock. More than one process may hold a shared lock for a given file descriptor at a given time. # `IO::Error` is raised if *blocking* is set to `false` and an existing exclusive lock is set. def flock_shared(blocking : Bool = true) : Nil system_flock_shared(blocking) end def flock_exclusive(blocking = true, &) flock_exclusive blocking begin yield ensure flock_unlock end end # Places an exclusive advisory lock. Only one process may hold an exclusive lock for a given file descriptor at a given time. # `IO::Error` is raised if *blocking* is set to `false` and any existing lock is set. def flock_exclusive(blocking : Bool = true) : Nil system_flock_exclusive(blocking) end # Removes an existing advisory lock held by this process. def flock_unlock : Nil system_flock_unlock end # Finalizes the file descriptor resource. # # This involves releasing the handle to the operating system, i.e. closing it. # It does *not* implicitly call `#flush`, so data waiting in the buffer may be # lost. # It's recommended to always close the file descriptor explicitly via `#close` # (or implicitly using the `.open` constructor). # # Resource release can be disabled with `close_on_finalize = false`. # # This method is a no-op if the file descriptor has already been closed. def finalize : Nil return if closed? || !close_on_finalize? Crystal::EventLoop.remove(self) file_descriptor_close { } # ignore error end def closed? : Bool @closed end def tty? : Bool system_tty? end def reopen(other : IO::FileDescriptor) : IO::FileDescriptor return other if self.fd == other.fd other.@fd_lock.reference do @fd_lock.reference { system_reopen(other) } end other end def inspect(io : IO) : Nil io << "#