├── .rdoc_options ├── .document ├── _doc └── io.rb ├── ext ├── io │ └── wait │ │ ├── extconf.rb │ │ ├── wait.c │ │ └── depend └── java │ ├── lib │ └── io │ │ └── wait.rb │ └── org │ └── jruby │ └── ext │ └── io │ └── wait │ └── IOWaitLibrary.java ├── test ├── lib │ └── helper.rb └── io │ └── wait │ ├── test_ractor.rb │ ├── test_io_wait_uncommon.rb │ └── test_io_wait.rb ├── .github ├── dependabot.yml └── workflows │ ├── test.yml │ └── push_gem.yml ├── rakelib ├── epoch.rake ├── sync_tool.rake ├── checksum.rake ├── version.rake └── changelogs.rake ├── .gitignore ├── Gemfile ├── .git-blame-ignore-revs ├── README.md ├── Rakefile ├── io-wait.gemspec └── COPYING /.rdoc_options: -------------------------------------------------------------------------------- 1 | --- 2 | main_page: README.md 3 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | COPYING 2 | ChangeLog 3 | README.md 4 | _doc/ 5 | ext/ 6 | logs/ChangeLog-* 7 | -------------------------------------------------------------------------------- /_doc/io.rb: -------------------------------------------------------------------------------- 1 | # See {IO}[https://docs.ruby-lang.org/en/master/IO.html] 2 | class IO 3 | end 4 | -------------------------------------------------------------------------------- /ext/io/wait/extconf.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'mkmf' 3 | 4 | create_makefile("io/wait") 5 | -------------------------------------------------------------------------------- /ext/java/lib/io/wait.rb: -------------------------------------------------------------------------------- 1 | require 'io/wait.jar' 2 | JRuby::Util.load_ext("org.jruby.ext.io.wait.IOWaitLibrary") 3 | -------------------------------------------------------------------------------- /test/lib/helper.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require "core_assertions" 3 | 4 | Test::Unit::TestCase.include Test::Unit::CoreAssertions 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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" /* abi_version */ 15 | 16 | /* 17 | * IO wait methods are built in ruby now, just for backward compatibility. 18 | */ 19 | 20 | void 21 | Init_wait(void) 22 | { 23 | } 24 | -------------------------------------------------------------------------------- /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 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | else 20 | require 'rake/extensiontask' 21 | extask = Rake::ExtensionTask.new(name) do |x| 22 | x.lib_dir.sub!(%r[(?=/|\z)], "/#{RUBY_VERSION}/#{x.platform}") 23 | end 24 | libs = ["lib/#{RUBY_VERSION}/#{extask.platform}"] 25 | end 26 | 27 | Rake::TestTask.new(:test) do |t| 28 | t.libs = libs 29 | t.libs << "test/lib" 30 | t.ruby_opts << "-rhelper" 31 | t.options = "--ignore-name=/ungetc_in_text/" 32 | t.test_files = FileList["test/**/test_*.rb"] 33 | end 34 | 35 | task :test => :compile 36 | 37 | task :default => :test 38 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /io-wait.gemspec: -------------------------------------------------------------------------------- 1 | _VERSION = "0.4.0.dev" 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 | spec.required_ruby_version = Gem::Requirement.new(">= 3.2") 14 | 15 | spec.metadata["homepage_uri"] = spec.homepage 16 | spec.metadata["source_code_uri"] = spec.homepage 17 | 18 | jruby = true if Gem::Platform.new('java') =~ spec.platform or RUBY_ENGINE == 'jruby' 19 | dir, gemspec = File.split(__FILE__) 20 | excludes = [ 21 | *%w[:^/.git* :^/Gemfile* :^/Rakefile* :^/bin/ :^/test/ :^/rakelib/ :^*.java], 22 | *(jruby ? %w[:^/ext/io] : %w[:^/ext/java]), 23 | ":(exclude,literal,top)#{gemspec}" 24 | ] 25 | files = IO.popen(%w[git ls-files -z --] + excludes, chdir: dir, &:read).split("\x0") 26 | 27 | spec.files = files 28 | spec.bindir = "exe" 29 | spec.executables = [] 30 | spec.require_paths = ["lib"] 31 | 32 | if jruby 33 | spec.platform = 'java' 34 | spec.files << "lib/io/wait.jar" 35 | spec.require_paths += ["ext/java/lib"] 36 | else 37 | spec.extensions = %w[ext/io/wait/extconf.rb] 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /.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: 3.2 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' 23 | os: 'ubuntu' 24 | platform: 'java' 25 | continue-on-error: true 26 | upload: true 27 | - ruby: 'jruby-head' 28 | os: 'ubuntu' 29 | platform: 'java' 30 | continue-on-error: true 31 | steps: 32 | - uses: actions/checkout@v6 33 | - name: Set up Ruby ${{ matrix.ruby }} 34 | uses: ruby/setup-ruby@v1 35 | with: 36 | ruby-version: ${{ matrix.ruby }} 37 | - name: Set up Bundler 38 | run: bundle install 39 | - name: Run test 40 | run: bundle exec rake 41 | continue-on-error: ${{ matrix.continue-on-error }} 42 | - id: build 43 | run: | 44 | rake build:sha512 45 | cat pkg/SHA512 46 | gem=${GITHUB_REPOSITORY#*/} 47 | echo "pkg=$gem-${RUNNING_OS%-*}" >> $GITHUB_OUTPUT 48 | env: 49 | RUNNING_OS: ${{matrix.platform || matrix.os}} 50 | if: ${{matrix.ruby == needs.ruby-versions.outputs.latest || matrix.upload}} 51 | shell: bash 52 | - name: Upload package 53 | uses: actions/upload-artifact@v6 54 | with: 55 | path: pkg/* 56 | name: ${{steps.build.outputs.pkg}} 57 | if: steps.build.outputs.pkg 58 | -------------------------------------------------------------------------------- /.github/workflows/push_gem.yml: -------------------------------------------------------------------------------- 1 | name: Publish gem to rubygems.org 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | push: 13 | if: github.repository == 'ruby/io-wait' 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | ruby: [ ruby, jruby ] 18 | 19 | environment: 20 | name: rubygems.org 21 | url: https://rubygems.org/gems/io-wait 22 | 23 | permissions: 24 | contents: write 25 | id-token: write 26 | 27 | steps: 28 | - name: Harden Runner 29 | uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 30 | with: 31 | egress-policy: audit 32 | 33 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 34 | 35 | - name: Set up Ruby 36 | uses: ruby/setup-ruby@eaecf785f6a34567a6d97f686bbb7bccc1ac1e5c # v1.237.0 37 | with: 38 | bundler-cache: true 39 | ruby-version: ${{ matrix.ruby }} 40 | 41 | - name: Set remote URL 42 | run: | 43 | # Attribute commits to the last committer on HEAD 44 | git config --global user.email "$(git log -1 --pretty=format:'%ae')" 45 | git config --global user.name "$(git log -1 --pretty=format:'%an')" 46 | git remote set-url origin "https://x-access-token:${{ github.token }}@github.com/$GITHUB_REPOSITORY" 47 | shell: bash 48 | 49 | - name: Configure trusted publishing credentials 50 | uses: rubygems/configure-rubygems-credentials@v1.0.0 51 | 52 | - name: Install dependencies 53 | run: bundle install 54 | shell: bash 55 | 56 | - name: Run release rake task 57 | run: bundle exec rake release 58 | shell: bash 59 | 60 | - name: Wait for release to propagate 61 | run: gem exec rubygems-await pkg/*.gem 62 | shell: bash 63 | 64 | - name: Create GitHub release 65 | run: | 66 | tag_name="$(git describe --tags --abbrev=0)" 67 | gh release create "${tag_name}" --verify-tag --generate-notes 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | if: ${{ matrix.ruby == 'ruby' }} 71 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/io/wait/test_io_wait_uncommon.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test/unit' 3 | require 'io/wait' 4 | 5 | # test uncommon device types to check portability problems 6 | # We may optimize IO#wait_*able for non-Linux kernels in the future 7 | class TestIOWaitUncommon < Test::Unit::TestCase 8 | def test_tty_wait 9 | check_dev('/dev/tty', mode: 'w+') do |tty| 10 | assert_include [ nil, tty ], tty.wait_readable(0) 11 | assert_equal tty, tty.wait_writable(1), 'portability test' 12 | end 13 | end 14 | 15 | def test_fifo_wait 16 | omit 'no mkfifo' unless File.respond_to?(:mkfifo) && IO.const_defined?(:NONBLOCK) 17 | require 'tmpdir' 18 | Dir.mktmpdir('rubytest-fifo') do |dir| 19 | fifo = "#{dir}/fifo" 20 | assert_equal 0, File.mkfifo(fifo) 21 | rd = Thread.new { File.open(fifo, IO::RDONLY|IO::NONBLOCK) } 22 | begin 23 | wr = File.open(fifo, IO::WRONLY|IO::NONBLOCK) 24 | rescue Errno::ENXIO 25 | Thread.pass 26 | end until wr 27 | assert_instance_of File, rd.value 28 | assert_instance_of File, wr 29 | rd = rd.value 30 | assert_nil rd.wait_readable(0) 31 | assert_same wr, wr.wait_writable(0) 32 | wr.syswrite 'hi' 33 | assert_same rd, rd.wait_readable(1) 34 | wr.close 35 | assert_equal 'hi', rd.gets 36 | rd.close 37 | end 38 | end 39 | 40 | # used to find portability problems because some ppoll implementations 41 | # are incomplete and do not work for certain "file" types 42 | def check_dev(dev, m = :wait_readable, mode: m == :wait_readable ? 'r' : 'w', &block) 43 | begin 44 | fp = File.open(dev, mode) 45 | rescue Errno::ENOENT 46 | return # Ignore silently 47 | rescue SystemCallError => e 48 | omit "#{dev} could not be opened #{e.message} (#{e.class})" 49 | end 50 | if block 51 | yield fp 52 | else 53 | assert_same fp, fp.__send__(m) 54 | end 55 | ensure 56 | fp&.close 57 | end 58 | 59 | def test_wait_readable_urandom 60 | check_dev('/dev/urandom') 61 | end 62 | 63 | def test_wait_readable_random 64 | check_dev('/dev/random') do |fp| 65 | assert_nothing_raised do 66 | fp.wait_readable(0) 67 | end 68 | end 69 | end 70 | 71 | def test_wait_readable_zero 72 | check_dev('/dev/zero') 73 | end 74 | 75 | def test_wait_writable_null 76 | check_dev(IO::NULL, :wait_writable) 77 | end 78 | 79 | def test_after_ungetc_wait_readable 80 | check_dev(IO::NULL, mode: "r") {|fp| 81 | fp.ungetc(?a) 82 | assert_predicate fp, :wait_readable 83 | } 84 | end 85 | 86 | def test_after_ungetc_in_text_wait_readable 87 | check_dev(IO::NULL, mode: "rt") {|fp| 88 | fp.ungetc(?a) 89 | assert_predicate fp, :wait_readable 90 | } 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /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 | class TestIOWait < Test::Unit::TestCase 8 | 9 | def setup 10 | if /mswin|mingw/ =~ RUBY_PLATFORM 11 | @r, @w = Socket.pair(Socket::AF_INET, Socket::SOCK_STREAM, 0) 12 | else 13 | @r, @w = IO.pipe 14 | end 15 | end 16 | 17 | def teardown 18 | @r.close unless @r.closed? 19 | @w.close unless @w.closed? 20 | end 21 | 22 | def test_wait 23 | omit 'unstable on MinGW' if /mingw/ =~ RUBY_PLATFORM 24 | assert_nil @r.wait(0) 25 | @w.syswrite "." 26 | sleep 0.1 27 | assert_equal @r, @r.wait(0) 28 | end 29 | 30 | def test_wait_buffered 31 | @w.syswrite ".\n!" 32 | assert_equal ".\n", @r.gets 33 | assert_equal true, @r.wait(0) 34 | end 35 | 36 | def test_wait_forever 37 | q = Thread::Queue.new 38 | th = Thread.new { q.pop; @w.syswrite "." } 39 | q.push(true) 40 | assert_equal @r, @r.wait 41 | ensure 42 | th.join 43 | end 44 | 45 | def test_wait_eof 46 | q = Thread::Queue.new 47 | th = Thread.new { q.pop; @w.close } 48 | ret = nil 49 | assert_nothing_raised(Timeout::Error) do 50 | q.push(true) 51 | t = EnvUtil.apply_timeout_scale(1) 52 | Timeout.timeout(t) { ret = @r.wait } 53 | end 54 | assert_equal @r, ret 55 | ensure 56 | th.join 57 | end 58 | 59 | def test_wait_readable 60 | assert_nil @r.wait_readable(0) 61 | @w.syswrite "." 62 | IO.select([@r]) 63 | assert_equal @r, @r.wait_readable(0) 64 | end 65 | 66 | def test_wait_readable_buffered 67 | @w.syswrite ".\n!" 68 | assert_equal ".\n", @r.gets 69 | assert_equal true, @r.wait_readable(0) 70 | end 71 | 72 | def test_wait_readable_forever 73 | q = Thread::Queue.new 74 | th = Thread.new { q.pop; @w.syswrite "." } 75 | q.push(true) 76 | assert_equal @r, @r.wait_readable 77 | ensure 78 | th.join 79 | end 80 | 81 | def test_wait_readable_eof 82 | q = Thread::Queue.new 83 | th = Thread.new { q.pop; @w.close } 84 | ret = nil 85 | assert_nothing_raised(Timeout::Error) do 86 | q.push(true) 87 | t = EnvUtil.apply_timeout_scale(1) 88 | Timeout.timeout(t) { ret = @r.wait_readable } 89 | end 90 | assert_equal @r, ret 91 | ensure 92 | th.join 93 | end 94 | 95 | def test_wait_writable 96 | assert_equal @w, @w.wait_writable 97 | end 98 | 99 | def test_wait_writable_timeout 100 | assert_equal @w, @w.wait_writable(0.01) 101 | written = fill_pipe 102 | assert_nil @w.wait_writable(0.01) 103 | @r.read(written) 104 | assert_equal @w, @w.wait_writable(0.01) 105 | end 106 | 107 | def test_wait_writable_EPIPE 108 | fill_pipe 109 | @r.close 110 | assert_equal @w, @w.wait_writable 111 | end 112 | 113 | def test_wait_writable_closed 114 | @w.close 115 | assert_raise(IOError) { @w.wait_writable } 116 | end 117 | 118 | def test_wait_readwrite 119 | assert_equal @r.wait(0, :write), @r.wait(0, :read_write) 120 | end 121 | 122 | def test_wait_readwrite_timeout 123 | assert_equal @w, @w.wait(0.01, :read_write) 124 | written = fill_pipe 125 | if /aix/ =~ RUBY_PLATFORM 126 | # IO#wait internally uses select(2) on AIX. 127 | # AIX's select(2) returns "readable" for the write-side fd 128 | # of a pipe, so @w.wait(0.01, :read_write) does not return nil. 129 | assert_equal @w, @w.wait(0.01, :read_write) 130 | else 131 | assert_nil @w.wait(0.01, :read_write) 132 | end 133 | @r.read(written) 134 | assert_equal @w, @w.wait(0.01, :read_write) 135 | end 136 | 137 | def test_wait_mask_writable 138 | omit("Missing IO::WRITABLE!") unless IO.const_defined?(:WRITABLE) 139 | assert_equal IO::WRITABLE, @w.wait(IO::WRITABLE, 0) 140 | end 141 | 142 | def test_wait_mask_readable 143 | omit("Missing IO::READABLE!") unless IO.const_defined?(:READABLE) 144 | @w.write("Hello World\n" * 3) 145 | assert_equal IO::READABLE, @r.wait(IO::READABLE, 0) 146 | 147 | @r.gets 148 | assert_equal IO::READABLE, @r.wait(IO::READABLE, 0) 149 | end 150 | 151 | def test_wait_mask_zero 152 | omit("Missing IO::WRITABLE!") unless IO.const_defined?(:WRITABLE) 153 | assert_raise(ArgumentError) do 154 | @w.wait(0, 0) 155 | end 156 | end 157 | 158 | def test_wait_mask_negative 159 | omit("Missing IO::WRITABLE!") unless IO.const_defined?(:WRITABLE) 160 | assert_raise(ArgumentError) do 161 | @w.wait(-6, 0) 162 | end 163 | end 164 | 165 | private 166 | 167 | def fill_pipe 168 | written = 0 169 | buf = " " * 4096 170 | begin 171 | written += @w.write_nonblock(buf) 172 | rescue Errno::EAGAIN, Errno::EWOULDBLOCK 173 | return written 174 | end while true 175 | end 176 | 177 | def sleep(time) 178 | super EnvUtil.apply_timeout_scale(time) 179 | end 180 | end if IO.method_defined?(:wait) 181 | -------------------------------------------------------------------------------- /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 | import static org.jruby.api.Warn.warnDeprecated; 48 | 49 | /** 50 | * @author Nick Sieger 51 | */ 52 | public class IOWaitLibrary implements Library { 53 | 54 | public void load(Ruby runtime, boolean wrap) { 55 | RubyClass ioClass = runtime.getIO(); 56 | ioClass.defineAnnotatedMethods(IOWaitLibrary.class); 57 | } 58 | 59 | @JRubyMethod(optional = 1) 60 | public static IRubyObject wait_readable(ThreadContext context, IRubyObject _io, IRubyObject[] argv) { 61 | RubyIO io = (RubyIO)_io; 62 | OpenFile fptr = io.getOpenFileChecked(); 63 | 64 | fptr.checkReadable(context); 65 | 66 | long tv = prepareTimeout(context, argv); 67 | 68 | if (fptr.readPending() != 0) return context.tru; 69 | 70 | return doWait(context, io, fptr, tv, SelectionKey.OP_READ | SelectionKey.OP_ACCEPT); 71 | } 72 | 73 | /** 74 | * waits until input available or timed out and returns self, or nil when EOF reached. 75 | */ 76 | @JRubyMethod(optional = 1) 77 | public static IRubyObject wait_writable(ThreadContext context, IRubyObject _io, IRubyObject[] argv) { 78 | RubyIO io = (RubyIO)_io; 79 | 80 | OpenFile fptr = io.getOpenFileChecked(); 81 | 82 | fptr.checkWritable(context); 83 | 84 | long tv = prepareTimeout(context, argv); 85 | 86 | return doWait(context, io, fptr, tv, SelectionKey.OP_CONNECT | SelectionKey.OP_WRITE); 87 | } 88 | 89 | @JRubyMethod(optional = 2) 90 | public static IRubyObject wait(ThreadContext context, IRubyObject _io, IRubyObject[] argv) { 91 | RubyIO io = (RubyIO)_io; 92 | 93 | OpenFile fptr = io.getOpenFileChecked(); 94 | 95 | int ops = 0; 96 | 97 | if (argv.length == 2) { 98 | if (argv[1] instanceof RubySymbol) { 99 | RubySymbol sym = (RubySymbol) argv[1]; 100 | switch (sym.asJavaString()) { // 7 bit comparison 101 | case "r": 102 | case "read": 103 | case "readable": 104 | ops |= SelectionKey.OP_ACCEPT | SelectionKey.OP_READ; 105 | break; 106 | case "w": 107 | case "write": 108 | case "writable": 109 | ops |= SelectionKey.OP_CONNECT | SelectionKey.OP_WRITE; 110 | break; 111 | case "rw": 112 | case "read_write": 113 | case "readable_writable": 114 | ops |= SelectionKey.OP_ACCEPT | SelectionKey.OP_READ | SelectionKey.OP_CONNECT | SelectionKey.OP_WRITE; 115 | break; 116 | default: 117 | throw context.runtime.newArgumentError("unsupported mode: " + sym); 118 | } 119 | } else { 120 | throw context.runtime.newArgumentError("unsupported mode: " + argv[1].getType()); 121 | } 122 | } else { 123 | ops |= SelectionKey.OP_ACCEPT | SelectionKey.OP_READ; 124 | } 125 | 126 | if ((ops & SelectionKey.OP_READ) == SelectionKey.OP_READ && fptr.readPending() != 0) return context.tru; 127 | 128 | long tv = prepareTimeout(context, argv); 129 | 130 | return doWait(context, io, fptr, tv, ops); 131 | } 132 | 133 | private static IRubyObject doWait(ThreadContext context, RubyIO io, OpenFile fptr, long tv, int ops) { 134 | boolean ready = fptr.ready(context.runtime, context.getThread(), ops, tv); 135 | fptr.checkClosed(); 136 | if (ready) return io; 137 | return context.nil; 138 | } 139 | 140 | private static long prepareTimeout(ThreadContext context, IRubyObject[] argv) { 141 | IRubyObject timeout; 142 | long tv; 143 | switch (argv.length) { 144 | case 2: 145 | case 1: 146 | timeout = argv[0]; 147 | break; 148 | default: 149 | timeout = context.nil; 150 | } 151 | 152 | if (timeout.isNil()) { 153 | tv = -1; 154 | } 155 | else { 156 | tv = (long)(RubyTime.convertTimeInterval(context, timeout) * 1000); 157 | if (tv < 0) throw context.runtime.newArgumentError("time interval must be positive"); 158 | } 159 | return tv; 160 | } 161 | } 162 | --------------------------------------------------------------------------------