stompserver-0.9.9/0000775000175000017500000000000011754732653012625 5ustar paulpaulstompserver-0.9.9/metadata.yml0000664000175000017500000000573011754732653015135 0ustar paulpaul--- !ruby/object:Gem::Specification rubygems_version: 0.9.4 specification_version: 1 name: stompserver version: !ruby/object:Gem::Version version: 0.9.9 date: 2008-01-31 00:00:00 -06:00 summary: A very light messaging server require_paths: - lib email: - lionel-dev@bouton.name homepage: " by Patrick Hurley, Lionel Bouton" rubyforge_project: stompserver description: "Stomp messaging server with file/dbm/memory/activerecord based FIFO queues, queue monitoring, and basic authentication. == SYNOPSYS: Handles basic message queue processing" autorequire: default_executable: bindir: bin has_rdoc: true required_ruby_version: !ruby/object:Gem::Version::Requirement requirements: - - ">" - !ruby/object:Gem::Version version: 0.0.0 version: platform: ruby signing_key: cert_chain: post_install_message: authors: - Lionel Bouton files: - History.txt - Manifest.txt - README.txt - Rakefile - STATUS - bin/stompserver - client/README.txt - client/both.rb - client/consume.rb - client/send.rb - config/stompserver.conf - etc/passwd.example - lib/stomp_server.rb - lib/stomp_server/protocols/http.rb - lib/stomp_server/protocols/stomp.rb - lib/stomp_server/queue.rb - lib/stomp_server/queue/activerecord_queue.rb - lib/stomp_server/queue/ar_message.rb - lib/stomp_server/queue/dbm_queue.rb - lib/stomp_server/queue/file_queue.rb - lib/stomp_server/queue/memory_queue.rb - lib/stomp_server/queue_manager.rb - lib/stomp_server/stomp_auth.rb - lib/stomp_server/stomp_frame.rb - lib/stomp_server/stomp_id.rb - lib/stomp_server/stomp_user.rb - lib/stomp_server/test_server.rb - lib/stomp_server/topic_manager.rb - setup.rb - test/tesly.rb - test/test_queue_manager.rb - test/test_stomp_frame.rb - test/test_topic_manager.rb - test_todo/test_stomp_server.rb test_files: - test/test_queue_manager.rb - test/test_stomp_frame.rb - test/test_topic_manager.rb rdoc_options: - --main - README.txt extra_rdoc_files: - History.txt - Manifest.txt - README.txt - client/README.txt executables: - stompserver extensions: [] requirements: [] dependencies: - !ruby/object:Gem::Dependency name: daemons version_requirement: version_requirements: !ruby/object:Gem::Version::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 1.0.2 version: - !ruby/object:Gem::Dependency name: eventmachine version_requirement: version_requirements: !ruby/object:Gem::Version::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 0.7.2 version: - !ruby/object:Gem::Dependency name: hoe version_requirement: version_requirements: !ruby/object:Gem::Version::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 1.1.1 version: - !ruby/object:Gem::Dependency name: hoe version_requirement: version_requirements: !ruby/object:Gem::Version::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 1.3.0 version: stompserver-0.9.9/test_todo/0000775000175000017500000000000011754732653014631 5ustar paulpaulstompserver-0.9.9/test_todo/test_stomp_server.rb0000644000175000017500000001400411754732653020742 0ustar paulpaulrequire 'stomp_server' require 'fileutils' require 'test/unit' unless defined? $ZENTEST and $ZENTEST require 'tesly' #$DEBUG = true class TestStompServer < Test::Unit::TestCase # Is it a mock? it is what we are testing, but of # course I am really testing the module, so I say # yes it is a mock :-) class MockStompServer include StompServer attr_accessor :sent, :connected def initialize @sent = '' @connected = true end def send_data(data) @sent += data end def close_connection_after_writing @connected = false end alias close_connection close_connection_after_writing def stomp(cmd, headers={}, body='', flush_prev = true) @sent = '' if flush_prev sf = StompFrame.new(cmd, headers, body) receive_data(sf.to_s) end def do_connect(flush = true) stomp('CONNECT') @sent = '' if flush end def self.make_client(start_connection=true, flush=true) ss = MockStompServer.new ss.post_init ss.do_connect(flush) if start_connection ss end end def assert_stomp_error(ss=@ss, match = nil) assert_match(/ERROR/, ss.sent) assert_match(match, ss.sent) if match assert(!ss.connected) end def setup @ss = MockStompServer.make_client end def test_version assert(StompServer.const_defined?(:VERSION)) end def test_invalid_command assert_nothing_raised do @ss.stomp('INVALID') end assert_stomp_error end def test_unconnected_command ss = MockStompServer.make_client(false) assert_nothing_raised do ss.stomp('SEND') end assert_stomp_error(ss) end def test_connect ss = MockStompServer.make_client(false) assert(!ss.connected) assert_nothing_raised do ss.connect(false) end assert_match(/CONNECTED/, ss.sent) assert(ss.connected) end def test_disconnect assert_nothing_raised do @ss.stomp('DISCONNECT') end assert(!@ss.connected) end def test_receipt assert_nothing_raised do @ss.stomp('SUBSCRIBE', {'receipt' => 'foobar'}) end assert_match(/RECEIPT/, @ss.sent) assert_match(/receipt-id:foobar/, @ss.sent) assert(@ss.connected) end def test_topic assert_equal('', @ss.sent) # setup two clients (@ss and this one) ss2 = MockStompServer.make_client assert_equal('', ss2.sent) @ss.stomp('SEND', {'destination' => '/topic/foo'}, 'Hi Pat') @ss.stomp('SEND', {'destination' => '/topic/foo'}, 'Hi Sue') assert_equal('', @ss.sent) assert_equal('', ss2.sent) ss2.stomp("SUBSCRIBE", {'destination' => '/topic/foo'}) assert_equal('', ss2.sent) @ss.stomp('SEND', {'destination' => '/topic/foo'}, 'Hi Pat') assert_match(/Hi Pat/, ss2.sent) assert_equal('', @ss.sent) end def test_bad_topic assert_equal('', @ss.sent) # setup two clients (@ss and this one) ss2 = MockStompServer.make_client assert_equal('', ss2.sent) ss2.stomp("SUBSCRIBE", {'destination' => '/badtopic/foo'}) assert_equal('', ss2.sent) @ss.stomp('SEND', {'destination' => '/badtopic/foo'}, 'Hi Pat') assert_match(/Hi Pat/, ss2.sent) assert_equal('', @ss.sent) end def test_multiple_subscriber_topic assert_equal('', @ss.sent) # setup two clients (@ss and this one) ss2 = MockStompServer.make_client assert_equal('', ss2.sent) @ss.stomp("SUBSCRIBE", {'destination' => '/topic/foo'}) ss2.stomp("SUBSCRIBE", {'destination' => '/topic/foo'}) assert_equal('', @ss.sent) assert_equal('', ss2.sent) @ss.stomp('SEND', {'destination' => '/topic/foo'}, 'Hi Pat') assert_match(/Hi Pat/, ss2.sent) assert_match(/Hi Pat/, @ss.sent) end def test_invalid_transaction @ss.stomp("SEND", {'transaction' => 't'}) assert_stomp_error end def test_simple_transaction ss1 = MockStompServer.make_client ss2 = MockStompServer.make_client ss2.stomp("SUBSCRIBE", {"destination" => '/topic/foo'}) assert_equal('', ss2.sent) ss1.stomp("BEGIN", {"transaction" => 'simple'}) ss1.stomp("SEND", {"transaction" => 'simple', 'destination' => '/topic/foo'}, 'Hi Pat') assert_equal('', ss2.sent) assert_equal('', ss1.sent) ss1.stomp("COMMIT", {"transaction" => 'simple'}) assert_equal('', ss1.sent) assert_match(/Hi Pat/, ss2.sent) end def test_simple_transaction2 ss1 = MockStompServer.make_client ss2 = MockStompServer.make_client ss2.stomp("BEGIN", {"transaction" => 'simple'}) ss2.stomp("SUBSCRIBE", {"transaction" => 'simple', "destination" => '/topic/foo'}) assert_equal('', ss2.sent) ss1.stomp("SEND", {'destination' => '/topic/foo'}, 'Hi Pat') assert_equal('', ss2.sent) assert_equal('', ss1.sent) ss2.stomp("COMMIT", {"transaction" => 'simple'}) assert_equal('', ss1.sent) assert_equal('', ss2.sent) ss1.stomp("SEND", {'destination' => '/topic/foo'}, 'Hi Pat') assert_match(/Hi Pat/, ss2.sent) end def test_simple_abort_transaction ss1 = MockStompServer.make_client ss2 = MockStompServer.make_client ss2.stomp("BEGIN", {"transaction" => 'simple'}) ss2.stomp("SUBSCRIBE", {"transaction" => 'simple', "destination" => '/topic/foo'}) assert_equal('', ss2.sent) ss1.stomp("SEND", {'destination' => '/topic/foo'}, 'Hi Pat') assert_equal('', ss2.sent) assert_equal('', ss1.sent) ss2.stomp("ABORT", {"transaction" => 'simple'}) assert_equal('', ss1.sent) assert_equal('', ss2.sent) ss1.stomp("SEND", {'destination' => '/topic/foo'}, 'Hi Pat') assert_match('', ss2.sent) end def test_simple_queue_message ss1 = MockStompServer.make_client ss1.stomp("SEND", {'destination' => '/queue/foo'}, 'Hi Pat') ss1.stomp("DISCONNECT") ss2 = MockStompServer.make_client ss2.stomp("SUBSCRIBE", {"destination" => '/queue/foo'}) assert_match(/Hi Pat/, ss2.sent) end end stompserver-0.9.9/test/0000775000175000017500000000000011754732653013604 5ustar paulpaulstompserver-0.9.9/test/test_topic_manager.rb0000644000175000017500000000252011754732653017775 0ustar paulpaulrequire 'stomp_server/topic_manager' require 'test/unit' unless defined? $ZENTEST and $ZENTEST class TestTopics < Test::Unit::TestCase class UserMock attr_accessor :data def initialize ; @data = '' ; end def stomp_send_data(data); @data += data.to_s ; end end class MessageMock attr_accessor :headers, :data, :command def initialize(dest, msg) @headers = { 'destination' => dest } @data = msg end def to_s ; @data ; end end def setup @t = StompServer::TopicManager.new end def test_subscribe u = UserMock.new t = 'foo' @t.subscribe(t, u) m1 = MessageMock.new('foo', 'foomsg') m2 = MessageMock.new('bar', 'barmsg') @t.sendmsg(m1) assert_equal(m1.data, u.data) u.data = '' @t.sendmsg(m2) assert_equal('', u.data) end def test_unsubscribe u = UserMock.new t = 'foo' @t.subscribe(t, u) m1 = MessageMock.new('foo', 'foomsg') @t.sendmsg(m1) assert_equal(m1.data, u.data) @t.unsubscribe(t,u) u.data = '' @t.sendmsg(m1) assert_equal('', u.data) end def test_sendmsg(msg) u = UserMock.new t = 'foo' @t.subscribe(t, u) m1 = MessageMock.new('foo', 'foomsg') @t.sendmsg(m1) assert_equal(m1.data, u.data) assert_equal('MESSAGE', m1.command) end end stompserver-0.9.9/test/test_stomp_frame.rb0000644000175000017500000000462111754732653017505 0ustar paulpaulrequire 'stomp_server/stomp_frame' require 'test/unit' unless defined? $ZENTEST and $ZENTEST class TestStompFrame < Test::Unit::TestCase def setup @sfr = StompServer::StompFrameRecognizer.new end def test_simpleframe @sfr << < id, 'destination' => dest, 'content-length' => msg.size.to_s } @frame = StompServer::StompFrame.new('MESSAGE', headers, body) @data = @frame.to_s end def to_s ; @data.to_s ; end end def teardown FileUtils.rm_rf(".queue_test") end def setup FileUtils.rm_rf(".queue_test") if File.directory?('.queue_test') @@qstore = StompServer::FileQueue.new(".queue_test") @@qstore.checkpoint_interval=0 @t = MockQueueManager.new(@@qstore) end # def test_subscribe # u = UserMock.new # t = 'foo' # @t.subscribe(t, u) # # m1 = MessageMock.new('foo', 'foomsg') # m2 = MessageMock.new('bar', 'barmsg') # @t.sendmsg(m1) # assert_equal(m1.data, u.data) # # u.data = '' # @t.sendmsg(m2) # assert_equal('', u.data) # end def test_subscribe2 t = 'sub2' m1 = MessageMock.new(t, 'sub2msg') @t.sendmsg(m1) u = UserMock.new @t.subscribe(t, u) assert_equal(m1.data, u.data) end # def test_unsubscribe # u = UserMock.new # t = 'foo' # @t.subscribe(t, u) # # m1 = MessageMock.new('foo', 'foomsg') # @t.sendmsg(m1) # assert_equal(m1.data, u.data) # # @t.unsubscribe(t,u) # u.data = '' # @t.sendmsg(m1) # assert_equal('', u.data) # end # def test_sendmsg # u = UserMock.new # t = 'foo' # @t.subscribe(t, u) # # m1 = MessageMock.new('foo', 'foomsg') # @t.sendmsg(m1) # assert_equal(m1.data, u.data) # assert_equal('MESSAGE', m1.command) # end def test_queued_sendmsg t = 'foo' m1 = MessageMock.new('foo', 'foomsg') @t.sendmsg(m1) u = UserMock.new @t.subscribe(t, u) assert_equal(m1.data, u.data) assert_equal('MESSAGE', m1.command) u2 = UserMock.new @t.subscribe(t, u2) assert_equal('', u2.data) end end stompserver-0.9.9/test/tesly.rb0000644000175000017500000000036711754732653015275 0ustar paulpaulbegin require 'tesly_reporter' module TeslyReporter class Config AppName = 'StompServer' unless const_defined? :AppName # put your code here # User = 'id' end end rescue LoadError => err # do nothing end stompserver-0.9.9/setup.rb0000644000175000017500000011156311754732653014317 0ustar paulpaul# # setup.rb # # Copyright (c) 2000-2005 Minero Aoki # # This program is free software. # You can distribute/modify this program under the terms of # the GNU LGPL, Lesser General Public License version 2.1. # unless Enumerable.method_defined?(:map) # Ruby 1.4.6 module Enumerable alias map collect end end unless File.respond_to?(:read) # Ruby 1.6 def File.read(fname) open(fname) {|f| return f.read } end end unless Errno.const_defined?(:ENOTEMPTY) # Windows? module Errno class ENOTEMPTY # We do not raise this exception, implementation is not needed. end end end def File.binread(fname) open(fname, 'rb') {|f| return f.read } end # for corrupted Windows' stat(2) def File.dir?(path) File.directory?((path[-1,1] == '/') ? path : path + '/') end class ConfigTable include Enumerable def initialize(rbconfig) @rbconfig = rbconfig @items = [] @table = {} # options @install_prefix = nil @config_opt = nil @verbose = true @no_harm = false end attr_accessor :install_prefix attr_accessor :config_opt attr_writer :verbose def verbose? @verbose end attr_writer :no_harm def no_harm? @no_harm end def [](key) lookup(key).resolve(self) end def []=(key, val) lookup(key).set val end def names @items.map {|i| i.name } end def each(&block) @items.each(&block) end def key?(name) @table.key?(name) end def lookup(name) @table[name] or setup_rb_error "no such config item: #{name}" end def add(item) @items.push item @table[item.name] = item end def remove(name) item = lookup(name) @items.delete_if {|i| i.name == name } @table.delete_if {|name, i| i.name == name } item end def load_script(path, inst = nil) if File.file?(path) MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path end end def savefile '.config' end def load_savefile begin File.foreach(savefile()) do |line| k, v = *line.split(/=/, 2) self[k] = v.strip end rescue Errno::ENOENT setup_rb_error $!.message + "\n#{File.basename($0)} config first" end end def save @items.each {|i| i.value } File.open(savefile(), 'w') {|f| @items.each do |i| f.printf "%s=%s\n", i.name, i.value if i.value? and i.value end } end def load_standard_entries standard_entries(@rbconfig).each do |ent| add ent end end def standard_entries(rbconfig) c = rbconfig rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT']) major = c['MAJOR'].to_i minor = c['MINOR'].to_i teeny = c['TEENY'].to_i version = "#{major}.#{minor}" # ruby ver. >= 1.4.4? newpath_p = ((major >= 2) or ((major == 1) and ((minor >= 5) or ((minor == 4) and (teeny >= 4))))) if c['rubylibdir'] # V > 1.6.3 libruby = "#{c['prefix']}/lib/ruby" librubyver = c['rubylibdir'] librubyverarch = c['archdir'] siteruby = c['sitedir'] siterubyver = c['sitelibdir'] siterubyverarch = c['sitearchdir'] elsif newpath_p # 1.4.4 <= V <= 1.6.3 libruby = "#{c['prefix']}/lib/ruby" librubyver = "#{c['prefix']}/lib/ruby/#{version}" librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" siteruby = c['sitedir'] siterubyver = "$siteruby/#{version}" siterubyverarch = "$siterubyver/#{c['arch']}" else # V < 1.4.4 libruby = "#{c['prefix']}/lib/ruby" librubyver = "#{c['prefix']}/lib/ruby/#{version}" librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" siteruby = "#{c['prefix']}/lib/ruby/#{version}/site_ruby" siterubyver = siteruby siterubyverarch = "$siterubyver/#{c['arch']}" end parameterize = lambda {|path| path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix') } if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } makeprog = arg.sub(/'/, '').split(/=/, 2)[1] else makeprog = 'make' end [ ExecItem.new('installdirs', 'std/site/home', 'std: install under libruby; site: install under site_ruby; home: install under $HOME')\ {|val, table| case val when 'std' table['rbdir'] = '$librubyver' table['sodir'] = '$librubyverarch' when 'site' table['rbdir'] = '$siterubyver' table['sodir'] = '$siterubyverarch' when 'home' setup_rb_error '$HOME was not set' unless ENV['HOME'] table['prefix'] = ENV['HOME'] table['rbdir'] = '$libdir/ruby' table['sodir'] = '$libdir/ruby' end }, PathItem.new('prefix', 'path', c['prefix'], 'path prefix of target environment'), PathItem.new('bindir', 'path', parameterize.call(c['bindir']), 'the directory for commands'), PathItem.new('libdir', 'path', parameterize.call(c['libdir']), 'the directory for libraries'), PathItem.new('datadir', 'path', parameterize.call(c['datadir']), 'the directory for shared data'), PathItem.new('mandir', 'path', parameterize.call(c['mandir']), 'the directory for man pages'), PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']), 'the directory for system configuration files'), PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']), 'the directory for local state data'), PathItem.new('libruby', 'path', libruby, 'the directory for ruby libraries'), PathItem.new('librubyver', 'path', librubyver, 'the directory for standard ruby libraries'), PathItem.new('librubyverarch', 'path', librubyverarch, 'the directory for standard ruby extensions'), PathItem.new('siteruby', 'path', siteruby, 'the directory for version-independent aux ruby libraries'), PathItem.new('siterubyver', 'path', siterubyver, 'the directory for aux ruby libraries'), PathItem.new('siterubyverarch', 'path', siterubyverarch, 'the directory for aux ruby binaries'), PathItem.new('rbdir', 'path', '$siterubyver', 'the directory for ruby scripts'), PathItem.new('sodir', 'path', '$siterubyverarch', 'the directory for ruby extentions'), PathItem.new('rubypath', 'path', rubypath, 'the path to set to #! line'), ProgramItem.new('rubyprog', 'name', rubypath, 'the ruby program using for installation'), ProgramItem.new('makeprog', 'name', makeprog, 'the make program to compile ruby extentions'), SelectItem.new('shebang', 'all/ruby/never', 'ruby', 'shebang line (#!) editing mode'), BoolItem.new('without-ext', 'yes/no', 'no', 'does not compile/install ruby extentions') ] end private :standard_entries def load_multipackage_entries multipackage_entries().each do |ent| add ent end end def multipackage_entries [ PackageSelectionItem.new('with', 'name,name...', '', 'ALL', 'package names that you want to install'), PackageSelectionItem.new('without', 'name,name...', '', 'NONE', 'package names that you do not want to install') ] end private :multipackage_entries ALIASES = { 'std-ruby' => 'librubyver', 'stdruby' => 'librubyver', 'rubylibdir' => 'librubyver', 'archdir' => 'librubyverarch', 'site-ruby-common' => 'siteruby', # For backward compatibility 'site-ruby' => 'siterubyver', # For backward compatibility 'bin-dir' => 'bindir', 'bin-dir' => 'bindir', 'rb-dir' => 'rbdir', 'so-dir' => 'sodir', 'data-dir' => 'datadir', 'ruby-path' => 'rubypath', 'ruby-prog' => 'rubyprog', 'ruby' => 'rubyprog', 'make-prog' => 'makeprog', 'make' => 'makeprog' } def fixup ALIASES.each do |ali, name| @table[ali] = @table[name] end @items.freeze @table.freeze @options_re = /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/ end def parse_opt(opt) m = @options_re.match(opt) or setup_rb_error "config: unknown option #{opt}" m.to_a[1,2] end def dllext @rbconfig['DLEXT'] end def value_config?(name) lookup(name).value? end class Item def initialize(name, template, default, desc) @name = name.freeze @template = template @value = default @default = default @description = desc end attr_reader :name attr_reader :description attr_accessor :default alias help_default default def help_opt "--#{@name}=#{@template}" end def value? true end def value @value end def resolve(table) @value.gsub(%r<\$([^/]+)>) { table[$1] } end def set(val) @value = check(val) end private def check(val) setup_rb_error "config: --#{name} requires argument" unless val val end end class BoolItem < Item def config_type 'bool' end def help_opt "--#{@name}" end private def check(val) return 'yes' unless val case val when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes' when /\An(o)?\z/i, /\Af(alse)\z/i then 'no' else setup_rb_error "config: --#{@name} accepts only yes/no for argument" end end end class PathItem < Item def config_type 'path' end private def check(path) setup_rb_error "config: --#{@name} requires argument" unless path path[0,1] == '$' ? path : File.expand_path(path) end end class ProgramItem < Item def config_type 'program' end end class SelectItem < Item def initialize(name, selection, default, desc) super @ok = selection.split('/') end def config_type 'select' end private def check(val) unless @ok.include?(val.strip) setup_rb_error "config: use --#{@name}=#{@template} (#{val})" end val.strip end end class ExecItem < Item def initialize(name, selection, desc, &block) super name, selection, nil, desc @ok = selection.split('/') @action = block end def config_type 'exec' end def value? false end def resolve(table) setup_rb_error "$#{name()} wrongly used as option value" end undef set def evaluate(val, table) v = val.strip.downcase unless @ok.include?(v) setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})" end @action.call v, table end end class PackageSelectionItem < Item def initialize(name, template, default, help_default, desc) super name, template, default, desc @help_default = help_default end attr_reader :help_default def config_type 'package' end private def check(val) unless File.dir?("packages/#{val}") setup_rb_error "config: no such package: #{val}" end val end end class MetaConfigEnvironment def initialize(config, installer) @config = config @installer = installer end def config_names @config.names end def config?(name) @config.key?(name) end def bool_config?(name) @config.lookup(name).config_type == 'bool' end def path_config?(name) @config.lookup(name).config_type == 'path' end def value_config?(name) @config.lookup(name).config_type != 'exec' end def add_config(item) @config.add item end def add_bool_config(name, default, desc) @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc) end def add_path_config(name, default, desc) @config.add PathItem.new(name, 'path', default, desc) end def set_config_default(name, default) @config.lookup(name).default = default end def remove_config(name) @config.remove(name) end # For only multipackage def packages raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer @installer.packages end # For only multipackage def declare_packages(list) raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer @installer.packages = list end end end # class ConfigTable # This module requires: #verbose?, #no_harm? module FileOperations def mkdir_p(dirname, prefix = nil) dirname = prefix + File.expand_path(dirname) if prefix $stderr.puts "mkdir -p #{dirname}" if verbose? return if no_harm? # Does not check '/', it's too abnormal. dirs = File.expand_path(dirname).split(%r<(?=/)>) if /\A[a-z]:\z/i =~ dirs[0] disk = dirs.shift dirs[0] = disk + dirs[0] end dirs.each_index do |idx| path = dirs[0..idx].join('') Dir.mkdir path unless File.dir?(path) end end def rm_f(path) $stderr.puts "rm -f #{path}" if verbose? return if no_harm? force_remove_file path end def rm_rf(path) $stderr.puts "rm -rf #{path}" if verbose? return if no_harm? remove_tree path end def remove_tree(path) if File.symlink?(path) remove_file path elsif File.dir?(path) remove_tree0 path else force_remove_file path end end def remove_tree0(path) Dir.foreach(path) do |ent| next if ent == '.' next if ent == '..' entpath = "#{path}/#{ent}" if File.symlink?(entpath) remove_file entpath elsif File.dir?(entpath) remove_tree0 entpath else force_remove_file entpath end end begin Dir.rmdir path rescue Errno::ENOTEMPTY # directory may not be empty end end def move_file(src, dest) force_remove_file dest begin File.rename src, dest rescue File.open(dest, 'wb') {|f| f.write File.binread(src) } File.chmod File.stat(src).mode, dest File.unlink src end end def force_remove_file(path) begin remove_file path rescue end end def remove_file(path) File.chmod 0777, path File.unlink path end def install(from, dest, mode, prefix = nil) $stderr.puts "install #{from} #{dest}" if verbose? return if no_harm? realdest = prefix ? prefix + File.expand_path(dest) : dest realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest) str = File.binread(from) if diff?(str, realdest) verbose_off { rm_f realdest if File.exist?(realdest) } File.open(realdest, 'wb') {|f| f.write str } File.chmod mode, realdest File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| if prefix f.puts realdest.sub(prefix, '') else f.puts realdest end } end end def diff?(new_content, path) return true unless File.exist?(path) new_content != File.binread(path) end def command(*args) $stderr.puts args.join(' ') if verbose? system(*args) or raise RuntimeError, "system(#{args.map{|a| a.inspect }.join(' ')}) failed" end def ruby(*args) command config('rubyprog'), *args end def make(task = nil) command(*[config('makeprog'), task].compact) end def extdir?(dir) File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb") end def files_of(dir) Dir.open(dir) {|d| return d.select {|ent| File.file?("#{dir}/#{ent}") } } end DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn ) def directories_of(dir) Dir.open(dir) {|d| return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT } end end # This module requires: #srcdir_root, #objdir_root, #relpath module HookScriptAPI def get_config(key) @config[key] end alias config get_config # obsolete: use metaconfig to change configuration def set_config(key, val) @config[key] = val end # # srcdir/objdir (works only in the package directory) # def curr_srcdir "#{srcdir_root()}/#{relpath()}" end def curr_objdir "#{objdir_root()}/#{relpath()}" end def srcfile(path) "#{curr_srcdir()}/#{path}" end def srcexist?(path) File.exist?(srcfile(path)) end def srcdirectory?(path) File.dir?(srcfile(path)) end def srcfile?(path) File.file?(srcfile(path)) end def srcentries(path = '.') Dir.open("#{curr_srcdir()}/#{path}") {|d| return d.to_a - %w(. ..) } end def srcfiles(path = '.') srcentries(path).select {|fname| File.file?(File.join(curr_srcdir(), path, fname)) } end def srcdirectories(path = '.') srcentries(path).select {|fname| File.dir?(File.join(curr_srcdir(), path, fname)) } end end class ToplevelInstaller Version = '3.4.1' Copyright = 'Copyright (c) 2000-2005 Minero Aoki' TASKS = [ [ 'all', 'do config, setup, then install' ], [ 'config', 'saves your configurations' ], [ 'show', 'shows current configuration' ], [ 'setup', 'compiles ruby extentions and others' ], [ 'install', 'installs files' ], [ 'test', 'run all tests in test/' ], [ 'clean', "does `make clean' for each extention" ], [ 'distclean',"does `make distclean' for each extention" ] ] def ToplevelInstaller.invoke config = ConfigTable.new(load_rbconfig()) config.load_standard_entries config.load_multipackage_entries if multipackage? config.fixup klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller) klass.new(File.dirname($0), config).invoke end def ToplevelInstaller.multipackage? File.dir?(File.dirname($0) + '/packages') end def ToplevelInstaller.load_rbconfig if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg } ARGV.delete(arg) load File.expand_path(arg.split(/=/, 2)[1]) $".push 'rbconfig.rb' else require 'rbconfig' end ::Config::CONFIG end def initialize(ardir_root, config) @ardir = File.expand_path(ardir_root) @config = config # cache @valid_task_re = nil end def config(key) @config[key] end def inspect "#<#{self.class} #{__id__()}>" end def invoke run_metaconfigs case task = parsearg_global() when nil, 'all' parsearg_config init_installers exec_config exec_setup exec_install else case task when 'config', 'test' ; when 'clean', 'distclean' @config.load_savefile if File.exist?(@config.savefile) else @config.load_savefile end __send__ "parsearg_#{task}" init_installers __send__ "exec_#{task}" end end def run_metaconfigs @config.load_script "#{@ardir}/metaconfig" end def init_installers @installer = Installer.new(@config, @ardir, File.expand_path('.')) end # # Hook Script API bases # def srcdir_root @ardir end def objdir_root '.' end def relpath '.' end # # Option Parsing # def parsearg_global while arg = ARGV.shift case arg when /\A\w+\z/ setup_rb_error "invalid task: #{arg}" unless valid_task?(arg) return arg when '-q', '--quiet' @config.verbose = false when '--verbose' @config.verbose = true when '--help' print_usage $stdout exit 0 when '--version' puts "#{File.basename($0)} version #{Version}" exit 0 when '--copyright' puts Copyright exit 0 else setup_rb_error "unknown global option '#{arg}'" end end nil end def valid_task?(t) valid_task_re() =~ t end def valid_task_re @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/ end def parsearg_no_options unless ARGV.empty? task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1) setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}" end end alias parsearg_show parsearg_no_options alias parsearg_setup parsearg_no_options alias parsearg_test parsearg_no_options alias parsearg_clean parsearg_no_options alias parsearg_distclean parsearg_no_options def parsearg_config evalopt = [] set = [] @config.config_opt = [] while i = ARGV.shift if /\A--?\z/ =~ i @config.config_opt = ARGV.dup break end name, value = *@config.parse_opt(i) if @config.value_config?(name) @config[name] = value else evalopt.push [name, value] end set.push name end evalopt.each do |name, value| @config.lookup(name).evaluate value, @config end # Check if configuration is valid set.each do |n| @config[n] if @config.value_config?(n) end end def parsearg_install @config.no_harm = false @config.install_prefix = '' while a = ARGV.shift case a when '--no-harm' @config.no_harm = true when /\A--prefix=/ path = a.split(/=/, 2)[1] path = File.expand_path(path) unless path[0,1] == '/' @config.install_prefix = path else setup_rb_error "install: unknown option #{a}" end end end def print_usage(out) out.puts 'Typical Installation Procedure:' out.puts " $ ruby #{File.basename $0} config" out.puts " $ ruby #{File.basename $0} setup" out.puts " # ruby #{File.basename $0} install (may require root privilege)" out.puts out.puts 'Detailed Usage:' out.puts " ruby #{File.basename $0} " out.puts " ruby #{File.basename $0} [] []" fmt = " %-24s %s\n" out.puts out.puts 'Global options:' out.printf fmt, '-q,--quiet', 'suppress message outputs' out.printf fmt, ' --verbose', 'output messages verbosely' out.printf fmt, ' --help', 'print this message' out.printf fmt, ' --version', 'print version and quit' out.printf fmt, ' --copyright', 'print copyright and quit' out.puts out.puts 'Tasks:' TASKS.each do |name, desc| out.printf fmt, name, desc end fmt = " %-24s %s [%s]\n" out.puts out.puts 'Options for CONFIG or ALL:' @config.each do |item| out.printf fmt, item.help_opt, item.description, item.help_default end out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's" out.puts out.puts 'Options for INSTALL:' out.printf fmt, '--no-harm', 'only display what to do if given', 'off' out.printf fmt, '--prefix=path', 'install path prefix', '' out.puts end # # Task Handlers # def exec_config @installer.exec_config @config.save # must be final end def exec_setup @installer.exec_setup end def exec_install @installer.exec_install end def exec_test @installer.exec_test end def exec_show @config.each do |i| printf "%-20s %s\n", i.name, i.value if i.value? end end def exec_clean @installer.exec_clean end def exec_distclean @installer.exec_distclean end end # class ToplevelInstaller class ToplevelInstallerMulti < ToplevelInstaller include FileOperations def initialize(ardir_root, config) super @packages = directories_of("#{@ardir}/packages") raise 'no package exists' if @packages.empty? @root_installer = Installer.new(@config, @ardir, File.expand_path('.')) end def run_metaconfigs @config.load_script "#{@ardir}/metaconfig", self @packages.each do |name| @config.load_script "#{@ardir}/packages/#{name}/metaconfig" end end attr_reader :packages def packages=(list) raise 'package list is empty' if list.empty? list.each do |name| raise "directory packages/#{name} does not exist"\ unless File.dir?("#{@ardir}/packages/#{name}") end @packages = list end def init_installers @installers = {} @packages.each do |pack| @installers[pack] = Installer.new(@config, "#{@ardir}/packages/#{pack}", "packages/#{pack}") end with = extract_selection(config('with')) without = extract_selection(config('without')) @selected = @installers.keys.select {|name| (with.empty? or with.include?(name)) \ and not without.include?(name) } end def extract_selection(list) a = list.split(/,/) a.each do |name| setup_rb_error "no such package: #{name}" unless @installers.key?(name) end a end def print_usage(f) super f.puts 'Inluded packages:' f.puts ' ' + @packages.sort.join(' ') f.puts end # # Task Handlers # def exec_config run_hook 'pre-config' each_selected_installers {|inst| inst.exec_config } run_hook 'post-config' @config.save # must be final end def exec_setup run_hook 'pre-setup' each_selected_installers {|inst| inst.exec_setup } run_hook 'post-setup' end def exec_install run_hook 'pre-install' each_selected_installers {|inst| inst.exec_install } run_hook 'post-install' end def exec_test run_hook 'pre-test' each_selected_installers {|inst| inst.exec_test } run_hook 'post-test' end def exec_clean rm_f @config.savefile run_hook 'pre-clean' each_selected_installers {|inst| inst.exec_clean } run_hook 'post-clean' end def exec_distclean rm_f @config.savefile run_hook 'pre-distclean' each_selected_installers {|inst| inst.exec_distclean } run_hook 'post-distclean' end # # lib # def each_selected_installers Dir.mkdir 'packages' unless File.dir?('packages') @selected.each do |pack| $stderr.puts "Processing the package `#{pack}' ..." if verbose? Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") Dir.chdir "packages/#{pack}" yield @installers[pack] Dir.chdir '../..' end end def run_hook(id) @root_installer.run_hook id end # module FileOperations requires this def verbose? @config.verbose? end # module FileOperations requires this def no_harm? @config.no_harm? end end # class ToplevelInstallerMulti class Installer FILETYPES = %w( bin lib ext data conf man ) include FileOperations include HookScriptAPI def initialize(config, srcroot, objroot) @config = config @srcdir = File.expand_path(srcroot) @objdir = File.expand_path(objroot) @currdir = '.' end def inspect "#<#{self.class} #{File.basename(@srcdir)}>" end def noop(rel) end # # Hook Script API base methods # def srcdir_root @srcdir end def objdir_root @objdir end def relpath @currdir end # # Config Access # # module FileOperations requires this def verbose? @config.verbose? end # module FileOperations requires this def no_harm? @config.no_harm? end def verbose_off begin save, @config.verbose = @config.verbose?, false yield ensure @config.verbose = save end end # # TASK config # def exec_config exec_task_traverse 'config' end alias config_dir_bin noop alias config_dir_lib noop def config_dir_ext(rel) extconf if extdir?(curr_srcdir()) end alias config_dir_data noop alias config_dir_conf noop alias config_dir_man noop def extconf ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt end # # TASK setup # def exec_setup exec_task_traverse 'setup' end def setup_dir_bin(rel) files_of(curr_srcdir()).each do |fname| update_shebang_line "#{curr_srcdir()}/#{fname}" end end alias setup_dir_lib noop def setup_dir_ext(rel) make if extdir?(curr_srcdir()) end alias setup_dir_data noop alias setup_dir_conf noop alias setup_dir_man noop def update_shebang_line(path) return if no_harm? return if config('shebang') == 'never' old = Shebang.load(path) if old $stderr.puts "warning: #{path}: Shebang line includes too many args. It is not portable and your program may not work." if old.args.size > 1 new = new_shebang(old) return if new.to_s == old.to_s else return unless config('shebang') == 'all' new = Shebang.new(config('rubypath')) end $stderr.puts "updating shebang: #{File.basename(path)}" if verbose? open_atomic_writer(path) {|output| File.open(path, 'rb') {|f| f.gets if old # discard output.puts new.to_s output.print f.read } } end def new_shebang(old) if /\Aruby/ =~ File.basename(old.cmd) Shebang.new(config('rubypath'), old.args) elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby' Shebang.new(config('rubypath'), old.args[1..-1]) else return old unless config('shebang') == 'all' Shebang.new(config('rubypath')) end end def open_atomic_writer(path, &block) tmpfile = File.basename(path) + '.tmp' begin File.open(tmpfile, 'wb', &block) File.rename tmpfile, File.basename(path) ensure File.unlink tmpfile if File.exist?(tmpfile) end end class Shebang def Shebang.load(path) line = nil File.open(path) {|f| line = f.gets } return nil unless /\A#!/ =~ line parse(line) end def Shebang.parse(line) cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ') new(cmd, args) end def initialize(cmd, args = []) @cmd = cmd @args = args end attr_reader :cmd attr_reader :args def to_s "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}") end end # # TASK install # def exec_install rm_f 'InstalledFiles' exec_task_traverse 'install' end def install_dir_bin(rel) install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755 end def install_dir_lib(rel) install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644 end def install_dir_ext(rel) return unless extdir?(curr_srcdir()) install_files rubyextentions('.'), "#{config('sodir')}/#{File.dirname(rel)}", 0555 end def install_dir_data(rel) install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644 end def install_dir_conf(rel) # FIXME: should not remove current config files # (rename previous file to .old/.org) install_files targetfiles(), "#{config('sysconfdir')}/#{rel}", 0644 end def install_dir_man(rel) install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644 end def install_files(list, dest, mode) mkdir_p dest, @config.install_prefix list.each do |fname| install fname, dest, mode, @config.install_prefix end end def libfiles glob_reject(%w(*.y *.output), targetfiles()) end def rubyextentions(dir) ents = glob_select("*.#{@config.dllext}", targetfiles()) if ents.empty? setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first" end ents end def targetfiles mapdir(existfiles() - hookfiles()) end def mapdir(ents) ents.map {|ent| if File.exist?(ent) then ent # objdir else "#{curr_srcdir()}/#{ent}" # srcdir end } end # picked up many entries from cvs-1.11.1/src/ignore.c JUNK_FILES = %w( core RCSLOG tags TAGS .make.state .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb *~ *.old *.bak *.BAK *.orig *.rej _$* *$ *.org *.in .* ) def existfiles glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.'))) end def hookfiles %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| %w( config setup install clean ).map {|t| sprintf(fmt, t) } }.flatten end def glob_select(pat, ents) re = globs2re([pat]) ents.select {|ent| re =~ ent } end def glob_reject(pats, ents) re = globs2re(pats) ents.reject {|ent| re =~ ent } end GLOB2REGEX = { '.' => '\.', '$' => '\$', '#' => '\#', '*' => '.*' } def globs2re(pats) /\A(?:#{ pats.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| GLOB2REGEX[ch] } }.join('|') })\z/ end # # TASK test # TESTDIR = 'test' def exec_test unless File.directory?('test') $stderr.puts 'no test in this package' if verbose? return end $stderr.puts 'Running tests...' if verbose? begin require 'test/unit' rescue LoadError setup_rb_error 'test/unit cannot loaded. You need Ruby 1.8 or later to invoke this task.' end runner = Test::Unit::AutoRunner.new(true) runner.to_run << TESTDIR runner.run end # # TASK clean # def exec_clean exec_task_traverse 'clean' rm_f @config.savefile rm_f 'InstalledFiles' end alias clean_dir_bin noop alias clean_dir_lib noop alias clean_dir_data noop alias clean_dir_conf noop alias clean_dir_man noop def clean_dir_ext(rel) return unless extdir?(curr_srcdir()) make 'clean' if File.file?('Makefile') end # # TASK distclean # def exec_distclean exec_task_traverse 'distclean' rm_f @config.savefile rm_f 'InstalledFiles' end alias distclean_dir_bin noop alias distclean_dir_lib noop def distclean_dir_ext(rel) return unless extdir?(curr_srcdir()) make 'distclean' if File.file?('Makefile') end alias distclean_dir_data noop alias distclean_dir_conf noop alias distclean_dir_man noop # # Traversing # def exec_task_traverse(task) run_hook "pre-#{task}" FILETYPES.each do |type| if type == 'ext' and config('without-ext') == 'yes' $stderr.puts 'skipping ext/* by user option' if verbose? next end traverse task, type, "#{task}_dir_#{type}" end run_hook "post-#{task}" end def traverse(task, rel, mid) dive_into(rel) { run_hook "pre-#{task}" __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') directories_of(curr_srcdir()).each do |d| traverse task, "#{rel}/#{d}", mid end run_hook "post-#{task}" } end def dive_into(rel) return unless File.dir?("#{@srcdir}/#{rel}") dir = File.basename(rel) Dir.mkdir dir unless File.dir?(dir) prevdir = Dir.pwd Dir.chdir dir $stderr.puts '---> ' + rel if verbose? @currdir = rel yield Dir.chdir prevdir $stderr.puts '<--- ' + rel if verbose? @currdir = File.dirname(rel) end def run_hook(id) path = [ "#{curr_srcdir()}/#{id}", "#{curr_srcdir()}/#{id}.rb" ].detect {|cand| File.file?(cand) } return unless path begin instance_eval File.read(path), path, 1 rescue raise if $DEBUG setup_rb_error "hook #{path} failed:\n" + $!.message end end end # class Installer class SetupError < StandardError; end def setup_rb_error(msg) raise SetupError, msg end if $0 == __FILE__ begin ToplevelInstaller.invoke rescue SetupError raise if $DEBUG $stderr.puts $!.message $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." exit 1 end end stompserver-0.9.9/lib/0000775000175000017500000000000011754732653013373 5ustar paulpaulstompserver-0.9.9/lib/stomp_server/0000775000175000017500000000000011754732653016123 5ustar paulpaulstompserver-0.9.9/lib/stomp_server/topic_manager.rb0000644000175000017500000000136611754732653021264 0ustar paulpaul# topic - non persistent, sent to all interested parties module StompServer class TopicManager attr_accessor :frame_index def initialize @frame_index =0 @topics = Hash.new { Array.new } puts "TopicManager initialized" end def index @frame_index end def next_index @frame_index += 1 end def subscribe(topic, user) @topics[topic] += [user] end def unsubscribe(topic, user) @topics[topic].delete(user) end def disconnect(user) @topics.each do |dest, queue| queue.delete_if { |qu| qu == user } end end def sendmsg(msg) msg.command = "MESSAGE" topic = msg.headers['destination'] @topics[topic].each do |user| user.stomp_send_data(msg) end end end end stompserver-0.9.9/lib/stomp_server/test_server.rb0000644000175000017500000000041111754732653021007 0ustar paulpaulrequire 'rubygems' require 'eventmachine' module StompServer module TestServer def post_init @transactions = {} @connected = false end def receive_data(data) end def unbind p "Unbind called" if $DEBUG @connected = false end end end stompserver-0.9.9/lib/stomp_server/stomp_user.rb0000644000175000017500000000041711754732653020650 0ustar paulpaul # Used when accessing queues/topics outside of EM require 'stomp_id' require 'stomp_frame' $MULTIUSER = true module StompServer class StompUser attr_accessor :data def initialize @data = [] end def stomp_send_data(data) @data << data.to_s end end end stompserver-0.9.9/lib/stomp_server/stomp_id.rb0000644000175000017500000000111511754732653020262 0ustar paulpaul # This class is instantiated in all the queue storage classes, plus the queue manager (for the statistic messages). It generates a unique # id for each message. The caller passes an additional identifier that is appended message-id, which is usually the id of the frame and is # different for each storage class. require 'socket' require 'resolv-replace' module StompServer class StompId def initialize @host = Socket.gethostname.to_s end def [](id) msgid = sprintf("%.6f",Time.now.to_f).to_s.sub('.','-') msgid = @host + '-' + msgid + '-' + id.to_s end end end stompserver-0.9.9/lib/stomp_server/stomp_frame.rb0000644000175000017500000000423411754732653020765 0ustar paulpaul module StompServer class StompFrame attr_accessor :command, :headers, :body def initialize(command=nil, headers=nil, body=nil) @command = command @headers = headers || {} @body = body || '' end def to_s result = @command + "\n" @headers['content-length'] = @body.size.to_s if @body.include?(0) @headers.each_pair do |key, value| result << "#{key}:#{value}\n" end result << "\n" result << @body.to_s result << "\000\n" end def dest #@dest || (@dest = @headers['destination']) @headers['destination'] end end class StompFrameRecognizer attr_accessor :frames def initialize @buffer = '' @body_length = nil @frame = StompServer::StompFrame.new @frames = [] end def parse_body(len) raise RuntimeError.new("Invalid stompframe (missing null term)") unless @buffer[len] == 0 @frame.body = @buffer[0...len] @buffer = @buffer[len+1..-1] @frames << @frame @frame = StompServer::StompFrame.new end def parse_binary_body if @buffer.length > @body_length parse_body(@body_length) end end def parse_text_body if pos = @buffer.index(0) parse_body(pos) end end def parse_header if match = @buffer.match(/^\s*(\S+)$\r?\n((?:[ \t]*.*?[ \t]*:[ \t]*.*?[ \t]*$\r?\n)*)\r?\n/) @frame.command, headers = match.captures @buffer = match.post_match headers.split(/\n/).each do |data| if data =~ /^\s*(\S+)\s*:\s*(.*?)\s*$/ @frame.headers[$1] = $2 end end # body_length is nil, if there is no content-length, otherwise it is the length (as in integer) @body_length = @frame.headers['content-length'] && @frame.headers['content-length'].to_i end end def parse count = @frames.size parse_header unless @frame.command if @frame.command if @body_length parse_binary_body else parse_text_body end end # parse_XXX_body return the frame if they succeed and nil if they fail # the result will fall through parse if count != @frames.size end def<< (buf) @buffer << buf parse end end end stompserver-0.9.9/lib/stomp_server/stomp_auth.rb0000644000175000017500000000075511754732653020640 0ustar paulpaulmodule StompServer class StompAuth attr_accessor :authorized def initialize(passfile='.passwd') @passfile = passfile @authorized = Hash.new if File.exists?(@passfile) file = File.read(@passfile) file.split(/\n/).each do |data| next if data =~/^\s*#/ data.gsub(/\s/,'') if data =~ /^\s*(\S+)\s*:\s*(.*?)\s*$/ @authorized[$1] = $2 end end end puts "Authorized users #{@authorized.keys}" if $DEBUG end end end stompserver-0.9.9/lib/stomp_server/queue_manager.rb0000644000175000017500000001441311754732653021267 0ustar paulpaul# QueueManager is used in conjunction with a storage class. The storage class MUST implement the following two methods: # # - enqueue(queue name, frame) # enqueue pushes a frame to the top of the queue in FIFO order. It's return value is ignored. enqueue must also set the # message-id and add it to the frame header before inserting the frame into the queue. # # - dequeue(queue name) # dequeue removes a frame from the bottom of the queue and returns it. # # - requeue(queue name,frame) # does the same as enqueue, except it puts the from at the bottom of the queue # # The storage class MAY implement the stop() method which can be used to do any housekeeping that needs to be done before # stompserver shuts down. stop() will be called when stompserver is shut down. # # The storage class MAY implement the monitor() method. monitor() should return a hash of hashes containing the queue statistics. # See the file queue for an example. Statistics are available to clients in /queue/monitor. # module StompServer class QueueMonitor def initialize(qstore,queues) @qstore = qstore @queues = queues @stompid = StompServer::StompId.new puts "QueueManager initialized" end def start count =0 EventMachine::add_periodic_timer 5, proc {count+=1; monitor(count) } end def monitor(count) return unless @qstore.methods.include?('monitor') users = @queues['/queue/monitor'] return if users.size == 0 stats = @qstore.monitor return if stats.size == 0 body = '' stats.each do |queue,qstats| body << "Queue: #{queue}\n" qstats.each {|stat,value| body << "#{stat}: #{value}\n"} body << "\n" end headers = { 'message-id' => @stompid[count], 'destination' => '/queue/monitor', 'content-length' => body.size.to_s } frame = StompServer::StompFrame.new('MESSAGE', headers, body) users.each {|user| user.connection.stomp_send_data(frame)} end end class QueueManager Struct::new('QueueUser', :connection, :ack) def initialize(qstore) @qstore = qstore @queues = Hash.new { Array.new } @pending = Hash.new if $STOMP_SERVER monitor = StompServer::QueueMonitor.new(@qstore,@queues) monitor.start puts "Queue monitor started" if $DEBUG end end def stop @qstore.stop if @qstore.methods.include?('stop') end def subscribe(dest, connection, use_ack=false) puts "Subscribing to #{dest}" user = Struct::QueueUser.new(connection, use_ack) @queues[dest] += [user] send_destination_backlog(dest,user) unless dest == '/queue/monitor' end # Send at most one frame to a connection # used when use_ack == true def send_a_backlog(connection) puts "Sending a backlog" if $DEBUG # lookup queues with data for this connection possible_queues = @queues.select{ |destination,users| @qstore.message_for?(destination) && users.detect{|u| u.connection == connection} } if possible_queues.empty? puts "Nothing left" if $DEBUG return end # Get a random one (avoid artificial priority between queues # without coding a whole scheduler, which might be desirable later) dest,users = possible_queues[rand(possible_queues.length)] user = users.find{|u| u.connection == connection} frame = @qstore.dequeue(dest) puts "Chose #{dest}" if $DEBUG send_to_user(frame, user) end def send_destination_backlog(dest,user) puts "Sending destination backlog for #{dest}" if $DEBUG if user.ack # only send one message (waiting for ack) frame = @qstore.dequeue(dest) send_to_user(frame, user) if frame else while frame = @qstore.dequeue(dest) send_to_user(frame, user) end end end def unsubscribe(dest, connection) puts "Unsubscribing from #{dest}" @queues.each do |d, queue| queue.delete_if { |qu| qu.connection == connection and d == dest} end @queues.delete(dest) if @queues[dest].empty? end def ack(connection, frame) puts "Acking #{frame.headers['message-id']}" if $DEBUG unless @pending[connection] puts "No message pending for connection!" return end msgid = frame.headers['message-id'] p_msgid = @pending[connection].headers['message-id'] if p_msgid != msgid # We don't know what happened, we requeue # (probably a client connecting to a restarted server) frame = @pending[connection] @qstore.requeue(frame.headers['destination'],frame) puts "Invalid message-id (received #{msgid} != #{p_msgid})" end @pending.delete connection # We are free to work now, look if there's something for us send_a_backlog(connection) end def disconnect(connection) puts "Disconnecting" frame = @pending[connection] if frame @qstore.requeue(frame.headers['destination'],frame) @pending.delete connection end @queues.each do |dest, queue| queue.delete_if { |qu| qu.connection == connection } @queues.delete(dest) if queue.empty? end end def send_to_user(frame, user) connection = user.connection if user.ack raise "other connection's end already busy" if @pending[connection] @pending[connection] = frame end connection.stomp_send_data(frame) end def sendmsg(frame) frame.command = "MESSAGE" dest = frame.headers['destination'] puts "Sending a message to #{dest}: " # Lookup a user willing to handle this destination available_users = @queues[dest].reject{|user| @pending[user.connection]} if available_users.empty? @qstore.enqueue(dest,frame) return end # Look for a user with ack (we favor reliability) reliable_user = available_users.find{|u| u.ack} if reliable_user # give it a message-id @qstore.assign_id(frame, dest) send_to_user(frame, reliable_user) else random_user = available_users[rand(available_users.length)] # Note message-id header isn't set but we won't need it anyway # could break some clients: fix this send_to_user(frame, random_user) end end # For protocol handlers that want direct access to the queue def dequeue(dest) @qstore.dequeue(dest) end def enqueue(frame) frame.command = "MESSAGE" dest = frame.headers['destination'] @qstore.enqueue(dest,frame) end end end stompserver-0.9.9/lib/stomp_server/queue/0000775000175000017500000000000011754732653017247 5ustar paulpaulstompserver-0.9.9/lib/stomp_server/queue/memory_queue.rb0000644000175000017500000000220111754732653022301 0ustar paulpaul module StompServer class MemoryQueue attr_accessor :checkpoint_interval def initialize @frame_index =0 @stompid = StompServer::StompId.new @stats = Hash.new @messages = Hash.new { Array.new } puts "MemoryQueue initialized" end def stop end def monitor stats = Hash.new @messages.keys.each do |dest| stats[dest] = {'size' => @messages[dest].size, 'enqueued' => @stats[dest][:enqueued], 'dequeued' => @stats[dest][:dequeued]} end stats end def dequeue(dest) if frame = @messages[dest].shift @stats[dest][:dequeued] += 1 return frame else return false end end def enqueue(dest,frame) @frame_index += 1 if @stats[dest] @stats[dest][:enqueued] += 1 else @stats[dest] = Hash.new @stats[dest][:enqueued] = 1 @stats[dest][:dequeued] = 0 end assign_id(frame, dest) requeue(dest, frame) end def requeue(dest,frame) @messages[dest] += [frame] end def message_for?(dest) !@messages[dest].empty? end def assign_id(frame, dest) frame.headers['message-id'] = @stompid[@frame_index] end end end stompserver-0.9.9/lib/stomp_server/queue/file_queue.rb0000644000175000017500000000243611754732653021722 0ustar paulpaul module StompServer class FileQueue < Queue def _close_queue(dest) Dir.delete(@queues[dest][:queue_dir]) if File.directory?(@queues[dest][:queue_dir]) end def _open_queue(dest) # handle clashes between _ and / queue_name = dest.gsub('_','__') queue_name = dest.gsub('/','_') queue_dir = @directory + '/' + queue_name @queues[dest][:queue_dir] = queue_dir Dir.mkdir(queue_dir) unless File.directory?(queue_dir) end def _writeframe(dest,frame_todump,msgid) filename = "#{@queues[dest][:queue_dir]}/#{msgid}" frame = frame_todump.dup frame_body = frame.body frame.body = '' frame_image = Marshal.dump(frame) framelen = sprintf("%08x", frame_image.length) bodylen = sprintf("%08x", frame_body.length) File.open(filename,'wb') {|f| f.syswrite("#{framelen}#{bodylen}#{frame_image}#{frame_body}")} return true end def _readframe(dest,msgid) filename = "#{@queues[dest][:queue_dir]}/#{msgid}" file = nil File.open(filename,'rb') {|f| file = f.read} frame_len = file[0,8].hex body_len = file[8,8].hex frame = Marshal::load(file[16,frame_len]) frame.body = file[(frame_len + 16),body_len] if File.delete(filename) result = frame else result = false end return result end end end stompserver-0.9.9/lib/stomp_server/queue/dbm_queue.rb0000644000175000017500000000321411754732653021540 0ustar paulpaul module StompServer class DBMQueue < Queue def initialize *args super # Please don't use dbm files for storing large frames, it's problematic at best and uses large amounts of memory. # sdbm croaks on marshalled data that contains certain characters, so we don't use it at all @dbm = false if RUBY_PLATFORM =~/linux|bsd/ types = ['bdb','dbm','gdbm'] else types = ['bdb','gdbm'] end types.each do |dbtype| begin require dbtype @dbm = dbtype puts "#{@dbm} loaded" break rescue LoadError => e end end raise "No DBM library found. Tried bdb,dbm,gdbm" unless @dbm @db = Hash.new @queues.keys.each {|q| _open_queue(q)} end def dbmopen(dbname) if @dbm == 'bdb' BDB::Hash.new(dbname, nil, "a") elsif @dbm == 'dbm' DBM.open(dbname) elsif @dbm == 'gdbm' GDBM.open(dbname) end end def _open_queue(dest) queue_name = dest.gsub('/', '_') dbname = @directory + '/' + queue_name @db[dest] = Hash.new @db[dest][:dbh] = dbmopen(dbname) @db[dest][:dbname] = dbname end def _close_queue(dest) @db[dest][:dbh].close dbname = @db[dest][:dbname] File.delete(dbname) if File.exists?(dbname) File.delete("#{dbname}.db") if File.exists?("#{dbname}.db") File.delete("#{dbname}.pag") if File.exists?("#{dbname}.pag") File.delete("#{dbname}.dir") if File.exists?("#{dbname}.dir") end def _writeframe(dest,frame,msgid) @db[dest][:dbh][msgid] = Marshal::dump(frame) end def _readframe(dest,msgid) frame_image = @db[dest][:dbh][msgid] Marshal::load(frame_image) end end end stompserver-0.9.9/lib/stomp_server/queue/ar_message.rb0000644000175000017500000000012511754732653021676 0ustar paulpaulrequire 'active_record' class ArMessage < ActiveRecord::Base serialize :frame end stompserver-0.9.9/lib/stomp_server/queue/activerecord_queue.rb0000644000175000017500000000600711754732653023453 0ustar paulpaul## Queue implementation using ActiveRecord ## ## all messages are stored in a single table ## they are indexed by 'stomp_id' which is the stomp 'message-id' header ## which must be unique accross all queues ## require 'stomp_server/queue/ar_message' require 'yaml' module StompServer class ActiveRecordQueue attr_accessor :checkpoint_interval def initialize(configdir, storagedir) # Default configuration, use SQLite for simplicity db_params = { 'adapter' => 'sqlite3', 'database' => "#{configdir}/stompserver_development" } # Load DB configuration db_config = "#{configdir}/database.yml" puts "reading from #{db_config}" if File.exists? db_config db_params.merge! YAML::load(File.open(db_config)) end puts "using #{db_params['database']} DB" # Setup activerecord ActiveRecord::Base.establish_connection(db_params) # Development fix this ActiveRecord::Base.logger = Logger.new(STDERR) ActiveRecord::Base.logger.level = Logger::INFO # we need the connection, it can't be done earlier ArMessage.reset_column_information reload_queues @stompid = StompServer::StompId.new end # Add a frame to the queue def enqueue(queue_name, frame) unless @frames[queue_name] @frames[queue_name] = { :last_index => 0, :frames => [], } end affect_msgid_and_store(frame, queue_name) @frames[queue_name][:frames] << frame end # Get and remove a frame from the queue def dequeue(queue_name) return nil unless @frames[queue_name] && !@frames[queue_name][:frames].empty? frame = @frames[queue_name][:frames].shift remove_from_store(frame.headers['message-id']) return frame end # Requeue the frame previously pending def requeue(queue_name, frame) @frames[queue_name][:frames] << frame ArMessage.create!(:stomp_id => frame.headers['message-id'], :frame => frame) end # remove a frame from the store def remove_from_store(message_id) ArMessage.find_by_stomp_id(message_id).destroy end # store a frame (assigning it a message-id) def affect_msgid_and_store(frame, queue_name) msgid = assign_id(frame, queue_name) ArMessage.create!(:stomp_id => msgid, :frame => frame) end def message_for?(queue_name) @frames[queue_name] && !@frames[queue_name][:frames].empty? end def assign_id(frame, queue_name) msgid = @stompid[@frames[queue_name][:last_index] += 1] frame.headers['message-id'] = msgid end private def reload_queues @frames = Hash.new ArMessage.find(:all).each { |message| frame = message.frame destination = frame.dest msgid = message.stomp_id @frames[destination] ||= Hash.new @frames[destination][:frames] ||= Array.new @frames[destination][:frames] << frame } # compute base index for each destination @frames.each_pair { |destination,hash| hash[:last_index] = hash[:frames].map{|f| f.headers['message-id'].match(/(\d+)\Z/)[0].to_i}.max } end end end stompserver-0.9.9/lib/stomp_server/queue.rb0000644000175000017500000001105711754732653017576 0ustar paulpaul module StompServer class Queue attr_accessor :checkpoint_interval def initialize(directory='.stompserver', delete_empty=true) @stompid = StompServer::StompId.new @delete_empty = delete_empty @directory = directory Dir.mkdir(@directory) unless File.directory?(@directory) if File.exists?("#{@directory}/qinfo") qinfo = Hash.new File.open("#{@directory}/qinfo", "rb") { |f| qinfo = Marshal.load(f.read)} @queues = qinfo[:queues] @frames = qinfo[:frames] else @queues = Hash.new @frames = Hash.new end @queues.keys.each do |dest| puts "Queue #{dest} size=#{@queues[dest][:size]} enqueued=#{@queues[dest][:enqueued]} dequeued=#{@queues[dest][:dequeued]}" if $DEBUG end puts "Queue initialized in #{@directory}" # Cleanup dead queues and save the state of the queues every so often. Alternatively we could save the queue state every X number # of frames that are put in the queue. Should probably also read it after saving it to confirm integrity. # Removed, this badly corrupt the queue when stopping with messages #EventMachine::add_periodic_timer 1800, proc {@queues.keys.each {|dest| close_queue(dest)};save_queue_state } end def stop puts "Shutting down Queue" @queues.keys.each {|dest| close_queue(dest)} @queues.keys.each do |dest| puts "Queue #{dest} size=#{@queues[dest][:size]} enqueued=#{@queues[dest][:enqueued]} dequeued=#{@queues[dest][:dequeued]}" if $DEBUG end save_queue_state end def save_queue_state now=Time.now @next_save ||=now if now >= @next_save puts "Saving Queue State" if $DEBUG qinfo = {:queues => @queues, :frames => @frames} # write then rename to make sure this is atomic File.open("#{@directory}/qinfo.new", "wb") { |f| f.write Marshal.dump(qinfo)} File.rename("#{@directory}/qinfo.new","#{@directory}/qinfo") @next_save=now+checkpoint_interval end end def monitor stats = Hash.new @queues.keys.each do |dest| stats[dest] = {'size' => @queues[dest][:size], 'enqueued' => @queues[dest][:enqueued], 'dequeued' => @queues[dest][:dequeued]} end stats end def close_queue(dest) if @queues[dest][:size] == 0 and @queues[dest][:frames].size == 0 and @delete_empty _close_queue(dest) @queues.delete(dest) @frames.delete(dest) puts "Queue #{dest} removed." if $DEBUG end end def open_queue(dest) @queues[dest] = Hash.new @frames[dest] = Hash.new @queues[dest][:size] = 0 @queues[dest][:frames] = Array.new @queues[dest][:msgid] = 1 @queues[dest][:enqueued] = 0 @queues[dest][:dequeued] = 0 @queues[dest][:exceptions] = 0 _open_queue(dest) puts "Created queue #{dest}" if $DEBUG end def requeue(dest,frame) open_queue(dest) unless @queues.has_key?(dest) msgid = frame.headers['message-id'] if frame.headers['max-exceptions'] and @frames[dest][msgid][:exceptions] >= frame.headers['max-exceptions'].to_i enqueue("/queue/deadletter",frame) return end writeframe(dest,frame,msgid) @queues[dest][:frames].unshift(msgid) @frames[dest][msgid][:exceptions] += 1 @queues[dest][:dequeued] -= 1 @queues[dest][:exceptions] += 1 @queues[dest][:size] += 1 save_queue_state return true end def enqueue(dest,frame) open_queue(dest) unless @queues.has_key?(dest) msgid = assign_id(frame, dest) writeframe(dest,frame,msgid) @queues[dest][:frames].push(msgid) @frames[dest][msgid] = Hash.new @frames[dest][msgid][:exceptions] =0 @frames[dest][msgid][:client_id] = frame.headers['client-id'] if frame.headers['client-id'] @frames[dest][msgid][:expires] = frame.headers['expires'] if frame.headers['expires'] @queues[dest][:msgid] += 1 @queues[dest][:enqueued] += 1 @queues[dest][:size] += 1 save_queue_state return true end def dequeue(dest) return false unless message_for?(dest) msgid = @queues[dest][:frames].shift frame = readframe(dest,msgid) @queues[dest][:size] -= 1 @queues[dest][:dequeued] += 1 @queues[dest].delete(msgid) close_queue(dest) save_queue_state return frame end def message_for?(dest) return (@queues.has_key?(dest) and (!@queues[dest][:frames].empty?)) end def writeframe(dest,frame,msgid) _writeframe(dest,frame,msgid) end def readframe(dest,msgid) _readframe(dest,msgid) end def assign_id(frame, dest) msg_id = @queues[dest].nil? ? 1 : @queues[dest][:msgid] frame.headers['message-id'] = @stompid[msg_id] end end end stompserver-0.9.9/lib/stomp_server/protocols/0000775000175000017500000000000011754732653020147 5ustar paulpaulstompserver-0.9.9/lib/stomp_server/protocols/stomp.rb0000644000175000017500000001101311754732653021630 0ustar paulpaul module StompServer module StompServer::Protocols VALID_COMMANDS = [:connect, :send, :subscribe, :unsubscribe, :begin, :commit, :abort, :ack, :disconnect] class Stomp < EventMachine::Connection def initialize *args super end def post_init @sfr = StompServer::StompFrameRecognizer.new @transactions = {} @connected = false end def receive_data(data) stomp_receive_data(data) end def stomp_receive_data(data) begin puts "receive_data: #{data.inspect}" if $DEBUG @sfr << data process_frames rescue Exception => e puts "err: #{e} #{e.backtrace.join("\n")}" send_error(e.to_s) close_connection_after_writing end end def stomp_receive_frame(frame) begin puts "receive_frame: #{frame.inspect}" if $DEBUG process_frame(frame) rescue Exception => e puts "err: #{e} #{e.backtrace.join("\n")}" send_error(e.to_s) close_connection_after_writing end end def process_frames frame = nil process_frame(frame) while frame = @sfr.frames.shift end def process_frame(frame) cmd = frame.command.downcase.to_sym raise "Unhandled frame: #{cmd}" unless VALID_COMMANDS.include?(cmd) raise "Not connected" if !@connected && cmd != :connect # I really like this code, but my needs are a little trickier # if trans = frame.headers['transaction'] handle_transaction(frame, trans, cmd) else cmd = :sendmsg if cmd == :send send(cmd, frame) end send_receipt(frame.headers['receipt']) if frame.headers['receipt'] end def handle_transaction(frame, trans, cmd) if [:begin, :commit, :abort].include?(cmd) send(cmd, frame, trans) else raise "transaction does not exist" unless @transactions.has_key?(trans) @transactions[trans] << frame end end def connect(frame) if @@auth_required unless frame.headers['login'] and frame.headers['passcode'] and @@stompauth.authorized[frame.headers['login']] == frame.headers['passcode'] raise "Invalid Login" end end puts "Connecting" if $DEBUG response = StompServer::StompFrame.new("CONNECTED", {'session' => 'wow'}) stomp_send_data(response) @connected = true end def sendmsg(frame) # set message id if frame.dest.match(%r|^/queue|) @@queue_manager.sendmsg(frame) else frame.headers['message-id'] = "msg-#stompcma-#{@@topic_manager.next_index}" @@topic_manager.sendmsg(frame) end end def subscribe(frame) use_ack = false use_ack = true if frame.headers['ack'] == 'client' if frame.dest =~ %r|^/queue| @@queue_manager.subscribe(frame.dest, self,use_ack) else @@topic_manager.subscribe(frame.dest, self) end end def unsubscribe(frame) if frame.dest =~ %r|^/queue| @@queue_manager.unsubscribe(frame.dest,self) else @@topic_manager.unsubscribe(frame.dest,self) end end def begin(frame, trans=nil) raise "Missing transaction" unless trans raise "transaction exists" if @transactions.has_key?(trans) @transactions[trans] = [] end def commit(frame, trans=nil) raise "Missing transaction" unless trans raise "transaction does not exist" unless @transactions.has_key?(trans) (@transactions[trans]).each do |frame| frame.headers.delete('transaction') process_frame(frame) end @transactions.delete(trans) end def abort(frame, trans=nil) raise "Missing transaction" unless trans raise "transaction does not exist" unless @transactions.has_key?(trans) @transactions.delete(trans) end def ack(frame) @@queue_manager.ack(self, frame) end def disconnect(frame) puts "Polite disconnect" if $DEBUG close_connection_after_writing end def unbind p "Unbind called" if $DEBUG @connected = false @@queue_manager.disconnect(self) @@topic_manager.disconnect(self) end def connected? @connected end def send_message(msg) msg.command = "MESSAGE" stomp_send_data(msg) end def send_receipt(id) send_frame("RECEIPT", { 'receipt-id' => id}) end def send_error(msg) send_frame("ERROR",{'message' => 'See below'},msg) end def stomp_send_data(frame) send_data(frame.to_s) puts "Sending frame #{frame.to_s}" if $DEBUG end def send_frame(command, headers={}, body='') headers['content-length'] = body.size.to_s response = StompServer::StompFrame.new(command, headers, body) stomp_send_data(response) end end end end stompserver-0.9.9/lib/stomp_server/protocols/http.rb0000644000175000017500000000661511754732653021461 0ustar paulpaul class Mongrel::HttpRequest attr_reader :body, :params def initialize(params, initial_body) @params = params @body = StringIO.new @body.write params.http_body end end module StompServer module StompServer::Protocols class Http < EventMachine::Connection def initialize *args super @buf = '' end def post_init @parser = Mongrel::HttpParser.new @params = Mongrel::HttpParams.new @nparsed = 0 @request = nil @request_method = nil @request_length = 0 @state = :headers @headers_out = {'Content-Length' => 0, 'Content-Type' => 'text/plain; charset=UTF-8'} end def receive_data data parse_request(data) end def parse_request data @buf << data case @state when :headers @nparsed = @parser.execute(@params, @buf, @nparsed) if @parser.finished? @request = Mongrel::HttpRequest.new(@params,@buf) @request_method = @request.params[Mongrel::Const::REQUEST_METHOD] content_length = @request.params[Mongrel::Const::CONTENT_LENGTH].to_i @request_length = @nparsed + content_length @remain = content_length - @request.params.http_body.length if @remain <= 0 @buf = @buf[@request_length+1..-1] || '' process_request post_init return end @request.body.write @request.params.http_body @state = :body end when :body @remain -= @request.body.write data[0...@remain] if @remain <= 0 @buf = @buf[@request_length+1..-1] || '' process_request post_init return end end end def process_request begin @request.body.rewind dest = @request.params[Mongrel::Const::REQUEST_PATH] case @request_method when 'PUT' @frame = StompServer::StompFrame.new @frame.command = 'SEND' @frame.body = @request.body.read @frame.headers['destination'] = dest if @@queue_manager.enqueue(@frame) create_response('200','Message Enqueued') else create_response('500','Error enqueueing message') end when 'GET' if frame = @@queue_manager.dequeue(dest) @headers_out['message-id'] = frame.headers['message-id'] create_response('200',frame.body) else create_response('404','No messages in queue') end else create_response('500','Invalid Command') end rescue Exception => e puts "err: #{e} #{e.backtrace.join("\n")}" create_response('500',e) end end def unbind puts "Closing connection" close_connection_after_writing end def create_response(code,response_text) response = '' @headers_out['Content-Length'] = response_text.size case code when '200' response << "HTTP/1.1 200 OK\r\n" when '500' response << "HTTP/1.1 500 Server Error\r\n" when '404' response << "HTTP/1.1 404 Message Not Found\r\n" end @headers_out.each_pair do |key, value| response << "#{key}:#{value}\r\n" end response << "\r\n" response << response_text send_data(response) unbind if @request.params['HTTP_CONNECTION'] == 'close' end end end end stompserver-0.9.9/lib/stomp_server.rb0000644000175000017500000001217211754732653016451 0ustar paulpaulrequire 'rubygems' require 'eventmachine' require 'stomp_server/stomp_frame' require 'stomp_server/stomp_id' require 'stomp_server/stomp_auth' require 'stomp_server/topic_manager' require 'stomp_server/queue_manager' require 'stomp_server/queue' require 'stomp_server/queue/memory_queue' require 'stomp_server/queue/file_queue' require 'stomp_server/queue/dbm_queue' require 'stomp_server/protocols/stomp' module StompServer VERSION = '0.9.9' class Configurator attr_accessor :opts def initialize @opts = nil @defaults = { :port => 61613, :host => "127.0.0.1", :debug => false, :queue => 'memory', :auth => false, :working_dir => Dir.getwd, :storage => ".stompserver", :logdir => 'log', :configfile => 'stompserver.conf', :logfile => 'stompserver.log', :pidfile => 'stompserver.pid', :checkpoint => 0 } @opts = getopts if opts[:debug] $DEBUG=true end end def getopts copts = OptionParser.new copts.on("-C", "--config=CONFIGFILE", String, "Configuration File (default: stompserver.conf)") {|c| @defaults[:configfile] = c} copts.on("-p", "--port=PORT", Integer, "Change the port (default: 61613)") {|p| @defaults[:port] = p} copts.on("-b", "--host=ADDR", String, "Change the host (default: localhost)") {|a| @defaults[:host] = a} copts.on("-q", "--queuetype=QUEUETYPE", String, "Queue type (memory|dbm|activerecord|file) (default: memory)") {|q| @defaults[:queue] = q} copts.on("-w", "--working_dir=DIR", String, "Change the working directory (default: current directory)") {|s| @defaults[:working_dir] = s} copts.on("-s", "--storage=DIR", String, "Change the storage directory (default: .stompserver, relative to working_dir)") {|s| @defaults[:storage] = s} copts.on("-d", "--debug", String, "Turn on debug messages") {|d| @defaults[:debug] = true} copts.on("-a", "--auth", String, "Require client authorization") {|a| @defaults[:auth] = true} copts.on("-c", "--checkpoint=SECONDS", Integer, "Time between checkpointing the queues in seconds (default: 0)") {|c| @defaults[:checkpoint] = c} copts.on("-h", "--help", "Show this message") do puts copts exit end puts copts.parse(ARGV) if File.exists?(@defaults[:configfile]) opts = @defaults.merge(YAML.load_file(@defaults[:configfile])) else opts = @defaults end opts[:etcdir] = File.join(opts[:working_dir],'etc') opts[:storage] = File.join(opts[:working_dir],opts[:storage]) opts[:logdir] = File.join(opts[:working_dir],opts[:logdir]) opts[:logfile] = File.join(opts[:logdir],opts[:logfile]) opts[:pidfile] = File.join(opts[:logdir],opts[:pidfile]) if opts[:auth] opts[:passwd] = File.join(opts[:etcdir],'.passwd') end return opts end end class Run attr_accessor :queue_manager, :auth_required, :stompauth, :topic_manager def initialize(opts) @opts = opts @queue_manager = nil @auth_required = nil @stompauth = nil @topic_manager = nil end def stop(pidfile) @queue_manager.stop puts "Stompserver shutting down" if $DEBUG EventMachine::stop_event_loop File.delete(pidfile) end def start begin if @opts[:group] puts "Changing group to #{@opts[:group]}." Process::GID.change_privilege(Etc.getgrnam(@opts[:group]).gid) end if @opts[:user] puts "Changing user to #{@opts[:user]}." Process::UID.change_privilege(Etc.getpwnam(@opts[:user]).uid) end rescue Errno::EPERM puts "FAILED to change user:group #{@opts[:user]}:#{@opts[:group]}: #$!" exit 1 end Dir.mkdir(@opts[:working_dir]) unless File.directory?(@opts[:working_dir]) Dir.mkdir(@opts[:logdir]) unless File.directory?(@opts[:logdir]) Dir.mkdir(@opts[:etcdir]) unless File.directory?(@opts[:etcdir]) if @opts[:daemon] Daemonize.daemonize(log_file=@opts[:logfile]) # change back to the original starting directory Dir.chdir(@opts[:working_dir]) end # Write pidfile open(@opts[:pidfile],"w") {|f| f.write(Process.pid) } if @opts[:queue] == 'dbm' qstore=StompServer::DBMQueue.new(@opts[:storage]) elsif @opts[:queue] == 'file' qstore=StompServer::FileQueue.new(@opts[:storage]) elsif @opts[:queue] == 'activerecord' require 'stomp_server/queue/activerecord_queue' qstore=StompServer::ActiveRecordQueue.new(@opts[:etcdir], @opts[:storage]) else qstore=StompServer::MemoryQueue.new end qstore.checkpoint_interval = @opts[:checkpoint] puts "Checkpoing interval is #{qstore.checkpoint_interval}" if $DEBUG @topic_manager = StompServer::TopicManager.new @queue_manager = StompServer::QueueManager.new(qstore) @auth_required = @opts[:auth] if @auth_required @stompauth = StompServer::StompAuth.new(@opts[:passwd]) end trap("INT") { puts "INT signal received.";stop(@opts[:pidfile]) } end end end stompserver-0.9.9/etc/0000775000175000017500000000000011754732653013400 5ustar paulpaulstompserver-0.9.9/etc/passwd.example0000644000175000017500000000026711754732653016261 0ustar paulpaul# Stompserver will look for a .passwd file in the directory you are running it from. One login/passcode per line, separated by a colon. # Comment lines are ignored testuser:testpass stompserver-0.9.9/config/0000775000175000017500000000000011754732653014072 5ustar paulpaulstompserver-0.9.9/config/stompserver.conf0000644000175000017500000000023011754732653017323 0ustar paulpaul--- :daemon: false :working_dir: /tmp/stompserver :storage: .queue :queue: file :auth: false :debug: false :group: :user: :host: 127.0.0.1 :port: 61613 stompserver-0.9.9/client/0000775000175000017500000000000011754732653014103 5ustar paulpaulstompserver-0.9.9/client/send.rb0000644000175000017500000000051611754732653015361 0ustar paulpaulrequire 'rubygems' require 'stomp' client = Stomp::Client.open "login", "passcode", "localhost", 61613 # sending 5 messages at once for i in 1..5 do m = "Go Sox #{i}!" puts m client.send("/queue/test", m, { "persistent" => true, "client-id" => "Client1", "reply-to" => "/queue/test", } ) end client.close stompserver-0.9.9/client/consume.rb0000644000175000017500000000060011754732653016073 0ustar paulpaulrequire 'rubygems' require 'stomp' client = Stomp::Client.open "login", "passcode", "localhost", 61613 client.subscribe("/queue/test", { "persistent" => true, "client-id" => "rubyClient", } ) do |message| puts "Got Reply: ID=#{message.headers['message-id']} BODY=#{message.body} on #{message.headers['destination']}" end gets client.close stompserver-0.9.9/client/both.rb0000644000175000017500000000112211754732653015356 0ustar paulpaulrequire 'rubygems' require 'stomp' client = Stomp::Client.open "login", "passcode", "localhost", 61613 client.subscribe("/queue/client2", { "persistent" => true, "ack" => 'client', "client-id" => "rubyClient", } ) do |message| puts "Got Reply: #{message.headers['message-id']} - #{message.body} on #{message.headers['destination']}" end for i in 1..5 do m = "Go Sox #{i}!" puts m client.send("/queue/client2", m, { "persistent" => true, "priority" => 4, "reply-to" => "/queue/client2", } ) end gets client.close stompserver-0.9.9/client/README.txt0000644000175000017500000000007511754732653015601 0ustar paulpaulSamples of how to use the ruby stomp client with stompserver stompserver-0.9.9/bin/0000775000175000017500000000000011754732653013375 5ustar paulpaulstompserver-0.9.9/bin/stompserver0000644000175000017500000000270011754732653015706 0ustar paulpaulrequire 'rubygems' require 'etc' require 'yaml' require 'daemons/daemonize' require 'stomp_server' require 'optparse' $STOMP_SERVER = true $HTTP_ENABLE = false if $HTTP_ENABLE require 'mongrel' require 'stomp_server/protocols/http' end EventMachine::run do ## Get the configuration and initialize the stomp engine config = StompServer::Configurator.new stomp = StompServer::Run.new(config.opts) stomp.start # Might want to uncomment this if you are sending large files #EventMachine::add_periodic_timer 10, proc {GC.start} puts "Client authorization enabled" if config.opts[:auth] puts "Debug enabled" if config.opts[:debug] ## Start protocol handlers puts "Stomp protocol handler starting on #{config.opts[:host]} port #{config.opts[:port]}" EventMachine.start_server(config.opts[:host], config.opts[:port], StompServer::Protocols::Stomp) {|s| s.instance_eval { @@auth_required=stomp.auth_required @@queue_manager=stomp.queue_manager @@topic_manager=stomp.topic_manager @@stompauth = stomp.stompauth } } if $HTTP_ENABLE puts "Http protocol handler starting on #{config.opts[:host]} port 8080" EventMachine.start_server(config.opts[:host], 8080, StompServer::Protocols::Http) {|s| s.instance_eval { @@auth_required=stomp.auth_required @@queue_manager=stomp.queue_manager @@topic_manager=stomp.topic_manager @@stompauth = stomp.stompauth } } end end stompserver-0.9.9/STATUS0000644000175000017500000000036711754732653013577 0ustar paulpaul - Unit tests are broken, need to override methods that call EM - Added http protocol handler. No pipelining or anything fancy, just PUT/GET requests which enqueue/dequeue messages from the queue one at a time. Uses the Mongrel http parser. stompserver-0.9.9/Rakefile0000644000175000017500000000140511754732653014270 0ustar paulpaul# -*- ruby -*- require 'rubygems' require 'hoe' $LOAD_PATH << "./lib" require 'stomp_server' Hoe.new('stompserver', StompServer::VERSION) do |p| p.rubyforge_name = 'stompserver' p.summary = 'A very light messaging server' p.description = p.paragraphs_of('README.txt', 2..4).join("\n\n") p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1] p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n") p.email = [ "lionel-dev@bouton.name" ] p.author = [ "Lionel Bouton" ] p.extra_deps = [ # This depencency is real, but if you are on a Win32 box # and don't have VC6, it can be a real problem ["daemons", ">= 1.0.2"], ["eventmachine", ">= 0.7.2"], ["hoe", ">= 1.1.1"], ] p.remote_rdoc_dir = '' end # vim: syntax=Ruby stompserver-0.9.9/README.txt0000644000175000017500000001426611754732653014332 0ustar paulpaulstompserver by Patrick Hurley, Lionel Bouton http://stompserver.rubyforge.org/ == DESCRIPTION: Stomp messaging server with file/dbm/memory/activerecord based FIFO queues, queue monitoring, and basic authentication. == SYNOPSYS: Handles basic message queue processing == REQUIREMENTS: * EventMachine == FEATURES/PROBLEMS: === Several queue storage backends Handles basic message queue processing using memory, file, or dbm based queues. Messages are sent and consumed in FIFO order (unless a client error happens, this should be corrected in the future). Topics are memory-only storage. You can select activerecord, file or dbm storage and the queues will use that, but topics will only be stored in memory. memory queues are of course the fastest ones but shouldn't be used if you want to ensure all messages are delivered. dbm queues will use berkeleydb if available, otherwise dbm or gdbm depending on the platform. sdbm does not work well with marshalled data. Note that these queues have not been tested in this release. For the file based storage, each frame is stored in a single file. The first 8 bytes contains the header length, the next 8 bytes contains the body length, then the headers are stored as a marshalled object followed by the body stored as a string. This storage is currently inefficient because queues are stored separately from messages, which forces a double write for data safety reasons on each message stored. The activerecord based storage expects to find a database.yml file in the configuration directory. It should be the most robust backend, but the slowest one. The database must have an ar_messages table which can be created with the following code (you are responsible to do so): ActiveRecord::Schema.define do create_table 'ar_messages' do |t| t.column 'stomp_id', :string, :null => false t.column 'frame', :text, :null => false end end You can read the frames with this model: class ArMessage < ActiveRecord::Base serialize :frame end The ar_message implementation will certainly change in the future. This is meant to be easily readable by a Rails application (which could handle the ar_messages table creation with a migration). === Limitations Stompserver not support any server to server messaging (although you could write a client to do this). === Monitoring Queues can be monitored via the monitor queue (this will probably not be supported this way in the future to avoid polluting the queue namespace). If you subscribe to /queue/monitor, you will receive a status message every 5 seconds that displays each queue, it's size, frames enqueued, and frames dequeued. Stats are sent in the same format of stomp headers, so they are easy to parse. Following is an example of a status message containing stats for 2 queues: Queue: /queue/client2 size: 0 dequeued: 400 enqueued: 400 Queue: /queue/test size: 50 dequeued: 250 enqueued: 300 === Access control Basic client authorization is also supported. If the -a flag is passed to stompserver on startup, and a .passwd file exists in the run directory, then clients will be required to provide a valid login and passcode. See passwd.example for the password file format. === Misc Whenever you stop the server, any queues with no messages will be removed, and the stats for that queue will be reset. If the queue has any messages remaining then the stats will be saved and available on the next restart. == INSTALL: * gem install stompserver stompserver will create a log, etc, and storage directory on startup in your current working directory, the value passed to as --working_dir parameter, or if using a config file it will use what you specified for working_dir. The configuration file is a yaml file and can be specified on the command line with -C . A sample is provided in config/stompserver.conf. Command line options will override options set in the yaml config file. To use the memory queue run as follows: stompserver -p 61613 -b 0.0.0.0 To use the file or dbm queue storage, use the -q switch and specificy either file or dbm. The file and dbm queues also take a storage directory specified with -s. .stompserver is the default directory if -s is not used. stompserver -p 61613 -b 0.0.0.0 -q file -s .stompfile Or stompserver -p 61613 -b 0.0.0.0 -q dbm -s .stompbdb To specify where the queue is stored on disk, use the -s flag followed by a storage directory. To enable client authorization use -a, for debugging use -d. stompserver -p 61613 -b 0.0.0.0 -q file -s .stompserver -a -d You cannot use the same storage directory for a file and dbm queue, they must be kept separate. To use the activerecord queue storage use -q activerecord: stompserver -p 61613 -b 0.0.0.0 -q activerecord It will try to read the etc/database.yml file in the working directory. Here's an example of a database.yml for a PostgreSQL database named stompserver on the 'dbserver' host usable by PostgreSQL's user 'foo' with password 'bar'(see ActiveRecord's documentation for the parameters needed by your database): adapter: postgresql database: stompserver username: foo password: bar host: dbserver == LICENSE: (The MIT License) Copyright (c) 2006 Patrick Hurley Copyright (c) 2007 Lionel Bouton Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. stompserver-0.9.9/Manifest.txt0000644000175000017500000000151411754732653015133 0ustar paulpaulHistory.txt Manifest.txt README.txt Rakefile STATUS bin/stompserver client/README.txt client/both.rb client/consume.rb client/send.rb config/stompserver.conf etc/passwd.example lib/stomp_server.rb lib/stomp_server/protocols/http.rb lib/stomp_server/protocols/stomp.rb lib/stomp_server/queue.rb lib/stomp_server/queue/activerecord_queue.rb lib/stomp_server/queue/ar_message.rb lib/stomp_server/queue/dbm_queue.rb lib/stomp_server/queue/file_queue.rb lib/stomp_server/queue/memory_queue.rb lib/stomp_server/queue_manager.rb lib/stomp_server/stomp_auth.rb lib/stomp_server/stomp_frame.rb lib/stomp_server/stomp_id.rb lib/stomp_server/stomp_user.rb lib/stomp_server/test_server.rb lib/stomp_server/topic_manager.rb setup.rb test/tesly.rb test/test_queue_manager.rb test/test_stomp_frame.rb test/test_topic_manager.rb test_todo/test_stomp_server.rb stompserver-0.9.9/History.txt0000644000175000017500000000345311754732653015032 0ustar paulpaul== 0.9.9 / 31 Jan 2008 * Fix for non-memory backends with empty queues * Fix for queue monitoring * Use atomic checkpoint, write to a new file and then rename it into place to avoid incomplete qinfo files being written * add configuration parameter for checkpoint time for better performance with large queues == 0.9.8 / 16 Aug 2007 * Several storage backends instead of Madeleine (allow tradeoffs between robustness with ActiveRecord and speed with memory with intermediary solutions based on file or dbm storages). * Better load distribution between clients acknowledging messages after processing them. * Monitoring support through a dedicated queue. == 0.9.5 / 18 Oct 2006 * Removed eventmachine dependency in the gem you still need eventmachine, but you can choose between eventmachine and eventmachine-win32, not sure how to put that in the gem. Also made another minor fix to the stompserver binary. == 0.9.4 / 17 Oct 2006 * Cleanup (wow I was tired last night) * Tested under Linux, added require 'rubygems' * Fixed stompserver binary - it now works and takes a few simple command args * Fixed up minor (tesly) test syntax issues == 0.9.3 / 16 Oct 2006 * Last one tonight I promise * Fixed incorrect gem requirement version on eventmachine * Fixed stompserver cmd added by gem == 0.9.2 / 16 Oct 2006 * More Queue issues resolved * Added more unit tests * All tests pass * Queue processing done in one large batch, this may change == 0.9.1 / 16 Oct 2006 * Stepping along * Fixed release issues * Fixed queue logic error (failed to detect offlines) == 0.9.0 / 16 Oct 2006 * Initialial Beta Release * Seems to work * Passes numerous test cases * Journals using madeleine * Needs documentaion * Needs to command line processing * Needs service/daemon options4