-require 'rbot/dbhash'
+#-- vim:sw=2:et
+#++
+#
+# :title: Registry: Persistent storage interface and factory
+#
+# This class provides persistent storage for plugins via a hash interface.
+# The default mode is an object store, so you can store ruby objects and
+# reference them with hash keys. This is because the default store/restore
+# methods of the plugins' RegistryAccessor are calls to Marshal.dump and
+# Marshal.restore,
+# for example:
+# blah = Hash.new
+# blah[:foo] = "fum"
+# @registry[:blah] = blah
+# then, even after the bot is shut down and disconnected, on the next run you
+# can access the blah object as it was, with:
+# blah = @registry[:blah]
+# The registry can of course be used to store simple strings, fixnums, etc as
+# well, and should be useful to store or cache plugin data or dynamic plugin
+# configuration.
+#
+# WARNING:
+# in object store mode, don't make the mistake of treating it like a live
+# object, e.g. (using the example above)
+# @registry[:blah][:foo] = "flump"
+# will NOT modify the object in the registry - remember that Registry#[]
+# returns a Marshal.restore'd object, the object you just modified in place
+# will disappear. You would need to:
+# blah = @registry[:blah]
+# blah[:foo] = "flump"
+# @registry[:blah] = blah
+#
+# If you don't need to store objects, and strictly want a persistant hash of
+# strings, you can override the store/restore methods to suit your needs, for
+# example (in your plugin):
+# def initialize
+# class << @registry
+# def store(val)
+# val
+# end
+# def restore(val)
+# val
+# end
+# end
+# end
+# Your plugins section of the registry is private, it has its own namespace
+# (derived from the plugin's class name, so change it and lose your data).
+# Calls to registry.each etc, will only iterate over your namespace.
module Irc
+class Bot
- # this class is now used purely for upgrading from prior versions of rbot
- # the new registry is split into multiple DBHash objects, one per plugin
- class BotRegistry
- def initialize(bot)
- @bot = bot
- upgrade_data
- upgrade_data2
- end
+class Registry
- # check for older versions of rbot with data formats that require updating
- # NB this function is called _early_ in init(), pretty much all you have to
- # work with is @bot.botclass.
- def upgrade_data
- if File.exist?("#{@bot.botclass}/registry.db")
- log "upgrading old-style (rbot 0.9.5 or earlier) plugin registry to new format"
- old = BDB::Hash.open("#{@bot.botclass}/registry.db", nil,
- "r+", 0600)
- new = BDB::CIBtree.open("#{@bot.botclass}/plugin_registry.db", nil,
- BDB::CREATE | BDB::EXCL,
- 0600)
- old.each {|k,v|
- new[k] = v
- }
- old.close
- new.close
- File.delete("#{@bot.botclass}/registry.db")
- end
+ # Dynamically loads the specified registry type library.
+ def initialize(format=nil)
+ @libpath = File.join(File.dirname(__FILE__), 'registry')
+ @format = format
+ load File.join(@libpath, @format+'.rb') if format
+ end
+
+ # Returns a list of supported registry database formats.
+ def discover
+ Dir.glob(File.join(@libpath, '*.rb')).map do |name|
+ File.basename(name, File.extname(name))
end
-
- def upgrade_data2
- if File.exist?("#{@bot.botclass}/plugin_registry.db")
- Dir.mkdir("#{@bot.botclass}/registry") unless File.exist?("#{@bot.botclass}/registry")
- env = BDB::Env.open("#{@bot.botclass}", BDB::INIT_TRANSACTION | BDB::CREATE | BDB::RECOVER)
- dbs = Hash.new
- log "upgrading previous (rbot 0.9.9 or earlier) plugin registry to new split format"
- old = BDB::CIBtree.open("#{@bot.botclass}/plugin_registry.db", nil,
- "r+", 0600, "env" => env)
- old.each {|k,v|
- prefix,key = k.split("/", 2)
- prefix.downcase!
- unless dbs.has_key?(prefix)
- log "creating db #{@bot.botclass}/registry/#{prefix}.db"
- dbs[prefix] = BDB::CIBtree.open("#{@bot.botclass}/registry/#{prefix}.db",
- nil, BDB::CREATE | BDB::EXCL,
- 0600, "env" => env)
-
- end
- dbs[prefix][key] = v
- }
- old.close
- File.rename("#{@bot.botclass}/plugin_registry.db", "#{@bot.botclass}/plugin_registry.db.old")
- dbs.each {|k,v|
- log "closing db #{k}"
- v.close
- }
- env.close
- end
+ end
+
+ # Creates a new Accessor object for the specified database filename.
+ def create(path, filename)
+ # The get_impl method will return a list of all the classes that
+ # implement the accessor interface, since we only ever load one
+ # (the configured one) accessor implementation, we can just assume
+ # it to be the correct accessor to use.
+ cls = AbstractAccessor.get_impl.first
+ db = cls.new(File.join(path, 'registry_' + @format, filename.downcase))
+ db.optimize
+ db
+ end
+
+ # Helper method that will return a list of supported registry formats.
+ def self.formats
+ @@formats ||= Registry.new.discover
+ end
+
+ # Will detect tokyocabinet registry location: ~/.rbot/registry/*.tdb
+ # and move it to its new location ~/.rbot/registry_tc/*.tdb
+ def migrate_registry_folder(path)
+ old_name = File.join(path, 'registry')
+ new_name = File.join(path, 'registry_tc')
+ if @format == 'tc' and File.exists?(old_name) and
+ not File.exists?(new_name) and
+ not Dir.glob(File.join(old_name, '*.tdb')).empty?
+ File.rename(old_name, new_name)
end
end
-
-
- # This class provides persistent storage for plugins via a hash interface.
- # The default mode is an object store, so you can store ruby objects and
- # reference them with hash keys. This is because the default store/restore
- # methods of the plugins' RegistryAccessor are calls to Marshal.dump and
- # Marshal.restore,
- # for example:
- # blah = Hash.new
- # blah[:foo] = "fum"
- # @registry[:blah] = blah
- # then, even after the bot is shut down and disconnected, on the next run you
- # can access the blah object as it was, with:
- # blah = @registry[:blah]
- # The registry can of course be used to store simple strings, fixnums, etc as
- # well, and should be useful to store or cache plugin data or dynamic plugin
- # configuration.
- #
- # WARNING:
- # in object store mode, don't make the mistake of treating it like a live
- # object, e.g. (using the example above)
- # @registry[:blah][:foo] = "flump"
- # will NOT modify the object in the registry - remember that BotRegistry#[]
- # returns a Marshal.restore'd object, the object you just modified in place
- # will disappear. You would need to:
- # blah = @registry[:blah]
- # blah[:foo] = "flump"
- # @registry[:blah] = blah
-
- # If you don't need to store objects, and strictly want a persistant hash of
- # strings, you can override the store/restore methods to suit your needs, for
- # example (in your plugin):
- # def initialize
- # class << @registry
- # def store(val)
- # val
- # end
- # def restore(val)
- # val
- # end
- # end
- # end
- # Your plugins section of the registry is private, it has its own namespace
- # (derived from the plugin's class name, so change it and lose your data).
- # Calls to registry.each etc, will only iterate over your namespace.
- class BotRegistryAccessor
- # plugins don't call this - a BotRegistryAccessor is created for them and
- # is accessible via @registry.
- def initialize(bot, name)
- @bot = bot
- @name = name.downcase
- @registry = DBTree.new bot, "registry/#{@name}"
+
+ # Abstract database accessor (a hash-like interface).
+ class AbstractAccessor
+
+ # lets the user define a recovery procedure in case the Marshal
+ # deserialization fails, it might be manually recover data.
+ # NOTE: weird legacy stuff, used by markov plugin (WTH?)
+ attr_accessor :recovery
+
+ def initialize(filename)
+ debug 'init registry accessor for: ' + filename
+ @filename = filename
+ @name = File.basename filename
+ @registry = nil
@default = nil
- # debug "initializing registry accessor with name #{@name}"
+ @recovery = nil
end
- def flush
- @registry.flush
- @registry.sync
+ def sub_registry(prefix)
+ path = File.join(@filename.gsub(/\.[^\/\.]+$/,''), prefix.to_s)
+ return self.class.new(path)
end
- def close
- @registry.close
+ # creates the registry / subregistry folders
+ def create_folders
+ debug 'create folders for: ' + @filename
+ dirs = File.dirname(@filename).split("/")
+ dirs.length.times { |i|
+ dir = dirs[0,i+1].join("/")+"/"
+ unless File.exist?(dir)
+ Dir.mkdir(dir)
+ end
+ }
+ end
+
+ # Will return true if the database file exists.
+ def dbexists?
+ File.exists? @filename
end
# convert value to string form for storing in the registry
begin
Marshal.restore(val)
rescue Exception => e
- warning "failed to restore marshal data for #{val.inspect}, falling back to default"
- debug e.inspect
- debug e.backtrace.join("\n")
- if @default != nil
+ error _("failed to restore marshal data for #{val.inspect}, attempting recovery or fallback to default")
+ debug e
+ if defined? @recovery and @recovery
begin
- return Marshal.restore(@default)
- rescue
- return nil
+ return @recovery.call(val)
+ rescue Exception => ee
+ error _("marshal recovery failed, trying default")
+ debug ee
end
- else
- return nil
end
+ return default
end
end
+ # Returned instead of nil if key wasnt found.
+ def set_default (default)
+ @default = default
+ end
+
+ def default
+ @default && (@default.dup rescue @default)
+ end
+
+ # Opens the database (if not already open) for read/write access.
+ def registry
+ create_folders unless dbexists?
+ end
+
+ # Forces flush/sync the database on disk.
+ def flush
+ return unless @registry
+ @registry.flush
+ end
+
+ # Should optimize/vacuum the database.
+ def optimize
+ return unless @registry
+ @registry.optimize
+ end
+
+ # Closes the database.
+ def close
+ return unless @registry
+ @registry.close
+ @registry = nil
+ end
+
# lookup a key in the registry
def [](key)
- if @registry.has_key?(key)
- return restore(@registry[key])
- elsif @default != nil
- return restore(@default)
+ if dbexists? and registry.has_key?(key.to_s)
+ return restore(registry[key.to_s])
else
- return nil
+ return default
end
end
# set a key in the registry
def []=(key,value)
- @registry[key] = store(value)
- end
-
- # set the default value for registry lookups, if the key sought is not
- # found, the default will be returned. The default default (har) is nil.
- def set_default (default)
- @default = store(default)
+ registry[key.to_s] = store(value)
end
- # just like Hash#each
+ # like Hash#each
def each(&block)
- @registry.each {|key,value|
- block.call(key, restore(value))
- }
+ return nil unless dbexists?
+ registry.each do |key|
+ block.call(key, self[key])
+ end
end
-
- # just like Hash#each_key
+
+ alias each_pair each
+
+ # like Hash#each_key
def each_key(&block)
- @registry.each {|key, value|
+ self.each do |key|
block.call(key)
- }
+ end
end
-
- # just like Hash#each_value
+
+ # like Hash#each_value
def each_value(&block)
- @registry.each {|key, value|
- block.call(restore(value))
- }
+ self.each do |key, value|
+ block.call(value)
+ end
end
# just like Hash#has_key?
def has_key?(key)
- return @registry.has_key?(key)
+ return nil unless dbexists?
+ return registry.has_key?(key.to_s)
end
+
alias include? has_key?
alias member? has_key?
+ alias key? has_key?
- # just like Hash#has_both?
- def has_both?(key, value)
- return @registry.has_both?(key, store(value))
- end
-
# just like Hash#has_value?
def has_value?(value)
- return @registry.has_value?(store(value))
+ return nil unless dbexists?
+ return registry.has_value?(store(value))
end
# just like Hash#index?
def index(value)
- ind = @registry.index(store(value))
- if ind
- return ind
- else
- return nil
+ self.each do |k,v|
+ return k if v == value
end
+ return nil
end
-
+
# delete a key from the registry
def delete(key)
- return @registry.delete(key)
+ return default unless dbexists?
+ return registry.delete(key.to_s)
end
# returns a list of your keys
def keys
- return @registry.keys
+ return [] unless dbexists?
+ return registry.keys
+ end
+
+ # just like Hash#has_both?
+ def has_both?(key, value)
+ return false unless dbexists?
+ registry.has_key?(key.to_s) and registry.has_value?(store(value))
end
# Return an array of all associations [key, value] in your namespace
def to_a
+ return [] unless dbexists?
ret = Array.new
- @registry.each {|key, value|
- ret << [key, restore(value)]
+ self.each {|key, value|
+ ret << [key, value]
}
return ret
end
-
+
# Return an hash of all associations {key => value} in your namespace
def to_hash
+ return {} unless dbexists?
ret = Hash.new
- @registry.each {|key, value|
- ret[key] = restore(value)
+ self.each {|key, value|
+ ret[key] = value
}
return ret
end
# empties the registry (restricted to your namespace)
def clear
- @registry.clear
+ return unless dbexists?
+ registry.clear
end
alias truncate clear
# returns an array of the values in your namespace of the registry
def values
+ return [] unless dbexists?
ret = Array.new
self.each {|k,v|
- ret << restore(v)
+ ret << v
}
return ret
end
- def sub_registry(prefix)
- return BotRegistryAccessor.new(@bot, @name + "/" + prefix)
- end
-
# returns the number of keys in your registry namespace
def length
- self.keys.length
+ return 0 unless dbexists?
+ registry.length
end
alias size length
+ # Returns all classes from the namespace that implement this interface
+ def self.get_impl
+ ObjectSpace.each_object(Class).select { |klass| klass < self }
+ end
end
-end
+end # Registry
+
+end # Bot
+end # Irc
+