8 if BDB::VERSION_MAJOR < 4
9 fatal "Your bdb (Berkeley DB) version #{BDB::VERSION} is too old!"
10 fatal "rbot will only run with bdb version 4 or higher, please upgrade."
11 fatal "For maximum reliability, upgrade to version 4.2 or higher."
12 raise BDB::Fatal, BDB::VERSION + " is too old"
15 if BDB::VERSION_MAJOR == 4 and BDB::VERSION_MINOR < 2
16 warning "Your bdb (Berkeley DB) version #{BDB::VERSION} may not be reliable."
17 warning "If possible, try upgrade version 4.2 or later."
20 warning "rbot couldn't load the bdb module. Old registries won't be upgraded"
22 warning "rbot couldn't load the bdb module: #{e.pretty_inspect}"
28 require 'tokyocabinet'
32 class DBFatal < Exception ; end
35 # DBHash is for tying a hash to disk (using bdb).
36 # Call it with an identifier, for example "mydata". It'll look for
37 # mydata.db, if it exists, it will load and reference that db.
38 # Otherwise it'll create and empty db called mydata.db
41 # absfilename:: use +key+ as an actual filename, don't prepend the bot's
42 # config path and don't append ".db"
43 def initialize(bot, key, absfilename=false)
46 relfilename = @bot.path key
48 if absfilename && File.exist?(key)
49 # db already exists, use it
50 @db = DBHash.open_db(key)
53 @db = DBHash.create_db(key)
54 elsif File.exist? relfilename
55 # db already exists, use it
56 @db = DBHash.open_db relfilename
59 @db = DBHash.create_db relfilename
63 def method_missing(method, *args, &block)
64 return @db.send(method, *args, &block)
67 def DBHash.create_db(name)
68 debug "DBHash: creating empty db #{name}"
69 return BDB::Hash.open(name, nil,
70 BDB::CREATE | BDB::EXCL, 0600)
73 def DBHash.open_db(name)
74 debug "DBHash: opening existing db #{name}"
75 return BDB::Hash.open(name, nil, "r+", 0600)
79 # make BTree lookups case insensitive
82 def bdb_bt_compare(a, b)
83 if a == nil || b == nil
84 warning "CIBTree: comparing #{a.inspect} (#{self[a].inspect}) with #{b.inspect} (#{self[b].inspect})"
86 (a||'').downcase <=> (b||'').downcase
93 class CIBDB < TokyoCabinet::BDB
94 # Since TokyoCabinet does not have the concept of an environment, we have to do the
95 # database management ourselves. In particular, we have to keep a list of open
96 # registries to be sure we to close all of them on exit
98 def self.close_bot_registries
99 @@bot_registries.each { |reg| reg.close }
102 def open(path, omode)
105 self.setcmpfunc(Proc.new do |a, b|
106 a.downcase <=> b.downcase
108 @@bot_registries << res
115 # DBTree is a BTree equivalent of DBHash, with case insensitive lookups.
117 # absfilename:: use +key+ as an actual filename, don't prepend the bot's
118 # config path and don't append ".db"
119 def initialize(bot, key, absfilename=false)
123 relfilename = @bot.path key
124 relfilename << '.tdb'
126 if absfilename && File.exist?(key)
127 # db already exists, use it
128 @db = DBTree.open_db(key)
131 @db = DBTree.create_db(key)
132 elsif File.exist? relfilename
133 # db already exists, use it
134 @db = DBTree.open_db relfilename
137 @db = DBTree.create_db relfilename
139 oldbasename = (absfilename ? key : relfilename).gsub(/\.tdb$/, ".db")
140 if File.exists? oldbasename and defined? BDB
142 warning "Upgrading old database #{oldbasename}..."
143 oldb = ::BDB::CIBtree.open(oldbasename, nil, "r", 0600)
146 @db.putlist k, (oldb.duplicates(k, false))
149 File.rename oldbasename, oldbasename+".bak"
154 def method_missing(method, *args, &block)
155 return @db.send(method, *args, &block)
158 def DBTree.create_db(name)
159 debug "DBTree: creating empty db #{name}"
160 db = TokyoCabinet::CIBDB.new
161 res = db.open(name, TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OCREAT | TokyoCabinet::CIBDB::OWRITER)
162 warning "DBTree: creating empty db #{name}: #{db.errmsg(db.ecode) unless res}"
166 def DBTree.open_db(name)
167 debug "DBTree: opening existing db #{name}"
168 db = TokyoCabinet::CIBDB.new
169 res = db.open(name, TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OWRITER)
170 warning "DBTree:opening db #{name}: #{db.errmsg(db.ecode) unless res}"
174 def DBTree.cleanup_logs()
182 def DBTree.cleanup_env()
183 CIBDB.close_bot_registries
193 # This class is now used purely for upgrading from prior versions of rbot
194 # the new registry is split into multiple DBHash objects, one per plugin
202 # check for older versions of rbot with data formats that require updating
203 # NB this function is called _early_ in init(), pretty much all you have to
204 # work with is @bot.botclass.
206 oldreg = @bot.path 'registry.db'
208 newreg = @bot.path 'plugin_registry.db'
209 if File.exist?(oldreg)
210 log _("upgrading old-style (rbot 0.9.5 or earlier) plugin registry to new format")
211 old = ::BDB::Hash.open(oldreg, nil, "r+", 0600)
212 new = ::BDB::CIBtree.open(newreg, nil, ::BDB::CREATE | ::BDB::EXCL, 0600)
218 File.rename(oldreg, oldreg + ".old")
221 warning "Won't upgrade data: BDB not installed" if File.exist? oldreg
226 oldreg = @bot.path 'plugin_registry.db'
228 warning "Won't upgrade data: BDB not installed" if File.exist? oldreg
231 newdir = @bot.path 'registry'
232 if File.exist?(oldreg)
233 Dir.mkdir(newdir) unless File.exist?(newdir)
234 env = BDB::Env.open(@bot.botclass, BDB::INIT_TRANSACTION | BDB::CREATE | BDB::RECOVER)# | BDB::TXN_NOSYNC)
236 log _("upgrading previous (rbot 0.9.9 or earlier) plugin registry to new split format")
237 old = BDB::CIBtree.open(oldreg, nil, "r+", 0600, "env" => env)
239 prefix,key = k.split("/", 2)
241 # subregistries were split with a +, now they are in separate folders
242 if prefix.gsub!(/\+/, "/")
243 # Ok, this code needs to be put in the db opening routines
244 dirs = File.dirname("#{@bot.botclass}/registry/#{prefix}.db").split("/")
245 dirs.length.times { |i|
246 dir = dirs[0,i+1].join("/")+"/"
247 unless File.exist?(dir)
248 log _("creating subregistry directory #{dir}")
253 unless dbs.has_key?(prefix)
254 log _("creating db #{@bot.botclass}/registry/#{prefix}.tdb")
255 dbs[prefix] = TokyoCabinet::CIBDB.open("#{@bot.botclass}/registry/#{prefix}.tdb",
256 TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OCREAT | TokyoCabinet::CIBDB::OWRITER)
261 File.rename(oldreg, oldreg + ".old")
263 log _("closing db #{k}")
270 # This class provides persistent storage for plugins via a hash interface.
271 # The default mode is an object store, so you can store ruby objects and
272 # reference them with hash keys. This is because the default store/restore
273 # methods of the plugins' RegistryAccessor are calls to Marshal.dump and
278 # @registry[:blah] = blah
279 # then, even after the bot is shut down and disconnected, on the next run you
280 # can access the blah object as it was, with:
281 # blah = @registry[:blah]
282 # The registry can of course be used to store simple strings, fixnums, etc as
283 # well, and should be useful to store or cache plugin data or dynamic plugin
287 # in object store mode, don't make the mistake of treating it like a live
288 # object, e.g. (using the example above)
289 # @registry[:blah][:foo] = "flump"
290 # will NOT modify the object in the registry - remember that Registry#[]
291 # returns a Marshal.restore'd object, the object you just modified in place
292 # will disappear. You would need to:
293 # blah = @registry[:blah]
294 # blah[:foo] = "flump"
295 # @registry[:blah] = blah
297 # If you don't need to store objects, and strictly want a persistant hash of
298 # strings, you can override the store/restore methods to suit your needs, for
299 # example (in your plugin):
310 # Your plugins section of the registry is private, it has its own namespace
311 # (derived from the plugin's class name, so change it and lose your data).
312 # Calls to registry.each etc, will only iterate over your namespace.
315 attr_accessor :recovery
317 # plugins don't call this - a Registry::Accessor is created for them and
318 # is accessible via @registry.
319 def initialize(bot, name)
321 @name = name.downcase
322 @filename = @bot.path 'registry', @name
323 dirs = File.dirname(@filename).split("/")
324 dirs.length.times { |i|
325 dir = dirs[0,i+1].join("/")+"/"
326 unless File.exist?(dir)
327 debug "creating subregistry directory #{dir}"
335 # debug "initializing registry accessor with name #{@name}"
339 @registry ||= DBTree.new @bot, "registry/#{@name}"
343 # debug "fushing registry #{registry}"
349 # debug "closing registry #{registry}"
354 # convert value to string form for storing in the registry
355 # defaults to Marshal.dump(val) but you can override this in your module's
356 # registry object to use any method you like.
357 # For example, if you always just handle strings use:
365 # restores object from string form, restore(store(val)) must return val.
366 # If you override store, you should override restore to reverse the
368 # For example, if you always just handle strings use:
375 rescue Exception => e
376 error _("failed to restore marshal data for #{val.inspect}, attempting recovery or fallback to default")
378 if defined? @recovery and @recovery
380 return @recovery.call(val)
381 rescue Exception => ee
382 error _("marshal recovery failed, trying default")
390 # lookup a key in the registry
392 if File.exist?(@filename) and registry.has_key?(key.to_s)
393 return restore(registry[key.to_s])
399 # set a key in the registry
401 registry[key.to_s] = store(value)
404 # set the default value for registry lookups, if the key sought is not
405 # found, the default will be returned. The default default (har) is nil.
406 def set_default (default)
411 @default && (@default.dup rescue @default)
414 # just like Hash#each
415 def each(set=nil, bulk=0, &block)
416 return nil unless File.exist?(@filename)
417 registry.fwmkeys(set.to_s).each {|key|
418 block.call(key, restore(registry[key]))
422 # just like Hash#each_key
423 def each_key(set=nil, bulk=0, &block)
424 return nil unless File.exist?(@filename)
425 registry.fwmkeys(set.to_s).each do |key|
430 # just like Hash#each_value
431 def each_value(set=nil, bulk=0, &block)
432 return nil unless File.exist?(@filename)
433 registry.fwmkeys(set.to_s).each do |key|
434 block.call(restore(registry[key]))
438 # just like Hash#has_key?
440 return false unless File.exist?(@filename)
441 return registry.has_key?(key.to_s)
444 alias include? has_key?
445 alias member? has_key?
448 # just like Hash#has_both?
449 def has_both?(key, value)
450 return false unless File.exist?(@filename)
451 registry.has_key?(key.to_s) and registry.has_value?(store(value))
454 # just like Hash#has_value?
455 def has_value?(value)
456 return false unless File.exist?(@filename)
457 return registry.has_value?(store(value))
460 # just like Hash#index?
463 return k if v == value
468 # delete a key from the registry
470 return default unless File.exist?(@filename)
471 return registry.delete(key.to_s)
474 # returns a list of your keys
476 return [] unless File.exist?(@filename)
480 # Return an array of all associations [key, value] in your namespace
482 return [] unless File.exist?(@filename)
484 registry.each {|key, value|
485 ret << [key, restore(value)]
490 # Return an hash of all associations {key => value} in your namespace
492 return {} unless File.exist?(@filename)
494 registry.each {|key, value|
495 ret[key] = restore(value)
500 # empties the registry (restricted to your namespace)
502 return true unless File.exist?(@filename)
507 # returns an array of the values in your namespace of the registry
509 return [] unless File.exist?(@filename)
517 def sub_registry(prefix)
518 return Accessor.new(@bot, @name + "/" + prefix.to_s)
521 # returns the number of keys in your registry namespace
523 return 0 unless File.exist?(@filename)
529 def putdup(key, value)
530 registry.putdup(key.to_s, store(value))
533 def putlist(key, values)
534 registry.putlist(key.to_s, value.map {|v| store(v)})
538 return [] unless File.exist?(@filename)
539 (registry.getlist(key.to_s) || []).map {|v| restore(v)}