diff --git a/doc/file/symbolic_links.md b/doc/file/symbolic_links.md new file mode 100644 index 00000000000000..74df11f5d8111b --- /dev/null +++ b/doc/file/symbolic_links.md @@ -0,0 +1,289 @@ +# Symbolic Links + +A [symbolic link][symbolic link] (sometimes called a _symlink_ or _soft link_) +is a filesystem entry that stores a filesystem path. +The stored path refers to a _target_ filesystem entry, which may or may not exist. +Further, the stored path need not even be a valid path. + +## Methods That Follow Symlinks + +Most Ruby methods that deal with filesystem paths "follow" symbolic links; +that is, if the path refers to a symlink, the method does not operate on the entry +at that path, but instead operates on the path stored in the symlink: + +```ruby +File.symlink('README.md', 'foo') +File.read('foo').size # => 3463 # Size of README.md, not foo. +File.delete('foo') +``` + +## Creating Symlinks + +Each of these methods creates a symlink: + +- File::symlink +- FileUtils::ln_s +- Pathname#make_symlink + +If the target path is itself a symlink, that symlink is not followed. + +Examples: + +```ruby +# Symlink for a file. +File.symlink('README.md', 'foo') +# Symlink for another symlink. +File.symlink('foo', 'bar') +File.read('README.md').size # => 3463 +File.read('foo').size # => 3463 +File.read('bar').size # => 3463 +# Symlink for a directory. +File.symlink('doc/', 'baz') +Dir.entries('doc/').size # => 33 +Dir.entries('baz').size # => 33 +File.unlink('foo', 'bar', 'baz') # Clean up. +``` + +## Querying Symlinks + +### `lstat` + +Each of these methods creates a File::Stat object for a filesystem entry: + +- File::lstat +- Pathname#lstat + +The object contains information for the entry at the given path, +even if that entry is a symlink; +i.e., symlinks are not followed. + +By contrast, each of the methods File::stat and Pathname#stat +_do_ follow symlinks. + +Examples: + +```ruby +linkpath = 'foo' +File.symlink('README.md', linkpath) +File::stat(linkpath).size # => 3469 # Size of file README.md. +File::lstat(linkpath).size # => 9 # Size of symlink linkpath. +File.unlink(linkpath) # Clean up. +``` + +### `symlink?` + +Each of these methods returns whether the entry at a given path is a symlink: + +- File::symlink? +- File::Stat#symlink? +- Pathname#symlink? + +If the entry is a symlink, it is not followed. + +Examples: + +```ruby +linkpath = 'foo' +File.symlink('README.md', 'foo') +File.symlink?(linkpath) # => true +File.symlink?('README.md') # => false +File.symlink?('nosuch') # => false +File.delete(linkpath) # Clean up. +``` + +### `readlink` + +Each of these methods returns the path stored in a symlink: + +- File::readlink +- Pathname#readlink + +Examples: + +```ruby +linkpath = 'foo' +File.symlink('README.md', 'foo') +File.readlink(linkpath) # => "README.md" +File.delete(linkpath) # Clean up. +``` + +## Modifying Symlinks + +### `lchmod` + +Each of these methods changes the mode of the symlink entry: + +- File::lchmod +- Pathname#lchmod + +These methods are not supported on Windows or Linux (raise NotImplementedError). + +### `lchown` + +Each of these methods changes the ownership of a symlink entry: + +- File::lchown +- Pathname#lchown + +Example: + +```ruby +# Super user; all privileges. +Process.uid # => 0 +Process.gid # => 0 +# Create regular file and symlink to it. +filepath = 't.tmp' +linkpath = 'foo' +File.write(filepath, '') +File.symlink(filepath, linkpath) +# Capture original statuses. +fstat0 = File.stat(filepath) +lstat0 = File.lstat(linkpath) +fstat0.uid # => 0 +fstat0.gid # => 0 +lstat0.uid # => 0 +lstat0.gid # => 0 +# Change owner for the symlink. +File.lchown(1000, 1000, linkpath) +# Capture new statuses. +fstat1 = File.stat(filepath) +lstat1 = File.lstat(linkpath) +# User id and group id for file not changed. +fstat1.uid # => 0 +fstat1.gid # => 0 +# User id and group id for link changed. +lstat1.uid # => 1000 +lstat1.gid # => 1000 +# Clean up. +File.delete(filepath, linkpath) +``` + +### `lutime` + +Each of these methods updates timestamps for a symlink entry: + +- File::lutime +- Pathname#lutime + +Example: + +```ruby +filepath = 'README.md' +linkpath = 'foo' +File.symlink(filepath, linkpath) +# Take snapshots of both. +fstat0 = File.stat(filepath) +lstat0 = File.lstat(linkpath) +# Fetch access times and modification times of both. +fstat0.atime # => 2026-09-03 07:46:04.940377552 -0500 +fstat0.mtime # => 2026-09-01 09:09:28.378987388 -0500 +lstat0.atime # => 2026-09-03 09:14:13.753865727 -0500 +lstat0.mtime # => 2026-09-03 09:14:13.753865727 -0500 +# Update access time and modification time of the symlink. +time = Time.now # => 2026-09-03 09:16:42.619702232 -0500 +File.lutime(time, time, linkpath) +# Take fresh snapshots of both. +fstat1 = File.stat(filepath) +lstat1 = File.lstat(linkpath) +# Fetch access time and modification time of file (not changed). +fstat1.atime # => 2026-09-03 07:46:04.940377552 -0500 +fstat1.mtime # => 2026-09-01 09:09:28.378987388 -0500 +# Fetch access time and modification time of link (changed). +lstat1.atime # => 2026-09-03 09:16:52.77029217 -0500 +lstat1.mtime # => 2026-09-03 09:16:42.619702232 -0500 +# Clean up. +File.delete(linkpath) +``` + +### `rename` + +Each of these methods changes the name of an entry (which need not be a symlink): + +- File::rename +- Pathname#rename + +Examples: + +```ruby +File.symlink('README.md', 'foo') +File.rename('foo', 'bar') +File.symlink?('bar') # => true +File.rename('bar', 'baz') +File.symlink?('baz') # => true +File.delete('baz') # Clean up. +``` + +## Removing Symlinks + +### `unlink` + +Each of these methods removes an entry (which need not be a symlink): + +- File::delete +- File::unlink +- Pathname#unlink (aliased as Pathname#delete) + +Example: + +```ruby +linkpath = 'foo' +File.symlink('README.md', linkpath) +File.unlink(linkpath) +``` + +## Methods That Don't Follow Symlinks + +Sometimes it's necessary to query, modify, or delete a symlink; +therefore certain Ruby methods do not follow symlinks, +but instead operate directly on the symlinks. + +### Symlink-Specific Methods + +Each method in the table below has a symlink-specific purpose. + +| Method Name | Effect | +|-----------------------|-----------------------------------------------------| +| File::Stat#symlink? | Returns whether a path is a symlink. | +| File::lchmod | Changes the mode of the symlink. See Note 1. | +| File::lchown | Changes the ownership of the symlink. See Note 2. | +| File::lstat | Creates a File::Stat object for a link. See Note 3. | +| File::lutime | Updates timestamps for the symlink. See Note 4. | +| File::readlink | Returns the path stored in a symlink. | +| File::symlink | Creates a symlink. | +| File::symlink? | Returns whether a path is a symlink. | +| FileUtils::ln_s | Creates a symlink. | +| FileUtils::ln_sf | Creates a symlink. | +| FileUtils::ln_sr | Creates a symlink. | +| Pathname#lchmod | Changes the mode of the symlink. See Note 1. | +| Pathname#lchown | Changes the ownership of the symlink. See Note 2. | +| Pathname#lstat | Creates a File::Stat object for a link. See Note 3. | +| Pathname#lutime | Updates timestamps for the symlink. See Note 4. | +| Pathname#make_symlink | Creates a symlink. | +| Pathname#readlink | Returns the path stored in a symlink. | +| Pathname#symlink? | Returns whether a path is a symlink. | + +Notes: + +1. File::chmod and Pathname#chmod follow symlinks before changing the mode. +1. File::chown and Pathname#chown follow symlinks before changing the ownership. +1. File::stat and Pathname#stat follow symlinks before creating the File::Stat object. +1. File::utime and Pathname#utime follow symlinks before updating timestamps. + +### Other Non-Following Methods + +The methods in the table below do not follow symlinks, +but instead operate directly on the entry at the path +(which may or may not be a symlink). + +| Method | Effect | +|----------------------------------------------|----------------------------------| +| File::delete | Removes the entry. | +| File::link | Creates a hard link. | +| File::rename | Changes the name of the entry. | +| File::unlink | Removes the entry. | +| FileUtils::link_entry | Creates a hard link. | +| FileUtils::ln (aliased as FileUtils.link) | Creates a hard link. | +| Pathname#rename | Changes the name of the entry. | +| Pathname#unlink (aliased as Pathname#delete) | Removes the entry. | + +[symbolic link]: https://en.wikipedia.org/wiki/Symbolic_link diff --git a/file.c b/file.c index cd206a692bb1cf..167cc32933b823 100644 --- a/file.c +++ b/file.c @@ -3119,7 +3119,7 @@ lchmod_internal(const char *path, void *mode) * call-seq: * File.lchmod(mode, *paths) -> paths_count * - * Not supported on some platforms (raises Errno:: ENOTSUP). + * Not supported on some platforms (raises NotImplementedError). * * When supported: like File::chmod, but does not follow symbolic links, * and therefore changes the mode of the entries given by `paths`; diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 79264a18882f06..39ff95fd3b47c0 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -1543,7 +1543,7 @@ def chmod(mode) File.chmod(mode, @path) end # call-seq: # lchmod(mode) -> 1 # - # Not supported on some platforms (raises Errno::ENOTSUP). + # Not supported on some platforms (raises NotImplementedError). # # When supported: like Pathname::chmod, but does not follow symbolic links, # and therefore changes the mode of the entry specified by `self`: diff --git a/struct.c b/struct.c index 84a0940ba7ae85..8bc1694700bdc5 100644 --- a/struct.c +++ b/struct.c @@ -831,12 +831,7 @@ struct_alloc(VALUE klass) { long n = num_members(klass); size_t embedded_size = offsetof(struct RStruct, as.ary) + (sizeof(VALUE) * n); - if (RCLASS_MAX_IV_COUNT(klass) > 0) { - embedded_size += sizeof(VALUE); - } - VALUE flags = T_STRUCT; - const long embed_len_max = RSTRUCT_EMBED_LEN_MASK >> RSTRUCT_EMBED_LEN_SHIFT; if (n > 0 && n <= embed_len_max && rb_gc_size_allocatable_p(embedded_size)) { diff --git a/test/-ext-/wait/test_wait.rb b/test/-ext-/wait/test_wait.rb index 8e53f067cf37b2..a0bbfcc302c3d6 100644 --- a/test/-ext-/wait/test_wait.rb +++ b/test/-ext-/wait/test_wait.rb @@ -26,6 +26,27 @@ def test_wait_for_invalid_fd RUBY end + def test_wait_for_invalid_fd_in_ractor + # Threads of a Ractor always run on the M:N scheduler, whose readiness + # probe must not report a closed fd as ready. + assert_ractor(<<~'RUBY') + error = Ractor.new do + r, w = IO.pipe + r.close + IO.for_fd(w.fileno).close + + begin + w.wait_writable + nil + rescue SystemCallError => e + e.class + end + end.value + + assert_equal Errno::EBADF, error + RUBY + end + def test_wait_for_closed_pipe IO.pipe do |r,w| w.close diff --git a/thread_sched_mn.c b/thread_sched_mn.c index 038fe0b5fba1df..bac0e9eeaf63c4 100644 --- a/thread_sched_mn.c +++ b/thread_sched_mn.c @@ -1343,23 +1343,16 @@ fd_waiters_arm(int fd, struct rb_fd_waiters *e, uint32_t want, bool consumed) } static bool -fd_readable_nonblock(int fd) +fd_ready_nonblock(int fd, short events) { struct pollfd pfd = { .fd = fd, - .events = POLLIN, + .events = events, }; - return poll(&pfd, 1, 0) != 0; -} -static bool -fd_writable_nonblock(int fd) -{ - struct pollfd pfd = { - .fd = fd, - .events = POLLOUT, - }; - return poll(&pfd, 1, 0) != 0; + // A "ready" answer makes the caller report the requested event, so a + // closed fd (POLLNVAL) must not count: it owes the caller EBADF. + return poll(&pfd, 1, 0) > 0 && !(pfd.revents & POLLNVAL); } static void @@ -1479,16 +1472,16 @@ timer_thread_register_waiting(rb_thread_t *th, int fd, enum thread_sched_waiting } if (flags & thread_sched_waiting_io_read) { - if (!(flags & thread_sched_waiting_io_force) && fd_readable_nonblock(fd)) { - RUBY_DEBUG_LOG("fd_readable_nonblock"); + if (!(flags & thread_sched_waiting_io_force) && fd_ready_nonblock(fd, POLLIN)) { + RUBY_DEBUG_LOG("fd readable"); return timer_thread_already_ready; } VM_ASSERT(fd >= 0); } if (flags & thread_sched_waiting_io_write) { - if (!(flags & thread_sched_waiting_io_force) && fd_writable_nonblock(fd)) { - RUBY_DEBUG_LOG("fd_writable_nonblock"); + if (!(flags & thread_sched_waiting_io_force) && fd_ready_nonblock(fd, POLLOUT)) { + RUBY_DEBUG_LOG("fd writable"); return timer_thread_already_ready; } VM_ASSERT(fd >= 0);