io_unblock 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +18 -0
- data/.gitignore +7 -0
- data/Gemfile +15 -0
- data/README.md +10 -0
- data/Rakefile +16 -0
- data/io_unblock.gemspec +23 -0
- data/lib/io_unblock.rb +10 -0
- data/lib/io_unblock/buffer.rb +50 -0
- data/lib/io_unblock/delegation.rb +91 -0
- data/lib/io_unblock/stream.rb +158 -0
- data/lib/io_unblock/version.rb +3 -0
- data/spec/io_unblock/buffer_spec.rb +46 -0
- data/spec/io_unblock/delegation_spec.rb +88 -0
- data/spec/io_unblock/stream_spec.rb +199 -0
- data/spec/spec_helper.rb +81 -0
- metadata +87 -0
data/.autotest
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# vim: set filetype=ruby :
|
2
|
+
|
3
|
+
require 'autotest/restart'
|
4
|
+
require 'autotest/growl'
|
5
|
+
|
6
|
+
Autotest.add_hook :initialize do |at|
|
7
|
+
at.testlib = 'minitest/spec'
|
8
|
+
|
9
|
+
at.clear_mappings
|
10
|
+
at.add_mapping(/^lib\/(.*)\.rb$/) { |f, m| "spec/#{m[1]}_spec.rb" }
|
11
|
+
at.add_mapping(/^spec\/.*_spec\.rb$/) { |f,_| f }
|
12
|
+
at.add_mapping(/^spec\/spec_helper\.rb$/) { |f,_|
|
13
|
+
at.files_matching(/^spec\/.*_spec\.rb$/)
|
14
|
+
}
|
15
|
+
|
16
|
+
at.add_exception '.git'
|
17
|
+
at.add_exception 'coverage'
|
18
|
+
end
|
data/.gitignore
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in io_unblock.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
group :test do
|
7
|
+
gem 'autotest'
|
8
|
+
gem 'minitest'
|
9
|
+
gem 'minitest-emoji'
|
10
|
+
end
|
11
|
+
|
12
|
+
group :human_testing do
|
13
|
+
gem 'autotest-growl'
|
14
|
+
gem 'simplecov'
|
15
|
+
end
|
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
Rake::TestTask.new do |t|
|
5
|
+
t.libs.push "lib"
|
6
|
+
t.test_files = FileList['spec/**/*_spec.rb']
|
7
|
+
t.verbose = true
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Generate a coverage report from tests with simplecov"
|
11
|
+
task :coverage do
|
12
|
+
ENV['SCOV'] = '1'
|
13
|
+
Rake::Task['test'].invoke
|
14
|
+
end
|
15
|
+
|
16
|
+
task :default => [:test]
|
data/io_unblock.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "io_unblock/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "io_unblock"
|
7
|
+
s.version = IoUnblock::VERSION
|
8
|
+
s.authors = ["Ian D. Eccles"]
|
9
|
+
s.email = ["ian.eccles@gmail.com"]
|
10
|
+
s.homepage = "https://github.com/iande/io_unblock"
|
11
|
+
s.summary = %q{Non-blocking IO reads/writes wrapped in a thread}
|
12
|
+
s.description = %q{Non-blocking IO reads/writes wrapped in a thread}
|
13
|
+
|
14
|
+
s.rubyforge_project = "io_unblock"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_development_dependency 'minitest'
|
22
|
+
s.add_development_dependency 'rake'
|
23
|
+
end
|
data/lib/io_unblock.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
module IoUnblock
|
2
|
+
# A very simple synchronized buffer.
|
3
|
+
#
|
4
|
+
# @api private
|
5
|
+
class Buffer
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@buffer = []
|
9
|
+
@mutex = Mutex.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def push bytes, cb
|
13
|
+
synched { @buffer.push [bytes, cb] }
|
14
|
+
end
|
15
|
+
|
16
|
+
def pop
|
17
|
+
synched { @buffer.pop }
|
18
|
+
end
|
19
|
+
|
20
|
+
def unshift bytes, cb
|
21
|
+
synched { @buffer.unshift [bytes, cb] }
|
22
|
+
end
|
23
|
+
|
24
|
+
def shift
|
25
|
+
synched { @buffer.shift }
|
26
|
+
end
|
27
|
+
|
28
|
+
def first
|
29
|
+
synched { @buffer.first }
|
30
|
+
end
|
31
|
+
|
32
|
+
def last
|
33
|
+
synched { @buffer.last }
|
34
|
+
end
|
35
|
+
|
36
|
+
def empty?
|
37
|
+
# Should we lock?
|
38
|
+
@buffer.empty?
|
39
|
+
end
|
40
|
+
|
41
|
+
def buffered?
|
42
|
+
!empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def synched &block
|
47
|
+
@mutex.synchronize(&block)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module IoUnblock
|
2
|
+
# Handles delegating read and write methods to their non-blocking
|
3
|
+
# counterparts on the IO object. If the IO object does not support
|
4
|
+
# non-blocking methods, falls back to blocking ones.
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
module Delegation
|
8
|
+
module NonBlockingWrites
|
9
|
+
def io_write bytes; io.write_nonblock bytes; end
|
10
|
+
private :io_write
|
11
|
+
end
|
12
|
+
|
13
|
+
module BlockingWrites
|
14
|
+
def io_write bytes; io.write bytes; end
|
15
|
+
private :io_write
|
16
|
+
end
|
17
|
+
|
18
|
+
module ForwardWriteable
|
19
|
+
def writeable?; io.writeable?; end
|
20
|
+
private :writeable?
|
21
|
+
end
|
22
|
+
|
23
|
+
module SelectWriteable
|
24
|
+
def writeable?; !!IO.select(nil, io_selector, nil, select_delay); end
|
25
|
+
private :writeable?
|
26
|
+
end
|
27
|
+
|
28
|
+
module NonBlockingReads
|
29
|
+
def io_read len; io.read_nonblock len; end
|
30
|
+
private :io_read
|
31
|
+
end
|
32
|
+
|
33
|
+
module PartialReads
|
34
|
+
def io_read len; io.readpartial len; end
|
35
|
+
private :io_read
|
36
|
+
end
|
37
|
+
|
38
|
+
module BlockingReads
|
39
|
+
def io_read len; io.read len; end
|
40
|
+
private :io_read
|
41
|
+
end
|
42
|
+
|
43
|
+
module ForwardReadable
|
44
|
+
def readable?; io.readable?; end
|
45
|
+
private :readable?
|
46
|
+
end
|
47
|
+
|
48
|
+
module SelectReadable
|
49
|
+
def readable?; !!IO.select(io_selector, nil, nil, select_delay); end
|
50
|
+
private :readable?
|
51
|
+
end
|
52
|
+
|
53
|
+
class << self
|
54
|
+
def define_io_methods stream
|
55
|
+
define_io_write stream
|
56
|
+
define_io_read stream
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def define_io_write stream
|
61
|
+
if stream.io.respond_to? :write_nonblock
|
62
|
+
stream.extend NonBlockingWrites
|
63
|
+
else
|
64
|
+
stream.extend BlockingWrites
|
65
|
+
end
|
66
|
+
|
67
|
+
if stream.io.respond_to? :writeable?
|
68
|
+
stream.extend ForwardWriteable
|
69
|
+
else
|
70
|
+
stream.extend SelectWriteable
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def define_io_read stream
|
75
|
+
if stream.io.respond_to? :read_nonblock
|
76
|
+
stream.extend NonBlockingReads
|
77
|
+
elsif stream.io.respond_to? :readpartial
|
78
|
+
stream.extend PartialReads
|
79
|
+
else
|
80
|
+
stream.extend BlockingReads
|
81
|
+
end
|
82
|
+
|
83
|
+
if stream.io.respond_to? :readable?
|
84
|
+
stream.extend ForwardReadable
|
85
|
+
else
|
86
|
+
stream.extend SelectReadable
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
module IoUnblock
|
2
|
+
class StreamError < IoUnblockError; end
|
3
|
+
|
4
|
+
class Stream
|
5
|
+
MAX_BYTES_PER_WRITE = 1024 * 8
|
6
|
+
MAX_BYTES_PER_READ = 1024 * 4
|
7
|
+
|
8
|
+
attr_reader :running, :connected, :io, :io_selector, :callbacks
|
9
|
+
attr_accessor :select_delay
|
10
|
+
alias :running? :running
|
11
|
+
alias :connected? :connected
|
12
|
+
|
13
|
+
# The given IO object, `io`, is assumed to be opened/connected.
|
14
|
+
# Global callbacks:
|
15
|
+
# - failed: called when IO access throws an exception that cannot
|
16
|
+
# be recovered from (opening the IO fails,
|
17
|
+
# TCP connection reset, unexpected EOF, etc.)
|
18
|
+
# - read: called when any data is read from the underlying IO
|
19
|
+
# object
|
20
|
+
# - wrote: called when any data is written to the underlying IO
|
21
|
+
# object
|
22
|
+
# - closed: called when the underlying IO object is closed (even
|
23
|
+
# if it is closed as a result of a failure)
|
24
|
+
# - started: called when the IO processing has started
|
25
|
+
# - stopped: called when the IO processing has stopped
|
26
|
+
# - looped: called when each time the IO processing loops
|
27
|
+
def initialize io, callbacks=nil
|
28
|
+
@io = io
|
29
|
+
@io_selector = [@io]
|
30
|
+
@processor = nil
|
31
|
+
@s_mutex = Mutex.new
|
32
|
+
@w_buff = IoUnblock::Buffer.new
|
33
|
+
@running = false
|
34
|
+
@connected = true
|
35
|
+
@callbacks = callbacks || {}
|
36
|
+
@select_delay = 0.1
|
37
|
+
Delegation.define_io_methods self
|
38
|
+
yield self if block_given?
|
39
|
+
end
|
40
|
+
|
41
|
+
def start &cb
|
42
|
+
@s_mutex.synchronize do
|
43
|
+
raise StreamError, "already started" if @running
|
44
|
+
@running = true
|
45
|
+
@processor = Thread.new do
|
46
|
+
trigger_callbacks :started, :start, &cb
|
47
|
+
read_and_write while running? && connected?
|
48
|
+
flush_and_close
|
49
|
+
trigger_callbacks :stopped, :stop, &cb
|
50
|
+
end
|
51
|
+
end
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
def stop
|
56
|
+
@s_mutex.synchronize do
|
57
|
+
if @running
|
58
|
+
@running = false
|
59
|
+
@processor.join
|
60
|
+
end
|
61
|
+
end
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
# The callback triggered here will be invoked only when all bytes
|
66
|
+
# have been written.
|
67
|
+
def write bytes, &cb
|
68
|
+
@w_buff.push bytes, cb
|
69
|
+
self
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
def trigger_callbacks named, *args, &other
|
74
|
+
other && other.call(*args)
|
75
|
+
@callbacks.key?(named) && @callbacks[named].call(*args)
|
76
|
+
end
|
77
|
+
|
78
|
+
def flush_and_close
|
79
|
+
_write while connected? && @w_buff.buffered?
|
80
|
+
io_close
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
def read_and_write
|
85
|
+
_read if read?
|
86
|
+
_write if write?
|
87
|
+
trigger_callbacks :looped, self
|
88
|
+
self
|
89
|
+
end
|
90
|
+
|
91
|
+
def _read
|
92
|
+
begin
|
93
|
+
bytes = io_read MAX_BYTES_PER_READ
|
94
|
+
trigger_callbacks(:read, bytes) if bytes
|
95
|
+
rescue Errno::EINTR, Errno::EAGAIN, Errno::EWOULDBLOCK
|
96
|
+
rescue Exception
|
97
|
+
force_close $!
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def _write
|
102
|
+
written = 0
|
103
|
+
while written < MAX_BYTES_PER_WRITE
|
104
|
+
bytes, cb = @w_buff.shift
|
105
|
+
break unless bytes
|
106
|
+
begin
|
107
|
+
w = io_write bytes
|
108
|
+
rescue Errno::EINTR, Errno::EAGAIN, Errno::EWOULDBLOCK
|
109
|
+
# writing will either block, or cannot otherwise be completed,
|
110
|
+
# put data back and try again some other day
|
111
|
+
@w_buff.unshift bytes, cb
|
112
|
+
break
|
113
|
+
rescue Exception
|
114
|
+
force_close $!
|
115
|
+
break
|
116
|
+
end
|
117
|
+
written += w
|
118
|
+
if w < bytes.size
|
119
|
+
@w_buff.unshift bytes[w..-1], cb
|
120
|
+
trigger_callbacks :wrote, bytes, w
|
121
|
+
else
|
122
|
+
trigger_callbacks :wrote, bytes, w, &cb
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def io_close
|
128
|
+
if connected?
|
129
|
+
@io.close rescue nil
|
130
|
+
@connected = false
|
131
|
+
trigger_callbacks :closed
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def force_close ex
|
136
|
+
io_close
|
137
|
+
trigger_callbacks :failed, ex
|
138
|
+
end
|
139
|
+
|
140
|
+
def write?
|
141
|
+
begin
|
142
|
+
connected? && @w_buff.buffered? && writeable?
|
143
|
+
rescue Exception
|
144
|
+
force_close $!
|
145
|
+
false
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def read?
|
150
|
+
begin
|
151
|
+
connected? && readable?
|
152
|
+
rescue Exception
|
153
|
+
force_close $!
|
154
|
+
false
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require File.expand_path("../../spec_helper.rb", __FILE__)
|
2
|
+
|
3
|
+
describe IoUnblock::Buffer do
|
4
|
+
before do
|
5
|
+
@buffer = IoUnblock::Buffer.new
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should be empty when nothing is buffered" do
|
9
|
+
@buffer.push :a, 1
|
10
|
+
@buffer.pop
|
11
|
+
@buffer.empty?.must_equal true
|
12
|
+
@buffer.buffered?.must_equal false
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should not be empty when stuff is buffered" do
|
16
|
+
@buffer.push :a, 1
|
17
|
+
@buffer.empty?.must_equal false
|
18
|
+
@buffer.buffered?.must_equal true
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should push to the end of the buffer" do
|
22
|
+
@buffer.push :a, 1
|
23
|
+
@buffer.push :b, 2
|
24
|
+
@buffer.first.must_equal [:a, 1]
|
25
|
+
@buffer.last.must_equal [:b, 2]
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should unshift to the beginning of the buffer" do
|
29
|
+
@buffer.unshift :a, 1
|
30
|
+
@buffer.unshift :b, 2
|
31
|
+
@buffer.last.must_equal [:a, 1]
|
32
|
+
@buffer.first.must_equal [:b, 2]
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should shift from the beginning" do
|
36
|
+
@buffer.push :a, 1
|
37
|
+
@buffer.push :b, 2
|
38
|
+
@buffer.shift.must_equal [:a, 1]
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should pop from the end" do
|
42
|
+
@buffer.push :a, 1
|
43
|
+
@buffer.push :b, 2
|
44
|
+
@buffer.pop.must_equal [:b, 2]
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require File.expand_path("../../spec_helper.rb", __FILE__)
|
2
|
+
|
3
|
+
describe IoUnblock::Delegation do
|
4
|
+
class FakeStream
|
5
|
+
attr_reader :io
|
6
|
+
def initialize io
|
7
|
+
@io = io
|
8
|
+
end
|
9
|
+
def io_selector; [@io]; end
|
10
|
+
def select_delay; 18; end
|
11
|
+
end
|
12
|
+
|
13
|
+
before do
|
14
|
+
@mock_io = MiniTest::Mock.new
|
15
|
+
@stream = FakeStream.new @mock_io
|
16
|
+
end
|
17
|
+
|
18
|
+
it "delegates to read_nonblock when available" do
|
19
|
+
@mock_io.expect(:read_nonblock, :reading, [42])
|
20
|
+
IoUnblock::Delegation.define_io_methods @stream
|
21
|
+
@stream.__send__ :io_read, 42
|
22
|
+
@mock_io.verify
|
23
|
+
end
|
24
|
+
|
25
|
+
it "delegates to readpartial when available" do
|
26
|
+
@mock_io.expect(:readpartial, :reading, [42])
|
27
|
+
IoUnblock::Delegation.define_io_methods @stream
|
28
|
+
@stream.__send__ :io_read, 42
|
29
|
+
@mock_io.verify
|
30
|
+
end
|
31
|
+
|
32
|
+
it "delegates to read if all else fails" do
|
33
|
+
@mock_io.expect(:read, :reading, [42])
|
34
|
+
IoUnblock::Delegation.define_io_methods @stream
|
35
|
+
@stream.__send__ :io_read, 42
|
36
|
+
@mock_io.verify
|
37
|
+
end
|
38
|
+
|
39
|
+
it "delegates to write_nonblock when available" do
|
40
|
+
@mock_io.expect(:write_nonblock, :writing, ['hello'])
|
41
|
+
IoUnblock::Delegation.define_io_methods @stream
|
42
|
+
@stream.__send__ :io_write, 'hello'
|
43
|
+
@mock_io.verify
|
44
|
+
end
|
45
|
+
|
46
|
+
it "delegates to write if all else fails" do
|
47
|
+
@mock_io.expect(:write, :writing, ['hello'])
|
48
|
+
IoUnblock::Delegation.define_io_methods @stream
|
49
|
+
@stream.__send__ :io_write, 'hello'
|
50
|
+
@mock_io.verify
|
51
|
+
end
|
52
|
+
|
53
|
+
it "delegates to IO.select when there is no readable? method" do
|
54
|
+
mocked = MiniTest::Mock.new
|
55
|
+
mocked.expect(:select, nil, [[@mock_io], nil, nil, 18])
|
56
|
+
real_io = IO
|
57
|
+
::IO = mocked
|
58
|
+
IoUnblock::Delegation.define_io_methods @stream
|
59
|
+
@stream.__send__ :readable?
|
60
|
+
::IO = real_io
|
61
|
+
mocked.verify
|
62
|
+
end
|
63
|
+
|
64
|
+
it "delegates to IO.select when there is no writeable? method" do
|
65
|
+
mocked = MiniTest::Mock.new
|
66
|
+
mocked.expect(:select, nil, [nil, [@mock_io], nil, 18])
|
67
|
+
real_io = IO
|
68
|
+
::IO = mocked
|
69
|
+
IoUnblock::Delegation.define_io_methods @stream
|
70
|
+
@stream.__send__ :writeable?
|
71
|
+
::IO = real_io
|
72
|
+
mocked.verify
|
73
|
+
end
|
74
|
+
|
75
|
+
it "delegates to readable? when the method is available" do
|
76
|
+
@mock_io.expect(:readable?, false, [])
|
77
|
+
IoUnblock::Delegation.define_io_methods @stream
|
78
|
+
@stream.__send__ :readable?
|
79
|
+
@mock_io.verify
|
80
|
+
end
|
81
|
+
|
82
|
+
it "delegates to writeable? when the method is available" do
|
83
|
+
@mock_io.expect(:writeable?, false, [])
|
84
|
+
IoUnblock::Delegation.define_io_methods @stream
|
85
|
+
@stream.__send__ :writeable?
|
86
|
+
@mock_io.verify
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
require File.expand_path("../../spec_helper.rb", __FILE__)
|
2
|
+
require 'stringio'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
$log = Logger.new $stdout
|
6
|
+
|
7
|
+
describe IoUnblock::Stream do
|
8
|
+
def dummy_io; @dummy_io ||= DummyIO.new; end
|
9
|
+
def stream; @stream ||= IoUnblock::Stream.new dummy_io; end
|
10
|
+
|
11
|
+
it "raises an exception if started twice" do
|
12
|
+
stream.start
|
13
|
+
lambda {
|
14
|
+
stream.start
|
15
|
+
}.must_raise IoUnblock::StreamError
|
16
|
+
stream.stop
|
17
|
+
end
|
18
|
+
|
19
|
+
it "reads only if the IO is ready for it" do
|
20
|
+
dummy_io.r_stream.string = 'test string'
|
21
|
+
dummy_io.readable = false
|
22
|
+
stream.start
|
23
|
+
Thread.pass until stream.running?
|
24
|
+
dummy_io.r_stream.pos.must_equal 0
|
25
|
+
dummy_io.readable = true
|
26
|
+
Thread.pass until dummy_io.r_stream.eof?
|
27
|
+
stream.stop
|
28
|
+
dummy_io.r_stream.pos.must_equal 11
|
29
|
+
end
|
30
|
+
|
31
|
+
it "closes the io when stopping" do
|
32
|
+
dummy_io.closed?.must_equal false
|
33
|
+
stream.start
|
34
|
+
Thread.pass until stream.running?
|
35
|
+
stream.stop
|
36
|
+
dummy_io.closed?.must_equal true
|
37
|
+
end
|
38
|
+
|
39
|
+
it "flushes all writes before stopping (even if io claims to be unwriteable)" do
|
40
|
+
dummy_io.writeable = false
|
41
|
+
stream.start
|
42
|
+
stream.write 'hello '
|
43
|
+
stream.write 'world.'
|
44
|
+
Thread.pass until stream.running?
|
45
|
+
dummy_io.w_stream.string.must_equal ''
|
46
|
+
stream.stop
|
47
|
+
dummy_io.w_stream.string.must_equal 'hello world.'
|
48
|
+
end
|
49
|
+
|
50
|
+
it "does not die on EINTER" do
|
51
|
+
dummy_io.raise_write = Errno::EINTR.new
|
52
|
+
dummy_io.raise_read = Errno::EINTR.new
|
53
|
+
stream.start
|
54
|
+
stream.write 'hello'
|
55
|
+
Thread.pass until dummy_io.raised_read?
|
56
|
+
stream.connected?.must_equal true
|
57
|
+
dummy_io.raise_read = nil
|
58
|
+
Thread.pass until dummy_io.raised_write?
|
59
|
+
stream.connected?.must_equal true
|
60
|
+
dummy_io.raise_write = nil
|
61
|
+
stream.stop
|
62
|
+
dummy_io.w_stream.string.must_equal 'hello'
|
63
|
+
end
|
64
|
+
|
65
|
+
it "does not die on EAGAIN" do
|
66
|
+
dummy_io.raise_write = Errno::EAGAIN.new
|
67
|
+
dummy_io.raise_read = Errno::EINTR.new
|
68
|
+
stream.start
|
69
|
+
stream.write 'hello'
|
70
|
+
Thread.pass until dummy_io.raised_read?
|
71
|
+
stream.connected?.must_equal true
|
72
|
+
dummy_io.raise_read = nil
|
73
|
+
Thread.pass until dummy_io.raised_write?
|
74
|
+
stream.connected?.must_equal true
|
75
|
+
dummy_io.raise_write = nil
|
76
|
+
stream.stop
|
77
|
+
dummy_io.w_stream.string.must_equal 'hello'
|
78
|
+
end
|
79
|
+
|
80
|
+
it "does not die on EWOULDBLOCK" do
|
81
|
+
dummy_io.raise_write = Errno::EWOULDBLOCK.new
|
82
|
+
dummy_io.raise_read = Errno::EINTR.new
|
83
|
+
stream.start
|
84
|
+
stream.write 'hello'
|
85
|
+
Thread.pass until dummy_io.raised_read?
|
86
|
+
stream.connected?.must_equal true
|
87
|
+
dummy_io.raise_read = nil
|
88
|
+
Thread.pass until dummy_io.raised_write?
|
89
|
+
stream.connected?.must_equal true
|
90
|
+
dummy_io.raise_write = nil
|
91
|
+
stream.stop
|
92
|
+
dummy_io.w_stream.string.must_equal 'hello'
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "callbacks" do
|
96
|
+
def called_with; @calls_received ||= []; end
|
97
|
+
def callback; @callback ||= lambda { |*a| called_with << a }; end
|
98
|
+
|
99
|
+
def callback_stream cbs=nil
|
100
|
+
IoUnblock::Stream.new(dummy_io, cbs)
|
101
|
+
end
|
102
|
+
|
103
|
+
it "triggers started when starting" do
|
104
|
+
cb_stream = callback_stream(started: callback)
|
105
|
+
cb_stream.start
|
106
|
+
cb_stream.stop
|
107
|
+
called_with.must_equal [ [:start] ]
|
108
|
+
end
|
109
|
+
|
110
|
+
it "triggers stopped when stopping" do
|
111
|
+
cb_stream = callback_stream(stopped: callback)
|
112
|
+
cb_stream.start
|
113
|
+
cb_stream.stop
|
114
|
+
called_with.must_equal [ [:stop] ]
|
115
|
+
end
|
116
|
+
|
117
|
+
it "triggers looped after each read/write cycle" do
|
118
|
+
cb_stream = callback_stream(looped: callback)
|
119
|
+
cb_stream.start
|
120
|
+
Thread.pass while called_with.empty?
|
121
|
+
cb_stream.stop
|
122
|
+
called_with.first.must_equal [cb_stream]
|
123
|
+
end
|
124
|
+
|
125
|
+
it "triggers wrote when writing" do
|
126
|
+
dummy_io.max_write = 3
|
127
|
+
cb_stream = callback_stream(wrote: callback)
|
128
|
+
cb_stream.start
|
129
|
+
cb_stream.write "hello"
|
130
|
+
cb_stream.stop
|
131
|
+
called_with.must_equal [ ['hello', 3], ['lo', 2] ]
|
132
|
+
end
|
133
|
+
|
134
|
+
it "triggers read when reading" do
|
135
|
+
dummy_io.max_read = 3
|
136
|
+
dummy_io.r_stream.string = 'hello'
|
137
|
+
cb_stream = callback_stream(read: callback)
|
138
|
+
cb_stream.start
|
139
|
+
Thread.pass until dummy_io.r_stream.eof?
|
140
|
+
cb_stream.stop
|
141
|
+
called_with.must_equal [ ['hel'], ['lo'] ]
|
142
|
+
end
|
143
|
+
|
144
|
+
it "triggers closed when closing" do
|
145
|
+
cb_stream = callback_stream(closed: callback)
|
146
|
+
cb_stream.start
|
147
|
+
cb_stream.stop
|
148
|
+
called_with.must_equal [ [] ]
|
149
|
+
end
|
150
|
+
|
151
|
+
it "triggers failed when reading raises an error" do
|
152
|
+
err = RuntimeError.new "fail"
|
153
|
+
cb_stream = callback_stream(failed: callback)
|
154
|
+
dummy_io.raise_read = err
|
155
|
+
cb_stream.start
|
156
|
+
Thread.pass until cb_stream.running?
|
157
|
+
Thread.pass while cb_stream.connected?
|
158
|
+
cb_stream.stop
|
159
|
+
called_with.must_equal [ [err] ]
|
160
|
+
end
|
161
|
+
|
162
|
+
it "triggers failed when writing raises an error" do
|
163
|
+
err = RuntimeError.new "fail"
|
164
|
+
cb_stream = callback_stream(failed: callback)
|
165
|
+
dummy_io.raise_write = err
|
166
|
+
cb_stream.start
|
167
|
+
cb_stream.write "hello"
|
168
|
+
cb_stream.stop
|
169
|
+
called_with.must_equal [ [err] ]
|
170
|
+
end
|
171
|
+
|
172
|
+
it "triggers the given callback when starting and stopping" do
|
173
|
+
stream.start(&callback)
|
174
|
+
stream.stop
|
175
|
+
called_with.must_equal [ [:start], [:stop]]
|
176
|
+
end
|
177
|
+
|
178
|
+
it "triggers the given callback after writing the full string" do
|
179
|
+
dummy_io.max_write = 3
|
180
|
+
stream.start
|
181
|
+
stream.write('hello', &callback)
|
182
|
+
stream.stop
|
183
|
+
called_with.must_equal [ ['lo', 2] ]
|
184
|
+
end
|
185
|
+
|
186
|
+
it "is not connected when failed is triggered" do
|
187
|
+
is_connected = true
|
188
|
+
cb_stream = callback_stream(
|
189
|
+
failed: lambda { |ex| is_connected = cb_stream.connected? }
|
190
|
+
)
|
191
|
+
err = RuntimeError.new "fail"
|
192
|
+
dummy_io.raise_write = err
|
193
|
+
cb_stream.start
|
194
|
+
cb_stream.write "hello"
|
195
|
+
cb_stream.stop
|
196
|
+
is_connected.must_equal false
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
if ENV['SCOV']
|
2
|
+
require 'simplecov'
|
3
|
+
SimpleCov.start do
|
4
|
+
puts "I am started?! #{Process.pid}"
|
5
|
+
add_filter "/spec/"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'minitest/autorun'
|
10
|
+
require 'minitest/emoji'
|
11
|
+
require 'io_unblock'
|
12
|
+
require 'stringio'
|
13
|
+
|
14
|
+
# Used to test IO stuff, using stringio objects for the write and
|
15
|
+
# read stream.
|
16
|
+
class DummyIO
|
17
|
+
attr_reader :closed, :w_stream, :r_stream, :raised_write, :raised_read
|
18
|
+
alias :closed? :closed
|
19
|
+
alias :raised_write? :raised_write
|
20
|
+
alias :raised_read? :raised_read
|
21
|
+
attr_accessor :readable, :writeable
|
22
|
+
attr_accessor :write_delay, :read_delay
|
23
|
+
attr_accessor :max_write, :max_read
|
24
|
+
attr_accessor :raise_read, :raise_write
|
25
|
+
alias :readable? :readable
|
26
|
+
alias :writeable? :writeable
|
27
|
+
|
28
|
+
def initialize *args, &block
|
29
|
+
@r_stream = StringIO.new
|
30
|
+
@w_stream = StringIO.new
|
31
|
+
@readable = @writeable = true
|
32
|
+
@read_delay = @write_delay = 0
|
33
|
+
@max_write = 0
|
34
|
+
@max_read = 0
|
35
|
+
@closed = false
|
36
|
+
@raise_read = nil
|
37
|
+
@raise_write = nil
|
38
|
+
@raised_write = false
|
39
|
+
@raised_read = false
|
40
|
+
end
|
41
|
+
|
42
|
+
def close
|
43
|
+
@closed = true
|
44
|
+
@w_stream.close
|
45
|
+
@r_stream.close
|
46
|
+
end
|
47
|
+
|
48
|
+
def write_nonblock bytes
|
49
|
+
sleep(@write_delay) if @write_delay > 0
|
50
|
+
do_raise_write
|
51
|
+
if @max_write > 0 && bytes.size > @max_write
|
52
|
+
@w_stream.write bytes[0...@max_write]
|
53
|
+
else
|
54
|
+
@w_stream.write bytes
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def read_nonblock len
|
59
|
+
sleep(@read_delay) if @read_delay > 0
|
60
|
+
do_raise_read
|
61
|
+
if @max_read > 0 && len > @max_read
|
62
|
+
@r_stream.read @max_read
|
63
|
+
else
|
64
|
+
@r_stream.read len
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def do_raise_write
|
69
|
+
if @raise_write
|
70
|
+
@raised_write = true
|
71
|
+
raise @raise_write
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def do_raise_read
|
76
|
+
if @raise_read
|
77
|
+
@raised_read = true
|
78
|
+
raise @raise_read
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: io_unblock
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ian D. Eccles
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-12 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: minitest
|
16
|
+
requirement: &2161332520 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2161332520
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rake
|
27
|
+
requirement: &2161331860 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2161331860
|
36
|
+
description: Non-blocking IO reads/writes wrapped in a thread
|
37
|
+
email:
|
38
|
+
- ian.eccles@gmail.com
|
39
|
+
executables: []
|
40
|
+
extensions: []
|
41
|
+
extra_rdoc_files: []
|
42
|
+
files:
|
43
|
+
- .autotest
|
44
|
+
- .gitignore
|
45
|
+
- Gemfile
|
46
|
+
- README.md
|
47
|
+
- Rakefile
|
48
|
+
- io_unblock.gemspec
|
49
|
+
- lib/io_unblock.rb
|
50
|
+
- lib/io_unblock/buffer.rb
|
51
|
+
- lib/io_unblock/delegation.rb
|
52
|
+
- lib/io_unblock/stream.rb
|
53
|
+
- lib/io_unblock/version.rb
|
54
|
+
- spec/io_unblock/buffer_spec.rb
|
55
|
+
- spec/io_unblock/delegation_spec.rb
|
56
|
+
- spec/io_unblock/stream_spec.rb
|
57
|
+
- spec/spec_helper.rb
|
58
|
+
homepage: https://github.com/iande/io_unblock
|
59
|
+
licenses: []
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options: []
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
requirements: []
|
77
|
+
rubyforge_project: io_unblock
|
78
|
+
rubygems_version: 1.8.12
|
79
|
+
signing_key:
|
80
|
+
specification_version: 3
|
81
|
+
summary: Non-blocking IO reads/writes wrapped in a thread
|
82
|
+
test_files:
|
83
|
+
- spec/io_unblock/buffer_spec.rb
|
84
|
+
- spec/io_unblock/delegation_spec.rb
|
85
|
+
- spec/io_unblock/stream_spec.rb
|
86
|
+
- spec/spec_helper.rb
|
87
|
+
has_rdoc:
|