X-Git-Url: https://git.netwichtig.de/gitweb/?a=blobdiff_plain;f=lib%2Frbot%2Fregistry.rb;h=61d4b33e02ef454d8cebcfb52617d53bc1f9eef0;hb=628bf10d8ad8536b98b85c529fe92e8ac56a9c4d;hp=41f9747f1801d96664a032afed2559d85c2c8853;hpb=60ee4632925ad75c7d5de2b99ec0267d5a9496bb;p=user%2Fhenk%2Fcode%2Fruby%2Frbot.git diff --git a/lib/rbot/registry.rb b/lib/rbot/registry.rb index 41f9747f..61d4b33e 100644 --- a/lib/rbot/registry.rb +++ b/lib/rbot/registry.rb @@ -1,162 +1,144 @@ -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 Registry - def initialize(bot) - @bot = bot - upgrade_data - upgrade_data2 +class Registry + + # 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 + # The get_impl method will return all implementations of the + # abstract 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. + accessors = AbstractAccessor.get_impl + if accessors.length > 1 + warning 'multiple accessor implementations loaded!' end + @accessor_class = accessors.first + end - # 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.rename("#{@bot.botclass}/registry.db", "#{@bot.botclass}/registry.db.old") - 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 + 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)# | BDB::TXN_NOSYNC) - 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! - # subregistries were split with a +, now they are in separate folders - if prefix.gsub!(/\+/, "/") - # Ok, this code needs to be put in the db opening routines - dirs = File.dirname("#{@bot.botclass}/registry/#{prefix}.db").split("/") - dirs.length.times { |i| - dir = dirs[0,i+1].join("/")+"/" - unless File.exist?(dir) - log _("creating subregistry directory #{dir}") - Dir.mkdir(dir) - end - } - end - 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 + # Creates a new Accessor object for the specified database filename. + def create(path, filename) + db = @accessor_class.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 + # Abstract database accessor (a hash-like interface). + class AbstractAccessor - # 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. - class Accessor + attr_reader :filename + # 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 - # plugins don't call this - a Registry::Accessor is created for them and - # is accessible via @registry. - def initialize(bot, name) - @bot = bot - @name = name.downcase - @filename = "#{@bot.botclass}/registry/#{@name}" - dirs = File.dirname(@filename).split("/") - dirs.length.times { |i| - dir = dirs[0,i+1].join("/")+"/" - unless File.exist?(dir) - debug "creating subregistry directory #{dir}" - Dir.mkdir(dir) - end - } - @filename << ".db" + def initialize(filename) + debug "init registry accessor of #{self.class} for: #{filename}" + @filename = filename + @name = File.basename filename @registry = nil @default = nil @recovery = nil - # debug "initializing registry accessor with name #{@name}" + @sub_registries = {} end - def registry - @registry ||= DBTree.new @bot, "registry/#{@name}" + def sub_registry(prefix) + path = File.join(@filename.gsub(/\.[^\/\.]+$/,''), prefix.to_s) + @sub_registries[path] ||= self.class.new(path) end - def flush - # debug "fushing registry #{registry}" - return if !@registry - registry.flush - registry.sync + # 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 - def close - # debug "closing registry #{registry}" - return if !@registry - registry.close + # Will return true if the database file exists. + def dbexists? + File.exists? @filename end # convert value to string form for storing in the registry @@ -195,10 +177,43 @@ class Bot 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 + # if not supported by the database, close/reopen: + close + registry + end + + # Should optimize/vacuum the database. (if supported) + def optimize + end + + # Closes the database. + def close + return unless @registry + @registry.close + @registry = nil + end + # lookup a key in the registry def [](key) - if File.exist?(@filename) && registry.has_key?(key) - return restore(registry[key]) + if dbexists? and registry.has_key?(key.to_s) + return restore(registry[key.to_s]) else return default end @@ -206,136 +221,125 @@ class Bot # 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 = default + registry[key.to_s] = store(value) end - def default - @default && (@default.dup rescue default) - end - - # just like Hash#each + # like Hash#each def each(&block) - return nil unless File.exist?(@filename) - registry.each {|key,value| + return nil unless dbexists? + registry.each do |key, value| block.call(key, restore(value)) - } + end end - # just like Hash#each_key + alias each_pair each + + # like Hash#each_key def each_key(&block) - return nil unless File.exist?(@filename) - 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) - return nil unless File.exist?(@filename) - 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 false unless File.exist?(@filename) - 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 false unless File.exist?(@filename) - return registry.has_both?(key, store(value)) - end - # just like Hash#has_value? def has_value?(value) - return false unless File.exist?(@filename) + return nil unless dbexists? return registry.has_value?(store(value)) end # just like Hash#index? def index(value) - return nil unless File.exist?(@filename) - 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 + # returns the value in success, nil otherwise def delete(key) - return default unless File.exist?(@filename) - return registry.delete(key) + return default unless dbexists? + value = registry.delete(key.to_s) + if value + restore(value) + end end # returns a list of your keys def keys - return [] unless File.exist?(@filename) + return [] unless dbexists? return registry.keys end # Return an array of all associations [key, value] in your namespace def to_a - return [] unless File.exist?(@filename) + 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 File.exist?(@filename) + 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 - return true unless File.exist?(@filename) + 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 File.exist?(@filename) + return [] unless dbexists? ret = Array.new self.each {|k,v| - ret << restore(v) + ret << v } return ret end - def sub_registry(prefix) - return Accessor.new(@bot, @name + "/" + prefix.to_s) - 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.ancestors[1] == self } + end end - end -end -end +end # Registry + +end # Bot +end # Irc +