From d01c540fddf40b63d50d2ab2fdac7c2e514602ba Mon Sep 17 00:00:00 2001 From: Ben Langfeld Date: Wed, 27 Jun 2012 20:18:39 +0100 Subject: [PATCH] Strip back to bare codebase * Retain minimal Stream --- Gemfile | 2 - Rakefile | 11 +- cucumber.yml | 2 - features/lexer.feature | 260 ----------------- features/step_definitions/lexer_steps.rb | 207 -------------- features/support/ami_fixtures.yml | 30 -- features/support/env.rb | 16 -- features/support/introspective_lexer.rb | 22 -- features/support/lexer_helper.rb | 97 ------- lib/ruby_fs.rb | 8 - lib/ruby_fs/action.rb | 147 ---------- lib/ruby_fs/client.rb | 202 ------------- lib/ruby_fs/error.rb | 25 -- lib/ruby_fs/event.rb | 16 -- lib/ruby_fs/lexer.rl.rb | 303 -------------------- lib/ruby_fs/lexer_machine.rl | 87 ------ lib/ruby_fs/metaprogramming.rb | 17 -- lib/ruby_fs/response.rb | 57 ---- lib/ruby_fs/stream.rb | 26 +- ruby_ami.gemspec | 2 - spec/ruby_fs/action_spec.rb | 187 ------------ spec/ruby_fs/client_spec.rb | 348 ----------------------- spec/ruby_fs/error_spec.rb | 7 - spec/ruby_fs/event_spec.rb | 73 ----- spec/ruby_fs/response_spec.rb | 51 ---- spec/ruby_fs/stream_spec.rb | 66 +---- 26 files changed, 16 insertions(+), 2253 deletions(-) delete mode 100644 cucumber.yml delete mode 100644 features/lexer.feature delete mode 100644 features/step_definitions/lexer_steps.rb delete mode 100644 features/support/ami_fixtures.yml delete mode 100644 features/support/env.rb delete mode 100644 features/support/introspective_lexer.rb delete mode 100644 features/support/lexer_helper.rb delete mode 100644 lib/ruby_fs/action.rb delete mode 100644 lib/ruby_fs/client.rb delete mode 100644 lib/ruby_fs/error.rb delete mode 100644 lib/ruby_fs/event.rb delete mode 100644 lib/ruby_fs/lexer.rl.rb delete mode 100644 lib/ruby_fs/lexer_machine.rl delete mode 100644 lib/ruby_fs/metaprogramming.rb delete mode 100644 lib/ruby_fs/response.rb delete mode 100644 spec/ruby_fs/action_spec.rb delete mode 100644 spec/ruby_fs/client_spec.rb delete mode 100644 spec/ruby_fs/error_spec.rb delete mode 100644 spec/ruby_fs/event_spec.rb delete mode 100644 spec/ruby_fs/response_spec.rb diff --git a/Gemfile b/Gemfile index 3074340..c80ee36 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,3 @@ source "http://rubygems.org" gemspec - -gem 'celluloid-io', :git => 'https://github.com/celluloid/celluloid-io.git' diff --git a/Rakefile b/Rakefile index ac36284..8c32cfa 100644 --- a/Rakefile +++ b/Rakefile @@ -8,19 +8,12 @@ RSpec::Core::RakeTask.new(:spec) do |spec| spec.rspec_opts = '--color' end -require 'cucumber' -require 'cucumber/rake/task' -require 'ci/reporter/rake/cucumber' -Cucumber::Rake::Task.new(:features) do |t| - t.cucumber_opts = %w{--tags ~@jruby} unless defined?(JRUBY_VERSION) -end - Cucumber::Rake::Task.new(:wip) do |t| t.cucumber_opts = %w{-p wip} end -task :default => [:ragel, :spec, :features] -task :ci => [:ragel, 'ci:setup:rspec', :spec, 'ci:setup:cucumber', :features] +task :default => [:ragel, :spec] +task :ci => [:ragel, 'ci:setup:rspec', :spec] require 'yard' YARD::Rake::YardocTask.new diff --git a/cucumber.yml b/cucumber.yml deleted file mode 100644 index 6455645..0000000 --- a/cucumber.yml +++ /dev/null @@ -1,2 +0,0 @@ -default: --tags ~@wip -wip: --wip --tags @wip diff --git a/features/lexer.feature b/features/lexer.feature deleted file mode 100644 index c63f37f..0000000 --- a/features/lexer.feature +++ /dev/null @@ -1,260 +0,0 @@ -Feature: Lexing AMI - As a RubyFS user - I want to lex the AMI protocol - So that I can control Asterisk asynchronously - - Scenario: Lexing only the initial AMI version header - Given a new lexer - And a version header for AMI 1.0 - - When the buffer is lexed - - Then the protocol should have lexed without syntax errors - And the version should be set to 1.0 - - Scenario: Lexing the initial AMI header and a login attempt - Given a new lexer - And a version header for AMI 1.0 - And a normal login success with events - - When the buffer is lexed - - Then the protocol should have lexed without syntax errors - And 1 message should have been received - - Scenario: Lexing the initial AMI header and then a Response:Follows section - Given a new lexer - And a version header for AMI 1.0 - And a multi-line Response:Follows body of ragel_description - - When the buffer is lexed - - Then the protocol should have lexed without syntax errors - And the 'follows' body of 1 message received should equal ragel_description - - Scenario: Lexing a Response:Follows section with no body - Given a new lexer - And a version header for AMI 1.0 - And a multi-line Response:Follows body of empty_String - - When the buffer is lexed - - Then the protocol should have lexed without syntax errors - And the 'follows' body of 1 message received should equal empty_string - - Scenario: Lexing a multi-line Response:Follows simulating the "core show channels" command - Given a new lexer - And a version header for AMI 1.0 - Given a multi-line Response:Follows body of show_channels_from_wayne - - When the buffer is lexed - - Then the protocol should have lexed without syntax errors - And the 'follows' body of 1 message received should equal show_channels_from_wayne - - Scenario: Lexing a multi-line Response:Follows simulating the "core show uptime" command - Given a new lexer - And a version header for AMI 1.0 - Given a multi-line Response:Follows response simulating uptime - - When the buffer is lexed - - Then the protocol should have lexed without syntax errors - And the first message received should have a key "System uptime" with value "46 minutes, 30 seconds" - - Scenario: Lexing a Response:Follows section which has a colon not on the first line - Given a new lexer - And a multi-line Response:Follows body of with_colon_after_first_line - - When the buffer is lexed - - Then the protocol should have lexed without syntax errors - And 1 message should have been received - And the 'follows' body of 1 message received should equal with_colon_after_first_line - - @wip - Scenario: Lexing an immediate response with a colon in it. - Given a new lexer - And an immediate response with text "markq has 0 calls (max unlimited) in 'ringall' strategy (0s holdtime), W:0, C:0, A:0, SL:0.0% within 0s\r\n No Members\r\n No Callers\r\n\r\n\r\n\r\n" - - When the buffer is lexed - - Then the protocol should have lexed without syntax errors - And 1 message should have been received - And 1 message should be an immediate response with text "markq has 0 calls (max unlimited) in 'ringall' strategy (0s holdtime), W:0, C:0, A:0, SL:0.0% within 0s\r\n No Members\r\n No Callers" - - Scenario: Lexing the initial AMI header and then an "Authentication Required" error. - Given a new lexer - And a version header for AMI 1.0 - And an Authentication Required error - - When the buffer is lexed - - Then the protocol should have lexed without syntax errors - - Scenario: Lexing the initial AMI header and then a Response:Follows section - Given a new lexer - And a version header for AMI 1.0 - And a multi-line Response:Follows body of ragel_description - And a multi-line Response:Follows body of ragel_description - - When the buffer is lexed - - Then the protocol should have lexed without syntax errors - And the 'follows' body of 2 messages received should equal ragel_description - - Scenario: Lexing a stanza without receiving an AMI header - Given a new lexer - And a normal login success with events - - When the buffer is lexed - - Then the protocol should have lexed without syntax errors - And 1 message should have been received - - Scenario: Receiving an immediate response as soon as the socket is opened - Given a new lexer - And an immediate response with text "Immediate responses are so ridiculous" - - When the buffer is lexed - - Then the protocol should have lexed without syntax errors - And 1 message should have been received - And 1 message should be an immediate response with text "Immediate responses are so ridiculous" - - Scenario: Receiving an immediate message surrounded by real messages - Given a new lexer - And a normal login success with events - And an immediate response with text "No queues have been created." - And a normal login success with events - - When the buffer is lexed - - Then the protocol should have lexed without syntax errors - And 3 messages should have been received - And 1 message should be an immediate response with text "No queues have been created." - - Scenario: Receiving a Pong after a simulated login - Given a new lexer - And a version header for AMI 1.0 - And a normal login success with events - And a Pong response with an ActionID of randomness - - When the buffer is lexed - - Then the protocol should have lexed without syntax errors - And 2 messages should have been received - - Scenario: Ten Pong responses in a row - Given a new lexer - And 5 Pong responses without an ActionID - And 5 Pong responses with an ActionID of randomness - - When the buffer is lexed - - Then the protocol should have lexed without syntax errors - And 10 messages should have been received - - Scenario: A Pong with an ActionID - Given a new lexer - And a Pong response with an ActionID of 1224469850.61673 - - When the buffer is lexed - - Then the first message received should have a key "ActionID" with value "1224469850.61673" - - Scenario: A response containing a floating point value - Given a new lexer - And a custom stanza named "call" - And the custom stanza named "call" has key "ActionID" with value "1224469850.61673" - And the custom stanza named "call" has key "Uniqueid" with value "1173223225.10309" - - When the custom stanza named "call" is added to the buffer - And the buffer is lexed - - Then the 1st message received should have a key "Uniqueid" with value "1173223225.10309" - - Scenario: Receiving a message with custom key/value pairs - Given a new lexer - And a custom stanza named "person" - And the custom stanza named "person" has key "ActionID" with value "1224469850.61673" - And the custom stanza named "person" has key "Name" with value "Jay Phillips" - And the custom stanza named "person" has key "Age" with value "21" - And the custom stanza named "person" has key "Location" with value "San Francisco, CA" - And the custom stanza named "person" has key "x-header" with value "" - And the custom stanza named "person" has key "Channel" with value "IAX2/127.0.0.1/4569-9904" - And the custom stanza named "person" has key "I have spaces" with value "i have trailing padding " - - When the custom stanza named "person" is added to the buffer - And the buffer is lexed - - Then the protocol should have lexed without syntax errors - And the first message received should have a key "Name" with value "Jay Phillips" - And the first message received should have a key "ActionID" with value "1224469850.61673" - And the first message received should have a key "Name" with value "Jay Phillips" - And the first message received should have a key "Age" with value "21" - And the first message received should have a key "Location" with value "San Francisco, CA" - And the first message received should have a key "x-header" with value "" - And the first message received should have a key "Channel" with value "IAX2/127.0.0.1/4569-9904" - And the first message received should have a key "I have spaces" with value "i have trailing padding " - - Scenario: Executing a stanza that was partially received - Given a new lexer - And a normal login success with events split into two pieces - - When the buffer is lexed - - Then the protocol should have lexed without syntax errors - And 1 message should have been received - - Scenario: Receiving an AMI error followed by a normal event - Given a new lexer - And an AMI error whose message is "Missing action in request" - And a normal login success with events - - When the buffer is lexed - - Then the protocol should have lexed without syntax errors - And 1 AMI error should have been received - And the 1st AMI error should have the message "Missing action in request" - And 1 message should have been received - - Scenario: Lexing an immediate response - Given a new lexer - And a normal login success with events - And an immediate response with text "Yes, plain English is sent sometimes over AMI." - And a normal login success with events - - When the buffer is lexed - - Then the protocol should have lexed without syntax errors - And 3 messages should have been received - And 1 message should be an immediate response with text "Yes, plain English is sent sometimes over AMI." - - Scenario: Lexing an AMI event - Given a new lexer - And a custom event with name "NewChannelEvent" identified by "this_event" - And a custom header for event identified by "this_event" whose key is "Foo" and value is "Bar" - And a custom header for event identified by "this_event" whose key is "Channel" and value is "IAX2/127.0.0.1:4569-9904" - And a custom header for event identified by "this_event" whose key is "AppData" and value is "agi://localhost" - - When the custom event identified by "this_event" is added to the buffer - And the buffer is lexed - - Then the protocol should have lexed without syntax errors - And 1 event should have been received - And the 1st event should have the name "NewChannelEvent" - And the 1st event should have key "Foo" with value "Bar" - And the 1st event should have key "Channel" with value "IAX2/127.0.0.1:4569-9904" - And the 1st event should have key "AppData" with value "agi://localhost" - - Scenario: Lexing an immediate packet with a colon in it (syntax error) - Given a new lexer - And syntactically invalid immediate_packet_with_colon - And a stanza break - - When the buffer is lexed - - Then 0 messages should have been received - And the protocol should have lexed with 1 syntax error - And the syntax error fixture named immediate_packet_with_colon should have been encountered diff --git a/features/step_definitions/lexer_steps.rb b/features/step_definitions/lexer_steps.rb deleted file mode 100644 index 01f4a75..0000000 --- a/features/step_definitions/lexer_steps.rb +++ /dev/null @@ -1,207 +0,0 @@ -Given "a new lexer" do - @lexer = IntrospectiveManagerStreamLexer.new - @custom_stanzas = {} - @custom_events = {} - - @GivenPong = lambda do |with_or_without, action_id, number| - number = number == "a" ? 1 : number.to_i - data = case with_or_without - when "with" then "Response: Pong\r\nActionID: #{action_id}\r\n\r\n" - when "without" then "Response: Pong\r\n\r\n" - else raise "Do not recognize preposition #{with_or_without.inspect}. Should be either 'with' or 'without'" - end - number.times do - @lexer << data - end - end -end - -Given "a version header for AMI $version" do |version| - @lexer << "Asterisk Call Manager/1.0\r\n" -end - -Given "a normal login success with events" do - @lexer << fixture('login/standard/success') -end - -Given "a normal login success with events split into two pieces" do - stanza = fixture('login/standard/success') - @lexer << stanza[0...3] - @lexer << stanza[3..-1] -end - -Given "a stanza break" do - @lexer << "\r\n\r\n" -end - -Given "a multi-line Response:Follows body of $method_name" do |method_name| - multi_line_response_body = send(:follows_body_text, method_name) - - multi_line_response = format_newlines(<<-RESPONSE + "\r\n") % multi_line_response_body -Response: Follows\r -Privilege: Command\r -ActionID: 123123\r -%s\r ---END COMMAND--\r\n\r - RESPONSE - - @lexer << multi_line_response -end - -Given "a multi-line Response:Follows response simulating uptime" do - uptime_response = "Response: Follows\r -Privilege: Command\r -System uptime: 46 minutes, 30 seconds\r ---END COMMAND--\r\n\r\n" - @lexer << uptime_response -end - -Given "syntactically invalid $name" do |name| - @lexer << send(:syntax_error_data, name) -end - -Given /^(\d+) Pong responses with an ActionID of ([\d\w.]+)$/ do |number, action_id| - @GivenPong.call "with", action_id, number -end - -Given /^a Pong response with an ActionID of ([\d\w.]+)$/ do |action_id| - @GivenPong.call "with", action_id, 1 -end - -Given /^(\d+) Pong responses without an ActionID$/ do |number| - @GivenPong.call "without", Time.now.to_f, number -end - -Given /^a custom stanza named "(\w+)"$/ do |name| - @custom_stanzas[name] = "Response: Success\r\n" -end - -Given 'the custom stanza named "$name" has key "$key" with value "$value"' do |name,key,value| - @custom_stanzas[name] << "#{key}: #{value}\r\n" -end - -Given 'an AMI error whose message is "$message"' do |message| - @lexer << "Response: Error\r\nMessage: #{message}\r\n\r\n" -end - -Given 'an immediate response with text "$text"' do |text| - @lexer << "#{text}\r\n\r\n" -end - -Given 'a custom event with name "$event_name" identified by "$identifier"' do |event_name, identifer| - @custom_events[identifer] = {:Event => event_name } -end - -Given 'a custom header for event identified by "$identifier" whose key is "$key" and value is "$value"' do |identifier, key, value| - @custom_events[identifier][key] = value -end - -Given "an Authentication Required error" do - @lexer << "Response: Error\r\nActionID: BPJeKqW2-SnVg-PyFs-vkXT-7AWVVPD0N3G7\r\nMessage: Authentication Required\r\n\r\n" -end - -Given "a follows packet with a colon in it" do - @lexer << follows_body_text("with_colon") -end - -######################################## -#### WHEN -######################################## - -When 'the custom stanza named "$name" is added to the buffer' do |name| - @lexer << (@custom_stanzas[name] + "\r\n") -end - -When 'the custom event identified by "$identifier" is added to the buffer' do |identifier| - custom_event = @custom_events[identifier].clone - event_name = custom_event.delete :Event - stringified_event = "Event: #{event_name}\r\n" - custom_event.each_pair do |key,value| - stringified_event << "#{key}: #{value}\r\n" - end - stringified_event << "\r\n" - @lexer << stringified_event -end - -When "the buffer is lexed" do - @lexer.resume! -end - -######################################## -#### THEN -######################################## - -Then "the protocol should have lexed without syntax errors" do - current_pointer = @lexer.send(:instance_variable_get, :@current_pointer) - data_ending_pointer = @lexer.send(:instance_variable_get, :@data_ending_pointer) - current_pointer.should == data_ending_pointer - @lexer.syntax_errors.size.should equal(0) -end - -Then /^the protocol should have lexed with (\d+) syntax errors?$/ do |number| - @lexer.syntax_errors.size.should == number.to_i -end - -Then "the syntax error fixture named $name should have been encountered" do |name| - irregularity = send(:syntax_error_data, name) - @lexer.syntax_errors.find { |error| error == irregularity }.should_not be_nil -end - -Then /^(\d+) messages? should have been received$/ do |number_received| - @lexer.received_messages.size.should == number_received.to_i -end - -Then /^the 'follows' body of (\d+) messages? received should equal (\w+)$/ do |number, method_name| - multi_line_response = follows_body_text method_name - @lexer.received_messages.should_not be_empty - @lexer.received_messages.select do |message| - message.text_body == multi_line_response - end.size.should == number.to_i -end - -Then "the version should be set to $version" do |version| - @lexer.ami_version.should eql(version.to_f) -end - -Then /^the ([\w\d]*) message received should have a key "([^\"]*)" with value "([^\"]*)"$/ do |ordered,key,value| - ordered = ordered[/^(\d+)\w+$/, 1].to_i - 1 - @lexer.received_messages[ordered][key].should eql(value) -end - -Then "$number AMI error should have been received" do |number| - @lexer.ami_errors.size.should equal(number.to_i) -end - -Then 'the $order AMI error should have the message "$message"' do |order, message| - order = order[/^(\d+)\w+$/, 1].to_i - 1 - @lexer.ami_errors[order].should be_kind_of(RubyFS::Error) - @lexer.ami_errors[order].message.should eql(message) -end - -Then '$number message should be an immediate response with text "$text"' do |number, text| - matching_immediate_responses = @lexer.received_messages.select do |response| - response.kind_of?(RubyFS::Response) && response.text_body == text - end - matching_immediate_responses.size.should equal(number.to_i) - matching_immediate_responses.first["ActionID"].should eql(nil) -end - -Then 'the $order event should have the name "$name"' do |order, name| - order = order[/^(\d+)\w+$/, 1].to_i - 1 - @lexer.received_messages.select do |response| - response.kind_of?(RubyFS::Event) - end[order].name.should eql(name) -end - -Then '$number event should have been received' do |number| - @lexer.received_messages.select do |response| - response.kind_of?(RubyFS::Event) - end.size.should equal(number.to_i) -end - -Then 'the $order event should have key "$key" with value "$value"' do |order, key, value| - order = order[/^(\d+)\w+$/, 1].to_i - 1 - @lexer.received_messages.select do |response| - response.kind_of?(RubyFS::Event) - end[order][key].should eql(value) -end diff --git a/features/support/ami_fixtures.yml b/features/support/ami_fixtures.yml deleted file mode 100644 index b6f6a36..0000000 --- a/features/support/ami_fixtures.yml +++ /dev/null @@ -1,30 +0,0 @@ -:login: - :standard: - :client: - Action: Login - Username: :string - Secret: :string - Events: {one_of: ["on", "off"]} - :success: - Response: Success - Message: Authentication accepted - :fail: - Response: Error - Message: Authentication failed - -:errors: - :missing_action: - Response: Error - Message: Missing action in request - -:pong: - :with_action_id: - ActionID: 1287381.1238 - Response: Pong - :without_action_id: - Response: Pong - :with_extra_keys: - ActionID: 1287381.1238 - Response: Pong - Blah: This is something arbitrary - Blahhh: something else arbitrary \ No newline at end of file diff --git a/features/support/env.rb b/features/support/env.rb deleted file mode 100644 index e961187..0000000 --- a/features/support/env.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'simplecov' -require 'simplecov-rcov' -class SimpleCov::Formatter::MergedFormatter - def format(result) - SimpleCov::Formatter::HTMLFormatter.new.format(result) - SimpleCov::Formatter::RcovFormatter.new.format(result) - end -end -SimpleCov.formatter = SimpleCov::Formatter::MergedFormatter -SimpleCov.start do - add_filter "/vendor/" -end - -require 'cucumber' -require 'rspec' -require 'ruby_fs' diff --git a/features/support/introspective_lexer.rb b/features/support/introspective_lexer.rb deleted file mode 100644 index cf8895a..0000000 --- a/features/support/introspective_lexer.rb +++ /dev/null @@ -1,22 +0,0 @@ -class IntrospectiveManagerStreamLexer < RubyFS::Lexer - attr_reader :received_messages, :syntax_errors, :ami_errors - - def initialize(*args) - super - @received_messages = [] - @syntax_errors = [] - @ami_errors = [] - end - - def message_received(message = @current_message) - @received_messages << message - end - - def error_received(error_message) - @ami_errors << error_message - end - - def syntax_error_encountered(ignored_chunk) - @syntax_errors << ignored_chunk - end -end diff --git a/features/support/lexer_helper.rb b/features/support/lexer_helper.rb deleted file mode 100644 index b82cbfe..0000000 --- a/features/support/lexer_helper.rb +++ /dev/null @@ -1,97 +0,0 @@ -FIXTURES = YAML.load_file File.dirname(__FILE__) + "/ami_fixtures.yml" - -def fixture(path, overrides = {}) - path_segments = path.split '/' - selected_event = path_segments.inject(FIXTURES.clone) do |hash, segment| - raise ArgumentError, path + " not found!" unless hash - hash[segment.to_sym] - end - - # Downcase all keys in the event and the overrides - selected_event = selected_event.inject({}) do |downcased_hash,(key,value)| - downcased_hash[key.to_s.downcase] = value - downcased_hash - end - - overrides = overrides.inject({}) do |downcased_hash,(key,value)| - downcased_hash[key.to_s.downcase] = value - downcased_hash - end - - # Replace variables in the selected_event with any overrides, ignoring case of the key - keys_with_variables = selected_event.select { |(key, value)| value.kind_of?(Symbol) || value.kind_of?(Hash) } - - keys_with_variables.each do |original_key, variable_type| - # Does an override an exist in the supplied list? - if overriden_pair = overrides.find { |(key, value)| key == original_key } - # We have an override! Let's replace the template value in the event with the overriden value - selected_event[original_key] = overriden_pair.last - else - # Based on the type, let's generate a placeholder. - selected_event[original_key] = case variable_type - when :string - rand(100000).to_s - when Hash - if variable_type.has_key? "one_of" - # Choose a random possibility - possibilities = variable_type['one_of'] - possibilities[rand(possibilities.size)] - else - raise "Unrecognized Hash fixture property! ##{variable_type.keys.to_sentence}" - end - else - raise "Unrecognized fixture variable type #{variable_type}!" - end - end - end - - hash_to_stanza(selected_event).tap do |event| - selected_event.each_pair do |key, value| - event.meta_def(key) { value } - end - end -end - -def hash_to_stanza(hash) - ordered_hash = hash.to_a - starter = hash.find { |(key, value)| key.strip =~ /^(Response|Action)$/i } - ordered_hash.unshift ordered_hash.delete(starter) if starter - ordered_hash.inject(String.new) do |stanza,(key, value)| - stanza + "#{key}: #{value}\r\n" - end + "\r\n" -end - -def format_newlines(string) - # HOLY FUCK THIS IS UGLY - tmp_replacement = random_string - string.gsub("\r\n", tmp_replacement). - gsub("\n", "\r\n"). - gsub(tmp_replacement, "\r\n") -end - -def random_string - (rand(1_000_000_000_000) + 1_000_000_000).to_s -end - -def follows_body_text(name) - case name - when "ragel_description" - "Ragel is a software development tool that allows user actions to - be embedded into the transitions of a regular expression's corresponding state machine, - eliminating the need to switch from the regular expression engine and user code execution - environment and back again." - when "with_colon_after_first_line" - "Host Username Refresh State Reg.Time \r\nlax.teliax.net:5060 jicksta 105 Registered Tue, 11 Nov 2008 02:29:55" - when "show_channels_from_wayne" - "Channel Location State Application(Data)\r\n0 active channels\r\n0 active calls" - when "empty_string" - "" - end -end - -def syntax_error_data(name) - case name - when "immediate_packet_with_colon" - "!IJ@MHY:!&@B*!B @ ! @^! @ !@ !\r!@ ! @ !@ ! !!m, \n\\n\n" - end -end diff --git a/lib/ruby_fs.rb b/lib/ruby_fs.rb index 1cf64d1..9958c8d 100644 --- a/lib/ruby_fs.rb +++ b/lib/ruby_fs.rb @@ -2,7 +2,6 @@ uuidtools future-resource logger - girl_friday countdownlatch celluloid/io }.each { |f| require f } @@ -15,13 +14,6 @@ module RubyFS end %w{ - action - client - error - event - lexer - metaprogramming - response stream version }.each { |f| require "ruby_fs/#{f}" } diff --git a/lib/ruby_fs/action.rb b/lib/ruby_fs/action.rb deleted file mode 100644 index f520aa5..0000000 --- a/lib/ruby_fs/action.rb +++ /dev/null @@ -1,147 +0,0 @@ -module RubyFS - class Action - attr_reader :name, :headers, :action_id - - attr_accessor :state - - CAUSAL_EVENT_NAMES = %w[queuestatus sippeers iaxpeers parkedcalls dahdishowchannels coreshowchannels - dbget status agents konferencelist] unless defined? CAUSAL_EVENT_NAMES - - def initialize(name, headers = {}, &block) - @name = name.to_s.downcase.freeze - @headers = headers.freeze - @action_id = UUIDTools::UUID.random_create.to_s - @response = FutureResource.new - @response_callback = block - @state = :new - @events = [] - @event_lock = Mutex.new - end - - [:new, :sent, :complete].each do |state| - define_method("#{state}?") { @state == state } - end - - def replies_with_action_id? - !UnsupportedActionName::UNSUPPORTED_ACTION_NAMES.include? name - end - - ## - # When sending an action with "causal events" (i.e. events which must be collected to form a proper - # response), AMI should send a particular event which instructs us that no more events will be sent. - # This event is called the "causal event terminator". - # - # Note: you must supply both the name of the event and any headers because it's possible that some uses of an - # action (i.e. same name, different headers) have causal events while other uses don't. - # - # @param [String] name the name of the event - # @param [Hash] the headers associated with this event - # @return [String] the downcase()'d name of the event name for which to wait - # - def has_causal_events? - CAUSAL_EVENT_NAMES.include? name - end - - ## - # Used to determine the event name for an action which has causal events. - # - # @param [String] action_name - # @return [String] The corresponding event name which signals the completion of the causal event sequence. - # - def causal_event_terminator_name - return unless has_causal_events? - case name - when "sippeers", "iaxpeers" - "peerlistcomplete" - when "dbget" - "dbgetresponse" - when "konferencelist" - "conferencelistcomplete" - else - name + "complete" - end - end - - ## - # Converts this action into a protocol-valid String, ready to be sent over a socket. - # - def to_s - @textual_representation ||= ( - "Action: #{@name}\r\nActionID: #{@action_id}\r\n" + - @headers.map { |(key,value)| "#{key}: #{value}" }.join("\r\n") + - (@headers.any? ? "\r\n\r\n" : "\r\n") - ) - end - - # - # If the response has simply not been received yet from Asterisk, the calling Thread will block until it comes - # in. Once the response comes in, subsequent calls immediately return a reference to the ManagerInterfaceResponse - # object. - # - def response(timeout = nil) - @response.resource(timeout).tap do |resp| - raise resp if resp.is_a? Exception - end - end - - def response=(other) - @state = :complete - @response.resource = other - @response_callback.call other if @response_callback - end - - def <<(message) - case message - when Error - self.response = message - when Event - raise StandardError, 'This action should not trigger events. Maybe it is now a causal action? This is most likely a bug in RubyFS' unless has_causal_events? - @event_lock.synchronize do - @events << message - end - self.response = @pending_response if message.name.downcase == causal_event_terminator_name - when Response - if has_causal_events? - @pending_response = message - else - self.response = message - end - end - end - - def events - @event_lock.synchronize do - @events.dup - end - end - - def eql?(other) - to_s == other.to_s - end - alias :== :eql? - - def sync_timeout - name.downcase == 'originate' && !headers[:async] ? 60 : 10 - end - - ## - # This class will be removed once this AMI library fully supports all known protocol anomalies. - # - class UnsupportedActionName < ArgumentError - UNSUPPORTED_ACTION_NAMES = %w[queues] unless defined? UNSUPPORTED_ACTION_NAMES - - # Blacklist some actions depends on the Asterisk version - def self.preinitialize(version) - if version < 1.8 - %w[iaxpeers muteaudio mixmonitormute aocmessage].each do |action| - UNSUPPORTED_ACTION_NAMES << action - end - end - end - - def initialize(name) - super "At the moment this AMI library doesn't support the #{name.inspect} action because it causes a protocol anomaly. Support for it will be coming shortly." - end - end - end -end # RubyFS diff --git a/lib/ruby_fs/client.rb b/lib/ruby_fs/client.rb deleted file mode 100644 index 41b3d3f..0000000 --- a/lib/ruby_fs/client.rb +++ /dev/null @@ -1,202 +0,0 @@ -module RubyFS - class Client - attr_reader :options, :action_queue, :events_stream, :actions_stream - - def initialize(options) - @options = options - @logger = options[:logger] - @logger.level = options[:log_level] || Logger::DEBUG if @logger - @event_handler = @options[:event_handler] - @state = :stopped - - stop_writing_actions - - @pending_actions = {} - @sent_actions = {} - @actions_lock = Mutex.new - - @action_queue = GirlFriday::WorkQueue.new(:actions, :size => 1, :error_handler => ErrorHandler) do |action| - @actions_write_blocker.wait - _send_action action - begin - action.response action.sync_timeout - rescue Timeout::Error => e - logger.error "Timed out waiting for a response to #{action}" - rescue RubyFS::Error - nil - end - end - - @message_processor = GirlFriday::WorkQueue.new(:messages, :size => 1, :error_handler => ErrorHandler) do |message| - handle_message message - end - - @event_processor = GirlFriday::WorkQueue.new(:events, :size => 2, :error_handler => ErrorHandler) do |event| - handle_event event - end - end - - [:started, :stopped, :ready].each do |state| - define_method("#{state}?") { @state == state } - end - - def start - @events_stream = start_stream lambda { |event| @event_processor << event } - @actions_stream = start_stream lambda { |message| @message_processor << message } - @state = :started - streams.each(&:join) - end - - def stop - streams.each do |stream| - begin - stream.terminate - rescue => e - logger.error e if logger - end - end - end - - def send_action(action, headers = {}, &block) - (action.is_a?(Action) ? action : Action.new(action, headers, &block)).tap do |action| - logger.trace "[QUEUE]: #{action.inspect}" if logger - register_pending_action action - action_queue << action - end - end - - def handle_message(message) - logger.trace "[RECV-ACTIONS]: #{message.inspect}" if logger - case message - when Stream::Connected - login_actions - when Stream::Disconnected - stop_writing_actions - stop - when Event - action = @current_action_with_causal_events - if action - message.action = action - action << message - @current_action_with_causal_events = nil if action.complete? - else - if message.name == 'FullyBooted' - pass_event message - start_writing_actions - else - raise StandardError, "Got an unexpected event on actions socket! This AMI command may have a multi-message response. Try making Adhearsion treat it as causal action #{message.inspect}" - end - end - when Response, Error - action = sent_action_with_id message.action_id - raise StandardError, "Received an AMI response with an unrecognized ActionID!! This may be an bug! #{message.inspect}" unless action - message.action = action - - # By this point the write loop will already have started blocking by calling the response() method on the - # action. Because we must collect more events before we wake the write loop up again, let's create these - # instance variable which will needed when the subsequent causal events come in. - @current_action_with_causal_events = action if action.has_causal_events? - - action << message - end - end - - def handle_event(event) - logger.trace "[RECV-EVENTS]: #{event.inspect}" if logger - case event - when Stream::Connected - login_events - when Stream::Disconnected - stop - else - pass_event event - end - end - - def _send_action(action) - logger.trace "[SEND]: #{action.inspect}" if logger - transition_action_to_sent action - actions_stream.send_action action - action.state = :sent - action - end - - private - - def pass_event(event) - @event_handler.call event if @event_handler.respond_to? :call - end - - def register_pending_action(action) - @actions_lock.synchronize do - @pending_actions[action.action_id] = action - end - end - - def transition_action_to_sent(action) - @actions_lock.synchronize do - @pending_actions.delete action.action_id - @sent_actions[action.action_id] = action - end - end - - def sent_action_with_id(action_id) - @actions_lock.synchronize do - @sent_actions.delete action_id - end - end - - def start_writing_actions - @actions_write_blocker.countdown! - end - - def stop_writing_actions - @actions_write_blocker = CountDownLatch.new 1 - end - - def login_actions - action = login_action do |response| - pass_event response if response.is_a? Error - send_action 'Events', 'EventMask' => 'Off' - end - - register_pending_action action - Thread.new { _send_action action } - end - - def login_events - login_action.tap do |action| - events_stream.send_action action - end - end - - def login_action(&block) - Action.new 'Login', - 'Username' => options[:username], - 'Secret' => options[:password], - 'Events' => 'On', - &block - end - - def start_stream(callback) - Stream.new @options[:host], @options[:port], callback - end - - def logger - super - rescue NoMethodError - @logger - end - - def streams - [actions_stream, events_stream].compact - end - - class ErrorHandler - def handle(error) - puts error.message - puts error.backtrace.join("\n") - end - end - end -end diff --git a/lib/ruby_fs/error.rb b/lib/ruby_fs/error.rb deleted file mode 100644 index 3c3277b..0000000 --- a/lib/ruby_fs/error.rb +++ /dev/null @@ -1,25 +0,0 @@ -module RubyFS - class Error < StandardError - attr_accessor :message, :action - - def initialize - @headers = Hash.new - end - - def [](key) - @headers[key] - end - - def []=(key,value) - @headers[key] = value - end - - def action_id - @headers['ActionID'] - end - - def inspect - "#<#{self.class} #{[:message, :headers].map { |c| "#{c}=#{self.__send__(c).inspect rescue nil}" }.compact * ', '}>" - end - end -end # RubyFS \ No newline at end of file diff --git a/lib/ruby_fs/event.rb b/lib/ruby_fs/event.rb deleted file mode 100644 index 88ad739..0000000 --- a/lib/ruby_fs/event.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'ruby_fs/response' - -module RubyFS - class Event < Response - attr_reader :name - - def initialize(name) - super() - @name = name - end - - def inspect_attributes - [:name] + super - end - end -end # RubyFS diff --git a/lib/ruby_fs/lexer.rl.rb b/lib/ruby_fs/lexer.rl.rb deleted file mode 100644 index 009e422..0000000 --- a/lib/ruby_fs/lexer.rl.rb +++ /dev/null @@ -1,303 +0,0 @@ -module RubyFS - class Lexer - - KILOBYTE = 1024 - BUFFER_SIZE = 128 * KILOBYTE unless defined? BUFFER_SIZE - - ## - # IMPORTANT! See method documentation for adjust_pointers! - # - # @see adjust_pointers - # - POINTERS = [ - :@current_pointer, - :@token_start, - :@token_end, - :@version_start, - :@event_name_start, - :@current_key_position, - :@current_value_position, - :@last_seen_value_end, - :@error_reason_start, - :@follows_text_start, - :@current_syntax_error_start, - :@immediate_response_start - ] - - %%{ - machine ami_protocol_parser; - - # All required Ragel actions are implemented as Ruby methods. - - # Executed after a "Response: Success" or "Response: Pong" - action init_success { init_success } - - action init_response_follows { init_response_follows } - - action init_error { init_error } - - action message_received { message_received @current_message } - action error_received { error_received @current_message } - - action version_starts { version_starts } - action version_stops { version_stops } - - action key_starts { key_starts } - action key_stops { key_stops } - - action value_starts { value_starts } - action value_stops { value_stops } - - action error_reason_starts { error_reason_starts } - action error_reason_stops { error_reason_stops } - - action syntax_error_starts { syntax_error_starts } - action syntax_error_stops { syntax_error_stops } - - action immediate_response_starts { immediate_response_starts } - action immediate_response_stops { immediate_response_stops } - - action follows_text_starts { follows_text_starts } - action follows_text_stops { follows_text_stops } - - action event_name_starts { event_name_starts } - action event_name_stops { event_name_stops } - - include ami_protocol_parser_machine "lexer_machine.rl"; - - }%%## - - attr_accessor :ami_version - - def initialize(delegate = nil) - @delegate = delegate - @data = "" - @current_pointer = 0 - @ragel_stack = [] - @ami_version = 0.0 - - %%{ - # All other variables become local, letting Ruby garbage collect them. This - # prevents us from having to manually reset them. - - variable data @data; - variable p @current_pointer; - variable pe @data_ending_pointer; - variable cs @current_state; - variable ts @token_start; - variable te @token_end; - variable act @ragel_act; - variable eof @eof; - variable stack @ragel_stack; - variable top @ragel_stack_top; - - write data; - write init; - }%%## - end - - def <<(new_data) - extend_buffer_with new_data - resume! - end - - def resume! - %%{ write exec; }%%## - end - - def extend_buffer_with(new_data) - length = new_data.size - - if length > BUFFER_SIZE - raise Exception, "ERROR: Buffer overrun! Input size (#{new_data.size}) larger than buffer (#{BUFFER_SIZE})" - end - - if length + @data.size > BUFFER_SIZE - if @data.size != @current_pointer - if @current_pointer < length - # We are about to shift more bytes off the array than we have - # parsed. This will cause the parser to lose state so - # integrity cannot be guaranteed. - raise Exception, "ERROR: Buffer overrun! AMI parser cannot guarantee sanity. New data size: #{new_data.size}; Current pointer at #{@current_pointer}; Data size: #{@data.size}" - end - end - @data.slice! 0...length - adjust_pointers -length - end - @data << new_data - @data_ending_pointer = @data.size - end - - protected - - ## - # This method will adjust all pointers into the buffer according - # to the supplied offset. This is necessary any time the buffer - # changes, for example when the sliding window is incremented forward - # after new data is received. - # - # It is VERY IMPORTANT that when any additional pointers are defined - # that they are added to this method. Unpredictable results may - # otherwise occur! - # - # @see https://adhearsion.lighthouseapp.com/projects/5871-adhearsion/tickets/72-ami-lexer-buffer-offset#ticket-72-26 - # - # @param offset Adjust pointers by offset. May be negative. - # - def adjust_pointers(offset) - POINTERS.each do |ptr| - value = instance_variable_get(ptr) - instance_variable_set(ptr, value + offset) if !value.nil? - end - end - - ## - # Called after a response or event has been successfully parsed. - # - # @param [Response, Event] message The message just received - # - def message_received(message) - @delegate.message_received message - end - - ## - # Called when there is an Error: stanza on the socket. Could be caused by executing an unrecognized command, trying - # to originate into an invalid priority, etc. Note: many errors' responses are actually tightly coupled to a - # Event which comes directly after it. Often the message will say something like "Channel status - # will follow". - # - # @param [String] reason The reason given in the Message: header for the error stanza. - # - def error_received(message) - @delegate.error_received message - end - - ## - # Called when there's a syntax error on the socket. This doesn't happen as often as it should because, in many cases, - # it's impossible to distinguish between a syntax error and an immediate packet. - # - # @param [String] ignored_chunk The offending text which caused the syntax error. - def syntax_error_encountered(ignored_chunk) - @delegate.syntax_error_encountered ignored_chunk - end - - def init_success - @current_message = Response.new - end - - def init_response_follows - @current_message = Response.new - end - - def init_error - @current_message = Error.new - end - - def version_starts - @version_start = @current_pointer - end - - def version_stops - self.ami_version = @data[@version_start...@current_pointer].to_f - @version_start = nil - end - - def event_name_starts - @event_name_start = @current_pointer - end - - def event_name_stops - event_name = @data[@event_name_start...@current_pointer] - @event_name_start = nil - @current_message = Event.new(event_name) - end - - def key_starts - @current_key_position = @current_pointer - end - - def key_stops - @current_key = @data[@current_key_position...@current_pointer] - end - - def value_starts - @current_value_position = @current_pointer - end - - def value_stops - @current_value = @data[@current_value_position...@current_pointer] - @last_seen_value_end = @current_pointer + 2 # 2 for \r\n - add_pair_to_current_message - end - - def error_reason_starts - @error_reason_start = @current_pointer - end - - def error_reason_stops - @current_message.message = @data[@error_reason_start...@current_pointer] - end - - def follows_text_starts - @follows_text_start = @current_pointer - end - - def follows_text_stops - text = @data[@last_seen_value_end..@current_pointer] - text.sub! /\r?\n--END COMMAND--/, "" - @current_message.text_body = text - @follows_text_start = nil - end - - def add_pair_to_current_message - @current_message[@current_key] = @current_value - reset_key_and_value_positions - end - - def reset_key_and_value_positions - @current_key, @current_value, @current_key_position, @current_value_position = nil - end - - def syntax_error_starts - @current_syntax_error_start = @current_pointer # Adding 1 since the pointer is still set to the last successful match - end - - def syntax_error_stops - # Subtracting 3 from @current_pointer below for "\r\n" which separates a stanza - offending_data = @data[@current_syntax_error_start...@current_pointer - 1] - syntax_error_encountered offending_data - @current_syntax_error_start = nil - end - - def immediate_response_starts - @immediate_response_start = @current_pointer - end - - def immediate_response_stops - message = @data[@immediate_response_start...(@current_pointer -1)] - message_received Response.from_immediate_response(message) - end - - ## - # This method is used primarily in debugging. - # - def view_buffer(message = nil) - message ||= "Viewing the buffer" - - buffer = @data.clone - buffer.insert(@current_pointer, "\033[0;31m\033[1;31m^\033[0m") - - buffer.gsub!("\r", "\\\\r") - buffer.gsub!("\n", "\\n\n") - - puts <<-INSPECTION -VVVVVVVVVVVVVVVVVVVVVVVVVVVVV -#### #{message} -############################# -#{buffer} -############################# -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - INSPECTION - end - end -end diff --git a/lib/ruby_fs/lexer_machine.rl b/lib/ruby_fs/lexer_machine.rl deleted file mode 100644 index 75087f0..0000000 --- a/lib/ruby_fs/lexer_machine.rl +++ /dev/null @@ -1,87 +0,0 @@ -%%{ #% - -######### -## This file is written with the Ragel programming language and parses the Asterisk Manager Interface protocol. It depends -## upon Ragel actions which should be implemented in another Ragel-parsed file which includes this file. -## -## Ragel was used because the AMI protocol is extremely non-deterministic and, in the edge cases, requires something both -## very robust and something which can recover from syntax errors. -## -## Note: This file is language agnostic. From this AMI parsers in many other languages can be generated. -######### - -machine ami_protocol_parser_machine; - -cr = "\r"; # A carriage return. Used before (almost) every newline character. -lf = "\n"; # Newline. Used (with cr) to separate key/value pairs and stanzas. -crlf = cr lf; # Means "carriage return and line feed". Used to separate key/value pairs and stanzas -loose_newline = cr? lf; # Used sometimes when the AMI protocol is nondeterministic about the delimiter - -white = [\t ]; # Single whitespace character, either a tab or a space -colon = ":" [ ]**; # Separates keys from values. "A colon followed by any number of spaces" -stanza_break = crlf crlf; # The seperator between two stanzas. -rest_of_line = (any* -- crlf); # Match all characters until the next line seperator. - -Prompt = "Asterisk Call Manager/" digit+ >version_starts "." digit+ %version_stops crlf; - -Key = ((alnum | print) -- (cr | lf | ":"))+; -KeyValuePair = Key >key_starts %key_stops colon rest_of_line >value_starts %value_stops crlf; - -FollowsDelimiter = loose_newline "--END COMMAND--"; - -Response = "Response"i colon; - -Success = Response "Success"i %init_success crlf @{ fgoto success; }; -Pong = Response "Pong"i %init_success crlf @{ fgoto success; }; -Event = "Event"i colon %event_name_starts rest_of_line %event_name_stops crlf @{ fgoto success; }; -Error = Response "Error"i %init_error crlf (("Message"i colon rest_of_line >error_reason_starts crlf >error_reason_stops) | KeyValuePair)+ crlf @error_received; -Follows = Response "Follows"i crlf @init_response_follows @{ fgoto response_follows; }; - -# For "Response: Follows" -FollowsBody = (any* -- FollowsDelimiter) >follows_text_starts FollowsDelimiter @follows_text_stops crlf; - -ImmediateResponse = (any+ -- (loose_newline | ":")) >immediate_response_starts loose_newline @immediate_response_stops @{fret;}; -SyntaxError = (any+ -- crlf) >syntax_error_starts crlf @syntax_error_stops; - -irregularity := |* - ImmediateResponse; # Performs the fret in the ImmediateResponse FSM - SyntaxError => { fret; }; -*|; - -# When a new socket is established, Asterisk will send the version of the protocol per the Prompt machine. Because it's -# tedious for unit tests to always send this, we'll put some intelligence into this parser to support going straight into -# the protocol-parsing machine. It's also conceivable that a variant of AMI would not send this initial information. -main := |* - Prompt => { fgoto protocol; }; - any => { - # If this scanner's look-ahead capability didn't match the prompt, let's ignore the need for a prompt - fhold; - fgoto protocol; - }; -*|; - -protocol := |* - Prompt; - Success; - Pong; - Event; - Error; - Follows crlf; - crlf => { fgoto protocol; }; # If we get a crlf out of place, let's just ignore it. - any => { - # If NONE of the above patterns match, we consider this a syntax error. The irregularity machine can recover gracefully. - fhold; - fcall irregularity; - }; -*|; - -success := KeyValuePair* crlf @message_received @{fgoto protocol;}; - -# For the "Response: Follows" protocol abnormality. What happens if there's a protocol irregularity in this state??? -response_follows := |* - KeyValuePair+; - FollowsBody; - crlf @{ message_received @current_message; fgoto protocol; }; -*|; - -}%% diff --git a/lib/ruby_fs/metaprogramming.rb b/lib/ruby_fs/metaprogramming.rb deleted file mode 100644 index dc65c55..0000000 --- a/lib/ruby_fs/metaprogramming.rb +++ /dev/null @@ -1,17 +0,0 @@ -class Object - def metaclass - class << self - self - end - end - - def meta_eval(&block) - metaclass.instance_eval &block - end - - def meta_def(name, &block) - meta_eval do - define_method name, &block - end - end -end diff --git a/lib/ruby_fs/response.rb b/lib/ruby_fs/response.rb deleted file mode 100644 index bf82ff5..0000000 --- a/lib/ruby_fs/response.rb +++ /dev/null @@ -1,57 +0,0 @@ -module RubyFS - ## - # This is the object containing a response from Asterisk. - # - # Note: not all responses have an ActionID! - # - class Response - class << self - def from_immediate_response(text) - new.tap do |instance| - instance.text_body = text - end - end - end - - attr_accessor :action, - :text_body # For "Response: Follows" sections - attr_reader :events - - def initialize - @headers = Hash.new - end - - def has_text_body? - !!@text_body - end - - def headers - @headers.clone - end - - def [](arg) - @headers[arg.to_s] - end - - def []=(key,value) - @headers[key.to_s] = value - end - - def action_id - @headers['ActionID'] - end - - def inspect - "#<#{self.class} #{inspect_attributes.map { |c| "#{c}=#{self.__send__(c).inspect rescue nil}" }.compact * ', '}>" - end - - def inspect_attributes - [:headers, :text_body, :events, :action] - end - - def eql?(o, *fields) - o.is_a?(self.class) && (fields + inspect_attributes).all? { |f| self.__send__(f) == o.__send__(f) } - end - alias :== :eql? - end -end # RubyFS diff --git a/lib/ruby_fs/stream.rb b/lib/ruby_fs/stream.rb index 255c510..0a4daee 100644 --- a/lib/ruby_fs/stream.rb +++ b/lib/ruby_fs/stream.rb @@ -17,7 +17,6 @@ def initialize(host, port, event_callback) super() @event_callback = event_callback logger.debug "Starting up..." - @lexer = Lexer.new self @socket = TCPSocket.from_ruby_socket ::TCPSocket.new(host, port) post_init run! @@ -36,35 +35,28 @@ def run def post_init @state = :started - @event_callback.call Connected.new + fire_event Connected.new end def send_data(data) - @socket.write data - end - - def send_action(action) - logger.debug "[SEND] #{action.to_s}" - send_data action.to_s + logger.debug "[SEND] #{data.to_s}" + @socket.write data.to_s end def receive_data(data) logger.debug "[RECV] #{data}" - @lexer << data - end - - def message_received(message) - logger.debug "[RECV] #{message.inspect}" - @event_callback.call message + fire_event data end - alias :error_received :message_received - def finalize logger.debug "Finalizing stream" @socket.close if @socket @state = :stopped - @event_callback.call Disconnected.new + fire_event Disconnected.new + end + + def fire_event(event) + @event_callback.call event end def logger diff --git a/ruby_ami.gemspec b/ruby_ami.gemspec index b6c636a..96545f6 100644 --- a/ruby_ami.gemspec +++ b/ruby_ami.gemspec @@ -21,12 +21,10 @@ Gem::Specification.new do |s| s.add_runtime_dependency %q, [">= 0"] s.add_runtime_dependency %q, ["~> 0.11.0"] s.add_runtime_dependency %q, [">= 0"] - s.add_runtime_dependency %q, [">= 0"] s.add_runtime_dependency %q, ["~> 1.0"] s.add_development_dependency %q, ["~> 1.0"] s.add_development_dependency %q, [">= 2.5.0"] - s.add_development_dependency %q, [">= 0"] s.add_development_dependency %q, [">= 1.6.3"] s.add_development_dependency %q, ["~> 0.6.0"] s.add_development_dependency %q, [">= 0"] diff --git a/spec/ruby_fs/action_spec.rb b/spec/ruby_fs/action_spec.rb deleted file mode 100644 index 6830c0b..0000000 --- a/spec/ruby_fs/action_spec.rb +++ /dev/null @@ -1,187 +0,0 @@ -require 'spec_helper' - -module RubyFS - describe Action do - let(:name) { 'foobar' } - let(:headers) { {'foo' => 'bar'} } - - subject do - Action.new name, headers do |response| - @foo = response - end - end - - it { should be_new } - - describe "SIPPeers actions" do - subject { Action.new('SIPPeers') } - its(:has_causal_events?) { should be true } - end - - describe "Queues actions" do - subject { Action.new('Queues') } - its(:replies_with_action_id?) { should == false } - end - - describe "IAXPeers actions" do - before { pending } - # FIXME: This test relies on the side effect that earlier tests have run - # and initialized the UnsupportedActionName::UNSUPPORTED_ACTION_NAMES - # constant for an "unknown" version of Asterisk. This should be fixed - # to be more specific about which version of Asterisk is under test. - # IAXPeers is supported (with Action IDs!) since Asterisk 1.8 - subject { Action.new('IAXPeers') } - its(:replies_with_action_id?) { should == false } - end - - describe "the ParkedCalls terminator event" do - subject { Action.new('ParkedCalls') } - its(:causal_event_terminator_name) { should == "parkedcallscomplete" } - end - - it "should properly convert itself into a String when additional headers are given" do - string = Action.new("Hawtsawce", "Monkey" => "Zoo").to_s - string.should =~ /^Action: Hawtsawce\r\n/i - string.should =~ /[^\n]\r\n\r\n$/ - string.should =~ /^(\w+:\s*[\w-]+\r\n){3}\r\n$/ - end - - it "should properly convert itself into a String when no additional headers are given" do - Action.new("Ping").to_s.should =~ /^Action: Ping\r\nActionID: [\w-]+\r\n\r\n$/i - Action.new("ParkedCalls").to_s.should =~ /^Action: ParkedCalls\r\nActionID: [\w-]+\r\n\r\n$/i - end - - it 'should be able to be marked as sent' do - subject.state = :sent - subject.should be_sent - end - - it 'should be able to be marked as complete' do - subject.state = :complete - subject.should be_complete - end - - describe '#<<' do - describe 'for a non-causal action' do - context 'with a response' do - let(:response) { Response.new } - - it 'should set the response' do - subject << response - subject.response.should be response - end - end - - context 'with an error' do - let(:error) { Error.new.tap { |e| e.message = 'AMI error' } } - - it 'should set the response and raise the error when reading it' do - subject << error - lambda { subject.response }.should raise_error Error, 'AMI error' - end - end - - context 'with an event' do - it 'should raise an error' do - lambda { subject << Event.new('foo') }.should raise_error StandardError, /causal action/ - end - end - end - - describe 'for a causal action' do - let(:name) { 'Status' } - - context 'with a response' do - let(:message) { Response.new } - - before { subject << message } - - it { should_not be_complete } - end - - context 'with an event' do - let(:event) { Event.new 'foo' } - - before { subject << event } - - its(:events) { should == [event] } - end - - context 'with a terminating event' do - let(:response) { Response.new } - let(:event) { Event.new 'StatusComplete' } - - before do - subject << response - subject.should_not be_complete - subject << event - end - - its(:events) { should == [event] } - - it { should be_complete } - - its(:response) { should be response } - end - end - end - - describe 'setting the response' do - let(:response) { :bar } - - before { subject.response = response } - - it { should be_complete } - its(:response) { should == response } - - it 'should call the response callback with the response' do - @foo.should == response - end - end - - describe 'comparison' do - describe 'with another Action' do - context 'with identical name and headers' do - let(:other) { Action.new name, headers } - it { should == other } - end - - context 'with identical name and different headers' do - let(:other) { Action.new name, 'boo' => 'baz' } - it { should_not == other } - end - - context 'with different name and identical headers' do - let(:other) { Action.new 'BARBAZ', headers } - it { should_not == other } - end - end - - it { should_not == :foo } - end - - describe "#sync_timeout" do - it "should be 10 seconds" do - subject.sync_timeout.should be == 10 - end - - context "for an asynchronous Originate" do - let(:name) { 'Originate' } - let(:headers) { {:async => true} } - - it "should be 60 seconds" do - subject.sync_timeout.should be == 10 - end - end - - context "for a synchronous Originate" do - let(:name) { 'Originate' } - let(:headers) { {:async => false} } - - it "should be 60 seconds" do - subject.sync_timeout.should be == 60 - end - end - end - end # Action -end # RubyFS diff --git a/spec/ruby_fs/client_spec.rb b/spec/ruby_fs/client_spec.rb deleted file mode 100644 index 949080d..0000000 --- a/spec/ruby_fs/client_spec.rb +++ /dev/null @@ -1,348 +0,0 @@ -require 'spec_helper' - -module RubyFS - describe Client do - let(:event_handler) { [] } - - let(:options) do - { - :host => '127.0.0.1', - :port => 50000 - rand(1000), - :username => 'username', - :password => 'password', - :event_handler => lambda { |event| event_handler << event } - } - end - - subject { Client.new options } - - it { should be_stopped } - - its(:options) { should == options } - - its(:action_queue) { should be_a GirlFriday::WorkQueue } - - its(:streams) { should == [] } - - describe 'starting up' do - before do - ms = MockServer.new - ms.expects(:receive_data).at_least_once - s = ServerMock.new options[:host], options[:port], ms - Thread.new { subject.start } - sleep 0.2 - end - - it { should be_started } - - its(:events_stream) { should be_a Stream } - its(:actions_stream) { should be_a Stream } - end - - describe 'logging in streams' do - context 'when the actions stream connects' do - let(:mock_actions_stream) { mock 'Actions Stream' } - - let :expected_login_action do - Action.new 'Login', - 'Username' => 'username', - 'Secret' => 'password', - 'Events' => 'On' - end - - before do - Action.any_instance.stubs(:response).returns(true) - subject.stubs(:actions_stream).returns mock_actions_stream - end - - it 'should log in' do - mock_actions_stream.expects(:send_action).with do |action| - action.to_s.should == expected_login_action.to_s - end - - subject.handle_message(Stream::Connected.new).join - end - end - - context 'when the events stream connects' do - let(:mock_events_stream) { mock 'Events Stream' } - - let :expected_login_action do - Action.new 'Login', - 'Username' => 'username', - 'Secret' => 'password', - 'Events' => 'On' - end - - before do - subject.stubs(:events_stream).returns mock_events_stream - end - - it 'should log in' do - mock_events_stream.expects(:send_action).with expected_login_action - - subject.handle_event Stream::Connected.new - - event_handler.should be_empty - end - end - end - - describe 'when the events stream disconnects' do - it 'should stop' do - subject.expects(:stop).once - subject.handle_event Stream::Disconnected.new - event_handler.should be_empty - end - end - - describe 'when the actions stream disconnects' do - before do - Action.any_instance.stubs(:response).returns(true) - end - - it 'should prevent further actions being sent' do - subject.expects(:_send_action).once - - GirlFriday::WorkQueue.immediate! - subject.handle_message Stream::Connected.new - GirlFriday::WorkQueue.queue! - subject.handle_message Stream::Disconnected.new - - action = Action.new 'foo' - subject.send_action action - - sleep 2 - - action.should be_new - end - - it 'should stop' do - subject.expects(:stop).once - subject.handle_message Stream::Disconnected.new - end - end - - describe 'when an event is received' do - let(:event) { Event.new 'foobar' } - - it 'should call the event handler' do - subject.handle_event event - event_handler.should == [event] - end - end - - describe 'when a FullyBooted event is received on the actions connection' do - let(:event) { Event.new 'FullyBooted' } - - let(:mock_actions_stream) { mock 'Actions Stream' } - - let :expected_login_action do - Action.new 'Login', - 'Username' => 'username', - 'Secret' => 'password', - 'Events' => 'On' - end - - let :expected_events_off_action do - Action.new 'Events', 'EventMask' => 'Off' - end - - it 'should call the event handler' do - subject.handle_message event - event_handler.should == [event] - end - - it 'should begin writing actions' do - subject.expects(:start_writing_actions).once - subject.handle_message event - end - - it 'should turn off events' do - Action.any_instance.stubs(:response).returns true - subject.stubs(:actions_stream).returns mock_actions_stream - - mock_actions_stream.expects(:send_action).once.with expected_login_action - mock_actions_stream.expects(:send_action).once.with expected_events_off_action - - login_action = subject.handle_message(Stream::Connected.new).join - login_action.value.response = true - - subject.handle_message event - sleep 0.5 - end - end - - describe 'sending actions' do - let(:action_name) { 'Login' } - let :headers do - { - 'Username' => 'username', - 'Secret' => 'password' - } - end - let(:expected_action) { Action.new action_name, headers } - - let :expected_response do - Response.new.tap do |response| - response['ActionID'] = expected_action.action_id - response['Message'] = 'Action completed' - end - end - - let(:mock_actions_stream) { mock 'Actions Stream' } - - before do - subject.stubs(:actions_stream).returns mock_actions_stream - subject.stubs(:login_actions).returns nil - end - - it 'should queue up actions to be sent' do - subject.handle_message Stream::Connected.new - subject.action_queue.expects(:<<).with expected_action - subject.send_action action_name, headers - end - - describe 'forcibly for testing' do - before do - subject.actions_stream.expects(:send_action).with expected_action - subject._send_action expected_action - end - - it 'should mark the action sent' do - expected_action.should be_sent - end - - let(:receive_response) { subject.handle_message expected_response } - - describe 'when a response is received' do - it 'should be sent to the action' do - expected_action.expects(:<<).once.with expected_response - receive_response - end - - it 'should know its action' do - receive_response - expected_response.action.should be expected_action - end - end - - describe 'when an error is received' do - let :expected_response do - Error.new.tap do |response| - response['ActionID'] = expected_action.action_id - response['Message'] = 'Action failed' - end - end - - it 'should be sent to the action' do - expected_action.expects(:<<).once.with expected_response - receive_response - end - - it 'should know its action' do - receive_response - expected_response.action.should be expected_action - end - end - - describe 'when an event is received' do - let(:event) { Event.new 'foo' } - - let(:receive_event) { subject.handle_message event } - - context 'for a causal event' do - let(:expected_action) { Action.new 'Status' } - - it 'should be sent to the action' do - expected_action.expects(:<<).once.with expected_response - expected_action.expects(:<<).once.with event - receive_response - receive_event - end - - it 'should know its action' do - expected_action.stubs :<< - receive_response - receive_event - event.action.should be expected_action - end - end - - context 'for a causal action which is complete' do - let(:expected_action) { Action.new 'Status' } - - before do - expected_action.stubs(:complete?).returns true - end - - it 'should raise an error' do - receive_response - receive_event - lambda { subject.handle_message Event.new('bar') }.should raise_error StandardError, /causal action/ - end - end - - context 'for a non-causal action' do - it 'should raise an error' do - lambda { receive_event }.should raise_error StandardError, /causal action/ - end - end - end - end - - describe 'from the queue' do - it 'should send actions to the stream and set their responses' do - subject.actions_stream.expects(:send_action).with expected_action - subject.handle_message Event.new('FullyBooted') - - Thread.new do - GirlFriday::WorkQueue.immediate! - subject.send_action expected_action - GirlFriday::WorkQueue.queue! - end - - sleep 0.1 - - subject.handle_message expected_response - expected_action.response.should be expected_response - end - - it 'should not send another action if the first action has not yet received a response' do - subject.actions_stream.expects(:send_action).once.with expected_action - subject.handle_message Event.new('FullyBooted') - actions = [] - - 2.times do - action = Action.new action_name, headers - actions << action - subject.send_action action - end - - sleep 2 - - actions.should have(2).actions - actions[0].should be_sent - actions[1].should be_new - end - end - end - - describe '#stop' do - let(:mock_actions_stream) { mock 'Actions Stream' } - let(:mock_events_stream) { mock 'Events Stream' } - - let(:streams) { [mock_actions_stream, mock_events_stream] } - - before do - subject.stubs(:actions_stream).returns mock_actions_stream - subject.stubs(:events_stream).returns mock_events_stream - end - - it 'should close both streams' do - streams.each { |s| s.expects :terminate } - subject.stop - end - end - end -end diff --git a/spec/ruby_fs/error_spec.rb b/spec/ruby_fs/error_spec.rb deleted file mode 100644 index 820a1ba..0000000 --- a/spec/ruby_fs/error_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'spec_helper' - -module RubyFS - describe Error do - pending - end # Error -end # RubyFS diff --git a/spec/ruby_fs/event_spec.rb b/spec/ruby_fs/event_spec.rb deleted file mode 100644 index c5d5af4..0000000 --- a/spec/ruby_fs/event_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -require 'spec_helper' - -module RubyFS - describe Event do - describe "equality" do - context "with the same name and the same headers" do - let :event1 do - Event.new('Hangup').tap do |e| - e['Channel'] = 'SIP/101-3f3f' - e['Uniqueid'] = '1094154427.10' - e['Cause'] = '0' - end - end - - let :event2 do - Event.new('Hangup').tap do |e| - e['Channel'] = 'SIP/101-3f3f' - e['Uniqueid'] = '1094154427.10' - e['Cause'] = '0' - end - end - - it "should be equal" do - event1.should be == event2 - end - end - - context "with a different name and the same headers" do - let :event1 do - Event.new('Hangup').tap do |e| - e['Channel'] = 'SIP/101-3f3f' - e['Uniqueid'] = '1094154427.10' - e['Cause'] = '0' - end - end - - let :event2 do - Event.new('Foo').tap do |e| - e['Channel'] = 'SIP/101-3f3f' - e['Uniqueid'] = '1094154427.10' - e['Cause'] = '0' - end - end - - it "should not be equal" do - event1.should_not be == event2 - end - end - - context "with the same name and different headers" do - let :event1 do - Event.new('Hangup').tap do |e| - e['Channel'] = 'SIP/101-3f3f' - e['Uniqueid'] = '1094154427.10' - e['Cause'] = '0' - end - end - - let :event2 do - Event.new('Hangup').tap do |e| - e['Channel'] = 'SIP/101-3f3f' - e['Uniqueid'] = '1094154427.10' - e['Cause'] = '1' - end - end - - it "should not be equal" do - event1.should_not be == event2 - end - end - end - end # Event -end # RubyFS diff --git a/spec/ruby_fs/response_spec.rb b/spec/ruby_fs/response_spec.rb deleted file mode 100644 index e63b8a2..0000000 --- a/spec/ruby_fs/response_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'spec_helper' - -module RubyFS - describe Response do - describe "equality" do - context "with the same headers" do - let :event1 do - Response.new.tap do |e| - e['Channel'] = 'SIP/101-3f3f' - e['Uniqueid'] = '1094154427.10' - e['Cause'] = '0' - end - end - - let :event2 do - Response.new.tap do |e| - e['Channel'] = 'SIP/101-3f3f' - e['Uniqueid'] = '1094154427.10' - e['Cause'] = '0' - end - end - - it "should be equal" do - event1.should be == event2 - end - end - - context "with different headers" do - let :event1 do - Response.new.tap do |e| - e['Channel'] = 'SIP/101-3f3f' - e['Uniqueid'] = '1094154427.10' - e['Cause'] = '0' - end - end - - let :event2 do - Response.new.tap do |e| - e['Channel'] = 'SIP/101-3f3f' - e['Uniqueid'] = '1094154427.10' - e['Cause'] = '1' - end - end - - it "should not be equal" do - event1.should_not be == event2 - end - end - end - end # Response -end # RubyFS diff --git a/spec/ruby_fs/stream_spec.rb b/spec/ruby_fs/stream_spec.rb index 9762b72..f976aab 100644 --- a/spec/ruby_fs/stream_spec.rb +++ b/spec/ruby_fs/stream_spec.rb @@ -50,83 +50,27 @@ def expect_disconnected_event end end - it "can send a command" do + it "can send data" do expect_connected_event expect_disconnected_event - action = Action.new('Command', 'Command' => 'RECORD FILE evil', 'ActionID' => 666, 'Events' => 'On') - mocked_server(1, lambda { @stream.send_action action }) do |val, server| - val.should == action.to_s + mocked_server(1, lambda { @stream.send_data "foo" }) do |val, server| + val.should == "foo" end end end it 'sends events to the client when the stream is ready' do mocked_server(1, lambda { @stream.send_data 'Foo' }) do |val, server| - server.send_data <<-EVENT -Event: Hangup -Channel: SIP/101-3f3f -Uniqueid: 1094154427.10 -Cause: 0 - - EVENT + server.send_data 'foo' end client_messages.should be == [ Stream::Connected.new, - Event.new('Hangup').tap do |e| - e['Channel'] = 'SIP/101-3f3f' - e['Uniqueid'] = '1094154427.10' - e['Cause'] = '0' - end, + 'foo', Stream::Disconnected.new ] end - it 'sends responses to the client when the stream is ready' do - mocked_server(1, lambda { @stream.send_data 'Foo' }) do |val, server| - server.send_data <<-EVENT -Response: Success -ActionID: ee33eru2398fjj290 -Message: Authentication accepted - - EVENT - end - - client_messages.should be == [ - Stream::Connected.new, - Response.new.tap do |r| - r['ActionID'] = 'ee33eru2398fjj290' - r['Message'] = 'Authentication accepted' - end, - Stream::Disconnected.new - ] - end - - it 'sends error to the client when the stream is ready and a bad command was send' do - client.expects(:message_received).times(3).with do |r| - case @sequence - when 1 - r.should be_a Stream::Connected - when 2 - r.should be_a Error - r['ActionID'].should == 'ee33eru2398fjj290' - r['Message'].should == 'You stupid git' - when 3 - r.should be_a Stream::Disconnected - end - @sequence += 1 - end - - mocked_server(1, lambda { @stream.send_data 'Foo' }) do |val, server| - server.send_data <<-EVENT -Response: Error -ActionID: ee33eru2398fjj290 -Message: You stupid git - - EVENT - end - end - it 'puts itself in the stopped state and fires a disconnected event when unbound' do expect_connected_event expect_disconnected_event