TokyoCabinet: only warn about skipped upgrade if the old reg was found
[rbot] / 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       newdir = @bot.path 'registry'
216       if File.exist?(oldreg)
217         Dir.mkdir(newdir) unless File.exist?(newdir)
218         env = BDB::Env.open(@bot.botclass, BDB::INIT_TRANSACTION | BDB::CREATE | BDB::RECOVER)# | BDB::TXN_NOSYNC)
219         dbs = Hash.new
220         log _("upgrading previous (rbot 0.9.9 or earlier) plugin registry to new split format")
221         old = BDB::CIBtree.open(oldreg, nil, "r+", 0600, "env" => env)
222         old.each {|k,v|
223           prefix,key = k.split("/", 2)
224           prefix.downcase!
225           # subregistries were split with a +, now they are in separate folders
226           if prefix.gsub!(/\+/, "/")
227             # Ok, this code needs to be put in the db opening routines
228             dirs = File.dirname("#{@bot.botclass}/registry/#{prefix}.db").split("/")
229             dirs.length.times { |i|
230               dir = dirs[0,i+1].join("/")+"/"
231               unless File.exist?(dir)
232                 log _("creating subregistry directory #{dir}")
233                 Dir.mkdir(dir)
234               end
235             }
236           end
237           unless dbs.has_key?(prefix)
238             log _("creating db #{@bot.botclass}/registry/#{prefix}.tdb")
239             dbs[prefix] = TokyoCabinet::CIBDB.open("#{@bot.botclass}/registry/#{prefix}.tdb",
240              TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OCREAT | TokyoCabinet::CIBDB::OWRITER)
241           end
242           dbs[prefix][key] = v
243         }
244         old.close
245         File.rename(oldreg, oldreg + ".old")
246         dbs.each {|k,v|
247           log _("closing db #{k}")
248           v.close
249         }
250         env.close
251       end
252     end
253
254   # This class provides persistent storage for plugins via a hash interface.
255   # The default mode is an object store, so you can store ruby objects and
256   # reference them with hash keys. This is because the default store/restore
257   # methods of the plugins' RegistryAccessor are calls to Marshal.dump and
258   # Marshal.restore,
259   # for example:
260   #   blah = Hash.new
261   #   blah[:foo] = "fum"
262   #   @registry[:blah] = blah
263   # then, even after the bot is shut down and disconnected, on the next run you
264   # can access the blah object as it was, with:
265   #   blah = @registry[:blah]
266   # The registry can of course be used to store simple strings, fixnums, etc as
267   # well, and should be useful to store or cache plugin data or dynamic plugin
268   # configuration.
269   #
270   # WARNING:
271   # in object store mode, don't make the mistake of treating it like a live
272   # object, e.g. (using the example above)
273   #   @registry[:blah][:foo] = "flump"
274   # will NOT modify the object in the registry - remember that Registry#[]
275   # returns a Marshal.restore'd object, the object you just modified in place
276   # will disappear. You would need to:
277   #   blah = @registry[:blah]
278   #   blah[:foo] = "flump"
279   #   @registry[:blah] = blah
280   #
281   # If you don't need to store objects, and strictly want a persistant hash of
282   # strings, you can override the store/restore methods to suit your needs, for
283   # example (in your plugin):
284   #   def initialize
285   #     class << @registry
286   #       def store(val)
287   #         val
288   #       end
289   #       def restore(val)
290   #         val
291   #       end
292   #     end
293   #   end
294   # Your plugins section of the registry is private, it has its own namespace
295   # (derived from the plugin's class name, so change it and lose your data).
296   # Calls to registry.each etc, will only iterate over your namespace.
297   class Accessor
298
299     attr_accessor :recovery
300
301     # plugins don't call this - a Registry::Accessor is created for them and
302     # is accessible via @registry.
303     def initialize(bot, name)
304       @bot = bot
305       @name = name.downcase
306       @filename = @bot.path 'registry', @name
307       dirs = File.dirname(@filename).split("/")
308       dirs.length.times { |i|
309         dir = dirs[0,i+1].join("/")+"/"
310         unless File.exist?(dir)
311           debug "creating subregistry directory #{dir}"
312           Dir.mkdir(dir)
313         end
314       }
315       @filename << ".tdb"
316       @registry = nil
317       @default = nil
318       @recovery = nil
319       # debug "initializing registry accessor with name #{@name}"
320     end
321
322     def registry
323         @registry ||= DBTree.new @bot, "registry/#{@name}"
324     end
325
326     def flush
327       # debug "fushing registry #{registry}"
328       return if !@registry
329       registry.sync
330     end
331
332     def close
333       # debug "closing registry #{registry}"
334       return if !@registry
335       registry.close
336     end
337
338     # convert value to string form for storing in the registry
339     # defaults to Marshal.dump(val) but you can override this in your module's
340     # registry object to use any method you like.
341     # For example, if you always just handle strings use:
342     #   def store(val)
343     #     val
344     #   end
345     def store(val)
346       Marshal.dump(val)
347     end
348
349     # restores object from string form, restore(store(val)) must return val.
350     # If you override store, you should override restore to reverse the
351     # action.
352     # For example, if you always just handle strings use:
353     #   def restore(val)
354     #     val
355     #   end
356     def restore(val)
357       begin
358         Marshal.restore(val)
359       rescue Exception => e
360         error _("failed to restore marshal data for #{val.inspect}, attempting recovery or fallback to default")
361         debug e
362         if defined? @recovery and @recovery
363           begin
364             return @recovery.call(val)
365           rescue Exception => ee
366             error _("marshal recovery failed, trying default")
367             debug ee
368           end
369         end
370         return default
371       end
372     end
373
374     # lookup a key in the registry
375     def [](key)
376       if File.exist?(@filename) and registry.has_key?(key.to_s)
377         return restore(registry[key.to_s])
378       else
379         return default
380       end
381     end
382
383     # set a key in the registry
384     def []=(key,value)
385       registry[key.to_s] = store(value)
386     end
387
388     # set the default value for registry lookups, if the key sought is not
389     # found, the default will be returned. The default default (har) is nil.
390     def set_default (default)
391       @default = default
392     end
393
394     def default
395       @default && (@default.dup rescue @default)
396     end
397
398     # just like Hash#each
399     def each(set=nil, bulk=0, &block)
400       return nil unless File.exist?(@filename)
401       registry.fwmkeys(set).each {|key|
402         block.call(key, restore(registry[key]))
403       }
404     end
405
406     # just like Hash#each_key
407     def each_key(set=nil, bulk=0, &block)
408       return nil unless File.exist?(@filename)
409       registry.fwmkeys(set).each do |key|
410         block.call(key)
411       end
412     end
413
414     # just like Hash#each_value
415     def each_value(set=nil, bulk=0, &block)
416       return nil unless File.exist?(@filename)
417       registry.fwmkeys(set).each do |key|
418         block.call(restore(registry[key]))
419       end
420     end
421
422     # just like Hash#has_key?
423     def has_key?(key)
424       return false unless File.exist?(@filename)
425       return registry.has_key?(key.to_s)
426     end
427
428     alias include? has_key?
429     alias member? has_key?
430     alias key? has_key?
431
432     # just like Hash#has_both?
433     def has_both?(key, value)
434       return false unless File.exist?(@filename)
435       registry.has_key?(key.to_s) and registry.has_value?(store(value))
436     end
437
438     # just like Hash#has_value?
439     def has_value?(value)
440       return false unless File.exist?(@filename)
441       return registry.has_value?(store(value))
442     end
443
444     # just like Hash#index?
445     def index(value)
446       self.each do |k,v|
447         return k if v == value
448       end
449       return nil
450     end
451
452     # delete a key from the registry
453     def delete(key)
454       return default unless File.exist?(@filename)
455       return registry.delete(key.to_s)
456     end
457
458     # returns a list of your keys
459     def keys
460       return [] unless File.exist?(@filename)
461       return registry.keys
462     end
463
464     # Return an array of all associations [key, value] in your namespace
465     def to_a
466       return [] unless File.exist?(@filename)
467       ret = Array.new
468       registry.each {|key, value|
469         ret << [key, restore(value)]
470       }
471       return ret
472     end
473
474     # Return an hash of all associations {key => value} in your namespace
475     def to_hash
476       return {} unless File.exist?(@filename)
477       ret = Hash.new
478       registry.each {|key, value|
479         ret[key] = restore(value)
480       }
481       return ret
482     end
483
484     # empties the registry (restricted to your namespace)
485     def clear
486       return true unless File.exist?(@filename)
487       registry.vanish
488     end
489     alias truncate clear
490
491     # returns an array of the values in your namespace of the registry
492     def values
493       return [] unless File.exist?(@filename)
494       ret = Array.new
495       self.each {|k,v|
496         ret << restore(v)
497       }
498       return ret
499     end
500
501     def sub_registry(prefix)
502       return Accessor.new(@bot, @name + "/" + prefix.to_s)
503     end
504
505     # returns the number of keys in your registry namespace
506     def length
507       return 0 unless File.exist?(@filename)
508       registry.length
509     end
510     alias size length
511
512     # That is btree!
513     def putdup(key, value)
514       registry.putdup(key.to_s, store(value))
515     end
516
517     def putlist(key, values)
518       registry.putlist(key.to_s, value.map {|v| store(v)})
519     end
520
521     def getlist(key)
522       return [] unless File.exist?(@filename)
523       (registry.getlist(key.to_s) || []).map {|v| restore(v)}
524     end
525   end
526
527   end
528 end
529 end