diff --git a/spec/std/crystal/fd_lock_spec.cr b/spec/std/crystal/fd_lock_spec.cr new file mode 100644 index 000000000000..883dcfa0a752 --- /dev/null +++ b/spec/std/crystal/fd_lock_spec.cr @@ -0,0 +1,134 @@ +require "spec" +require "crystal/fd_lock" +require "wait_group" + +describe Crystal::FdLock do + describe "#reference" do + it "acquires" do + lock = Crystal::FdLock.new + called = 0 + + lock.reference { called += 1 } + lock.reference { called += 1 } + + called.should eq(2) + end + + it "allows reentrancy (side effect)" do + lock = Crystal::FdLock.new + called = 0 + + lock.reference { called += 1 } + lock.reference do + lock.reference { called += 1 } + end + + called.should eq(2) + end + + it "acquires shared reference" do + lock = Crystal::FdLock.new + + ready = WaitGroup.new(1) + release = Channel(String).new + + spawn do + lock.reference do + ready.done + + select + when release.send("ok") + when timeout(1.second) + release.send("timeout") + end + end + end + + ready.wait + lock.reference { } + + release.receive.should eq("ok") + end + + it "raises when closed" do + lock = Crystal::FdLock.new + lock.try_close? { } + + called = false + expect_raises(IO::Error, "Closed") do + lock.reference { called = true } + end + + called.should be_false + end + end + + describe "#try_close?" do + it "closes" do + lock = Crystal::FdLock.new + lock.closed?.should be_false + + called = false + lock.try_close? { called = true }.should be_true + lock.closed?.should be_true + called.should be_true + end + + it "closes once" do + lock = Crystal::FdLock.new + + called = 0 + + WaitGroup.wait do |wg| + 10.times do + wg.spawn do + lock.try_close? { called += 1 } + lock.try_close? { called += 1 } + end + end + end + + called.should eq(1) + end + + it "waits for all references to return" do + lock = Crystal::FdLock.new + + ready = WaitGroup.new(10) + exceptions = Channel(Exception).new(10) + + WaitGroup.wait do |wg| + 10.times do + wg.spawn do + begin + lock.reference do + ready.done + Fiber.yield + end + rescue ex + exceptions.send(ex) + end + end + end + + ready.wait + + called = false + lock.try_close? { called = true }.should eq(true) + lock.closed?.should be_true + end + exceptions.close + + if ex = exceptions.receive? + raise ex + end + end + end + + it "#reset" do + lock = Crystal::FdLock.new + lock.try_close? { } + lock.reset + lock.try_close? { }.should eq(true) + end +end diff --git a/src/crystal/event_loop/iocp.cr b/src/crystal/event_loop/iocp.cr index a78721ba4372..e6885ed9ac82 100644 --- a/src/crystal/event_loop/iocp.cr +++ b/src/crystal/event_loop/iocp.cr @@ -436,7 +436,7 @@ class Crystal::EventLoop::IOCP < Crystal::EventLoop # AcceptEx does not automatically set the socket options on the accepted # socket to match those of the listening socket, we need to ask for that # explicitly with SO_UPDATE_ACCEPT_CONTEXT - socket.system_setsockopt client_handle, LibC::SO_UPDATE_ACCEPT_CONTEXT, socket.fd + System::Socket.setsockopt client_handle, LibC::SO_UPDATE_ACCEPT_CONTEXT, socket.fd true else diff --git a/src/crystal/event_loop/libevent.cr b/src/crystal/event_loop/libevent.cr index d0eef0f88425..33638066caaa 100644 --- a/src/crystal/event_loop/libevent.cr +++ b/src/crystal/event_loop/libevent.cr @@ -178,10 +178,13 @@ class Crystal::EventLoop::LibEvent < Crystal::EventLoop file_descriptor.evented_close end - def close(file_descriptor : Crystal::System::FileDescriptor) : Nil + def before_close(file_descriptor : Crystal::System::FileDescriptor) : Nil # perform cleanup before LibC.close. Using a file descriptor after it has # been closed is never defined and can always lead to undefined results file_descriptor.evented_close + end + + def close(file_descriptor : Crystal::System::FileDescriptor) : Nil file_descriptor.file_descriptor_close end @@ -299,10 +302,13 @@ class Crystal::EventLoop::LibEvent < Crystal::EventLoop end end - def close(socket : ::Socket) : Nil + def before_close(socket : ::Socket) : Nil # perform cleanup before LibC.close. Using a file descriptor after it has # been closed is never defined and can always lead to undefined results socket.evented_close + end + + def close(socket : ::Socket) : Nil socket.socket_close end diff --git a/src/crystal/event_loop/polling.cr b/src/crystal/event_loop/polling.cr index 61c10fe85567..11787f29f53e 100644 --- a/src/crystal/event_loop/polling.cr +++ b/src/crystal/event_loop/polling.cr @@ -232,10 +232,13 @@ abstract class Crystal::EventLoop::Polling < Crystal::EventLoop resume_all(file_descriptor) end - def close(file_descriptor : System::FileDescriptor) : Nil + def before_close(file_descriptor : System::FileDescriptor) : Nil # perform cleanup before LibC.close. Using a file descriptor after it has # been closed is never defined and can always lead to undefined results resume_all(file_descriptor) + end + + def close(file_descriptor : System::FileDescriptor) : Nil file_descriptor.file_descriptor_close end @@ -363,10 +366,13 @@ abstract class Crystal::EventLoop::Polling < Crystal::EventLoop end end - def close(socket : ::Socket) : Nil + def before_close(socket : ::Socket) : Nil # perform cleanup before LibC.close. Using a file descriptor after it has # been closed is never defined and can always lead to undefined results resume_all(socket) + end + + def close(socket : ::Socket) : Nil socket.socket_close end diff --git a/src/crystal/event_loop/wasi.cr b/src/crystal/event_loop/wasi.cr index 19b2f5e65868..a1e3c928413f 100644 --- a/src/crystal/event_loop/wasi.cr +++ b/src/crystal/event_loop/wasi.cr @@ -80,8 +80,11 @@ class Crystal::EventLoop::Wasi < Crystal::EventLoop raise NotImplementedError.new("Crystal::EventLoop#reopened(FileDescriptor)") end - def close(file_descriptor : Crystal::System::FileDescriptor) : Nil + def before_close(file_descriptor : Crystal::System::FileDescriptor) : Nil file_descriptor.evented_close + end + + def close(file_descriptor : Crystal::System::FileDescriptor) : Nil file_descriptor.file_descriptor_close end @@ -133,8 +136,11 @@ class Crystal::EventLoop::Wasi < Crystal::EventLoop raise NotImplementedError.new "Crystal::Wasi::EventLoop#accept" end - def close(socket : ::Socket) : Nil + def before_close(socket : ::Socket) : Nil socket.evented_close + end + + def close(socket : ::Socket) : Nil socket.socket_close end diff --git a/src/crystal/fd_lock.cr b/src/crystal/fd_lock.cr new file mode 100644 index 000000000000..5367a0038769 --- /dev/null +++ b/src/crystal/fd_lock.cr @@ -0,0 +1,124 @@ +# :nodoc: +# +# Tracks active references over a system file descriptor (fd). +# +# The fdlock can be closed at any time, but the actual system close will wait +# until there are no more references left. This avoids potential races when a +# thread might try to read a fd that has been closed and has been reused by the +# OS. +struct Crystal::FdLock + CLOSED = 1_u32 # the fdlock has been closed + REF = 2_u32 # the ref counter increment + MASK = ~(REF - 1) # mask for the ref counter + + {% if flag?(:preview_mt) %} + @m = Atomic(UInt32).new(0_u32) + @closing : Fiber? + {% else %} + @closed = false + {% end %} + + # Borrows a reference for the duration of the block. Raises if the fdlock is + # closed while trying to borrow. + def reference(& : -> F) : F forall F + {% if flag?(:preview_mt) %} + m, success = @m.compare_and_set(0_u32, REF, :acquire, :relaxed) + increment_slow(m) unless success + + begin + yield + ensure + m = @m.sub(REF, :release) + handle_last_ref(m) + end + {% else %} + raise IO::Error.new("Closed") if @closed + yield + {% end %} + end + + private def increment_slow(m) + while true + if (m & CLOSED) == CLOSED + raise IO::Error.new("Closed") + end + m, success = @m.compare_and_set(m, m + REF, :acquire, :relaxed) + break if success + end + end + + private def handle_last_ref(m) + return unless (m & CLOSED) == CLOSED # is closed? + return unless (m & MASK) == REF # was the last ref? + + # the last ref after close is responsible to resume the closing fiber + @closing.not_nil!("BUG: expected a closing fiber to resume.").enqueue + end + + # Closes the fdlock. Blocks for as long as there are references. + # + # Returns true if the fdlock has been closed: no fiber can acquire a reference + # anymore, the calling fiber fully owns the fd and can safely close it. + # + # Returns false if the fdlock has already been closed: the calling fiber + # doesn't own the fd and musn't close it (there might still be active + # references). + def try_close?(&before_close : ->) : Bool + {% if flag?(:preview_mt) %} + m = @m.get(:relaxed) + + # increment ref and close (abort if already closed) + while true + if (m & CLOSED) == CLOSED + return false + end + m, success = @m.compare_and_set(m, (m + REF) | CLOSED, :acquire, :relaxed) + break if success + end + + # set the current fiber as the closing fiber (to be resumed by the last ref) + # then decrement ref + @closing = Fiber.current + m = @m.sub(REF, :release) + + begin + # before close callback + yield + ensure + # wait for the last ref... unless we're the last ref! + Fiber.suspend unless (m & MASK) == REF + end + + @closing = nil + true + {% else %} + if @closed + false + else + @closed = true + yield + true + end + {% end %} + end + + # Resets the fdlock back to its pristine state so it can be used again. + # Assumes the caller owns the fdlock. This is required by + # `TCPSocket#initialize`. + def reset : Nil + {% if flag?(:preview_mt) %} + @m.lazy_set(0_u32) + @closing = nil + {% else %} + @closed = false + {% end %} + end + + def closed? : Bool + {% if flag?(:preview_mt) %} + (@m.get(:relaxed) & CLOSED) == CLOSED + {% else %} + @closed + {% end %} + end +end diff --git a/src/crystal/system/socket.cr b/src/crystal/system/socket.cr index be1c3afa8b78..932fb8db2f58 100644 --- a/src/crystal/system/socket.cr +++ b/src/crystal/system/socket.cr @@ -54,11 +54,11 @@ module Crystal::System::Socket # private def system_linger=(val) - # private def system_getsockopt(fd, optname, optval, level = LibC::SOL_SOCKET, &) + # private def system_getsockopt(optname, optval, level = LibC::SOL_SOCKET, &) - # private def system_getsockopt(fd, optname, optval, level = LibC::SOL_SOCKET) + # private def system_getsockopt(optname, optval, level = LibC::SOL_SOCKET) - # private def system_setsockopt(fd, optname, optval, level = LibC::SOL_SOCKET) + # private def system_setsockopt(optname, optval, level = LibC::SOL_SOCKET) # private def system_blocking? @@ -70,6 +70,8 @@ module Crystal::System::Socket # private def system_close_on_exec=(arg : Bool) + # private def system_fcntl(cmd, arg = 0) + # def self.fcntl(fd, cmd, arg = 0) # def self.socketpair(type : ::Socket::Type, protocol : ::Socket::Protocol, blocking : Bool) : {Handle, Handle} diff --git a/src/crystal/system/unix/file_descriptor.cr b/src/crystal/system/unix/file_descriptor.cr index 1afe22b08ca7..b09d84fb2ee1 100644 --- a/src/crystal/system/unix/file_descriptor.cr +++ b/src/crystal/system/unix/file_descriptor.cr @@ -3,6 +3,7 @@ require "termios" {% if flag?(:android) && LibC::ANDROID_API < 28 %} require "c/sys/ioctl" {% end %} +require "crystal/fd_lock" # :nodoc: module Crystal::System::FileDescriptor @@ -18,13 +19,17 @@ module Crystal::System::FileDescriptor STDOUT_HANDLE = 1 STDERR_HANDLE = 2 + @fd_lock = FdLock.new + private def system_blocking? - flags = fcntl(LibC::F_GETFL) + flags = FileDescriptor.fcntl(fd, LibC::F_GETFL) !flags.bits_set? LibC::O_NONBLOCK end private def system_blocking=(value) - FileDescriptor.set_blocking(fd, value) + @fd_lock.reference do + FileDescriptor.set_blocking(fd, value) + end end protected def self.get_blocking(fd : Handle) @@ -56,12 +61,12 @@ module Crystal::System::FileDescriptor end private def system_close_on_exec? - flags = fcntl(LibC::F_GETFD) + flags = FileDescriptor.fcntl(fd, LibC::F_GETFD) flags.bits_set? LibC::FD_CLOEXEC end private def system_close_on_exec=(arg : Bool) - fcntl(LibC::F_SETFD, arg ? LibC::FD_CLOEXEC : 0) + system_fcntl(LibC::F_SETFD, arg ? LibC::FD_CLOEXEC : 0) arg end @@ -69,12 +74,20 @@ module Crystal::System::FileDescriptor LibC.fcntl(fd, LibC::F_GETFL) == -1 end + private def system_fcntl(cmd, arg) + @fd_lock.reference { FileDescriptor.fcntl(fd, cmd, arg) } + end + def self.fcntl(fd, cmd, arg = 0) r = LibC.fcntl(fd, cmd, arg) raise IO::Error.from_errno("fcntl() failed") if r == -1 r end + private def system_fcntl(cmd, arg = 0) + FileDescriptor.fcntl(fd, cmd, arg) + end + def self.system_info(fd) stat = uninitialized LibC::Stat ret = File.fstat(fd, pointerof(stat)) @@ -87,12 +100,13 @@ module Crystal::System::FileDescriptor end private def system_info - FileDescriptor.system_info fd + @fd_lock.reference { FileDescriptor.system_info(fd) } end private def system_seek(offset, whence : IO::Seek) : Nil - seek_value = LibC.lseek(fd, offset, whence) - + seek_value = @fd_lock.reference do + LibC.lseek(fd, offset, whence) + end if seek_value == -1 raise IO::Error.from_errno "Unable to seek", target: self end @@ -109,19 +123,24 @@ module Crystal::System::FileDescriptor end private def system_reopen(other : IO::FileDescriptor) - {% if LibC.has_method?(:dup3) %} - flags = other.close_on_exec? ? LibC::O_CLOEXEC : 0 - if LibC.dup3(other.fd, fd, flags) == -1 - raise IO::Error.from_errno("Could not reopen file descriptor") - end - {% else %} - Process.lock_read do - if LibC.dup2(other.fd, fd) == -1 - raise IO::Error.from_errno("Could not reopen file descriptor") - end - self.close_on_exec = other.close_on_exec? + other.@fd_lock.reference do + @fd_lock.reference do + {% if LibC.has_method?(:dup3) %} + flags = other.close_on_exec? ? LibC::O_CLOEXEC : 0 + if LibC.dup3(other.fd, fd, flags) == -1 + raise IO::Error.from_errno("Could not reopen file descriptor") + end + {% else %} + Process.lock_read do + if LibC.dup2(other.fd, fd) == -1 + raise IO::Error.from_errno("Could not reopen file descriptor") + end + flags = other.close_on_exec? ? LibC::FD_CLOEXEC : 0 + FileDescriptor.fcntl(fd, LibC::F_SETFD, flags) + end + {% end %} end - {% end %} + end # Mark the handle open, since we had to have dup'd a live handle. @closed = false @@ -130,7 +149,9 @@ module Crystal::System::FileDescriptor end private def system_close - event_loop.close(self) + if @fd_lock.try_close? { event_loop.before_close(self) } + event_loop.close(self) + end end def file_descriptor_close(&) : Nil @@ -191,7 +212,7 @@ module Crystal::System::FileDescriptor end private def flock(op) : Bool - if 0 == LibC.flock(fd, op) + if 0 == @fd_lock.reference { LibC.flock(fd, op) } true else errno = Errno.value @@ -204,7 +225,7 @@ module Crystal::System::FileDescriptor end private def system_fsync(flush_metadata = true) : Nil - ret = + ret = @fd_lock.reference do if flush_metadata LibC.fsync(fd) else @@ -214,6 +235,7 @@ module Crystal::System::FileDescriptor LibC.fdatasync(fd) {% end %} end + end if ret != 0 raise IO::Error.from_errno("Error syncing file", target: self) @@ -241,7 +263,9 @@ module Crystal::System::FileDescriptor end def self.pread(file, buffer, offset) - bytes_read = LibC.pread(file.fd, buffer, buffer.size, offset).to_i64 + bytes_read = file.@fd_lock.reference do + LibC.pread(file.fd, buffer, buffer.size, offset).to_i64 + end if bytes_read == -1 raise IO::Error.from_errno("Error reading file", target: file) @@ -284,10 +308,10 @@ module Crystal::System::FileDescriptor end private def system_echo(enable : Bool, mode = nil) - new_mode = mode || FileDescriptor.tcgetattr(fd) + new_mode = mode || system_tcgetattr flags = LibC::ECHO | LibC::ECHOE | LibC::ECHOK | LibC::ECHONL new_mode.c_lflag = enable ? (new_mode.c_lflag | flags) : (new_mode.c_lflag & ~flags) - if FileDescriptor.tcsetattr(fd, LibC::TCSANOW, pointerof(new_mode)) != 0 + if system_tcsetattr(LibC::TCSANOW, pointerof(new_mode)) != 0 raise IO::Error.from_errno("tcsetattr") end end @@ -300,7 +324,7 @@ module Crystal::System::FileDescriptor end private def system_raw(enable : Bool, mode = nil) - new_mode = mode || FileDescriptor.tcgetattr(fd) + new_mode = mode || system_tcgetattr if enable new_mode = FileDescriptor.cfmakeraw(new_mode) else @@ -308,7 +332,7 @@ module Crystal::System::FileDescriptor new_mode.c_oflag |= LibC::OPOST new_mode.c_lflag |= LibC::ECHO | LibC::ECHOE | LibC::ECHOK | LibC::ECHONL | LibC::ICANON | LibC::ISIG | LibC::IEXTEN end - if FileDescriptor.tcsetattr(fd, LibC::TCSANOW, pointerof(new_mode)) != 0 + if system_tcsetattr(LibC::TCSANOW, pointerof(new_mode)) != 0 raise IO::Error.from_errno("tcsetattr") end end @@ -322,16 +346,16 @@ module Crystal::System::FileDescriptor @[AlwaysInline] private def system_console_mode(&) - before = FileDescriptor.tcgetattr(fd) + before = system_tcgetattr begin yield before ensure - FileDescriptor.tcsetattr(fd, LibC::TCSANOW, pointerof(before)) + system_tcsetattr(LibC::TCSANOW, pointerof(before)) end end @[AlwaysInline] - def self.tcgetattr(fd) + private def system_tcgetattr termios = uninitialized LibC::Termios {% if LibC.has_method?(:tcgetattr) %} ret = LibC.tcgetattr(fd, pointerof(termios)) @@ -344,25 +368,27 @@ module Crystal::System::FileDescriptor end @[AlwaysInline] - def self.tcsetattr(fd, optional_actions, termios_p) - {% if LibC.has_method?(:tcsetattr) %} - LibC.tcsetattr(fd, optional_actions, termios_p) - {% else %} - optional_actions = optional_actions.value if optional_actions.is_a?(Termios::LineControl) - cmd = case optional_actions - when LibC::TCSANOW - LibC::TCSETS - when LibC::TCSADRAIN - LibC::TCSETSW - when LibC::TCSAFLUSH - LibC::TCSETSF - else - Errno.value = Errno::EINVAL - return LibC::Int.new(-1) - end - - LibC.ioctl(fd, cmd, termios_p) - {% end %} + private def system_tcsetattr(optional_actions, termios_p) + @fd_lock.reference do + {% if LibC.has_method?(:tcsetattr) %} + LibC.tcsetattr(fd, optional_actions, termios_p) + {% else %} + optional_actions = optional_actions.value if optional_actions.is_a?(Termios::LineControl) + cmd = case optional_actions + when LibC::TCSANOW + LibC::TCSETS + when LibC::TCSADRAIN + LibC::TCSETSW + when LibC::TCSAFLUSH + LibC::TCSETSF + else + Errno.value = Errno::EINVAL + return LibC::Int.new(-1) + end + + LibC.ioctl(fd, cmd, termios_p) + {% end %} + end end @[AlwaysInline] @@ -380,4 +406,12 @@ module Crystal::System::FileDescriptor {% end %} termios end + + private def system_read(slice : Bytes) : Int32 + @fd_lock.reference { event_loop.read(self, slice) } + end + + private def system_write(slice : Bytes) : Int32 + @fd_lock.reference { event_loop.write(self, slice) } + end end diff --git a/src/crystal/system/unix/socket.cr b/src/crystal/system/unix/socket.cr index 05379b48ae9f..06c82bdc4cfd 100644 --- a/src/crystal/system/unix/socket.cr +++ b/src/crystal/system/unix/socket.cr @@ -1,6 +1,7 @@ require "c/netdb" require "c/netinet/tcp" require "c/sys/socket" +require "crystal/fd_lock" module Crystal::System::Socket {% if IO.has_constant?(:Evented) %} @@ -9,6 +10,8 @@ module Crystal::System::Socket alias Handle = Int32 + @fd_lock = FdLock.new + def self.socket(family, type, protocol, blocking) : Handle {% if LibC.has_constant?(:SOCK_CLOEXEC) %} flags = type.value | LibC::SOCK_CLOEXEC @@ -20,8 +23,8 @@ module Crystal::System::Socket Process.lock_read do fd = LibC.socket(family, type, protocol) raise ::Socket::Error.from_errno("Failed to create socket") if fd == -1 - Socket.fcntl(fd, LibC::F_SETFD, LibC::FD_CLOEXEC) - Socket.fcntl(fd, LibC::F_SETFL, Socket.fcntl(fd, LibC::F_GETFL) | LibC::O_NONBLOCK) unless blocking + FileDescriptor.fcntl(fd, LibC::F_SETFD, LibC::FD_CLOEXEC) + FileDescriptor.fcntl(fd, LibC::F_SETFL, FileDescriptor.fcntl(fd, LibC::F_GETFL) | LibC::O_NONBLOCK) unless blocking fd end {% end %} @@ -36,25 +39,25 @@ module Crystal::System::Socket # Tries to bind the socket to a local address. # Yields an `Socket::BindError` if the binding failed. private def system_bind(addr, addrstr, &) - unless LibC.bind(fd, addr, addr.size) == 0 + unless @fd_lock.reference { LibC.bind(fd, addr, addr.size) } == 0 yield ::Socket::BindError.from_errno("Could not bind to '#{addrstr}'") end end private def system_listen(backlog, &) - unless LibC.listen(fd, backlog) == 0 + unless @fd_lock.reference { LibC.listen(fd, backlog) } == 0 yield ::Socket::Error.from_errno("Listen failed") end end private def system_close_read - if LibC.shutdown(fd, LibC::SHUT_RD) != 0 + if @fd_lock.reference { LibC.shutdown(fd, LibC::SHUT_RD) } != 0 raise ::Socket::Error.from_errno("shutdown read") end end private def system_close_write - if LibC.shutdown(fd, LibC::SHUT_WR) != 0 + if @fd_lock.reference { LibC.shutdown(fd, LibC::SHUT_WR) } != 0 raise ::Socket::Error.from_errno("shutdown write") end end @@ -84,7 +87,7 @@ module Crystal::System::Socket end private def system_reuse_port? : Bool - system_getsockopt(fd, LibC::SO_REUSEPORT, 0) do |value| + system_getsockopt(LibC::SO_REUSEPORT, 0) do |value| return value != 0 end @@ -135,62 +138,66 @@ module Crystal::System::Socket val end - private def system_getsockopt(fd, optname, optval, level = LibC::SOL_SOCKET, &) + private def system_getsockopt(optname, optval, level = LibC::SOL_SOCKET, &) optsize = LibC::SocklenT.new(sizeof(typeof(optval))) ret = LibC.getsockopt(fd, level, optname, pointerof(optval), pointerof(optsize)) yield optval if ret == 0 ret end - private def system_getsockopt(fd, optname, optval, level = LibC::SOL_SOCKET) - system_getsockopt(fd, optname, optval, level) { |value| return value } + private def system_getsockopt(optname, optval, level = LibC::SOL_SOCKET) + system_getsockopt(optname, optval, level) { |value| return value } raise ::Socket::Error.from_errno("getsockopt #{optname}") end - private def system_setsockopt(fd, optname, optval, level = LibC::SOL_SOCKET) + private def system_setsockopt(optname, optval, level = LibC::SOL_SOCKET) optsize = LibC::SocklenT.new(sizeof(typeof(optval))) - ret = LibC.setsockopt(fd, level, optname, pointerof(optval), optsize) + ret = @fd_lock.reference do + LibC.setsockopt(fd, level, optname, pointerof(optval), optsize) + end raise ::Socket::Error.from_errno("setsockopt #{optname}") if ret == -1 ret end private def system_blocking? - Socket.get_blocking(fd) + FileDescriptor.get_blocking(fd) end private def system_blocking=(value) - Socket.set_blocking(fd, value) + @fd_lock.reference do + FileDescriptor.set_blocking(fd, value) + end end def self.get_blocking(fd : Handle) - fcntl(fd, LibC::F_GETFL) & LibC::O_NONBLOCK == 0 + FileDescriptor.get_blocking(fd) end def self.set_blocking(fd : Handle, value : Bool) - flags = fcntl(fd, LibC::F_GETFL) - if value - flags &= ~LibC::O_NONBLOCK - else - flags |= LibC::O_NONBLOCK - end - fcntl(fd, LibC::F_SETFL, flags) + FileDescriptor.set_blocking(fd, value) end private def system_close_on_exec? - flags = fcntl(LibC::F_GETFD) + flags = FileDescriptor.fcntl(fd, 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) + system_fcntl(LibC::F_SETFD, arg ? LibC::FD_CLOEXEC : 0) arg end + private def system_fcntl(cmd, arg) + @fd_lock.reference { Socket.fcntl(fd, cmd, arg) } + end + def self.fcntl(fd, cmd, arg = 0) - r = LibC.fcntl fd, cmd, arg - raise ::Socket::Error.from_errno("fcntl() failed") if r == -1 - r + FileDescriptor.fcntl(fd, cmd, arg) + end + + private def system_fcntl(cmd, arg = 0) + @fd_lock.reference { FileDescriptor.fcntl(fd, cmd, arg) } end def self.socketpair(type : ::Socket::Type, protocol : ::Socket::Protocol, blocking : Bool) : {Handle, Handle} @@ -207,11 +214,11 @@ module Crystal::System::Socket if LibC.socketpair(::Socket::Family::UNIX, type, protocol, fds) == -1 raise ::Socket::Error.new("socketpair() failed") end - fcntl(fds[0], LibC::F_SETFD, LibC::FD_CLOEXEC) - fcntl(fds[1], LibC::F_SETFD, LibC::FD_CLOEXEC) + FileDescriptor.fcntl(fds[0], LibC::F_SETFD, LibC::FD_CLOEXEC) + FileDescriptor.fcntl(fds[1], LibC::F_SETFD, LibC::FD_CLOEXEC) unless blocking - fcntl(fds[0], LibC::F_SETFL, fcntl(fds[0], LibC::F_GETFL) | LibC::O_NONBLOCK) - fcntl(fds[1], LibC::F_SETFL, fcntl(fds[1], LibC::F_GETFL) | LibC::O_NONBLOCK) + FileDescriptor.fcntl(fds[0], LibC::F_SETFL, FileDescriptor.fcntl(fds[0], LibC::F_GETFL) | LibC::O_NONBLOCK) + FileDescriptor.fcntl(fds[1], LibC::F_SETFL, FileDescriptor.fcntl(fds[1], LibC::F_GETFL) | LibC::O_NONBLOCK) end end {% end %} @@ -224,7 +231,10 @@ module Crystal::System::Socket end private def system_close - event_loop.close(self) + if @fd_lock.try_close? { event_loop.before_close(self) } + event_loop.close(self) + end + @fd_lock.reset end def socket_close(&) @@ -347,4 +357,24 @@ module Crystal::System::Socket val end {% end %} + + private def system_send_to(bytes : Bytes, addr : ::Socket::Address) + @fd_lock.reference { event_loop.send_to(self, bytes, addr) } + end + + private def system_receive_from(bytes : Bytes) : Tuple(Int32, ::Socket::Address) + @fd_lock.reference { event_loop.receive_from(self, bytes) } + end + + private def system_connect(addr, timeout = nil) + @fd_lock.reference { event_loop.connect(self, addr, timeout) } + end + + private def system_read(slice : Bytes) : Int32 + @fd_lock.reference { event_loop.read(self, slice) } + end + + private def system_write(slice : Bytes) : Int32 + @fd_lock.reference { event_loop.write(self, slice) } + end end diff --git a/src/crystal/system/wasi/file_descriptor.cr b/src/crystal/system/wasi/file_descriptor.cr index 540efa95b9f3..e0f89acae806 100644 --- a/src/crystal/system/wasi/file_descriptor.cr +++ b/src/crystal/system/wasi/file_descriptor.cr @@ -8,9 +8,11 @@ module Crystal::System::FileDescriptor end def self.fcntl(fd, cmd, arg = 0) - r = LibC.fcntl(fd, cmd, arg) - raise IO::Error.from_errno("fcntl() failed") if r == -1 - r + FileDescriptor.fcntl(fd, cmd, arg) + end + + private def system_fcntl(cmd, arg = 0) + FileDescriptor.fcntl(fd, cmd, arg) end def self.get_blocking(fd : Handle) diff --git a/src/crystal/system/wasi/socket.cr b/src/crystal/system/wasi/socket.cr index edb811837c6c..2a49deecceb1 100644 --- a/src/crystal/system/wasi/socket.cr +++ b/src/crystal/system/wasi/socket.cr @@ -89,15 +89,15 @@ module Crystal::System::Socket raise NotImplementedError.new "Crystal::System::Socket#system_linge=" end - private def system_getsockopt(fd, optname, optval, level = LibC::SOL_SOCKET, &) + private def system_getsockopt(optname, optval, level = LibC::SOL_SOCKET, &) raise NotImplementedError.new "Crystal::System::Socket#system_getsockopt" end - private def system_getsockopt(fd, optname, optval, level = LibC::SOL_SOCKET) + private def system_getsockopt(optname, optval, level = LibC::SOL_SOCKET) raise NotImplementedError.new "Crystal::System::Socket#system_getsockopt" end - private def system_setsockopt(fd, optname, optval, level = LibC::SOL_SOCKET) + private def system_setsockopt(optname, optval, level = LibC::SOL_SOCKET) raise NotImplementedError.new "Crystal::System::Socket#system_setsockopt" end @@ -139,6 +139,10 @@ module Crystal::System::Socket r end + private def system_fcntl(cmd, arg = 0) + FileDescriptor.system_fcntl(fd, cmd, arg) + end + private def system_tty? LibC.isatty(fd) == 1 end diff --git a/src/crystal/system/win32/file_descriptor.cr b/src/crystal/system/win32/file_descriptor.cr index 266d8a73501e..0d841db510fd 100644 --- a/src/crystal/system/win32/file_descriptor.cr +++ b/src/crystal/system/win32/file_descriptor.cr @@ -154,6 +154,10 @@ module Crystal::System::FileDescriptor raise NotImplementedError.new "Crystal::System::FileDescriptor.fcntl" end + private def system_fcntl(cmd, arg = 0) + raise NotImplementedError.new "Crystal::System::FileDescriptor#system_fcntl" + end + protected def windows_handle LibC::HANDLE.new(fd) end diff --git a/src/crystal/system/win32/socket.cr b/src/crystal/system/win32/socket.cr index 08599c3a803b..4405f899ec26 100644 --- a/src/crystal/system/win32/socket.cr +++ b/src/crystal/system/win32/socket.cr @@ -86,9 +86,9 @@ module Crystal::System::Socket @blocking = blocking unless blocking.nil? unless @family.unix? - system_getsockopt(handle, LibC::SO_REUSEADDR, 0) do |value| + Socket.getsockopt(handle, LibC::SO_REUSEADDR, 0) do |value| if value == 0 - system_setsockopt(handle, LibC::SO_EXCLUSIVEADDRUSE, 1) + Socket.setsockopt(handle, LibC::SO_EXCLUSIVEADDRUSE, 1) end end end @@ -310,20 +310,28 @@ module Crystal::System::Socket val end - private def system_getsockopt(handle, optname, optval, level = LibC::SOL_SOCKET, &) + private def system_getsockopt(optname, optval, level = LibC::SOL_SOCKET, &) + Socket.getsockopt(fd, optname, optval, level) { |value| yield value } + end + + private def system_getsockopt(optname, optval, level = LibC::SOL_SOCKET) + Socket.getsockopt(fd, optname, optval, level) { |value| return value } + raise ::Socket::Error.from_wsa_error("getsockopt #{optname}") + end + + private def system_setsockopt(optname, optval, level = LibC::SOL_SOCKET) + Socket.setsockopt(fd, optname, optval, level) + end + + protected def self.getsockopt(handle, optname, optval, level = LibC::SOL_SOCKET, &) optsize = sizeof(typeof(optval)) ret = LibC.getsockopt(handle, level, optname, pointerof(optval).as(UInt8*), pointerof(optsize)) yield optval if ret == 0 ret end - private def system_getsockopt(fd, optname, optval, level = LibC::SOL_SOCKET) - system_getsockopt(fd, optname, optval, level) { |value| return value } - raise ::Socket::Error.from_wsa_error("getsockopt #{optname}") - end - # :nodoc: - def system_setsockopt(handle, optname, optval, level = LibC::SOL_SOCKET) + protected def self.setsockopt(handle, optname, optval, level = LibC::SOL_SOCKET) optsize = sizeof(typeof(optval)) ret = LibC.setsockopt(handle, level, optname, pointerof(optval).as(UInt8*), optsize) @@ -369,6 +377,10 @@ module Crystal::System::Socket raise NotImplementedError.new "Crystal::System::Socket.fcntl" end + private def system_fcntl(cmd, arg = 0) + raise NotImplementedError.new "Crystal::System::Socket#system_fcntl" + end + private def system_tty? LibC.GetConsoleMode(LibC::HANDLE.new(fd), out _) != 0 end diff --git a/src/io/console.cr b/src/io/console.cr index f6e2a11ee033..72655f955e0f 100644 --- a/src/io/console.cr +++ b/src/io/console.cr @@ -92,7 +92,7 @@ class IO::FileDescriptor < IO @[Deprecated] macro noecho_from_tc_mode! mode.c_lflag &= ~(Termios::LocalMode.flags(ECHO, ECHOE, ECHOK, ECHONL).value) - Crystal::System::FileDescriptor.tcsetattr(fd, Termios::LineControl::TCSANOW, pointerof(mode)) + system_tcsetattr(Termios::LineControl::TCSANOW, pointerof(mode)) end @[Deprecated] @@ -109,12 +109,12 @@ class IO::FileDescriptor < IO Termios::LocalMode::ICANON | Termios::LocalMode::ISIG | Termios::LocalMode::IEXTEN).value - Crystal::System::FileDescriptor.tcsetattr(fd, Termios::LineControl::TCSANOW, pointerof(mode)) + system_tcsetattr(Termios::LineControl::TCSANOW, pointerof(mode)) end @[Deprecated] macro raw_from_tc_mode! Crystal::System::FileDescriptor.cfmakeraw(pointerof(mode)) - Crystal::System::FileDescriptor.tcsetattr(fd, Termios::LineControl::TCSANOW, pointerof(mode)) + system_tcsetattr(Termios::LineControl::TCSANOW, pointerof(mode)) end end diff --git a/src/io/file_descriptor.cr b/src/io/file_descriptor.cr index a39529e4103f..b3f80376b153 100644 --- a/src/io/file_descriptor.cr +++ b/src/io/file_descriptor.cr @@ -133,7 +133,7 @@ class IO::FileDescriptor < IO end def fcntl(cmd : Int, arg : Int = 0) : Int - Crystal::System::FileDescriptor.fcntl(fd, cmd, arg) + system_fcntl(cmd, arg) end # Returns a `File::Info` object for this file descriptor, or raises diff --git a/src/socket.cr b/src/socket.cr index 660827c8b6e7..e5581b7badbe 100644 --- a/src/socket.cr +++ b/src/socket.cr @@ -416,25 +416,25 @@ class Socket < IO # Returns the modified *optval*. protected def getsockopt(optname, optval, level = LibC::SOL_SOCKET) - system_getsockopt(fd, optname, optval, level) + system_getsockopt(optname, optval, level) end protected def getsockopt(optname, optval, level = LibC::SOL_SOCKET, &) - system_getsockopt(fd, optname, optval, level) { |value| yield value } + system_getsockopt(optname, optval, level) { |value| yield value } end protected def setsockopt(optname, optval, level = LibC::SOL_SOCKET) - system_setsockopt(fd, optname, optval, level) + system_setsockopt(optname, optval, level) end private def getsockopt_bool(optname, level = LibC::SOL_SOCKET) - ret = getsockopt optname, 0, level + ret = system_getsockopt optname, 0, level ret != 0 end private def setsockopt_bool(optname, optval : Bool, level = LibC::SOL_SOCKET) v = optval ? 1 : 0 - setsockopt optname, v, level + system_setsockopt optname, v, level optval end @@ -482,7 +482,7 @@ class Socket < IO end def fcntl(cmd, arg = 0) - self.class.fcntl fd, cmd, arg + system_fcntl(cmd, arg) end # Finalizes the socket resource.