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)
123 @db = DBTree.create_db(key)
125 elsif File.exist? relfilename
126 # db already exists, use it
127 @db = DBTree.open_db relfilename
128 @fname = relfilename.dup
131 @db = DBTree.create_db relfilename
132 @fname = relfilename.dup
134 oldbasename = (absfilename ? key : relfilename).gsub(/\.tdb$/, ".db")
135 if File.exists? oldbasename and defined? BDB
137 warning "Upgrading old database #{oldbasename}..."
138 oldb = ::BDB::CIBtree.open(oldbasename, nil, "r", 0600)
141 @db.putlist k, (oldb.duplicates(k, false))
144 File.rename oldbasename, oldbasename+".bak"
149 def method_missing(method, *args, &block)
150 return @db.send(method, *args, &block)
153 # Since TokyoCabinet does not have the concept of an environment, we have to do the
154 # database management ourselves. In particular, we have to keep a list of open
155 # registries to be sure we to close all of them on exit
157 def self.close_bot_registries
158 @@bot_registries.each { |name, reg| reg.close }
159 @@bot_registries.clear
163 db = @@bot_registries.delete(@fname)
165 error "We think we have #{@db} from #{@fname}, TC pseudo-env gives us #{db}"
170 def DBTree.create_db(name)
171 debug "DBTree: creating empty db #{name}"
172 if @@bot_registries.key? name
173 error "DBTree: creating assumingly allocated db #{name}?!"
174 return @@bot_registries[name]
176 db = TokyoCabinet::CIBDB.new
177 res = db.open(name, TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OCREAT | TokyoCabinet::CIBDB::OWRITER)
179 @@bot_registries[name] = db
181 error "DBTree: creating empty db #{name}: #{db.errmsg(db.ecode)}"
186 def DBTree.open_db(name)
187 debug "DBTree: opening existing db #{name}"
188 if @@bot_registries.key? name
189 return @@bot_registries[name]
191 db = TokyoCabinet::CIBDB.new
192 res = db.open(name, TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OWRITER)
194 @@bot_registries[name] = db
196 error "DBTree: opening db #{name}: #{db.errmsg(db.ecode)}"
201 def DBTree.cleanup_logs()
209 def DBTree.cleanup_env()
210 DBTree.close_bot_registries
220 # This class is now used purely for upgrading from prior versions of rbot
221 # the new registry is split into multiple DBHash objects, one per plugin
229 # check for older versions of rbot with data formats that require updating
230 # NB this function is called _early_ in init(), pretty much all you have to
231 # work with is @bot.botclass.
233 oldreg = @bot.path 'registry.db'
235 newreg = @bot.path 'plugin_registry.db'
236 if File.exist?(oldreg)
237 log _("upgrading old-style (rbot 0.9.5 or earlier) plugin registry to new format")
238 old = ::BDB::Hash.open(oldreg, nil, "r+", 0600)
239 new = ::BDB::CIBtree.open(newreg, nil, ::BDB::CREATE | ::BDB::EXCL, 0600)
245 File.rename(oldreg, oldreg + ".old")
248 warning "Won't upgrade data: BDB not installed" if File.exist? oldreg
253 oldreg = @bot.path 'plugin_registry.db'
255 warning "Won't upgrade data: BDB not installed" if File.exist? oldreg
258 newdir = @bot.path 'registry'
259 if File.exist?(oldreg)
260 Dir.mkdir(newdir) unless File.exist?(newdir)
261 env = BDB::Env.open(@bot.botclass, BDB::INIT_TRANSACTION | BDB::CREATE | BDB::RECOVER)# | BDB::TXN_NOSYNC)
263 log _("upgrading previous (rbot 0.9.9 or earlier) plugin registry to new split format")
264 old = BDB::CIBtree.open(oldreg, nil, "r+", 0600, "env" => env)
266 prefix,key = k.split("/", 2)
268 # subregistries were split with a +, now they are in separate folders
269 if prefix.gsub!(/\+/, "/")
270 # Ok, this code needs to be put in the db opening routines
271 dirs = File.dirname("#{@bot.botclass}/registry/#{prefix}.db").split("/")
272 dirs.length.times { |i|
273 dir = dirs[0,i+1].join("/")+"/"
274 unless File.exist?(dir)
275 log _("creating subregistry directory #{dir}")
280 unless dbs.has_key?(prefix)
281 log _("creating db #{@bot.botclass}/registry/#{prefix}.tdb")
282 dbs[prefix] = TokyoCabinet::CIBDB.open("#{@bot.botclass}/registry/#{prefix}.tdb",
283 TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OCREAT | TokyoCabinet::CIBDB::OWRITER)
288 File.rename(oldreg, oldreg + ".old")
290 log _("closing db #{k}")
297 # This class provides persistent storage for plugins via a hash interface.
298 # The default mode is an object store, so you can store ruby objects and
299 # reference them with hash keys. This is because the default store/restore
300 # methods of the plugins' RegistryAccessor are calls to Marshal.dump and
305 # @registry[:blah] = blah
306 # then, even after the bot is shut down and disconnected, on the next run you
307 # can access the blah object as it was, with:
308 # blah = @registry[:blah]
309 # The registry can of course be used to store simple strings, fixnums, etc as
310 # well, and should be useful to store or cache plugin data or dynamic plugin
314 # in object store mode, don't make the mistake of treating it like a live
315 # object, e.g. (using the example above)
316 # @registry[:blah][:foo] = "flump"
317 # will NOT modify the object in the registry - remember that Registry#[]
318 # returns a Marshal.restore'd object, the object you just modified in place
319 # will disappear. You would need to:
320 # blah = @registry[:blah]
321 # blah[:foo] = "flump"
322 # @registry[:blah] = blah
324 # If you don't need to store objects, and strictly want a persistant hash of
325 # strings, you can override the store/restore methods to suit your needs, for
326 # example (in your plugin):
337 # Your plugins section of the registry is private, it has its own namespace
338 # (derived from the plugin's class name, so change it and lose your data).
339 # Calls to registry.each etc, will only iterate over your namespace.
342 attr_accessor :recovery
344 # plugins don't call this - a Registry::Accessor is created for them and
345 # is accessible via @registry.
346 def initialize(bot, name)
348 @name = name.downcase
349 @filename = @bot.path 'registry', @name
350 dirs = File.dirname(@filename).split("/")
351 dirs.length.times { |i|
352 dir = dirs[0,i+1].join("/")+"/"
353 unless File.exist?(dir)
354 debug "creating subregistry directory #{dir}"
362 # debug "initializing registry accessor with name #{@name}"
366 @registry ||= DBTree.new @bot, "registry/#{@name}"
370 # debug "fushing registry #{registry}"
376 # debug "closing registry #{registry}"
381 # convert value to string form for storing in the registry
382 # defaults to Marshal.dump(val) but you can override this in your module's
383 # registry object to use any method you like.
384 # For example, if you always just handle strings use:
392 # restores object from string form, restore(store(val)) must return val.
393 # If you override store, you should override restore to reverse the
395 # For example, if you always just handle strings use:
402 rescue Exception => e
403 error _("failed to restore marshal data for #{val.inspect}, attempting recovery or fallback to default")
405 if defined? @recovery and @recovery
407 return @recovery.call(val)
408 rescue Exception => ee
409 error _("marshal recovery failed, trying default")
417 # lookup a key in the registry
419 if File.exist?(@filename) and registry.has_key?(key.to_s)
420 return restore(registry[key.to_s])
426 # set a key in the registry
428 registry[key.to_s] = store(value)
431 # set the default value for registry lookups, if the key sought is not
432 # found, the default will be returned. The default default (har) is nil.
433 def set_default (default)
438 @default && (@default.dup rescue @default)
441 # just like Hash#each
442 def each(set=nil, bulk=0, &block)
443 return nil unless File.exist?(@filename)
444 registry.fwmkeys(set.to_s).each {|key|
445 block.call(key, restore(registry[key]))
449 # just like Hash#each_key
450 def each_key(set=nil, bulk=0, &block)
451 return nil unless File.exist?(@filename)
452 registry.fwmkeys(set.to_s).each do |key|
457 # just like Hash#each_value
458 def each_value(set=nil, bulk=0, &block)
459 return nil unless File.exist?(@filename)
460 registry.fwmkeys(set.to_s).each do |key|
461 block.call(restore(registry[key]))
465 # just like Hash#has_key?
467 return false unless File.exist?(@filename)
468 return registry.has_key?(key.to_s)
471 alias include? has_key?
472 alias member? has_key?
475 # just like Hash#has_both?
476 def has_both?(key, value)
477 return false unless File.exist?(@filename)
478 registry.has_key?(key.to_s) and registry.has_value?(store(value))
481 # just like Hash#has_value?
482 def has_value?(value)
483 return false unless File.exist?(@filename)
484 return registry.has_value?(store(value))
487 # just like Hash#index?
490 return k if v == value
495 # delete a key from the registry
497 return default unless File.exist?(@filename)
498 return registry.delete(key.to_s)
501 # returns a list of your keys
503 return [] unless File.exist?(@filename)
507 # Return an array of all associations [key, value] in your namespace
509 return [] unless File.exist?(@filename)
511 registry.each {|key, value|
512 ret << [key, restore(value)]
517 # Return an hash of all associations {key => value} in your namespace
519 return {} unless File.exist?(@filename)
521 registry.each {|key, value|
522 ret[key] = restore(value)
527 # empties the registry (restricted to your namespace)
529 return true unless File.exist?(@filename)
534 # returns an array of the values in your namespace of the registry
536 return [] unless File.exist?(@filename)
544 def sub_registry(prefix)
545 return Accessor.new(@bot, @name + "/" + prefix.to_s)
548 # returns the number of keys in your registry namespace
550 return 0 unless File.exist?(@filename)
556 def putdup(key, value)
557 registry.putdup(key.to_s, store(value))
560 def putlist(key, values)
561 registry.putlist(key.to_s, value.map {|v| store(v)})
565 return [] unless File.exist?(@filename)
566 (registry.getlist(key.to_s) || []).map {|v| restore(v)}