├── log └── .gitignore ├── .coveralls.yml ├── logo.png ├── .rspec ├── lib └── celluloid │ ├── io │ ├── version.rb │ ├── mailbox.rb │ ├── ssl_server.rb │ ├── unix_socket.rb │ ├── ssl_socket.rb │ ├── unix_server.rb │ ├── tcp_server.rb │ ├── udp_socket.rb │ ├── reactor.rb │ ├── socket.rb │ ├── dns_resolver.rb │ ├── tcp_socket.rb │ └── stream.rb │ └── io.rb ├── tasks ├── rspec.task └── benchmarks.task ├── Rakefile ├── spec ├── celluloid │ ├── io │ │ ├── mailbox_spec.rb │ │ ├── udp_socket_spec.rb │ │ ├── reactor_spec.rb │ │ ├── dns_resolver_spec.rb │ │ ├── actor_spec.rb │ │ ├── unix_server_spec.rb │ │ ├── tcp_server_spec.rb │ │ ├── ssl_server_spec.rb │ │ ├── socket_spec.rb │ │ ├── unix_socket_spec.rb │ │ ├── ssl_socket_spec.rb │ │ └── tcp_socket_spec.rb │ └── io_spec.rb ├── support │ └── examples │ │ ├── classes.rb │ │ └── methods.rb ├── spec_helper.rb └── fixtures │ ├── client.crt │ ├── server.crt │ ├── server.key │ └── client.key ├── Guardfile ├── .gitignore ├── Gemfile ├── examples ├── echo_client.rb ├── echo_unix_client.rb ├── echo_unix_server.rb └── echo_server.rb ├── LICENSE.txt ├── celluloid-io.gemspec ├── .travis.yml ├── benchmarks └── actor.rb ├── .rubocop.yml ├── CHANGES.md └── README.md /log/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service-name: travis-pro 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celluloid/celluloid-io/HEAD/logo.png -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | --backtrace 4 | --order random 5 | --warnings 6 | -------------------------------------------------------------------------------- /lib/celluloid/io/version.rb: -------------------------------------------------------------------------------- 1 | module Celluloid 2 | module IO 3 | VERSION = "0.17.3" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /tasks/rspec.task: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | 3 | RSpec::Core::RakeTask.new 4 | 5 | RSpec::Core::RakeTask.new(:rcov) do |task| 6 | task.rcov = true 7 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | Dir["tasks/**/*.task"].each { |task| load task } 4 | 5 | task default: :spec 6 | task ci: %w(spec benchmark) 7 | -------------------------------------------------------------------------------- /spec/celluloid/io/mailbox_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Celluloid::IO::Mailbox, library: :IO do 4 | it_behaves_like "a Celluloid Mailbox" 5 | end 6 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard "rspec", cli: "--format documentation" do 2 | watch(%r{^spec/.+_spec\.rb$}) 3 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 4 | watch("spec/spec_helper.rb") { "spec/" } 5 | end 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /spec/support/examples/classes.rb: -------------------------------------------------------------------------------- 1 | class WrapperActor 2 | include Celluloid::IO 3 | execute_block_on_receiver :wrap 4 | 5 | def wrap 6 | yield 7 | end 8 | end 9 | 10 | def with_wrapper_actor 11 | WrapperActor.new 12 | end 13 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "bundler/setup" 3 | 4 | module Specs 5 | ALLOW_SLOW_MAILBOXES = true # TODO: Remove hax. 6 | end 7 | 8 | require "celluloid/rspec" 9 | require "celluloid/io" 10 | 11 | Dir[*Specs::INCLUDE_PATHS].map { |f| require f } 12 | -------------------------------------------------------------------------------- /lib/celluloid/io/mailbox.rb: -------------------------------------------------------------------------------- 1 | module Celluloid 2 | module IO 3 | # An alternative implementation of Celluloid::Mailbox using Reactor 4 | class Mailbox < Celluloid::Mailbox::Evented 5 | def initialize 6 | super(Reactor) 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem "pry" 7 | end 8 | 9 | group :test do 10 | gem "rspec", "~> 3", require: false 11 | gem "rspec-retry", "~> 0.5", require: false 12 | gem "rubocop", "= 0.45.0", require: false 13 | gem "coveralls", ">= 0.8", require: false 14 | gem "benchmark-ips", require: false 15 | end 16 | 17 | group :development, :test do 18 | gem "rake" 19 | end 20 | -------------------------------------------------------------------------------- /examples/echo_client.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "rubygems" 4 | require "bundler/setup" 5 | require "celluloid/autostart" 6 | require "celluloid/io" 7 | 8 | class EchoClient 9 | include Celluloid::IO 10 | 11 | def initialize(host, port) 12 | puts "*** Connecting to echo server on #{host}:#{port}" 13 | 14 | # This is actually creating a Celluloid::IO::TCPSocket 15 | @socket = TCPSocket.new(host, port) 16 | end 17 | 18 | def echo(s) 19 | @socket.write(s) 20 | @socket.readpartial(4096) 21 | end 22 | end 23 | 24 | client = EchoClient.new("127.0.0.1", 1234) 25 | puts client.echo("TEST FOR ECHO") 26 | -------------------------------------------------------------------------------- /tasks/benchmarks.task: -------------------------------------------------------------------------------- 1 | require 'timeout' 2 | 3 | desc "Run Celluloid benchmarks" 4 | task :benchmark do 5 | # Travis has an out-of-date version of rbx that rashes on the benchmarks 6 | exit 0 if ENV['CI'] and RUBY_ENGINE == 'rbx' 7 | 8 | begin 9 | Timeout.timeout(120) do 10 | glob = File.expand_path("../../benchmarks/*.rb", __FILE__) 11 | Dir[glob].each { |benchmark| load benchmark } 12 | end 13 | rescue Exception, Timeout::Error => ex 14 | puts "ERROR: Couldn't complete benchmark: #{ex.class}: #{ex}" 15 | puts " #{ex.backtrace.join("\n ")}" 16 | 17 | exit 1 unless ENV['CI'] # Hax for running benchmarks on Travis 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/celluloid/io_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Celluloid::IO, library: :IO do 4 | context "copy_stream" do 5 | let(:host) { "127.0.0.1" } 6 | let(:port) { 23_456 } 7 | 8 | it "copies streams from Celluloid::IO sockets" do 9 | server = described_class::TCPServer.new(host, port) 10 | client = ::TCPSocket.new(host, port) 11 | peer = server.accept 12 | expect(peer).to be_a described_class::TCPSocket 13 | 14 | my_own_bits = File.read(__FILE__) 15 | file = File.open(__FILE__, "r") 16 | 17 | described_class.copy_stream(file, peer) 18 | expect(client.read(file.stat.size)).to eq my_own_bits 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /examples/echo_unix_client.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "celluloid/autostart" 3 | require "celluloid/io" 4 | 5 | class EchoUNIXClient 6 | include Celluloid::IO 7 | finalizer :finalize 8 | 9 | def initialize(socket_path) 10 | puts "*** connecting to #{socket_path}" 11 | @socket_path = socket_path 12 | @socket = UNIXSocket.open(socket_path) 13 | end 14 | 15 | def echo(msg) 16 | puts "*** send to server: '#{msg}'" 17 | @socket.puts(msg) 18 | data = @socket.readline.chomp 19 | puts "*** server answer '#{data}'" 20 | data 21 | end 22 | 23 | def finalize 24 | @socket.close if @socket 25 | end 26 | end 27 | 28 | c = EchoUNIXClient.new("/tmp/sock_test") 29 | c.echo("DATA") 30 | -------------------------------------------------------------------------------- /lib/celluloid/io/ssl_server.rb: -------------------------------------------------------------------------------- 1 | require "socket" 2 | 3 | module Celluloid 4 | module IO 5 | # SSLServer wraps a TCPServer to provide immediate SSL accept 6 | class SSLServer 7 | extend Forwardable 8 | def_delegators :@tcp_server, :listen, :shutdown, :close, :closed?, :to_io 9 | 10 | attr_accessor :start_immediately 11 | attr_reader :tcp_server 12 | 13 | def initialize(server, ctx) 14 | @tcp_server = Socket.try_convert(server) 15 | @ctx = ctx 16 | @start_immediately = true 17 | end 18 | 19 | def accept 20 | sock = @tcp_server.accept 21 | begin 22 | ssl = Celluloid::IO::SSLSocket.new(sock, @ctx) 23 | ssl.accept if @start_immediately 24 | ssl 25 | rescue OpenSSL::SSL::SSLError 26 | sock.close 27 | raise 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /examples/echo_unix_server.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "celluloid/autostart" 3 | require "celluloid/io" 4 | 5 | class EchoUNIXServer 6 | include Celluloid::IO 7 | finalizer :finalize 8 | 9 | attr_reader :socket_path, :server 10 | 11 | def initialize(socket_path) 12 | puts "*** start server #{socket_path}" 13 | @socket_path = socket_path 14 | @server = UNIXServer.open(socket_path) 15 | async.run 16 | end 17 | 18 | def run 19 | loop { async.handle_connection @server.accept } 20 | end 21 | 22 | def handle_connection(socket) 23 | loop do 24 | data = socket.readline 25 | puts "*** gets data #{data}" 26 | socket.write(data) 27 | end 28 | 29 | rescue EOFError 30 | puts "*** disconnected" 31 | 32 | ensure 33 | socket.close 34 | end 35 | 36 | def finalize 37 | if @server 38 | @server.close 39 | File.delete(@socket_path) 40 | end 41 | end 42 | end 43 | 44 | supervisor = EchoUNIXServer.supervise("/tmp/sock_test") 45 | trap("INT") { supervisor.terminate; exit } 46 | sleep 47 | -------------------------------------------------------------------------------- /lib/celluloid/io/unix_socket.rb: -------------------------------------------------------------------------------- 1 | require "socket" 2 | 3 | module Celluloid 4 | module IO 5 | # UNIXSocket with combined blocking and evented support 6 | class UNIXSocket < Stream 7 | # Open a UNIX connection. 8 | def self.open(socket_path, &block) 9 | new(socket_path, &block) 10 | end 11 | 12 | # Convert a Ruby UNIXSocket into a Celluloid::IO::UNIXSocket 13 | # DEPRECATED: to be removed in a future release 14 | # @deprecated use .new instead 15 | def self.from_ruby_socket(ruby_socket) 16 | new(ruby_socket) 17 | end 18 | 19 | # Open a UNIX connection. 20 | def initialize(socket_path, &block) 21 | # Allow users to pass in a Ruby UNIXSocket directly 22 | if socket_path.is_a? ::UNIXSocket 23 | super(socket_path) 24 | return 25 | end 26 | 27 | # FIXME: not doing non-blocking connect 28 | if block 29 | super ::UNIXSocket.open(socket_path, &block) 30 | else 31 | super ::UNIXSocket.new(socket_path) 32 | end 33 | end 34 | 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/celluloid/io/udp_socket_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Celluloid::IO::UDPSocket, library: :IO do 4 | let(:payload) { "ohai" } 5 | let(:example_port) { assign_port } 6 | subject do 7 | Celluloid::IO::UDPSocket.new.tap do |sock| 8 | sock.bind example_addr, example_port 9 | end 10 | end 11 | 12 | after { subject.close } 13 | 14 | context "inside Celluloid::IO" do 15 | it "should be evented" do 16 | expect(within_io_actor { Celluloid::IO.evented? }).to be_truthy 17 | end 18 | 19 | it "sends and receives packets" do 20 | within_io_actor do 21 | subject.send payload, 0, example_addr, example_port 22 | expect(subject.recvfrom(payload.size).first).to eq(payload) 23 | end 24 | end 25 | end 26 | 27 | context "outside Celluloid::IO" do 28 | it "should be blocking" do 29 | expect(Celluloid::IO).not_to be_evented 30 | end 31 | 32 | it "sends and receives packets" do 33 | subject.send payload, 0, example_addr, example_port 34 | expect(subject.recvfrom(payload.size).first).to eq(payload) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /examples/echo_server.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Run this as: bundle exec examples/echo_server.rb 4 | 5 | require "bundler/setup" 6 | require "celluloid/autostart" 7 | require "celluloid/io" 8 | 9 | class EchoServer 10 | include Celluloid::IO 11 | finalizer :finalize 12 | 13 | def initialize(host, port) 14 | puts "*** Starting echo server on #{host}:#{port}" 15 | 16 | # Since we included Celluloid::IO, we're actually making a 17 | # Celluloid::IO::TCPServer here 18 | @server = TCPServer.new(host, port) 19 | async.run 20 | end 21 | 22 | def finalize 23 | @server.close if @server 24 | end 25 | 26 | def run 27 | loop { async.handle_connection @server.accept } 28 | end 29 | 30 | def handle_connection(socket) 31 | _, port, host = socket.peeraddr 32 | puts "*** Received connection from #{host}:#{port}" 33 | loop { socket.write socket.readpartial(4096) } 34 | rescue EOFError 35 | puts "*** #{host}:#{port} disconnected" 36 | socket.close 37 | end 38 | end 39 | 40 | supervisor = EchoServer.supervise("127.0.0.1", 1234) 41 | trap("INT") { supervisor.terminate; exit } 42 | sleep 43 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Tony Arcieri 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /spec/celluloid/io/reactor_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Celluloid::IO::Reactor, library: :IO do 4 | let(:payload) { "balls" } 5 | let(:example_port) { assign_port } 6 | 7 | it "shouldn't crash" do 8 | server = ::TCPServer.new example_addr, example_port 9 | 10 | thread = Thread.new { server.accept } 11 | 12 | socket = within_io_actor { Celluloid::IO::TCPSocket.new example_addr, example_port } 13 | peer = thread.value 14 | peer_thread = Thread.new { loop { peer << payload } } 15 | handle = false 16 | 17 | # Main server body: 18 | within_io_actor do 19 | begin 20 | timeout(2) do 21 | loop do 22 | socket.readpartial(2046) 23 | end 24 | end 25 | # rescuing timeout, ok. rescuing terminated exception, is it ok? TODO: investigate 26 | rescue Celluloid::TaskTerminated, Celluloid::TaskTimeout, Timeout::Error 27 | ensure 28 | socket.readpartial(2046) 29 | handle = true 30 | end 31 | end 32 | 33 | expect(handle).to be_truthy 34 | 35 | server.close 36 | peer.close 37 | socket.close 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /celluloid-io.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # frozen_string_literal: true 3 | 4 | lib = File.expand_path("../lib", __FILE__) 5 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 6 | require "celluloid/io/version" 7 | 8 | Gem::Specification.new do |gem| 9 | gem.name = "celluloid-io" 10 | gem.version = Celluloid::IO::VERSION 11 | gem.license = "MIT" 12 | gem.authors = ["Tony Arcieri", "Donovan Keme"] 13 | gem.email = ["tony.arcieri@gmail.com", "code@extremist.digital"] 14 | gem.description = "Evented IO for Celluloid actors" 15 | gem.summary = "Celluloid::IO allows you to monitor multiple IO objects within a Celluloid actor" 16 | gem.homepage = "http://github.com/celluloid/celluloid-io" 17 | 18 | gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } 19 | gem.files = `git ls-files`.split("\n") 20 | gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 21 | gem.require_paths = ["lib"] 22 | 23 | gem.add_dependency "nio4r", ">= 1.1" 24 | 25 | gem.add_development_dependency "rb-fsevent", "~> 0.9.1" if RUBY_PLATFORM =~ /darwin/ 26 | end 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | script: rake ci 2 | language: ruby 3 | rvm: 4 | - rbx-2 5 | - jruby 6 | - 2.3.0 7 | - 2.2.2 8 | - 2.2.0 9 | - 2.1.4 10 | - 2.0.0 11 | - 1.9.3 12 | - ruby-head 13 | - jruby-head 14 | 15 | matrix: 16 | fast_finish: true 17 | allow_failures: 18 | - rvm: 1.9.3 19 | - rvm: ruby-head 20 | - rvm: jruby-head 21 | - env: CELLULOID_BACKPORTED=true 22 | - env: CELLULOID_BACKPORTED=false CELLULOID_TASK_CLASS=Threaded 23 | - env: CELLULOID_BACKPORTED=true CELLULOID_TASK_CLASS=Threaded 24 | 25 | env: 26 | global: 27 | - NUMBER_OF_PROCESSORS=4 CELLULOID_CONFIG_FILE=.env-ci 28 | matrix: 29 | - CELLULOID_BACKPORTED=true 30 | - CELLULOID_BACKPORTED=false 31 | - CELLULOID_BACKPORTED=false CELLULOID_TASK_CLASS=Threaded 32 | - CELLULOID_BACKPORTED=true CELLULOID_TASK_CLASS=Threaded 33 | 34 | notifications: 35 | irc: "irc.freenode.org#celluloid" 36 | 37 | before_install: 38 | # Only use 1 job until Travis fixes the rbx --jobs issue. 39 | - if [ "$TRAVIS_RUBY_VERSION" == "rbx-2" ] ; then export BUNDLE_JOBS=1 ; else export BUNDLE_JOBS=4; fi 40 | 41 | sudo: false 42 | install: bundle install --without=development 43 | -------------------------------------------------------------------------------- /lib/celluloid/io/ssl_socket.rb: -------------------------------------------------------------------------------- 1 | require "openssl" 2 | 3 | module Celluloid 4 | module IO 5 | # SSLSocket with Celluloid::IO support 6 | class SSLSocket < Stream 7 | extend Forwardable 8 | 9 | def_delegators :to_io, 10 | :cert, 11 | :cipher, 12 | :client_ca, 13 | :peeraddr, 14 | :peer_cert, 15 | :peer_cert_chain, 16 | :post_connection_check, 17 | :verify_result, 18 | :sync_close= 19 | 20 | def initialize(io, ctx = OpenSSL::SSL::SSLContext.new) 21 | @context = ctx 22 | socket = OpenSSL::SSL::SSLSocket.new(::IO.try_convert(io), @context) 23 | socket.sync_close = true if socket.respond_to?(:sync_close=) 24 | super(socket) 25 | end 26 | 27 | def connect 28 | to_io.connect_nonblock 29 | self 30 | rescue ::IO::WaitReadable 31 | wait_readable 32 | retry 33 | end 34 | 35 | def accept 36 | to_io.accept_nonblock 37 | self 38 | rescue ::IO::WaitReadable 39 | wait_readable 40 | retry 41 | rescue ::IO::WaitWritable 42 | wait_writable 43 | retry 44 | end 45 | 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/fixtures/client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDmjCCAoICCQDwZ8yE/0n4PjANBgkqhkiG9w0BAQUFADCBjjELMAkGA1UEBhMC 3 | VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28x 4 | EjAQBgNVBAoTCUNlbGx1bG9pZDEbMBkGA1UEAxMSY2xpZW50LmV4YW1wbGUuY29t 5 | MSEwHwYJKoZIhvcNAQkBFhJjbGllbnRAZXhhbXBsZS5jb20wHhcNMTIxMTI1MTkx 6 | NjI2WhcNMjIxMTIzMTkxNjI2WjCBjjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh 7 | bGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoTCUNlbGx1 8 | bG9pZDEbMBkGA1UEAxMSY2xpZW50LmV4YW1wbGUuY29tMSEwHwYJKoZIhvcNAQkB 9 | FhJjbGllbnRAZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK 10 | AoIBAQDV6zzpkAIX4FijnytX84GgHb8hYdNJAn+9g57XGrqtAH6BlLoANFl4n/+y 11 | nEQwBqlrNnfstPrf7sPezytntSVyufSE+LGUGBJA/jyjQCMcEe8+4bfOC2ZhCpvn 12 | I2dKNKwsmM+DyWs/PVl7XEAZF2P4iQ8eGLVFjph+KA/D79cHkIeGt4FEA4xJWqKf 13 | +Kjftxo1cBqLx2dUiucRL7tva3ingAqYSs/i82jKLGlj7fdRMytOx87Nhs35RWpu 14 | 66l7hvpetx3t2wU2obKOzKhS4ycaZ2AptEDNXKaBTQ5lejSRxFBCpYQtqmkd0bMG 15 | /T5ZfXC45axj9a2rj8AKZct+mLCzAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAHzr 16 | b4VTktAi+8baGRJCXupt0Ari8ffoWhsYerELFLQF7K2sluxOqCsGEEF21e99fZxP 17 | lisLi0DIQ7cNlOGjRJ3xaydE74Fsry3xBNKoR8I7OMr9VFsrC54tc0x7NQ7bRHy6 18 | kCjSwKN4I2KWJjQ8yf8mIalmUKOmb/hirzna8io4CiDeJGZ1XNAQ9dl1RHRW442G 19 | GTu2ofAtU8TlzilZyclMY/lN7whw7sKP+pPr6fpAOJZsR64IzbBcWHHjJhx70XOx 20 | jnd5FB1oXnuupgPqEKmatSCytrue8GTkanB8VZ6+Zd/4XgTkie3UtCZW8R+NL/Lo 21 | us/+Ks3WIDyYdPSPnbE= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /spec/fixtures/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDmjCCAoICCQD+dJ16wNIKnzANBgkqhkiG9w0BAQUFADCBjjELMAkGA1UEBhMC 3 | VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28x 4 | EjAQBgNVBAoTCUNlbGx1bG9pZDEbMBkGA1UEAxMSc2VydmVyLmV4YW1wbGUuY29t 5 | MSEwHwYJKoZIhvcNAQkBFhJzZXJ2ZXJAZXhhbXBsZS5jb20wHhcNMTIxMTI1MTkx 6 | NjAwWhcNMjIxMTIzMTkxNjAwWjCBjjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh 7 | bGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoTCUNlbGx1 8 | bG9pZDEbMBkGA1UEAxMSc2VydmVyLmV4YW1wbGUuY29tMSEwHwYJKoZIhvcNAQkB 9 | FhJzZXJ2ZXJAZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK 10 | AoIBAQCpLs3Xg00RtoG4+SKaaNfw6Dve9d0Kkg0CNfU8AsxOgTIU1Qu8s+bzKkqe 11 | 66NfCa6T8VPpk9VbIlF2ONdgY4o8muV1q+mS6j2HDAtWPiDjP+9YOwGf/DT3LhSb 12 | g8k+alL2cqe7B1XNUsNFEvQ+yQLlj9MWKb7nbYDM8aqdv46KGoDj9v9rfm4QiKwI 13 | B6u/KoQG22YF7sT4O44jU/u20xcm3rV1Elg0gC/UP/YqnuMPCZYcK/Z9vYGtDJ6G 14 | 8OYDcPZSZBdkqlffhYBssSj3R7sTCqoh4ms08ukcGycbvUO+cWrPKnmySsGaCYtG 15 | kp7QsH1ec7QGA01hZL2yL8CuJMUbAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBABE4 16 | gYVSdC87NhpA49k0vcLLU7v7mU3a3no/vu1CIqQxzx8/xh26Qi3aGb1s9MgHpF2Z 17 | NiB1irXER2tyz/F3qCi8OCo7eNsndmDjj4GnkBjEPTtqRxH9imRWw4bJyqwqFHcu 18 | 1kczCZa+2VFQFEL4ErGycPFKM59ppTcJ0IxbK7PCGzO75TRQoAl52+3Aob+oejPP 19 | qFbiqNlV1T3EKa5yLdvOC5sLrEcfm3iMxmOtNVzp9OBhjXfm8Q1zgYs4VyJXzLMK 20 | wf956w2YEbpTAAzNc53zly/Jhr4MnFsa9Mn1oYp9Rfjzw/qJtXw+a3PtEKrO4XNe 21 | TsKHsAkj8XvUrhliiNQ= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /benchmarks/actor.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'bundler/setup' 5 | require 'celluloid/io' 6 | require 'benchmark/ips' 7 | 8 | #de TODO: Consolidate with Celluloid benchmarking actor. 9 | class BenchmarkingActor 10 | include Celluloid::IO 11 | 12 | def initialize 13 | @condition = Celluloid::Condition.new 14 | end 15 | 16 | def example_method; end 17 | 18 | def finished 19 | @condition.signal 20 | end 21 | 22 | def wait_until_finished 23 | @condition.wait 24 | end 25 | end 26 | 27 | example_actor = BenchmarkingActor.new 28 | mailbox = Celluloid::IO::Mailbox.new 29 | 30 | latch_in, latch_out = Queue.new, Queue.new 31 | latch = Thread.new do 32 | while true 33 | n = latch_in.pop 34 | for i in 0...n; mailbox.receive; end 35 | latch_out << :done 36 | end 37 | end 38 | 39 | Benchmark.ips do |ips| 40 | ips.report("spawn") { BenchmarkingActor.new.terminate } 41 | 42 | ips.report("calls") { example_actor.example_method } 43 | 44 | ips.report("async calls") do |n| 45 | waiter = example_actor.future.wait_until_finished 46 | 47 | for i in 1..n; example_actor.async.example_method; end 48 | example_actor.async.finished 49 | 50 | waiter.value 51 | end 52 | 53 | # Deadlocking o_O 54 | =begin 55 | ips.report("messages") do |n| 56 | latch_in << n 57 | for i in 0...n; mailbox << :message; end 58 | latch_out.pop 59 | end 60 | =end 61 | end 62 | -------------------------------------------------------------------------------- /lib/celluloid/io/unix_server.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | 3 | module Celluloid 4 | module IO 5 | # UNIXServer with combined blocking and evented support 6 | class UNIXServer < Socket 7 | extend Forwardable 8 | def_delegators :to_io, :listen, :sysaccept 9 | 10 | def self.open(socket_path) 11 | self.new(socket_path) 12 | end 13 | 14 | # @overload initialize(socket_path) 15 | # @param socket_path [String] 16 | # 17 | # @overload initialize(socket) 18 | # @param socket [::UNIXServer] 19 | def initialize(socket) 20 | if socket.kind_of? ::BasicSocket 21 | # socket 22 | fail ArgumentError, "wrong kind of socket (#{socket.class} for UNIXServer)" unless socket.kind_of? ::UNIXServer 23 | super(socket) 24 | else 25 | begin 26 | super(::UNIXServer.new(socket)) 27 | rescue => ex 28 | # Translate the EADDRINUSE jRuby exception. 29 | raise unless RUBY_PLATFORM == 'java' 30 | if ex.class.name == "IOError" && # Won't agree to .is_a?(IOError) 31 | ex.message.include?("in use") 32 | raise Errno::EADDRINUSE.new(ex.message) 33 | end 34 | raise 35 | end 36 | end 37 | end 38 | 39 | def accept 40 | Celluloid::IO.wait_readable(to_io) 41 | accept_nonblock 42 | end 43 | 44 | def accept_nonblock 45 | Celluloid::IO::UNIXSocket.new(to_io.accept_nonblock) 46 | end 47 | 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/fixtures/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAqS7N14NNEbaBuPkimmjX8Og73vXdCpINAjX1PALMToEyFNUL 3 | vLPm8ypKnuujXwmuk/FT6ZPVWyJRdjjXYGOKPJrldavpkuo9hwwLVj4g4z/vWDsB 4 | n/w09y4Um4PJPmpS9nKnuwdVzVLDRRL0PskC5Y/TFim+522AzPGqnb+OihqA4/b/ 5 | a35uEIisCAervyqEBttmBe7E+DuOI1P7ttMXJt61dRJYNIAv1D/2Kp7jDwmWHCv2 6 | fb2BrQyehvDmA3D2UmQXZKpX34WAbLEo90e7EwqqIeJrNPLpHBsnG71DvnFqzyp5 7 | skrBmgmLRpKe0LB9XnO0BgNNYWS9si/AriTFGwIDAQABAoIBAGKRoV4p4rIqOiQy 8 | CuYZpY53T8KUToeFFk0ucMXY/33RqgMXKTJ1Ql50SmuS8GlDs9IALZqOBiWFth6B 9 | +YHwHK84s+2+DmUJUnWnH8fMhM7CBknKfyTeBWHqGBmPS6WwvstVe8HtASGSUbCh 10 | 3WnjJWvoQtzLz6z4UK2XM4ea/ooY+hlcw6DM+jZuTstzLFE/9BPoHueaW8UemjaH 11 | ZUXMKm3+I3iIjHszUUWM59bS9pOBn/YvIJbVE5wMIVCP2YXDCgfpV2Z4nDiAHppn 12 | fnha2TzHzlPMgwhBpz06r6G8X+A6gJl98TDSK41nIMyXqiZ2aoALL3UOssAMfUHr 13 | 2y9CGdECgYEA27F1IyUW3JgqCeqZ7eCeT4emhAvyohzM5pzWI7C8ZosF14zFRpek 14 | TgmjdTGMQ1EZVVkyj85RyvMm3MkcKOHetc5g2jJg3EkxvAV+PMs7yjpqg3itEjC6 15 | vIhXLoXdq+FuruA2h4O0hi6yuf1FCQYtpNLTe49qetjsaWzwwowHwlMCgYEAxSRo 16 | k+AdpoNXblnIhd0EaKjGAsHFrC039o7JqQe/mKAiXaGiidIDk5Vt/ChT6Qa6fiLq 17 | cdysCn+tSCt/DdRrELZohc0ipuy5/agQmJgWoW7oay8ldzxHP9VevWo4UuqVudW9 18 | evhKe0a9uXCrSimvZ5PJk91lmBx92FBeP6Y+qRkCgYAXQsvPQ88O3kGdOSzBJgY9 19 | D3TPCGDRT1FWnYaC0uSvysp8jxgYKFgqNxUKhIuAWSbghYg397VrUqFrwRNtNLUa 20 | 9NYGZE0jJdDRQpeiIjaba+H5N57DjUtISPtKHrxgxYatl2nOoWBM0Mb1sF5N3UyZ 21 | 5gSkUYQJq8wkQXegcakkpwKBgEdvvgV3vMbN6SyvlB4NzL8wCTCOjtapPBI4A5Mg 22 | n6jqvgk3vPI8C9e62jP5WQ6jxYhXlqTT1fOn+F6ihFO6mWFg99ckUl4ygeMMt5bT 23 | 5b9xtP7CAs2GJjtXUhFJIEfLgZ3pedPJjRPGupEr5qXlHQ5nWzAdlebczC1KUhy2 24 | XRZhAoGAGA3SAAF79PK3c3+bOOviXxDFuH5TCBmbtEpJ+/jCbwR6Z8fMRswZJ3Gc 25 | l8eNMsB+Kfif+806xAgLxsyhYuyvF6rE/V34GKjW22T1gwk6gY/rOgFn42jo8lwl 26 | HFbSB+IG0+Go0m+0QmyRh7SyPvDNtyYzBFFdl9U8JYYGM0Nfgd0= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /spec/fixtures/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA1es86ZACF+BYo58rV/OBoB2/IWHTSQJ/vYOe1xq6rQB+gZS6 3 | ADRZeJ//spxEMAapazZ37LT63+7D3s8rZ7Ulcrn0hPixlBgSQP48o0AjHBHvPuG3 4 | zgtmYQqb5yNnSjSsLJjPg8lrPz1Ze1xAGRdj+IkPHhi1RY6YfigPw+/XB5CHhreB 5 | RAOMSVqin/io37caNXAai8dnVIrnES+7b2t4p4AKmErP4vNoyixpY+33UTMrTsfO 6 | zYbN+UVqbuupe4b6Xrcd7dsFNqGyjsyoUuMnGmdgKbRAzVymgU0OZXo0kcRQQqWE 7 | LappHdGzBv0+WX1wuOWsY/Wtq4/ACmXLfpiwswIDAQABAoIBAQCFDDcpaUj0ArP+ 8 | qEuz+x6/MGEk6QwZV7WNcGSFkvlSCoGkJJV+9RBExvao5yo92JbcuNbj4Tg7uOwY 9 | EzAC45a0AVZEscz4t/P6emXKf2SW28y6hnbkbxCxAIEwxENE0vfXEP/YDplmjsit 10 | whWXxYWHGe/OHz33UhYkONR9YBmUeLrtloRNUV82XDSpn4d7toLKaZW2kOFl4nFR 11 | SQ3pDPk1hleG8AZcfnF2LwaPx1XjPwBnXY9FK2jyNupVghfCw/Sv91dbbVkkIG14 12 | A8WpZKAXjXXOcTroof5+NJSPXzYrRuvP8K6H2zGj7F/AsP4hiZqGkb4tel0yH5VM 13 | oLCUTHqhAoGBAPysxeoT1ytajQ55UV1yjsnQ3jF9YcWsZwPEJgMI+bt+QzAfqqZs 14 | Kuvg8Gi7ukbcc2pKwXv+ma9HLJq/pQbWlfxcMNulY0VCPY/ceaPen+EfCJTApVpY 15 | YFS25i/JnIp9IudpQBuLHz9Yy4f1W2VoeG/mFqOmUxiTx4LM87G6rdtDAoGBANi7 16 | 5raiwDS+rD91l5FLq3kdvgSDgYk4hh7BBJNJt8vhJYInIev5eb/Z41X8PfqWa2ir 17 | 9aanpMYhWDJxbdSQDd3/op6jtOZM7InLceAm2O29VY2+HW5ePpc21AHsqoZpFYEZ 18 | YP8xvbSjJzfkrYr4y+aAMXONVAi4afqG7x6GqCXRAoGBAPbzFWu1gHKKyZn/0Bn4 19 | wL1WOhM8a7Z6zSPNLSmCODGbMadzC6Ijzb9D1TNHZsOi6dpUvc2mBCZe9aU48N1C 20 | FMzUfZvuhJtIJkrYPLp/9tpbLlPUBMfL4Dprl4XVEf34V4i8QT+qNRwAeMukbXMr 21 | K6qRwkanZEd9B107WmG2Bf1pAoGACpld1g6tgabVe6D/kY52y0yGD2hy/Ef0Xyqn 22 | U6CmSWUwVWYehZDEwHoiYQEd8tRKWmsWb1kBeOMGkijz6xJEa1fmFwYAgca/RpnZ 23 | btHXiADbXzwt6kjXnMOEqLdvO3WGJLMeCDzhfyT/dP9M8V/rcNFSGcmOk4KZRDQ3 24 | G3IQZRECgYBqHqvxHeL087UHXE1tdAGFVfxOiYE1afksJbkV06VnO8XXr9sNSWwy 25 | YjXVY9k6U1BFo7jHrWr6TkeMkB45wyn/fasHKU7qsPt0joRFkXMCzwl376hto3Tg 26 | RGXQCA4RUQXkxaDctJ5TgcF7MhK7tAFd1aBVlxwGENLYUVL0ZPaMrw== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /lib/celluloid/io/tcp_server.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | 3 | module Celluloid 4 | module IO 5 | # TCPServer with combined blocking and evented support 6 | class TCPServer < Socket 7 | extend Forwardable 8 | def_delegators :to_io, :listen, :sysaccept, :addr 9 | 10 | # @overload initialize(port) 11 | # Opens a tcp server on the given port. 12 | # @param port [Numeric] 13 | # 14 | # @overload initialize(hostname, port) 15 | # Opens a tcp server on the given port and interface. 16 | # @param hostname [String] 17 | # @param port [Numeric] 18 | # 19 | # @overload initialize(socket) 20 | # Wraps an already existing tcp server instance. 21 | # @param socket [::TCPServer] 22 | def initialize(*args) 23 | if args.first.kind_of? ::BasicSocket 24 | # socket 25 | socket = args.first 26 | fail ArgumentError, "wrong number of arguments (#{args.size} for 1)" if args.size != 1 27 | fail ArgumentError, "wrong kind of socket (#{socket.class} for TCPServer)" unless socket.kind_of? ::TCPServer 28 | super(socket) 29 | else 30 | super(::TCPServer.new(*args)) 31 | end 32 | end 33 | 34 | # @return [TCPSocket] 35 | def accept 36 | Celluloid::IO.wait_readable(to_io) 37 | accept_nonblock 38 | end 39 | 40 | # @return [TCPSocket] 41 | def accept_nonblock 42 | Celluloid::IO::TCPSocket.new(to_io.accept_nonblock) 43 | end 44 | 45 | # Convert a Ruby TCPServer into a Celluloid::IO::TCPServer 46 | # @deprecated Use .new instead. 47 | def self.from_ruby_server(ruby_server) 48 | warn "#from_ruby_server is deprecated please use .new instead" 49 | 50 | self.new(ruby_server) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/celluloid/io/dns_resolver_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Celluloid::IO::DNSResolver, library: :IO do 4 | context '#resolve' do 5 | it "resolves hostnames statically from hosts file without nameservers" do 6 | # /etc/resolv.conf doesn't exist on Mac OSX when no networking is 7 | # disabled, thus .nameservers would return nil 8 | expect(Celluloid::IO::DNSResolver).to receive(:nameservers).at_most(:once) { nil } 9 | resolver = Celluloid::IO::DNSResolver.new 10 | expect(resolver.resolve("localhost")).to eq(Resolv::IPv4.create("127.0.0.1")).or eq(Resolv::IPv6.create("::1")) 11 | end 12 | 13 | it "resolves hostnames" do 14 | resolver = Celluloid::IO::DNSResolver.new 15 | expect(resolver.resolve("localhost")).to eq(Resolv::IPv4.create("127.0.0.1")).or eq(Resolv::IPv6.create("::1")) 16 | end 17 | 18 | it "resolves domain names" do 19 | resolver = Celluloid::IO::DNSResolver.new 20 | nameservers = resolver.resolve("celluloid.io") 21 | expect(nameservers).to include Resolv::IPv4.create("104.28.21.100") 22 | expect(nameservers).to include Resolv::IPv4.create("104.28.20.100") 23 | end 24 | 25 | it "resolves CNAME responses" do 26 | resolver = Celluloid::IO::DNSResolver.new 27 | results = resolver.resolve("www.google.com") 28 | if results.is_a?(Array) 29 | results.all? { |i| expect(i).to be_an_instance_of(Resolv::IPv4) } 30 | else 31 | expect(results).to be_an_instance_of(Resolv::IPv4) 32 | end 33 | # www.yahoo.com will be resolved randomly whether multiple or 34 | # single entry. 35 | results = resolver.resolve("www.yahoo.com") 36 | if results.is_a?(Array) 37 | results.all? { |i| expect(i).to be_an_instance_of(Resolv::IPv4) } 38 | else 39 | expect(results).to be_an_instance_of(Resolv::IPv4) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/celluloid/io/udp_socket.rb: -------------------------------------------------------------------------------- 1 | module Celluloid 2 | module IO 3 | # UDPSockets with combined blocking and evented support 4 | class UDPSocket < Socket 5 | extend Forwardable 6 | def_delegators :to_io, :bind, :connect, :send, :recvfrom_nonblock 7 | 8 | # @overload initialize(address_family) 9 | # Opens a new udp socket using address_family. 10 | # @param address_family [Numeric] 11 | # 12 | # @overload initialize(socket) 13 | # Wraps an already existing udp socket. 14 | # @param socket [::UDPSocket] 15 | def initialize(*args) 16 | if args.first.kind_of? ::BasicSocket 17 | # socket 18 | socket = args.first 19 | fail ArgumentError, "wrong number of arguments (#{args.size} for 1)" if args.size != 1 20 | fail ArgumentError, "wrong kind of socket (#{socket.class} for UDPSocket)" unless socket.kind_of? ::UDPSocket 21 | super(socket) 22 | else 23 | super(::UDPSocket.new(*args)) 24 | end 25 | end 26 | 27 | # Wait until the socket is readable 28 | def wait_readable; Celluloid::IO.wait_readable(self); end 29 | 30 | # Receives up to maxlen bytes from socket. flags is zero or more of the 31 | # MSG_ options. The first element of the results, mesg, is the data 32 | # received. The second element, sender_addrinfo, contains 33 | # protocol-specific address information of the sender. 34 | def recvfrom(maxlen, flags = 0) 35 | begin 36 | socket = to_io 37 | if socket.respond_to? :recvfrom_nonblock 38 | socket.recvfrom_nonblock(maxlen, flags) 39 | else 40 | # FIXME: hax for JRuby 41 | socket.recvfrom(maxlen, flags) 42 | end 43 | rescue ::IO::WaitReadable 44 | wait_readable 45 | retry 46 | end 47 | end 48 | 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/celluloid/io/actor_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Celluloid::IO, library: :IO do 4 | it_behaves_like "a Celluloid Actor", Celluloid::IO do 5 | let(:example_port) { assign_port } 6 | 7 | context :timeouts do 8 | let :sleeping_actor_class do 9 | Class.new do 10 | include Celluloid::IO 11 | def initialize(addr, port) 12 | @server = Celluloid::IO::TCPServer.new(addr, port) 13 | async { @server.accept ; sleep 10 } 14 | end 15 | end 16 | end 17 | let :foo_actor_class do 18 | Class.new do 19 | include Celluloid::IO 20 | def initialize(addr, port) 21 | @sock = Celluloid::IO::TCPSocket.new(addr, port) 22 | end 23 | 24 | # returns true if the operation timedout 25 | def timedout_read(duration) 26 | begin 27 | timeout(duration) do 28 | @sock.wait_readable 29 | end 30 | rescue Celluloid::TaskTimeout 31 | return true 32 | end 33 | false 34 | end 35 | 36 | # returns true if it cannot write (socket is already registered) 37 | def failed_write 38 | begin 39 | @sock.wait_readable 40 | rescue ArgumentError # IO Selector Exception 41 | return true 42 | end 43 | false 44 | end 45 | end 46 | end 47 | 48 | it "frees up the socket when a timeout error occurs" do 49 | a1 = sleeping_actor_class.new(example_addr, example_port) 50 | a2 = foo_actor_class.new(example_addr, example_port) 51 | 52 | expect(a2.timedout_read(1)).to eq true # this ensures that the socket timeouted trying to read 53 | skip "not implemented" 54 | expect(a2.failed_write).to eq false # this ensures that the socket isn't usable anymore 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/support/examples/methods.rb: -------------------------------------------------------------------------------- 1 | def fixture_dir 2 | Pathname.new File.expand_path("../../../fixtures", __FILE__) 3 | end 4 | 5 | # Would use Addrinfo(addr,0) but the class is missing/unstable on RBX. 6 | def assign_port 7 | port = 12_000 + Random.rand(1024) 8 | attempts = 0 9 | begin 10 | socket = ::TCPServer.new(example_addr, port) 11 | rescue Errno::ECONNREFUSED, Errno::EADDRINUSE => ex 12 | raise ex.class.new("Tried #{attempts} times to assign port.") unless attempts < Specs::MAX_ATTEMPTS 13 | attempts += 1 14 | port += 1 15 | socket.close rescue nil 16 | sleep 0.126 17 | retry 18 | end 19 | return port 20 | ensure 21 | socket.close rescue nil 22 | end 23 | 24 | def example_addr 25 | "127.0.0.1" 26 | end 27 | 28 | def example_unix_sock 29 | "/tmp/cell_sock" 30 | end 31 | 32 | def within_io_actor(&block) 33 | actor = WrapperActor.new 34 | actor.wrap(&block) 35 | ensure 36 | actor.terminate if actor.alive? rescue nil 37 | end 38 | 39 | def with_tcp_server(port) 40 | server = Celluloid::IO::TCPServer.new(example_addr, port) 41 | begin 42 | yield server 43 | ensure 44 | server.close 45 | end 46 | end 47 | 48 | def with_unix_server 49 | server = Celluloid::IO::UNIXServer.open(example_unix_sock) 50 | begin 51 | yield server 52 | ensure 53 | server.close 54 | File.delete(example_unix_sock) 55 | end 56 | end 57 | 58 | def with_connected_sockets(port) 59 | with_tcp_server(port) do |server| 60 | client = Celluloid::IO::TCPSocket.new(example_addr, port) 61 | peer = server.accept 62 | 63 | begin 64 | yield peer, client 65 | ensure 66 | begin 67 | client.close 68 | peer.close 69 | rescue 70 | end 71 | end 72 | end 73 | end 74 | 75 | def with_connected_unix_sockets 76 | with_unix_server do |server| 77 | client = Celluloid::IO::UNIXSocket.new(example_unix_sock) 78 | peer = server.accept 79 | 80 | begin 81 | yield peer, client 82 | ensure 83 | begin 84 | client.close 85 | peer.close 86 | rescue 87 | end 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/celluloid/io.rb: -------------------------------------------------------------------------------- 1 | require "celluloid/io/version" 2 | 3 | require "celluloid" 4 | require "celluloid/io/dns_resolver" 5 | require "celluloid/io/mailbox" 6 | require "celluloid/io/reactor" 7 | require "celluloid/io/socket" 8 | require "celluloid/io/stream" 9 | 10 | require "celluloid/io/tcp_server" 11 | require "celluloid/io/tcp_socket" 12 | require "celluloid/io/udp_socket" 13 | require "celluloid/io/unix_server" 14 | require "celluloid/io/unix_socket" 15 | 16 | require "celluloid/io/ssl_server" 17 | require "celluloid/io/ssl_socket" 18 | 19 | module Celluloid 20 | # Actors with evented IO support 21 | module IO 22 | # Default size to read from or write to the stream for buffer operations 23 | BLOCK_SIZE = 1024 * 16 24 | 25 | def self.included(klass) 26 | klass.send :include, Celluloid 27 | klass.mailbox_class Celluloid::IO::Mailbox 28 | end 29 | 30 | def self.evented? 31 | actor = Thread.current[:celluloid_actor] 32 | actor && actor.mailbox.is_a?(Celluloid::IO::Mailbox) 33 | end 34 | 35 | def self.try_convert(src) 36 | ::IO.try_convert(src) 37 | end 38 | 39 | def self.copy_stream(src, dst, copy_length = nil, src_offset = nil) 40 | fail NotImplementedError, "length/offset not supported" if copy_length || src_offset 41 | 42 | src, dst = try_convert(src), try_convert(dst) 43 | 44 | # FIXME: this always goes through the reactor, and can block on file I/O 45 | while data = src.read(BLOCK_SIZE) 46 | dst << data 47 | end 48 | end 49 | 50 | def wait_readable(io) 51 | io = io.to_io 52 | if IO.evented? 53 | mailbox = Thread.current[:celluloid_mailbox] 54 | mailbox.reactor.wait_readable(io) 55 | else 56 | Kernel.select([io]) 57 | end 58 | nil 59 | end 60 | module_function :wait_readable 61 | 62 | def wait_writable(io) 63 | io = io.to_io 64 | if IO.evented? 65 | mailbox = Thread.current[:celluloid_mailbox] 66 | mailbox.reactor.wait_writable(io) 67 | else 68 | Kernel.select([], [io]) 69 | end 70 | nil 71 | end 72 | module_function :wait_writable 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/celluloid/io/reactor.rb: -------------------------------------------------------------------------------- 1 | require "nio" 2 | 3 | module Celluloid 4 | module IO 5 | # React to external I/O events. This is kinda sorta supposed to resemble the 6 | # Reactor design pattern. 7 | class Reactor 8 | extend Forwardable 9 | 10 | # Unblock the reactor (i.e. to signal it from another thread) 11 | def_delegator :@selector, :wakeup 12 | # Terminate the reactor 13 | def_delegator :@selector, :close, :shutdown 14 | 15 | def initialize 16 | @selector = NIO::Selector.new 17 | end 18 | 19 | # Wait for the given IO object to become readable 20 | def wait_readable(io) 21 | wait io, :r 22 | end 23 | 24 | # Wait for the given IO object to become writable 25 | def wait_writable(io) 26 | wait io, :w 27 | end 28 | 29 | # Wait for the given IO operation to complete 30 | def wait(io, set) 31 | # zomg ugly type conversion :( 32 | unless io.is_a?(::IO) || io.is_a?(OpenSSL::SSL::SSLSocket) 33 | if io.respond_to? :to_io 34 | io = io.to_io 35 | elsif ::IO.respond_to? :try_convert 36 | io = ::IO.try_convert(io) 37 | end 38 | 39 | fail TypeError, "can't convert #{io.class} into IO" unless io.is_a?(::IO) 40 | end 41 | 42 | monitor = @selector.register(io, set) 43 | monitor.value = Task.current 44 | 45 | begin 46 | Task.suspend :iowait 47 | ensure 48 | # In all cases we want to ensure that the monitor is closed once we 49 | # have woken up. However, in some cases, the monitor is already 50 | # invalid, e.g. in the case that we are terminating. We catch this 51 | # case explicitly. 52 | monitor.close unless monitor.closed? 53 | end 54 | end 55 | 56 | # Run the reactor, waiting for events or wakeup signal 57 | def run_once(timeout = nil) 58 | @selector.select(timeout) do |monitor| 59 | task = monitor.value 60 | 61 | if task.running? 62 | task.resume 63 | else 64 | Logger.warn("reactor attempted to resume a dead task") 65 | end 66 | end 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/celluloid/io/unix_server_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Celluloid::IO::UNIXServer, library: :IO do 4 | let(:example_port) { assign_port } 5 | 6 | describe "#accept" do 7 | let(:payload) { "ohai" } 8 | 9 | context "inside Celluloid::IO" do 10 | it "should be evented" do 11 | with_unix_server do |subject| 12 | expect(within_io_actor { Celluloid::IO.evented? }).to be_truthy 13 | end 14 | end 15 | 16 | it "accepts a connection and returns a Celluloid::IO::UNIXSocket" do 17 | pending if RUBY_PLATFORM == "java" 18 | with_unix_server do |subject| 19 | thread = Thread.new { UNIXSocket.new(example_unix_sock) } 20 | peer = within_io_actor { subject.accept } 21 | expect(peer).to be_a Celluloid::IO::UNIXSocket 22 | 23 | client = thread.value 24 | client.write payload 25 | expect(peer.read(payload.size)).to eq payload 26 | end 27 | end 28 | 29 | it "raises if server already up" do 30 | with_unix_server do |subject| 31 | within_io_actor do 32 | expect do 33 | Celluloid::IO::UNIXServer.open(example_unix_sock) 34 | end.to raise_error(Errno::EADDRINUSE) 35 | end 36 | end 37 | end 38 | 39 | context "outside Celluloid::IO" do 40 | it "should be blocking" do 41 | with_unix_server do |subject| 42 | expect(Celluloid::IO).not_to be_evented 43 | end 44 | end 45 | 46 | it "accepts a connection and returns a Celluloid::IO::UNIXSocket" do 47 | with_unix_server do |subject| 48 | thread = Thread.new { UNIXSocket.new(example_unix_sock) } 49 | peer = subject.accept 50 | expect(peer).to be_a Celluloid::IO::UNIXSocket 51 | 52 | client = thread.value 53 | client.write payload 54 | expect(peer.read(payload.size)).to eq payload 55 | end 56 | end 57 | 58 | it "raises if server already up" do 59 | with_unix_server do |subject| 60 | expect do 61 | Celluloid::IO::UNIXServer.open(example_unix_sock) 62 | end.to raise_error(Errno::EADDRINUSE) 63 | end 64 | end 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/celluloid/io/socket.rb: -------------------------------------------------------------------------------- 1 | module Celluloid 2 | module IO 3 | # Base class for all classes that wrap a ruby socket. 4 | # @abstract 5 | class Socket 6 | extend Forwardable 7 | 8 | def_delegators :@socket, :close, :close_read, :close_write, :closed? 9 | def_delegators :@socket, :read_nonblock, :write_nonblock 10 | def_delegators :@socket, :addr, :getsockopt, :setsockopt, :getsockname, :fcntl 11 | 12 | # @param socket [BasicSocket, OpenSSL::SSL::SSLSocket] 13 | def initialize(socket) 14 | case socket 15 | when ::BasicSocket, OpenSSL::SSL::SSLSocket 16 | @socket = socket 17 | else 18 | raise ArgumentError, "expected a socket, got #{socket.inspect}" 19 | end 20 | end 21 | 22 | # Returns the wrapped socket. 23 | # @return [BasicSocket, OpenSSL::SSL::SSLSocket] 24 | def to_io 25 | @socket 26 | end 27 | 28 | # Compatibility 29 | Constants = ::Socket::Constants 30 | include Constants 31 | 32 | # Celluloid::IO:Socket.new behaves like Socket.new for compatibility. 33 | # This is is not problematic since Celluloid::IO::Socket is abstract. 34 | # To instantiate a socket use one of its subclasses. 35 | def self.new(*args) 36 | if self == Celluloid::IO::Socket 37 | return ::Socket.new(*args) 38 | else 39 | super 40 | end 41 | end 42 | 43 | # Tries to convert the given ruby socket into a subclass of GenericSocket. 44 | # @param socket 45 | # @return [SSLSocket, TCPServer, TCPSocket, UDPSocket, UNIXServer, UNIXSocket] 46 | # @return [nil] if the socket can't be converted 47 | def self.try_convert(socket, convert_io = true) 48 | case socket 49 | when Celluloid::IO::Socket, Celluloid::IO::SSLServer 50 | socket 51 | when ::TCPServer 52 | TCPServer.new(socket) 53 | when ::TCPSocket 54 | TCPSocket.new(socket) 55 | when ::UDPSocket 56 | UDPSocket.new(socket) 57 | when ::UNIXServer 58 | UNIXServer.new(socket) 59 | when ::UNIXSocket 60 | UNIXSocket.new(socket) 61 | when OpenSSL::SSL::SSLServer 62 | SSLServer.new(socket.to_io, socket.instance_variable_get(:@ctx)) 63 | when OpenSSL::SSL::SSLSocket 64 | SSLSocket.new(socket) 65 | else 66 | if convert_io 67 | return try_convert(IO.try_convert(socket), false) 68 | end 69 | nil 70 | end 71 | end 72 | 73 | class << self 74 | extend Forwardable 75 | def_delegators '::Socket', *(::Socket.methods - self.methods - [:try_convert]) 76 | end 77 | 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | DisplayCopNames: true 3 | 4 | # 5 | # Lint 6 | # 7 | 8 | Lint/AssignmentInCondition: 9 | Enabled: false # TODO: enable 10 | 11 | Lint/EmptyWhen: 12 | Enabled: false 13 | 14 | Lint/Loop: 15 | Enabled: false # TODO: enable 16 | 17 | Lint/HandleExceptions: 18 | Enabled: false # TODO: enable 19 | 20 | Lint/NonLocalExitFromIterator: 21 | Enabled: false # TODO: enable 22 | 23 | Lint/RescueException: 24 | Enabled: false # TODO: explicitly mark cases where we want to rescue Exception 25 | 26 | Lint/ShadowedException: 27 | Enabled: false # TODO; enable 28 | 29 | Lint/UselessAssignment: 30 | Enabled: false # TODO: enable 31 | 32 | # 33 | # Metrics 34 | # 35 | 36 | Metrics/AbcSize: 37 | Enabled: false # TODO: enable 38 | 39 | Metrics/BlockLength: 40 | Exclude: 41 | - "celluloid.gemspec" 42 | - "spec/**/*" 43 | 44 | Metrics/BlockNesting: 45 | Enabled: false # TODO: enable 46 | 47 | Metrics/ClassLength: 48 | Max: 250 49 | 50 | Metrics/CyclomaticComplexity: 51 | Enabled: false # TODO: enable 52 | 53 | Metrics/LineLength: 54 | Max: 200 # TODO: decrease 55 | 56 | Metrics/MethodLength: 57 | Max: 100 58 | 59 | Metrics/ModuleLength: 60 | Max: 250 61 | 62 | Metrics/ParameterLists: 63 | Enabled: false # TODO: enable 64 | 65 | Metrics/PerceivedComplexity: 66 | Enabled: false # TODO: enable 67 | 68 | # 69 | # Style 70 | # 71 | 72 | Style/AccessorMethodName: 73 | Enabled: false # TODO: enable 74 | 75 | Style/CaseEquality: 76 | Enabled: false 77 | 78 | Style/ClassAndModuleChildren: 79 | Enabled: false 80 | 81 | Style/ClassVars: 82 | Enabled: false # TODO: enable 83 | 84 | Style/Documentation: 85 | Enabled: false # TODO: enable 86 | 87 | Style/DoubleNegation: 88 | Enabled: false # TODO: enable(?) 89 | 90 | Style/For: 91 | Enabled: false # TODO: enable 92 | 93 | Style/FrozenStringLiteralComment: 94 | Enabled: false # TODO: enable 95 | 96 | Style/GuardClause: 97 | Enabled: false # TODO: enable 98 | 99 | Style/IfInsideElse: 100 | Enabled: false # TODO: enable 101 | 102 | Style/MethodMissing: 103 | Enabled: false # TODO: enable 104 | 105 | Style/ModuleFunction: 106 | Enabled: false 107 | 108 | Style/NumericPredicate: 109 | Enabled: false 110 | 111 | Style/RegexpLiteral: 112 | Enabled: false # TODO: enable 113 | 114 | Style/RescueModifier: 115 | Enabled: false # TODO: enable 116 | 117 | Style/SafeNavigation: 118 | Enabled: false 119 | 120 | Style/Semicolon: 121 | Enabled: false 122 | 123 | Style/SingleLineBlockParams: 124 | Enabled: false 125 | 126 | Style/StringLiterals: 127 | EnforcedStyle: double_quotes 128 | 129 | Style/StructInheritance: 130 | Enabled: false # TODO: enable 131 | 132 | Style/TernaryParentheses: 133 | Enabled: false 134 | -------------------------------------------------------------------------------- /spec/celluloid/io/tcp_server_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Celluloid::IO::TCPServer, library: :IO do 4 | context "#accept" do 5 | let(:payload) { "ohai" } 6 | let(:example_port) { assign_port } 7 | 8 | it "can be initialized without a host" do 9 | expect { Celluloid::IO::TCPServer.new(2000).close }.to_not raise_error 10 | end 11 | 12 | context "inside Celluloid::IO" do 13 | it "should be evented" do 14 | with_tcp_server(example_port) do |subject| 15 | expect(within_io_actor { Celluloid::IO.evented? }).to be_truthy 16 | end 17 | end 18 | 19 | it "accepts a connection and returns a Celluloid::IO::TCPSocket" do 20 | with_tcp_server(example_port) do |subject| 21 | thread = Thread.new { TCPSocket.new(example_addr, example_port) } 22 | peer = within_io_actor { subject.accept } 23 | expect(peer).to be_a Celluloid::IO::TCPSocket 24 | 25 | client = thread.value 26 | client.write payload 27 | expect(peer.read(payload.size)).to eq payload 28 | end 29 | end 30 | 31 | it "sends information to the client later" do 32 | class LaterActor 33 | include Celluloid::IO 34 | 35 | def send_later(socket) 36 | peer = socket.accept 37 | after(0.4) { peer.write "1" } 38 | after(0.4) { peer.write "2" } 39 | peer 40 | end 41 | end 42 | with_tcp_server(example_port) do |subject| 43 | thread = Thread.new { TCPSocket.new(example_addr, example_port) } 44 | actor = LaterActor.new 45 | begin 46 | peer = actor.send_later(subject) 47 | client = thread.value 48 | client.write payload 49 | expect(peer.read(payload.size)).to eq payload # confirm the client read 50 | Timeout::timeout(1) { expect(client.read(1)).to eq "1" } 51 | Timeout::timeout(2) { expect(client.read(1)).to eq "2" } 52 | ensure 53 | actor.terminate if actor.alive? 54 | end 55 | end 56 | end 57 | 58 | context "outside Celluloid::IO" do 59 | it "should be blocking" do 60 | with_tcp_server(example_port) do |subject| 61 | expect(Celluloid::IO).not_to be_evented 62 | end 63 | end 64 | 65 | it "accepts a connection and returns a Celluloid::IO::TCPSocket" do 66 | with_tcp_server(example_port) do |subject| 67 | thread = Thread.new { TCPSocket.new(example_addr, example_port) } 68 | peer = subject.accept 69 | expect(peer).to be_a Celluloid::IO::TCPSocket 70 | 71 | client = thread.value 72 | client.write payload 73 | expect(peer.read(payload.size)).to eq payload 74 | end 75 | end 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/celluloid/io/dns_resolver.rb: -------------------------------------------------------------------------------- 1 | require "ipaddr" 2 | require "resolv" 3 | 4 | module Celluloid 5 | module IO 6 | # Asynchronous DNS resolver using Celluloid::IO::UDPSocket 7 | class DNSResolver 8 | # Maximum UDP packet we'll accept 9 | MAX_PACKET_SIZE = 512 10 | DNS_PORT = 53 11 | 12 | @mutex = Mutex.new 13 | @identifier = 1 14 | 15 | def self.generate_id 16 | @mutex.synchronize { @identifier = (@identifier + 1) & 0xFFFF } 17 | end 18 | 19 | def self.nameservers 20 | Resolv::DNS::Config.default_config_hash[:nameserver] 21 | end 22 | 23 | def initialize 24 | # early return for edge case when there are no nameservers configured 25 | # but we still want to be able to static lookups using #resolve_hostname 26 | @nameservers = self.class.nameservers or return 27 | 28 | @server = IPAddr.new(@nameservers.sample) 29 | 30 | # The non-blocking secret sauce is here, as this is actually a 31 | # Celluloid::IO::UDPSocket 32 | @socket = UDPSocket.new(@server.family) 33 | end 34 | 35 | def resolve(hostname) 36 | if host = resolve_hostname(hostname) 37 | unless ip_address = resolve_host(host) 38 | fail Resolv::ResolvError, "invalid entry in hosts file: #{host}" 39 | end 40 | return ip_address 41 | end 42 | 43 | query = build_query(hostname) 44 | @socket.send query.encode, 0, @server.to_s, DNS_PORT 45 | data, _ = @socket.recvfrom(MAX_PACKET_SIZE) 46 | response = Resolv::DNS::Message.decode(data) 47 | 48 | addrs = [] 49 | # The answer might include IN::CNAME entries so filters them out 50 | # to include IN::A & IN::AAAA entries only. 51 | response.each_answer { |name, ttl, value| addrs << value.address if value.respond_to?(:address) } 52 | 53 | return if addrs.empty? 54 | return addrs.first if addrs.size == 1 55 | addrs 56 | end 57 | 58 | private 59 | 60 | def resolve_hostname(hostname) 61 | # Resolv::Hosts#getaddresses pushes onto a stack 62 | # so since we want the first occurance, simply 63 | # pop off the stack. 64 | resolv.getaddresses(hostname).pop 65 | rescue 66 | end 67 | 68 | def resolv 69 | @resolv ||= Resolv::Hosts.new 70 | end 71 | 72 | def build_query(hostname) 73 | Resolv::DNS::Message.new.tap do |query| 74 | query.id = self.class.generate_id 75 | query.rd = 1 76 | query.add_question hostname, Resolv::DNS::Resource::IN::A 77 | end 78 | end 79 | 80 | def resolve_host(host) 81 | resolve_ip(Resolv::IPv4, host) || get_address(host) || resolve_ip(Resolv::IPv6, host) 82 | end 83 | 84 | def resolve_ip(klass, host) 85 | klass.create(host) 86 | rescue ArgumentError 87 | end 88 | 89 | private 90 | 91 | def get_address(host) 92 | Resolv::Hosts.new(host).getaddress 93 | rescue 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /spec/celluloid/io/ssl_server_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Celluloid::IO::SSLServer, library: :IO do 4 | let(:client_cert) { OpenSSL::X509::Certificate.new fixture_dir.join("client.crt").read } 5 | let(:client_key) { OpenSSL::PKey::RSA.new fixture_dir.join("client.key").read } 6 | let(:client_context) do 7 | OpenSSL::SSL::SSLContext.new.tap do |context| 8 | context.cert = client_cert 9 | context.key = client_key 10 | end 11 | end 12 | 13 | let(:example_port) { assign_port } 14 | let(:server_cert) { OpenSSL::X509::Certificate.new fixture_dir.join("server.crt").read } 15 | let(:server_key) { OpenSSL::PKey::RSA.new fixture_dir.join("server.key").read } 16 | let(:server_context) do 17 | OpenSSL::SSL::SSLContext.new.tap do |context| 18 | context.cert = server_cert 19 | context.key = server_key 20 | end 21 | end 22 | 23 | describe "#accept" do 24 | let(:payload) { "ohai" } 25 | 26 | context "inside Celluloid::IO" do 27 | it "should be evented" do 28 | with_ssl_server(example_port) do |subject| 29 | expect(within_io_actor { Celluloid::IO.evented? }).to be_truthy 30 | end 31 | end 32 | 33 | it "accepts a connection and returns a Celluloid::IO::SSLSocket" do 34 | with_ssl_server(example_port) do |subject| 35 | thread = Thread.new do 36 | raw = TCPSocket.new(example_addr, example_port) 37 | OpenSSL::SSL::SSLSocket.new(raw, client_context).connect 38 | end 39 | peer = within_io_actor { subject.accept } 40 | expect(peer).to be_a Celluloid::IO::SSLSocket 41 | 42 | client = thread.value 43 | client.write payload 44 | expect(peer.read(payload.size)).to eq payload 45 | end 46 | end 47 | end 48 | 49 | context "outside Celluloid::IO" do 50 | it "should be blocking" do 51 | with_ssl_server(example_port) do |subject| 52 | expect(Celluloid::IO).not_to be_evented 53 | end 54 | end 55 | 56 | it "accepts a connection and returns a Celluloid::IO::SSLSocket" do 57 | with_ssl_server(example_port) do |subject| 58 | thread = Thread.new do 59 | raw = TCPSocket.new(example_addr, example_port) 60 | OpenSSL::SSL::SSLSocket.new(raw, client_context).connect 61 | end 62 | peer = subject.accept 63 | expect(peer).to be_a Celluloid::IO::SSLSocket 64 | 65 | client = thread.value 66 | client.write payload 67 | expect(peer.read(payload.size)).to eq payload 68 | end 69 | end 70 | end 71 | end 72 | 73 | describe "#initialize" do 74 | it "should auto-wrap a raw ::TCPServer" do 75 | raw_server = ::TCPServer.new(example_addr, example_port) 76 | with_ssl_server(example_port, raw_server) do |ssl_server| 77 | expect(ssl_server.tcp_server.class).to eq(Celluloid::IO::TCPServer) 78 | end 79 | end 80 | end 81 | 82 | def with_ssl_server(port, raw_server = nil) 83 | raw_server ||= Celluloid::IO::TCPServer.new(example_addr, port) 84 | server = Celluloid::IO::SSLServer.new(raw_server, server_context) 85 | begin 86 | yield server 87 | ensure 88 | server.close 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 0.17.3 (2016-01-18) 2 | ----- 3 | * [#163](https://github.com/celluloid/celluloid-io/pull/163) 4 | Support Ruby 2.3.0. 5 | 6 | * [#162](https://github.com/celluloid/celluloid-io/pull/162) 7 | Fix broken specs. 8 | 9 | * [#160](https://github.com/celluloid/celluloid-io/pull/160) 10 | Use a common super class for all socket wrappers. 11 | ([@hannesg]) 12 | 13 | * [#159](https://github.com/celluloid/celluloid-io/pull/159) 14 | UNIXSocket: don't delegate #readline and #puts. 15 | ([@hannesg]) 16 | 17 | * [#158](https://github.com/celluloid/celluloid-io/pull/158) 18 | Use unix sockets in unix spec instead of tcp sockets. 19 | ([@hannesg]) 20 | 21 | * [#157](https://github.com/celluloid/celluloid-io/pull/157) 22 | Stream#close is not called in subclasses. 23 | ([@hannesg]) 24 | 25 | * [#155](https://github.com/celluloid/celluloid-io/pull/155) 26 | Only close Selector it not already closed. 27 | 28 | * [#98](https://github.com/celluloid/celluloid-io/pull/98) 29 | Added spec for writing later to a socket within a request/response cycle 30 | using the timer primitives. 31 | ([@TiagoCardoso1983]) 32 | 33 | 0.17.2 (2015-09-30) 34 | ----- 35 | * Revamped test suite, using shared RSpec configuration layer provided by Celluloid itself. 36 | * Updated gem dependencies provided by Celluloid::Sync... extraneous gems removed, or marked as development dependencies. 37 | 38 | 0.17.1 (2015-08-24) 39 | ----- 40 | * Minor bug fixes. Synchronize gem dependencies. 41 | 42 | 0.17.0 (2015-08-07) 43 | ----- 44 | * Compatibility with Celluloid 0.17.0+ 45 | * Adjust class name for Celluloid::Mailbox::Evented, per 0.17.0 of Celluloid. 46 | 47 | 0.16.2 (2015-01-30) 48 | ----- 49 | * More TCPSocket compatibility fixes 50 | * Ensure monitors are closed when tasks resume 51 | * Fix Errno::EAGAIN handling in Stream#syswrite 52 | 53 | 0.16.1 (2014-10-08) 54 | ----- 55 | * Revert read/write interest patch as it caused file descriptor leaks 56 | 57 | 0.16.0 (2014-09-04) 58 | ----- 59 | * Fix bug handling simultaneous read/write interests 60 | * Use Resolv::DNS::Config to obtain nameservers 61 | * Celluloid::IO.copy_stream support (uses a background thread) 62 | 63 | 0.15.0 (2013-09-04) 64 | ----- 65 | * Improved DNS resolver with less NIH and more Ruby stdlib goodness 66 | * Better match Ruby stdlib TCPServer API 67 | * Add missing #send and #recv on Celluloid::IO::TCPSocket 68 | * Add missing #setsockopt method on Celluloid::IO::TCPServer 69 | * Add missing #peeraddr method on Celluloid::IO::SSLSocket 70 | 71 | 0.14.0 (2013-05-07) 72 | ----- 73 | * Add `close_read`/`close_write` delegates for rack-hijack support 74 | * Depend on EventedMailbox from core 75 | 76 | 0.13.1 77 | ----- 78 | * Remove overhead for `wait_readable`/`wait_writable` 79 | 80 | 0.13.0 81 | ----- 82 | * Support for many, many more IO methods, particularly line-oriented 83 | methods like #gets, #readline, and #readlines 84 | * Initial SSL support via Celluloid::IO::SSLSocket and 85 | Celluloid::IO::SSLServer 86 | * Concurrent writes between tasks of the same actor are now coordinated 87 | using Celluloid::Conditions instead of signals 88 | * Celluloid 0.13 compatibility fixes 89 | 90 | 0.12.0 91 | ----- 92 | * Tracking release for Celluloid 0.12.0 93 | 94 | 0.11.0 95 | ----- 96 | * "Unofficial" SSL support (via nio4r 0.4.0) 97 | 98 | 0.10.0 99 | ----- 100 | * Read/write operations are now atomic across tasks 101 | * True non-blocking connect support 102 | * Non-blocking DNS resolution support 103 | 104 | 0.9.0 105 | ----- 106 | * TCPServer, TCPSocket, and UDPSocket classes in Celluloid::IO namespace 107 | with both evented and blocking I/O support 108 | * Celluloid::IO::Mailbox.new now takes a single parameter to specify an 109 | alternative reactor (e.g. Celluloid::ZMQ::Reactor) 110 | 111 | 0.8.0 112 | ----- 113 | * Switch to nio4r-based reactor 114 | * Compatibility with Celluloid 0.8.0 API changes 115 | 116 | 0.7.0 117 | ----- 118 | * Initial release forked from Celluloid 119 | 120 | [@TiagoCardoso1983]: https://github.com/TiagoCardoso1983 121 | [@hannesg]: https://github.com/hannesg 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Celluloid::IO](https://github.com/celluloid/celluloid-io/raw/master/logo.png) 2 | ================ 3 | [![Gem Version](https://badge.fury.io/rb/celluloid-io.svg)](http://rubygems.org/gems/celluloid-io) 4 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/celluloid/celluloid-io/blob/master/LICENSE.txt) 5 | [![Build Status](https://secure.travis-ci.org/celluloid/celluloid-io.svg?branch=master)](http://travis-ci.org/celluloid/celluloid-io) 6 | [![Maintained: no](https://img.shields.io/maintenance/no/2016.svg)](https://github.com/celluloid/celluloid/issues/779) 7 | 8 | You don't have to choose between threaded and evented IO! Celluloid::IO 9 | provides an event-driven IO system for building fast, scalable network 10 | applications that integrates directly with the 11 | [Celluloid actor library](https://github.com/celluloid/celluloid), making it 12 | easy to combine both threaded and evented concepts. Celluloid::IO is ideal for 13 | servers which handle large numbers of mostly-idle connections, such as Websocket 14 | servers or chat/messaging systems. 15 | 16 | Celluloid::IO provides a different class of actor: one that's slightly slower 17 | and heavier than standard Celluloid actors, but one which contains a 18 | high-performance reactor just like EventMachine or Cool.io. This means 19 | Celluloid::IO actors have the power of both Celluloid actors and evented 20 | I/O loops. Unlike certain other evented I/O systems which limit you to a 21 | single event loop per process, Celluloid::IO lets you make as many actors as 22 | you want, system resources permitting. 23 | 24 | Rather than callbacks, Celluloid::IO exposes a synchronous API built on duck 25 | types of Ruby's own IO classes, such as TCPServer and TCPSocket. These classes 26 | work identically to their core Ruby counterparts, but in the scope of 27 | Celluloid::IO actors provide "evented" performance. Since they're drop-in 28 | replacements for the standard classes, there's no need to rewrite every 29 | library just to take advantage of Celluloid::IO's event loop and you can 30 | freely switch between evented and blocking IO even over the lifetime of a 31 | single connection. 32 | 33 | Celluloid::IO uses the [nio4r gem](https://github.com/celluloid/nio4r) 34 | to monitor IO objects, which provides cross-platform and cross-Ruby 35 | implementation access to high-performance system calls such as epoll 36 | and kqueue. 37 | 38 | Like Celluloid::IO? [Join the Celluloid Google Group](http://groups.google.com/group/celluloid-ruby) 39 | 40 | Documentation 41 | ------------- 42 | 43 | [Please see the Celluloid::IO Wiki](https://github.com/celluloid/celluloid-io/wiki) 44 | for more detailed documentation and usage notes. 45 | 46 | [YARD documentation](http://rubydoc.info/github/celluloid/celluloid-io/frames) 47 | is also available 48 | 49 | Installation 50 | ------------ 51 | 52 | Add this line to your application's Gemfile: 53 | 54 | gem 'celluloid-io' 55 | 56 | And then execute: 57 | 58 | $ bundle 59 | 60 | Or install it yourself as: 61 | 62 | $ gem install celluloid-io 63 | 64 | Inside of your Ruby program, require Celluloid::IO with: 65 | 66 | require 'celluloid/io' 67 | 68 | Supported Platforms 69 | ------------------- 70 | 71 | `Celluloid::IO` works on all Ruby (MRI) versions between `1.9.3` & `2.3.1`, 72 | JRuby's `1.6.*`, `1.7.*` and `9.*` series, and Rubinius' `2.*` and `3.*` series. 73 | 74 | JRuby or Rubinius are the preferred platforms as they support true thread-level 75 | parallelism when executing Ruby code, whereas MRI/YARV is constrained by a global 76 | interpreter lock (GIL) and can only execute one thread at a time. 77 | 78 | Celluloid::IO requires Ruby 1.9 mode on all interpreters, at minimum. 79 | 80 | Contributing to Celluloid::IO 81 | ----------------------------- 82 | 83 | * Fork this repository on github 84 | * Make your changes and send me a pull request 85 | * If I like them I'll merge them 86 | * If I've accepted a patch, feel free to ask for a commit bit! 87 | 88 | License 89 | ------- 90 | 91 | Copyright (c) 2011-2016 Tony Arcieri. Distributed under the MIT License. See 92 | LICENSE.txt for further details. 93 | 94 | Contains code originally from the RubySpec project also under the MIT License. 95 | Copyright (c) 2008 Engine Yard, Inc. All rights reserved. 96 | 97 | Contains code originally from the 'OpenSSL for Ruby 2' project released under 98 | the Ruby license. Copyright (C) 2001 GOTOU YUUZOU. All rights reserved. 99 | -------------------------------------------------------------------------------- /spec/celluloid/io/socket_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Celluloid::IO::Socket, library: :IO do 4 | let(:logger) { Specs::FakeLogger.current } 5 | let(:example_port) { assign_port } 6 | 7 | context '.try_convert' do 8 | 9 | subject{ described_class.try_convert(socket) } 10 | 11 | after(:each) do 12 | if subject.respond_to? :close 13 | subject.close 14 | else 15 | socket.close if socket.respond_to? :close 16 | end 17 | end 18 | 19 | context 'with a Celluloid Socket' do 20 | let(:socket){ Celluloid::IO::UDPSocket.new } 21 | 22 | it 'returns given socket' do 23 | expect(subject).to be socket 24 | end 25 | end 26 | 27 | context 'with a ::TCPServer' do 28 | let(:socket){ ::TCPServer.new(example_port) } 29 | 30 | it 'creates a Celluloid::IO::TCPServer' do 31 | expect(subject).to be_a Celluloid::IO::TCPServer 32 | end 33 | end 34 | 35 | context 'with a ::TCPSocket' do 36 | let!(:server){ 37 | ::TCPServer.new example_addr, example_port 38 | } 39 | after(:each){ 40 | server.close 41 | } 42 | 43 | let(:socket){ 44 | ::TCPSocket.new example_addr, example_port 45 | } 46 | 47 | it 'creates a Celluloid::IO::TCPSocket' do 48 | expect(subject).to be_a Celluloid::IO::TCPSocket 49 | end 50 | end 51 | 52 | context 'with a ::UDPSocket' do 53 | let(:socket){ ::UDPSocket.new } 54 | 55 | it 'creates a Celluloid::IO::UDPServer' do 56 | expect(subject).to be_a Celluloid::IO::UDPSocket 57 | end 58 | end 59 | 60 | context 'with a ::UNIXServer' do 61 | let(:socket){ ::UNIXServer.new(example_unix_sock) } 62 | 63 | it 'creates a Celluloid::IO::UNIXServer' do 64 | expect(subject).to be_a Celluloid::IO::UNIXServer 65 | end 66 | end 67 | 68 | context 'with a ::UNIXSocket' do 69 | let!(:server){ 70 | ::UNIXServer.new(example_unix_sock) 71 | } 72 | after(:each){ 73 | server.close 74 | } 75 | 76 | let(:socket){ 77 | ::UNIXSocket.new example_unix_sock 78 | } 79 | 80 | it 'creates a Celluloid::IO::UNIXSocket' do 81 | expect(subject).to be_a Celluloid::IO::UNIXSocket 82 | end 83 | end 84 | 85 | context 'with an OpenSSL::SSL::SSLServer' do 86 | let(:socket){ 87 | OpenSSL::SSL::SSLServer.new(::TCPServer.new(example_addr, example_port), OpenSSL::SSL::SSLContext.new) 88 | } 89 | 90 | it 'creates a Celluloid::IO::SSLServer' do 91 | expect(subject).to be_a Celluloid::IO::SSLServer 92 | end 93 | end 94 | 95 | context 'with an OpenSSL::SSL::SSLSocket' do 96 | let!(:server){ 97 | OpenSSL::SSL::SSLServer.new(::TCPServer.new(example_addr, example_port), OpenSSL::SSL::SSLContext.new) 98 | } 99 | after(:each){ 100 | server.close 101 | } 102 | 103 | let(:socket){ 104 | OpenSSL::SSL::SSLSocket.new(::TCPSocket.new(example_addr, example_port)) 105 | } 106 | 107 | it 'creates a Celluloid::IO::SSLSocket' do 108 | expect(subject).to be_a Celluloid::IO::SSLSocket 109 | end 110 | end 111 | 112 | context 'with an object responding to #to_io' do 113 | let(:real){ 114 | ::UDPSocket.new 115 | } 116 | 117 | let(:socket){ 118 | proxy = double(:socket) 119 | allow(proxy).to receive(:to_io){ real } 120 | allow(proxy).to receive(:close){ real.close } 121 | proxy 122 | } 123 | 124 | it 'creates a celluloid socket' do 125 | expect(subject).to be_a described_class 126 | end 127 | 128 | it 'uses the returned IO' do 129 | expect(subject.to_io).to be socket.to_io 130 | end 131 | end 132 | 133 | context 'with a simple object' do 134 | let(:socket){ Object.new } 135 | 136 | it 'returns nil' do 137 | expect(subject).to be_nil 138 | end 139 | end 140 | end 141 | 142 | context 'compatibility with ::Socket' do 143 | 144 | context '.new' do 145 | it "creates basic sockets" do 146 | socket = Celluloid::IO::Socket.new(Celluloid::IO::Socket::AF_INET, Celluloid::IO::Socket::SOCK_STREAM, 0) 147 | expect(socket).to be_a ::Socket 148 | socket.close 149 | end 150 | end 151 | 152 | context '.pair' do 153 | it "creates basic sockets" do 154 | a,b = Celluloid::IO::Socket.pair( Celluloid::IO::Socket::AF_UNIX, Celluloid::IO::Socket::SOCK_DGRAM, 0) 155 | expect(a).to be_a ::Socket 156 | expect(b).to be_a ::Socket 157 | a.close 158 | b.close 159 | end 160 | end 161 | 162 | context '.for_fd' do 163 | it "creates basic sockets" do 164 | socket = Celluloid::IO::Socket.new(Celluloid::IO::Socket::AF_INET, Celluloid::IO::Socket::SOCK_STREAM, 0) 165 | copy = Celluloid::IO::Socket.for_fd(socket.fileno) 166 | expect(copy).to be_a ::Socket 167 | copy.close 168 | end 169 | end 170 | 171 | end 172 | 173 | end 174 | -------------------------------------------------------------------------------- /lib/celluloid/io/tcp_socket.rb: -------------------------------------------------------------------------------- 1 | require "socket" 2 | require "resolv" 3 | 4 | module Celluloid 5 | module IO 6 | # TCPSocket with combined blocking and evented support 7 | class TCPSocket < Stream 8 | extend Forwardable 9 | 10 | def_delegators :to_io, :peeraddr 11 | 12 | # Open a TCP socket, yielding it to the given block and closing it 13 | # automatically when done (if a block is given) 14 | def self.open(*args, &_block) 15 | sock = new(*args) 16 | return sock unless block_given? 17 | 18 | begin 19 | yield(sock) 20 | ensure 21 | sock.close 22 | end 23 | end 24 | 25 | # Convert a Ruby TCPSocket into a Celluloid::IO::TCPSocket 26 | # DEPRECATED: to be removed in a future release 27 | # @deprecated Use {Celluloid::IO::TCPSocket#new} instead. 28 | def self.from_ruby_socket(ruby_socket) 29 | new(ruby_socket) 30 | end 31 | 32 | # @overload initialize(remote_host, remote_port = nil, local_host = nil, local_port = nil) 33 | # Opens a TCP connection to remote_host on remote_port. If local_host 34 | # and local_port are specified, then those parameters are used on the 35 | # local end to establish the connection. 36 | # @param remote_host [String, Resolv::IPv4, Resolv::IPv6] 37 | # @param remote_port [Numeric] 38 | # @param local_host [String] 39 | # @param local_port [Numeric] 40 | # 41 | # @overload initialize(socket) 42 | # Wraps an already existing tcp socket. 43 | # @param socket [::TCPSocket] 44 | # 45 | def initialize(*args) 46 | if args.first.kind_of? ::BasicSocket 47 | # socket 48 | socket = args.first 49 | fail ArgumentError, "wrong number of arguments (#{args.size} for 1)" if args.size != 1 50 | fail ArgumentError, "wrong kind of socket (#{socket.class} for TCPSocket)" unless socket.kind_of? ::TCPSocket 51 | super(socket) 52 | else 53 | super(create_socket(*args)) 54 | end 55 | end 56 | 57 | # Receives a message 58 | def recv(maxlen, flags = nil) 59 | fail NotImplementedError, "flags not supported" if flags && !flags.zero? 60 | readpartial(maxlen) 61 | end 62 | 63 | # Send a message 64 | def send(msg, flags, dest_sockaddr = nil) 65 | fail NotImplementedError, "dest_sockaddr not supported" if dest_sockaddr 66 | fail NotImplementedError, "flags not supported" unless flags.zero? 67 | write(msg) 68 | end 69 | 70 | # @return [Resolv::IPv4, Resolv::IPv6] 71 | def addr 72 | socket = to_io 73 | ra = socket.remote_address 74 | if ra.ipv4? 75 | return Resolv::IPv4.create(ra.ip_address) 76 | elsif ra.ipv6? 77 | return Resolv::IPv6.create(ra.ip_address) 78 | else 79 | raise ArgumentError, "not an ip socket: #{socket.inspect}" 80 | end 81 | end 82 | private 83 | 84 | def create_socket(remote_host, remote_port = nil, local_host = nil, local_port = nil) 85 | # Is it an IPv4 address? 86 | begin 87 | addr = Resolv::IPv4.create(remote_host) 88 | rescue ArgumentError 89 | end 90 | 91 | # Guess it's not IPv4! Is it IPv6? 92 | unless addr 93 | begin 94 | addr = Resolv::IPv6.create(remote_host) 95 | rescue ArgumentError 96 | end 97 | end 98 | 99 | # Guess it's not an IP address, so let's try DNS 100 | unless addr 101 | addrs = Array(DNSResolver.new.resolve(remote_host)) 102 | fail Resolv::ResolvError, "DNS result has no information for #{remote_host}" if addrs.empty? 103 | 104 | # Pseudorandom round-robin DNS support :/ 105 | addr = addrs[rand(addrs.size)] 106 | end 107 | 108 | case addr 109 | when Resolv::IPv4 110 | family = Socket::AF_INET 111 | when Resolv::IPv6 112 | family = Socket::AF_INET6 113 | else fail ArgumentError, "unsupported address class: #{addr.class}" 114 | end 115 | 116 | socket = Socket.new(family, Socket::SOCK_STREAM, 0) 117 | socket.bind Addrinfo.tcp(local_host, local_port) if local_host 118 | 119 | begin 120 | socket.connect_nonblock Socket.sockaddr_in(remote_port, addr.to_s) 121 | rescue Errno::EINPROGRESS, Errno::EALREADY 122 | # JRuby raises EINPROGRESS, MRI raises EALREADY 123 | Celluloid::IO.wait_writable(socket) 124 | 125 | # HAX: for some reason we need to finish_connect ourselves on JRuby 126 | # This logic is unnecessary but JRuby still throws Errno::EINPROGRESS 127 | # if we retry the non-blocking connect instead of just finishing it 128 | retry unless RUBY_PLATFORM == "java" && socket.to_channel.finish_connect 129 | rescue Errno::EISCONN 130 | # We're now connected! Yay exceptions for flow control 131 | # NOTE: This is the approach the Ruby stdlib docs suggest ;_; 132 | end 133 | 134 | return socket 135 | end 136 | end 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /spec/celluloid/io/unix_socket_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Celluloid::IO::UNIXSocket, library: :IO do 4 | if RUBY_PLATFORM == "java" 5 | before(:each) do 6 | pending "jRuby support" 7 | fail "Avoid potential deadlock under jRuby" 8 | end 9 | end 10 | 11 | let(:payload) { "ohai" } 12 | let(:example_port) { assign_port } 13 | let(:logger) { Specs::FakeLogger.current } 14 | 15 | context "inside Celluloid::IO" do 16 | it "connects to UNIX servers" do 17 | server = ::UNIXServer.open example_unix_sock 18 | thread = Thread.new { server.accept } 19 | socket = within_io_actor { Celluloid::IO::UNIXSocket.open example_unix_sock } 20 | peer = thread.value 21 | 22 | peer << payload 23 | expect(within_io_actor { socket.read(payload.size) }).to eq payload 24 | 25 | server.close 26 | socket.close 27 | peer.close 28 | File.delete(example_unix_sock) 29 | end 30 | 31 | it "should be evented" do 32 | with_connected_unix_sockets do |subject| 33 | expect(within_io_actor { Celluloid::IO.evented? }).to be_truthy 34 | end 35 | end 36 | 37 | it "read complete payload when nil size is given to #read" do 38 | with_connected_unix_sockets do |subject, peer| 39 | peer << payload 40 | expect(within_io_actor { subject.read(nil) }).to eq payload 41 | end 42 | end 43 | 44 | it "read complete payload when no size is given to #read" do 45 | with_connected_unix_sockets do |subject, peer| 46 | peer << payload 47 | expect(within_io_actor { subject.read }).to eq payload 48 | end 49 | end 50 | 51 | it "reads data" do 52 | with_connected_unix_sockets do |subject, peer| 53 | peer << payload 54 | expect(within_io_actor { subject.read(payload.size) }).to eq payload 55 | end 56 | end 57 | 58 | it "reads data in binary encoding" do 59 | with_connected_unix_sockets do |subject, peer| 60 | peer << payload 61 | expect(within_io_actor { subject.read(payload.size).encoding }).to eq Encoding::BINARY 62 | end 63 | end 64 | 65 | it "reads partial data" do 66 | with_connected_unix_sockets do |subject, peer| 67 | peer << payload * 2 68 | expect(within_io_actor { subject.readpartial(payload.size) }).to eq payload 69 | end 70 | end 71 | 72 | it "reads partial data in binary encoding" do 73 | with_connected_unix_sockets do |subject, peer| 74 | peer << payload * 2 75 | expect(within_io_actor { subject.readpartial(payload.size).encoding }).to eq Encoding::BINARY 76 | end 77 | end 78 | 79 | it "writes data" do 80 | with_connected_unix_sockets do |subject, peer| 81 | within_io_actor { subject << payload } 82 | expect(peer.read(payload.size)).to eq payload 83 | end 84 | end 85 | 86 | it "raises Errno::ENOENT when the connection is refused" do 87 | allow(logger).to receive(:crash).with("Actor crashed!", Errno::ENOENT) 88 | expect do 89 | within_io_actor { Celluloid::IO::UNIXSocket.open(example_unix_sock) } 90 | end.to raise_error(Errno::ENOENT) 91 | end 92 | 93 | it "raises EOFError when partial reading from a closed socket" do 94 | allow(logger).to receive(:crash).with("Actor crashed!", EOFError) 95 | with_connected_unix_sockets do |subject, peer| 96 | peer.close 97 | expect do 98 | within_io_actor { subject.readpartial(payload.size) } 99 | end.to raise_error(EOFError) 100 | end 101 | end 102 | 103 | context "eof?" do 104 | it "blocks actor then returns by close" do 105 | with_connected_unix_sockets do |subject, peer| 106 | started_at = Time.now 107 | Thread.new { sleep 0.5; peer.close; } 108 | within_io_actor { subject.eof? } 109 | expect(Time.now - started_at).to be > 0.5 110 | end 111 | end 112 | 113 | it "blocks until gets the next byte" do 114 | allow(logger).to receive(:crash).with("Actor crashed!", Celluloid::TaskTimeout) 115 | with_connected_unix_sockets do |subject, peer| 116 | peer << 0x00 117 | peer.flush 118 | expect do 119 | within_io_actor do 120 | subject.read(1) 121 | Celluloid.timeout(0.5) do 122 | expect(subject.eof?).to be_falsey 123 | end 124 | end 125 | end.to raise_error(Celluloid::TaskTimeout) 126 | end 127 | end 128 | end 129 | end 130 | 131 | context "outside Celluloid::IO" do 132 | it "connects to UNIX servers" do 133 | server = ::UNIXServer.new example_unix_sock 134 | thread = Thread.new { server.accept } 135 | socket = Celluloid::IO::UNIXSocket.open example_unix_sock 136 | peer = thread.value 137 | 138 | peer << payload 139 | expect(socket.read(payload.size)).to eq payload 140 | 141 | server.close 142 | socket.close 143 | peer.close 144 | File.delete example_unix_sock 145 | end 146 | 147 | it "should be blocking" do 148 | with_connected_unix_sockets do |subject| 149 | expect(Celluloid::IO).not_to be_evented 150 | end 151 | end 152 | 153 | it "reads data" do 154 | with_connected_unix_sockets do |subject, peer| 155 | peer << payload 156 | expect(subject.read(payload.size)).to eq payload 157 | end 158 | end 159 | 160 | it "reads partial data" do 161 | with_connected_unix_sockets do |subject, peer| 162 | peer << payload * 2 163 | expect(subject.readpartial(payload.size)).to eq payload 164 | end 165 | end 166 | 167 | it "writes data" do 168 | with_connected_unix_sockets do |subject, peer| 169 | subject << payload 170 | expect(peer.read(payload.size)).to eq payload 171 | end 172 | end 173 | end 174 | 175 | context 'puts' do 176 | it 'uses the write buffer' do 177 | with_connected_unix_sockets do |subject, peer| 178 | subject.sync = false 179 | subject << "a" 180 | subject.puts "b" 181 | subject << "c" 182 | subject.flush 183 | subject.close 184 | expect(peer.read).to eq "ab\nc" 185 | end 186 | end 187 | end 188 | 189 | context 'readline' do 190 | it 'uses the read buffer' do 191 | with_connected_unix_sockets do |subject, peer| 192 | peer << "xline one\nline two\n" 193 | subject.getc # read one character to fill buffer 194 | Timeout::timeout(1){ 195 | # this will block if the buffer is not used 196 | expect(subject.readline).to eq "line one\n" 197 | expect(subject.readline).to eq "line two\n" 198 | } 199 | end 200 | end 201 | end 202 | 203 | end 204 | -------------------------------------------------------------------------------- /spec/celluloid/io/ssl_socket_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "openssl" 3 | 4 | RSpec.describe Celluloid::IO::SSLSocket, library: :IO do 5 | let(:request) { "ping" } 6 | let(:response) { "pong" } 7 | 8 | let(:example_port) { assign_port } 9 | let(:client_cert) { OpenSSL::X509::Certificate.new fixture_dir.join("client.crt").read } 10 | let(:client_key) { OpenSSL::PKey::RSA.new fixture_dir.join("client.key").read } 11 | let(:client_context) do 12 | OpenSSL::SSL::SSLContext.new.tap do |context| 13 | context.cert = client_cert 14 | context.key = client_key 15 | end 16 | end 17 | 18 | after(:each) do 19 | client.close rescue nil 20 | server.close rescue nil 21 | end 22 | 23 | let(:client) do 24 | attempts = 0 25 | socket = begin 26 | Timeout.timeout(Specs::MAX_EXECUTION) do 27 | begin 28 | TCPSocket.new example_addr, example_port 29 | rescue Errno::ECONNREFUSED 30 | raise if attempts >= Specs::MAX_ATTEMPTS 31 | attempts += 1 32 | # HAX: sometimes this fails to connect? o_O 33 | # ... This can often fail 20 times in a row ... so yeah 34 | # This is quite likely due to the Thread.pass style spinlocks for startup 35 | # Seems gimpy, but sleep and retry 36 | sleep 0.0126 37 | retry 38 | end 39 | end 40 | rescue => ex 41 | attempted = "Tried #{attempts} times to instantiate socket." 42 | raise ex.class.new(attempted) 43 | end 44 | return socket 45 | end 46 | 47 | let(:ssl_client) { Celluloid::IO::SSLSocket.new client, client_context } 48 | 49 | let(:server_cert) { OpenSSL::X509::Certificate.new fixture_dir.join("server.crt").read } 50 | let(:server_key) { OpenSSL::PKey::RSA.new fixture_dir.join("server.key").read } 51 | let(:server_context) do 52 | OpenSSL::SSL::SSLContext.new.tap do |context| 53 | context.cert = server_cert 54 | context.key = server_key 55 | end 56 | end 57 | 58 | let(:server) { TCPServer.new example_addr, example_port } 59 | let(:ssl_server) { OpenSSL::SSL::SSLServer.new(server, server_context) } 60 | let(:server_thread) do 61 | server = Thread.new { ssl_server.accept }.tap do |thread| 62 | Thread.pass while thread.status && thread.status != "sleep" 63 | thread.join unless thread.status 64 | end 65 | server 66 | end 67 | 68 | let(:celluloid_server) { Celluloid::IO::TCPServer.new example_addr, example_port } 69 | let(:raw_server_thread) do 70 | server = Thread.new { celluloid_server.accept }.tap do |thread| 71 | Thread.pass while thread.status && thread.status != "sleep" 72 | thread.join unless thread.status 73 | end 74 | server 75 | end 76 | 77 | context "duck typing ::SSLSocket" do 78 | it "responds to #peeraddr" do 79 | with_ssl_sockets do |ssl_client, ssl_peer| 80 | expect { ssl_client.peeraddr }.to_not raise_error 81 | end 82 | end 83 | end 84 | 85 | context "inside Celluloid::IO" do 86 | it "connects to SSL servers over TCP" do 87 | with_ssl_sockets do |ssl_client, ssl_peer| 88 | within_io_actor do 89 | ssl_peer << request 90 | expect(ssl_client.read(request.size)).to eq(request) 91 | 92 | ssl_client << response 93 | expect(ssl_peer.read(response.size)).to eq(response) 94 | end 95 | end 96 | end 97 | 98 | it "starts SSL on a connected TCP socket" do 99 | if RUBY_PLATFORM == "java" 100 | pending "JRuby support" 101 | fail "Bypassing potential deadlock." 102 | end 103 | with_raw_sockets do |client, peer| 104 | within_io_actor do 105 | peer << request 106 | expect(client.read(request.size)).to eq(request) 107 | 108 | client << response 109 | expect(peer.read(response.size)).to eq(response) 110 | 111 | # now that we've written bytes, upgrade to SSL 112 | client_thread = Thread.new { OpenSSL::SSL::SSLSocket.new(client).connect } 113 | ssl_peer = Celluloid::IO::SSLSocket.new peer, server_context 114 | expect(ssl_peer).to eq(ssl_peer.accept) 115 | ssl_client = client_thread.value 116 | 117 | ssl_peer << request 118 | expect(ssl_client.read(request.size)).to eq(request) 119 | 120 | ssl_client << response 121 | expect(ssl_peer.read(response.size)).to eq(response) 122 | end 123 | end 124 | end 125 | end 126 | 127 | context "outside Celluloid::IO" do 128 | it "connects to SSL servers over TCP" do 129 | with_ssl_sockets do |ssl_client, ssl_peer| 130 | ssl_peer << request 131 | expect(ssl_client.read(request.size)).to eq(request) 132 | 133 | ssl_client << response 134 | expect(ssl_peer.read(response.size)).to eq(response) 135 | end 136 | end 137 | 138 | it "starts SSL on a connected TCP socket" do 139 | if RUBY_PLATFORM == "java" 140 | pending "JRuby support" 141 | fail "Bypassing potential deadlock." 142 | end 143 | with_raw_sockets do |client, peer| 144 | peer << request 145 | expect(client.read(request.size)).to eq(request) 146 | 147 | client << response 148 | expect(peer.read(response.size)).to eq(response) 149 | 150 | # now that we've written bytes, upgrade to SSL 151 | client_thread = Thread.new { OpenSSL::SSL::SSLSocket.new(client).connect } 152 | ssl_peer = Celluloid::IO::SSLSocket.new peer, server_context 153 | expect(ssl_peer).to eq(ssl_peer.accept) 154 | ssl_client = client_thread.value 155 | 156 | ssl_peer << request 157 | expect(ssl_client.read(request.size)).to eq(request) 158 | 159 | ssl_client << response 160 | expect(ssl_peer.read(response.size)).to eq(response) 161 | end 162 | end 163 | end 164 | 165 | it "knows its cert" do 166 | # FIXME: seems bad? o_O 167 | pending "wtf is wrong with this on JRuby" if RUBY_PLATFORM == "java" 168 | with_ssl_sockets do |ssl_client| 169 | expect(ssl_client.cert.to_der).to eq(client_cert.to_der) 170 | end 171 | end 172 | 173 | it "knows its peer_cert" do 174 | with_ssl_sockets do |ssl_client| 175 | expect(ssl_client.peer_cert.to_der).to eq(ssl_client.to_io.peer_cert.to_der) 176 | end 177 | end 178 | 179 | it "knows its peer_cert_chain" do 180 | with_ssl_sockets do |ssl_client| 181 | expect(ssl_client.peer_cert_chain.zip(ssl_client.to_io.peer_cert_chain).map do |c1, c2| 182 | c1.to_der == c2.to_der 183 | end).to be_all 184 | end 185 | end 186 | 187 | it "knows its cipher" do 188 | with_ssl_sockets do |ssl_client| 189 | expect(ssl_client.cipher).to eq(ssl_client.to_io.cipher) 190 | end 191 | end 192 | 193 | it "knows its client_ca" do 194 | # jruby-openssl does not implement this method 195 | pending "jruby-openssl support" if RUBY_PLATFORM == "java" 196 | 197 | with_ssl_sockets do |ssl_client| 198 | expect(ssl_client.client_ca).to eq(ssl_client.to_io.client_ca) 199 | end 200 | end 201 | 202 | it "verifies peer certificates" do 203 | # FIXME: JRuby seems to be giving the wrong result here o_O 204 | pending "jruby-openssl support" if RUBY_PLATFORM == "java" 205 | 206 | with_ssl_sockets do |ssl_client, ssl_peer| 207 | expect(ssl_client.verify_result).to eq(OpenSSL::X509::V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT) 208 | end 209 | end 210 | 211 | def with_ssl_sockets 212 | server_thread 213 | sleep 0.222 # Needs time to spin up, or will throw out Errno::ECONNECTREFUSED to client. 214 | ssl_client.connect 215 | 216 | begin 217 | ssl_peer = server_thread.value 218 | yield ssl_client, ssl_peer 219 | ensure 220 | server_thread.join 221 | ssl_server.close 222 | ssl_client.close 223 | ssl_peer.close 224 | end 225 | end 226 | 227 | def with_raw_sockets 228 | raw_server_thread 229 | sleep 0.222 # Needs time to spin up, or will throw out Errno::ECONNECTREFUSED to client. 230 | client 231 | 232 | begin 233 | peer = raw_server_thread.value 234 | yield client, peer 235 | ensure 236 | raw_server_thread.join 237 | celluloid_server.close 238 | client.close 239 | peer.close 240 | end 241 | end 242 | end 243 | -------------------------------------------------------------------------------- /spec/celluloid/io/tcp_socket_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Celluloid::IO::TCPSocket, library: :IO do 4 | let(:payload) { "ohai" } 5 | let(:example_port) { assign_port } 6 | let(:logger) { Specs::FakeLogger.current } 7 | 8 | context "inside Celluloid::IO" do 9 | describe ".open" do 10 | it "returns the open socket" do 11 | server = ::TCPServer.new example_addr, example_port 12 | thread = Thread.new { server.accept } 13 | 14 | socket = within_io_actor { Celluloid::IO::TCPSocket.open(example_addr, example_port) } 15 | expect(socket).to be_a(Celluloid::IO::TCPSocket) 16 | 17 | server.close 18 | thread.terminate 19 | socket.close 20 | end 21 | context "when passed a block" do 22 | it "returns the block evaluation" do 23 | server = ::TCPServer.new example_addr, example_port 24 | thread = Thread.new { server.accept } 25 | 26 | value = within_io_actor { Celluloid::IO::TCPSocket.open(example_addr, example_port) { true } } 27 | expect(value).to be_truthy 28 | 29 | server.close 30 | thread.terminate 31 | end 32 | end 33 | end 34 | 35 | it "connects to TCP servers" do 36 | server = ::TCPServer.new example_addr, example_port 37 | thread = Thread.new { server.accept } 38 | socket = within_io_actor { Celluloid::IO::TCPSocket.new example_addr, example_port } 39 | peer = thread.value 40 | 41 | peer << payload 42 | expect(within_io_actor { socket.read(payload.size) }).to eq payload 43 | 44 | server.close 45 | socket.close 46 | peer.close 47 | end 48 | 49 | it "should be evented" do 50 | with_connected_sockets(example_port) do |subject| 51 | expect(within_io_actor { Celluloid::IO.evented? }).to be_truthy 52 | end 53 | end 54 | 55 | it "read complete payload when nil size is given to #read" do 56 | with_connected_sockets(example_port) do |subject, peer| 57 | peer << payload 58 | expect(within_io_actor { subject.read(nil) }).to eq payload 59 | end 60 | end 61 | 62 | it "read complete payload when no size is given to #read" do 63 | with_connected_sockets(example_port) do |subject, peer| 64 | peer << payload 65 | expect(within_io_actor { subject.read }).to eq payload 66 | end 67 | end 68 | 69 | it "reads data" do 70 | with_connected_sockets(example_port) do |subject, peer| 71 | peer << payload 72 | expect(within_io_actor { subject.read(payload.size) }).to eq payload 73 | end 74 | end 75 | 76 | it "reads data in binary encoding" do 77 | with_connected_sockets(example_port) do |subject, peer| 78 | peer << payload 79 | expect(within_io_actor { subject.read(payload.size).encoding }).to eq Encoding::BINARY 80 | end 81 | end 82 | 83 | it "reads partial data" do 84 | with_connected_sockets(example_port) do |subject, peer| 85 | peer << payload * 2 86 | expect(within_io_actor { subject.readpartial(payload.size) }).to eq payload 87 | end 88 | end 89 | 90 | it "reads partial data in binary encoding" do 91 | with_connected_sockets(example_port) do |subject, peer| 92 | peer << payload * 2 93 | expect(within_io_actor { subject.readpartial(payload.size).encoding }).to eq Encoding::BINARY 94 | end 95 | end 96 | 97 | it "writes data" do 98 | with_connected_sockets(example_port) do |subject, peer| 99 | within_io_actor { subject << payload } 100 | expect(peer.read(payload.size)).to eq payload 101 | end 102 | end 103 | 104 | it "raises Errno::ECONNREFUSED when the connection is refused" do 105 | allow(logger).to receive(:crash).with("Actor crashed!", Errno::ECONNREFUSED) 106 | expect do 107 | within_io_actor { ::TCPSocket.new(example_addr, example_port) } 108 | end.to raise_error(Errno::ECONNREFUSED) 109 | end 110 | 111 | context "eof?" do 112 | it "blocks actor then returns by close" do 113 | with_connected_sockets(example_port) do |subject, peer| 114 | started_at = Time.now 115 | Thread.new { sleep 0.5; peer.close; } 116 | within_io_actor { subject.eof? } 117 | expect(Time.now - started_at).to be > 0.5 118 | end 119 | end 120 | 121 | it "blocks until gets the next byte" do 122 | allow(logger).to receive(:crash).with("Actor crashed!", Celluloid::TaskTimeout) 123 | with_connected_sockets(example_port) do |subject, peer| 124 | peer << 0x00 125 | peer.flush 126 | expect do 127 | within_io_actor do 128 | subject.read(1) 129 | Celluloid.timeout(0.5) do 130 | expect(subject.eof?).to be_falsey 131 | end 132 | end 133 | end.to raise_error(Celluloid::TaskTimeout) 134 | end 135 | end 136 | end 137 | 138 | context "readpartial" do 139 | it "raises EOFError when reading from a closed socket" do 140 | allow(logger).to receive(:crash).with("Actor crashed!", EOFError) 141 | with_connected_sockets(example_port) do |subject, peer| 142 | peer.close 143 | expect do 144 | within_io_actor { subject.readpartial(payload.size) } 145 | end.to raise_error(EOFError) 146 | end 147 | end 148 | 149 | it "raises IOError when active sockets are closed across threads" do 150 | pending "not implemented" 151 | 152 | with_connected_sockets(example_port) do |subject, peer| 153 | actor = with_wrapper_actor 154 | allow(logger).to receive(:crash).with("Actor crashed!", IOError) 155 | begin 156 | read_future = actor.future.wrap do 157 | subject.readpartial(payload.size) 158 | end 159 | sleep 0.1 160 | subject.close 161 | expect { read_future.value 0.25 }.to raise_error(IOError) 162 | ensure 163 | actor.terminate if actor.alive? 164 | end 165 | end 166 | end 167 | 168 | it "raises IOError when partial reading from a socket the peer closed" do 169 | pending "async block running on receiver" 170 | with_connected_sockets(example_port) do |subject, peer| 171 | actor = with_wrapper_actor 172 | allow(logger).to receive(:crash).with("Actor crashed!", IOError) 173 | begin 174 | actor.async.wrap { sleep 0.01; peer.close } 175 | expect do 176 | within_io_actor { subject.readpartial(payload.size) } 177 | end.to raise_error(IOError) 178 | ensure 179 | actor.terminate if actor.alive? 180 | end 181 | end 182 | end 183 | end 184 | 185 | context "close" do 186 | 187 | it "flushes remaining data" do 188 | with_connected_sockets(example_port) do |subject, peer| 189 | subject.sync = false 190 | within_io_actor { subject << payload } 191 | expect{ peer.read_nonblock payload.length }.to raise_exception ::IO::WaitReadable 192 | within_io_actor { subject.close } 193 | expect(peer.read).to eq payload 194 | end 195 | end 196 | 197 | end 198 | end 199 | 200 | context "outside Celluloid::IO" do 201 | it "connects to TCP servers" do 202 | server = ::TCPServer.new example_addr, example_port 203 | thread = Thread.new { server.accept } 204 | socket = Celluloid::IO::TCPSocket.new example_addr, example_port 205 | peer = thread.value 206 | 207 | peer << payload 208 | expect(socket.read(payload.size)).to eq payload 209 | 210 | server.close 211 | socket.close 212 | peer.close 213 | end 214 | 215 | it "should be blocking" do 216 | with_connected_sockets(example_port) do |subject| 217 | expect(Celluloid::IO).not_to be_evented 218 | end 219 | end 220 | 221 | it "reads data" do 222 | with_connected_sockets(example_port) do |subject, peer| 223 | peer << payload 224 | expect(subject.read(payload.size)).to eq payload 225 | end 226 | end 227 | 228 | it "reads partial data" do 229 | with_connected_sockets(example_port) do |subject, peer| 230 | peer << payload * 2 231 | expect(subject.readpartial(payload.size)).to eq payload 232 | end 233 | end 234 | 235 | it "writes data" do 236 | with_connected_sockets(example_port) do |subject, peer| 237 | subject << payload 238 | expect(peer.read(payload.size)).to eq payload 239 | end 240 | end 241 | end 242 | end 243 | -------------------------------------------------------------------------------- /lib/celluloid/io/stream.rb: -------------------------------------------------------------------------------- 1 | # Partially adapted from Ruby's OpenSSL::Buffering 2 | # Originally from the 'OpenSSL for Ruby 2' project 3 | # Copyright (C) 2001 GOTOU YUUZOU 4 | # All rights reserved. 5 | # 6 | # This program is licenced under the same licence as Ruby. 7 | 8 | module Celluloid 9 | module IO 10 | # Base class of all streams in Celluloid::IO 11 | class Stream < Socket 12 | include Enumerable 13 | 14 | # The "sync mode" of the stream 15 | # 16 | # See IO#sync for full details. 17 | attr_accessor :sync 18 | 19 | def initialize(socket) 20 | super 21 | @eof = false 22 | @sync = true 23 | @read_buffer = ''.force_encoding(Encoding::ASCII_8BIT) 24 | @write_buffer = ''.force_encoding(Encoding::ASCII_8BIT) 25 | 26 | @read_latch = Latch.new 27 | @write_latch = Latch.new 28 | end 29 | 30 | # Wait until the current object is readable 31 | def wait_readable; Celluloid::IO.wait_readable(self); end 32 | 33 | # Wait until the current object is writable 34 | def wait_writable; Celluloid::IO.wait_writable(self); end 35 | 36 | # System read via the nonblocking subsystem 37 | def sysread(length = nil, buffer = nil) 38 | buffer ||= ''.force_encoding(Encoding::ASCII_8BIT) 39 | 40 | @read_latch.synchronize do 41 | begin 42 | read_nonblock(length, buffer) 43 | rescue ::IO::WaitReadable 44 | wait_readable 45 | retry 46 | end 47 | end 48 | 49 | buffer 50 | end 51 | 52 | # System write via the nonblocking subsystem 53 | def syswrite(string) 54 | length = string.length 55 | total_written = 0 56 | 57 | remaining = string 58 | 59 | @write_latch.synchronize do 60 | while total_written < length 61 | begin 62 | written = write_nonblock(remaining) 63 | rescue ::IO::WaitWritable 64 | wait_writable 65 | retry 66 | rescue EOFError 67 | return total_written 68 | rescue Errno::EAGAIN 69 | wait_writable 70 | retry 71 | end 72 | 73 | total_written += written 74 | 75 | # FIXME: mutating the original buffer here. Seems bad. 76 | remaining.slice!(0, written) if written < remaining.length 77 | end 78 | end 79 | 80 | total_written 81 | end 82 | 83 | # Reads +size+ bytes from the stream. If +buf+ is provided it must 84 | # reference a string which will receive the data. 85 | # 86 | # See IO#read for full details. 87 | def read(size=nil, buf=nil) 88 | if size == 0 89 | if buf 90 | buf.clear 91 | return buf 92 | else 93 | return "" 94 | end 95 | end 96 | 97 | until @eof 98 | break if size && size <= @read_buffer.size 99 | fill_rbuff 100 | break unless size 101 | end 102 | 103 | ret = consume_rbuff(size) || "" 104 | 105 | if buf 106 | buf.replace(ret) 107 | ret = buf 108 | end 109 | 110 | (size && ret.empty?) ? nil : ret 111 | end 112 | 113 | # Reads at most +maxlen+ bytes from the stream. If +buf+ is provided it 114 | # must reference a string which will receive the data. 115 | # 116 | # See IO#readpartial for full details. 117 | def readpartial(maxlen, buf=nil) 118 | if maxlen == 0 119 | if buf 120 | buf.clear 121 | return buf 122 | else 123 | return "" 124 | end 125 | end 126 | 127 | if @read_buffer.empty? 128 | begin 129 | return sysread(maxlen, buf) 130 | rescue Errno::EAGAIN 131 | retry 132 | end 133 | end 134 | 135 | ret = consume_rbuff(maxlen) 136 | 137 | if buf 138 | buf.replace(ret) 139 | ret = buf 140 | end 141 | 142 | raise EOFError if ret.empty? 143 | ret 144 | end 145 | 146 | # Reads the next line from the stream. Lines are separated by +eol+. If 147 | # +limit+ is provided the result will not be longer than the given number of 148 | # bytes. 149 | # 150 | # +eol+ may be a String or Regexp. 151 | # 152 | # Unlike IO#gets the line read will not be assigned to +$_+. 153 | # 154 | # Unlike IO#gets the separator must be provided if a limit is provided. 155 | def gets(eol=$/, limit=nil) 156 | idx = @read_buffer.index(eol) 157 | 158 | until @eof 159 | break if idx 160 | fill_rbuff 161 | idx = @read_buffer.index(eol) 162 | end 163 | 164 | if eol.is_a?(Regexp) 165 | size = idx ? idx+$&.size : nil 166 | else 167 | size = idx ? idx+eol.size : nil 168 | end 169 | 170 | if limit and limit >= 0 171 | size = [size, limit].min 172 | end 173 | 174 | consume_rbuff(size) 175 | end 176 | 177 | # Executes the block for every line in the stream where lines are separated 178 | # by +eol+. 179 | # 180 | # See also #gets 181 | def each(eol=$/) 182 | while line = self.gets(eol) 183 | yield line 184 | end 185 | end 186 | alias each_line each 187 | 188 | # Reads lines from the stream which are separated by +eol+. 189 | # 190 | # See also #gets 191 | def readlines(eol=$/) 192 | ary = [] 193 | 194 | while line = self.gets(eol) 195 | ary << line 196 | end 197 | 198 | ary 199 | end 200 | 201 | # Reads a line from the stream which is separated by +eol+. 202 | # 203 | # Raises EOFError if at end of file. 204 | def readline(eol=$/) 205 | raise EOFError if eof? 206 | gets(eol) 207 | end 208 | 209 | # Reads one character from the stream. Returns nil if called at end of 210 | # file. 211 | def getc 212 | read(1) 213 | end 214 | 215 | # Calls the given block once for each byte in the stream. 216 | def each_byte # :yields: byte 217 | while c = getc 218 | yield(c.ord) 219 | end 220 | end 221 | 222 | # Reads a one-character string from the stream. Raises an EOFError at end 223 | # of file. 224 | def readchar 225 | raise EOFError if eof? 226 | getc 227 | end 228 | 229 | # Pushes character +c+ back onto the stream such that a subsequent buffered 230 | # character read will return it. 231 | # 232 | # Unlike IO#getc multiple bytes may be pushed back onto the stream. 233 | # 234 | # Has no effect on unbuffered reads (such as #sysread). 235 | def ungetc(c) 236 | @read_buffer[0,0] = c.chr 237 | end 238 | 239 | # Returns true if the stream is at file which means there is no more data to 240 | # be read. 241 | def eof? 242 | fill_rbuff if !@eof && @read_buffer.empty? 243 | @eof && @read_buffer.empty? 244 | end 245 | alias eof eof? 246 | 247 | # Writes +s+ to the stream. If the argument is not a string it will be 248 | # converted using String#to_s. Returns the number of bytes written. 249 | def write(s) 250 | do_write(s) 251 | s.bytesize 252 | end 253 | 254 | # Writes +s+ to the stream. +s+ will be converted to a String using 255 | # String#to_s. 256 | def << (s) 257 | do_write(s) 258 | self 259 | end 260 | 261 | # Writes +args+ to the stream along with a record separator. 262 | # 263 | # See IO#puts for full details. 264 | def puts(*args) 265 | s = "" 266 | if args.empty? 267 | s << "\n" 268 | end 269 | 270 | args.each do |arg| 271 | s << arg.to_s 272 | if $/ && /\n\z/ !~ s 273 | s << "\n" 274 | end 275 | end 276 | 277 | do_write(s) 278 | nil 279 | end 280 | 281 | # Writes +args+ to the stream. 282 | # 283 | # See IO#print for full details. 284 | def print(*args) 285 | s = "" 286 | args.each { |arg| s << arg.to_s } 287 | do_write(s) 288 | nil 289 | end 290 | 291 | # Formats and writes to the stream converting parameters under control of 292 | # the format string. 293 | # 294 | # See Kernel#sprintf for format string details. 295 | def printf(s, *args) 296 | do_write(s % args) 297 | nil 298 | end 299 | 300 | # Flushes buffered data to the stream. 301 | def flush 302 | osync = @sync 303 | @sync = true 304 | do_write "" 305 | return self 306 | ensure 307 | @sync = osync 308 | end 309 | 310 | # Closes the stream and flushes any unwritten data. 311 | def close 312 | flush rescue nil 313 | super 314 | end 315 | 316 | ####### 317 | private 318 | ####### 319 | 320 | # Fills the buffer from the underlying stream 321 | def fill_rbuff 322 | begin 323 | @read_buffer << sysread(BLOCK_SIZE) 324 | rescue Errno::EAGAIN 325 | retry 326 | rescue EOFError 327 | @eof = true 328 | end 329 | end 330 | 331 | # Consumes +size+ bytes from the buffer 332 | def consume_rbuff(size=nil) 333 | if @read_buffer.empty? 334 | nil 335 | else 336 | size = @read_buffer.size unless size 337 | ret = @read_buffer[0, size] 338 | @read_buffer[0, size] = "" 339 | ret 340 | end 341 | end 342 | 343 | # Writes +s+ to the buffer. When the buffer is full or #sync is true the 344 | # buffer is flushed to the underlying stream. 345 | def do_write(s) 346 | @write_buffer << s 347 | @write_buffer.force_encoding(Encoding::BINARY) 348 | 349 | if @sync or @write_buffer.size > BLOCK_SIZE or idx = @write_buffer.rindex($/) 350 | remain = idx ? idx + $/.size : @write_buffer.length 351 | nwritten = 0 352 | 353 | while remain > 0 354 | str = @write_buffer[nwritten,remain] 355 | begin 356 | nwrote = syswrite(str) 357 | rescue Errno::EAGAIN 358 | retry 359 | end 360 | remain -= nwrote 361 | nwritten += nwrote 362 | end 363 | 364 | @write_buffer[0,nwritten] = "" 365 | end 366 | end 367 | 368 | # Perform an operation exclusively, uncontested by other tasks 369 | class Latch 370 | def initialize 371 | @owner = nil 372 | @waiters = 0 373 | @condition = Celluloid::Condition.new 374 | end 375 | 376 | # Synchronize an operation across all tasks in the current actor 377 | def synchronize 378 | actor = Thread.current[:celluloid_actor] 379 | return yield unless actor 380 | 381 | if @owner || @waiters > 0 382 | @waiters += 1 383 | @condition.wait 384 | @waiters -= 1 385 | end 386 | 387 | @owner = Task.current 388 | 389 | begin 390 | ret = yield 391 | ensure 392 | @owner = nil 393 | @condition.signal if @waiters > 0 394 | end 395 | 396 | ret 397 | end 398 | end 399 | end 400 | end 401 | end 402 | --------------------------------------------------------------------------------