]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/registry/tc.rb
eae6e29e6a6bd62e195244c2fc922f7a5437b669
[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   if defined? BDB
33   # DBHash is for tying a hash to disk (using bdb).
34   # Call it with an identifier, for example "mydata". It'll look for
35   # mydata.db, if it exists, it will load and reference that db.
36   # Otherwise it'll create and empty db called mydata.db
37   class DBHash
38
39     # absfilename:: use +key+ as an actual filename, don't prepend the bot's
40     #               config path and don't append ".db"
41     def initialize(bot, key, absfilename=false)
42       @bot = bot
43       @key = key
44       relfilename = @bot.path key
45       relfilename << '.db'
46       if absfilename && File.exist?(key)
47         # db already exists, use it
48         @db = DBHash.open_db(key)
49       elsif absfilename
50         # create empty db
51         @db = DBHash.create_db(key)
52       elsif File.exist? relfilename
53         # db already exists, use it
54         @db = DBHash.open_db relfilename
55       else
56         # create empty db
57         @db = DBHash.create_db relfilename
58       end
59     end
60
61     def method_missing(method, *args, &block)
62       return @db.send(method, *args, &block)
63     end
64
65     def DBHash.create_db(name)
66       debug "DBHash: creating empty db #{name}"
67       return BDB::Hash.open(name, nil,
68       BDB::CREATE | BDB::EXCL, 0600)
69     end
70
71     def DBHash.open_db(name)
72       debug "DBHash: opening existing db #{name}"
73       return BDB::Hash.open(name, nil, "r+", 0600)
74     end
75
76   end
77   # make BTree lookups case insensitive
78   module ::BDB
79     class CIBtree < Btree
80       def bdb_bt_compare(a, b)
81         if a == nil || b == nil
82           warning "CIBTree: comparing #{a.inspect} (#{self[a].inspect}) with #{b.inspect} (#{self[b].inspect})"
83         end
84         (a||'').downcase <=> (b||'').downcase
85       end
86     end
87   end
88   end
89
90   module ::TokyoCabinet
91     class CIBDB < TokyoCabinet::BDB
92       def open(path, omode)
93         res = super
94         if res
95           self.setcmpfunc(Proc.new do |a, b|
96             a.downcase <=> b.downcase
97           end)
98         end
99         res
100       end
101     end
102   end
103
104   # DBTree is a BTree equivalent of DBHash, with case insensitive lookups.
105   class DBTree
106     # absfilename:: use +key+ as an actual filename, don't prepend the bot's
107     #               config path and don't append ".db"
108     def initialize(bot, key, absfilename=false)
109       @bot = bot
110       @key = key
111
112       relfilename = @bot.path key
113       relfilename << '.tdb'
114
115       if absfilename && File.exist?(key)
116         # db already exists, use it
117         @db = DBTree.open_db(key)
118       elsif absfilename
119         # create empty db
120         @db = DBTree.create_db(key)
121       elsif File.exist? relfilename
122         # db already exists, use it
123         @db = DBTree.open_db relfilename
124       else
125         # create empty db
126         @db = DBTree.create_db relfilename
127       end
128       oldbasename = (absfilename ? key : relfilename).gsub(/\.tdb$/, ".db")
129       if File.exists? oldbasename and defined? BDB
130         # upgrading
131         warning "Upgrading old database #{oldbasename}..."
132         oldb = ::BDB::Btree.open(oldbasename, nil, "r", 0600)
133         oldb.each_key do |k|
134           @db.outlist k
135           @db.putlist k, (oldb.duplicates(k, false))
136         end
137         oldb.close
138         File.rename oldbasename, oldbasename+".bak"
139       end
140       @db
141     end
142
143     def method_missing(method, *args, &block)
144       return @db.send(method, *args, &block)
145     end
146
147     def DBTree.create_db(name)
148       debug "DBTree: creating empty db #{name}"
149       db = TokyoCabinet::CIBDB.new
150       res = db.open(name, TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OCREAT | TokyoCabinet::CIBDB::OWRITER)
151        warning "DBTree: creating empty db #{name}: #{db.errmsg(db.ecode) unless res}"
152       return db
153     end
154
155     def DBTree.open_db(name)
156       debug "DBTree: opening existing db #{name}"
157       db = TokyoCabinet::CIBDB.new
158       res = db.open(name, TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OWRITER)
159        warning "DBTree:opening db #{name}: #{db.errmsg(db.ecode) unless res}"
160       return db
161     end
162
163     def DBTree.cleanup_logs()
164       # no-op
165     end
166
167     def DBTree.stats()
168       # no-op
169     end
170
171     def DBTree.cleanup_env()
172       # no-op
173     end
174
175   end
176
177 end
178
179 module Irc
180 class Bot
181
182   # This class is now used purely for upgrading from prior versions of rbot
183   # the new registry is split into multiple DBHash objects, one per plugin
184   class Registry
185     def initialize(bot)
186       @bot = bot
187       upgrade_data
188       upgrade_data2
189     end
190
191     # check for older versions of rbot with data formats that require updating
192     # NB this function is called _early_ in init(), pretty much all you have to
193     # work with is @bot.botclass.
194     def upgrade_data
195       oldreg = @bot.path 'registry.db'
196       if defined? DBHash
197         newreg = @bot.path 'plugin_registry.db'
198         if File.exist?(oldreg)
199           log _("upgrading old-style (rbot 0.9.5 or earlier) plugin registry to new format")
200           old = ::BDB::Hash.open(oldreg, nil, "r+", 0600)
201           new = ::BDB::CIBtree.open(newreg, nil, ::BDB::CREATE | ::BDB::EXCL, 0600)
202           old.each {|k,v|
203             new[k] = v
204           }
205           old.close
206           new.close
207           File.rename(oldreg, oldreg + ".old")
208         end
209       else
210         warning "Won't upgrade data: BDB not installed" if File.exist? oldreg
211       end
212     end
213
214     def upgrade_data2
215       oldreg = @bot.path 'plugin_registry.db'
216       if not defined? BDB
217         warning "Won't upgrade data: BDB not installed" if File.exist? oldreg
218         return
219       end
220       newdir = @bot.path 'registry'
221       if File.exist?(oldreg)
222         Dir.mkdir(newdir) unless File.exist?(newdir)
223         env = BDB::Env.open(@bot.botclass, BDB::INIT_TRANSACTION | BDB::CREATE | BDB::RECOVER)# | BDB::TXN_NOSYNC)
224         dbs = Hash.new
225         log _("upgrading previous (rbot 0.9.9 or earlier) plugin registry to new split format")
226         old = BDB::CIBtree.open(oldreg, nil, "r+", 0600, "env" => env)
227         old.each {|k,v|
228           prefix,key = k.split("/", 2)
229           prefix.downcase!
230           # subregistries were split with a +, now they are in separate folders
231           if prefix.gsub!(/\+/, "/")
232             # Ok, this code needs to be put in the db opening routines
233             dirs = File.dirname("#{@bot.botclass}/registry/#{prefix}.db").split("/")
234             dirs.length.times { |i|
235               dir = dirs[0,i+1].join("/")+"/"
236               unless File.exist?(dir)
237                 log _("creating subregistry directory #{dir}")
238                 Dir.mkdir(dir)
239               end
240             }
241           end
242           unless dbs.has_key?(prefix)
243             log _("creating db #{@bot.botclass}/registry/#{prefix}.tdb")
244             dbs[prefix] = TokyoCabinet::CIBDB.open("#{@bot.botclass}/registry/#{prefix}.tdb",
245              TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OCREAT | TokyoCabinet::CIBDB::OWRITER)
246           end
247           dbs[prefix][key] = v
248         }
249         old.close
250         File.rename(oldreg, oldreg + ".old")
251         dbs.each {|k,v|
252           log _("closing db #{k}")
253           v.close
254         }
255         env.close
256       end
257     end
258
259   # This class provides persistent storage for plugins via a hash interface.
260   # The default mode is an object store, so you can store ruby objects and
261   # reference them with hash keys. This is because the default store/restore
262   # methods of the plugins' RegistryAccessor are calls to Marshal.dump and
263   # Marshal.restore,
264   # for example:
265   #   blah = Hash.new
266   #   blah[:foo] = "fum"
267   #   @registry[:blah] = blah
268   # then, even after the bot is shut down and disconnected, on the next run you
269   # can access the blah object as it was, with:
270   #   blah = @registry[:blah]
271   # The registry can of course be used to store simple strings, fixnums, etc as
272   # well, and should be useful to store or cache plugin data or dynamic plugin
273   # configuration.
274   #
275   # WARNING:
276   # in object store mode, don't make the mistake of treating it like a live
277   # object, e.g. (using the example above)
278   #   @registry[:blah][:foo] = "flump"
279   # will NOT modify the object in the registry - remember that Registry#[]
280   # returns a Marshal.restore'd object, the object you just modified in place
281   # will disappear. You would need to:
282   #   blah = @registry[:blah]
283   #   blah[:foo] = "flump"
284   #   @registry[:blah] = blah
285   #
286   # If you don't need to store objects, and strictly want a persistant hash of
287   # strings, you can override the store/restore methods to suit your needs, for
288   # example (in your plugin):
289   #   def initialize
290   #     class << @registry
291   #       def store(val)
292   #         val
293   #       end
294   #       def restore(val)
295   #         val
296   #       end
297   #     end
298   #   end
299   # Your plugins section of the registry is private, it has its own namespace
300   # (derived from the plugin's class name, so change it and lose your data).
301   # Calls to registry.each etc, will only iterate over your namespace.
302   class Accessor
303
304     attr_accessor :recovery
305
306     # plugins don't call this - a Registry::Accessor is created for them and
307     # is accessible via @registry.
308     def initialize(bot, name)
309       @bot = bot
310       @name = name.downcase
311       @filename = @bot.path 'registry', @name
312       dirs = File.dirname(@filename).split("/")
313       dirs.length.times { |i|
314         dir = dirs[0,i+1].join("/")+"/"
315         unless File.exist?(dir)
316           debug "creating subregistry directory #{dir}"
317           Dir.mkdir(dir)
318         end
319       }
320       @filename << ".tdb"
321       @registry = nil
322       @default = nil
323       @recovery = nil
324       # debug "initializing registry accessor with name #{@name}"
325     end
326
327     def registry
328         @registry ||= DBTree.new @bot, "registry/#{@name}"
329     end
330
331     def flush
332       # debug "fushing registry #{registry}"
333       return if !@registry
334       registry.sync
335     end
336
337     def close
338       # debug "closing registry #{registry}"
339       return if !@registry
340       registry.close
341     end
342
343     # convert value to string form for storing in the registry
344     # defaults to Marshal.dump(val) but you can override this in your module's
345     # registry object to use any method you like.
346     # For example, if you always just handle strings use:
347     #   def store(val)
348     #     val
349     #   end
350     def store(val)
351       Marshal.dump(val)
352     end
353
354     # restores object from string form, restore(store(val)) must return val.
355     # If you override store, you should override restore to reverse the
356     # action.
357     # For example, if you always just handle strings use:
358     #   def restore(val)
359     #     val
360     #   end
361     def restore(val)
362       begin
363         Marshal.restore(val)
364       rescue Exception => e
365         error _("failed to restore marshal data for #{val.inspect}, attempting recovery or fallback to default")
366         debug e
367         if defined? @recovery and @recovery
368           begin
369             return @recovery.call(val)
370           rescue Exception => ee
371             error _("marshal recovery failed, trying default")
372             debug ee
373           end
374         end
375         return default
376       end
377     end
378
379     # lookup a key in the registry
380     def [](key)
381       if File.exist?(@filename) and registry.has_key?(key.to_s)
382         return restore(registry[key.to_s])
383       else
384         return default
385       end
386     end
387
388     # set a key in the registry
389     def []=(key,value)
390       registry[key.to_s] = store(value)
391     end
392
393     # set the default value for registry lookups, if the key sought is not
394     # found, the default will be returned. The default default (har) is nil.
395     def set_default (default)
396       @default = default
397     end
398
399     def default
400       @default && (@default.dup rescue @default)
401     end
402
403     # just like Hash#each
404     def each(set=nil, bulk=0, &block)
405       return nil unless File.exist?(@filename)
406       registry.fwmkeys(set).each {|key|
407         block.call(key, restore(registry[key]))
408       }
409     end
410
411     # just like Hash#each_key
412     def each_key(set=nil, bulk=0, &block)
413       return nil unless File.exist?(@filename)
414       registry.fwmkeys(set).each do |key|
415         block.call(key)
416       end
417     end
418
419     # just like Hash#each_value
420     def each_value(set=nil, bulk=0, &block)
421       return nil unless File.exist?(@filename)
422       registry.fwmkeys(set).each do |key|
423         block.call(restore(registry[key]))
424       end
425     end
426
427     # just like Hash#has_key?
428     def has_key?(key)
429       return false unless File.exist?(@filename)
430       return registry.has_key?(key.to_s)
431     end
432
433     alias include? has_key?
434     alias member? has_key?
435     alias key? has_key?
436
437     # just like Hash#has_both?
438     def has_both?(key, value)
439       return false unless File.exist?(@filename)
440       registry.has_key?(key.to_s) and registry.has_value?(store(value))
441     end
442
443     # just like Hash#has_value?
444     def has_value?(value)
445       return false unless File.exist?(@filename)
446       return registry.has_value?(store(value))
447     end
448
449     # just like Hash#index?
450     def index(value)
451       self.each do |k,v|
452         return k if v == value
453       end
454       return nil
455     end
456
457     # delete a key from the registry
458     def delete(key)
459       return default unless File.exist?(@filename)
460       return registry.delete(key.to_s)
461     end
462
463     # returns a list of your keys
464     def keys
465       return [] unless File.exist?(@filename)
466       return registry.keys
467     end
468
469     # Return an array of all associations [key, value] in your namespace
470     def to_a
471       return [] unless File.exist?(@filename)
472       ret = Array.new
473       registry.each {|key, value|
474         ret << [key, restore(value)]
475       }
476       return ret
477     end
478
479     # Return an hash of all associations {key => value} in your namespace
480     def to_hash
481       return {} unless File.exist?(@filename)
482       ret = Hash.new
483       registry.each {|key, value|
484         ret[key] = restore(value)
485       }
486       return ret
487     end
488
489     # empties the registry (restricted to your namespace)
490     def clear
491       return true unless File.exist?(@filename)
492       registry.vanish
493     end
494     alias truncate clear
495
496     # returns an array of the values in your namespace of the registry
497     def values
498       return [] unless File.exist?(@filename)
499       ret = Array.new
500       self.each {|k,v|
501         ret << restore(v)
502       }
503       return ret
504     end
505
506     def sub_registry(prefix)
507       return Accessor.new(@bot, @name + "/" + prefix.to_s)
508     end
509
510     # returns the number of keys in your registry namespace
511     def length
512       return 0 unless File.exist?(@filename)
513       registry.length
514     end
515     alias size length
516
517     # That is btree!
518     def putdup(key, value)
519       registry.putdup(key.to_s, store(value))
520     end
521
522     def putlist(key, values)
523       registry.putlist(key.to_s, value.map {|v| store(v)})
524     end
525
526     def getlist(key)
527       return [] unless File.exist?(@filename)
528       (registry.getlist(key.to_s) || []).map {|v| restore(v)}
529     end
530   end
531
532   end
533 end
534 end