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
97 self.setcmpfunc(Proc.new do |a, b|
98 a.downcase <=> b.downcase
106 # DBTree is a BTree equivalent of DBHash, with case insensitive lookups.
108 # absfilename:: use +key+ as an actual filename, don't prepend the bot's
109 # config path and don't append ".db"
110 def initialize(bot, key, absfilename=false)
114 relfilename = @bot.path key
115 relfilename << '.tdb'
117 if absfilename && File.exist?(key)
118 # db already exists, use it
119 @db = DBTree.open_db(key)
122 @db = DBTree.create_db(key)
123 elsif File.exist? relfilename
124 # db already exists, use it
125 @db = DBTree.open_db relfilename
128 @db = DBTree.create_db relfilename
130 oldbasename = (absfilename ? key : relfilename).gsub(/\.tdb$/, ".db")
131 if File.exists? oldbasename and defined? BDB
133 warning "Upgrading old database #{oldbasename}..."
134 oldb = ::BDB::CIBtree.open(oldbasename, nil, "r", 0600)
137 @db.putlist k, (oldb.duplicates(k, false))
140 File.rename oldbasename, oldbasename+".bak"
145 def method_missing(method, *args, &block)
146 return @db.send(method, *args, &block)
149 # Since TokyoCabinet does not have the concept of an environment, we have to do the
150 # database management ourselves. In particular, we have to keep a list of open
151 # registries to be sure we to close all of them on exit
153 def self.close_bot_registries
154 @@bot_registries.each { |name, reg| reg.close }
155 @@bot_registries.clear
158 def DBTree.create_db(name)
159 debug "DBTree: creating empty db #{name}"
160 if @@bot_registries.key? name
161 error "DBTree: creating assumingly allocated db #{name}?!"
162 return @@bot_registries[name]
164 db = TokyoCabinet::CIBDB.new
165 res = db.open(name, TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OCREAT | TokyoCabinet::CIBDB::OWRITER)
167 @@bot_registries[name] = db
169 error "DBTree: creating empty db #{name}: #{db.errmsg(db.ecode)}"
174 def DBTree.open_db(name)
175 debug "DBTree: opening existing db #{name}"
176 if @@bot_registries.key? name
177 return @@bot_registries[name]
179 db = TokyoCabinet::CIBDB.new
180 res = db.open(name, TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OWRITER)
182 @@bot_registries[name] = db
184 error "DBTree: opening db #{name}: #{db.errmsg(db.ecode)}"
189 def DBTree.cleanup_logs()
197 def DBTree.cleanup_env()
198 DBTree.close_bot_registries
208 # This class is now used purely for upgrading from prior versions of rbot
209 # the new registry is split into multiple DBHash objects, one per plugin
217 # check for older versions of rbot with data formats that require updating
218 # NB this function is called _early_ in init(), pretty much all you have to
219 # work with is @bot.botclass.
221 oldreg = @bot.path 'registry.db'
223 newreg = @bot.path 'plugin_registry.db'
224 if File.exist?(oldreg)
225 log _("upgrading old-style (rbot 0.9.5 or earlier) plugin registry to new format")
226 old = ::BDB::Hash.open(oldreg, nil, "r+", 0600)
227 new = ::BDB::CIBtree.open(newreg, nil, ::BDB::CREATE | ::BDB::EXCL, 0600)
233 File.rename(oldreg, oldreg + ".old")
236 warning "Won't upgrade data: BDB not installed" if File.exist? oldreg
241 oldreg = @bot.path 'plugin_registry.db'
243 warning "Won't upgrade data: BDB not installed" if File.exist? oldreg
246 newdir = @bot.path 'registry'
247 if File.exist?(oldreg)
248 Dir.mkdir(newdir) unless File.exist?(newdir)
249 env = BDB::Env.open(@bot.botclass, BDB::INIT_TRANSACTION | BDB::CREATE | BDB::RECOVER)# | BDB::TXN_NOSYNC)
251 log _("upgrading previous (rbot 0.9.9 or earlier) plugin registry to new split format")
252 old = BDB::CIBtree.open(oldreg, nil, "r+", 0600, "env" => env)
254 prefix,key = k.split("/", 2)
256 # subregistries were split with a +, now they are in separate folders
257 if prefix.gsub!(/\+/, "/")
258 # Ok, this code needs to be put in the db opening routines
259 dirs = File.dirname("#{@bot.botclass}/registry/#{prefix}.db").split("/")
260 dirs.length.times { |i|
261 dir = dirs[0,i+1].join("/")+"/"
262 unless File.exist?(dir)
263 log _("creating subregistry directory #{dir}")
268 unless dbs.has_key?(prefix)
269 log _("creating db #{@bot.botclass}/registry/#{prefix}.tdb")
270 dbs[prefix] = TokyoCabinet::CIBDB.open("#{@bot.botclass}/registry/#{prefix}.tdb",
271 TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OCREAT | TokyoCabinet::CIBDB::OWRITER)
276 File.rename(oldreg, oldreg + ".old")
278 log _("closing db #{k}")
285 # This class provides persistent storage for plugins via a hash interface.
286 # The default mode is an object store, so you can store ruby objects and
287 # reference them with hash keys. This is because the default store/restore
288 # methods of the plugins' RegistryAccessor are calls to Marshal.dump and
293 # @registry[:blah] = blah
294 # then, even after the bot is shut down and disconnected, on the next run you
295 # can access the blah object as it was, with:
296 # blah = @registry[:blah]
297 # The registry can of course be used to store simple strings, fixnums, etc as
298 # well, and should be useful to store or cache plugin data or dynamic plugin
302 # in object store mode, don't make the mistake of treating it like a live
303 # object, e.g. (using the example above)
304 # @registry[:blah][:foo] = "flump"
305 # will NOT modify the object in the registry - remember that Registry#[]
306 # returns a Marshal.restore'd object, the object you just modified in place
307 # will disappear. You would need to:
308 # blah = @registry[:blah]
309 # blah[:foo] = "flump"
310 # @registry[:blah] = blah
312 # If you don't need to store objects, and strictly want a persistant hash of
313 # strings, you can override the store/restore methods to suit your needs, for
314 # example (in your plugin):
325 # Your plugins section of the registry is private, it has its own namespace
326 # (derived from the plugin's class name, so change it and lose your data).
327 # Calls to registry.each etc, will only iterate over your namespace.
330 attr_accessor :recovery
332 # plugins don't call this - a Registry::Accessor is created for them and
333 # is accessible via @registry.
334 def initialize(bot, name)
336 @name = name.downcase
337 @filename = @bot.path 'registry', @name
338 dirs = File.dirname(@filename).split("/")
339 dirs.length.times { |i|
340 dir = dirs[0,i+1].join("/")+"/"
341 unless File.exist?(dir)
342 debug "creating subregistry directory #{dir}"
350 # debug "initializing registry accessor with name #{@name}"
354 @registry ||= DBTree.new @bot, "registry/#{@name}"
358 # debug "fushing registry #{registry}"
364 # debug "closing registry #{registry}"
369 # convert value to string form for storing in the registry
370 # defaults to Marshal.dump(val) but you can override this in your module's
371 # registry object to use any method you like.
372 # For example, if you always just handle strings use:
380 # restores object from string form, restore(store(val)) must return val.
381 # If you override store, you should override restore to reverse the
383 # For example, if you always just handle strings use:
390 rescue Exception => e
391 error _("failed to restore marshal data for #{val.inspect}, attempting recovery or fallback to default")
393 if defined? @recovery and @recovery
395 return @recovery.call(val)
396 rescue Exception => ee
397 error _("marshal recovery failed, trying default")
405 # lookup a key in the registry
407 if File.exist?(@filename) and registry.has_key?(key.to_s)
408 return restore(registry[key.to_s])
414 # set a key in the registry
416 registry[key.to_s] = store(value)
419 # set the default value for registry lookups, if the key sought is not
420 # found, the default will be returned. The default default (har) is nil.
421 def set_default (default)
426 @default && (@default.dup rescue @default)
429 # just like Hash#each
430 def each(set=nil, bulk=0, &block)
431 return nil unless File.exist?(@filename)
432 registry.fwmkeys(set.to_s).each {|key|
433 block.call(key, restore(registry[key]))
437 # just like Hash#each_key
438 def each_key(set=nil, bulk=0, &block)
439 return nil unless File.exist?(@filename)
440 registry.fwmkeys(set.to_s).each do |key|
445 # just like Hash#each_value
446 def each_value(set=nil, bulk=0, &block)
447 return nil unless File.exist?(@filename)
448 registry.fwmkeys(set.to_s).each do |key|
449 block.call(restore(registry[key]))
453 # just like Hash#has_key?
455 return false unless File.exist?(@filename)
456 return registry.has_key?(key.to_s)
459 alias include? has_key?
460 alias member? has_key?
463 # just like Hash#has_both?
464 def has_both?(key, value)
465 return false unless File.exist?(@filename)
466 registry.has_key?(key.to_s) and registry.has_value?(store(value))
469 # just like Hash#has_value?
470 def has_value?(value)
471 return false unless File.exist?(@filename)
472 return registry.has_value?(store(value))
475 # just like Hash#index?
478 return k if v == value
483 # delete a key from the registry
485 return default unless File.exist?(@filename)
486 return registry.delete(key.to_s)
489 # returns a list of your keys
491 return [] unless File.exist?(@filename)
495 # Return an array of all associations [key, value] in your namespace
497 return [] unless File.exist?(@filename)
499 registry.each {|key, value|
500 ret << [key, restore(value)]
505 # Return an hash of all associations {key => value} in your namespace
507 return {} unless File.exist?(@filename)
509 registry.each {|key, value|
510 ret[key] = restore(value)
515 # empties the registry (restricted to your namespace)
517 return true unless File.exist?(@filename)
522 # returns an array of the values in your namespace of the registry
524 return [] unless File.exist?(@filename)
532 def sub_registry(prefix)
533 return Accessor.new(@bot, @name + "/" + prefix.to_s)
536 # returns the number of keys in your registry namespace
538 return 0 unless File.exist?(@filename)
544 def putdup(key, value)
545 registry.putdup(key.to_s, store(value))
548 def putlist(key, values)
549 registry.putlist(key.to_s, value.map {|v| store(v)})
553 return [] unless File.exist?(@filename)
554 (registry.getlist(key.to_s) || []).map {|v| restore(v)}