├── .editorconfig ├── .github └── workflows │ ├── async-head.yaml │ ├── async-v1.yaml │ ├── coverage.yaml │ ├── documentation.yaml │ ├── test-external.yaml │ └── test.yaml ├── .gitignore ├── .mailmap ├── .rspec ├── async-io.gemspec ├── benchmark └── pipe.rb ├── examples ├── allocations │ ├── byteslice.rb │ ├── memory.rb │ └── read_chunks.rb ├── chat │ ├── client.rb │ └── server.rb ├── defer │ └── worker.rb ├── echo │ ├── client.rb │ └── server.rb ├── issues │ ├── broken_ssl.rb │ └── pipes.rb ├── millions │ ├── client.rb │ └── server.rb ├── ssl │ ├── cert.crt │ ├── client.rb │ ├── key.pem │ └── server.rb ├── udp.rb ├── udp │ ├── client.rb │ └── server.rb └── unix │ ├── client.rb │ └── server.rb ├── gems.rb ├── gems ├── async-head.rb └── async-v1.rb ├── lib └── async │ ├── io.rb │ └── io │ ├── address.rb │ ├── address_endpoint.rb │ ├── binary_string.rb │ ├── buffer.rb │ ├── composite_endpoint.rb │ ├── endpoint.rb │ ├── endpoint │ └── each.rb │ ├── generic.rb │ ├── host_endpoint.rb │ ├── notification.rb │ ├── peer.rb │ ├── protocol │ ├── generic.rb │ └── line.rb │ ├── server.rb │ ├── shared_endpoint.rb │ ├── socket.rb │ ├── socket_endpoint.rb │ ├── ssl_endpoint.rb │ ├── ssl_socket.rb │ ├── standard.rb │ ├── stream.rb │ ├── tcp_socket.rb │ ├── threads.rb │ ├── trap.rb │ ├── udp_socket.rb │ ├── unix_endpoint.rb │ ├── unix_socket.rb │ └── version.rb ├── license.md ├── readme.md ├── release.cert ├── spec ├── addrinfo.rb ├── async │ └── io │ │ ├── buffer_spec.rb │ │ ├── c10k_spec.rb │ │ ├── echo_spec.rb │ │ ├── endpoint_spec.rb │ │ ├── generic_examples.rb │ │ ├── generic_spec.rb │ │ ├── notification_spec.rb │ │ ├── protocol │ │ └── line_spec.rb │ │ ├── shared_endpoint │ │ └── server_spec.rb │ │ ├── shared_endpoint_spec.rb │ │ ├── socket │ │ ├── tcp_spec.rb │ │ └── udp_spec.rb │ │ ├── socket_spec.rb │ │ ├── ssl_server_spec.rb │ │ ├── ssl_socket_spec.rb │ │ ├── standard_spec.rb │ │ ├── stream_context.rb │ │ ├── stream_spec.rb │ │ ├── tcp_socket_spec.rb │ │ ├── threads_spec.rb │ │ ├── trap_spec.rb │ │ ├── udp_socket_spec.rb │ │ ├── unix_endpoint_spec.rb │ │ └── unix_socket_spec.rb └── spec_helper.rb └── tea.yaml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | 7 | [*.{yml,yaml}] 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.github/workflows/async-head.yaml: -------------------------------------------------------------------------------- 1 | name: Async head 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{matrix.os}}-latest 8 | 9 | strategy: 10 | matrix: 11 | os: 12 | - ubuntu 13 | 14 | ruby: 15 | - head 16 | 17 | env: 18 | BUNDLE_GEMFILE: gems/async-head.rb 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{matrix.ruby}} 25 | bundler-cache: true 26 | 27 | - name: Run tests 28 | timeout-minutes: 5 29 | run: bundle exec rspec 30 | -------------------------------------------------------------------------------- /.github/workflows/async-v1.yaml: -------------------------------------------------------------------------------- 1 | name: Async v1 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{matrix.os}}-latest 8 | 9 | strategy: 10 | matrix: 11 | os: 12 | - ubuntu 13 | 14 | ruby: 15 | - 2.7 16 | 17 | env: 18 | BUNDLE_GEMFILE: gems/async-v1.rb 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{matrix.ruby}} 25 | bundler-cache: true 26 | 27 | - name: Run tests 28 | timeout-minutes: 5 29 | run: bundle exec rspec 30 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yaml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | COVERAGE: PartialSummary 11 | 12 | jobs: 13 | test: 14 | name: ${{matrix.ruby}} on ${{matrix.os}} 15 | runs-on: ${{matrix.os}}-latest 16 | 17 | strategy: 18 | matrix: 19 | os: 20 | - ubuntu 21 | - macos 22 | 23 | ruby: 24 | - "3.3" 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{matrix.ruby}} 31 | bundler-cache: true 32 | 33 | - name: Run tests 34 | timeout-minutes: 5 35 | run: bundle exec bake test 36 | 37 | - uses: actions/upload-artifact@v3 38 | with: 39 | name: coverage-${{matrix.os}}-${{matrix.ruby}} 40 | path: .covered.db 41 | 42 | validate: 43 | needs: test 44 | runs-on: ubuntu-latest 45 | 46 | steps: 47 | - uses: actions/checkout@v4 48 | - uses: ruby/setup-ruby@v1 49 | with: 50 | ruby-version: "3.3" 51 | bundler-cache: true 52 | 53 | - uses: actions/download-artifact@v3 54 | 55 | - name: Validate coverage 56 | timeout-minutes: 5 57 | run: bundle exec bake covered:validate --paths */.covered.db \; 58 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages: 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | # Allow one concurrent deployment: 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: true 18 | 19 | env: 20 | CONSOLE_OUTPUT: XTerm 21 | BUNDLE_WITH: maintenance 22 | 23 | jobs: 24 | generate: 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - uses: ruby/setup-ruby@v1 31 | with: 32 | ruby-version: "3.3" 33 | bundler-cache: true 34 | 35 | - name: Installing packages 36 | run: sudo apt-get install wget 37 | 38 | - name: Generate documentation 39 | timeout-minutes: 5 40 | run: bundle exec bake utopia:project:static --force no 41 | 42 | - name: Upload documentation artifact 43 | uses: actions/upload-pages-artifact@v2 44 | with: 45 | path: docs 46 | 47 | deploy: 48 | runs-on: ubuntu-latest 49 | 50 | environment: 51 | name: github-pages 52 | url: ${{steps.deployment.outputs.page_url}} 53 | 54 | needs: generate 55 | steps: 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v3 59 | -------------------------------------------------------------------------------- /.github/workflows/test-external.yaml: -------------------------------------------------------------------------------- 1 | name: Test External 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | 11 | jobs: 12 | test: 13 | name: ${{matrix.ruby}} on ${{matrix.os}} 14 | runs-on: ${{matrix.os}}-latest 15 | 16 | strategy: 17 | matrix: 18 | os: 19 | - ubuntu 20 | - macos 21 | 22 | ruby: 23 | - "2.7" 24 | - "3.0" 25 | - "3.1" 26 | - "3.2" 27 | - "3.3" 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: ruby/setup-ruby@v1 32 | with: 33 | ruby-version: ${{matrix.ruby}} 34 | bundler-cache: true 35 | 36 | - name: Run tests 37 | timeout-minutes: 10 38 | run: bundle exec bake test:external 39 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | 11 | jobs: 12 | test: 13 | name: ${{matrix.ruby}} on ${{matrix.os}} 14 | runs-on: ${{matrix.os}}-latest 15 | continue-on-error: ${{matrix.experimental}} 16 | 17 | strategy: 18 | matrix: 19 | os: 20 | - ubuntu 21 | - macos 22 | 23 | ruby: 24 | - "3.1" 25 | - "3.2" 26 | - "3.3" 27 | 28 | experimental: [false] 29 | 30 | include: 31 | - os: ubuntu 32 | ruby: truffleruby 33 | experimental: true 34 | - os: ubuntu 35 | ruby: jruby 36 | experimental: true 37 | - os: ubuntu 38 | ruby: head 39 | experimental: true 40 | 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: ruby/setup-ruby@v1 44 | with: 45 | ruby-version: ${{matrix.ruby}} 46 | bundler-cache: true 47 | 48 | - name: Run tests 49 | timeout-minutes: 10 50 | run: bundle exec bake test 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /pkg/ 3 | /gems.locked 4 | /.covered.db 5 | /external 6 | 7 | /.rspec 8 | /.rspec_status 9 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Aurora Nockert 2 | Hal Brodigan 3 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --warnings 3 | --require spec_helper -------------------------------------------------------------------------------- /async-io.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/async/io/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "async-io" 7 | spec.version = Async::IO::VERSION 8 | 9 | spec.summary = "Provides support for asynchonous TCP, UDP, UNIX and SSL sockets." 10 | spec.authors = ["Samuel Williams", "Olle Jonsson", "Benoit Daloze", "Thibaut Girka", "Hal Brodigan", "Janko Marohnić", "Aurora Nockert", "Bruno Sutic", "Cyril Roelandt", "Hasan Kumar", "Jiang Jinyang", "Maruth Goyal", "Patrik Wenger"] 11 | spec.license = "MIT" 12 | 13 | spec.cert_chain = ['release.cert'] 14 | spec.signing_key = File.expand_path('~/.gem/release.pem') 15 | 16 | spec.homepage = "https://github.com/socketry/async-io" 17 | 18 | spec.metadata = { 19 | "documentation_uri" => "https://socketry.github.io/async-io/", 20 | "source_code_uri" => "https://github.com/socketry/async-io.git", 21 | } 22 | 23 | spec.files = Dir.glob(['{lib}/**/*', '*.md'], File::FNM_DOTMATCH, base: __dir__) 24 | 25 | spec.required_ruby_version = ">= 2.5" 26 | 27 | spec.add_dependency "async" 28 | end 29 | -------------------------------------------------------------------------------- /benchmark/pipe.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2022-2023, by Samuel Williams. 6 | 7 | require 'async' 8 | require 'async/io' 9 | 10 | require 'benchmark/ips' 11 | 12 | def measure(pipe, count) 13 | i, o = pipe 14 | 15 | count.times do 16 | o.write("Hello World") 17 | i.read(11) 18 | end 19 | end 20 | 21 | Benchmark.ips do |benchmark| 22 | benchmark.time = 10 23 | benchmark.warmup = 2 24 | 25 | benchmark.report("Async::IO.pipe") do |count| 26 | Async do |task| 27 | measure(::Async::IO.pipe, count) 28 | end 29 | end 30 | 31 | benchmark.report("IO.pipe") do |count| 32 | Async do |task| 33 | measure(::IO.pipe, count) 34 | end 35 | end 36 | 37 | benchmark.compare! 38 | end 39 | -------------------------------------------------------------------------------- /examples/allocations/byteslice.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2018-2023, by Samuel Williams. 6 | 7 | require_relative 'memory' 8 | 9 | string = nil 10 | 11 | measure_memory("Initial allocation") do 12 | string = "a" * 5*1024*1024 13 | string.freeze 14 | end # => 5.0 MB 15 | 16 | measure_memory("Byteslice from start to middle") do 17 | # Why does this need to allocate memory? Surely it can share the original allocation? 18 | x = string.byteslice(0, string.bytesize / 2) 19 | end # => 2.5 MB 20 | 21 | measure_memory("Byteslice from middle to end") do 22 | string.byteslice(string.bytesize / 2, string.bytesize) 23 | end # => 0.0 MB 24 | 25 | measure_memory("Slice! from start to middle") do 26 | string.dup.slice!(0, string.bytesize / 2) 27 | end # => 7.5 MB 28 | 29 | measure_memory("Byte slice into two halves") do 30 | head = string.byteslice(0, string.bytesize / 2) # 2.5 MB 31 | remainder = string.byteslice(string.bytesize / 2, string.bytesize) # Shared 32 | end # 2.5 MB 33 | -------------------------------------------------------------------------------- /examples/allocations/memory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | 6 | def measure_memory(annotation = "Memory allocated") 7 | GC.disable 8 | 9 | start_memory = `ps -p #{Process::pid} -o rss`.split("\n")[1].chomp.to_i 10 | 11 | yield 12 | 13 | ensure 14 | end_memory = `ps -p #{Process::pid} -o rss`.split("\n")[1].chomp.to_i 15 | memory_usage = (end_memory - start_memory).to_f / 1024 16 | 17 | puts "#{memory_usage.round(1)} MB: #{annotation}" 18 | GC.enable 19 | end 20 | -------------------------------------------------------------------------------- /examples/allocations/read_chunks.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2018-2023, by Samuel Williams. 6 | 7 | require_relative 'memory' 8 | 9 | require_relative "../../lib/async/io/stream" 10 | require "stringio" 11 | 12 | measure_memory("Stream setup") do 13 | @io = StringIO.new("a" * (50*1024*1024)) 14 | @stream = Async::IO::Stream.new(@io) 15 | end # 50.0 MB 16 | 17 | measure_memory("Read all chunks") do 18 | while chunk = @stream.read_partial 19 | chunk.clear 20 | end 21 | end # 0.5 MB 22 | -------------------------------------------------------------------------------- /examples/chat/client.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2018-2023, by Samuel Williams. 6 | 7 | $LOAD_PATH << File.expand_path("../../lib", __dir__) 8 | 9 | require 'async' 10 | require 'async/notification' 11 | require 'async/io/stream' 12 | require 'async/io/host_endpoint' 13 | require 'async/io/protocol/line' 14 | 15 | class User < Async::IO::Protocol::Line 16 | end 17 | 18 | endpoint = Async::IO::Endpoint.parse(ARGV.pop || "tcp://localhost:7138") 19 | 20 | input = Async::IO::Protocol::Line.new( 21 | Async::IO::Stream.new( 22 | Async::IO::Generic.new($stdin) 23 | ) 24 | ) 25 | 26 | Async do |task| 27 | socket = endpoint.connect 28 | stream = Async::IO::Stream.new(socket) 29 | user = User.new(stream) 30 | 31 | # This is used to track whether either reading from stdin failed or reading from network failed. 32 | finished = Async::Notification.new 33 | 34 | # Read lines from stdin and write to network. 35 | terminal = task.async do 36 | while line = input.read_line 37 | user.write_lines line 38 | end 39 | rescue EOFError 40 | # It's okay, we are disconnecting, because stdin has closed. 41 | ensure 42 | finished.signal 43 | end 44 | 45 | # Read lines from network and write to stdout. 46 | network = task.async do 47 | while line = user.read_line 48 | puts line 49 | end 50 | ensure 51 | finished.signal 52 | end 53 | 54 | # Wait for any of the above processes to finish: 55 | finished.wait 56 | ensure 57 | # Stop all the nested tasks if we are exiting: 58 | network.stop if network 59 | terminal.stop if terminal 60 | user.close if user 61 | end 62 | -------------------------------------------------------------------------------- /examples/chat/server.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2018-2023, by Samuel Williams. 6 | # Copyright, 2020, by Bruno Sutic. 7 | 8 | $LOAD_PATH << File.expand_path("../../lib", __dir__) 9 | 10 | require 'set' 11 | require 'logger' 12 | 13 | require 'async' 14 | require 'async/io/host_endpoint' 15 | require 'async/io/protocol/line' 16 | 17 | class User < Async::IO::Protocol::Line 18 | attr_accessor :name 19 | 20 | def login! 21 | self.write_lines "Tell me your name, traveller:" 22 | self.name = self.read_line 23 | end 24 | 25 | def to_s 26 | @name || "unknown" 27 | end 28 | end 29 | 30 | class Server 31 | def initialize 32 | @users = Set.new 33 | end 34 | 35 | def broadcast(*message) 36 | puts *message 37 | 38 | @users.each do |user| 39 | begin 40 | user.write_lines(*message) 41 | rescue EOFError 42 | # In theory, it's possible this will fail if the remote end has disconnected. Each user has it's own task running `#connected`, and eventually `user.read_line` will fail. When it does, the disconnection logic will be invoked. A better way to do this would be to have a message queue, but for the sake of keeping this example simple, this is by far the better option. 43 | end 44 | end 45 | end 46 | 47 | def connected(user) 48 | user.login! 49 | 50 | broadcast("#{user} has joined") 51 | 52 | user.write_lines("currently connected: #{@users.map(&:to_s).join(', ')}") 53 | 54 | while message = user.read_line 55 | broadcast("#{user.name}: #{message}") 56 | end 57 | rescue EOFError 58 | # It's okay, client has disconnected. 59 | ensure 60 | disconnected(user) 61 | end 62 | 63 | def disconnected(user, reason = "quit") 64 | @users.delete(user) 65 | 66 | broadcast("#{user} has disconnected: #{reason}") 67 | end 68 | 69 | def run(endpoint) 70 | Async do |task| 71 | endpoint.accept do |peer| 72 | stream = Async::IO::Stream.new(peer) 73 | user = User.new(stream) 74 | 75 | @users << user 76 | 77 | connected(user) 78 | end 79 | end 80 | end 81 | end 82 | 83 | Console.logger.level = Logger::INFO 84 | Console.logger.info("Starting server...") 85 | server = Server.new 86 | 87 | endpoint = Async::IO::Endpoint.parse(ARGV.pop || "tcp://localhost:7138") 88 | server.run(endpoint) 89 | -------------------------------------------------------------------------------- /examples/defer/worker.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2020-2023, by Samuel Williams. 6 | 7 | require 'async' 8 | require 'async/io/notification' 9 | 10 | def defer(*args, &block) 11 | Async do 12 | notification = Async::IO::Notification.new 13 | 14 | thread = Thread.new(*args) do 15 | yield 16 | ensure 17 | notification.signal 18 | end 19 | 20 | notification.wait 21 | thread.join 22 | end 23 | end 24 | 25 | Async do 26 | 10.times do 27 | defer do 28 | puts "I'm going to sleep" 29 | sleep 1 30 | puts "I'm going to wake up" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /examples/echo/client.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2019-2023, by Samuel Williams. 6 | 7 | $LOAD_PATH.unshift File.expand_path("../../lib", __dir__) 8 | 9 | require 'async' 10 | require 'async/io/trap' 11 | require 'async/io/host_endpoint' 12 | require 'async/io/stream' 13 | 14 | endpoint = Async::IO::Endpoint.tcp('localhost', 4578) 15 | 16 | Async do |task| 17 | endpoint.connect do |peer| 18 | stream = Async::IO::Stream.new(peer) 19 | 20 | while true 21 | task.sleep 1 22 | stream.puts "Hello World!" 23 | puts stream.gets.inspect 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /examples/echo/server.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2019-2023, by Samuel Williams. 6 | 7 | $LOAD_PATH.unshift File.expand_path("../../lib", __dir__) 8 | 9 | require 'async' 10 | require 'async/io/trap' 11 | require 'async/io/host_endpoint' 12 | require 'async/io/stream' 13 | 14 | endpoint = Async::IO::Endpoint.tcp('localhost', 4578) 15 | 16 | interrupt = Async::IO::Trap.new(:INT) 17 | 18 | Async do |top| 19 | interrupt.install! 20 | 21 | endpoint.bind do |server, task| 22 | Console.logger.info(server) {"Accepting connections on #{server.local_address.inspect}"} 23 | 24 | task.async do |subtask| 25 | interrupt.wait 26 | 27 | Console.logger.info(server) {"Closing server socket..."} 28 | server.close 29 | 30 | interrupt.default! 31 | 32 | Console.logger.info(server) {"Waiting for connections to close..."} 33 | subtask.sleep(4) 34 | 35 | Console.logger.info(server) do |buffer| 36 | buffer.puts "Stopping all tasks..." 37 | task.print_hierarchy(buffer) 38 | buffer.puts "", "Reactor Hierarchy" 39 | task.reactor.print_hierarchy(buffer) 40 | end 41 | 42 | task.stop 43 | end 44 | 45 | server.listen(128) 46 | 47 | server.accept_each do |peer| 48 | stream = Async::IO::Stream.new(peer) 49 | 50 | while chunk = stream.read_partial 51 | Console.logger.debug(self) {chunk.inspect} 52 | stream.write(chunk) 53 | stream.flush 54 | 55 | Console.logger.info(server) do |buffer| 56 | task.reactor.print_hierarchy(buffer) 57 | end 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /examples/issues/broken_ssl.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2018-2023, by Samuel Williams. 6 | 7 | require 'socket' 8 | require 'openssl' 9 | 10 | server = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM) 11 | server.bind(Addrinfo.tcp('127.0.0.1', 4433)) 12 | server.listen(128) 13 | 14 | ssl_server = OpenSSL::SSL::SSLServer.new(server, OpenSSL::SSL::SSLContext.new) 15 | 16 | puts ssl_server.addr 17 | 18 | # openssl/ssl.rb:234:in `addr': undefined method `addr' for # (NoMethodError) 19 | -------------------------------------------------------------------------------- /examples/issues/pipes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019-2023, by Samuel Williams. 5 | 6 | require 'async' 7 | require_relative '../../lib/async/io' 8 | require 'digest/sha1' 9 | require 'securerandom' 10 | 11 | Async.run do |task| 12 | r, w = IO.pipe.map { |io| Async::IO.try_convert(io) } 13 | 14 | task.async do |subtask| 15 | s = Digest::SHA1.new 16 | l = 0 17 | 100.times do 18 | bytes = SecureRandom.bytes(4000) 19 | s << bytes 20 | w << bytes 21 | l += bytes.bytesize 22 | end 23 | w.close 24 | p [:write, l, s.hexdigest] 25 | end 26 | 27 | task.async do |subtask| 28 | s = Digest::SHA1.new 29 | l = 0 30 | while b = r.read(4096) 31 | s << b 32 | l += b.bytesize 33 | end 34 | p [:read, l, s.hexdigest] 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /examples/millions/client.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2018-2023, by Samuel Williams. 6 | # Copyright, 2020, by Bruno Sutic. 7 | 8 | $LOAD_PATH << File.expand_path("../../lib", __dir__) 9 | 10 | require 'async/reactor' 11 | require 'async/io/host_endpoint' 12 | 13 | require 'async/container' 14 | require 'async/container/forked' 15 | 16 | endpoint = Async::IO::Endpoint.parse(ARGV.pop || "tcp://localhost:7234") 17 | 18 | CONNECTIONS = 1_000_000 19 | 20 | CONCURRENCY = Async::Container.processor_count 21 | TASKS = 16 22 | REPEATS = (CONNECTIONS.to_f / (TASKS * CONCURRENCY)).ceil 23 | 24 | puts "Starting #{CONCURRENCY} processes, running #{TASKS} tasks, making #{REPEATS} connections." 25 | puts "Total number of connections: #{CONCURRENCY * TASKS * REPEATS}!" 26 | 27 | begin 28 | container = Async::Container::Forked.new 29 | 30 | container.run(count: CONCURRENCY) do 31 | Async do |task| 32 | connections = [] 33 | 34 | TASKS.times do 35 | task.async do 36 | REPEATS.times do 37 | $stdout.write "." 38 | connections << endpoint.connect 39 | end 40 | end 41 | end 42 | end 43 | end 44 | 45 | container.wait 46 | ensure 47 | container.stop if container 48 | end 49 | -------------------------------------------------------------------------------- /examples/millions/server.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2018-2023, by Samuel Williams. 6 | # Copyright, 2020, by Bruno Sutic. 7 | 8 | $LOAD_PATH << File.expand_path("../../lib", __dir__) 9 | 10 | require 'set' 11 | require 'logger' 12 | 13 | require 'async' 14 | require 'async/reactor' 15 | require 'async/io/host_endpoint' 16 | require 'async/io/protocol/line' 17 | 18 | class Server 19 | def initialize 20 | @connections = [] 21 | end 22 | 23 | def run(endpoint) 24 | Async do |task| 25 | task.async do |subtask| 26 | while true 27 | subtask.sleep 10 28 | puts "Connection count: #{@connections.size}" 29 | end 30 | end 31 | 32 | 33 | endpoint.accept do |peer| 34 | stream = Async::IO::Stream.new(peer) 35 | 36 | @connections << stream 37 | end 38 | end 39 | end 40 | end 41 | 42 | Console.logger.level = Logger::INFO 43 | Console.logger.info("Starting server...") 44 | server = Server.new 45 | 46 | endpoint = Async::IO::Endpoint.parse(ARGV.pop || "tcp://localhost:7234") 47 | server.run(endpoint) 48 | -------------------------------------------------------------------------------- /examples/ssl/cert.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE7TCCAtWgAwIBAgIBADANBgkqhkiG9w0BAQsFADA6MRIwEAYDVQQDDAlsb2Nh 3 | bGhvc3QxETAPBgNVBAsMCGFzeW5jLWlvMREwDwYDVQQKDAhTb2NrZXRyeTAeFw0y 4 | MzA5MjkyMzAyMTRaFw0yNDA5MjgyMzAyMTRaMDoxEjAQBgNVBAMMCWxvY2FsaG9z 5 | dDERMA8GA1UECwwIYXN5bmMtaW8xETAPBgNVBAoMCFNvY2tldHJ5MIICIjANBgkq 6 | hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtXIHml2E3yyVVHauPWDTrKWZZcT+fBua 7 | GHZF58qyiJ7niP6ilmtxGil8ubPTb2wGUrQpNc6Pn8YY1o0XZbrg9oyQs7X5BlRu 8 | GBBbxdmJTZi7/PpWHvfKsCZc2/ntS/NsMW8ig6PWMrzZn+VWvlQj7aP/BbD+dugg 9 | 7sSUUdlYGJdOddDfVpXbMqWswgvK9jYd/KywghmWBRvl8C+bp0yBcIQtbG02ukEd 10 | wIygDItInNvpHJ+njcv8QchUvPWOQgV1dZO6MTRaOeT45Dwf8X9ecuuKeoz6OH5Q 11 | pau9Ri1jKadM2WwPJzRfhVUKtWttoXWiL8LyhI3kGLFkuemEcVEhbjpTQysmi4Qs 12 | sk/Wcma6eK3/iume2euR01wE6mip0hTBDuE5l0MZZdJKSep08AXZRBX8pnvHEd82 13 | h3GPIgyO2EO7wbgM4lfGidERRbasoJUIyTmTo1poe6UwdjNKMdBYew1LmCPmcRbT 14 | 4dTyChR4bMPrJWd0xXiPZEFT8AJmFduumJEu0AyN/1xMPtRqDkx7sytqMa1n8NsG 15 | c0bAoesXisdS2Peoa6G5ekMOzqFKrP1yc7dtvu8hjQtVK2uO1Y+KCSyUojFMIBGy 16 | d/v2EB/e1corqQ2BZmvzFQ4vQrZ5344fvc9B3PHswmvR0clAFLnkc1BaNY3A0p84 17 | /ptaSjAB9qkCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAbIehU4aXrhp+uCdk8xKy 18 | W7LxiJfPhF/v2crVvuqpv9+SKlPDcB7auXyIKc7cchO3P1XlNM/s+Uw6VA6QZxP9 19 | eC5dqhZ0HcagTtTTqhlaF1pzpOXXA/sGacmyeHZQu0jvilifiWNtEtqDngbBi320 20 | qjlmknsTFP1ptFDNikZPspKfn0mhzENnYDxA27K+UDMOdoqzoNxCXPYKst/1F3vS 21 | +u2A+4Z76TYvNQmf/uTcfrFEssgH+2L0q+8CPsMflHjFuBXI8m4AUa7QYSTavSkn 22 | siWMJv+JKpW81P/pQ9L/hDPKIEWpNm/J8llOdp8AVvl514VYgreV5mvwqxHxvwob 23 | fARlqBDeOKFnC1F5KZkBayd6k6te9ZGXc68JqFqzskXG6FqzCAc0rpDSzM76NWuT 24 | IhxtK8ceoekBGOMhOUIWTbFk2jqNrGbME24/j2Y5tbTuTRsuqSlKIOM2Ao3iyuzb 25 | zdqPgqqZJjac46FEhxO820LwxcGwN4FrTh0fniMa066Aws87FnZE9j7Q6rWOJMSL 26 | ItIUovKnB6thIo0OWYuj5CQrodnKvOaWYd8kWPE3kPbtd0nQ9OOoMCHf+CPVo9kN 27 | QJVFmjNEQeiq/51uvVtJs66kXRIEKWDy04S3paf9YexSxHZXBTwmch0yqJeuG6nr 28 | wUaVIUMR7tbVLe6RGgXlVcU= 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /examples/ssl/client.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2023, by Hal Brodigan. 6 | 7 | require 'async' 8 | require 'async/io' 9 | require 'async/io/stream' 10 | 11 | endpoint = Async::IO::Endpoint.ssl('localhost',5678) 12 | 13 | Async do |async| 14 | endpoint.connect do |socket| 15 | stream = Async::IO::Stream.new(socket) 16 | 17 | (1..).each do |i| 18 | stream.puts "test #{i}" 19 | puts stream.gets 20 | sleep 1 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /examples/ssl/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJJwIBAAKCAgEAtXIHml2E3yyVVHauPWDTrKWZZcT+fBuaGHZF58qyiJ7niP6i 3 | lmtxGil8ubPTb2wGUrQpNc6Pn8YY1o0XZbrg9oyQs7X5BlRuGBBbxdmJTZi7/PpW 4 | HvfKsCZc2/ntS/NsMW8ig6PWMrzZn+VWvlQj7aP/BbD+dugg7sSUUdlYGJdOddDf 5 | VpXbMqWswgvK9jYd/KywghmWBRvl8C+bp0yBcIQtbG02ukEdwIygDItInNvpHJ+n 6 | jcv8QchUvPWOQgV1dZO6MTRaOeT45Dwf8X9ecuuKeoz6OH5Qpau9Ri1jKadM2WwP 7 | JzRfhVUKtWttoXWiL8LyhI3kGLFkuemEcVEhbjpTQysmi4Qssk/Wcma6eK3/iume 8 | 2euR01wE6mip0hTBDuE5l0MZZdJKSep08AXZRBX8pnvHEd82h3GPIgyO2EO7wbgM 9 | 4lfGidERRbasoJUIyTmTo1poe6UwdjNKMdBYew1LmCPmcRbT4dTyChR4bMPrJWd0 10 | xXiPZEFT8AJmFduumJEu0AyN/1xMPtRqDkx7sytqMa1n8NsGc0bAoesXisdS2Peo 11 | a6G5ekMOzqFKrP1yc7dtvu8hjQtVK2uO1Y+KCSyUojFMIBGyd/v2EB/e1corqQ2B 12 | ZmvzFQ4vQrZ5344fvc9B3PHswmvR0clAFLnkc1BaNY3A0p84/ptaSjAB9qkCAwEA 13 | AQKCAgAQqSSRFFLB0lxw6cfki2pMYVVVTrImb7tl0SBooQhlOqAciHMh+EIlqpcR 14 | DzXUNplbCT96eRnfjCdWNhTmqrMC+JPF6Kjx35lPXNssbuXoaeSjHVKAm/Sw2Yjv 15 | ywJy1aqC3IdRCp99v7EE+WBOcDfvV35whZjMDtMNUaAj5t7rvSL/dvs2/mInA6b6 16 | F26ezofQ3oODorlPhUkHFbwHb+M3028/VETw8885UcBVfomfm6LLVc6jGs6yNKSx 17 | SCP0pH8tWXAAmiPJyqBfBrVRLcacq0unJqBIRTL/D2a9FahUGBBaq401pp25xr4J 18 | vBBQQdavZ4lwyf2eldVfq9VVOWOmDUZI/zcCdBajobiYiYW8WqPxnzc5ecMXfMK+ 19 | gygtCDTLlQFt5W7YQg8AF92yjCma0JKPyhguElL6hKtVY9yOQHGYtkdzhZh1xmnO 20 | w/KdjWH63h8yLKsuOAKC+dEU9SmFvcXqUyW2pYHdKHQnPIRZTPYP13TIlWyXxpAc 21 | R8VLlBM4HR0Mh5uYOnaeMM3TuSOolBzCcqCGeuodamP6uYu5qAt/U1BdOIzr+KfH 22 | JSkp17lr1omP2uVmp8KDubAicSoDCeDcOb3eUo+V07bdNlxvpZ5+1cxaP9bGSJpC 23 | e2oZX5QCYZk63sy/kT381/GFiHdYlycZi5EMSkc7QOx+hbLRzQKCAQEA9loj8qax 24 | 82j9iJY4yLF/F6gQYBlUN621xh3sq5/o8YoWVTwkmA+mzAmBOXZoLv5nVuPtGqsB 25 | /j7YGSmFlo2zIA2CGtYTCldlaVqYqwSjp4SJy3kl3fsypvQIRBw0UrGbP9n7ZMX3 26 | pTi2BDboVesQDyzz+a/fyVoXRDMTFmoCRy1fl77D7XMec8jnw03jzn3NJJ2Q7FgS 27 | DekO1WTsZ7HNiR9cj3H0JtuI5BZcaiKG0d6VMlPxmfpOesgNThAVaM4D5CL/KhFH 28 | y0fRCRTLXyDMIroFSvUSK1e3nl73a2sw6E5ETJ+6dXqvRXww2S4nyeLd/kqGlifP 29 | V3Z3s3qxNRM/NQKCAQEAvI0nBuWD58ccVtkuIwxvyBSdVuTWZH8Uxnajz7TEkW9I 30 | XKRBMDkeLoSAogdfRZ02AsYNS4rwBrL3ipn0gxv+gG/NdgxfhkT6qB3qOBAxFToh 31 | inRH/yk2xw0B9l3Y+v0ToDzN6RTDXs16sZwZ0FoWQYF2vNYoDMTrsPl6EpkO+wav 32 | s8fjVucSICGNsUv1nYe7boKVReAlRcyPt5u08Boai3WW2q6Zupu6JlPSWYRFZmPD 33 | dznh5yR6ICazhUTN8W7KMBU711Xl5tpDQ0uUyKt9VLIqWLsEAGlqtvjSPxxN5wGk 34 | qdG/9ddm7HSSznfwfX3PiadtxCwq52H6r0Mn8OkEJQKCAQBsCWn8b1hZHDEb5m1u 35 | rlDDSiQsUM9bP6YZBWSWe6GH0/wpUx/lQ+/tSTnPvnrAKTeepoSEDnzoSB0zI79/ 36 | IBNvOh1VsY5WGa/SvTV4wqcWvwxDHJUbvZ8gEqNRY2Ea5uLj5K/YKO2LGszQGlhX 37 | 3aeEUD2KwbONtSHA+fkj+keXeXGTtchs6PBw7KmfQBhopUkDBsrYq2L3kL69nO4E 38 | J7iwqv1HtzzQkbR9+sR6kzu27DtX//JTia9DL1qOYoVRGhAgy6xFgaCPqKYmqmTq 39 | ChMcI6JQlhtqwKQ5IwK0rCSdrD1NsTrvbGdTPLybch9m7URX0c0mKr1GaSDjqCnw 40 | 0ckFAoIBAHXUE854gXzHbgtL/0wByp0TXNvfd6cnz1jS7T2wrqJgE61pLB+xog3Z 41 | 2fTnfH8pZoZHNCnAMo1NK+qemTGRvfKPa6tYwh1LYATNZQASBkpIoItAbXmkTMoJ 42 | c+986Eq4+pnJRbhhtFG7QPBbJ7qPgZcAC66oejC4or1eug2DPtxaalSEFqrJDges 43 | UDq6yEvgdZ3y1svq7f/3fNx13pWpCmuaWWJheEooZSTsfuhYA3Kf55fLQUPMBNGw 44 | vcELpVM6M/nmWxYPZPNl9GDKi7j3igPyrwnyHOcQ+ZJMXj74NiqlWTySik5chMJB 45 | ezixtUUG6ToRukO8gjdEgH8kDYK8a00CggEAV/56KqNrQWf7WYN2N/9qv2tiJ6BU 46 | rvv1Pp0IsVxf+upmhTSpQqEtyF7+tB7FhMdRKNzr4sUUWEZ8F8LoVuzcAbQ7eIHV 47 | pM8Jq49ngudK+AlP+Wz4ZeErkQdsV6XgL5UTpb+olvBlZwccndudJmC05IgXov06 48 | aNr8cO1o5ZlzfbRPZLRiCfMsuTWQdeNr0aPnQbLHZJ7AItx4edZZgCfWRClSrOmp 49 | UTqJoja1lzw3eHndy0cYDWrxjY4BFw24IydbPOcuaHVG2JUvrLlRTZCnx6O90yWD 50 | WfK56YbdhwS2mycgTXf02fjbPssGM/+gbpKeEwIbj3jnHmo8qY2Pu7nT5A== 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /examples/ssl/server.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2023, by Hal Brodigan. 6 | 7 | require 'async' 8 | require 'async/io' 9 | require 'async/io/stream' 10 | 11 | key_file = File.join(__dir__,'key.pem') 12 | cert_file = File.join(__dir__,'cert.crt') 13 | 14 | ssl_context = OpenSSL::SSL::SSLContext.new 15 | ssl_context.key = OpenSSL::PKey::RSA.new(File.read(key_file)) 16 | ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(cert_file)) 17 | 18 | endpoint = Async::IO::Endpoint.ssl('localhost',5678, ssl_context: ssl_context) 19 | 20 | Async do |async| 21 | endpoint.accept do |peer| 22 | stream = Async::IO::Stream.new(peer) 23 | 24 | while line = stream.gets 25 | puts "received: #{line}" 26 | stream.puts "you sent: #{line}" 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /examples/udp.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2021-2023, by Samuel Williams. 6 | 7 | require 'async' 8 | require_relative '../lib/async/io' 9 | 10 | endpoint = Async::IO::Endpoint.udp("localhost", 5300) 11 | 12 | Async do |task| 13 | endpoint.bind do |socket| 14 | # This block executes for both IPv4 and IPv6 UDP sockets: 15 | loop do 16 | data, address = socket.recvfrom(1024) 17 | pp data 18 | pp address 19 | end 20 | end 21 | 22 | # This will try connecting to all addresses and yield for the first one that successfully connects: 23 | endpoint.connect do |socket| 24 | loop do 25 | task.sleep rand(1..10) 26 | socket.send "Hello World!", 0 27 | end 28 | end 29 | end 30 | 31 | -------------------------------------------------------------------------------- /examples/udp/client.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2019-2023, by Samuel Williams. 6 | 7 | require 'async' 8 | require 'async/io' 9 | 10 | endpoint = Async::IO::Endpoint.udp("localhost", 5678) 11 | 12 | Async do |task| 13 | endpoint.connect do |socket| 14 | socket.send("Hello World") 15 | pp socket.recv(1024) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /examples/udp/server.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2019-2023, by Samuel Williams. 6 | 7 | require 'async' 8 | require 'async/io' 9 | 10 | endpoint = Async::IO::Endpoint.udp("localhost", 5678) 11 | 12 | Async do |task| 13 | endpoint.bind do |socket| 14 | while true 15 | data, address = socket.recvfrom(1024) 16 | socket.send(data.reverse, 0, address) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /examples/unix/client.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2023, by Samuel Williams. 6 | 7 | require "socket" 8 | 9 | 10.times do 10 | UNIXSocket.open("./tmp.sock") do |socket| 11 | socket << "hello\n" 12 | p socket.read(6) 13 | socket.close 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /examples/unix/server.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2023, by Samuel Williams. 6 | 7 | require 'async/io' 8 | require 'async/io/unix_endpoint' 9 | 10 | @server = Async::IO::Endpoint.unix("./tmp.sock") 11 | 12 | Async do |task| 13 | @server.accept do |client| 14 | Console.logger.info(client, "Accepted connection") 15 | a = client.read(6) 16 | sleep 1 17 | client.send "elloh\n" 18 | client.close_write 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /gems.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | # Copyright, 2018, by Thibaut Girka. 6 | # Copyright, 2021, by Olle Jonsson. 7 | 8 | source 'https://rubygems.org' 9 | 10 | gemspec 11 | 12 | group :maintenance, optional: true do 13 | gem "bake-modernize" 14 | gem "bake-gem" 15 | 16 | gem "utopia-project" 17 | end 18 | 19 | group :test do 20 | gem "rspec", "~> 3.6" 21 | gem "async-rspec", "~> 1.10" 22 | gem "covered" 23 | 24 | gem "bake" 25 | gem "bake-test" 26 | gem "bake-test-external" 27 | 28 | gem 'benchmark-ips' 29 | 30 | gem 'http' 31 | gem "async-container", "~> 0.15" 32 | gem "rack-test" 33 | end 34 | -------------------------------------------------------------------------------- /gems/async-head.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2023, by Samuel Williams. 5 | 6 | source 'https://rubygems.org' 7 | 8 | gemspec path: "../" 9 | eval_gemfile "../gems.rb" 10 | 11 | gem 'async', git: "https://github.com/socketry/async" 12 | -------------------------------------------------------------------------------- /gems/async-v1.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2023, by Samuel Williams. 5 | 6 | source 'https://rubygems.org' 7 | 8 | gemspec path: "../" 9 | eval_gemfile "../gems.rb" 10 | 11 | gem 'async', '~> 1.0' 12 | -------------------------------------------------------------------------------- /lib/async/io.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2024, by Samuel Williams. 5 | 6 | require 'async' 7 | 8 | require_relative "io/generic" 9 | require_relative "io/socket" 10 | require_relative "io/version" 11 | 12 | require_relative "io/endpoint" 13 | require_relative "io/endpoint/each" 14 | require_relative "io/shared_endpoint" 15 | 16 | module Async 17 | module IO 18 | def self.file_descriptor_limit 19 | Process.getrlimit(Process::RLIMIT_NOFILE).first 20 | end 21 | 22 | def self.buffer? 23 | ::IO.const_defined?(:Buffer) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/async/io/address.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | 6 | require 'socket' 7 | 8 | module Async 9 | module IO 10 | Address = Addrinfo 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/async/io/address_endpoint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | 6 | require_relative 'endpoint' 7 | 8 | module Async 9 | module IO 10 | # This class will open and close the socket automatically. 11 | class AddressEndpoint < Endpoint 12 | def initialize(address, **options) 13 | super(**options) 14 | 15 | @address = address 16 | end 17 | 18 | def to_s 19 | "\#<#{self.class} #{@address.inspect}>" 20 | end 21 | 22 | attr :address 23 | 24 | # Bind a socket to the given address. If a block is given, the socket will be automatically closed when the block exits. 25 | # @yield [Socket] the bound socket 26 | # @return [Socket] the bound socket 27 | def bind(&block) 28 | Socket.bind(@address, **@options, &block) 29 | end 30 | 31 | # Connects a socket to the given address. If a block is given, the socket will be automatically closed when the block exits. 32 | # @return [Socket] the connected socket 33 | def connect(&block) 34 | Socket.connect(@address, **@options, &block) 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/async/io/binary_string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | 6 | require_relative 'buffer' 7 | 8 | module Async 9 | module IO 10 | # This is deprecated. 11 | BinaryString = Buffer 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/async/io/buffer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | 6 | module Async 7 | module IO 8 | class Buffer < String 9 | BINARY = Encoding::BINARY 10 | 11 | def initialize 12 | super 13 | 14 | force_encoding(BINARY) 15 | end 16 | 17 | def << string 18 | if string.encoding == BINARY 19 | super(string) 20 | else 21 | super(string.b) 22 | end 23 | 24 | return self 25 | end 26 | 27 | alias concat << 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/async/io/composite_endpoint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2023, by Samuel Williams. 5 | 6 | require_relative 'endpoint' 7 | 8 | module Async 9 | module IO 10 | class CompositeEndpoint < Endpoint 11 | def initialize(endpoints, **options) 12 | super(**options) 13 | @endpoints = endpoints 14 | end 15 | 16 | def each(&block) 17 | @endpoints.each(&block) 18 | end 19 | 20 | def connect(&block) 21 | error = nil 22 | 23 | @endpoints.each do |endpoint| 24 | begin 25 | return endpoint.connect(&block) 26 | rescue => error 27 | end 28 | end 29 | 30 | raise error 31 | end 32 | 33 | def bind(&block) 34 | @endpoints.map(&:bind) 35 | end 36 | end 37 | 38 | class Endpoint 39 | def self.composite(*endpoints, **options) 40 | CompositeEndpoint.new(endpoints, **options) 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/async/io/endpoint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2024, by Samuel Williams. 5 | # Copyright, 2019, by Olle Jonsson. 6 | 7 | require_relative 'address' 8 | require_relative 'socket' 9 | 10 | require 'uri' 11 | 12 | module Async 13 | module IO 14 | # Endpoints represent a way of connecting or binding to an address. 15 | class Endpoint 16 | def initialize(**options) 17 | @options = options.freeze 18 | end 19 | 20 | def with(**options) 21 | dup = self.dup 22 | 23 | dup.options = @options.merge(options) 24 | 25 | return dup 26 | end 27 | 28 | attr_accessor :options 29 | 30 | # @return [String] The hostname of the bound socket. 31 | def hostname 32 | @options[:hostname] 33 | end 34 | 35 | # If `SO_REUSEPORT` is enabled on a socket, the socket can be successfully bound even if there are existing sockets bound to the same address, as long as all prior bound sockets also had `SO_REUSEPORT` set before they were bound. 36 | # @return [Boolean, nil] The value for `SO_REUSEPORT`. 37 | def reuse_port 38 | @options[:reuse_port] 39 | end 40 | 41 | # If `SO_REUSEADDR` is enabled on a socket prior to binding it, the socket can be successfully bound unless there is a conflict with another socket bound to exactly the same combination of source address and port. Additionally, when set, binding a socket to the address of an existing socket in `TIME_WAIT` is not an error. 42 | # @return [Boolean] The value for `SO_REUSEADDR`. 43 | def reuse_address 44 | @options[:reuse_address] 45 | end 46 | 47 | # Controls SO_LINGER. The amount of time the socket will stay in the `TIME_WAIT` state after being closed. 48 | # @return [Integer, nil] The value for SO_LINGER. 49 | def linger 50 | @options[:linger] 51 | end 52 | 53 | # @return [Numeric] The default timeout for socket operations. 54 | def timeout 55 | @options[:timeout] 56 | end 57 | 58 | # @return [Address] the address to bind to before connecting. 59 | def local_address 60 | @options[:local_address] 61 | end 62 | 63 | # Endpoints sometimes have multiple paths. 64 | # @yield [Endpoint] Enumerate all discrete paths as endpoints. 65 | def each 66 | return to_enum unless block_given? 67 | 68 | yield self 69 | end 70 | 71 | # Accept connections from the specified endpoint. 72 | # @param backlog [Integer] the number of connections to listen for. 73 | def accept(backlog = Socket::SOMAXCONN, &block) 74 | bind do |server| 75 | server.listen(backlog) 76 | 77 | server.accept_each(&block) 78 | end 79 | end 80 | 81 | # Create an Endpoint instance by URI scheme. The host and port of the URI will be passed to the Endpoint factory method, along with any options. 82 | # 83 | # @param string [String] URI as string. Scheme will decide implementation used. 84 | # @param options keyword arguments passed through to {#initialize} 85 | # 86 | # @see Endpoint.ssl ssl - invoked when parsing a URL with the ssl scheme "ssl://127.0.0.1" 87 | # @see Endpoint.tcp tcp - invoked when parsing a URL with the tcp scheme: "tcp://127.0.0.1" 88 | # @see Endpoint.udp udp - invoked when parsing a URL with the udp scheme: "udp://127.0.0.1" 89 | # @see Endpoint.unix unix - invoked when parsing a URL with the unix scheme: "unix://127.0.0.1" 90 | def self.parse(string, **options) 91 | uri = URI.parse(string) 92 | 93 | self.public_send(uri.scheme, uri.host, uri.port, **options) 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/async/io/endpoint/each.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | # Copyright, 2019, by Olle Jonsson. 6 | 7 | require_relative "../host_endpoint" 8 | require_relative "../socket_endpoint" 9 | require_relative "../ssl_endpoint" 10 | 11 | module Async 12 | module IO 13 | class Endpoint 14 | def self.try_convert(specification) 15 | if specification.is_a? self 16 | specification 17 | elsif specification.is_a? Array 18 | self.send(*specification) 19 | elsif specification.is_a? String 20 | self.parse(specification) 21 | elsif specification.is_a? ::BasicSocket 22 | self.socket(specification) 23 | elsif specification.is_a? Generic 24 | self.new(specification) 25 | else 26 | raise ArgumentError.new("Not sure how to convert #{specification} to endpoint!") 27 | end 28 | end 29 | 30 | # Generate a list of endpoints from an array. 31 | def self.each(specifications, &block) 32 | return to_enum(:each, specifications) unless block_given? 33 | 34 | specifications.each do |specification| 35 | yield try_convert(specification) 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/async/io/generic.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | # Copyright, 2021, by Aurora Nockert. 6 | # Copyright, 2023, by Patrik Wenger. 7 | 8 | require 'async/wrapper' 9 | require 'forwardable' 10 | 11 | module Async 12 | module IO 13 | # The default block size for IO buffers. Defaults to 64KB (typical pipe buffer size). 14 | BLOCK_SIZE = ENV.fetch('ASYNC_IO_BLOCK_SIZE', 1024*64).to_i 15 | 16 | # The maximum read size when appending to IO buffers. Defaults to 8MB. 17 | MAXIMUM_READ_SIZE = ENV.fetch('ASYNC_IO_MAXIMUM_READ_SIZE', BLOCK_SIZE * 128).to_i 18 | 19 | # Convert a Ruby ::IO object to a wrapped instance: 20 | def self.try_convert(io, &block) 21 | if wrapper_class = Generic::WRAPPERS[io.class] 22 | wrapper_class.new(io, &block) 23 | else 24 | raise ArgumentError.new("Unsure how to wrap #{io.class}!") 25 | end 26 | end 27 | 28 | def self.pipe 29 | ::IO.pipe.map(&Generic.method(:new)) 30 | end 31 | 32 | # Represents an asynchronous IO within a reactor. 33 | class Generic < Wrapper 34 | extend Forwardable 35 | 36 | WRAPPERS = {} 37 | 38 | class << self 39 | # @!macro [attach] wrap_blocking_method 40 | # @method $1 41 | # Invokes `$2` on the underlying {io}. If the operation would block, the current task is paused until the operation can succeed, at which point it's resumed and the operation is completed. 42 | def wrap_blocking_method(new_name, method_name, invert: true, &block) 43 | if block_given? 44 | define_method(new_name, &block) 45 | else 46 | define_method(new_name) do |*args| 47 | async_send(method_name, *args) 48 | end 49 | end 50 | 51 | if invert 52 | # We wrap the original _nonblock method, ignoring options. 53 | define_method(method_name) do |*args, exception: false| 54 | async_send(method_name, *args) 55 | end 56 | end 57 | end 58 | 59 | attr :wrapped_klass 60 | 61 | def wraps(klass, *additional_methods) 62 | @wrapped_klass = klass 63 | WRAPPERS[klass] = self 64 | 65 | # These are methods implemented by the wrapped class, that we aren't overriding, that may be of interest: 66 | # fallback_methods = klass.instance_methods(false) - instance_methods 67 | # puts "Forwarding #{klass} methods #{fallback_methods} to @io" 68 | 69 | def_delegators :@io, *additional_methods 70 | end 71 | 72 | # Instantiate a wrapped instance of the class, and optionally yield it to a given block, closing it afterwards. 73 | def wrap(*args) 74 | wrapper = self.new(@wrapped_klass.new(*args)) 75 | 76 | return wrapper unless block_given? 77 | 78 | begin 79 | yield wrapper 80 | ensure 81 | wrapper.close 82 | end 83 | end 84 | end 85 | 86 | wraps ::IO, :external_encoding, :internal_encoding, :autoclose?, :autoclose=, :pid, :stat, :binmode, :flush, :set_encoding, :set_encoding_by_bom, :to_path, :to_io, :to_i, :reopen, :fileno, :fsync, :fdatasync, :sync, :sync=, :tell, :seek, :rewind, :path, :pos, :pos=, :eof, :eof?, :close_on_exec?, :close_on_exec=, :closed?, :close_read, :close_write, :isatty, :tty?, :binmode?, :sysseek, :advise, :ioctl, :fcntl, :nread, :ready?, :pread, :pwrite, :pathconf 87 | 88 | # Read the specified number of bytes from the input stream. This is fast path. 89 | # @example 90 | # data = io.sysread(512) 91 | wrap_blocking_method :sysread, :read_nonblock 92 | 93 | alias readpartial read_nonblock 94 | 95 | # Read `length` bytes of data from the underlying I/O. If length is unspecified, read everything. 96 | def read(length = nil, buffer = nil) 97 | if buffer 98 | buffer.clear 99 | else 100 | buffer = String.new 101 | end 102 | 103 | if length 104 | return String.new(encoding: Encoding::BINARY) if length <= 0 105 | 106 | # Fast path: 107 | if buffer = self.sysread(length, buffer) 108 | 109 | # Slow path: 110 | while buffer.bytesize < length 111 | # Slow path: 112 | if chunk = self.sysread(length - buffer.bytesize) 113 | buffer << chunk 114 | else 115 | break 116 | end 117 | end 118 | 119 | return buffer 120 | else 121 | return nil 122 | end 123 | else 124 | buffer = self.sysread(BLOCK_SIZE, buffer) 125 | 126 | while chunk = self.sysread(BLOCK_SIZE) 127 | buffer << chunk 128 | end 129 | 130 | return buffer 131 | end 132 | end 133 | 134 | # Write entire buffer to output stream. This is fast path. 135 | # @example 136 | # io.syswrite("Hello World") 137 | wrap_blocking_method :syswrite, :write_nonblock 138 | 139 | def write(buffer) 140 | # Fast path: 141 | written = self.syswrite(buffer) 142 | remaining = buffer.bytesize - written 143 | 144 | while remaining > 0 145 | # Slow path: 146 | length = self.syswrite(buffer.byteslice(written, remaining)) 147 | 148 | remaining -= length 149 | written += length 150 | end 151 | 152 | return written 153 | end 154 | 155 | def << buffer 156 | write(buffer) 157 | return self 158 | end 159 | 160 | def dup 161 | super.tap do |copy| 162 | copy.timeout = self.timeout 163 | end 164 | end 165 | 166 | def wait(timeout = self.timeout, mode = :read) 167 | case mode 168 | when :read 169 | wait_readable(timeout) 170 | when :write 171 | wait_writable(timeout) 172 | else 173 | wait_any(timeout) 174 | end 175 | rescue TimeoutError 176 | return nil 177 | end 178 | 179 | def nonblock 180 | true 181 | end 182 | 183 | def nonblock= value 184 | true 185 | end 186 | 187 | def nonblock? 188 | true 189 | end 190 | 191 | def connected? 192 | !@io.closed? 193 | end 194 | 195 | def readable? 196 | @io.readable? 197 | end 198 | 199 | attr_accessor :timeout 200 | 201 | protected 202 | 203 | def async_send(*arguments, timeout: self.timeout) 204 | while true 205 | result = @io.__send__(*arguments, exception: false) 206 | 207 | case result 208 | when :wait_readable 209 | wait_readable(timeout) 210 | when :wait_writable 211 | wait_writable(timeout) 212 | else 213 | return result 214 | end 215 | end 216 | end 217 | end 218 | end 219 | end 220 | -------------------------------------------------------------------------------- /lib/async/io/host_endpoint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | # Copyright, 2019, by Olle Jonsson. 6 | # Copyright, 2020, by Benoit Daloze. 7 | 8 | require_relative 'address_endpoint' 9 | 10 | module Async 11 | module IO 12 | class HostEndpoint < Endpoint 13 | def initialize(specification, **options) 14 | super(**options) 15 | 16 | @specification = specification 17 | end 18 | 19 | def to_s 20 | nodename, service, family, socktype, protocol, flags = @specification 21 | 22 | "\#<#{self.class} name=#{nodename.inspect} service=#{service.inspect} family=#{family.inspect} type=#{socktype.inspect} protocol=#{protocol.inspect} flags=#{flags.inspect}>" 23 | end 24 | 25 | def address 26 | @specification 27 | end 28 | 29 | def hostname 30 | @specification.first 31 | end 32 | 33 | # Try to connect to the given host by connecting to each address in sequence until a connection is made. 34 | # @yield [Socket] the socket which is being connected, may be invoked more than once 35 | # @return [Socket] the connected socket 36 | # @raise if no connection could complete successfully 37 | def connect 38 | last_error = nil 39 | 40 | task = Task.current 41 | 42 | Addrinfo.foreach(*@specification) do |address| 43 | begin 44 | wrapper = Socket.connect(address, **@options, task: task) 45 | rescue Errno::ECONNREFUSED, Errno::ENETUNREACH, Errno::EAGAIN 46 | last_error = $! 47 | else 48 | return wrapper unless block_given? 49 | 50 | begin 51 | return yield wrapper, task 52 | ensure 53 | wrapper.close 54 | end 55 | end 56 | end 57 | 58 | raise last_error 59 | end 60 | 61 | # Invokes the given block for every address which can be bound to. 62 | # @yield [Socket] the bound socket 63 | # @return [Array] an array of bound sockets 64 | def bind(&block) 65 | Addrinfo.foreach(*@specification).map do |address| 66 | Socket.bind(address, **@options, &block) 67 | end 68 | end 69 | 70 | # @yield [AddressEndpoint] address endpoints by resolving the given host specification 71 | def each 72 | return to_enum unless block_given? 73 | 74 | Addrinfo.foreach(*@specification) do |address| 75 | yield AddressEndpoint.new(address, **@options) 76 | end 77 | end 78 | end 79 | 80 | class Endpoint 81 | # @param args nodename, service, family, socktype, protocol, flags. `socktype` will be set to Socket::SOCK_STREAM. 82 | # @param options keyword arguments passed on to {HostEndpoint#initialize} 83 | # 84 | # @return [HostEndpoint] 85 | def self.tcp(*args, **options) 86 | args[3] = ::Socket::SOCK_STREAM 87 | 88 | HostEndpoint.new(args, **options) 89 | end 90 | 91 | # @param args nodename, service, family, socktype, protocol, flags. `socktype` will be set to Socket::SOCK_DGRAM. 92 | # @param options keyword arguments passed on to {HostEndpoint#initialize} 93 | # 94 | # @return [HostEndpoint] 95 | def self.udp(*args, **options) 96 | args[3] = ::Socket::SOCK_DGRAM 97 | 98 | HostEndpoint.new(args, **options) 99 | end 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/async/io/notification.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | 6 | require_relative 'generic' 7 | 8 | module Async 9 | module IO 10 | # A cross-reactor/process notification pipe. 11 | class Notification 12 | def initialize 13 | pipe = ::IO.pipe 14 | 15 | # We could call wait and signal from different reactors/threads/processes, so we don't create wrappers here, because they are not thread safe by design. 16 | @input = pipe.first 17 | @output = pipe.last 18 | end 19 | 20 | def close 21 | @input.close 22 | @output.close 23 | end 24 | 25 | # Wait for signal to be called. 26 | # @return [Object] 27 | def wait 28 | wrapper = Async::IO::Generic.new(@input) 29 | wrapper.read(1) 30 | ensure 31 | # Remove the wrapper from the reactor. 32 | wrapper.reactor = nil 33 | end 34 | 35 | # Signal to a given task that it should resume operations. 36 | # @return [void] 37 | def signal 38 | wrapper = Async::IO::Generic.new(@output) 39 | wrapper.write(".") 40 | ensure 41 | wrapper.reactor = nil 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/async/io/peer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2020-2023, by Samuel Williams. 5 | 6 | require 'socket' 7 | 8 | module Async 9 | module IO 10 | module Peer 11 | include ::Socket::Constants 12 | 13 | # Is it likely that the socket is still connected? 14 | # May return false positive, but won't return false negative. 15 | def connected? 16 | return false if @io.closed? 17 | 18 | # If we can wait for the socket to become readable, we know that the socket may still be open. 19 | result = to_io.recv_nonblock(1, MSG_PEEK, exception: false) 20 | 21 | # No data was available - newer Ruby can return nil instead of empty string: 22 | return false if result.nil? 23 | 24 | # Either there was some data available, or we can wait to see if there is data avaialble. 25 | return !result.empty? || result == :wait_readable 26 | 27 | rescue Errno::ECONNRESET 28 | # This might be thrown by recv_nonblock. 29 | return false 30 | end 31 | 32 | def eof 33 | !connected? 34 | end 35 | 36 | def eof? 37 | !connected? 38 | end 39 | 40 | # Best effort to set *_NODELAY if it makes sense. Swallows errors where possible. 41 | def sync=(value) 42 | super 43 | 44 | case self.protocol 45 | when 0, IPPROTO_TCP 46 | self.setsockopt(IPPROTO_TCP, TCP_NODELAY, value ? 1 : 0) 47 | else 48 | Console.logger.warn(self) {"Unsure how to sync=#{value} for #{self.protocol}!"} 49 | end 50 | rescue Errno::EINVAL 51 | # On Darwin, sometimes occurs when the connection is not yet fully formed. Empirically, TCP_NODELAY is enabled despite this result. 52 | rescue Errno::EOPNOTSUPP 53 | # Some platforms may simply not support the operation. 54 | # Console.logger.warn(self) {"Unable to set sync=#{value}!"} 55 | end 56 | 57 | def sync 58 | case self.protocol 59 | when IPPROTO_TCP 60 | self.getsockopt(IPPROTO_TCP, TCP_NODELAY).bool 61 | else 62 | true 63 | end && super 64 | end 65 | 66 | def type 67 | self.local_address.socktype 68 | end 69 | 70 | def protocol 71 | self.local_address.protocol 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/async/io/protocol/generic.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | 6 | require_relative '../stream' 7 | 8 | module Async 9 | module IO 10 | module Protocol 11 | class Generic 12 | def initialize(stream) 13 | @stream = stream 14 | end 15 | 16 | def closed? 17 | @stream.closed? 18 | end 19 | 20 | def close 21 | @stream.close 22 | end 23 | 24 | attr :stream 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/async/io/protocol/line.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | 6 | require_relative 'generic' 7 | 8 | module Async 9 | module IO 10 | module Protocol 11 | class Line < Generic 12 | def initialize(stream, eol = $/) 13 | super(stream) 14 | 15 | @eol = eol 16 | end 17 | 18 | attr :eol 19 | 20 | def write_lines(*args) 21 | if args.empty? 22 | @stream.write(@eol) 23 | else 24 | args.each do |arg| 25 | @stream.write(arg) 26 | @stream.write(@eol) 27 | end 28 | end 29 | 30 | @stream.flush 31 | end 32 | 33 | def read_line 34 | @stream.read_until(@eol) or @stream.eof! 35 | end 36 | 37 | def peek_line 38 | @stream.peek do |read_buffer| 39 | if index = read_buffer.index(@eol) 40 | return read_buffer.slice(0, index) 41 | end 42 | end 43 | 44 | raise EOFError 45 | end 46 | 47 | def each_line 48 | return to_enum(:each_line) unless block_given? 49 | 50 | while line = @stream.read_until(@eol) 51 | yield line 52 | end 53 | end 54 | 55 | def read_lines 56 | @stream.read.split(@eol) 57 | end 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/async/io/server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2020-2023, by Samuel Williams. 5 | 6 | require 'async/task' 7 | 8 | module Async 9 | module IO 10 | module Server 11 | def accept_each(timeout: nil, task: Task.current) 12 | task.annotate "accepting connections #{self.local_address.inspect} [fd=#{self.fileno}]" 13 | 14 | callback = lambda do |io, address| 15 | yield io, address, task: task 16 | end 17 | 18 | while true 19 | self.accept(timeout: timeout, task: task, &callback) 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/async/io/shared_endpoint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2024, by Samuel Williams. 5 | # Copyright, 2019, by Olle Jonsson. 6 | 7 | require_relative 'endpoint' 8 | require_relative 'composite_endpoint' 9 | 10 | module Async 11 | module IO 12 | # Pre-connect and pre-bind sockets so that it can be used between processes. 13 | class SharedEndpoint < Endpoint 14 | # Create a new `SharedEndpoint` by binding to the given endpoint. 15 | def self.bound(endpoint, backlog: Socket::SOMAXCONN, close_on_exec: false, **options) 16 | sockets = Array(endpoint.bind(**options)) 17 | 18 | wrappers = sockets.each do |server| 19 | # This is somewhat optional. We want to have a generic interface as much as possible so that users of this interface can just call it without knowing a lot of internal details. Therefore, we ignore errors here if it's because the underlying socket does not support the operation. 20 | begin 21 | server.listen(backlog) 22 | rescue Errno::EOPNOTSUPP 23 | # Ignore. 24 | end 25 | 26 | server.close_on_exec = close_on_exec 27 | 28 | if server.respond_to?(:reactor=) 29 | server.reactor = nil 30 | end 31 | end 32 | 33 | return self.new(endpoint, wrappers) 34 | end 35 | 36 | # Create a new `SharedEndpoint` by connecting to the given endpoint. 37 | def self.connected(endpoint, close_on_exec: false) 38 | wrapper = endpoint.connect 39 | 40 | wrapper.close_on_exec = close_on_exec 41 | 42 | if wrapper.respond_to?(:reactor=) 43 | wrapper.reactor = nil 44 | end 45 | 46 | return self.new(endpoint, [wrapper]) 47 | end 48 | 49 | def initialize(endpoint, wrappers, **options) 50 | super(**options) 51 | 52 | @endpoint = endpoint 53 | @wrappers = wrappers 54 | end 55 | 56 | attr :endpoint 57 | attr :wrappers 58 | 59 | def local_address_endpoint(**options) 60 | endpoints = @wrappers.map do |wrapper| 61 | # Forward the options to the internal endpoints: 62 | AddressEndpoint.new(wrapper.to_io.local_address, **options) 63 | end 64 | 65 | return CompositeEndpoint.new(endpoints) 66 | end 67 | 68 | def remote_address_endpoint(**options) 69 | endpoints = @wrappers.map do |wrapper| 70 | AddressEndpoint.new(wrapper.to_io.remote_address) 71 | end 72 | 73 | return CompositeEndpoint.new(endpoints, **options) 74 | end 75 | 76 | # Close all the internal wrappers. 77 | def close 78 | @wrappers.each(&:close) 79 | @wrappers.clear 80 | end 81 | 82 | def bind 83 | task = Async::Task.current 84 | 85 | @wrappers.each do |server| 86 | task.async do |task| 87 | task.annotate "binding to #{server.inspect}" 88 | yield server, task 89 | end 90 | end 91 | end 92 | 93 | def connect 94 | task = Async::Task.current 95 | 96 | @wrappers.each do |peer| 97 | peer = peer.dup 98 | 99 | task.async do |task| 100 | task.annotate "connected to #{peer.inspect} [#{peer.fileno}]" 101 | 102 | begin 103 | yield peer, task 104 | ensure 105 | peer.close 106 | end 107 | end 108 | end 109 | end 110 | 111 | def accept(backlog = nil, **options, &block) 112 | bind do |server| 113 | server.accept_each(**options, &block) 114 | end 115 | end 116 | 117 | def to_s 118 | "\#<#{self.class} #{@wrappers.size} descriptors for #{@endpoint}>" 119 | end 120 | end 121 | 122 | class Endpoint 123 | def bound(**options) 124 | SharedEndpoint.bound(self, **options) 125 | end 126 | 127 | def connected(**options) 128 | SharedEndpoint.connected(self, **options) 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/async/io/socket.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | # Copyright, 2018, by Thibaut Girka. 6 | # Copyright, 2022, by Hal Brodigan. 7 | 8 | require 'socket' 9 | require 'async/task' 10 | 11 | require_relative 'peer' 12 | require_relative 'server' 13 | require_relative 'generic' 14 | 15 | module Async 16 | module IO 17 | class BasicSocket < Generic 18 | wraps ::BasicSocket, :setsockopt, :connect_address, :close_read, :close_write, :local_address, :remote_address, :do_not_reverse_lookup, :do_not_reverse_lookup=, :shutdown, :getsockopt, :getsockname, :getpeername, :getpeereid 19 | 20 | wrap_blocking_method :recv, :recv_nonblock 21 | wrap_blocking_method :recvmsg, :recvmsg_nonblock 22 | 23 | wrap_blocking_method :sendmsg, :sendmsg_nonblock 24 | wrap_blocking_method :send, :sendmsg_nonblock, invert: false 25 | 26 | include Peer 27 | end 28 | 29 | class Socket < BasicSocket 30 | wraps ::Socket, :bind, :ipv6only!, :listen 31 | 32 | wrap_blocking_method :recvfrom, :recvfrom_nonblock 33 | 34 | # @raise Errno::EAGAIN the connection failed due to the remote end being overloaded. 35 | def connect(*args) 36 | begin 37 | async_send(:connect_nonblock, *args) 38 | rescue Errno::EISCONN 39 | # We are now connected. 40 | end 41 | end 42 | 43 | alias connect_nonblock connect 44 | 45 | # @param timeout [Numeric] the maximum time to wait for accepting a connection, if specified. 46 | def accept(timeout: nil, task: Task.current) 47 | peer, address = async_send(:accept_nonblock, timeout: timeout) 48 | wrapper = Socket.new(peer, task.reactor) 49 | 50 | wrapper.timeout = self.timeout 51 | 52 | return wrapper, address unless block_given? 53 | 54 | task.async do |task| 55 | task.annotate "incoming connection #{address.inspect} [fd=#{wrapper.fileno}]" 56 | 57 | begin 58 | yield wrapper, address 59 | ensure 60 | wrapper.close 61 | end 62 | end 63 | end 64 | 65 | alias accept_nonblock accept 66 | alias sysaccept accept 67 | 68 | # Build and wrap the underlying io. 69 | # @option reuse_port [Boolean] Allow this port to be bound in multiple processes. 70 | # @option reuse_address [Boolean] Allow this port to be bound in multiple processes. 71 | def self.build(*args, timeout: nil, reuse_address: true, reuse_port: nil, linger: nil, task: Task.current) 72 | socket = wrapped_klass.new(*args) 73 | 74 | if reuse_address 75 | socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) 76 | end 77 | 78 | if reuse_port 79 | socket.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1) 80 | end 81 | 82 | if linger 83 | socket.setsockopt(SOL_SOCKET, SO_LINGER, linger) 84 | end 85 | 86 | yield socket 87 | 88 | wrapper = self.new(socket, task.reactor) 89 | wrapper.timeout = timeout 90 | 91 | return wrapper 92 | rescue Exception 93 | socket.close if socket 94 | 95 | raise 96 | end 97 | 98 | # Establish a connection to a given `remote_address`. 99 | # @example 100 | # socket = Async::IO::Socket.connect(Async::IO::Address.tcp("8.8.8.8", 53)) 101 | # @param remote_address [Address] The remote address to connect to. 102 | # @option local_address [Address] The local address to bind to before connecting. 103 | def self.connect(remote_address, local_address: nil, task: Task.current, **options) 104 | Console.logger.debug(self) {"Connecting to #{remote_address.inspect}"} 105 | 106 | task.annotate "connecting to #{remote_address.inspect}" 107 | 108 | wrapper = build(remote_address.afamily, remote_address.socktype, remote_address.protocol, **options) do |socket| 109 | if local_address 110 | if defined?(IP_BIND_ADDRESS_NO_PORT) 111 | # Inform the kernel (Linux 4.2+) to not reserve an ephemeral port when using bind(2) with a port number of 0. The port will later be automatically chosen at connect(2) time, in a way that allows sharing a source port as long as the 4-tuple is unique. 112 | socket.setsockopt(SOL_IP, IP_BIND_ADDRESS_NO_PORT, 1) 113 | end 114 | 115 | socket.bind(local_address.to_sockaddr) 116 | end 117 | end 118 | 119 | begin 120 | wrapper.connect(remote_address.to_sockaddr) 121 | task.annotate "connected to #{remote_address.inspect} [fd=#{wrapper.fileno}]" 122 | rescue Exception 123 | wrapper.close 124 | raise 125 | end 126 | 127 | return wrapper unless block_given? 128 | 129 | begin 130 | yield wrapper, task 131 | ensure 132 | wrapper.close 133 | end 134 | end 135 | 136 | # Bind to a local address. 137 | # @example 138 | # socket = Async::IO::Socket.bind(Async::IO::Address.tcp("0.0.0.0", 9090)) 139 | # @param local_address [Address] The local address to bind to. 140 | # @option protocol [Integer] The socket protocol to use. 141 | def self.bind(local_address, protocol: 0, task: Task.current, **options, &block) 142 | Console.logger.debug(self) {"Binding to #{local_address.inspect}"} 143 | 144 | wrapper = build(local_address.afamily, local_address.socktype, protocol, **options) do |socket| 145 | socket.bind(local_address.to_sockaddr) 146 | end 147 | 148 | return wrapper unless block_given? 149 | 150 | task.async do |task| 151 | task.annotate "binding to #{wrapper.local_address.inspect}" 152 | 153 | begin 154 | yield wrapper, task 155 | ensure 156 | wrapper.close 157 | end 158 | end 159 | end 160 | 161 | # Bind to a local address and accept connections in a loop. 162 | def self.accept(*args, backlog: SOMAXCONN, &block) 163 | bind(*args) do |server, task| 164 | server.listen(backlog) if backlog 165 | 166 | server.accept_each(task: task, &block) 167 | end 168 | end 169 | 170 | include Server 171 | 172 | def self.pair(*args) 173 | ::Socket.pair(*args).map(&self.method(:new)) 174 | end 175 | end 176 | 177 | class IPSocket < BasicSocket 178 | wraps ::IPSocket, :addr, :peeraddr 179 | 180 | wrap_blocking_method :recvfrom, :recvfrom_nonblock 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /lib/async/io/socket_endpoint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | 6 | require_relative 'endpoint' 7 | 8 | module Async 9 | module IO 10 | # This class doesn't exert ownership over the specified socket, wraps a native ::IO. 11 | class SocketEndpoint < Endpoint 12 | def initialize(socket, **options) 13 | super(**options) 14 | 15 | # This socket should already be in the required state. 16 | @socket = Async::IO.try_convert(socket) 17 | end 18 | 19 | def to_s 20 | "\#<#{self.class} #{@socket.inspect}>" 21 | end 22 | 23 | attr :socket 24 | 25 | def bind(&block) 26 | if block_given? 27 | begin 28 | yield @socket 29 | ensure 30 | @socket.reactor = nil 31 | end 32 | else 33 | return @socket 34 | end 35 | end 36 | 37 | def connect(&block) 38 | if block_given? 39 | begin 40 | yield @socket 41 | ensure 42 | @socket.reactor = nil 43 | end 44 | else 45 | return @socket 46 | end 47 | end 48 | end 49 | 50 | class Endpoint 51 | def self.socket(socket, **options) 52 | SocketEndpoint.new(socket, **options) 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/async/io/ssl_endpoint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2024, by Samuel Williams. 5 | # Copyright, 2019, by Olle Jonsson. 6 | 7 | require_relative 'host_endpoint' 8 | require_relative 'ssl_socket' 9 | 10 | module Async 11 | module IO 12 | class SSLEndpoint < Endpoint 13 | def initialize(endpoint, **options) 14 | super(**options) 15 | 16 | @endpoint = endpoint 17 | 18 | if ssl_context = options[:ssl_context] 19 | @context = build_context(ssl_context) 20 | else 21 | @context = nil 22 | end 23 | end 24 | 25 | def to_s 26 | "\#<#{self.class} #{@endpoint}>" 27 | end 28 | 29 | def address 30 | @endpoint.address 31 | end 32 | 33 | def hostname 34 | @options[:hostname] || @endpoint.hostname 35 | end 36 | 37 | attr :endpoint 38 | attr :options 39 | 40 | def params 41 | @options[:ssl_params] 42 | end 43 | 44 | def build_context(context = OpenSSL::SSL::SSLContext.new) 45 | if params = self.params 46 | context.set_params(params) 47 | end 48 | 49 | context.setup 50 | context.freeze 51 | 52 | return context 53 | end 54 | 55 | def context 56 | @context ||= build_context 57 | end 58 | 59 | # Connect to the underlying endpoint and establish a SSL connection. 60 | # @yield [Socket] the socket which is being connected 61 | # @return [Socket] the connected socket 62 | def bind 63 | if block_given? 64 | @endpoint.bind do |server| 65 | yield SSLServer.new(server, context) 66 | end 67 | else 68 | @endpoint.bind.map do |server| 69 | SSLServer.new(server, context) 70 | end 71 | end 72 | end 73 | 74 | # Connect to the underlying endpoint and establish a SSL connection. 75 | # @yield [Socket] the socket which is being connected 76 | # @return [Socket] the connected socket 77 | def connect(&block) 78 | SSLSocket.connect(@endpoint.connect, context, hostname, &block) 79 | end 80 | 81 | def each 82 | return to_enum unless block_given? 83 | 84 | @endpoint.each do |endpoint| 85 | yield self.class.new(endpoint, **@options) 86 | end 87 | end 88 | end 89 | 90 | # Backwards compatibility. 91 | SecureEndpoint = SSLEndpoint 92 | 93 | class Endpoint 94 | # @param args 95 | # @param ssl_context [OpenSSL::SSL::SSLContext, nil] 96 | # @param hostname [String, nil] 97 | # @param options keyword arguments passed through to {Endpoint.tcp} 98 | # 99 | # @return [SSLEndpoint] 100 | def self.ssl(*args, ssl_context: nil, hostname: nil, **options) 101 | SSLEndpoint.new(self.tcp(*args, **options), ssl_context: ssl_context, hostname: hostname) 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/async/io/ssl_socket.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | 6 | require_relative 'socket' 7 | 8 | require 'openssl' 9 | 10 | module Async 11 | module IO 12 | SSLError = OpenSSL::SSL::SSLError 13 | 14 | # Asynchronous TCP socket wrapper. 15 | class SSLSocket < Generic 16 | wraps OpenSSL::SSL::SSLSocket, :alpn_protocol, :cert, :cipher, :client_ca, :context, :export_keying_material, :finished_message, :peer_finished_message, :getsockopt, :hostname, :hostname=, :npn_protocol, :peer_cert, :peer_cert_chain, :pending, :post_connection_check, :setsockopt, :session, :session=, :session_reused?, :ssl_version, :state, :sync_close, :sync_close=, :sysclose, :verify_result, :tmp_key 17 | 18 | wrap_blocking_method :accept, :accept_nonblock 19 | wrap_blocking_method :connect, :connect_nonblock 20 | 21 | def self.connect(socket, context, hostname = nil, &block) 22 | client = self.new(socket, context) 23 | 24 | # Used for SNI: 25 | if hostname 26 | client.hostname = hostname 27 | end 28 | 29 | begin 30 | client.connect 31 | rescue 32 | # If the connection fails (e.g. certificates are invalid), the caller never sees the socket, so we close it and raise the exception up the chain. 33 | client.close 34 | 35 | raise 36 | end 37 | 38 | return client unless block_given? 39 | 40 | begin 41 | yield client 42 | ensure 43 | client.close 44 | end 45 | end 46 | 47 | include Peer 48 | 49 | def initialize(socket, context) 50 | if socket.is_a?(self.class.wrapped_klass) 51 | super 52 | else 53 | io = self.class.wrapped_klass.new(socket.to_io, context) 54 | if socket.respond_to?(:reactor) 55 | super(io, socket.reactor) 56 | 57 | # We detach the socket from the reactor, otherwise it's possible to add the file descriptor to the selector twice, which is bad. 58 | socket.reactor = nil 59 | else 60 | super(io) 61 | end 62 | 63 | # This ensures that when the internal IO is closed, it also closes the internal socket: 64 | io.sync_close = true 65 | 66 | if socket.respond_to?(:timeout) 67 | @timeout = socket.timeout 68 | end 69 | end 70 | end 71 | 72 | def local_address 73 | @io.to_io.local_address 74 | end 75 | 76 | def remote_address 77 | @io.to_io.remote_address 78 | end 79 | 80 | def close_write 81 | # Invokes SSL_shutdown, which sends a close_notify message to the peer. 82 | @io.__send__(:stop) 83 | end 84 | 85 | def close_read 86 | @io.to_io.shutdown(Socket::SHUT_RD) 87 | end 88 | 89 | def shutdown(how) 90 | @io.flush 91 | @io.to_io.shutdown(how) 92 | end 93 | end 94 | 95 | # We reimplement this from scratch because the native implementation doesn't expose the underlying server/context that we need to implement non-blocking accept. 96 | class SSLServer 97 | extend Forwardable 98 | 99 | def initialize(server, context) 100 | @server = server 101 | @context = context 102 | end 103 | 104 | def fileno 105 | @server.fileno 106 | end 107 | 108 | def dup 109 | self.class.new(@server.dup, @context) 110 | end 111 | 112 | def_delegators :@server, :local_address, :setsockopt, :getsockopt, :close, :close_on_exec=, :reactor=, :timeout, :timeout=, :to_io 113 | 114 | attr :server 115 | attr :context 116 | 117 | def listen(*args) 118 | @server.listen(*args) 119 | end 120 | 121 | def accept(task: Task.current, timeout: nil) 122 | peer, address = @server.accept 123 | 124 | if timeout and peer.respond_to?(:timeout=) 125 | peer.timeout = timeout 126 | end 127 | 128 | wrapper = SSLSocket.new(peer, @context) 129 | 130 | return wrapper, address unless block_given? 131 | 132 | task.async do |task| 133 | task.annotate "accepting secure connection #{address.inspect}" 134 | 135 | begin 136 | # You want to do this in a nested async task or you might suffer from head-of-line blocking. 137 | wrapper.accept 138 | 139 | yield wrapper, address 140 | ensure 141 | wrapper.close 142 | end 143 | end 144 | end 145 | 146 | include Server 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /lib/async/io/standard.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | 6 | require_relative 'generic' 7 | 8 | module Async 9 | module IO 10 | class StandardInput < Generic 11 | def initialize(io = $stdin) 12 | super(io) 13 | end 14 | end 15 | 16 | class StandardOutput < Generic 17 | def initialize(io = $stdout) 18 | super(io) 19 | end 20 | end 21 | 22 | class StandardError < Generic 23 | def initialize(io = $stderr) 24 | super(io) 25 | end 26 | end 27 | 28 | STDIN = StandardInput.new 29 | STDOUT = StandardOutput.new 30 | STDERR = StandardError.new 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/async/io/stream.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | # Copyright, 2018, by Janko Marohnić. 6 | # Copyright, 2021, by Aurora Nockert. 7 | # Copyright, 2023, by Maruth Goyal. 8 | 9 | require_relative 'buffer' 10 | require_relative 'generic' 11 | 12 | require 'async/semaphore' 13 | 14 | module Async 15 | module IO 16 | class Stream 17 | BLOCK_SIZE = IO::BLOCK_SIZE 18 | 19 | def self.open(path, mode = "r+", **options) 20 | stream = self.new(File.open(path, mode), **options) 21 | 22 | return stream unless block_given? 23 | 24 | begin 25 | yield stream 26 | ensure 27 | stream.close 28 | end 29 | end 30 | 31 | def initialize(io, block_size: BLOCK_SIZE, maximum_read_size: MAXIMUM_READ_SIZE, sync: true, deferred: false) 32 | @io = io 33 | @eof = false 34 | 35 | @pending = 0 36 | # This field is ignored, but used to mean, try to buffer packets in a single iteration of the reactor. 37 | # @deferred = deferred 38 | 39 | @writing = Async::Semaphore.new(1) 40 | 41 | # We don't want Ruby to do any IO buffering. 42 | @io.sync = sync 43 | 44 | @block_size = block_size 45 | @maximum_read_size = maximum_read_size 46 | 47 | @read_buffer = Buffer.new 48 | @write_buffer = Buffer.new 49 | @drain_buffer = Buffer.new 50 | 51 | # Used as destination buffer for underlying reads. 52 | @input_buffer = Buffer.new 53 | end 54 | 55 | attr :io 56 | 57 | attr :block_size 58 | 59 | # Reads `size` bytes from the stream. If size is not specified, read until end of file. 60 | def read(size = nil) 61 | return String.new(encoding: Encoding::BINARY) if size == 0 62 | 63 | if size 64 | until @eof or @read_buffer.bytesize >= size 65 | # Compute the amount of data we need to read from the underlying stream: 66 | read_size = size - @read_buffer.bytesize 67 | 68 | # Don't read less than @block_size to avoid lots of small reads: 69 | fill_read_buffer(read_size > @block_size ? read_size : @block_size) 70 | end 71 | else 72 | until @eof 73 | fill_read_buffer 74 | end 75 | end 76 | 77 | return consume_read_buffer(size) 78 | end 79 | 80 | # Read at most `size` bytes from the stream. Will avoid reading from the underlying stream if possible. 81 | def read_partial(size = nil) 82 | return String.new(encoding: Encoding::BINARY) if size == 0 83 | 84 | if !@eof and @read_buffer.empty? 85 | fill_read_buffer 86 | end 87 | 88 | return consume_read_buffer(size) 89 | end 90 | 91 | def read_exactly(size, exception: EOFError) 92 | if buffer = read(size) 93 | if buffer.bytesize != size 94 | raise exception, "could not read enough data" 95 | end 96 | 97 | return buffer 98 | end 99 | 100 | raise exception, "encountered eof while reading data" 101 | end 102 | 103 | def readpartial(size = nil) 104 | read_partial(size) or raise EOFError, "Encountered eof while reading data!" 105 | end 106 | 107 | # Efficiently read data from the stream until encountering pattern. 108 | # @param pattern [String] The pattern to match. 109 | # @return [String] The contents of the stream up until the pattern, which is consumed but not returned. 110 | def read_until(pattern, offset = 0, chomp: true) 111 | # We don't want to split on the pattern, so we subtract the size of the pattern. 112 | split_offset = pattern.bytesize - 1 113 | 114 | until index = @read_buffer.index(pattern, offset) 115 | offset = @read_buffer.bytesize - split_offset 116 | 117 | offset = 0 if offset < 0 118 | 119 | return unless fill_read_buffer 120 | end 121 | 122 | @read_buffer.freeze 123 | matched = @read_buffer.byteslice(0, index+(chomp ? 0 : pattern.bytesize)) 124 | @read_buffer = @read_buffer.byteslice(index+pattern.bytesize, @read_buffer.bytesize) 125 | 126 | return matched 127 | end 128 | 129 | def peek(size = nil) 130 | if size 131 | until @eof or @read_buffer.bytesize >= size 132 | # Compute the amount of data we need to read from the underlying stream: 133 | read_size = size - @read_buffer.bytesize 134 | 135 | # Don't read less than @block_size to avoid lots of small reads: 136 | fill_read_buffer(read_size > @block_size ? read_size : @block_size) 137 | end 138 | return @read_buffer.slice(0, [size, @read_buffer.size].min) 139 | end 140 | until (block_given? && yield(@read_buffer)) or @eof 141 | fill_read_buffer 142 | end 143 | return @read_buffer 144 | end 145 | 146 | def gets(separator = $/, **options) 147 | read_until(separator, **options) 148 | end 149 | 150 | # Flushes buffered data to the stream. 151 | def flush 152 | return if @write_buffer.empty? 153 | 154 | @writing.acquire do 155 | # Flip the write buffer and drain buffer: 156 | @write_buffer, @drain_buffer = @drain_buffer, @write_buffer 157 | 158 | begin 159 | @io.write(@drain_buffer) 160 | ensure 161 | # If the write operation fails, we still need to clear this buffer, and the data is essentially lost. 162 | @drain_buffer.clear 163 | end 164 | end 165 | end 166 | 167 | # Writes `string` to the buffer. When the buffer is full or #sync is true the 168 | # buffer is flushed to the underlying `io`. 169 | # @param string the string to write to the buffer. 170 | # @return the number of bytes appended to the buffer. 171 | def write(string) 172 | @write_buffer << string 173 | 174 | if @write_buffer.bytesize >= @block_size 175 | flush 176 | end 177 | 178 | return string.bytesize 179 | end 180 | 181 | # Writes `string` to the stream and returns self. 182 | def <<(string) 183 | write(string) 184 | 185 | return self 186 | end 187 | 188 | def puts(*arguments, separator: $/) 189 | arguments.each do |argument| 190 | @write_buffer << argument << separator 191 | end 192 | 193 | flush 194 | end 195 | 196 | def connected? 197 | @io.connected? 198 | end 199 | 200 | def readable? 201 | @io.readable? 202 | end 203 | 204 | def closed? 205 | @io.closed? 206 | end 207 | 208 | def close_read 209 | @io.close_read 210 | end 211 | 212 | def close_write 213 | flush 214 | ensure 215 | @io.close_write 216 | end 217 | 218 | # Best effort to flush any unwritten data, and then close the underling IO. 219 | def close 220 | return if @io.closed? 221 | 222 | begin 223 | flush 224 | rescue 225 | # We really can't do anything here unless we want #close to raise exceptions. 226 | ensure 227 | @io.close 228 | end 229 | end 230 | 231 | # Returns true if the stream is at file which means there is no more data to be read. 232 | def eof? 233 | if !@read_buffer.empty? 234 | return false 235 | elsif @eof 236 | return true 237 | else 238 | return @io.eof? 239 | end 240 | end 241 | 242 | alias eof eof? 243 | 244 | def eof! 245 | @read_buffer.clear 246 | @eof = true 247 | 248 | raise EOFError 249 | end 250 | 251 | private 252 | 253 | def sysread(size, buffer) 254 | while true 255 | result = @io.read_nonblock(size, buffer, exception: false) 256 | 257 | case result 258 | when :wait_readable 259 | @io.wait_readable 260 | when :wait_writable 261 | @io.wait_writable 262 | else 263 | return result 264 | end 265 | end 266 | end 267 | 268 | # Fills the buffer from the underlying stream. 269 | def fill_read_buffer(size = @block_size) 270 | # We impose a limit because the underlying `read` system call can fail if we request too much data in one go. 271 | if size > @maximum_read_size 272 | size = @maximum_read_size 273 | end 274 | 275 | # This effectively ties the input and output stream together. 276 | flush 277 | 278 | if @read_buffer.empty? 279 | if sysread(size, @read_buffer) 280 | # Console.logger.debug(self, name: "read") {@read_buffer.inspect} 281 | return true 282 | end 283 | else 284 | if chunk = sysread(size, @input_buffer) 285 | @read_buffer << chunk 286 | # Console.logger.debug(self, name: "read") {@read_buffer.inspect} 287 | 288 | return true 289 | end 290 | end 291 | 292 | # else for both cases above: 293 | @eof = true 294 | return false 295 | end 296 | 297 | # Consumes at most `size` bytes from the buffer. 298 | # @param size [Integer|nil] The amount of data to consume. If nil, consume entire buffer. 299 | def consume_read_buffer(size = nil) 300 | # If we are at eof, and the read buffer is empty, we can't consume anything. 301 | return nil if @eof && @read_buffer.empty? 302 | 303 | result = nil 304 | 305 | if size.nil? or size >= @read_buffer.bytesize 306 | # Consume the entire read buffer: 307 | result = @read_buffer 308 | @read_buffer = Buffer.new 309 | else 310 | # This approach uses more memory. 311 | # result = @read_buffer.slice!(0, size) 312 | 313 | # We know that we are not going to reuse the original buffer. 314 | # But byteslice will generate a hidden copy. So let's freeze it first: 315 | @read_buffer.freeze 316 | 317 | result = @read_buffer.byteslice(0, size) 318 | @read_buffer = @read_buffer.byteslice(size, @read_buffer.bytesize) 319 | end 320 | 321 | return result 322 | end 323 | end 324 | end 325 | end 326 | -------------------------------------------------------------------------------- /lib/async/io/tcp_socket.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | # Copyright, 2018, by Jiang Jinyang. 6 | 7 | require_relative 'socket' 8 | require_relative 'stream' 9 | require 'fcntl' 10 | 11 | module Async 12 | module IO 13 | # Asynchronous TCP socket wrapper. 14 | class TCPSocket < IPSocket 15 | wraps ::TCPSocket 16 | 17 | def initialize(remote_host, remote_port = nil, local_host = nil, local_port = nil) 18 | if remote_host.is_a? ::TCPSocket 19 | super(remote_host) 20 | else 21 | remote_address = Addrinfo.tcp(remote_host, remote_port) 22 | local_address = Addrinfo.tcp(local_host, local_port) if local_host 23 | 24 | # We do this unusual dance to avoid leaking an "open" socket instance. 25 | socket = Socket.connect(remote_address, local_address: local_address) 26 | fd = socket.fcntl(Fcntl::F_DUPFD) 27 | Console.logger.debug(self) {"Connected to #{remote_address.inspect}: #{fd}"} 28 | socket.close 29 | 30 | super(::TCPSocket.for_fd(fd)) 31 | 32 | # The equivalent blocking operation. Unfortunately there is no trivial way to make this non-blocking. 33 | # super(::TCPSocket.new(remote_host, remote_port, local_host, local_port)) 34 | end 35 | 36 | @stream = Stream.new(self) 37 | end 38 | 39 | class << self 40 | alias open new 41 | end 42 | 43 | def close 44 | @stream.flush 45 | super 46 | end 47 | 48 | include Peer 49 | 50 | attr :stream 51 | 52 | # The way this buffering works is pretty atrocious. 53 | def_delegators :@stream, :gets, :puts 54 | 55 | def sysread(size, buffer = nil) 56 | data = @stream.read_partial(size) 57 | 58 | if buffer 59 | buffer.replace(data) 60 | end 61 | 62 | return data 63 | end 64 | end 65 | 66 | # Asynchronous TCP server wrappper. 67 | class TCPServer < TCPSocket 68 | wraps ::TCPServer, :listen 69 | 70 | def initialize(*args) 71 | if args.first.is_a? ::TCPServer 72 | super(args.first) 73 | else 74 | # We assume this operation doesn't block (for long): 75 | super(::TCPServer.new(*args)) 76 | end 77 | end 78 | 79 | def accept(timeout: nil, task: Task.current) 80 | peer, address = async_send(:accept_nonblock, timeout: timeout) 81 | 82 | wrapper = TCPSocket.new(peer) 83 | 84 | wrapper.timeout = self.timeout 85 | 86 | return wrapper, address unless block_given? 87 | 88 | begin 89 | yield wrapper, address 90 | ensure 91 | wrapper.close 92 | end 93 | end 94 | 95 | alias accept_nonblock accept 96 | alias sysaccept accept 97 | 98 | include Server 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/async/io/threads.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2020-2023, by Samuel Williams. 5 | 6 | require_relative 'notification' 7 | 8 | module Async 9 | module IO 10 | class Threads 11 | def initialize(parent: nil) 12 | @parent = parent 13 | end 14 | 15 | if Async::Scheduler.supported? 16 | def async(parent: (@parent or Task.current)) 17 | parent.async do 18 | thread = ::Thread.new do 19 | yield 20 | end 21 | 22 | thread.join 23 | rescue Stop 24 | if thread&.alive? 25 | thread.raise(Stop) 26 | end 27 | 28 | begin 29 | thread.join 30 | rescue Stop 31 | # Ignore. 32 | end 33 | end 34 | end 35 | else 36 | def async(parent: (@parent or Task.current)) 37 | parent.async do |task| 38 | notification = Async::IO::Notification.new 39 | 40 | thread = ::Thread.new do 41 | yield 42 | ensure 43 | notification.signal 44 | end 45 | 46 | task.annotate "Waiting for thread to finish..." 47 | 48 | notification.wait 49 | 50 | thread.value 51 | ensure 52 | if thread&.alive? 53 | thread.raise(Stop) 54 | 55 | begin 56 | thread.join 57 | rescue Stop 58 | # Ignore. 59 | end 60 | end 61 | 62 | notification&.close 63 | end 64 | end 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/async/io/trap.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | 6 | require_relative 'notification' 7 | 8 | require 'thread' 9 | 10 | module Async 11 | module IO 12 | # A cross-reactor/process notification pipe. 13 | class Trap 14 | def initialize(name) 15 | @name = name 16 | @notifications = [] 17 | 18 | @installed = false 19 | @mutex = Mutex.new 20 | end 21 | 22 | def to_s 23 | "\#<#{self.class} #{@name}>" 24 | end 25 | 26 | # Ignore the trap within the current process. Can be invoked before forking and/or invoking `install!` to assert default behaviour. 27 | def ignore! 28 | Signal.trap(@name, :IGNORE) 29 | end 30 | 31 | def default! 32 | Signal.trap(@name, :DEFAULT) 33 | end 34 | 35 | # Install the trap into the current process. Thread safe. 36 | # @return [Boolean] whether the trap was installed or not. If the trap was already installed, returns nil. 37 | def install! 38 | return if @installed 39 | 40 | @mutex.synchronize do 41 | return if @installed 42 | 43 | Signal.trap(@name, &self.method(:trigger)) 44 | 45 | @installed = true 46 | end 47 | 48 | return true 49 | end 50 | 51 | # Wait until the signal occurs. If a block is given, execute in a loop. 52 | # @yield [Async::Task] the current task. 53 | def wait(task: Task.current, &block) 54 | task.annotate("waiting for signal #{@name}") 55 | 56 | notification = Notification.new 57 | @notifications << notification 58 | 59 | if block_given? 60 | while true 61 | notification.wait 62 | yield task 63 | end 64 | else 65 | notification.wait 66 | end 67 | ensure 68 | if notification 69 | notification.close 70 | @notifications.delete(notification) 71 | end 72 | end 73 | 74 | # Deprecated. 75 | alias trap wait 76 | 77 | # In order to avoid blocking the reactor, specify `transient: true` as an option. 78 | def async(parent: Task.current, **options, &block) 79 | parent.async(**options) do |task| 80 | self.wait(task: task, &block) 81 | end 82 | end 83 | 84 | # Signal all waiting tasks that the trap occurred. 85 | # @return [void] 86 | def trigger(signal_number = nil) 87 | @notifications.each(&:signal) 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/async/io/udp_socket.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | 6 | require_relative 'socket' 7 | 8 | module Async 9 | module IO 10 | # Asynchronous UDP socket wrapper. 11 | class UDPSocket < IPSocket 12 | wraps ::UDPSocket, :bind 13 | 14 | def initialize(family) 15 | if family.is_a? ::UDPSocket 16 | super(family) 17 | else 18 | super(::UDPSocket.new(family)) 19 | end 20 | end 21 | 22 | # We pass `send` through directly, but in theory it might block. Internally, it uses sendto. 23 | def_delegators :@io, :send, :connect 24 | 25 | # This function is so fucked. Why does `UDPSocket#recvfrom` return the remote address as an array, but `Socket#recfrom` return it as an `Addrinfo`? You should prefer `recvmsg`. 26 | wrap_blocking_method :recvfrom, :recvfrom_nonblock 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/async/io/unix_endpoint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019-2023, by Samuel Williams. 5 | # Copyright, 2019, by Olle Jonsson. 6 | # Copyright, 2023, by Hasan Kumar. 7 | 8 | require_relative 'address_endpoint' 9 | 10 | module Async 11 | module IO 12 | # This class doesn't exert ownership over the specified unix socket and ensures exclusive access by using `flock` where possible. 13 | class UNIXEndpoint < AddressEndpoint 14 | def initialize(path, type, **options) 15 | # I wonder if we should implement chdir behaviour in here if path is longer than 104 characters. 16 | super(Address.unix(path, type), **options) 17 | 18 | @path = path 19 | end 20 | 21 | def to_s 22 | "\#<#{self.class} #{@path.inspect}>" 23 | end 24 | 25 | attr :path 26 | 27 | def bound? 28 | self.connect do 29 | return true 30 | end 31 | rescue Errno::ECONNREFUSED 32 | return false 33 | end 34 | 35 | def bind(&block) 36 | Socket.bind(@address, **@options, &block) 37 | rescue Errno::EADDRINUSE 38 | # If you encounter EADDRINUSE from `bind()`, you can check if the socket is actually accepting connections by attempting to `connect()` to it. If the socket is still bound by an active process, the connection will succeed. Otherwise, it should be safe to `unlink()` the path and try again. 39 | if !bound? 40 | File.unlink(@path) rescue nil 41 | retry 42 | else 43 | raise 44 | end 45 | end 46 | end 47 | 48 | class Endpoint 49 | # @param path [String] 50 | # @param type Socket type 51 | # @param options keyword arguments passed through to {UNIXEndpoint#initialize} 52 | # 53 | # @return [UNIXEndpoint] 54 | def self.unix(path = "", type = ::Socket::SOCK_STREAM, **options) 55 | UNIXEndpoint.new(path, type, **options) 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/async/io/unix_socket.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | 6 | require_relative 'socket' 7 | 8 | module Async 9 | module IO 10 | class UNIXSocket < BasicSocket 11 | # `send_io`, `recv_io` and `recvfrom` may block but no non-blocking implementation available. 12 | wraps ::UNIXSocket, :path, :addr, :peeraddr, :send_io, :recv_io, :recvfrom 13 | 14 | include Peer 15 | end 16 | 17 | class UNIXServer < UNIXSocket 18 | wraps ::UNIXServer, :listen 19 | 20 | def accept 21 | peer = async_send(:accept_nonblock) 22 | wrapper = UNIXSocket.new(peer, self.reactor) 23 | 24 | return wrapper unless block_given? 25 | 26 | begin 27 | yield wrapper 28 | ensure 29 | wrapper.close 30 | end 31 | end 32 | 33 | alias sysaccept accept 34 | alias accept_nonblock accept 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/async/io/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2024, by Samuel Williams. 5 | 6 | module Async 7 | module IO 8 | VERSION = "1.43.2" 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright, 2017-2024, by Samuel Williams. 4 | Copyright, 2017-2023, by Olle Jonsson. 5 | Copyright, 2018, by Janko Marohnić. 6 | Copyright, 2018, by Jiang Jinyang. 7 | Copyright, 2018, by Thibaut Girka. 8 | Copyright, 2019-2020, by Benoit Daloze. 9 | Copyright, 2020, by Cyril Roelandt. 10 | Copyright, 2020, by Bruno Sutic. 11 | Copyright, 2021, by Aurora Nockert. 12 | Copyright, 2022-2023, by Hal Brodigan. 13 | Copyright, 2023, by Hasan Kumar. 14 | Copyright, 2023, by Maruth Goyal. 15 | Copyright, 2023, by Patrik Wenger. 16 | 17 | Permission is hereby granted, free of charge, to any person obtaining a copy 18 | of this software and associated documentation files (the "Software"), to deal 19 | in the Software without restriction, including without limitation the rights 20 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 21 | copies of the Software, and to permit persons to whom the Software is 22 | furnished to do so, subject to the following conditions: 23 | 24 | The above copyright notice and this permission notice shall be included in all 25 | copies or substantial portions of the Software. 26 | 27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 30 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 32 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 33 | SOFTWARE. 34 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Async::IO 2 | 3 | > [!CAUTION] 4 | > This library is deprecated and should not be used in new projects. Instead, you should use for the endpoint-related functionality, and for stream/buffering functionality. 5 | 6 | Async::IO provides builds on [async](https://github.com/socketry/async) and provides asynchronous wrappers for `IO`, `Socket`, and related classes. 7 | 8 | [![Development Status](https://github.com/socketry/async-io/workflows/Test/badge.svg)](https://github.com/socketry/async-io/actions?workflow=Test) 9 | 10 | ## Installation 11 | 12 | Add this line to your application's Gemfile: 13 | 14 | ``` ruby 15 | gem 'async-io' 16 | ``` 17 | 18 | And then execute: 19 | 20 | $ bundle 21 | 22 | Or install it yourself as: 23 | 24 | $ gem install async-io 25 | 26 | ## Usage 27 | 28 | Basic echo server (from `spec/async/io/echo_spec.rb`): 29 | 30 | ``` ruby 31 | require 'async/io' 32 | 33 | def echo_server(endpoint) 34 | Async do |task| 35 | # This is a synchronous block within the current task: 36 | endpoint.accept do |client| 37 | # This is an asynchronous block within the current reactor: 38 | data = client.read 39 | 40 | # This produces out-of-order responses. 41 | task.sleep(rand * 0.01) 42 | 43 | client.write(data.reverse) 44 | client.close_write 45 | end 46 | end 47 | end 48 | 49 | def echo_client(endpoint, data) 50 | Async do |task| 51 | endpoint.connect do |peer| 52 | peer.write(data) 53 | peer.close_write 54 | 55 | message = peer.read 56 | 57 | puts "Sent #{data}, got response: #{message}" 58 | end 59 | end 60 | end 61 | 62 | Async do 63 | endpoint = Async::IO::Endpoint.tcp('0.0.0.0', 9000) 64 | 65 | server = echo_server(endpoint) 66 | 67 | 5.times.map do |i| 68 | echo_client(endpoint, "Hello World #{i}") 69 | end.each(&:wait) 70 | 71 | server.stop 72 | end 73 | ``` 74 | 75 | ### Timeouts 76 | 77 | Timeouts add a temporal limit to the execution of your code. If the IO doesn't respond in time, it will fail. Timeouts are high level concerns and you generally shouldn't use them except at the very highest level of your program. 78 | 79 | ``` ruby 80 | message = task.with_timeout(5) do 81 | begin 82 | peer.read 83 | rescue Async::TimeoutError 84 | nil # The timeout was triggered. 85 | end 86 | end 87 | ``` 88 | 89 | Any `yield` operation can cause a timeout to trigger. Non-`async` functions might not timeout because they are outside the scope of `async`. 90 | 91 | #### Wrapper Timeouts 92 | 93 | Asynchronous operations may block forever. You can assign a per-wrapper operation timeout duration. All asynchronous operations will be bounded by this timeout. 94 | 95 | ``` ruby 96 | peer.timeout = 1 97 | peer.read # If this takes more than 1 second, Async::TimeoutError will be raised. 98 | ``` 99 | 100 | The benefit of this approach is that it applies to all operations. Typically, this would be configured by the user, and set to something pretty high, e.g. 120 seconds. 101 | 102 | ### Reading Characters 103 | 104 | This example shows how to read one character at a time as the user presses it on the keyboard, and echos it back out as uppercase: 105 | 106 | ``` ruby 107 | require 'async' 108 | require 'async/io/stream' 109 | require 'io/console' 110 | 111 | $stdin.raw! 112 | $stdin.echo = false 113 | 114 | Async do |task| 115 | stdin = Async::IO::Stream.new( 116 | Async::IO::Generic.new($stdin) 117 | ) 118 | 119 | while character = stdin.read(1) 120 | $stdout.write character.upcase 121 | end 122 | end 123 | ``` 124 | 125 | ### Deferred Buffering 126 | 127 | `Async::IO::Stream.new(..., deferred:true)` creates a deferred stream which increases latency slightly, but reduces the number of total packets sent. It does this by combining all calls `Stream#flush` within a single iteration of the reactor. This is typically more useful on the client side, but can also be useful on the server side when individual packets have high latency. It should be preferable to send one 100 byte packet than 10x 10 byte packets. 128 | 129 | Servers typically only deal with one request per iteartion of the reactor so it's less useful. Clients which make multiple requests can benefit significantly e.g. HTTP/2 clients can merge many requests into a single packet. Because HTTP/2 recommends disabling Nagle's algorithm, this is often beneficial. 130 | 131 | ## Contributing 132 | 133 | We welcome contributions to this project. 134 | 135 | 1. Fork it. 136 | 2. Create your feature branch (`git checkout -b my-new-feature`). 137 | 3. Commit your changes (`git commit -am 'Add some feature'`). 138 | 4. Push to the branch (`git push origin my-new-feature`). 139 | 5. Create new Pull Request. 140 | 141 | ### Developer Certificate of Origin 142 | 143 | This project uses the [Developer Certificate of Origin](https://developercertificate.org/). All contributors to this project must agree to this document to have their contributions accepted. 144 | 145 | ### Contributor Covenant 146 | 147 | This project is governed by the [Contributor Covenant](https://www.contributor-covenant.org/). All contributors and participants agree to abide by its terms. 148 | 149 | ## See Also 150 | 151 | - [async](https://github.com/socketry/async) — Asynchronous event-driven reactor. 152 | - [async-process](https://github.com/socketry/async-process) — Asynchronous process spawning/waiting. 153 | - [async-websocket](https://github.com/socketry/async-websocket) — Asynchronous client and server websockets. 154 | - [async-dns](https://github.com/socketry/async-dns) — Asynchronous DNS resolver and server. 155 | - [async-rspec](https://github.com/socketry/async-rspec) — Shared contexts for running async specs. 156 | -------------------------------------------------------------------------------- /release.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11 3 | ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK 4 | CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz 5 | MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd 6 | MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj 7 | bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB 8 | igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2 9 | 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW 10 | sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE 11 | e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN 12 | XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss 13 | RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn 14 | tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM 15 | zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW 16 | xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O 17 | BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs 18 | aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs 19 | aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE 20 | cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl 21 | xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/ 22 | c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp 23 | 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws 24 | JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP 25 | eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt 26 | Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8 27 | voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg= 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /spec/addrinfo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | 6 | class Addrinfo 7 | def == other 8 | self.to_s == other.to_s 9 | end 10 | 11 | def != other 12 | self.to_s != other.to_s 13 | end 14 | 15 | def <=> other 16 | self.to_s <=> other.to_s 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/async/io/buffer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | 6 | require 'async/io/buffer' 7 | 8 | RSpec.describe Async::IO::Buffer do 9 | include_context Async::RSpec::Memory 10 | 11 | let!(:string) {"Hello World!".b} 12 | subject! {described_class.new} 13 | 14 | it "should be binary encoding" do 15 | expect(subject.encoding).to be Encoding::BINARY 16 | end 17 | 18 | it "should not allocate strings when concatenating" do 19 | expect do 20 | subject << string 21 | end.to limit_allocations.of(String, size: 0, count: 0) 22 | end 23 | 24 | it "can append unicode strings to binary buffer" do 25 | 2.times do 26 | subject << "Føøbar" 27 | end 28 | 29 | expect(subject).to eq "FøøbarFøøbar".b 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/async/io/c10k_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | # Copyright, 2020, by Benoit Daloze. 6 | 7 | require 'async/io' 8 | require 'benchmark' 9 | require 'open3' 10 | 11 | # require 'ruby-prof' 12 | 13 | RSpec.describe "c10k echo client/server", if: Process.respond_to?(:fork) do 14 | # macOS has a rediculously hard time to do this. 15 | # sudo sysctl -w net.inet.ip.portrange.first=10000 16 | # sudo sysctl -w net.inet.ip.portrange.hifirst=10000 17 | # Probably due to the use of select. 18 | 19 | let(:repeats) do 20 | if limit = Async::IO.file_descriptor_limit 21 | if limit > 1024*10 22 | 10_000 23 | else 24 | [1, limit - 100].max 25 | end 26 | else 27 | 10_000 28 | end 29 | end 30 | 31 | let(:server_address) {Async::IO::Address.tcp('0.0.0.0', 10101)} 32 | 33 | def echo_server(server_address) 34 | Async do |task| 35 | connections = [] 36 | 37 | Async::IO::Socket.bind(server_address) do |server| 38 | server.listen(Socket::SOMAXCONN) 39 | 40 | while connections.size < repeats 41 | peer, address = server.accept 42 | connections << peer 43 | end 44 | end.wait 45 | 46 | Console.logger.info("Releasing #{connections.size} connections...") 47 | 48 | while connection = connections.pop 49 | connection.write(".") 50 | connection.close 51 | end 52 | end 53 | end 54 | 55 | def echo_client(server_address, data, responses) 56 | Async do |task| 57 | begin 58 | Async::IO::Socket.connect(server_address) do |peer| 59 | responses << peer.read(1) 60 | end 61 | rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EADDRINUSE 62 | Console.logger.warn(data, $!) 63 | # If the connection was refused, it means the server probably can't accept connections any faster than it currently is, so we simply retry. 64 | retry 65 | end 66 | end 67 | end 68 | 69 | def fork_server 70 | pid = fork do 71 | # profile = RubyProf::Profile.new(merge_fibers: true) 72 | # profile.start 73 | 74 | echo_server(server_address) 75 | # ensure 76 | # result = profile.stop 77 | # printer = RubyProf::FlatPrinter.new(result) 78 | # printer.print(STDOUT) 79 | end 80 | 81 | yield 82 | ensure 83 | Process.kill(:KILL, pid) 84 | Process.wait(pid) 85 | end 86 | 87 | around(:each) do |example| 88 | duration = Benchmark.realtime do 89 | example.run 90 | end 91 | 92 | example.reporter.message "Handled #{repeats} connections in #{duration.round(2)}s: #{(repeats/duration).round(2)}req/s" 93 | end 94 | 95 | it "should wait until all clients are connected" do 96 | fork_server do 97 | # profile = RubyProf::Profile.new(merge_fibers: true) 98 | # profile.start 99 | 100 | Async do |task| 101 | responses = [] 102 | 103 | tasks = repeats.times.map do |i| 104 | # puts "Starting client #{i} on #{task}..." if (i % 1000) == 0 105 | 106 | echo_client(server_address, "Hello World #{i}", responses) 107 | end 108 | 109 | # task.reactor.print_hierarchy 110 | 111 | tasks.each(&:wait) 112 | 113 | expect(responses.size).to be repeats 114 | end 115 | 116 | # ensure 117 | # result = profile.stop 118 | # printer = RubyProf::FlatPrinter.new(result) 119 | # printer.print(STDOUT) 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /spec/async/io/echo_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | # Copyright, 2020, by Benoit Daloze. 6 | 7 | require 'async/io' 8 | 9 | RSpec.describe "echo client/server" do 10 | include_context Async::RSpec::Reactor 11 | 12 | let(:server_address) {Async::IO::Address.tcp('0.0.0.0', 9002)} 13 | 14 | def echo_server(server_address) 15 | Async do |task| 16 | # This is a synchronous block within the current task: 17 | Async::IO::Socket.accept(server_address) do |client| 18 | # This is an asynchronous block within the current reactor: 19 | data = client.read(512) 20 | 21 | # This produces out-of-order responses. 22 | task.sleep(rand * 0.01) 23 | 24 | client.write(data) 25 | end 26 | end 27 | end 28 | 29 | def echo_client(server_address, data, responses) 30 | Async do |task| 31 | Async::IO::Socket.connect(server_address) do |peer| 32 | result = peer.write(data) 33 | peer.close_write 34 | 35 | message = peer.read(data.bytesize) 36 | 37 | responses << message 38 | end 39 | end 40 | end 41 | 42 | let(:repeats) {10} 43 | 44 | it "should echo several messages" do 45 | server = echo_server(server_address) 46 | responses = [] 47 | 48 | tasks = repeats.times.map do |i| 49 | echo_client(server_address, "Hello World #{i}", responses) 50 | end 51 | 52 | # task.reactor.print_hierarchy 53 | 54 | tasks.each(&:wait) 55 | server.stop 56 | 57 | expect(responses.size).to be repeats 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/async/io/endpoint_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | 6 | require 'async/io/endpoint' 7 | 8 | require 'async/io/tcp_socket' 9 | require 'async/io/socket_endpoint' 10 | require 'async/io/ssl_endpoint' 11 | 12 | RSpec.describe Async::IO::Endpoint do 13 | include_context Async::RSpec::Reactor 14 | 15 | describe Async::IO::Endpoint.ssl('0.0.0.0', 5234, hostname: "lolcathost") do 16 | it "should have hostname" do 17 | expect(subject.hostname).to be == "lolcathost" 18 | end 19 | 20 | it "shouldn't have a timeout duration" do 21 | expect(subject.timeout).to be_nil 22 | end 23 | end 24 | 25 | describe Async::IO::Endpoint.tcp('0.0.0.0', 5234, reuse_port: true, timeout: 10) do 26 | it "should be a tcp binding" do 27 | subject.bind do |server| 28 | expect(server.local_address.socktype).to be == ::Socket::SOCK_STREAM 29 | end 30 | end 31 | 32 | it "should have a timeout duration" do 33 | expect(subject.timeout).to be 10 34 | end 35 | 36 | it "should print nicely" do 37 | expect(subject.to_s).to include('0.0.0.0', '5234') 38 | end 39 | 40 | it "has options" do 41 | expect(subject.options[:reuse_port]).to be true 42 | end 43 | 44 | it "has hostname" do 45 | expect(subject.hostname).to be == '0.0.0.0' 46 | end 47 | 48 | it "has local address" do 49 | address = Async::IO::Address.tcp('127.0.0.1', 8080) 50 | expect(subject.with(local_address: address).local_address).to be == address 51 | end 52 | 53 | let(:message) {"Hello World!"} 54 | 55 | it "can connect to bound server" do 56 | server_task = reactor.async do 57 | subject.accept do |io| 58 | expect(io.timeout).to be == 10 59 | io.write message 60 | io.close 61 | end 62 | end 63 | 64 | io = subject.connect 65 | expect(io.timeout).to be == 10 66 | expect(io.read(message.bytesize)).to be == message 67 | io.close 68 | 69 | server_task.stop 70 | end 71 | end 72 | 73 | describe Async::IO::Endpoint.tcp('0.0.0.0', 0) do 74 | it "should be a tcp binding" do 75 | subject.bind do |server| 76 | expect(server.local_address.ip_port).to be > 10000 77 | end 78 | end 79 | end 80 | 81 | describe Async::IO::SocketEndpoint.new(TCPServer.new('0.0.0.0', 1234)) do 82 | it "should bind to given socket" do 83 | subject.bind do |server| 84 | expect(server).to be == subject.socket 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /spec/async/io/generic_examples.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | # Copyright, 2021, by Aurora Nockert. 6 | 7 | RSpec.shared_examples Async::IO::Generic do |ignore_methods| 8 | let(:instance_methods) {described_class.wrapped_klass.public_instance_methods(false) - (ignore_methods || [])} 9 | let(:wrapped_instance_methods) {described_class.public_instance_methods} 10 | 11 | it "should wrap a class" do 12 | expect(described_class.wrapped_klass).to_not be_nil 13 | end 14 | 15 | it "should wrap underlying instance methods" do 16 | expect(wrapped_instance_methods.sort).to include(*instance_methods.sort) 17 | end 18 | 19 | # This needs to be reviewed in more detail. 20 | # 21 | # let(:singleton_methods) {described_class.wrapped_klass.singleton_methods(false)} 22 | # let(:wrapped_singleton_methods) {described_class.singleton_methods(false)} 23 | # 24 | # it "should wrap underlying class methods" do 25 | # singleton_methods.each do |method| 26 | # expect(wrapped_singleton_methods).to include(method) 27 | # end 28 | # end 29 | end 30 | 31 | RSpec.shared_examples Async::IO do 32 | let(:data) {"Hello World!"} 33 | 34 | it "should read data" do 35 | io.write(data) 36 | expect(subject.read(data.bytesize)).to be == data 37 | end 38 | 39 | it "should read less than available data" do 40 | io.write(data) 41 | expect(subject.read(1)).to be == data[0] 42 | end 43 | 44 | it "should read all available data" do 45 | io.write(data) 46 | io.close_write 47 | 48 | expect(subject.read(data.bytesize * 2)).to be == data 49 | end 50 | 51 | it "should read all available data" do 52 | io.write(data) 53 | io.close_write 54 | 55 | expect(subject.read).to be == data 56 | end 57 | 58 | context "has the right encoding" do 59 | it "with a normal read" do 60 | io.write(data) 61 | expect(subject.read(1).encoding).to be == Encoding::BINARY 62 | end 63 | 64 | it "with a zero-length read" do 65 | expect(subject.read(0).encoding).to be == Encoding::BINARY 66 | end 67 | end 68 | 69 | context "are not frozen" do 70 | it "with a normal read" do 71 | io.write(data) 72 | expect(subject.read(1).frozen?).to be == false 73 | end 74 | 75 | it "with a zero-length read" do 76 | expect(subject.read(0).frozen?).to be == false 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/async/io/generic_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | # Copyright, 2023, by Patrik Wenger. 6 | 7 | require 'async/io' 8 | require 'async/clock' 9 | 10 | require_relative 'generic_examples' 11 | 12 | RSpec.describe Async::IO::Generic do 13 | include_context Async::RSpec::Reactor 14 | 15 | CONSOLE_METHODS = [:beep, :cooked, :cooked!, :cursor, :cursor=, :echo=, :echo?,:getch, :getpass, :goto, :iflush, :ioflush, :noecho, :oflush,:pressed?, :raw, :raw!, :winsize, :winsize=] 16 | # On TruffleRuby, IO#encode_with needs to be defined for YAML.dump as a public method, allow it 17 | ignore = [:encode_with, :check_winsize_changed, :clear_screen, :console_mode, :console_mode=, :cursor_down, :cursor_left, :cursor_right, :cursor_up, :erase_line, :erase_screen, :goto_column, :scroll_backward, :scroll_forward, :wait_priority] 18 | 19 | it_should_behave_like Async::IO::Generic, [ 20 | :bytes, :chars, :codepoints, :each, :each_byte, :each_char, :each_codepoint, :each_line, :getbyte, :getc, :gets, :lineno, :lineno=, :lines, :print, :printf, :putc, :puts, :readbyte, :readchar, :readline, :readlines, :ungetbyte, :ungetc 21 | ] + CONSOLE_METHODS + ignore 22 | 23 | let(:message) {"Hello World!"} 24 | 25 | let(:pipe) {IO.pipe} 26 | let(:input) {Async::IO::Generic.new(pipe.first)} 27 | let(:output) {Async::IO::Generic.new(pipe.last)} 28 | 29 | it "should send and receive data within the same reactor" do 30 | received = nil 31 | 32 | output_task = reactor.async do 33 | received = input.read(1024) 34 | input.close 35 | end 36 | 37 | reactor.async do 38 | output.write(message) 39 | output.close 40 | end 41 | 42 | output_task.wait 43 | expect(received).to be == message 44 | end 45 | 46 | describe '#wait' do 47 | let(:wait_duration) {0.1} 48 | 49 | it "can wait for :read and :write" do 50 | reader = reactor.async do |task| 51 | duration = Async::Clock.measure do 52 | input.wait(1, :read) 53 | end 54 | 55 | expect(duration).to be >= wait_duration 56 | expect(input.read(1024)).to be == message 57 | 58 | input.close 59 | end 60 | 61 | writer = reactor.async do |task| 62 | duration = Async::Clock.measure do 63 | output.wait(1, :write) 64 | end 65 | 66 | task.sleep(wait_duration) 67 | 68 | output.write(message) 69 | output.close 70 | end 71 | 72 | [reader, writer].each(&:wait) 73 | end 74 | 75 | it "can wait for anything" do 76 | reader = reactor.async do |task| 77 | duration = Async::Clock.measure do 78 | input.wait(1, nil) 79 | end 80 | 81 | expect(duration).to be >= wait_duration 82 | expect(input.read(1024)).to be == message 83 | 84 | input.close 85 | end 86 | 87 | writer = reactor.async do |task| 88 | duration = Async::Clock.measure do 89 | output.wait(1, :write) 90 | end 91 | 92 | task.sleep(wait_duration) 93 | 94 | output.write(message) 95 | output.close 96 | end 97 | 98 | [reader, writer].each(&:wait) 99 | end 100 | 101 | it "can return nil when timeout is exceeded" do 102 | reader = reactor.async do |task| 103 | duration = Async::Clock.measure do 104 | expect(input.wait(wait_duration, :read)).to be_nil 105 | end 106 | 107 | expect(duration).to be >= wait_duration 108 | 109 | input.close 110 | end 111 | 112 | [reader].each(&:wait) 113 | 114 | output.close 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /spec/async/io/notification_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | 6 | require 'async/io/notification' 7 | 8 | RSpec.describe Async::IO::Notification do 9 | include_context Async::RSpec::Reactor 10 | 11 | it "should wait for notification" do 12 | waiting_task = reactor.async do 13 | subject.wait 14 | end 15 | 16 | expect(waiting_task.status).to be :running 17 | 18 | signalling_task = reactor.async do 19 | subject.signal 20 | end 21 | 22 | signalling_task.wait 23 | waiting_task.wait 24 | 25 | expect(waiting_task).to be_complete 26 | 27 | subject.close 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/async/io/protocol/line_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | 6 | require 'async/io/protocol/line' 7 | require 'async/io/socket' 8 | 9 | RSpec.describe Async::IO::Protocol::Line do 10 | include_context Async::RSpec::Reactor 11 | 12 | let(:pipe) {@pipe = Async::IO::Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM)} 13 | let(:remote) {pipe.first} 14 | subject {described_class.new(Async::IO::Stream.new(pipe.last, deferred: true), "\n")} 15 | 16 | after(:each) {defined?(@pipe) && @pipe&.each(&:close)} 17 | 18 | context "default line ending" do 19 | subject {described_class.new(nil)} 20 | 21 | it "should have default eol terminator" do 22 | expect(subject.eol).to_not be_nil 23 | end 24 | end 25 | 26 | describe '#write_lines' do 27 | it "should write line" do 28 | subject.write_lines "Hello World" 29 | subject.close 30 | 31 | expect(remote.read).to be == "Hello World\n" 32 | end 33 | end 34 | 35 | describe '#read_line' do 36 | before(:each) do 37 | remote.write "Hello World\n" 38 | remote.close 39 | end 40 | 41 | it "should read one line" do 42 | expect(subject.read_line).to be == "Hello World" 43 | end 44 | 45 | it "should be binary encoding" do 46 | expect(subject.read_line.encoding).to be == Encoding::BINARY 47 | end 48 | end 49 | 50 | describe '#read_lines' do 51 | before(:each) do 52 | remote.write "Hello\nWorld\n" 53 | remote.close 54 | end 55 | 56 | it "should read multiple lines" do 57 | expect(subject.read_lines).to be == ["Hello", "World"] 58 | end 59 | 60 | it "should be binary encoding" do 61 | expect(subject.read_lines.first.encoding).to be == Encoding::BINARY 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/async/io/shared_endpoint/server_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | # Copyright, 2021, by Olle Jonsson. 6 | 7 | require 'async/io/ssl_socket' 8 | require 'async/rspec/ssl' 9 | 10 | require 'async/io/host_endpoint' 11 | require 'async/io/shared_endpoint' 12 | 13 | require 'async/container' 14 | 15 | RSpec.shared_examples_for Async::IO::SharedEndpoint do |container_class| 16 | include_context Async::RSpec::SSL::VerifiedContexts 17 | include_context Async::RSpec::SSL::ValidCertificate 18 | 19 | let!(:endpoint) {Async::IO::Endpoint.tcp("127.0.0.1", 6781, reuse_port: true)} 20 | let!(:server_endpoint) {Async::IO::SSLEndpoint.new(endpoint, ssl_context: server_context)} 21 | let!(:client_endpoint) {Async::IO::SSLEndpoint.new(endpoint, ssl_context: client_context)} 22 | 23 | let!(:bound_endpoint) do 24 | Async do 25 | Async::IO::SharedEndpoint.bound(server_endpoint) 26 | end.wait 27 | end 28 | 29 | let(:container) {container_class.new} 30 | 31 | it "can use bound endpoint in container" do 32 | container.async do 33 | bound_endpoint.accept do |peer| 34 | peer.write "Hello World" 35 | peer.close 36 | end 37 | end 38 | 39 | Async do 40 | client_endpoint.connect do |peer| 41 | expect(peer.read(11)).to eq "Hello World" 42 | end 43 | end 44 | 45 | container.stop 46 | bound_endpoint.close 47 | end 48 | end 49 | 50 | RSpec.describe Async::Container::Forked, if: Process.respond_to?(:fork) do 51 | it_behaves_like Async::IO::SharedEndpoint, described_class 52 | end 53 | 54 | RSpec.describe Async::Container::Threaded, if: (RUBY_PLATFORM !~ /darwin/ && RUBY_ENGINE != "jruby") do 55 | it_behaves_like Async::IO::SharedEndpoint, described_class 56 | end 57 | -------------------------------------------------------------------------------- /spec/async/io/shared_endpoint_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2024, by Samuel Williams. 5 | 6 | require 'async/io/host_endpoint' 7 | require 'async/io/shared_endpoint' 8 | 9 | RSpec.describe Async::IO::SharedEndpoint do 10 | include_context Async::RSpec::Reactor 11 | 12 | describe '#bound' do 13 | let(:endpoint) {Async::IO::Endpoint.tcp("localhost", 5123, timeout: 10)} 14 | 15 | it "can bind to shared endpoint" do 16 | bound_endpoint = described_class.bound(endpoint) 17 | expect(bound_endpoint.wrappers).to_not be_empty 18 | 19 | wrapper = bound_endpoint.wrappers.first 20 | expect(wrapper).to be_a Async::IO::Socket 21 | expect(wrapper.timeout).to be == endpoint.timeout 22 | expect(wrapper).to_not be_close_on_exec 23 | 24 | bound_endpoint.close 25 | end 26 | 27 | it "can specify close_on_exec" do 28 | bound_endpoint = described_class.bound(endpoint, close_on_exec: true) 29 | expect(bound_endpoint.wrappers).to_not be_empty 30 | 31 | wrapper = bound_endpoint.wrappers.first 32 | expect(wrapper).to be_close_on_exec 33 | 34 | bound_endpoint.close 35 | end 36 | 37 | it "can close a bound endpoint to terminate accept loop" do 38 | bound_endpoint = described_class.bound(endpoint) 39 | expect(bound_endpoint.wrappers).to_not be_empty 40 | 41 | server_task = Async do 42 | bound_endpoint.accept do |io| 43 | io.close 44 | end 45 | end 46 | 47 | connect = proc do 48 | endpoint.connect do |io| 49 | io.write "Hello World" 50 | io.close 51 | end 52 | end 53 | 54 | connect.call 55 | 56 | wrapper = bound_endpoint.wrappers.first 57 | expect(wrapper).to be_a Async::IO::Socket 58 | 59 | bound_endpoint.close 60 | expect(wrapper).to be_closed 61 | 62 | expect do 63 | begin 64 | # Either ECONNRESET or ECONNREFUSED can be raised here. 65 | connect.call 66 | rescue Errno::ECONNRESET 67 | raise Errno::ECONNREFUSED 68 | end 69 | end.to raise_error(Errno::ECONNREFUSED) 70 | end 71 | end 72 | 73 | describe '#connected' do 74 | let(:endpoint) {Async::IO::Endpoint.tcp("localhost", 5124, timeout: 10)} 75 | 76 | it "can connect to shared endpoint" do 77 | server_task = reactor.async do 78 | endpoint.accept do |io| 79 | io.close 80 | end 81 | end 82 | 83 | connected_endpoint = described_class.connected(endpoint) 84 | expect(connected_endpoint.wrappers).to_not be_empty 85 | 86 | wrapper = connected_endpoint.wrappers.first 87 | expect(wrapper).to be_a Async::IO::Socket 88 | expect(wrapper.timeout).to be == endpoint.timeout 89 | expect(wrapper).to_not be_close_on_exec 90 | 91 | connected_endpoint.close 92 | server_task.stop 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /spec/async/io/socket/tcp_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | 6 | require 'async/io/tcp_socket' 7 | require 'async/io/address' 8 | 9 | RSpec.describe Async::IO::Socket do 10 | include_context Async::RSpec::Reactor 11 | 12 | # Shared port for localhost network tests. 13 | let(:server_address) {Async::IO::Address.tcp("127.0.0.1", 6788)} 14 | let(:local_address) {Async::IO::Address.tcp("127.0.0.1", 0)} 15 | let(:data) {"The quick brown fox jumped over the lazy dog."} 16 | 17 | let!(:server_task) do 18 | # Accept a single incoming connection and then finish. 19 | Async::IO::Socket.bind(server_address) do |server| 20 | server.listen(10) 21 | 22 | server.accept do |peer, address| 23 | data = peer.read(512) 24 | peer.write(data) 25 | end 26 | end 27 | end 28 | 29 | describe 'basic tcp server' do 30 | it "should start server and send data" do 31 | Async::IO::Socket.connect(server_address) do |client| 32 | client.write(data) 33 | client.close_write 34 | 35 | expect(client.read(512)).to be == data 36 | end 37 | end 38 | end 39 | 40 | describe 'non-blocking tcp connect' do 41 | it "can specify local address" do 42 | Async::IO::Socket.connect(server_address, local_address: local_address) do |client| 43 | client.write(data) 44 | client.close_write 45 | 46 | expect(client.read(512)).to be == data 47 | end 48 | end 49 | 50 | it "should start server and send data" do 51 | Async::IO::Socket.connect(server_address) do |client| 52 | client.write(data) 53 | client.close_write 54 | 55 | expect(client.read(512)).to be == data 56 | end 57 | end 58 | 59 | it "can connect socket and read/write in a different task" do 60 | socket = Async::IO::Socket.connect(server_address) 61 | 62 | expect(socket).to_not be_nil 63 | expect(socket).to be_kind_of Async::Wrapper 64 | 65 | reactor.async do 66 | socket.write(data) 67 | socket.close_write 68 | 69 | expect(socket.read(512)).to be == data 70 | end.wait 71 | 72 | socket.close 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/async/io/socket/udp_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | 6 | require 'async/io/udp_socket' 7 | 8 | RSpec.describe Async::IO::Socket do 9 | include_context Async::RSpec::Reactor 10 | 11 | # Shared port for localhost network tests. 12 | let!(:server_address) {Async::IO::Address.udp("127.0.0.1", 6778)} 13 | let(:data) {"The quick brown fox jumped over the lazy dog."} 14 | 15 | let!(:server_task) do 16 | reactor.async do 17 | Async::IO::Socket.bind(server_address) do |server| 18 | packet, address = server.recvfrom(512) 19 | 20 | server.send(packet, 0, address) 21 | end 22 | end 23 | end 24 | 25 | describe 'basic udp server' do 26 | it "should echo data back to peer" do 27 | Async::IO::Socket.connect(server_address) do |client| 28 | client.send(data) 29 | response = client.recv(512) 30 | 31 | expect(response).to be == data 32 | end 33 | end 34 | 35 | it "should use unconnected socket" do 36 | Async::IO::UDPSocket.wrap(server_address.afamily) do |client| 37 | client.send(data, 0, server_address) 38 | response, address = client.recvfrom(512) 39 | 40 | expect(response).to be == data 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/async/io/socket_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | # Copyright, 2018, by Thibaut Girka. 6 | 7 | require 'async/io/socket' 8 | require 'async/io/address' 9 | 10 | require_relative 'generic_examples' 11 | 12 | RSpec.describe Async::IO::BasicSocket do 13 | it_should_behave_like Async::IO::Generic 14 | end 15 | 16 | RSpec.describe Async::IO::Socket do 17 | include_context Async::RSpec::Reactor 18 | 19 | it_should_behave_like Async::IO::Generic 20 | 21 | describe '#connect' do 22 | let(:address) {Async::IO::Address.tcp('127.0.0.1', 12345)} 23 | 24 | it "should fail to connect if no listening server" do 25 | expect do 26 | Async::IO::Socket.connect(address) 27 | end.to raise_exception(Errno::ECONNREFUSED) 28 | end 29 | 30 | it "should close the socket when interrupted by a timeout" do 31 | wrapper = double() 32 | expect(Async::IO::Socket).to receive(:build).and_return(wrapper) 33 | expect(wrapper).to receive(:connect).and_raise Async::TimeoutError 34 | expect(wrapper).to receive(:close) 35 | expect do 36 | Async::IO::Socket.connect(address) 37 | end.to raise_exception(Async::TimeoutError) 38 | end 39 | end 40 | 41 | describe '#bind' do 42 | it "should fail to bind to port < 1024" do 43 | address = Async::IO::Address.tcp('127.0.0.1', 1) 44 | 45 | expect do 46 | Async::IO::Socket.bind(address) 47 | end.to raise_exception(Errno::EACCES) 48 | end 49 | 50 | it "can bind to port 0" do 51 | address = Async::IO::Address.tcp('127.0.0.1', 0) 52 | 53 | Async::IO::Socket.bind(address) do |socket| 54 | expect(socket.local_address.ip_port).to be > 10000 55 | 56 | expect(Async::Task.current.annotation).to include("#{socket.local_address.ip_port}") 57 | end 58 | end 59 | end 60 | 61 | describe '#sync' do 62 | it "should set TCP_NODELAY" do 63 | address = Async::IO::Address.tcp('127.0.0.1', 0) 64 | 65 | socket = Async::IO::Socket.wrap(::Socket::AF_INET, ::Socket::SOCK_STREAM, ::Socket::IPPROTO_TCP) 66 | 67 | socket.sync = true 68 | expect(socket.getsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY).bool).to be true 69 | 70 | socket.close 71 | end 72 | end 73 | 74 | describe '#timeout' do 75 | subject{described_class.pair(:UNIX, :STREAM, 0)} 76 | 77 | it "should timeout while waiting to receive data" do 78 | s1, s2 = *subject 79 | 80 | s2.timeout = 1 81 | 82 | expect{s2.recv(32)}.to raise_exception(Async::TimeoutError, "execution expired") 83 | 84 | s1.close 85 | s2.close 86 | end 87 | end 88 | 89 | describe '.pair' do 90 | subject{described_class.pair(:UNIX, :STREAM, 0)} 91 | 92 | it "should be able to send and recv" do 93 | s1, s2 = *subject 94 | 95 | s1.send "Hello World", 0 96 | s1.close 97 | 98 | expect(s2.recv(32)).to be == "Hello World" 99 | s2.close 100 | end 101 | 102 | it "should be connected" do 103 | s1, s2 = *subject 104 | 105 | expect(s1).to be_connected 106 | 107 | s1.close 108 | 109 | expect(s2).to_not be_connected 110 | 111 | s2.close 112 | end 113 | end 114 | 115 | context '.pipe' do 116 | let(:sockets) do 117 | @sockets = described_class.pair(Socket::AF_UNIX, Socket::SOCK_STREAM) 118 | end 119 | 120 | after do 121 | @sockets&.each(&:close) 122 | end 123 | 124 | let(:io) {sockets.first} 125 | subject {sockets.last} 126 | 127 | it_should_behave_like Async::IO 128 | end 129 | end 130 | 131 | RSpec.describe Async::IO::IPSocket do 132 | it_should_behave_like Async::IO::Generic, [:inspect] 133 | end 134 | -------------------------------------------------------------------------------- /spec/async/io/ssl_server_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | # Copyright, 2019, by Benoit Daloze. 6 | 7 | require 'async/io/ssl_socket' 8 | require 'async/io/ssl_endpoint' 9 | 10 | require 'async/rspec/ssl' 11 | require 'async/queue' 12 | 13 | require_relative 'generic_examples' 14 | 15 | RSpec.describe Async::IO::SSLServer do 16 | include_context Async::RSpec::Reactor 17 | 18 | context 'single host' do 19 | include_context Async::RSpec::SSL::VerifiedContexts 20 | include_context Async::RSpec::SSL::ValidCertificate 21 | 22 | let(:endpoint) {Async::IO::Endpoint.tcp("127.0.0.1", 6780, reuse_port: true)} 23 | let(:server_endpoint) {Async::IO::SSLEndpoint.new(endpoint, ssl_context: server_context)} 24 | let(:client_endpoint) {Async::IO::SSLEndpoint.new(endpoint, ssl_context: client_context)} 25 | 26 | let(:data) {"What one programmer can do in one month, two programmers can do in two months."} 27 | 28 | it "can see through to address" do 29 | expect(server_endpoint.address).to be == endpoint.address 30 | end 31 | 32 | it 'can accept_each connections' do 33 | ready = Async::Queue.new 34 | 35 | # Accept a single incoming connection and then finish. 36 | server_task = reactor.async do |task| 37 | server_endpoint.bind do |server| 38 | server.listen(10) 39 | 40 | ready.enqueue(true) 41 | 42 | server.accept_each do |peer, address| 43 | data = peer.read(512) 44 | peer.write(data) 45 | end 46 | end 47 | end 48 | 49 | reactor.async do |task| 50 | ready.dequeue 51 | 52 | client_endpoint.connect do |client| 53 | client.write(data) 54 | client.close_write 55 | 56 | expect(client.read(512)).to be == data 57 | end 58 | 59 | server_task.stop 60 | end.wait 61 | end 62 | end 63 | 64 | context 'multiple hosts' do 65 | let(:hosts) {['test.com', 'example.com']} 66 | 67 | include_context Async::RSpec::SSL::HostCertificates 68 | 69 | let(:endpoint) {Async::IO::Endpoint.tcp("127.0.0.1", 6782, reuse_port: true)} 70 | let(:server_endpoint) {Async::IO::SSLEndpoint.new(endpoint, ssl_context: server_context)} 71 | let(:valid_client_endpoint) {Async::IO::SSLEndpoint.new(endpoint, hostname: 'example.com', ssl_context: client_context)} 72 | let(:invalid_client_endpoint) {Async::IO::SSLEndpoint.new(endpoint, hostname: 'fleeb.com', ssl_context: client_context)} 73 | 74 | let(:data) {"What one programmer can do in one month, two programmers can do in two months."} 75 | 76 | before do 77 | certificates 78 | end 79 | 80 | it 'can select correct host' do 81 | ready = Async::Queue.new 82 | 83 | # Accept a single incoming connection and then finish. 84 | server_task = reactor.async do |task| 85 | server_endpoint.bind do |server| 86 | server.listen(10) 87 | 88 | ready.enqueue(true) 89 | 90 | server.accept_each do |peer, address| 91 | expect(peer.hostname).to be == 'example.com' 92 | 93 | data = peer.read(512) 94 | peer.write(data) 95 | end 96 | end 97 | end 98 | 99 | reactor.async do 100 | ready.dequeue 101 | 102 | valid_client_endpoint.connect do |client| 103 | client.write(data) 104 | client.close_write 105 | 106 | expect(client.read(512)).to be == data 107 | end 108 | 109 | server_task.stop 110 | end.wait 111 | end 112 | 113 | it 'it fails with invalid host' do 114 | ready = Async::Queue.new 115 | 116 | # Accept a single incoming connection and then finish. 117 | server_task = reactor.async do |task| 118 | server_endpoint.bind do |server| 119 | server.listen(10) 120 | 121 | ready.enqueue(true) 122 | 123 | server.accept_each do |peer, address| 124 | peer.close 125 | end 126 | end 127 | end 128 | 129 | reactor.async do 130 | ready.dequeue 131 | 132 | expect do 133 | invalid_client_endpoint.connect do |client| 134 | end 135 | end.to raise_exception(OpenSSL::SSL::SSLError, /handshake failure/) 136 | 137 | server_task.stop 138 | end.wait 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /spec/async/io/ssl_socket_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | 6 | require 'async/io/ssl_endpoint' 7 | require 'async/queue' 8 | require 'async/rspec/ssl' 9 | 10 | require_relative 'generic_examples' 11 | 12 | RSpec.describe Async::IO::SSLSocket do 13 | it_should_behave_like Async::IO::Generic 14 | 15 | describe "#connect" do 16 | include_context Async::RSpec::Reactor 17 | include_context Async::RSpec::SSL::VerifiedContexts 18 | 19 | # Shared port for localhost network tests. 20 | let!(:endpoint) {Async::IO::Endpoint.tcp("127.0.0.1", 6779, reuse_port: true, timeout: 10)} 21 | let!(:server_endpoint) {Async::IO::SSLEndpoint.new(endpoint, ssl_context: server_context, timeout: 20)} 22 | let!(:client_endpoint) {Async::IO::SSLEndpoint.new(endpoint, ssl_context: client_context, timeout: 20)} 23 | 24 | let(:data) {"The quick brown fox jumped over the lazy dog."} 25 | 26 | let!(:server_task) do 27 | ready = Async::Queue.new 28 | 29 | # Accept a single incoming connection and then finish. 30 | reactor.async do |task| 31 | server_endpoint.bind do |server| 32 | ready.enqueue(server) 33 | server.listen(10) 34 | 35 | begin 36 | server.accept do |peer, address| 37 | expect(peer.timeout).to be == 10 38 | 39 | data = peer.read(512) 40 | peer.write(data) 41 | end 42 | rescue OpenSSL::SSL::SSLError 43 | # ignore. 44 | end 45 | end 46 | end 47 | 48 | ready.dequeue 49 | end 50 | 51 | context "with a trusted certificate" do 52 | include_context Async::RSpec::SSL::ValidCertificate 53 | 54 | it "should start server and send data" do 55 | reactor.async do 56 | client_endpoint.connect do |client| 57 | # expect(client).to be_connected 58 | expect(client.timeout).to be == 10 59 | 60 | client.write(data) 61 | client.close_write 62 | 63 | expect(client.read(512)).to be == data 64 | end 65 | end 66 | end 67 | end 68 | 69 | context "with an untrusted certificate" do 70 | include_context Async::RSpec::SSL::InvalidCertificate 71 | 72 | it "should fail to connect" do 73 | reactor.async do 74 | expect do 75 | client_endpoint.connect 76 | end.to raise_exception(OpenSSL::SSL::SSLError) 77 | end.wait 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/async/io/standard_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | 6 | require 'async/io/standard' 7 | 8 | RSpec.describe Async::IO::STDIN do 9 | include_context Async::RSpec::Reactor 10 | 11 | it "should be able to read" do 12 | expect(subject.read(0)).to be == "" 13 | end 14 | end 15 | 16 | RSpec.describe Async::IO::STDOUT do 17 | include_context Async::RSpec::Reactor 18 | 19 | it "should be able to write" do 20 | expect(subject.write("")).to be == 0 21 | end 22 | end 23 | 24 | RSpec.describe Async::IO::STDERR do 25 | include_context Async::RSpec::Reactor 26 | 27 | it "should be able to write" do 28 | expect(subject.write("")).to be == 0 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/async/io/stream_context.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019-2023, by Samuel Williams. 5 | 6 | require 'async/rspec/buffer' 7 | require 'async/io/stream' 8 | 9 | RSpec.shared_context Async::IO::Stream do 10 | include_context Async::RSpec::Buffer 11 | subject {described_class.new(buffer)} 12 | let(:io) {subject.io} 13 | end 14 | -------------------------------------------------------------------------------- /spec/async/io/stream_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | # Copyright, 2018, by Janko Marohnić. 6 | # Copyright, 2019, by Benoit Daloze. 7 | # Copyright, 2021, by Aurora Nockert. 8 | # Copyright, 2023, by Maruth Goyal. 9 | # Copyright, 2023, by Olle Jonsson. 10 | 11 | require 'async/io' 12 | require 'async/io/socket' 13 | require 'async/clock' 14 | 15 | require_relative 'generic_examples' 16 | require_relative 'stream_context' 17 | 18 | RSpec.describe Async::IO::Stream do 19 | # This constant is part of the public interface, but was renamed to `Async::IO::BLOCK_SIZE`. 20 | describe "::BLOCK_SIZE" do 21 | it "should exist and be reasonable" do 22 | expect(Async::IO::Stream::BLOCK_SIZE).to be_between(1024, 1024*128) 23 | end 24 | end 25 | 26 | context "native I/O", if: RUBY_VERSION >= "3.1" do 27 | let(:sockets) do 28 | @sockets = ::Socket.pair(::Socket::AF_UNIX, ::Socket::SOCK_STREAM) 29 | end 30 | 31 | after do 32 | @sockets.each(&:close) 33 | end 34 | 35 | let(:io) {sockets.first} 36 | subject {described_class.new(sockets.last)} 37 | 38 | it "can read data" do 39 | io.write("Hello World") 40 | io.close_write 41 | 42 | expect(subject.read).to be == "Hello World" 43 | end 44 | end 45 | 46 | context "socket I/O" do 47 | let(:sockets) do 48 | @sockets = Async::IO::Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM) 49 | end 50 | 51 | after do 52 | @sockets&.each(&:close) 53 | end 54 | 55 | let(:io) {sockets.first} 56 | subject {described_class.new(sockets.last)} 57 | 58 | it_should_behave_like Async::IO 59 | 60 | describe '#drain_write_buffer' do 61 | include_context Async::RSpec::Reactor 62 | let(:output) {described_class.new(sockets.last)} 63 | subject {described_class.new(sockets.first)} 64 | 65 | let(:buffer_size) {1024*6} 66 | 67 | it "can interleave calls to flush" do 68 | tasks = 2.times.map do |i| 69 | reactor.async do 70 | buffer = i.to_s * buffer_size 71 | 128.times do 72 | output.write(buffer) 73 | output.flush 74 | end 75 | end 76 | end 77 | 78 | reactor.async do 79 | tasks.each(&:wait) 80 | output.close 81 | end 82 | 83 | Async::Task.current.sleep(1) 84 | 85 | while buffer = subject.read(buffer_size) 86 | expect(buffer).to be == (buffer[0] * buffer_size) 87 | end 88 | end 89 | 90 | it "handles write failures" do 91 | subject.close 92 | 93 | task = reactor.async do 94 | output.write("Hello World") 95 | output.flush 96 | end 97 | 98 | expect do 99 | task.wait 100 | end.to raise_error(Errno::EPIPE) 101 | 102 | write_buffer = output.instance_variable_get(:@write_buffer) 103 | drain_buffer = output.instance_variable_get(:@drain_buffer) 104 | 105 | expect(write_buffer).to be_empty 106 | expect(drain_buffer).to be_empty 107 | end 108 | end 109 | 110 | describe '#close_read' do 111 | subject {described_class.new(sockets.last)} 112 | 113 | it "can close the reading end of the stream" do 114 | expect(subject.io).to receive(:close_read).and_call_original 115 | 116 | subject.close_read 117 | 118 | # Ruby <= 2.4 raises an exception even with exception: false 119 | # expect(stream.read).to be_nil 120 | end 121 | 122 | it "can close the writing end of the stream" do 123 | expect(subject.io).to receive(:close_write).and_call_original 124 | 125 | subject.write("Oh yes!") 126 | subject.close_write 127 | 128 | expect do 129 | subject.write("Oh no!") 130 | subject.flush 131 | end.to raise_error(IOError, /not opened for writing/) 132 | end 133 | end 134 | 135 | describe '#read_exactly' do 136 | it "can read several bytes" do 137 | io.write("hello\nworld\n") 138 | 139 | expect(subject.read_exactly(4)).to be == 'hell' 140 | end 141 | 142 | it "can raise exception if io is eof" do 143 | io.close 144 | 145 | expect do 146 | subject.read_exactly(4) 147 | end.to raise_error(EOFError) 148 | end 149 | end 150 | end 151 | 152 | context "performance (BLOCK_SIZE: #{Async::IO::BLOCK_SIZE} MAXIMUM_READ_SIZE: #{Async::IO::MAXIMUM_READ_SIZE})" do 153 | include_context Async::RSpec::Reactor 154 | 155 | let!(:stream) {described_class.open("/dev/zero")} 156 | after {stream.close} 157 | 158 | it "can read data quickly" do |example| 159 | data = nil 160 | 161 | duration = Async::Clock.measure do 162 | data = stream.read(1024**3) 163 | end 164 | 165 | size = data.bytesize / 1024**2 166 | rate = size / duration 167 | 168 | example.reporter.message "Read #{size.round(2)}MB of data at #{rate.round(2)}MB/s." 169 | 170 | expect(rate).to be > 128 171 | end 172 | end 173 | 174 | context "buffered I/O" do 175 | include_context Async::IO::Stream 176 | include_context Async::RSpec::Memory 177 | include_context Async::RSpec::Reactor 178 | 179 | describe '#read' do 180 | it "can read zero length" do 181 | result = subject.read(0) 182 | 183 | expect(result).to be == "" 184 | expect(result.encoding).to be == Encoding::BINARY 185 | end 186 | 187 | it "reads everything" do 188 | io.write "Hello World" 189 | io.seek(0) 190 | 191 | expect(subject.io).to receive(:read_nonblock).and_call_original.twice 192 | 193 | expect(subject.read).to be == "Hello World" 194 | expect(subject).to be_eof 195 | end 196 | 197 | it "reads only the amount requested" do 198 | io.write "Hello World" 199 | io.seek(0) 200 | 201 | expect(subject.io).to receive(:read_nonblock).and_call_original.once 202 | 203 | expect(subject.read_partial(4)).to be == "Hell" 204 | expect(subject).to_not be_eof 205 | 206 | expect(subject.read_partial(20)).to be == "o World" 207 | expect(subject).to be_eof 208 | end 209 | 210 | it "peeks everything" do 211 | io.write "Hello World" 212 | io.seek(0) 213 | 214 | expect(subject.io).to receive(:read_nonblock).and_call_original.twice 215 | 216 | expect(subject.peek).to be == "Hello World" 217 | expect(subject.read).to be == "Hello World" 218 | expect(subject).to be_eof 219 | end 220 | 221 | it "peeks only the amount requested" do 222 | io.write "Hello World" 223 | io.seek(0) 224 | 225 | expect(subject.io).to receive(:read_nonblock).and_call_original.twice 226 | 227 | expect(subject.peek(4)).to be == "Hell" 228 | expect(subject.read_partial(4)).to be == "Hell" 229 | expect(subject).to_not be_eof 230 | 231 | expect(subject.peek(20)).to be == "o World" 232 | expect(subject.read_partial(20)).to be == "o World" 233 | expect(subject).to be_eof 234 | end 235 | 236 | it "peeks everything when requested bytes is too large" do 237 | io.write "Hello World" 238 | io.seek(0) 239 | 240 | expect(subject.io).to receive(:read_nonblock).and_call_original.twice 241 | 242 | expect(subject.peek(400)).to be == "Hello World" 243 | expect(subject.read_partial(400)).to be == "Hello World" 244 | expect(subject).to be_eof 245 | end 246 | 247 | context "with large content", if: !Async::IO.buffer? do 248 | it "allocates expected amount of bytes" do 249 | io.write("." * 16*1024) 250 | io.seek(0) 251 | 252 | buffer = nil 253 | 254 | expect do 255 | # The read buffer is already allocated, and it will be resized to fit the incoming data. It will be swapped with an empty buffer. 256 | buffer = subject.read(16*1024) 257 | end.to limit_allocations.of(String, count: 1, size: 0) 258 | 259 | expect(buffer.size).to be == 16*1024 260 | end 261 | end 262 | end 263 | 264 | describe '#read_until' do 265 | it "can read a line" do 266 | io.write("hello\nworld\n") 267 | io.seek(0) 268 | 269 | expect(subject.read_until("\n")).to be == 'hello' 270 | expect(subject.read_until("\n")).to be == 'world' 271 | expect(subject.read_until("\n")).to be_nil 272 | end 273 | 274 | context "with 1-byte block size" do 275 | subject! {Async::IO::Stream.new(buffer, block_size: 1)} 276 | 277 | it "can read a line with a multi-byte pattern" do 278 | io.write("hello\r\nworld\r\n") 279 | io.seek(0) 280 | 281 | expect(subject.read_until("\r\n")).to be == 'hello' 282 | expect(subject.read_until("\r\n")).to be == 'world' 283 | expect(subject.read_until("\r\n")).to be_nil 284 | end 285 | end 286 | 287 | context "with large content", if: !Async::IO.buffer? do 288 | it "allocates expected amount of bytes" do 289 | subject 290 | 291 | expect do 292 | subject.read_until("b") 293 | end.to limit_allocations.of(String, size: 0, count: 1) 294 | end 295 | end 296 | end 297 | 298 | describe '#flush' do 299 | it "should not call write if write buffer is empty" do 300 | expect(subject.io).to_not receive(:write) 301 | 302 | subject.flush 303 | end 304 | 305 | it "should flush underlying data when it exceeds block size" do 306 | expect(subject.io).to receive(:write).and_call_original.once 307 | 308 | subject.block_size.times do 309 | subject.write("!") 310 | end 311 | end 312 | end 313 | 314 | describe '#read_partial' do 315 | before(:each) do 316 | string = "Hello World!" 317 | 318 | io.write(string * (1 + (Async::IO::BLOCK_SIZE / string.bytesize))) 319 | io.seek(0) 320 | end 321 | 322 | it "should avoid calling read" do 323 | expect(subject.io).to receive(:read_nonblock).and_call_original.once 324 | 325 | expect(subject.read_partial(12)).to be == "Hello World!" 326 | end 327 | 328 | context "with large content", if: !Async::IO.buffer? do 329 | it "allocates only the amount required" do 330 | expect do 331 | subject.read(4*1024) 332 | end.to limit_allocations.of(String, count: 2, size: 4*1024+1) 333 | end 334 | 335 | it "allocates exact number of bytes being read" do 336 | expect do 337 | subject.read_partial(subject.block_size * 2) 338 | end.to limit_allocations.of(String, count: 1, size: 0) 339 | end 340 | 341 | it "allocates expected amount of bytes" do 342 | buffer = nil 343 | 344 | expect do 345 | buffer = subject.read_partial 346 | end.to limit_allocations.of(String, count: 1) 347 | 348 | expect(buffer.size).to be == subject.block_size 349 | end 350 | end 351 | 352 | context "has the right encoding" do 353 | it "with a normal partial_read" do 354 | expect(subject.read_partial(1).encoding).to be == Encoding::BINARY 355 | end 356 | 357 | it "with a zero-length partial_read" do 358 | expect(subject.read_partial(0).encoding).to be == Encoding::BINARY 359 | end 360 | end 361 | end 362 | 363 | describe '#write' do 364 | it "should read one line" do 365 | expect(subject.io).to receive(:write).and_call_original.once 366 | 367 | subject.write "Hello World\n" 368 | subject.flush 369 | 370 | io.seek(0) 371 | expect(subject.read).to be == "Hello World\n" 372 | end 373 | end 374 | 375 | describe '#eof' do 376 | it "should terminate subject" do 377 | expect do 378 | subject.eof! 379 | end.to raise_exception(EOFError) 380 | 381 | expect(subject).to be_eof 382 | end 383 | end 384 | 385 | describe '#close' do 386 | it 'can be closed even if underlying io is closed' do 387 | io.close 388 | 389 | expect(subject.io).to be_closed 390 | 391 | # Put some data in the write buffer 392 | subject.write "." 393 | 394 | expect do 395 | subject.close 396 | end.to_not raise_exception 397 | end 398 | end 399 | end 400 | end 401 | -------------------------------------------------------------------------------- /spec/async/io/tcp_socket_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | 6 | require 'async/io/tcp_socket' 7 | 8 | require_relative 'generic_examples' 9 | 10 | RSpec.describe Async::IO::TCPSocket do 11 | include_context Async::RSpec::Reactor 12 | 13 | it_should_behave_like Async::IO::Generic 14 | 15 | # Shared port for localhost network tests. 16 | let(:server_address) {Async::IO::Address.tcp("localhost", 6788)} 17 | let(:data) {"The quick brown fox jumped over the lazy dog."} 18 | 19 | describe Async::IO::TCPServer do 20 | it_should_behave_like Async::IO::Generic 21 | end 22 | 23 | describe Async::IO::TCPServer do 24 | let!(:server_task) do 25 | reactor.async do |task| 26 | server = Async::IO::TCPServer.new("localhost", 6788) 27 | 28 | peer, address = server.accept 29 | 30 | data = peer.gets 31 | peer.puts(data) 32 | peer.flush 33 | 34 | peer.close 35 | server.close 36 | end 37 | end 38 | 39 | let(:client) {Async::IO::TCPSocket.new("localhost", 6788)} 40 | 41 | it "can read into output buffer" do 42 | client.puts("Hello World") 43 | client.flush 44 | 45 | buffer = String.new 46 | # 20 is bigger than echo response... 47 | data = client.read(20, buffer) 48 | 49 | expect(buffer).to_not be_empty 50 | expect(buffer).to be == data 51 | 52 | client.close 53 | server_task.wait 54 | end 55 | 56 | it "should start server and send data" do 57 | # Accept a single incoming connection and then finish. 58 | client.puts(data) 59 | client.flush 60 | 61 | expect(client.gets).to be == data 62 | 63 | client.close 64 | server_task.wait 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/async/io/threads_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2020-2023, by Samuel Williams. 5 | 6 | require 'async/io/threads' 7 | 8 | RSpec.describe Async::IO::Threads do 9 | include_context Async::RSpec::Reactor 10 | 11 | describe '#async' do 12 | it "can schedule work on a different thread" do 13 | thread = subject.async do 14 | Thread.current 15 | end.wait 16 | 17 | expect(thread).to be_kind_of Thread 18 | expect(thread).to_not be Thread.current 19 | end 20 | 21 | it "can kill thread when stopping task" do 22 | sleeping = Async::IO::Notification.new 23 | 24 | thread = nil 25 | 26 | task = subject.async do 27 | thread = Thread.current 28 | sleeping.signal 29 | sleep 30 | end 31 | 32 | sleeping.wait 33 | 34 | task.stop 35 | 10.times do 36 | pp thread 37 | sleep(0.1) 38 | break unless thread.status 39 | end 40 | 41 | expect(thread.status).to be_nil 42 | ensure 43 | sleeping.close 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/async/io/trap_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | 6 | require 'async/io/trap' 7 | 8 | RSpec.describe Async::IO::Trap do 9 | include_context Async::RSpec::Reactor 10 | 11 | subject {described_class.new(:USR2)} 12 | 13 | it "can ignore signal" do 14 | subject.ignore! 15 | 16 | Process.kill(:USR2, Process.pid) 17 | end 18 | 19 | it "should wait for signal" do 20 | trapped = false 21 | 22 | waiting_task = reactor.async do 23 | subject.wait do 24 | trapped = true 25 | break 26 | end 27 | end 28 | 29 | subject.trigger 30 | 31 | waiting_task.wait 32 | 33 | expect(trapped).to be_truthy 34 | end 35 | 36 | it "should create transient task" do 37 | task = subject.async(transient: true) do 38 | # Trapped. 39 | end 40 | 41 | expect(task).to be_transient 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/async/io/udp_socket_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | 6 | require 'async/io/udp_socket' 7 | 8 | require_relative 'generic_examples' 9 | 10 | RSpec.describe Async::IO::UDPSocket do 11 | include_context Async::RSpec::Reactor 12 | 13 | it_should_behave_like Async::IO::Generic 14 | 15 | let(:data) {"The quick brown fox jumped over the lazy dog."} 16 | 17 | it "should echo data back to peer" do 18 | reactor.async do 19 | server = Async::IO::UDPSocket.new(Socket::AF_INET) 20 | server.bind("127.0.0.1", 6778) 21 | 22 | packet, address = server.recvfrom(512) 23 | server.send(packet, 0, address[3], address[1]) 24 | 25 | server.close 26 | end 27 | 28 | reactor.async do 29 | client = Async::IO::UDPSocket.new(Socket::AF_INET) 30 | client.connect("127.0.0.1", 6778) 31 | 32 | client.send(data, 0) 33 | response = client.recv(512) 34 | client.close 35 | 36 | expect(response).to be == data 37 | end.wait 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/async/io/unix_endpoint_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019-2023, by Samuel Williams. 5 | # Copyright, 2023, by Hasan Kumar. 6 | 7 | require 'async/io/unix_endpoint' 8 | require 'async/io/stream' 9 | 10 | RSpec.describe Async::IO::UNIXEndpoint do 11 | include_context Async::RSpec::Reactor 12 | 13 | let(:data) {"The quick brown fox jumped over the lazy dog."} 14 | let(:path) {File.join(__dir__, "unix-socket")} 15 | subject {described_class.unix(path)} 16 | 17 | it "should echo data back to peer" do 18 | server_task = reactor.async do 19 | subject.accept do |peer| 20 | peer.send(peer.recv(512)) 21 | end 22 | end 23 | 24 | subject.connect do |client| 25 | client.send(data) 26 | 27 | response = client.recv(512) 28 | 29 | expect(response).to be == data 30 | end 31 | 32 | server_task.stop 33 | end 34 | 35 | it "should not fail to bind if there are no existing bindings on the socket" do 36 | server_task1 = reactor.async do 37 | subject.bind 38 | end 39 | server_task1.stop 40 | 41 | server_task2 = reactor.async do 42 | expect do 43 | subject.bind 44 | end.to_not raise_error 45 | end 46 | server_task2.stop 47 | end 48 | 49 | it "should fails to bind if there is an existing binding" do 50 | condition = Async::Condition.new 51 | 52 | reactor.async do 53 | condition.wait 54 | 55 | expect do 56 | subject.bind 57 | end.to raise_error(Errno::EADDRINUSE) 58 | end 59 | 60 | server_task = reactor.async do 61 | subject.bind do |server| 62 | server.listen(1) 63 | condition.signal 64 | end 65 | end 66 | 67 | server_task.stop 68 | end 69 | 70 | context "using buffered stream" do 71 | it "can use stream to read and write data" do 72 | server_task = reactor.async do |task| 73 | subject.accept do |peer| 74 | stream = Async::IO::Stream.new(peer) 75 | stream.write(stream.read) 76 | stream.close 77 | end 78 | end 79 | 80 | reactor.async do 81 | subject.connect do |client| 82 | stream = Async::IO::Stream.new(client) 83 | 84 | stream.write(data) 85 | stream.close_write 86 | 87 | expect(stream.read).to be == data 88 | end 89 | end.wait 90 | 91 | server_task.stop 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/async/io/unix_socket_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | 6 | require 'async/io/unix_socket' 7 | 8 | require_relative 'generic_examples' 9 | require 'fileutils' 10 | 11 | RSpec.describe Async::IO::UNIXSocket do 12 | include_context Async::RSpec::Reactor 13 | 14 | it_should_behave_like Async::IO::Generic 15 | 16 | let(:path) {File.join(__dir__, "unix-socket")} 17 | let(:data) {"The quick brown fox jumped over the lazy dog."} 18 | 19 | before(:each) do 20 | FileUtils.rm_f path 21 | end 22 | 23 | after do 24 | FileUtils.rm_f path 25 | end 26 | 27 | it "should echo data back to peer" do 28 | reactor.async do 29 | Async::IO::UNIXServer.wrap(path) do |server| 30 | server.accept do |peer| 31 | peer.send(peer.recv(512)) 32 | end 33 | end 34 | end 35 | 36 | Async::IO::UNIXSocket.wrap(path) do |client| 37 | client.send(data) 38 | 39 | response = client.recv(512) 40 | 41 | expect(response).to be == data 42 | end 43 | end 44 | end 45 | 46 | RSpec.describe Async::IO::UNIXServer do 47 | it_should_behave_like Async::IO::Generic 48 | end 49 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | 6 | require 'covered/rspec' 7 | require "async/rspec" 8 | 9 | require_relative 'addrinfo' 10 | 11 | RSpec.configure do |config| 12 | # Enable flags like --only-failures and --next-failure 13 | config.example_status_persistence_file_path = ".rspec_status" 14 | 15 | config.expect_with :rspec do |c| 16 | c.syntax = :expect 17 | end 18 | end 19 | 20 | Signal.trap(:INT) { raise Interrupt } 21 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0x03d7E2c0cf7813867DDb318674B66CC53B8497dA' 6 | quorum: 1 7 | --------------------------------------------------------------------------------