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