From 63809a7258d2b14e0abd82859b90db02649061bf Mon Sep 17 00:00:00 2001 From: Nick Gauthier Date: Tue, 26 Jan 2010 13:50:44 -0500 Subject: [PATCH] added pipe class. docced and tested --- Rakefile | 2 +- lib/hydra.rb | 1 + lib/hydra/pipe.rb | 109 +++++++++++++++++++++++++++++++++++++++++++++ test/test_hydra.rb | 7 --- test/test_pipe.rb | 38 ++++++++++++++++ 5 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 lib/hydra/pipe.rb delete mode 100644 test/test_hydra.rb create mode 100644 test/test_pipe.rb diff --git a/Rakefile b/Rakefile index dc0faeb..af857fa 100644 --- a/Rakefile +++ b/Rakefile @@ -10,7 +10,7 @@ begin gem.email = "nick@smartlogicsolutions.com" gem.homepage = "http://github.com/ngauthier/hydra" gem.authors = ["Nick Gauthier"] - gem.add_development_dependency "thoughtbot-shoulda", ">= 0" + gem.add_development_dependency "shoulda", "= 2.10.3" # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings end Jeweler::GemcutterTasks.new diff --git a/lib/hydra.rb b/lib/hydra.rb index e69de29..7f4ad76 100644 --- a/lib/hydra.rb +++ b/lib/hydra.rb @@ -0,0 +1 @@ +require 'hydra/pipe' diff --git a/lib/hydra/pipe.rb b/lib/hydra/pipe.rb new file mode 100644 index 0000000..f372894 --- /dev/null +++ b/lib/hydra/pipe.rb @@ -0,0 +1,109 @@ +module Hydra #:nodoc: + # Read and write between two processes via pipes. For example: + # @pipe = Hydra::Pipe.new + # Process.fork do + # @pipe.identify_as_child + # sleep(1) + # puts "A message from my parent:\n#{@pipe.gets}" + # @pipe.close + # end + # @pipe.identify_as_parent + # @pipe.write "Hello, Child!" + # @pipe.close + # When the process forks, the pipe is copied. When a pipe is + # identified as a parent or child, it is choosing which ends + # of the pipe to use. + # + # A pipe is actually two pipes: + # + # Parent === Pipe 1 ==> Child + # Parent <== Pipe 2 === Child + class Pipe + # Creates a new uninitialized pipe pair. + def initialize + @child_read, @parent_write = IO.pipe + @parent_read, @child_write = IO.pipe + [@parent_write, @child_write].each{|io| io.sync = true} + end + + # Read a line from a pipe. It will have a trailing newline. + def gets + force_identification + @reader.gets + end + + # Write a line to a pipe. It must have a trailing newline. + def write(str) + force_identification + begin + @writer.write(str) + return str + rescue Errno::EPIPE + raise Hydra::PipeError::Broken + end + end + + # Returns true if there is nothing to read (right now). However it is + # not exactly eof, if the other side writes, this will return false. + # + # It's a good way to tell if there is anything to process right now, + # otherwise, you can sleep. + def eof? + @reader.eof? + end + + # Identify this side of the pipe as the child. + def identify_as_child + @parent_write.close + @parent_read.close + @reader = @child_read + @writer = @child_write + end + + # Identify this side of the pipe as the parent + def identify_as_parent + @child_write.close + @child_read.close + @reader = @parent_read + @writer = @parent_write + end + + # closes the pipes. Once a pipe is closed on one end, the other + # end will get a PipeError::Broken if it tries to write. + def close + done_reading + done_writing + end + + private + def done_writing #:nodoc: + @writer.close unless @writer.closed? + end + + def done_reading #:nodoc: + @reader.close unless @reader.closed? + end + + def force_identification #:nodoc: + raise PipeError::Unidentified if @reader.nil? or @writer.nil? + end + end + + module PipeError #:nodoc: + # Raised if you try to read or write to a pipe when it is unidentified. + # Use identify_as_parent and identify_as_child to identify a pipe. + class Unidentified < RuntimeError + def message #:nodoc: + "Must identify as child or parent" + end + end + # Raised when a pipe has been broken between two processes. + # This happens when a process exits, and is a signal that + # there is no more data to communicate. + class Broken < RuntimeError + def message #:nodoc: + "Other side closed the connection" + end + end + end +end diff --git a/test/test_hydra.rb b/test/test_hydra.rb deleted file mode 100644 index 0bff8b9..0000000 --- a/test/test_hydra.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'helper' - -class TestHydra < Test::Unit::TestCase - should "probably rename this file and start testing for real" do - flunk "hey buddy, you should probably rename this file and start testing for real" - end -end diff --git a/test/test_pipe.rb b/test/test_pipe.rb new file mode 100644 index 0000000..52932e9 --- /dev/null +++ b/test/test_pipe.rb @@ -0,0 +1,38 @@ +require 'helper' + +class TestPipeDream < Test::Unit::TestCase + context "a pipe" do + setup do + @pipe = Hydra::Pipe.new + end + should "be able to write messages" do + Process.fork do + @pipe.identify_as_child + assert_equal "Test Message\n", @pipe.gets + @pipe.write "Message Received\n" + @pipe.write "Second Message\n" + @pipe.close + end + @pipe.identify_as_parent + @pipe.write "Test Message\n" + assert_equal "Message Received\n", @pipe.gets + assert !@pipe.eof? + assert_equal "Second Message\n", @pipe.gets + assert @pipe.eof? + assert_raise Hydra::PipeError::Broken do + @pipe.write "anybody home?" + end + @pipe.close + end + should "not allow writing if unidentified" do + assert_raise Hydra::PipeError::Unidentified do + @pipe.write "hey\n" + end + end + should "not allow reading if unidentified" do + assert_raise Hydra::PipeError::Unidentified do + @pipe.gets + end + end + end +end