]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/registry/tc.rb
8ffd3e40f0a7e76dc8511c24d74b515813cc4695
[user/henk/code/ruby/rbot.git] / lib / rbot / registry / tc.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: DB interface
5
6 begin
7   require 'bdb'
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"
13   end
14
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."
18   end
19 rescue LoadError
20   warning "rbot couldn't load the bdb module. Old registries won't be upgraded"
21 rescue Exception => e
22   warning "rbot couldn't load the bdb module: #{e.pretty_inspect}"
23 end
24
25
26
27
28 require 'tokyocabinet'
29
30 module Irc
31
32   class DBFatal < Exception ; end
33
34   if defined? BDB
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
39   class DBHash
40
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)
44       @bot = bot
45       @key = key
46       relfilename = @bot.path key
47       relfilename << '.db'
48       if absfilename && File.exist?(key)
49         # db already exists, use it
50         @db = DBHash.open_db(key)
51       elsif absfilename
52         # create empty db
53         @db = DBHash.create_db(key)
54       elsif File.exist? relfilename
55         # db already exists, use it
56         @db = DBHash.open_db relfilename
57       else
58         # create empty db
59         @db = DBHash.create_db relfilename
60       end
61     end
62
63     def method_missing(method, *args, &block)
64       return @db.send(method, *args, &block)
65     end
66
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)
71     end
72
73     def DBHash.open_db(name)
74       debug "DBHash: opening existing db #{name}"
75       return BDB::Hash.open(name, nil, "r+", 0600)
76     end
77
78   end
79   # make BTree lookups case insensitive
80   module ::BDB
81     class CIBtree < Btree
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})"
85         end
86         (a||'').downcase <=> (b||'').downcase
87       end
88     end
89   end
90   end
91
92   module ::TokyoCabinet
93     class CIBDB < TokyoCabinet::BDB
94       def open(path, omode)
95         res = super
96         if res
97           self.setcmpfunc(Proc.new do |a, b|
98             a.downcase <=> b.downcase
99           end)
100         end
101         res
102       end
103     end
104   end
105
106   # DBTree is a BTree equivalent of DBHash, with case insensitive lookups.
107   class DBTree
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)
111       @bot = bot
112       @key = key
113
114       relfilename = @bot.path key
115       relfilename << '.tdb'
116
117       if absfilename && File.exist?(key)
118         # db already exists, use it
119         @db = DBTree.open_db(key)
120         @fname = key.dup
121       elsif absfilename
122         # create empty db
123         @db = DBTree.create_db(key)
124         @fname = key.dup
125       elsif File.exist? relfilename
126         # db already exists, use it
127         @db = DBTree.open_db relfilename
128         @fname = relfilename.dup
129       else
130         # create empty db
131         @db = DBTree.create_db relfilename
132         @fname = relfilename.dup
133       end
134       oldbasename = (absfilename ? key : relfilename).gsub(/\.tdb$/, ".db")
135       if File.exists? oldbasename and defined? BDB
136         # upgrading
137         warning "Upgrading old database #{oldbasename}..."
138         oldb = ::BDB::CIBtree.open(oldbasename, nil, "r", 0600)
139         oldb.each_key do |k|
140           @db.outlist k
141           @db.putlist k, (oldb.duplicates(k, false))
142         end
143         oldb.close
144         File.rename oldbasename, oldbasename+".bak"
145       end
146       @db
147     end
148
149     def method_missing(method, *args, &block)
150       return @db.send(method, *args, &block)
151     end
152
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
156     @@bot_registries={ }
157     def self.close_bot_registries
158       @@bot_registries.each { |name, reg| reg.close }
159       @@bot_registries.clear
160     end
161
162     def close
163       db = @@bot_registries.delete(@fname)
164       if db != @db
165         error "We think we have #{@db} from #{@fname}, TC pseudo-env gives us #{db}"
166       end
167       @db.close
168     end
169
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]
175       end
176       db = TokyoCabinet::CIBDB.new
177       res = db.open(name, TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OCREAT | TokyoCabinet::CIBDB::OWRITER)
178       if res
179         @@bot_registries[name] = db
180       else
181         error "DBTree: creating empty db #{name}: #{db.errmsg(db.ecode)}"
182       end
183       return db
184     end
185
186     def DBTree.open_db(name)
187       debug "DBTree: opening existing db #{name}"
188       if @@bot_registries.key? name
189         return @@bot_registries[name]
190       end
191       db = TokyoCabinet::CIBDB.new
192       res = db.open(name, TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OWRITER)
193       if res
194         @@bot_registries[name] = db
195       else
196         error "DBTree: opening db #{name}: #{db.errmsg(db.ecode)}"
197       end
198       return db
199     end
200
201     def DBTree.cleanup_logs()
202       # no-op
203     end
204
205     def DBTree.stats()
206       # no-op
207     end
208
209     def DBTree.cleanup_env()
210       DBTree.close_bot_registries
211     end
212
213   end
214
215 end
216
217 module Irc
218 class Bot
219
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
222   class Registry
223     def initialize(bot)
224       @bot = bot
225       upgrade_data
226       upgrade_data2
227     end
228
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.
232     def upgrade_data
233       oldreg = @bot.path 'registry.db'
234       if defined? DBHash
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)
240           old.each {|k,v|
241             new[k] = v
242           }
243           old.close
244           new.close
245           File.rename(oldreg, oldreg + ".old")
246         end
247       else
248         warning "Won't upgrade data: BDB not installed" if File.exist? oldreg
249       end
250     end
251
252     def upgrade_data2
253       oldreg = @bot.path 'plugin_registry.db'
254       if not defined? BDB
255         warning "Won't upgrade data: BDB not installed" if File.exist? oldreg
256         return
257       end
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)
262         dbs = Hash.new
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)
265         old.each {|k,v|
266           prefix,key = k.split("/", 2)
267           prefix.downcase!
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}")
276                 Dir.mkdir(dir)
277               end
278             }
279           end
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)
284           end
285           dbs[prefix][key] = v
286         }
287         old.close
288         File.rename(oldreg, oldreg + ".old")
289         dbs.each {|k,v|
290           log _("closing db #{k}")
291           v.close
292         }
293         env.close
294       end
295     end
296
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
301   # Marshal.restore,
302   # for example:
303   #   blah = Hash.new
304   #   blah[:foo] = "fum"
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
311   # configuration.
312   #
313   # WARNING:
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
323   #
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):
327   #   def initialize
328   #     class << @registry
329   #       def store(val)
330   #         val
331   #       end
332   #       def restore(val)
333   #         val
334   #       end
335   #     end
336   #   end
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.
340   class Accessor
341
342     attr_accessor :recovery
343
344     # plugins don't call this - a Registry::Accessor is created for them and
345     # is accessible via @registry.
346     def initialize(bot, name)
347       @bot = bot
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}"
355           Dir.mkdir(dir)
356         end
357       }
358       @filename << ".tdb"
359       @registry = nil
360       @default = nil
361       @recovery = nil
362       # debug "initializing registry accessor with name #{@name}"
363     end
364
365     def registry
366         @registry ||= DBTree.new @bot, "registry/#{@name}"
367     end
368
369     def flush
370       # debug "fushing registry #{registry}"
371       return if !@registry
372       registry.sync
373     end
374
375     def close
376       # debug "closing registry #{registry}"
377       return if !@registry
378       registry.close
379     end
380
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:
385     #   def store(val)
386     #     val
387     #   end
388     def store(val)
389       Marshal.dump(val)
390     end
391
392     # restores object from string form, restore(store(val)) must return val.
393     # If you override store, you should override restore to reverse the
394     # action.
395     # For example, if you always just handle strings use:
396     #   def restore(val)
397     #     val
398     #   end
399     def restore(val)
400       begin
401         Marshal.restore(val)
402       rescue Exception => e
403         error _("failed to restore marshal data for #{val.inspect}, attempting recovery or fallback to default")
404         debug e
405         if defined? @recovery and @recovery
406           begin
407             return @recovery.call(val)
408           rescue Exception => ee
409             error _("marshal recovery failed, trying default")
410             debug ee
411           end
412         end
413         return default
414       end
415     end
416
417     # lookup a key in the registry
418     def [](key)
419       if File.exist?(@filename) and registry.has_key?(key.to_s)
420         return restore(registry[key.to_s])
421       else
422         return default
423       end
424     end
425
426     # set a key in the registry
427     def []=(key,value)
428       registry[key.to_s] = store(value)
429     end
430
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)
434       @default = default
435     end
436
437     def default
438       @default && (@default.dup rescue @default)
439     end
440
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]))
446       }
447     end
448
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|
453         block.call(key)
454       end
455     end
456
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]))
462       end
463     end
464
465     # just like Hash#has_key?
466     def has_key?(key)
467       return false unless File.exist?(@filename)
468       return registry.has_key?(key.to_s)
469     end
470
471     alias include? has_key?
472     alias member? has_key?
473     alias key? has_key?
474
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))
479     end
480
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))
485     end
486
487     # just like Hash#index?
488     def index(value)
489       self.each do |k,v|
490         return k if v == value
491       end
492       return nil
493     end
494
495     # delete a key from the registry
496     def delete(key)
497       return default unless File.exist?(@filename)
498       return registry.delete(key.to_s)
499     end
500
501     # returns a list of your keys
502     def keys
503       return [] unless File.exist?(@filename)
504       return registry.keys
505     end
506
507     # Return an array of all associations [key, value] in your namespace
508     def to_a
509       return [] unless File.exist?(@filename)
510       ret = Array.new
511       registry.each {|key, value|
512         ret << [key, restore(value)]
513       }
514       return ret
515     end
516
517     # Return an hash of all associations {key => value} in your namespace
518     def to_hash
519       return {} unless File.exist?(@filename)
520       ret = Hash.new
521       registry.each {|key, value|
522         ret[key] = restore(value)
523       }
524       return ret
525     end
526
527     # empties the registry (restricted to your namespace)
528     def clear
529       return true unless File.exist?(@filename)
530       registry.vanish
531     end
532     alias truncate clear
533
534     # returns an array of the values in your namespace of the registry
535     def values
536       return [] unless File.exist?(@filename)
537       ret = Array.new
538       self.each {|k,v|
539         ret << restore(v)
540       }
541       return ret
542     end
543
544     def sub_registry(prefix)
545       return Accessor.new(@bot, @name + "/" + prefix.to_s)
546     end
547
548     # returns the number of keys in your registry namespace
549     def length
550       return 0 unless File.exist?(@filename)
551       registry.length
552     end
553     alias size length
554
555     # That is btree!
556     def putdup(key, value)
557       registry.putdup(key.to_s, store(value))
558     end
559
560     def putlist(key, values)
561       registry.putlist(key.to_s, value.map {|v| store(v)})
562     end
563
564     def getlist(key)
565       return [] unless File.exist?(@filename)
566       (registry.getlist(key.to_s) || []).map {|v| restore(v)}
567     end
568   end
569
570   end
571 end
572 end