adhearsion 0.8.2 → 0.8.3

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,4 +1,11 @@
1
+ 0.8.3
2
+ - The "uniqueid" call channel variable available in dialplan.rb is now *always* a String
3
+ - Renamed interruptable_play to interruptible_play and made interruptible_play() public instead of protected.
4
+ - Fixed an Asterisk Manager Interface parsing issue in which colons sometimes got stuck into the key name.
5
+ - AGI "request" variable coercer will not blow up if no request is given. (Helps in testing with netcat/telnet)
6
+
1
7
  0.8.2
8
+ - When a call hangs up, Adhearsion will no longer show random exceptions (that were okay) and instead allows the user to rescue a Hangup exception.
2
9
  - ManagerInterfaceResponse now include()s DRbUndumped, allowing send_action() to be called directly over DRb.
3
10
  - Fixes an inconsequential bug when CTL-C'ing Adhearsion.
4
11
 
@@ -112,7 +112,7 @@ ADHEARSION_FILES = %w{
112
112
 
113
113
  Gem::Specification.new do |s|
114
114
  s.name = "adhearsion"
115
- s.version = "0.8.2"
115
+ s.version = "0.8.3"
116
116
 
117
117
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
118
118
  s.authors = ["Jay Phillips"]
@@ -2,7 +2,7 @@ methods_for :rpc do
2
2
 
3
3
  # Simply create proxy methods for the high-level AMI methods
4
4
 
5
- [:send_action, :introduce, :originate, :call_into_context, :call_and_exec].each do |method_name|
5
+ [:send_action, :introduce, :originate, :call_into_context, :call_and_exec, :ping].each do |method_name|
6
6
  define_method(method_name) do |*args|
7
7
  if VoIP::Asterisk.manager_interface
8
8
  VoIP::Asterisk.manager_interface.send(method_name, *args)
@@ -29,7 +29,7 @@ initialization do
29
29
 
30
30
  host, port = config.values_at "host", "port"
31
31
 
32
- username, password = COMPONENTS.sandbox["username"], COMPONENTS.sandbox["password"]
32
+ username, password = COMPONENTS.sandbox["username"].to_s, COMPONENTS.sandbox["password"].to_s
33
33
 
34
34
  if username.blank? || password.blank? || username == "user123"
35
35
  ahn_log.sandbox.error "You must specify your username and password in this component's config file!"
@@ -5,7 +5,7 @@ unless defined? Adhearsion
5
5
  require File.dirname(__FILE__) + "/../adhearsion/lib/adhearsion.rb"
6
6
  else
7
7
  require 'rubygems'
8
- gem 'adhearsion', '>= 0.7.999'
8
+ gem 'adhearsion', '>= 0.8.2'
9
9
  require 'adhearsion'
10
10
  end
11
11
  end
@@ -5,7 +5,7 @@ module Adhearsion
5
5
  ##
6
6
  # Shuts down the framework.
7
7
  #
8
- def self.shutdown!
8
+ def shutdown!
9
9
  ahn_log "Shutting down gracefully at #{Time.now}."
10
10
  Events.stop!
11
11
  exit
@@ -2,7 +2,7 @@ module Adhearsion #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0 unless defined? MAJOR
4
4
  MINOR = 8 unless defined? MINOR
5
- TINY = 2 unless defined? TINY
5
+ TINY = 3 unless defined? TINY
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.') unless defined? STRING
8
8
  end
@@ -4,12 +4,12 @@ require 'adhearsion/voip/menu_state_machine/menu_class'
4
4
  module Adhearsion
5
5
  module VoIP
6
6
  module Asterisk
7
- module Commands
7
+ module Commands
8
8
 
9
9
  RESPONSE_PREFIX = "200 result=" unless defined? RESPONSE_PREFIX
10
10
 
11
11
  # These are the status messages that asterisk will issue after a dial command is executed.
12
- # More information here: http://www.voip-info.org/wiki/index.php?page=Asterisk+variable+DIALSTATUS
12
+ #
13
13
  # Here is a current list of dial status messages which are not all necessarily supported by adhearsion:
14
14
  #
15
15
  # ANSWER: Call is answered. A successful dial. The caller reached the callee.
@@ -22,8 +22,8 @@ module Adhearsion
22
22
  # TORTURE: Privacy mode, callee chose to send caller to torture menu
23
23
  # INVALIDARGS: Error parsing Dial command arguments (added for Asterisk 1.4.1, SVN r53135-53136)
24
24
  #
25
- #
26
- DIAL_STATUSES = Hash.new(:unknown).merge(:answer => :answered,
25
+ # @see http://www.voip-info.org/wiki/index.php?page=Asterisk+variable+DIALSTATUS Asterisk Variable DIALSTATUS
26
+ DIAL_STATUSES = Hash.new(:unknown).merge(:answer => :answered, #:doc:
27
27
  :congestion => :congested,
28
28
  :busy => :busy,
29
29
  :cancel => :cancelled,
@@ -42,11 +42,13 @@ module Adhearsion
42
42
  end
43
43
  } unless defined? DYNAMIC_FEATURE_EXTENSIONS
44
44
 
45
- def write(message)
45
+ # Utility method to write to pbx.
46
+ def write(message)
46
47
  to_pbx.print(message)
47
48
  end
48
49
 
49
- def read
50
+ # Utility method to read from pbx. Hangup if nil.
51
+ def read
50
52
  returning from_pbx.gets do |message|
51
53
  raise Hangup if message.nil?
52
54
  ahn_log.agi.debug "<<< #{message}"
@@ -58,7 +60,7 @@ module Adhearsion
58
60
  # FAGI protocol.
59
61
  # It is not recommended that you call this method directly unless you plan to write a new command method
60
62
  # in which case use this method you to communicate directly with an Asterisk server via the FAGI protocol.
61
- # For more information about FAGI visit: http://www.voip-info.org/wiki/view/Asterisk+FastAGI
63
+ # @see http://www.voip-info.org/wiki/view/Asterisk+FastAGI More information about FAGI
62
64
  def raw_response(message = nil)
63
65
  ahn_log.agi.debug ">>> #{message}"
64
66
  write message if message
@@ -79,8 +81,7 @@ module Adhearsion
79
81
  end
80
82
 
81
83
  # This asterisk dialplan command allows you to instruct Asterisk to start applications
82
- # which are typically run from extensions.conf. For a complete list of these commands
83
- # please visit: http://www.voip-info.org/wiki/view/Asterisk+-+documentation+of+application+commands
84
+ # which are typically run from extensions.conf.
84
85
  #
85
86
  # The most common commands are already made available through the FAGI interface provided
86
87
  # by this code base. For commands that do not fall into this category, then exec is what you
@@ -89,22 +90,19 @@ module Adhearsion
89
90
  # For example, if there are specific asterisk modules you have loaded that will not
90
91
  # available through the standard commands provided through FAGI - then you can used EXEC.
91
92
  #
92
- # Example:
93
- # execute 'SIPAddHeader', '"Call-Info: answer-after=0"
94
- #
95
- # Using execute in this way will add a header to an existing SIP call.
93
+ # @example Using execute in this way will add a header to an existing SIP call.
94
+ # execute 'SIPAddHeader', '"Call-Info: answer-after=0"
96
95
  #
96
+ # @see http://www.voip-info.org/wiki/view/Asterisk+-+documentation+of+application+commands Asterisk Dialplan Commands
97
97
  def execute(application, *arguments)
98
98
  result = raw_response("EXEC #{application} #{arguments * '|'}")
99
99
  return false if error?(result)
100
100
  result
101
101
  end
102
102
 
103
- # Hangs up the current channel.
104
- # After this command is issued, your application will stop executing.
105
- # This should be used in the same way you would call the ruby exit() method to exit an application.
106
- # If it is necessary to do some additional cleanup tasks before returning control back to asterisk, then
107
- # make sure you have setup a begin...ensure block in the context of your adhearsion application dialplan.
103
+ # Hangs up the current channel. After this command is issued, you will not be able to send any more AGI
104
+ # commands but the dialplan Thread will still continue, allowing you to do any post-call work.
105
+ #
108
106
  def hangup
109
107
  raw_response 'HANGUP'
110
108
  end
@@ -118,11 +116,13 @@ module Adhearsion
118
116
  # file encoded using the current channel's codec, if one exists. If not, it will transcode from
119
117
  # the default codec (GSM). Asterisk stores its sound files in /var/lib/asterisk/sounds.
120
118
  #
121
- # Usage:
122
- #
119
+ # @example Play file hello-world.???
123
120
  # play 'hello-world'
121
+ # @example Speak current time
124
122
  # play Time.now
123
+ # @example Play sound file, speak number, play two more sound files
125
124
  # play %w"a-connect-charge-of 22 cents-per-minute will-apply"
125
+ # @example Play two sound files
126
126
  # play "you-sound-cute", "what-are-you-wearing"
127
127
  #
128
128
  def play(*arguments)
@@ -138,9 +138,11 @@ module Adhearsion
138
138
  #
139
139
  # Silence and maxduration is specified in seconds.
140
140
  #
141
- # Usage:
142
- # record
141
+ # @example Asterisk generated filename
142
+ # filename = record
143
+ # @example Specified filename
143
144
  # record '/path/to/my-file.gsm'
145
+ # @example All options specified
144
146
  # record 'my-file.gsm', :silence => 5, :maxduration => 120
145
147
  #
146
148
  def record(*args)
@@ -162,12 +164,13 @@ module Adhearsion
162
164
  execute "SendDTMF", digits.to_s
163
165
  end
164
166
 
167
+ # The with_next_message method...
165
168
  def with_next_message(&block)
166
169
  raise LocalJumpError, "Must supply a block" unless block_given?
167
170
  block.call(next_message)
168
171
  end
169
172
 
170
- # This command shouled be used to advance to the next message in the Asterisk Comedian Voicemail application
173
+ # This command should be used to advance to the next message in the Asterisk Comedian Voicemail application
171
174
  def next_message
172
175
  @call.inbox.pop
173
176
  end
@@ -177,11 +180,9 @@ module Adhearsion
177
180
  not @call.inbox.empty?
178
181
  end
179
182
 
180
- # = Menu Command
183
+ # Menu creates an interactive menu for the caller.
181
184
  #
182
- # The following documentation was derived from this blog post on Jay Phillips' blog:
183
- #
184
- # http://jicksta.com/articles/2008/02/11/menu-command
185
+ # The following documentation was derived from a post on Jay Phillips' blog (see below).
185
186
  #
186
187
  # The menu() command solves the problem of building enormous input-fetching state machines in Ruby without first-class
187
188
  # message passing facilities or an external DSL.
@@ -244,18 +245,18 @@ module Adhearsion
244
245
  # hook after the other hook (e.g. +on_invalid+, then +on_failure+).
245
246
  #
246
247
  # When the +menu()+ state machine runs through the defined rules, it must distinguish between exact and potential matches.
247
- # Its important to understand the differences between these and how they affect the overall outcome:
248
+ # It's important to understand the differences between these and how they affect the overall outcome:
248
249
  #
249
- # |---------------|-------------------|------------------------------------------------------|
250
- # | exact matches | potential matches | result |
251
- # |---------------|-------------------|------------------------------------------------------|
252
- # | 0 | 0 | Fail and start over |
253
- # | 1 | 0 | Match found! |
254
- # | 0 | >0 | Get another digit |
255
- # | >1 | 0 | Go with the first exact match |
256
- # | 1 | >0 | Get another digit. If timeout, use exact match |
257
- # | >1 | >0 | Get another digit. If timeout, use first exact match |
258
- # |---------------|-------------------|------------------------------------------------------|
250
+ # |---------------|-------------------|------------------------------------------------------|
251
+ # | exact matches | potential matches | result |
252
+ # |---------------|-------------------|------------------------------------------------------|
253
+ # | 0 | 0 | Fail and start over |
254
+ # | 1 | 0 | Match found! |
255
+ # | 0 | >0 | Get another digit |
256
+ # | >1 | 0 | Go with the first exact match |
257
+ # | 1 | >0 | Get another digit. If timeout, use exact match |
258
+ # | >1 | >0 | Get another digit. If timeout, use first exact match |
259
+ # |---------------|-------------------|------------------------------------------------------|
259
260
  #
260
261
  # == Database integration
261
262
  #
@@ -290,6 +291,7 @@ module Adhearsion
290
291
  # that caused the jump. After all, the context doesn’t necessary need to be the endpoint from a menu; it can be its own entry
291
292
  # point, making menu() effectively a pipeline of re-creating the call.
292
293
  #
294
+ # @see http://jicksta.com/articles/2008/02/11/menu-command Original Blog Post
293
295
  def menu(*args, &block)
294
296
  options = args.last.kind_of?(Hash) ? args.pop : {}
295
297
  sound_files = args.flatten
@@ -407,11 +409,11 @@ module Adhearsion
407
409
  buffer << key
408
410
  return buffer if number_of_digits && number_of_digits == buffer.length
409
411
  end
410
- key = wait_for_digit timeout || -1
412
+ key = wait_for_digit(timeout || -1)
411
413
  end
412
414
  end
413
415
 
414
- # An alternative to DialplanContextProc#+@. When jumping to a context, it will *not* resume executing
416
+ # Jumps to a context. An alternative to DialplanContextProc#+@. When jumping to a context, it will *not* resume executing
415
417
  # the former context when the jumped-to context has finished executing. Make sure you don't have any
416
418
  # +ensure+ closures which you expect to execute when the call has finished, as they will run when
417
419
  # this method is called.
@@ -439,6 +441,9 @@ module Adhearsion
439
441
  raise Adhearsion::VoIP::DSL::Dialplan::ControlPassingException.new(context)
440
442
  end
441
443
 
444
+ # The queue method puts a call into a call queue to be answered by an agent registered with that queue.
445
+ # The queue method takes a queue_name as an argument to place the caller in the appropriate queue.
446
+ # @see http://www.voip-info.org/wiki-Asterisk+cmd+Queue Full information on the Asterisk Queue
442
447
  def queue(queue_name)
443
448
  queue_name = queue_name.to_s
444
449
 
@@ -523,7 +528,7 @@ module Adhearsion
523
528
  # Used to join a particular conference with the MeetMe application. To
524
529
  # use MeetMe, be sure you have a proper timing device configured on your
525
530
  # Asterisk box. MeetMe is Asterisk's built-in conferencing program.
526
- # More info: http://www.voip-info.org/wiki-Asterisk+cmd+MeetMe
531
+ # @see http://www.voip-info.org/wiki-Asterisk+cmd+MeetMe Asterisk Meetme Application Information
527
532
  def join(conference_id, options={})
528
533
  conference_id = conference_id.to_s.scan(/\w/).join
529
534
  command_flags = options[:options].to_s # This is a passthrough string straight to Asterisk
@@ -536,9 +541,9 @@ module Adhearsion
536
541
  end
537
542
 
538
543
  # Issue this command to access a channel variable that exists in the asterisk dialplan (i.e. extensions.conf)
539
- # A complete description is available here: http://www.voip-info.org/wiki/view/get+variable
540
544
  # Use get_variable to pass information from other modules or high level configurations from the asterisk dialplan
541
545
  # to the adhearsion dialplan.
546
+ # @see: http://www.voip-info.org/wiki/view/get+variable Asterisk Get Variable
542
547
  def get_variable(variable_name)
543
548
  result = raw_response("GET VARIABLE #{variable_name}")
544
549
  case result
@@ -550,15 +555,18 @@ module Adhearsion
550
555
  end
551
556
 
552
557
  # Use set_variable to pass information back to the asterisk dial plan.
553
- # A complete decription is available here: http://www.voip-info.org/wiki/view/set+variable
554
558
  # Keep in mind that the variables are not global variables. These variables only exist for the channel
555
559
  # related to the call that is being serviced by the particular instance of your adhearsion application.
556
560
  # You will not be able to pass information back to the asterisk dialplan for other instances of your adhearsion
557
561
  # application to share. Once the channel is "hungup" then the variables are cleared and their information is gone.
562
+ # @see http://www.voip-info.org/wiki/view/set+variable Asterisk Set Variable
558
563
  def set_variable(variable_name, value)
559
564
  raw_response("SET VARIABLE %s %p" % [variable_name.to_s, value.to_s]) == "200 result=1"
560
565
  end
561
566
 
567
+ # The variable method allows you to either set or get a channel variable from Asterisk
568
+ # The method takes a hash key/value pair if you would like to set a variable
569
+ # Or a single string with the variable to get from Asterisk
562
570
  def variable(*args)
563
571
  if args.last.kind_of? Hash
564
572
  assignments = args.pop
@@ -575,6 +583,10 @@ module Adhearsion
575
583
  end
576
584
  end
577
585
 
586
+ # Use the voicemail method to send a caller to a voicemail box to leave a message.
587
+ # @see http://www.voip-info.org/tiki-index.php?page=Asterisk+cmd+VoiceMail Asterisk Voicemail
588
+ # The method takes the mailbox_number of the user to leave a message for and a
589
+ # greeting_option that will determine which message gets played to the caller.
578
590
  def voicemail(*args)
579
591
  options_hash = args.last.kind_of?(Hash) ? args.pop : {}
580
592
  mailbox_number = args.shift
@@ -607,6 +619,9 @@ module Adhearsion
607
619
  end
608
620
  end
609
621
 
622
+ # The voicemail_main method puts a caller into the voicemail system to fetch their voicemail
623
+ # or set options for their voicemail box.
624
+ # @see http://www.voip-info.org/wiki-Asterisk+cmd+VoiceMailMain Asterisk VoiceMailMain Command
610
625
  def voicemail_main(options={})
611
626
  mailbox, context, folder = options.values_at :mailbox, :context, :folder
612
627
  authenticate = options.has_key?(:authenticate) ? options[:authenticate] : true
@@ -643,42 +658,43 @@ module Adhearsion
643
658
  voicemail_main
644
659
  end
645
660
 
646
- # Use this command to dial an extension i.e. "phone number" in asterisk
647
- # This command maps to the Asterisk DIAL command in the asterisk dialplan: http://www.voip-info.org/wiki-Asterisk+cmd+Dial
661
+ # Use this command to dial an extension or "phone number" in asterisk.
662
+ # This command maps to the Asterisk DIAL command in the asterisk dialplan.
648
663
  #
649
664
  # The first parameter, number, must be a string that represents the extension or "number" that asterisk should dial.
650
665
  # Be careful to not just specify a number like 5001, 9095551001
651
666
  # You must specify a properly formatted string as Asterisk would expect to use in order to understand
652
667
  # whether the call should be dialed using SIP, IAX, or some other means.
653
- # Examples:
654
- #
655
- # Make a call to the PSTN using my SIP provider for VoIP termination:
656
- # dial("SIP/19095551001@my.sip.voip.terminator.us")
657
- #
658
- # Make 3 Simulataneous calls to the SIP extensions separated by & symbols, try for 15 seconds and use the callerid
659
- # for this call specified by the variable my_callerid
660
- # dial "SIP/jay-desk-650&SIP/jay-desk-601&SIP/jay-desk-601-2", :for => 15.seconds, :caller_id => my_callerid
661
- #
662
- # Make a call using the IAX provider to the PSTN
663
- # dial("IAX2/my.id@voipjet/19095551234", :name=>"John Doe", :caller_id=>"9095551234")
664
668
  #
665
669
  # Options Parameter:
666
- # :caller_id - the caller id number to be used when the call is placed. It is advised you properly adhere to the
670
+ #
671
+ # +:caller_id+ - the caller id number to be used when the call is placed. It is advised you properly adhere to the
667
672
  # policy of VoIP termination providers with respect to caller id values.
668
673
  #
669
- # :name - this is the name which should be passed with the caller ID information
674
+ # +:name+ - this is the name which should be passed with the caller ID information
670
675
  # if :name=>"John Doe" and :caller_id => "444-333-1000" then the compelete CID and name would be "John Doe" <4443331000>
671
676
  # support for caller id information varies from country to country and from one VoIP termination provider to another.
672
677
  #
673
- # :for - this option can be thought of best as a timeout. i.e. timeout after :for if no one answers the call
678
+ # +:for+ - this option can be thought of best as a timeout. i.e. timeout after :for if no one answers the call
674
679
  # For example, dial("SIP/jay-desk-650&SIP/jay-desk-601&SIP/jay-desk-601-2", :for => 15.seconds, :caller_id => callerid)
675
- # this call will timeout after 15 seconds if 1 of the 3 extensions being dialed do not pick prior to the 15 second time limit
680
+ # this call will timeout after 15 seconds if 1 of the 3 extensions being dialed do not pick prior to the 15 second time limit
676
681
  #
677
- # :options - This is a string of options like "Tr" which are supported by the asterisk DIAL application.
678
- # for a complete list of these options and their usage please visit: http://www.voip-info.org/wiki-Asterisk+cmd+Dial
682
+ # +:options+ - This is a string of options like "Tr" which are supported by the asterisk DIAL application.
683
+ # for a complete list of these options and their usage please check the link below.
679
684
  #
680
- # :confirm - ?
685
+ # +:confirm+ - ?
686
+ #
687
+ # @example Make a call to the PSTN using my SIP provider for VoIP termination
688
+ # dial("SIP/19095551001@my.sip.voip.terminator.us")
681
689
  #
690
+ # @example Make 3 Simulataneous calls to the SIP extensions separated by & symbols, try for 15 seconds and use the callerid
691
+ # for this call specified by the variable my_callerid
692
+ # dial "SIP/jay-desk-650&SIP/jay-desk-601&SIP/jay-desk-601-2", :for => 15.seconds, :caller_id => my_callerid
693
+ #
694
+ # @example Make a call using the IAX provider to the PSTN
695
+ # dial("IAX2/my.id@voipjet/19095551234", :name=>"John Doe", :caller_id=>"9095551234")
696
+ #
697
+ # @see http://www.voip-info.org/wiki-Asterisk+cmd+Dial Asterisk Dial Command
682
698
  def dial(number, options={})
683
699
  *recognized_options = :caller_id, :name, :for, :options, :confirm
684
700
 
@@ -728,29 +744,44 @@ module Adhearsion
728
744
  yield
729
745
  Time.now - start_time
730
746
  end
747
+
748
+ #
749
+ # This will play a sequence of files, stopping the playback if a digit is pressed. If a digit is pressed, it will be
750
+ # returned as a String. If the files played with no keypad input, nil will be returned.
751
+ #
752
+ def interruptible_play(*files)
753
+ files.flatten.each do |file|
754
+ result = result_digit_from raw_response("EXEC BACKGROUND #{file}")
755
+ return result if result != 0.chr
756
+ end
757
+ nil
758
+ end
731
759
 
732
760
  protected
733
761
 
762
+ # wait_for_digits waits for the input of digits based on the number of milliseconds
734
763
  def wait_for_digit(timeout=-1)
735
764
  timeout *= 1_000 if timeout != -1
736
765
  result = result_digit_from raw_response("WAIT FOR DIGIT #{timeout.to_i}")
737
766
  (result == 0.chr) ? nil : result
738
767
  end
739
768
 
769
+ ##
770
+ # Deprecated name of interruptible_play(). This is a misspelling!
771
+ #
740
772
  def interruptable_play(*files)
741
- files.flatten.each do |file|
742
- result = result_digit_from raw_response("EXEC BACKGROUND #{file}")
743
- return result if result != 0.chr
744
- end
745
- nil
773
+ ahn_log.deprecation.warn 'Please change your code to use interruptible_play() instead. "interruptable" is a misspelling! interruptable_play() will work for now but will be deprecated in the future!'
774
+ interruptible_play(*files)
746
775
  end
747
776
 
777
+ # set_callier_id_number method allows setting of the callerid number of the call
748
778
  def set_caller_id_number(caller_id)
749
779
  return unless caller_id
750
780
  raise ArgumentError, "Caller ID must be numerical" if caller_id.to_s !~ /^\d+$/
751
781
  raw_response %(SET CALLERID %p) % caller_id
752
782
  end
753
783
 
784
+ # set_caller_id_name method allows the setting of the callerid name of the call
754
785
  def set_caller_id_name(caller_id_name)
755
786
  return unless caller_id_name
756
787
  variable "CALLERID(name)" => caller_id_name
@@ -902,8 +933,8 @@ module Adhearsion
902
933
 
903
934
  # timeout with pressed digits: 200 result=<digits> (timeout)
904
935
  # timeout without pressed digits: 200 result= (timeout)
905
- # (http://www.voip-info.org/wiki/view/get+data)
906
- def input_timed_out?(result)
936
+ # @see http://www.voip-info.org/wiki/view/get+data AGI Get Data
937
+ def input_timed_out?(result)
907
938
  result.starts_with?(response_prefix) && result.ends_with?('(timeout)')
908
939
  end
909
940