This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
# BufferedTokenizer takes a delimiter upon instantiation, or acts line-based
|
||||
# by default. It allows input to be spoon-fed from some outside source which
|
||||
# receives arbitrary length datagrams which may-or-may-not contain the token
|
||||
# by which entities are delimited. In this respect it's ideally paired with
|
||||
# something like EventMachine (http://rubyeventmachine.com/).
|
||||
class BufferedTokenizer
|
||||
# New BufferedTokenizers will operate on lines delimited by a delimiter,
|
||||
# which is by default the global input delimiter $/ ("\n").
|
||||
#
|
||||
# The input buffer is stored as an array. This is by far the most efficient
|
||||
# approach given language constraints (in C a linked list would be a more
|
||||
# appropriate data structure). Segments of input data are stored in a list
|
||||
# which is only joined when a token is reached, substantially reducing the
|
||||
# number of objects required for the operation.
|
||||
def initialize(delimiter = $/)
|
||||
@delimiter = delimiter
|
||||
@input = []
|
||||
@tail = ''
|
||||
@trim = @delimiter.length - 1
|
||||
end
|
||||
|
||||
# Extract takes an arbitrary string of input data and returns an array of
|
||||
# tokenized entities, provided there were any available to extract. This
|
||||
# makes for easy processing of datagrams using a pattern like:
|
||||
#
|
||||
# tokenizer.extract(data).map { |entity| Decode(entity) }.each do ...
|
||||
#
|
||||
# Using -1 makes split to return "" if the token is at the end of
|
||||
# the string, meaning the last element is the start of the next chunk.
|
||||
def extract(data)
|
||||
if @trim > 0
|
||||
tail_end = @tail.slice!(-@trim, @trim) # returns nil if string is too short
|
||||
data = tail_end + data if tail_end
|
||||
end
|
||||
|
||||
@input << @tail
|
||||
entities = data.split(@delimiter, -1)
|
||||
@tail = entities.shift
|
||||
|
||||
unless entities.empty?
|
||||
@input << @tail
|
||||
entities.unshift @input.join
|
||||
@input.clear
|
||||
@tail = entities.pop
|
||||
end
|
||||
|
||||
entities
|
||||
end
|
||||
|
||||
# Flush the contents of the input buffer, i.e. return the input buffer even though
|
||||
# a token has not yet been encountered
|
||||
def flush
|
||||
@input << @tail
|
||||
buffer = @input.join
|
||||
@input.clear
|
||||
@tail = "" # @tail.clear is slightly faster, but not supported on 1.8.7
|
||||
buffer
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
module EventMachine
|
||||
# Utility method for coercing arguments to an object that responds to :call.
|
||||
# Accepts an object and a method name to send to, or a block, or an object
|
||||
# that responds to :call.
|
||||
#
|
||||
# @example EventMachine.Callback used with a block. Returns that block.
|
||||
#
|
||||
# cb = EventMachine.Callback do |msg|
|
||||
# puts(msg)
|
||||
# end
|
||||
# # returned object is a callable
|
||||
# cb.call('hello world')
|
||||
#
|
||||
#
|
||||
# @example EventMachine.Callback used with an object (to be more specific, class object) and a method name, returns an object that responds to #call
|
||||
#
|
||||
# cb = EventMachine.Callback(Object, :puts)
|
||||
# # returned object is a callable that delegates to Kernel#puts (in this case Object.puts)
|
||||
# cb.call('hello world')
|
||||
#
|
||||
#
|
||||
# @example EventMachine.Callback used with an object that responds to #call. Returns the argument.
|
||||
#
|
||||
# cb = EventMachine.Callback(proc{ |msg| puts(msg) })
|
||||
# # returned object is a callable
|
||||
# cb.call('hello world')
|
||||
#
|
||||
#
|
||||
# @overload Callback(object, method)
|
||||
# Wraps `method` invocation on `object` into an object that responds to #call that proxies all the arguments to that method
|
||||
# @param [Object] Object to invoke method on
|
||||
# @param [Symbol] Method name
|
||||
# @return [<#call>] An object that responds to #call that takes any number of arguments and invokes method on object with those arguments
|
||||
#
|
||||
# @overload Callback(object)
|
||||
# Returns callable object as is, without any coercion
|
||||
# @param [<#call>] An object that responds to #call
|
||||
# @return [<#call>] Its argument
|
||||
#
|
||||
# @overload Callback(&block)
|
||||
# Returns block passed to it without any coercion
|
||||
# @return [<#call>] Block passed to this method
|
||||
#
|
||||
# @raise [ArgumentError] When argument doesn't respond to #call, method name is missing or when invoked without arguments and block isn't given
|
||||
#
|
||||
# @return [<#call>]
|
||||
def self.Callback(object = nil, method = nil, &blk)
|
||||
if object && method
|
||||
lambda { |*args| object.__send__ method, *args }
|
||||
else
|
||||
if object.respond_to? :call
|
||||
object
|
||||
else
|
||||
blk || raise(ArgumentError)
|
||||
end # if
|
||||
end # if
|
||||
end # self.Callback
|
||||
end # EventMachine
|
||||
@@ -0,0 +1,69 @@
|
||||
module EventMachine
|
||||
# Provides a simple thread-safe way to transfer data between (typically) long running
|
||||
# tasks in {EventMachine.defer} and event loop thread.
|
||||
#
|
||||
# @example
|
||||
#
|
||||
# channel = EventMachine::Channel.new
|
||||
# sid = channel.subscribe { |msg| p [:got, msg] }
|
||||
#
|
||||
# channel.push('hello world')
|
||||
# channel.unsubscribe(sid)
|
||||
#
|
||||
#
|
||||
class Channel
|
||||
def initialize
|
||||
@subs = {}
|
||||
@uid = 0
|
||||
end
|
||||
|
||||
# Return the number of current subscribers.
|
||||
def num_subscribers
|
||||
return @subs.size
|
||||
end
|
||||
|
||||
# Takes any arguments suitable for EM::Callback() and returns a subscriber
|
||||
# id for use when unsubscribing.
|
||||
#
|
||||
# @return [Integer] Subscribe identifier
|
||||
# @see #unsubscribe
|
||||
def subscribe(*a, &b)
|
||||
name = gen_id
|
||||
EM.schedule { @subs[name] = EM::Callback(*a, &b) }
|
||||
|
||||
name
|
||||
end
|
||||
|
||||
# Removes subscriber from the list.
|
||||
#
|
||||
# @param [Integer] Subscriber identifier
|
||||
# @see #subscribe
|
||||
def unsubscribe(name)
|
||||
EM.schedule { @subs.delete name }
|
||||
end
|
||||
|
||||
# Add items to the channel, which are pushed out to all subscribers.
|
||||
def push(*items)
|
||||
items = items.dup
|
||||
EM.schedule { items.each { |i| @subs.values.each { |s| s.call i } } }
|
||||
end
|
||||
alias << push
|
||||
|
||||
# Fetches one message from the channel.
|
||||
def pop(*a, &b)
|
||||
EM.schedule {
|
||||
name = subscribe do |*args|
|
||||
unsubscribe(name)
|
||||
EM::Callback(*a, &b).call(*args)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @private
|
||||
def gen_id
|
||||
@uid += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,304 @@
|
||||
# = EM::Completion
|
||||
#
|
||||
# A completion is a callback container for various states of completion. In
|
||||
# its most basic form it has a start state and a finish state.
|
||||
#
|
||||
# This implementation includes some hold-back from the EM::Deferrable
|
||||
# interface in order to be compatible - but it has a much cleaner
|
||||
# implementation.
|
||||
#
|
||||
# In general it is preferred that this implementation be used as a state
|
||||
# callback container than EM::DefaultDeferrable or other classes including
|
||||
# EM::Deferrable. This is because it is generally more sane to keep this level
|
||||
# of state in a dedicated state-back container. This generally leads to more
|
||||
# malleable interfaces and software designs, as well as eradicating nasty bugs
|
||||
# that result from abstraction leakage.
|
||||
#
|
||||
# == Basic Usage
|
||||
#
|
||||
# As already mentioned, the basic usage of a Completion is simply for its two
|
||||
# final states, :succeeded and :failed.
|
||||
#
|
||||
# An asynchronous operation will complete at some future point in time, and
|
||||
# users often want to react to this event. API authors will want to expose
|
||||
# some common interface to react to these events.
|
||||
#
|
||||
# In the following example, the user wants to know when a short lived
|
||||
# connection has completed its exchange with the remote server. The simple
|
||||
# protocol just waits for an ack to its message.
|
||||
#
|
||||
# class Protocol < EM::Connection
|
||||
# include EM::P::LineText2
|
||||
#
|
||||
# def initialize(message, completion)
|
||||
# @message, @completion = message, completion
|
||||
# @completion.completion { close_connection }
|
||||
# @completion.timeout(1, :timeout)
|
||||
# end
|
||||
#
|
||||
# def post_init
|
||||
# send_data(@message)
|
||||
# end
|
||||
#
|
||||
# def receive_line(line)
|
||||
# case line
|
||||
# when /ACK/i
|
||||
# @completion.succeed line
|
||||
# when /ERR/i
|
||||
# @completion.fail :error, line
|
||||
# else
|
||||
# @completion.fail :unknown, line
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def unbind
|
||||
# @completion.fail :disconnected unless @completion.completed?
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class API
|
||||
# attr_reader :host, :port
|
||||
#
|
||||
# def initialize(host = 'example.org', port = 8000)
|
||||
# @host, @port = host, port
|
||||
# end
|
||||
#
|
||||
# def request(message)
|
||||
# completion = EM::Deferrable::Completion.new
|
||||
# EM.connect(host, port, Protocol, message, completion)
|
||||
# completion
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# api = API.new
|
||||
# completion = api.request('stuff')
|
||||
# completion.callback do |line|
|
||||
# puts "API responded with: #{line}"
|
||||
# end
|
||||
# completion.errback do |type, line|
|
||||
# case type
|
||||
# when :error
|
||||
# puts "API error: #{line}"
|
||||
# when :unknown
|
||||
# puts "API returned unknown response: #{line}"
|
||||
# when :disconnected
|
||||
# puts "API server disconnected prematurely"
|
||||
# when :timeout
|
||||
# puts "API server did not respond in a timely fashion"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# == Advanced Usage
|
||||
#
|
||||
# This completion implementation also supports more state callbacks and
|
||||
# arbitrary states (unlike the original Deferrable API). This allows for basic
|
||||
# stateful process encapsulation. One might use this to setup state callbacks
|
||||
# for various states in an exchange like in the basic usage example, except
|
||||
# where the applicaiton could be made to react to "connected" and
|
||||
# "disconnected" states additionally.
|
||||
#
|
||||
# class Protocol < EM::Connection
|
||||
# def initialize(completion)
|
||||
# @response = []
|
||||
# @completion = completion
|
||||
# @completion.stateback(:disconnected) do
|
||||
# @completion.succeed @response.join
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def connection_completed
|
||||
# @host, @port = Socket.unpack_sockaddr_in get_peername
|
||||
# @completion.change_state(:connected, @host, @port)
|
||||
# send_data("GET http://example.org/ HTTP/1.0\r\n\r\n")
|
||||
# end
|
||||
#
|
||||
# def receive_data(data)
|
||||
# @response << data
|
||||
# end
|
||||
#
|
||||
# def unbind
|
||||
# @completion.change_state(:disconnected, @host, @port)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# completion = EM::Deferrable::Completion.new
|
||||
# completion.stateback(:connected) do |host, port|
|
||||
# puts "Connected to #{host}:#{port}"
|
||||
# end
|
||||
# completion.stateback(:disconnected) do |host, port|
|
||||
# puts "Disconnected from #{host}:#{port}"
|
||||
# end
|
||||
# completion.callback do |response|
|
||||
# puts response
|
||||
# end
|
||||
#
|
||||
# EM.connect('example.org', 80, Protocol, completion)
|
||||
#
|
||||
# == Timeout
|
||||
#
|
||||
# The Completion also has a timeout. The timeout is global and is not aware of
|
||||
# states apart from completion states. The timeout is only engaged if #timeout
|
||||
# is called, and it will call fail if it is reached.
|
||||
#
|
||||
# == Completion states
|
||||
#
|
||||
# By default there are two completion states, :succeeded and :failed. These
|
||||
# states can be modified by subclassing and overrding the #completion_states
|
||||
# method. Completion states are special, in that callbacks for all completion
|
||||
# states are explcitly cleared when a completion state is entered. This
|
||||
# prevents errors that could arise from accidental unterminated timeouts, and
|
||||
# other such user errors.
|
||||
#
|
||||
# == Other notes
|
||||
#
|
||||
# Several APIs have been carried over from EM::Deferrable for compatibility
|
||||
# reasons during a transitionary period. Specifically cancel_errback and
|
||||
# cancel_callback are implemented, but their usage is to be strongly
|
||||
# discouraged. Due to the already complex nature of reaction systems, dynamic
|
||||
# callback deletion only makes the problem much worse. It is always better to
|
||||
# add correct conditionals to the callback code, or use more states, than to
|
||||
# address such implementaiton issues with conditional callbacks.
|
||||
|
||||
module EventMachine
|
||||
|
||||
class Completion
|
||||
# This is totally not used (re-implemented), it's here in case people check
|
||||
# for kind_of?
|
||||
include EventMachine::Deferrable
|
||||
|
||||
attr_reader :state, :value
|
||||
|
||||
def initialize
|
||||
@state = :unknown
|
||||
@callbacks = Hash.new { |h,k| h[k] = [] }
|
||||
@value = []
|
||||
@timeout_timer = nil
|
||||
end
|
||||
|
||||
# Enter the :succeeded state, setting the result value if given.
|
||||
def succeed(*args)
|
||||
change_state(:succeeded, *args)
|
||||
end
|
||||
# The old EM method:
|
||||
alias set_deferred_success succeed
|
||||
|
||||
# Enter the :failed state, setting the result value if given.
|
||||
def fail(*args)
|
||||
change_state(:failed, *args)
|
||||
end
|
||||
# The old EM method:
|
||||
alias set_deferred_failure fail
|
||||
|
||||
# Statebacks are called when you enter (or are in) the named state.
|
||||
def stateback(state, *a, &b)
|
||||
# The following is quite unfortunate special casing for :completed
|
||||
# statebacks, but it's a necessary evil for latent completion
|
||||
# definitions.
|
||||
|
||||
if :completed == state || !completed? || @state == state
|
||||
@callbacks[state] << EM::Callback(*a, &b)
|
||||
end
|
||||
execute_callbacks
|
||||
self
|
||||
end
|
||||
|
||||
# Callbacks are called when you enter (or are in) a :succeeded state.
|
||||
def callback(*a, &b)
|
||||
stateback(:succeeded, *a, &b)
|
||||
end
|
||||
|
||||
# Errbacks are called when you enter (or are in) a :failed state.
|
||||
def errback(*a, &b)
|
||||
stateback(:failed, *a, &b)
|
||||
end
|
||||
|
||||
# Completions are called when you enter (or are in) either a :failed or a
|
||||
# :succeeded state. They are stored as a special (reserved) state called
|
||||
# :completed.
|
||||
def completion(*a, &b)
|
||||
stateback(:completed, *a, &b)
|
||||
end
|
||||
|
||||
# Enter a new state, setting the result value if given. If the state is one
|
||||
# of :succeeded or :failed, then :completed callbacks will also be called.
|
||||
def change_state(state, *args)
|
||||
@value = args
|
||||
@state = state
|
||||
|
||||
EM.schedule { execute_callbacks }
|
||||
end
|
||||
|
||||
# The old EM method:
|
||||
alias set_deferred_status change_state
|
||||
|
||||
# Indicates that we've reached some kind of completion state, by default
|
||||
# this is :succeeded or :failed. Due to these semantics, the :completed
|
||||
# state is reserved for internal use.
|
||||
def completed?
|
||||
completion_states.any? { |s| state == s }
|
||||
end
|
||||
|
||||
# Completion states simply returns a list of completion states, by default
|
||||
# this is :succeeded and :failed.
|
||||
def completion_states
|
||||
[:succeeded, :failed]
|
||||
end
|
||||
|
||||
# Schedule a time which if passes before we enter a completion state, this
|
||||
# deferrable will be failed with the given arguments.
|
||||
def timeout(time, *args)
|
||||
cancel_timeout
|
||||
@timeout_timer = EM::Timer.new(time) do
|
||||
fail(*args) unless completed?
|
||||
end
|
||||
end
|
||||
|
||||
# Disable the timeout
|
||||
def cancel_timeout
|
||||
if @timeout_timer
|
||||
@timeout_timer.cancel
|
||||
@timeout_timer = nil
|
||||
end
|
||||
end
|
||||
|
||||
# Remove an errback. N.B. Some errbacks cannot be deleted. Usage is NOT
|
||||
# recommended, this is an anti-pattern.
|
||||
def cancel_errback(*a, &b)
|
||||
@callbacks[:failed].delete(EM::Callback(*a, &b))
|
||||
end
|
||||
|
||||
# Remove a callback. N.B. Some callbacks cannot be deleted. Usage is NOT
|
||||
# recommended, this is an anti-pattern.
|
||||
def cancel_callback(*a, &b)
|
||||
@callbacks[:succeeded].delete(EM::Callback(*a, &b))
|
||||
end
|
||||
|
||||
private
|
||||
# Execute all callbacks for the current state. If in a completed state, then
|
||||
# call any statebacks associated with the completed state.
|
||||
def execute_callbacks
|
||||
execute_state_callbacks(state)
|
||||
if completed?
|
||||
execute_state_callbacks(:completed)
|
||||
clear_dead_callbacks
|
||||
cancel_timeout
|
||||
end
|
||||
end
|
||||
|
||||
# Iterate all callbacks for a given state, and remove then call them.
|
||||
def execute_state_callbacks(state)
|
||||
while callback = @callbacks[state].shift
|
||||
callback.call(*value)
|
||||
end
|
||||
end
|
||||
|
||||
# If we enter a completion state, clear other completion states after all
|
||||
# callback chains are completed. This means that operation specific
|
||||
# callbacks can't be dual-called, which is most common user error.
|
||||
def clear_dead_callbacks
|
||||
completion_states.each do |state|
|
||||
@callbacks[state].clear
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,770 @@
|
||||
module EventMachine
|
||||
class FileNotFoundException < Exception
|
||||
end
|
||||
|
||||
# EventMachine::Connection is a class that is instantiated
|
||||
# by EventMachine's processing loop whenever a new connection
|
||||
# is created. (New connections can be either initiated locally
|
||||
# to a remote server or accepted locally from a remote client.)
|
||||
# When a Connection object is instantiated, it <i>mixes in</i>
|
||||
# the functionality contained in the user-defined module
|
||||
# specified in calls to {EventMachine.connect} or {EventMachine.start_server}.
|
||||
# User-defined handler modules may redefine any or all of the standard
|
||||
# methods defined here, as well as add arbitrary additional code
|
||||
# that will also be mixed in.
|
||||
#
|
||||
# EventMachine manages one object inherited from EventMachine::Connection
|
||||
# (and containing the mixed-in user code) for every network connection
|
||||
# that is active at any given time.
|
||||
# The event loop will automatically call methods on EventMachine::Connection
|
||||
# objects whenever specific events occur on the corresponding connections,
|
||||
# as described below.
|
||||
#
|
||||
# This class is never instantiated by user code, and does not publish an
|
||||
# initialize method. The instance methods of EventMachine::Connection
|
||||
# which may be called by the event loop are:
|
||||
#
|
||||
# * {#post_init}
|
||||
# * {#connection_completed}
|
||||
# * {#receive_data}
|
||||
# * {#unbind}
|
||||
# * {#ssl_verify_peer} (if TLS is used)
|
||||
# * {#ssl_handshake_completed}
|
||||
#
|
||||
# All of the other instance methods defined here are called only by user code.
|
||||
#
|
||||
# @see file:docs/GettingStarted.md EventMachine tutorial
|
||||
class Connection
|
||||
# @private
|
||||
attr_accessor :signature
|
||||
|
||||
# @private
|
||||
alias original_method method
|
||||
|
||||
# Override .new so subclasses don't have to call super and can ignore
|
||||
# connection-specific arguments
|
||||
#
|
||||
# @private
|
||||
def self.new(sig, *args)
|
||||
allocate.instance_eval do
|
||||
# Store signature
|
||||
@signature = sig
|
||||
# associate_callback_target sig
|
||||
|
||||
# Call a superclass's #initialize if it has one
|
||||
initialize(*args)
|
||||
|
||||
# post initialize callback
|
||||
post_init
|
||||
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
# Stubbed initialize so legacy superclasses can safely call super
|
||||
#
|
||||
# @private
|
||||
def initialize(*args)
|
||||
end
|
||||
|
||||
# Called by the event loop immediately after the network connection has been established,
|
||||
# and before resumption of the network loop.
|
||||
# This method is generally not called by user code, but is called automatically
|
||||
# by the event loop. The base-class implementation is a no-op.
|
||||
# This is a very good place to initialize instance variables that will
|
||||
# be used throughout the lifetime of the network connection.
|
||||
#
|
||||
# @see #connection_completed
|
||||
# @see #unbind
|
||||
# @see #send_data
|
||||
# @see #receive_data
|
||||
def post_init
|
||||
end
|
||||
|
||||
# Called by the event loop whenever data has been received by the network connection.
|
||||
# It is never called by user code. {#receive_data} is called with a single parameter, a String containing
|
||||
# the network protocol data, which may of course be binary. You will
|
||||
# generally redefine this method to perform your own processing of the incoming data.
|
||||
#
|
||||
# Here's a key point which is essential to understanding the event-driven
|
||||
# programming model: <i>EventMachine knows absolutely nothing about the protocol
|
||||
# which your code implements.</i> You must not make any assumptions about
|
||||
# the size of the incoming data packets, or about their alignment on any
|
||||
# particular intra-message or PDU boundaries (such as line breaks).
|
||||
# receive_data can and will send you arbitrary chunks of data, with the
|
||||
# only guarantee being that the data is presented to your code in the order
|
||||
# it was collected from the network. Don't even assume that the chunks of
|
||||
# data will correspond to network packets, as EventMachine can and will coalesce
|
||||
# several incoming packets into one, to improve performance. The implication for your
|
||||
# code is that you generally will need to implement some kind of a state machine
|
||||
# in your redefined implementation of receive_data. For a better understanding
|
||||
# of this, read through the examples of specific protocol handlers in EventMachine::Protocols
|
||||
#
|
||||
# The base-class implementation (which will be invoked only if you didn't override it in your protocol handler)
|
||||
# simply prints incoming data packet size to stdout.
|
||||
#
|
||||
# @param [String] data Opaque incoming data.
|
||||
# @note Depending on the protocol, buffer sizes and OS networking stack configuration, incoming data may or may not be "a complete message".
|
||||
# It is up to this handler to detect content boundaries to determine whether all the content (for example, full HTTP request)
|
||||
# has been received and can be processed.
|
||||
#
|
||||
# @see #post_init
|
||||
# @see #connection_completed
|
||||
# @see #unbind
|
||||
# @see #send_data
|
||||
# @see file:docs/GettingStarted.md EventMachine tutorial
|
||||
def receive_data data
|
||||
puts "............>>>#{data.length}"
|
||||
end
|
||||
|
||||
# Called by EventMachine when the SSL/TLS handshake has
|
||||
# been completed, as a result of calling #start_tls to initiate SSL/TLS on the connection.
|
||||
#
|
||||
# This callback exists because {#post_init} and {#connection_completed} are **not** reliable
|
||||
# for indicating when an SSL/TLS connection is ready to have its certificate queried for.
|
||||
#
|
||||
# @see #get_peer_cert
|
||||
def ssl_handshake_completed
|
||||
end
|
||||
|
||||
# Called by EventMachine when :verify_peer => true has been passed to {#start_tls}.
|
||||
# It will be called with each certificate in the certificate chain provided by the remote peer.
|
||||
#
|
||||
# The cert will be passed as a String in PEM format, the same as in {#get_peer_cert}. It is up to user defined
|
||||
# code to perform a check on the certificates. The return value from this callback is used to accept or deny the peer.
|
||||
# A return value that is not nil or false triggers acceptance. If the peer is not accepted, the connection
|
||||
# will be subsequently closed.
|
||||
#
|
||||
# @example This server always accepts all peers
|
||||
#
|
||||
# module AcceptServer
|
||||
# def post_init
|
||||
# start_tls(:verify_peer => true)
|
||||
# end
|
||||
#
|
||||
# def ssl_verify_peer(cert)
|
||||
# true
|
||||
# end
|
||||
#
|
||||
# def ssl_handshake_completed
|
||||
# $server_handshake_completed = true
|
||||
# end
|
||||
# end
|
||||
#
|
||||
#
|
||||
# @example This server never accepts any peers
|
||||
#
|
||||
# module DenyServer
|
||||
# def post_init
|
||||
# start_tls(:verify_peer => true)
|
||||
# end
|
||||
#
|
||||
# def ssl_verify_peer(cert)
|
||||
# # Do not accept the peer. This should now cause the connection to shut down
|
||||
# # without the SSL handshake being completed.
|
||||
# false
|
||||
# end
|
||||
#
|
||||
# def ssl_handshake_completed
|
||||
# $server_handshake_completed = true
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# @see #start_tls
|
||||
def ssl_verify_peer(cert)
|
||||
end
|
||||
|
||||
# called by the framework whenever a connection (either a server or client connection) is closed.
|
||||
# The close can occur because your code intentionally closes it (using {#close_connection} and {#close_connection_after_writing}),
|
||||
# because the remote peer closed the connection, or because of a network error.
|
||||
# You may not assume that the network connection is still open and able to send or
|
||||
# receive data when the callback to unbind is made. This is intended only to give
|
||||
# you a chance to clean up associations your code may have made to the connection
|
||||
# object while it was open.
|
||||
#
|
||||
# If you want to detect which peer has closed the connection, you can override {#close_connection} in your protocol handler
|
||||
# and set an @ivar.
|
||||
#
|
||||
# @example Overriding Connection#close_connection to distinguish connections closed on our side
|
||||
#
|
||||
# class MyProtocolHandler < EventMachine::Connection
|
||||
#
|
||||
# # ...
|
||||
#
|
||||
# def close_connection(*args)
|
||||
# @intentionally_closed_connection = true
|
||||
# super(*args)
|
||||
# end
|
||||
#
|
||||
# def unbind
|
||||
# if @intentionally_closed_connection
|
||||
# # ...
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# # ...
|
||||
#
|
||||
# end
|
||||
#
|
||||
# @see #post_init
|
||||
# @see #connection_completed
|
||||
# @see file:docs/GettingStarted.md EventMachine tutorial
|
||||
def unbind
|
||||
end
|
||||
|
||||
# Called by the reactor after attempting to relay incoming data to a descriptor (set as a proxy target descriptor with
|
||||
# {EventMachine.enable_proxy}) that has already been closed.
|
||||
#
|
||||
# @see EventMachine.enable_proxy
|
||||
def proxy_target_unbound
|
||||
end
|
||||
|
||||
# called when the reactor finished proxying all
|
||||
# of the requested bytes.
|
||||
def proxy_completed
|
||||
end
|
||||
|
||||
# EventMachine::Connection#proxy_incoming_to is called only by user code. It sets up
|
||||
# a low-level proxy relay for all data inbound for this connection, to the connection given
|
||||
# as the argument. This is essentially just a helper method for enable_proxy.
|
||||
#
|
||||
# @see EventMachine.enable_proxy
|
||||
def proxy_incoming_to(conn,bufsize=0)
|
||||
EventMachine::enable_proxy(self, conn, bufsize)
|
||||
end
|
||||
|
||||
# A helper method for {EventMachine.disable_proxy}
|
||||
def stop_proxying
|
||||
EventMachine::disable_proxy(self)
|
||||
end
|
||||
|
||||
# The number of bytes proxied to another connection. Reset to zero when
|
||||
# EventMachine::Connection#proxy_incoming_to is called, and incremented whenever data is proxied.
|
||||
def get_proxied_bytes
|
||||
EventMachine::get_proxied_bytes(@signature)
|
||||
end
|
||||
|
||||
# EventMachine::Connection#close_connection is called only by user code, and never
|
||||
# by the event loop. You may call this method against a connection object in any
|
||||
# callback handler, whether or not the callback was made against the connection
|
||||
# you want to close. close_connection <i>schedules</i> the connection to be closed
|
||||
# at the next available opportunity within the event loop. You may not assume that
|
||||
# the connection is closed when close_connection returns. In particular, the framework
|
||||
# will callback the unbind method for the particular connection at a point shortly
|
||||
# after you call close_connection. You may assume that the unbind callback will
|
||||
# take place sometime after your call to close_connection completes. In other words,
|
||||
# the unbind callback will not re-enter your code "inside" of your call to close_connection.
|
||||
# However, it's not guaranteed that a future version of EventMachine will not change
|
||||
# this behavior.
|
||||
#
|
||||
# {#close_connection} will *silently discard* any outbound data which you have
|
||||
# sent to the connection using {EventMachine::Connection#send_data} but which has not
|
||||
# yet been sent across the network. If you want to avoid this behavior, use
|
||||
# {EventMachine::Connection#close_connection_after_writing}.
|
||||
#
|
||||
def close_connection after_writing = false
|
||||
EventMachine::close_connection @signature, after_writing
|
||||
end
|
||||
|
||||
# Removes given connection from the event loop.
|
||||
# The connection's socket remains open and its file descriptor number is returned.
|
||||
def detach
|
||||
EventMachine::detach_fd @signature
|
||||
end
|
||||
|
||||
def get_sock_opt level, option
|
||||
EventMachine::get_sock_opt @signature, level, option
|
||||
end
|
||||
|
||||
def set_sock_opt level, optname, optval
|
||||
EventMachine::set_sock_opt @signature, level, optname, optval
|
||||
end
|
||||
|
||||
# A variant of {#close_connection}.
|
||||
# All of the descriptive comments given for close_connection also apply to
|
||||
# close_connection_after_writing, *with one exception*: if the connection has
|
||||
# outbound data sent using send_dat but which has not yet been sent across the network,
|
||||
# close_connection_after_writing will schedule the connection to be closed *after*
|
||||
# all of the outbound data has been safely written to the remote peer.
|
||||
#
|
||||
# Depending on the amount of outgoing data and the speed of the network,
|
||||
# considerable time may elapse between your call to close_connection_after_writing
|
||||
# and the actual closing of the socket (at which time the unbind callback will be called
|
||||
# by the event loop). During this time, you *may not* call send_data to transmit
|
||||
# additional data (that is, the connection is closed for further writes). In very
|
||||
# rare cases, you may experience a receive_data callback after your call to {#close_connection_after_writing},
|
||||
# depending on whether incoming data was in the process of being received on the connection
|
||||
# at the moment when you called {#close_connection_after_writing}. Your protocol handler must
|
||||
# be prepared to properly deal with such data (probably by ignoring it).
|
||||
#
|
||||
# @see #close_connection
|
||||
# @see #send_data
|
||||
def close_connection_after_writing
|
||||
close_connection true
|
||||
end
|
||||
|
||||
# Call this method to send data to the remote end of the network connection. It takes a single String argument,
|
||||
# which may contain binary data. Data is buffered to be sent at the end of this event loop tick (cycle).
|
||||
#
|
||||
# When used in a method that is event handler (for example, {#post_init} or {#connection_completed}, it will send
|
||||
# data to the other end of the connection that generated the event.
|
||||
# You can also call {#send_data} to write to other connections. For more information see The Chat Server Example in the
|
||||
# {file:docs/GettingStarted.md EventMachine tutorial}.
|
||||
#
|
||||
# If you want to send some data and then immediately close the connection, make sure to use {#close_connection_after_writing}
|
||||
# instead of {#close_connection}.
|
||||
#
|
||||
#
|
||||
# @param [String] data Data to send asynchronously
|
||||
#
|
||||
# @see file:docs/GettingStarted.md EventMachine tutorial
|
||||
# @see Connection#receive_data
|
||||
# @see Connection#post_init
|
||||
# @see Connection#unbind
|
||||
def send_data data
|
||||
data = data.to_s
|
||||
size = data.bytesize if data.respond_to?(:bytesize)
|
||||
size ||= data.size
|
||||
EventMachine::send_data @signature, data, size
|
||||
end
|
||||
|
||||
# Returns true if the connection is in an error state, false otherwise.
|
||||
#
|
||||
# In general, you can detect the occurrence of communication errors or unexpected
|
||||
# disconnection by the remote peer by handing the {#unbind} method. In some cases, however,
|
||||
# it's useful to check the status of the connection using {#error?} before attempting to send data.
|
||||
# This function is synchronous but it will return immediately without blocking.
|
||||
#
|
||||
# @return [Boolean] true if the connection is in an error state, false otherwise
|
||||
def error?
|
||||
errno = EventMachine::report_connection_error_status(@signature)
|
||||
case errno
|
||||
when 0
|
||||
false
|
||||
when -1
|
||||
true
|
||||
else
|
||||
EventMachine::ERRNOS[errno]
|
||||
end
|
||||
end
|
||||
|
||||
# Called by the event loop when a remote TCP connection attempt completes successfully.
|
||||
# You can expect to get this notification after calls to {EventMachine.connect}. Remember that EventMachine makes remote connections
|
||||
# asynchronously, just as with any other kind of network event. This method
|
||||
# is intended primarily to assist with network diagnostics. For normal protocol
|
||||
# handling, use #post_init to perform initial work on a new connection (such as sending initial set of data).
|
||||
# {Connection#post_init} will always be called. This method will only be called in case of a successful completion.
|
||||
# A connection attempt which fails will result a call to {Connection#unbind} after the failure.
|
||||
#
|
||||
# @see Connection#post_init
|
||||
# @see Connection#unbind
|
||||
# @see file:docs/GettingStarted.md EventMachine tutorial
|
||||
def connection_completed
|
||||
end
|
||||
|
||||
# Call {#start_tls} at any point to initiate TLS encryption on connected streams.
|
||||
# The method is smart enough to know whether it should perform a server-side
|
||||
# or a client-side handshake. An appropriate place to call {#start_tls} is in
|
||||
# your redefined {#post_init} method, or in the {#connection_completed} handler for
|
||||
# an outbound connection.
|
||||
#
|
||||
#
|
||||
# @option args [String] :cert_chain_file (nil) local path of a readable file that contants a chain of X509 certificates in
|
||||
# the [PEM format](http://en.wikipedia.org/wiki/Privacy_Enhanced_Mail),
|
||||
# with the most-resolved certificate at the top of the file, successive intermediate
|
||||
# certs in the middle, and the root (or CA) cert at the bottom.
|
||||
#
|
||||
# @option args [String] :private_key_file (nil) local path of a readable file that must contain a private key in the [PEM format](http://en.wikipedia.org/wiki/Privacy_Enhanced_Mail).
|
||||
#
|
||||
# @option args [Boolean] :verify_peer (false) indicates whether a server should request a certificate from a peer, to be verified by user code.
|
||||
# If true, the {#ssl_verify_peer} callback on the {EventMachine::Connection} object is called with each certificate
|
||||
# in the certificate chain provided by the peer. See documentation on {#ssl_verify_peer} for how to use this.
|
||||
#
|
||||
# @option args [Boolean] :fail_if_no_peer_cert (false) Used in conjunction with verify_peer. If set the SSL handshake will be terminated if the peer does not provide a certificate.
|
||||
#
|
||||
#
|
||||
# @option args [String] :cipher_list ("ALL:!ADH:!LOW:!EXP:!DES-CBC3-SHA:@STRENGTH") indicates the available SSL cipher values. Default value is "ALL:!ADH:!LOW:!EXP:!DES-CBC3-SHA:@STRENGTH". Check the format of the OpenSSL cipher string at http://www.openssl.org/docs/apps/ciphers.html#CIPHER_LIST_FORMAT.
|
||||
#
|
||||
# @option args [String] :ecdh_curve (nil) The curve for ECDHE ciphers. See available ciphers with 'openssl ecparam -list_curves'
|
||||
#
|
||||
# @option args [String] :dhparam (nil) The local path of a file containing DH parameters for EDH ciphers in [PEM format](http://en.wikipedia.org/wiki/Privacy_Enhanced_Mail) See: 'openssl dhparam'
|
||||
#
|
||||
# @option args [Array] :ssl_version (TLSv1 TLSv1_1 TLSv1_2) indicates the allowed SSL/TLS versions. Possible values are: {SSLv2}, {SSLv3}, {TLSv1}, {TLSv1_1}, {TLSv1_2}.
|
||||
#
|
||||
# @example Using TLS with EventMachine
|
||||
#
|
||||
# require 'rubygems'
|
||||
# require 'eventmachine'
|
||||
#
|
||||
# module Handler
|
||||
# def post_init
|
||||
# start_tls(:private_key_file => '/tmp/server.key', :cert_chain_file => '/tmp/server.crt', :verify_peer => false)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# EventMachine.run do
|
||||
# EventMachine.start_server("127.0.0.1", 9999, Handler)
|
||||
# end
|
||||
#
|
||||
# @param [Hash] args
|
||||
#
|
||||
# @todo support passing an encryption parameter, which can be string or Proc, to get a passphrase
|
||||
# for encrypted private keys.
|
||||
# @todo support passing key material via raw strings or Procs that return strings instead of
|
||||
# just filenames.
|
||||
#
|
||||
# @see #ssl_verify_peer
|
||||
def start_tls args={}
|
||||
priv_key = args[:private_key_file]
|
||||
cert_chain = args[:cert_chain_file]
|
||||
verify_peer = args[:verify_peer]
|
||||
sni_hostname = args[:sni_hostname]
|
||||
cipher_list = args[:cipher_list]
|
||||
ssl_version = args[:ssl_version]
|
||||
ecdh_curve = args[:ecdh_curve]
|
||||
dhparam = args[:dhparam]
|
||||
fail_if_no_peer_cert = args[:fail_if_no_peer_cert]
|
||||
|
||||
[priv_key, cert_chain].each do |file|
|
||||
next if file.nil? or file.empty?
|
||||
raise FileNotFoundException,
|
||||
"Could not find #{file} for start_tls" unless File.exist? file
|
||||
end
|
||||
|
||||
protocols_bitmask = 0
|
||||
if ssl_version.nil?
|
||||
protocols_bitmask |= EventMachine::EM_PROTO_TLSv1
|
||||
protocols_bitmask |= EventMachine::EM_PROTO_TLSv1_1
|
||||
protocols_bitmask |= EventMachine::EM_PROTO_TLSv1_2
|
||||
else
|
||||
[ssl_version].flatten.each do |p|
|
||||
case p.to_s.downcase
|
||||
when 'sslv2'
|
||||
protocols_bitmask |= EventMachine::EM_PROTO_SSLv2
|
||||
when 'sslv3'
|
||||
protocols_bitmask |= EventMachine::EM_PROTO_SSLv3
|
||||
when 'tlsv1'
|
||||
protocols_bitmask |= EventMachine::EM_PROTO_TLSv1
|
||||
when 'tlsv1_1'
|
||||
protocols_bitmask |= EventMachine::EM_PROTO_TLSv1_1
|
||||
when 'tlsv1_2'
|
||||
protocols_bitmask |= EventMachine::EM_PROTO_TLSv1_2
|
||||
else
|
||||
raise("Unrecognized SSL/TLS Protocol: #{p}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
EventMachine::set_tls_parms(@signature, priv_key || '', cert_chain || '', verify_peer, fail_if_no_peer_cert, sni_hostname || '', cipher_list || '', ecdh_curve || '', dhparam || '', protocols_bitmask)
|
||||
EventMachine::start_tls @signature
|
||||
end
|
||||
|
||||
# If [TLS](http://en.wikipedia.org/wiki/Transport_Layer_Security) is active on the connection, returns the remote [X509 certificate](http://en.wikipedia.org/wiki/X.509)
|
||||
# as a string, in the popular [PEM format](http://en.wikipedia.org/wiki/Privacy_Enhanced_Mail). This can then be used for arbitrary validation
|
||||
# of a peer's certificate in your code.
|
||||
#
|
||||
# This should be called in/after the {#ssl_handshake_completed} callback, which indicates
|
||||
# that SSL/TLS is active. Using this callback is important, because the certificate may not
|
||||
# be available until the time it is executed. Using #post_init or #connection_completed is
|
||||
# not adequate, because the SSL handshake may still be taking place.
|
||||
#
|
||||
# This method will return `nil` if:
|
||||
#
|
||||
# * EventMachine is not built with [OpenSSL](http://www.openssl.org) support
|
||||
# * [TLS](http://en.wikipedia.org/wiki/Transport_Layer_Security) is not active on the connection
|
||||
# * TLS handshake is not yet complete
|
||||
# * Remote peer for any other reason has not presented a certificate
|
||||
#
|
||||
#
|
||||
# @example Getting peer TLS certificate information in EventMachine
|
||||
#
|
||||
# module Handler
|
||||
# def post_init
|
||||
# puts "Starting TLS"
|
||||
# start_tls
|
||||
# end
|
||||
#
|
||||
# def ssl_handshake_completed
|
||||
# puts get_peer_cert
|
||||
# close_connection
|
||||
# end
|
||||
#
|
||||
# def unbind
|
||||
# EventMachine::stop_event_loop
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# EventMachine.run do
|
||||
# EventMachine.connect "mail.google.com", 443, Handler
|
||||
# end
|
||||
#
|
||||
# # Will output:
|
||||
# # -----BEGIN CERTIFICATE-----
|
||||
# # MIIDIjCCAougAwIBAgIQbldpChBPqv+BdPg4iwgN8TANBgkqhkiG9w0BAQUFADBM
|
||||
# # MQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkg
|
||||
# # THRkLjEWMBQGA1UEAxMNVGhhd3RlIFNHQyBDQTAeFw0wODA1MDIxNjMyNTRaFw0w
|
||||
# # OTA1MDIxNjMyNTRaMGkxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh
|
||||
# # MRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRMwEQYDVQQKEwpHb29nbGUgSW5jMRgw
|
||||
# # FgYDVQQDEw9tYWlsLmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ
|
||||
# # AoGBALlkxdh2QXegdElukCSOV2+8PKiONIS+8Tu9K7MQsYpqtLNC860zwOPQ2NLI
|
||||
# # 3Zp4jwuXVTrtzGuiqf5Jioh35Ig3CqDXtLyZoypjZUQcq4mlLzHlhIQ4EhSjDmA7
|
||||
# # Ffw9y3ckSOQgdBQWNLbquHh9AbEUjmhkrYxIqKXeCnRKhv6nAgMBAAGjgecwgeQw
|
||||
# # KAYDVR0lBCEwHwYIKwYBBQUHAwEGCCsGAQUFBwMCBglghkgBhvhCBAEwNgYDVR0f
|
||||
# # BC8wLTAroCmgJ4YlaHR0cDovL2NybC50aGF3dGUuY29tL1RoYXd0ZVNHQ0NBLmNy
|
||||
# # bDByBggrBgEFBQcBAQRmMGQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9vY3NwLnRoYXd0
|
||||
# # ZS5jb20wPgYIKwYBBQUHMAKGMmh0dHA6Ly93d3cudGhhd3RlLmNvbS9yZXBvc2l0
|
||||
# # b3J5L1RoYXd0ZV9TR0NfQ0EuY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQEF
|
||||
# # BQADgYEAsRwpLg1dgCR1gYDK185MFGukXMeQFUvhGqF8eT/CjpdvezyKVuz84gSu
|
||||
# # 6ccMXgcPQZGQN/F4Xug+Q01eccJjRSVfdvR5qwpqCj+6BFl5oiKDBsveSkrmL5dz
|
||||
# # s2bn7TdTSYKcLeBkjXxDLHGBqLJ6TNCJ3c4/cbbG5JhGvoema94=
|
||||
# # -----END CERTIFICATE-----
|
||||
#
|
||||
# You can do whatever you want with the certificate String, such as load it
|
||||
# as a certificate object using the OpenSSL library, and check its fields.
|
||||
#
|
||||
# @return [String] the remote [X509 certificate](http://en.wikipedia.org/wiki/X.509), in the popular [PEM format](http://en.wikipedia.org/wiki/Privacy_Enhanced_Mail),
|
||||
# if TLS is active on the connection
|
||||
#
|
||||
# @see Connection#start_tls
|
||||
# @see Connection#ssl_handshake_completed
|
||||
def get_peer_cert
|
||||
EventMachine::get_peer_cert @signature
|
||||
end
|
||||
|
||||
def get_cipher_bits
|
||||
EventMachine::get_cipher_bits @signature
|
||||
end
|
||||
|
||||
def get_cipher_name
|
||||
EventMachine::get_cipher_name @signature
|
||||
end
|
||||
|
||||
def get_cipher_protocol
|
||||
EventMachine::get_cipher_protocol @signature
|
||||
end
|
||||
|
||||
def get_sni_hostname
|
||||
EventMachine::get_sni_hostname @signature
|
||||
end
|
||||
|
||||
# Sends UDP messages.
|
||||
#
|
||||
# This method may be called from any Connection object that refers
|
||||
# to an open datagram socket (see EventMachine#open_datagram_socket).
|
||||
# The method sends a UDP (datagram) packet containing the data you specify,
|
||||
# to a remote peer specified by the IP address and port that you give
|
||||
# as parameters to the method.
|
||||
# Observe that you may send a zero-length packet (empty string).
|
||||
# However, you may not send an arbitrarily-large data packet because
|
||||
# your operating system will enforce a platform-specific limit on
|
||||
# the size of the outbound packet. (Your kernel
|
||||
# will respond in a platform-specific way if you send an overlarge
|
||||
# packet: some will send a truncated packet, some will complain, and
|
||||
# some will silently drop your request).
|
||||
# On LANs, it's usually OK to send datagrams up to about 4000 bytes in length,
|
||||
# but to be really safe, send messages smaller than the Ethernet-packet
|
||||
# size (typically about 1400 bytes). Some very restrictive WANs
|
||||
# will either drop or truncate packets larger than about 500 bytes.
|
||||
#
|
||||
# @param [String] data Data to send asynchronously
|
||||
# @param [String] recipient_address IP address of the recipient
|
||||
# @param [String] recipient_port Port of the recipient
|
||||
def send_datagram data, recipient_address, recipient_port
|
||||
data = data.to_s
|
||||
size = data.bytesize if data.respond_to?(:bytesize)
|
||||
size ||= data.size
|
||||
EventMachine::send_datagram @signature, data, size, recipient_address, Integer(recipient_port)
|
||||
end
|
||||
|
||||
|
||||
# This method is used with stream-connections to obtain the identity
|
||||
# of the remotely-connected peer. If a peername is available, this method
|
||||
# returns a sockaddr structure. The method returns nil if no peername is available.
|
||||
# You can use Socket.unpack_sockaddr_in and its variants to obtain the
|
||||
# values contained in the peername structure returned from #get_peername.
|
||||
#
|
||||
# @example How to get peer IP address and port with EventMachine
|
||||
#
|
||||
# require 'socket'
|
||||
#
|
||||
# module Handler
|
||||
# def receive_data data
|
||||
# port, ip = Socket.unpack_sockaddr_in(get_peername)
|
||||
# puts "got #{data.inspect} from #{ip}:#{port}"
|
||||
# end
|
||||
# end
|
||||
def get_peername
|
||||
EventMachine::get_peername @signature
|
||||
end
|
||||
|
||||
# Used with stream-connections to obtain the identity
|
||||
# of the local side of the connection. If a local name is available, this method
|
||||
# returns a sockaddr structure. The method returns nil if no local name is available.
|
||||
# You can use {Socket.unpack_sockaddr_in} and its variants to obtain the
|
||||
# values contained in the local-name structure returned from this method.
|
||||
#
|
||||
# @example
|
||||
#
|
||||
# require 'socket'
|
||||
#
|
||||
# module Handler
|
||||
# def receive_data data
|
||||
# port, ip = Socket.unpack_sockaddr_in(get_sockname)
|
||||
# puts "got #{data.inspect}"
|
||||
# end
|
||||
# end
|
||||
def get_sockname
|
||||
EventMachine::get_sockname @signature
|
||||
end
|
||||
|
||||
# Returns the PID (kernel process identifier) of a subprocess
|
||||
# associated with this Connection object. For use with {EventMachine.popen}
|
||||
# and similar methods. Returns nil when there is no meaningful subprocess.
|
||||
#
|
||||
# @return [Integer]
|
||||
def get_pid
|
||||
EventMachine::get_subprocess_pid @signature
|
||||
end
|
||||
|
||||
# Returns a subprocess exit status. Only useful for {EventMachine.popen}. Call it in your
|
||||
# {#unbind} handler.
|
||||
#
|
||||
# @return [Integer]
|
||||
def get_status
|
||||
EventMachine::get_subprocess_status @signature
|
||||
end
|
||||
|
||||
# The number of seconds since the last send/receive activity on this connection.
|
||||
def get_idle_time
|
||||
EventMachine::get_idle_time @signature
|
||||
end
|
||||
|
||||
# comm_inactivity_timeout returns the current value (float in seconds) of the inactivity-timeout
|
||||
# property of network-connection and datagram-socket objects. A nonzero value
|
||||
# indicates that the connection or socket will automatically be closed if no read or write
|
||||
# activity takes place for at least that number of seconds.
|
||||
# A zero value (the default) specifies that no automatic timeout will take place.
|
||||
def comm_inactivity_timeout
|
||||
EventMachine::get_comm_inactivity_timeout @signature
|
||||
end
|
||||
|
||||
# Allows you to set the inactivity-timeout property for
|
||||
# a network connection or datagram socket. Specify a non-negative float value in seconds.
|
||||
# If the value is greater than zero, the connection or socket will automatically be closed
|
||||
# if no read or write activity takes place for at least that number of seconds.
|
||||
# Specify a value of zero to indicate that no automatic timeout should take place.
|
||||
# Zero is the default value.
|
||||
def comm_inactivity_timeout= value
|
||||
EventMachine::set_comm_inactivity_timeout @signature, value.to_f
|
||||
end
|
||||
alias set_comm_inactivity_timeout comm_inactivity_timeout=
|
||||
|
||||
# The duration after which a TCP connection in the connecting state will fail.
|
||||
# It is important to distinguish this value from {EventMachine::Connection#comm_inactivity_timeout},
|
||||
# which looks at how long since data was passed on an already established connection.
|
||||
# The value is a float in seconds.
|
||||
#
|
||||
# @return [Float] The duration after which a TCP connection in the connecting state will fail, in seconds.
|
||||
def pending_connect_timeout
|
||||
EventMachine::get_pending_connect_timeout @signature
|
||||
end
|
||||
|
||||
# Sets the duration after which a TCP connection in a
|
||||
# connecting state will fail.
|
||||
#
|
||||
# @param [Float, #to_f] value Connection timeout in seconds
|
||||
def pending_connect_timeout= value
|
||||
EventMachine::set_pending_connect_timeout @signature, value.to_f
|
||||
end
|
||||
alias set_pending_connect_timeout pending_connect_timeout=
|
||||
|
||||
# Reconnect to a given host/port with the current instance
|
||||
#
|
||||
# @param [String] server Hostname or IP address
|
||||
# @param [Integer] port Port to reconnect to
|
||||
def reconnect server, port
|
||||
EventMachine::reconnect server, port, self
|
||||
end
|
||||
|
||||
|
||||
# Like {EventMachine::Connection#send_data}, this sends data to the remote end of
|
||||
# the network connection. {EventMachine::Connection#send_file_data} takes a
|
||||
# filename as an argument, though, and sends the contents of the file, in one
|
||||
# chunk.
|
||||
#
|
||||
# @param [String] filename Local path of the file to send
|
||||
#
|
||||
# @see #send_data
|
||||
# @author Kirk Haines
|
||||
def send_file_data filename
|
||||
EventMachine::send_file_data @signature, filename
|
||||
end
|
||||
|
||||
# Open a file on the filesystem and send it to the remote peer. This returns an
|
||||
# object of type {EventMachine::Deferrable}. The object's callbacks will be executed
|
||||
# on the reactor main thread when the file has been completely scheduled for
|
||||
# transmission to the remote peer. Its errbacks will be called in case of an error (such as file-not-found).
|
||||
# This method employs various strategies to achieve the fastest possible performance,
|
||||
# balanced against minimum consumption of memory.
|
||||
#
|
||||
# Warning: this feature has an implicit dependency on an outboard extension,
|
||||
# evma_fastfilereader. You must install this extension in order to use {#stream_file_data}
|
||||
# with files larger than a certain size (currently 8192 bytes).
|
||||
#
|
||||
# @option args [Boolean] :http_chunks (false) If true, this method will stream the file data in a format
|
||||
# compatible with the HTTP chunked-transfer encoding
|
||||
#
|
||||
# @param [String] filename Local path of the file to stream
|
||||
# @param [Hash] args Options
|
||||
#
|
||||
# @return [EventMachine::Deferrable]
|
||||
def stream_file_data filename, args={}
|
||||
EventMachine::FileStreamer.new( self, filename, args )
|
||||
end
|
||||
|
||||
# Watches connection for readability. Only possible if the connection was created
|
||||
# using {EventMachine.attach} and had {EventMachine.notify_readable}/{EventMachine.notify_writable} defined on the handler.
|
||||
#
|
||||
# @see #notify_readable?
|
||||
def notify_readable= mode
|
||||
EventMachine::set_notify_readable @signature, mode
|
||||
end
|
||||
|
||||
# @return [Boolean] true if the connection is being watched for readability.
|
||||
def notify_readable?
|
||||
EventMachine::is_notify_readable @signature
|
||||
end
|
||||
|
||||
# Watches connection for writeability. Only possible if the connection was created
|
||||
# using {EventMachine.attach} and had {EventMachine.notify_readable}/{EventMachine.notify_writable} defined on the handler.
|
||||
#
|
||||
# @see #notify_writable?
|
||||
def notify_writable= mode
|
||||
EventMachine::set_notify_writable @signature, mode
|
||||
end
|
||||
|
||||
# Returns true if the connection is being watched for writability.
|
||||
def notify_writable?
|
||||
EventMachine::is_notify_writable @signature
|
||||
end
|
||||
|
||||
# Pause a connection so that {#send_data} and {#receive_data} events are not fired until {#resume} is called.
|
||||
# @see #resume
|
||||
def pause
|
||||
EventMachine::pause_connection @signature
|
||||
end
|
||||
|
||||
# Resume a connection's {#send_data} and {#receive_data} events.
|
||||
# @see #pause
|
||||
def resume
|
||||
EventMachine::resume_connection @signature
|
||||
end
|
||||
|
||||
# @return [Boolean] true if the connect was paused using {EventMachine::Connection#pause}.
|
||||
# @see #pause
|
||||
# @see #resume
|
||||
def paused?
|
||||
EventMachine::connection_paused? @signature
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,210 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 16 Jul 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
module Deferrable
|
||||
autoload :Pool, 'em/deferrable/pool'
|
||||
|
||||
# Specify a block to be executed if and when the Deferrable object receives
|
||||
# a status of :succeeded. See #set_deferred_status for more information.
|
||||
#
|
||||
# Calling this method on a Deferrable object whose status is not yet known
|
||||
# will cause the callback block to be stored on an internal list.
|
||||
# If you call this method on a Deferrable whose status is :succeeded, the
|
||||
# block will be executed immediately, receiving the parameters given to the
|
||||
# prior #set_deferred_status call.
|
||||
#
|
||||
#--
|
||||
# If there is no status, add a callback to an internal list.
|
||||
# If status is succeeded, execute the callback immediately.
|
||||
# If status is failed, do nothing.
|
||||
#
|
||||
def callback &block
|
||||
return unless block
|
||||
@deferred_status ||= :unknown
|
||||
if @deferred_status == :succeeded
|
||||
block.call(*@deferred_args)
|
||||
elsif @deferred_status != :failed
|
||||
@callbacks ||= []
|
||||
@callbacks.unshift block # << block
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# Cancels an outstanding callback to &block if any. Undoes the action of #callback.
|
||||
#
|
||||
def cancel_callback block
|
||||
@callbacks ||= []
|
||||
@callbacks.delete block
|
||||
end
|
||||
|
||||
# Specify a block to be executed if and when the Deferrable object receives
|
||||
# a status of :failed. See #set_deferred_status for more information.
|
||||
#--
|
||||
# If there is no status, add an errback to an internal list.
|
||||
# If status is failed, execute the errback immediately.
|
||||
# If status is succeeded, do nothing.
|
||||
#
|
||||
def errback &block
|
||||
return unless block
|
||||
@deferred_status ||= :unknown
|
||||
if @deferred_status == :failed
|
||||
block.call(*@deferred_args)
|
||||
elsif @deferred_status != :succeeded
|
||||
@errbacks ||= []
|
||||
@errbacks.unshift block # << block
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# Cancels an outstanding errback to &block if any. Undoes the action of #errback.
|
||||
#
|
||||
def cancel_errback block
|
||||
@errbacks ||= []
|
||||
@errbacks.delete block
|
||||
end
|
||||
|
||||
# Sets the "disposition" (status) of the Deferrable object. See also the large set of
|
||||
# sugarings for this method.
|
||||
# Note that if you call this method without arguments,
|
||||
# no arguments will be passed to the callback/errback.
|
||||
# If the user has coded these with arguments, then the
|
||||
# user code will throw an argument exception.
|
||||
# Implementors of deferrable classes <b>must</b>
|
||||
# document the arguments they will supply to user callbacks.
|
||||
#
|
||||
# OBSERVE SOMETHING VERY SPECIAL here: you may call this method even
|
||||
# on the INSIDE of a callback. This is very useful when a previously-registered
|
||||
# callback wants to change the parameters that will be passed to subsequently-registered
|
||||
# ones.
|
||||
#
|
||||
# You may give either :succeeded or :failed as the status argument.
|
||||
#
|
||||
# If you pass :succeeded, then all of the blocks passed to the object using the #callback
|
||||
# method (if any) will be executed BEFORE the #set_deferred_status method returns. All of the blocks
|
||||
# passed to the object using #errback will be discarded.
|
||||
#
|
||||
# If you pass :failed, then all of the blocks passed to the object using the #errback
|
||||
# method (if any) will be executed BEFORE the #set_deferred_status method returns. All of the blocks
|
||||
# passed to the object using # callback will be discarded.
|
||||
#
|
||||
# If you pass any arguments to #set_deferred_status in addition to the status argument,
|
||||
# they will be passed as arguments to any callbacks or errbacks that are executed.
|
||||
# It's your responsibility to ensure that the argument lists specified in your callbacks and
|
||||
# errbacks match the arguments given in calls to #set_deferred_status, otherwise Ruby will raise
|
||||
# an ArgumentError.
|
||||
#
|
||||
#--
|
||||
# We're shifting callbacks off and discarding them as we execute them.
|
||||
# This is valid because by definition callbacks are executed no more than
|
||||
# once. It also has the magic effect of permitting recursive calls, which
|
||||
# means that a callback can call #set_deferred_status and change the parameters
|
||||
# that will be sent to subsequent callbacks down the chain.
|
||||
#
|
||||
# Changed @callbacks and @errbacks from push/shift to unshift/pop, per suggestion
|
||||
# by Kirk Haines, to work around the memory leak bug that still exists in many Ruby
|
||||
# versions.
|
||||
#
|
||||
# Changed 15Sep07: after processing callbacks or errbacks, CLEAR the other set of
|
||||
# handlers. This gets us a little closer to the behavior of Twisted's "deferred,"
|
||||
# which only allows status to be set once. Prior to making this change, it was possible
|
||||
# to "succeed" a Deferrable (triggering its callbacks), and then immediately "fail" it,
|
||||
# triggering its errbacks! That is clearly undesirable, but it's just as undesirable
|
||||
# to raise an exception is status is set more than once on a Deferrable. The latter
|
||||
# behavior would invalidate the idiom of resetting arguments by setting status from
|
||||
# within a callback or errback, but more seriously it would cause spurious errors
|
||||
# if a Deferrable was timed out and then an attempt was made to succeed it. See the
|
||||
# comments under the new method #timeout.
|
||||
#
|
||||
def set_deferred_status status, *args
|
||||
cancel_timeout
|
||||
@errbacks ||= nil
|
||||
@callbacks ||= nil
|
||||
@deferred_status = status
|
||||
@deferred_args = args
|
||||
case @deferred_status
|
||||
when :succeeded
|
||||
if @callbacks
|
||||
while cb = @callbacks.pop
|
||||
cb.call(*@deferred_args)
|
||||
end
|
||||
end
|
||||
@errbacks.clear if @errbacks
|
||||
when :failed
|
||||
if @errbacks
|
||||
while eb = @errbacks.pop
|
||||
eb.call(*@deferred_args)
|
||||
end
|
||||
end
|
||||
@callbacks.clear if @callbacks
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Setting a timeout on a Deferrable causes it to go into the failed state after
|
||||
# the Timeout expires (passing no arguments to the object's errbacks).
|
||||
# Setting the status at any time prior to a call to the expiration of the timeout
|
||||
# will cause the timer to be cancelled.
|
||||
def timeout seconds, *args
|
||||
cancel_timeout
|
||||
me = self
|
||||
@deferred_timeout = EventMachine::Timer.new(seconds) {me.fail(*args)}
|
||||
self
|
||||
end
|
||||
|
||||
# Cancels an outstanding timeout if any. Undoes the action of #timeout.
|
||||
#
|
||||
def cancel_timeout
|
||||
@deferred_timeout ||= nil
|
||||
if @deferred_timeout
|
||||
@deferred_timeout.cancel
|
||||
@deferred_timeout = nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Sugar for set_deferred_status(:succeeded, ...)
|
||||
#
|
||||
def succeed *args
|
||||
set_deferred_status :succeeded, *args
|
||||
end
|
||||
alias set_deferred_success succeed
|
||||
|
||||
# Sugar for set_deferred_status(:failed, ...)
|
||||
#
|
||||
def fail *args
|
||||
set_deferred_status :failed, *args
|
||||
end
|
||||
alias set_deferred_failure fail
|
||||
end
|
||||
|
||||
|
||||
# DefaultDeferrable is an otherwise empty class that includes Deferrable.
|
||||
# This is very useful when you just need to return a Deferrable object
|
||||
# as a way of communicating deferred status to some other part of a program.
|
||||
class DefaultDeferrable
|
||||
include Deferrable
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
warn "EM::Deferrable::Pool is deprecated, please use EM::Pool"
|
||||
EM::Deferrable::Pool = EM::Pool
|
||||
@@ -0,0 +1,73 @@
|
||||
module EventMachine
|
||||
# Utility class that is useful for file monitoring. Supported events are
|
||||
#
|
||||
# * File is modified
|
||||
# * File is deleted
|
||||
# * File is moved
|
||||
#
|
||||
# @note On Mac OS X, file watching only works when kqueue is enabled
|
||||
#
|
||||
# @see EventMachine.watch_file
|
||||
class FileWatch < Connection
|
||||
# @private
|
||||
Cmodified = 'modified'.freeze
|
||||
# @private
|
||||
Cdeleted = 'deleted'.freeze
|
||||
# @private
|
||||
Cmoved = 'moved'.freeze
|
||||
|
||||
|
||||
# @private
|
||||
def receive_data(data)
|
||||
case data
|
||||
when Cmodified
|
||||
file_modified
|
||||
when Cdeleted
|
||||
file_deleted
|
||||
when Cmoved
|
||||
file_moved
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the path that is being monitored.
|
||||
#
|
||||
# @note Current implementation does not pick up on the new filename after a rename occurs.
|
||||
#
|
||||
# @return [String]
|
||||
# @see EventMachine.watch_file
|
||||
def path
|
||||
@path
|
||||
end
|
||||
|
||||
# Will be called when the file is modified. Supposed to be redefined by subclasses.
|
||||
#
|
||||
# @abstract
|
||||
def file_modified
|
||||
end
|
||||
|
||||
# Will be called when the file is deleted. Supposed to be redefined by subclasses.
|
||||
# When the file is deleted, stop_watching will be called after this to make sure everything is
|
||||
# cleaned up correctly.
|
||||
#
|
||||
# @note On Linux (with {http://en.wikipedia.org/wiki/Inotify inotify}), this method will not be called until *all* open file descriptors to
|
||||
# the file have been closed.
|
||||
#
|
||||
# @abstract
|
||||
def file_deleted
|
||||
end
|
||||
|
||||
# Will be called when the file is moved or renamed. Supposed to be redefined by subclasses.
|
||||
#
|
||||
# @abstract
|
||||
def file_moved
|
||||
end
|
||||
|
||||
# Discontinue monitoring of the file.
|
||||
#
|
||||
# This involves cleaning up the underlying monitoring details with kqueue/inotify, and in turn firing {EventMachine::Connection#unbind}.
|
||||
# This will be called automatically when a file is deleted. User code may call it as well.
|
||||
def stop_watching
|
||||
EventMachine::unwatch_filename(@signature)
|
||||
end # stop_watching
|
||||
end # FileWatch
|
||||
end # EventMachine
|
||||
@@ -0,0 +1,61 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 16 Jul 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
#--
|
||||
# This defines EventMachine::Deferrable#future, which requires
|
||||
# that the rest of EventMachine::Deferrable has already been seen.
|
||||
# (It's in deferrable.rb.)
|
||||
|
||||
module EventMachine
|
||||
module Deferrable
|
||||
|
||||
# A future is a sugaring of a typical deferrable usage.
|
||||
#--
|
||||
# Evaluate arg (which may be an expression or a block).
|
||||
# What's the class of arg?
|
||||
# If arg is an ordinary expression, then return it.
|
||||
# If arg is deferrable (responds to :set_deferred_status),
|
||||
# then look at the arguments. If either callback or errback
|
||||
# are defined, then use them. If neither are defined, then
|
||||
# use the supplied block (if any) as the callback.
|
||||
# Then return arg.
|
||||
def self.future arg, cb=nil, eb=nil, &blk
|
||||
arg = arg.call if arg.respond_to?(:call)
|
||||
|
||||
if arg.respond_to?(:set_deferred_status)
|
||||
if cb || eb
|
||||
arg.callback(&cb) if cb
|
||||
arg.errback(&eb) if eb
|
||||
else
|
||||
arg.callback(&blk) if blk
|
||||
end
|
||||
end
|
||||
|
||||
arg
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
module EventMachine
|
||||
# A simple iterator for concurrent asynchronous work.
|
||||
#
|
||||
# Unlike ruby's built-in iterators, the end of the current iteration cycle is signaled manually,
|
||||
# instead of happening automatically after the yielded block finishes executing. For example:
|
||||
#
|
||||
# (0..10).each{ |num| }
|
||||
#
|
||||
# becomes:
|
||||
#
|
||||
# EM::Iterator.new(0..10).each{ |num,iter| iter.next }
|
||||
#
|
||||
# This is especially useful when doing asynchronous work via reactor libraries and
|
||||
# functions. For example, given a sync and async http api:
|
||||
#
|
||||
# response = sync_http_get(url); ...
|
||||
# async_http_get(url){ |response| ... }
|
||||
#
|
||||
# a synchronous iterator such as:
|
||||
#
|
||||
# responses = urls.map{ |url| sync_http_get(url) }
|
||||
# ...
|
||||
# puts 'all done!'
|
||||
#
|
||||
# could be written as:
|
||||
#
|
||||
# EM::Iterator.new(urls).map(proc{ |url,iter|
|
||||
# async_http_get(url){ |res|
|
||||
# iter.return(res)
|
||||
# }
|
||||
# }, proc{ |responses|
|
||||
# ...
|
||||
# puts 'all done!'
|
||||
# })
|
||||
#
|
||||
# Now, you can take advantage of the asynchronous api to issue requests in parallel. For example,
|
||||
# to fetch 10 urls at a time, simply pass in a concurrency of 10:
|
||||
#
|
||||
# EM::Iterator.new(urls, 10).each do |url,iter|
|
||||
# async_http_get(url){ iter.next }
|
||||
# end
|
||||
#
|
||||
class Iterator
|
||||
Stop = "EM::Stop"
|
||||
# Create a new parallel async iterator with specified concurrency.
|
||||
#
|
||||
# i = EM::Iterator.new(1..100, 10)
|
||||
#
|
||||
# will create an iterator over the range that processes 10 items at a time. Iteration
|
||||
# is started via #each, #map or #inject
|
||||
#
|
||||
# The list may either be an array-like object, or a proc that returns a new object
|
||||
# to be processed each time it is called. If a proc is used, it must return
|
||||
# EventMachine::Iterator::Stop to signal the end of the iterations.
|
||||
#
|
||||
def initialize(list, concurrency = 1)
|
||||
raise ArgumentError, 'concurrency must be bigger than zero' unless (concurrency > 0)
|
||||
if list.respond_to?(:call)
|
||||
@list = nil
|
||||
@list_proc = list
|
||||
elsif list.respond_to?(:to_a)
|
||||
@list = list.to_a.dup
|
||||
@list_proc = nil
|
||||
else
|
||||
raise ArgumentError, 'argument must be a proc or an array'
|
||||
end
|
||||
@concurrency = concurrency
|
||||
|
||||
@started = false
|
||||
@ended = false
|
||||
end
|
||||
|
||||
# Change the concurrency of this iterator. Workers will automatically be spawned or destroyed
|
||||
# to accomodate the new concurrency level.
|
||||
#
|
||||
def concurrency=(val)
|
||||
old = @concurrency
|
||||
@concurrency = val
|
||||
|
||||
spawn_workers if val > old and @started and !@ended
|
||||
end
|
||||
attr_reader :concurrency
|
||||
|
||||
# Iterate over a set of items using the specified block or proc.
|
||||
#
|
||||
# EM::Iterator.new(1..100).each do |num, iter|
|
||||
# puts num
|
||||
# iter.next
|
||||
# end
|
||||
#
|
||||
# An optional second proc is invoked after the iteration is complete.
|
||||
#
|
||||
# EM::Iterator.new(1..100).each(
|
||||
# proc{ |num,iter| iter.next },
|
||||
# proc{ puts 'all done' }
|
||||
# )
|
||||
#
|
||||
def each(foreach=nil, after=nil, &blk)
|
||||
raise ArgumentError, 'proc or block required for iteration' unless foreach ||= blk
|
||||
raise RuntimeError, 'cannot iterate over an iterator more than once' if @started or @ended
|
||||
|
||||
@started = true
|
||||
@pending = 0
|
||||
@workers = 0
|
||||
|
||||
all_done = proc{
|
||||
after.call if after and @ended and @pending == 0
|
||||
}
|
||||
|
||||
@process_next = proc{
|
||||
# p [:process_next, :pending=, @pending, :workers=, @workers, :ended=, @ended, :concurrency=, @concurrency, :list=, @list]
|
||||
unless @ended or @workers > @concurrency
|
||||
item = next_item()
|
||||
if item.equal?(Stop)
|
||||
@ended = true
|
||||
@workers -= 1
|
||||
all_done.call
|
||||
else
|
||||
@pending += 1
|
||||
|
||||
is_done = false
|
||||
on_done = proc{
|
||||
raise RuntimeError, 'already completed this iteration' if is_done
|
||||
is_done = true
|
||||
|
||||
@pending -= 1
|
||||
|
||||
if @ended
|
||||
all_done.call
|
||||
else
|
||||
EM.next_tick(@process_next)
|
||||
end
|
||||
}
|
||||
class << on_done
|
||||
alias :next :call
|
||||
end
|
||||
|
||||
foreach.call(item, on_done)
|
||||
end
|
||||
else
|
||||
@workers -= 1
|
||||
end
|
||||
}
|
||||
|
||||
spawn_workers
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
# Collect the results of an asynchronous iteration into an array.
|
||||
#
|
||||
# EM::Iterator.new(%w[ pwd uptime uname date ], 2).map(proc{ |cmd,iter|
|
||||
# EM.system(cmd){ |output,status|
|
||||
# iter.return(output)
|
||||
# }
|
||||
# }, proc{ |results|
|
||||
# p results
|
||||
# })
|
||||
#
|
||||
def map(foreach, after)
|
||||
index = 0
|
||||
|
||||
inject([], proc{ |results,item,iter|
|
||||
i = index
|
||||
index += 1
|
||||
|
||||
is_done = false
|
||||
on_done = proc{ |res|
|
||||
raise RuntimeError, 'already returned a value for this iteration' if is_done
|
||||
is_done = true
|
||||
|
||||
results[i] = res
|
||||
iter.return(results)
|
||||
}
|
||||
class << on_done
|
||||
alias :return :call
|
||||
def next
|
||||
raise NoMethodError, 'must call #return on a map iterator'
|
||||
end
|
||||
end
|
||||
|
||||
foreach.call(item, on_done)
|
||||
}, proc{ |results|
|
||||
after.call(results)
|
||||
})
|
||||
end
|
||||
|
||||
# Inject the results of an asynchronous iteration onto a given object.
|
||||
#
|
||||
# EM::Iterator.new(%w[ pwd uptime uname date ], 2).inject({}, proc{ |hash,cmd,iter|
|
||||
# EM.system(cmd){ |output,status|
|
||||
# hash[cmd] = status.exitstatus == 0 ? output.strip : nil
|
||||
# iter.return(hash)
|
||||
# }
|
||||
# }, proc{ |results|
|
||||
# p results
|
||||
# })
|
||||
#
|
||||
def inject(obj, foreach, after)
|
||||
each(proc{ |item,iter|
|
||||
is_done = false
|
||||
on_done = proc{ |res|
|
||||
raise RuntimeError, 'already returned a value for this iteration' if is_done
|
||||
is_done = true
|
||||
|
||||
obj = res
|
||||
iter.next
|
||||
}
|
||||
class << on_done
|
||||
alias :return :call
|
||||
def next
|
||||
raise NoMethodError, 'must call #return on an inject iterator'
|
||||
end
|
||||
end
|
||||
|
||||
foreach.call(obj, item, on_done)
|
||||
}, proc{
|
||||
after.call(obj)
|
||||
})
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Spawn workers to consume items from the iterator's enumerator based on the current concurrency level.
|
||||
#
|
||||
def spawn_workers
|
||||
EM.next_tick(start_worker = proc{
|
||||
if @workers < @concurrency and !@ended
|
||||
# p [:spawning_worker, :workers=, @workers, :concurrency=, @concurrency, :ended=, @ended]
|
||||
@workers += 1
|
||||
@process_next.call
|
||||
EM.next_tick(start_worker)
|
||||
end
|
||||
})
|
||||
nil
|
||||
end
|
||||
|
||||
# Return the next item from @list or @list_proc.
|
||||
# Once items have run out, will return EM::Iterator::Stop. Procs must supply this themselves
|
||||
def next_item
|
||||
if @list_proc
|
||||
@list_proc.call
|
||||
else
|
||||
@list.empty? ? Stop : @list.shift
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: pass in one object instead of two? .each{ |iter| puts iter.current; iter.next }
|
||||
# TODO: support iter.pause/resume/stop/break/continue?
|
||||
# TODO: create some exceptions instead of using RuntimeError
|
||||
@@ -0,0 +1,66 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 16 Jul 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
=begin
|
||||
|
||||
Message Routing in EventMachine.
|
||||
|
||||
The goal here is to enable "routing points," objects that can send and receive
|
||||
"messages," which are delimited streams of bytes. The boundaries of a message
|
||||
are preserved as it passes through the reactor system.
|
||||
|
||||
There will be several module methods defined in EventMachine to create route-point
|
||||
objects (which will probably have a base class of EventMachine::MessageRouter
|
||||
until someone suggests a better name).
|
||||
|
||||
As with I/O objects, routing objects will receive events by having the router
|
||||
core call methods on them. And of course user code can and will define handlers
|
||||
to deal with events of interest.
|
||||
|
||||
The message router base class only really needs a receive_message method. There will
|
||||
be an EM module-method to send messages, in addition to the module methods to create
|
||||
the various kinds of message receivers.
|
||||
|
||||
The simplest kind of message receiver object can receive messages by being named
|
||||
explicitly in a parameter to EM#send_message. More sophisticated receivers can define
|
||||
pub-sub selectors and message-queue names. And they can also define channels for
|
||||
route-points in other processes or even on other machines.
|
||||
|
||||
A message is NOT a marshallable entity. Rather, it's a chunk of flat content more like
|
||||
an Erlang message. Initially, all content submitted for transmission as a message will
|
||||
have the to_s method called on it. Eventually, we'll be able to transmit certain structured
|
||||
data types (XML and YAML documents, Structs within limits) and have them reconstructed
|
||||
on the other end.
|
||||
|
||||
A fundamental goal of the message-routing capability is to interoperate seamlessly with
|
||||
external systems, including non-Ruby systems like ActiveMQ. We will define various protocol
|
||||
handlers for things like Stomp and possibly AMQP, but these will be wrapped up and hidden
|
||||
from the users of the basic routing capability.
|
||||
|
||||
As with Erlang, a critical goal is for programs that are built to use message-passing to work
|
||||
WITHOUT CHANGE when the code is re-based on a multi-process system.
|
||||
|
||||
=end
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
module EventMachine
|
||||
# A simple async resource pool based on a resource and work queue. Resources
|
||||
# are enqueued and work waits for resources to become available.
|
||||
#
|
||||
# @example
|
||||
# require 'em-http-request'
|
||||
#
|
||||
# EM.run do
|
||||
# pool = EM::Pool.new
|
||||
# spawn = lambda { pool.add EM::HttpRequest.new('http://example.org') }
|
||||
# 10.times { spawn[] }
|
||||
# done, scheduled = 0, 0
|
||||
#
|
||||
# check = lambda do
|
||||
# done += 1
|
||||
# if done >= scheduled
|
||||
# EM.stop
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# pool.on_error { |conn| spawn[] }
|
||||
#
|
||||
# 100.times do |i|
|
||||
# scheduled += 1
|
||||
# pool.perform do |conn|
|
||||
# req = conn.get :path => '/', :keepalive => true
|
||||
#
|
||||
# req.callback do
|
||||
# p [:success, conn.object_id, i, req.response.size]
|
||||
# check[]
|
||||
# end
|
||||
#
|
||||
# req.errback { check[] }
|
||||
#
|
||||
# req
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Resources are expected to be controlled by an object responding to a
|
||||
# deferrable/completion style API with callback and errback blocks.
|
||||
#
|
||||
class Pool
|
||||
|
||||
def initialize
|
||||
@resources = EM::Queue.new
|
||||
@removed = []
|
||||
@contents = []
|
||||
@on_error = nil
|
||||
end
|
||||
|
||||
def add resource
|
||||
@contents << resource
|
||||
requeue resource
|
||||
end
|
||||
|
||||
def remove resource
|
||||
@contents.delete resource
|
||||
@removed << resource
|
||||
end
|
||||
|
||||
# Returns a list for introspection purposes only. You should *NEVER* call
|
||||
# modification or work oriented methods on objects in this list. A good
|
||||
# example use case is periodic statistics collection against a set of
|
||||
# connection resources.
|
||||
#
|
||||
# @example
|
||||
# pool.contents.inject(0) { |sum, connection| connection.num_bytes }
|
||||
def contents
|
||||
@contents.dup
|
||||
end
|
||||
|
||||
# Define a default catch-all for when the deferrables returned by work
|
||||
# blocks enter a failed state. By default all that happens is that the
|
||||
# resource is returned to the pool. If on_error is defined, this block is
|
||||
# responsible for re-adding the resource to the pool if it is still usable.
|
||||
# In other words, it is generally assumed that on_error blocks explicitly
|
||||
# handle the rest of the lifetime of the resource.
|
||||
def on_error *a, &b
|
||||
@on_error = EM::Callback(*a, &b)
|
||||
end
|
||||
|
||||
# Perform a given #call-able object or block. The callable object will be
|
||||
# called with a resource from the pool as soon as one is available, and is
|
||||
# expected to return a deferrable.
|
||||
#
|
||||
# The deferrable will have callback and errback added such that when the
|
||||
# deferrable enters a finished state, the object is returned to the pool.
|
||||
#
|
||||
# If on_error is defined, then objects are not automatically returned to the
|
||||
# pool.
|
||||
def perform(*a, &b)
|
||||
work = EM::Callback(*a, &b)
|
||||
|
||||
@resources.pop do |resource|
|
||||
if removed? resource
|
||||
@removed.delete resource
|
||||
reschedule work
|
||||
else
|
||||
process work, resource
|
||||
end
|
||||
end
|
||||
end
|
||||
alias reschedule perform
|
||||
|
||||
# A peek at the number of enqueued jobs waiting for resources
|
||||
def num_waiting
|
||||
@resources.num_waiting
|
||||
end
|
||||
|
||||
# Removed will show resources in a partial pruned state. Resources in the
|
||||
# removed list may not appear in the contents list if they are currently in
|
||||
# use.
|
||||
def removed? resource
|
||||
@removed.include? resource
|
||||
end
|
||||
|
||||
protected
|
||||
def requeue resource
|
||||
@resources.push resource
|
||||
end
|
||||
|
||||
def failure resource
|
||||
if @on_error
|
||||
@contents.delete resource
|
||||
@on_error.call resource
|
||||
# Prevent users from calling a leak.
|
||||
@removed.delete resource
|
||||
else
|
||||
requeue resource
|
||||
end
|
||||
end
|
||||
|
||||
def completion deferrable, resource
|
||||
deferrable.callback { requeue resource }
|
||||
deferrable.errback { failure resource }
|
||||
end
|
||||
|
||||
def process work, resource
|
||||
deferrable = work.call resource
|
||||
if deferrable.kind_of?(EM::Deferrable)
|
||||
completion deferrable, resource
|
||||
else
|
||||
raise ArgumentError, "deferrable expected from work"
|
||||
end
|
||||
rescue
|
||||
failure resource
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
module EventMachine
|
||||
|
||||
# This is subclassed from EventMachine::Connection for use with the process monitoring API. Read the
|
||||
# documentation on the instance methods of this class, and for a full explanation see EventMachine.watch_process.
|
||||
class ProcessWatch < Connection
|
||||
# @private
|
||||
Cfork = 'fork'.freeze
|
||||
# @private
|
||||
Cexit = 'exit'.freeze
|
||||
|
||||
# @private
|
||||
def receive_data(data)
|
||||
case data
|
||||
when Cfork
|
||||
process_forked
|
||||
when Cexit
|
||||
process_exited
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the pid that EventMachine::watch_process was originally called with.
|
||||
def pid
|
||||
@pid
|
||||
end
|
||||
|
||||
# Should be redefined with the user's custom callback that will be fired when the prcess is forked.
|
||||
#
|
||||
# There is currently not an easy way to get the pid of the forked child.
|
||||
def process_forked
|
||||
end
|
||||
|
||||
# Should be redefined with the user's custom callback that will be fired when the process exits.
|
||||
#
|
||||
# stop_watching is called automatically after this callback
|
||||
def process_exited
|
||||
end
|
||||
|
||||
# Discontinue monitoring of the process.
|
||||
# This will be called automatically when a process dies. User code may call it as well.
|
||||
def stop_watching
|
||||
EventMachine::unwatch_pid(@signature)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,123 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 13 Dec 07
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-08 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
module EventMachine
|
||||
|
||||
# EM::DeferrableChildProcess is a sugaring of a common use-case
|
||||
# involving EM::popen.
|
||||
# Call the #open method on EM::DeferrableChildProcess, passing
|
||||
# a command-string. #open immediately returns an EM::Deferrable
|
||||
# object. It also schedules the forking of a child process, which
|
||||
# will execute the command passed to #open.
|
||||
# When the forked child terminates, the Deferrable will be signalled
|
||||
# and execute its callbacks, passing the data that the child process
|
||||
# wrote to stdout.
|
||||
#
|
||||
class DeferrableChildProcess < EventMachine::Connection
|
||||
include EventMachine::Deferrable
|
||||
|
||||
# @private
|
||||
def initialize
|
||||
super
|
||||
@data = []
|
||||
end
|
||||
|
||||
# Sugars a common use-case involving forked child processes.
|
||||
# #open takes a String argument containing an shell command
|
||||
# string (including arguments if desired). #open immediately
|
||||
# returns an EventMachine::Deferrable object, without blocking.
|
||||
#
|
||||
# It also invokes EventMachine#popen to run the passed-in
|
||||
# command in a forked child process.
|
||||
#
|
||||
# When the forked child terminates, the Deferrable that
|
||||
# #open calls its callbacks, passing the data returned
|
||||
# from the child process.
|
||||
#
|
||||
def self.open cmd
|
||||
EventMachine.popen( cmd, DeferrableChildProcess )
|
||||
end
|
||||
|
||||
# @private
|
||||
def receive_data data
|
||||
@data << data
|
||||
end
|
||||
|
||||
# @private
|
||||
def unbind
|
||||
succeed( @data.join )
|
||||
end
|
||||
end
|
||||
|
||||
# @private
|
||||
class SystemCmd < EventMachine::Connection
|
||||
def initialize cb
|
||||
@cb = cb
|
||||
@output = []
|
||||
end
|
||||
def receive_data data
|
||||
@output << data
|
||||
end
|
||||
def unbind
|
||||
@cb.call @output.join(''), get_status if @cb
|
||||
end
|
||||
end
|
||||
|
||||
# EM::system is a simple wrapper for EM::popen. It is similar to Kernel::system, but requires a
|
||||
# single string argument for the command and performs no shell expansion.
|
||||
#
|
||||
# The block or proc passed to EM::system is called with two arguments: the output generated by the command,
|
||||
# and a Process::Status that contains information about the command's execution.
|
||||
#
|
||||
# EM.run{
|
||||
# EM.system('ls'){ |output,status| puts output if status.exitstatus == 0 }
|
||||
# }
|
||||
#
|
||||
# You can also supply an additional proc to send some data to the process:
|
||||
#
|
||||
# EM.run{
|
||||
# EM.system('sh', proc{ |process|
|
||||
# process.send_data("echo hello\n")
|
||||
# process.send_data("exit\n")
|
||||
# }, proc{ |out,status|
|
||||
# puts(out)
|
||||
# })
|
||||
# }
|
||||
#
|
||||
# Like EventMachine.popen, EventMachine.system currently does not work on windows.
|
||||
# It returns the pid of the spawned process.
|
||||
def EventMachine::system cmd, *args, &cb
|
||||
cb ||= args.pop if args.last.is_a? Proc
|
||||
init = args.pop if args.last.is_a? Proc
|
||||
|
||||
# merge remaining arguments into the command
|
||||
cmd = [cmd, *args] if args.any?
|
||||
|
||||
EM.get_subprocess_pid(EM.popen(cmd, SystemCmd, cb) do |c|
|
||||
init[c] if init
|
||||
end.signature)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
module EventMachine
|
||||
# This module contains various protocol implementations, including:
|
||||
# - HttpClient and HttpClient2
|
||||
# - Stomp
|
||||
# - Memcache
|
||||
# - SmtpClient and SmtpServer
|
||||
# - SASLauth and SASLauthclient
|
||||
# - LineProtocol, LineAndTextProtocol and LineText2
|
||||
# - HeaderAndContentProtocol
|
||||
# - Postgres3
|
||||
# - ObjectProtocol
|
||||
#
|
||||
# The protocol implementations live in separate files in the protocols/ subdirectory,
|
||||
# but are auto-loaded when they are first referenced in your application.
|
||||
#
|
||||
# EventMachine::Protocols is also aliased to EM::P for easier usage.
|
||||
#
|
||||
module Protocols
|
||||
# TODO : various autotools are completely useless with the lack of naming
|
||||
# convention, we need to correct that!
|
||||
autoload :TcpConnectTester, 'em/protocols/tcptest'
|
||||
autoload :HttpClient, 'em/protocols/httpclient'
|
||||
autoload :HttpClient2, 'em/protocols/httpclient2'
|
||||
autoload :LineAndTextProtocol, 'em/protocols/line_and_text'
|
||||
autoload :HeaderAndContentProtocol, 'em/protocols/header_and_content'
|
||||
autoload :LineText2, 'em/protocols/linetext2'
|
||||
autoload :Stomp, 'em/protocols/stomp'
|
||||
autoload :SmtpClient, 'em/protocols/smtpclient'
|
||||
autoload :SmtpServer, 'em/protocols/smtpserver'
|
||||
autoload :SASLauth, 'em/protocols/saslauth'
|
||||
autoload :Memcache, 'em/protocols/memcache'
|
||||
autoload :Postgres3, 'em/protocols/postgres3'
|
||||
autoload :ObjectProtocol, 'em/protocols/object_protocol'
|
||||
autoload :Socks4, 'em/protocols/socks4'
|
||||
autoload :LineProtocol, 'em/protocols/line_protocol'
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,138 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 15 Nov 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
|
||||
# === Usage
|
||||
#
|
||||
# class RequestHandler < EM::P::HeaderAndContentProtocol
|
||||
# def receive_request headers, content
|
||||
# p [:request, headers, content]
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# EM.run{
|
||||
# EM.start_server 'localhost', 80, RequestHandler
|
||||
# }
|
||||
#
|
||||
#--
|
||||
# Originally, this subclassed LineAndTextProtocol, which in
|
||||
# turn relies on BufferedTokenizer, which doesn't gracefully
|
||||
# handle the transitions between lines and binary text.
|
||||
# Changed 13Sep08 by FCianfrocca.
|
||||
class HeaderAndContentProtocol < Connection
|
||||
include LineText2
|
||||
|
||||
ContentLengthPattern = /Content-length:\s*(\d+)/i
|
||||
|
||||
def initialize *args
|
||||
super
|
||||
init_for_request
|
||||
end
|
||||
|
||||
def receive_line line
|
||||
case @hc_mode
|
||||
when :discard_blanks
|
||||
unless line == ""
|
||||
@hc_mode = :headers
|
||||
receive_line line
|
||||
end
|
||||
when :headers
|
||||
if line == ""
|
||||
raise "unrecognized state" unless @hc_headers.length > 0
|
||||
if respond_to?(:receive_headers)
|
||||
receive_headers @hc_headers
|
||||
end
|
||||
# @hc_content_length will be nil, not 0, if there was no content-length header.
|
||||
if @hc_content_length.to_i > 0
|
||||
set_binary_mode @hc_content_length
|
||||
else
|
||||
dispatch_request
|
||||
end
|
||||
else
|
||||
@hc_headers << line
|
||||
if ContentLengthPattern =~ line
|
||||
# There are some attacks that rely on sending multiple content-length
|
||||
# headers. This is a crude protection, but needs to become tunable.
|
||||
raise "extraneous content-length header" if @hc_content_length
|
||||
@hc_content_length = $1.to_i
|
||||
end
|
||||
if @hc_headers.length == 1 and respond_to?(:receive_first_header_line)
|
||||
receive_first_header_line line
|
||||
end
|
||||
end
|
||||
else
|
||||
raise "internal error, unsupported mode"
|
||||
end
|
||||
end
|
||||
|
||||
def receive_binary_data text
|
||||
@hc_content = text
|
||||
dispatch_request
|
||||
end
|
||||
|
||||
def dispatch_request
|
||||
if respond_to?(:receive_request)
|
||||
receive_request @hc_headers, @hc_content
|
||||
end
|
||||
init_for_request
|
||||
end
|
||||
private :dispatch_request
|
||||
|
||||
def init_for_request
|
||||
@hc_mode = :discard_blanks
|
||||
@hc_headers = []
|
||||
# originally was @hc_headers ||= []; @hc_headers.clear to get a performance
|
||||
# boost, but it's counterproductive because a subclassed handler will have to
|
||||
# call dup to use the header array we pass in receive_headers.
|
||||
|
||||
@hc_content_length = nil
|
||||
@hc_content = ""
|
||||
end
|
||||
private :init_for_request
|
||||
|
||||
# Basically a convenience method. We might create a subclass that does this
|
||||
# automatically. But it's such a performance killer.
|
||||
def headers_2_hash hdrs
|
||||
self.class.headers_2_hash hdrs
|
||||
end
|
||||
|
||||
class << self
|
||||
def headers_2_hash hdrs
|
||||
hash = {}
|
||||
hdrs.each {|h|
|
||||
if /\A([^\s:]+)\s*:\s*/ =~ h
|
||||
tail = $'.dup
|
||||
hash[ $1.downcase.gsub(/-/,"_").intern ] = tail
|
||||
end
|
||||
}
|
||||
hash
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,300 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 16 July 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
|
||||
# <b>Note:</b> This class is deprecated and will be removed. Please use EM-HTTP-Request instead.
|
||||
#
|
||||
# @example
|
||||
# EventMachine.run {
|
||||
# http = EventMachine::Protocols::HttpClient.request(
|
||||
# :host => server,
|
||||
# :port => 80,
|
||||
# :request => "/index.html",
|
||||
# :query_string => "parm1=value1&parm2=value2"
|
||||
# )
|
||||
# http.callback {|response|
|
||||
# puts response[:status]
|
||||
# puts response[:headers]
|
||||
# puts response[:content]
|
||||
# }
|
||||
# }
|
||||
#--
|
||||
# TODO:
|
||||
# Add streaming so we can support enormous POSTs. Current max is 20meg.
|
||||
# Timeout for connections that run too long or hang somewhere in the middle.
|
||||
# Persistent connections (HTTP/1.1), may need a associated delegate object.
|
||||
# DNS: Some way to cache DNS lookups for hostnames we connect to. Ruby's
|
||||
# DNS lookups are unbelievably slow.
|
||||
# HEAD requests.
|
||||
# Convenience methods for requests. get, post, url, etc.
|
||||
# SSL.
|
||||
# Handle status codes like 304, 100, etc.
|
||||
# Refactor this code so that protocol errors all get handled one way (an exception?),
|
||||
# instead of sprinkling set_deferred_status :failed calls everywhere.
|
||||
class HttpClient < Connection
|
||||
include EventMachine::Deferrable
|
||||
|
||||
MaxPostContentLength = 20 * 1024 * 1024
|
||||
|
||||
def initialize
|
||||
warn "HttpClient is deprecated and will be removed. EM-Http-Request should be used instead."
|
||||
@connected = false
|
||||
end
|
||||
|
||||
# @param args [Hash] The request arguments
|
||||
# @option args [String] :host The host IP/DNS name
|
||||
# @option args [Integer] :port The port to connect too
|
||||
# @option args [String] :verb The request type [GET | POST | DELETE | PUT]
|
||||
# @option args [String] :request The request path
|
||||
# @option args [Hash] :basic_auth The basic auth credentials (:username and :password)
|
||||
# @option args [String] :content The request content
|
||||
# @option args [String] :contenttype The content type (e.g. text/plain)
|
||||
# @option args [String] :query_string The query string
|
||||
# @option args [String] :host_header The host header to set
|
||||
# @option args [String] :cookie Cookies to set
|
||||
def self.request( args = {} )
|
||||
args[:port] ||= 80
|
||||
EventMachine.connect( args[:host], args[:port], self ) {|c|
|
||||
# According to the docs, we will get here AFTER post_init is called.
|
||||
c.instance_eval {@args = args}
|
||||
}
|
||||
end
|
||||
|
||||
def post_init
|
||||
@start_time = Time.now
|
||||
@data = ""
|
||||
@read_state = :base
|
||||
end
|
||||
|
||||
# We send the request when we get a connection.
|
||||
# AND, we set an instance variable to indicate we passed through here.
|
||||
# That allows #unbind to know whether there was a successful connection.
|
||||
# NB: This naive technique won't work when we have to support multiple
|
||||
# requests on a single connection.
|
||||
def connection_completed
|
||||
@connected = true
|
||||
send_request @args
|
||||
end
|
||||
|
||||
def send_request args
|
||||
args[:verb] ||= args[:method] # Support :method as an alternative to :verb.
|
||||
args[:verb] ||= :get # IS THIS A GOOD IDEA, to default to GET if nothing was specified?
|
||||
|
||||
verb = args[:verb].to_s.upcase
|
||||
unless ["GET", "POST", "PUT", "DELETE", "HEAD"].include?(verb)
|
||||
set_deferred_status :failed, {:status => 0} # TODO, not signalling the error type
|
||||
return # NOTE THE EARLY RETURN, we're not sending any data.
|
||||
end
|
||||
|
||||
request = args[:request] || "/"
|
||||
unless request[0,1] == "/"
|
||||
request = "/" + request
|
||||
end
|
||||
|
||||
qs = args[:query_string] || ""
|
||||
if qs.length > 0 and qs[0,1] != '?'
|
||||
qs = "?" + qs
|
||||
end
|
||||
|
||||
version = args[:version] || "1.1"
|
||||
|
||||
# Allow an override for the host header if it's not the connect-string.
|
||||
host = args[:host_header] || args[:host] || "_"
|
||||
# For now, ALWAYS tuck in the port string, although we may want to omit it if it's the default.
|
||||
port = args[:port].to_i != 80 ? ":#{args[:port]}" : ""
|
||||
|
||||
# POST items.
|
||||
postcontenttype = args[:contenttype] || "application/octet-stream"
|
||||
postcontent = args[:content] || ""
|
||||
raise "oversized content in HTTP POST" if postcontent.length > MaxPostContentLength
|
||||
|
||||
# ESSENTIAL for the request's line-endings to be CRLF, not LF. Some servers misbehave otherwise.
|
||||
# TODO: We ASSUME the caller wants to send a 1.1 request. May not be a good assumption.
|
||||
req = [
|
||||
"#{verb} #{request}#{qs} HTTP/#{version}",
|
||||
"Host: #{host}#{port}",
|
||||
"User-agent: Ruby EventMachine",
|
||||
]
|
||||
|
||||
if verb == "POST" || verb == "PUT"
|
||||
req << "Content-type: #{postcontenttype}"
|
||||
req << "Content-length: #{postcontent.length}"
|
||||
end
|
||||
|
||||
# TODO, this cookie handler assumes it's getting a single, semicolon-delimited string.
|
||||
# Eventually we will want to deal intelligently with arrays and hashes.
|
||||
if args[:cookie]
|
||||
req << "Cookie: #{args[:cookie]}"
|
||||
end
|
||||
|
||||
# Allow custom HTTP headers, e.g. SOAPAction
|
||||
args[:custom_headers].each do |k,v|
|
||||
req << "#{k}: #{v}"
|
||||
end if args[:custom_headers]
|
||||
|
||||
# Basic-auth stanza contributed by Matt Murphy.
|
||||
if args[:basic_auth]
|
||||
basic_auth_string = ["#{args[:basic_auth][:username]}:#{args[:basic_auth][:password]}"].pack('m').strip.gsub(/\n/,'')
|
||||
req << "Authorization: Basic #{basic_auth_string}"
|
||||
end
|
||||
|
||||
req << ""
|
||||
reqstring = req.map {|l| "#{l}\r\n"}.join
|
||||
send_data reqstring
|
||||
|
||||
if verb == "POST" || verb == "PUT"
|
||||
send_data postcontent
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def receive_data data
|
||||
while data and data.length > 0
|
||||
case @read_state
|
||||
when :base
|
||||
# Perform any per-request initialization here and don't consume any data.
|
||||
@data = ""
|
||||
@headers = []
|
||||
@content_length = nil # not zero
|
||||
@content = ""
|
||||
@status = nil
|
||||
@chunked = false
|
||||
@chunk_length = nil
|
||||
@read_state = :header
|
||||
@connection_close = nil
|
||||
when :header
|
||||
ary = data.split( /\r?\n/m, 2 )
|
||||
if ary.length == 2
|
||||
data = ary.last
|
||||
if ary.first == ""
|
||||
if (@content_length and @content_length > 0) || @chunked || @connection_close
|
||||
@read_state = :content
|
||||
else
|
||||
dispatch_response
|
||||
@read_state = :base
|
||||
end
|
||||
else
|
||||
@headers << ary.first
|
||||
if @headers.length == 1
|
||||
parse_response_line
|
||||
elsif ary.first =~ /\Acontent-length:\s*/i
|
||||
# Only take the FIRST content-length header that appears,
|
||||
# which we can distinguish because @content_length is nil.
|
||||
# TODO, it's actually a fatal error if there is more than one
|
||||
# content-length header, because the caller is presumptively
|
||||
# a bad guy. (There is an exploit that depends on multiple
|
||||
# content-length headers.)
|
||||
@content_length ||= $'.to_i
|
||||
elsif ary.first =~ /\Aconnection:\s*close/i
|
||||
@connection_close = true
|
||||
elsif ary.first =~ /\Atransfer-encoding:\s*chunked/i
|
||||
@chunked = true
|
||||
end
|
||||
end
|
||||
else
|
||||
@data << data
|
||||
data = ""
|
||||
end
|
||||
when :content
|
||||
if @chunked && @chunk_length
|
||||
bytes_needed = @chunk_length - @chunk_read
|
||||
new_data = data[0, bytes_needed]
|
||||
@chunk_read += new_data.length
|
||||
@content += new_data
|
||||
data = data[bytes_needed..-1] || ""
|
||||
if @chunk_length == @chunk_read && data[0,2] == "\r\n"
|
||||
@chunk_length = nil
|
||||
data = data[2..-1]
|
||||
end
|
||||
elsif @chunked
|
||||
if (m = data.match(/\A(\S*)\r\n/m))
|
||||
data = data[m[0].length..-1]
|
||||
@chunk_length = m[1].to_i(16)
|
||||
@chunk_read = 0
|
||||
if @chunk_length == 0
|
||||
dispatch_response
|
||||
@read_state = :base
|
||||
end
|
||||
end
|
||||
elsif @content_length
|
||||
# If there was no content-length header, we have to wait until the connection
|
||||
# closes. Everything we get until that point is content.
|
||||
# TODO: Must impose a content-size limit, and also must implement chunking.
|
||||
# Also, must support either temporary files for large content, or calling
|
||||
# a content-consumer block supplied by the user.
|
||||
bytes_needed = @content_length - @content.length
|
||||
@content += data[0, bytes_needed]
|
||||
data = data[bytes_needed..-1] || ""
|
||||
if @content_length == @content.length
|
||||
dispatch_response
|
||||
@read_state = :base
|
||||
end
|
||||
else
|
||||
@content << data
|
||||
data = ""
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# We get called here when we have received an HTTP response line.
|
||||
# It's an opportunity to throw an exception or trigger other exceptional
|
||||
# handling.
|
||||
def parse_response_line
|
||||
if @headers.first =~ /\AHTTP\/1\.[01] ([\d]{3})/
|
||||
@status = $1.to_i
|
||||
else
|
||||
set_deferred_status :failed, {
|
||||
:status => 0 # crappy way of signifying an unrecognized response. TODO, find a better way to do this.
|
||||
}
|
||||
close_connection
|
||||
end
|
||||
end
|
||||
private :parse_response_line
|
||||
|
||||
def dispatch_response
|
||||
@read_state = :base
|
||||
set_deferred_status :succeeded, {
|
||||
:content => @content,
|
||||
:headers => @headers,
|
||||
:status => @status
|
||||
}
|
||||
# TODO, we close the connection for now, but this is wrong for persistent clients.
|
||||
close_connection
|
||||
end
|
||||
|
||||
def unbind
|
||||
if !@connected
|
||||
set_deferred_status :failed, {:status => 0} # YECCCCH. Find a better way to signal no-connect/network error.
|
||||
elsif (@read_state == :content and @content_length == nil)
|
||||
dispatch_response
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,600 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 16 July 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
|
||||
# <b>Note:</b> This class is deprecated and will be removed. Please use EM-HTTP-Request instead.
|
||||
#
|
||||
# === Usage
|
||||
#
|
||||
# EM.run{
|
||||
# conn = EM::Protocols::HttpClient2.connect 'google.com', 80
|
||||
#
|
||||
# req = conn.get('/')
|
||||
# req.callback{ |response|
|
||||
# p(response.status)
|
||||
# p(response.headers)
|
||||
# p(response.content)
|
||||
# }
|
||||
# }
|
||||
class HttpClient2 < Connection
|
||||
include LineText2
|
||||
|
||||
def initialize
|
||||
warn "HttpClient2 is deprecated and will be removed. EM-Http-Request should be used instead."
|
||||
|
||||
@authorization = nil
|
||||
@closed = nil
|
||||
@requests = nil
|
||||
end
|
||||
|
||||
# @private
|
||||
class Request
|
||||
include Deferrable
|
||||
|
||||
attr_reader :version
|
||||
attr_reader :status
|
||||
attr_reader :header_lines
|
||||
attr_reader :headers
|
||||
attr_reader :content
|
||||
attr_reader :internal_error
|
||||
|
||||
def initialize conn, args
|
||||
@conn = conn
|
||||
@args = args
|
||||
@header_lines = []
|
||||
@headers = {}
|
||||
@blanks = 0
|
||||
@chunk_trailer = nil
|
||||
@chunking = nil
|
||||
end
|
||||
|
||||
def send_request
|
||||
az = @args[:authorization] and az = "Authorization: #{az}\r\n"
|
||||
|
||||
r = [
|
||||
"#{@args[:verb]} #{@args[:uri]} HTTP/#{@args[:version] || "1.1"}\r\n",
|
||||
"Host: #{@args[:host_header] || "_"}\r\n",
|
||||
az || "",
|
||||
"\r\n"
|
||||
]
|
||||
@conn.send_data r.join
|
||||
end
|
||||
|
||||
|
||||
#--
|
||||
#
|
||||
def receive_line ln
|
||||
if @chunk_trailer
|
||||
receive_chunk_trailer(ln)
|
||||
elsif @chunking
|
||||
receive_chunk_header(ln)
|
||||
else
|
||||
receive_header_line(ln)
|
||||
end
|
||||
end
|
||||
|
||||
#--
|
||||
#
|
||||
def receive_chunk_trailer ln
|
||||
if ln.length == 0
|
||||
@conn.pop_request
|
||||
succeed(self)
|
||||
else
|
||||
p "Received chunk trailer line"
|
||||
end
|
||||
end
|
||||
|
||||
#--
|
||||
# Allow up to ten blank lines before we get a real response line.
|
||||
# Allow no more than 100 lines in the header.
|
||||
#
|
||||
def receive_header_line ln
|
||||
if ln.length == 0
|
||||
if @header_lines.length > 0
|
||||
process_header
|
||||
else
|
||||
@blanks += 1
|
||||
if @blanks > 10
|
||||
@conn.close_connection
|
||||
end
|
||||
end
|
||||
else
|
||||
@header_lines << ln
|
||||
if @header_lines.length > 100
|
||||
@internal_error = :bad_header
|
||||
@conn.close_connection
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#--
|
||||
# Cf RFC 2616 pgh 3.6.1 for the format of HTTP chunks.
|
||||
#
|
||||
def receive_chunk_header ln
|
||||
if ln.length > 0
|
||||
chunksize = ln.to_i(16)
|
||||
if chunksize > 0
|
||||
@conn.set_text_mode(ln.to_i(16))
|
||||
else
|
||||
@content = @content ? @content.join : ''
|
||||
@chunk_trailer = true
|
||||
end
|
||||
else
|
||||
# We correctly come here after each chunk gets read.
|
||||
# p "Got A BLANK chunk line"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
#--
|
||||
# We get a single chunk. Append it to the incoming content and switch back to line mode.
|
||||
#
|
||||
def receive_chunked_text text
|
||||
# p "RECEIVED #{text.length} CHUNK"
|
||||
(@content ||= []) << text
|
||||
end
|
||||
|
||||
|
||||
#--
|
||||
# TODO, inefficient how we're handling this. Part of it is done so as to
|
||||
# make sure we don't have problems in detecting chunked-encoding, content-length,
|
||||
# etc.
|
||||
#
|
||||
HttpResponseRE = /\AHTTP\/(1.[01]) ([\d]{3})/i
|
||||
ClenRE = /\AContent-length:\s*(\d+)/i
|
||||
ChunkedRE = /\ATransfer-encoding:\s*chunked/i
|
||||
ColonRE = /\:\s*/
|
||||
|
||||
def process_header
|
||||
unless @header_lines.first =~ HttpResponseRE
|
||||
@conn.close_connection
|
||||
@internal_error = :bad_request
|
||||
end
|
||||
@version = $1.dup
|
||||
@status = $2.dup.to_i
|
||||
|
||||
clen = nil
|
||||
chunks = nil
|
||||
@header_lines.each_with_index do |e,ix|
|
||||
if ix > 0
|
||||
hdr,val = e.split(ColonRE,2)
|
||||
(@headers[hdr.downcase] ||= []) << val
|
||||
end
|
||||
|
||||
if clen == nil and e =~ ClenRE
|
||||
clen = $1.dup.to_i
|
||||
end
|
||||
if e =~ ChunkedRE
|
||||
chunks = true
|
||||
end
|
||||
end
|
||||
|
||||
if clen
|
||||
# If the content length is zero we should not call set_text_mode,
|
||||
# because a value of zero will make it wait forever, hanging the
|
||||
# connection. Just return success instead, with empty content.
|
||||
if clen == 0 then
|
||||
@content = ""
|
||||
@conn.pop_request
|
||||
succeed(self)
|
||||
else
|
||||
@conn.set_text_mode clen
|
||||
end
|
||||
elsif chunks
|
||||
@chunking = true
|
||||
else
|
||||
# Chunked transfer, multipart, or end-of-connection.
|
||||
# For end-of-connection, we need to go the unbind
|
||||
# method and suppress its desire to fail us.
|
||||
p "NO CLEN"
|
||||
p @args[:uri]
|
||||
p @header_lines
|
||||
@internal_error = :unsupported_clen
|
||||
@conn.close_connection
|
||||
end
|
||||
end
|
||||
private :process_header
|
||||
|
||||
|
||||
def receive_text text
|
||||
@chunking ? receive_chunked_text(text) : receive_sized_text(text)
|
||||
end
|
||||
|
||||
#--
|
||||
# At the present time, we only handle contents that have a length
|
||||
# specified by the content-length header.
|
||||
#
|
||||
def receive_sized_text text
|
||||
@content = text
|
||||
@conn.pop_request
|
||||
succeed(self)
|
||||
end
|
||||
end
|
||||
|
||||
# Make a connection to a remote HTTP server.
|
||||
# Can take either a pair of arguments (which will be interpreted as
|
||||
# a hostname/ip-address and a port), or a hash.
|
||||
# If the arguments are a hash, then supported values include:
|
||||
# :host => a hostname or ip-address
|
||||
# :port => a port number
|
||||
# :ssl => true to enable ssl
|
||||
def self.connect *args
|
||||
if args.length == 2
|
||||
args = {:host=>args[0], :port=>args[1]}
|
||||
else
|
||||
args = args.first
|
||||
end
|
||||
|
||||
h,prt,ssl = args[:host], Integer(args[:port]), (args[:tls] || args[:ssl])
|
||||
conn = EM.connect( h, prt, self )
|
||||
conn.start_tls if ssl
|
||||
conn.set_default_host_header( h, prt, ssl )
|
||||
conn
|
||||
end
|
||||
|
||||
# Get a url
|
||||
#
|
||||
# req = conn.get(:uri => '/')
|
||||
# req.callback{|response| puts response.content }
|
||||
#
|
||||
def get args
|
||||
if args.is_a?(String)
|
||||
args = {:uri=>args}
|
||||
end
|
||||
args[:verb] = "GET"
|
||||
request args
|
||||
end
|
||||
|
||||
# Post to a url
|
||||
#
|
||||
# req = conn.post('/data')
|
||||
# req.callback{|response| puts response.content }
|
||||
#--
|
||||
# XXX there's no way to supply a POST body.. wtf?
|
||||
def post args
|
||||
if args.is_a?(String)
|
||||
args = {:uri=>args}
|
||||
end
|
||||
args[:verb] = "POST"
|
||||
request args
|
||||
end
|
||||
|
||||
|
||||
#--
|
||||
# Compute and remember a string to be used as the host header in HTTP requests
|
||||
# unless the user overrides it with an argument to #request.
|
||||
#
|
||||
# @private
|
||||
def set_default_host_header host, port, ssl
|
||||
if (ssl and port != 443) or (!ssl and port != 80)
|
||||
@host_header = "#{host}:#{port}"
|
||||
else
|
||||
@host_header = host
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# @private
|
||||
def post_init
|
||||
super
|
||||
@connected = EM::DefaultDeferrable.new
|
||||
end
|
||||
|
||||
# @private
|
||||
def connection_completed
|
||||
super
|
||||
@connected.succeed
|
||||
end
|
||||
|
||||
#--
|
||||
# All pending requests, if any, must fail.
|
||||
# We might come here without ever passing through connection_completed
|
||||
# in case we can't connect to the server. We'll also get here when the
|
||||
# connection closes (either because the server closes it, or we close it
|
||||
# due to detecting an internal error or security violation).
|
||||
# In either case, run down all pending requests, if any, and signal failure
|
||||
# on them.
|
||||
#
|
||||
# Set and remember a flag (@closed) so we can immediately fail any
|
||||
# subsequent requests.
|
||||
#
|
||||
# @private
|
||||
def unbind
|
||||
super
|
||||
@closed = true
|
||||
(@requests || []).each {|r| r.fail}
|
||||
end
|
||||
|
||||
# @private
|
||||
def request args
|
||||
args[:host_header] = @host_header unless args.has_key?(:host_header)
|
||||
args[:authorization] = @authorization unless args.has_key?(:authorization)
|
||||
r = Request.new self, args
|
||||
if @closed
|
||||
r.fail
|
||||
else
|
||||
(@requests ||= []).unshift r
|
||||
@connected.callback {r.send_request}
|
||||
end
|
||||
r
|
||||
end
|
||||
|
||||
# @private
|
||||
def receive_line ln
|
||||
if req = @requests.last
|
||||
req.receive_line ln
|
||||
else
|
||||
p "??????????"
|
||||
p ln
|
||||
end
|
||||
end
|
||||
|
||||
# @private
|
||||
def receive_binary_data text
|
||||
@requests.last.receive_text text
|
||||
end
|
||||
|
||||
#--
|
||||
# Called by a Request object when it completes.
|
||||
#
|
||||
# @private
|
||||
def pop_request
|
||||
@requests.pop
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
=begin
|
||||
class HttpClient2x < Connection
|
||||
include LineText2
|
||||
|
||||
# TODO: Make this behave appropriate in case a #connect fails.
|
||||
# Currently, this produces no errors.
|
||||
|
||||
# Make a connection to a remote HTTP server.
|
||||
# Can take either a pair of arguments (which will be interpreted as
|
||||
# a hostname/ip-address and a port), or a hash.
|
||||
# If the arguments are a hash, then supported values include:
|
||||
# :host => a hostname or ip-address;
|
||||
# :port => a port number
|
||||
#--
|
||||
# TODO, support optional encryption arguments like :ssl
|
||||
def self.connect *args
|
||||
if args.length == 2
|
||||
args = {:host=>args[0], :port=>args[1]}
|
||||
else
|
||||
args = args.first
|
||||
end
|
||||
|
||||
h,prt = args[:host],Integer(args[:port])
|
||||
EM.connect( h, prt, self, h, prt )
|
||||
end
|
||||
|
||||
|
||||
#--
|
||||
# Sugars a connection that makes a single request and then
|
||||
# closes the connection. Matches the behavior and the arguments
|
||||
# of the original implementation of class HttpClient.
|
||||
#
|
||||
# Intended primarily for back compatibility, but the idiom
|
||||
# is probably useful so it's not deprecated.
|
||||
# We return a Deferrable, as did the original implementation.
|
||||
#
|
||||
# Because we're improving the way we deal with errors and exceptions
|
||||
# (specifically, HTTP response codes other than 2xx will trigger the
|
||||
# errback rather than the callback), this may break some existing code.
|
||||
#
|
||||
def self.request args
|
||||
c = connect args
|
||||
end
|
||||
|
||||
#--
|
||||
# Requests can be pipelined. When we get a request, add it to the
|
||||
# front of a queue as an array. The last element of the @requests
|
||||
# array is always the oldest request received. Each element of the
|
||||
# @requests array is a two-element array consisting of a hash with
|
||||
# the original caller's arguments, and an initially-empty Ostruct
|
||||
# containing the data we retrieve from the server's response.
|
||||
# Maintain the instance variable @current_response, which is the response
|
||||
# of the oldest pending request. That's just to make other code a little
|
||||
# easier. If the variable doesn't exist when we come here, we're
|
||||
# obviously the first request being made on the connection.
|
||||
#
|
||||
# The reason for keeping this method private (and requiring use of the
|
||||
# convenience methods #get, #post, #head, etc) is to avoid the small
|
||||
# performance penalty of canonicalizing the verb.
|
||||
#
|
||||
def request args
|
||||
d = EventMachine::DefaultDeferrable.new
|
||||
|
||||
if @closed
|
||||
d.fail
|
||||
return d
|
||||
end
|
||||
|
||||
o = OpenStruct.new
|
||||
o.deferrable = d
|
||||
(@requests ||= []).unshift [args, o]
|
||||
@current_response ||= @requests.last.last
|
||||
@connected.callback {
|
||||
az = args[:authorization] and az = "Authorization: #{az}\r\n"
|
||||
|
||||
r = [
|
||||
"#{args[:verb]} #{args[:uri]} HTTP/#{args[:version] || "1.1"}\r\n",
|
||||
"Host: #{args[:host_header] || @host_header}\r\n",
|
||||
az || "",
|
||||
"\r\n"
|
||||
]
|
||||
p r
|
||||
send_data r.join
|
||||
}
|
||||
o.deferrable
|
||||
end
|
||||
private :request
|
||||
|
||||
def get args
|
||||
if args.is_a?(String)
|
||||
args = {:uri=>args}
|
||||
end
|
||||
args[:verb] = "GET"
|
||||
request args
|
||||
end
|
||||
|
||||
def initialize host, port
|
||||
super
|
||||
@host_header = "#{host}:#{port}"
|
||||
end
|
||||
def post_init
|
||||
super
|
||||
@connected = EM::DefaultDeferrable.new
|
||||
end
|
||||
|
||||
|
||||
def connection_completed
|
||||
super
|
||||
@connected.succeed
|
||||
end
|
||||
|
||||
#--
|
||||
# Make sure to throw away any leftover incoming data if we've
|
||||
# been closed due to recognizing an error.
|
||||
#
|
||||
# Generate an internal error if we get an unreasonable number of
|
||||
# header lines. It could be malicious.
|
||||
#
|
||||
def receive_line ln
|
||||
p ln
|
||||
return if @closed
|
||||
|
||||
if ln.length > 0
|
||||
(@current_response.headers ||= []).push ln
|
||||
abort_connection if @current_response.headers.length > 100
|
||||
else
|
||||
process_received_headers
|
||||
end
|
||||
end
|
||||
|
||||
#--
|
||||
# We come here when we've seen all the headers for a particular request.
|
||||
# What we do next depends on the response line (which should be the
|
||||
# first line in the header set), and whether there is content to read.
|
||||
# We may transition into a text-reading state to read content, or
|
||||
# we may abort the connection, or we may go right back into parsing
|
||||
# responses for the next response in the chain.
|
||||
#
|
||||
# We make an ASSUMPTION that the first line is an HTTP response.
|
||||
# Anything else produces an error that aborts the connection.
|
||||
# This may not be enough, because it may be that responses to pipelined
|
||||
# requests will come with a blank-line delimiter.
|
||||
#
|
||||
# Any non-2xx response will be treated as a fatal error, and abort the
|
||||
# connection. We will set up the status and other response parameters.
|
||||
# TODO: we will want to properly support 1xx responses, which some versions
|
||||
# of IIS copiously generate.
|
||||
# TODO: We need to give the option of not aborting the connection with certain
|
||||
# non-200 responses, in order to work with NTLM and other authentication
|
||||
# schemes that work at the level of individual connections.
|
||||
#
|
||||
# Some error responses will get sugarings. For example, we'll return the
|
||||
# Location header in the response in case of a 301/302 response.
|
||||
#
|
||||
# Possible dispositions here:
|
||||
# 1) No content to read (either content-length is zero or it's a HEAD request);
|
||||
# 2) Switch to text mode to read a specific number of bytes;
|
||||
# 3) Read a chunked or multipart response;
|
||||
# 4) Read till the server closes the connection.
|
||||
#
|
||||
# Our reponse to the client can be either to wait till all the content
|
||||
# has been read and then to signal caller's deferrable, or else to signal
|
||||
# it when we finish the processing the headers and then expect the caller
|
||||
# to have given us a block to call as the content comes in. And of course
|
||||
# the latter gets stickier with chunks and multiparts.
|
||||
#
|
||||
HttpResponseRE = /\AHTTP\/(1.[01]) ([\d]{3})/i
|
||||
ClenRE = /\AContent-length:\s*(\d+)/i
|
||||
def process_received_headers
|
||||
abort_connection unless @current_response.headers.first =~ HttpResponseRE
|
||||
@current_response.version = $1.dup
|
||||
st = $2.dup
|
||||
@current_response.status = st.to_i
|
||||
abort_connection unless st[0,1] == "2"
|
||||
|
||||
clen = nil
|
||||
@current_response.headers.each do |e|
|
||||
if clen == nil and e =~ ClenRE
|
||||
clen = $1.dup.to_i
|
||||
end
|
||||
end
|
||||
|
||||
if clen
|
||||
set_text_mode clen
|
||||
end
|
||||
end
|
||||
private :process_received_headers
|
||||
|
||||
|
||||
def receive_binary_data text
|
||||
@current_response.content = text
|
||||
@current_response.deferrable.succeed @current_response
|
||||
@requests.pop
|
||||
@current_response = (@requests.last || []).last
|
||||
set_line_mode
|
||||
end
|
||||
|
||||
|
||||
|
||||
# We've received either a server error or an internal error.
|
||||
# Close the connection and abort any pending requests.
|
||||
#--
|
||||
# When should we call close_connection? It will cause #unbind
|
||||
# to be fired. Should the user expect to see #unbind before
|
||||
# we call #receive_http_error, or the other way around?
|
||||
#
|
||||
# Set instance variable @closed. That's used to inhibit further
|
||||
# processing of any inbound data after an error has been recognized.
|
||||
#
|
||||
# We shouldn't have to worry about any leftover outbound data,
|
||||
# because we call close_connection (not close_connection_after_writing).
|
||||
# That ensures that any pipelined requests received after an error
|
||||
# DO NOT get streamed out to the server on this connection.
|
||||
# Very important. TODO, write a unit-test to establish that behavior.
|
||||
#
|
||||
def abort_connection
|
||||
close_connection
|
||||
@closed = true
|
||||
@current_response.deferrable.fail( @current_response )
|
||||
end
|
||||
|
||||
|
||||
#------------------------
|
||||
# Below here are user-overridable methods.
|
||||
|
||||
end
|
||||
=end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,125 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 15 November 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
# A protocol that handles line-oriented data with interspersed binary text.
|
||||
#
|
||||
# This version is optimized for performance. See EventMachine::Protocols::LineText2
|
||||
# for a version which is optimized for correctness with regard to binary text blocks
|
||||
# that can switch back to line mode.
|
||||
class LineAndTextProtocol < Connection
|
||||
MaxBinaryLength = 32*1024*1024
|
||||
|
||||
def initialize *args
|
||||
super
|
||||
lbp_init_line_state
|
||||
end
|
||||
|
||||
def receive_data data
|
||||
if @lbp_mode == :lines
|
||||
begin
|
||||
@lpb_buffer.extract(data).each do |line|
|
||||
receive_line(line.chomp) if respond_to?(:receive_line)
|
||||
end
|
||||
rescue
|
||||
receive_error('overlength line') if respond_to?(:receive_error)
|
||||
close_connection
|
||||
return
|
||||
end
|
||||
else
|
||||
if @lbp_binary_limit > 0
|
||||
wanted = @lbp_binary_limit - @lbp_binary_bytes_received
|
||||
chunk = nil
|
||||
if data.length > wanted
|
||||
chunk = data.slice!(0...wanted)
|
||||
else
|
||||
chunk = data
|
||||
data = ""
|
||||
end
|
||||
@lbp_binary_buffer[@lbp_binary_bytes_received...(@lbp_binary_bytes_received+chunk.length)] = chunk
|
||||
@lbp_binary_bytes_received += chunk.length
|
||||
if @lbp_binary_bytes_received == @lbp_binary_limit
|
||||
receive_binary_data(@lbp_binary_buffer) if respond_to?(:receive_binary_data)
|
||||
lbp_init_line_state
|
||||
end
|
||||
receive_data(data) if data.length > 0
|
||||
else
|
||||
receive_binary_data(data) if respond_to?(:receive_binary_data)
|
||||
data = ""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def unbind
|
||||
if @lbp_mode == :binary and @lbp_binary_limit > 0
|
||||
if respond_to?(:receive_binary_data)
|
||||
receive_binary_data( @lbp_binary_buffer[0...@lbp_binary_bytes_received] )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Set up to read the supplied number of binary bytes.
|
||||
# This recycles all the data currently waiting in the line buffer, if any.
|
||||
# If the limit is nil, then ALL subsequent data will be treated as binary
|
||||
# data and passed to the upstream protocol handler as we receive it.
|
||||
# If a limit is given, we'll hold the incoming binary data and not
|
||||
# pass it upstream until we've seen it all, or until there is an unbind
|
||||
# (in which case we'll pass up a partial).
|
||||
# Specifying nil for the limit (the default) means there is no limit.
|
||||
# Specifiyng zero for the limit will cause an immediate transition back to line mode.
|
||||
#
|
||||
def set_binary_mode size = nil
|
||||
if @lbp_mode == :lines
|
||||
if size == 0
|
||||
receive_binary_data("") if respond_to?(:receive_binary_data)
|
||||
# Do no more work here. Stay in line mode and keep consuming data.
|
||||
else
|
||||
@lbp_binary_limit = size.to_i # (nil will be stored as zero)
|
||||
if @lbp_binary_limit > 0
|
||||
raise "Overlength" if @lbp_binary_limit > MaxBinaryLength # arbitrary sanity check
|
||||
@lbp_binary_buffer = "\0" * @lbp_binary_limit
|
||||
@lbp_binary_bytes_received = 0
|
||||
end
|
||||
|
||||
@lbp_mode = :binary
|
||||
receive_data @lpb_buffer.flush
|
||||
end
|
||||
else
|
||||
raise "invalid operation"
|
||||
end
|
||||
end
|
||||
|
||||
#--
|
||||
# For internal use, establish protocol baseline for handling lines.
|
||||
def lbp_init_line_state
|
||||
@lpb_buffer = BufferedTokenizer.new("\n")
|
||||
@lbp_mode = :lines
|
||||
end
|
||||
private :lbp_init_line_state
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
module EventMachine
|
||||
module Protocols
|
||||
# LineProtocol will parse out newline terminated strings from a receive_data stream
|
||||
#
|
||||
# module Server
|
||||
# include EM::P::LineProtocol
|
||||
#
|
||||
# def receive_line(line)
|
||||
# send_data("you said: #{line}")
|
||||
# end
|
||||
# end
|
||||
#
|
||||
module LineProtocol
|
||||
# @private
|
||||
def receive_data data
|
||||
(@buf ||= '') << data
|
||||
|
||||
while @buf.slice!(/(.*?)\r?\n/)
|
||||
receive_line($1)
|
||||
end
|
||||
end
|
||||
|
||||
# Invoked with lines received over the network
|
||||
def receive_line(line)
|
||||
# stub
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,179 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 15 November 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
# In the grand, time-honored tradition of re-inventing the wheel, we offer
|
||||
# here YET ANOTHER protocol that handles line-oriented data with interspersed
|
||||
# binary text. This one trades away some of the performance optimizations of
|
||||
# EventMachine::Protocols::LineAndTextProtocol in order to get better correctness
|
||||
# with regard to binary text blocks that can switch back to line mode. It also
|
||||
# permits the line-delimiter to change in midstream.
|
||||
# This was originally written to support Stomp.
|
||||
module LineText2
|
||||
# TODO! We're not enforcing the limits on header lengths and text-lengths.
|
||||
# When we get around to that, call #receive_error if the user defined it, otherwise
|
||||
# throw exceptions.
|
||||
|
||||
MaxBinaryLength = 32*1024*1024
|
||||
|
||||
#--
|
||||
# Will loop internally until there's no data left to read.
|
||||
# That way the user-defined handlers we call can modify the
|
||||
# handling characteristics on a per-token basis.
|
||||
#
|
||||
def receive_data data
|
||||
return unless (data and data.length > 0)
|
||||
|
||||
# Do this stuff in lieu of a constructor.
|
||||
@lt2_mode ||= :lines
|
||||
@lt2_delimiter ||= "\n"
|
||||
@lt2_linebuffer ||= []
|
||||
|
||||
remaining_data = data
|
||||
|
||||
while remaining_data.length > 0
|
||||
if @lt2_mode == :lines
|
||||
delimiter_string = case @lt2_delimiter
|
||||
when Regexp
|
||||
remaining_data.slice(@lt2_delimiter)
|
||||
else
|
||||
@lt2_delimiter
|
||||
end
|
||||
ix = remaining_data.index(delimiter_string) if delimiter_string
|
||||
if ix
|
||||
@lt2_linebuffer << remaining_data[0...ix]
|
||||
ln = @lt2_linebuffer.join
|
||||
@lt2_linebuffer.clear
|
||||
if @lt2_delimiter == "\n"
|
||||
ln.chomp!
|
||||
end
|
||||
receive_line ln
|
||||
remaining_data = remaining_data[(ix+delimiter_string.length)..-1]
|
||||
else
|
||||
@lt2_linebuffer << remaining_data
|
||||
remaining_data = ""
|
||||
end
|
||||
elsif @lt2_mode == :text
|
||||
if @lt2_textsize
|
||||
needed = @lt2_textsize - @lt2_textpos
|
||||
will_take = if remaining_data.length > needed
|
||||
needed
|
||||
else
|
||||
remaining_data.length
|
||||
end
|
||||
|
||||
@lt2_textbuffer << remaining_data[0...will_take]
|
||||
tail = remaining_data[will_take..-1]
|
||||
|
||||
@lt2_textpos += will_take
|
||||
if @lt2_textpos >= @lt2_textsize
|
||||
# Reset line mode (the default behavior) BEFORE calling the
|
||||
# receive_binary_data. This makes it possible for user code
|
||||
# to call set_text_mode, enabling chains of text blocks
|
||||
# (which can possibly be of different sizes).
|
||||
set_line_mode
|
||||
receive_binary_data @lt2_textbuffer.join
|
||||
receive_end_of_binary_data
|
||||
end
|
||||
|
||||
remaining_data = tail
|
||||
else
|
||||
receive_binary_data remaining_data
|
||||
remaining_data = ""
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# The line delimiter may be a regular expression or a string. Anything
|
||||
# passed to set_delimiter other than a regular expression will be
|
||||
# converted to a string.
|
||||
def set_delimiter delim
|
||||
@lt2_delimiter = case delim
|
||||
when Regexp
|
||||
delim
|
||||
else
|
||||
delim.to_s
|
||||
end
|
||||
end
|
||||
|
||||
# Called internally but also exposed to user code, for the case in which
|
||||
# processing of binary data creates a need to transition back to line mode.
|
||||
# We support an optional parameter to "throw back" some data, which might
|
||||
# be an umprocessed chunk of the transmitted binary data, or something else
|
||||
# entirely.
|
||||
def set_line_mode data=""
|
||||
@lt2_mode = :lines
|
||||
(@lt2_linebuffer ||= []).clear
|
||||
receive_data data.to_s
|
||||
end
|
||||
|
||||
def set_text_mode size=nil
|
||||
if size == 0
|
||||
set_line_mode
|
||||
else
|
||||
@lt2_mode = :text
|
||||
(@lt2_textbuffer ||= []).clear
|
||||
@lt2_textsize = size # which can be nil, signifying no limit
|
||||
@lt2_textpos = 0
|
||||
end
|
||||
end
|
||||
|
||||
# Alias for #set_text_mode, added for back-compatibility with LineAndTextProtocol.
|
||||
def set_binary_mode size=nil
|
||||
set_text_mode size
|
||||
end
|
||||
|
||||
# In case of a dropped connection, we'll send a partial buffer to user code
|
||||
# when in sized text mode. User overrides of #receive_binary_data need to
|
||||
# be aware that they may get a short buffer.
|
||||
def unbind
|
||||
@lt2_mode ||= nil
|
||||
if @lt2_mode == :text and @lt2_textpos > 0
|
||||
receive_binary_data @lt2_textbuffer.join
|
||||
end
|
||||
end
|
||||
|
||||
# Stub. Should be subclassed by user code.
|
||||
def receive_line ln
|
||||
# no-op
|
||||
end
|
||||
|
||||
# Stub. Should be subclassed by user code.
|
||||
def receive_binary_data data
|
||||
# no-op
|
||||
end
|
||||
|
||||
# Stub. Should be subclassed by user code.
|
||||
# This is called when transitioning internally from text mode
|
||||
# back to line mode. Useful when client code doesn't want
|
||||
# to keep track of how much data it's received.
|
||||
def receive_end_of_binary_data
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,331 @@
|
||||
module EventMachine
|
||||
module Protocols
|
||||
# Implements the Memcache protocol (http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt).
|
||||
# Requires memcached >= 1.2.4 w/ noreply support
|
||||
#
|
||||
# == Usage example
|
||||
#
|
||||
# EM.run{
|
||||
# cache = EM::P::Memcache.connect 'localhost', 11211
|
||||
#
|
||||
# cache.set :a, 'hello'
|
||||
# cache.set :b, 'hi'
|
||||
# cache.set :c, 'how are you?'
|
||||
# cache.set :d, ''
|
||||
#
|
||||
# cache.get(:a){ |v| p v }
|
||||
# cache.get_hash(:a, :b, :c, :d){ |v| p v }
|
||||
# cache.get(:a,:b,:c,:d){ |a,b,c,d| p [a,b,c,d] }
|
||||
#
|
||||
# cache.get(:a,:z,:b,:y,:d){ |a,z,b,y,d| p [a,z,b,y,d] }
|
||||
#
|
||||
# cache.get(:missing){ |m| p [:missing=, m] }
|
||||
# cache.set(:missing, 'abc'){ p :stored }
|
||||
# cache.get(:missing){ |m| p [:missing=, m] }
|
||||
# cache.del(:missing){ p :deleted }
|
||||
# cache.get(:missing){ |m| p [:missing=, m] }
|
||||
# }
|
||||
#
|
||||
module Memcache
|
||||
include EM::Deferrable
|
||||
|
||||
##
|
||||
# constants
|
||||
|
||||
unless defined? Cempty
|
||||
# @private
|
||||
Cstored = 'STORED'.freeze
|
||||
# @private
|
||||
Cend = 'END'.freeze
|
||||
# @private
|
||||
Cdeleted = 'DELETED'.freeze
|
||||
# @private
|
||||
Cunknown = 'NOT_FOUND'.freeze
|
||||
# @private
|
||||
Cerror = 'ERROR'.freeze
|
||||
|
||||
# @private
|
||||
Cempty = ''.freeze
|
||||
# @private
|
||||
Cdelimiter = "\r\n".freeze
|
||||
end
|
||||
|
||||
##
|
||||
# commands
|
||||
|
||||
# Get the value associated with one or multiple keys
|
||||
#
|
||||
# cache.get(:a){ |v| p v }
|
||||
# cache.get(:a,:b,:c,:d){ |a,b,c,d| p [a,b,c,d] }
|
||||
#
|
||||
def get *keys
|
||||
raise ArgumentError unless block_given?
|
||||
|
||||
callback{
|
||||
keys = keys.map{|k| k.to_s.gsub(/\s/,'_') }
|
||||
send_data "get #{keys.join(' ')}\r\n"
|
||||
@get_cbs << [keys, proc{ |values|
|
||||
yield *keys.map{ |k| values[k] }
|
||||
}]
|
||||
}
|
||||
end
|
||||
|
||||
# Set the value for a given key
|
||||
#
|
||||
# cache.set :a, 'hello'
|
||||
# cache.set(:missing, 'abc'){ puts "stored the value!" }
|
||||
#
|
||||
def set key, val, exptime = 0, &cb
|
||||
callback{
|
||||
val = val.to_s
|
||||
send_cmd :set, key, 0, exptime, val.respond_to?(:bytesize) ? val.bytesize : val.size, !block_given?
|
||||
send_data val
|
||||
send_data Cdelimiter
|
||||
@set_cbs << cb if cb
|
||||
}
|
||||
end
|
||||
|
||||
# Gets multiple values as a hash
|
||||
#
|
||||
# cache.get_hash(:a, :b, :c, :d){ |h| puts h[:a] }
|
||||
#
|
||||
def get_hash *keys
|
||||
raise ArgumentError unless block_given?
|
||||
|
||||
get *keys do |*values|
|
||||
yield keys.inject({}){ |hash, k| hash.update k => values[keys.index(k)] }
|
||||
end
|
||||
end
|
||||
|
||||
# Delete the value associated with a key
|
||||
#
|
||||
# cache.del :a
|
||||
# cache.del(:b){ puts "deleted the value!" }
|
||||
#
|
||||
def delete key, expires = 0, &cb
|
||||
callback{
|
||||
send_data "delete #{key} #{expires}#{cb ? '' : ' noreply'}\r\n"
|
||||
@del_cbs << cb if cb
|
||||
}
|
||||
end
|
||||
alias del delete
|
||||
|
||||
# Connect to a memcached server (must support NOREPLY, memcached >= 1.2.4)
|
||||
def self.connect host = 'localhost', port = 11211
|
||||
EM.connect host, port, self, host, port
|
||||
end
|
||||
|
||||
def send_cmd cmd, key, flags = 0, exptime = 0, bytes = 0, noreply = false
|
||||
send_data "#{cmd} #{key} #{flags} #{exptime} #{bytes}#{noreply ? ' noreply' : ''}\r\n"
|
||||
end
|
||||
private :send_cmd
|
||||
|
||||
##
|
||||
# errors
|
||||
|
||||
# @private
|
||||
class ParserError < StandardError
|
||||
end
|
||||
|
||||
##
|
||||
# em hooks
|
||||
|
||||
# @private
|
||||
def initialize host, port = 11211
|
||||
@host, @port = host, port
|
||||
end
|
||||
|
||||
# @private
|
||||
def connection_completed
|
||||
@get_cbs = []
|
||||
@set_cbs = []
|
||||
@del_cbs = []
|
||||
|
||||
@values = {}
|
||||
|
||||
@reconnecting = false
|
||||
@connected = true
|
||||
succeed
|
||||
# set_delimiter "\r\n"
|
||||
# set_line_mode
|
||||
end
|
||||
|
||||
#--
|
||||
# 19Feb09 Switched to a custom parser, LineText2 is recursive and can cause
|
||||
# stack overflows when there is too much data.
|
||||
# include EM::P::LineText2
|
||||
# @private
|
||||
def receive_data data
|
||||
(@buffer||='') << data
|
||||
|
||||
while index = @buffer.index(Cdelimiter)
|
||||
begin
|
||||
line = @buffer.slice!(0,index+2)
|
||||
process_cmd line
|
||||
rescue ParserError
|
||||
@buffer[0...0] = line
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#--
|
||||
# def receive_line line
|
||||
# @private
|
||||
def process_cmd line
|
||||
case line.strip
|
||||
when /^VALUE\s+(.+?)\s+(\d+)\s+(\d+)/ # VALUE <key> <flags> <bytes>
|
||||
bytes = Integer($3)
|
||||
# set_binary_mode bytes+2
|
||||
# @cur_key = $1
|
||||
if @buffer.size >= bytes + 2
|
||||
@values[$1] = @buffer.slice!(0,bytes)
|
||||
@buffer.slice!(0,2) # \r\n
|
||||
else
|
||||
raise ParserError
|
||||
end
|
||||
|
||||
when Cend # END
|
||||
if entry = @get_cbs.shift
|
||||
keys, cb = entry
|
||||
cb.call(@values)
|
||||
end
|
||||
@values = {}
|
||||
|
||||
when Cstored # STORED
|
||||
if cb = @set_cbs.shift
|
||||
cb.call(true)
|
||||
end
|
||||
|
||||
when Cdeleted # DELETED
|
||||
if cb = @del_cbs.shift
|
||||
cb.call(true)
|
||||
end
|
||||
|
||||
when Cunknown # NOT_FOUND
|
||||
if cb = @del_cbs.shift
|
||||
cb.call(false)
|
||||
end
|
||||
|
||||
else
|
||||
p [:MEMCACHE_UNKNOWN, line]
|
||||
end
|
||||
end
|
||||
|
||||
#--
|
||||
# def receive_binary_data data
|
||||
# @values[@cur_key] = data[0..-3]
|
||||
# end
|
||||
|
||||
# @private
|
||||
def unbind
|
||||
if @connected or @reconnecting
|
||||
EM.add_timer(1){ reconnect @host, @port }
|
||||
@connected = false
|
||||
@reconnecting = true
|
||||
@deferred_status = nil
|
||||
else
|
||||
raise 'Unable to connect to memcached server'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if __FILE__ == $0
|
||||
# ruby -I ext:lib -r eventmachine -rubygems lib/protocols/memcache.rb
|
||||
require 'em/spec'
|
||||
|
||||
# @private
|
||||
class TestConnection
|
||||
include EM::P::Memcache
|
||||
def send_data data
|
||||
sent_data << data
|
||||
end
|
||||
def sent_data
|
||||
@sent_data ||= ''
|
||||
end
|
||||
|
||||
def initialize
|
||||
connection_completed
|
||||
end
|
||||
end
|
||||
|
||||
EM.describe EM::Protocols::Memcache do
|
||||
|
||||
before{
|
||||
@c = TestConnection.new
|
||||
}
|
||||
|
||||
should 'send get requests' do
|
||||
@c.get('a'){}
|
||||
@c.sent_data.should == "get a\r\n"
|
||||
done
|
||||
end
|
||||
|
||||
should 'send set requests' do
|
||||
@c.set('a', 1){}
|
||||
@c.sent_data.should == "set a 0 0 1\r\n1\r\n"
|
||||
done
|
||||
end
|
||||
|
||||
should 'use noreply on set without block' do
|
||||
@c.set('a', 1)
|
||||
@c.sent_data.should == "set a 0 0 1 noreply\r\n1\r\n"
|
||||
done
|
||||
end
|
||||
|
||||
should 'send delete requests' do
|
||||
@c.del('a')
|
||||
@c.sent_data.should == "delete a 0 noreply\r\n"
|
||||
done
|
||||
end
|
||||
|
||||
should 'work when get returns no values' do
|
||||
@c.get('a'){ |a|
|
||||
a.should.be.nil
|
||||
done
|
||||
}
|
||||
|
||||
@c.receive_data "END\r\n"
|
||||
end
|
||||
|
||||
should 'invoke block on set' do
|
||||
@c.set('a', 1){
|
||||
done
|
||||
}
|
||||
|
||||
@c.receive_data "STORED\r\n"
|
||||
end
|
||||
|
||||
should 'invoke block on delete' do
|
||||
@c.delete('a'){ |found|
|
||||
found.should.be.false
|
||||
}
|
||||
@c.delete('b'){ |found|
|
||||
found.should.be.true
|
||||
done
|
||||
}
|
||||
|
||||
@c.receive_data "NOT_FOUND\r\n"
|
||||
@c.receive_data "DELETED\r\n"
|
||||
end
|
||||
|
||||
should 'parse split responses' do
|
||||
@c.get('a'){ |a|
|
||||
a.should == 'abc'
|
||||
done
|
||||
}
|
||||
|
||||
@c.receive_data "VAL"
|
||||
@c.receive_data "UE a 0 "
|
||||
@c.receive_data "3\r\n"
|
||||
@c.receive_data "ab"
|
||||
@c.receive_data "c"
|
||||
@c.receive_data "\r\n"
|
||||
@c.receive_data "EN"
|
||||
@c.receive_data "D\r\n"
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
module EventMachine
|
||||
module Protocols
|
||||
# ObjectProtocol allows for easy communication using marshaled ruby objects
|
||||
#
|
||||
# module RubyServer
|
||||
# include EM::P::ObjectProtocol
|
||||
#
|
||||
# def receive_object obj
|
||||
# send_object({'you said' => obj})
|
||||
# end
|
||||
# end
|
||||
#
|
||||
module ObjectProtocol
|
||||
# By default returns Marshal, override to return JSON or YAML, or any
|
||||
# other serializer/deserializer responding to #dump and #load.
|
||||
def serializer
|
||||
Marshal
|
||||
end
|
||||
|
||||
# @private
|
||||
def receive_data data
|
||||
(@buf ||= '') << data
|
||||
|
||||
while @buf.size >= 4
|
||||
if @buf.size >= 4+(size=@buf.unpack('N').first)
|
||||
@buf.slice!(0,4)
|
||||
receive_object serializer.load(@buf.slice!(0,size))
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Invoked with ruby objects received over the network
|
||||
def receive_object obj
|
||||
# stub
|
||||
end
|
||||
|
||||
# Sends a ruby object over the network
|
||||
def send_object obj
|
||||
data = serializer.dump(obj)
|
||||
send_data [data.respond_to?(:bytesize) ? data.bytesize : data.size, data].pack('Na*')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,246 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 15 November 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-08 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
require 'postgres-pr/message'
|
||||
require 'postgres-pr/connection'
|
||||
require 'stringio'
|
||||
|
||||
# @private
|
||||
class StringIO
|
||||
# Reads exactly +n+ bytes.
|
||||
#
|
||||
# If the data read is nil an EOFError is raised.
|
||||
#
|
||||
# If the data read is too short an IOError is raised
|
||||
def readbytes(n)
|
||||
str = read(n)
|
||||
if str == nil
|
||||
raise EOFError, "End of file reached"
|
||||
end
|
||||
if str.size < n
|
||||
raise IOError, "data truncated"
|
||||
end
|
||||
str
|
||||
end
|
||||
alias read_exactly_n_bytes readbytes
|
||||
end
|
||||
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
# PROVISIONAL IMPLEMENTATION of an evented Postgres client.
|
||||
# This implements version 3 of the Postgres wire protocol, which will work
|
||||
# with any Postgres version from roughly 7.4 onward.
|
||||
#
|
||||
# Objective: we want to access Postgres databases without requiring threads.
|
||||
# Until now this has been a problem because the Postgres client implementations
|
||||
# have all made use of blocking I/O calls, which is incompatible with a
|
||||
# thread-free evented model.
|
||||
#
|
||||
# But rather than re-implement the Postgres Wire3 protocol, we're taking advantage
|
||||
# of the existing postgres-pr library, which was originally written by Michael
|
||||
# Neumann but (at this writing) appears to be no longer maintained. Still, it's
|
||||
# in basically a production-ready state, and the wire protocol isn't that complicated
|
||||
# anyway.
|
||||
#
|
||||
# We're tucking in a bunch of require statements that may not be present in garden-variety
|
||||
# EM installations. Until we find a good way to only require these if a program
|
||||
# requires postgres, this file will need to be required explicitly.
|
||||
#
|
||||
# We need to monkeypatch StringIO because it lacks the #readbytes method needed
|
||||
# by postgres-pr.
|
||||
# The StringIO monkeypatch is lifted from the standard library readbytes.rb,
|
||||
# which adds method #readbytes directly to class IO. But StringIO is not a subclass of IO.
|
||||
# It is modified to raise an IOError instead of TruncatedDataException since the exception is unused.
|
||||
#
|
||||
# We cloned the handling of postgres messages from lib/postgres-pr/connection.rb
|
||||
# in the postgres-pr library, and modified it for event-handling.
|
||||
#
|
||||
# TODO: The password handling in dispatch_conn_message is totally incomplete.
|
||||
#
|
||||
#
|
||||
# We return Deferrables from the user-level operations surfaced by this interface.
|
||||
# Experimentally, we're using the pattern of always returning a boolean value as the
|
||||
# first argument of a deferrable callback to indicate success or failure. This is
|
||||
# instead of the traditional pattern of calling Deferrable#succeed or #fail, and
|
||||
# requiring the user to define both a callback and an errback function.
|
||||
#
|
||||
# === Usage
|
||||
# EM.run {
|
||||
# db = EM.connect_unix_domain( "/tmp/.s.PGSQL.5432", EM::P::Postgres3 )
|
||||
# db.connect( dbname, username, psw ).callback do |status|
|
||||
# if status
|
||||
# db.query( "select * from some_table" ).callback do |status, result, errors|
|
||||
# if status
|
||||
# result.rows.each do |row|
|
||||
# p row
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# }
|
||||
class Postgres3 < EventMachine::Connection
|
||||
include PostgresPR
|
||||
|
||||
def initialize
|
||||
@data = ""
|
||||
@params = {}
|
||||
end
|
||||
|
||||
def connect db, user, psw=nil
|
||||
d = EM::DefaultDeferrable.new
|
||||
d.timeout 15
|
||||
|
||||
if @pending_query || @pending_conn
|
||||
d.succeed false, "Operation already in progress"
|
||||
else
|
||||
@pending_conn = d
|
||||
prms = {"user"=>user, "database"=>db}
|
||||
@user = user
|
||||
if psw
|
||||
@password = psw
|
||||
#prms["password"] = psw
|
||||
end
|
||||
send_data PostgresPR::StartupMessage.new( 3 << 16, prms ).dump
|
||||
end
|
||||
|
||||
d
|
||||
end
|
||||
|
||||
def query sql
|
||||
d = EM::DefaultDeferrable.new
|
||||
d.timeout 15
|
||||
|
||||
if @pending_query || @pending_conn
|
||||
d.succeed false, "Operation already in progress"
|
||||
else
|
||||
@r = PostgresPR::Connection::Result.new
|
||||
@e = []
|
||||
@pending_query = d
|
||||
send_data PostgresPR::Query.dump(sql)
|
||||
end
|
||||
|
||||
d
|
||||
end
|
||||
|
||||
|
||||
def receive_data data
|
||||
@data << data
|
||||
while @data.length >= 5
|
||||
pktlen = @data[1...5].unpack("N").first
|
||||
if @data.length >= (1 + pktlen)
|
||||
pkt = @data.slice!(0...(1+pktlen))
|
||||
m = StringIO.open( pkt, "r" ) {|io| PostgresPR::Message.read( io ) }
|
||||
if @pending_conn
|
||||
dispatch_conn_message m
|
||||
elsif @pending_query
|
||||
dispatch_query_message m
|
||||
else
|
||||
raise "Unexpected message from database"
|
||||
end
|
||||
else
|
||||
break # very important, break out of the while
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def unbind
|
||||
if o = (@pending_query || @pending_conn)
|
||||
o.succeed false, "lost connection"
|
||||
end
|
||||
end
|
||||
|
||||
# Cloned and modified from the postgres-pr.
|
||||
def dispatch_conn_message msg
|
||||
case msg
|
||||
when AuthentificationClearTextPassword
|
||||
raise ArgumentError, "no password specified" if @password.nil?
|
||||
send_data PasswordMessage.new(@password).dump
|
||||
|
||||
when AuthentificationCryptPassword
|
||||
raise ArgumentError, "no password specified" if @password.nil?
|
||||
send_data PasswordMessage.new(@password.crypt(msg.salt)).dump
|
||||
|
||||
when AuthentificationMD5Password
|
||||
raise ArgumentError, "no password specified" if @password.nil?
|
||||
require 'digest/md5'
|
||||
|
||||
m = Digest::MD5.hexdigest(@password + @user)
|
||||
m = Digest::MD5.hexdigest(m + msg.salt)
|
||||
m = 'md5' + m
|
||||
send_data PasswordMessage.new(m).dump
|
||||
|
||||
when AuthentificationKerberosV4, AuthentificationKerberosV5, AuthentificationSCMCredential
|
||||
raise "unsupported authentification"
|
||||
|
||||
when AuthentificationOk
|
||||
when ErrorResponse
|
||||
raise msg.field_values.join("\t")
|
||||
when NoticeResponse
|
||||
@notice_processor.call(msg) if @notice_processor
|
||||
when ParameterStatus
|
||||
@params[msg.key] = msg.value
|
||||
when BackendKeyData
|
||||
# TODO
|
||||
#p msg
|
||||
when ReadyForQuery
|
||||
# TODO: use transaction status
|
||||
pc,@pending_conn = @pending_conn,nil
|
||||
pc.succeed true
|
||||
else
|
||||
raise "unhandled message type"
|
||||
end
|
||||
end
|
||||
|
||||
# Cloned and modified from the postgres-pr.
|
||||
def dispatch_query_message msg
|
||||
case msg
|
||||
when DataRow
|
||||
@r.rows << msg.columns
|
||||
when CommandComplete
|
||||
@r.cmd_tag = msg.cmd_tag
|
||||
when ReadyForQuery
|
||||
pq,@pending_query = @pending_query,nil
|
||||
pq.succeed true, @r, @e
|
||||
when RowDescription
|
||||
@r.fields = msg.fields
|
||||
when CopyInResponse
|
||||
when CopyOutResponse
|
||||
when EmptyQueryResponse
|
||||
when ErrorResponse
|
||||
# TODO
|
||||
@e << msg
|
||||
when NoticeResponse
|
||||
@notice_processor.call(msg) if @notice_processor
|
||||
else
|
||||
# TODO
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,175 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 15 November 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
|
||||
# Implements SASL authd.
|
||||
# This is a very, very simple protocol that mimics the one used
|
||||
# by saslauthd and pwcheck, two outboard daemons included in the
|
||||
# standard SASL library distro.
|
||||
# The only thing this is really suitable for is SASL PLAIN
|
||||
# (user+password) authentication, but the SASL libs that are
|
||||
# linked into standard servers (like imapd and sendmail) implement
|
||||
# the other ones.
|
||||
#
|
||||
# SASL-auth is intended for reasonably fast operation inside a
|
||||
# single machine, so it has no transport-security (although there
|
||||
# have been multi-machine extensions incorporating transport-layer
|
||||
# encryption).
|
||||
#
|
||||
# The standard saslauthd module generally runs privileged and does
|
||||
# its work by referring to the system-account files.
|
||||
#
|
||||
# This feature was added to EventMachine to enable the development
|
||||
# of custom authentication/authorization engines for standard servers.
|
||||
#
|
||||
# To use SASLauth, include it in a class that subclasses EM::Connection,
|
||||
# and reimplement the validate method.
|
||||
#
|
||||
# The typical way to incorporate this module into an authentication
|
||||
# daemon would be to set it as the handler for a UNIX-domain socket.
|
||||
# The code might look like this:
|
||||
#
|
||||
# EM.start_unix_domain_server( "/var/run/saslauthd/mux", MyHandler )
|
||||
# File.chmod( 0777, "/var/run/saslauthd/mux")
|
||||
#
|
||||
# The chmod is probably needed to ensure that unprivileged clients can
|
||||
# access the UNIX-domain socket.
|
||||
#
|
||||
# It's also a very good idea to drop superuser privileges (if any), after
|
||||
# the UNIX-domain socket has been opened.
|
||||
#--
|
||||
# Implementation details: assume the client can send us pipelined requests,
|
||||
# and that the client will close the connection.
|
||||
#
|
||||
# The client sends us four values, each encoded as a two-byte length field in
|
||||
# network order followed by the specified number of octets.
|
||||
# The fields specify the username, password, service name (such as imap),
|
||||
# and the "realm" name. We send back the barest minimum reply, a single
|
||||
# field also encoded as a two-octet length in network order, followed by
|
||||
# either "NO" or "OK" - simplicity itself.
|
||||
#
|
||||
# We enforce a maximum field size just as a sanity check.
|
||||
# We do NOT automatically time out the connection.
|
||||
#
|
||||
# The code we use to parse out the values is ugly and probably slow.
|
||||
# Improvements welcome.
|
||||
#
|
||||
module SASLauth
|
||||
|
||||
MaxFieldSize = 128*1024
|
||||
def post_init
|
||||
super
|
||||
@sasl_data = ""
|
||||
@sasl_values = []
|
||||
end
|
||||
|
||||
def receive_data data
|
||||
@sasl_data << data
|
||||
while @sasl_data.length >= 2
|
||||
len = (@sasl_data[0,2].unpack("n")).first
|
||||
raise "SASL Max Field Length exceeded" if len > MaxFieldSize
|
||||
if @sasl_data.length >= (len + 2)
|
||||
@sasl_values << @sasl_data[2,len]
|
||||
@sasl_data.slice!(0...(2+len))
|
||||
if @sasl_values.length == 4
|
||||
send_data( validate(*@sasl_values) ? "\0\002OK" : "\0\002NO" )
|
||||
@sasl_values.clear
|
||||
end
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def validate username, psw, sysname, realm
|
||||
p username
|
||||
p psw
|
||||
p sysname
|
||||
p realm
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
# Implements the SASL authd client protocol.
|
||||
# This is a very, very simple protocol that mimics the one used
|
||||
# by saslauthd and pwcheck, two outboard daemons included in the
|
||||
# standard SASL library distro.
|
||||
# The only thing this is really suitable for is SASL PLAIN
|
||||
# (user+password) authentication, but the SASL libs that are
|
||||
# linked into standard servers (like imapd and sendmail) implement
|
||||
# the other ones.
|
||||
#
|
||||
# You can use this module directly as a handler for EM Connections,
|
||||
# or include it in a module or handler class of your own.
|
||||
#
|
||||
# First connect to a SASL server (it's probably a TCP server, or more
|
||||
# likely a Unix-domain socket). Then call the #validate? method,
|
||||
# passing at least a username and a password. #validate? returns
|
||||
# a Deferrable which will either succeed or fail, depending
|
||||
# on the status of the authentication operation.
|
||||
#
|
||||
module SASLauthclient
|
||||
MaxFieldSize = 128*1024
|
||||
|
||||
def validate? username, psw, sysname=nil, realm=nil
|
||||
|
||||
str = [username, psw, sysname, realm].map {|m|
|
||||
[(m || "").length, (m || "")]
|
||||
}.flatten.pack( "nA*" * 4 )
|
||||
send_data str
|
||||
|
||||
d = EM::DefaultDeferrable.new
|
||||
@queries.unshift d
|
||||
d
|
||||
end
|
||||
|
||||
def post_init
|
||||
@sasl_data = ""
|
||||
@queries = []
|
||||
end
|
||||
|
||||
def receive_data data
|
||||
@sasl_data << data
|
||||
|
||||
while @sasl_data.length > 2
|
||||
len = (@sasl_data[0,2].unpack("n")).first
|
||||
raise "SASL Max Field Length exceeded" if len > MaxFieldSize
|
||||
if @sasl_data.length >= (len + 2)
|
||||
val = @sasl_data[2,len]
|
||||
@sasl_data.slice!(0...(2+len))
|
||||
q = @queries.pop
|
||||
(val == "NO") ? q.fail : q.succeed
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,394 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 16 July 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
require 'ostruct'
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
|
||||
# Simple SMTP client
|
||||
#
|
||||
# @example
|
||||
# email = EM::Protocols::SmtpClient.send(
|
||||
# :domain=>"example.com",
|
||||
# :host=>'localhost',
|
||||
# :port=>25, # optional, defaults 25
|
||||
# :starttls=>true, # use ssl
|
||||
# :from=>"sender@example.com",
|
||||
# :to=> ["to_1@example.com", "to_2@example.com"],
|
||||
# :header=> {"Subject" => "This is a subject line"},
|
||||
# :body=> "This is the body of the email"
|
||||
# )
|
||||
# email.callback{
|
||||
# puts 'Email sent!'
|
||||
# }
|
||||
# email.errback{ |e|
|
||||
# puts 'Email failed!'
|
||||
# }
|
||||
#
|
||||
# Sending generated emails (using Mail)
|
||||
#
|
||||
# mail = Mail.new do
|
||||
# from 'alice@example.com'
|
||||
# to 'bob@example.com'
|
||||
# subject 'This is a test email'
|
||||
# body 'Hello, world!'
|
||||
# end
|
||||
#
|
||||
# email = EM::P::SmtpClient.send(
|
||||
# :domain=>'example.com',
|
||||
# :from=>mail.from.first,
|
||||
# :to=>mail.to,
|
||||
# :message=>mail.to_s
|
||||
# )
|
||||
#
|
||||
class SmtpClient < Connection
|
||||
include EventMachine::Deferrable
|
||||
include EventMachine::Protocols::LineText2
|
||||
|
||||
def initialize
|
||||
@succeeded = nil
|
||||
@responder = nil
|
||||
@code = nil
|
||||
@msg = nil
|
||||
end
|
||||
|
||||
# :host => required String
|
||||
# a string containing the IP address or host name of the SMTP server to connect to.
|
||||
# :port => optional
|
||||
# defaults to 25.
|
||||
# :domain => required String
|
||||
# This is passed as the argument to the EHLO command.
|
||||
# :starttls => optional Boolean
|
||||
# If it evaluates true, then the client will initiate STARTTLS with
|
||||
# the server, and abort the connection if the negotiation doesn't succeed.
|
||||
# TODO, need to be able to pass certificate parameters with this option.
|
||||
# :auth => optional Hash of auth parameters
|
||||
# If not given, then no auth will be attempted.
|
||||
# (In that case, the connection will be aborted if the server requires auth.)
|
||||
# Specify the hash value :type to determine the auth type, along with additional parameters
|
||||
# depending on the type.
|
||||
# Currently only :type => :plain is supported. Pass additional parameters :username (String),
|
||||
# and :password (either a String or a Proc that will be called at auth-time).
|
||||
#
|
||||
# @example
|
||||
# :auth => {:type=>:plain, :username=>"mickey@disney.com", :password=>"mouse"}
|
||||
#
|
||||
# :from => required String
|
||||
# Specifies the sender of the message. Will be passed as the argument
|
||||
# to the MAIL FROM. Do NOT enclose the argument in angle-bracket (<>) characters.
|
||||
# The connection will abort if the server rejects the value.
|
||||
# :to => required String or Array of Strings
|
||||
# The recipient(s) of the message. Do NOT enclose
|
||||
# any of the values in angle-brackets (<>) characters. It's NOT a fatal error if one or more
|
||||
# recipients are rejected by the server. (Of course, if ALL of them are, the server will most
|
||||
# likely trigger an error when we try to send data.) An array of codes containing the status
|
||||
# of each requested recipient is available after the call completes. TODO, we should define
|
||||
# an overridable stub that will be called on rejection of a recipient or a sender, giving
|
||||
# user code the chance to try again or abort the connection.
|
||||
#
|
||||
# One of either :message, :content, or :header and :body is required:
|
||||
#
|
||||
# :message => String
|
||||
# A valid RFC2822 Internet Message.
|
||||
# :content => String
|
||||
# Raw data which MUST be in correct SMTP body format, with escaped leading dots and a trailing
|
||||
# dot line.
|
||||
# :header => String or Hash of values to be transmitted in the header of the message.
|
||||
# The hash keys are the names of the headers (do NOT append a trailing colon), and the values
|
||||
# are strings containing the header values. TODO, support Arrays of header values, which would
|
||||
# cause us to send that specific header line more than once.
|
||||
#
|
||||
# @example
|
||||
# :header => {"Subject" => "Bogus", "CC" => "myboss@example.com"}
|
||||
#
|
||||
# :body => Optional String or Array of Strings, defaults blank.
|
||||
# This will be passed as the body of the email message.
|
||||
# TODO, this needs to be significantly beefed up. As currently written, this requires the caller
|
||||
# to properly format the input into CRLF-delimited lines of 7-bit characters in the standard
|
||||
# SMTP transmission format. We need to be able to automatically convert binary data, and add
|
||||
# correct line-breaks to text data.
|
||||
#
|
||||
# :verbose => Optional.
|
||||
# If true, will cause a lot of information (including the server-side of the
|
||||
# conversation) to be dumped to $>.
|
||||
#
|
||||
def self.send args={}
|
||||
args[:port] ||= 25
|
||||
args[:body] ||= ""
|
||||
|
||||
=begin
|
||||
(I don't think it's possible for EM#connect to throw an exception under normal
|
||||
circumstances, so this original code is stubbed out. A connect-failure will result
|
||||
in the #unbind method being called without calling #connection_completed.)
|
||||
begin
|
||||
EventMachine.connect( args[:host], args[:port], self) {|c|
|
||||
# According to the EM docs, we will get here AFTER post_init is called.
|
||||
c.args = args
|
||||
c.set_comm_inactivity_timeout 60
|
||||
}
|
||||
rescue
|
||||
# We'll get here on a connect error. This code mimics the effect
|
||||
# of a call to invoke_internal_error. Would be great to DRY this up.
|
||||
# (Actually, it may be that we never get here, if EM#connect catches
|
||||
# its errors internally.)
|
||||
d = EM::DefaultDeferrable.new
|
||||
d.set_deferred_status(:failed, {:error=>[:connect, 500, "unable to connect to server"]})
|
||||
d
|
||||
end
|
||||
=end
|
||||
EventMachine.connect( args[:host], args[:port], self) {|c|
|
||||
# According to the EM docs, we will get here AFTER post_init is called.
|
||||
c.args = args
|
||||
c.set_comm_inactivity_timeout 60
|
||||
}
|
||||
end
|
||||
|
||||
attr_writer :args
|
||||
|
||||
# @private
|
||||
def post_init
|
||||
@return_values = OpenStruct.new
|
||||
@return_values.start_time = Time.now
|
||||
end
|
||||
|
||||
# @private
|
||||
def connection_completed
|
||||
@responder = :receive_signon
|
||||
@msg = []
|
||||
end
|
||||
|
||||
# We can get here in a variety of ways, all of them being failures unless
|
||||
# the @succeeded flag is set. If a protocol success was recorded, then don't
|
||||
# set a deferred success because the caller will already have done it
|
||||
# (no need to wait until the connection closes to invoke the callbacks).
|
||||
#
|
||||
# @private
|
||||
def unbind
|
||||
unless @succeeded
|
||||
@return_values.elapsed_time = Time.now - @return_values.start_time
|
||||
@return_values.responder = @responder
|
||||
@return_values.code = @code
|
||||
@return_values.message = @msg
|
||||
set_deferred_status(:failed, @return_values)
|
||||
end
|
||||
end
|
||||
|
||||
# @private
|
||||
def receive_line ln
|
||||
$>.puts ln if @args[:verbose]
|
||||
@range = ln[0...1].to_i
|
||||
@code = ln[0...3].to_i
|
||||
@msg << ln[4..-1]
|
||||
unless ln[3...4] == '-'
|
||||
$>.puts @responder if @args[:verbose]
|
||||
send @responder
|
||||
@msg.clear
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# We encountered an error from the server and will close the connection.
|
||||
# Use the error and message the server returned.
|
||||
#
|
||||
def invoke_error
|
||||
@return_values.elapsed_time = Time.now - @return_values.start_time
|
||||
@return_values.responder = @responder
|
||||
@return_values.code = @code
|
||||
@return_values.message = @msg
|
||||
set_deferred_status :failed, @return_values
|
||||
send_data "QUIT\r\n"
|
||||
close_connection_after_writing
|
||||
end
|
||||
|
||||
# We encountered an error on our side of the protocol and will close the connection.
|
||||
# Use an extra-protocol error code (900) and use the message from the caller.
|
||||
#
|
||||
def invoke_internal_error msg = "???"
|
||||
@return_values.elapsed_time = Time.now - @return_values.start_time
|
||||
@return_values.responder = @responder
|
||||
@return_values.code = 900
|
||||
@return_values.message = msg
|
||||
set_deferred_status :failed, @return_values
|
||||
send_data "QUIT\r\n"
|
||||
close_connection_after_writing
|
||||
end
|
||||
|
||||
def send_ehlo
|
||||
send_data "EHLO #{@args[:domain]}\r\n"
|
||||
end
|
||||
|
||||
def receive_signon
|
||||
return invoke_error unless @range == 2
|
||||
send_ehlo
|
||||
@responder = :receive_ehlo_response
|
||||
end
|
||||
def receive_ehlo_response
|
||||
return invoke_error unless @range == 2
|
||||
@server_caps = @msg
|
||||
invoke_starttls
|
||||
end
|
||||
|
||||
def invoke_starttls
|
||||
if @args[:starttls]
|
||||
# It would be more sociable to first ask if @server_caps contains
|
||||
# the string "STARTTLS" before we invoke it, but hey, life's too short.
|
||||
send_data "STARTTLS\r\n"
|
||||
@responder = :receive_starttls_response
|
||||
else
|
||||
invoke_auth
|
||||
end
|
||||
end
|
||||
def receive_starttls_response
|
||||
return invoke_error unless @range == 2
|
||||
start_tls
|
||||
invoke_ehlo_over_tls
|
||||
end
|
||||
|
||||
def invoke_ehlo_over_tls
|
||||
send_ehlo
|
||||
@responder = :receive_ehlo_over_tls_response
|
||||
end
|
||||
def receive_ehlo_over_tls_response
|
||||
return invoke_error unless @range == 2
|
||||
invoke_auth
|
||||
end
|
||||
|
||||
# Perform an authentication. If the caller didn't request one, then fall through
|
||||
# to the mail-from state.
|
||||
def invoke_auth
|
||||
if @args[:auth]
|
||||
if @args[:auth][:type] == :plain
|
||||
psw = @args[:auth][:password]
|
||||
if psw.respond_to?(:call)
|
||||
psw = psw.call
|
||||
end
|
||||
#str = Base64::encode64("\0#{@args[:auth][:username]}\0#{psw}").chomp
|
||||
str = ["\0#{@args[:auth][:username]}\0#{psw}"].pack("m").gsub(/\n/, '')
|
||||
send_data "AUTH PLAIN #{str}\r\n"
|
||||
@responder = :receive_auth_response
|
||||
else
|
||||
return invoke_internal_error("unsupported auth type")
|
||||
end
|
||||
else
|
||||
invoke_mail_from
|
||||
end
|
||||
end
|
||||
def receive_auth_response
|
||||
return invoke_error unless @range == 2
|
||||
invoke_mail_from
|
||||
end
|
||||
|
||||
def invoke_mail_from
|
||||
send_data "MAIL FROM: <#{@args[:from]}>\r\n"
|
||||
@responder = :receive_mail_from_response
|
||||
end
|
||||
def receive_mail_from_response
|
||||
return invoke_error unless @range == 2
|
||||
invoke_rcpt_to
|
||||
end
|
||||
|
||||
def invoke_rcpt_to
|
||||
@rcpt_responses ||= []
|
||||
l = @rcpt_responses.length
|
||||
to = @args[:to].is_a?(Array) ? @args[:to] : [@args[:to].to_s]
|
||||
if l < to.length
|
||||
send_data "RCPT TO: <#{to[l]}>\r\n"
|
||||
@responder = :receive_rcpt_to_response
|
||||
else
|
||||
e = @rcpt_responses.select {|rr| rr.last == 2}
|
||||
if e and e.length > 0
|
||||
invoke_data
|
||||
else
|
||||
invoke_error
|
||||
end
|
||||
end
|
||||
end
|
||||
def receive_rcpt_to_response
|
||||
@rcpt_responses << [@code, @msg, @range]
|
||||
invoke_rcpt_to
|
||||
end
|
||||
|
||||
def escape_leading_dots(s)
|
||||
s.gsub(/^\./, '..')
|
||||
end
|
||||
|
||||
def invoke_data
|
||||
send_data "DATA\r\n"
|
||||
@responder = :receive_data_response
|
||||
end
|
||||
def receive_data_response
|
||||
return invoke_error unless @range == 3
|
||||
|
||||
# The data to send can be given in either @args[:message], @args[:content], or the
|
||||
# combination of @args[:header] and @args[:body].
|
||||
#
|
||||
# - @args[:message] (String) MUST be a valid RFC2822 Internet Message
|
||||
#
|
||||
# - @args[:content] (String) MUST be in correct SMTP body format, with escaped
|
||||
# leading dots and a trailing dot line
|
||||
#
|
||||
# - @args[:header] (Hash or String)
|
||||
# - @args[:body] (Array or String)
|
||||
if @args[:message]
|
||||
send_data escape_leading_dots(@args[:message].to_s)
|
||||
send_data "\r\n.\r\n"
|
||||
elsif @args[:content]
|
||||
send_data @args[:content].to_s
|
||||
else
|
||||
# The header can be a hash or an array.
|
||||
if @args[:header].is_a?(Hash)
|
||||
(@args[:header] || {}).each {|k,v| send_data escape_leading_dots("#{k}: #{v}\r\n") }
|
||||
else
|
||||
send_data escape_leading_dots(@args[:header].to_s)
|
||||
end
|
||||
send_data "\r\n"
|
||||
|
||||
if @args[:body].is_a?(Array)
|
||||
@args[:body].each {|e| send_data escape_leading_dots(e)}
|
||||
else
|
||||
send_data escape_leading_dots(@args[:body].to_s)
|
||||
end
|
||||
|
||||
send_data "\r\n.\r\n"
|
||||
end
|
||||
|
||||
@responder = :receive_message_response
|
||||
end
|
||||
def receive_message_response
|
||||
return invoke_error unless @range == 2
|
||||
send_data "QUIT\r\n"
|
||||
close_connection_after_writing
|
||||
@succeeded = true
|
||||
@return_values.elapsed_time = Time.now - @return_values.start_time
|
||||
@return_values.responder = @responder
|
||||
@return_values.code = @code
|
||||
@return_values.message = @msg
|
||||
set_deferred_status :succeeded, @return_values
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,666 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 16 July 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
|
||||
# This is a protocol handler for the server side of SMTP.
|
||||
# It's NOT a complete SMTP server obeying all the semantics of servers conforming to
|
||||
# RFC2821. Rather, it uses overridable method stubs to communicate protocol states
|
||||
# and data to user code. User code is responsible for doing the right things with the
|
||||
# data in order to get complete and correct SMTP server behavior.
|
||||
#
|
||||
# Simple SMTP server example:
|
||||
#
|
||||
# class EmailServer < EM::P::SmtpServer
|
||||
# def receive_plain_auth(user, pass)
|
||||
# true
|
||||
# end
|
||||
#
|
||||
# def get_server_domain
|
||||
# "mock.smtp.server.local"
|
||||
# end
|
||||
#
|
||||
# def get_server_greeting
|
||||
# "mock smtp server greets you with impunity"
|
||||
# end
|
||||
#
|
||||
# def receive_sender(sender)
|
||||
# current.sender = sender
|
||||
# true
|
||||
# end
|
||||
#
|
||||
# def receive_recipient(recipient)
|
||||
# current.recipient = recipient
|
||||
# true
|
||||
# end
|
||||
#
|
||||
# def receive_message
|
||||
# current.received = true
|
||||
# current.completed_at = Time.now
|
||||
#
|
||||
# p [:received_email, current]
|
||||
# @current = OpenStruct.new
|
||||
# true
|
||||
# end
|
||||
#
|
||||
# def receive_ehlo_domain(domain)
|
||||
# @ehlo_domain = domain
|
||||
# true
|
||||
# end
|
||||
#
|
||||
# def receive_data_command
|
||||
# current.data = ""
|
||||
# true
|
||||
# end
|
||||
#
|
||||
# def receive_data_chunk(data)
|
||||
# current.data << data.join("\n")
|
||||
# true
|
||||
# end
|
||||
#
|
||||
# def receive_transaction
|
||||
# if @ehlo_domain
|
||||
# current.ehlo_domain = @ehlo_domain
|
||||
# @ehlo_domain = nil
|
||||
# end
|
||||
# true
|
||||
# end
|
||||
#
|
||||
# def current
|
||||
# @current ||= OpenStruct.new
|
||||
# end
|
||||
#
|
||||
# def self.start(host = 'localhost', port = 1025)
|
||||
# require 'ostruct'
|
||||
# @server = EM.start_server host, port, self
|
||||
# end
|
||||
#
|
||||
# def self.stop
|
||||
# if @server
|
||||
# EM.stop_server @server
|
||||
# @server = nil
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def self.running?
|
||||
# !!@server
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# EM.run{ EmailServer.start }
|
||||
#
|
||||
#--
|
||||
# Useful paragraphs in RFC-2821:
|
||||
# 4.3.2: Concise list of command-reply sequences, in essence a text representation
|
||||
# of the command state-machine.
|
||||
#
|
||||
# STARTTLS is defined in RFC2487.
|
||||
# Observe that there are important rules governing whether a publicly-referenced server
|
||||
# (meaning one whose Internet address appears in public MX records) may require the
|
||||
# non-optional use of TLS.
|
||||
# Non-optional TLS does not apply to EHLO, NOOP, QUIT or STARTTLS.
|
||||
class SmtpServer < EventMachine::Connection
|
||||
include Protocols::LineText2
|
||||
|
||||
HeloRegex = /\AHELO\s*/i
|
||||
EhloRegex = /\AEHLO\s*/i
|
||||
QuitRegex = /\AQUIT/i
|
||||
MailFromRegex = /\AMAIL FROM:\s*/i
|
||||
RcptToRegex = /\ARCPT TO:\s*/i
|
||||
DataRegex = /\ADATA/i
|
||||
NoopRegex = /\ANOOP/i
|
||||
RsetRegex = /\ARSET/i
|
||||
VrfyRegex = /\AVRFY\s+/i
|
||||
ExpnRegex = /\AEXPN\s+/i
|
||||
HelpRegex = /\AHELP/i
|
||||
StarttlsRegex = /\ASTARTTLS/i
|
||||
AuthRegex = /\AAUTH\s+/i
|
||||
|
||||
|
||||
# Class variable containing default parameters that can be overridden
|
||||
# in application code.
|
||||
# Individual objects of this class will make an instance-local copy of
|
||||
# the class variable, so that they can be reconfigured on a per-instance
|
||||
# basis.
|
||||
#
|
||||
# Chunksize is the number of data lines we'll buffer before
|
||||
# sending them to the application. TODO, make this user-configurable.
|
||||
#
|
||||
@@parms = {
|
||||
:chunksize => 4000,
|
||||
:verbose => false
|
||||
}
|
||||
def self.parms= parms={}
|
||||
@@parms.merge!(parms)
|
||||
end
|
||||
|
||||
|
||||
|
||||
def initialize *args
|
||||
super
|
||||
@parms = @@parms
|
||||
init_protocol_state
|
||||
end
|
||||
|
||||
def parms= parms={}
|
||||
@parms.merge!(parms)
|
||||
end
|
||||
|
||||
# In SMTP, the server talks first. But by a (perhaps flawed) axiom in EM,
|
||||
# #post_init will execute BEFORE the block passed to #start_server, for any
|
||||
# given accepted connection. Since in this class we'll probably be getting
|
||||
# a lot of initialization parameters, we want the guts of post_init to
|
||||
# run AFTER the application has initialized the connection object. So we
|
||||
# use a spawn to schedule the post_init to run later.
|
||||
# It's a little weird, I admit. A reasonable alternative would be to set
|
||||
# parameters as a class variable and to do that before accepting any connections.
|
||||
#
|
||||
# OBSOLETE, now we have @@parms. But the spawn is nice to keep as an illustration.
|
||||
#
|
||||
def post_init
|
||||
#send_data "220 #{get_server_greeting}\r\n" (ORIGINAL)
|
||||
#(EM.spawn {|x| x.send_data "220 #{x.get_server_greeting}\r\n"}).notify(self)
|
||||
(EM.spawn {|x| x.send_server_greeting}).notify(self)
|
||||
end
|
||||
|
||||
def send_server_greeting
|
||||
send_data "220 #{get_server_greeting}\r\n"
|
||||
end
|
||||
|
||||
def receive_line ln
|
||||
@@parms[:verbose] and $>.puts ">>> #{ln}"
|
||||
|
||||
return process_data_line(ln) if @state.include?(:data)
|
||||
return process_auth_line(ln) if @state.include?(:auth_incomplete)
|
||||
|
||||
case ln
|
||||
when EhloRegex
|
||||
process_ehlo $'.dup
|
||||
when HeloRegex
|
||||
process_helo $'.dup
|
||||
when MailFromRegex
|
||||
process_mail_from $'.dup
|
||||
when RcptToRegex
|
||||
process_rcpt_to $'.dup
|
||||
when DataRegex
|
||||
process_data
|
||||
when RsetRegex
|
||||
process_rset
|
||||
when VrfyRegex
|
||||
process_vrfy
|
||||
when ExpnRegex
|
||||
process_expn
|
||||
when HelpRegex
|
||||
process_help
|
||||
when NoopRegex
|
||||
process_noop
|
||||
when QuitRegex
|
||||
process_quit
|
||||
when StarttlsRegex
|
||||
process_starttls
|
||||
when AuthRegex
|
||||
process_auth $'.dup
|
||||
else
|
||||
process_unknown
|
||||
end
|
||||
end
|
||||
|
||||
# TODO - implement this properly, the implementation is a stub!
|
||||
def process_help
|
||||
send_data "250 Ok, but unimplemented\r\n"
|
||||
end
|
||||
|
||||
# RFC2821, 3.5.3 Meaning of VRFY or EXPN Success Response:
|
||||
# A server MUST NOT return a 250 code in response to a VRFY or EXPN
|
||||
# command unless it has actually verified the address. In particular,
|
||||
# a server MUST NOT return 250 if all it has done is to verify that the
|
||||
# syntax given is valid. In that case, 502 (Command not implemented)
|
||||
# or 500 (Syntax error, command unrecognized) SHOULD be returned.
|
||||
#
|
||||
# TODO - implement this properly, the implementation is a stub!
|
||||
def process_vrfy
|
||||
send_data "502 Command not implemented\r\n"
|
||||
end
|
||||
# TODO - implement this properly, the implementation is a stub!
|
||||
def process_expn
|
||||
send_data "502 Command not implemented\r\n"
|
||||
end
|
||||
|
||||
#--
|
||||
# This is called at several points to restore the protocol state
|
||||
# to a pre-transaction state. In essence, we "forget" having seen
|
||||
# any valid command except EHLO and STARTTLS.
|
||||
# We also have to callback user code, in case they're keeping track
|
||||
# of senders, recipients, and whatnot.
|
||||
#
|
||||
# We try to follow the convention of avoiding the verb "receive" for
|
||||
# internal method names except receive_line (which we inherit), and
|
||||
# using only receive_xxx for user-overridable stubs.
|
||||
#
|
||||
# init_protocol_state is called when we initialize the connection as
|
||||
# well as during reset_protocol_state. It does NOT call the user
|
||||
# override method. This enables us to promise the users that they
|
||||
# won't see the overridable fire except after EHLO and RSET, and
|
||||
# after a message has been received. Although the latter may be wrong.
|
||||
# The standard may allow multiple DATA segments with the same set of
|
||||
# senders and recipients.
|
||||
#
|
||||
def reset_protocol_state
|
||||
init_protocol_state
|
||||
s,@state = @state,[]
|
||||
@state << :starttls if s.include?(:starttls)
|
||||
@state << :ehlo if s.include?(:ehlo)
|
||||
receive_transaction
|
||||
end
|
||||
def init_protocol_state
|
||||
@state ||= []
|
||||
end
|
||||
|
||||
|
||||
#--
|
||||
# EHLO/HELO is always legal, per the standard. On success
|
||||
# it always clears buffers and initiates a mail "transaction."
|
||||
# Which means that a MAIL FROM must follow.
|
||||
#
|
||||
# Per the standard, an EHLO/HELO or a RSET "initiates" an email
|
||||
# transaction. Thereafter, MAIL FROM must be received before
|
||||
# RCPT TO, before DATA. Not sure what this specific ordering
|
||||
# achieves semantically, but it does make it easier to
|
||||
# implement. We also support user-specified requirements for
|
||||
# STARTTLS and AUTH. We make it impossible to proceed to MAIL FROM
|
||||
# without fulfilling tls and/or auth, if the user specified either
|
||||
# or both as required. We need to check the extension standard
|
||||
# for auth to see if a credential is discarded after a RSET along
|
||||
# with all the rest of the state. We'll behave as if it is.
|
||||
# Now clearly, we can't discard tls after its been negotiated
|
||||
# without dropping the connection, so that flag doesn't get cleared.
|
||||
#
|
||||
def process_ehlo domain
|
||||
if receive_ehlo_domain domain
|
||||
send_data "250-#{get_server_domain}\r\n"
|
||||
if @@parms[:starttls]
|
||||
send_data "250-STARTTLS\r\n"
|
||||
end
|
||||
if @@parms[:auth]
|
||||
send_data "250-AUTH PLAIN\r\n"
|
||||
end
|
||||
send_data "250-NO-SOLICITING\r\n"
|
||||
# TODO, size needs to be configurable.
|
||||
send_data "250 SIZE 20000000\r\n"
|
||||
reset_protocol_state
|
||||
@state << :ehlo
|
||||
else
|
||||
send_data "550 Requested action not taken\r\n"
|
||||
end
|
||||
end
|
||||
|
||||
def process_helo domain
|
||||
if receive_ehlo_domain domain.dup
|
||||
send_data "250 #{get_server_domain}\r\n"
|
||||
reset_protocol_state
|
||||
@state << :ehlo
|
||||
else
|
||||
send_data "550 Requested action not taken\r\n"
|
||||
end
|
||||
end
|
||||
|
||||
def process_quit
|
||||
send_data "221 Ok\r\n"
|
||||
close_connection_after_writing
|
||||
end
|
||||
|
||||
def process_noop
|
||||
send_data "250 Ok\r\n"
|
||||
end
|
||||
|
||||
def process_unknown
|
||||
send_data "500 Unknown command\r\n"
|
||||
end
|
||||
|
||||
#--
|
||||
# So far, only AUTH PLAIN is supported but we should do at least LOGIN as well.
|
||||
# TODO, support clients that send AUTH PLAIN with no parameter, expecting a 3xx
|
||||
# response and a continuation of the auth conversation.
|
||||
#
|
||||
def process_auth str
|
||||
if @state.include?(:auth)
|
||||
send_data "503 auth already issued\r\n"
|
||||
elsif str =~ /\APLAIN\s?/i
|
||||
if $'.length == 0
|
||||
# we got a partial response, so let the client know to send the rest
|
||||
@state << :auth_incomplete
|
||||
send_data("334 \r\n")
|
||||
else
|
||||
# we got the initial response, so go ahead & process it
|
||||
process_auth_line($')
|
||||
end
|
||||
#elsif str =~ /\ALOGIN\s+/i
|
||||
else
|
||||
send_data "504 auth mechanism not available\r\n"
|
||||
end
|
||||
end
|
||||
|
||||
def process_auth_line(line)
|
||||
plain = line.unpack("m").first
|
||||
_,user,psw = plain.split("\000")
|
||||
|
||||
succeeded = proc {
|
||||
send_data "235 authentication ok\r\n"
|
||||
@state << :auth
|
||||
}
|
||||
failed = proc {
|
||||
send_data "535 invalid authentication\r\n"
|
||||
}
|
||||
auth = receive_plain_auth user,psw
|
||||
|
||||
if auth.respond_to?(:callback)
|
||||
auth.callback(&succeeded)
|
||||
auth.errback(&failed)
|
||||
else
|
||||
(auth ? succeeded : failed).call
|
||||
end
|
||||
|
||||
@state.delete :auth_incomplete
|
||||
end
|
||||
|
||||
#--
|
||||
# Unusually, we can deal with a Deferrable returned from the user application.
|
||||
# This was added to deal with a special case in a particular application, but
|
||||
# it would be a nice idea to add it to the other user-code callbacks.
|
||||
#
|
||||
def process_data
|
||||
unless @state.include?(:rcpt)
|
||||
send_data "503 Operation sequence error\r\n"
|
||||
else
|
||||
succeeded = proc {
|
||||
send_data "354 Send it\r\n"
|
||||
@state << :data
|
||||
@databuffer = []
|
||||
}
|
||||
failed = proc {
|
||||
send_data "550 Operation failed\r\n"
|
||||
}
|
||||
|
||||
d = receive_data_command
|
||||
|
||||
if d.respond_to?(:callback)
|
||||
d.callback(&succeeded)
|
||||
d.errback(&failed)
|
||||
else
|
||||
(d ? succeeded : failed).call
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def process_rset
|
||||
reset_protocol_state
|
||||
receive_reset
|
||||
send_data "250 Ok\r\n"
|
||||
end
|
||||
|
||||
def unbind
|
||||
connection_ended
|
||||
end
|
||||
|
||||
#--
|
||||
# STARTTLS may not be issued before EHLO, or unless the user has chosen
|
||||
# to support it.
|
||||
#
|
||||
# If :starttls_options is present and :starttls is set in the parms
|
||||
# pass the options in :starttls_options to start_tls. Do this if you want to use
|
||||
# your own certificate
|
||||
# e.g. {:cert_chain_file => "/etc/ssl/cert.pem", :private_key_file => "/etc/ssl/private/cert.key"}
|
||||
|
||||
def process_starttls
|
||||
if @@parms[:starttls]
|
||||
if @state.include?(:starttls)
|
||||
send_data "503 TLS Already negotiated\r\n"
|
||||
elsif ! @state.include?(:ehlo)
|
||||
send_data "503 EHLO required before STARTTLS\r\n"
|
||||
else
|
||||
send_data "220 Start TLS negotiation\r\n"
|
||||
start_tls(@@parms[:starttls_options] || {})
|
||||
@state << :starttls
|
||||
end
|
||||
else
|
||||
process_unknown
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
#--
|
||||
# Requiring TLS is touchy, cf RFC2784.
|
||||
# Requiring AUTH seems to be much more reasonable.
|
||||
# We don't currently support any notion of deriving an authentication from the TLS
|
||||
# negotiation, although that would certainly be reasonable.
|
||||
# We DON'T allow MAIL FROM to be given twice.
|
||||
# We DON'T enforce all the various rules for validating the sender or
|
||||
# the reverse-path (like whether it should be null), and notifying the reverse
|
||||
# path in case of delivery problems. All of that is left to the calling application.
|
||||
#
|
||||
def process_mail_from sender
|
||||
if (@@parms[:starttls]==:required and !@state.include?(:starttls))
|
||||
send_data "550 This server requires STARTTLS before MAIL FROM\r\n"
|
||||
elsif (@@parms[:auth]==:required and !@state.include?(:auth))
|
||||
send_data "550 This server requires authentication before MAIL FROM\r\n"
|
||||
elsif @state.include?(:mail_from)
|
||||
send_data "503 MAIL already given\r\n"
|
||||
else
|
||||
unless receive_sender sender
|
||||
send_data "550 sender is unacceptable\r\n"
|
||||
else
|
||||
send_data "250 Ok\r\n"
|
||||
@state << :mail_from
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#--
|
||||
# Since we require :mail_from to have been seen before we process RCPT TO,
|
||||
# we don't need to repeat the tests for TLS and AUTH.
|
||||
# Note that we don't remember or do anything else with the recipients.
|
||||
# All of that is on the user code.
|
||||
# TODO: we should enforce user-definable limits on the total number of
|
||||
# recipients per transaction.
|
||||
# We might want to make sure that a given recipient is only seen once, but
|
||||
# for now we'll let that be the user's problem.
|
||||
#
|
||||
# User-written code can return a deferrable from receive_recipient.
|
||||
#
|
||||
def process_rcpt_to rcpt
|
||||
unless @state.include?(:mail_from)
|
||||
send_data "503 MAIL is required before RCPT\r\n"
|
||||
else
|
||||
succeeded = proc {
|
||||
send_data "250 Ok\r\n"
|
||||
@state << :rcpt unless @state.include?(:rcpt)
|
||||
}
|
||||
failed = proc {
|
||||
send_data "550 recipient is unacceptable\r\n"
|
||||
}
|
||||
|
||||
d = receive_recipient rcpt
|
||||
|
||||
if d.respond_to?(:set_deferred_status)
|
||||
d.callback(&succeeded)
|
||||
d.errback(&failed)
|
||||
else
|
||||
(d ? succeeded : failed).call
|
||||
end
|
||||
|
||||
=begin
|
||||
unless receive_recipient rcpt
|
||||
send_data "550 recipient is unacceptable\r\n"
|
||||
else
|
||||
send_data "250 Ok\r\n"
|
||||
@state << :rcpt unless @state.include?(:rcpt)
|
||||
end
|
||||
=end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Send the incoming data to the application one chunk at a time, rather than
|
||||
# one line at a time. That lets the application be a little more flexible about
|
||||
# storing to disk, etc.
|
||||
# Since we clear the chunk array every time we submit it, the caller needs to be
|
||||
# aware to do things like dup it if he wants to keep it around across calls.
|
||||
#
|
||||
# Resets the transaction upon disposition of the incoming message.
|
||||
# RFC5321 says this about the MAIL FROM command:
|
||||
# "This command tells the SMTP-receiver that a new mail transaction is
|
||||
# starting and to reset all its state tables and buffers, including any
|
||||
# recipients or mail data."
|
||||
#
|
||||
# Equivalent behaviour is implemented by resetting after a completed transaction.
|
||||
#
|
||||
# User-written code can return a Deferrable as a response from receive_message.
|
||||
#
|
||||
def process_data_line ln
|
||||
if ln == "."
|
||||
if @databuffer.length > 0
|
||||
receive_data_chunk @databuffer
|
||||
@databuffer.clear
|
||||
end
|
||||
|
||||
|
||||
succeeded = proc {
|
||||
send_data "250 Message accepted\r\n"
|
||||
reset_protocol_state
|
||||
}
|
||||
failed = proc {
|
||||
send_data "550 Message rejected\r\n"
|
||||
reset_protocol_state
|
||||
}
|
||||
d = receive_message
|
||||
|
||||
if d.respond_to?(:set_deferred_status)
|
||||
d.callback(&succeeded)
|
||||
d.errback(&failed)
|
||||
else
|
||||
(d ? succeeded : failed).call
|
||||
end
|
||||
|
||||
@state.delete :data
|
||||
else
|
||||
# slice off leading . if any
|
||||
ln.slice!(0...1) if ln[0] == ?.
|
||||
@databuffer << ln
|
||||
if @databuffer.length > @@parms[:chunksize]
|
||||
receive_data_chunk @databuffer
|
||||
@databuffer.clear
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
#------------------------------------------
|
||||
# Everything from here on can be overridden in user code.
|
||||
|
||||
# The greeting returned in the initial connection message to the client.
|
||||
def get_server_greeting
|
||||
"EventMachine SMTP Server"
|
||||
end
|
||||
# The domain name returned in the first line of the response to a
|
||||
# successful EHLO or HELO command.
|
||||
def get_server_domain
|
||||
"Ok EventMachine SMTP Server"
|
||||
end
|
||||
|
||||
# A false response from this user-overridable method will cause a
|
||||
# 550 error to be returned to the remote client.
|
||||
#
|
||||
def receive_ehlo_domain domain
|
||||
true
|
||||
end
|
||||
|
||||
# Return true or false to indicate that the authentication is acceptable.
|
||||
def receive_plain_auth user, password
|
||||
true
|
||||
end
|
||||
|
||||
# Receives the argument of the MAIL FROM command. Return false to
|
||||
# indicate to the remote client that the sender is not accepted.
|
||||
# This can only be successfully called once per transaction.
|
||||
#
|
||||
def receive_sender sender
|
||||
true
|
||||
end
|
||||
|
||||
# Receives the argument of a RCPT TO command. Can be given multiple
|
||||
# times per transaction. Return false to reject the recipient.
|
||||
#
|
||||
def receive_recipient rcpt
|
||||
true
|
||||
end
|
||||
|
||||
# Sent when the remote peer issues the RSET command.
|
||||
# Since RSET is not allowed to fail (according to the protocol),
|
||||
# we ignore any return value from user overrides of this method.
|
||||
#
|
||||
def receive_reset
|
||||
end
|
||||
|
||||
# Sent when the remote peer has ended the connection.
|
||||
#
|
||||
def connection_ended
|
||||
end
|
||||
|
||||
# Called when the remote peer sends the DATA command.
|
||||
# Returning false will cause us to send a 550 error to the peer.
|
||||
# This can be useful for dealing with problems that arise from processing
|
||||
# the whole set of sender and recipients.
|
||||
#
|
||||
def receive_data_command
|
||||
true
|
||||
end
|
||||
|
||||
# Sent when data from the remote peer is available. The size can be controlled
|
||||
# by setting the :chunksize parameter. This call can be made multiple times.
|
||||
# The goal is to strike a balance between sending the data to the application one
|
||||
# line at a time, and holding all of a very large message in memory.
|
||||
#
|
||||
def receive_data_chunk data
|
||||
@smtps_msg_size ||= 0
|
||||
@smtps_msg_size += data.join.length
|
||||
STDERR.write "<#{@smtps_msg_size}>"
|
||||
end
|
||||
|
||||
# Sent after a message has been completely received. User code
|
||||
# must return true or false to indicate whether the message has
|
||||
# been accepted for delivery.
|
||||
def receive_message
|
||||
@@parms[:verbose] and $>.puts "Received complete message"
|
||||
true
|
||||
end
|
||||
|
||||
# This is called when the protocol state is reset. It happens
|
||||
# when the remote client calls EHLO/HELO or RSET.
|
||||
def receive_transaction
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,66 @@
|
||||
module EventMachine
|
||||
module Protocols
|
||||
# Basic SOCKS v4 client implementation
|
||||
#
|
||||
# Use as you would any regular connection:
|
||||
#
|
||||
# class MyConn < EM::P::Socks4
|
||||
# def post_init
|
||||
# send_data("sup")
|
||||
# end
|
||||
#
|
||||
# def receive_data(data)
|
||||
# send_data("you said: #{data}")
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# EM.connect socks_host, socks_port, MyConn, host, port
|
||||
#
|
||||
class Socks4 < Connection
|
||||
def initialize(host, port)
|
||||
@host = Socket.gethostbyname(host).last
|
||||
@port = port
|
||||
@socks_error_code = nil
|
||||
@buffer = ''
|
||||
setup_methods
|
||||
end
|
||||
|
||||
def setup_methods
|
||||
class << self
|
||||
def post_init; socks_post_init; end
|
||||
def receive_data(*a); socks_receive_data(*a); end
|
||||
end
|
||||
end
|
||||
|
||||
def restore_methods
|
||||
class << self
|
||||
remove_method :post_init
|
||||
remove_method :receive_data
|
||||
end
|
||||
end
|
||||
|
||||
def socks_post_init
|
||||
header = [4, 1, @port, @host, 0].flatten.pack("CCnA4C")
|
||||
send_data(header)
|
||||
end
|
||||
|
||||
def socks_receive_data(data)
|
||||
@buffer << data
|
||||
return if @buffer.size < 8
|
||||
|
||||
header_resp = @buffer.slice! 0, 8
|
||||
_, r = header_resp.unpack("cc")
|
||||
if r != 90
|
||||
@socks_error_code = r
|
||||
close_connection
|
||||
return
|
||||
end
|
||||
|
||||
restore_methods
|
||||
|
||||
post_init
|
||||
receive_data(@buffer) unless @buffer.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,205 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 15 November 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
|
||||
# Implements Stomp (http://docs.codehaus.org/display/STOMP/Protocol).
|
||||
#
|
||||
# == Usage example
|
||||
#
|
||||
# module StompClient
|
||||
# include EM::Protocols::Stomp
|
||||
#
|
||||
# def connection_completed
|
||||
# connect :login => 'guest', :passcode => 'guest'
|
||||
# end
|
||||
#
|
||||
# def receive_msg msg
|
||||
# if msg.command == "CONNECTED"
|
||||
# subscribe '/some/topic'
|
||||
# else
|
||||
# p ['got a message', msg]
|
||||
# puts msg.body
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# EM.run{
|
||||
# EM.connect 'localhost', 61613, StompClient
|
||||
# }
|
||||
#
|
||||
module Stomp
|
||||
include LineText2
|
||||
|
||||
class Message
|
||||
# The command associated with the message, usually 'CONNECTED' or 'MESSAGE'
|
||||
attr_accessor :command
|
||||
# Hash containing headers such as destination and message-id
|
||||
attr_accessor :header
|
||||
alias :headers :header
|
||||
# Body of the message
|
||||
attr_accessor :body
|
||||
|
||||
# @private
|
||||
def initialize
|
||||
@header = {}
|
||||
@state = :precommand
|
||||
@content_length = nil
|
||||
end
|
||||
# @private
|
||||
def consume_line line
|
||||
if @state == :precommand
|
||||
unless line =~ /\A\s*\Z/
|
||||
@command = line
|
||||
@state = :headers
|
||||
end
|
||||
elsif @state == :headers
|
||||
if line == ""
|
||||
if @content_length
|
||||
yield( [:sized_text, @content_length+1] )
|
||||
else
|
||||
@state = :body
|
||||
yield( [:unsized_text] )
|
||||
end
|
||||
elsif line =~ /\A([^:]+):(.+)\Z/
|
||||
k = $1.dup.strip
|
||||
v = $2.dup.strip
|
||||
@header[k] = v
|
||||
if k == "content-length"
|
||||
@content_length = v.to_i
|
||||
end
|
||||
else
|
||||
# This is a protocol error. How to signal it?
|
||||
end
|
||||
elsif @state == :body
|
||||
@body = line
|
||||
yield( [:dispatch] )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# @private
|
||||
def send_frame verb, headers={}, body=""
|
||||
body = body.to_s
|
||||
ary = [verb, "\n"]
|
||||
body_bytesize = body.bytesize if body.respond_to? :bytesize
|
||||
body_bytesize ||= body.size
|
||||
headers.each {|k,v| ary << "#{k}:#{v}\n" }
|
||||
ary << "content-length: #{body_bytesize}\n"
|
||||
ary << "content-type: text/plain; charset=UTF-8\n" unless headers.has_key? 'content-type'
|
||||
ary << "\n"
|
||||
ary << body
|
||||
ary << "\0"
|
||||
send_data ary.join
|
||||
end
|
||||
|
||||
# @private
|
||||
def receive_line line
|
||||
@stomp_initialized || init_message_reader
|
||||
@stomp_message.consume_line(line) {|outcome|
|
||||
if outcome.first == :sized_text
|
||||
set_text_mode outcome[1]
|
||||
elsif outcome.first == :unsized_text
|
||||
set_delimiter "\0"
|
||||
elsif outcome.first == :dispatch
|
||||
receive_msg(@stomp_message) if respond_to?(:receive_msg)
|
||||
init_message_reader
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
# @private
|
||||
def receive_binary_data data
|
||||
@stomp_message.body = data[0..-2]
|
||||
receive_msg(@stomp_message) if respond_to?(:receive_msg)
|
||||
init_message_reader
|
||||
end
|
||||
|
||||
# @private
|
||||
def init_message_reader
|
||||
@stomp_initialized = true
|
||||
set_delimiter "\n"
|
||||
set_line_mode
|
||||
@stomp_message = Message.new
|
||||
end
|
||||
|
||||
# Invoked with an incoming Stomp::Message received from the STOMP server
|
||||
def receive_msg msg
|
||||
# stub, overwrite this in your handler
|
||||
end
|
||||
|
||||
# CONNECT command, for authentication
|
||||
#
|
||||
# connect :login => 'guest', :passcode => 'guest'
|
||||
#
|
||||
def connect parms={}
|
||||
send_frame "CONNECT", parms
|
||||
end
|
||||
|
||||
# SEND command, for publishing messages to a topic
|
||||
#
|
||||
# send '/topic/name', 'some message here'
|
||||
#
|
||||
def send destination, body, parms={}
|
||||
send_frame "SEND", parms.merge( :destination=>destination ), body.to_s
|
||||
end
|
||||
|
||||
# SUBSCRIBE command, for subscribing to topics
|
||||
#
|
||||
# subscribe '/topic/name', false
|
||||
#
|
||||
def subscribe dest, ack=false
|
||||
send_frame "SUBSCRIBE", {:destination=>dest, :ack=>(ack ? "client" : "auto")}
|
||||
end
|
||||
|
||||
# ACK command, for acknowledging receipt of messages
|
||||
#
|
||||
# module StompClient
|
||||
# include EM::P::Stomp
|
||||
#
|
||||
# def connection_completed
|
||||
# connect :login => 'guest', :passcode => 'guest'
|
||||
# # subscribe with ack mode
|
||||
# subscribe '/some/topic', true
|
||||
# end
|
||||
#
|
||||
# def receive_msg msg
|
||||
# if msg.command == "MESSAGE"
|
||||
# ack msg.headers['message-id']
|
||||
# puts msg.body
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
def ack msgid
|
||||
send_frame "ACK", 'message-id'=> msgid
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 16 July 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
|
||||
# @private
|
||||
class TcpConnectTester < Connection
|
||||
include EventMachine::Deferrable
|
||||
|
||||
def self.test( host, port )
|
||||
EventMachine.connect( host, port, self )
|
||||
end
|
||||
|
||||
def post_init
|
||||
@start_time = Time.now
|
||||
end
|
||||
|
||||
def connection_completed
|
||||
@completed = true
|
||||
set_deferred_status :succeeded, (Time.now - @start_time)
|
||||
close_connection
|
||||
end
|
||||
|
||||
def unbind
|
||||
set_deferred_status :failed, (Time.now - @start_time) unless @completed
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,80 @@
|
||||
module EventMachine
|
||||
# A cross thread, reactor scheduled, linear queue.
|
||||
#
|
||||
# This class provides a simple queue abstraction on top of the reactor
|
||||
# scheduler. It services two primary purposes:
|
||||
#
|
||||
# * API sugar for stateful protocols
|
||||
# * Pushing processing onto the reactor thread
|
||||
#
|
||||
# @example
|
||||
#
|
||||
# q = EM::Queue.new
|
||||
# q.push('one', 'two', 'three')
|
||||
# 3.times do
|
||||
# q.pop { |msg| puts(msg) }
|
||||
# end
|
||||
#
|
||||
class Queue
|
||||
def initialize
|
||||
@sink = []
|
||||
@drain = []
|
||||
@popq = []
|
||||
end
|
||||
|
||||
# Pop items off the queue, running the block on the reactor thread. The pop
|
||||
# will not happen immediately, but at some point in the future, either in
|
||||
# the next tick, if the queue has data, or when the queue is populated.
|
||||
#
|
||||
# @return [NilClass] nil
|
||||
def pop(*a, &b)
|
||||
cb = EM::Callback(*a, &b)
|
||||
EM.schedule do
|
||||
if @drain.empty?
|
||||
@drain = @sink
|
||||
@sink = []
|
||||
end
|
||||
if @drain.empty?
|
||||
@popq << cb
|
||||
else
|
||||
cb.call @drain.shift
|
||||
end
|
||||
end
|
||||
nil # Always returns nil
|
||||
end
|
||||
|
||||
# Push items onto the queue in the reactor thread. The items will not appear
|
||||
# in the queue immediately, but will be scheduled for addition during the
|
||||
# next reactor tick.
|
||||
def push(*items)
|
||||
EM.schedule do
|
||||
@sink.push(*items)
|
||||
unless @popq.empty?
|
||||
@drain = @sink
|
||||
@sink = []
|
||||
@popq.shift.call @drain.shift until @drain.empty? || @popq.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
alias :<< :push
|
||||
|
||||
# @return [Boolean]
|
||||
# @note This is a peek, it's not thread safe, and may only tend toward accuracy.
|
||||
def empty?
|
||||
@drain.empty? && @sink.empty?
|
||||
end
|
||||
|
||||
# @return [Integer] Queue size
|
||||
# @note This is a peek, it's not thread safe, and may only tend toward accuracy.
|
||||
def size
|
||||
@drain.size + @sink.size
|
||||
end
|
||||
|
||||
# @return [Integer] Waiting size
|
||||
# @note This is a peek at the number of jobs that are currently waiting on the Queue
|
||||
def num_waiting
|
||||
@popq.size
|
||||
end
|
||||
|
||||
end # Queue
|
||||
end # EventMachine
|
||||
@@ -0,0 +1,232 @@
|
||||
module EventMachine
|
||||
module DNS
|
||||
class Resolver
|
||||
|
||||
def self.windows?
|
||||
if RUBY_PLATFORM =~ /mswin32|cygwin|mingw|bccwin/
|
||||
require 'win32/resolv'
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
HOSTS_FILE = windows? ? Win32::Resolv.get_hosts_path : '/etc/hosts'
|
||||
|
||||
@hosts = nil
|
||||
@nameservers = nil
|
||||
@socket = nil
|
||||
|
||||
def self.resolve(hostname)
|
||||
Request.new(socket, hostname)
|
||||
end
|
||||
|
||||
def self.socket
|
||||
if @socket && @socket.error?
|
||||
@socket = Socket.open
|
||||
else
|
||||
@socket ||= Socket.open
|
||||
end
|
||||
end
|
||||
|
||||
def self.nameservers=(ns)
|
||||
@nameservers = ns
|
||||
end
|
||||
|
||||
def self.nameservers
|
||||
return @nameservers if @nameservers
|
||||
|
||||
if windows?
|
||||
_, ns = Win32::Resolv.get_resolv_info
|
||||
return @nameservers = ns || []
|
||||
end
|
||||
|
||||
@nameservers = []
|
||||
IO.readlines('/etc/resolv.conf').each do |line|
|
||||
if line =~ /^nameserver (.+)$/
|
||||
@nameservers << $1.split(/\s+/).first
|
||||
end
|
||||
end
|
||||
|
||||
@nameservers
|
||||
rescue
|
||||
@nameservers = []
|
||||
end
|
||||
|
||||
def self.nameserver
|
||||
nameservers.shuffle.first
|
||||
end
|
||||
|
||||
def self.hosts
|
||||
return @hosts if @hosts
|
||||
|
||||
@hosts = {}
|
||||
IO.readlines(HOSTS_FILE).each do |line|
|
||||
next if line =~ /^#/
|
||||
addr, host = line.split(/\s+/)
|
||||
|
||||
next unless addr && host
|
||||
@hosts[host] ||= []
|
||||
@hosts[host] << addr
|
||||
end
|
||||
|
||||
@hosts
|
||||
rescue
|
||||
@hosts = {}
|
||||
end
|
||||
end
|
||||
|
||||
class RequestIdAlreadyUsed < RuntimeError; end
|
||||
|
||||
class Socket < EventMachine::Connection
|
||||
def self.open
|
||||
EventMachine::open_datagram_socket('0.0.0.0', 0, self)
|
||||
end
|
||||
|
||||
def initialize
|
||||
@nameserver = nil
|
||||
end
|
||||
|
||||
def post_init
|
||||
@requests = {}
|
||||
end
|
||||
|
||||
def start_timer
|
||||
@timer ||= EM.add_periodic_timer(0.1, &method(:tick))
|
||||
end
|
||||
|
||||
def stop_timer
|
||||
EM.cancel_timer(@timer)
|
||||
@timer = nil
|
||||
end
|
||||
|
||||
def unbind
|
||||
end
|
||||
|
||||
def tick
|
||||
@requests.each do |id,req|
|
||||
req.tick
|
||||
end
|
||||
end
|
||||
|
||||
def register_request(id, req)
|
||||
if @requests.has_key?(id)
|
||||
raise RequestIdAlreadyUsed
|
||||
else
|
||||
@requests[id] = req
|
||||
end
|
||||
|
||||
start_timer
|
||||
end
|
||||
|
||||
def deregister_request(id, req)
|
||||
@requests.delete(id)
|
||||
stop_timer if @requests.length == 0
|
||||
end
|
||||
|
||||
def send_packet(pkt)
|
||||
send_datagram(pkt, nameserver, 53)
|
||||
end
|
||||
|
||||
def nameserver=(ns)
|
||||
@nameserver = ns
|
||||
end
|
||||
|
||||
def nameserver
|
||||
@nameserver || Resolver.nameserver
|
||||
end
|
||||
|
||||
# Decodes the packet, looks for the request and passes the
|
||||
# response over to the requester
|
||||
def receive_data(data)
|
||||
msg = nil
|
||||
begin
|
||||
msg = Resolv::DNS::Message.decode data
|
||||
rescue
|
||||
else
|
||||
req = @requests[msg.id]
|
||||
if req
|
||||
@requests.delete(msg.id)
|
||||
stop_timer if @requests.length == 0
|
||||
req.receive_answer(msg)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Request
|
||||
include Deferrable
|
||||
attr_accessor :retry_interval, :max_tries
|
||||
|
||||
def initialize(socket, hostname)
|
||||
@socket = socket
|
||||
@hostname = hostname
|
||||
@tries = 0
|
||||
@last_send = Time.at(0)
|
||||
@retry_interval = 3
|
||||
@max_tries = 5
|
||||
|
||||
if addrs = Resolver.hosts[hostname]
|
||||
succeed addrs
|
||||
else
|
||||
EM.next_tick { tick }
|
||||
end
|
||||
end
|
||||
|
||||
def tick
|
||||
# Break early if nothing to do
|
||||
return if @last_send + @retry_interval > Time.now
|
||||
if @tries < @max_tries
|
||||
send
|
||||
else
|
||||
@socket.deregister_request(@id, self)
|
||||
fail 'retries exceeded'
|
||||
end
|
||||
end
|
||||
|
||||
def receive_answer(msg)
|
||||
addrs = []
|
||||
msg.each_answer do |name,ttl,data|
|
||||
if data.kind_of?(Resolv::DNS::Resource::IN::A) ||
|
||||
data.kind_of?(Resolv::DNS::Resource::IN::AAAA)
|
||||
addrs << data.address.to_s
|
||||
end
|
||||
end
|
||||
|
||||
if addrs.empty?
|
||||
fail "rcode=#{msg.rcode}"
|
||||
else
|
||||
succeed addrs
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def send
|
||||
@tries += 1
|
||||
@last_send = Time.now
|
||||
@socket.send_packet(packet.encode)
|
||||
end
|
||||
|
||||
def id
|
||||
begin
|
||||
@id = rand(65535)
|
||||
@socket.register_request(@id, self)
|
||||
rescue RequestIdAlreadyUsed
|
||||
retry
|
||||
end unless defined?(@id)
|
||||
|
||||
@id
|
||||
end
|
||||
|
||||
def packet
|
||||
msg = Resolv::DNS::Message.new
|
||||
msg.id = id
|
||||
msg.rd = 1
|
||||
msg.add_question @hostname, Resolv::DNS::Resource::IN::A
|
||||
msg
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,84 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 25 Aug 2007
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
# Support for Erlang-style processes.
|
||||
#
|
||||
class SpawnedProcess
|
||||
# Send a message to the spawned process
|
||||
def notify *x
|
||||
me = self
|
||||
EM.next_tick {
|
||||
# A notification executes in the context of this
|
||||
# SpawnedProcess object. That makes self and notify
|
||||
# work as one would expect.
|
||||
#
|
||||
y = me.call(*x)
|
||||
if y and y.respond_to?(:pull_out_yield_block)
|
||||
a,b = y.pull_out_yield_block
|
||||
set_receiver a
|
||||
self.notify if b
|
||||
end
|
||||
}
|
||||
end
|
||||
alias_method :resume, :notify
|
||||
alias_method :run, :notify # for formulations like (EM.spawn {xxx}).run
|
||||
|
||||
def set_receiver blk
|
||||
(class << self ; self ; end).class_eval do
|
||||
remove_method :call if method_defined? :call
|
||||
define_method :call, blk
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# @private
|
||||
class YieldBlockFromSpawnedProcess
|
||||
def initialize block, notify
|
||||
@block = [block,notify]
|
||||
end
|
||||
def pull_out_yield_block
|
||||
@block
|
||||
end
|
||||
end
|
||||
|
||||
# Spawn an erlang-style process
|
||||
def self.spawn &block
|
||||
s = SpawnedProcess.new
|
||||
s.set_receiver block
|
||||
s
|
||||
end
|
||||
|
||||
# @private
|
||||
def self.yield &block
|
||||
return YieldBlockFromSpawnedProcess.new( block, false )
|
||||
end
|
||||
|
||||
# @private
|
||||
def self.yield_and_notify &block
|
||||
return YieldBlockFromSpawnedProcess.new( block, true )
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,118 @@
|
||||
module EventMachine
|
||||
# Streams a file over a given connection. Streaming begins once the object is
|
||||
# instantiated. Typically FileStreamer instances are not reused.
|
||||
#
|
||||
# Streaming uses buffering for files larger than 16K and uses so-called fast file reader (a C++ extension)
|
||||
# if available (it is part of eventmachine gem itself).
|
||||
#
|
||||
# @example
|
||||
#
|
||||
# module FileSender
|
||||
# def post_init
|
||||
# streamer = EventMachine::FileStreamer.new(self, '/tmp/bigfile.tar')
|
||||
# streamer.callback{
|
||||
# # file was sent successfully
|
||||
# close_connection_after_writing
|
||||
# }
|
||||
# end
|
||||
# end
|
||||
#
|
||||
#
|
||||
# @author Francis Cianfrocca
|
||||
class FileStreamer
|
||||
include Deferrable
|
||||
|
||||
# Use mapped streamer for files bigger than 16k
|
||||
MappingThreshold = 16384
|
||||
# Wait until next tick to send more data when 50k is still in the outgoing buffer
|
||||
BackpressureLevel = 50000
|
||||
# Send 16k chunks at a time
|
||||
ChunkSize = 16384
|
||||
|
||||
# @param [EventMachine::Connection] connection
|
||||
# @param [String] filename File path
|
||||
#
|
||||
# @option args [Boolean] :http_chunks (false) Use HTTP 1.1 style chunked-encoding semantics.
|
||||
def initialize connection, filename, args = {}
|
||||
@connection = connection
|
||||
@http_chunks = args[:http_chunks]
|
||||
|
||||
if File.exist?(filename)
|
||||
@size = File.size(filename)
|
||||
if @size <= MappingThreshold
|
||||
stream_without_mapping filename
|
||||
else
|
||||
stream_with_mapping filename
|
||||
end
|
||||
else
|
||||
fail "file not found"
|
||||
end
|
||||
end
|
||||
|
||||
# @private
|
||||
def stream_without_mapping filename
|
||||
if @http_chunks
|
||||
@connection.send_data "#{@size.to_s(16)}\r\n"
|
||||
@connection.send_file_data filename
|
||||
@connection.send_data "\r\n0\r\n\r\n"
|
||||
else
|
||||
@connection.send_file_data filename
|
||||
end
|
||||
succeed
|
||||
end
|
||||
private :stream_without_mapping
|
||||
|
||||
# @private
|
||||
def stream_with_mapping filename
|
||||
ensure_mapping_extension_is_present
|
||||
|
||||
@position = 0
|
||||
@mapping = EventMachine::FastFileReader::Mapper.new filename
|
||||
stream_one_chunk
|
||||
end
|
||||
private :stream_with_mapping
|
||||
|
||||
# Used internally to stream one chunk at a time over multiple reactor ticks
|
||||
# @private
|
||||
def stream_one_chunk
|
||||
loop {
|
||||
if @position < @size
|
||||
if @connection.get_outbound_data_size > BackpressureLevel
|
||||
EventMachine::next_tick {stream_one_chunk}
|
||||
break
|
||||
else
|
||||
len = @size - @position
|
||||
len = ChunkSize if (len > ChunkSize)
|
||||
|
||||
@connection.send_data( "#{len.to_s(16)}\r\n" ) if @http_chunks
|
||||
@connection.send_data( @mapping.get_chunk( @position, len ))
|
||||
@connection.send_data("\r\n") if @http_chunks
|
||||
|
||||
@position += len
|
||||
end
|
||||
else
|
||||
@connection.send_data "0\r\n\r\n" if @http_chunks
|
||||
@mapping.close
|
||||
succeed
|
||||
break
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
#
|
||||
# We use an outboard extension class to get memory-mapped files.
|
||||
# It's outboard to avoid polluting the core distro, but that means
|
||||
# there's a "hidden" dependency on it. The first time we get here in
|
||||
# any run, try to load up the dependency extension. User code will see
|
||||
# a LoadError if it's not available, but code that doesn't require
|
||||
# mapped files will work fine without it. This is a somewhat difficult
|
||||
# compromise between usability and proper modularization.
|
||||
#
|
||||
# @private
|
||||
def ensure_mapping_extension_is_present
|
||||
@@fastfilereader ||= (require 'fastfilereaderext')
|
||||
end
|
||||
private :ensure_mapping_extension_is_present
|
||||
|
||||
end # FileStreamer
|
||||
end # EventMachine
|
||||
@@ -0,0 +1,90 @@
|
||||
module EventMachine
|
||||
# = EventMachine::ThreadedResource
|
||||
#
|
||||
# A threaded resource is a "quick and dirty" wrapper around the concept of
|
||||
# wiring up synchronous code into a standard EM::Pool. This is useful to keep
|
||||
# interfaces coherent and provide a simple approach at "making an interface
|
||||
# async-ish".
|
||||
#
|
||||
# General usage is to wrap libraries that do not support EventMachine, or to
|
||||
# have a specific number of dedicated high-cpu worker resources.
|
||||
#
|
||||
# == Basic Usage example
|
||||
#
|
||||
# This example requires the cassandra gem. The cassandra gem contains an
|
||||
# EventMachine interface, but it's sadly Fiber based and thus only works on
|
||||
# 1.9. It also requires (potentially) complex stack switching logic to reach
|
||||
# completion of nested operations. By contrast this approach provides a block
|
||||
# in which normal synchronous code can occur, but makes no attempt to wire the
|
||||
# IO into EventMachines C++ IO implementations, instead relying on the reactor
|
||||
# pattern in rb_thread_select.
|
||||
#
|
||||
# cassandra_dispatcher = ThreadedResource.new do
|
||||
# Cassandra.new('allthethings', '127.0.0.1:9160')
|
||||
# end
|
||||
#
|
||||
# pool = EM::Pool.new
|
||||
#
|
||||
# pool.add cassandra_dispatcher
|
||||
#
|
||||
# # If we don't care about the result:
|
||||
# pool.perform do |dispatcher|
|
||||
# # The following block executes inside a dedicated thread, and should not
|
||||
# # access EventMachine things:
|
||||
# dispatcher.dispatch do |cassandra|
|
||||
# cassandra.insert(:Things, '10', 'stuff' => 'things')
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# # Example where we care about the result:
|
||||
# pool.perform do |dispatcher|
|
||||
# # The dispatch block is executed in the resources thread.
|
||||
# completion = dispatcher.dispatch do |cassandra|
|
||||
# cassandra.get(:Things, '10', 'stuff')
|
||||
# end
|
||||
#
|
||||
# # This block will be yielded on the EM thread:
|
||||
# completion.callback do |result|
|
||||
# EM.do_something_with(result)
|
||||
# end
|
||||
#
|
||||
# completion
|
||||
# end
|
||||
class ThreadedResource
|
||||
|
||||
# The block should return the resource that will be yielded in a dispatch.
|
||||
def initialize
|
||||
@resource = yield
|
||||
|
||||
@running = true
|
||||
@queue = ::Queue.new
|
||||
@thread = Thread.new do
|
||||
@queue.pop.call while @running
|
||||
end
|
||||
end
|
||||
|
||||
# Called on the EM thread, generally in a perform block to return a
|
||||
# completion for the work.
|
||||
def dispatch
|
||||
completion = EM::Completion.new
|
||||
@queue << lambda do
|
||||
begin
|
||||
result = yield @resource
|
||||
completion.succeed result
|
||||
rescue => e
|
||||
completion.fail e
|
||||
end
|
||||
end
|
||||
completion
|
||||
end
|
||||
|
||||
# Kill the internal thread. should only be used to cleanup - generally
|
||||
# only required for tests.
|
||||
def shutdown
|
||||
@running = false
|
||||
@queue << lambda {}
|
||||
@thread.join
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,85 @@
|
||||
module EventMachine
|
||||
# Creates and immediately starts an EventMachine::TickLoop
|
||||
def self.tick_loop(*a, &b)
|
||||
TickLoop.new(*a, &b).start
|
||||
end
|
||||
|
||||
# A TickLoop is useful when one needs to distribute amounts of work
|
||||
# throughout ticks in order to maintain response times. It is also useful for
|
||||
# simple repeated checks and metrics.
|
||||
# @example
|
||||
# # Here we run through an array one item per tick until it is empty,
|
||||
# # printing each element.
|
||||
# # When the array is empty, we return :stop from the callback, and the
|
||||
# # loop will terminate.
|
||||
# # When the loop terminates, the on_stop callbacks will be called.
|
||||
# EM.run do
|
||||
# array = (1..100).to_a
|
||||
#
|
||||
# tickloop = EM.tick_loop do
|
||||
# if array.empty?
|
||||
# :stop
|
||||
# else
|
||||
# puts array.shift
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# tickloop.on_stop { EM.stop }
|
||||
# end
|
||||
#
|
||||
class TickLoop
|
||||
|
||||
# Arguments: A callback (EM::Callback) to call each tick. If the call
|
||||
# returns +:stop+ then the loop will be stopped. Any other value is
|
||||
# ignored.
|
||||
def initialize(*a, &b)
|
||||
@work = EM::Callback(*a, &b)
|
||||
@stops = []
|
||||
@stopped = true
|
||||
end
|
||||
|
||||
# Arguments: A callback (EM::Callback) to call once on the next stop (or
|
||||
# immediately if already stopped).
|
||||
def on_stop(*a, &b)
|
||||
if @stopped
|
||||
EM::Callback(*a, &b).call
|
||||
else
|
||||
@stops << EM::Callback(*a, &b)
|
||||
end
|
||||
end
|
||||
|
||||
# Stop the tick loop immediately, and call it's on_stop callbacks.
|
||||
def stop
|
||||
@stopped = true
|
||||
until @stops.empty?
|
||||
@stops.shift.call
|
||||
end
|
||||
end
|
||||
|
||||
# Query if the loop is stopped.
|
||||
def stopped?
|
||||
@stopped
|
||||
end
|
||||
|
||||
# Start the tick loop, will raise argument error if the loop is already
|
||||
# running.
|
||||
def start
|
||||
raise ArgumentError, "double start" unless @stopped
|
||||
@stopped = false
|
||||
schedule
|
||||
end
|
||||
|
||||
private
|
||||
def schedule
|
||||
EM.next_tick do
|
||||
next if @stopped
|
||||
if @work.call == :stop
|
||||
stop
|
||||
else
|
||||
schedule
|
||||
end
|
||||
end
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,61 @@
|
||||
module EventMachine
|
||||
# Creates a one-time timer
|
||||
#
|
||||
# timer = EventMachine::Timer.new(5) do
|
||||
# # this will never fire because we cancel it
|
||||
# end
|
||||
# timer.cancel
|
||||
#
|
||||
class Timer
|
||||
# Create a new timer that fires after a given number of seconds
|
||||
def initialize interval, callback=nil, &block
|
||||
@signature = EventMachine::add_timer(interval, callback || block)
|
||||
end
|
||||
|
||||
# Cancel the timer
|
||||
def cancel
|
||||
EventMachine.send :cancel_timer, @signature
|
||||
end
|
||||
end
|
||||
|
||||
# Creates a periodic timer
|
||||
#
|
||||
# @example
|
||||
# n = 0
|
||||
# timer = EventMachine::PeriodicTimer.new(5) do
|
||||
# puts "the time is #{Time.now}"
|
||||
# timer.cancel if (n+=1) > 5
|
||||
# end
|
||||
#
|
||||
class PeriodicTimer
|
||||
# Create a new periodic timer that executes every interval seconds
|
||||
def initialize interval, callback=nil, &block
|
||||
@interval = interval
|
||||
@code = callback || block
|
||||
@cancelled = false
|
||||
@work = method(:fire)
|
||||
schedule
|
||||
end
|
||||
|
||||
# Cancel the periodic timer
|
||||
def cancel
|
||||
@cancelled = true
|
||||
end
|
||||
|
||||
# Fire the timer every interval seconds
|
||||
attr_accessor :interval
|
||||
|
||||
# @private
|
||||
def schedule
|
||||
EventMachine::add_timer @interval, @work
|
||||
end
|
||||
|
||||
# @private
|
||||
def fire
|
||||
unless @cancelled
|
||||
@code.call
|
||||
schedule
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
module EventMachine
|
||||
VERSION = "1.2.7"
|
||||
end
|
||||
Reference in New Issue
Block a user