]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/plugins.rb
Better reporting of plugin failures
[user/henk/code/ruby/rbot.git] / lib / rbot / plugins.rb
index 893bb4142650da70c11f6f3137db7314a8c6e9d3..2a07cfb0910077b34bf40599af80ee3fd39cb540 100644 (file)
@@ -1,4 +1,7 @@
 module Irc
+    BotConfig.register BotConfigArrayValue.new('plugins.blacklist',
+      :default => [], :wizard => false, :requires_restart => true,
+      :desc => "Plugins that should not be loaded")
 module Plugins
   require 'rbot/messagemapper'
 
@@ -7,8 +10,13 @@ module Plugins
   # the following methods, it will be called as appropriate:
   #
   # map(template, options)::
+  # map!(template, options)::
   #    map is the new, cleaner way to respond to specific message formats
-  #    without littering your plugin code with regexps. examples:
+  #    without littering your plugin code with regexps. The difference
+  #    between map and map! is that map! will not register the new command
+  #    as an alternative name for the plugin.
+  #
+  #    Examples:
   #
   #      plugin.map 'karmastats', :action => 'karma_stats'
   #
@@ -16,7 +24,7 @@ module Plugins
   #      def karma_stats(m, params)
   #        m.reply "..."
   #      end
-  #      
+  #
   #      # the default action is the first component
   #      plugin.map 'karma'
   #
@@ -29,10 +37,10 @@ module Plugins
   #        item = params[:key]
   #        m.reply 'karma for #{item}'
   #      end
-  #      
+  #
   #      # you can setup defaults, to make parameters optional
   #      plugin.map 'karma :key', :defaults => {:key => 'defaultvalue'}
-  #      
+  #
   #      # the default auth check is also against the first component
   #      # but that can be changed
   #      plugin.map 'karmastats', :auth => 'karma'
@@ -42,18 +50,13 @@ module Plugins
   #      plugin.map 'karmastats', :public false,
   #    end
   #
-  #    To activate your maps, you simply register them
-  #    plugin.register_maps
-  #    This also sets the privmsg handler to use the map lookups for
-  #    handling messages. You can still use listen(), kick() etc methods
-  # 
   # listen(UserMessage)::
   #                        Called for all messages of any type. To
   #                        differentiate them, use message.kind_of? It'll be
   #                        either a PrivMessage, NoticeMessage, KickMessage,
   #                        QuitMessage, PartMessage, JoinMessage, NickMessage,
   #                        etc.
-  #                              
+  #
   # privmsg(PrivMessage)::
   #                        called for a PRIVMSG if the first word matches one
   #                        the plugin register()d for. Use m.plugin to get
@@ -70,7 +73,7 @@ module Plugins
   # part(PartMessage)::
   #                        Called when a user (or the bot) parts a channel
   #
-  # quit(QuitMessage)::    
+  # quit(QuitMessage)::
   #                        Called when a user (or the bot) quits IRC
   #
   # nick(NickMessage)::
@@ -81,7 +84,7 @@ module Plugins
   #
   # connect()::            Called when a server is joined successfully, but
   #                        before autojoin channels are joined (no params)
-  # 
+  #
   # save::                 Called when you are required to save your plugin's
   #                        state, if you maintain data between sessions
   #
@@ -99,6 +102,16 @@ module Plugins
       @registry = BotRegistryAccessor.new(@bot, self.class.to_s.gsub(/^.*::/, ""))
     end
 
+    def flush_registry
+      # debug "Flushing #{@registry}"
+      @registry.flush
+    end
+
+    def cleanup
+      # debug "Closing #{@registry}"
+      @registry.close
+    end
+
     def map(*args)
       @handler.map(*args)
       # register this map
@@ -111,12 +124,24 @@ module Plugins
       end
     end
 
+    def map!(*args)
+      @handler.map(*args)
+      # register this map
+      name = @handler.last.items[0]
+      self.register name, {:hidden => true}
+      unless self.respond_to?('privmsg')
+        def self.privmsg(m)
+          @handler.handle(m)
+        end
+      end
+    end
+
     # return an identifier for this plugin, defaults to a list of the message
     # prefixes handled (used for error messages etc)
     def name
       @names.join("|")
     end
-    
+
     # return a help string for your module. for complex modules, you may wish
     # to break your help into topics, and return a list of available topics if
     # +topic+ is nil. +plugin+ is passed containing the matching prefix for
@@ -125,14 +150,15 @@ module Plugins
     def help(plugin, topic)
       "no help"
     end
-    
+
     # register the plugin as a handler for messages prefixed +name+
     # this can be called multiple times for a plugin to handle multiple
     # message prefixes
-    def register(name)
+    def register(name,opts={})
+      raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
       return if Plugins.plugins.has_key?(name)
       Plugins.plugins[name] = self
-      @names << name
+      @names << name unless opts.fetch(:hidden, false)
     end
 
     # default usage method provided as a utility for simple plugins. The
@@ -160,7 +186,7 @@ module Plugins
       @dirs = dirlist
       scan
     end
-    
+
     # access to associated bot
     def Plugins.bot
       @@bot
@@ -173,7 +199,12 @@ module Plugins
 
     # load plugins from pre-assigned list of directories
     def scan
-      processed = Array.new
+      @blacklist = Array.new
+      @@bot.config['plugins.blacklist'].each { |p|
+        @blacklist << p+".rb"
+      }
+      @failed = Array.new
+      processed = @blacklist.dup
       dirs = Array.new
       dirs << Config::datadir + "/plugins"
       dirs += @dirs
@@ -194,15 +225,33 @@ module Plugins
             # the idea here is to prevent namespace pollution. perhaps there
             # is another way?
             plugin_module = Module.new
-            
+
             begin
               plugin_string = IO.readlines(tmpfilename).join("")
               debug "loading plugin #{tmpfilename}"
               plugin_module.module_eval(plugin_string)
               processed << file
-            rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
-              puts "warning: plugin #{tmpfilename} load failed: " + err
-              puts err.backtrace.join("\n")
+            rescue Exception => err
+              # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
+              warning "plugin #{tmpfilename} load failed\n" + err.inspect
+              debug err.backtrace.join("\n")
+              bt = err.backtrace.select { |line|
+                line.match(/^(\(eval\)|#{tmpfilename}):\d+/)
+              }
+              bt.map! { |el|
+                el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
+                  "#{tmpfilename}#{$1}#{$3}"
+                }
+              }
+              msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
+                "#{tmpfilename}#{$1}#{$3}"
+              }
+              newerr = err.class.new(msg)
+              newerr.set_backtrace(bt)
+              # debug "Simplified error: " << newerr.inspect
+              # debug newerr.backtrace.join("\n")
+              @failed << { :name => tmpfilename, :err => newerr }
+              # debug "Failures: #{@failed.inspect}"
             end
           }
         end
@@ -211,6 +260,7 @@ module Plugins
 
     # call the save method for each active plugin
     def save
+      delegate 'flush_registry'
       delegate 'save'
     end
 
@@ -231,11 +281,13 @@ module Plugins
     # return list of help topics (plugin names)
     def helptopics
       if(@@plugins.length > 0)
-        # return " [plugins: " + @@plugins.keys.sort.join(", ") + "]"
-        return " [#{length} plugins: " + @@plugins.values.uniq.collect{|p| p.name}.sort.join(", ") + "]"
+        list = " [#{length} plugin#{'s' if length > 1}: " + @@plugins.values.uniq.collect{|p| p.name}.sort.join(", ")
       else
-        return " [no plugins active]" 
+        list = " [no plugins active"
       end
+      list << "; #{Reverse}#{@failed.length} plugin#{'s' if @failed.length > 1} failed to load#{Reverse}: use #{Bold}help pluginfailures#{Bold} to see why" unless @failed.empty?
+      list << "]"
+      return list
     end
 
     def length
@@ -244,22 +296,32 @@ module Plugins
 
     # return help for +topic+ (call associated plugin's help method)
     def help(topic="")
+      if topic =~ /plugin\s*fail(?:ure)?s?\s*(trace(?:back)?s?)?/
+        # debug "Failures: #{@failed.inspect}"
+        return "no plugins failed to load" if @failed.empty?
+        return (@failed.inject(Array.new) { |list, p|
+          list << "#{Bold}#{p[:name]}#{Bold} failed with #{p[:err].class}: #{p[:err]}"
+          list << "#{Bold}#{p[:name]}#{Bold} failed at #{p[:err].backtrace.join(', ')}" if $1 and not p[:err].backtrace.empty?
+          list
+        }).join("\n")
+      end
       if(topic =~ /^(\S+)\s*(.*)$/)
         key = $1
         params = $2
         if(@@plugins.has_key?(key))
           begin
             return @@plugins[key].help(key, params)
-          rescue TimeoutError, StandardError, NameError, SyntaxError => err
-            puts "plugin #{@@plugins[key].name} help() failed: " + err
-            puts err.backtrace.join("\n")
+          rescue Exception => err
+          #rescue TimeoutError, StandardError, NameError, SyntaxError => err
+            error "plugin #{@@plugins[key].name} help() failed: #{err.class}: #{err}"
+            error err.backtrace.join("\n")
           end
         else
           return false
         end
       end
     end
-    
+
     # see if each plugin handles +method+, and if so, call it, passing
     # +message+ as a parameter
     def delegate(method, *args)
@@ -267,9 +329,10 @@ module Plugins
         if(p.respond_to? method)
           begin
             p.send method, *args
-          rescue TimeoutError, StandardError, NameError, SyntaxError => err
-            puts "plugin #{p.name} #{method}() failed: " + err
-            puts err.backtrace.join("\n")
+          rescue Exception => err
+            #rescue TimeoutError, StandardError, NameError, SyntaxError => err
+            error "plugin #{p.name} #{method}() failed: #{err.class}: #{err}"
+            error err.backtrace.join("\n")
           end
         end
       }
@@ -284,9 +347,10 @@ module Plugins
           @@bot.auth.allow?(m.plugin, m.source, m.replyto))
         begin
           @@plugins[m.plugin].privmsg(m)
-        rescue TimeoutError, StandardError, NameError, SyntaxError => err
-          puts "plugin #{@@plugins[m.plugin].name} privmsg() failed: " + err
-          puts err.backtrace.join("\n")
+        rescue Exception => err
+          #rescue TimeoutError, StandardError, NameError, SyntaxError => err
+          error "plugin #{@@plugins[m.plugin].name} privmsg() failed: #{err.class}: #{err}"
+          error err.backtrace.join("\n")
         end
         return true
       end