async-io 1.22.0 → 1.23.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/examples/issues/pipes.rb +32 -0
- data/lib/async/io/generic.rb +83 -16
- data/lib/async/io/socket.rb +1 -1
- data/lib/async/io/ssl_endpoint.rb +1 -1
- data/lib/async/io/ssl_socket.rb +21 -11
- data/lib/async/io/stream.rb +24 -32
- data/lib/async/io/tcp_socket.rb +15 -41
- data/lib/async/io/version.rb +1 -1
- data/spec/async/io/echo_spec.rb +2 -1
- data/spec/async/io/generic_examples.rb +28 -0
- data/spec/async/io/generic_spec.rb +2 -3
- data/spec/async/io/socket/tcp_spec.rb +4 -0
- data/spec/async/io/socket_spec.rb +16 -0
- data/spec/async/io/ssl_server_spec.rb +2 -0
- data/spec/async/io/ssl_socket_spec.rb +2 -1
- data/spec/async/io/stream_context.rb +28 -0
- data/spec/async/io/stream_spec.rb +201 -153
- data/spec/async/io/tcp_socket_spec.rb +10 -6
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b15a84835cd91c8d303fb774bcfab585180115153fab3006f0e2e179d9d06a91
|
4
|
+
data.tar.gz: 89d3f2c2c676234ce38c494d582502a78b7ff4f2a19a9eaa926dcdf95c45d770
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 42716527198f1cc5228b6c714cd43dd7ec8174aeab1a899db158e6e44c13067e8cc2c705048a0c00384334b19c5b9fe56e0c220cc676a1a190f8e0c6c9034a8b
|
7
|
+
data.tar.gz: '01831255a14a052f6a2fc04973d6a64002edbb31885ea8683ef0c84f104a4a74abd3a06ccc75f28115150bd6892456a9733af16e66280685ebe63dfedae83b26'
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# wat.rb
|
2
|
+
require 'async'
|
3
|
+
require_relative '../../lib/async/io'
|
4
|
+
require 'digest/sha1'
|
5
|
+
require 'securerandom'
|
6
|
+
|
7
|
+
Async.run do |task|
|
8
|
+
r, w = IO.pipe.map { |io| Async::IO.try_convert(io) }
|
9
|
+
|
10
|
+
task.async do |subtask|
|
11
|
+
s = Digest::SHA1.new
|
12
|
+
l = 0
|
13
|
+
100.times do
|
14
|
+
bytes = SecureRandom.bytes(4000)
|
15
|
+
s << bytes
|
16
|
+
w << bytes
|
17
|
+
l += bytes.bytesize
|
18
|
+
end
|
19
|
+
w.close
|
20
|
+
p [:write, l, s.hexdigest]
|
21
|
+
end
|
22
|
+
|
23
|
+
task.async do |subtask|
|
24
|
+
s = Digest::SHA1.new
|
25
|
+
l = 0
|
26
|
+
while b = r.read(4096)
|
27
|
+
s << b
|
28
|
+
l += b.bytesize
|
29
|
+
end
|
30
|
+
p [:read, l, s.hexdigest]
|
31
|
+
end
|
32
|
+
end
|
data/lib/async/io/generic.rb
CHANGED
@@ -23,6 +23,10 @@ require 'forwardable'
|
|
23
23
|
|
24
24
|
module Async
|
25
25
|
module IO
|
26
|
+
# The default block size for IO buffers.
|
27
|
+
# BLOCK_SIZE = ENV.fetch('BLOCK_SIZE', 1024*16).to_i
|
28
|
+
BLOCK_SIZE = 1024*8
|
29
|
+
|
26
30
|
# Convert a Ruby ::IO object to a wrapped instance:
|
27
31
|
def self.try_convert(io, &block)
|
28
32
|
if wrapper_class = Generic::WRAPPERS[io.class]
|
@@ -42,17 +46,20 @@ module Async
|
|
42
46
|
# @!macro [attach] wrap_blocking_method
|
43
47
|
# @method $1
|
44
48
|
# 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.
|
45
|
-
def wrap_blocking_method(new_name, method_name, invert: true)
|
46
|
-
|
47
|
-
|
49
|
+
def wrap_blocking_method(new_name, method_name, invert: true, &block)
|
50
|
+
if block_given?
|
51
|
+
define_method(new_name, &block)
|
52
|
+
else
|
53
|
+
define_method(new_name) do |*args|
|
54
|
+
async_send(method_name, *args)
|
55
|
+
end
|
48
56
|
end
|
49
57
|
|
50
58
|
if invert
|
51
|
-
# We
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
def_delegators :@io, method_name
|
59
|
+
# We wrap the original _nonblock method, ignoring options.
|
60
|
+
define_method(method_name) do |*args, exception: false|
|
61
|
+
async_send(method_name, *args)
|
62
|
+
end
|
56
63
|
end
|
57
64
|
end
|
58
65
|
|
@@ -85,17 +92,77 @@ module Async
|
|
85
92
|
|
86
93
|
wraps ::IO, :external_encoding, :internal_encoding, :autoclose?, :autoclose=, :pid, :stat, :binmode, :flush, :set_encoding, :to_io, :to_i, :reopen, :fileno, :fsync, :fdatasync, :sync, :sync=, :tell, :seek, :rewind, :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
94
|
|
95
|
+
# Read the specified number of bytes from the input stream. This is fast path.
|
88
96
|
# @example
|
89
|
-
# data = io.
|
90
|
-
wrap_blocking_method :
|
91
|
-
|
92
|
-
|
97
|
+
# data = io.sysread(512)
|
98
|
+
wrap_blocking_method :sysread, :read_nonblock
|
99
|
+
|
100
|
+
# Read `length` bytes of data from the underlying I/O. If length is unspecified, read everything.
|
101
|
+
def read(length = nil, buffer = nil)
|
102
|
+
if buffer
|
103
|
+
buffer.clear
|
104
|
+
else
|
105
|
+
buffer = String.new
|
106
|
+
end
|
107
|
+
|
108
|
+
if length
|
109
|
+
return "" if length <= 0
|
110
|
+
|
111
|
+
# Fast path:
|
112
|
+
if buffer = self.sysread(length, buffer)
|
113
|
+
|
114
|
+
# Slow path:
|
115
|
+
while buffer.bytesize < length
|
116
|
+
# Slow path:
|
117
|
+
if chunk = self.sysread(length - buffer.bytesize)
|
118
|
+
buffer << chunk
|
119
|
+
else
|
120
|
+
break
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
return buffer
|
125
|
+
else
|
126
|
+
return nil
|
127
|
+
end
|
128
|
+
else
|
129
|
+
buffer = self.sysread(BLOCK_SIZE, buffer)
|
130
|
+
|
131
|
+
while chunk = self.sysread(BLOCK_SIZE)
|
132
|
+
buffer << chunk
|
133
|
+
end
|
134
|
+
|
135
|
+
return buffer
|
136
|
+
end
|
137
|
+
end
|
93
138
|
|
139
|
+
# Write entire buffer to output stream. This is fast path.
|
94
140
|
# @example
|
95
|
-
# io.
|
96
|
-
wrap_blocking_method :
|
97
|
-
|
98
|
-
alias
|
141
|
+
# io.syswrite("Hello World")
|
142
|
+
wrap_blocking_method :syswrite, :write_nonblock
|
143
|
+
|
144
|
+
alias readpartial read_nonblock
|
145
|
+
|
146
|
+
def write(buffer)
|
147
|
+
# Fast path:
|
148
|
+
written = self.syswrite(buffer)
|
149
|
+
remaining = buffer.bytesize - written
|
150
|
+
|
151
|
+
while remaining > 0
|
152
|
+
# Slow path:
|
153
|
+
length = self.syswrite(buffer.byteslice(written, remaining))
|
154
|
+
|
155
|
+
remaining -= length
|
156
|
+
written += length
|
157
|
+
end
|
158
|
+
|
159
|
+
return written
|
160
|
+
end
|
161
|
+
|
162
|
+
def << buffer
|
163
|
+
write(buffer)
|
164
|
+
return self
|
165
|
+
end
|
99
166
|
|
100
167
|
def dup
|
101
168
|
super.tap do |copy|
|
data/lib/async/io/socket.rb
CHANGED
@@ -58,7 +58,7 @@ module Async
|
|
58
58
|
# On Darwin, sometimes occurs when the connection is not yet fully formed. Empirically, TCP_NODELAY is enabled despite this result.
|
59
59
|
rescue Errno::EOPNOTSUPP
|
60
60
|
# Some platforms may simply not support the operation.
|
61
|
-
Async.logger.warn(self) {"Unable to set sync=#{value}!"}
|
61
|
+
# Async.logger.warn(self) {"Unable to set sync=#{value}!"}
|
62
62
|
end
|
63
63
|
|
64
64
|
def sync
|
data/lib/async/io/ssl_socket.rb
CHANGED
@@ -33,9 +33,6 @@ module Async
|
|
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
36
|
def self.connect(socket, context, hostname = nil, &block)
|
40
37
|
client = self.new(socket, context)
|
41
38
|
|
@@ -62,14 +59,6 @@ module Async
|
|
62
59
|
end
|
63
60
|
end
|
64
61
|
|
65
|
-
def local_address
|
66
|
-
@io.to_io.local_address
|
67
|
-
end
|
68
|
-
|
69
|
-
def remote_address
|
70
|
-
@io.to_io.remote_address
|
71
|
-
end
|
72
|
-
|
73
62
|
include Peer
|
74
63
|
|
75
64
|
def initialize(socket, context)
|
@@ -90,6 +79,27 @@ module Async
|
|
90
79
|
super(io, socket.reactor)
|
91
80
|
end
|
92
81
|
end
|
82
|
+
|
83
|
+
def local_address
|
84
|
+
@io.to_io.local_address
|
85
|
+
end
|
86
|
+
|
87
|
+
def remote_address
|
88
|
+
@io.to_io.remote_address
|
89
|
+
end
|
90
|
+
|
91
|
+
def close_write
|
92
|
+
self.shutdown(Socket::SHUT_WR)
|
93
|
+
end
|
94
|
+
|
95
|
+
def close_read
|
96
|
+
self.shutdown(Socket::SHUT_RD)
|
97
|
+
end
|
98
|
+
|
99
|
+
def shutdown(how)
|
100
|
+
@io.flush
|
101
|
+
@io.to_io.shutdown(how)
|
102
|
+
end
|
93
103
|
end
|
94
104
|
|
95
105
|
# We reimplement this from scratch because the native implementation doesn't expose the underlying server/context that we need to implement non-blocking accept.
|
data/lib/async/io/stream.rb
CHANGED
@@ -24,9 +24,7 @@ require_relative 'generic'
|
|
24
24
|
module Async
|
25
25
|
module IO
|
26
26
|
class Stream
|
27
|
-
|
28
|
-
# BLOCK_SIZE = ENV.fetch('BLOCK_SIZE', 1024*16).to_i
|
29
|
-
BLOCK_SIZE = 1024*8
|
27
|
+
BLOCK_SIZE = IO::BLOCK_SIZE
|
30
28
|
|
31
29
|
def initialize(io, block_size: BLOCK_SIZE, sync: true)
|
32
30
|
@io = io
|
@@ -72,13 +70,29 @@ module Async
|
|
72
70
|
def read_partial(size = nil)
|
73
71
|
return '' if size == 0
|
74
72
|
|
75
|
-
|
76
|
-
|
73
|
+
unless @eof
|
74
|
+
if size and @read_buffer.bytesize < size
|
75
|
+
fill_read_buffer(size > @block_size ? size : @block_size)
|
76
|
+
elsif @read_buffer.empty?
|
77
|
+
fill_read_buffer
|
78
|
+
end
|
77
79
|
end
|
78
80
|
|
79
81
|
return consume_read_buffer(size)
|
80
82
|
end
|
81
83
|
|
84
|
+
def read_exactly(size, exception: EOFError)
|
85
|
+
if buffer = read(size)
|
86
|
+
if buffer.bytesize != size
|
87
|
+
raise exception, "could not read enough data"
|
88
|
+
end
|
89
|
+
|
90
|
+
return buffer
|
91
|
+
end
|
92
|
+
|
93
|
+
raise exception, "encountered eof while reading data"
|
94
|
+
end
|
95
|
+
|
82
96
|
alias readpartial read_partial
|
83
97
|
|
84
98
|
# Efficiently read data from the stream until encountering pattern.
|
@@ -115,12 +129,12 @@ module Async
|
|
115
129
|
# @return the number of bytes appended to the buffer.
|
116
130
|
def write(string)
|
117
131
|
if @write_buffer.empty? and string.bytesize >= @block_size
|
118
|
-
|
132
|
+
@io.write(string)
|
119
133
|
else
|
120
134
|
@write_buffer << string
|
121
135
|
|
122
136
|
if @write_buffer.size >= @block_size
|
123
|
-
|
137
|
+
@io.write(@write_buffer)
|
124
138
|
@write_buffer.clear
|
125
139
|
end
|
126
140
|
end
|
@@ -138,7 +152,7 @@ module Async
|
|
138
152
|
# Flushes buffered data to the stream.
|
139
153
|
def flush
|
140
154
|
unless @write_buffer.empty?
|
141
|
-
|
155
|
+
@io.write(@write_buffer)
|
142
156
|
@write_buffer.clear
|
143
157
|
end
|
144
158
|
end
|
@@ -207,9 +221,9 @@ module Async
|
|
207
221
|
|
208
222
|
# Fills the buffer from the underlying stream.
|
209
223
|
def fill_read_buffer(size = @block_size)
|
210
|
-
if @read_buffer.empty? and @io.
|
224
|
+
if @read_buffer.empty? and @io.read_nonblock(size, @read_buffer, exception: false)
|
211
225
|
return true
|
212
|
-
elsif chunk = @io.
|
226
|
+
elsif chunk = @io.read_nonblock(size, @input_buffer, exception: false)
|
213
227
|
@read_buffer << chunk
|
214
228
|
return true
|
215
229
|
else
|
@@ -245,28 +259,6 @@ module Async
|
|
245
259
|
|
246
260
|
return result
|
247
261
|
end
|
248
|
-
|
249
|
-
# Write a buffer to the underlying stream.
|
250
|
-
# @param buffer [String] The string to write, any encoding is okay.
|
251
|
-
def syswrite(buffer)
|
252
|
-
remaining = buffer.bytesize
|
253
|
-
|
254
|
-
# Fast path:
|
255
|
-
written = @io.write(buffer)
|
256
|
-
return if written == remaining
|
257
|
-
|
258
|
-
# Slow path:
|
259
|
-
remaining -= written
|
260
|
-
|
261
|
-
while remaining > 0
|
262
|
-
wrote = @io.write(buffer.byteslice(written, remaining))
|
263
|
-
|
264
|
-
remaining -= wrote
|
265
|
-
written += wrote
|
266
|
-
end
|
267
|
-
|
268
|
-
return written
|
269
|
-
end
|
270
262
|
end
|
271
263
|
end
|
272
264
|
end
|
data/lib/async/io/tcp_socket.rb
CHANGED
@@ -28,33 +28,7 @@ module Async
|
|
28
28
|
class TCPSocket < IPSocket
|
29
29
|
wraps ::TCPSocket
|
30
30
|
|
31
|
-
|
32
|
-
def initialize(io)
|
33
|
-
@io = io
|
34
|
-
end
|
35
|
-
|
36
|
-
def sync= value
|
37
|
-
@io.sync = value
|
38
|
-
end
|
39
|
-
|
40
|
-
def close
|
41
|
-
@io.close
|
42
|
-
end
|
43
|
-
|
44
|
-
def read(*args)
|
45
|
-
@io.sysread(*args)
|
46
|
-
end
|
47
|
-
|
48
|
-
def write(*args)
|
49
|
-
@io.syswrite(*args)
|
50
|
-
end
|
51
|
-
|
52
|
-
def flush
|
53
|
-
@io.flush
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
def initialize(remote_host, remote_port = nil, local_host = nil, local_port = 0)
|
31
|
+
def initialize(remote_host, remote_port = nil, local_host = nil, local_port = nil)
|
58
32
|
if remote_host.is_a? ::TCPSocket
|
59
33
|
super(remote_host)
|
60
34
|
else
|
@@ -73,33 +47,33 @@ module Async
|
|
73
47
|
# super(::TCPSocket.new(remote_host, remote_port, local_host, local_port))
|
74
48
|
end
|
75
49
|
|
76
|
-
@
|
50
|
+
@stream = Stream.new(self)
|
77
51
|
end
|
78
52
|
|
79
53
|
class << self
|
80
54
|
alias open new
|
81
55
|
end
|
82
56
|
|
83
|
-
|
57
|
+
def close
|
58
|
+
@stream.flush
|
59
|
+
super
|
60
|
+
end
|
84
61
|
|
85
|
-
|
62
|
+
include Peer
|
86
63
|
|
87
|
-
|
64
|
+
attr :stream
|
88
65
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
super
|
93
|
-
end
|
66
|
+
# The way this buffering works is pretty atrocious.
|
67
|
+
def_delegators :@stream, :gets, :puts
|
94
68
|
|
95
|
-
def
|
96
|
-
|
69
|
+
def sysread(size, buffer = nil)
|
70
|
+
data = @stream.read_partial(size)
|
97
71
|
|
98
|
-
if
|
99
|
-
|
72
|
+
if buffer
|
73
|
+
buffer.replace(data)
|
100
74
|
end
|
101
75
|
|
102
|
-
return
|
76
|
+
return data
|
103
77
|
end
|
104
78
|
end
|
105
79
|
|
data/lib/async/io/version.rb
CHANGED
data/spec/async/io/echo_spec.rb
CHANGED
@@ -44,8 +44,9 @@ RSpec.describe "echo client/server" do
|
|
44
44
|
Async do |task|
|
45
45
|
Async::IO::Socket.connect(server_address) do |peer|
|
46
46
|
result = peer.write(data)
|
47
|
+
peer.close_write
|
47
48
|
|
48
|
-
message = peer.read(
|
49
|
+
message = peer.read(data.bytesize)
|
49
50
|
|
50
51
|
responses << message
|
51
52
|
end
|
@@ -22,3 +22,31 @@ RSpec.shared_examples Async::IO::Generic do |ignore_methods|
|
|
22
22
|
# end
|
23
23
|
# end
|
24
24
|
end
|
25
|
+
|
26
|
+
RSpec.shared_examples Async::IO do
|
27
|
+
let(:data) {"Hello World!"}
|
28
|
+
|
29
|
+
it "should read data" do
|
30
|
+
io.write(data)
|
31
|
+
expect(subject.read(data.bytesize)).to be == data
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should read less than available data" do
|
35
|
+
io.write(data)
|
36
|
+
expect(subject.read(1)).to be == data[0]
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should read all available data" do
|
40
|
+
io.write(data)
|
41
|
+
io.close_write
|
42
|
+
|
43
|
+
expect(subject.read(data.bytesize * 2)).to be == data
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should read all available data" do
|
47
|
+
io.write(data)
|
48
|
+
io.close_write
|
49
|
+
|
50
|
+
expect(subject.read).to be == data
|
51
|
+
end
|
52
|
+
end
|
@@ -43,17 +43,16 @@ RSpec.describe Async::IO::Generic do
|
|
43
43
|
|
44
44
|
output_task = reactor.async do
|
45
45
|
received = input.read(1024)
|
46
|
+
input.close
|
46
47
|
end
|
47
48
|
|
48
49
|
reactor.async do
|
49
50
|
output.write(message)
|
51
|
+
output.close
|
50
52
|
end
|
51
53
|
|
52
54
|
output_task.wait
|
53
55
|
expect(received).to be == message
|
54
|
-
|
55
|
-
input.close
|
56
|
-
output.close
|
57
56
|
end
|
58
57
|
|
59
58
|
describe '#wait' do
|
@@ -47,6 +47,7 @@ RSpec.describe Async::IO::Socket do
|
|
47
47
|
reactor.async do
|
48
48
|
Async::IO::Socket.connect(server_address) do |client|
|
49
49
|
client.write(data)
|
50
|
+
client.close_write
|
50
51
|
|
51
52
|
expect(client.read(512)).to be == data
|
52
53
|
end
|
@@ -59,6 +60,7 @@ RSpec.describe Async::IO::Socket do
|
|
59
60
|
reactor.async do |task|
|
60
61
|
Async::IO::Socket.connect(server_address, local_address: local_address) do |client|
|
61
62
|
client.write(data)
|
63
|
+
client.close_write
|
62
64
|
|
63
65
|
expect(client.read(512)).to be == data
|
64
66
|
end
|
@@ -69,6 +71,7 @@ RSpec.describe Async::IO::Socket do
|
|
69
71
|
reactor.async do |task|
|
70
72
|
Async::IO::Socket.connect(server_address) do |client|
|
71
73
|
client.write(data)
|
74
|
+
client.close_write
|
72
75
|
|
73
76
|
expect(client.read(512)).to be == data
|
74
77
|
end
|
@@ -84,6 +87,7 @@ RSpec.describe Async::IO::Socket do
|
|
84
87
|
|
85
88
|
reactor.async do
|
86
89
|
socket.write(data)
|
90
|
+
socket.close_write
|
87
91
|
|
88
92
|
expect(socket.read(512)).to be == data
|
89
93
|
|
@@ -19,6 +19,7 @@
|
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
21
|
require 'async/io/socket'
|
22
|
+
require 'async/io/address'
|
22
23
|
|
23
24
|
require_relative 'generic_examples'
|
24
25
|
|
@@ -124,6 +125,21 @@ RSpec.describe Async::IO::Socket do
|
|
124
125
|
s2.close
|
125
126
|
end
|
126
127
|
end
|
128
|
+
|
129
|
+
context '.pipe' do
|
130
|
+
let(:sockets) do
|
131
|
+
@sockets = described_class.pair(Socket::AF_UNIX, Socket::SOCK_STREAM)
|
132
|
+
end
|
133
|
+
|
134
|
+
after do
|
135
|
+
@sockets&.each(&:close)
|
136
|
+
end
|
137
|
+
|
138
|
+
let(:io) {sockets.first}
|
139
|
+
subject {sockets.last}
|
140
|
+
|
141
|
+
it_should_behave_like Async::IO
|
142
|
+
end
|
127
143
|
end
|
128
144
|
|
129
145
|
RSpec.describe Async::IO::IPSocket do
|
@@ -52,6 +52,7 @@ RSpec.describe Async::IO::SSLServer do
|
|
52
52
|
reactor.async do
|
53
53
|
client_endpoint.connect do |client|
|
54
54
|
client.write(data)
|
55
|
+
client.close_write
|
55
56
|
|
56
57
|
expect(client.read(512)).to be == data
|
57
58
|
end
|
@@ -91,6 +92,7 @@ RSpec.describe Async::IO::SSLServer do
|
|
91
92
|
reactor.async do
|
92
93
|
valid_client_endpoint.connect do |client|
|
93
94
|
client.write(data)
|
95
|
+
client.close_write
|
94
96
|
|
95
97
|
expect(client.read(512)).to be == data
|
96
98
|
end
|
@@ -18,7 +18,7 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
-
require 'async/io/
|
21
|
+
require 'async/io/ssl_endpoint'
|
22
22
|
|
23
23
|
require 'async/rspec/ssl'
|
24
24
|
require_relative 'generic_examples'
|
@@ -69,6 +69,7 @@ RSpec.describe Async::IO::SSLSocket do
|
|
69
69
|
expect(client.timeout).to be == 10
|
70
70
|
|
71
71
|
client.write(data)
|
72
|
+
client.close_write
|
72
73
|
|
73
74
|
expect(client.read(512)).to be == data
|
74
75
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# Copyright, 2019, 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 'async/rspec/buffer'
|
22
|
+
require 'async/io/stream'
|
23
|
+
|
24
|
+
RSpec.shared_context Async::IO::Stream do
|
25
|
+
include_context Async::RSpec::Buffer
|
26
|
+
subject {described_class.new(buffer)}
|
27
|
+
let(:io) {subject.io}
|
28
|
+
end
|
@@ -18,203 +18,251 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
-
require 'async/io/
|
22
|
-
|
21
|
+
require 'async/io/socket'
|
22
|
+
|
23
|
+
require_relative 'generic_examples'
|
24
|
+
require_relative 'stream_context'
|
23
25
|
|
24
26
|
RSpec.describe Async::IO::Stream do
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
27
|
+
# This constant is part of the public interface, but was renamed to `Async::IO::BLOCK_SIZE`.
|
28
|
+
describe "::BLOCK_SIZE" do
|
29
|
+
it "should exist and be reasonable" do
|
30
|
+
expect(Async::IO::Stream::BLOCK_SIZE).to be_between(1024, 1024*32)
|
31
|
+
end
|
32
|
+
end
|
31
33
|
|
32
|
-
|
33
|
-
let(:sockets)
|
34
|
-
|
35
|
-
|
34
|
+
context "socket I/O" do
|
35
|
+
let(:sockets) do
|
36
|
+
@sockets = Async::IO::Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM)
|
37
|
+
end
|
36
38
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
expect do
|
41
|
-
stream.read
|
42
|
-
end.to raise_error(IOError, /not opened for reading/)
|
39
|
+
after do
|
40
|
+
@sockets&.each(&:close)
|
43
41
|
end
|
44
42
|
|
45
|
-
|
46
|
-
|
43
|
+
let(:io) {sockets.first}
|
44
|
+
subject {described_class.new(sockets.last)}
|
45
|
+
|
46
|
+
it_should_behave_like Async::IO
|
47
|
+
|
48
|
+
describe '#close_read' do
|
49
|
+
let(:sockets) do
|
50
|
+
@sockets = Async::IO::Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM)
|
51
|
+
end
|
47
52
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
end
|
54
|
-
|
55
|
-
describe '#read' do
|
56
|
-
it "should read everything" do
|
57
|
-
io.write "Hello World"
|
58
|
-
io.seek(0)
|
53
|
+
after do
|
54
|
+
@sockets&.each(&:close)
|
55
|
+
end
|
56
|
+
|
57
|
+
subject {described_class.new(sockets.last)}
|
59
58
|
|
60
|
-
|
59
|
+
it "can close the reading end of the stream" do
|
60
|
+
expect(subject.io).to receive(:close_read).and_call_original
|
61
|
+
|
62
|
+
subject.close_read
|
63
|
+
|
64
|
+
# Ruby <= 2.4 raises an exception even with exception: false
|
65
|
+
# expect(stream.read).to be_nil
|
66
|
+
end
|
61
67
|
|
62
|
-
|
63
|
-
|
68
|
+
it "can close the writing end of the stream" do
|
69
|
+
expect(subject.io).to receive(:close_write).and_call_original
|
70
|
+
|
71
|
+
subject.write("Oh yes!")
|
72
|
+
subject.close_write
|
73
|
+
|
74
|
+
expect do
|
75
|
+
subject.write("Oh no!")
|
76
|
+
subject.flush
|
77
|
+
end.to raise_error(IOError, /not opened for writing/)
|
78
|
+
end
|
64
79
|
end
|
65
80
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
expect(stream.read(4)).to be == "Hell"
|
73
|
-
expect(stream).to_not be_eof
|
81
|
+
describe '#read_exactly' do
|
82
|
+
it "can read several bytes" do
|
83
|
+
io.write("hello\nworld\n")
|
84
|
+
|
85
|
+
expect(subject.read_exactly(4)).to be == 'hell'
|
86
|
+
end
|
74
87
|
|
75
|
-
|
76
|
-
|
88
|
+
it "can raise exception if io is eof" do
|
89
|
+
io.close
|
90
|
+
|
91
|
+
expect do
|
92
|
+
subject.read_exactly(4)
|
93
|
+
end.to raise_error(EOFError)
|
94
|
+
end
|
77
95
|
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context "buffered I/O" do
|
99
|
+
include_context Async::IO::Stream
|
100
|
+
include_context Async::RSpec::Memory
|
101
|
+
include_context Async::RSpec::Reactor
|
78
102
|
|
79
|
-
|
80
|
-
it "
|
81
|
-
io.write
|
103
|
+
describe '#read' do
|
104
|
+
it "should read everything" do
|
105
|
+
io.write "Hello World"
|
82
106
|
io.seek(0)
|
83
107
|
|
84
|
-
|
108
|
+
expect(subject.io).to receive(:read_nonblock).and_call_original.twice
|
85
109
|
|
86
|
-
expect
|
87
|
-
|
88
|
-
|
89
|
-
|
110
|
+
expect(subject.read).to be == "Hello World"
|
111
|
+
expect(subject).to be_eof
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should read only the amount requested" do
|
115
|
+
io.write "Hello World"
|
116
|
+
io.seek(0)
|
90
117
|
|
91
|
-
expect(
|
118
|
+
expect(subject.io).to receive(:read_nonblock).and_call_original.twice
|
92
119
|
|
93
|
-
|
120
|
+
expect(subject.read_partial(4)).to be == "Hell"
|
121
|
+
expect(subject).to_not be_eof
|
122
|
+
|
123
|
+
expect(subject.read_partial(20)).to be == "o World"
|
124
|
+
expect(subject).to be_eof
|
125
|
+
end
|
126
|
+
|
127
|
+
context "with large content" do
|
128
|
+
it "allocates expected amount of bytes" do
|
129
|
+
io.write("." * 16*1024)
|
130
|
+
io.seek(0)
|
131
|
+
|
132
|
+
buffer = nil
|
133
|
+
|
134
|
+
expect do
|
135
|
+
# The read buffer is already allocated, and it will be resized to fit the incoming data. It will be swapped with an empty buffer.
|
136
|
+
buffer = subject.read(16*1024)
|
137
|
+
end.to limit_allocations.of(String, count: 1, size: 0)
|
138
|
+
|
139
|
+
expect(buffer.size).to be == 16*1024
|
140
|
+
end
|
94
141
|
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
describe '#read_until' do
|
99
|
-
it "can read a line" do
|
100
|
-
io.write("hello\nworld\n")
|
101
|
-
io.seek(0)
|
102
|
-
|
103
|
-
expect(stream.read_until("\n")).to be == 'hello'
|
104
|
-
expect(stream.read_until("\n")).to be == 'world'
|
105
|
-
expect(stream.read_until("\n")).to be_nil
|
106
142
|
end
|
107
143
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
it "can read a line with a multi-byte pattern" do
|
112
|
-
io.write("hello\r\nworld\r\n")
|
144
|
+
describe '#read_until' do
|
145
|
+
it "can read a line" do
|
146
|
+
io.write("hello\nworld\n")
|
113
147
|
io.seek(0)
|
114
148
|
|
115
|
-
expect(
|
116
|
-
expect(
|
117
|
-
expect(
|
149
|
+
expect(subject.read_until("\n")).to be == 'hello'
|
150
|
+
expect(subject.read_until("\n")).to be == 'world'
|
151
|
+
expect(subject.read_until("\n")).to be_nil
|
118
152
|
end
|
119
|
-
end
|
120
153
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
154
|
+
context "with 1-byte block size" do
|
155
|
+
subject! {Async::IO::Stream.new(buffer, block_size: 1)}
|
156
|
+
|
157
|
+
it "can read a line with a multi-byte pattern" do
|
158
|
+
io.write("hello\r\nworld\r\n")
|
159
|
+
io.seek(0)
|
160
|
+
|
161
|
+
expect(subject.read_until("\r\n")).to be == 'hello'
|
162
|
+
expect(subject.read_until("\r\n")).to be == 'world'
|
163
|
+
expect(subject.read_until("\r\n")).to be_nil
|
164
|
+
end
|
126
165
|
end
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
describe '#flush' do
|
131
|
-
it "should not call write if write buffer is empty" do
|
132
|
-
expect(io).to_not receive(:write)
|
133
166
|
|
134
|
-
|
167
|
+
context "with large content" do
|
168
|
+
it "allocates expected amount of bytes" do
|
169
|
+
subject
|
170
|
+
|
171
|
+
expect do
|
172
|
+
subject.read_until("b")
|
173
|
+
end.to limit_allocations.of(String, size: 0, count: 1)
|
174
|
+
end
|
175
|
+
end
|
135
176
|
end
|
136
177
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
178
|
+
describe '#flush' do
|
179
|
+
it "should not call write if write buffer is empty" do
|
180
|
+
expect(subject.io).to_not receive(:write)
|
181
|
+
|
182
|
+
subject.flush
|
142
183
|
end
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
describe '#read_partial' do
|
147
|
-
before(:each) do
|
148
|
-
io.write "Hello World!" * 1024
|
149
|
-
io.seek(0)
|
150
|
-
end
|
151
184
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
185
|
+
it "should flush underlying data when it exceeds block size" do
|
186
|
+
expect(subject.io).to receive(:write).and_call_original.once
|
187
|
+
|
188
|
+
subject.block_size.times do
|
189
|
+
subject.write("!")
|
190
|
+
end
|
191
|
+
end
|
156
192
|
end
|
157
193
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
end.to limit_allocations.of(String, count: 2, size: 4*1024+1)
|
194
|
+
describe '#read_partial' do
|
195
|
+
before(:each) do
|
196
|
+
io.write("Hello World!" * 1024)
|
197
|
+
io.seek(0)
|
163
198
|
end
|
164
199
|
|
165
|
-
it "
|
166
|
-
expect
|
167
|
-
|
168
|
-
|
200
|
+
it "should avoid calling read" do
|
201
|
+
expect(subject.io).to receive(:read_nonblock).and_call_original.once
|
202
|
+
|
203
|
+
expect(subject.read_partial(12)).to be == "Hello World!"
|
169
204
|
end
|
170
205
|
|
171
|
-
|
172
|
-
|
206
|
+
context "with large content" do
|
207
|
+
it "allocates only the amount required" do
|
208
|
+
expect do
|
209
|
+
subject.read(4*1024)
|
210
|
+
end.to limit_allocations.of(String, count: 2, size: 4*1024+1)
|
211
|
+
end
|
173
212
|
|
174
|
-
|
175
|
-
|
176
|
-
|
213
|
+
it "allocates exact number of bytes being read" do
|
214
|
+
expect do
|
215
|
+
subject.read_partial(16*1024)
|
216
|
+
end.to limit_allocations.of(String, count: 1, size: 0)
|
217
|
+
end
|
177
218
|
|
178
|
-
|
219
|
+
it "allocates expected amount of bytes" do
|
220
|
+
buffer = nil
|
221
|
+
|
222
|
+
expect do
|
223
|
+
buffer = subject.read_partial
|
224
|
+
end.to limit_allocations.of(String, count: 1)
|
225
|
+
|
226
|
+
expect(buffer.size).to be == subject.block_size
|
227
|
+
end
|
179
228
|
end
|
180
229
|
end
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
expect(stream.read).to be == "Hello World\n"
|
230
|
+
|
231
|
+
describe '#write' do
|
232
|
+
it "should read one line" do
|
233
|
+
expect(subject.io).to receive(:write).and_call_original.once
|
234
|
+
|
235
|
+
subject.write "Hello World\n"
|
236
|
+
subject.flush
|
237
|
+
|
238
|
+
io.seek(0)
|
239
|
+
expect(subject.read).to be == "Hello World\n"
|
240
|
+
end
|
193
241
|
end
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
242
|
+
|
243
|
+
describe '#eof' do
|
244
|
+
it "should terminate subject" do
|
245
|
+
expect do
|
246
|
+
subject.eof!
|
247
|
+
end.to raise_exception(EOFError)
|
248
|
+
|
249
|
+
expect(subject).to be_eof
|
250
|
+
end
|
203
251
|
end
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
end
|
252
|
+
|
253
|
+
describe '#close' do
|
254
|
+
it 'can be closed even if underlying io is closed' do
|
255
|
+
io.close
|
256
|
+
|
257
|
+
expect(subject.io).to be_closed
|
258
|
+
|
259
|
+
# Put some data in the write buffer
|
260
|
+
subject.write "."
|
261
|
+
|
262
|
+
expect do
|
263
|
+
subject.close
|
264
|
+
end.to_not raise_exception
|
265
|
+
end
|
218
266
|
end
|
219
267
|
end
|
220
268
|
end
|
@@ -22,7 +22,7 @@ require 'async/io/tcp_socket'
|
|
22
22
|
|
23
23
|
require_relative 'generic_examples'
|
24
24
|
|
25
|
-
RSpec.describe Async::IO::TCPSocket do
|
25
|
+
RSpec.describe Async::IO::TCPSocket, timeout: 1 do
|
26
26
|
include_context Async::RSpec::Reactor
|
27
27
|
|
28
28
|
it_should_behave_like Async::IO::Generic
|
@@ -44,6 +44,7 @@ RSpec.describe Async::IO::TCPSocket do
|
|
44
44
|
|
45
45
|
data = peer.gets
|
46
46
|
peer.puts(data)
|
47
|
+
peer.flush
|
47
48
|
|
48
49
|
peer.close
|
49
50
|
server.close
|
@@ -52,15 +53,16 @@ RSpec.describe Async::IO::TCPSocket do
|
|
52
53
|
|
53
54
|
let(:client) {Async::IO::TCPSocket.new("localhost", 6788)}
|
54
55
|
|
55
|
-
it "can read into
|
56
|
+
it "can read into output buffer" do
|
56
57
|
client.puts("Hello World")
|
58
|
+
client.flush
|
57
59
|
|
58
|
-
|
60
|
+
buffer = String.new
|
59
61
|
# 20 is bigger than echo response...
|
60
|
-
data = client.read(20,
|
62
|
+
data = client.read(20, buffer)
|
61
63
|
|
62
|
-
expect(
|
63
|
-
expect(
|
64
|
+
expect(buffer).to_not be_empty
|
65
|
+
expect(buffer).to be == data
|
64
66
|
|
65
67
|
client.close
|
66
68
|
server_task.wait
|
@@ -69,6 +71,8 @@ RSpec.describe Async::IO::TCPSocket do
|
|
69
71
|
it "should start server and send data" do
|
70
72
|
# Accept a single incoming connection and then finish.
|
71
73
|
client.puts(data)
|
74
|
+
client.flush
|
75
|
+
|
72
76
|
expect(client.gets).to be == data
|
73
77
|
|
74
78
|
client.close
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: async-io
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.23.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-05-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: async
|
@@ -130,6 +130,7 @@ files:
|
|
130
130
|
- examples/chat/client.rb
|
131
131
|
- examples/chat/server.rb
|
132
132
|
- examples/issues/broken_ssl.rb
|
133
|
+
- examples/issues/pipes.rb
|
133
134
|
- examples/millions/client.rb
|
134
135
|
- examples/millions/server.rb
|
135
136
|
- examples/udp/client.rb
|
@@ -176,6 +177,7 @@ files:
|
|
176
177
|
- spec/async/io/ssl_server_spec.rb
|
177
178
|
- spec/async/io/ssl_socket_spec.rb
|
178
179
|
- spec/async/io/standard_spec.rb
|
180
|
+
- spec/async/io/stream_context.rb
|
179
181
|
- spec/async/io/stream_spec.rb
|
180
182
|
- spec/async/io/tcp_socket_spec.rb
|
181
183
|
- spec/async/io/trap_spec.rb
|
@@ -204,7 +206,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
204
206
|
- !ruby/object:Gem::Version
|
205
207
|
version: '0'
|
206
208
|
requirements: []
|
207
|
-
rubygems_version: 3.0.
|
209
|
+
rubygems_version: 3.0.2
|
208
210
|
signing_key:
|
209
211
|
specification_version: 4
|
210
212
|
summary: Provides support for asynchonous TCP, UDP, UNIX and SSL sockets.
|
@@ -226,6 +228,7 @@ test_files:
|
|
226
228
|
- spec/async/io/ssl_server_spec.rb
|
227
229
|
- spec/async/io/ssl_socket_spec.rb
|
228
230
|
- spec/async/io/standard_spec.rb
|
231
|
+
- spec/async/io/stream_context.rb
|
229
232
|
- spec/async/io/stream_spec.rb
|
230
233
|
- spec/async/io/tcp_socket_spec.rb
|
231
234
|
- spec/async/io/trap_spec.rb
|