├── .git-blame-ignore-revs ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── COPYING ├── Gemfile ├── README.md ├── Rakefile ├── ext ├── io │ └── wait │ │ ├── depend │ │ ├── extconf.rb │ │ └── wait.c └── java │ ├── lib │ └── io │ │ └── wait.rb │ └── org │ └── jruby │ └── ext │ └── io │ └── wait │ └── IOWaitLibrary.java ├── io-wait.gemspec ├── rakelib ├── changelogs.rake ├── checksum.rake ├── epoch.rake ├── sync_tool.rake └── version.rake └── test ├── io └── wait │ ├── test_io_wait.rb │ ├── test_io_wait_uncommon.rb │ └── test_ractor.rb └── lib └── helper.rb /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # This is a file used by GitHub to ignore the following commits on `git blame`. 2 | # 3 | # You can also do the same thing in your local repository with: 4 | # $ git config --local blame.ignoreRevsFile .git-blame-ignore-revs 5 | 6 | # Expand tabs 7 | e8ab2ab28a1481cc10b06e9a9add302b1ef0dbf7 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruby-versions: 7 | if: ${{ startsWith(github.repository, 'ruby/') || github.event_name != 'schedule' }} 8 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 9 | with: 10 | engine: cruby 11 | min_version: 2.6 12 | 13 | test: 14 | needs: ruby-versions 15 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 16 | runs-on: ${{ matrix.os }}-latest 17 | strategy: 18 | matrix: 19 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 20 | os: [ ubuntu, macos, windows ] 21 | include: 22 | - ruby: 'jruby-9.3' 23 | os: 'ubuntu' 24 | platform: 'java' 25 | upload: true 26 | - ruby: 'jruby-9.4' 27 | os: 'ubuntu' 28 | platform: 'java' 29 | continue-on-error: true 30 | outputs: 31 | gem: ${{steps.build.outputs.gem}} 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Set up Ruby ${{ matrix.ruby }} 35 | uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: ${{ matrix.ruby }} 38 | - name: Set up Bundler 39 | run: bundle install 40 | - name: Run test 41 | run: bundle exec rake 42 | continue-on-error: ${{ matrix.continue-on-error }} 43 | - id: build 44 | run: | 45 | rake build:sha512 46 | cat pkg/SHA512 47 | gem=${GITHUB_REPOSITORY#*/} 48 | echo "gem=$gem" >> $GITHUB_OUTPUT 49 | echo "pkg=$gem-${RUNNING_OS%-*}" >> $GITHUB_OUTPUT 50 | env: 51 | RUNNING_OS: ${{matrix.platform || matrix.os}} 52 | if: ${{matrix.ruby == needs.ruby-versions.outputs.latest || matrix.upload}} 53 | shell: bash 54 | - name: Upload package 55 | uses: actions/upload-artifact@v4 56 | with: 57 | path: pkg/* 58 | name: ${{steps.build.outputs.pkg}} 59 | if: steps.build.outputs.pkg 60 | 61 | install: 62 | runs-on: ${{ matrix.os }}-latest 63 | needs: test 64 | strategy: 65 | matrix: 66 | ruby: [ '2.5' ] 67 | os: [ ubuntu, windows ] 68 | steps: 69 | - uses: actions/checkout@v4 70 | - name: Set up Ruby 71 | uses: ruby/setup-ruby@v1 72 | with: 73 | ruby-version: ${{ matrix.ruby }} 74 | - uses: actions/download-artifact@master 75 | with: 76 | name: ${{needs.test.outputs.gem}}-macos 77 | path: pkg 78 | - name: Install gem 79 | run: gem install pkg/${{needs.test.outputs.gem}}*.gem 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /lib/ 7 | /logs/ 8 | /pkg/ 9 | /spec/reports/ 10 | /tmp/ 11 | *.bundle 12 | *.dll 13 | *.so 14 | *.jar 15 | ChangeLog 16 | Gemfile.lock 17 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a. place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b. use the modified software only within your corporation or 18 | organization. 19 | 20 | c. give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d. make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a. distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b. accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c. give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d. make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in io-wait.gemspec 4 | gemspec 5 | 6 | group :development do 7 | gem "rake" 8 | gem "rake-compiler" 9 | gem "test-unit" 10 | gem "test-unit-ruby-core" 11 | gem 'ruby-maven', :platforms => :jruby 12 | end 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # io-wait 2 | 3 | This gem provides the feature for waiting until IO is readable or writable without blocking. 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | ```ruby 10 | gem 'io-wait' 11 | ``` 12 | 13 | And then execute: 14 | 15 | $ bundle install 16 | 17 | Or install it yourself as: 18 | 19 | $ gem install io-wait 20 | 21 | ## Development 22 | 23 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 24 | 25 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 26 | 27 | ## Contributing 28 | 29 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/io-wait. 30 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | name = "io/wait" 5 | 6 | case 7 | when RUBY_ENGINE == "jruby" 8 | require 'rake/javaextensiontask' 9 | Rake::JavaExtensionTask.new("wait") do |ext| 10 | require 'maven/ruby/maven' 11 | ext.source_version = '1.8' 12 | ext.target_version = '1.8' 13 | ext.ext_dir = 'ext/java' 14 | ext.lib_dir = 'lib/io' 15 | end 16 | task "build" => "lib/io/wait.jar" 17 | task "lib/io/wait.jar" => "compile" 18 | libs = ["ext/java/lib", "lib"] 19 | when RUBY_VERSION < "2.6" 20 | task :compile do 21 | # noop 22 | end 23 | 24 | libs = [] 25 | else 26 | require 'rake/extensiontask' 27 | extask = Rake::ExtensionTask.new(name) do |x| 28 | x.lib_dir.sub!(%r[(?=/|\z)], "/#{RUBY_VERSION}/#{x.platform}") 29 | end 30 | libs = ["lib/#{RUBY_VERSION}/#{extask.platform}"] 31 | end 32 | 33 | Rake::TestTask.new(:test) do |t| 34 | t.libs = libs 35 | t.libs << "test/lib" 36 | t.ruby_opts << "-rhelper" 37 | t.test_files = FileList["test/**/test_*.rb"] 38 | end 39 | 40 | task :test => :compile 41 | 42 | task :default => :test 43 | -------------------------------------------------------------------------------- /ext/io/wait/depend: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED DEPENDENCIES START 2 | wait.o: $(RUBY_EXTCONF_H) 3 | wait.o: $(arch_hdrdir)/ruby/config.h 4 | wait.o: $(hdrdir)/ruby.h 5 | # wait.o: $(hdrdir)/ruby/assert.h # not in 2.6 6 | wait.o: $(hdrdir)/ruby/backward.h 7 | wait.o: $(hdrdir)/ruby/defines.h 8 | wait.o: $(hdrdir)/ruby/encoding.h 9 | wait.o: $(hdrdir)/ruby/intern.h 10 | wait.o: $(hdrdir)/ruby/io.h 11 | wait.o: $(hdrdir)/ruby/missing.h 12 | wait.o: $(hdrdir)/ruby/onigmo.h 13 | wait.o: $(hdrdir)/ruby/oniguruma.h 14 | wait.o: $(hdrdir)/ruby/ruby.h 15 | wait.o: $(hdrdir)/ruby/st.h 16 | wait.o: $(hdrdir)/ruby/subst.h 17 | wait.o: wait.c 18 | # AUTOGENERATED DEPENDENCIES END 19 | -------------------------------------------------------------------------------- /ext/io/wait/extconf.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'mkmf' 3 | 4 | if RUBY_VERSION < "2.6" 5 | File.write("Makefile", dummy_makefile($srcdir).join("")) 6 | else 7 | target = "io/wait" 8 | have_func("rb_io_wait") 9 | have_func("rb_io_descriptor") 10 | unless macro_defined?("DOSISH", "#include ") 11 | have_header(ioctl_h = "sys/ioctl.h") or ioctl_h = nil 12 | fionread = %w[sys/ioctl.h sys/filio.h sys/socket.h].find do |h| 13 | have_macro("FIONREAD", [h, ioctl_h].compact) 14 | end 15 | if fionread 16 | $defs << "-DFIONREAD_HEADER=\"<#{fionread}>\"" 17 | create_makefile(target) 18 | end 19 | else 20 | if have_func("rb_w32_ioctlsocket", "ruby.h") 21 | have_func("rb_w32_is_socket", "ruby.h") 22 | create_makefile(target) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /ext/io/wait/wait.c: -------------------------------------------------------------------------------- 1 | /* -*- c-file-style: "ruby"; indent-tabs-mode: t -*- */ 2 | /********************************************************************** 3 | 4 | io/wait.c - 5 | 6 | $Author$ 7 | created at: Tue Aug 28 09:08:06 JST 2001 8 | 9 | All the files in this distribution are covered under the Ruby's 10 | license (see the file COPYING). 11 | 12 | **********************************************************************/ 13 | 14 | #include "ruby.h" 15 | #include "ruby/io.h" 16 | 17 | #include 18 | #if defined(HAVE_UNISTD_H) && (defined(__sun)) 19 | #include 20 | #endif 21 | #if defined(HAVE_SYS_IOCTL_H) 22 | #include 23 | #endif 24 | #if defined(FIONREAD_HEADER) 25 | #include FIONREAD_HEADER 26 | #endif 27 | 28 | #ifdef HAVE_RB_W32_IOCTLSOCKET 29 | #define ioctl ioctlsocket 30 | #define ioctl_arg u_long 31 | #define ioctl_arg2num(i) ULONG2NUM(i) 32 | #else 33 | #define ioctl_arg int 34 | #define ioctl_arg2num(i) INT2NUM(i) 35 | #endif 36 | 37 | #ifdef HAVE_RB_W32_IS_SOCKET 38 | #define FIONREAD_POSSIBLE_P(fd) rb_w32_is_socket(fd) 39 | #else 40 | #define FIONREAD_POSSIBLE_P(fd) ((void)(fd),Qtrue) 41 | #endif 42 | 43 | #ifndef HAVE_RB_IO_WAIT 44 | static struct timeval * 45 | get_timeout(int argc, VALUE *argv, struct timeval *timerec) 46 | { 47 | VALUE timeout = Qnil; 48 | rb_check_arity(argc, 0, 1); 49 | if (!argc || NIL_P(timeout = argv[0])) { 50 | return NULL; 51 | } 52 | else { 53 | *timerec = rb_time_interval(timeout); 54 | return timerec; 55 | } 56 | } 57 | 58 | static int 59 | wait_for_single_fd(rb_io_t *fptr, int events, struct timeval *tv) 60 | { 61 | int i = rb_wait_for_single_fd(fptr->fd, events, tv); 62 | if (i < 0) 63 | rb_sys_fail(0); 64 | rb_io_check_closed(fptr); 65 | return (i & events); 66 | } 67 | #endif 68 | 69 | /* 70 | * call-seq: 71 | * io.nread -> int 72 | * 73 | * Returns number of bytes that can be read without blocking. 74 | * Returns zero if no information available. 75 | * 76 | * You must require 'io/wait' to use this method. 77 | */ 78 | 79 | static VALUE 80 | io_nread(VALUE io) 81 | { 82 | rb_io_t *fptr; 83 | int len; 84 | ioctl_arg n; 85 | 86 | GetOpenFile(io, fptr); 87 | rb_io_check_readable(fptr); 88 | len = rb_io_read_pending(fptr); 89 | if (len > 0) return INT2FIX(len); 90 | 91 | #ifdef HAVE_RB_IO_DESCRIPTOR 92 | int fd = rb_io_descriptor(io); 93 | #else 94 | int fd = fptr->fd; 95 | #endif 96 | 97 | if (!FIONREAD_POSSIBLE_P(fd)) return INT2FIX(0); 98 | if (ioctl(fd, FIONREAD, &n)) return INT2FIX(0); 99 | if (n > 0) return ioctl_arg2num(n); 100 | return INT2FIX(0); 101 | } 102 | 103 | #ifdef HAVE_RB_IO_WAIT 104 | static VALUE 105 | io_wait_event(VALUE io, int event, VALUE timeout, int return_io) 106 | { 107 | VALUE result = rb_io_wait(io, RB_INT2NUM(event), timeout); 108 | 109 | if (!RB_TEST(result)) { 110 | return Qnil; 111 | } 112 | 113 | int mask = RB_NUM2INT(result); 114 | 115 | if (mask & event) { 116 | if (return_io) 117 | return io; 118 | else 119 | return result; 120 | } 121 | else { 122 | return Qfalse; 123 | } 124 | } 125 | #endif 126 | 127 | /* 128 | * call-seq: 129 | * io.ready? -> truthy or falsy 130 | * 131 | * Returns a truthy value if input available without blocking, or a 132 | * falsy value. 133 | * 134 | * You must require 'io/wait' to use this method. 135 | */ 136 | 137 | static VALUE 138 | io_ready_p(VALUE io) 139 | { 140 | rb_io_t *fptr; 141 | #ifndef HAVE_RB_IO_WAIT 142 | struct timeval tv = {0, 0}; 143 | #endif 144 | 145 | GetOpenFile(io, fptr); 146 | rb_io_check_readable(fptr); 147 | if (rb_io_read_pending(fptr)) return Qtrue; 148 | 149 | #ifndef HAVE_RB_IO_WAIT 150 | return wait_for_single_fd(fptr, RB_WAITFD_IN, &tv) ? Qtrue : Qfalse; 151 | #else 152 | return io_wait_event(io, RUBY_IO_READABLE, RB_INT2NUM(0), 1); 153 | #endif 154 | } 155 | 156 | /* Ruby 3.2+ can define these methods. This macro indicates that case. */ 157 | #ifndef RUBY_IO_WAIT_METHODS 158 | 159 | /* 160 | * call-seq: 161 | * io.wait_readable -> truthy or falsy 162 | * io.wait_readable(timeout) -> truthy or falsy 163 | * 164 | * Waits until IO is readable and returns a truthy value, or a falsy 165 | * value when times out. Returns a truthy value immediately when 166 | * buffered data is available. 167 | * 168 | * You must require 'io/wait' to use this method. 169 | */ 170 | 171 | static VALUE 172 | io_wait_readable(int argc, VALUE *argv, VALUE io) 173 | { 174 | rb_io_t *fptr; 175 | #ifndef HAVE_RB_IO_WAIT 176 | struct timeval timerec; 177 | struct timeval *tv; 178 | #endif 179 | 180 | GetOpenFile(io, fptr); 181 | rb_io_check_readable(fptr); 182 | 183 | #ifndef HAVE_RB_IO_WAIT 184 | tv = get_timeout(argc, argv, &timerec); 185 | #endif 186 | if (rb_io_read_pending(fptr)) return Qtrue; 187 | 188 | #ifndef HAVE_RB_IO_WAIT 189 | if (wait_for_single_fd(fptr, RB_WAITFD_IN, tv)) { 190 | return io; 191 | } 192 | return Qnil; 193 | #else 194 | rb_check_arity(argc, 0, 1); 195 | VALUE timeout = (argc == 1 ? argv[0] : Qnil); 196 | 197 | return io_wait_event(io, RUBY_IO_READABLE, timeout, 1); 198 | #endif 199 | } 200 | 201 | /* 202 | * call-seq: 203 | * io.wait_writable -> truthy or falsy 204 | * io.wait_writable(timeout) -> truthy or falsy 205 | * 206 | * Waits until IO is writable and returns a truthy value or a falsy 207 | * value when times out. 208 | * 209 | * You must require 'io/wait' to use this method. 210 | */ 211 | static VALUE 212 | io_wait_writable(int argc, VALUE *argv, VALUE io) 213 | { 214 | rb_io_t *fptr; 215 | #ifndef HAVE_RB_IO_WAIT 216 | struct timeval timerec; 217 | struct timeval *tv; 218 | #endif 219 | 220 | GetOpenFile(io, fptr); 221 | rb_io_check_writable(fptr); 222 | 223 | #ifndef HAVE_RB_IO_WAIT 224 | tv = get_timeout(argc, argv, &timerec); 225 | if (wait_for_single_fd(fptr, RB_WAITFD_OUT, tv)) { 226 | return io; 227 | } 228 | return Qnil; 229 | #else 230 | rb_check_arity(argc, 0, 1); 231 | VALUE timeout = (argc == 1 ? argv[0] : Qnil); 232 | 233 | return io_wait_event(io, RUBY_IO_WRITABLE, timeout, 1); 234 | #endif 235 | } 236 | 237 | #ifdef HAVE_RB_IO_WAIT 238 | /* 239 | * call-seq: 240 | * io.wait_priority -> truthy or falsy 241 | * io.wait_priority(timeout) -> truthy or falsy 242 | * 243 | * Waits until IO is priority and returns a truthy value or a falsy 244 | * value when times out. Priority data is sent and received using 245 | * the Socket::MSG_OOB flag and is typically limited to streams. 246 | * 247 | * You must require 'io/wait' to use this method. 248 | */ 249 | static VALUE 250 | io_wait_priority(int argc, VALUE *argv, VALUE io) 251 | { 252 | rb_io_t *fptr = NULL; 253 | 254 | RB_IO_POINTER(io, fptr); 255 | rb_io_check_readable(fptr); 256 | 257 | if (rb_io_read_pending(fptr)) return Qtrue; 258 | 259 | rb_check_arity(argc, 0, 1); 260 | VALUE timeout = argc == 1 ? argv[0] : Qnil; 261 | 262 | return io_wait_event(io, RUBY_IO_PRIORITY, timeout, 1); 263 | } 264 | #endif 265 | 266 | static int 267 | wait_mode_sym(VALUE mode) 268 | { 269 | if (mode == ID2SYM(rb_intern("r"))) { 270 | return RB_WAITFD_IN; 271 | } 272 | if (mode == ID2SYM(rb_intern("read"))) { 273 | return RB_WAITFD_IN; 274 | } 275 | if (mode == ID2SYM(rb_intern("readable"))) { 276 | return RB_WAITFD_IN; 277 | } 278 | if (mode == ID2SYM(rb_intern("w"))) { 279 | return RB_WAITFD_OUT; 280 | } 281 | if (mode == ID2SYM(rb_intern("write"))) { 282 | return RB_WAITFD_OUT; 283 | } 284 | if (mode == ID2SYM(rb_intern("writable"))) { 285 | return RB_WAITFD_OUT; 286 | } 287 | if (mode == ID2SYM(rb_intern("rw"))) { 288 | return RB_WAITFD_IN|RB_WAITFD_OUT; 289 | } 290 | if (mode == ID2SYM(rb_intern("read_write"))) { 291 | return RB_WAITFD_IN|RB_WAITFD_OUT; 292 | } 293 | if (mode == ID2SYM(rb_intern("readable_writable"))) { 294 | return RB_WAITFD_IN|RB_WAITFD_OUT; 295 | } 296 | rb_raise(rb_eArgError, "unsupported mode: %"PRIsVALUE, mode); 297 | return 0; 298 | } 299 | 300 | #ifdef HAVE_RB_IO_WAIT 301 | static inline rb_io_event_t 302 | io_event_from_value(VALUE value) 303 | { 304 | int events = RB_NUM2INT(value); 305 | 306 | if (events <= 0) rb_raise(rb_eArgError, "Events must be positive integer!"); 307 | 308 | return events; 309 | } 310 | #endif 311 | 312 | /* 313 | * call-seq: 314 | * io.wait(events, timeout) -> event mask, false or nil 315 | * io.wait(timeout = nil, mode = :read) -> self, true, or false 316 | * 317 | * Waits until the IO becomes ready for the specified events and returns the 318 | * subset of events that become ready, or a falsy value when times out. 319 | * 320 | * The events can be a bit mask of +IO::READABLE+, +IO::WRITABLE+ or 321 | * +IO::PRIORITY+. 322 | * 323 | * Returns a truthy value immediately when buffered data is available. 324 | * 325 | * Optional parameter +mode+ is one of +:read+, +:write+, or 326 | * +:read_write+. 327 | * 328 | * You must require 'io/wait' to use this method. 329 | */ 330 | 331 | static VALUE 332 | io_wait(int argc, VALUE *argv, VALUE io) 333 | { 334 | #ifndef HAVE_RB_IO_WAIT 335 | rb_io_t *fptr; 336 | struct timeval timerec; 337 | struct timeval *tv = NULL; 338 | int event = 0; 339 | int i; 340 | 341 | GetOpenFile(io, fptr); 342 | for (i = 0; i < argc; ++i) { 343 | if (SYMBOL_P(argv[i])) { 344 | event |= wait_mode_sym(argv[i]); 345 | } 346 | else { 347 | *(tv = &timerec) = rb_time_interval(argv[i]); 348 | } 349 | } 350 | /* rb_time_interval() and might_mode() might convert the argument */ 351 | rb_io_check_closed(fptr); 352 | if (!event) event = RB_WAITFD_IN; 353 | if ((event & RB_WAITFD_IN) && rb_io_read_pending(fptr)) 354 | return Qtrue; 355 | if (wait_for_single_fd(fptr, event, tv)) 356 | return io; 357 | return Qnil; 358 | #else 359 | VALUE timeout = Qundef; 360 | rb_io_event_t events = 0; 361 | int i, return_io = 0; 362 | 363 | /* The documented signature for this method is actually incorrect. 364 | * A single timeout is allowed in any position, and multiple symbols can be given. 365 | * Whether this is intentional or not, I don't know, and as such I consider this to 366 | * be a legacy/slow path. */ 367 | if (argc != 2 || (RB_SYMBOL_P(argv[0]) || RB_SYMBOL_P(argv[1]))) { 368 | /* We'd prefer to return the actual mask, but this form would return the io itself: */ 369 | return_io = 1; 370 | 371 | /* Slow/messy path: */ 372 | for (i = 0; i < argc; i += 1) { 373 | if (RB_SYMBOL_P(argv[i])) { 374 | events |= wait_mode_sym(argv[i]); 375 | } 376 | else if (timeout == Qundef) { 377 | rb_time_interval(timeout = argv[i]); 378 | } 379 | else { 380 | rb_raise(rb_eArgError, "timeout given more than once"); 381 | } 382 | } 383 | 384 | if (timeout == Qundef) timeout = Qnil; 385 | 386 | if (events == 0) { 387 | events = RUBY_IO_READABLE; 388 | } 389 | } 390 | else /* argc == 2 and neither are symbols */ { 391 | /* This is the fast path: */ 392 | events = io_event_from_value(argv[0]); 393 | timeout = argv[1]; 394 | } 395 | 396 | if (events & RUBY_IO_READABLE) { 397 | rb_io_t *fptr = NULL; 398 | RB_IO_POINTER(io, fptr); 399 | 400 | if (rb_io_read_pending(fptr)) { 401 | /* This was the original behaviour: */ 402 | if (return_io) return Qtrue; 403 | /* New behaviour always returns an event mask: */ 404 | else return RB_INT2NUM(RUBY_IO_READABLE); 405 | } 406 | } 407 | 408 | return io_wait_event(io, events, timeout, return_io); 409 | #endif 410 | } 411 | 412 | #endif /* RUBY_IO_WAIT_METHODS */ 413 | 414 | /* 415 | * IO wait methods 416 | */ 417 | 418 | void 419 | Init_wait(void) 420 | { 421 | #ifdef HAVE_RB_EXT_RACTOR_SAFE 422 | RB_EXT_RACTOR_SAFE(true); 423 | #endif 424 | 425 | rb_define_method(rb_cIO, "nread", io_nread, 0); 426 | rb_define_method(rb_cIO, "ready?", io_ready_p, 0); 427 | 428 | #ifndef RUBY_IO_WAIT_METHODS 429 | rb_define_method(rb_cIO, "wait", io_wait, -1); 430 | 431 | rb_define_method(rb_cIO, "wait_readable", io_wait_readable, -1); 432 | rb_define_method(rb_cIO, "wait_writable", io_wait_writable, -1); 433 | #ifdef HAVE_RB_IO_WAIT 434 | rb_define_method(rb_cIO, "wait_priority", io_wait_priority, -1); 435 | #endif 436 | #endif 437 | } 438 | -------------------------------------------------------------------------------- /ext/java/lib/io/wait.rb: -------------------------------------------------------------------------------- 1 | require 'io/wait.jar' 2 | JRuby::Util.load_ext("org.jruby.ext.io.wait.IOWaitLibrary") 3 | -------------------------------------------------------------------------------- /ext/java/org/jruby/ext/io/wait/IOWaitLibrary.java: -------------------------------------------------------------------------------- 1 | /***** BEGIN LICENSE BLOCK ***** 2 | * Version: EPL 2.0/GPL 2.0/LGPL 2.1 3 | * 4 | * The contents of this file are subject to the Eclipse Public 5 | * License Version 2.0 (the "License"); you may not use this file 6 | * except in compliance with the License. You may obtain a copy of 7 | * the License at http://www.eclipse.org/legal/epl-v20.html 8 | * 9 | * Software distributed under the License is distributed on an "AS 10 | * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or 11 | * implied. See the License for the specific language governing 12 | * rights and limitations under the License. 13 | * 14 | * Copyright (C) 2006 Nick Sieger 15 | * 16 | * Alternatively, the contents of this file may be used under the terms of 17 | * either of the GNU General Public License Version 2 or later (the "GPL"), 18 | * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 19 | * in which case the provisions of the GPL or the LGPL are applicable instead 20 | * of those above. If you wish to allow use of your version of this file only 21 | * under the terms of either the GPL or the LGPL, and not to allow others to 22 | * use your version of this file under the terms of the EPL, indicate your 23 | * decision by deleting the provisions above and replace them with the notice 24 | * and other provisions required by the GPL or the LGPL. If you do not delete 25 | * the provisions above, a recipient may use your version of this file under 26 | * the terms of any one of the EPL, the GPL or the LGPL. 27 | ***** END LICENSE BLOCK *****/ 28 | 29 | package org.jruby.ext.io.wait; 30 | 31 | import org.jruby.Ruby; 32 | import org.jruby.RubyBoolean; 33 | import org.jruby.RubyClass; 34 | import org.jruby.RubyIO; 35 | import org.jruby.RubyNumeric; 36 | import org.jruby.RubySymbol; 37 | import org.jruby.RubyTime; 38 | import org.jruby.anno.JRubyMethod; 39 | import org.jruby.runtime.Helpers; 40 | import org.jruby.runtime.ThreadContext; 41 | import org.jruby.runtime.builtin.IRubyObject; 42 | import org.jruby.runtime.load.Library; 43 | import org.jruby.util.io.OpenFile; 44 | 45 | import java.nio.channels.SelectionKey; 46 | 47 | /** 48 | * @author Nick Sieger 49 | */ 50 | public class IOWaitLibrary implements Library { 51 | 52 | public void load(Ruby runtime, boolean wrap) { 53 | RubyClass ioClass = runtime.getIO(); 54 | ioClass.defineAnnotatedMethods(IOWaitLibrary.class); 55 | } 56 | 57 | @JRubyMethod 58 | public static IRubyObject nread(ThreadContext context, IRubyObject _io) { 59 | Ruby runtime = context.runtime; 60 | OpenFile fptr; 61 | int len; 62 | // ioctl_arg n; 63 | RubyIO io = (RubyIO)_io; 64 | 65 | fptr = io.getOpenFileChecked(); 66 | fptr.checkReadable(context); 67 | len = fptr.readPending(); 68 | if (len > 0) return runtime.newFixnum(len); 69 | // TODO: better effort to get available bytes from our channel 70 | // if (!FIONREAD_POSSIBLE_P(fptr->fd)) return INT2FIX(0); 71 | // if (ioctl(fptr->fd, FIONREAD, &n)) return INT2FIX(0); 72 | // if (n > 0) return ioctl_arg2num(n); 73 | // Because we can't get an actual system-level buffer available count, we fake it by returning 1 if ready 74 | return RubyNumeric.int2fix(runtime, fptr.readyNow(context) ? 1 : 0); 75 | } 76 | 77 | /** 78 | * returns non-nil if input available without blocking, false if EOF or not open/readable, otherwise nil. 79 | */ 80 | @JRubyMethod(name = "ready?") 81 | public static IRubyObject ready(ThreadContext context, IRubyObject _io) { 82 | RubyIO io = (RubyIO)_io; 83 | OpenFile fptr; 84 | // ioctl_arg n; 85 | 86 | fptr = io.getOpenFileChecked(); 87 | fptr.checkReadable(context); 88 | if (fptr.readPending() != 0) return context.tru; 89 | // TODO: better effort to get available bytes from our channel 90 | // if (!FIONREAD_POSSIBLE_P(fptr->fd)) return Qnil; 91 | // if (ioctl(fptr->fd, FIONREAD, &n)) return Qnil; 92 | // if (n > 0) return Qtrue; 93 | return RubyBoolean.newBoolean(context, fptr.readyNow(context)); 94 | } 95 | 96 | @JRubyMethod(optional = 1) 97 | public static IRubyObject wait_readable(ThreadContext context, IRubyObject _io, IRubyObject[] argv) { 98 | RubyIO io = (RubyIO)_io; 99 | OpenFile fptr = io.getOpenFileChecked(); 100 | 101 | fptr.checkReadable(context); 102 | 103 | long tv = prepareTimeout(context, argv); 104 | 105 | if (fptr.readPending() != 0) return context.tru; 106 | 107 | return doWait(context, io, fptr, tv, SelectionKey.OP_READ | SelectionKey.OP_ACCEPT); 108 | } 109 | 110 | /** 111 | * waits until input available or timed out and returns self, or nil when EOF reached. 112 | */ 113 | @JRubyMethod(optional = 1) 114 | public static IRubyObject wait_writable(ThreadContext context, IRubyObject _io, IRubyObject[] argv) { 115 | RubyIO io = (RubyIO)_io; 116 | 117 | OpenFile fptr = io.getOpenFileChecked(); 118 | 119 | fptr.checkWritable(context); 120 | 121 | long tv = prepareTimeout(context, argv); 122 | 123 | return doWait(context, io, fptr, tv, SelectionKey.OP_CONNECT | SelectionKey.OP_WRITE); 124 | } 125 | 126 | @JRubyMethod(optional = 2) 127 | public static IRubyObject wait(ThreadContext context, IRubyObject _io, IRubyObject[] argv) { 128 | RubyIO io = (RubyIO)_io; 129 | 130 | OpenFile fptr = io.getOpenFileChecked(); 131 | 132 | int ops = 0; 133 | 134 | if (argv.length == 2) { 135 | if (argv[1] instanceof RubySymbol) { 136 | RubySymbol sym = (RubySymbol) argv[1]; 137 | switch (sym.asJavaString()) { // 7 bit comparison 138 | case "r": 139 | case "read": 140 | case "readable": 141 | ops |= SelectionKey.OP_ACCEPT | SelectionKey.OP_READ; 142 | break; 143 | case "w": 144 | case "write": 145 | case "writable": 146 | ops |= SelectionKey.OP_CONNECT | SelectionKey.OP_WRITE; 147 | break; 148 | case "rw": 149 | case "read_write": 150 | case "readable_writable": 151 | ops |= SelectionKey.OP_ACCEPT | SelectionKey.OP_READ | SelectionKey.OP_CONNECT | SelectionKey.OP_WRITE; 152 | break; 153 | default: 154 | throw context.runtime.newArgumentError("unsupported mode: " + sym); 155 | } 156 | } else { 157 | throw context.runtime.newArgumentError("unsupported mode: " + argv[1].getType()); 158 | } 159 | } else { 160 | ops |= SelectionKey.OP_ACCEPT | SelectionKey.OP_READ; 161 | } 162 | 163 | if ((ops & SelectionKey.OP_READ) == SelectionKey.OP_READ && fptr.readPending() != 0) return context.tru; 164 | 165 | long tv = prepareTimeout(context, argv); 166 | 167 | return doWait(context, io, fptr, tv, ops); 168 | } 169 | 170 | private static IRubyObject doWait(ThreadContext context, RubyIO io, OpenFile fptr, long tv, int ops) { 171 | boolean ready = fptr.ready(context.runtime, context.getThread(), ops, tv); 172 | fptr.checkClosed(); 173 | if (ready) return io; 174 | return context.nil; 175 | } 176 | 177 | private static long prepareTimeout(ThreadContext context, IRubyObject[] argv) { 178 | IRubyObject timeout; 179 | long tv; 180 | switch (argv.length) { 181 | case 2: 182 | case 1: 183 | timeout = argv[0]; 184 | break; 185 | default: 186 | timeout = context.nil; 187 | } 188 | 189 | if (timeout.isNil()) { 190 | tv = -1; 191 | } 192 | else { 193 | tv = (long)(RubyTime.convertTimeInterval(context, timeout) * 1000); 194 | if (tv < 0) throw context.runtime.newArgumentError("time interval must be positive"); 195 | } 196 | return tv; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /io-wait.gemspec: -------------------------------------------------------------------------------- 1 | _VERSION = "0.3.1" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "io-wait" 5 | spec.version = _VERSION 6 | spec.authors = ["Nobu Nakada", "Charles Oliver Nutter"] 7 | spec.email = ["nobu@ruby-lang.org", "headius@headius.com"] 8 | 9 | spec.summary = %q{Waits until IO is readable or writable without blocking.} 10 | spec.description = %q{Waits until IO is readable or writable without blocking.} 11 | spec.homepage = "https://github.com/ruby/io-wait" 12 | spec.licenses = ["Ruby", "BSD-2-Clause"] 13 | 14 | spec.metadata["homepage_uri"] = spec.homepage 15 | spec.metadata["source_code_uri"] = spec.homepage 16 | 17 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 18 | `git ls-files -z`.split("\x0").reject do |f| 19 | File.identical?(f, __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features|rakelib)/|\.(?:git|travis|circleci)|appveyor|Rakefile)}) 20 | end 21 | end 22 | spec.bindir = "exe" 23 | spec.executables = [] 24 | spec.require_paths = ["lib"] 25 | 26 | jruby = true if Gem::Platform.new('java') =~ spec.platform or RUBY_ENGINE == 'jruby' 27 | spec.files.delete_if do |f| 28 | f.end_with?(".java") or 29 | f.start_with?("ext/") && (jruby ^ f.start_with?("ext/java/")) 30 | end 31 | if jruby 32 | spec.platform = 'java' 33 | spec.files << "lib/io/wait.jar" 34 | spec.require_paths += ["ext/java/lib"] 35 | else 36 | spec.extensions = %w[ext/io/wait/extconf.rb] 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /rakelib/changelogs.rake: -------------------------------------------------------------------------------- 1 | task "build" => "changelogs" 2 | 3 | changelog = proc do |output, ver = nil, prev = nil| 4 | ver &&= Gem::Version.new(ver) 5 | range = [[prev], [ver, "HEAD"]].map {|ver, branch| ver ? "v#{ver.to_s}" : branch}.compact.join("..") 6 | IO.popen(%W[git log --format=fuller --topo-order --no-merges #{range}]) do |log| 7 | line = log.gets 8 | FileUtils.mkpath(File.dirname(output)) 9 | File.open(output, "wb") do |f| 10 | f.print "-*- coding: utf-8 -*-\n\n", line 11 | log.each_line do |line| 12 | line.sub!(/^(?!:)(?:Author|Commit)?(?:Date)?: /, ' \&') 13 | line.sub!(/ +$/, '') 14 | f.print(line) 15 | end 16 | end 17 | end 18 | end 19 | 20 | tags = IO.popen(%w[git tag -l v[0-9]*]).grep(/v(.*)/) {$1} 21 | tags.sort_by! {|tag| tag.scan(/\d+/).map(&:to_i)} 22 | tags.inject(nil) do |prev, tag| 23 | task("logs/ChangeLog-#{tag}") {|t| changelog[t.name, tag, prev]} 24 | tag 25 | end 26 | 27 | desc "Make ChangeLog" 28 | task "ChangeLog", [:ver, :prev] do |t, ver: nil, prev: tags.last| 29 | changelog[t.name, ver, prev] 30 | end 31 | 32 | changelogs = ["ChangeLog", *tags.map {|tag| "logs/ChangeLog-#{tag}"}] 33 | task "changelogs" => changelogs 34 | CLOBBER.concat(changelogs) << "logs" 35 | -------------------------------------------------------------------------------- /rakelib/checksum.rake: -------------------------------------------------------------------------------- 1 | # build:checksum prior to rubygems 3.4 calculates meaningless values 2 | desc "Generate SHA512 checksums" 3 | task "build:sha512" => "pkg/SHA512" 4 | task "pkg/SHA512" => "build" do |t| 5 | require 'digest/sha2' 6 | File.open(t.name, "wb") do |f| 7 | Dir.glob("pkg/*.gem") do |pkg| 8 | f.print(Digest::SHA512.file(pkg), " *", File.basename(pkg), "\n") 9 | end 10 | end 11 | Bundler.ui.confirm "SHA512 checksums are written to #{t.name}." 12 | end 13 | -------------------------------------------------------------------------------- /rakelib/epoch.rake: -------------------------------------------------------------------------------- 1 | task "build" => "date_epoch" 2 | 3 | task "date_epoch" do 4 | ENV["SOURCE_DATE_EPOCH"] = IO.popen(%W[git -C #{__dir__} log -1 --format=%ct], &:read).chomp 5 | end 6 | -------------------------------------------------------------------------------- /rakelib/sync_tool.rake: -------------------------------------------------------------------------------- 1 | task :sync_tool do 2 | require 'fileutils' 3 | FileUtils.cp "../ruby/tool/lib/core_assertions.rb", "./test/lib" 4 | FileUtils.cp "../ruby/tool/lib/envutil.rb", "./test/lib" 5 | FileUtils.cp "../ruby/tool/lib/find_executable.rb", "./test/lib" 6 | end 7 | -------------------------------------------------------------------------------- /rakelib/version.rake: -------------------------------------------------------------------------------- 1 | class << (helper = Bundler::GemHelper.instance) 2 | def update_gemspec 3 | path = gemspec.loaded_from 4 | File.open(path, "r+b") do |f| 5 | d = f.read 6 | if d.sub!(/^(_VERSION\s*=\s*)".*"/) {$1 + gemspec.version.to_s.dump} 7 | f.rewind 8 | f.truncate(0) 9 | f.print(d) 10 | end 11 | end 12 | end 13 | 14 | def commit_bump 15 | sh(%W[git commit -m bump\ up\ to\ #{gemspec.version} 16 | #{gemspec.loaded_from}]) 17 | end 18 | 19 | def version=(v) 20 | gemspec.version = v 21 | update_gemspec 22 | commit_bump 23 | end 24 | end 25 | 26 | major, minor, teeny = helper.gemspec.version.segments 27 | 28 | task "bump:teeny" do 29 | helper.version = Gem::Version.new("#{major}.#{minor}.#{teeny+1}") 30 | end 31 | 32 | task "bump:minor" do 33 | helper.version = Gem::Version.new("#{major}.#{minor+1}.0") 34 | end 35 | 36 | task "bump:major" do 37 | helper.version = Gem::Version.new("#{major+1}.0.0") 38 | end 39 | 40 | task "bump" => "bump:teeny" 41 | 42 | task "tag" do 43 | helper.__send__(:tag_version) 44 | end 45 | -------------------------------------------------------------------------------- /test/io/wait/test_io_wait.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: us-ascii -*- 2 | # frozen_string_literal: false 3 | require 'test/unit' 4 | require 'timeout' 5 | require 'socket' 6 | 7 | # For `IO#ready?` and `IO#nread`: 8 | require 'io/wait' 9 | 10 | class TestIOWait < Test::Unit::TestCase 11 | 12 | def setup 13 | if /mswin|mingw/ =~ RUBY_PLATFORM 14 | @r, @w = Socket.pair(Socket::AF_INET, Socket::SOCK_STREAM, 0) 15 | else 16 | @r, @w = IO.pipe 17 | end 18 | end 19 | 20 | def teardown 21 | @r.close unless @r.closed? 22 | @w.close unless @w.closed? 23 | end 24 | 25 | def test_nread 26 | assert_equal 0, @r.nread 27 | @w.syswrite "." 28 | sleep 0.1 29 | assert_equal 1, @r.nread 30 | end 31 | 32 | def test_nread_buffered 33 | @w.syswrite ".\n!" 34 | assert_equal ".\n", @r.gets 35 | assert_equal 1, @r.nread 36 | end 37 | 38 | def test_ready? 39 | omit 'unstable on MinGW' if /mingw/ =~ RUBY_PLATFORM 40 | assert_not_predicate @r, :ready?, "shouldn't ready, but ready" 41 | @w.syswrite "." 42 | sleep 0.1 43 | assert_predicate @r, :ready?, "should ready, but not" 44 | end 45 | 46 | def test_buffered_ready? 47 | @w.syswrite ".\n!" 48 | assert_equal ".\n", @r.gets 49 | assert_predicate @r, :ready? 50 | end 51 | 52 | def test_wait 53 | omit 'unstable on MinGW' if /mingw/ =~ RUBY_PLATFORM 54 | assert_nil @r.wait(0) 55 | @w.syswrite "." 56 | sleep 0.1 57 | assert_equal @r, @r.wait(0) 58 | end 59 | 60 | def test_wait_buffered 61 | @w.syswrite ".\n!" 62 | assert_equal ".\n", @r.gets 63 | assert_equal true, @r.wait(0) 64 | end 65 | 66 | def test_wait_forever 67 | q = Thread::Queue.new 68 | th = Thread.new { q.pop; @w.syswrite "." } 69 | q.push(true) 70 | assert_equal @r, @r.wait 71 | ensure 72 | th.join 73 | end 74 | 75 | def test_wait_eof 76 | q = Thread::Queue.new 77 | th = Thread.new { q.pop; @w.close } 78 | ret = nil 79 | assert_nothing_raised(Timeout::Error) do 80 | q.push(true) 81 | t = EnvUtil.apply_timeout_scale(0.1) 82 | Timeout.timeout(t) { ret = @r.wait } 83 | end 84 | assert_equal @r, ret 85 | ensure 86 | th.join 87 | end 88 | 89 | def test_wait_readable 90 | assert_nil @r.wait_readable(0) 91 | @w.syswrite "." 92 | IO.select([@r]) 93 | assert_equal @r, @r.wait_readable(0) 94 | end 95 | 96 | def test_wait_readable_buffered 97 | @w.syswrite ".\n!" 98 | assert_equal ".\n", @r.gets 99 | assert_equal true, @r.wait_readable(0) 100 | end 101 | 102 | def test_wait_readable_forever 103 | q = Thread::Queue.new 104 | th = Thread.new { q.pop; @w.syswrite "." } 105 | q.push(true) 106 | assert_equal @r, @r.wait_readable 107 | ensure 108 | th.join 109 | end 110 | 111 | def test_wait_readable_eof 112 | q = Thread::Queue.new 113 | th = Thread.new { q.pop; @w.close } 114 | ret = nil 115 | assert_nothing_raised(Timeout::Error) do 116 | q.push(true) 117 | t = EnvUtil.apply_timeout_scale(0.1) 118 | Timeout.timeout(t) { ret = @r.wait_readable } 119 | end 120 | assert_equal @r, ret 121 | ensure 122 | th.join 123 | end 124 | 125 | def test_wait_writable 126 | assert_equal @w, @w.wait_writable 127 | end 128 | 129 | def test_wait_writable_timeout 130 | assert_equal @w, @w.wait_writable(0.01) 131 | written = fill_pipe 132 | assert_nil @w.wait_writable(0.01) 133 | @r.read(written) 134 | assert_equal @w, @w.wait_writable(0.01) 135 | end 136 | 137 | def test_wait_writable_EPIPE 138 | fill_pipe 139 | @r.close 140 | assert_equal @w, @w.wait_writable 141 | end 142 | 143 | def test_wait_writable_closed 144 | @w.close 145 | assert_raise(IOError) { @w.wait_writable } 146 | end 147 | 148 | def test_wait_readwrite 149 | assert_equal @r.wait(0, :write), @r.wait(0, :read_write) 150 | end 151 | 152 | def test_wait_readwrite_timeout 153 | assert_equal @w, @w.wait(0.01, :read_write) 154 | written = fill_pipe 155 | if /aix/ =~ RUBY_PLATFORM 156 | # IO#wait internally uses select(2) on AIX. 157 | # AIX's select(2) returns "readable" for the write-side fd 158 | # of a pipe, so @w.wait(0.01, :read_write) does not return nil. 159 | assert_equal @w, @w.wait(0.01, :read_write) 160 | else 161 | assert_nil @w.wait(0.01, :read_write) 162 | end 163 | @r.read(written) 164 | assert_equal @w, @w.wait(0.01, :read_write) 165 | end 166 | 167 | def test_wait_mask_writable 168 | omit("Missing IO::WRITABLE!") unless IO.const_defined?(:WRITABLE) 169 | assert_equal IO::WRITABLE, @w.wait(IO::WRITABLE, 0) 170 | end 171 | 172 | def test_wait_mask_readable 173 | omit("Missing IO::READABLE!") unless IO.const_defined?(:READABLE) 174 | @w.write("Hello World\n" * 3) 175 | assert_equal IO::READABLE, @r.wait(IO::READABLE, 0) 176 | 177 | @r.gets 178 | assert_equal IO::READABLE, @r.wait(IO::READABLE, 0) 179 | end 180 | 181 | def test_wait_mask_zero 182 | omit("Missing IO::WRITABLE!") unless IO.const_defined?(:WRITABLE) 183 | assert_raise(ArgumentError) do 184 | @w.wait(0, 0) 185 | end 186 | end 187 | 188 | def test_wait_mask_negative 189 | omit("Missing IO::WRITABLE!") unless IO.const_defined?(:WRITABLE) 190 | assert_raise(ArgumentError) do 191 | @w.wait(-6, 0) 192 | end 193 | end 194 | 195 | private 196 | 197 | def fill_pipe 198 | written = 0 199 | buf = " " * 4096 200 | begin 201 | written += @w.write_nonblock(buf) 202 | rescue Errno::EAGAIN, Errno::EWOULDBLOCK 203 | return written 204 | end while true 205 | end 206 | 207 | def sleep(time) 208 | super EnvUtil.apply_timeout_scale(time) 209 | end 210 | end if IO.method_defined?(:wait) 211 | -------------------------------------------------------------------------------- /test/io/wait/test_io_wait_uncommon.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test/unit' 3 | 4 | # test uncommon device types to check portability problems 5 | # We may optimize IO#wait_*able for non-Linux kernels in the future 6 | class TestIOWaitUncommon < Test::Unit::TestCase 7 | def test_tty_wait 8 | check_dev('/dev/tty', mode: 'w+') do |tty| 9 | assert_include [ nil, tty ], tty.wait_readable(0) 10 | assert_equal tty, tty.wait_writable(1), 'portability test' 11 | end 12 | end 13 | 14 | def test_fifo_wait 15 | omit 'no mkfifo' unless File.respond_to?(:mkfifo) && IO.const_defined?(:NONBLOCK) 16 | require 'tmpdir' 17 | Dir.mktmpdir('rubytest-fifo') do |dir| 18 | fifo = "#{dir}/fifo" 19 | assert_equal 0, File.mkfifo(fifo) 20 | rd = Thread.new { File.open(fifo, IO::RDONLY|IO::NONBLOCK) } 21 | begin 22 | wr = File.open(fifo, IO::WRONLY|IO::NONBLOCK) 23 | rescue Errno::ENXIO 24 | Thread.pass 25 | end until wr 26 | assert_instance_of File, rd.value 27 | assert_instance_of File, wr 28 | rd = rd.value 29 | assert_nil rd.wait_readable(0) 30 | assert_same wr, wr.wait_writable(0) 31 | wr.syswrite 'hi' 32 | assert_same rd, rd.wait_readable(1) 33 | wr.close 34 | assert_equal 'hi', rd.gets 35 | rd.close 36 | end 37 | end 38 | 39 | # used to find portability problems because some ppoll implementations 40 | # are incomplete and do not work for certain "file" types 41 | def check_dev(dev, m = :wait_readable, mode: m == :wait_readable ? 'r' : 'w', &block) 42 | begin 43 | fp = File.open(dev, mode) 44 | rescue Errno::ENOENT 45 | return # Ignore silently 46 | rescue SystemCallError => e 47 | omit "#{dev} could not be opened #{e.message} (#{e.class})" 48 | end 49 | if block 50 | yield fp 51 | else 52 | assert_same fp, fp.__send__(m) 53 | end 54 | ensure 55 | fp&.close 56 | end 57 | 58 | def test_wait_readable_urandom 59 | check_dev('/dev/urandom') 60 | end 61 | 62 | def test_wait_readable_random 63 | check_dev('/dev/random') do |fp| 64 | assert_nothing_raised do 65 | fp.wait_readable(0) 66 | end 67 | end 68 | end 69 | 70 | def test_wait_readable_zero 71 | check_dev('/dev/zero') 72 | end 73 | 74 | def test_wait_writable_null 75 | check_dev(IO::NULL, :wait_writable) 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/io/wait/test_ractor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test/unit' 3 | require 'rbconfig' 4 | 5 | class TestIOWaitInRactor < Test::Unit::TestCase 6 | def test_ractor 7 | ext = "/io/wait.#{RbConfig::CONFIG['DLEXT']}" 8 | path = $".find {|path| path.end_with?(ext)} 9 | assert_in_out_err(%W[-r#{path}], <<-"end;", ["true"], []) 10 | class Ractor 11 | alias value take 12 | end unless Ractor.method_defined? :value # compat with Ruby 3.4 and olders 13 | 14 | $VERBOSE = nil 15 | r = Ractor.new do 16 | $stdout.equal?($stdout.wait_writable) 17 | end 18 | puts r.value 19 | end; 20 | end 21 | end if defined? Ractor 22 | -------------------------------------------------------------------------------- /test/lib/helper.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require "core_assertions" 3 | 4 | Test::Unit::TestCase.include Test::Unit::CoreAssertions 5 | --------------------------------------------------------------------------------