io-stream 0.6.0 → 0.7.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
- checksums.yaml.gz.sig +0 -0
- data/lib/io/stream/buffered.rb +28 -7
- data/lib/io/stream/generic.rb +18 -322
- data/lib/io/stream/openssl.rb +15 -0
- data/lib/io/stream/readable.rb +324 -0
- data/lib/io/stream/shim/buffered.rb +16 -0
- data/lib/io/stream/shim/readable.rb +8 -0
- data/lib/io/stream/string_buffer.rb +5 -0
- data/lib/io/stream/version.rb +1 -1
- data/lib/io/stream/writable.rb +92 -0
- data/lib/io/stream.rb +5 -0
- data/license.md +1 -1
- data/readme.md +58 -4
- data/releases.md +70 -0
- data.tar.gz.sig +0 -0
- metadata +7 -11
- metadata.gz.sig +4 -5
- data/lib/io/stream/shim/shim.md +0 -4
- data/lib/io/stream/shim/timeout.rb +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c48d464c539d56862e04437f88dee2997e4393951efca83cf622a23bac9e4b75
|
4
|
+
data.tar.gz: 610277102a9233e5d470daabd4f12917d429dc6a141cf52897ff95af3a06de95
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5761c359f16364169757dca90c4f41bfb87269ab3219b6a565b0081aafb3146f2ff6b11e77a95bebd478b31cbc702752e486c484666536f286e4ccdb4a2c6520
|
7
|
+
data.tar.gz: 6e44c1ce99d3741c6c9e8f65976f5664d287586e5a704749466a70992dcfcf5bac55ab84e1429cc5a22417af889ee7938af55ec63081309b3522629a9e05721b
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/lib/io/stream/buffered.rb
CHANGED
@@ -6,7 +6,13 @@
|
|
6
6
|
require_relative "generic"
|
7
7
|
|
8
8
|
module IO::Stream
|
9
|
+
# A buffered stream implementation that wraps an underlying IO object to provide efficient buffered reading and writing.
|
9
10
|
class Buffered < Generic
|
11
|
+
# Open a file and wrap it in a buffered stream.
|
12
|
+
# @parameter path [String] The file path to open.
|
13
|
+
# @parameter mode [String] The file mode (e.g., "r+", "w", "a").
|
14
|
+
# @parameter options [Hash] Additional options passed to the stream constructor.
|
15
|
+
# @returns [IO::Stream::Buffered] A buffered stream wrapping the opened file.
|
10
16
|
def self.open(path, mode = "r+", **options)
|
11
17
|
stream = self.new(::File.open(path, mode), **options)
|
12
18
|
|
@@ -19,6 +25,10 @@ module IO::Stream
|
|
19
25
|
end
|
20
26
|
end
|
21
27
|
|
28
|
+
# Wrap an existing IO object in a buffered stream.
|
29
|
+
# @parameter io [IO] The IO object to wrap.
|
30
|
+
# @parameter options [Hash] Additional options passed to the stream constructor.
|
31
|
+
# @returns [IO::Stream::Buffered] A buffered stream wrapping the IO object.
|
22
32
|
def self.wrap(io, **options)
|
23
33
|
if io.respond_to?(:buffered=)
|
24
34
|
io.buffered = false
|
@@ -37,6 +47,8 @@ module IO::Stream
|
|
37
47
|
end
|
38
48
|
end
|
39
49
|
|
50
|
+
# Initialize a new buffered stream.
|
51
|
+
# @parameter io [IO] The underlying IO object to wrap.
|
40
52
|
def initialize(io, ...)
|
41
53
|
super(...)
|
42
54
|
|
@@ -47,34 +59,43 @@ module IO::Stream
|
|
47
59
|
@timeout = nil
|
48
60
|
end
|
49
61
|
end
|
50
|
-
|
62
|
+
|
63
|
+
# @attribute [IO] The wrapped IO object.
|
51
64
|
attr :io
|
52
|
-
|
65
|
+
|
66
|
+
# Get the underlying IO object.
|
67
|
+
# @returns [IO] The underlying IO object.
|
53
68
|
def to_io
|
54
69
|
@io.to_io
|
55
70
|
end
|
56
|
-
|
71
|
+
|
72
|
+
# Check if the stream is closed.
|
73
|
+
# @returns [Boolean] True if the stream is closed.
|
57
74
|
def closed?
|
58
75
|
@io.closed?
|
59
76
|
end
|
60
|
-
|
77
|
+
|
78
|
+
# Close the read end of the stream.
|
61
79
|
def close_read
|
62
80
|
@io.close_read
|
63
81
|
end
|
64
|
-
|
82
|
+
|
83
|
+
# Close the write end of the stream.
|
65
84
|
def close_write
|
66
85
|
super
|
67
86
|
ensure
|
68
87
|
@io.close_write
|
69
88
|
end
|
70
|
-
|
89
|
+
|
90
|
+
# Check if the stream is readable.
|
91
|
+
# @returns [Boolean] True if the stream is readable.
|
71
92
|
def readable?
|
72
93
|
super && @io.readable?
|
73
94
|
end
|
74
95
|
|
75
96
|
protected
|
76
97
|
|
77
|
-
if RUBY_VERSION >= "3.3.0"
|
98
|
+
if RUBY_VERSION >= "3.3.0" and RUBY_VERSION < "3.3.6"
|
78
99
|
def sysclose
|
79
100
|
# https://bugs.ruby-lang.org/issues/20723
|
80
101
|
Thread.new{@io.close}.join
|
data/lib/io/stream/generic.rb
CHANGED
@@ -4,247 +4,32 @@
|
|
4
4
|
# Copyright, 2023-2024, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative "string_buffer"
|
7
|
+
require_relative "readable"
|
8
|
+
require_relative "writable"
|
7
9
|
|
8
10
|
require_relative "shim/buffered"
|
9
11
|
require_relative "shim/readable"
|
10
|
-
require_relative "shim/timeout"
|
11
12
|
|
12
13
|
require_relative "openssl"
|
13
14
|
|
14
15
|
module IO::Stream
|
15
|
-
#
|
16
|
-
BLOCK_SIZE = ENV.fetch("IO_STREAM_BLOCK_SIZE", 1024*64).to_i
|
17
|
-
|
18
|
-
# The maximum read size when appending to IO buffers. Defaults to 8MB.
|
19
|
-
MAXIMUM_READ_SIZE = ENV.fetch("IO_STREAM_MAXIMUM_READ_SIZE", BLOCK_SIZE * 128).to_i
|
20
|
-
|
21
|
-
class LimitError < StandardError
|
22
|
-
end
|
23
|
-
|
16
|
+
# Base class for stream implementations providing common functionality.
|
24
17
|
class Generic
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
@writing = ::Thread::Mutex.new
|
29
|
-
|
30
|
-
@block_size = block_size
|
31
|
-
@maximum_read_size = maximum_read_size
|
32
|
-
|
33
|
-
@read_buffer = StringBuffer.new
|
34
|
-
@write_buffer = StringBuffer.new
|
35
|
-
|
36
|
-
# Used as destination buffer for underlying reads.
|
37
|
-
@input_buffer = StringBuffer.new
|
38
|
-
end
|
39
|
-
|
40
|
-
attr_accessor :block_size
|
41
|
-
|
42
|
-
# Reads `size` bytes from the stream. If size is not specified, read until end of file.
|
43
|
-
def read(size = nil)
|
44
|
-
return String.new(encoding: Encoding::BINARY) if size == 0
|
45
|
-
|
46
|
-
if size
|
47
|
-
until @eof or @read_buffer.bytesize >= size
|
48
|
-
# Compute the amount of data we need to read from the underlying stream:
|
49
|
-
read_size = size - @read_buffer.bytesize
|
50
|
-
|
51
|
-
# Don't read less than @block_size to avoid lots of small reads:
|
52
|
-
fill_read_buffer(read_size > @block_size ? read_size : @block_size)
|
53
|
-
end
|
54
|
-
else
|
55
|
-
until @eof
|
56
|
-
fill_read_buffer
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
return consume_read_buffer(size)
|
61
|
-
end
|
62
|
-
|
63
|
-
# Read at most `size` bytes from the stream. Will avoid reading from the underlying stream if possible.
|
64
|
-
def read_partial(size = nil)
|
65
|
-
return String.new(encoding: Encoding::BINARY) if size == 0
|
66
|
-
|
67
|
-
if !@eof and @read_buffer.empty?
|
68
|
-
fill_read_buffer
|
69
|
-
end
|
70
|
-
|
71
|
-
return consume_read_buffer(size)
|
72
|
-
end
|
73
|
-
|
74
|
-
def read_exactly(size, exception: EOFError)
|
75
|
-
if buffer = read(size)
|
76
|
-
if buffer.bytesize != size
|
77
|
-
raise exception, "could not read enough data"
|
78
|
-
end
|
79
|
-
|
80
|
-
return buffer
|
81
|
-
end
|
82
|
-
|
83
|
-
raise exception, "encountered eof while reading data"
|
84
|
-
end
|
85
|
-
|
86
|
-
# This is a compatibility shim for existing code that uses `readpartial`.
|
87
|
-
def readpartial(size = nil)
|
88
|
-
read_partial(size) or raise EOFError, "Encountered eof while reading data!"
|
89
|
-
end
|
90
|
-
|
91
|
-
private def index_of(pattern, offset, limit)
|
92
|
-
# We don't want to split on the pattern, so we subtract the size of the pattern.
|
93
|
-
split_offset = pattern.bytesize - 1
|
94
|
-
|
95
|
-
until index = @read_buffer.index(pattern, offset)
|
96
|
-
offset = @read_buffer.bytesize - split_offset
|
97
|
-
|
98
|
-
offset = 0 if offset < 0
|
99
|
-
|
100
|
-
return nil if limit and offset >= limit
|
101
|
-
return nil unless fill_read_buffer
|
102
|
-
end
|
103
|
-
|
104
|
-
return index
|
105
|
-
end
|
106
|
-
|
107
|
-
# Efficiently read data from the stream until encountering pattern.
|
108
|
-
# @parameter pattern [String] The pattern to match.
|
109
|
-
# @parameter offset [Integer] The offset to start searching from.
|
110
|
-
# @parameter limit [Integer] The maximum number of bytes to read, including the pattern (even if chomped).
|
111
|
-
# @returns [String | Nil] The contents of the stream up until the pattern, which is consumed but not returned.
|
112
|
-
def read_until(pattern, offset = 0, limit: nil, chomp: true)
|
113
|
-
if index = index_of(pattern, offset, limit)
|
114
|
-
return nil if limit and index >= limit
|
115
|
-
|
116
|
-
@read_buffer.freeze
|
117
|
-
matched = @read_buffer.byteslice(0, index+(chomp ? 0 : pattern.bytesize))
|
118
|
-
@read_buffer = @read_buffer.byteslice(index+pattern.bytesize, @read_buffer.bytesize)
|
119
|
-
|
120
|
-
return matched
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
def peek(size = nil)
|
125
|
-
if size
|
126
|
-
until @eof or @read_buffer.bytesize >= size
|
127
|
-
# Compute the amount of data we need to read from the underlying stream:
|
128
|
-
read_size = size - @read_buffer.bytesize
|
129
|
-
|
130
|
-
# Don't read less than @block_size to avoid lots of small reads:
|
131
|
-
fill_read_buffer(read_size > @block_size ? read_size : @block_size)
|
132
|
-
end
|
133
|
-
return @read_buffer[..([size, @read_buffer.size].min - 1)]
|
134
|
-
end
|
135
|
-
until (block_given? && yield(@read_buffer)) or @eof
|
136
|
-
fill_read_buffer
|
137
|
-
end
|
138
|
-
return @read_buffer
|
139
|
-
end
|
140
|
-
|
141
|
-
def gets(separator = $/, limit = nil, chomp: false)
|
142
|
-
# Compatibility with IO#gets:
|
143
|
-
if separator.is_a?(Integer)
|
144
|
-
limit = separator
|
145
|
-
separator = $/
|
146
|
-
end
|
147
|
-
|
148
|
-
# We don't want to split in the middle of the separator, so we subtract the size of the separator from the start of the search:
|
149
|
-
split_offset = separator.bytesize - 1
|
150
|
-
|
151
|
-
offset = 0
|
152
|
-
|
153
|
-
until index = @read_buffer.index(separator, offset)
|
154
|
-
offset = @read_buffer.bytesize - split_offset
|
155
|
-
offset = 0 if offset < 0
|
156
|
-
|
157
|
-
# If a limit was given, and the offset is beyond the limit, we should return up to the limit:
|
158
|
-
if limit and offset >= limit
|
159
|
-
# As we didn't find the separator, there is nothing to chomp either.
|
160
|
-
return consume_read_buffer(limit)
|
161
|
-
end
|
162
|
-
|
163
|
-
# If we can't read any more data, we should return what we have:
|
164
|
-
return consume_read_buffer unless fill_read_buffer
|
165
|
-
end
|
166
|
-
|
167
|
-
# If the index of the separator was beyond the limit:
|
168
|
-
if limit and index >= limit
|
169
|
-
# Return up to the limit:
|
170
|
-
return consume_read_buffer(limit)
|
171
|
-
end
|
172
|
-
|
173
|
-
# Freeze the read buffer, as this enables us to use byteslice without generating a hidden copy:
|
174
|
-
@read_buffer.freeze
|
175
|
-
|
176
|
-
line = @read_buffer.byteslice(0, index+(chomp ? 0 : separator.bytesize))
|
177
|
-
@read_buffer = @read_buffer.byteslice(index+separator.bytesize, @read_buffer.bytesize)
|
178
|
-
|
179
|
-
return line
|
180
|
-
end
|
18
|
+
include Readable
|
19
|
+
include Writable
|
181
20
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
# If the write operation fails, we still need to clear this buffer, and the data is essentially lost.
|
187
|
-
buffer.clear
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
# Flushes buffered data to the stream.
|
192
|
-
def flush
|
193
|
-
return if @write_buffer.empty?
|
194
|
-
|
195
|
-
@writing.synchronize do
|
196
|
-
self.drain(@write_buffer)
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
# Writes `string` to the buffer. When the buffer is full or #sync is true the
|
201
|
-
# buffer is flushed to the underlying `io`.
|
202
|
-
# @parameter string [String] the string to write to the buffer.
|
203
|
-
# @returns [Integer] the number of bytes appended to the buffer.
|
204
|
-
def write(string, flush: false)
|
205
|
-
@writing.synchronize do
|
206
|
-
@write_buffer << string
|
207
|
-
|
208
|
-
flush |= (@write_buffer.bytesize >= @block_size)
|
209
|
-
|
210
|
-
if flush
|
211
|
-
self.drain(@write_buffer)
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
|
-
return string.bytesize
|
216
|
-
end
|
217
|
-
|
218
|
-
# Writes `string` to the stream and returns self.
|
219
|
-
def <<(string)
|
220
|
-
write(string)
|
221
|
-
|
222
|
-
return self
|
223
|
-
end
|
224
|
-
|
225
|
-
def puts(*arguments, separator: $/)
|
226
|
-
return if arguments.empty?
|
227
|
-
|
228
|
-
@writing.synchronize do
|
229
|
-
arguments.each do |argument|
|
230
|
-
@write_buffer << argument << separator
|
231
|
-
end
|
232
|
-
|
233
|
-
self.drain(@write_buffer)
|
234
|
-
end
|
21
|
+
# Initialize a new generic stream.
|
22
|
+
# @parameter options [Hash] Options passed to included modules.
|
23
|
+
def initialize(**options)
|
24
|
+
super(**options)
|
235
25
|
end
|
236
26
|
|
27
|
+
# Check if the stream is closed.
|
28
|
+
# @returns [Boolean] False by default, should be overridden by subclasses.
|
237
29
|
def closed?
|
238
30
|
false
|
239
31
|
end
|
240
32
|
|
241
|
-
def close_read
|
242
|
-
end
|
243
|
-
|
244
|
-
def close_write
|
245
|
-
flush
|
246
|
-
end
|
247
|
-
|
248
33
|
# Best effort to flush any unwritten data, and then close the underling IO.
|
249
34
|
def close
|
250
35
|
return if closed?
|
@@ -258,115 +43,26 @@ module IO::Stream
|
|
258
43
|
end
|
259
44
|
end
|
260
45
|
|
261
|
-
# Determins if the stream has consumed all available data. May block if the stream is not readable.
|
262
|
-
# See {readable?} for a non-blocking alternative.
|
263
|
-
#
|
264
|
-
# @returns [Boolean] If the stream is at file which means there is no more data to be read.
|
265
|
-
def eof?
|
266
|
-
if !@read_buffer.empty?
|
267
|
-
return false
|
268
|
-
elsif @eof
|
269
|
-
return true
|
270
|
-
else
|
271
|
-
return !self.fill_read_buffer
|
272
|
-
end
|
273
|
-
end
|
274
|
-
|
275
|
-
def eof!
|
276
|
-
@read_buffer.clear
|
277
|
-
@eof = true
|
278
|
-
|
279
|
-
raise EOFError
|
280
|
-
end
|
281
|
-
|
282
|
-
# Whether there is a chance that a read operation will succeed or not.
|
283
|
-
# @returns [Boolean] If the stream is readable, i.e. a `read` operation has a chance of success.
|
284
|
-
def readable?
|
285
|
-
# If we are at the end of the file, we can't read any more data:
|
286
|
-
if @eof
|
287
|
-
return false
|
288
|
-
end
|
289
|
-
|
290
|
-
# If the read buffer is not empty, we can read more data:
|
291
|
-
if !@read_buffer.empty?
|
292
|
-
return true
|
293
|
-
end
|
294
|
-
|
295
|
-
# If the underlying stream is readable, we can read more data:
|
296
|
-
return !closed?
|
297
|
-
end
|
298
|
-
|
299
46
|
protected
|
300
47
|
|
48
|
+
# Closes the underlying IO stream.
|
49
|
+
# This method should be implemented by subclasses to handle the specific closing logic.
|
301
50
|
def sysclose
|
302
51
|
raise NotImplementedError
|
303
52
|
end
|
304
53
|
|
54
|
+
# Writes data to the underlying stream.
|
55
|
+
# This method should be implemented by subclasses to handle the specific writing logic.
|
56
|
+
# @parameter buffer [String] The data to write.
|
57
|
+
# @returns [Integer] The number of bytes written.
|
305
58
|
def syswrite(buffer)
|
306
59
|
raise NotImplementedError
|
307
60
|
end
|
308
61
|
|
309
62
|
# Reads data from the underlying stream as efficiently as possible.
|
63
|
+
# This method should be implemented by subclasses to handle the specific reading logic.
|
310
64
|
def sysread(size, buffer)
|
311
65
|
raise NotImplementedError
|
312
66
|
end
|
313
|
-
|
314
|
-
private
|
315
|
-
|
316
|
-
# Fills the buffer from the underlying stream.
|
317
|
-
def fill_read_buffer(size = @block_size)
|
318
|
-
# We impose a limit because the underlying `read` system call can fail if we request too much data in one go.
|
319
|
-
if size > @maximum_read_size
|
320
|
-
size = @maximum_read_size
|
321
|
-
end
|
322
|
-
|
323
|
-
# This effectively ties the input and output stream together.
|
324
|
-
flush
|
325
|
-
|
326
|
-
if @read_buffer.empty?
|
327
|
-
if sysread(size, @read_buffer)
|
328
|
-
# Console.info(self, name: "read") {@read_buffer.inspect}
|
329
|
-
return true
|
330
|
-
end
|
331
|
-
else
|
332
|
-
if chunk = sysread(size, @input_buffer)
|
333
|
-
@read_buffer << chunk
|
334
|
-
# Console.info(self, name: "read") {@read_buffer.inspect}
|
335
|
-
|
336
|
-
return true
|
337
|
-
end
|
338
|
-
end
|
339
|
-
|
340
|
-
# else for both cases above:
|
341
|
-
@eof = true
|
342
|
-
return false
|
343
|
-
end
|
344
|
-
|
345
|
-
# Consumes at most `size` bytes from the buffer.
|
346
|
-
# @parameter size [Integer|nil] The amount of data to consume. If nil, consume entire buffer.
|
347
|
-
def consume_read_buffer(size = nil)
|
348
|
-
# If we are at eof, and the read buffer is empty, we can't consume anything.
|
349
|
-
return nil if @eof && @read_buffer.empty?
|
350
|
-
|
351
|
-
result = nil
|
352
|
-
|
353
|
-
if size.nil? or size >= @read_buffer.bytesize
|
354
|
-
# Consume the entire read buffer:
|
355
|
-
result = @read_buffer
|
356
|
-
@read_buffer = StringBuffer.new
|
357
|
-
else
|
358
|
-
# This approach uses more memory.
|
359
|
-
# result = @read_buffer.slice!(0, size)
|
360
|
-
|
361
|
-
# We know that we are not going to reuse the original buffer.
|
362
|
-
# But byteslice will generate a hidden copy. So let's freeze it first:
|
363
|
-
@read_buffer.freeze
|
364
|
-
|
365
|
-
result = @read_buffer.byteslice(0, size)
|
366
|
-
@read_buffer = @read_buffer.byteslice(size, @read_buffer.bytesize)
|
367
|
-
end
|
368
|
-
|
369
|
-
return result
|
370
|
-
end
|
371
67
|
end
|
372
68
|
end
|
data/lib/io/stream/openssl.rb
CHANGED
@@ -5,52 +5,67 @@
|
|
5
5
|
|
6
6
|
require "openssl"
|
7
7
|
|
8
|
+
# @namespace
|
8
9
|
module OpenSSL
|
10
|
+
# @namespace
|
9
11
|
module SSL
|
12
|
+
# SSL socket extensions for stream compatibility.
|
10
13
|
class SSLSocket
|
11
14
|
unless method_defined?(:close_read)
|
15
|
+
# Close the read end of the SSL socket.
|
12
16
|
def close_read
|
13
17
|
# Ignored.
|
14
18
|
end
|
15
19
|
end
|
16
20
|
|
17
21
|
unless method_defined?(:close_write)
|
22
|
+
# Close the write end of the SSL socket.
|
18
23
|
def close_write
|
19
24
|
self.stop
|
20
25
|
end
|
21
26
|
end
|
22
27
|
|
23
28
|
unless method_defined?(:wait_readable)
|
29
|
+
# Wait for the SSL socket to become readable.
|
24
30
|
def wait_readable(...)
|
25
31
|
to_io.wait_readable(...)
|
26
32
|
end
|
27
33
|
end
|
28
34
|
|
29
35
|
unless method_defined?(:wait_writable)
|
36
|
+
# Wait for the SSL socket to become writable.
|
30
37
|
def wait_writable(...)
|
31
38
|
to_io.wait_writable(...)
|
32
39
|
end
|
33
40
|
end
|
34
41
|
|
35
42
|
unless method_defined?(:timeout)
|
43
|
+
# Get the timeout for SSL socket operations.
|
44
|
+
# @returns [Numeric | Nil] The timeout value.
|
36
45
|
def timeout
|
37
46
|
to_io.timeout
|
38
47
|
end
|
39
48
|
end
|
40
49
|
|
41
50
|
unless method_defined?(:timeout=)
|
51
|
+
# Set the timeout for SSL socket operations.
|
52
|
+
# @parameter value [Numeric | Nil] The timeout value.
|
42
53
|
def timeout=(value)
|
43
54
|
to_io.timeout = value
|
44
55
|
end
|
45
56
|
end
|
46
57
|
|
47
58
|
unless method_defined?(:buffered?)
|
59
|
+
# Check if the SSL socket is buffered.
|
60
|
+
# @returns [Boolean] True if the SSL socket is buffered.
|
48
61
|
def buffered?
|
49
62
|
return to_io.buffered?
|
50
63
|
end
|
51
64
|
end
|
52
65
|
|
53
66
|
unless method_defined?(:buffered=)
|
67
|
+
# Set the buffered state of the SSL socket.
|
68
|
+
# @parameter value [Boolean] True to enable buffering, false to disable.
|
54
69
|
def buffered=(value)
|
55
70
|
to_io.buffered = value
|
56
71
|
end
|
@@ -0,0 +1,324 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2023-2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative "string_buffer"
|
7
|
+
|
8
|
+
module IO::Stream
|
9
|
+
# The default block size for IO buffers. Defaults to 256KB (optimized for modern SSDs and networks).
|
10
|
+
BLOCK_SIZE = ENV.fetch("IO_STREAM_BLOCK_SIZE", 1024*256).to_i
|
11
|
+
|
12
|
+
# The minimum read size for efficient I/O operations. Defaults to the same as BLOCK_SIZE.
|
13
|
+
MINIMUM_READ_SIZE = ENV.fetch("IO_STREAM_MINIMUM_READ_SIZE", BLOCK_SIZE).to_i
|
14
|
+
|
15
|
+
# The maximum read size for a single read operation. This limit exists because:
|
16
|
+
# 1. System calls like read() cannot handle requests larger than SSIZE_MAX
|
17
|
+
# 2. Very large reads can cause memory pressure and poor interactive performance
|
18
|
+
# 3. Most socket buffers and pipe capacities are much smaller anyway
|
19
|
+
# On 64-bit systems SSIZE_MAX is ~8.8 million MB, on 32-bit it's ~2GB.
|
20
|
+
# Our default of 16MB provides a good balance of throughput and responsiveness, and is page aligned.
|
21
|
+
# It is also a multiple of the minimum read size, so that we can read in chunks without exceeding the maximum.
|
22
|
+
MAXIMUM_READ_SIZE = ENV.fetch("IO_STREAM_MAXIMUM_READ_SIZE", MINIMUM_READ_SIZE * 64).to_i
|
23
|
+
|
24
|
+
# A module providing readable stream functionality.
|
25
|
+
#
|
26
|
+
# You must implement the `sysread` method to read data from the underlying IO.
|
27
|
+
module Readable
|
28
|
+
# Initialize readable stream functionality.
|
29
|
+
# @parameter minimum_read_size [Integer] The minimum size for read operations.
|
30
|
+
# @parameter maximum_read_size [Integer] The maximum size for read operations.
|
31
|
+
# @parameter block_size [Integer] Legacy parameter, use minimum_read_size instead.
|
32
|
+
def initialize(minimum_read_size: MINIMUM_READ_SIZE, maximum_read_size: MAXIMUM_READ_SIZE, block_size: nil, **, &block)
|
33
|
+
@done = false
|
34
|
+
@read_buffer = StringBuffer.new
|
35
|
+
# Used as destination buffer for underlying reads.
|
36
|
+
@input_buffer = StringBuffer.new
|
37
|
+
|
38
|
+
# Support legacy block_size parameter for backwards compatibility
|
39
|
+
@minimum_read_size = block_size || minimum_read_size
|
40
|
+
@maximum_read_size = maximum_read_size
|
41
|
+
|
42
|
+
super(**, &block) if defined?(super)
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_accessor :minimum_read_size
|
46
|
+
|
47
|
+
# Legacy accessor for backwards compatibility
|
48
|
+
# @returns [Integer] The minimum read size.
|
49
|
+
def block_size
|
50
|
+
@minimum_read_size
|
51
|
+
end
|
52
|
+
|
53
|
+
# Legacy setter for backwards compatibility
|
54
|
+
# @parameter value [Integer] The minimum read size.
|
55
|
+
def block_size=(value)
|
56
|
+
@minimum_read_size = value
|
57
|
+
end
|
58
|
+
|
59
|
+
# Read data from the stream.
|
60
|
+
# @parameter size [Integer | Nil] The number of bytes to read. If nil, read until end of stream.
|
61
|
+
# @returns [String] The data read from the stream.
|
62
|
+
def read(size = nil)
|
63
|
+
return String.new(encoding: Encoding::BINARY) if size == 0
|
64
|
+
|
65
|
+
if size
|
66
|
+
until @done or @read_buffer.bytesize >= size
|
67
|
+
# Compute the amount of data we need to read from the underlying stream:
|
68
|
+
read_size = size - @read_buffer.bytesize
|
69
|
+
|
70
|
+
# Don't read less than @minimum_read_size to avoid lots of small reads:
|
71
|
+
fill_read_buffer(read_size > @minimum_read_size ? read_size : @minimum_read_size)
|
72
|
+
end
|
73
|
+
else
|
74
|
+
until @done
|
75
|
+
fill_read_buffer
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
return consume_read_buffer(size)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Read at most `size` bytes from the stream. Will avoid reading from the underlying stream if possible.
|
83
|
+
def read_partial(size = nil)
|
84
|
+
return String.new(encoding: Encoding::BINARY) if size == 0
|
85
|
+
|
86
|
+
if !@done and @read_buffer.empty?
|
87
|
+
fill_read_buffer
|
88
|
+
end
|
89
|
+
|
90
|
+
return consume_read_buffer(size)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Read exactly the specified number of bytes.
|
94
|
+
# @parameter size [Integer] The number of bytes to read.
|
95
|
+
# @parameter exception [Class] The exception to raise if not enough data is available.
|
96
|
+
# @returns [String] The data read from the stream.
|
97
|
+
def read_exactly(size, exception: EOFError)
|
98
|
+
if buffer = read(size)
|
99
|
+
if buffer.bytesize != size
|
100
|
+
raise exception, "Could not read enough data!"
|
101
|
+
end
|
102
|
+
|
103
|
+
return buffer
|
104
|
+
end
|
105
|
+
|
106
|
+
raise exception, "Encountered done while reading data!"
|
107
|
+
end
|
108
|
+
|
109
|
+
# This is a compatibility shim for existing code that uses `readpartial`.
|
110
|
+
def readpartial(size = nil)
|
111
|
+
read_partial(size) or raise EOFError, "Encountered done while reading data!"
|
112
|
+
end
|
113
|
+
|
114
|
+
# Find the index of a pattern in the read buffer, reading more data if needed.
|
115
|
+
# @parameter pattern [String] The pattern to search for.
|
116
|
+
# @parameter offset [Integer] The offset to start searching from.
|
117
|
+
# @parameter limit [Integer | Nil] The maximum number of bytes to read while searching.
|
118
|
+
# @returns [Integer | Nil] The index of the pattern, or nil if not found.
|
119
|
+
private def index_of(pattern, offset, limit)
|
120
|
+
# We don't want to split on the pattern, so we subtract the size of the pattern.
|
121
|
+
split_offset = pattern.bytesize - 1
|
122
|
+
|
123
|
+
until index = @read_buffer.index(pattern, offset)
|
124
|
+
offset = @read_buffer.bytesize - split_offset
|
125
|
+
|
126
|
+
offset = 0 if offset < 0
|
127
|
+
|
128
|
+
return nil if limit and offset >= limit
|
129
|
+
return nil unless fill_read_buffer
|
130
|
+
end
|
131
|
+
|
132
|
+
return index
|
133
|
+
end
|
134
|
+
|
135
|
+
# Efficiently read data from the stream until encountering pattern.
|
136
|
+
# @parameter pattern [String] The pattern to match.
|
137
|
+
# @parameter offset [Integer] The offset to start searching from.
|
138
|
+
# @parameter limit [Integer] The maximum number of bytes to read, including the pattern (even if chomped).
|
139
|
+
# @returns [String | Nil] The contents of the stream up until the pattern, which is consumed but not returned.
|
140
|
+
def read_until(pattern, offset = 0, limit: nil, chomp: true)
|
141
|
+
if index = index_of(pattern, offset, limit)
|
142
|
+
return nil if limit and index >= limit
|
143
|
+
|
144
|
+
@read_buffer.freeze
|
145
|
+
matched = @read_buffer.byteslice(0, index+(chomp ? 0 : pattern.bytesize))
|
146
|
+
@read_buffer = @read_buffer.byteslice(index+pattern.bytesize, @read_buffer.bytesize)
|
147
|
+
|
148
|
+
return matched
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Peek at data in the buffer without consuming it.
|
153
|
+
# @parameter size [Integer | Nil] The number of bytes to peek at. If nil, peek at all available data.
|
154
|
+
# @returns [String] The data in the buffer without consuming it.
|
155
|
+
def peek(size = nil)
|
156
|
+
if size
|
157
|
+
until @done or @read_buffer.bytesize >= size
|
158
|
+
# Compute the amount of data we need to read from the underlying stream:
|
159
|
+
read_size = size - @read_buffer.bytesize
|
160
|
+
|
161
|
+
# Don't read less than @minimum_read_size to avoid lots of small reads:
|
162
|
+
fill_read_buffer(read_size > @minimum_read_size ? read_size : @minimum_read_size)
|
163
|
+
end
|
164
|
+
return @read_buffer[..([size, @read_buffer.size].min - 1)]
|
165
|
+
end
|
166
|
+
until (block_given? && yield(@read_buffer)) or @done
|
167
|
+
fill_read_buffer
|
168
|
+
end
|
169
|
+
return @read_buffer
|
170
|
+
end
|
171
|
+
|
172
|
+
# Read a line from the stream, similar to IO#gets.
|
173
|
+
# @parameter separator [String] The line separator to search for.
|
174
|
+
# @parameter limit [Integer | Nil] The maximum number of bytes to read.
|
175
|
+
# @parameter chomp [Boolean] Whether to remove the separator from the returned line.
|
176
|
+
# @returns [String | Nil] The line read from the stream, or nil if at end of stream.
|
177
|
+
def gets(separator = $/, limit = nil, chomp: false)
|
178
|
+
# Compatibility with IO#gets:
|
179
|
+
if separator.is_a?(Integer)
|
180
|
+
limit = separator
|
181
|
+
separator = $/
|
182
|
+
end
|
183
|
+
|
184
|
+
# We don't want to split in the middle of the separator, so we subtract the size of the separator from the start of the search:
|
185
|
+
split_offset = separator.bytesize - 1
|
186
|
+
|
187
|
+
offset = 0
|
188
|
+
|
189
|
+
until index = @read_buffer.index(separator, offset)
|
190
|
+
offset = @read_buffer.bytesize - split_offset
|
191
|
+
offset = 0 if offset < 0
|
192
|
+
|
193
|
+
# If a limit was given, and the offset is beyond the limit, we should return up to the limit:
|
194
|
+
if limit and offset >= limit
|
195
|
+
# As we didn't find the separator, there is nothing to chomp either.
|
196
|
+
return consume_read_buffer(limit)
|
197
|
+
end
|
198
|
+
|
199
|
+
# If we can't read any more data, we should return what we have:
|
200
|
+
return consume_read_buffer unless fill_read_buffer
|
201
|
+
end
|
202
|
+
|
203
|
+
# If the index of the separator was beyond the limit:
|
204
|
+
if limit and index >= limit
|
205
|
+
# Return up to the limit:
|
206
|
+
return consume_read_buffer(limit)
|
207
|
+
end
|
208
|
+
|
209
|
+
# Freeze the read buffer, as this enables us to use byteslice without generating a hidden copy:
|
210
|
+
@read_buffer.freeze
|
211
|
+
|
212
|
+
line = @read_buffer.byteslice(0, index+(chomp ? 0 : separator.bytesize))
|
213
|
+
@read_buffer = @read_buffer.byteslice(index+separator.bytesize, @read_buffer.bytesize)
|
214
|
+
|
215
|
+
return line
|
216
|
+
end
|
217
|
+
|
218
|
+
# Determins if the stream has consumed all available data. May block if the stream is not readable.
|
219
|
+
# See {readable?} for a non-blocking alternative.
|
220
|
+
#
|
221
|
+
# @returns [Boolean] If the stream is at file which means there is no more data to be read.
|
222
|
+
def done?
|
223
|
+
if !@read_buffer.empty?
|
224
|
+
return false
|
225
|
+
elsif @done
|
226
|
+
return true
|
227
|
+
else
|
228
|
+
return !self.fill_read_buffer
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
alias eof? done?
|
233
|
+
|
234
|
+
# Mark the stream as done and raise `EOFError`.
|
235
|
+
def done!
|
236
|
+
@read_buffer.clear
|
237
|
+
@done = true
|
238
|
+
|
239
|
+
raise EOFError
|
240
|
+
end
|
241
|
+
|
242
|
+
alias eof! done!
|
243
|
+
|
244
|
+
# Whether there is a chance that a read operation will succeed or not.
|
245
|
+
# @returns [Boolean] If the stream is readable, i.e. a `read` operation has a chance of success.
|
246
|
+
def readable?
|
247
|
+
# If we are at the end of the file, we can't read any more data:
|
248
|
+
if @done
|
249
|
+
return false
|
250
|
+
end
|
251
|
+
|
252
|
+
# If the read buffer is not empty, we can read more data:
|
253
|
+
if !@read_buffer.empty?
|
254
|
+
return true
|
255
|
+
end
|
256
|
+
|
257
|
+
# If the underlying stream is readable, we can read more data:
|
258
|
+
return !closed?
|
259
|
+
end
|
260
|
+
|
261
|
+
# Close the read end of the stream.
|
262
|
+
def close_read
|
263
|
+
end
|
264
|
+
|
265
|
+
private
|
266
|
+
|
267
|
+
# Fills the buffer from the underlying stream.
|
268
|
+
def fill_read_buffer(size = @minimum_read_size)
|
269
|
+
# Limit the read size to avoid exceeding SSIZE_MAX and to manage memory usage.
|
270
|
+
# Very large reads can also hurt interactive performance by blocking for too long.
|
271
|
+
if size > @maximum_read_size
|
272
|
+
size = @maximum_read_size
|
273
|
+
end
|
274
|
+
|
275
|
+
# This effectively ties the input and output stream together.
|
276
|
+
flush
|
277
|
+
|
278
|
+
if @read_buffer.empty?
|
279
|
+
if sysread(size, @read_buffer)
|
280
|
+
# Console.info(self, name: "read") {@read_buffer.inspect}
|
281
|
+
return true
|
282
|
+
end
|
283
|
+
else
|
284
|
+
if chunk = sysread(size, @input_buffer)
|
285
|
+
@read_buffer << chunk
|
286
|
+
# Console.info(self, name: "read") {@read_buffer.inspect}
|
287
|
+
|
288
|
+
return true
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# else for both cases above:
|
293
|
+
@done = true
|
294
|
+
return false
|
295
|
+
end
|
296
|
+
|
297
|
+
# Consumes at most `size` bytes from the buffer.
|
298
|
+
# @parameter size [Integer | Nil] The amount of data to consume. If nil, consume entire buffer.
|
299
|
+
def consume_read_buffer(size = nil)
|
300
|
+
# If we are at done, and the read buffer is empty, we can't consume anything.
|
301
|
+
return nil if @done && @read_buffer.empty?
|
302
|
+
|
303
|
+
result = nil
|
304
|
+
|
305
|
+
if size.nil? or size >= @read_buffer.bytesize
|
306
|
+
# Consume the entire read buffer:
|
307
|
+
result = @read_buffer
|
308
|
+
@read_buffer = StringBuffer.new
|
309
|
+
else
|
310
|
+
# This approach uses more memory.
|
311
|
+
# result = @read_buffer.slice!(0, size)
|
312
|
+
|
313
|
+
# We know that we are not going to reuse the original buffer.
|
314
|
+
# But byteslice will generate a hidden copy. So let's freeze it first:
|
315
|
+
@read_buffer.freeze
|
316
|
+
|
317
|
+
result = @read_buffer.byteslice(0, size)
|
318
|
+
@read_buffer = @read_buffer.byteslice(size, @read_buffer.bytesize)
|
319
|
+
end
|
320
|
+
|
321
|
+
return result
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
@@ -5,10 +5,14 @@
|
|
5
5
|
|
6
6
|
unless IO.method_defined?(:buffered?, false)
|
7
7
|
class IO
|
8
|
+
# Check if the IO is buffered.
|
9
|
+
# @returns [Boolean] True if the IO is buffered (not synchronized).
|
8
10
|
def buffered?
|
9
11
|
return !self.sync
|
10
12
|
end
|
11
13
|
|
14
|
+
# Set the buffered state of the IO.
|
15
|
+
# @parameter value [Boolean] True to enable buffering, false to disable.
|
12
16
|
def buffered=(value)
|
13
17
|
self.sync = !value
|
14
18
|
end
|
@@ -18,13 +22,18 @@ end
|
|
18
22
|
require "socket"
|
19
23
|
|
20
24
|
unless BasicSocket.method_defined?(:buffered?, false)
|
25
|
+
# Socket extensions for buffering support.
|
21
26
|
class BasicSocket
|
27
|
+
# Check if this socket uses TCP protocol.
|
28
|
+
# @returns [Boolean] True if the socket is TCP over IPv4 or IPv6.
|
22
29
|
def ip_protocol_tcp?
|
23
30
|
local_address = self.local_address
|
24
31
|
|
25
32
|
return (local_address.afamily == ::Socket::AF_INET || local_address.afamily == ::Socket::AF_INET6) && local_address.socktype == ::Socket::SOCK_STREAM
|
26
33
|
end
|
27
34
|
|
35
|
+
# Check if the socket is buffered.
|
36
|
+
# @returns [Boolean] True if the socket is buffered.
|
28
37
|
def buffered?
|
29
38
|
return false unless super
|
30
39
|
|
@@ -35,6 +44,8 @@ unless BasicSocket.method_defined?(:buffered?, false)
|
|
35
44
|
end
|
36
45
|
end
|
37
46
|
|
47
|
+
# Set the buffered state of the socket.
|
48
|
+
# @parameter value [Boolean] True to enable buffering, false to disable.
|
38
49
|
def buffered=(value)
|
39
50
|
super
|
40
51
|
|
@@ -53,11 +64,16 @@ end
|
|
53
64
|
require "stringio"
|
54
65
|
|
55
66
|
unless StringIO.method_defined?(:buffered?, false)
|
67
|
+
# StringIO extensions for buffering support.
|
56
68
|
class StringIO
|
69
|
+
# Check if the StringIO is buffered.
|
70
|
+
# @returns [Boolean] True if the StringIO is buffered (not synchronized).
|
57
71
|
def buffered?
|
58
72
|
return !self.sync
|
59
73
|
end
|
60
74
|
|
75
|
+
# Set the buffered state of the StringIO.
|
76
|
+
# @parameter value [Boolean] True to enable buffering, false to disable.
|
61
77
|
def buffered=(value)
|
62
78
|
self.sync = !value
|
63
79
|
end
|
@@ -5,6 +5,8 @@
|
|
5
5
|
|
6
6
|
class IO
|
7
7
|
unless method_defined?(:readable?, false)
|
8
|
+
# Check if the IO is readable.
|
9
|
+
# @returns [Boolean] True if the IO is readable (not closed).
|
8
10
|
def readable?
|
9
11
|
# Do not call `eof?` here as it is not concurrency-safe and it can block.
|
10
12
|
!closed?
|
@@ -16,6 +18,8 @@ require "socket"
|
|
16
18
|
|
17
19
|
class BasicSocket
|
18
20
|
unless method_defined?(:readable?, false)
|
21
|
+
# Check if the socket is readable.
|
22
|
+
# @returns [Boolean] True if the socket is readable.
|
19
23
|
def readable?
|
20
24
|
# If we can wait for the socket to become readable, we know that the socket may still be open.
|
21
25
|
result = self.recv_nonblock(1, ::Socket::MSG_PEEK, exception: false)
|
@@ -36,6 +40,8 @@ require "stringio"
|
|
36
40
|
|
37
41
|
class StringIO
|
38
42
|
unless method_defined?(:readable?, false)
|
43
|
+
# Check if the StringIO is readable.
|
44
|
+
# @returns [Boolean] True if the StringIO is readable (not at EOF).
|
39
45
|
def readable?
|
40
46
|
!eof?
|
41
47
|
end
|
@@ -46,6 +52,8 @@ require "openssl"
|
|
46
52
|
|
47
53
|
class OpenSSL::SSL::SSLSocket
|
48
54
|
unless method_defined?(:readable?, false)
|
55
|
+
# Check if the SSL socket is readable.
|
56
|
+
# @returns [Boolean] True if the SSL socket is readable.
|
49
57
|
def readable?
|
50
58
|
to_io.readable?
|
51
59
|
end
|
@@ -4,15 +4,20 @@
|
|
4
4
|
# Copyright, 2023-2024, by Samuel Williams.
|
5
5
|
|
6
6
|
module IO::Stream
|
7
|
+
# A specialized string buffer for binary data with automatic encoding handling.
|
7
8
|
class StringBuffer < String
|
8
9
|
BINARY = Encoding::BINARY
|
9
10
|
|
11
|
+
# Initialize a new string buffer with binary encoding.
|
10
12
|
def initialize
|
11
13
|
super
|
12
14
|
|
13
15
|
force_encoding(BINARY)
|
14
16
|
end
|
15
17
|
|
18
|
+
# Append a string to the buffer, converting to binary encoding if necessary.
|
19
|
+
# @parameter string [String] The string to append.
|
20
|
+
# @returns [StringBuffer] Self for method chaining.
|
16
21
|
def << string
|
17
22
|
if string.encoding == BINARY
|
18
23
|
super(string)
|
data/lib/io/stream/version.rb
CHANGED
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2023-2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative "readable"
|
7
|
+
|
8
|
+
module IO::Stream
|
9
|
+
# The minimum write size before flushing. Defaults to 64KB.
|
10
|
+
MINIMUM_WRITE_SIZE = ENV.fetch("IO_STREAM_MINIMUM_WRITE_SIZE", BLOCK_SIZE).to_i
|
11
|
+
|
12
|
+
# A module providing writable stream functionality.
|
13
|
+
#
|
14
|
+
# You must implement the `syswrite` method to write data to the underlying IO.
|
15
|
+
module Writable
|
16
|
+
# Initialize writable stream functionality.
|
17
|
+
# @parameter minimum_write_size [Integer] The minimum buffer size before flushing.
|
18
|
+
def initialize(minimum_write_size: MINIMUM_WRITE_SIZE, **, &block)
|
19
|
+
@writing = ::Thread::Mutex.new
|
20
|
+
@write_buffer = StringBuffer.new
|
21
|
+
@minimum_write_size = minimum_write_size
|
22
|
+
|
23
|
+
super(**, &block) if defined?(super)
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_accessor :minimum_write_size
|
27
|
+
|
28
|
+
# Flushes buffered data to the stream.
|
29
|
+
def flush
|
30
|
+
return if @write_buffer.empty?
|
31
|
+
|
32
|
+
@writing.synchronize do
|
33
|
+
self.drain(@write_buffer)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Writes `string` to the buffer. When the buffer is full or #sync is true the
|
38
|
+
# buffer is flushed to the underlying `io`.
|
39
|
+
# @parameter string [String] the string to write to the buffer.
|
40
|
+
# @returns [Integer] the number of bytes appended to the buffer.
|
41
|
+
def write(string, flush: false)
|
42
|
+
@writing.synchronize do
|
43
|
+
@write_buffer << string
|
44
|
+
|
45
|
+
flush |= (@write_buffer.bytesize >= @minimum_write_size)
|
46
|
+
|
47
|
+
if flush
|
48
|
+
self.drain(@write_buffer)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
return string.bytesize
|
53
|
+
end
|
54
|
+
|
55
|
+
# Appends `string` to the buffer and returns self for method chaining.
|
56
|
+
# @parameter string [String] the string to write to the stream.
|
57
|
+
def <<(string)
|
58
|
+
write(string)
|
59
|
+
|
60
|
+
return self
|
61
|
+
end
|
62
|
+
|
63
|
+
# Write arguments to the stream followed by a separator and flush immediately.
|
64
|
+
# @parameter arguments [Array] The arguments to write to the stream.
|
65
|
+
# @parameter separator [String] The separator to append after each argument.
|
66
|
+
def puts(*arguments, separator: $/)
|
67
|
+
return if arguments.empty?
|
68
|
+
|
69
|
+
@writing.synchronize do
|
70
|
+
arguments.each do |argument|
|
71
|
+
@write_buffer << argument << separator
|
72
|
+
end
|
73
|
+
|
74
|
+
self.drain(@write_buffer)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Close the write end of the stream by flushing any remaining data.
|
79
|
+
def close_write
|
80
|
+
flush
|
81
|
+
end
|
82
|
+
|
83
|
+
private def drain(buffer)
|
84
|
+
begin
|
85
|
+
syswrite(buffer)
|
86
|
+
ensure
|
87
|
+
# If the write operation fails, we still need to clear this buffer, and the data is essentially lost.
|
88
|
+
buffer.clear
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/io/stream.rb
CHANGED
@@ -6,10 +6,15 @@
|
|
6
6
|
require_relative "stream/version"
|
7
7
|
require_relative "stream/buffered"
|
8
8
|
|
9
|
+
# @namespace
|
9
10
|
class IO
|
11
|
+
# @namespace
|
10
12
|
module Stream
|
11
13
|
end
|
12
14
|
|
15
|
+
# Convert any IO-like object into a buffered stream.
|
16
|
+
# @parameter io [IO] The IO object to wrap.
|
17
|
+
# @returns [IO::Stream::Buffered] A buffered stream wrapper.
|
13
18
|
def self.Stream(io)
|
14
19
|
if io.is_a?(Stream::Buffered)
|
15
20
|
io
|
data/license.md
CHANGED
data/readme.md
CHANGED
@@ -8,6 +8,64 @@ Provide a buffered stream implementation for Ruby, independent of the underlying
|
|
8
8
|
|
9
9
|
Please see the [project documentation](https://socketry.github.io/io-stream) for more details.
|
10
10
|
|
11
|
+
## Releases
|
12
|
+
|
13
|
+
Please see the [project releases](https://socketry.github.io/io-streamreleases/index) for all releases.
|
14
|
+
|
15
|
+
### v0.7.0
|
16
|
+
|
17
|
+
- Split stream functionality into separate `Readable` and `Writable` modules for better modularity and composition.
|
18
|
+
- Remove unused timeout shim functionality.
|
19
|
+
- 100% documentation coverage.
|
20
|
+
|
21
|
+
### v0.6.1
|
22
|
+
|
23
|
+
- Fix compatibility with Ruby v3.3.0 - v3.3.6 where broken `@io.close` could hang.
|
24
|
+
|
25
|
+
### v0.6.0
|
26
|
+
|
27
|
+
- Improve compatibility of `gets` implementation to better match Ruby's IO\#gets behavior.
|
28
|
+
|
29
|
+
### v0.5.0
|
30
|
+
|
31
|
+
- Add support for `read_until(limit:)` parameter to limit the amount of data read.
|
32
|
+
- Minor documentation improvements.
|
33
|
+
|
34
|
+
### v0.4.3
|
35
|
+
|
36
|
+
- Add comprehensive tests for `buffered?` method on `SSLSocket`.
|
37
|
+
- Ensure TLS connections have correct buffering behavior.
|
38
|
+
- Improve test suite organization and readability.
|
39
|
+
|
40
|
+
### v0.4.2
|
41
|
+
|
42
|
+
- Add external test suite for better integration testing.
|
43
|
+
- Update dependencies and improve code style with RuboCop.
|
44
|
+
|
45
|
+
### v0.4.1
|
46
|
+
|
47
|
+
- Add compatibility fix for `SSLSocket` raising `EBADF` errors.
|
48
|
+
- Fix `IO#close` hang issue in certain scenarios.
|
49
|
+
- Add `#to_io` method to `IO::Stream::Buffered` for better compatibility.
|
50
|
+
- Modernize gem structure and dependencies.
|
51
|
+
|
52
|
+
### v0.4.0
|
53
|
+
|
54
|
+
- Add convenient `IO.Stream()` constructor method for creating buffered streams.
|
55
|
+
|
56
|
+
### v0.3.0
|
57
|
+
|
58
|
+
- Add support for timeouts with compatibility shims for various IO types.
|
59
|
+
|
60
|
+
### v0.2.0
|
61
|
+
|
62
|
+
- Prefer `write_nonblock` in `syswrite` implementation for better non-blocking behavior.
|
63
|
+
- Add test cases for crash scenarios.
|
64
|
+
|
65
|
+
## See Also
|
66
|
+
|
67
|
+
- [async-io](https://github.com/socketry/async-io) — Where this implementation originally came from.
|
68
|
+
|
11
69
|
## Contributing
|
12
70
|
|
13
71
|
We welcome contributions to this project.
|
@@ -25,7 +83,3 @@ In order to protect users of this project, we require all contributors to comply
|
|
25
83
|
### Community Guidelines
|
26
84
|
|
27
85
|
This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
|
28
|
-
|
29
|
-
## See Also
|
30
|
-
|
31
|
-
- [async-io](https://github.com/socketry/async-io) — Where this implementation originally came from.
|
data/releases.md
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# Releases
|
2
|
+
|
3
|
+
## v0.7.0
|
4
|
+
|
5
|
+
- Split stream functionality into separate `Readable` and `Writable` modules for better modularity and composition.
|
6
|
+
- Remove unused timeout shim functionality.
|
7
|
+
- 100% documentation coverage.
|
8
|
+
|
9
|
+
## v0.6.1
|
10
|
+
|
11
|
+
- Fix compatibility with Ruby v3.3.0 - v3.3.6 where broken `@io.close` could hang.
|
12
|
+
|
13
|
+
## v0.6.0
|
14
|
+
|
15
|
+
- Improve compatibility of `gets` implementation to better match Ruby's IO\#gets behavior.
|
16
|
+
|
17
|
+
## v0.5.0
|
18
|
+
|
19
|
+
- Add support for `read_until(limit:)` parameter to limit the amount of data read.
|
20
|
+
- Minor documentation improvements.
|
21
|
+
|
22
|
+
## v0.4.3
|
23
|
+
|
24
|
+
- Add comprehensive tests for `buffered?` method on `SSLSocket`.
|
25
|
+
- Ensure TLS connections have correct buffering behavior.
|
26
|
+
- Improve test suite organization and readability.
|
27
|
+
|
28
|
+
## v0.4.2
|
29
|
+
|
30
|
+
- Add external test suite for better integration testing.
|
31
|
+
- Update dependencies and improve code style with RuboCop.
|
32
|
+
|
33
|
+
## v0.4.1
|
34
|
+
|
35
|
+
- Add compatibility fix for `SSLSocket` raising `EBADF` errors.
|
36
|
+
- Fix `IO#close` hang issue in certain scenarios.
|
37
|
+
- Add `#to_io` method to `IO::Stream::Buffered` for better compatibility.
|
38
|
+
- Modernize gem structure and dependencies.
|
39
|
+
|
40
|
+
## v0.4.0
|
41
|
+
|
42
|
+
- Add convenient `IO.Stream()` constructor method for creating buffered streams.
|
43
|
+
|
44
|
+
## v0.3.0
|
45
|
+
|
46
|
+
- Add support for timeouts with compatibility shims for various IO types.
|
47
|
+
|
48
|
+
## v0.2.0
|
49
|
+
|
50
|
+
- Prefer `write_nonblock` in `syswrite` implementation for better non-blocking behavior.
|
51
|
+
- Add test cases for crash scenarios.
|
52
|
+
|
53
|
+
## v0.1.1
|
54
|
+
|
55
|
+
- Improve buffering compatibility by falling back to `sync=` when `buffered=` is not available.
|
56
|
+
|
57
|
+
## v0.1.0
|
58
|
+
|
59
|
+
- Rename `IO::Stream::BufferedStream` to `IO::Stream::Buffered` for consistency.
|
60
|
+
- Add comprehensive tests and improved OpenSSL support with compatibility shims.
|
61
|
+
- Improve compatibility with Darwin/macOS systems.
|
62
|
+
- Fix monkey patches for various IO types.
|
63
|
+
- Add support for `StringIO#buffered?` method.
|
64
|
+
|
65
|
+
## v0.0.1
|
66
|
+
|
67
|
+
- Initial release with basic buffered stream functionality.
|
68
|
+
- Provide `IO::Stream::Buffered` class for efficient buffered I/O operations.
|
69
|
+
- Add `readable?` method to check stream readability status.
|
70
|
+
- Include basic test suite.
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: io-stream
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain:
|
11
10
|
- |
|
@@ -37,10 +36,8 @@ cert_chain:
|
|
37
36
|
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
38
37
|
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
39
38
|
-----END CERTIFICATE-----
|
40
|
-
date:
|
39
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
41
40
|
dependencies: []
|
42
|
-
description:
|
43
|
-
email:
|
44
41
|
executables: []
|
45
42
|
extensions: []
|
46
43
|
extra_rdoc_files: []
|
@@ -49,21 +46,21 @@ files:
|
|
49
46
|
- lib/io/stream/buffered.rb
|
50
47
|
- lib/io/stream/generic.rb
|
51
48
|
- lib/io/stream/openssl.rb
|
49
|
+
- lib/io/stream/readable.rb
|
52
50
|
- lib/io/stream/shim/buffered.rb
|
53
51
|
- lib/io/stream/shim/readable.rb
|
54
|
-
- lib/io/stream/shim/shim.md
|
55
|
-
- lib/io/stream/shim/timeout.rb
|
56
52
|
- lib/io/stream/string_buffer.rb
|
57
53
|
- lib/io/stream/version.rb
|
54
|
+
- lib/io/stream/writable.rb
|
58
55
|
- license.md
|
59
56
|
- readme.md
|
57
|
+
- releases.md
|
60
58
|
homepage: https://github.com/socketry/io-stream
|
61
59
|
licenses:
|
62
60
|
- MIT
|
63
61
|
metadata:
|
64
62
|
documentation_uri: https://socketry.github.io/io-stream
|
65
63
|
source_code_uri: https://github.com/socketry/io-stream.git
|
66
|
-
post_install_message:
|
67
64
|
rdoc_options: []
|
68
65
|
require_paths:
|
69
66
|
- lib
|
@@ -71,15 +68,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
71
68
|
requirements:
|
72
69
|
- - ">="
|
73
70
|
- !ruby/object:Gem::Version
|
74
|
-
version: '3.
|
71
|
+
version: '3.2'
|
75
72
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
73
|
requirements:
|
77
74
|
- - ">="
|
78
75
|
- !ruby/object:Gem::Version
|
79
76
|
version: '0'
|
80
77
|
requirements: []
|
81
|
-
rubygems_version: 3.
|
82
|
-
signing_key:
|
78
|
+
rubygems_version: 3.6.7
|
83
79
|
specification_version: 4
|
84
80
|
summary: Provides a generic stream wrapper for IO instances.
|
85
81
|
test_files: []
|
metadata.gz.sig
CHANGED
@@ -1,5 +1,4 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
�6��}[Wsc��<������'�vP��O������:N)�Sn�!��2��:�gp�K.@7�\�y8yᵚO��\��'��8�^N��/�V�@��k���d0pݵ]��#�Vs%���I(9kX��eO76�
|
1
|
+
9�o����\q�J�IJ�5�R���
|
2
|
+
Bi<�\�'[ޤk�âtpx2
|
3
|
+
5�*��g����j�K��>J��;� ���Q�e���C�*�z�+>�%ov�M�*�p[�!���̹���R���V�*�:��bS�v-�h/��y�a���7<����[,�w��,�kB��
|
4
|
+
)�iq�� �|G�+�iB~͙W���V>Ft(�Y�<|�;�߳�����m`�{@;�}�%Ax+��ad�KO����b��}������L�[d��`��m��tunn�x2#�?%�,&d=�JS�t�^�E���̡,���y�����j�
|
data/lib/io/stream/shim/shim.md
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Released under the MIT License.
|
4
|
-
# Copyright, 2024, by Samuel Williams.
|
5
|
-
|
6
|
-
class IO
|
7
|
-
unless const_defined?(:TimeoutError)
|
8
|
-
# Compatibility shim.
|
9
|
-
class TimeoutError < IOError
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
unless method_defined?(:timeout)
|
14
|
-
# Compatibility shim.
|
15
|
-
attr_accessor :timeout
|
16
|
-
end
|
17
|
-
end
|