async-io 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,11 +28,42 @@ module Async
28
28
 
29
29
  # Asynchronous TCP socket wrapper.
30
30
  class SSLSocket < Generic
31
- wraps ::OpenSSL::SSL::SSLSocket, :alpn_protocol, :cert, :cipher, :client_ca, :hostname=, :npn_protocol, :peer_cert, :peer_cert_chain, :pending, :post_connection_check, :session, :session=, :session_reused?, :ssl_version, :state
31
+ wraps ::OpenSSL::SSL::SSLSocket, :alpn_protocol, :cert, :cipher, :client_ca, :close, :context, :hostname, :hostname=, :npn_protocol, :peer_cert, :peer_cert_chain, :pending, :post_connection_check, :session, :session=, :session_reused?, :ssl_version, :state, :sync_close, :sync_close=, :sysclose, :verify_result, :tmp_key
32
32
 
33
33
  wrap_blocking_method :accept, :accept_nonblock
34
34
  wrap_blocking_method :connect, :connect_nonblock
35
35
 
36
+ alias syswrite write
37
+ alias sysread read
38
+
39
+ # It's hard to know what #to_io / #io should do. So, they are omitted.
40
+
41
+ def self.connect(socket, context, hostname = nil, &block)
42
+ client = self.wrap(socket, context)
43
+
44
+ # Used for SNI:
45
+ if hostname
46
+ client.hostname = hostname
47
+ end
48
+
49
+ begin
50
+ client.connect
51
+ rescue
52
+ # 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.
53
+ client.close
54
+
55
+ raise
56
+ end
57
+
58
+ return client unless block_given?
59
+
60
+ begin
61
+ yield client
62
+ ensure
63
+ client.close
64
+ end
65
+ end
66
+
36
67
  def local_address
37
68
  @io.to_io.local_address
38
69
  end
@@ -41,9 +72,8 @@ module Async
41
72
  @io.to_io.remote_address
42
73
  end
43
74
 
44
- # This method/implementation might change in the future, don't depend on it :)
45
- def self.connect_socket(socket, context)
46
- io = wrapped_klass.new(socket.to_io, context)
75
+ def self.wrap(socket, context)
76
+ io = @wrapped_klass.new(socket.to_io, context)
47
77
 
48
78
  # This ensures that when the internal IO is closed, it also closes the internal socket:
49
79
  io.sync_close = true
@@ -52,6 +82,7 @@ module Async
52
82
  end
53
83
  end
54
84
 
85
+ # We reimplement this from scratch because the native implementation doesn't expose the underlying server/context that we need to implement non-blocking accept.
55
86
  class SSLServer
56
87
  extend Forwardable
57
88
 
@@ -60,13 +91,11 @@ module Async
60
91
  @context = context
61
92
  end
62
93
 
63
- def_delegators :@server, :local_address, :setsockopt, :getsockopt
94
+ def_delegators :@server, :local_address, :setsockopt, :getsockopt, :close
64
95
 
65
96
  attr :server
66
97
  attr :context
67
98
 
68
- include ServerSocket
69
-
70
99
  def listen(*args)
71
100
  @server.listen(*args)
72
101
  end
@@ -74,7 +103,7 @@ module Async
74
103
  def accept(task: Task.current)
75
104
  peer, address = @server.accept
76
105
 
77
- wrapper = SSLSocket.connect_socket(peer, @context)
106
+ wrapper = SSLSocket.wrap(peer, @context)
78
107
 
79
108
  return wrapper, address unless block_given?
80
109
 
@@ -82,6 +111,7 @@ module Async
82
111
  task.annotate "accepting secure connection #{address}"
83
112
 
84
113
  begin
114
+ # You want to do this in a nested async task or you might suffer from head-of-line blocking.
85
115
  wrapper.accept
86
116
 
87
117
  yield wrapper, address
@@ -92,6 +122,8 @@ module Async
92
122
  end
93
123
  end
94
124
  end
125
+
126
+ include Server
95
127
  end
96
128
  end
97
129
  end
@@ -0,0 +1,29 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'generic'
22
+
23
+ module Async
24
+ module IO
25
+ STDIN = Generic.new($stdin)
26
+ STDOUT = Generic.new($stdout)
27
+ STDERR = Generic.new($stderr)
28
+ end
29
+ end
@@ -49,6 +49,10 @@ module Async
49
49
  @buffer = Stream.new(self)
50
50
  end
51
51
 
52
+ class << self
53
+ alias open new
54
+ end
55
+
52
56
  attr :buffer
53
57
 
54
58
  def_delegators :@buffer, :gets, :puts, :flush
@@ -72,8 +76,19 @@ module Async
72
76
 
73
77
  wrapper = TCPSocket.new(peer)
74
78
 
75
- return wrapper, address
79
+ return wrapper, address unless block_given?
80
+
81
+ begin
82
+ yield wrapper, address
83
+ ensure
84
+ wrapper.close
85
+ end
76
86
  end
87
+
88
+ alias accept_nonblock accept
89
+ alias sysaccept accept
90
+
91
+ include Server
77
92
  end
78
93
  end
79
94
  end
@@ -23,14 +23,12 @@ require_relative 'socket'
23
23
  module Async
24
24
  module IO
25
25
  class UNIXSocket < BasicSocket
26
- wraps ::UNIXSocket, :path, :addr, :peeraddr
27
-
28
- # TODO Currently unimplemented.
29
- # , :send_io, :recv_io
26
+ # `send_io`, `recv_io` and `recvfrom` may block but no non-blocking implementation available.
27
+ wraps ::UNIXSocket, :path, :addr, :peeraddr, :send_io, :recv_io, :recvfrom
30
28
  end
31
29
 
32
30
  class UNIXServer < UNIXSocket
33
- wraps ::UNIXServer
31
+ wraps ::UNIXServer, :listen
34
32
 
35
33
  def accept
36
34
  peer = async_send(:accept_nonblock)
@@ -44,6 +42,9 @@ module Async
44
42
  wrapper.close
45
43
  end
46
44
  end
45
+
46
+ alias sysaccept accept
47
+ alias accept_nonblock accept
47
48
  end
48
49
  end
49
50
  end
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Async
22
22
  module IO
23
- VERSION = "1.4.0"
23
+ VERSION = "1.5.0"
24
24
  end
25
25
  end
@@ -27,7 +27,7 @@ RSpec.describe "echo client/server" do
27
27
  # sudo sysctl -w net.inet.ip.portrange.hifirst=10000
28
28
  # Probably due to the use of select.
29
29
 
30
- let(:repeats) {10000}
30
+ let(:repeats) {RUBY_PLATFORM =~ /darwin/ ? 100 : 10000}
31
31
  let(:server_address) {Async::IO::Address.tcp('0.0.0.0', 10102)}
32
32
 
33
33
  def echo_server(server_address)
@@ -18,8 +18,8 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
- # This seems to break rbx
22
- # require 'async/io/endpoint'
21
+ require 'async/io/endpoint'
22
+ require 'async/io/tcp_socket'
23
23
 
24
24
  RSpec.describe Async::IO::Endpoint do
25
25
  include_context Async::RSpec::Reactor
@@ -35,7 +35,7 @@ RSpec.describe Async::IO::Endpoint do
35
35
  describe Async::IO::SocketEndpoint.new(TCPServer.new('0.0.0.0', 1234)) do
36
36
  it "should bind to given socket" do
37
37
  subject.bind do |server|
38
- expect(server.io).to be == subject.specification
38
+ expect(server).to be == subject.socket
39
39
  end
40
40
  end
41
41
  end
@@ -0,0 +1,24 @@
1
+
2
+ RSpec.shared_examples Async::IO::Generic do |ignore_methods|
3
+ let(:instance_methods) {described_class.wrapped_klass.instance_methods(false) - (ignore_methods || [])}
4
+ let(:wrapped_instance_methods) {described_class.instance_methods}
5
+
6
+ it "should wrap a class" do
7
+ expect(described_class.wrapped_klass).to_not be_nil
8
+ end
9
+
10
+ it "should wrap underlying instance methods" do
11
+ expect(wrapped_instance_methods.sort).to include(*instance_methods.sort)
12
+ end
13
+
14
+ # This needs to be reviewed in more detail.
15
+ #
16
+ # let(:singleton_methods) {described_class.wrapped_klass.singleton_methods(false)}
17
+ # let(:wrapped_singleton_methods) {described_class.singleton_methods(false)}
18
+ #
19
+ # it "should wrap underlying class methods" do
20
+ # singleton_methods.each do |method|
21
+ # expect(wrapped_singleton_methods).to include(method)
22
+ # end
23
+ # end
24
+ end
@@ -20,9 +20,15 @@
20
20
 
21
21
  require 'async/io'
22
22
 
23
+ require_relative 'generic_examples'
24
+
23
25
  RSpec.describe Async::IO::Generic do
24
26
  include_context Async::RSpec::Reactor
25
27
 
28
+ it_should_behave_like Async::IO::Generic, [
29
+ :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
30
+ ]
31
+
26
32
  let(:pipe) {IO.pipe}
27
33
  let(:input) {Async::IO::Generic.new(pipe.first)}
28
34
  let(:output) {Async::IO::Generic.new(pipe.last)}
@@ -20,16 +20,16 @@
20
20
 
21
21
  require 'async/io/tcp_socket'
22
22
 
23
- RSpec.describe Async::Reactor do
24
- include_context Async::RSpec::Leaks
23
+ RSpec.describe Async::IO::Socket do
24
+ include_context Async::RSpec::Reactor
25
25
 
26
26
  # Shared port for localhost network tests.
27
27
  let(:server_address) {Async::IO::Address.tcp("localhost", 6788)}
28
28
  let(:data) {"The quick brown fox jumped over the lazy dog."}
29
29
 
30
- around(:each) do |example|
30
+ let!(:server_task) do
31
31
  # Accept a single incoming connection and then finish.
32
- subject.async do |task|
32
+ reactor.async do |task|
33
33
  Async::IO::Socket.bind(server_address) do |server|
34
34
  server.listen(10)
35
35
 
@@ -39,21 +39,14 @@ RSpec.describe Async::Reactor do
39
39
  end
40
40
  end
41
41
  end
42
-
43
- result = example.run
44
-
45
- if result.is_a? Exception
46
- result
47
- else
48
- subject.run
49
- end
50
42
  end
51
43
 
52
44
  describe 'basic tcp server' do
53
45
  it "should start server and send data" do
54
- subject.async do
46
+ reactor.async do
55
47
  Async::IO::Socket.connect(server_address) do |client|
56
48
  client.write(data)
49
+
57
50
  expect(client.read(512)).to be == data
58
51
  end
59
52
  end
@@ -62,53 +55,29 @@ RSpec.describe Async::Reactor do
62
55
 
63
56
  describe 'non-blocking tcp connect' do
64
57
  it "should start server and send data" do
65
- subject.async do |task|
58
+ reactor.async do |task|
66
59
  Async::IO::Socket.connect(server_address) do |client|
67
60
  client.write(data)
61
+
68
62
  expect(client.read(512)).to be == data
69
63
  end
70
64
  end
71
65
  end
72
66
 
73
67
  it "can connect socket and read/write in a different task" do
74
- socket = nil
75
-
76
- subject.async do |task|
68
+ reactor.async do |task|
77
69
  socket = Async::IO::Socket.connect(server_address)
78
70
 
79
- # Stop the reactor once the connection was made.
80
- subject.stop
81
- end
82
-
83
- subject.run
84
-
85
- expect(socket).to_not be_nil
86
- expect(socket).to be_kind_of Async::Wrapper
87
-
88
- subject.async do
89
- socket.write(data)
90
-
91
- expect(socket.read(512)).to be == data
92
- end
93
-
94
- subject.run
95
-
96
- socket.close
97
- end
98
-
99
- it "can't use a socket in nested tasks" do
100
- subject.async do |task|
101
- socket = Async::IO::Socket.connect(server_address)
71
+ expect(socket).to_not be_nil
102
72
  expect(socket).to be_kind_of Async::Wrapper
103
73
 
104
- expect do
105
- subject.async do
106
- socket.write(data)
107
- # expect(socket.read(512)).to be == data
108
- end
109
- end.to_not raise_error
110
-
111
- socket.close
74
+ reactor.async do
75
+ socket.write(data)
76
+
77
+ expect(socket.read(512)).to be == data
78
+
79
+ socket.close
80
+ end
112
81
  end
113
82
  end
114
83
  end
@@ -20,24 +20,26 @@
20
20
 
21
21
  require 'async/io/udp_socket'
22
22
 
23
- RSpec.describe Async::Reactor do
24
- include_context Async::RSpec::Leaks
23
+ RSpec.describe Async::IO::Socket do
24
+ include_context Async::RSpec::Reactor
25
25
 
26
26
  # Shared port for localhost network tests.
27
27
  let(:server_address) {Async::IO::Address.udp("127.0.0.1", 6778)}
28
28
  let(:data) {"The quick brown fox jumped over the lazy dog."}
29
29
 
30
+ let!(:server_task) do
31
+ reactor.async do
32
+ Async::IO::Socket.bind(server_address) do |server|
33
+ packet, address = server.recvfrom(512)
34
+
35
+ server.send(packet, 0, address)
36
+ end
37
+ end
38
+ end
39
+
30
40
  describe 'basic udp server' do
31
41
  it "should echo data back to peer" do
32
- subject.async do
33
- Async::IO::Socket.bind(server_address) do |server|
34
- packet, address = server.recvfrom(512)
35
-
36
- server.send(packet, 0, address)
37
- end
38
- end
39
-
40
- subject.async do
42
+ reactor.async do
41
43
  Async::IO::Socket.connect(server_address) do |client|
42
44
  client.send(data)
43
45
  response = client.recv(512)
@@ -45,20 +47,10 @@ RSpec.describe Async::Reactor do
45
47
  expect(response).to be == data
46
48
  end
47
49
  end
48
-
49
- subject.run
50
50
  end
51
51
 
52
52
  it "should use unconnected socket" do
53
- subject.async do
54
- Async::IO::Socket.bind(server_address) do |server|
55
- packet, address = server.recvfrom(512)
56
-
57
- server.send(packet, 0, address)
58
- end
59
- end
60
-
61
- subject.async do
53
+ reactor.async do
62
54
  Async::IO::UDPSocket.wrap(server_address.afamily) do |client|
63
55
  client.send(data, 0, server_address)
64
56
  response, address = client.recvfrom(512)
@@ -66,8 +58,6 @@ RSpec.describe Async::Reactor do
66
58
  expect(response).to be == data
67
59
  end
68
60
  end
69
-
70
- subject.run
71
61
  end
72
62
  end
73
63
  end