forked from mojombo/ernie
/
ernie.rb
222 lines (195 loc) · 5.57 KB
/
ernie.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
require 'rubygems'
require 'bert'
require 'logger'
class Ernie
VERSION = '2.5.1'
class << self
attr_accessor :mods, :current_mod, :log
attr_accessor :auto_start
attr_accessor :count, :virgin_procline
end
self.count = 0
self.virgin_procline = $0
self.mods = {}
self.current_mod = nil
self.log = Logger.new(STDOUT)
self.log.level = Logger::FATAL
self.auto_start = true
# Record a module.
# +name+ is the module Symbol
# +block+ is the Block containing function definitions
#
# Returns nothing
def self.mod(name, block)
m = Mod.new(name)
self.current_mod = m
self.mods[name] = m
block.call
end
# Record a function.
# +name+ is the function Symbol
# +block+ is the Block to associate
#
# Returns nothing
def self.fun(name, block)
self.current_mod.fun(name, block)
end
# Expose all public methods in a Ruby module:
# +name+ is the ernie module Symbol
# +mixin+ is the ruby module whose public methods are exposed
#
# Returns nothing
def self.expose(name, mixin)
context = Object.new
context.extend mixin
mod(name, lambda {
mixin.public_instance_methods.each do |meth|
fun(meth.to_sym, context.method(meth))
end
})
context
end
# Set the logfile to given path.
# +file+ is the String path to the logfile
#
# Returns nothing
def self.logfile(file)
self.log = Logger.new(file)
end
# Set the log level.
# +level+ is the Logger level (Logger::WARN, etc)
#
# Returns nothing
def self.loglevel(level)
self.log.level = level
end
# Dispatch the request to the proper mod:fun.
# +mod+ is the module Symbol
# +fun+ is the function Symbol
# +args+ is the Array of arguments
#
# Returns the Ruby object response
def self.dispatch(mod, fun, args)
self.mods[mod] || raise(ServerError.new("No such module '#{mod}'"))
self.mods[mod].funs[fun] || raise(ServerError.new("No such function '#{mod}:#{fun}'"))
self.mods[mod].funs[fun].call(*args)
end
# Read the length header from the wire.
# +input+ is the IO from which to read
#
# Returns the size Integer if one was read
# Returns nil otherwise
def self.read_4(input)
raw = input.read(4)
return nil unless raw
raw.unpack('N').first
end
# Read a BERP from the wire and decode it to a Ruby object.
# +input+ is the IO from which to read
#
# Returns a Ruby object if one could be read
# Returns nil otherwise
def self.read_berp(input)
packet_size = self.read_4(input)
return nil unless packet_size
bert = input.read(packet_size)
BERT.decode(bert)
end
# Write the given Ruby object to the wire as a BERP.
# +output+ is the IO on which to write
# +ruby+ is the Ruby object to encode
#
# Returns nothing
def self.write_berp(output, ruby)
data = BERT.encode(ruby)
output.write([data.length].pack("N"))
output.write(data)
end
# Start the processing loop.
#
# Loops forever
def self.start
self.procline('starting')
self.log.info("(#{Process.pid}) Starting")
self.log.debug(self.mods.inspect)
input = IO.new(3)
output = IO.new(4)
input.sync = true
output.sync = true
loop do
self.procline('waiting')
iruby = self.read_berp(input)
self.count += 1
unless iruby
puts "Could not read BERP length header. Ernie server may have gone away. Exiting now."
self.log.info("(#{Process.pid}) Could not read BERP length header. Ernie server may have gone away. Exiting now.")
exit!
end
if iruby.size == 4 && iruby[0] == :call
mod, fun, args = iruby[1..3]
self.procline("#{mod}:#{fun}(#{args})")
self.log.info("-> " + iruby.inspect)
begin
res = self.dispatch(mod, fun, args)
oruby = t[:reply, res]
self.log.debug("<- " + oruby.inspect)
write_berp(output, oruby)
rescue ServerError => e
oruby = t[:error, t[:server, 0, e.class.to_s, e.message, e.backtrace]]
self.log.error("<- " + oruby.inspect)
self.log.error(e.backtrace.join("\n"))
write_berp(output, oruby)
rescue Object => e
oruby = t[:error, t[:user, 0, e.class.to_s, e.message, e.backtrace]]
self.log.error("<- " + oruby.inspect)
self.log.error(e.backtrace.join("\n"))
write_berp(output, oruby)
end
elsif iruby.size == 4 && iruby[0] == :cast
mod, fun, args = iruby[1..3]
self.procline("#{mod}:#{fun}(#{args})")
self.log.info("-> " + [:cast, mod, fun, args].inspect)
begin
self.dispatch(mod, fun, args)
rescue Object => e
# ignore
end
write_berp(output, t[:noreply])
else
self.procline("invalid request")
self.log.error("-> " + iruby.inspect)
oruby = t[:error, t[:server, 0, "Invalid request: #{iruby.inspect}"]]
self.log.error("<- " + oruby.inspect)
write_berp(output, oruby)
end
end
end
def self.procline(msg)
$0 = "ernie handler #{VERSION} (ruby) - #{self.virgin_procline} - [#{self.count}] #{msg}"[0..159]
end
def self.version
VERSION
end
end
class Ernie::ServerError < StandardError; end
class Ernie::Mod
attr_accessor :name, :funs
def initialize(name)
self.name = name
self.funs = {}
end
def fun(name, block)
raise TypeError, "block required" if block.nil?
self.funs[name] = block
end
end
# Root level calls
def logfile(name)
Ernie.logfile(name)
end
def loglevel(level)
Ernie.loglevel(level)
end
at_exit do
Ernie.start if Ernie.auto_start
end