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 def DBTree.create_db(name)
150 debug "DBTree: creating empty db #{name}"
151 db = TokyoCabinet::CIBDB.new
152 res = db.open(name, TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OCREAT | TokyoCabinet::CIBDB::OWRITER)
153 warning "DBTree: creating empty db #{name}: #{db.errmsg(db.ecode) unless res}"
157 def DBTree.open_db(name)
158 debug "DBTree: opening existing db #{name}"
159 db = TokyoCabinet::CIBDB.new
160 res = db.open(name, TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OWRITER)
161 warning "DBTree:opening db #{name}: #{db.errmsg(db.ecode) unless res}"
165 def DBTree.cleanup_logs()
173 def DBTree.cleanup_env()
184 # This class is now used purely for upgrading from prior versions of rbot
185 # the new registry is split into multiple DBHash objects, one per plugin
193 # check for older versions of rbot with data formats that require updating
194 # NB this function is called _early_ in init(), pretty much all you have to
195 # work with is @bot.botclass.
197 oldreg = @bot.path 'registry.db'
199 newreg = @bot.path 'plugin_registry.db'
200 if File.exist?(oldreg)
201 log _("upgrading old-style (rbot 0.9.5 or earlier) plugin registry to new format")
202 old = ::BDB::Hash.open(oldreg, nil, "r+", 0600)
203 new = ::BDB::CIBtree.open(newreg, nil, ::BDB::CREATE | ::BDB::EXCL, 0600)
209 File.rename(oldreg, oldreg + ".old")
212 warning "Won't upgrade data: BDB not installed" if File.exist? oldreg
217 oldreg = @bot.path 'plugin_registry.db'
219 warning "Won't upgrade data: BDB not installed" if File.exist? oldreg
222 newdir = @bot.path 'registry'
223 if File.exist?(oldreg)
224 Dir.mkdir(newdir) unless File.exist?(newdir)
225 env = BDB::Env.open(@bot.botclass, BDB::INIT_TRANSACTION | BDB::CREATE | BDB::RECOVER)# | BDB::TXN_NOSYNC)
227 log _("upgrading previous (rbot 0.9.9 or earlier) plugin registry to new split format")
228 old = BDB::CIBtree.open(oldreg, nil, "r+", 0600, "env" => env)
230 prefix,key = k.split("/", 2)
232 # subregistries were split with a +, now they are in separate folders
233 if prefix.gsub!(/\+/, "/")
234 # Ok, this code needs to be put in the db opening routines
235 dirs = File.dirname("#{@bot.botclass}/registry/#{prefix}.db").split("/")
236 dirs.length.times { |i|
237 dir = dirs[0,i+1].join("/")+"/"
238 unless File.exist?(dir)
239 log _("creating subregistry directory #{dir}")
244 unless dbs.has_key?(prefix)
245 log _("creating db #{@bot.botclass}/registry/#{prefix}.tdb")
246 dbs[prefix] = TokyoCabinet::CIBDB.open("#{@bot.botclass}/registry/#{prefix}.tdb",
247 TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OCREAT | TokyoCabinet::CIBDB::OWRITER)
252 File.rename(oldreg, oldreg + ".old")
254 log _("closing db #{k}")
261 # This class provides persistent storage for plugins via a hash interface.
262 # The default mode is an object store, so you can store ruby objects and
263 # reference them with hash keys. This is because the default store/restore
264 # methods of the plugins' RegistryAccessor are calls to Marshal.dump and
269 # @registry[:blah] = blah
270 # then, even after the bot is shut down and disconnected, on the next run you
271 # can access the blah object as it was, with:
272 # blah = @registry[:blah]
273 # The registry can of course be used to store simple strings, fixnums, etc as
274 # well, and should be useful to store or cache plugin data or dynamic plugin
278 # in object store mode, don't make the mistake of treating it like a live
279 # object, e.g. (using the example above)
280 # @registry[:blah][:foo] = "flump"
281 # will NOT modify the object in the registry - remember that Registry#[]
282 # returns a Marshal.restore'd object, the object you just modified in place
283 # will disappear. You would need to:
284 # blah = @registry[:blah]
285 # blah[:foo] = "flump"
286 # @registry[:blah] = blah
288 # If you don't need to store objects, and strictly want a persistant hash of
289 # strings, you can override the store/restore methods to suit your needs, for
290 # example (in your plugin):
301 # Your plugins section of the registry is private, it has its own namespace
302 # (derived from the plugin's class name, so change it and lose your data).
303 # Calls to registry.each etc, will only iterate over your namespace.
306 attr_accessor :recovery
308 # plugins don't call this - a Registry::Accessor is created for them and
309 # is accessible via @registry.
310 def initialize(bot, name)
312 @name = name.downcase
313 @filename = @bot.path 'registry', @name
314 dirs = File.dirname(@filename).split("/")
315 dirs.length.times { |i|
316 dir = dirs[0,i+1].join("/")+"/"
317 unless File.exist?(dir)
318 debug "creating subregistry directory #{dir}"
326 # debug "initializing registry accessor with name #{@name}"
330 @registry ||= DBTree.new @bot, "registry/#{@name}"
334 # debug "fushing registry #{registry}"
340 # debug "closing registry #{registry}"
345 # convert value to string form for storing in the registry
346 # defaults to Marshal.dump(val) but you can override this in your module's
347 # registry object to use any method you like.
348 # For example, if you always just handle strings use:
356 # restores object from string form, restore(store(val)) must return val.
357 # If you override store, you should override restore to reverse the
359 # For example, if you always just handle strings use:
366 rescue Exception => e
367 error _("failed to restore marshal data for #{val.inspect}, attempting recovery or fallback to default")
369 if defined? @recovery and @recovery
371 return @recovery.call(val)
372 rescue Exception => ee
373 error _("marshal recovery failed, trying default")
381 # lookup a key in the registry
383 if File.exist?(@filename) and registry.has_key?(key.to_s)
384 return restore(registry[key.to_s])
390 # set a key in the registry
392 registry[key.to_s] = store(value)
395 # set the default value for registry lookups, if the key sought is not
396 # found, the default will be returned. The default default (har) is nil.
397 def set_default (default)
402 @default && (@default.dup rescue @default)
405 # just like Hash#each
406 def each(set=nil, bulk=0, &block)
407 return nil unless File.exist?(@filename)
408 registry.fwmkeys(set).each {|key|
409 block.call(key, restore(registry[key]))
413 # just like Hash#each_key
414 def each_key(set=nil, bulk=0, &block)
415 return nil unless File.exist?(@filename)
416 registry.fwmkeys(set).each do |key|
421 # just like Hash#each_value
422 def each_value(set=nil, bulk=0, &block)
423 return nil unless File.exist?(@filename)
424 registry.fwmkeys(set).each do |key|
425 block.call(restore(registry[key]))
429 # just like Hash#has_key?
431 return false unless File.exist?(@filename)
432 return registry.has_key?(key.to_s)
435 alias include? has_key?
436 alias member? has_key?
439 # just like Hash#has_both?
440 def has_both?(key, value)
441 return false unless File.exist?(@filename)
442 registry.has_key?(key.to_s) and registry.has_value?(store(value))
445 # just like Hash#has_value?
446 def has_value?(value)
447 return false unless File.exist?(@filename)
448 return registry.has_value?(store(value))
451 # just like Hash#index?
454 return k if v == value
459 # delete a key from the registry
461 return default unless File.exist?(@filename)
462 return registry.delete(key.to_s)
465 # returns a list of your keys
467 return [] unless File.exist?(@filename)
471 # Return an array of all associations [key, value] in your namespace
473 return [] unless File.exist?(@filename)
475 registry.each {|key, value|
476 ret << [key, restore(value)]
481 # Return an hash of all associations {key => value} in your namespace
483 return {} unless File.exist?(@filename)
485 registry.each {|key, value|
486 ret[key] = restore(value)
491 # empties the registry (restricted to your namespace)
493 return true unless File.exist?(@filename)
498 # returns an array of the values in your namespace of the registry
500 return [] unless File.exist?(@filename)
508 def sub_registry(prefix)
509 return Accessor.new(@bot, @name + "/" + prefix.to_s)
512 # returns the number of keys in your registry namespace
514 return 0 unless File.exist?(@filename)
520 def putdup(key, value)
521 registry.putdup(key.to_s, store(value))
524 def putlist(key, values)
525 registry.putlist(key.to_s, value.map {|v| store(v)})
529 return [] unless File.exist?(@filename)
530 (registry.getlist(key.to_s) || []).map {|v| restore(v)}