win32-pipe 0.2.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.
- data/CHANGES +34 -0
- data/MANIFEST +15 -0
- data/README +56 -0
- data/Rakefile +65 -0
- data/examples/test_client.rb +30 -0
- data/examples/test_client_async.rb +82 -0
- data/examples/test_server.rb +33 -0
- data/examples/test_server_async.rb +100 -0
- data/lib/win32/pipe.rb +253 -0
- data/lib/win32/pipe/client.rb +49 -0
- data/lib/win32/pipe/server.rb +96 -0
- data/test/tc_pipe.rb +122 -0
- data/test/tc_pipe_client.rb +28 -0
- data/test/tc_pipe_server.rb +34 -0
- data/win32-pipe.gemspec +26 -0
- metadata +68 -0
data/CHANGES
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
== 0.2.0 - 28-May-2008
|
2
|
+
* Now pure Ruby.
|
3
|
+
* Major interface change. Pipe::Server.new and Pipe::Client.new replace the
|
4
|
+
Pipe.new_server and Pipe.new_client methods, respectively.
|
5
|
+
* An optional 3rd argument, the open mode, is now accepted which allows finer
|
6
|
+
control over how pipes are created.
|
7
|
+
* Several pipe mode and open mode constants were added.
|
8
|
+
* The asynchronous pipe server actually works now.
|
9
|
+
* Added the Pipe#name method.
|
10
|
+
* Added the Pipe#asynchronous? method.
|
11
|
+
* Added the Pipe#size method as an alias for Pipe#length.
|
12
|
+
* Added a Rakefile with tasks for testing and installation.
|
13
|
+
* Added a gemspec and uploaded a gem file to RubyForge.
|
14
|
+
* Merged the doc files into the README and/or replaced them with inlined
|
15
|
+
comments that are RDoc friendly.
|
16
|
+
|
17
|
+
== 0.1.2 - 1-Mar-2005
|
18
|
+
* Moved the 'examples' directory to the toplevel directory.
|
19
|
+
* Made the CHANGES and README files rdoc friendly.
|
20
|
+
|
21
|
+
== 0.1.1 - 25-Aug-2004
|
22
|
+
* Added many more tests to the test suite.
|
23
|
+
* Moved the example programs to doc/examples.
|
24
|
+
* Fixed minor bugs in the asynchronous client and server test programs.
|
25
|
+
* Removed the pipe.html file. You can generate your own html documentation
|
26
|
+
using rd2 on the pipe.rd file.
|
27
|
+
|
28
|
+
== 0.1.0 - 13-Feb-2004
|
29
|
+
* Asynchronous support added (thanks Park Heesob)
|
30
|
+
* Sample test programs added. See files under 'test'.
|
31
|
+
* Documentation updates.
|
32
|
+
|
33
|
+
== 0.0.1 - 20-Nov-2003
|
34
|
+
* Initial release
|
data/MANIFEST
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
* MANIFEST
|
2
|
+
* README
|
3
|
+
* CHANGES
|
4
|
+
* Rakefile
|
5
|
+
* win32-pipe.gemspec
|
6
|
+
* examples/test_server.rb
|
7
|
+
* examples/test_server_async.rb
|
8
|
+
* examples/test_client.rb
|
9
|
+
* examples/test_client_async.rb
|
10
|
+
* lib/win32/pipe.rb
|
11
|
+
* lib/win32/pipe/client.rb
|
12
|
+
* lib/win32/pipe/server.rb
|
13
|
+
* test/tc_pipe.rb
|
14
|
+
* test/tc_pipe_client.rb
|
15
|
+
* test/tc_pipe_server.rb
|
data/README
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
= Description
|
2
|
+
A Ruby interface for named pipes on Windows.
|
3
|
+
|
4
|
+
= Prerequisites
|
5
|
+
* windows-pr 0.8.5 or later
|
6
|
+
|
7
|
+
= Installation
|
8
|
+
== Local
|
9
|
+
rake install (non-gem) or rake install_gem (gem)
|
10
|
+
|
11
|
+
== Remote
|
12
|
+
gem install win32-pipe
|
13
|
+
|
14
|
+
= Synopsis
|
15
|
+
require 'win32/pipe'
|
16
|
+
include Win32
|
17
|
+
|
18
|
+
# In server.rb
|
19
|
+
pipe_server = Pipe::Server.new("foo_pipe")
|
20
|
+
pipe_server.connect
|
21
|
+
data = pipe_server.read
|
22
|
+
puts "Got #{data} from client"
|
23
|
+
pipe_server.close
|
24
|
+
|
25
|
+
# In client.rb (run from a different shell)
|
26
|
+
pipe_client = Pipe::Client.new("foo_pipe")
|
27
|
+
pipe_client.write("Hello World")
|
28
|
+
pipe_client.close
|
29
|
+
|
30
|
+
= What's a named pipe?
|
31
|
+
A pipe with a name - literally. In practice, it will feel more like a cross
|
32
|
+
between a socket and a pipe. At least, it does to me.
|
33
|
+
|
34
|
+
= What good is it?
|
35
|
+
My hope is that it can be used in certain circumstances where a fork might
|
36
|
+
be desirable, but which is not possible on Windows. It could also be handy
|
37
|
+
for the traditional "piping data to a server" usage. And if you come up
|
38
|
+
with anything cool, please let us all know!
|
39
|
+
|
40
|
+
= Future Plans
|
41
|
+
* Add transactions
|
42
|
+
|
43
|
+
= License
|
44
|
+
Ruby's
|
45
|
+
|
46
|
+
= Warranty
|
47
|
+
This package is provided "as is" and without any express or
|
48
|
+
implied warranties, including, without limitation, the implied
|
49
|
+
warranties of merchantability and fitness for a particular purpose.
|
50
|
+
|
51
|
+
= Copyright
|
52
|
+
(C) 2003-2008, Daniel J. Berger, All Rights Reserved.
|
53
|
+
|
54
|
+
= Authors
|
55
|
+
Daniel Berger
|
56
|
+
Park Heesob
|
data/Rakefile
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/clean'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rbconfig'
|
5
|
+
include Config
|
6
|
+
|
7
|
+
desc 'Install the win32-pipe library (non-gem)'
|
8
|
+
task :install do
|
9
|
+
sitelibdir = CONFIG['sitelibdir']
|
10
|
+
|
11
|
+
pipe_installdir = File.join(sitelibdir, 'win32')
|
12
|
+
sub_installdir = File.join(sitelibdir, 'win32', 'pipe')
|
13
|
+
|
14
|
+
pipe_file = File.join('lib', 'win32', 'pipe.rb')
|
15
|
+
client_file = File.join('lib', 'win32', 'pipe', 'client.rb')
|
16
|
+
server_file = File.join('lib', 'win32', 'pipe', 'server.rb')
|
17
|
+
|
18
|
+
FileUtils.mkdir_p(sub_installdir)
|
19
|
+
|
20
|
+
FileUtils.cp(pipe_file, pipe_installdir, :verbose => true)
|
21
|
+
FileUtils.cp(client_file, sub_installdir, :verbose => true)
|
22
|
+
FileUtils.cp(server_file, sub_installdir, :verbose => true)
|
23
|
+
end
|
24
|
+
|
25
|
+
desc 'Install the win32-pipe library'
|
26
|
+
task :install_c => [:build] do
|
27
|
+
Dir.chdir('ext'){
|
28
|
+
sh 'nmake install'
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
desc "Clean any build files for win32-pipe"
|
33
|
+
task :clean do
|
34
|
+
Dir.chdir('ext') do
|
35
|
+
if File.exists?('pipe.so') ||
|
36
|
+
File.exists?('win32/pipe.so')
|
37
|
+
then
|
38
|
+
sh 'nmake distclean'
|
39
|
+
rm 'win32/pipe.so' if File.exists?('win32/pipe.so')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
desc "Build win32-pipe (but don't install it)"
|
45
|
+
task :build => [:clean] do
|
46
|
+
Dir.chdir('ext') do
|
47
|
+
ruby 'extconf.rb'
|
48
|
+
sh 'nmake'
|
49
|
+
mv 'pipe.so', 'win32' # For the test suite
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
desc "Run the sample program"
|
54
|
+
task :example do |t|
|
55
|
+
Dir.chdir('examples'){
|
56
|
+
sh 'ruby test.rb'
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
Rake::TestTask.new('test') do |test|
|
61
|
+
test.libs << 'lib/win32'
|
62
|
+
test.libs << 'lib/win32/pipe'
|
63
|
+
test.test_files = FileList['test/tc*']
|
64
|
+
test.warning = true
|
65
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
#########################################################################
|
2
|
+
# test_client.rb
|
3
|
+
#
|
4
|
+
# Simple client test. Be sure to start the server first in a separate
|
5
|
+
# terminal. You can run this example via the 'rake example_client' task.
|
6
|
+
#
|
7
|
+
# Modify this code as you see fit.
|
8
|
+
#########################################################################
|
9
|
+
require 'win32/pipe'
|
10
|
+
include Win32
|
11
|
+
|
12
|
+
Thread.new { loop { sleep 0.01 } } # Allow Ctrl-C
|
13
|
+
|
14
|
+
puts "VERSION: " + Pipe::VERSION
|
15
|
+
|
16
|
+
# Block form
|
17
|
+
Pipe::Client.new('foo') do |pipe|
|
18
|
+
puts "Connected..."
|
19
|
+
pipe.write("Ruby rocks!")
|
20
|
+
data = pipe.read
|
21
|
+
puts "Got [#{data}] back from server"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Non-block form
|
25
|
+
#pclient = Pipe::Client.new('foo')
|
26
|
+
#puts "Connected..."
|
27
|
+
#pclient.write("Ruby rocks!")
|
28
|
+
#data = pclient.read
|
29
|
+
#puts "Got [#{data}] back from server"
|
30
|
+
#pclient.close
|
@@ -0,0 +1,82 @@
|
|
1
|
+
#########################################################################
|
2
|
+
# test_client_async.rb
|
3
|
+
#
|
4
|
+
# Simple client test. Be sure to start the server first in a separate
|
5
|
+
# terminal. You can run this example via the 'rake example_async_client'
|
6
|
+
# task.
|
7
|
+
#########################################################################
|
8
|
+
require 'win32/pipe'
|
9
|
+
include Win32
|
10
|
+
|
11
|
+
puts "VERSION: " + Pipe::VERSION
|
12
|
+
|
13
|
+
Thread.new { loop { sleep 0.01 } } # Allow Ctrl-C
|
14
|
+
|
15
|
+
CONNECTING_STATE = 0
|
16
|
+
READING_STATE = 1
|
17
|
+
WRITING_STATE = 2
|
18
|
+
|
19
|
+
class MyPipe < Pipe::Client
|
20
|
+
def read_complete
|
21
|
+
puts "read_complete"
|
22
|
+
puts "Got [#{buffer}] back from server"
|
23
|
+
@state = WRITING_STATE
|
24
|
+
end
|
25
|
+
|
26
|
+
def write_complete
|
27
|
+
puts "write_complete"
|
28
|
+
@state = READING_STATE
|
29
|
+
end
|
30
|
+
|
31
|
+
def mainloop
|
32
|
+
@state = WRITING_STATE
|
33
|
+
while true
|
34
|
+
if wait(1) # wait for 1 second
|
35
|
+
if pending? # IO is pending
|
36
|
+
case @state
|
37
|
+
when READING_STATE
|
38
|
+
if transferred == 0
|
39
|
+
reconnect
|
40
|
+
break
|
41
|
+
end
|
42
|
+
read_complete
|
43
|
+
break
|
44
|
+
when WRITING_STATE
|
45
|
+
if transferred != length
|
46
|
+
reconnect
|
47
|
+
break
|
48
|
+
end
|
49
|
+
write_complete
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
case @state
|
54
|
+
when READING_STATE
|
55
|
+
if read
|
56
|
+
if not pending?
|
57
|
+
read_complete
|
58
|
+
break
|
59
|
+
end
|
60
|
+
end
|
61
|
+
when WRITING_STATE
|
62
|
+
if write("Ruby rocks!")
|
63
|
+
if not pending?
|
64
|
+
write_complete
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
sleep(1)
|
71
|
+
puts "pipe client is running"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
flags = Pipe::DEFAULT_OPEN_MODE | Pipe::OVERLAPPED
|
77
|
+
|
78
|
+
MyPipe.new('foo', nil, flags) do |client|
|
79
|
+
puts "Connected..."
|
80
|
+
client.mainloop
|
81
|
+
end
|
82
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
#########################################################################
|
2
|
+
# test_server.rb
|
3
|
+
#
|
4
|
+
# A simple named pipe server. Start this up in its own terminal window.
|
5
|
+
# You may have to use the task manager to kill it if you don't connect
|
6
|
+
# with the test client program.
|
7
|
+
#
|
8
|
+
# You can start this server with the 'rake example_server' task. Modify
|
9
|
+
# this code as you see fit.
|
10
|
+
#########################################################################
|
11
|
+
require 'win32/pipe'
|
12
|
+
include Win32
|
13
|
+
|
14
|
+
Thread.new { loop { sleep 0.01 } } # Allow Ctrl-C
|
15
|
+
|
16
|
+
puts "VERSION: " + Pipe::VERSION
|
17
|
+
|
18
|
+
# Block form
|
19
|
+
Pipe::Server.new('foo') do |pipe|
|
20
|
+
pipe.connect
|
21
|
+
data = pipe.read
|
22
|
+
puts "Got [#{data}]"
|
23
|
+
pipe.write "Thanks for the data!"
|
24
|
+
end
|
25
|
+
|
26
|
+
# Non-block form
|
27
|
+
#pserver = Pipe::Server.new('foo')
|
28
|
+
#pserver.connect # put server in wait connect
|
29
|
+
#data = pserver.read
|
30
|
+
#puts "Got [#{data}]"
|
31
|
+
#pserver.write("Thanks for the data!")
|
32
|
+
#pserver.disconnect
|
33
|
+
#pserver.close
|
@@ -0,0 +1,100 @@
|
|
1
|
+
#######################################################################
|
2
|
+
# test_server_async.rb
|
3
|
+
#
|
4
|
+
# A simple, asynchronous named pipe server. Start this up in its own
|
5
|
+
# terminal window. You can run this program via the
|
6
|
+
# 'rake example_async_server' task.
|
7
|
+
#######################################################################
|
8
|
+
require 'win32/pipe'
|
9
|
+
include Win32
|
10
|
+
|
11
|
+
puts "VERSION: " + Pipe::VERSION
|
12
|
+
|
13
|
+
Thread.new { loop { sleep 0.01 } } # Allow Ctrl-C
|
14
|
+
|
15
|
+
CONNECTING_STATE = 0
|
16
|
+
READING_STATE = 1
|
17
|
+
WRITING_STATE = 2
|
18
|
+
|
19
|
+
class MyPipe < Pipe::Server
|
20
|
+
def connected
|
21
|
+
puts "connected"
|
22
|
+
@state = READING_STATE
|
23
|
+
end
|
24
|
+
|
25
|
+
def read_complete
|
26
|
+
puts "read_complete"
|
27
|
+
puts "Got [#{buffer}]"
|
28
|
+
@state = WRITING_STATE
|
29
|
+
end
|
30
|
+
|
31
|
+
def write_complete
|
32
|
+
puts "write_complete"
|
33
|
+
disconnect
|
34
|
+
@state = CONNECTING_STATE
|
35
|
+
end
|
36
|
+
|
37
|
+
def reconnect
|
38
|
+
disconnect
|
39
|
+
mainloop
|
40
|
+
end
|
41
|
+
|
42
|
+
def mainloop
|
43
|
+
@state = CONNECTING_STATE
|
44
|
+
while true
|
45
|
+
if wait(1) # wait for 1 second
|
46
|
+
if pending? # IO is pending
|
47
|
+
case @state
|
48
|
+
when CONNECTING_STATE
|
49
|
+
connected
|
50
|
+
when READING_STATE
|
51
|
+
if transferred == 0
|
52
|
+
reconnect
|
53
|
+
break
|
54
|
+
end
|
55
|
+
read_complete
|
56
|
+
when WRITING_STATE
|
57
|
+
if transferred != length
|
58
|
+
reconnect
|
59
|
+
break
|
60
|
+
end
|
61
|
+
write_complete
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
case @state
|
66
|
+
when CONNECTING_STATE
|
67
|
+
if connect
|
68
|
+
connected
|
69
|
+
end
|
70
|
+
when READING_STATE
|
71
|
+
if read
|
72
|
+
if !pending?
|
73
|
+
read_complete
|
74
|
+
end
|
75
|
+
else
|
76
|
+
reconnect
|
77
|
+
end
|
78
|
+
when WRITING_STATE
|
79
|
+
if write("Thanks for the data!")
|
80
|
+
if not pending?
|
81
|
+
write_complete
|
82
|
+
end
|
83
|
+
else
|
84
|
+
reconnect
|
85
|
+
break
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
sleep(1)
|
91
|
+
puts "pipe server is running"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
flags = Pipe::ACCESS_DUPLEX | Pipe::OVERLAPPED
|
97
|
+
|
98
|
+
MyPipe.new('foo', 0, flags) do |pipe|
|
99
|
+
pipe.mainloop
|
100
|
+
end
|
data/lib/win32/pipe.rb
ADDED
@@ -0,0 +1,253 @@
|
|
1
|
+
require 'windows/pipe'
|
2
|
+
require 'windows/synchronize'
|
3
|
+
require 'windows/handle'
|
4
|
+
require 'windows/file'
|
5
|
+
require 'windows/error'
|
6
|
+
|
7
|
+
# The Win32 module serves as a namespace only.
|
8
|
+
module Win32
|
9
|
+
# The Pipe class is an abstract base class for the Pipe::Server and
|
10
|
+
# Pipe::Client classes. Do not use this directly.
|
11
|
+
#
|
12
|
+
class Pipe
|
13
|
+
include Windows::Pipe
|
14
|
+
include Windows::Synchronize
|
15
|
+
include Windows::Handle
|
16
|
+
include Windows::File
|
17
|
+
include Windows::Error
|
18
|
+
|
19
|
+
class Error < StandardError; end
|
20
|
+
|
21
|
+
# The version of this library
|
22
|
+
VERSION = '0.2.0'
|
23
|
+
|
24
|
+
PIPE_BUFFER_SIZE = 512 #:nodoc:
|
25
|
+
PIPE_TIMEOUT = 5000 #:nodoc:
|
26
|
+
|
27
|
+
# Blocking mode is enabled
|
28
|
+
WAIT = PIPE_WAIT
|
29
|
+
|
30
|
+
# Nonblocking mode is enabled
|
31
|
+
NOWAIT = PIPE_NOWAIT
|
32
|
+
|
33
|
+
# The pipe is bi-directional. Both server and client processes can read
|
34
|
+
# from and write to the pipe.
|
35
|
+
ACCESS_DUPLEX = PIPE_ACCESS_DUPLEX
|
36
|
+
|
37
|
+
# The flow of data in the pipe goes from client to server only.
|
38
|
+
ACCESS_INBOUND = PIPE_ACCESS_INBOUND
|
39
|
+
|
40
|
+
# The flow of data in the pipe goes from server to client only.
|
41
|
+
ACCESS_OUTBOUND = PIPE_ACCESS_OUTBOUND
|
42
|
+
|
43
|
+
# Data is written to the pipe as a stream of bytes.
|
44
|
+
TYPE_BYTE = PIPE_TYPE_BYTE
|
45
|
+
|
46
|
+
# Data is written to the pipe as a stream of messages.
|
47
|
+
TYPE_MESSAGE = PIPE_TYPE_MESSAGE
|
48
|
+
|
49
|
+
# Data is read from the pipe as a stream of bytes.
|
50
|
+
READMODE_BYTE = PIPE_READMODE_BYTE
|
51
|
+
|
52
|
+
# Data is read from the pipe as a stream of messages.
|
53
|
+
READMODE_MESSAGE = PIPE_READMODE_MESSAGE
|
54
|
+
|
55
|
+
# All instances beyond the first will fail with access denied errors.
|
56
|
+
FIRST_PIPE_INSTANCE = FILE_FLAG_FIRST_PIPE_INSTANCE
|
57
|
+
|
58
|
+
# Functions do not return until the data is written across the network.
|
59
|
+
WRITE_THROUGH = FILE_FLAG_WRITE_THROUGH
|
60
|
+
|
61
|
+
# Overlapped mode enables asynchronous communication.
|
62
|
+
OVERLAPPED = FILE_FLAG_OVERLAPPED
|
63
|
+
|
64
|
+
# The default pipe mode
|
65
|
+
DEFAULT_PIPE_MODE = NOWAIT
|
66
|
+
|
67
|
+
# The default open mode
|
68
|
+
DEFAULT_OPEN_MODE = FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH
|
69
|
+
|
70
|
+
# The data still in the pipe's buffer
|
71
|
+
attr_reader :buffer
|
72
|
+
|
73
|
+
# The number of bytes to be written to the pipe.
|
74
|
+
attr_reader :size
|
75
|
+
|
76
|
+
# The number of characters that are actually transferred over the pipe.
|
77
|
+
attr_reader :transferred
|
78
|
+
|
79
|
+
# The full name of the pipe, e.g. "\\\\.\\pipe\\my_pipe"
|
80
|
+
attr_reader :name
|
81
|
+
|
82
|
+
# The pipe mode of the pipe.
|
83
|
+
attr_reader :open_mode
|
84
|
+
|
85
|
+
# The open mode of the pipe.
|
86
|
+
attr_reader :pipe_mode
|
87
|
+
|
88
|
+
# Abstract initializer for base class. This handles automatic prepending
|
89
|
+
# of '\\.\pipe\' to each named pipe so that you don't have to. Don't
|
90
|
+
# use this directly. Add the full implementation in subclasses.
|
91
|
+
#
|
92
|
+
# The default pipe mode is PIPE_WAIT.
|
93
|
+
#
|
94
|
+
# The default open mode is FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH.
|
95
|
+
#
|
96
|
+
def initialize(name, pipe_mode = DEFAULT_PIPE_MODE, open_mode = DEFAULT_OPEN_MODE)
|
97
|
+
@name = "\\\\.\\pipe\\" + name
|
98
|
+
|
99
|
+
@pipe_mode = pipe_mode.nil? ? DEFAULT_PIPE_MODE : pipe_mode
|
100
|
+
@open_mode = open_mode.nil? ? DEFAULT_OPEN_MODE : open_mode
|
101
|
+
|
102
|
+
@pipe = nil
|
103
|
+
@pending_io = false
|
104
|
+
@buffer = 0.chr * PIPE_BUFFER_SIZE
|
105
|
+
@size = 0
|
106
|
+
@overlapped = 0.chr * 20 # sizeof(OVERLAPPED)
|
107
|
+
@transferred = 0
|
108
|
+
@asynchronous = false
|
109
|
+
|
110
|
+
if open_mode & FILE_FLAG_OVERLAPPED > 0
|
111
|
+
@asynchronous = true
|
112
|
+
end
|
113
|
+
|
114
|
+
if @asynchronous
|
115
|
+
@event = CreateEvent(nil, true, true, nil)
|
116
|
+
@overlapped[16, 4] = [@event].pack('L')
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Disconnects the pipe.
|
121
|
+
def disconnect
|
122
|
+
DisconnectNamedPipe(@pipe)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Closes the pipe.
|
126
|
+
#
|
127
|
+
def close
|
128
|
+
CloseHandle(@pipe)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns whether or not there is a pending IO operation on the pipe.
|
132
|
+
#
|
133
|
+
def pending?
|
134
|
+
@pending_io
|
135
|
+
end
|
136
|
+
|
137
|
+
# Returns whether or not the pipe is asynchronous.
|
138
|
+
#
|
139
|
+
def asynchronous?
|
140
|
+
@asynchronous
|
141
|
+
end
|
142
|
+
|
143
|
+
# Reads data from the pipe. You can read data from either end of a named
|
144
|
+
# pipe.
|
145
|
+
#
|
146
|
+
def read
|
147
|
+
bytes = [0].pack('L')
|
148
|
+
@buffer = 0.chr * PIPE_BUFFER_SIZE
|
149
|
+
|
150
|
+
if @asynchronous
|
151
|
+
bool = ReadFile(@pipe, @buffer, @buffer.size, bytes, @overlapped)
|
152
|
+
|
153
|
+
bytes_read = bytes.unpack('L').first
|
154
|
+
|
155
|
+
if bool && bytes_read > 0
|
156
|
+
@pending_io = false
|
157
|
+
@buffer = @buffer[0, bytes_read]
|
158
|
+
return true
|
159
|
+
end
|
160
|
+
|
161
|
+
error = GetLastError()
|
162
|
+
if !bool && error == ERROR_IO_PENDING
|
163
|
+
@pending_io = true
|
164
|
+
return true
|
165
|
+
end
|
166
|
+
|
167
|
+
return false
|
168
|
+
else
|
169
|
+
unless ReadFile(@pipe, @buffer, @buffer.size, bytes, nil)
|
170
|
+
raise Error, get_last_error
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
@buffer.unpack("A*")
|
175
|
+
end
|
176
|
+
|
177
|
+
# Writes 'data' to the pipe. You can write data to either end of a
|
178
|
+
# named pipe.
|
179
|
+
#
|
180
|
+
def write(data)
|
181
|
+
@buffer = data
|
182
|
+
@size = data.size
|
183
|
+
bytes = [0].pack('L')
|
184
|
+
|
185
|
+
if @asynchronous
|
186
|
+
bool = WriteFile(@pipe, @buffer, @buffer.size, bytes, @overlapped)
|
187
|
+
|
188
|
+
bytes_written = bytes.unpack('L').first
|
189
|
+
|
190
|
+
if bool && bytes_written > 0
|
191
|
+
@pending_io = false
|
192
|
+
return true
|
193
|
+
end
|
194
|
+
|
195
|
+
error = GetLastError()
|
196
|
+
|
197
|
+
if !bool && error == ERROR_IO_PENDING
|
198
|
+
@pending_io = true
|
199
|
+
return true
|
200
|
+
end
|
201
|
+
|
202
|
+
return false
|
203
|
+
else
|
204
|
+
unless WriteFile(@pipe, @buffer, @buffer.size, bytes, 0)
|
205
|
+
raise Error, get_last_error
|
206
|
+
end
|
207
|
+
|
208
|
+
return true
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Returns the pipe object if an event (such as a client connection)
|
213
|
+
# occurs within the +max_time+ specified (in seconds). Otherwise, it
|
214
|
+
# returns false.
|
215
|
+
#
|
216
|
+
def wait(max_time = nil)
|
217
|
+
unless @asynchronous
|
218
|
+
raise Error, 'cannot wait in synchronous (blocking) mode'
|
219
|
+
end
|
220
|
+
|
221
|
+
max_time = max_time ? max_time * 1000 : INFINITE
|
222
|
+
|
223
|
+
wait = WaitForSingleObject(@event, max_time)
|
224
|
+
|
225
|
+
if wait == WAIT_TIMEOUT
|
226
|
+
return false
|
227
|
+
else
|
228
|
+
if wait != WAIT_OBJECT_0
|
229
|
+
raise Error, get_last_error
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
if @pending_io
|
234
|
+
transferred = [0].pack('L')
|
235
|
+
bool = GetOverlappedResult(@pipe, @overlapped, transferred, false)
|
236
|
+
|
237
|
+
unless bool
|
238
|
+
raise Error, get_last_error
|
239
|
+
end
|
240
|
+
|
241
|
+
@transferred = transferred.unpack('L')[0]
|
242
|
+
@buffer = @buffer[0, @transferred]
|
243
|
+
end
|
244
|
+
|
245
|
+
self
|
246
|
+
end
|
247
|
+
|
248
|
+
alias length size
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
require 'win32/pipe/server'
|
253
|
+
require 'win32/pipe/client'
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# The Win32 module serves as a namespace only
|
2
|
+
module Win32
|
3
|
+
# The Pipe::Client class encapsulates the client side of a named pipe
|
4
|
+
# connection.
|
5
|
+
#
|
6
|
+
class Pipe::Client < Pipe
|
7
|
+
# Create and return a new Pipe::Client instance.
|
8
|
+
#
|
9
|
+
# The default pipe mode is PIPE_WAIT.
|
10
|
+
#
|
11
|
+
# The default open mode is FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH.
|
12
|
+
#--
|
13
|
+
# 2147483776 is FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH
|
14
|
+
def initialize(name, pipe_mode = DEFAULT_PIPE_MODE, open_mode = DEFAULT_OPEN_MODE)
|
15
|
+
super(name, pipe_mode, open_mode)
|
16
|
+
|
17
|
+
@pipe = CreateFile(
|
18
|
+
@name,
|
19
|
+
GENERIC_READ | GENERIC_WRITE,
|
20
|
+
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
21
|
+
nil,
|
22
|
+
OPEN_EXISTING,
|
23
|
+
@open_mode,
|
24
|
+
nil
|
25
|
+
)
|
26
|
+
|
27
|
+
error = GetLastError()
|
28
|
+
|
29
|
+
if error == ERROR_PIPE_BUSY
|
30
|
+
unless WaitNamedPipe(@name, NMPWAIT_WAIT_FOREVER)
|
31
|
+
raise Error, get_last_error
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
if @pipe == INVALID_HANDLE_VALUE
|
36
|
+
raise Error, get_last_error
|
37
|
+
end
|
38
|
+
|
39
|
+
if block_given?
|
40
|
+
begin
|
41
|
+
yield self
|
42
|
+
ensure
|
43
|
+
disconnect
|
44
|
+
close
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# The Win32 module serves as a namespace only.
|
2
|
+
module Win32
|
3
|
+
# The Pipe::Server class encapsulates the server side of a named pipe
|
4
|
+
# connection.
|
5
|
+
class Pipe::Server < Pipe
|
6
|
+
|
7
|
+
# Creates and returns a new Pipe::Server instance, using +name+ as the
|
8
|
+
# name for the pipe. Note that this does not actually connect the pipe.
|
9
|
+
# Use Pipe::Server#connect for that.
|
10
|
+
#
|
11
|
+
# The default pipe_mode is PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT.
|
12
|
+
#
|
13
|
+
# The default open_mode is Pipe::ACCESS_DUPLEX.
|
14
|
+
#--
|
15
|
+
# The default pipe_mode also happens to be 0.
|
16
|
+
#
|
17
|
+
def initialize(name, pipe_mode = 0, open_mode = Pipe::ACCESS_DUPLEX)
|
18
|
+
super(name, pipe_mode, open_mode)
|
19
|
+
|
20
|
+
@pipe = CreateNamedPipe(
|
21
|
+
@name,
|
22
|
+
@open_mode,
|
23
|
+
@pipe_mode,
|
24
|
+
PIPE_UNLIMITED_INSTANCES,
|
25
|
+
PIPE_BUFFER_SIZE,
|
26
|
+
PIPE_BUFFER_SIZE,
|
27
|
+
PIPE_TIMEOUT,
|
28
|
+
0
|
29
|
+
)
|
30
|
+
|
31
|
+
if @pipe == INVALID_HANDLE_VALUE
|
32
|
+
raise Error, get_last_error
|
33
|
+
end
|
34
|
+
|
35
|
+
if block_given?
|
36
|
+
begin
|
37
|
+
yield self
|
38
|
+
ensure
|
39
|
+
close
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Enables the named pipe server process to wait for a client process
|
45
|
+
# to connect to an instance of a named pipe. In other words, it puts
|
46
|
+
# the server in 'connection wait' status.
|
47
|
+
#
|
48
|
+
# In synchronous mode always returns true on success. In asynchronous
|
49
|
+
# mode returns true if there is pending IO, or false otherwise.
|
50
|
+
#
|
51
|
+
def connect
|
52
|
+
if @asynchronous
|
53
|
+
# An overlapped ConnectNamedPipe should return 0
|
54
|
+
if ConnectNamedPipe(@pipe, @overlapped)
|
55
|
+
raise Error, get_last_error
|
56
|
+
end
|
57
|
+
|
58
|
+
error = GetLastError()
|
59
|
+
|
60
|
+
case error
|
61
|
+
when ERROR_IO_PENDING
|
62
|
+
@pending_io = true
|
63
|
+
when ERROR_PIPE_CONNECTED
|
64
|
+
unless SetEvent(@event)
|
65
|
+
raise Error, get_last_error(error)
|
66
|
+
end
|
67
|
+
when ERROR_PIPE_LISTENING
|
68
|
+
# Do nothing
|
69
|
+
else
|
70
|
+
raise Error, get_last_error(error)
|
71
|
+
end
|
72
|
+
|
73
|
+
if @pending_io
|
74
|
+
return false
|
75
|
+
else
|
76
|
+
return true
|
77
|
+
end
|
78
|
+
else
|
79
|
+
unless ConnectNamedPipe(@pipe, nil)
|
80
|
+
raise Error, get_last_error
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
return true
|
85
|
+
end
|
86
|
+
|
87
|
+
# Close the server. This will flush file buffers, disconnect the
|
88
|
+
# pipe, and close the pipe handle.
|
89
|
+
#
|
90
|
+
def close
|
91
|
+
FlushFileBuffers(@pipe)
|
92
|
+
DisconnectNamedPipe(@pipe)
|
93
|
+
super
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/test/tc_pipe.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
##########################################################################
|
2
|
+
# tc_pipe.rb
|
3
|
+
#
|
4
|
+
# Test suite for the win32-pipe library. This test suite should be run
|
5
|
+
# via the 'rake test' task.
|
6
|
+
##########################################################################
|
7
|
+
require 'test/unit'
|
8
|
+
require 'win32/pipe'
|
9
|
+
include Win32
|
10
|
+
|
11
|
+
class TC_Win32_Pipe < Test::Unit::TestCase
|
12
|
+
def setup
|
13
|
+
@pipe = Pipe.new('foo')
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_version
|
17
|
+
assert_equal('0.2.0', Pipe::VERSION)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_name
|
21
|
+
assert_respond_to(@pipe, :name)
|
22
|
+
assert_nothing_raised{ @pipe.name }
|
23
|
+
assert_equal("\\\\.\\pipe\\foo", @pipe.name)
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_pipe_mode
|
27
|
+
assert_respond_to(@pipe, :pipe_mode)
|
28
|
+
assert_nothing_raised{ @pipe.pipe_mode }
|
29
|
+
assert_equal(Pipe::DEFAULT_PIPE_MODE, @pipe.pipe_mode)
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_open_mode
|
33
|
+
assert_respond_to(@pipe, :open_mode)
|
34
|
+
assert_nothing_raised{ @pipe.open_mode }
|
35
|
+
assert_equal(Pipe::DEFAULT_OPEN_MODE, @pipe.open_mode)
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_buffer
|
39
|
+
assert_respond_to(@pipe, :buffer)
|
40
|
+
assert_nothing_raised{ @pipe.buffer }
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_size
|
44
|
+
assert_respond_to(@pipe, :size)
|
45
|
+
assert_nothing_raised{ @pipe.size }
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_length_alias
|
49
|
+
assert_respond_to(@pipe, :length)
|
50
|
+
assert_equal(true, @pipe.method(:length) == @pipe.method(:size))
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_pending
|
54
|
+
assert_respond_to(@pipe, :pending?)
|
55
|
+
assert_nothing_raised{ @pipe.pending? }
|
56
|
+
assert_equal(false, @pipe.pending?)
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_asynchronous
|
60
|
+
assert_respond_to(@pipe, :asynchronous?)
|
61
|
+
assert_nothing_raised{ @pipe.asynchronous? }
|
62
|
+
assert_equal(false, @pipe.asynchronous?)
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_read
|
66
|
+
assert_respond_to(@pipe, :read)
|
67
|
+
assert_raises(Pipe::Error){ @pipe.read } # Nothing to read
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_transferred
|
71
|
+
assert_respond_to(@pipe, :transferred)
|
72
|
+
assert_nothing_raised{ @pipe.transferred }
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_wait
|
76
|
+
assert_respond_to(@pipe, :wait)
|
77
|
+
assert_raises(Pipe::Error){ @pipe.wait } # Can't wait in blocking mode
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_write
|
81
|
+
assert_respond_to(@pipe, :write)
|
82
|
+
assert_raises(ArgumentError){ @pipe.write } # Must have 1 argument
|
83
|
+
assert_raises(Pipe::Error){ @pipe.write("foo") } # Nothing to write to
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_disconnect
|
87
|
+
assert_respond_to(@pipe, :disconnect)
|
88
|
+
assert_nothing_raised{ @pipe.disconnect }
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_close
|
92
|
+
assert_respond_to(@pipe, :close)
|
93
|
+
assert_nothing_raised{ @pipe.close }
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_pipe_mode_constants
|
97
|
+
assert_not_nil(Pipe::WAIT)
|
98
|
+
assert_not_nil(Pipe::NOWAIT)
|
99
|
+
assert_not_nil(Pipe::TYPE_BYTE)
|
100
|
+
assert_not_nil(Pipe::TYPE_MESSAGE)
|
101
|
+
assert_not_nil(Pipe::READMODE_BYTE)
|
102
|
+
assert_not_nil(Pipe::READMODE_MESSAGE)
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_open_mode_constants
|
106
|
+
assert_not_nil(Pipe::ACCESS_DUPLEX)
|
107
|
+
assert_not_nil(Pipe::ACCESS_INBOUND)
|
108
|
+
assert_not_nil(Pipe::ACCESS_OUTBOUND)
|
109
|
+
assert_not_nil(Pipe::FIRST_PIPE_INSTANCE)
|
110
|
+
assert_not_nil(Pipe::WRITE_THROUGH)
|
111
|
+
assert_not_nil(Pipe::OVERLAPPED)
|
112
|
+
end
|
113
|
+
|
114
|
+
def test_other_constants
|
115
|
+
assert_not_nil(Pipe::INFINITE)
|
116
|
+
end
|
117
|
+
|
118
|
+
def teardown
|
119
|
+
@pipe.close
|
120
|
+
@pipe = nil
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
##########################################################################
|
2
|
+
# tc_pipe_client.rb
|
3
|
+
#
|
4
|
+
# Test suite for the Pipe::Client class. This test suite should be run
|
5
|
+
# as part of the 'rake test' task.
|
6
|
+
##########################################################################
|
7
|
+
require 'test/unit'
|
8
|
+
require 'win32/pipe'
|
9
|
+
include Win32
|
10
|
+
|
11
|
+
class TC_Win32_Pipe_Client < Test::Unit::TestCase
|
12
|
+
def setup
|
13
|
+
@pipe = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_constructor_basic
|
17
|
+
assert_respond_to(Pipe::Client, :new)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_constructor_expected_errors
|
21
|
+
assert_raise(ArgumentError){ Pipe::Client.new }
|
22
|
+
assert_raise(TypeError){ Pipe::Client.new(1) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def teardown
|
26
|
+
@pipe = nil
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
##########################################################################
|
2
|
+
# tc_pipe_server.rb
|
3
|
+
#
|
4
|
+
# Test suite for the Pipe::Server class. This test suite should be run
|
5
|
+
# as part of the 'rake test' task.
|
6
|
+
##########################################################################
|
7
|
+
require 'test/unit'
|
8
|
+
require 'win32/pipe'
|
9
|
+
include Win32
|
10
|
+
|
11
|
+
class TC_Win32_Pipe_Server < Test::Unit::TestCase
|
12
|
+
def setup
|
13
|
+
@pipe = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_constructor_basic
|
17
|
+
assert_respond_to(Pipe::Server, :new)
|
18
|
+
assert_nothing_raised{ Pipe::Server.new('foo') }
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_connect
|
22
|
+
assert_nothing_raised{ @pipe = Pipe::Server.new('foo') }
|
23
|
+
assert_respond_to(@pipe, :connect)
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_constructor_expected_errors
|
27
|
+
assert_raise(ArgumentError){ Pipe::Server.new }
|
28
|
+
assert_raise(TypeError){ Pipe::Server.new(1) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def teardown
|
32
|
+
@pipe = nil
|
33
|
+
end
|
34
|
+
end
|
data/win32-pipe.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
|
3
|
+
spec = Gem::Specification.new do |gem|
|
4
|
+
gem.name = "win32-pipe"
|
5
|
+
gem.version = "0.2.0"
|
6
|
+
gem.author = "Daniel J. Berger"
|
7
|
+
gem.email = "djberg96@gmail.com"
|
8
|
+
gem.homepage = "http://www.rubyforge.org/projects/win32utils"
|
9
|
+
gem.platform = Gem::Platform::RUBY
|
10
|
+
gem.summary = "An interface for named pipes on MS Windows"
|
11
|
+
gem.description = "An interface for named pipes on MS Windows"
|
12
|
+
gem.test_files = Dir["test/tc_*.rb"]
|
13
|
+
gem.has_rdoc = true
|
14
|
+
gem.extra_rdoc_files = ['CHANGES', 'README', 'MANIFEST']
|
15
|
+
gem.rubyforge_project = "win32utils"
|
16
|
+
|
17
|
+
files = Dir["doc/*"] + Dir["examples/*"] + Dir["lib/win32/**/*.rb"]
|
18
|
+
files += Dir["test/*"] + Dir["[A-Z]*"]
|
19
|
+
files.delete_if{ |item| item.include?("CVS") }
|
20
|
+
gem.files = files
|
21
|
+
end
|
22
|
+
|
23
|
+
if $0 == __FILE__
|
24
|
+
Gem.manage_gems
|
25
|
+
Gem::Builder.new(spec).build
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.4
|
3
|
+
specification_version: 1
|
4
|
+
name: win32-pipe
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.2.0
|
7
|
+
date: 2008-05-28 00:00:00 -06:00
|
8
|
+
summary: An interface for named pipes on MS Windows
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: djberg96@gmail.com
|
12
|
+
homepage: http://www.rubyforge.org/projects/win32utils
|
13
|
+
rubyforge_project: win32utils
|
14
|
+
description: An interface for named pipes on MS Windows
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Daniel J. Berger
|
31
|
+
files:
|
32
|
+
- examples/test_client.rb
|
33
|
+
- examples/test_client_async.rb
|
34
|
+
- examples/test_server.rb
|
35
|
+
- examples/test_server_async.rb
|
36
|
+
- lib/win32/pipe/client.rb
|
37
|
+
- lib/win32/pipe/server.rb
|
38
|
+
- lib/win32/pipe.rb
|
39
|
+
- test/tc_pipe.rb
|
40
|
+
- test/tc_pipe_client.rb
|
41
|
+
- test/tc_pipe_server.rb
|
42
|
+
- CHANGES
|
43
|
+
- examples
|
44
|
+
- ext
|
45
|
+
- lib
|
46
|
+
- MANIFEST
|
47
|
+
- Rakefile
|
48
|
+
- README
|
49
|
+
- test
|
50
|
+
- win32-pipe.gemspec
|
51
|
+
test_files:
|
52
|
+
- test/tc_pipe.rb
|
53
|
+
- test/tc_pipe_client.rb
|
54
|
+
- test/tc_pipe_server.rb
|
55
|
+
rdoc_options: []
|
56
|
+
|
57
|
+
extra_rdoc_files:
|
58
|
+
- CHANGES
|
59
|
+
- README
|
60
|
+
- MANIFEST
|
61
|
+
executables: []
|
62
|
+
|
63
|
+
extensions: []
|
64
|
+
|
65
|
+
requirements: []
|
66
|
+
|
67
|
+
dependencies: []
|
68
|
+
|