├── VERSION ├── lib ├── bertrem │ ├── mod.rb │ ├── action.rb │ ├── client.rb │ └── server.rb └── bertrem.rb ├── Rakefile ├── LICENSE ├── bertrem.gemspec └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.7 -------------------------------------------------------------------------------- /lib/bertrem/mod.rb: -------------------------------------------------------------------------------- 1 | module BERTREM 2 | class Mod 3 | 4 | attr_accessor :name, :funs 5 | 6 | def initialize(name) 7 | self.name = name 8 | self.funs = {} 9 | end 10 | 11 | def fun(name, block) 12 | raise TypeError, "block required" if block.nil? 13 | self.funs[name] = block 14 | end 15 | 16 | end 17 | 18 | end -------------------------------------------------------------------------------- /lib/bertrem.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | require 'bertrpc' 3 | require 'eventmachine' 4 | 5 | require 'bertrem/action' 6 | require 'bertrem/mod' 7 | require 'bertrem/client' 8 | require 'bertrem/server' 9 | 10 | module BERTREM 11 | def self.version 12 | File.read(File.join(File.dirname(__FILE__), *%w[.. VERSION])).chomp 13 | rescue 14 | 'unknown' 15 | end 16 | 17 | VERSION = self.version 18 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | begin 5 | require 'jeweler' 6 | Jeweler::Tasks.new do |gem| 7 | gem.name = "bertrem" 8 | gem.summary = %Q{BERTREM is a Ruby EventMachine BERT-RPC client and server library.} 9 | gem.email = "b@b3k.us" 10 | gem.homepage = "http://github.com/b/bertrem" 11 | gem.authors = ["Benjamin Black"] 12 | gem.add_dependency('bertrpc', '>= 1.1.2', '< 2.0.0') 13 | gem.add_dependency('eventmachine') 14 | # gem is a Gem::Specification... 15 | # see http://www.rubygems.org/read/chapter/20 for additional settings 16 | end 17 | 18 | rescue LoadError 19 | puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler" 20 | end 21 | 22 | task :console do 23 | exec('irb -Ilib -rbertrem') 24 | end -------------------------------------------------------------------------------- /lib/bertrem/action.rb: -------------------------------------------------------------------------------- 1 | require 'eventmachine' 2 | require 'bertrpc' 3 | 4 | module BERTRPC 5 | class Action 6 | 7 | [:execute, :write, :transaction, :connect_to].each do |m| 8 | remove_method m if method_defined?(m) 9 | end 10 | 11 | def execute 12 | transaction(encode_ruby_request(t[@req.kind, @mod, @fun, @args])) 13 | @svc.requests.unshift(EM::DefaultDeferrable.new).first 14 | end 15 | 16 | def write(bert) 17 | @svc.send_data([bert.length].pack("N")) 18 | @svc.send_data(bert) 19 | end 20 | 21 | def transaction(bert_request) 22 | if @req.options 23 | if @req.options[:cache] && @req.options[:cache][0] == :validation 24 | token = @req.options[:cache][1] 25 | info_bert = encode_ruby_request([:info, :cache, [:validation, token]]) 26 | write(info_bert) 27 | end 28 | end 29 | 30 | write(bert_request) 31 | end 32 | 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Benjamin Black 2 | 3 | Derived in part from work that is 4 | Copyright (c) 2009 Tom Preston-Werner 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /bertrem.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{bertrem} 8 | s.version = "0.0.7" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Benjamin Black"] 12 | s.date = %q{2010-01-14} 13 | s.email = %q{b@b3k.us} 14 | s.extra_rdoc_files = [ 15 | "LICENSE", 16 | "README.md" 17 | ] 18 | s.files = [ 19 | "LICENSE", 20 | "README.md", 21 | "Rakefile", 22 | "VERSION", 23 | "bertrem.gemspec", 24 | "lib/bertrem.rb", 25 | "lib/bertrem/action.rb", 26 | "lib/bertrem/client.rb", 27 | "lib/bertrem/mod.rb", 28 | "lib/bertrem/server.rb" 29 | ] 30 | s.homepage = %q{http://github.com/b/bertrem} 31 | s.rdoc_options = ["--charset=UTF-8"] 32 | s.require_paths = ["lib"] 33 | s.rubygems_version = %q{1.3.5} 34 | s.summary = %q{BERTREM is a Ruby EventMachine BERT-RPC client and server library.} 35 | 36 | if s.respond_to? :specification_version then 37 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 38 | s.specification_version = 3 39 | 40 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 41 | s.add_runtime_dependency(%q, [">= 1.1.2", "< 2.0.0"]) 42 | s.add_runtime_dependency(%q, [">= 0"]) 43 | else 44 | s.add_dependency(%q, [">= 1.1.2", "< 2.0.0"]) 45 | s.add_dependency(%q, [">= 0"]) 46 | end 47 | else 48 | s.add_dependency(%q, [">= 1.1.2", "< 2.0.0"]) 49 | s.add_dependency(%q, [">= 0"]) 50 | end 51 | end 52 | 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | BERTREM 2 | ====== 3 | 4 | By Benjamin Black (b@b3k.us) 5 | 6 | BERTREM is a BERT-RPC client and server implementation that uses an EventMachine server to accept incoming connections, and then delegates the request to loadable Ruby handlers. BERTREM is derived from [Ernie](http://github.com/mojombo/ernie), by Tom Preston-Warner. 7 | 8 | See the full BERT-RPC specification at [bert-rpc.org](http://bert-rpc.org). 9 | 10 | BERTREM currently supports the following BERT-RPC features: 11 | 12 | * `call` requests 13 | * `cast` requests 14 | 15 | 16 | Installation 17 | ------------ 18 | 19 | $ gem install bertrem -s http://gemcutter.org 20 | 21 | 22 | Example Handler 23 | --------------- 24 | 25 | A simple Ruby module for use in a BERTREM server: 26 | 27 | require 'bertrem' 28 | 29 | module Calc 30 | def add(a, b) 31 | a + b 32 | end 33 | end 34 | 35 | 36 | Example Server 37 | -------------- 38 | 39 | A simple BERTREM server using the Calc module defined above: 40 | 41 | require 'eventmachine' 42 | require 'bertrem' 43 | 44 | EM.run { 45 | BERTREM::Server.expose(:calc, Calc) 46 | svc = BERTREM::Server.start('localhost', 9999) 47 | } 48 | 49 | 50 | Logging 51 | ------- 52 | 53 | You can have logging sent to a file by adding these lines to your handler: 54 | 55 | logfile('/var/log/bertrem.log') 56 | loglevel(Logger::INFO) 57 | 58 | This will log startup info, requests, and error messages to the log. Choosing 59 | Logger::DEBUG will include the response (be careful, doing this can generate 60 | very large log files). 61 | 62 | 63 | Using the BERTRPC gem to make calls to BERTREM 64 | ---------------------------------------------- 65 | 66 | The BERTREM client supports persistent connections, so you can send multiple requests over the same service connection and responses will return in the order the requests were sent: 67 | 68 | require 'eventmachine' 69 | require 'bertrem' 70 | 71 | EM.run { 72 | client = BERTREM::Client.service('localhost', 9999, true) 73 | rpc = client.call.calc.add(6, 2) 74 | rpc.callback { |res| 75 | puts "Got response! -> #{res}" 76 | } 77 | 78 | rpc = client.call.calc.add(2, 2) 79 | rpc.callback { |res| 80 | puts "Got response! -> #{res}" 81 | } 82 | } 83 | # Got response! -> 8 84 | # Got response! -> 4 85 | 86 | Alternatively, you can make BERT-RPC calls from Ruby with the [BERTRPC gem](http://github.com/mojombo/bertrpc): 87 | 88 | require 'bertrpc' 89 | 90 | svc = BERTRPC::Service.new('localhost', 8000) 91 | svc.call.calc.add(1, 2) 92 | # => 3 93 | 94 | 95 | Contribute 96 | ---------- 97 | 98 | If you'd like to hack on BERTREM, start by forking my repo on GitHub: 99 | 100 | http://github.com/b/bertrem 101 | 102 | To get all of the dependencies, install the gem first 103 | 104 | The best way to get your changes merged back into core is as follows: 105 | 106 | 1. Clone down your fork 107 | 1. Create a topic branch to contain your change 108 | 1. Hack away 109 | 1. Add tests and make sure everything still passes by running `rake` 110 | 1. If you are adding new functionality, document it in the README.md 111 | 1. Do not change the version number, I will do that on my end 112 | 1. If necessary, rebase your commits into logical chunks, without errors 113 | 1. Push the branch up to GitHub 114 | 1. Send me (b) a pull request for your branch 115 | 116 | 117 | Copyright 118 | --------- 119 | 120 | Copyright (c) 2009 Benjamin Black. See LICENSE for details. 121 | -------------------------------------------------------------------------------- /lib/bertrem/client.rb: -------------------------------------------------------------------------------- 1 | require 'bertrpc' 2 | require 'logger' 3 | require 'eventmachine' 4 | 5 | module BERTREM 6 | # NOTE: ernie closes connections after responding, so we can't send 7 | # multiple requests per connection. Hence, the default for 8 | # persistent is false. If you are working with a server that 9 | # supports more than one request per connection, like 10 | # BERTREM::Server, call BERTREM.service with persistent = true 11 | # and it will Just Work. 12 | class Client < EventMachine::Connection 13 | include BERTRPC::Encodes 14 | 15 | attr_accessor :requests 16 | 17 | class Request 18 | attr_accessor :kind, :options 19 | 20 | def initialize(svc, kind, options) 21 | @svc = svc 22 | @kind = kind 23 | @options = options 24 | end 25 | 26 | def method_missing(cmd, *args) 27 | BERTRPC::Mod.new(@svc, self, cmd) 28 | end 29 | 30 | end 31 | 32 | class << self 33 | attr_accessor :persistent, :err_callback 34 | end 35 | 36 | self.persistent = false 37 | self.err_callback = Proc.new {|msg| raise BERTREM::ConnectionError.new(msg)} 38 | 39 | def self.service(host, port, persistent = false, timeout = nil) 40 | self.persistent = persistent 41 | c = EM.connect(host, port, self) 42 | c.pending_connect_timeout = timeout if timeout 43 | c 44 | end 45 | 46 | def post_init 47 | @receive_buf = ""; @receive_len = 0; @more = false 48 | @requests = [] 49 | end 50 | 51 | def unbind 52 | super 53 | @receive_buf = ""; @receive_len = 0; @more = false 54 | (@requests || []).each {|r| r.fail} 55 | Client.err_callback.call("Connection to server lost!") if error? 56 | end 57 | 58 | def persistent 59 | Client.persistent 60 | end 61 | 62 | def receive_data(bert_response) 63 | @receive_buf << bert_response 64 | 65 | while @receive_buf.length > 0 66 | unless @more 67 | begin 68 | if @receive_buf.length > 4 69 | @receive_len = @receive_buf.slice!(0..3).unpack('N').first if @receive_len == 0 70 | raise BERTRPC::ProtocolError.new(BERTRPC::ProtocolError::NO_DATA) unless @receive_buf.length > 0 71 | else 72 | raise BERTRPC::ProtocolError.new(BERTRPC::ProtocolError::NO_HEADER) 73 | end 74 | rescue Exception => e 75 | log "Bad BERT message: #{e.message}" 76 | next 77 | end 78 | end 79 | 80 | if @receive_buf.length >= @receive_len 81 | bert = @receive_buf.slice!(0..(@receive_len - 1)) 82 | @receive_len = 0; @more = false 83 | @requests.pop.succeed(decode_bert_response(bert)) 84 | break unless persistent 85 | else 86 | @more = true 87 | break 88 | end 89 | end 90 | 91 | close_connection unless (persistent || @more) 92 | end 93 | 94 | def call(options = nil) 95 | verify_options(options) 96 | Request.new(self, :call, options) 97 | end 98 | 99 | def cast(options = nil) 100 | verify_options(options) 101 | Request.new(self, :cast, options) 102 | end 103 | 104 | def verify_options(options) 105 | if options 106 | if cache = options[:cache] 107 | unless cache[0] == :validation && cache[1].is_a?(String) 108 | raise BERTRPC::InvalidOption.new("Valid :cache args are [:validation, String]") 109 | end 110 | else 111 | raise BERTRPC::InvalidOption.new("Valid options are :cache") 112 | end 113 | end 114 | end 115 | 116 | end 117 | 118 | end 119 | 120 | class BERTREM::ConnectionError < StandardError ; end -------------------------------------------------------------------------------- /lib/bertrem/server.rb: -------------------------------------------------------------------------------- 1 | require 'bert' 2 | require 'logger' 3 | require 'eventmachine' 4 | 5 | module BERTREM 6 | class Server < EventMachine::Connection 7 | include BERTRPC::Encodes 8 | 9 | # This class derived from Ernie/ernie.rb 10 | 11 | class << self 12 | attr_accessor :mods, :current_mod, :log 13 | end 14 | 15 | self.mods = {} 16 | self.current_mod = nil 17 | self.log = Logger.new(STDOUT) 18 | self.log.level = Logger::INFO 19 | 20 | def self.start(host, port) 21 | EM.start_server(host, port, self) 22 | end 23 | 24 | # Record a module. 25 | # +name+ is the module Symbol 26 | # +block+ is the Block containing function definitions 27 | # 28 | # Returns nothing 29 | def self.mod(name, block) 30 | m = Mod.new(name) 31 | self.current_mod = m 32 | self.mods[name] = m 33 | block.call 34 | end 35 | 36 | # Record a function. 37 | # +name+ is the function Symbol 38 | # +block+ is the Block to associate 39 | # 40 | # Returns nothing 41 | def self.fun(name, block) 42 | self.current_mod.fun(name, block) 43 | end 44 | 45 | # Expose all public methods in a Ruby module: 46 | # +name+ is the ernie module Symbol 47 | # +mixin+ is the ruby module whose public methods are exposed 48 | # 49 | # Returns nothing 50 | def self.expose(name, mixin) 51 | context = Object.new 52 | context.extend mixin 53 | self.mod(name, lambda { 54 | mixin.public_instance_methods.each do |meth| 55 | self.fun(meth.to_sym, context.method(meth)) 56 | end 57 | }) 58 | context 59 | end 60 | 61 | # Set the logfile to given path. 62 | # +file+ is the String path to the logfile 63 | # 64 | # Returns nothing 65 | def self.logfile(file) 66 | self.log = Logger.new(file) 67 | end 68 | 69 | # Set the log level. 70 | # +level+ is the Logger level (Logger::WARN, etc) 71 | # 72 | # Returns nothing 73 | def self.loglevel(level) 74 | self.log.level = level 75 | end 76 | 77 | # Dispatch the request to the proper mod:fun. 78 | # +mod+ is the module Symbol 79 | # +fun+ is the function Symbol 80 | # +args+ is the Array of arguments 81 | # 82 | # Returns the Ruby object response 83 | def self.dispatch(mod, fun, args) 84 | mods[mod] || raise(ServerError.new("No such module '#{mod}'")) 85 | mods[mod].funs[fun] || raise(ServerError.new("No such function '#{mod}:#{fun}'")) 86 | mods[mod].funs[fun].call(*args) 87 | end 88 | 89 | # Write the given Ruby object to the wire as a BERP. 90 | # +output+ is the IO on which to write 91 | # +ruby+ is the Ruby object to encode 92 | # 93 | # Returns nothing 94 | def write_berp(ruby) 95 | data = BERT.encode(ruby) 96 | send_data([data.length].pack("N")) 97 | send_data(data) 98 | end 99 | 100 | def post_init 101 | @receive_buf = ""; @receive_len = 0; @more = false 102 | #start_tls(:private_key_file => '/tmp/server.key', :cert_chain_file => '/tmp/server.crt', :verify_peer => false) 103 | Server.log.info("(#{Process.pid}) Starting") 104 | Server.log.debug(Server.mods.inspect) 105 | end 106 | 107 | def unbind 108 | super 109 | @receive_buf = ""; @receive_len = 0; @more = false 110 | end 111 | 112 | # Receive data on the connection. 113 | # 114 | def receive_data(bert_request) 115 | @receive_buf << bert_request 116 | 117 | while @receive_buf.length > 0 do 118 | unless @more 119 | begin 120 | if @receive_buf.length > 4 121 | @receive_len = @receive_buf.slice!(0..3).unpack('N').first if @receive_len == 0 122 | raise BERTRPC::ProtocolError.new(BERTRPC::ProtocolError::NO_DATA) unless @receive_buf.length > 0 123 | else 124 | raise BERTRPC::ProtocolError.new(BERTRPC::ProtocolError::NO_HEADER) 125 | end 126 | rescue Exception => e 127 | Server.log.error("Bad BERT message: #{e.message}") 128 | next 129 | end 130 | end 131 | 132 | if @receive_buf.length >= @receive_len 133 | bert = @receive_buf.slice!(0..(@receive_len - 1)) 134 | @receive_len = 0; @more = false 135 | iruby = BERT.decode(bert) 136 | 137 | unless iruby 138 | Server.log.info("(#{Process.pid}) No Ruby in this here packet. On to the next one...") 139 | next 140 | end 141 | 142 | if iruby.size == 4 && iruby[0] == :call 143 | mod, fun, args = iruby[1..3] 144 | Server.log.debug("-> " + iruby.inspect) 145 | begin 146 | res = Server.dispatch(mod, fun, args) 147 | oruby = t[:reply, res] 148 | Server.log.debug("<- " + oruby.inspect) 149 | write_berp(oruby) 150 | rescue ServerError => e 151 | oruby = t[:error, t[:server, 0, e.class.to_s, e.message, e.backtrace]] 152 | Server.log.error("<- " + oruby.inspect) 153 | Server.log.error(e.backtrace.join("\n")) 154 | write_berp(oruby) 155 | rescue Object => e 156 | oruby = t[:error, t[:user, 0, e.class.to_s, e.message, e.backtrace]] 157 | Server.log.error("<- " + oruby.inspect) 158 | Server.log.error(e.backtrace.join("\n")) 159 | write_berp(oruby) 160 | end 161 | elsif iruby.size == 4 && iruby[0] == :cast 162 | mod, fun, args = iruby[1..3] 163 | Server.log.debug("-> " + [:cast, mod, fun, args].inspect) 164 | begin 165 | Server.dispatch(mod, fun, args) 166 | rescue Object => e 167 | # ignore 168 | end 169 | write_berp(t[:noreply]) 170 | else 171 | Server.log.error("-> " + iruby.inspect) 172 | oruby = t[:error, t[:server, 0, "Invalid request: #{iruby.inspect}"]] 173 | Server.log.error("<- " + oruby.inspect) 174 | write_berp(oruby) 175 | end 176 | else 177 | @more = true 178 | break 179 | end 180 | end 181 | end 182 | end 183 | 184 | end 185 | 186 | class BERTREM::ServerError < StandardError; end 187 | --------------------------------------------------------------------------------