async-io 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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