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