├── .gitignore ├── .rspec ├── .ruby-version ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── _guard-core ├── console ├── guard └── setup ├── examples ├── beams.rb ├── echo_server.rb ├── echo_server_with_raw_socket.rb ├── http_requests.rb └── monkey_patch.rb ├── lib ├── lightio.rb └── lightio │ ├── core.rb │ ├── core │ ├── backend │ │ └── nio.rb │ ├── beam.rb │ ├── future.rb │ ├── ioloop.rb │ └── light_fiber.rb │ ├── errors.rb │ ├── library.rb │ ├── library │ ├── base.rb │ ├── file.rb │ ├── io.rb │ ├── kernel_ext.rb │ ├── openssl.rb │ ├── queue.rb │ ├── sized_queue.rb │ ├── socket.rb │ ├── thread.rb │ ├── threads_wait.rb │ └── timeout.rb │ ├── module.rb │ ├── module │ ├── base.rb │ ├── file.rb │ ├── io.rb │ ├── openssl.rb │ ├── socket.rb │ ├── thread.rb │ └── threads_wait.rb │ ├── monkey.rb │ ├── raw_proxy.rb │ ├── version.rb │ ├── watchers.rb │ ├── watchers │ ├── io.rb │ ├── schedule.rb │ ├── timer.rb │ └── watcher.rb │ └── wrap.rb ├── lightio.gemspec └── spec ├── helper_methods.rb ├── lightio ├── core │ ├── beam_spec.rb │ ├── future_spec.rb │ ├── ioloop_spec.rb │ └── light_fiber_spec.rb ├── library │ ├── file_spec.rb │ ├── io_spec.rb │ ├── kernel_ext_spec.rb │ ├── mutex_spec.rb │ ├── openssl_spec.rb │ ├── queue_spec.rb │ ├── sized_queue_spec.rb │ ├── socket_spec.rb │ ├── thread_spec.rb │ ├── threads_wait_spec.rb │ └── timeout_spec.rb ├── monkey_patch │ ├── io_spec.rb │ ├── net_http_spec.rb │ ├── openssl_spec.rb │ └── timeout_spec.rb ├── monkey_spec.rb └── watchers │ ├── io_spec.rb │ └── timer_spec.rb ├── lightio_spec.rb ├── monkey_patch.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | 13 | .idea -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format progress 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.5.0-dev 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - jruby-9.1.15.0 # latest stable 5 | - 2.3.4 6 | - 2.4.1 7 | - 2.5.0 8 | - ruby-head 9 | 10 | env: 11 | global: 12 | - JRUBY_OPTS="--dev -J-Djruby.launch.inproc=true -J-Xmx1024M" 13 | - COVERAGE=true 14 | 15 | matrix: 16 | allow_failures: 17 | - rvm: ruby-head 18 | - rvm: jruby-9.1.15.0 19 | fast_finish: true 20 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at jjyruby@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in lightio.gemspec 6 | gemspec 7 | 8 | group :development do 9 | gem 'guard' 10 | gem 'guard-rspec' 11 | gem 'guard-bundler' 12 | end 13 | 14 | group :test do 15 | gem 'coveralls', require: false 16 | end 17 | 18 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | lightio (0.4.4) 5 | nio4r (~> 2.2) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | coderay (1.1.1) 11 | coveralls (0.8.21) 12 | json (>= 1.8, < 3) 13 | simplecov (~> 0.14.1) 14 | term-ansicolor (~> 1.3) 15 | thor (~> 0.19.4) 16 | tins (~> 1.6) 17 | diff-lcs (1.3) 18 | docile (1.1.5) 19 | ffi (1.11.1) 20 | formatador (0.2.5) 21 | guard (2.14.1) 22 | formatador (>= 0.2.4) 23 | listen (>= 2.7, < 4.0) 24 | lumberjack (~> 1.0) 25 | nenv (~> 0.1) 26 | notiffany (~> 0.0) 27 | pry (>= 0.9.12) 28 | shellany (~> 0.0) 29 | thor (>= 0.18.1) 30 | guard-bundler (2.1.0) 31 | bundler (~> 1.0) 32 | guard (~> 2.2) 33 | guard-compat (~> 1.1) 34 | guard-compat (1.2.1) 35 | guard-rspec (4.7.3) 36 | guard (~> 2.1) 37 | guard-compat (~> 1.1) 38 | rspec (>= 2.99.0, < 4.0) 39 | json (2.1.0) 40 | listen (3.1.5) 41 | rb-fsevent (~> 0.9, >= 0.9.4) 42 | rb-inotify (~> 0.9, >= 0.9.7) 43 | ruby_dep (~> 1.2) 44 | lumberjack (1.0.12) 45 | method_source (0.8.2) 46 | nenv (0.3.0) 47 | nio4r (2.2.0) 48 | notiffany (0.1.1) 49 | nenv (~> 0.1) 50 | shellany (~> 0.0) 51 | pry (0.10.4) 52 | coderay (~> 1.1.0) 53 | method_source (~> 0.8.1) 54 | slop (~> 3.4) 55 | rake (10.1.0) 56 | rb-fsevent (0.10.2) 57 | rb-inotify (0.9.10) 58 | ffi (>= 0.5.0, < 2) 59 | rspec (3.7.0) 60 | rspec-core (~> 3.7.0) 61 | rspec-expectations (~> 3.7.0) 62 | rspec-mocks (~> 3.7.0) 63 | rspec-core (3.7.0) 64 | rspec-support (~> 3.7.0) 65 | rspec-expectations (3.7.0) 66 | diff-lcs (>= 1.2.0, < 2.0) 67 | rspec-support (~> 3.7.0) 68 | rspec-mocks (3.7.0) 69 | diff-lcs (>= 1.2.0, < 2.0) 70 | rspec-support (~> 3.7.0) 71 | rspec-support (3.7.0) 72 | ruby_dep (1.5.0) 73 | shellany (0.0.1) 74 | simplecov (0.14.1) 75 | docile (~> 1.1.0) 76 | json (>= 1.8, < 3) 77 | simplecov-html (~> 0.10.0) 78 | simplecov-html (0.10.2) 79 | slop (3.6.0) 80 | term-ansicolor (1.6.0) 81 | tins (~> 1.0) 82 | thor (0.19.4) 83 | tins (1.16.3) 84 | 85 | PLATFORMS 86 | ruby 87 | 88 | DEPENDENCIES 89 | bundler (~> 1.16) 90 | coveralls 91 | guard 92 | guard-bundler 93 | guard-rspec 94 | lightio! 95 | rake (~> 10.0) 96 | rspec (~> 3.0) 97 | 98 | BUNDLED WITH 99 | 1.16.1 100 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | directories %w(. lib spec) \ 2 | .select {|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")} 3 | 4 | guard :bundler do 5 | watch('Gemfile') 6 | end 7 | 8 | guard :rspec, all_after_pass: false, all_on_start: false, failed_mode: :keep, cmd: 'bundle exec rspec' do 9 | watch(%r{^(lib|spec)/(.+?)(_spec)?\.rb}) {|m| "spec/#{m[2]}_spec.rb"} 10 | end 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Jiang Jinyang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LightIO 2 | 3 | 4 | [![Gem Version](https://badge.fury.io/rb/lightio.svg)](http://rubygems.org/gems/lightio) 5 | [![Build Status](https://travis-ci.org/socketry/lightio.svg?branch=master)](https://travis-ci.org/socketry/lightio) 6 | [![Coverage Status](https://coveralls.io/repos/github/socketry/lightio/badge.svg?branch=master)](https://coveralls.io/github/socketry/lightio?branch=master) 7 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/jjyr/lightio/blob/master/LICENSE.txt) 8 | [![Gitter](https://badges.gitter.im/join.svg)](https://gitter.im/lightio-dev/Lobby) 9 | 10 | LightIO provides green thread to ruby. Like Golang's goroutine, or Crystal's fiber. In LightIO it is called beam. 11 | 12 | Example: 13 | 14 | ``` ruby 15 | require 'lightio' 16 | 17 | start = Time.now 18 | 19 | beams = 1000.times.map do 20 | # LightIO::Beam is green-thread, use it instead Thread 21 | LightIO::Beam.new do 22 | # do some io operations in beam 23 | LightIO.sleep(1) 24 | end 25 | end 26 | 27 | beams.each(&:join) 28 | seconds = Time.now - start 29 | puts "1000 beams take #{seconds - 1} seconds to create" 30 | 31 | ``` 32 | 33 | 34 | LightIO ship ruby stdlib compatible library under `LightIO` or `LightIO::Library` namespace, 35 | these libraries provide the ability to schedule LightIO beams when IO operations occur. 36 | 37 | 38 | LightIO also provide a monkey patch, it replaces ruby `Thread` with `LightIO::Thread`, and also replaces `IO` related classes. 39 | 40 | Example: 41 | 42 | ``` ruby 43 | require 'lightio' 44 | # apply monkey patch at beginning 45 | LightIO::Monkey.patch_all! 46 | 47 | require 'net/http' 48 | 49 | host = 'github.com' 50 | port = 443 51 | 52 | start = Time.now 53 | 54 | 10.times.map do 55 | Thread.new do 56 | Net::HTTP.start(host, port, use_ssl: true) do |http| 57 | res = http.request_get('/ping') 58 | p res.code 59 | end 60 | end 61 | end.each(&:join) 62 | 63 | puts "#{Time.now - start} seconds" 64 | 65 | ``` 66 | 67 | See [Examples](/examples) for detail. 68 | 69 | ### You Should Know 70 | 71 | In fact ruby core team already plan to implement `Thread::Green` in core language, see https://bugs.ruby-lang.org/issues/13618 72 | 73 | It means if ruby implemented `Thread::Green`, this library has no reason to exist. 74 | But as a crazy userland implemented green thread library, it bring lots of fun to me, so I will continue to maintain it, and welcome to use. 75 | 76 | 77 | See [Wiki](https://github.com/jjyr/lightio/wiki) and [Roadmap](https://github.com/jjyr/lightio/wiki/Current-status-and-roadmap) to get more information. 78 | 79 | LightIO is build upon [nio4r](https://github.com/socketry/nio4r). Get heavily inspired by [gevent](http://www.gevent.org/), [async-io](https://github.com/socketry/async-io). 80 | 81 | 82 | ## Installation 83 | 84 | Add this line to your application's Gemfile: 85 | 86 | ```ruby 87 | gem 'lightio' 88 | ``` 89 | 90 | And then execute: 91 | 92 | $ bundle 93 | 94 | Or install it yourself as: 95 | 96 | $ gem install lightio 97 | 98 | ## Documentation 99 | 100 | [Please see LightIO Wiki](https://github.com/jjyr/lightio/wiki) for more information. 101 | 102 | The following documentations is also usable: 103 | 104 | * [Basic usage](https://github.com/socketry/lightio/wiki/Basic-Usage) 105 | * [YARD documentation](http://www.rubydoc.info/github/socketry/lightio/master) 106 | * [Examples](/examples) 107 | 108 | ## Discussion 109 | 110 | https://groups.google.com/group/lightio 111 | 112 | ## Development 113 | 114 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 115 | 116 | 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). 117 | 118 | ## Contributing 119 | 120 | Bug reports and pull requests are welcome on GitHub at https://github.com/jjyr/lightio. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 121 | 122 | ## License 123 | 124 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 125 | 126 | Copyright, 2017-2018, by [Jiang Jinyang](http://justjjy.com/) 127 | 128 | ## Code of Conduct 129 | 130 | Everyone interacting in the Lightio project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/lightio/blob/master/CODE_OF_CONDUCT.md). 131 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:"spec:library") do |t| 5 | t.exclude_pattern = 'spec/**/monkey_spec.rb' 6 | t.rspec_opts = "--tag ~skip_library" 7 | end 8 | 9 | RSpec::Core::RakeTask.new(:"spec:monkey_patch") do |t| 10 | t.rspec_opts = "-r monkey_patch.rb --tag ~skip_monkey_patch" 11 | end 12 | 13 | task :default => :spec 14 | 15 | task :spec => [:"spec:library", :"spec:monkey_patch"] 16 | -------------------------------------------------------------------------------- /bin/_guard-core: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application '_guard-core' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | bundle_binstub = File.expand_path("../bundle", __FILE__) 12 | load(bundle_binstub) if File.file?(bundle_binstub) 13 | 14 | require "pathname" 15 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 16 | Pathname.new(__FILE__).realpath) 17 | 18 | require "rubygems" 19 | require "bundler/setup" 20 | 21 | load Gem.bin_path("guard", "_guard-core") 22 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "lightio" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/guard: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'guard' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | bundle_binstub = File.expand_path("../bundle", __FILE__) 12 | load(bundle_binstub) if File.file?(bundle_binstub) 13 | 14 | require "pathname" 15 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 16 | Pathname.new(__FILE__).realpath) 17 | 18 | require "rubygems" 19 | require "bundler/setup" 20 | 21 | load Gem.bin_path("guard", "guard") 22 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /examples/beams.rb: -------------------------------------------------------------------------------- 1 | require 'lightio' 2 | 3 | start = Time.now 4 | 5 | beams = 1000.times.map do 6 | # LightIO::Beam is a thread-like executor, use it instead Thread 7 | LightIO::Beam.new do 8 | # do some io operations in beam 9 | LightIO.sleep(1) 10 | end 11 | end 12 | 13 | beams.each(&:join) 14 | seconds = Time.now - start 15 | puts "1000 beams take #{seconds - 1} seconds to create" 16 | -------------------------------------------------------------------------------- /examples/echo_server.rb: -------------------------------------------------------------------------------- 1 | # Example from https://github.com/socketry/nio4r/blob/master/examples/echo_server.rb 2 | # rewrite it in lightio for demonstrate 3 | # this example demonstrate LightIO Libraries API 4 | # look LightIO::Library namespace to find more 5 | 6 | require 'lightio' 7 | 8 | class EchoServer 9 | def initialize(host, port) 10 | @server = LightIO::TCPServer.new(host, port) 11 | end 12 | 13 | def run 14 | while (socket = @server.accept) 15 | _, port, host = socket.peeraddr 16 | puts "accept connection from #{host}:#{port}" 17 | 18 | # LightIO::Beam is lightweight executor, provide thread-like interface 19 | # just start new beam for per socket 20 | LightIO::Beam.new(socket) do |socket| 21 | loop do 22 | echo(socket) 23 | end 24 | end 25 | end 26 | end 27 | 28 | def echo(socket) 29 | data = socket.readpartial(4096) 30 | socket.write(data) 31 | rescue EOFError 32 | _, port, host = socket.peeraddr 33 | puts "*** #{host}:#{port} disconnected" 34 | socket.close 35 | raise 36 | end 37 | end 38 | 39 | 40 | EchoServer.new('localhost', 3000).run if __FILE__ == $0 41 | -------------------------------------------------------------------------------- /examples/echo_server_with_raw_socket.rb: -------------------------------------------------------------------------------- 1 | # Example from https://github.com/socketry/nio4r/blob/master/examples/echo_server.rb 2 | # rewrite it in lightio for demonstrate 3 | # this example demonstrate LightIO low-level API 4 | # how to use ruby 'raw'(unpatched) socket with LightIO 5 | 6 | require 'lightio' 7 | require 'socket' 8 | 9 | class EchoServer 10 | def initialize(host, port) 11 | @server = TCPServer.new(host, port) 12 | end 13 | 14 | def run 15 | # wait server until readable 16 | server_watcher = LightIO::Watchers::IO.new(@server, :r) 17 | while server_watcher.wait_readable 18 | socket = @server.accept 19 | _, port, host = socket.peeraddr 20 | puts "accept connection from #{host}:#{port}" 21 | 22 | # LightIO::Beam is lightweight executor, provide thread-like interface 23 | # just start new beam for per socket 24 | LightIO::Beam.new(socket) do |socket| 25 | socket_watcher = LightIO::Watchers::IO.new(socket, :r) 26 | begin 27 | while socket_watcher.wait_readable 28 | echo(socket) 29 | end 30 | rescue EOFError 31 | _, port, host = socket.peeraddr 32 | puts "*** #{host}:#{port} disconnected" 33 | # remove close socket watcher 34 | socket_watcher.close 35 | socket.close 36 | end 37 | end 38 | end 39 | end 40 | 41 | def echo(socket) 42 | data = socket.read_nonblock(4096) 43 | socket.write_nonblock(data) 44 | end 45 | end 46 | 47 | 48 | EchoServer.new('localhost', 3000).run if __FILE__ == $0 49 | -------------------------------------------------------------------------------- /examples/http_requests.rb: -------------------------------------------------------------------------------- 1 | require 'lightio' 2 | # apply monkey patch at beginning 3 | LightIO::Monkey.patch_all! 4 | 5 | require 'net/http' 6 | 7 | host = 'github.com' 8 | port = 443 9 | 10 | start = Time.now 11 | 12 | 10.times.map do 13 | Thread.new do 14 | Net::HTTP.start(host, port, use_ssl: true) do |http| 15 | res = http.request_get('/ping') 16 | p res.code 17 | end 18 | end 19 | end.each(&:join) 20 | 21 | p Time.now - start -------------------------------------------------------------------------------- /examples/monkey_patch.rb: -------------------------------------------------------------------------------- 1 | require 'lightio' 2 | 3 | # apply monkey patch as early as possible 4 | # after monkey patch, it just normal ruby code 5 | LightIO::Monkey.patch_all! 6 | 7 | 8 | TCPServer.open('localhost', 3000) do |server| 9 | while (socket = server.accept) 10 | _, port, host = socket.peeraddr 11 | puts "accept connection from #{host}:#{port}" 12 | 13 | # Don't worry, Thread.new create green threads, it cost very light 14 | Thread.new(socket) do |socket| 15 | data = nil 16 | begin 17 | socket.write(data) while (data = socket.readpartial(4096)) 18 | rescue EOFError 19 | _, port, host = socket.peeraddr 20 | puts "*** #{host}:#{port} disconnected" 21 | socket.close 22 | end 23 | end 24 | 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/lightio.rb: -------------------------------------------------------------------------------- 1 | # LightIO 2 | require 'lightio/version' 3 | require 'lightio/errors' 4 | require 'lightio/raw_proxy' 5 | require 'lightio/core' 6 | require 'lightio/watchers' 7 | require 'lightio/wrap' 8 | require 'lightio/module' 9 | require 'lightio/library' 10 | require 'lightio/monkey' 11 | 12 | # LightIO provide light-weight executor: LightIO::Beam and batch io libraries, 13 | # view LightIO::Core::Beam to learn how to concurrent programming with Beam, 14 | # view LightIO::Watchers::IO to learn how to manage 'raw' io objects, 15 | # Core and Library modules are included under LightIO namespace, so you can use LightIO::Beam for convenient 16 | module LightIO 17 | include Core 18 | include Library 19 | end 20 | -------------------------------------------------------------------------------- /lib/lightio/core.rb: -------------------------------------------------------------------------------- 1 | # Core 2 | require_relative 'core/ioloop' 3 | require_relative 'core/light_fiber' 4 | require_relative 'core/future' 5 | require_relative 'core/beam' 6 | 7 | # LightIO::Core include core classes: Beam, IOloop, Future 8 | # 9 | # view examples for clues 10 | module LightIO::Core 11 | end 12 | -------------------------------------------------------------------------------- /lib/lightio/core/backend/nio.rb: -------------------------------------------------------------------------------- 1 | # use nio4r implement event loop, inspired from eventmachine/pure_ruby implement 2 | require 'nio' 3 | require 'set' 4 | require 'forwardable' 5 | module LightIO::Core 6 | module Backend 7 | 8 | class Error < RuntimeError 9 | end 10 | 11 | class UnknownTimer < Error 12 | end 13 | 14 | class Timers 15 | def generate_uuid 16 | @ix ||= 0 17 | @ix += 1 18 | end 19 | 20 | def initialize 21 | @timers = SortedSet.new 22 | @timers_registry = {} 23 | end 24 | 25 | def add_timer(timer) 26 | uuid = generate_uuid 27 | @timers.add([Time.now + timer.interval, uuid]) 28 | @timers_registry[uuid] = timer.callback 29 | end 30 | 31 | def cancel_timer(timer) 32 | raise Error, "unregistered timer" unless timer.uuid && @timers_registry.has_key?(timer.uuid) 33 | @timers_registry[uuid] = false 34 | end 35 | 36 | def fire(current_loop_time) 37 | @timers.each do |t| 38 | if t.first <= current_loop_time 39 | @timers.delete(t) 40 | callback = @timers_registry.delete(t.last) 41 | next if callback == false # timer cancelled 42 | raise UnknownTimer, "timer id: #{t.last}" if callback.nil? 43 | callback.call 44 | else 45 | break 46 | end 47 | end 48 | end 49 | end 50 | 51 | # LightIO use NIO as default event-driving backend 52 | class NIO 53 | extend Forwardable 54 | def_delegators :@selector, :closed?, :empty?, :backend, :wakeup 55 | 56 | attr_reader :running 57 | 58 | def initialize 59 | # @selector = NIO::Selector.new 60 | @current_loop_time = nil 61 | @running = false 62 | @timers = Timers.new 63 | @callbacks = [] 64 | @selector = ::NIO::Selector.new(env_backend) 65 | end 66 | 67 | def run 68 | raise Error, "already running" if @running 69 | @running = true 70 | loop do 71 | @current_loop_time = Time.now 72 | run_timers 73 | run_callbacks 74 | handle_selectables 75 | end 76 | end 77 | 78 | 79 | def add_callback(&blk) 80 | @callbacks << blk 81 | end 82 | 83 | def add_timer(timer) 84 | timer.uuid = @timers.add_timer(timer) 85 | end 86 | 87 | def cancel_timer(timer) 88 | @timers.cancel_timer(timer) 89 | end 90 | 91 | def add_io_wait(io, interests, &blk) 92 | monitor = @selector.register(io, interests) 93 | monitor.value = blk 94 | monitor 95 | end 96 | 97 | def cancel_io_wait(io) 98 | @selector.deregister(io) 99 | end 100 | 101 | def stop 102 | return if closed? 103 | @running = false 104 | @selector.close 105 | end 106 | 107 | alias close stop 108 | 109 | def env_backend 110 | key = 'LIGHTIO_BACKEND'.freeze 111 | ENV.has_key?(key) ? ENV[key].to_sym : nil 112 | end 113 | 114 | private 115 | 116 | def run_timers 117 | @timers.fire(@current_loop_time) 118 | end 119 | 120 | def handle_selectables 121 | @selector.select(0) do |monitor| 122 | # invoke callback if io is ready 123 | monitor.value.call(monitor.io) 124 | end 125 | end 126 | 127 | def run_callbacks 128 | # prevent 'add new callbacks' during callback call, new callbacks will run in next turn 129 | callbacks = @callbacks 130 | @callbacks = [] 131 | while (callback = callbacks.shift) 132 | callback.call 133 | end 134 | end 135 | end 136 | end 137 | end -------------------------------------------------------------------------------- /lib/lightio/core/beam.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module LightIO::Core 4 | # Beam is light-weight executor, provide thread-like interface 5 | # 6 | # @Example: 7 | # #- initialize with block 8 | # b = Beam.new{puts "hello"} 9 | # b.join 10 | # #output: hello 11 | # 12 | # b = Beam.new(1,2,3){|one, two, three| puts [one, two, three].join(",") } 13 | # b.join 14 | # #output: 1,2,3 15 | # 16 | # #- use join wait beam done 17 | # b = Beam.new(){LightIO.sleep 3} 18 | # b.join 19 | # b.alive? # false 20 | class Beam < LightFiber 21 | 22 | # special class for simulate Thread#raise for Beam 23 | class BeamError 24 | attr_reader :error, :parent 25 | extend Forwardable 26 | 27 | def_delegators :@error, :message, :backtrace 28 | 29 | def initialize(error) 30 | @error = error 31 | @parent = Beam.current 32 | end 33 | end 34 | 35 | attr_reader :error 36 | attr_accessor :on_dead 37 | 38 | # Create a new beam 39 | # 40 | # Beam is light-weight executor, provide thread-like interface 41 | # 42 | # Beam.new("hello"){|hello| puts hello } 43 | # 44 | # @param [Array] args pass arguments to Beam block 45 | # @param [Proc] blk block to execute 46 | # @return [Beam] 47 | def initialize(*args, &blk) 48 | raise LightIO::Error, "must be called with a block" unless blk 49 | super() { 50 | begin 51 | @value = yield(*args) 52 | rescue Exception => e 53 | @error = e 54 | end 55 | # mark as dead 56 | dead 57 | # transfer back to parent(caller fiber) after schedule 58 | parent.transfer 59 | } 60 | # schedule beam in ioloop 61 | ioloop.add_callback {transfer} 62 | @alive = true 63 | end 64 | 65 | def alive? 66 | super && @alive 67 | end 68 | 69 | # block and wait beam return a value 70 | def value 71 | if alive? 72 | self.parent = Beam.current 73 | ioloop.transfer 74 | end 75 | check_and_raise_error 76 | @value 77 | end 78 | 79 | # Block and wait beam dead 80 | # 81 | # @param [Numeric] limit wait limit seconds if limit > 0, return nil if beam still alive, else return beam self 82 | # @return [Beam, nil] 83 | def join(limit=nil) 84 | # try directly get result 85 | if !alive? || limit.nil? || limit <= 0 86 | # call value to raise error 87 | value 88 | return self 89 | end 90 | 91 | # return to current beam if beam done within time limit 92 | origin_parent = parent 93 | self.parent = Beam.current 94 | # set a transfer back timer 95 | timer = LightIO::Watchers::Timer.new(limit) 96 | timer.set_callback do 97 | if alive? 98 | caller_beam = parent 99 | # resume to origin parent 100 | self.parent = origin_parent 101 | caller_beam.transfer 102 | end 103 | end 104 | ioloop.add_timer(timer) 105 | ioloop.transfer 106 | 107 | if alive? 108 | nil 109 | else 110 | check_and_raise_error 111 | self 112 | end 113 | end 114 | 115 | # Kill beam 116 | # 117 | # @return [Beam] 118 | def kill 119 | dead 120 | parent.transfer if self == Beam.current 121 | self 122 | end 123 | 124 | # Fiber not provide raise method, so we have to simulate one 125 | # @param [BeamError] error currently only support raise BeamError 126 | def raise(error, message=nil, backtrace=nil) 127 | unless error.is_a?(BeamError) 128 | message ||= error.respond_to?(:message) ? error.message : nil 129 | backtrace ||= error.respond_to?(:backtrace) ? error.backtrace : nil 130 | super(error, message, backtrace) 131 | end 132 | self.parent = error.parent if error.parent 133 | if Beam.current == self 134 | raise(error.error, message, backtrace) 135 | else 136 | @error ||= error 137 | end 138 | end 139 | 140 | class << self 141 | 142 | # Schedule beams 143 | # 144 | # normally beam should be auto scheduled, use this method to manually trigger a schedule 145 | # 146 | # @return [nil] 147 | def pass 148 | running = IOloop.current.running 149 | schedule = LightIO::Watchers::Schedule.new 150 | IOloop.current.wait(schedule) 151 | # make sure ioloop run once 152 | pass unless running 153 | end 154 | end 155 | 156 | private 157 | 158 | # mark beam as dead 159 | def dead 160 | @alive = false 161 | on_dead.call(self) if on_dead 162 | end 163 | 164 | # Beam transfer back to parent after schedule 165 | # parent is fiber or beam who called value/join methods 166 | # if not present a parent, Beam will transfer to ioloop 167 | def parent=(parent) 168 | @parent = parent 169 | end 170 | 171 | # get parent/ioloop to transfer back 172 | def parent 173 | @parent || ioloop 174 | end 175 | 176 | def check_and_raise_error 177 | raise @error if @error 178 | end 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /lib/lightio/core/future.rb: -------------------------------------------------------------------------------- 1 | module LightIO::Core 2 | # Provide a safe way to transfer beam/fiber control flow. 3 | # 4 | # @Example: 5 | # future = Future.new 6 | # # future#value will block current beam 7 | # Beam.new{future.value} 8 | # # use transfer to set value 9 | # future.transfer(1) 10 | class Future 11 | def initialize 12 | @value = nil 13 | @ioloop = IOloop.current 14 | @state = :init 15 | @light_fiber = nil 16 | end 17 | 18 | def done? 19 | @state == :done 20 | end 21 | 22 | # Transfer and set result value 23 | # 24 | # use this method to set back result 25 | def transfer(value=nil) 26 | raise LightIO::Error, "state error" if done? 27 | @value = value 28 | done! 29 | @light_fiber.transfer if @light_fiber 30 | end 31 | 32 | def value=(value) 33 | transfer(value) 34 | end 35 | 36 | # Get value 37 | # 38 | # this method will block current beam/fiber, until future result is set. 39 | def value 40 | return @value if done? 41 | raise LightIO::Error, 'already used' if @light_fiber 42 | @light_fiber = LightFiber.current 43 | @ioloop.transfer 44 | @value 45 | end 46 | 47 | private 48 | 49 | def done! 50 | @state = :done 51 | end 52 | end 53 | end -------------------------------------------------------------------------------- /lib/lightio/core/ioloop.rb: -------------------------------------------------------------------------------- 1 | require 'lightio/core/backend/nio' 2 | require 'forwardable' 3 | 4 | module LightIO::Core 5 | # IOloop like a per-threaded EventMachine (cause fiber cannot resume cross threads) 6 | # 7 | # IOloop handle io waiting and schedule beams, user do not supposed to directly use this class 8 | class IOloop 9 | 10 | def initialize 11 | @fiber = Fiber.new {run} 12 | @backend = Backend::NIO.new 13 | end 14 | 15 | extend Forwardable 16 | def_delegators :@backend, :run, :add_timer, :add_callback, :add_io_wait, :cancel_io_wait, :backend, 17 | :close, :closed?, :stop, :running 18 | 19 | # Wait a watcher, watcher can be a timer or socket. 20 | # see LightIO::Watchers module for detail 21 | def wait(watcher) 22 | future = Future.new 23 | # add watcher to loop 24 | id = Object.new 25 | watcher.set_callback {|err| future.transfer([id, err])} 26 | watcher.start(self) 27 | # trigger a fiber switch 28 | # wait until watcher is ok 29 | # then do work 30 | response_id, err = future.value 31 | current_beam = LightIO::Core::Beam.current 32 | if response_id != id 33 | raise LightIO::InvalidTransferError, "expect #{id}, but get #{response_id}" 34 | elsif err 35 | # if future return a err 36 | # simulate Thread#raise to Beam , that we can shutdown beam blocking by socket accepting 37 | # transfer back to which beam occur this err 38 | # not sure this is a right way to do it 39 | current_beam.raise(err) if current_beam.is_a?(LightIO::Core::Beam) 40 | end 41 | # check beam error after wait 42 | current_beam.send(:check_and_raise_error) if current_beam.is_a?(LightIO::Core::Beam) 43 | end 44 | 45 | def transfer 46 | @fiber.transfer 47 | end 48 | 49 | THREAD_PROXY = ::LightIO::RawProxy.new(::Thread, 50 | methods: [:current], 51 | instance_methods: [:thread_variable_get, :thread_variable_set, :thread_variable?]) 52 | 53 | class << self 54 | # return current ioloop or create new one 55 | def current 56 | key = :"lightio.ioloop" 57 | current_thread = THREAD_PROXY.send(:current) 58 | unless THREAD_PROXY.instance_send(current_thread, :thread_variable?, key) 59 | THREAD_PROXY.instance_send(current_thread, :thread_variable_set, key, IOloop.new) 60 | end 61 | THREAD_PROXY.instance_send(current_thread, :thread_variable_get, key) 62 | end 63 | end 64 | end 65 | 66 | # Initialize IOloop 67 | IOloop.current 68 | end -------------------------------------------------------------------------------- /lib/lightio/core/light_fiber.rb: -------------------------------------------------------------------------------- 1 | require 'fiber' 2 | 3 | module LightIO::Core 4 | # LightFiber is internal represent, we make slight extend on ruby Fiber to bind fibers to IOLoop 5 | # 6 | # SHOULD NOT BE USED DIRECTLY 7 | class LightFiber < Fiber 8 | attr_reader :ioloop 9 | attr_accessor :on_transfer 10 | 11 | ROOT_FIBER = Fiber.current 12 | 13 | def initialize(ioloop: IOloop.current, &blk) 14 | @ioloop = ioloop 15 | super(&blk) 16 | end 17 | 18 | def transfer 19 | on_transfer.call(LightFiber.current, self) if on_transfer 20 | super 21 | end 22 | 23 | class << self 24 | def is_root?(fiber) 25 | ROOT_FIBER == fiber 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/lightio/errors.rb: -------------------------------------------------------------------------------- 1 | module LightIO 2 | class Error < RuntimeError 3 | end 4 | 5 | class InvalidTransferError < Error 6 | end 7 | end -------------------------------------------------------------------------------- /lib/lightio/library.rb: -------------------------------------------------------------------------------- 1 | require_relative 'library/base' 2 | require_relative 'library/queue' 3 | require_relative 'library/sized_queue' 4 | require_relative 'library/kernel_ext' 5 | require_relative 'library/timeout' 6 | require_relative 'library/io' 7 | require_relative 'library/file' 8 | require_relative 'library/socket' 9 | require_relative 'library/openssl' 10 | require_relative 'library/thread' 11 | require_relative 'library/threads_wait' 12 | 13 | module LightIO 14 | # Library include modules can cooperative with LightIO::Beam 15 | module Library 16 | # extend library modules 17 | def self.included(base) 18 | base.extend KernelExt 19 | base.extend Timeout 20 | end 21 | end 22 | end -------------------------------------------------------------------------------- /lib/lightio/library/base.rb: -------------------------------------------------------------------------------- 1 | module LightIO::Library 2 | module Base 3 | module MockMethods 4 | protected 5 | def mock(klass) 6 | @mock_klass = klass 7 | define_alias_methods 8 | define_method_missing(singleton_class, @mock_klass) 9 | define_instance_method_missing(self, :@obj) 10 | define_mock_methods 11 | define_inherited 12 | extend_class_methods 13 | end 14 | 15 | attr_reader :mock_klass 16 | 17 | private 18 | 19 | def define_alias_methods 20 | class_methods_module = LightIO::Module.const_get("#{mock_klass}::ClassMethods") rescue nil 21 | return unless class_methods_module 22 | methods = class_methods_module.instance_methods(false).select {|method| mock_klass.respond_to?(method)} 23 | methods.each do |method| 24 | origin_method_name = "origin_#{method}" 25 | mock_klass.singleton_class.__send__(:alias_method, origin_method_name, method) 26 | mock_klass.singleton_class.__send__(:protected, origin_method_name) 27 | end 28 | end 29 | 30 | def define_method_missing(base, target_var) 31 | base.send(:define_method, :method_missing) {|*args| target_var.__send__(*args)} 32 | base.send(:define_method, :respond_to_missing?) {|method, *| target_var.respond_to?(method)} 33 | end 34 | 35 | def define_instance_method_missing(base, target_var) 36 | base.send(:define_method, :method_missing) {|*args| instance_variable_get(target_var).__send__(*args)} 37 | base.send(:define_method, :respond_to_missing?) {|method, *| instance_variable_get(target_var).respond_to?(method)} 38 | end 39 | 40 | def define_mock_methods 41 | define_method :is_a? do |klass| 42 | mock_klass = self.class.__send__(:call_method_from_ancestors, :mock_klass) 43 | return super(klass) unless mock_klass 44 | mock_klass <= klass || super(klass) 45 | end 46 | 47 | alias_method :kind_of?, :is_a? 48 | 49 | define_method :instance_of? do |klass| 50 | mock_klass = self.class.__send__(:mock_klass) 51 | return super(klass) unless mock_klass 52 | mock_klass == klass || super(klass) 53 | end 54 | end 55 | 56 | def call_method_from_ancestors(method) 57 | __send__(method) || begin 58 | self.ancestors.each do |klass| 59 | result = klass.__send__(method) 60 | break result if result 61 | end 62 | end 63 | end 64 | 65 | def define_inherited 66 | mock_klass.define_singleton_method(:inherited) do |klass| 67 | super(klass) 68 | library_super_class = LightIO::Module::Base.find_library_class(self) 69 | library_klass = Class.new(library_super_class) do 70 | include LightIO::Library::Base 71 | mock klass 72 | end 73 | if klass.name 74 | LightIO::Library::Base.send(:full_const_set, LightIO::Library, klass.name, library_klass) 75 | else 76 | LightIO::Library::Base.send(:nameless_classes)[klass] = library_klass 77 | end 78 | klass.define_singleton_method :new do |*args, &blk| 79 | obj = library_klass.__send__ :allocate 80 | obj.__send__ :initialize, *args, &blk 81 | obj 82 | end 83 | end 84 | end 85 | 86 | def extend_class_methods 87 | class_methods_module = LightIO::Module.const_get("#{mock_klass}::ClassMethods") 88 | self.__send__ :extend, class_methods_module 89 | rescue NameError 90 | nil 91 | end 92 | end 93 | 94 | module ClassMethods 95 | def _wrap(obj) 96 | if obj.instance_of? self 97 | obj 98 | else 99 | mock_obj = allocate 100 | mock_obj.instance_variable_set(:@obj, obj) 101 | mock_obj.__send__(:call_lightio_initialize) 102 | mock_obj 103 | end 104 | end 105 | end 106 | 107 | def initialize(*args) 108 | obj = self.class.send(:call_method_from_ancestors, :mock_klass).send(:origin_new, *args) 109 | @obj = obj 110 | call_lightio_initialize 111 | @obj 112 | end 113 | 114 | private 115 | def call_lightio_initialize 116 | __send__(:lightio_initialize) if respond_to?(:lightio_initialize, true) 117 | end 118 | 119 | def light_io_raw_obj 120 | @obj 121 | end 122 | 123 | class << self 124 | def included(base) 125 | base.send :extend, MockMethods 126 | base.send :extend, ClassMethods 127 | end 128 | 129 | private 130 | def nameless_classes 131 | @nick_classes ||= {} 132 | end 133 | 134 | def full_const_set(base, mod_name, const) 135 | mods = mod_name.split("::") 136 | mod_name = mods.pop 137 | full_mod_name = base.to_s 138 | mods.each do |mod| 139 | parent_mod = Object.const_get(full_mod_name) 140 | parent_mod.const_get(mod) && next rescue nil 141 | parent_mod.const_set(mod, Module.new) 142 | full_mod_name = "#{full_mod_name}::#{mod}" 143 | end 144 | Object.const_get(full_mod_name).const_set(mod_name, const) 145 | end 146 | end 147 | end 148 | end -------------------------------------------------------------------------------- /lib/lightio/library/file.rb: -------------------------------------------------------------------------------- 1 | module LightIO::Library 2 | class File < LightIO::Library::IO 3 | include Base 4 | include LightIO::Wrap::IOWrapper 5 | 6 | mock ::File 7 | extend LightIO::Module::File::ClassMethods 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/lightio/library/io.rb: -------------------------------------------------------------------------------- 1 | require 'io/wait' 2 | 3 | module LightIO::Library 4 | class IO 5 | include Base 6 | include LightIO::Wrap::IOWrapper 7 | 8 | mock ::IO 9 | extend LightIO::Module::IO::ClassMethods 10 | 11 | def to_io 12 | self 13 | end 14 | 15 | # abstract for io-like operations 16 | module IOMethods 17 | def lightio_initialize 18 | @readbuf = StringIO.new 19 | @readbuf.set_encoding(@obj.external_encoding) if @obj.respond_to?(:external_encoding) 20 | @eof = nil 21 | @seek = 0 22 | end 23 | 24 | def wait(timeout = nil, mode = :read) 25 | # avoid wait if can immediately return 26 | wait_result = if RUBY_VERSION < '2.4' 27 | if mode == :read 28 | @obj.wait_readable(0) 29 | elsif mode == :write 30 | @obj.wait_writable(0) 31 | end 32 | else 33 | @obj.wait(0, mode) 34 | end 35 | (wait_result || io_watcher.wait(timeout, mode)) && self 36 | end 37 | 38 | def wait_readable(timeout = nil) 39 | wait(timeout, :read) && self 40 | end 41 | 42 | def wait_writable(timeout = nil) 43 | wait(timeout, :write) && self 44 | end 45 | 46 | def write(string) 47 | s = StringIO.new(string.to_s) 48 | remain_size = s.size 49 | loop do 50 | result = write_nonblock(s.read, exception: false) 51 | case result 52 | when :wait_writable 53 | io_watcher.wait_writable 54 | else 55 | remain_size -= result 56 | unless remain_size.zero? 57 | s.seek(result) 58 | end 59 | return result 60 | end 61 | end 62 | end 63 | 64 | alias << write 65 | 66 | def read(length = nil, outbuf = nil) 67 | while !fill_read_buf && (length.nil? || length > @readbuf.length - @readbuf.pos) 68 | wait_readable 69 | end 70 | @readbuf.read(length, outbuf) 71 | end 72 | 73 | def readpartial(maxlen, outbuf = nil) 74 | raise ArgumentError, "negative length #{maxlen} given" if maxlen < 0 75 | fill_read_buf 76 | while @readbuf.eof? && !io_eof? 77 | wait_readable 78 | fill_read_buf 79 | end 80 | @readbuf.readpartial(maxlen, outbuf) 81 | end 82 | 83 | def getbyte 84 | read(1) 85 | end 86 | 87 | def getc 88 | fill_read_buf 89 | until (c = @readbuf.getc) 90 | return nil if nonblock_eof? 91 | wait_readable 92 | fill_read_buf 93 | end 94 | c 95 | end 96 | 97 | def readline(*args) 98 | line = gets(*args) 99 | raise EOFError, 'end of file reached' if line.nil? 100 | line 101 | end 102 | 103 | def readlines(*args) 104 | until fill_read_buf 105 | wait_readable 106 | end 107 | @readbuf.readlines(*args) 108 | end 109 | 110 | def readchar 111 | c = getc 112 | raise EOFError, 'end of file reached' if c.nil? 113 | c 114 | end 115 | 116 | def readbyte 117 | b = getbyte 118 | raise EOFError, 'end of file reached' if b.nil? 119 | b 120 | end 121 | 122 | def eof 123 | # until eof have a value 124 | fill_read_buf 125 | while @readbuf.eof? && @eof.nil? 126 | wait_readable 127 | fill_read_buf 128 | end 129 | nonblock_eof? 130 | end 131 | 132 | alias eof? eof 133 | 134 | def gets(*args) 135 | raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..2)" if args.size > 2 136 | sep = $/ 137 | if args[0].is_a?(Numeric) 138 | limit = args[0] 139 | else 140 | sep = args[0] if args.size > 0 141 | limit = args[1] if args[1].is_a?(Numeric) 142 | end 143 | until fill_read_buf 144 | break if limit && limit <= @readbuf.length 145 | break if sep && @readbuf.string.index(sep) 146 | wait_readable 147 | end 148 | @readbuf.gets(*args) 149 | end 150 | 151 | def print(*obj) 152 | obj.each do |s| 153 | write(s) 154 | end 155 | end 156 | 157 | def printf(*args) 158 | write(sprintf(*args)) 159 | end 160 | 161 | def puts(*obj) 162 | obj.each do |s| 163 | write(s) 164 | write($/) 165 | end 166 | end 167 | 168 | def flush 169 | @obj.flush 170 | self 171 | end 172 | 173 | def close(*args) 174 | # close watcher before io closed 175 | io_watcher.close 176 | @obj.close 177 | end 178 | 179 | private 180 | 181 | def nonblock_eof? 182 | @readbuf.eof? && io_eof? 183 | end 184 | 185 | def io_eof? 186 | @eof 187 | end 188 | 189 | BUF_CHUNK_SIZE = 1024 * 16 190 | 191 | def fill_read_buf 192 | return true if @eof 193 | while (data = @obj.read_nonblock(BUF_CHUNK_SIZE, exception: false)) 194 | case data 195 | when :wait_readable, :wait_writable 196 | # set eof to unknown(nil) 197 | @eof = nil 198 | return nil 199 | else 200 | # set eof to false 201 | @eof = false if @eof.nil? 202 | @readbuf.string << data 203 | end 204 | end 205 | # set eof to true 206 | @eof = true 207 | end 208 | end 209 | 210 | def set_encoding(*args) 211 | @readbuf.set_encoding(*args) 212 | super(*args) 213 | end 214 | 215 | def lineno 216 | @readbuf.lineno 217 | end 218 | 219 | def lineno= no 220 | @readbuf.lineno = no 221 | end 222 | 223 | def rewind 224 | # clear buf if seek offset is not zero 225 | unless @seek.zero? 226 | @seek = 0 227 | @readbuf.string.clear 228 | end 229 | @readbuf.rewind 230 | end 231 | 232 | def seek(*args) 233 | @readbuf.string.clear 234 | @seek = args[0] 235 | @obj.seek(*args) 236 | end 237 | 238 | def binmode 239 | @obj.binmode 240 | self 241 | end 242 | 243 | prepend IOMethods 244 | end 245 | end 246 | -------------------------------------------------------------------------------- /lib/lightio/library/kernel_ext.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | require 'forwardable' 3 | module LightIO::Library 4 | module KernelExt 5 | KERNEL_PROXY = ::LightIO::RawProxy.new(::Kernel, 6 | methods: [:spawn, :`]) 7 | 8 | extend Forwardable 9 | 10 | def sleep(*duration) 11 | if duration.size > 1 12 | raise ArgumentError, "wrong number of arguments (given #{duration.size}, expected 0..1)" 13 | elsif duration.size == 0 14 | LightIO::IOloop.current.transfer 15 | end 16 | duration = duration[0] 17 | if duration.zero? 18 | LightIO::Beam.pass 19 | return 20 | end 21 | timer = LightIO::Watchers::Timer.new duration 22 | LightIO::IOloop.current.wait(timer) 23 | end 24 | 25 | def spawn(*commands, **options) 26 | options = options.dup 27 | options.each do |key, v| 28 | if key.is_a?(LightIO::Library::IO) 29 | options.delete(key) 30 | key = convert_io_or_array_to_raw(key) 31 | options[key] = v 32 | end 33 | if (io = convert_io_or_array_to_raw(v)) 34 | options[key] = io 35 | end 36 | end 37 | KERNEL_PROXY.send(:spawn, *commands, **options) 38 | end 39 | 40 | def `(cmd) 41 | Open3.popen3(cmd, out: STDOUT, err: STDERR) do |stdin, stdout, stderr, wait_thr| 42 | output = LightIO::Library::IO._wrap(stdout).read 43 | KERNEL_PROXY.send(:`, "exit #{wait_thr.value.exitstatus}") 44 | return output 45 | end 46 | end 47 | 48 | def system(*cmd, **opt) 49 | Open3.popen3(*cmd, **opt) do |stdin, stdout, stderr, wait_thr| 50 | return nil if LightIO::Library::IO._wrap(stderr).read.size > 0 51 | return wait_thr.value.exitstatus == 0 52 | end 53 | rescue Errno::ENOENT 54 | nil 55 | end 56 | 57 | def_delegators :stdin, :gets, :readline, :readlines 58 | 59 | private 60 | def convert_io_or_array_to_raw(io_or_array) 61 | if io_or_array.is_a?(LightIO::Library::IO) 62 | io_or_array.send(:light_io_raw_obj) 63 | elsif io_or_array.is_a?(Array) 64 | io_or_array.map {|io| convert_io_or_array_to_raw(io)} 65 | end 66 | end 67 | 68 | def stdin 69 | @stdin ||= LightIO::Library::IO._wrap($stdin) 70 | end 71 | 72 | def stdout 73 | @stdin ||= LightIO::Library::IO._wrap($stdout) 74 | end 75 | 76 | def stderr 77 | @stdin ||= LightIO::Library::IO._wrap($stderr) 78 | end 79 | end 80 | end -------------------------------------------------------------------------------- /lib/lightio/library/openssl.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | module LightIO::Library 3 | module OpenSSL 4 | module SSL 5 | class SSLSocket 6 | include Base 7 | include LightIO::Wrap::IOWrapper 8 | 9 | mock ::OpenSSL::SSL::SSLSocket 10 | prepend LightIO::Library::IO::IOMethods 11 | 12 | wrap_blocking_methods :connect, :accept 13 | 14 | def initialize(io, *args) 15 | if io.is_a?(LightIO::Library::IO) 16 | @_wrapped_socket = io 17 | io = io.send(:light_io_raw_obj) 18 | end 19 | super(io, *args) 20 | end 21 | 22 | def accept_nonblock 23 | socket = @obj.accept_nonblock(*args) 24 | socket.is_a?(Symbol) ? socket : self.class._wrap(socket) 25 | end 26 | 27 | def to_io 28 | @_wrapped_socket || @obj.io 29 | end 30 | 31 | alias io to_io 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/lightio/library/queue.rb: -------------------------------------------------------------------------------- 1 | module LightIO::Library 2 | class Queue 3 | extend Base::MockMethods 4 | mock ::Queue 5 | 6 | def initialize 7 | @queue = [] 8 | @waiters = [] 9 | @close = false 10 | end 11 | 12 | def close() 13 | #This is a stub, used for indexing 14 | @close = true 15 | @waiters.each {|w| w.transfer nil} 16 | self 17 | end 18 | 19 | # closed? 20 | # 21 | # Returns +true+ if the queue is closed. 22 | def closed?() 23 | @close 24 | end 25 | 26 | # push(object) 27 | # enq(object) 28 | # <<(object) 29 | # 30 | # Pushes the given +object+ to the queue. 31 | def push(object) 32 | raise ClosedQueueError, "queue closed" if @close 33 | if (waiter = @waiters.shift) 34 | future = LightIO::Future.new 35 | LightIO::IOloop.current.add_callback { 36 | waiter.transfer(object) 37 | future.transfer 38 | } 39 | future.value 40 | else 41 | @queue << object 42 | end 43 | self 44 | end 45 | 46 | alias enq push 47 | alias << push 48 | # pop(non_block=false) 49 | # deq(non_block=false) 50 | # shift(non_block=false) 51 | # 52 | # Retrieves data from the queue. 53 | # 54 | # If the queue is empty, the calling thread is suspended until data is pushed 55 | # onto the queue. If +non_block+ is true, the thread isn't suspended, and an 56 | # exception is raised. 57 | def pop(non_block=false) 58 | if @close 59 | return empty? ? nil : @queue.pop 60 | end 61 | if empty? 62 | if non_block 63 | raise ThreadError, 'queue empty' 64 | else 65 | future = LightIO::Future.new 66 | @waiters << future 67 | future.value 68 | end 69 | else 70 | @queue.pop 71 | end 72 | end 73 | 74 | alias deq pop 75 | alias shift pop 76 | # empty? 77 | # 78 | # Returns +true+ if the queue is empty. 79 | def empty?() 80 | @queue.empty? 81 | end 82 | 83 | # Removes all objects from the queue. 84 | def clear() 85 | @queue.clear 86 | self 87 | end 88 | 89 | # length 90 | # size 91 | # 92 | # Returns the length of the queue. 93 | def length() 94 | @queue.size 95 | end 96 | 97 | alias size length 98 | # Returns the number of threads waiting on the queue. 99 | def num_waiting() 100 | @waiters.size 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/lightio/library/sized_queue.rb: -------------------------------------------------------------------------------- 1 | require_relative 'queue' 2 | 3 | module LightIO::Library 4 | class SizedQueue < LightIO::Library::Queue 5 | extend Base::MockMethods 6 | mock ::SizedQueue 7 | 8 | attr_accessor :max 9 | 10 | def initialize(max) 11 | raise ArgumentError, 'queue size must be positive' unless max > 0 12 | super() 13 | @max = max 14 | @enqueue_waiters = [] 15 | end 16 | 17 | def push(object) 18 | raise ClosedQueueError, "queue closed" if @close 19 | if size >= max 20 | future = LightIO::Future.new 21 | @enqueue_waiters << future 22 | future.value 23 | end 24 | super 25 | self 26 | end 27 | 28 | alias enq push 29 | alias << push 30 | 31 | def pop(non_block=false) 32 | result = super 33 | check_release_enqueue_waiter 34 | result 35 | end 36 | 37 | alias deq pop 38 | alias shift pop 39 | 40 | def clear 41 | result = super 42 | check_release_enqueue_waiter 43 | result 44 | end 45 | 46 | def max=(value) 47 | @max = value 48 | check_release_enqueue_waiter if size < max 49 | end 50 | 51 | def num_waiting 52 | super + @enqueue_waiters.size 53 | end 54 | 55 | private 56 | def check_release_enqueue_waiter 57 | if @enqueue_waiters.any? 58 | future = LightIO::Future.new 59 | LightIO::IOloop.current.add_callback { 60 | @enqueue_waiters.shift.transfer 61 | future.transfer 62 | } 63 | future.value 64 | end 65 | end 66 | end 67 | end -------------------------------------------------------------------------------- /lib/lightio/library/socket.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | 3 | module LightIO::Library 4 | 5 | class Addrinfo 6 | include Base 7 | include LightIO::Wrap::Wrapper 8 | 9 | mock ::Addrinfo 10 | extend LightIO::Module::Addrinfo::ClassMethods 11 | 12 | module WrapHelper 13 | protected 14 | def wrap_socket_method(method) 15 | define_method method do |*args| 16 | socket = self.class.wrap_to_library(@obj.send(method, *args)) 17 | if block_given? 18 | begin 19 | yield socket 20 | ensure 21 | socket.close 22 | end 23 | else 24 | socket 25 | end 26 | end 27 | end 28 | 29 | def wrap_socket_methods(*methods) 30 | methods.each {|m| wrap_socket_method(m)} 31 | end 32 | 33 | def wrap_addrinfo_return_method(method) 34 | define_method method do |*args| 35 | result = @obj.send(method, *args) 36 | if result.is_a?(::Addrinfo) 37 | self.class.wrap_to_library(result) 38 | elsif result.respond_to?(:map) 39 | result.map {|r| self.class.wrap_to_library(r)} 40 | else 41 | result 42 | end 43 | end 44 | end 45 | 46 | def wrap_addrinfo_return_methods(*methods) 47 | methods.each {|m| wrap_addrinfo_return_method(m)} 48 | end 49 | end 50 | 51 | include LightIO::Module::Base::Helper 52 | extend WrapHelper 53 | 54 | wrap_socket_methods :bind, :connect, :connect_from, :connect_to, :listen 55 | wrap_addrinfo_return_methods :family_addrinfo, :ipv6_to_ipv4 56 | end 57 | 58 | class BasicSocket < LightIO::Library::IO 59 | include Base 60 | include LightIO::Wrap::IOWrapper 61 | mock ::BasicSocket 62 | extend LightIO::Module::BasicSocket::ClassMethods 63 | 64 | wrap_blocking_methods :recv, :recvmsg, :sendmsg 65 | 66 | extend Forwardable 67 | def_delegators :io_watcher, :wait, :wait_writable 68 | 69 | def shutdown(*args) 70 | # close watcher before io shutdown 71 | io_watcher.close 72 | @obj.shutdown(*args) 73 | end 74 | end 75 | 76 | class Socket < BasicSocket 77 | include Base 78 | include LightIO::Wrap::IOWrapper 79 | mock ::Socket 80 | extend LightIO::Module::Socket::ClassMethods 81 | 82 | wrap_blocking_methods :connect, :recvfrom, :accept 83 | 84 | def sys_accept 85 | io_watcher.wait_readable 86 | @obj.sys_accept 87 | end 88 | 89 | class Ifaddr 90 | include Base 91 | mock ::Socket::Ifaddr 92 | 93 | def addr 94 | @obj.addr && Addrinfo._wrap(@obj.addr) 95 | end 96 | 97 | def broadaddr 98 | @obj.broadaddr && Addrinfo._wrap(@obj.broadaddr) 99 | end 100 | 101 | def dstaddr 102 | @obj.dstaddr && Addrinfo._wrap(@obj.dstaddr) 103 | end 104 | 105 | def netmask 106 | @obj.netmask && Addrinfo._wrap(@obj.netmask) 107 | end 108 | end 109 | 110 | include ::Socket::Constants 111 | Option = ::Socket::Option 112 | UDPSource = ::Socket::UDPSource 113 | SocketError = ::SocketError 114 | Addrinfo = Addrinfo 115 | 116 | def accept 117 | socket, addrinfo = wait_nonblock(:accept_nonblock) 118 | [self.class._wrap(socket), Addrinfo._wrap(addrinfo)] 119 | end 120 | 121 | def accept_nonblock(*args) 122 | socket, addrinfo = @obj.accept_nonblock(*args) 123 | if socket.is_a?(Symbol) 124 | [socket, nil] 125 | else 126 | [self.class._wrap(socket), Addrinfo._wrap(addrinfo)] 127 | end 128 | end 129 | end 130 | 131 | 132 | class IPSocket < BasicSocket 133 | include Base 134 | mock ::IPSocket 135 | end 136 | 137 | class TCPSocket < IPSocket 138 | include Base 139 | mock ::TCPSocket 140 | wrap_methods_run_in_threads_pool :gethostbyname 141 | 142 | def initialize(*args) 143 | raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 2..4)" if args.size < 2 || args.size > 4 144 | host, port = args[0..1] 145 | local_host, local_port = args[2..3] 146 | addrinfo = Addrinfo.getaddrinfo(host, port, nil, :STREAM)[0] 147 | socket = ::Socket.send(:origin_new, addrinfo.afamily, Socket::SOCK_STREAM, 0) 148 | if local_host || local_port 149 | local_address = Socket.sockaddr_in(local_port, local_host) 150 | socket.bind(local_address) 151 | end 152 | remote_address = Socket.sockaddr_in(addrinfo.ip_port, addrinfo.ip_address) 153 | @obj = socket 154 | wait_nonblock(:connect_nonblock, remote_address) 155 | @obj 156 | lightio_initialize 157 | end 158 | 159 | private 160 | def connect_nonblock(*args) 161 | @obj.connect_nonblock(*args) 162 | end 163 | end 164 | 165 | class TCPServer < TCPSocket 166 | include Base 167 | mock ::TCPServer 168 | 169 | def initialize(*args) 170 | @obj = ::TCPServer.send(:origin_new, *args) 171 | lightio_initialize 172 | end 173 | 174 | def accept 175 | socket = wait_nonblock(:accept_nonblock) 176 | TCPSocket._wrap(socket) 177 | end 178 | 179 | def accept_nonblock(*args) 180 | socket = @obj.accept_nonblock(*args) 181 | socket.is_a?(Symbol) ? socket : TCPSocket._wrap(socket) 182 | end 183 | 184 | def sys_accept 185 | io_watcher.wait_readable 186 | @obj.sys_accept 187 | end 188 | end 189 | 190 | class UDPSocket < IPSocket 191 | include Base 192 | mock ::UDPSocket 193 | 194 | wrap_blocking_methods :recvfrom 195 | end 196 | 197 | class UNIXSocket < BasicSocket 198 | include Base 199 | mock ::UNIXSocket 200 | 201 | def send_io(io) 202 | io = io.send(:light_io_raw_obj) if io.is_a?(LightIO::Library::IO) 203 | @obj.send_io(io) 204 | end 205 | 206 | def recv_io(*args) 207 | io = @obj.recv_io(*args) 208 | if (wrapper = LightIO.const_get(io.class.to_s)) 209 | return wrapper._wrap(io) if wrapper.respond_to?(:_wrap) 210 | end 211 | io 212 | end 213 | end 214 | 215 | class UNIXServer < UNIXSocket 216 | include Base 217 | mock ::UNIXServer 218 | 219 | def sys_accept 220 | io_watcher.wait_readable 221 | @obj.sys_accpet 222 | end 223 | 224 | def accept 225 | socket = wait_nonblock(:accept_nonblock) 226 | UNIXSocket._wrap(socket) 227 | end 228 | 229 | def accept_nonblock(*args) 230 | socket = @obj.accept_nonblock(*args) 231 | socket.is_a?(Symbol) ? socket : UNIXSocket._wrap(socket) 232 | end 233 | end 234 | end 235 | -------------------------------------------------------------------------------- /lib/lightio/library/thread.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | require_relative 'queue' 3 | 4 | module LightIO::Library 5 | class ThreadGroup 6 | include Base 7 | include LightIO::Wrap::Wrapper 8 | mock ::ThreadGroup 9 | 10 | def add(thread) 11 | if @obj.enclosed? 12 | raise ThreadError, "can't move from the enclosed thread group" 13 | elsif thread.is_a?(LightIO::Library::Thread) 14 | # let thread decide how to add to group 15 | thread.send(:add_to_group, self) 16 | else 17 | @obj.add(thread) 18 | end 19 | self 20 | end 21 | 22 | def list 23 | @obj.list + threads 24 | end 25 | 26 | private 27 | def threads 28 | @threads ||= [] 29 | end 30 | 31 | Default = ThreadGroup._wrap(::ThreadGroup::Default) 32 | end 33 | 34 | 35 | class Thread 36 | # constants 37 | ThreadError = ::ThreadError 38 | Queue = LightIO::Library::Queue 39 | Backtrace = ::Thread::Backtrace 40 | SizedQueue = LightIO::Library::SizedQueue 41 | 42 | extend Base::MockMethods 43 | mock ::Thread 44 | 45 | extend LightIO::Module::Thread::ClassMethods 46 | extend Forwardable 47 | 48 | def initialize(*args, &blk) 49 | init_core(*args, &blk) 50 | end 51 | 52 | def_delegators :@beam, :alive?, :value 53 | 54 | def_delegators :"Thread.main", 55 | :abort_on_exception, 56 | :abort_on_exception=, 57 | :pending_interrupt?, 58 | :add_trace_func, 59 | :backtrace, 60 | :backtrace_locations, 61 | :priority, 62 | :priority=, 63 | :safe_level 64 | 65 | def kill 66 | @beam.kill && self 67 | end 68 | 69 | alias exit kill 70 | alias terminate kill 71 | 72 | def status 73 | if self.class.current == self 74 | 'run' 75 | elsif alive? 76 | @beam.error.nil? ? 'sleep' : 'abouting' 77 | else 78 | @beam.error.nil? ? false : nil 79 | end 80 | end 81 | 82 | def thread_variables 83 | thread_values.keys 84 | end 85 | 86 | def thread_variable_get(name) 87 | thread_values[name.to_sym] 88 | end 89 | 90 | def thread_variable_set(name, value) 91 | thread_values[name.to_sym] = value 92 | end 93 | 94 | def thread_variable?(key) 95 | thread_values.key?(key) 96 | end 97 | 98 | def [](name) 99 | fiber_values[name.to_sym] 100 | end 101 | 102 | def []=(name, val) 103 | fiber_values[name.to_sym] = val 104 | end 105 | 106 | def group 107 | @group 108 | end 109 | 110 | def inspect 111 | "#" 112 | end 113 | 114 | def join(limit=nil) 115 | @beam.join(limit) && self 116 | end 117 | 118 | def key?(sym) 119 | fiber_values.has_key?(sym) 120 | end 121 | 122 | def keys 123 | fiber_values.keys 124 | end 125 | 126 | def raise(exception, message=nil, backtrace=nil) 127 | @beam.raise(LightIO::Beam::BeamError.new(exception), message, backtrace) 128 | end 129 | 130 | def run 131 | Kernel.raise ThreadError, 'killed thread' unless alive? 132 | Thread.pass 133 | end 134 | 135 | alias wakeup run 136 | 137 | def stop? 138 | !alive? || status == 'sleep' 139 | end 140 | 141 | private 142 | def init_core(*args, &blk) 143 | @beam = LightIO::Beam.new(*args, &blk) 144 | @beam.on_dead = proc {on_dead} 145 | @beam.on_transfer = proc {|from, to| on_transfer(from, to)} 146 | # register this thread 147 | thread_values 148 | # add self to ThreadGroup::Default 149 | add_to_group(LightIO::Library::ThreadGroup::Default) 150 | # remove thread and thread variables 151 | ObjectSpace.define_finalizer(self, LightIO::Library::Thread.finalizer(self.object_id)) 152 | end 153 | 154 | # add self to thread group 155 | def add_to_group(group) 156 | # remove from old group 157 | remove_from_group 158 | @group = group 159 | @group.send(:threads) << self 160 | end 161 | 162 | # remove thread from group when dead 163 | def remove_from_group 164 | @group.send(:threads).delete(self) if @group 165 | end 166 | 167 | def on_dead 168 | # release references 169 | remove_from_group 170 | end 171 | 172 | def on_transfer(from, to) 173 | Thread.instance_variable_set(:@current_thread, self) 174 | end 175 | 176 | def thread_values 177 | Thread.send(:threads)[object_id] ||= {} 178 | end 179 | 180 | def fibers_and_values 181 | @fibers_and_values ||= {} 182 | end 183 | 184 | def fiber_values 185 | beam_or_fiber = LightIO::Beam.current 186 | # only consider non-root fiber 187 | if !beam_or_fiber.instance_of?(::Fiber) || LightIO::LightFiber.is_root?(beam_or_fiber) 188 | beam_or_fiber = @beam 189 | end 190 | fibers_and_values[beam_or_fiber] ||= {} 191 | end 192 | 193 | class << self 194 | extend Forwardable 195 | def_delegators :'::Thread', 196 | :DEBUG, 197 | :DEBUG=, 198 | :handle_interrupt, 199 | :abort_on_exception, 200 | :abort_on_exception=, 201 | :pending_interrupt? 202 | 203 | def method_missing(*args) 204 | ::Thread.__send__(*args) 205 | end 206 | 207 | def respond_to?(*args) 208 | ::Thread.respond_to?(*args) 209 | end 210 | 211 | def respond_to_missing?(method, *) 212 | ::Thread.respond_to?(method) 213 | end 214 | 215 | private 216 | 217 | # threads and threads variables 218 | def threads 219 | thrs = Thread.instance_variable_get(:@threads) 220 | thrs || Thread.instance_variable_set(:@threads, {}) 221 | end 222 | 223 | def thread_mutex 224 | mutex = Thread.instance_variable_get(:@thread_mutex) 225 | mutex || Thread.instance_variable_set(:@thread_mutex, LightIO::Library::Mutex.new) 226 | end 227 | end 228 | 229 | class Mutex 230 | extend Base::MockMethods 231 | mock ::Mutex 232 | 233 | def initialize 234 | @queue = LightIO::Library::Queue.new 235 | @queue << true 236 | @locked_thread = nil 237 | end 238 | 239 | def lock 240 | raise ThreadError, "deadlock; recursive locking" if owned? 241 | @queue.pop 242 | @locked_thread = LightIO::Thread.current 243 | self 244 | end 245 | 246 | def unlock 247 | raise ThreadError, "Attempt to unlock a mutex which is not locked" unless owned? 248 | @locked_thread = nil 249 | @queue << true 250 | self 251 | end 252 | 253 | def locked? 254 | !@locked_thread.nil? 255 | end 256 | 257 | def owned? 258 | @locked_thread == LightIO::Thread.current 259 | end 260 | 261 | def sleep(timeout=nil) 262 | unlock 263 | LightIO.sleep(timeout) 264 | lock 265 | end 266 | 267 | def synchronize 268 | raise ThreadError, 'must be called with a block' unless block_given? 269 | lock 270 | begin 271 | yield 272 | ensure 273 | unlock 274 | end 275 | end 276 | 277 | def try_lock 278 | if @locked_thread.nil? 279 | lock 280 | true 281 | else 282 | false 283 | end 284 | end 285 | end 286 | 287 | class ConditionVariable 288 | extend Base::MockMethods 289 | mock ::ConditionVariable 290 | 291 | def initialize 292 | @queue = LightIO::Library::Queue.new 293 | end 294 | 295 | 296 | def broadcast 297 | signal until @queue.num_waiting == 0 298 | self 299 | end 300 | 301 | def signal 302 | @queue << true unless @queue.num_waiting == 0 303 | self 304 | end 305 | 306 | def wait(mutex, timeout=nil) 307 | mutex.unlock 308 | begin 309 | LightIO::Library::Timeout.timeout(timeout) do 310 | @queue.pop 311 | end 312 | rescue Timeout::Error 313 | nil 314 | end 315 | mutex.lock 316 | self 317 | end 318 | end 319 | end 320 | 321 | Mutex = Thread::Mutex 322 | ConditionVariable = Thread::ConditionVariable 323 | end 324 | -------------------------------------------------------------------------------- /lib/lightio/library/threads_wait.rb: -------------------------------------------------------------------------------- 1 | require 'thwait' 2 | 3 | module LightIO::Library 4 | class ThreadsWait 5 | ErrNoWaitingThread = ::ThreadsWait::ErrNoWaitingThread 6 | ErrNoFinishedThread = ::ThreadsWait::ErrNoFinishedThread 7 | 8 | extend Base::MockMethods 9 | mock ::ThreadsWait 10 | 11 | extend LightIO::Module::ThreadsWait::ClassMethods 12 | 13 | attr_reader :threads 14 | 15 | def initialize(*threads) 16 | @threads = threads 17 | end 18 | 19 | def all_waits 20 | until empty? 21 | thr = next_wait 22 | yield thr if block_given? 23 | end 24 | end 25 | 26 | def empty? 27 | @threads.empty? 28 | end 29 | 30 | def finished? 31 | @threads.any? {|thr| !thr.alive?} 32 | end 33 | 34 | def join(*threads) 35 | join_nowait(*threads) 36 | next_wait 37 | end 38 | 39 | def join_nowait(*threads) 40 | @threads.concat(threads) 41 | end 42 | 43 | def next_wait(nonblock=nil) 44 | raise ::ThreadsWait::ErrNoWaitingThread, 'No threads for waiting.' if empty? 45 | @threads.each do |thr| 46 | if thr.alive? && nonblock 47 | next 48 | elsif thr.alive? 49 | thr.join 50 | end 51 | # thr should dead 52 | @threads.delete(thr) 53 | return thr 54 | end 55 | raise ::ThreadsWait::ErrNoFinishedThread, 'No finished threads.' 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/lightio/library/timeout.rb: -------------------------------------------------------------------------------- 1 | require 'timeout' 2 | module LightIO::Library 3 | module Timeout 4 | extend self 5 | Error = ::Timeout::Error 6 | 7 | def timeout(sec, klass=Error, &blk) 8 | return yield(sec) if sec.nil? or sec.zero? 9 | beam = LightIO::Beam.new(sec, &blk) 10 | message = "execution expired" 11 | if beam.join(sec).nil? 12 | raise klass, message 13 | else 14 | beam.value 15 | end 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /lib/lightio/module.rb: -------------------------------------------------------------------------------- 1 | require_relative 'module/base' 2 | require_relative 'module/io' 3 | require_relative 'module/file' 4 | require_relative 'module/socket' 5 | require_relative 'module/openssl' 6 | require_relative 'module/thread' 7 | require_relative 'module/threads_wait' 8 | 9 | module LightIO 10 | module Module 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/lightio/module/base.rb: -------------------------------------------------------------------------------- 1 | module LightIO::Module 2 | module Base 3 | class << self 4 | def find_library_class(klass) 5 | return LightIO::Library::Base.send(:nameless_classes)[klass] if klass.name.nil? 6 | name = klass.name 7 | begin 8 | LightIO::Library.const_get(name) 9 | rescue NameError 10 | # retry without namespace 11 | namespace_index = name.rindex("::") 12 | raise if namespace_index.nil? 13 | class_name = name[(namespace_index + 2)..-1] 14 | LightIO::Library.const_get(class_name) 15 | end 16 | end 17 | end 18 | 19 | module NewHelper 20 | protected 21 | def define_new_for_modules(*mods) 22 | mods.each {|mod| define_new_for_module(mod)} 23 | end 24 | 25 | def define_new_for_module(mod) 26 | LightIO::Module.send(:module_eval, <<-STR, __FILE__, __LINE__ + 1) 27 | module #{mod} 28 | module ClassMethods 29 | def new(*args, &blk) 30 | obj = LightIO::Library::#{mod}.__send__ :allocate 31 | obj.__send__ :initialize, *args, &blk 32 | obj 33 | end 34 | end 35 | end 36 | STR 37 | end 38 | end 39 | 40 | module Helper 41 | protected 42 | def wrap_to_library(obj) 43 | return _wrap(obj) if self.respond_to?(:_wrap) 44 | find_library_class._wrap(obj) 45 | end 46 | 47 | def find_library_class 48 | Base.find_library_class(self) 49 | end 50 | end 51 | end 52 | end -------------------------------------------------------------------------------- /lib/lightio/module/file.rb: -------------------------------------------------------------------------------- 1 | module LightIO::Module 2 | extend Base::NewHelper 3 | 4 | define_new_for_module "File" 5 | module File 6 | include Base 7 | module ClassMethods 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/lightio/module/io.rb: -------------------------------------------------------------------------------- 1 | module LightIO::Module 2 | extend Base::NewHelper 3 | 4 | define_new_for_module "IO" 5 | 6 | module IO 7 | include LightIO::Module::Base 8 | 9 | class << self 10 | # helper methods 11 | def convert_to_io(io) 12 | unless io.respond_to?(:to_io) 13 | raise TypeError, "no implicit conversion of #{io.class} into IO" 14 | end 15 | to_io = io.is_a?(LightIO::Library::IO) ? io : io.to_io 16 | unless to_io.is_a?(LightIO::Library::IO) 17 | raise TypeError, "can't convert #{io.class} to IO (#{io.class}#to_io gives #{to_io.class})" unless to_io.is_a?(::IO) 18 | 19 | # try wrap raw io instead of raise error 20 | wrapped_io = to_io.instance_variable_get(:@_lightio_wrapped_io) 21 | unless wrapped_io 22 | wrapped_io = LightIO::Library::IO._wrap(to_io) 23 | to_io.instance_variable_set(:@_lightio_wrapped_io, wrapped_io) 24 | end 25 | to_io = wrapped_io 26 | # raise TypeError, "can't process raw IO, use LightIO::IO._wrap(obj) to wrap it" 27 | end 28 | to_io 29 | end 30 | 31 | def get_io_watcher(io) 32 | unless io.is_a?(LightIO::Library::IO) 33 | io = convert_to_io(io) 34 | end 35 | io.__send__(:io_watcher) 36 | end 37 | end 38 | 39 | module ClassMethods 40 | include LightIO::Module::Base::Helper 41 | 42 | def open(*args) 43 | io = self.new(*args) 44 | return io unless block_given? 45 | begin 46 | yield io 47 | ensure 48 | io.close if io.respond_to? :close 49 | end 50 | end 51 | 52 | def pipe(*args) 53 | r, w = origin_pipe(*args) 54 | if block_given? 55 | begin 56 | return yield r, w 57 | ensure 58 | w.close 59 | r.close 60 | end 61 | end 62 | [wrap_to_library(r), wrap_to_library(w)] 63 | end 64 | 65 | def copy_stream(*args) 66 | raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 2..4)" unless (2..4).include?(args.size) 67 | src, dst, copy_length, src_offset = args 68 | src = src.respond_to?(:to_io) ? src.to_io : LightIO::Library::File.open(src, 'r') unless src.is_a?(IO) 69 | dst = dst.respond_to?(:to_io) ? dst.to_io : LightIO::Library::File.open(dst, 'w') unless dst.is_a?(IO) 70 | buf_size = 4096 71 | copy_chars = 0 72 | buf_size = [buf_size, copy_length].min if copy_length 73 | src.seek(src_offset) if src_offset 74 | while (buf = src.read(buf_size)) 75 | size = dst.write(buf) 76 | copy_chars += size 77 | if copy_length 78 | copy_length -= size 79 | break if copy_length.zero? 80 | buf_size = [buf_size, copy_length].min 81 | end 82 | end 83 | copy_chars 84 | end 85 | 86 | def select(read_fds, write_fds=nil, _except_fds=nil, timeout=nil) 87 | timer = timeout && Time.now 88 | read_fds ||= [] 89 | write_fds ||= [] 90 | loop do 91 | # make sure io registered, then clear io watcher status 92 | read_fds.each {|fd| LightIO::Module::IO.get_io_watcher(fd).tap {|io| io.readable?; io.clear_status}} 93 | write_fds.each {|fd| LightIO::Module::IO.get_io_watcher(fd).tap {|io| io.writable?; io.clear_status}} 94 | # run ioloop once 95 | LightIO.sleep 0 96 | r_fds = read_fds.select {|fd| 97 | io = LightIO::Module::IO.convert_to_io(fd) 98 | io.closed? ? raise(IOError, 'closed stream') : LightIO::Module::IO.get_io_watcher(io).readable? 99 | } 100 | w_fds = write_fds.select {|fd| 101 | io = LightIO::Module::IO.convert_to_io(fd) 102 | io.closed? ? raise(IOError, 'closed stream') : LightIO::Module::IO.get_io_watcher(io).writable? 103 | } 104 | e_fds = [] 105 | if r_fds.empty? && w_fds.empty? 106 | if timeout && Time.now - timer > timeout 107 | return nil 108 | end 109 | else 110 | return [r_fds, w_fds, e_fds] 111 | end 112 | end 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/lightio/module/openssl.rb: -------------------------------------------------------------------------------- 1 | module LightIO::Module 2 | extend Base::NewHelper 3 | 4 | module OpenSSL 5 | module SSL 6 | module SSLSocket 7 | end 8 | end 9 | end 10 | 11 | define_new_for_module 'OpenSSL::SSL::SSLSocket' 12 | end 13 | -------------------------------------------------------------------------------- /lib/lightio/module/socket.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | 3 | module LightIO::Module 4 | extend Base::NewHelper 5 | 6 | define_new_for_modules *%w{Addrinfo Socket IPSocket TCPSocket TCPServer UDPSocket UNIXSocket UNIXServer} 7 | 8 | module Addrinfo 9 | include LightIO::Module::Base 10 | 11 | module WrapperHelper 12 | protected 13 | def wrap_class_addrinfo_return_method(method) 14 | define_method method do |*args| 15 | result = __send__(:"origin_#{method}", *args) 16 | if result.is_a?(::Addrinfo) 17 | wrap_to_library(result) 18 | elsif result.respond_to?(:map) 19 | result.map {|r| wrap_to_library(r)} 20 | else 21 | result 22 | end 23 | end 24 | end 25 | 26 | def wrap_class_addrinfo_return_methods(*methods) 27 | methods.each {|m| wrap_class_addrinfo_return_method(m)} 28 | end 29 | end 30 | 31 | module ClassMethods 32 | include LightIO::Module::Base::Helper 33 | extend WrapperHelper 34 | 35 | def foreach(*args, &block) 36 | LightIO::Library::Addrinfo.getaddrinfo(*args).each(&block) 37 | end 38 | 39 | wrap_class_addrinfo_return_methods :getaddrinfo, :ip, :udp, :tcp, :unix 40 | end 41 | end 42 | 43 | module BasicSocket 44 | include LightIO::Module::Base 45 | 46 | module ClassMethods 47 | include LightIO::Module::Base::Helper 48 | 49 | def for_fd(fd) 50 | wrap_to_library(origin_for_fd(fd)) 51 | end 52 | end 53 | end 54 | 55 | module Socket 56 | include LightIO::Module::Base 57 | 58 | module ClassMethods 59 | include LightIO::Module::Base::Helper 60 | extend LightIO::Wrap::Wrapper::HelperMethods 61 | ## implement ::Socket class methods 62 | wrap_methods_run_in_threads_pool :getaddrinfo, :gethostbyaddr, :gethostbyname, :gethostname, 63 | :getnameinfo, :getservbyname 64 | 65 | def getifaddrs 66 | origin_getifaddrs.map {|ifaddr| LightIO::Library::Socket::Ifaddr._wrap(ifaddr)} 67 | end 68 | 69 | def socketpair(domain, type, protocol) 70 | origin_socketpair(domain, type, protocol).map {|s| wrap_to_library(s)} 71 | end 72 | 73 | alias_method :pair, :socketpair 74 | 75 | def unix_server_socket(path) 76 | if block_given? 77 | origin_unix_server_socket(path) {|s| yield wrap_to_library(s)} 78 | else 79 | wrap_to_library(origin_unix_server_socket(path)) 80 | end 81 | end 82 | 83 | def ip_sockets_port0(ai_list, reuseaddr) 84 | origin_ip_sockets_port0(ai_list, reuseaddr).map {|s| wrap_to_library(s)} 85 | end 86 | end 87 | end 88 | 89 | 90 | module IPSocket 91 | include LightIO::Module::Base 92 | 93 | module ClassMethods 94 | extend LightIO::Wrap::Wrapper::HelperMethods 95 | wrap_methods_run_in_threads_pool :getaddress 96 | end 97 | end 98 | 99 | module TCPSocket 100 | include LightIO::Module::Base 101 | end 102 | 103 | module TCPServer 104 | include LightIO::Module::Base 105 | end 106 | 107 | module UDPSocket 108 | include LightIO::Module::Base 109 | end 110 | 111 | module UNIXSocket 112 | include LightIO::Module::Base 113 | 114 | module ClassMethods 115 | include LightIO::Module::Base::Helper 116 | 117 | def socketpair(*args) 118 | origin_socketpair(*args).map {|io| wrap_to_library(io)} 119 | end 120 | 121 | alias_method :pair, :socketpair 122 | end 123 | end 124 | 125 | module UNIXServer 126 | include LightIO::Module::Base 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/lightio/module/thread.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | 3 | module LightIO::Module 4 | extend Base::NewHelper 5 | 6 | define_new_for_modules *%w{ThreadGroup Mutex Queue SizedQueue ConditionVariable} 7 | 8 | module Thread 9 | include LightIO::Module::Base 10 | 11 | module ClassMethods 12 | extend Forwardable 13 | 14 | def new(*args, &blk) 15 | obj = LightIO::Library::Thread.__send__ :allocate 16 | obj.__send__ :initialize, *args, &blk 17 | obj 18 | end 19 | 20 | def fork(*args, &blk) 21 | obj = LightIO::Library::Thread.__send__ :allocate 22 | obj.send(:init_core, *args, &blk) 23 | obj 24 | end 25 | 26 | alias start fork 27 | 28 | def kill(thr) 29 | thr.kill 30 | end 31 | 32 | def current 33 | return main if LightIO::Core::LightFiber.is_root?(Fiber.current) 34 | LightIO::Library::Thread.instance_variable_get(:@current_thread) || origin_current 35 | end 36 | 37 | def exclusive(&blk) 38 | LightIO::Library::Thread.__send__(:thread_mutex).synchronize(&blk) 39 | end 40 | 41 | def list 42 | thread_list = [] 43 | LightIO::Library::Thread.__send__(:threads).keys.each {|id| 44 | begin 45 | thr = ObjectSpace._id2ref(id) 46 | unless thr.alive? 47 | # manually remove thr from threads 48 | thr.kill 49 | next 50 | end 51 | thread_list << thr 52 | rescue RangeError 53 | # mean object is recycled 54 | # just wait ruby GC call finalizer to remove it from threads 55 | next 56 | end 57 | } 58 | thread_list 59 | end 60 | 61 | def pass 62 | LightIO::Beam.pass 63 | end 64 | 65 | alias stop pass 66 | 67 | def finalizer(object_id) 68 | proc {LightIO::Library::Thread.__send__(:threads).delete(object_id)} 69 | end 70 | 71 | def main 72 | origin_main 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/lightio/module/threads_wait.rb: -------------------------------------------------------------------------------- 1 | require 'thwait' 2 | 3 | module LightIO::Module 4 | module ThreadsWait 5 | include LightIO::Module::Base 6 | 7 | module ClassMethods 8 | def all_waits(*threads, &blk) 9 | LightIO::Library::ThreadsWait.new(*threads).all_waits(&blk) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/lightio/monkey.rb: -------------------------------------------------------------------------------- 1 | module LightIO 2 | module Monkey 3 | class PatchError < StandardError 4 | end 5 | 6 | IO_PATCH_CONSTANTS = %w{IO File Socket Socket::Ifaddr TCPServer TCPSocket BasicSocket Addrinfo IPSocket UDPSocket UNIXSocket UNIXServer OpenSSL::SSL::SSLSocket}.freeze 7 | THREAD_PATCH_CONSTANTS = %w{Thread ThreadGroup Queue SizedQueue ConditionVariable Mutex ThreadsWait}.freeze 8 | 9 | @patched 10 | 11 | class << self 12 | def patch_all! 13 | # Fix https://github.com/socketry/lightio/issues/7 14 | begin 15 | require 'ffi' 16 | rescue LoadError 17 | nil 18 | end 19 | 20 | patch_thread! 21 | patch_io! 22 | patch_kernel! 23 | nil 24 | end 25 | 26 | def unpatch_all! 27 | unpatch_thread! 28 | unpatch_io! 29 | unpatch_kernel! 30 | nil 31 | end 32 | 33 | def patched?(obj) 34 | patched.key?(obj) && !patched[obj]&.empty? 35 | end 36 | 37 | def patch_thread! 38 | require 'thread' 39 | THREAD_PATCH_CONSTANTS.each {|klass_name| patch!(klass_name)} 40 | patch_method!(Timeout, :timeout, LightIO::Timeout.method(:timeout)) 41 | nil 42 | end 43 | 44 | def unpatch_thread! 45 | require 'thread' 46 | THREAD_PATCH_CONSTANTS.each {|klass_name| unpatch!(klass_name)} 47 | unpatch_method!(Timeout, :timeout) 48 | nil 49 | end 50 | 51 | def patch_io! 52 | require 'socket' 53 | IO_PATCH_CONSTANTS.each {|klass_name| patch!(klass_name)} 54 | patch_method!(Process, :spawn, LightIO.method(:spawn).to_proc) 55 | nil 56 | end 57 | 58 | def unpatch_io! 59 | require 'socket' 60 | IO_PATCH_CONSTANTS.each {|klass_name| unpatch!(klass_name)} 61 | unpatch_method!(Process, :spawn) 62 | nil 63 | end 64 | 65 | def patch_kernel! 66 | patch_kernel_method!(:sleep, LightIO.method(:sleep)) 67 | patch_kernel_method!(:select, LightIO::Library::IO.method(:select)) 68 | patch_kernel_method!(:open, LightIO::Library::File.method(:open).to_proc) 69 | patch_kernel_method!(:spawn, LightIO.method(:spawn).to_proc) 70 | patch_kernel_method!(:`, LightIO.method(:`).to_proc) 71 | patch_kernel_method!(:system, LightIO.method(:system).to_proc) 72 | %w{gets readline readlines}.each do |method| 73 | patch_kernel_method!(method.to_sym, LightIO.method(method.to_sym).to_proc) 74 | end 75 | nil 76 | end 77 | 78 | def unpatch_kernel! 79 | unpatch_kernel_method!(:sleep) 80 | unpatch_kernel_method!(:select) 81 | unpatch_kernel_method!(:open) 82 | unpatch_kernel_method!(:spawn) 83 | unpatch_kernel_method!(:`) 84 | unpatch_kernel_method!(:system) 85 | %w{gets readline readlines}.each do |method| 86 | unpatch_kernel_method!(method.to_sym, LightIO.method(method.to_sym).to_proc) 87 | end 88 | nil 89 | end 90 | 91 | private 92 | def patch!(klass_name) 93 | klass = Object.const_get(klass_name) 94 | raise PatchError, "already patched constant #{klass}" if patched?(klass) 95 | patched[klass] = {} 96 | class_methods_module = find_class_methods_module(klass_name) 97 | methods = class_methods_module && find_monkey_patch_class_methods(klass_name) 98 | return unless class_methods_module && methods 99 | methods.each do |method_name| 100 | method = class_methods_module.instance_method(method_name) 101 | patch_method!(klass, method_name, method) 102 | end 103 | rescue 104 | patched.delete(klass) 105 | raise 106 | end 107 | 108 | def unpatch!(klass_name) 109 | klass = Object.const_get(klass_name) 110 | raise PatchError, "can't find patched constant #{klass}" unless patched?(klass) 111 | unpatch_method!(klass, :new) 112 | find_monkey_patch_class_methods(klass_name).each do |method_name| 113 | unpatch_method!(klass, method_name) 114 | end 115 | patched.delete(klass) 116 | end 117 | 118 | def patch_kernel_method!(method_name, method) 119 | patch_method!(Kernel, method_name, method) 120 | patch_instance_method!(Kernel, method_name, method) 121 | end 122 | 123 | def unpatch_kernel_method!(method_name) 124 | unpatch_method!(Kernel, method_name) 125 | unpatch_instance_method!(Kernel, method_name) 126 | end 127 | 128 | def find_class_methods_module(klass_name) 129 | LightIO::Module.const_get("#{klass_name}::ClassMethods", false) 130 | rescue NameError 131 | nil 132 | end 133 | 134 | def find_monkey_patch_class_methods(klass_name) 135 | find_class_methods_module(klass_name).instance_methods 136 | end 137 | 138 | def patched_method?(obj, method) 139 | patched?(obj) && patched[obj].key?(method) 140 | end 141 | 142 | def patched_methods(const) 143 | patched[const] ||= {} 144 | end 145 | 146 | def patch_method!(const, method, patched_method) 147 | raise PatchError, "already patched method #{const}.#{method}" if patched_method?(const, method) 148 | patched_methods(const)[method] = patched_method 149 | const.send(:define_singleton_method, method, patched_method) 150 | nil 151 | end 152 | 153 | def unpatch_method!(const, method) 154 | raise PatchError, "can't find patched method #{const}.#{method}" unless patched_method?(const, method) 155 | origin_method = patched_methods(const).delete(method) 156 | const.send(:define_singleton_method, method, origin_method) 157 | nil 158 | end 159 | 160 | def patched_instance_method?(obj, method) 161 | patched_instance_methods(obj).key?(method) 162 | end 163 | 164 | def patched_instance_methods(const) 165 | (patched[:instance_methods] ||= {})[const] ||= {} 166 | end 167 | 168 | def patch_instance_method!(const, method, patched_method) 169 | raise PatchError, "already patched method #{const}.#{method}" if patched_instance_method?(const, method) 170 | patched_instance_methods(const)[method] = patched_method 171 | const.send(:define_method, method, patched_method) 172 | nil 173 | end 174 | 175 | def unpatch_instance_method!(const, method) 176 | raise PatchError, "can't find patched method #{const}.#{method}" unless patched_instance_method?(const, method) 177 | origin_method = patched_instance_methods(const).delete(method) 178 | const.send(:define_method, method, origin_method) 179 | nil 180 | end 181 | 182 | def patched 183 | @patched ||= {} 184 | end 185 | end 186 | end 187 | end -------------------------------------------------------------------------------- /lib/lightio/raw_proxy.rb: -------------------------------------------------------------------------------- 1 | # helper for access raw ruby object methods 2 | # use it to avoid monkey patch affect 3 | 4 | module LightIO 5 | class RawProxy 6 | def initialize(klass, methods: [], instance_methods: []) 7 | @klass = klass 8 | @methods = methods.map {|method| [method.to_sym, klass.method(method)]}.to_h 9 | @instance_methods = instance_methods.map {|method| [method.to_sym, klass.instance_method(method)]}.to_h 10 | end 11 | 12 | def send(method, *args) 13 | method = method.to_sym 14 | return method_missing(method, *args) unless @methods.key?(method) 15 | @methods[method].call(*args) 16 | end 17 | 18 | def instance_send(instance, method, *args) 19 | method = method.to_sym 20 | return method_missing(method, *args) unless @instance_methods.key?(method) 21 | @instance_methods[method].bind(instance).call(*args) 22 | end 23 | end 24 | end -------------------------------------------------------------------------------- /lib/lightio/version.rb: -------------------------------------------------------------------------------- 1 | module LightIO 2 | VERSION = "0.4.4" 3 | end 4 | -------------------------------------------------------------------------------- /lib/lightio/watchers.rb: -------------------------------------------------------------------------------- 1 | require_relative 'watchers/watcher' 2 | require_relative 'watchers/timer' 3 | require_relative 'watchers/schedule' 4 | require_relative 'watchers/io' 5 | 6 | # Watcher is a abstract struct, for libraries to interact with ioloop 7 | # see IOloop#wait method 8 | module LightIO::Watchers 9 | end -------------------------------------------------------------------------------- /lib/lightio/watchers/io.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module LightIO::Watchers 4 | # LightIO::Watchers::IO provide a NIO::Monitor wrap to manage 'raw' socket / io 5 | # 6 | # @Example: 7 | # #- wait_read for server socket 8 | # io_watcher = LightIO::Watchers::IO.new(server_socket, :r) 9 | # loop do 10 | # io_watcher.wait_read 11 | # client_socket = server_socket.accept 12 | # # do something 13 | # end 14 | # io_watcher.close 15 | class IO < Watcher 16 | # Create a io watcher 17 | # @param [Socket] io An IO-able object 18 | # @param [Symbol] interests :r, :w, :rw - Is io readable? writeable? or both 19 | # @return [LightIO::Watchers::IO] 20 | def initialize(io, interests=:rw) 21 | @io = io 22 | @ioloop = LightIO::Core::IOloop.current 23 | @waiting = false 24 | @error = nil 25 | # maintain socket status, see https://github.com/socketry/lightio/issues/1 26 | @readiness = nil 27 | @monitor = nil 28 | end 29 | 30 | # NIO::Monitor 31 | def monitor(interests=:rw) 32 | @monitor ||= begin 33 | raise @error if @error 34 | monitor = @ioloop.add_io_wait(@io, interests) {callback_on_waiting} 35 | ObjectSpace.define_finalizer(self, self.class.finalizer(monitor)) 36 | monitor 37 | end 38 | end 39 | 40 | class << self 41 | def finalizer(monitor) 42 | proc {monitor.close if monitor && !monitor.close?} 43 | end 44 | end 45 | 46 | extend Forwardable 47 | def_delegators :monitor, :interests, :interests= 48 | 49 | def closed? 50 | # check @monitor exists, avoid unnecessary monitor created 51 | return true unless @monitor 52 | monitor.closed? 53 | end 54 | 55 | # this method return previous IO.select status 56 | # should avoid to directly use 57 | def readable? 58 | check_monitor_read 59 | @readiness == :r || @readiness == :rw 60 | end 61 | 62 | # this method return previous IO.select status 63 | # should avoid to directly use 64 | def writable? 65 | check_monitor_write 66 | @readiness == :w || @readiness == :rw 67 | end 68 | 69 | alias :writeable? :writable? 70 | 71 | def clear_status 72 | @readiness = nil 73 | end 74 | 75 | # Blocking until io is readable 76 | # @param [Numeric] timeout return nil after timeout seconds, otherwise return self 77 | # @return [LightIO::Watchers::IO, nil] 78 | def wait_readable(timeout=nil) 79 | wait timeout, :read 80 | end 81 | 82 | # Blocking until io is writable 83 | # @param [Numeric] timeout return nil after timeout seconds, otherwise return self 84 | # @return [LightIO::Watchers::IO, nil] 85 | def wait_writable(timeout=nil) 86 | wait timeout, :write 87 | end 88 | 89 | def wait(timeout=nil, mode=:read) 90 | LightIO::Timeout.timeout(timeout) do 91 | check_monitor(mode) 92 | in_waiting(mode) do 93 | wait_in_ioloop 94 | end 95 | self 96 | end 97 | rescue Timeout::Error 98 | nil 99 | end 100 | 101 | def close 102 | set_close_error 103 | return if closed? 104 | monitor.close 105 | callback_on_waiting 106 | end 107 | 108 | 109 | # just implement IOloop#wait watcher interface 110 | def start(ioloop) 111 | # do nothing 112 | end 113 | 114 | def set_callback(&blk) 115 | @callback = blk 116 | end 117 | 118 | private 119 | def set_close_error 120 | @error ||= IOError.new('closed stream') 121 | end 122 | 123 | def check_monitor(mode) 124 | case mode 125 | when :read 126 | check_monitor_read 127 | when :write 128 | check_monitor_write 129 | when :read_write 130 | check_monitor_read_write 131 | else 132 | raise ArgumentError, "get unknown value #{mode}" 133 | end 134 | end 135 | 136 | def check_monitor_read 137 | if monitor(:r).interests == :w 138 | monitor.interests = :rw 139 | end 140 | end 141 | 142 | def check_monitor_write 143 | if monitor(:w).interests == :r 144 | monitor.interests = :rw 145 | end 146 | end 147 | 148 | def check_monitor_read_write 149 | if monitor(:rw).interests != :rw 150 | monitor.interests = :rw 151 | end 152 | end 153 | 154 | # Blocking until io interests is satisfied 155 | def wait_in_ioloop 156 | raise LightIO::Error, "Watchers::IO can't cross threads" if @ioloop != LightIO::Core::IOloop.current 157 | raise EOFError, "can't wait closed IO watcher" if @monitor.closed? 158 | @ioloop.wait(self) 159 | end 160 | 161 | def in_waiting(mode) 162 | @waiting = mode 163 | yield 164 | @waiting = false 165 | end 166 | 167 | def callback_on_waiting 168 | # update readiness on callback 169 | @readiness = monitor.readiness 170 | # only call callback on waiting 171 | return unless io_is_ready? 172 | if @error 173 | # if error occurred in io waiting, send it to callback, see IOloop#wait 174 | callback&.call(LightIO::Core::Beam::BeamError.new(@error)) 175 | else 176 | callback&.call 177 | end 178 | end 179 | 180 | def io_is_ready? 181 | return false unless @waiting 182 | return true if closed? 183 | if @waiting == :r 184 | readable? 185 | elsif @waiting == :w 186 | writable? 187 | else 188 | readable? || writable? 189 | end 190 | end 191 | end 192 | end -------------------------------------------------------------------------------- /lib/lightio/watchers/schedule.rb: -------------------------------------------------------------------------------- 1 | module LightIO 2 | module Watchers 3 | class Schedule < Watcher 4 | def start(ioloop) 5 | ioloop.add_callback(&@callback) 6 | end 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /lib/lightio/watchers/timer.rb: -------------------------------------------------------------------------------- 1 | module LightIO 2 | module Watchers 3 | class Timer < Watcher 4 | attr_reader :interval 5 | attr_accessor :uuid 6 | 7 | def initialize(interval) 8 | @interval = interval 9 | end 10 | 11 | def start(ioloop) 12 | ioloop.add_timer(self) 13 | end 14 | end 15 | end 16 | end -------------------------------------------------------------------------------- /lib/lightio/watchers/watcher.rb: -------------------------------------------------------------------------------- 1 | module LightIO 2 | module Watchers 3 | class Watcher 4 | attr_reader :callback 5 | 6 | def set_callback(&blk) 7 | raise Error, "already has callback" if @callback 8 | @callback = blk 9 | end 10 | 11 | def start(backend) 12 | raise 13 | end 14 | end 15 | end 16 | end -------------------------------------------------------------------------------- /lib/lightio/wrap.rb: -------------------------------------------------------------------------------- 1 | # wrap module 2 | # wrap ruby objects, and make it work with lightio 3 | 4 | module LightIO::Wrap 5 | # wrapper for normal ruby objects 6 | module Wrapper 7 | # both works in class scope and singleton class scope 8 | module HelperMethods 9 | protected 10 | # run method in thread pool for performance 11 | def wrap_methods_run_in_threads_pool(*args) 12 | #TODO 13 | end 14 | end 15 | 16 | class << self 17 | def included(base) 18 | base.send :extend, HelperMethods 19 | end 20 | end 21 | end 22 | 23 | # wrapper for ruby io objects 24 | module IOWrapper 25 | # wrap raw ruby io objects 26 | # 27 | # @param [IO, Socket] io raw ruby io object 28 | def initialize(*args) 29 | @obj ||= super 30 | end 31 | 32 | protected 33 | # wait io nonblock method 34 | # 35 | # @param [Symbol] method method name, example: wait_nonblock 36 | # @param [args] args arguments pass to method 37 | def wait_nonblock(method, *args) 38 | loop do 39 | result = __send__(method, *args, exception: false) 40 | case result 41 | when :wait_readable 42 | io_watcher.wait_readable 43 | when :wait_writable 44 | io_watcher.wait_writable 45 | else 46 | return result 47 | end 48 | end 49 | end 50 | 51 | def io_watcher 52 | @io_watcher ||= LightIO::Watchers::IO.new(@obj) 53 | end 54 | 55 | module ClassMethods 56 | # include Wrapper::ClassMethods 57 | protected 58 | # wrap blocking method with "#{method}_nonblock" 59 | # 60 | # @param [Symbol] method method name, example: wait 61 | def wrap_blocking_method(method) 62 | define_method method do |*args| 63 | wait_nonblock(:"#{method}_nonblock", *args) 64 | end 65 | end 66 | 67 | def wrap_blocking_methods(*methods) 68 | methods.each {|m| wrap_blocking_method(m)} 69 | end 70 | end 71 | 72 | class << self 73 | def included(base) 74 | base.send :extend, ClassMethods 75 | base.send :include, Wrapper 76 | end 77 | end 78 | end 79 | end -------------------------------------------------------------------------------- /lightio.gemspec: -------------------------------------------------------------------------------- 1 | 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "lightio/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "lightio" 8 | spec.version = LightIO::VERSION 9 | spec.authors = ["Jiang Jinyang"] 10 | spec.email = ["jjyruby@gmail.com"] 11 | 12 | spec.summary = %q{LightIO is a ruby networking library, that combines ruby fiber and fast IO event loop.} 13 | spec.description = %q{The intent of LightIO is to provide ruby stdlib compatible modules, that user can use these modules instead stdlib, to gain the benefits of IO event loop without care any details about react or async programming.} 14 | spec.homepage = "https://github.com/jjyr/lightio" 15 | spec.license = "MIT" 16 | 17 | spec.required_ruby_version = '>= 2.3.4' 18 | 19 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 20 | f.match(%r{^(test|spec|features)/}) 21 | end 22 | spec.bindir = "exe" 23 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 24 | spec.require_paths = ["lib"] 25 | 26 | spec.add_runtime_dependency "nio4r", "~> 2.2" 27 | spec.add_development_dependency "bundler", "~> 1.16" 28 | spec.add_development_dependency "rake", "~> 10.0" 29 | spec.add_development_dependency "rspec", "~> 3.0" 30 | end 31 | -------------------------------------------------------------------------------- /spec/helper_methods.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | 3 | module HelperMethods 4 | def pick_random_port 5 | socket = TCPServer.new(0) 6 | socket.addr[1] 7 | ensure 8 | socket.close 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/lightio/core/beam_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe LightIO::Beam do 4 | describe "#initialize" do 5 | it "should not execute" do 6 | expect(LightIO::Beam.new {1 / 0}).not_to be_nil 7 | end 8 | end 9 | 10 | describe "#value" do 11 | it "get value" do 12 | expect(LightIO::Beam.new {1 + 2}.value).to be 3 13 | end 14 | 15 | it "pass arguments" do 16 | expect(LightIO::Beam.new(1, 2) {|one, two| one + two}.value).to be 3 17 | end 18 | 19 | it "raise error" do 20 | expect {LightIO::Beam.new {1 / 0}.value}.to raise_error ZeroDivisionError 21 | end 22 | end 23 | 24 | describe "#join" do 25 | it "work well" do 26 | t = nil 27 | beam = LightIO::Beam.new {t = true} 28 | expect(t).to be_nil 29 | beam.join 30 | expect(t).to be true 31 | end 32 | 33 | it "with a limit time" do 34 | t1 = Time.now 35 | duration = 10 36 | expect(LightIO::Beam.new {LightIO.sleep(duration)}.join(0.01)).to be_nil 37 | expect(Time.now - t1).to be < duration 38 | end 39 | 40 | it "within limit time" do 41 | start = Time.now 42 | beam = LightIO::Beam.new {1} 43 | beam.join(10) 44 | expect(Time.now - start).to be < 1 45 | end 46 | end 47 | 48 | describe "#pass" do 49 | it 'works' do 50 | result = [] 51 | b1 = LightIO::Beam.new {result << 1; LightIO::Beam.pass; result << 3} 52 | b2 = LightIO::Beam.new {result << 2; LightIO::Beam.pass; result << 4} 53 | b1.join; b2.join 54 | expect(result).to eq([1, 2, 3, 4]) 55 | end 56 | 57 | it "call from non beam" do 58 | expect(LightIO::Beam.pass).to be_nil 59 | end 60 | end 61 | 62 | describe "#alive?" do 63 | it "works" do 64 | beam = LightIO::Beam.new {1 + 2} 65 | expect(beam.alive?).to be_truthy 66 | beam.value 67 | expect(beam.alive?).to be_falsey 68 | end 69 | 70 | it "dead if error raised" do 71 | beam = LightIO::Beam.new {1 / 0} 72 | expect(beam.alive?).to be_truthy 73 | expect {beam.value}.to raise_error ZeroDivisionError 74 | expect(beam.alive?).to be_falsey 75 | end 76 | end 77 | 78 | describe "#kill" do 79 | it 'works' do 80 | beam = LightIO::Beam.new {1 + 2} 81 | expect(beam.alive?).to be_truthy 82 | expect(beam.kill).to be beam 83 | expect(beam.alive?).to be_falsey 84 | expect(beam.value).to be_nil 85 | end 86 | 87 | it 'kill self' do 88 | result = [] 89 | beam = LightIO::Beam.new {result << 1; LightIO::Beam.current.kill; result << 2} 90 | expect(beam.alive?).to be_truthy 91 | beam.join 92 | expect(beam.alive?).to be_falsey 93 | expect(beam.value).to be_nil 94 | expect(result).to be == [1] 95 | end 96 | end 97 | 98 | describe "concurrent" do 99 | it "should concurrent schedule" do 100 | t1 = Time.now 101 | beams = 20.times.map {LightIO::Beam.new {LightIO.sleep 0.01}} 102 | beams.each {|b| b.join} 103 | expect(Time.now - t1).to be < 0.2 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /spec/lightio/core/future_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe LightIO::Future do 4 | describe "#? " do 5 | end 6 | end -------------------------------------------------------------------------------- /spec/lightio/core/ioloop_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe LightIO::IOloop do 4 | it "auto started" do 5 | t = Thread.new {LightIO::IOloop.current.closed?} 6 | expect(t.value).to be_falsey 7 | end 8 | 9 | it "per threaded", skip_monkey_patch: true do 10 | t = Thread.new {LightIO::IOloop.current} 11 | expect(t.value).not_to eq LightIO::IOloop.current 12 | end 13 | 14 | describe "#close", skip_monkey_patch: true do 15 | it "#closed?" do 16 | result = [] 17 | t = Thread.new { 18 | result << LightIO::IOloop.current.closed? 19 | LightIO::IOloop.current.close 20 | result << LightIO::IOloop.current.closed? 21 | } 22 | t.join 23 | expect(result).to eql [false, true] 24 | end 25 | 26 | it "raise error" do 27 | t = Thread.new { 28 | LightIO::IOloop.current.stop 29 | r, w = LightIO::IO.pipe 30 | r.wait_readable 31 | } 32 | expect {t.value}.to raise_error(IOError, 'selector is closed') 33 | end 34 | end 35 | end -------------------------------------------------------------------------------- /spec/lightio/core/light_fiber_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe LightIO::LightFiber do 4 | describe "#current " do 5 | it "can find current light fiber" do 6 | light_fiber = LightIO::LightFiber.new { 7 | LightIO::LightFiber.yield LightIO::LightFiber.current 8 | } 9 | expect(light_fiber.resume).to eq(light_fiber) 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /spec/lightio/library/file_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe LightIO::Library::IO do 4 | describe "#open" do 5 | it "return file" do 6 | f = LightIO::Library::File.open('/dev/stdin', 'r') 7 | expect(f).to be_a(LightIO::Library::File) 8 | expect(f).to be_a(File) 9 | expect(f).to be_kind_of(LightIO::Library::File) 10 | expect(f).to be_kind_of(File) 11 | f.close 12 | end 13 | end 14 | 15 | describe "#pipe" do 16 | it "works with beam" do 17 | r1, w1 = LightIO::Library::File.pipe 18 | r2, w2 = LightIO::Library::File.pipe 19 | b1 = LightIO::Beam.new {r1.gets} 20 | b2 = LightIO::Beam.new {r2.gets} 21 | b1.join(0.01); b2.join(0.01) 22 | w1.puts "foo " 23 | w2.puts "bar" 24 | expect(b1.value + b2.value).to be == "foo \nbar\n" 25 | [r1, r2, w1, w2].each(&:close) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/lightio/library/io_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe LightIO::Library::IO do 4 | describe "act as IO" do 5 | it "#is_a?" do 6 | io = LightIO::Library::IO.new(1) 7 | expect(io).to be_a(LightIO::Library::IO) 8 | expect(io).to be_a(IO) 9 | expect(io).to be_kind_of(LightIO::Library::IO) 10 | expect(io).to be_kind_of(IO) 11 | io.close 12 | end 13 | 14 | 15 | it "#instance_of?" do 16 | io = LightIO::Library::IO.new(1) 17 | expect(io).to be_an_instance_of(LightIO::Library::IO) 18 | expect(io).to be_an_instance_of(IO) 19 | io.close 20 | end 21 | end 22 | 23 | 24 | describe "#wait methods" do 25 | it "#wait_readable" do 26 | r1, w1 = LightIO::Library::IO.pipe 27 | r2, w2 = LightIO::Library::IO.pipe 28 | b1 = LightIO::Beam.new {r1.gets} 29 | b2 = LightIO::Beam.new {r2.gets} 30 | b1.join(0.01); b2.join(0.01) 31 | w1.puts "foo " 32 | w2.puts "bar" 33 | expect(b1.value + b2.value).to be == "foo \nbar\n" 34 | [r1, r2, w1, w2].each(&:close) 35 | end 36 | end 37 | 38 | 39 | describe "#write" do 40 | it "#wait works" do 41 | r, w = LightIO::Library::IO.pipe 42 | if RUBY_VERSION > '2.5.0' 43 | w.write "Hello", " ", "IO" 44 | else 45 | w.write "Hello IO" 46 | end 47 | w.close 48 | expect(r.read).to be == "Hello IO" 49 | r.close 50 | end 51 | end 52 | 53 | describe "#close" do 54 | it 'should close io watcher too' do 55 | r, w = LightIO::Library::IO.pipe 56 | r.close 57 | w.close 58 | expect(r.__send__(:io_watcher).closed?).to be_truthy 59 | expect(w.__send__(:io_watcher).closed?).to be_truthy 60 | end 61 | 62 | it 'call on closed io' do 63 | r, w = LightIO::Library::IO.pipe 64 | r.close 65 | w.close 66 | expect {r.read(1)}.to raise_error(IOError) 67 | expect {w.write("test")}.to raise_error(IOError) 68 | end 69 | end 70 | 71 | describe "#pipe" do 72 | it 'closed after block' do 73 | rd, wd = LightIO::Library::IO.pipe do |r, w| 74 | [r, w] 75 | end 76 | expect(rd.closed?).to be_truthy 77 | expect(wd.closed?).to be_truthy 78 | end 79 | end 80 | 81 | describe "#select" do 82 | it 'should return select fds' do 83 | r1, w1 = LightIO::Library::IO.pipe 84 | r2, w2 = LightIO::Library::IO.pipe 85 | LightIO.sleep 0.1 86 | read_fds, write_fds = LightIO::Library::IO.select([r1, r2], [w1, w2]) 87 | expect(read_fds).to be == [] 88 | expect(write_fds).to be == [w1, w2] 89 | w1.close 90 | LightIO.sleep 0.1 91 | read_fds, write_fds = LightIO::Library::IO.select([r1, r2], [w2]) 92 | expect(read_fds).to be == [r1] 93 | expect(write_fds).to be == [w2] 94 | r1.close 95 | r2.close 96 | w2.close 97 | end 98 | 99 | it 'should raise io error if fd is closed' do 100 | r, w = LightIO::Library::IO.pipe 101 | w.close 102 | expect {LightIO::Library::IO.select([r], [w])}.to raise_error(IOError) 103 | r.close 104 | end 105 | 106 | it 'should blocking until timeout if no io readable' do 107 | r, w = LightIO::Library::IO.pipe 108 | expect(LightIO::Library::IO.select([r], [], [], 0.0001)).to be_nil 109 | r.close 110 | w.close 111 | end 112 | 113 | it 'immediately return readable fd' do 114 | r1, w1 = LightIO::Library::IO.pipe 115 | w1.close 116 | read_fds, write_fds = LightIO::Library::IO.select([r1], nil, nil, 0) 117 | expect(read_fds).to be == [r1] 118 | expect(write_fds).to be == [] 119 | r1.close 120 | end 121 | 122 | context 'implicit conversion' do 123 | 124 | class A_TO_IO 125 | attr_reader :to_io 126 | 127 | def initialize(io) 128 | @to_io = io 129 | end 130 | end 131 | 132 | class B_TO_IO 133 | def to_io 134 | 1 135 | end 136 | end 137 | 138 | it 'should convert implicitly' do 139 | r1, w1 = LightIO::Library::IO.pipe 140 | a_r1, a_w1 = A_TO_IO.new(r1), A_TO_IO.new(w1) 141 | r2, w2 = LightIO::Library::IO.pipe 142 | a_r2, a_w2 = A_TO_IO.new(r2), A_TO_IO.new(w2) 143 | LightIO.sleep 0.1 144 | read_fds, write_fds = LightIO::Library::IO.select([a_r1, a_r2], [a_w1, a_w2]) 145 | expect(read_fds).to be == [] 146 | expect(write_fds).to be == [a_w1, a_w2] 147 | w1.close 148 | LightIO.sleep 0.1 149 | read_fds, write_fds = LightIO::Library::IO.select([a_r1, a_r2], [a_w2]) 150 | expect(read_fds).to be == [a_r1] 151 | expect(write_fds).to be == [a_w2] 152 | r1.close 153 | r2.close 154 | w2.close 155 | end 156 | 157 | it 'raise error if no #to_io method' do 158 | expect { 159 | LightIO::Library::IO.select([1], nil) 160 | }.to raise_error(TypeError, "no implicit conversion of #{1.class} into IO") 161 | end 162 | 163 | it 'raise error if #to_io return not IO' do 164 | expect { 165 | LightIO::Library::IO.select([B_TO_IO.new], nil) 166 | }.to raise_error(TypeError, "can't convert B_TO_IO to IO (B_TO_IO#to_io gives #{1.class})") 167 | end 168 | end 169 | 170 | context 'with raw io', skip_monkey_patch: true do 171 | it 'auto wrap raw io' do 172 | r, w = IO.pipe 173 | r_fds, w_fds = LightIO::Library::IO.select(nil, [w]) 174 | expect(w_fds).to eq [w] 175 | end 176 | end 177 | end 178 | 179 | describe "#read" do 180 | let(:pipe) {LightIO::Library::IO.pipe} 181 | after {pipe.each(&:close) rescue nil} 182 | 183 | it 'length is negative' do 184 | r, w = pipe 185 | expect {r.read(-1)}.to raise_error(ArgumentError) 186 | end 187 | 188 | context 'length is nil' do 189 | it "should read until EOF" do 190 | r, w = pipe 191 | w.puts "hello" 192 | w.puts "world" 193 | w.close 194 | expect(r.read).to eq "hello\nworld\n" 195 | expect(r.read).to eq "" 196 | expect(r.read(nil)).to eq "" 197 | end 198 | 199 | it "use outbuf" do 200 | r, w = pipe 201 | w.puts "hello" 202 | w.puts "world" 203 | w.close 204 | outbuf = "origin content should be remove" 205 | expect(r.read(nil, outbuf)).to eq "hello\nworld\n" 206 | expect(outbuf).to eq "hello\nworld\n" 207 | end 208 | 209 | it "blocking until read EOF" do 210 | r, w = pipe 211 | w.puts "hello" 212 | w.puts "world" 213 | expect do 214 | LightIO::Timeout.timeout(0.0001) do 215 | r.read 216 | end 217 | end.to raise_error(LightIO::Timeout::Error) 218 | w.close 219 | expect(r.read).to eq "hello\nworld\n" 220 | end 221 | 222 | it 'read eof' do 223 | r, w = pipe 224 | t1 = LightIO::Beam.new {r.read} 225 | t2 = LightIO::Beam.new {w.close} 226 | t1.join; t2.join 227 | expect(t1.value).to be == '' 228 | end 229 | end 230 | 231 | context 'length is positive' do 232 | it "should read length" do 233 | r, w = pipe 234 | w.puts "hello" 235 | w.puts "world" 236 | w.close 237 | expect(r.read(5)).to eq "hello" 238 | expect(r.read(1)).to eq "\n" 239 | expect(r.read).to eq "world\n" 240 | end 241 | 242 | it "longer length" do 243 | r, w = pipe 244 | w.puts "hello" 245 | w.puts "world" 246 | w.close 247 | expect(r.read(30)).to eq "hello\nworld\n" 248 | end 249 | 250 | it "use outbuf" do 251 | r, w = pipe 252 | w.puts "hello" 253 | w.puts "world" 254 | w.close 255 | outbuf = "origin content should be remove" 256 | expect(r.read(5, outbuf)).to eq "hello" 257 | expect(outbuf).to eq "hello" 258 | end 259 | 260 | it "blocking until read length" do 261 | r, w = pipe 262 | w.write "hello" 263 | expect do 264 | LightIO::Timeout.timeout(0.0001) do 265 | r.read(10) 266 | end 267 | end.to raise_error(LightIO::Timeout::Error) 268 | w.write "world" 269 | w.close 270 | expect(r.read(10)).to eq "helloworld" 271 | end 272 | 273 | it 'read eof' do 274 | r, w = pipe 275 | t1 = LightIO::Beam.new {r.read(1)} 276 | t2 = LightIO::Beam.new {w.close} 277 | t1.join; t2.join 278 | expect(t1.value).to be_nil 279 | end 280 | end 281 | 282 | describe "#readpartial" do 283 | let(:pipe) {LightIO::Library::IO.pipe} 284 | after {pipe.each(&:close) rescue nil} 285 | 286 | it 'length is negative' do 287 | r, w = pipe 288 | expect {r.readpartial(-1)}.to raise_error(ArgumentError) 289 | end 290 | 291 | it "return immediately content" do 292 | r, w = pipe 293 | w << "hello" 294 | expect(r.readpartial(4096)).to eq "hello" 295 | end 296 | 297 | it "raise EOF" do 298 | r, w = pipe 299 | w << "hello" 300 | w.close 301 | expect(r.readpartial(4096)).to eq "hello" 302 | expect {r.readpartial(4096)}.to raise_error EOFError 303 | end 304 | 305 | it "with outbuf" do 306 | r, w = pipe 307 | w << "hello" 308 | outbuf = "origin content should be remove" 309 | expect(r.readpartial(4096, outbuf)).to eq "hello" 310 | expect(outbuf).to eq "hello" 311 | end 312 | 313 | it "blocking until readable" do 314 | r, w = pipe 315 | expect do 316 | LightIO::Timeout.timeout(0.0001) do 317 | r.readpartial(4096) 318 | end 319 | end.to raise_error(LightIO::Timeout::Error) 320 | w << "hello world" 321 | w.close 322 | expect(r.readpartial(4096)).to eq "hello world" 323 | end 324 | end 325 | end 326 | 327 | describe "#getbyte" do 328 | let(:pipe) {LightIO::Library::IO.pipe} 329 | after {pipe.each(&:close) rescue nil} 330 | 331 | it 'read byte' do 332 | r, w = pipe 333 | t1 = LightIO::Beam.new {r.getbyte} 334 | t2 = LightIO::Beam.new {w.putc('n')} 335 | t1.join; t2.join 336 | expect(t1.value).to be == 'n' 337 | end 338 | 339 | it 'read eof' do 340 | r, w = pipe 341 | t1 = LightIO::Beam.new {r.getbyte} 342 | t2 = LightIO::Beam.new {w.close} 343 | t1.join; t2.join 344 | expect(t1.value).to be_nil 345 | end 346 | end 347 | 348 | describe "#getchar" do 349 | let(:pipe) {LightIO::Library::IO.pipe} 350 | after {pipe.each(&:close) rescue nil} 351 | 352 | it 'read char' do 353 | r, w = pipe 354 | t1 = LightIO::Beam.new {r.getc} 355 | t2 = LightIO::Beam.new {w.putc('光')} 356 | t1.join; t2.join 357 | expect(t1.value).to be == '光' 358 | end 359 | 360 | it 'read eof' do 361 | r, w = pipe 362 | t1 = LightIO::Beam.new {r.getbyte} 363 | t2 = LightIO::Beam.new {w.close} 364 | t1.join; t2.join 365 | expect(t1.value).to be_nil 366 | end 367 | end 368 | 369 | describe "#eof?" do 370 | let(:pipe) {LightIO::Library::IO.pipe} 371 | after {pipe.each(&:close) rescue nil} 372 | 373 | it 'block until eof' do 374 | r, w = pipe 375 | t1 = LightIO::Beam.new {r.eof?} 376 | expect(t1.join(0.001)).to be_nil 377 | w.close 378 | expect(t1.value).to be_truthy 379 | end 380 | 381 | it 'block until readable' do 382 | r, w = pipe 383 | t1 = LightIO::Beam.new {r.eof?} 384 | expect(t1.join(0.001)).to be_nil 385 | w << 'ok' 386 | expect(t1.value).to be_falsey 387 | end 388 | 389 | it 'read eof' do 390 | r, w = pipe 391 | w.close 392 | expect(r.eof?).to be_truthy 393 | end 394 | end 395 | 396 | describe "#gets" do 397 | let(:pipe) {LightIO::Library::IO.pipe} 398 | after {pipe.each(&:close) rescue nil} 399 | 400 | context 'gets' do 401 | it 'gets' do 402 | r, w = pipe 403 | w << "hello" 404 | t1 = LightIO::Beam.new {r.gets} 405 | expect(t1.join(0.001)).to be_nil 406 | w.write($/) 407 | expect(t1.value).to be == "hello\n" 408 | end 409 | 410 | it 'get all' do 411 | r, w = pipe 412 | w << "hello" 413 | t1 = LightIO::Beam.new {r.gets(nil)} 414 | expect(t1.join(0.001)).to be_nil 415 | w.write($/) 416 | expect(t1.join(0.001)).to be_nil 417 | w.close 418 | expect(t1.value).to be == "hello\n" 419 | end 420 | 421 | it 'read eof end' do 422 | r, w = pipe 423 | w << "hello" 424 | t1 = LightIO::Beam.new {r.gets} 425 | expect(t1.join(0.001)).to be_nil 426 | w.close 427 | expect(t1.value).to be == "hello" 428 | end 429 | 430 | it 'read eof' do 431 | r, w = pipe 432 | w.close 433 | t1 = LightIO::Beam.new {r.gets} 434 | expect(t1.value).to be_nil 435 | end 436 | 437 | it 'end with another char' do 438 | r, w = pipe 439 | w << 'hello' 440 | t1 = LightIO::Beam.new {r.gets('o')} 441 | expect(t1.value).to be == 'hello' 442 | end 443 | end 444 | 445 | context 'gets limit' do 446 | it 'non block' do 447 | r, w = pipe 448 | w << 'hello' 449 | expect(r.gets(3)).to be == 'hel' 450 | end 451 | 452 | it 'block' do 453 | r, w = pipe 454 | w << 'he' 455 | t1 = LightIO::Beam.new {r.gets(3)} 456 | expect(t1.join(0.001)).to be_nil 457 | w << 'l' 458 | expect(t1.value).to be == 'hel' 459 | end 460 | 461 | it 'sep' do 462 | r, w = pipe 463 | w.puts "he" 464 | t1 = LightIO::Beam.new {r.gets(5)} 465 | expect(t1.value).to be == "he\n" 466 | end 467 | end 468 | end 469 | 470 | describe "#readline" do 471 | let(:pipe) {LightIO::Library::IO.pipe} 472 | after {pipe.each(&:close) rescue nil} 473 | 474 | it 'EOFError' do 475 | r, w = pipe 476 | w.close 477 | t1 = LightIO::Beam.new {r.readline} 478 | expect {t1.value}.to raise_error EOFError 479 | end 480 | end 481 | 482 | describe "#readchar" do 483 | let(:pipe) {LightIO::Library::IO.pipe} 484 | after {pipe.each(&:close) rescue nil} 485 | 486 | it 'EOFError' do 487 | r, w = pipe 488 | w.close 489 | t1 = LightIO::Beam.new {r.readchar} 490 | expect {t1.value}.to raise_error EOFError 491 | end 492 | end 493 | 494 | describe "#readlines" do 495 | let(:pipe) {LightIO::Library::IO.pipe} 496 | after {pipe.each(&:close) rescue nil} 497 | 498 | it 'readlines' do 499 | r, w = pipe 500 | w.puts "hello" 501 | w.puts "world" 502 | w.close 503 | t1 = LightIO::Beam.new {r.readlines} 504 | expect(t1.value).to be == ["hello\n", "world\n"] 505 | end 506 | 507 | it 'block' do 508 | r, w = pipe 509 | w.puts "hello" 510 | w.puts "world" 511 | t1 = LightIO::Beam.new {r.readlines} 512 | expect(t1.join(0.001)).to be_nil 513 | w.close 514 | end 515 | 516 | it 'eof' do 517 | r, w = pipe 518 | w.close 519 | t1 = LightIO::Beam.new {r.readlines} 520 | expect(t1.value).to be == [] 521 | end 522 | 523 | it 'read all' do 524 | r, w = pipe 525 | w.puts "hello" 526 | w.puts "world" 527 | w.close 528 | t1 = LightIO::Beam.new {r.readlines(nil)} 529 | expect(t1.value).to be == ["hello\nworld\n"] 530 | end 531 | 532 | it 'limit' do 533 | r, w = pipe 534 | w.puts "hello" 535 | w.puts "world" 536 | w.close 537 | t1 = LightIO::Beam.new {r.readlines(3)} 538 | expect(t1.value).to be == ["hel", "lo\n", "wor", "ld\n"] 539 | end 540 | end 541 | 542 | describe '#open' do 543 | it 'with block' do 544 | stdout = nil 545 | LightIO::Library::IO.open(1) do |io| 546 | stdout = io 547 | expect(io.to_i).to be == STDOUT.to_i 548 | end 549 | expect(stdout.closed?).to be_truthy 550 | end 551 | 552 | it 'without block' do 553 | io = LightIO::Library::IO.open(1) 554 | expect(io.to_i).to be == STDOUT.to_i 555 | expect(io.closed?).to be_falsey 556 | io.close 557 | end 558 | end 559 | 560 | describe '#copy_stream' do 561 | it 'call' do 562 | r1, w1 = LightIO::Library::IO.pipe 563 | r2, w2 = LightIO::Library::IO.pipe 564 | w1 << 'Hello world' 565 | w1.close 566 | LightIO::Library::IO.copy_stream(r1, w2) 567 | expect(w2.closed?).to be_falsey 568 | w2.close 569 | expect(r2.read).to eq 'Hello world' 570 | r1.close 571 | r2.close 572 | end 573 | 574 | it 'with copy_length' do 575 | r1, w1 = LightIO::Library::IO.pipe 576 | r2, w2 = LightIO::Library::IO.pipe 577 | w1 << 'Hello world' 578 | w1.close 579 | LightIO::Library::IO.copy_stream(r1, w2, 5) 580 | expect(w2.closed?).to be_falsey 581 | w2.close 582 | expect(r2.read).to eq 'Hello' 583 | r1.close 584 | r2.close 585 | end 586 | 587 | it 'with src_offset' do 588 | r1 = LightIO::Library::File.open("./README.md") 589 | r2, w2 = LightIO::Library::IO.pipe 590 | LightIO::Library::IO.copy_stream(r1, w2, 7, 2) 591 | expect(w2.closed?).to be_falsey 592 | w2.close 593 | expect(r2.read).to eq 'LightIO' 594 | r1.close 595 | r2.close 596 | end 597 | end 598 | end -------------------------------------------------------------------------------- /spec/lightio/library/kernel_ext_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe LightIO::Library::KernelExt do 4 | context '#spawn' do 5 | it 'spawn' do 6 | from = Time.now.to_i 7 | LightIO.spawn("sleep 10") 8 | expect(Time.now.to_i - from).to be < 1 9 | end 10 | 11 | it 'spawn with io' do 12 | r, w = LightIO::Library::IO.pipe 13 | LightIO.spawn("date", out: w) 14 | expect(r.gets.split(':').size).to eq 3 15 | r.close; w.close 16 | end 17 | end 18 | 19 | context '#spawn' do 20 | it 'spawn' do 21 | from = Time.now.to_i 22 | LightIO.spawn("sleep 10") 23 | expect(Time.now.to_i - from).to be < 1 24 | end 25 | 26 | it 'spawn with io' do 27 | r, w = LightIO::Library::IO.pipe 28 | LightIO.spawn("date", out: w) 29 | expect(r.gets.split(':').size).to eq 3 30 | r.close; w.close 31 | end 32 | end 33 | 34 | context '#`' do 35 | it '`' do 36 | expect(LightIO.`("echo hello world")).to eq "hello world\n" 37 | expect($?.exitstatus).to eq 0 38 | LightIO.`("exit 128") 39 | expect($?.exitstatus).to eq 128 40 | end 41 | 42 | it 'error' do 43 | expect {LightIO.`("echeoooooo")}.to raise_error(Errno::ENOENT, 'No such file or directory - echeoooooo') 44 | expect($?.exitstatus).to eq 127 45 | end 46 | 47 | it 'concurrent' do 48 | start = Time.now 49 | 10.times.map do 50 | LightIO::Beam.new {LightIO.`("sleep 0.2")} 51 | end.each(&:join) 52 | expect(Time.now - start).to be < 1 53 | end 54 | end 55 | 56 | context '#system' do 57 | it 'system' do 58 | expect(LightIO.system("echo hello world")).to be_truthy 59 | expect(LightIO.system("exit", "128")).to be_falsey 60 | end 61 | 62 | it 'error' do 63 | expect(LightIO.system("echeoooooo")).to be_nil 64 | end 65 | 66 | it 'concurrent' do 67 | start = Time.now 68 | 10.times.map do 69 | LightIO::Beam.new {LightIO.system("sleep 0.2")} 70 | end.each(&:join) 71 | expect(Time.now - start).to be < 1 72 | end 73 | end 74 | end -------------------------------------------------------------------------------- /spec/lightio/library/mutex_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe LightIO::Mutex do 4 | describe "act as Mutex" do 5 | it "#is_a?" do 6 | obj = LightIO::Library::Mutex.new 7 | expect(obj).to be_a(LightIO::Library::Mutex) 8 | expect(obj).to be_a(Mutex) 9 | expect(obj).to be_kind_of(LightIO::Library::Mutex) 10 | expect(obj).to be_kind_of(Mutex) 11 | end 12 | 13 | it "#instance_of?" do 14 | obj = LightIO::Library::Mutex.new 15 | expect(obj).to be_an_instance_of(LightIO::Library::Mutex) 16 | expect(obj).to be_an_instance_of(Mutex) 17 | end 18 | end 19 | 20 | describe "#lock" do 21 | it "can't lock self" do 22 | m = LightIO::Mutex.new 23 | m.lock 24 | expect {m.lock}.to raise_error(ThreadError) 25 | end 26 | 27 | it "lock works" do 28 | m = LightIO::Mutex.new 29 | result = [] 30 | m.lock 31 | thr = LightIO::Thread.new { 32 | m.lock 33 | result << 1 34 | } 35 | thr.wakeup 36 | thr.wakeup 37 | expect(result).to be == [] 38 | m.unlock 39 | thr.join 40 | expect(result).to be == [1] 41 | end 42 | end 43 | 44 | describe "#unlock" do 45 | it "raise if not locked" do 46 | m = LightIO::Mutex.new 47 | t = LightIO::Thread.fork do 48 | m.lock 49 | end 50 | t.join 51 | expect(m.locked?).to be_truthy 52 | expect {m.unlock}.to raise_error ThreadError 53 | end 54 | end 55 | 56 | describe "#owned?" do 57 | it "correct" do 58 | m = LightIO::Mutex.new 59 | m.lock 60 | t = LightIO::Thread.new do 61 | m.owned? 62 | end 63 | expect(t.value).to be_falsey 64 | expect(m.owned?).to be_truthy 65 | end 66 | end 67 | 68 | describe "#sleep" do 69 | it "correct" do 70 | m = LightIO::Mutex.new 71 | m.lock 72 | result = [] 73 | t = LightIO::Thread.new {m.lock; result << 1; m.unlock} 74 | # call run twice to make sure it sleep by lock 75 | t.run 76 | t.run 77 | expect(result).to be == [] 78 | m.sleep(0.0001) 79 | expect(result).to be == [1] 80 | expect(m.locked?).to be_truthy 81 | end 82 | end 83 | 84 | describe "#synchronize" do 85 | it "correct" do 86 | result = [] 87 | m = LightIO::Mutex.new 88 | t1 = LightIO::Thread.new {m.synchronize {result << 1; LightIO.sleep(0.0001); result << 2;}} 89 | t2 = LightIO::Thread.new {m.synchronize {result << 3; LightIO.sleep(0.0001); result << 4;}} 90 | t1.join; t2.join 91 | expect(m.locked?).to be_falsey 92 | expect(result).to be == [1, 2, 3, 4] 93 | end 94 | end 95 | 96 | describe "#try_lock" do 97 | it "try_lock" do 98 | m = LightIO::Mutex.new 99 | expect(m.try_lock).to be_truthy 100 | expect(m.try_lock).to be_falsey 101 | end 102 | end 103 | 104 | describe LightIO::ConditionVariable do 105 | describe "act as ConditionVariable" do 106 | it "#is_a?" do 107 | obj = LightIO::Library::ConditionVariable.new 108 | expect(obj).to be_a(LightIO::Library::ConditionVariable) 109 | expect(obj).to be_a(ConditionVariable) 110 | expect(obj).to be_kind_of(LightIO::Library::ConditionVariable) 111 | expect(obj).to be_kind_of(ConditionVariable) 112 | end 113 | 114 | it "#instance_of?" do 115 | obj = LightIO::Library::ConditionVariable.new 116 | expect(obj).to be_an_instance_of(LightIO::Library::ConditionVariable) 117 | expect(obj).to be_an_instance_of(ConditionVariable) 118 | end 119 | end 120 | 121 | it '#wait & #signal' do 122 | mutex = LightIO::Mutex.new 123 | resource = LightIO::ConditionVariable.new 124 | 125 | sum = 0 126 | 127 | a = LightIO::Thread.new { 128 | mutex.synchronize { 129 | resource.wait(mutex) 130 | sum *= 2 131 | } 132 | } 133 | 134 | b = LightIO::Thread.new { 135 | mutex.synchronize { 136 | sum =+20 137 | resource.signal 138 | } 139 | } 140 | 141 | a.join 142 | b.run if a.alive? 143 | expect(sum).to be == 40 144 | end 145 | 146 | it '#signal' do 147 | mutex = LightIO::Mutex.new 148 | resource = LightIO::ConditionVariable.new 149 | 150 | # not effected 151 | resource.signal 152 | resource.signal 153 | resource.signal 154 | 155 | sum = 0 156 | 157 | a = LightIO::Thread.new { 158 | mutex.synchronize { 159 | resource.wait(mutex) 160 | sum *= 2 161 | } 162 | } 163 | 164 | b = LightIO::Thread.new { 165 | mutex.synchronize { 166 | sum =+20 167 | resource.signal 168 | } 169 | } 170 | 171 | a.join 172 | b.run if b.alive? 173 | expect(sum).to be == 40 174 | end 175 | 176 | 177 | it '#boardcast' do 178 | mutex = LightIO::Mutex.new 179 | resource = LightIO::ConditionVariable.new 180 | 181 | sum = 0 182 | 183 | a = LightIO::Thread.new { 184 | mutex.synchronize { 185 | resource.wait(mutex) 186 | sum *= 2 187 | } 188 | } 189 | 190 | b = LightIO::Thread.new { 191 | mutex.synchronize { 192 | resource.wait(mutex) 193 | sum *= 2 194 | } 195 | } 196 | 197 | c = LightIO::Thread.new { 198 | mutex.synchronize { 199 | sum += 1 200 | resource.broadcast 201 | } 202 | } 203 | 204 | a.join 205 | b.join if b.alive? 206 | c.join if c.alive? 207 | expect(sum).to be == 4 208 | end 209 | end 210 | end -------------------------------------------------------------------------------- /spec/lightio/library/openssl_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe OpenSSL::SSL::SSLSocket do 2 | describe "#to_io" do 3 | it 'to_io return socket' do 4 | r, w = LightIO::Library::IO.pipe 5 | s = LightIO::Library::OpenSSL::SSL::SSLSocket.new(w) 6 | expect(s.to_io).to be_instance_of(LightIO::Library::IO) 7 | expect(s.to_io).to eq s.io 8 | s.close 9 | r.close; w.close 10 | end 11 | 12 | it 'to_io return raw socket', skip_monkey_patch: true do 13 | r, w = IO.pipe 14 | s = LightIO::Library::OpenSSL::SSL::SSLSocket.new(w) 15 | expect(s.to_io).to be_instance_of(IO) 16 | expect(s.to_io).to_not be_instance_of(LightIO::Library::IO) 17 | s.close 18 | r.close; w.close 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/lightio/library/queue_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe LightIO::Queue do 4 | describe "act as Queue" do 5 | it "#is_a?" do 6 | obj = LightIO::Library::Queue.new 7 | expect(obj).to be_a(LightIO::Library::Queue) 8 | expect(obj).to be_a(Queue) 9 | expect(obj).to be_kind_of(LightIO::Library::Queue) 10 | expect(obj).to be_kind_of(Queue) 11 | end 12 | 13 | it "#instance_of?" do 14 | obj = LightIO::Library::Queue.new 15 | expect(obj).to be_an_instance_of(LightIO::Library::Queue) 16 | expect(obj).to be_an_instance_of(Queue) 17 | end 18 | end 19 | 20 | describe "queue " do 21 | it "works" do 22 | q = LightIO::Queue.new 23 | b = LightIO::Beam.new {q.pop} 24 | b.join(0.01) 25 | expect(q.num_waiting).to be == 1 26 | q << "yes" 27 | expect(b.value).to be == "yes" 28 | end 29 | 30 | it "works with beams" do 31 | q = LightIO::Queue.new 32 | beams = 3.times.map {LightIO::Beam.new {q.pop}} 33 | beams.each {|b| b.join(0.01)} 34 | expect(q.num_waiting).to be == 3 35 | q << "one" 36 | q << "two" 37 | q << "three" 38 | expect(beams.map(&:value)).to be == ["one", "two", "three"] 39 | end 40 | 41 | it 'push and pop' do 42 | q = LightIO::Queue.new 43 | q << 4 44 | expect(q.pop).to be == 4 45 | end 46 | end 47 | end -------------------------------------------------------------------------------- /spec/lightio/library/sized_queue_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe LightIO::SizedQueue do 4 | describe "act as SizedQueue" do 5 | it "#is_a?" do 6 | obj = LightIO::Library::SizedQueue.new(1) 7 | expect(obj).to be_a(LightIO::Library::SizedQueue) 8 | expect(obj).to be_a(Queue) 9 | expect(obj).to be_kind_of(LightIO::Library::SizedQueue) 10 | expect(obj).to be_kind_of(SizedQueue) 11 | end 12 | 13 | it "#instance_of?" do 14 | obj = LightIO::Library::SizedQueue.new(1) 15 | expect(obj).to be_an_instance_of(LightIO::Library::SizedQueue) 16 | expect(obj).to be_an_instance_of(SizedQueue) 17 | end 18 | end 19 | 20 | describe "#new" do 21 | it "max must > 0" do 22 | expect {LightIO::SizedQueue.new(0)}.to raise_error ArgumentError 23 | end 24 | 25 | it 'blocking pop' do 26 | q = LightIO::SizedQueue.new(1) 27 | b = LightIO::Beam.new {q.pop} 28 | b.join(0.01) 29 | expect(q.num_waiting).to be == 1 30 | q << "yes" 31 | expect(b.value).to be == "yes" 32 | end 33 | 34 | it 'blocking enqueue' do 35 | q = LightIO::SizedQueue.new(1) 36 | b = LightIO::Beam.new {q << 1; q << 1} 37 | b.join(0.01) 38 | expect(q.num_waiting).to be == 1 39 | expect(q.size).to be == 1 40 | q.pop 41 | expect(b.value).to be == q 42 | end 43 | 44 | it "#clear" do 45 | q = LightIO::SizedQueue.new(1) 46 | b = LightIO::Beam.new {q << 1; q << 1} 47 | b.join(0.01) 48 | expect(q.num_waiting).to be == 1 49 | expect(q.size).to be == 1 50 | q.clear # release the blocking 51 | expect(b.value).to be == q 52 | expect(q.empty?).to be_falsey 53 | expect(q.num_waiting).to be == 0 54 | q.clear 55 | expect(q.empty?).to be_truthy 56 | end 57 | 58 | it '#max=' do 59 | q = LightIO::SizedQueue.new(2) 60 | b = LightIO::Beam.new {q << 1; q << 1; q << 1} 61 | b.join(0.01) 62 | expect(q.num_waiting).to be == 1 63 | expect(q.size).to be == 2 64 | q.max = 1 # still blocking 65 | expect(q.num_waiting).to be == 1 66 | expect(q.size).to be == 2 67 | expect(b.join(0.01)).to be_nil 68 | q.max = 3 # release 69 | expect(b.value).to be == q 70 | expect(q.size).to be == 3 71 | expect(q.num_waiting).to be == 0 72 | end 73 | end 74 | end -------------------------------------------------------------------------------- /spec/lightio/library/socket_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'date' 3 | require 'socket' 4 | 5 | class EchoServer 6 | def initialize(host, port) 7 | @server = LightIO::TCPServer.new(host, port) 8 | end 9 | 10 | def run 11 | while (socket = @server.accept) 12 | _, port, host = socket.peeraddr 13 | # puts "accept connection from #{host}:#{port}" 14 | 15 | # LightIO::Beam is lightweight executor, provide thread-like interface 16 | # just start new beam for per socket 17 | LightIO::Beam.new(socket) do |socket| 18 | while echo(socket) 19 | end 20 | end 21 | end 22 | end 23 | 24 | def echo(socket) 25 | data = socket.readpartial(4096) 26 | socket.write(data) 27 | rescue EOFError 28 | _, port, host = socket.peeraddr 29 | # puts "*** #{host}:#{port} disconnected" 30 | socket.close 31 | nil 32 | end 33 | 34 | def close 35 | @server.close 36 | end 37 | end 38 | 39 | RSpec.describe LightIO::Library::Socket do 40 | describe "inherited" do 41 | it "inherited correctly" do 42 | expect(LightIO::BasicSocket).to be < LightIO::IO 43 | expect(LightIO::Socket).to be < LightIO::BasicSocket 44 | expect(LightIO::IPSocket).to be < LightIO::BasicSocket 45 | expect(LightIO::TCPSocket).to be < LightIO::IPSocket 46 | expect(LightIO::TCPServer).to be < LightIO::TCPSocket 47 | end 48 | end 49 | 50 | describe "#accept & #connect" do 51 | let(:port) {pick_random_port} 52 | let(:beam) {LightIO::Beam.new do 53 | LightIO::TCPServer.open(port) {|serv| 54 | s = serv.accept 55 | expect(s).to be_a LightIO::Library::TCPSocket 56 | s.puts Date.today 57 | s.close 58 | } 59 | end} 60 | 61 | it "work with raw socket client" do 62 | begin 63 | client = TCPSocket.new 'localhost', port 64 | rescue Errno::ECONNREFUSED 65 | beam.join(0.0001) 66 | retry 67 | end 68 | beam.join(0.0001) 69 | expect(client.gets).to be == "#{Date.today.to_s}\n" 70 | client.close 71 | end 72 | 73 | it "work with TCPSocket" do 74 | begin 75 | client = LightIO::Library::TCPSocket.new 'localhost', port 76 | rescue Errno::ECONNREFUSED 77 | beam.join(0.0001) 78 | retry 79 | end 80 | beam.join(0.0001) 81 | expect(client.gets).to be == "#{Date.today.to_s}\n" 82 | client.close 83 | end 84 | 85 | it "work with Socket" do 86 | begin 87 | client = LightIO::Socket.new(LightIO::Socket::AF_INET, LightIO::Socket::SOCK_STREAM) 88 | client.connect LightIO::Socket.pack_sockaddr_in(port, '127.0.0.1') 89 | rescue Errno::EISCONN 90 | # mean connect is success before ruby 2.2.7 *_* en... 91 | nil 92 | rescue Errno::ECONNREFUSED 93 | beam.join(0.0001) 94 | retry 95 | end 96 | beam.join(0.0001) 97 | expect(client.gets).to be == "#{Date.today.to_s}\n" 98 | client.close 99 | end 100 | end 101 | 102 | describe "#accept_nonblock" do 103 | let(:port) {pick_random_port} 104 | let(:beam) {LightIO::Beam.new do 105 | LightIO::TCPServer.open(port) {|serv| 106 | expect(serv).to be_a LightIO::Library::TCPServer 107 | LightIO::IO.select([serv]) 108 | s = serv.accept_nonblock 109 | expect(s).to be_a LightIO::Library::TCPSocket 110 | s.puts Date.today 111 | s.close 112 | } 113 | end} 114 | 115 | it "work with raw socket client" do 116 | begin 117 | client = TCPSocket.new 'localhost', port 118 | rescue Errno::ECONNREFUSED 119 | beam.join(0.001) 120 | retry 121 | end 122 | beam.join(0.001) 123 | expect(client.gets).to be == "#{Date.today.to_s}\n" 124 | client.close 125 | end 126 | 127 | it "work with TCPSocket" do 128 | begin 129 | client = LightIO::Library::TCPSocket.new 'localhost', port 130 | rescue Errno::ECONNREFUSED 131 | beam.join(0.0001) 132 | retry 133 | end 134 | beam.join(0.0001) 135 | expect(client.gets).to be == "#{Date.today.to_s}\n" 136 | client.close 137 | end 138 | end 139 | 140 | context "some methods should return correct type" do 141 | let(:port) {pick_random_port} 142 | let(:beam) {LightIO::Beam.new do 143 | LightIO::TCPServer.open(port) {|serv| 144 | s = serv.accept 145 | s.puts Date.today 146 | s.close 147 | } 148 | end} 149 | 150 | let (:client) { 151 | begin 152 | client = LightIO::TCPSocket.new 'localhost', port 153 | rescue Errno::ECONNREFUSED 154 | beam.join(0.0001) 155 | retry 156 | end 157 | } 158 | 159 | # bind localhost seems have some problem on MACOS, it connect two fd and never release them(even call close). 160 | # so just don't close them to avoid duplication fd. 161 | after { 162 | #client.close 163 | } 164 | 165 | it "#for_fd" do 166 | s = LightIO::Socket.for_fd(client.fileno) 167 | expect(s).to a_kind_of(LightIO::Socket) 168 | s = LightIO::TCPSocket.for_fd(client.fileno) 169 | expect(s).to a_kind_of(LightIO::TCPSocket) 170 | s = LightIO::TCPServer.for_fd(client.fileno) 171 | expect(s).to a_kind_of(LightIO::TCPServer) 172 | end 173 | 174 | it "#to_io" do 175 | expect(client.to_io).to a_kind_of(LightIO::TCPSocket) 176 | s = LightIO::Socket.for_fd(client.fileno) 177 | expect(s.to_io).to a_kind_of(LightIO::Socket) 178 | s = LightIO::TCPSocket.for_fd(client.fileno) 179 | expect(s.to_io).to a_kind_of(LightIO::TCPSocket) 180 | s = LightIO::TCPServer.for_fd(client.fileno) 181 | expect(s.to_io).to a_kind_of(LightIO::TCPServer) 182 | 183 | r, w = LightIO::IO.pipe 184 | expect(r.to_io).to a_kind_of(LightIO::IO) 185 | r.close 186 | w.close 187 | end 188 | end 189 | 190 | 191 | describe "echo server and multi clients" do 192 | it "multi clients" do 193 | port = pick_random_port 194 | server = EchoServer.new('localhost', port) 195 | beam = LightIO::Beam.new do 196 | server.run 197 | end 198 | b1 = LightIO::Beam.new do 199 | client = LightIO::TCPSocket.new('localhost', port) 200 | response = "" 201 | 3.times { 202 | msg = "hello from b1" 203 | client.write(msg) 204 | response << client.readpartial(4096) 205 | LightIO.sleep(0) 206 | } 207 | client.close 208 | response 209 | end 210 | b2 = LightIO::Beam.new do 211 | client = LightIO::TCPSocket.new('localhost', port) 212 | response = "" 213 | 3.times { 214 | msg = "hello from b2" 215 | client.write(msg) 216 | response << client.readpartial(4096) 217 | LightIO.sleep(0) 218 | } 219 | client.close 220 | response 221 | end 222 | expect(b1.value).to be == "hello from b1hello from b1hello from b1" 223 | expect(b2.value).to be == "hello from b2hello from b2hello from b2" 224 | server.close 225 | expect {beam.value}.to raise_error(IOError) 226 | end 227 | end 228 | 229 | describe LightIO::Library::Socket::Ifaddr do 230 | it '#getifaddrs' do 231 | ifaddrs = LightIO::Library::Socket.getifaddrs 232 | ifaddrs.each do |ifaddr| 233 | [:addr, :broadaddr, :dstaddr, :netmask].each do |m| 234 | result = ifaddr.send(m) 235 | next if result.nil? 236 | expect(result).to be_kind_of(LightIO::Library::Addrinfo) 237 | end 238 | end 239 | end 240 | end 241 | end 242 | 243 | RSpec.describe LightIO::Library::TCPServer do 244 | describe "act as TCPServer" do 245 | it "#is_a?" do 246 | LightIO::TCPServer.open(pick_random_port) {|serv| 247 | obj = serv 248 | expect(obj).to be_a(LightIO::Library::TCPServer) 249 | expect(obj).to be_a(TCPServer) 250 | expect(obj).to be_a(LightIO::Library::TCPSocket) 251 | expect(obj).to be_a(TCPSocket) 252 | expect(obj).to be_a(LightIO::Library::IPSocket) 253 | expect(obj).to be_a(IPSocket) 254 | expect(obj).to be_a(LightIO::Library::BasicSocket) 255 | expect(obj).to be_a(BasicSocket) 256 | expect(obj).to be_a(LightIO::Library::IO) 257 | expect(obj).to be_a(IO) 258 | expect(obj).to be_kind_of(LightIO::Library::TCPServer) 259 | expect(obj).to be_kind_of(TCPServer) 260 | } 261 | end 262 | 263 | it "#instance_of?" do 264 | LightIO::TCPServer.open(pick_random_port) {|serv| 265 | obj = serv 266 | expect(obj).to_not be_an_instance_of(LightIO::Library::BasicSocket) 267 | expect(obj).to_not be_an_instance_of(BasicSocket) 268 | expect(obj).to_not be_an_instance_of(LightIO::Library::IO) 269 | expect(obj).to_not be_an_instance_of(IO) 270 | expect(obj).to be_an_instance_of(LightIO::Library::TCPServer) 271 | expect(obj).to be_an_instance_of(TCPServer) 272 | } 273 | end 274 | end 275 | end 276 | 277 | RSpec.describe LightIO::Library::UNIXServer do 278 | it '#send_io' do 279 | r, w = LightIO::Library::IO.pipe 280 | s1, s2 = LightIO::Library::UNIXSocket.pair 281 | s1.send_io w 282 | out = s2.recv_io 283 | 284 | expect(out.fileno).not_to eq(w.fileno) 285 | 286 | out.puts "hello" # outputs "hello\n" to standard output. 287 | out.close 288 | expect(r.gets).to eq("hello\n") 289 | r.close; w.close 290 | end 291 | end 292 | -------------------------------------------------------------------------------- /spec/lightio/library/thread_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe LightIO::Thread do 4 | describe "act as Thread" do 5 | it "#is_a?" do 6 | obj = LightIO::Library::Thread.new {} 7 | expect(obj).to be_a(LightIO::Library::Thread) 8 | expect(obj).to be_a(Thread) 9 | expect(obj).to be_kind_of(LightIO::Library::Thread) 10 | expect(obj).to be_kind_of(Thread) 11 | end 12 | 13 | it "#instance_of?" do 14 | obj = LightIO::Library::Thread.new {} 15 | expect(obj).to be_an_instance_of(LightIO::Library::Thread) 16 | expect(obj).to be_an_instance_of(Thread) 17 | end 18 | end 19 | 20 | describe "#new" do 21 | it "execute and return value" do 22 | t = LightIO::Thread.new do 23 | 1 24 | end 25 | expect(t.value).to be == 1 26 | end 27 | end 28 | 29 | 30 | describe "#fork" do 31 | it "execute and return value" do 32 | t = LightIO::Thread.fork do 33 | 1 34 | end 35 | expect(t.value).to be == 1 36 | end 37 | end 38 | 39 | describe "#join" do 40 | it "wait executed" do 41 | result = [] 42 | t = LightIO::Thread.new do 43 | result << "hello" 44 | end 45 | t.join 46 | expect(result).to be == ["hello"] 47 | end 48 | 49 | it "return nil if timeout" do 50 | t = LightIO::Thread.new do 51 | LightIO.sleep(1) 52 | end 53 | expect(t.join(0.00001)).to be_nil 54 | end 55 | 56 | it "return self if not timeout" do 57 | t = LightIO::Thread.new do 58 | LightIO.sleep(0.0001) 59 | end 60 | expect(t.join(0.001)).to be == t 61 | end 62 | end 63 | 64 | describe "#exit kill terminate" do 65 | it "exit and dead" do 66 | t1 = LightIO::Thread.new {}.kill 67 | t2 = LightIO::Thread.new {}.exit 68 | t3 = LightIO::Thread.new {}.terminate 69 | expect(t1.alive?).to be_falsey 70 | expect(t2.alive?).to be_falsey 71 | expect(t3.alive?).to be_falsey 72 | end 73 | 74 | it "kill it" do 75 | t = LightIO::Thread.new {} 76 | expect(t.alive?).to be_truthy 77 | expect(LightIO::Thread.kill(t)).to be == t 78 | expect(t.alive?).to be_falsey 79 | end 80 | 81 | it "kill it multi times" do 82 | t = LightIO::Thread.new {} 83 | expect(t.alive?).to be_truthy 84 | expect(t.kill).to be == t 85 | expect(t.kill).to be == t 86 | end 87 | end 88 | 89 | describe "#status" do 90 | it "current thread status is run" do 91 | expect(Thread.current.status).to be == 'run' 92 | expect(Thread.new {Thread.current.status}.value).to be == 'run' 93 | end 94 | 95 | it "sleep if thread blocking" do 96 | t = LightIO::Thread.new {LightIO.sleep(5)} 97 | t.join(0.00001) 98 | expect(t.status).to be == 'sleep' 99 | end 100 | 101 | it "terminate" do 102 | t = LightIO::Thread.new {LightIO.sleep(5)} 103 | t.terminate 104 | expect(t.status).to be_falsey 105 | end 106 | 107 | it "terminated with exception" do 108 | t = LightIO::Thread.new {1 / 0} 109 | t.join rescue nil 110 | expect(t.status).to be_nil 111 | end 112 | 113 | it "aborting" do 114 | t = LightIO::Thread.new {LightIO.sleep} 115 | t.raise "about" 116 | expect(t.status).to be == 'abouting' 117 | end 118 | end 119 | 120 | describe "#abort_on_exception" do 121 | it "not nil" do 122 | expect(LightIO::Thread.abort_on_exception).to_not be_nil 123 | expect(LightIO::Thread.main.abort_on_exception).to_not be_nil 124 | end 125 | end 126 | 127 | describe "#current" do 128 | it "get current thread" do 129 | t = LightIO::Thread.new {LightIO::Thread.current} 130 | expect(t).to be == t.value 131 | end 132 | 133 | it "return main thread" do 134 | t = LightIO::Thread.current 135 | expect(t).to be == LightIO::Thread.main 136 | end 137 | 138 | it "play with beams" do 139 | t = LightIO::Thread.new {LightIO::Timeout.timeout(0.01) {LightIO::Thread.current}} 140 | expect(t).to be == t.value 141 | end 142 | 143 | it "play with fiber" do 144 | t = LightIO::Thread.new {Fiber.new {LightIO::Thread.current}.resume} 145 | expect(t).to be == t.value 146 | end 147 | end 148 | 149 | describe "#exclusive" do 150 | it "result correct" do 151 | result = [] 152 | add_ab = proc do |a, b| 153 | LightIO::Thread.exclusive do 154 | result << a 155 | LightIO.sleep(0.001) 156 | result << b 157 | end 158 | end 159 | t1 = LightIO::Thread.new {add_ab.call(1, 2)} 160 | t2 = LightIO::Thread.new {add_ab.call(3, 4)} 161 | t1.join; t2.join 162 | expect(result).to be == [1, 2, 3, 4] 163 | end 164 | end 165 | 166 | describe "#list" do 167 | it "return Threads" do 168 | t1 = LightIO::Thread.new {} 169 | threads = LightIO::Thread.list 170 | expect(threads.all?(&:alive?)).to be_truthy 171 | expect(threads.include?(t1)).to be_truthy 172 | t1.join 173 | expect(LightIO::Thread.list.include?(t1)).to be_falsey 174 | end 175 | end 176 | 177 | describe "#pass" do 178 | it "pass" do 179 | result = [] 180 | t1 = LightIO::Thread.new {result << 1; LightIO::Thread.pass; result << 3} 181 | t2 = LightIO::Thread.new {result << 2; LightIO::Thread.pass; result << 4} 182 | t1.join; t2.join 183 | expect(LightIO::Thread.stop).to be_nil 184 | expect(result).to be == [1, 2, 3, 4] 185 | end 186 | end 187 | 188 | describe "#[]" do 189 | it "can only save symbol" do 190 | t1 = LightIO::Thread.new {LightIO::Thread.current[:name] = "hello"} 191 | t2 = LightIO::Thread.new {LightIO::Thread.current["name"] = "hello"} 192 | t1.join; t2.join 193 | expect(t1[:name]).to be == "hello" 194 | expect(t1["name"]).to be == "hello" 195 | expect(t2[:name]).to be == "hello" 196 | end 197 | 198 | it "belongs to fiber scope" do 199 | t1 = LightIO::Thread.new { 200 | LightIO::Thread.current[:name] = "hello" 201 | Fiber.new { 202 | expect(t1[:name]).to be_nil 203 | t1[:name] = "only in fiber scope" 204 | }.resume 205 | } 206 | t1.join 207 | expect(t1[:name]).to be == "hello" 208 | end 209 | 210 | describe "#key?" do 211 | it "can only save symbol" do 212 | t1 = LightIO::Thread.new {LightIO::Thread.current[:name] = "hello"} 213 | t1.join 214 | expect(t1.key?(:name)).to be_truthy 215 | expect(t1.key?(:none)).to be_falsey 216 | end 217 | end 218 | 219 | describe "#keys" do 220 | it "return keys" do 221 | t1 = LightIO::Thread.new {} 222 | t1[:name] = 1 223 | t1[:hello] = true 224 | expect(t1.keys).to be == [:name, :hello] 225 | end 226 | end 227 | end 228 | 229 | describe "#priority" do 230 | it "not nil" do 231 | expect(LightIO::Thread.new {}.priority).to_not be_nil 232 | end 233 | end 234 | 235 | describe "#raise" do 236 | it "raise error will kill a thread" do 237 | t = LightIO::Thread.new {LightIO.sleep(0.0001)} 238 | t.raise(ArgumentError) 239 | expect {t.value}.to raise_error ArgumentError 240 | expect(t.alive?).to be_falsey 241 | end 242 | end 243 | 244 | describe "#run" do 245 | it "wakeup sleeping thread" do 246 | result = [] 247 | t = LightIO::Thread.new {result << 1; LightIO::Thread.stop; result << 3} 248 | t.run 249 | result << 2 250 | t.run 251 | expect(result).to be == [1, 2, 3] 252 | end 253 | 254 | it 'wakeup dead thread' do 255 | thr = LightIO::Thread.new {} 256 | thr.kill 257 | expect {thr.wakeup}.to raise_error(ThreadError) 258 | end 259 | end 260 | 261 | describe "#stop?" do 262 | it "dead thread" do 263 | t = LightIO::Thread.new {} 264 | t.kill 265 | expect(t.stop?).to be_truthy 266 | end 267 | 268 | it "sleep thread" do 269 | t = LightIO::Thread.new {} 270 | expect(t.stop?).to be_truthy 271 | end 272 | 273 | it "sleep thread" do 274 | t = LightIO::Thread.new {LightIO::Thread.current.stop?} 275 | expect(t.value).to be_falsey 276 | end 277 | end 278 | 279 | describe "#thread_variables" do 280 | it "can only save symbol" do 281 | t1 = LightIO::Thread.new {LightIO::Thread.current.thread_variable_set(:name, "hello")} 282 | t2 = LightIO::Thread.new {LightIO::Thread.current.thread_variable_set("name", "hello")} 283 | t1.join; t2.join 284 | expect(t1.thread_variable_get(:name)).to be == "hello" 285 | expect(t1.thread_variable_get("name")).to be == "hello" 286 | expect(t2.thread_variable_get(:name)).to be == "hello" 287 | end 288 | 289 | it "belongs to thread scope" do 290 | t1 = LightIO::Thread.new { 291 | LightIO::Thread.current.thread_variable_set(:name, "hello") 292 | Fiber.new { 293 | expect(t1.thread_variable_get(:name)).to be == "hello" 294 | t1.thread_variable_set(:name, "in thread scope") 295 | }.resume 296 | } 297 | t1.join 298 | expect(t1.thread_variable_get(:name)).to be == "in thread scope" 299 | end 300 | 301 | describe "#thread_variable?" do 302 | it "can only save symbol" do 303 | t1 = LightIO::Thread.new {LightIO::Thread.current.thread_variable_set(:name, "hello")} 304 | t1.join 305 | expect(t1.thread_variable?(:name)).to be_truthy 306 | expect(t1.thread_variable?(:none)).to be_falsey 307 | end 308 | end 309 | 310 | describe "#thread_variables" do 311 | it "return keys" do 312 | t1 = LightIO::Thread.new {} 313 | t1.thread_variable_set(:name, 1) 314 | t1.thread_variable_set(:hello, true) 315 | expect(t1.thread_variables).to be == [:name, :hello] 316 | end 317 | end 318 | end 319 | end 320 | 321 | RSpec.describe LightIO::ThreadGroup do 322 | describe "#list" do 323 | it "should have more threads than native" do 324 | expect(Set.new(LightIO::ThreadGroup::Default.list)).to be > Set.new(ThreadGroup::Default.list) 325 | end 326 | 327 | it "removed if thread dead" do 328 | thr = LightIO::Thread.new {} 329 | thr_group = LightIO::ThreadGroup.new 330 | thr_group.add(thr) 331 | expect(thr_group.list).to be == [thr] 332 | thr.kill 333 | expect(thr_group.list).to be == [] 334 | expect(thr.group).to be == thr_group 335 | end 336 | end 337 | 338 | describe "#add" do 339 | it "add to another group" do 340 | thr = LightIO::Thread.new {} 341 | expect(thr.group).to be == LightIO::ThreadGroup::Default 342 | thr_group = LightIO::ThreadGroup.new 343 | thr_group.add(thr) 344 | expect(LightIO::ThreadGroup::Default.list.include?(thr)).to be_falsey 345 | expect(thr_group.list).to be == [thr] 346 | end 347 | 348 | it "play with native Thread" do 349 | thr = LightIO::Thread.main 350 | thr_group = LightIO::ThreadGroup.new 351 | thr_group.add(thr) 352 | expect(LightIO::ThreadGroup::Default.list.include?(thr)).to be_falsey 353 | expect(thr_group.list).to be == [thr] 354 | LightIO::ThreadGroup::Default.add(thr) 355 | end 356 | 357 | it "#enclose" do 358 | thr = LightIO::Thread.new {} 359 | expect(thr.group).to be == LightIO::ThreadGroup::Default 360 | thr_group = LightIO::ThreadGroup.new 361 | thr_group.enclose 362 | expect(thr_group.enclosed?).to be_truthy 363 | expect {thr_group.add(thr)}.to raise_error(ThreadError) 364 | expect(LightIO::ThreadGroup::Default.list.include?(thr)).to be_truthy 365 | expect(thr_group.list).to be == [] 366 | end 367 | end 368 | end -------------------------------------------------------------------------------- /spec/lightio/library/threads_wait_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe LightIO::ThreadsWait do 4 | describe "act as ThreadsWait" do 5 | it "#is_a?" do 6 | obj = LightIO::Library::ThreadsWait.new 7 | expect(obj).to be_a(LightIO::Library::ThreadsWait) 8 | expect(obj).to be_a(ThreadsWait) 9 | expect(obj).to be_kind_of(LightIO::Library::ThreadsWait) 10 | expect(obj).to be_kind_of(ThreadsWait) 11 | end 12 | 13 | it "#instance_of?" do 14 | obj = LightIO::Library::ThreadsWait.new {} 15 | expect(obj).to be_an_instance_of(LightIO::Library::ThreadsWait) 16 | expect(obj).to be_an_instance_of(ThreadsWait) 17 | end 18 | end 19 | 20 | describe "#all_waits" do 21 | it "wait all terminated" do 22 | threads = 5.times.map {LightIO::Thread.new {}} 23 | result = [] 24 | LightIO::ThreadsWait.all_waits(*threads) do |t| 25 | result << t 26 | end 27 | expect(result) == threads 28 | expect(threads.any?(&:alive?)).to be_falsey 29 | end 30 | end 31 | 32 | describe "#next_wait" do 33 | it "non threads for waiting" do 34 | tw = LightIO::ThreadsWait.new 35 | expect(tw.empty?).to be_truthy 36 | expect {tw.next_wait}.to raise_error ThreadsWait::ErrNoWaitingThread 37 | end 38 | 39 | it "nonblock" do 40 | threads = 2.times.map {LightIO::Thread.new {LightIO.sleep(10)}} 41 | thr = LightIO::Thread.new {} 42 | thr.kill 43 | threads << thr 44 | tw = LightIO::ThreadsWait.new(*threads) 45 | expect(tw.finished?).to be_truthy 46 | expect(tw.next_wait(true)).to be == thr 47 | expect(tw.finished?).to be_falsey 48 | end 49 | 50 | it "nonblock but nofinished thread" do 51 | threads = 1.times.map {LightIO::Thread.new {LightIO.sleep(10)}} 52 | tw = LightIO::ThreadsWait.new(*threads) 53 | expect {tw.next_wait(true)}.to raise_error ThreadsWait::ErrNoFinishedThread 54 | end 55 | end 56 | end -------------------------------------------------------------------------------- /spec/lightio/library/timeout_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe LightIO::Timeout do 4 | describe "#timeout " do 5 | it "not reach timeout" do 6 | result = LightIO::Timeout.timeout(0.2) do 7 | 1 8 | end 9 | expect(result).to be == 1 10 | end 11 | 12 | it "timeout" do 13 | expect do 14 | LightIO::Timeout.timeout(0.01) do 15 | LightIO.sleep 5 16 | end 17 | end.to raise_error LightIO::Timeout::Error 18 | end 19 | 20 | it "not timeout" do 21 | start = Time.now 22 | LightIO::Timeout.timeout(10) do 23 | 1 24 | end 25 | expect(Time.now - start).to be < 1 26 | end 27 | 28 | it "timeout block operations" do 29 | start = Time.now 30 | expect { 31 | LightIO::Timeout.timeout(0.1) do 32 | LightIO.gets 33 | end 34 | }.to raise_error(Timeout::Error) 35 | expect(Time.now - start).to be < 1 36 | end 37 | end 38 | end -------------------------------------------------------------------------------- /spec/lightio/monkey_patch/io_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe IO do 2 | describe 'inherited from IO with nameless class' do 3 | it "#is_a? & #instance_of?" do 4 | klass = Class.new(IO) 5 | io = klass.new(1) 6 | expect(io).to be_a(LightIO::Library::IO) 7 | expect(io).to be_a(IO) 8 | expect(io).to_not be_an_instance_of(LightIO::Library::IO) 9 | expect(io).to_not be_an_instance_of(IO) 10 | io.close 11 | end 12 | 13 | it ".pipe" do 14 | klass = Class.new(IO) 15 | r, w = klass.pipe 16 | expect(r).to be_an_instance_of(klass) 17 | expect(w).to be_an_instance_of(klass) 18 | w << "hello" 19 | w.close 20 | expect(r.read).to eq "hello" 21 | r.close; w.close 22 | end 23 | end 24 | 25 | class IOFake < ::IO 26 | end 27 | 28 | describe 'inherited from IO' do 29 | it "#is_a? & #instance_of?" do 30 | io = IOFake.new(1) 31 | expect(io).to be_a(LightIO::Library::IO) 32 | expect(io).to be_a(IO) 33 | expect(io).to_not be_an_instance_of(LightIO::Library::IO) 34 | expect(io).to_not be_an_instance_of(IO) 35 | io.close 36 | end 37 | 38 | it ".pipe" do 39 | r, w = IOFake.pipe 40 | expect(r).to be_an_instance_of(IOFake) 41 | expect(w).to be_an_instance_of(IOFake) 42 | w << "hello" 43 | w.close 44 | expect(r.read).to eq "hello" 45 | r.close; w.close 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/lightio/monkey_patch/net_http_spec.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'webrick' 3 | 4 | RSpec.describe Net::HTTP, skip_library: true do 5 | let(:port) {6666} 6 | let(:server) { 7 | WEBrick::HTTPServer.new(BindAddress: 'localhost', Port: port).tap do |server| 8 | server.mount_proc '/' do |req, res| 9 | res.body = 'Hello, world!' 10 | end 11 | 12 | server.mount_proc '/sleep' do |req, res| 13 | sleep 0.2 14 | end 15 | end 16 | } 17 | before(:each) {Thread.new {server.start}} 18 | after(:each) {server.shutdown} 19 | 20 | it 'should not block' do 21 | start = Time.now 22 | 10.times.map do 23 | Thread.new do 24 | Net::HTTP.start('localhost', port) do |http| 25 | res = http.request_get('/sleep') 26 | expect(res.code).to eq "200" 27 | end 28 | end 29 | end.each(&:join) 30 | expect(Time.now - start).to be < 1 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/lightio/monkey_patch/openssl_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe OpenSSL::SSL::SSLSocket, skip_library: true do 2 | describe 'new' do 3 | it "#is_a? & #instance_of?" do 4 | r, w = IO.pipe 5 | s = OpenSSL::SSL::SSLSocket.new(w) 6 | expect(s).to be_a(LightIO::Library::OpenSSL::SSL::SSLSocket) 7 | expect(s).to be_a(OpenSSL::SSL::SSLSocket) 8 | expect(s).to be_an_instance_of(LightIO::Library::OpenSSL::SSL::SSLSocket) 9 | expect(s).to be_an_instance_of(OpenSSL::SSL::SSLSocket) 10 | s.close 11 | r.close; w.close 12 | end 13 | 14 | it "select" do 15 | r, w = IO.pipe 16 | s = OpenSSL::SSL::SSLSocket.new(w) 17 | reads, writes, _ = IO.select([], [w]) 18 | expect(writes) == [w] 19 | s.close 20 | r.close; w.close 21 | end 22 | end 23 | 24 | describe 'inherited' do 25 | it 'success' do 26 | MySSLSocket = Class.new(::OpenSSL::SSL::SSLSocket) 27 | expect(MySSLSocket < OpenSSL::SSL::SSLSocket) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/lightio/monkey_patch/timeout_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Timeout, skip_library: true do 2 | it 'should raise when block' do 3 | expect do 4 | Timeout.timeout(0.1) do 5 | gets 6 | end 7 | end.to raise_error(Timeout::Error) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/lightio/monkey_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe LightIO::Monkey, skip_library: true do 4 | describe '#patch_thread!' do 5 | it '#patched?' do 6 | expect(LightIO::Monkey.patched?(LightIO)).to be_falsey 7 | expect(LightIO::Monkey.patched?(Thread)).to be_truthy 8 | expect(LightIO::Monkey.patched?(ThreadGroup)).to be_truthy 9 | expect(LightIO::Monkey.patched?(Thread::Mutex)).to be_truthy 10 | expect(LightIO::Monkey.patched?(Thread::Queue)).to be_truthy 11 | expect(LightIO::Monkey.patched?(Thread::SizedQueue)).to be_truthy 12 | expect(LightIO::Monkey.patched?(Thread::ConditionVariable)).to be_truthy 13 | expect(LightIO::Monkey.patched?(Mutex)).to be_truthy 14 | expect(LightIO::Monkey.patched?(Queue)).to be_truthy 15 | expect(LightIO::Monkey.patched?(SizedQueue)).to be_truthy 16 | expect(LightIO::Monkey.patched?(ConditionVariable)).to be_truthy 17 | expect(LightIO::Monkey.patched?(Timeout)).to be_truthy 18 | expect(LightIO::Monkey.patched?(ThreadsWait)).to be_truthy 19 | expect(LightIO::Monkey.patched?(Thread::ThreadsWait)).to be_truthy if RUBY_VERSION < '2.5.0' 20 | end 21 | 22 | it 'class methods is patched' do 23 | expect(Thread.new {}).to be_a(LightIO::Library::Thread) 24 | expect(Thread.new {Thread.current}.value).to be_a(LightIO::Library::Thread) 25 | end 26 | end 27 | 28 | describe '#patch_io!' do 29 | it '#patched?' do 30 | expect(LightIO::Monkey.patched?(LightIO)).to be_falsey 31 | expect(LightIO::Monkey.patched?(IO)).to be_truthy 32 | expect(LightIO::Monkey.patched?(File)).to be_truthy 33 | expect(LightIO::Monkey.patched?(Socket)).to be_truthy 34 | expect(LightIO::Monkey.patched?(TCPSocket)).to be_truthy 35 | expect(LightIO::Monkey.patched?(TCPServer)).to be_truthy 36 | expect(LightIO::Monkey.patched?(BasicSocket)).to be_truthy 37 | expect(LightIO::Monkey.patched?(Addrinfo)).to be_truthy 38 | expect(LightIO::Monkey.patched?(IPSocket)).to be_truthy 39 | expect(LightIO::Monkey.patched?(UDPSocket)).to be_truthy 40 | expect(LightIO::Monkey.patched?(UNIXSocket)).to be_truthy 41 | expect(LightIO::Monkey.patched?(UNIXServer)).to be_truthy 42 | expect(LightIO::Monkey.patched?(Process)).to be_truthy 43 | expect(LightIO::Monkey.patched?(OpenSSL::SSL::SSLSocket)).to be_truthy 44 | end 45 | 46 | it '#new' do 47 | io = IO.new(1) 48 | expect(io).to be_a(LightIO::Library::IO) 49 | io.close 50 | end 51 | 52 | it 'class methods is patched' do 53 | r, w = IO.pipe 54 | expect(r).to be_a(LightIO::Library::IO) 55 | expect(w).to be_a(LightIO::Library::IO) 56 | r.close; w.close 57 | end 58 | 59 | describe File do 60 | it '#new' do 61 | f = File.new("README.md", "r") 62 | expect(f).to be_a(File) 63 | f.close 64 | end 65 | 66 | it "#open" do 67 | f = File.new("README.md", "r") 68 | expect(f).to be_a(File) 69 | f.close 70 | end 71 | end 72 | 73 | describe "#accept_nonblock" do 74 | let(:port) {pick_random_port} 75 | let(:beam) {LightIO::Beam.new do 76 | TCPServer.open(port) {|serv| 77 | expect(serv).to be_a LightIO::Library::TCPServer 78 | IO.select([serv]) 79 | s = serv.accept_nonblock 80 | expect(s).to be_a LightIO::Library::TCPSocket 81 | s.puts Process.pid.to_s 82 | s.close 83 | } 84 | end} 85 | 86 | it "work with raw socket client" do 87 | begin 88 | client = TCPSocket.new 'localhost', port 89 | rescue Errno::ECONNREFUSED 90 | beam.join(0.0001) 91 | retry 92 | end 93 | beam.join(0.0001) 94 | expect(client.gets).to be == "#{Process.pid.to_s}\n" 95 | client.close 96 | end 97 | end 98 | 99 | describe Process do 100 | context '#spawn' do 101 | it 'spawn' do 102 | from = Time.now.to_i 103 | Process.spawn("sleep 10") 104 | expect(Time.now.to_i - from).to be < 1 105 | end 106 | 107 | it 'spawn with io' do 108 | r, w = LightIO::Library::IO.pipe 109 | Process.spawn("date", out: w) 110 | expect(r.gets.split(':').size).to eq 3 111 | r.close; w.close 112 | end 113 | end 114 | end 115 | end 116 | 117 | describe '#patch_kernel!' do 118 | it '#patched?' do 119 | expect(LightIO::Monkey.patched?(LightIO)).to be_falsey 120 | end 121 | 122 | context 'kernel' do 123 | it '#select patched' do 124 | r, w = IO.pipe 125 | w.close 126 | expect { 127 | read_fds, write_fds = Kernel.select([r], nil, nil, 0) 128 | }.to_not raise_error 129 | end 130 | 131 | it '#sleep patched' do 132 | start = Time.now 133 | 100.times.map {LightIO::Beam.new {Kernel.sleep 0.1}}.each(&:join) 134 | expect(Time.now - start).to be < 1 135 | end 136 | 137 | it '#open patched' do 138 | f = Kernel.open('/dev/stdin', 'r') 139 | expect(f).to be_a(LightIO::Library::File) 140 | f.close 141 | end 142 | end 143 | 144 | context 'main' do 145 | it '#select patched' do 146 | r, w = IO.pipe 147 | w.close 148 | expect { 149 | read_fds, write_fds = select([r], nil, nil, 0) 150 | }.to_not raise_error 151 | end 152 | 153 | it '#sleep patched' do 154 | start = Time.now 155 | 100.times.map {LightIO::Beam.new {sleep 0.1}}.each(&:join) 156 | expect(Time.now - start).to be < 1 157 | end 158 | 159 | it '#open patched' do 160 | f = open('/dev/stdin', 'r') 161 | expect(f).to be_a(LightIO::Library::File) 162 | f.close 163 | end 164 | end 165 | 166 | context '#spawn' do 167 | it 'spawn' do 168 | from = Time.now.to_i 169 | spawn("sleep 10") 170 | expect(Time.now.to_i - from).to be < 1 171 | end 172 | 173 | it 'spawn with io' do 174 | r, w = IO.pipe 175 | spawn("date", out: w) 176 | expect(r.gets.split(':').size).to eq 3 177 | r.close; w.close 178 | end 179 | end 180 | 181 | context 'test open3' do 182 | require 'open3' 183 | it '#select patched' do 184 | from = Time.now.to_i 185 | Open3.popen3("sleep 10") 186 | expect(Time.now.to_i - from).to be < 1 187 | end 188 | end 189 | 190 | context '`' do 191 | it 'concurrent' do 192 | start = Time.now 193 | 10.times.map do 194 | LightIO::Beam.new {`sleep 0.2`} 195 | end.each(&:join) 196 | expect(Time.now - start).to be < 1 197 | end 198 | end 199 | 200 | context 'system' do 201 | it 'concurrent' do 202 | start = Time.now 203 | 10.times.map do 204 | LightIO::Beam.new {system("sleep 0.2")} 205 | end.each(&:join) 206 | expect(Time.now - start).to be < 1 207 | end 208 | end 209 | 210 | context 'io methods' do 211 | it 'should yield' do 212 | result = [] 213 | Thread.new {result << 1} 214 | expect(result).to eq [] 215 | expect { 216 | Timeout.timeout(0.1) {gets} 217 | }.to raise_error(Timeout::Error) 218 | expect(result).to eq [1] 219 | end 220 | end 221 | end 222 | end -------------------------------------------------------------------------------- /spec/lightio/watchers/io_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe LightIO::Watchers::IO, skip_monkey_patch: true do 4 | let(:pipe) {IO.send :origin_pipe} 5 | describe "Watchers::IO" do 6 | it 'can not call wait on closed io watcher' do 7 | r, w = pipe 8 | io_watcher = LightIO::Watchers::IO.new(r, :r) 9 | io_watcher.close 10 | expect {io_watcher.wait_readable}.to raise_error(IOError) 11 | end 12 | 13 | it 'can not cross threads' do 14 | r, w = pipe 15 | io_watcher = LightIO::Watchers::IO.new(r, :r) 16 | expect {Thread.send(:origin_start) {io_watcher.wait_readable}.value}.to raise_error(LightIO::Error) 17 | io_watcher.close 18 | r.close; w.close 19 | end 20 | end 21 | 22 | describe "Watchers::IO with beam" do 23 | it "#wait_read works" do 24 | r, w = pipe 25 | b = LightIO::Beam.new { 26 | io_watcher = LightIO::Watchers::IO.new(r, :r) 27 | io_watcher.wait_readable 28 | data = r.read 29 | io_watcher.close 30 | data 31 | } 32 | expect(b.join(0.001)).to be_nil 33 | w.write "Hello IO" 34 | w.close 35 | expect(b.value).to be == "Hello IO" 36 | end 37 | 38 | it "#wait_read and #wait_write between beams" do 39 | r, w = pipe 40 | b = LightIO::Beam.new { 41 | io_watcher = LightIO::Watchers::IO.new(r, :r) 42 | io_watcher.wait_readable 43 | data = r.read 44 | io_watcher.close 45 | data 46 | } 47 | b2 = LightIO::Beam.new { 48 | io_watcher = LightIO::Watchers::IO.new(w, :w) 49 | io_watcher.wait_writable 50 | w << "Hello" 51 | io_watcher.close 52 | w.close 53 | } 54 | b.join; b2.join 55 | expect(b.value).to be == "Hello" 56 | end 57 | 58 | it "#wait_read on main fiber" do 59 | r, w = pipe 60 | LightIO::Beam.new {w << "Hello from Beam"; w.close} 61 | io_watcher = LightIO::Watchers::IO.new(r, :r) 62 | io_watcher.wait_readable 63 | expect(r.read).to be == "Hello from Beam" 64 | io_watcher.close 65 | end 66 | end 67 | 68 | describe "#wait_read with timeout" do 69 | it "beam wait until timeout" do 70 | r, _w = pipe 71 | b = LightIO::Beam.new { 72 | io_watcher = LightIO::Watchers::IO.new(r, :r) 73 | data = if io_watcher.wait_readable(0.001) 74 | r.read 75 | end 76 | io_watcher.close 77 | data 78 | } 79 | expect(b.value).to be_nil 80 | end 81 | 82 | it "root wait until timeout" do 83 | r, _w = pipe 84 | io_watcher = LightIO::Watchers::IO.new(r, :r) 85 | data = if io_watcher.wait_readable(0.001) 86 | r.read 87 | end 88 | io_watcher.close 89 | data 90 | expect(data).to be_nil 91 | end 92 | end 93 | 94 | describe "#wait_read multiple io at same time" do 95 | it "two beams wait two pipe" do 96 | results = 2.times.map do 97 | r, w = IO.send(:origin_pipe) 98 | cr, cw = IO.send(:origin_pipe) 99 | b = LightIO::Beam.new(cr, w) {|r, w| 100 | io_watcher = LightIO::Watchers::IO.new(r, :r) 101 | loop do 102 | io_watcher.wait_readable 103 | data = r.readline 104 | w.puts(data) 105 | end 106 | io_watcher.close 107 | } 108 | [b, r, cw] 109 | end 110 | b1, r1, w1 = results[0] 111 | b2, r2, w2 = results[1] 112 | b1.join(0.001); b2.join(0.001) 113 | w1.puts("hello") 114 | r1_watcher = LightIO::Watchers::IO.new(r1, :r) 115 | r1_watcher.wait_readable 116 | expect(r1.readline).to be == "hello\n" 117 | w2.puts("world") 118 | r2_watcher = LightIO::Watchers::IO.new(r2, :r) 119 | r2_watcher.wait_readable 120 | expect(r2.readline).to be == "world\n" 121 | w1.puts("b1 still works") 122 | r1_watcher.wait_readable 123 | expect(r1.readline).to be == "b1 still works\n" 124 | w2.puts("b2 is also cool") 125 | r2_watcher.wait_readable 126 | expect(r2.readline).to be == "b2 is also cool\n" 127 | r1_watcher.close 128 | r2_watcher.close 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /spec/lightio/watchers/timer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe LightIO::Watchers::Timer do 4 | describe "register timer " do 5 | it "wait for interval seconds" do 6 | t1 = Time.now 7 | interval = 0.1 8 | timer = LightIO::Watchers::Timer.new interval 9 | LightIO::IOloop.current.wait(timer) 10 | expect(Time.now - t1).to be > interval 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /spec/lightio_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe LightIO do 2 | it "has a version number" do 3 | expect(LightIO::VERSION).not_to be nil 4 | end 5 | 6 | describe "#sleep" do 7 | it "sleep work" do 8 | t1 = Time.now 9 | duration = 0.01 10 | LightIO.sleep duration 11 | expect(Time.now - t1).to be > duration 12 | end 13 | 14 | it "can sleep zero time" do 15 | t1 = Time.now 16 | duration = 0.1 17 | LightIO.sleep 0 18 | expect(Time.now - t1).to be < duration 19 | end 20 | 21 | it "sleep forever" do 22 | expect {LightIO.timeout(0.01) do 23 | LightIO.sleep 24 | end}.to raise_error LightIO::Timeout::Error 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/monkey_patch.rb: -------------------------------------------------------------------------------- 1 | require 'lightio' 2 | 3 | LightIO::Monkey.patch_all! 4 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV['COVERAGE'] 2 | require 'coveralls' 3 | Coveralls.wear! 4 | end 5 | 6 | require "bundler/setup" 7 | require "lightio" 8 | require_relative "helper_methods" 9 | 10 | RSpec.configure do |config| 11 | # Enable flags like --only-failures and --next-failure 12 | config.example_status_persistence_file_path = ".rspec_status" 13 | 14 | # Disable RSpec exposing methods globally on `Module` and `main` 15 | config.disable_monkey_patching! 16 | 17 | config.expect_with :rspec do |c| 18 | c.syntax = :expect 19 | end 20 | 21 | config.include HelperMethods 22 | end 23 | --------------------------------------------------------------------------------