diff --git a/src/crystal/system/file.cr b/src/crystal/system/file.cr new file mode 100644 index 000000000000..03f1c7a376f2 --- /dev/null +++ b/src/crystal/system/file.cr @@ -0,0 +1 @@ +require "./unix/file" diff --git a/src/crystal/system/file_descriptor.cr b/src/crystal/system/file_descriptor.cr new file mode 100644 index 000000000000..ad9428863bc3 --- /dev/null +++ b/src/crystal/system/file_descriptor.cr @@ -0,0 +1 @@ +require "./unix/file_descriptor" diff --git a/src/crystal/system/unix/file.cr b/src/crystal/system/unix/file.cr new file mode 100644 index 000000000000..7a96485e373f --- /dev/null +++ b/src/crystal/system/unix/file.cr @@ -0,0 +1,230 @@ +require "c/sys/file" + +# :nodoc: +module Crystal::System::File + def self.open(filename, mode, perm) + oflag = open_flag(mode) | LibC::O_CLOEXEC + + fd = LibC.open(filename.check_no_null_byte, oflag, perm) + if fd < 0 + raise Errno.new("Error opening file '#{filename}' with mode '#{mode}'") + end + fd + end + + private def self.open_flag(mode) + if mode.size == 0 + raise "Invalid access mode #{mode}" + end + + m = 0 + o = 0 + case mode[0] + when 'r' + m = LibC::O_RDONLY + when 'w' + m = LibC::O_WRONLY + o = LibC::O_CREAT | LibC::O_TRUNC + when 'a' + m = LibC::O_WRONLY + o = LibC::O_CREAT | LibC::O_APPEND + else + raise "Invalid access mode #{mode}" + end + + case mode.size + when 1 + # Nothing + when 2 + case mode[1] + when '+' + m = LibC::O_RDWR + when 'b' + # Nothing + else + raise "Invalid access mode #{mode}" + end + else + raise "Invalid access mode #{mode}" + end + + oflag = m | o + end + + def self.mktemp(name, extension) + tmpdir = tempdir + ::File::SEPARATOR + path = "#{tmpdir}#{name}.XXXXXX#{extension}" + + if extension + fd = LibC.mkstemps(path, extension.bytesize) + else + fd = LibC.mkstemp(path) + end + + raise Errno.new("mkstemp") if fd == -1 + {fd, path} + end + + def self.tempdir + tmpdir = ENV["TMPDIR"]? || "/tmp" + tmpdir.rchop(::File::SEPARATOR) + end + + def self.stat(path) + if LibC.stat(path.check_no_null_byte, out stat) != 0 + raise Errno.new("Unable to get stat for '#{path}'") + end + ::File::Stat.new(stat) + end + + def self.lstat(path) + if LibC.lstat(path.check_no_null_byte, out stat) != 0 + raise Errno.new("Unable to get lstat for '#{path}'") + end + ::File::Stat.new(stat) + end + + def self.empty?(path) + begin + stat(path).size == 0 + rescue Errno + raise Errno.new("Error determining size of '#{path}'") + end + end + + def self.exists?(path) + accessible?(path, LibC::F_OK) + end + + def self.readable?(path) : Bool + accessible?(path, LibC::R_OK) + end + + def self.writable?(path) : Bool + accessible?(path, LibC::W_OK) + end + + def self.executable?(path) : Bool + accessible?(path, LibC::X_OK) + end + + private def self.accessible?(path, flag) + LibC.access(path.check_no_null_byte, flag) == 0 + end + + def self.file?(path) : Bool + if LibC.stat(path.check_no_null_byte, out stat) != 0 + if Errno.value == Errno::ENOENT + return false + else + raise Errno.new("stat") + end + end + ::File::Stat.new(stat).file? + end + + def self.chown(path, uid : Int, gid : Int, follow_symlinks) + ret = if !follow_symlinks && symlink?(path) + LibC.lchown(path, uid, gid) + else + LibC.chown(path, uid, gid) + end + raise Errno.new("Error changing owner of '#{path}'") if ret == -1 + end + + def self.chmod(path, mode : Int) + if LibC.chmod(path, mode) == -1 + raise Errno.new("Error changing permissions of '#{path}'") + end + end + + def self.delete(path) + err = LibC.unlink(path.check_no_null_byte) + if err == -1 + raise Errno.new("Error deleting file '#{path}'") + end + end + + def self.real_path(path) + real_path_ptr = LibC.realpath(path, nil) + raise Errno.new("Error resolving real path of #{path}") unless real_path_ptr + String.new(real_path_ptr).tap { LibC.free(real_path_ptr.as(Void*)) } + end + + def self.link(old_path, new_path) + ret = LibC.link(old_path.check_no_null_byte, new_path.check_no_null_byte) + raise Errno.new("Error creating link from #{old_path} to #{new_path}") if ret != 0 + ret + end + + def self.symlink(old_path, new_path) + ret = LibC.symlink(old_path.check_no_null_byte, new_path.check_no_null_byte) + raise Errno.new("Error creating symlink from #{old_path} to #{new_path}") if ret != 0 + ret + end + + def self.symlink?(path) + if LibC.lstat(path.check_no_null_byte, out stat) != 0 + if Errno.value == Errno::ENOENT + return false + else + raise Errno.new("stat") + end + end + (stat.st_mode & LibC::S_IFMT) == LibC::S_IFLNK + end + + def self.rename(old_filename, new_filename) + code = LibC.rename(old_filename.check_no_null_byte, new_filename.check_no_null_byte) + if code != 0 + raise Errno.new("Error renaming file '#{old_filename}' to '#{new_filename}'") + end + end + + def self.utime(atime : ::Time, mtime : ::Time, filename : String) : Nil + timevals = uninitialized LibC::Timeval[2] + timevals[0] = to_timeval(atime) + timevals[1] = to_timeval(mtime) + ret = LibC.utimes(filename, timevals) + if ret != 0 + raise Errno.new("Error setting time to file '#{filename}'") + end + end + + private def self.to_timeval(time : ::Time) + t = uninitialized LibC::Timeval + t.tv_sec = typeof(t.tv_sec).new(time.to_local.epoch) + t.tv_usec = typeof(t.tv_usec).new(0) + t + end + + private def system_truncate(size) : Nil + flush + code = LibC.ftruncate(fd, size) + if code != 0 + raise Errno.new("Error truncating file '#{path}'") + end + end + + private def system_flock_shared(blocking) + flock LibC::FlockOp::SH, blocking + end + + private def system_flock_exclusive(blocking) + flock LibC::FlockOp::EX, blocking + end + + private def system_flock_unlock + flock LibC::FlockOp::UN + end + + private def flock(op : LibC::FlockOp, blocking : Bool = true) + op |= LibC::FlockOp::NB unless blocking + + if LibC.flock(@fd, op) != 0 + raise Errno.new("flock") + end + + nil + end +end diff --git a/src/crystal/system/unix/file_descriptor.cr b/src/crystal/system/unix/file_descriptor.cr new file mode 100644 index 000000000000..3a2fe197ad47 --- /dev/null +++ b/src/crystal/system/unix/file_descriptor.cr @@ -0,0 +1,141 @@ +require "c/fcntl" + +# :nodoc: +module Crystal::System::FileDescriptor + include IO::Syscall + + @fd : Int32 + + @read_event : Event::Event? + @write_event : Event::Event? + + private def unbuffered_read(slice : Bytes) + read_syscall_helper(slice, "Error reading file") do + # `to_i32` is acceptable because `Slice#size` is a Int32 + LibC.read(@fd, slice, slice.size).to_i32 + end + end + + private def unbuffered_write(slice : Bytes) + write_syscall_helper(slice, "Error writing file") do |slice| + LibC.write(@fd, slice, slice.size).tap do |return_code| + if return_code == -1 && Errno.value == Errno::EBADF + raise IO::Error.new "File not open for writing" + end + end + end + end + + private def system_blocking? + fcntl(LibC::F_GETFL) & LibC::O_NONBLOCK == 0 + end + + private def system_blocking=(value) + current_flags = fcntl(LibC::F_GETFL) + new_flags = current_flags + if value + new_flags &= ~LibC::O_NONBLOCK + else + new_flags |= LibC::O_NONBLOCK + end + fcntl(LibC::F_SETFL, new_flags) unless new_flags == current_flags + end + + private def system_close_on_exec? + flags = fcntl(LibC::F_GETFD) + (flags & LibC::FD_CLOEXEC) == LibC::FD_CLOEXEC + end + + private def system_close_on_exec=(arg : Bool) + fcntl(LibC::F_SETFD, arg ? LibC::FD_CLOEXEC : 0) + arg + end + + def self.fcntl(fd, cmd, arg = 0) + r = LibC.fcntl(fd, cmd, arg) + raise Errno.new("fcntl() failed") if r == -1 + r + end + + private def system_stat + if LibC.fstat(@fd, out stat) != 0 + raise Errno.new("Unable to get stat") + end + ::File::Stat.new(stat) + end + + private def system_seek(offset, whence : IO::Seek) : Nil + seek_value = LibC.lseek(@fd, offset, whence) + + if seek_value == -1 + raise Errno.new "Unable to seek" + end + end + + private def system_pos + pos = LibC.lseek(@fd, 0, IO::Seek::Current) + raise Errno.new "Unable to tell" if pos == -1 + pos + end + + private def system_tty? + LibC.isatty(fd) == 1 + end + + private def system_reopen(other : IO::FileDescriptor) + {% if LibC.methods.includes? "dup3".id %} + # dup doesn't copy the CLOEXEC flag, so copy it manually using dup3 + flags = other.close_on_exec? ? LibC::O_CLOEXEC : 0 + if LibC.dup3(other.fd, self.fd, flags) == -1 + raise Errno.new("Could not reopen file descriptor") + end + {% else %} + # dup doesn't copy the CLOEXEC flag, copy it manually to the new + if LibC.dup2(other.fd, self.fd) == -1 + raise Errno.new("Could not reopen file descriptor") + end + + if other.close_on_exec? + self.close_on_exec = true + end + {% end %} + + # We are now pointing to a new file descriptor, we need to re-register + # events with libevent and enqueue readers and writers again. + @read_event.try &.free + @read_event = nil + + @write_event.try &.free + @write_event = nil + + reschedule_waiting + end + + private def add_read_event(timeout = @read_timeout) : Nil + event = @read_event ||= Scheduler.create_fd_read_event(self) + event.add timeout + end + + private def add_write_event(timeout = @write_timeout) : Nil + event = @write_event ||= Scheduler.create_fd_write_event(self) + event.add timeout + end + + private def system_close + if LibC.close(@fd) != 0 + case Errno.value + when Errno::EINTR, Errno::EINPROGRESS + # ignore + else + raise Errno.new("Error closing file") + end + end + ensure + @read_event.try &.free + @read_event = nil + @write_event.try &.free + @write_event = nil + + reschedule_waiting + end +end diff --git a/src/crystal/system/unix/getrandom.cr b/src/crystal/system/unix/getrandom.cr index 208def070e49..95b2828feca3 100644 --- a/src/crystal/system/unix/getrandom.cr +++ b/src/crystal/system/unix/getrandom.cr @@ -6,7 +6,7 @@ require "c/sys/syscall" module Crystal::System::Random @@initialized = false @@getrandom_available = false - @@urandom : File? + @@urandom : ::File? private def self.init @@initialized = true @@ -14,7 +14,7 @@ module Crystal::System::Random if sys_getrandom(Bytes.new(16)) >= 0 @@getrandom_available = true else - urandom = File.open("/dev/urandom", "r") + urandom = ::File.open("/dev/urandom", "r") return unless urandom.stat.chardev? urandom.close_on_exec = true diff --git a/src/crystal/system/unix/urandom.cr b/src/crystal/system/unix/urandom.cr index 424641890d40..a11d076f5839 100644 --- a/src/crystal/system/unix/urandom.cr +++ b/src/crystal/system/unix/urandom.cr @@ -3,12 +3,12 @@ module Crystal::System::Random @@initialized = false - @@urandom : File? + @@urandom : ::File? private def self.init @@initialized = true - urandom = File.open("/dev/urandom", "r") + urandom = ::File.open("/dev/urandom", "r") return unless urandom.stat.chardev? urandom.close_on_exec = true diff --git a/src/file.cr b/src/file.cr index c6f817cbe87e..cad9896d37f1 100644 --- a/src/file.cr +++ b/src/file.cr @@ -1,8 +1,4 @@ -require "c/fcntl" -require "c/stdio" -require "c/stdlib" -require "c/sys/stat" -require "c/unistd" +require "crystal/system/file" class File < IO::FileDescriptor # The file/directory separator character. `'/'` in Unix, `'\\'` in Windows. @@ -22,56 +18,18 @@ class File < IO::FileDescriptor # :nodoc: DEFAULT_CREATE_MODE = LibC::S_IRUSR | LibC::S_IWUSR | LibC::S_IRGRP | LibC::S_IROTH - def initialize(filename : String, mode = "r", perm = DEFAULT_CREATE_MODE, encoding = nil, invalid = nil) - oflag = open_flag(mode) | LibC::O_CLOEXEC + include Crystal::System::File - fd = LibC.open(filename.check_no_null_byte, oflag, perm) - if fd < 0 - raise Errno.new("Error opening file '#{filename}' with mode '#{mode}'") - end - - @path = filename + # This constructor is provided for subclasses to be able to initialize an + # `IO::FileDescriptor` with a *path* and *fd*. + private def initialize(@path, fd, blocking = false, encoding = nil, invalid = nil) self.set_encoding(encoding, invalid: invalid) if encoding - super(fd, blocking: true) + super(fd, blocking) end - protected def open_flag(mode) - if mode.size == 0 - raise "Invalid access mode #{mode}" - end - - m = 0 - o = 0 - case mode[0] - when 'r' - m = LibC::O_RDONLY - when 'w' - m = LibC::O_WRONLY - o = LibC::O_CREAT | LibC::O_TRUNC - when 'a' - m = LibC::O_WRONLY - o = LibC::O_CREAT | LibC::O_APPEND - else - raise "Invalid access mode #{mode}" - end - - case mode.size - when 1 - # Nothing - when 2 - case mode[1] - when '+' - m = LibC::O_RDWR - when 'b' - # Nothing - else - raise "Invalid access mode #{mode}" - end - else - raise "Invalid access mode #{mode}" - end - - oflag = m | o + def self.new(filename : String, mode = "r", perm = DEFAULT_CREATE_MODE, encoding = nil, invalid = nil) + fd = Crystal::System::File.open(filename, mode, perm) + new(filename, fd, blocking: true, encoding: encoding, invalid: invalid) end getter path : String @@ -86,10 +44,7 @@ class File < IO::FileDescriptor # File.stat("foo").mtime # => 2015-09-23 06:24:19 UTC # ``` def self.stat(path) : Stat - if LibC.stat(path.check_no_null_byte, out stat) != 0 - raise Errno.new("Unable to get stat for '#{path}'") - end - Stat.new(stat) + Crystal::System::File.stat(path) end # Returns a `File::Stat` object for the file given by *path* or raises @@ -102,10 +57,7 @@ class File < IO::FileDescriptor # File.lstat("foo").mtime # => 2015-09-23 06:24:19 UTC # ``` def self.lstat(path) : Stat - if LibC.lstat(path.check_no_null_byte, out stat) != 0 - raise Errno.new("Unable to get lstat for '#{path}'") - end - Stat.new(stat) + Crystal::System::File.lstat(path) end # Returns `true` if *path* exists else returns `false` @@ -117,7 +69,7 @@ class File < IO::FileDescriptor # File.exists?("foo") # => true # ``` def self.exists?(path) : Bool - accessible?(path, LibC::F_OK) + Crystal::System::File.exists?(path) end # Returns `true` if the file at *path* is empty, otherwise returns `false`. @@ -130,11 +82,7 @@ class File < IO::FileDescriptor # File.empty?("foo") # => false # ``` def self.empty?(path) : Bool - begin - stat(path).size == 0 - rescue Errno - raise Errno.new("Error determining size of '#{path}'") - end + Crystal::System::File.empty?(path) end # Returns `true` if *path* is readable by the real user id of this process else returns `false`. @@ -144,7 +92,7 @@ class File < IO::FileDescriptor # File.readable?("foo") # => true # ``` def self.readable?(path) : Bool - accessible?(path, LibC::R_OK) + Crystal::System::File.readable?(path) end # Returns `true` if *path* is writable by the real user id of this process else returns `false`. @@ -154,7 +102,7 @@ class File < IO::FileDescriptor # File.writable?("foo") # => true # ``` def self.writable?(path) : Bool - accessible?(path, LibC::W_OK) + Crystal::System::File.writable?(path) end # Returns `true` if *path* is executable by the real user id of this process else returns `false`. @@ -164,12 +112,7 @@ class File < IO::FileDescriptor # File.executable?("foo") # => false # ``` def self.executable?(path) : Bool - accessible?(path, LibC::X_OK) - end - - # Convenience method to avoid code on LibC.access calls. Not meant to be called by users of this class. - private def self.accessible?(path, flag) - LibC.access(path.check_no_null_byte, flag) == 0 + Crystal::System::File.executable?(path) end # Returns `true` if given *path* exists and is a file. @@ -182,14 +125,7 @@ class File < IO::FileDescriptor # File.file?("foobar") # => false # ``` def self.file?(path) : Bool - if LibC.stat(path.check_no_null_byte, out stat) != 0 - if Errno.value == Errno::ENOENT - return false - else - raise Errno.new("stat") - end - end - File::Stat.new(stat).file? + Crystal::System::File.file?(path) end # Returns `true` if the given *path* exists and is a directory. @@ -273,13 +209,8 @@ class File < IO::FileDescriptor # File.chown("foo", gid: 100) # changes foo's gid # File.chown("foo", gid: 100, follow_symlinks: true) # changes baz's gid # ``` - def self.chown(path, uid : Int? = -1, gid : Int = -1, follow_symlinks = false) - ret = if !follow_symlinks && symlink?(path) - LibC.lchown(path, uid, gid) - else - LibC.chown(path, uid, gid) - end - raise Errno.new("Error changing owner of '#{path}'") if ret == -1 + def self.chown(path, uid : Int = -1, gid : Int = -1, follow_symlinks = false) + Crystal::System::File.chown(path, uid, gid, follow_symlinks) end # Changes the permissions of the specified file. @@ -295,9 +226,7 @@ class File < IO::FileDescriptor # File.stat("foo").perm # => 0o700 # ``` def self.chmod(path, mode : Int) - if LibC.chmod(path, mode) == -1 - raise Errno.new("Error changing permissions of '#{path}'") - end + Crystal::System::File.chmod(path, mode) end # Delete the file at *path*. Deleting non-existent file will raise an exception. @@ -308,10 +237,7 @@ class File < IO::FileDescriptor # File.delete("./bar") # raises Errno (No such file or directory) # ``` def self.delete(path) - err = LibC.unlink(path.check_no_null_byte) - if err == -1 - raise Errno.new("Error deleting file '#{path}'") - end + Crystal::System::File.delete(path) end # Returns *filename*'s extension, or an empty string if it has no extension. @@ -382,36 +308,23 @@ class File < IO::FileDescriptor # Resolves the real path of *path* by following symbolic links. def self.real_path(path) : String - real_path_ptr = LibC.realpath(path, nil) - raise Errno.new("Error resolving real path of #{path}") unless real_path_ptr - String.new(real_path_ptr).tap { LibC.free(real_path_ptr.as(Void*)) } + Crystal::System::File.real_path(path) end # Creates a new link (also known as a hard link) at *new_path* to an existing file # given by *old_path*. def self.link(old_path, new_path) - ret = LibC.link(old_path.check_no_null_byte, new_path.check_no_null_byte) - raise Errno.new("Error creating link from #{old_path} to #{new_path}") if ret != 0 - ret + Crystal::System::File.link(old_path, new_path) end # Creates a symbolic link at *new_path* to an existing file given by *old_path. def self.symlink(old_path, new_path) - ret = LibC.symlink(old_path.check_no_null_byte, new_path.check_no_null_byte) - raise Errno.new("Error creating symlink from #{old_path} to #{new_path}") if ret != 0 - ret + Crystal::System::File.symlink(old_path, new_path) end # Returns `true` if the *path* is a symbolic link. def self.symlink?(path) : Bool - if LibC.lstat(path.check_no_null_byte, out stat) != 0 - if Errno.value == Errno::ENOENT - return false - else - raise Errno.new("stat") - end - end - (stat.st_mode & LibC::S_IFMT) == LibC::S_IFLNK + Crystal::System::File.symlink?(path) end # Opens the file named by *filename*. If a file is being created, its initial @@ -576,23 +489,13 @@ class File < IO::FileDescriptor # File.exists?("afile") # => false # File.exists?("afile.cr") # => true # ``` - def self.rename(old_filename, new_filename) - code = LibC.rename(old_filename.check_no_null_byte, new_filename.check_no_null_byte) - if code != 0 - raise Errno.new("Error renaming file '#{old_filename}' to '#{new_filename}'") - end - code + def self.rename(old_filename, new_filename) : Nil + Crystal::System::File.rename(old_filename, new_filename) end # Sets the access and modification times of *filename*. def self.utime(atime : Time, mtime : Time, filename : String) : Nil - timevals = uninitialized LibC::Timeval[2] - timevals[0] = to_timeval(atime) - timevals[1] = to_timeval(mtime) - ret = LibC.utimes(filename, timevals) - if ret != 0 - raise Errno.new("Error setting time to file '#{filename}'") - end + Crystal::System::File.utime(atime, mtime, filename) end # Attempts to set the access and modification times of the file named @@ -604,13 +507,6 @@ class File < IO::FileDescriptor utime time, time, filename end - private def self.to_timeval(time : Time) - t = uninitialized LibC::Timeval - t.tv_sec = typeof(t.tv_sec).new(time.to_local.epoch) - t.tv_usec = typeof(t.tv_usec).new(0) - t - end - # Return the size in bytes of the currently opened file. def size stat.size @@ -618,13 +514,9 @@ class File < IO::FileDescriptor # Truncates the file to the specified *size*. Requires that the current file is opened # for writing. - def truncate(size = 0) + def truncate(size = 0) : Nil flush - code = LibC.ftruncate(fd, size) - if code != 0 - raise Errno.new("Error truncating file '#{path}'") - end - code + system_truncate(size) end # Yields an `IO` to read a section inside this file. @@ -654,6 +546,44 @@ class File < IO::FileDescriptor io << ">" io end + + # TODO: use fcntl/lockf instead of flock (which doesn't lock over NFS) + # TODO: always use non-blocking locks, yield fiber until resource becomes available + + def flock_shared(blocking = true) + flock_shared blocking + begin + yield + ensure + flock_unlock + end + end + + # Place a shared advisory lock. More than one process may hold a shared lock for a given file at a given time. + # `Errno::EWOULDBLOCK` is raised if *blocking* is set to `false` and an existing exclusive lock is set. + def flock_shared(blocking = true) + system_flock_shared(blocking) + end + + def flock_exclusive(blocking = true) + flock_exclusive blocking + begin + yield + ensure + flock_unlock + end + end + + # Place an exclusive advisory lock. Only one process may hold an exclusive lock for a given file at a given time. + # `Errno::EWOULDBLOCK` is raised if *blocking* is set to `false` and any existing lock is set. + def flock_exclusive(blocking = true) + system_flock_exclusive(blocking) + end + + # Remove an existing advisory lock held by this process. + def flock_unlock + system_flock_unlock + end end require "./file/*" diff --git a/src/file/flock.cr b/src/file/flock.cr deleted file mode 100644 index b88634251ea5..000000000000 --- a/src/file/flock.cr +++ /dev/null @@ -1,61 +0,0 @@ -# TODO: use fcntl/lockf instead of flock (which doesn't lock over NFS) -# TODO: always use non-blocking locks, yield fiber until resource becomes available - -lib LibC - @[Flags] - enum FlockOp - SH = 0x1 - EX = 0x2 - NB = 0x4 - UN = 0x8 - end - - fun flock(fd : Int, op : FlockOp) : Int -end - -class File - def flock_shared(blocking = true) - flock_shared blocking - begin - yield - ensure - flock_unlock - end - end - - # Place a shared advisory lock. More than one process may hold a shared lock for a given file at a given time. - # `Errno::EWOULDBLOCK` is raised if *blocking* is set to `false` and an existing exclusive lock is set. - def flock_shared(blocking = true) - flock LibC::FlockOp::SH, blocking - end - - def flock_exclusive(blocking = true) - flock_exclusive blocking - begin - yield - ensure - flock_unlock - end - end - - # Place an exclusive advisory lock. Only one process may hold an exclusive lock for a given file at a given time. - # `Errno::EWOULDBLOCK` is raised if *blocking* is set to `false` and any existing lock is set. - def flock_exclusive(blocking = true) - flock LibC::FlockOp::EX, blocking - end - - # Remove an existing advisory lock held by this process. - def flock_unlock - flock LibC::FlockOp::UN - end - - private def flock(op : LibC::FlockOp, blocking : Bool = true) - op |= LibC::FlockOp::NB unless blocking - - if LibC.flock(@fd, op) != 0 - raise Errno.new("flock") - end - - nil - end -end diff --git a/src/io/file_descriptor.cr b/src/io/file_descriptor.cr index 4250ccc69ed5..c501c050e6fd 100644 --- a/src/io/file_descriptor.cr +++ b/src/io/file_descriptor.cr @@ -1,15 +1,16 @@ require "./syscall" -require "c/fcntl" +require "crystal/system/file_descriptor" # An `IO` over a file descriptor. class IO::FileDescriptor < IO + include Crystal::System::FileDescriptor include IO::Buffered - include IO::Syscall - @read_event : Event::Event? - @write_event : Event::Event? + # The raw file-descriptor. It is defined to be an `Int`, but it's size is + # platform-specific. + getter fd - def initialize(@fd : Int32, blocking = false) + def initialize(@fd, blocking = false) @closed = false unless blocking @@ -18,45 +19,33 @@ class IO::FileDescriptor < IO end def blocking - fcntl(LibC::F_GETFL) & LibC::O_NONBLOCK == 0 + system_blocking? end def blocking=(value) - current_flags = fcntl(LibC::F_GETFL) - new_flags = current_flags - if value - new_flags &= ~LibC::O_NONBLOCK - else - new_flags |= LibC::O_NONBLOCK - end - fcntl(LibC::F_SETFL, new_flags) unless new_flags == current_flags + self.system_blocking = value end def close_on_exec? - flags = fcntl(LibC::F_GETFD) - (flags & LibC::FD_CLOEXEC) == LibC::FD_CLOEXEC + system_close_on_exec? end - def close_on_exec=(arg : Bool) - fcntl(LibC::F_SETFD, arg ? LibC::FD_CLOEXEC : 0) - arg + def close_on_exec=(value : Bool) + self.system_close_on_exec = value end - def self.fcntl(fd, cmd, arg = 0) - r = LibC.fcntl fd, cmd, arg - raise Errno.new("fcntl() failed") if r == -1 - r - end + {% unless flag?(:win32) %} + def self.fcntl(fd, cmd, arg = 0) + Crystal::System::FileDescriptor.fcntl(fd, cmd, arg) + end - def fcntl(cmd, arg = 0) - self.class.fcntl @fd, cmd, arg - end + def fcntl(cmd, arg = 0) + Crystal::System::FileDescriptor.fcntl(@fd, cmd, arg) + end + {% end %} def stat - if LibC.fstat(@fd, out stat) != 0 - raise Errno.new("Unable to get stat") - end - File::Stat.new(stat) + system_stat end # Seeks to a given *offset* (in bytes) according to the *whence* argument. @@ -77,11 +66,8 @@ class IO::FileDescriptor < IO flush offset -= @in_buffer_rem.size if whence.current? - seek_value = LibC.lseek(@fd, offset, whence) - if seek_value == -1 - raise Errno.new "Unable to seek" - end + system_seek(offset, whence) @in_buffer_rem = Bytes.empty @@ -113,10 +99,7 @@ class IO::FileDescriptor < IO def pos check_open - seek_value = LibC.lseek(@fd, 0, Seek::Current) - raise Errno.new "Unable to tell" if seek_value == -1 - - seek_value - @in_buffer_rem.size + system_pos - @in_buffer_rem.size end # Sets the current position (in bytes) in this `IO`. @@ -133,10 +116,6 @@ class IO::FileDescriptor < IO value end - def fd - @fd - end - def finalize return if closed? @@ -148,16 +127,11 @@ class IO::FileDescriptor < IO end def tty? - LibC.isatty(fd) == 1 + system_tty? end def reopen(other : IO::FileDescriptor) - if LibC.dup2(other.fd, self.fd) == -1 - raise Errno.new("Could not reopen file descriptor") - end - - # flag is lost after dup - self.close_on_exec = true + system_reopen(other) other end @@ -177,63 +151,14 @@ class IO::FileDescriptor < IO pp.text inspect end - private def unbuffered_read(slice : Bytes) - read_syscall_helper(slice, "Error reading file") do - # `to_i32` is acceptable because `Slice#size` is a Int32 - LibC.read(@fd, slice, slice.size).to_i32 - end - end - - private def unbuffered_write(slice : Bytes) - write_syscall_helper(slice, "Error writing file") do |slice| - LibC.write(@fd, slice, slice.size).tap do |return_code| - if return_code == -1 && Errno.value == Errno::EBADF - raise IO::Error.new "File not open for writing" - end - end - end - end - - private def add_read_event(timeout = @read_timeout) - event = @read_event ||= Scheduler.create_fd_read_event(self) - event.add timeout - nil - end - - private def add_write_event(timeout = @write_timeout) - event = @write_event ||= Scheduler.create_fd_write_event(self) - event.add timeout - nil - end - private def unbuffered_rewind - seek(0, IO::Seek::Set) - self + self.pos = 0 end private def unbuffered_close return if @closed - err = nil - if LibC.close(@fd) != 0 - case Errno.value - when Errno::EINTR, Errno::EINPROGRESS - # ignore - else - err = Errno.new("Error closing file") - end - end - - @closed = true - - @read_event.try &.free - @read_event = nil - @write_event.try &.free - @write_event = nil - - reschedule_waiting - - raise err if err + system_close ensure @closed = true end private def unbuffered_flush diff --git a/src/lib_c/aarch64-linux-gnu/c/sys/file.cr b/src/lib_c/aarch64-linux-gnu/c/sys/file.cr new file mode 100644 index 000000000000..e2bd3fd4b816 --- /dev/null +++ b/src/lib_c/aarch64-linux-gnu/c/sys/file.cr @@ -0,0 +1,11 @@ +lib LibC + @[Flags] + enum FlockOp + SH = 0x1 + EX = 0x2 + NB = 0x4 + UN = 0x8 + end + + fun flock(fd : Int, op : FlockOp) : Int +end diff --git a/src/lib_c/amd64-unknown-openbsd/c/sys/file.cr b/src/lib_c/amd64-unknown-openbsd/c/sys/file.cr new file mode 100644 index 000000000000..e2bd3fd4b816 --- /dev/null +++ b/src/lib_c/amd64-unknown-openbsd/c/sys/file.cr @@ -0,0 +1,11 @@ +lib LibC + @[Flags] + enum FlockOp + SH = 0x1 + EX = 0x2 + NB = 0x4 + UN = 0x8 + end + + fun flock(fd : Int, op : FlockOp) : Int +end diff --git a/src/lib_c/arm-linux-gnueabihf/c/sys/file.cr b/src/lib_c/arm-linux-gnueabihf/c/sys/file.cr new file mode 100644 index 000000000000..e2bd3fd4b816 --- /dev/null +++ b/src/lib_c/arm-linux-gnueabihf/c/sys/file.cr @@ -0,0 +1,11 @@ +lib LibC + @[Flags] + enum FlockOp + SH = 0x1 + EX = 0x2 + NB = 0x4 + UN = 0x8 + end + + fun flock(fd : Int, op : FlockOp) : Int +end diff --git a/src/lib_c/i686-linux-gnu/c/sys/file.cr b/src/lib_c/i686-linux-gnu/c/sys/file.cr new file mode 100644 index 000000000000..e2bd3fd4b816 --- /dev/null +++ b/src/lib_c/i686-linux-gnu/c/sys/file.cr @@ -0,0 +1,11 @@ +lib LibC + @[Flags] + enum FlockOp + SH = 0x1 + EX = 0x2 + NB = 0x4 + UN = 0x8 + end + + fun flock(fd : Int, op : FlockOp) : Int +end diff --git a/src/lib_c/i686-linux-musl/c/sys/file.cr b/src/lib_c/i686-linux-musl/c/sys/file.cr new file mode 100644 index 000000000000..e2bd3fd4b816 --- /dev/null +++ b/src/lib_c/i686-linux-musl/c/sys/file.cr @@ -0,0 +1,11 @@ +lib LibC + @[Flags] + enum FlockOp + SH = 0x1 + EX = 0x2 + NB = 0x4 + UN = 0x8 + end + + fun flock(fd : Int, op : FlockOp) : Int +end diff --git a/src/lib_c/x86_64-linux-gnu/c/sys/file.cr b/src/lib_c/x86_64-linux-gnu/c/sys/file.cr new file mode 100644 index 000000000000..e2bd3fd4b816 --- /dev/null +++ b/src/lib_c/x86_64-linux-gnu/c/sys/file.cr @@ -0,0 +1,11 @@ +lib LibC + @[Flags] + enum FlockOp + SH = 0x1 + EX = 0x2 + NB = 0x4 + UN = 0x8 + end + + fun flock(fd : Int, op : FlockOp) : Int +end diff --git a/src/lib_c/x86_64-linux-musl/c/sys/file.cr b/src/lib_c/x86_64-linux-musl/c/sys/file.cr new file mode 100644 index 000000000000..e2bd3fd4b816 --- /dev/null +++ b/src/lib_c/x86_64-linux-musl/c/sys/file.cr @@ -0,0 +1,11 @@ +lib LibC + @[Flags] + enum FlockOp + SH = 0x1 + EX = 0x2 + NB = 0x4 + UN = 0x8 + end + + fun flock(fd : Int, op : FlockOp) : Int +end diff --git a/src/lib_c/x86_64-macosx-darwin/c/sys/file.cr b/src/lib_c/x86_64-macosx-darwin/c/sys/file.cr new file mode 100644 index 000000000000..e2bd3fd4b816 --- /dev/null +++ b/src/lib_c/x86_64-macosx-darwin/c/sys/file.cr @@ -0,0 +1,11 @@ +lib LibC + @[Flags] + enum FlockOp + SH = 0x1 + EX = 0x2 + NB = 0x4 + UN = 0x8 + end + + fun flock(fd : Int, op : FlockOp) : Int +end diff --git a/src/lib_c/x86_64-portbld-freebsd/c/sys/file.cr b/src/lib_c/x86_64-portbld-freebsd/c/sys/file.cr new file mode 100644 index 000000000000..e2bd3fd4b816 --- /dev/null +++ b/src/lib_c/x86_64-portbld-freebsd/c/sys/file.cr @@ -0,0 +1,11 @@ +lib LibC + @[Flags] + enum FlockOp + SH = 0x1 + EX = 0x2 + NB = 0x4 + UN = 0x8 + end + + fun flock(fd : Int, op : FlockOp) : Int +end diff --git a/src/tempfile.cr b/src/tempfile.cr index 90b2079ada01..175f19a8d29f 100644 --- a/src/tempfile.cr +++ b/src/tempfile.cr @@ -38,21 +38,13 @@ require "c/stdlib" # ``` # Tempfile.new("foo", ".png").path # => "/tmp/foo.ulBCPS.png" # ``` -class Tempfile < IO::FileDescriptor +class Tempfile < File # Creates a `Tempfile` with the given filename and extension. - def initialize(name, extension = nil) - tmpdir = self.class.dirname + File::SEPARATOR - @path = "#{tmpdir}#{name}.XXXXXX#{extension}" - fileno = if extension - LibC.mkstemps(@path, extension.bytesize) - else - LibC.mkstemp(@path) - end - - if fileno == -1 - raise Errno.new("mkstemp") - end - super(fileno, blocking: true) + # + # *encoding* and *invalid* are passed to `IO#set_encoding`. + def initialize(name, extension = nil, encoding = nil, invalid = nil) + fileno, path = Crystal::System::File.mktemp(name, extension) + super(path, fileno, blocking: true, encoding: encoding, invalid: invalid) end # Retrieves the full path of a this tempfile. @@ -87,11 +79,7 @@ class Tempfile < IO::FileDescriptor # Tempfile.dirname # => "/tmp" # ``` def self.dirname : String - unless tmpdir = ENV["TMPDIR"]? - tmpdir = "/tmp" - end - tmpdir = tmpdir + File::SEPARATOR unless tmpdir.ends_with? File::SEPARATOR - File.dirname(tmpdir) + Crystal::System::File.tempdir end # Deletes this tempfile.