xchan.rb 0.16.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/tests.yml +26 -0
- data/.gitignore +9 -0
- data/.gitlab-ci.yml +12 -0
- data/.projectile +1 -0
- data/.rubocop.yml +34 -0
- data/.yardopts +4 -0
- data/Gemfile +6 -0
- data/LICENSE +15 -0
- data/README.md +242 -0
- data/Rakefile.rb +9 -0
- data/lib/xchan/bytes.rb +109 -0
- data/lib/xchan/mixin.rb +20 -0
- data/lib/xchan/stat.rb +63 -0
- data/lib/xchan/tempfile.rb +347 -0
- data/lib/xchan/unix_socket.rb +329 -0
- data/lib/xchan/version.rb +5 -0
- data/lib/xchan.rb +53 -0
- data/share/xchan.rb/examples/read_operations/1_blocking_read.rb +20 -0
- data/share/xchan.rb/examples/read_operations/2_nonblocking_read.rb +17 -0
- data/share/xchan.rb/examples/serialization/1_serializers.rb +20 -0
- data/share/xchan.rb/examples/setup.rb +4 -0
- data/share/xchan.rb/examples/socket/2_options.rb +12 -0
- data/share/xchan.rb/examples/stress_tests/1_parallel_access_stress_test.rb +10 -0
- data/share/xchan.rb/examples/write_operations/1_blocking_write.rb +10 -0
- data/share/xchan.rb/examples/write_operations/2_nonblocking_write.rb +24 -0
- data/test/readme_test.rb +44 -0
- data/test/setup.rb +7 -0
- data/test/xchan_test.rb +192 -0
- data/xchan.rb.gemspec +22 -0
- metadata +170 -0
@@ -0,0 +1,347 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
require 'tmpdir'
|
3
|
+
|
4
|
+
##
|
5
|
+
# {Chan::Tempfile Chan::Tempfile} is a fork of Tempfile from
|
6
|
+
# Ruby's standard library. The primary difference between
|
7
|
+
# {Chan::Tempfile Chan::Tempfile}, and the standard library is
|
8
|
+
# that [ruby/tempfile#22](https://github.com/ruby/tempfile/pull/22)
|
9
|
+
# is applied in this fork.
|
10
|
+
class Chan::Tempfile < DelegateClass(File)
|
11
|
+
VERSION = "0.1.3"
|
12
|
+
|
13
|
+
# Creates a file in the underlying file system;
|
14
|
+
# returns a new \Tempfile object based on that file.
|
15
|
+
#
|
16
|
+
# If possible, consider instead using Tempfile.create, which:
|
17
|
+
#
|
18
|
+
# - Avoids the performance cost of delegation,
|
19
|
+
# incurred when Tempfile.new calls its superclass <tt>DelegateClass(File)</tt>.
|
20
|
+
# - Does not rely on a finalizer to close and unlink the file,
|
21
|
+
# which can be unreliable.
|
22
|
+
#
|
23
|
+
# Creates and returns file whose:
|
24
|
+
#
|
25
|
+
# - Class is \Tempfile (not \File, as in Tempfile.create).
|
26
|
+
# - Directory is the system temporary directory (system-dependent).
|
27
|
+
# - Generated filename is unique in that directory.
|
28
|
+
# - Permissions are <tt>0600</tt>;
|
29
|
+
# see {File Permissions}[https://docs.ruby-lang.org/en/master/File.html#label-File+Permissions].
|
30
|
+
# - Mode is <tt>'w+'</tt> (read/write mode, positioned at the end).
|
31
|
+
#
|
32
|
+
# The underlying file is removed when the \Tempfile object dies
|
33
|
+
# and is reclaimed by the garbage collector.
|
34
|
+
#
|
35
|
+
# Example:
|
36
|
+
#
|
37
|
+
# f = Tempfile.new # => #<Tempfile:/tmp/20220505-17839-1s0kt30>
|
38
|
+
# f.class # => Tempfile
|
39
|
+
# f.path # => "/tmp/20220505-17839-1s0kt30"
|
40
|
+
# f.stat.mode.to_s(8) # => "100600"
|
41
|
+
# File.exist?(f.path) # => true
|
42
|
+
# File.unlink(f.path) #
|
43
|
+
# File.exist?(f.path) # => false
|
44
|
+
#
|
45
|
+
# Argument +basename+, if given, may be one of:
|
46
|
+
#
|
47
|
+
# - A string: the generated filename begins with +basename+:
|
48
|
+
#
|
49
|
+
# Tempfile.new('foo') # => #<Tempfile:/tmp/foo20220505-17839-1whk2f>
|
50
|
+
#
|
51
|
+
# - An array of two strings <tt>[prefix, suffix]</tt>:
|
52
|
+
# the generated filename begins with +prefix+ and ends with +suffix+:
|
53
|
+
#
|
54
|
+
# Tempfile.new(%w/foo .jpg/) # => #<Tempfile:/tmp/foo20220505-17839-58xtfi.jpg>
|
55
|
+
#
|
56
|
+
# With arguments +basename+ and +tmpdir+, the file is created in directory +tmpdir+:
|
57
|
+
#
|
58
|
+
# Tempfile.new('foo', '.') # => #<Tempfile:./foo20220505-17839-xfstr8>
|
59
|
+
#
|
60
|
+
# Keyword arguments +mode+ and +options+ are passed directly to method
|
61
|
+
# {File.open}[https://docs.ruby-lang.org/en/master/File.html#method-c-open]:
|
62
|
+
#
|
63
|
+
# - The value given with +mode+ must be an integer,
|
64
|
+
# and may be expressed as the logical OR of constants defined in
|
65
|
+
# {File::Constants}[https://docs.ruby-lang.org/en/master/File/Constants.html].
|
66
|
+
# - For +options+, see {Open Options}[https://docs.ruby-lang.org/en/master/IO.html#class-IO-label-Open+Options].
|
67
|
+
#
|
68
|
+
# Related: Tempfile.create.
|
69
|
+
#
|
70
|
+
def initialize(basename="", tmpdir=nil, mode: 0, perm: 0600, **options)
|
71
|
+
warn "Tempfile.new doesn't call the given block.", uplevel: 1 if block_given?
|
72
|
+
|
73
|
+
@unlinked = false
|
74
|
+
@mode = mode|File::RDWR|File::CREAT|File::EXCL
|
75
|
+
::Dir::Tmpname.create(basename, tmpdir, **options) do |tmpname, n, opts|
|
76
|
+
@tmpfile = File.open(tmpname, @mode, perm, **opts)
|
77
|
+
@perm = perm
|
78
|
+
@opts = opts.freeze
|
79
|
+
end
|
80
|
+
ObjectSpace.define_finalizer(self, Remover.new(@tmpfile))
|
81
|
+
|
82
|
+
super(@tmpfile)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Opens or reopens the file with mode "r+".
|
86
|
+
def open
|
87
|
+
_close
|
88
|
+
mode = @mode & ~(File::CREAT|File::EXCL)
|
89
|
+
@tmpfile = File.open(@tmpfile.path, mode, **@opts)
|
90
|
+
__setobj__(@tmpfile)
|
91
|
+
end
|
92
|
+
|
93
|
+
def _close # :nodoc:
|
94
|
+
@tmpfile.close
|
95
|
+
end
|
96
|
+
protected :_close
|
97
|
+
|
98
|
+
# Closes the file. If +unlink_now+ is true, then the file will be unlinked
|
99
|
+
# (deleted) after closing. Of course, you can choose to later call #unlink
|
100
|
+
# if you do not unlink it now.
|
101
|
+
#
|
102
|
+
# If you don't explicitly unlink the temporary file, the removal
|
103
|
+
# will be delayed until the object is finalized.
|
104
|
+
def close(unlink_now=false)
|
105
|
+
_close
|
106
|
+
unlink if unlink_now
|
107
|
+
end
|
108
|
+
|
109
|
+
# Closes and unlinks (deletes) the file. Has the same effect as called
|
110
|
+
# <tt>close(true)</tt>.
|
111
|
+
def close!
|
112
|
+
close(true)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Unlinks (deletes) the file from the filesystem. One should always unlink
|
116
|
+
# the file after using it, as is explained in the "Explicit close" good
|
117
|
+
# practice section in the Tempfile overview:
|
118
|
+
#
|
119
|
+
# file = Tempfile.new('foo')
|
120
|
+
# begin
|
121
|
+
# # ...do something with file...
|
122
|
+
# ensure
|
123
|
+
# file.close
|
124
|
+
# file.unlink # deletes the temp file
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# === Unlink-before-close
|
128
|
+
#
|
129
|
+
# On POSIX systems it's possible to unlink a file before closing it. This
|
130
|
+
# practice is explained in detail in the Tempfile overview (section
|
131
|
+
# "Unlink after creation"); please refer there for more information.
|
132
|
+
#
|
133
|
+
# However, unlink-before-close may not be supported on non-POSIX operating
|
134
|
+
# systems. Microsoft Windows is the most notable case: unlinking a non-closed
|
135
|
+
# file will result in an error, which this method will silently ignore. If
|
136
|
+
# you want to practice unlink-before-close whenever possible, then you should
|
137
|
+
# write code like this:
|
138
|
+
#
|
139
|
+
# file = Tempfile.new('foo')
|
140
|
+
# file.unlink # On Windows this silently fails.
|
141
|
+
# begin
|
142
|
+
# # ... do something with file ...
|
143
|
+
# ensure
|
144
|
+
# file.close! # Closes the file handle. If the file wasn't unlinked
|
145
|
+
# # because #unlink failed, then this method will attempt
|
146
|
+
# # to do so again.
|
147
|
+
# end
|
148
|
+
def unlink
|
149
|
+
return if @unlinked
|
150
|
+
begin
|
151
|
+
File.unlink(@tmpfile.path)
|
152
|
+
rescue Errno::ENOENT
|
153
|
+
rescue Errno::EACCES
|
154
|
+
# may not be able to unlink on Windows; just ignore
|
155
|
+
return
|
156
|
+
end
|
157
|
+
ObjectSpace.undefine_finalizer(self)
|
158
|
+
@unlinked = true
|
159
|
+
end
|
160
|
+
alias delete unlink
|
161
|
+
|
162
|
+
# Returns the full path name of the temporary file.
|
163
|
+
# This will be nil if #unlink has been called.
|
164
|
+
def path
|
165
|
+
@unlinked ? nil : @tmpfile.path
|
166
|
+
end
|
167
|
+
|
168
|
+
# Returns the size of the temporary file. As a side effect, the IO
|
169
|
+
# buffer is flushed before determining the size.
|
170
|
+
def size
|
171
|
+
if !@tmpfile.closed?
|
172
|
+
@tmpfile.size # File#size calls rb_io_flush_raw()
|
173
|
+
else
|
174
|
+
File.size(@tmpfile.path)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
alias length size
|
178
|
+
|
179
|
+
# :stopdoc:
|
180
|
+
def inspect
|
181
|
+
if @tmpfile.closed?
|
182
|
+
"#<#{self.class}:#{path} (closed)>"
|
183
|
+
else
|
184
|
+
"#<#{self.class}:#{path}>"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
class Remover # :nodoc:
|
189
|
+
def initialize(tmpfile)
|
190
|
+
@pid = Process.pid
|
191
|
+
@tmpfile = tmpfile
|
192
|
+
end
|
193
|
+
|
194
|
+
def call(*args)
|
195
|
+
return if @pid != Process.pid
|
196
|
+
|
197
|
+
$stderr.puts "removing #{@tmpfile.path}..." if $DEBUG
|
198
|
+
|
199
|
+
@tmpfile.close
|
200
|
+
begin
|
201
|
+
File.unlink(@tmpfile.path)
|
202
|
+
rescue Errno::ENOENT
|
203
|
+
end
|
204
|
+
|
205
|
+
$stderr.puts "done" if $DEBUG
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
class << self
|
210
|
+
# :startdoc:
|
211
|
+
# Creates a new Tempfile.
|
212
|
+
#
|
213
|
+
# This method is not recommended and exists mostly for backward compatibility.
|
214
|
+
# Please use Tempfile.create instead, which avoids the cost of delegation,
|
215
|
+
# does not rely on a finalizer, and also unlinks the file when given a block.
|
216
|
+
#
|
217
|
+
# Tempfile.open is still appropriate if you need the Tempfile to be unlinked
|
218
|
+
# by a finalizer and you cannot explicitly know where in the program the
|
219
|
+
# Tempfile can be unlinked safely.
|
220
|
+
#
|
221
|
+
# If no block is given, this is a synonym for Tempfile.new.
|
222
|
+
#
|
223
|
+
# If a block is given, then a Tempfile object will be constructed,
|
224
|
+
# and the block is run with the Tempfile object as argument. The Tempfile
|
225
|
+
# object will be automatically closed after the block terminates.
|
226
|
+
# However, the file will *not* be unlinked and needs to be manually unlinked
|
227
|
+
# with Tempfile#close! or Tempfile#unlink. The finalizer will try to unlink
|
228
|
+
# but should not be relied upon as it can keep the file on the disk much
|
229
|
+
# longer than intended. For instance, on CRuby, finalizers can be delayed
|
230
|
+
# due to conservative stack scanning and references left in unused memory.
|
231
|
+
#
|
232
|
+
# The call returns the value of the block.
|
233
|
+
#
|
234
|
+
# In any case, all arguments (<code>*args</code>) will be passed to Tempfile.new.
|
235
|
+
#
|
236
|
+
# Tempfile.open('foo', '/home/temp') do |f|
|
237
|
+
# # ... do something with f ...
|
238
|
+
# end
|
239
|
+
#
|
240
|
+
# # Equivalent:
|
241
|
+
# f = Tempfile.open('foo', '/home/temp')
|
242
|
+
# begin
|
243
|
+
# # ... do something with f ...
|
244
|
+
# ensure
|
245
|
+
# f.close
|
246
|
+
# end
|
247
|
+
def self.open(*args, **kw)
|
248
|
+
tempfile = new(*args, **kw)
|
249
|
+
|
250
|
+
if block_given?
|
251
|
+
begin
|
252
|
+
yield(tempfile)
|
253
|
+
ensure
|
254
|
+
tempfile.close
|
255
|
+
end
|
256
|
+
else
|
257
|
+
tempfile
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# Creates a file in the underlying file system;
|
264
|
+
# returns a new \File object based on that file.
|
265
|
+
#
|
266
|
+
# With no block given and no arguments, creates and returns file whose:
|
267
|
+
#
|
268
|
+
# - Class is {File}[https://docs.ruby-lang.org/en/master/File.html] (not \Tempfile).
|
269
|
+
# - Directory is the system temporary directory (system-dependent).
|
270
|
+
# - Generated filename is unique in that directory.
|
271
|
+
# - Permissions are <tt>0600</tt>;
|
272
|
+
# see {File Permissions}[https://docs.ruby-lang.org/en/master/File.html#label-File+Permissions].
|
273
|
+
# - Mode is <tt>'w+'</tt> (read/write mode, positioned at the end).
|
274
|
+
#
|
275
|
+
# With no block, the file is not removed automatically,
|
276
|
+
# and so should be explicitly removed.
|
277
|
+
#
|
278
|
+
# Example:
|
279
|
+
#
|
280
|
+
# f = Tempfile.create # => #<File:/tmp/20220505-9795-17ky6f6>
|
281
|
+
# f.class # => File
|
282
|
+
# f.path # => "/tmp/20220505-9795-17ky6f6"
|
283
|
+
# f.stat.mode.to_s(8) # => "100600"
|
284
|
+
# File.exist?(f.path) # => true
|
285
|
+
# File.unlink(f.path)
|
286
|
+
# File.exist?(f.path) # => false
|
287
|
+
#
|
288
|
+
# Argument +basename+, if given, may be one of:
|
289
|
+
#
|
290
|
+
# - A string: the generated filename begins with +basename+:
|
291
|
+
#
|
292
|
+
# Tempfile.create('foo') # => #<File:/tmp/foo20220505-9795-1gok8l9>
|
293
|
+
#
|
294
|
+
# - An array of two strings <tt>[prefix, suffix]</tt>:
|
295
|
+
# the generated filename begins with +prefix+ and ends with +suffix+:
|
296
|
+
#
|
297
|
+
# Tempfile.create(%w/foo .jpg/) # => #<File:/tmp/foo20220505-17839-tnjchh.jpg>
|
298
|
+
#
|
299
|
+
# With arguments +basename+ and +tmpdir+, the file is created in directory +tmpdir+:
|
300
|
+
#
|
301
|
+
# Tempfile.create('foo', '.') # => #<File:./foo20220505-9795-1emu6g8>
|
302
|
+
#
|
303
|
+
# Keyword arguments +mode+ and +options+ are passed directly to method
|
304
|
+
# {File.open}[https://docs.ruby-lang.org/en/master/File.html#method-c-open]:
|
305
|
+
#
|
306
|
+
# - The value given with +mode+ must be an integer,
|
307
|
+
# and may be expressed as the logical OR of constants defined in
|
308
|
+
# {File::Constants}[https://docs.ruby-lang.org/en/master/File/Constants.html].
|
309
|
+
# - For +options+, see {Open Options}[https://docs.ruby-lang.org/en/master/IO.html#class-IO-label-Open+Options].
|
310
|
+
#
|
311
|
+
# With a block given, creates the file as above, passes it to the block,
|
312
|
+
# and returns the block's value;
|
313
|
+
# before the return, the file object is closed and the underlying file is removed:
|
314
|
+
#
|
315
|
+
# Tempfile.create {|file| file.path } # => "/tmp/20220505-9795-rkists"
|
316
|
+
#
|
317
|
+
# Related: Tempfile.new.
|
318
|
+
#
|
319
|
+
module Chan
|
320
|
+
def Tempfile.create(basename="", tmpdir=nil, mode: 0, perm: 0600, **options)
|
321
|
+
tmpfile = nil
|
322
|
+
Dir::Tmpname.create(basename, tmpdir, **options) do |tmpname, n, opts|
|
323
|
+
mode |= File::RDWR|File::CREAT|File::EXCL
|
324
|
+
tmpfile = File.open(tmpname, mode, perm, **opts)
|
325
|
+
end
|
326
|
+
if block_given?
|
327
|
+
begin
|
328
|
+
yield tmpfile
|
329
|
+
ensure
|
330
|
+
unless tmpfile.closed?
|
331
|
+
if File.identical?(tmpfile, tmpfile.path)
|
332
|
+
unlinked = File.unlink tmpfile.path rescue nil
|
333
|
+
end
|
334
|
+
tmpfile.close
|
335
|
+
end
|
336
|
+
unless unlinked
|
337
|
+
begin
|
338
|
+
File.unlink tmpfile.path
|
339
|
+
rescue Errno::ENOENT
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
else
|
344
|
+
tmpfile
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
@@ -0,0 +1,329 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# The {Chan::UNIXSocket Chan::UNIXSocket} class implements a channel
|
5
|
+
# for interprocess communication (IPC) using an unnamed UNIXSocket.
|
6
|
+
class Chan::UNIXSocket
|
7
|
+
require "socket"
|
8
|
+
require "lockf"
|
9
|
+
require_relative "bytes"
|
10
|
+
|
11
|
+
##
|
12
|
+
# @example
|
13
|
+
# ch = Chan::UNIXSocket.new(:marshal)
|
14
|
+
# ch.send([1,2,3])
|
15
|
+
# ch.recv.pop # => 3
|
16
|
+
# ch.close
|
17
|
+
#
|
18
|
+
# @param [Symbol, <#dump, #load>] serializer
|
19
|
+
# A serializer.
|
20
|
+
#
|
21
|
+
# @param [Integer] socket
|
22
|
+
# A socket type (eg Socket::SOCK_STREAM).
|
23
|
+
#
|
24
|
+
# @param [String] tmpdir
|
25
|
+
# Path to a directory where temporary files will be stored.
|
26
|
+
#
|
27
|
+
# @return [Chan::UNIXSocket]
|
28
|
+
# Returns an instance of {Chan::UNIXSocket Chan::UNIXSocket}.
|
29
|
+
def initialize(serializer, tmpdir: Dir.tmpdir, socket: Socket::SOCK_DGRAM)
|
30
|
+
@serializer = Chan.serializers[serializer]&.call || serializer
|
31
|
+
@r, @w = ::UNIXSocket.pair(socket)
|
32
|
+
@bytes = Chan::Bytes.new(tmpdir)
|
33
|
+
@lock = LockFile.new Chan.temporary_file("xchan.lock", tmpdir:)
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# @return [<#dump, #load>]
|
38
|
+
# Returns the serializer used by the channel.
|
39
|
+
def serializer
|
40
|
+
@serializer
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# @return [Boolean]
|
45
|
+
# Returns true when the channel is closed.
|
46
|
+
def closed?
|
47
|
+
@r.closed? and @w.closed?
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Closes the channel.
|
52
|
+
#
|
53
|
+
# @raise [IOError]
|
54
|
+
# When the channel is closed.
|
55
|
+
#
|
56
|
+
# @return [void]
|
57
|
+
def close
|
58
|
+
@lock.lock
|
59
|
+
raise IOError, "channel is closed" if closed?
|
60
|
+
[@r, @w, @bytes, @lock.file].each(&:close)
|
61
|
+
rescue IOError => ex
|
62
|
+
@lock.release
|
63
|
+
raise(ex)
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# @group Write methods
|
68
|
+
|
69
|
+
##
|
70
|
+
# Performs a blocking write
|
71
|
+
#
|
72
|
+
# @param [Object] object
|
73
|
+
# An object to write to the channel.
|
74
|
+
#
|
75
|
+
# @raise [IOError]
|
76
|
+
# When the channel is closed.
|
77
|
+
#
|
78
|
+
# @return [Object]
|
79
|
+
# Returns the number of bytes written to the channel.
|
80
|
+
def send(object)
|
81
|
+
send_nonblock(object)
|
82
|
+
rescue Chan::WaitWritable, Chan::WaitLockable
|
83
|
+
retry
|
84
|
+
end
|
85
|
+
alias_method :write, :send
|
86
|
+
|
87
|
+
##
|
88
|
+
# Performs a non-blocking write
|
89
|
+
#
|
90
|
+
# @param [Object] object
|
91
|
+
# An object to write to the channel.
|
92
|
+
#
|
93
|
+
# @raise [IOError]
|
94
|
+
# When the channel is closed.
|
95
|
+
#
|
96
|
+
# @raise [Chan::WaitWritable]
|
97
|
+
# When a write to the underlying IO blocks.
|
98
|
+
#
|
99
|
+
# @raise [Chan::WaitLockable]
|
100
|
+
# When a write blocks because of a lock held by another process.
|
101
|
+
#
|
102
|
+
# @return [Integer, nil]
|
103
|
+
# Returns the number of bytes written to the channel.
|
104
|
+
def send_nonblock(object)
|
105
|
+
@lock.lock_nonblock
|
106
|
+
raise IOError, "channel closed" if closed?
|
107
|
+
len = @w.write_nonblock(serialize(object))
|
108
|
+
@bytes.push(len)
|
109
|
+
len.tap { @lock.release }
|
110
|
+
rescue IOError, IO::WaitWritable, Errno::ENOBUFS => ex
|
111
|
+
@lock.release
|
112
|
+
raise Chan::WaitWritable, ex.message
|
113
|
+
rescue Errno::EWOULDBLOCK => ex
|
114
|
+
raise Chan::WaitLockable, ex.message
|
115
|
+
end
|
116
|
+
alias_method :write_nonblock, :send_nonblock
|
117
|
+
|
118
|
+
##
|
119
|
+
# @endgroup
|
120
|
+
|
121
|
+
##
|
122
|
+
# @group Read methods
|
123
|
+
|
124
|
+
##
|
125
|
+
# Performs a blocking read
|
126
|
+
#
|
127
|
+
# @raise [IOError]
|
128
|
+
# When the channel is closed.
|
129
|
+
#
|
130
|
+
# @return [Object]
|
131
|
+
# Returns an object from the channel.
|
132
|
+
def recv
|
133
|
+
recv_nonblock
|
134
|
+
rescue Chan::WaitReadable
|
135
|
+
wait_readable
|
136
|
+
retry
|
137
|
+
rescue Chan::WaitLockable
|
138
|
+
retry
|
139
|
+
end
|
140
|
+
alias_method :read, :recv
|
141
|
+
|
142
|
+
##
|
143
|
+
# Performs a non-blocking read
|
144
|
+
#
|
145
|
+
# @raise [IOError]
|
146
|
+
# When the channel is closed.
|
147
|
+
#
|
148
|
+
# @raise [Chan::WaitReadable]
|
149
|
+
# When a read from the underlying IO blocks.
|
150
|
+
#
|
151
|
+
# @raise [Chan::WaitLockable]
|
152
|
+
# When a read blocks because of a lock held by another process.
|
153
|
+
#
|
154
|
+
# @return [Object]
|
155
|
+
# Returns an object from the channel.
|
156
|
+
def recv_nonblock
|
157
|
+
@lock.lock_nonblock
|
158
|
+
raise IOError, "closed channel" if closed?
|
159
|
+
len = @bytes.shift
|
160
|
+
obj = deserialize(@r.read_nonblock(len.zero? ? 1 : len))
|
161
|
+
obj.tap { @lock.release }
|
162
|
+
rescue IOError => ex
|
163
|
+
@lock.release
|
164
|
+
raise(ex)
|
165
|
+
rescue IO::WaitReadable => ex
|
166
|
+
@bytes.unshift(len)
|
167
|
+
@lock.release
|
168
|
+
raise Chan::WaitReadable, ex.message
|
169
|
+
rescue Errno::EAGAIN => ex
|
170
|
+
raise Chan::WaitLockable, ex.message
|
171
|
+
end
|
172
|
+
alias_method :read_nonblock, :recv_nonblock
|
173
|
+
|
174
|
+
##
|
175
|
+
# @endgroup
|
176
|
+
|
177
|
+
##
|
178
|
+
# @example
|
179
|
+
# ch = xchan
|
180
|
+
# 1.upto(4) { ch.send(_1) }
|
181
|
+
# ch.to_a.last # => 4
|
182
|
+
#
|
183
|
+
# @return [Array<Object>]
|
184
|
+
# Returns the consumed contents of the channel.
|
185
|
+
def to_a
|
186
|
+
lock do
|
187
|
+
[].tap { _1.push(recv) until empty? }
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
##
|
192
|
+
# @return [Boolean]
|
193
|
+
# Returns true when the channel is empty.
|
194
|
+
def empty?
|
195
|
+
return true if closed?
|
196
|
+
lock { size.zero? }
|
197
|
+
end
|
198
|
+
|
199
|
+
##
|
200
|
+
# @group Size methods
|
201
|
+
|
202
|
+
##
|
203
|
+
# @return [Integer]
|
204
|
+
# Returns the total number of bytes written to the channel.
|
205
|
+
def bytes_sent
|
206
|
+
lock { @bytes.stat.bytes_written }
|
207
|
+
end
|
208
|
+
alias_method :bytes_written, :bytes_sent
|
209
|
+
|
210
|
+
##
|
211
|
+
# @return [Integer]
|
212
|
+
# Returns the total number of bytes read from the channel.
|
213
|
+
def bytes_received
|
214
|
+
lock { @bytes.stat.bytes_read }
|
215
|
+
end
|
216
|
+
alias_method :bytes_read, :bytes_received
|
217
|
+
|
218
|
+
##
|
219
|
+
# @return [Integer]
|
220
|
+
# Returns the number of objects waiting to be read.
|
221
|
+
def size
|
222
|
+
lock { @bytes.size }
|
223
|
+
end
|
224
|
+
|
225
|
+
##
|
226
|
+
# @endgroup
|
227
|
+
|
228
|
+
##
|
229
|
+
# @group Wait methods
|
230
|
+
|
231
|
+
##
|
232
|
+
# Waits for the channel to become readable.
|
233
|
+
#
|
234
|
+
# @param [Float, Integer, nil] s
|
235
|
+
# The number of seconds to wait. Waits indefinitely when "nil".
|
236
|
+
#
|
237
|
+
# @return [Chan::UNIXSocket, nil]
|
238
|
+
# Returns self when the channel is readable, otherwise returns nil.
|
239
|
+
def wait_readable(s = nil)
|
240
|
+
@r.wait_readable(s) and self
|
241
|
+
end
|
242
|
+
|
243
|
+
##
|
244
|
+
# Waits for the channel to become writable.
|
245
|
+
#
|
246
|
+
# @param [Float, Integer, nil] s
|
247
|
+
# The number of seconds to wait. Waits indefinitely when "nil".
|
248
|
+
#
|
249
|
+
# @return [Chan::UNIXSocket, nil]
|
250
|
+
# Returns self when the channel is writable, otherwise returns nil.
|
251
|
+
def wait_writable(s = nil)
|
252
|
+
@w.wait_writable(s) and self
|
253
|
+
end
|
254
|
+
|
255
|
+
##
|
256
|
+
# @endgroup
|
257
|
+
|
258
|
+
##
|
259
|
+
# @group Socket options
|
260
|
+
|
261
|
+
##
|
262
|
+
# @param [String, Symbol] target
|
263
|
+
# `:reader`, or `:writer`.
|
264
|
+
#
|
265
|
+
# @param [Integer] level
|
266
|
+
# The level (eg `Socket::SOL_SOCKET` for the socket level).
|
267
|
+
#
|
268
|
+
# @param [Integer] option_name
|
269
|
+
# The name of an option (eg `Socket::SO_RCVBUF`).
|
270
|
+
#
|
271
|
+
# @param [Boolean, Integer] option_value
|
272
|
+
# The option value (eg 12345)
|
273
|
+
#
|
274
|
+
# @return [Integer]
|
275
|
+
# Returns 0 on success.
|
276
|
+
def setsockopt(target, level, option_name, option_value)
|
277
|
+
@lock.lock
|
278
|
+
if !%w[reader writer].include?(target.to_s)
|
279
|
+
raise ArgumentError, "target can be ':reader', or ':writer'"
|
280
|
+
end
|
281
|
+
target = (target == :reader) ? @r : @w
|
282
|
+
target.setsockopt(level, option_name, option_value)
|
283
|
+
ensure
|
284
|
+
@lock.release
|
285
|
+
end
|
286
|
+
|
287
|
+
##
|
288
|
+
# @param [String, Symbol] target
|
289
|
+
# `:reader`, or `:writer`.
|
290
|
+
#
|
291
|
+
# @param [Integer] level
|
292
|
+
# The level (eg `Socket::SOL_SOCKET` for the socket level).
|
293
|
+
#
|
294
|
+
# @param [Integer] option_name
|
295
|
+
# The name of an option (eg `Socket::SO_RCVBUF`).
|
296
|
+
#
|
297
|
+
# @return [Socket::Option]
|
298
|
+
# Returns an instance of `Socket::Option`.
|
299
|
+
def getsockopt(target, level, option_name)
|
300
|
+
@lock.lock
|
301
|
+
if !%w[reader writer].include?(target.to_s)
|
302
|
+
raise ArgumentError, "target can be ':reader', or ':writer'"
|
303
|
+
end
|
304
|
+
target = (target == :reader) ? @r : @w
|
305
|
+
target.getsockopt(level, option_name)
|
306
|
+
ensure
|
307
|
+
@lock.release
|
308
|
+
end
|
309
|
+
|
310
|
+
##
|
311
|
+
# @endgroup
|
312
|
+
|
313
|
+
private
|
314
|
+
|
315
|
+
def lock
|
316
|
+
@lock.lock
|
317
|
+
yield
|
318
|
+
ensure
|
319
|
+
@lock.release
|
320
|
+
end
|
321
|
+
|
322
|
+
def serialize(obj)
|
323
|
+
@serializer.dump(obj)
|
324
|
+
end
|
325
|
+
|
326
|
+
def deserialize(str)
|
327
|
+
@serializer.load(str)
|
328
|
+
end
|
329
|
+
end
|