async-io 1.22.0 → 1.23.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.
- 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
|