]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/commitdiff
Thu Jul 28 23:45:26 BST 2005 Tom Gilbert <tom@linuxbrit.co.uk>
authorTom Gilbert <tom@linuxbrit.co.uk>
Thu, 28 Jul 2005 23:55:59 +0000 (23:55 +0000)
committerTom Gilbert <tom@linuxbrit.co.uk>
Thu, 28 Jul 2005 23:55:59 +0000 (23:55 +0000)
  * Reworked the Timer module. The Timer now has a smart thread manager to
start/stop the tick() thread. This means the timer isn't called every 0.1
seconds to see what needs doing, which is much more efficient
  * reworked the ircsocket queue mechanism to use a Timer
* reworked the nickserv plugin to use maps
* made server.reconnect_wait configurable
* added Class tracing mechanism to bin/rbot, use --trace Classname for
debugging

ChangeLog
bin/rbot
data/rbot/plugins/cal.rb
data/rbot/plugins/nickserv.rb
data/rbot/templates/levels.rbot
lib/rbot/config.rb
lib/rbot/ircbot.rb
lib/rbot/ircsocket.rb
lib/rbot/plugins.rb
lib/rbot/timer.rb

index 30d2390c95520b188cad26ba45757f67f0794c89..fcb65aacfab2a47a7f543c81efe6defd2cb3c491 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,14 @@
+Thu Jul 28 23:45:26 BST 2005  Tom Gilbert <tom@linuxbrit.co.uk>
+
+  * Reworked the Timer module. The Timer now has a smart thread manager to
+       start/stop the tick() thread. This means the timer isn't called every 0.1
+       seconds to see what needs doing, which is much more efficient
+  * reworked the ircsocket queue mechanism to use a Timer
+       * reworked the nickserv plugin to use maps
+       * made server.reconnect_wait configurable
+       * added Class tracing mechanism to bin/rbot, use --trace Classname for
+       debugging
+
 Tue Jul 26 14:41:34 BST 2005  Tom Gilbert <tom@linuxbrit.co.uk>
 
   * Prevent multiple plugin registrations of the same name
index 962f3d0d992c0e449c7e01f88a174f9c5a559683..f65cb949875b0270663f59a11b6ef7ca2a3a26b0 100755 (executable)
--- a/bin/rbot
+++ b/bin/rbot
@@ -35,7 +35,7 @@ rescue LoadError => e
   exit 2
 end
   
-$debug = true
+$debug = false
 $version="0.9.8"
 $opts = Hash.new
 
@@ -47,7 +47,8 @@ end
 
 opts = GetoptLong.new(
   ["--debug", "-d", GetoptLong::NO_ARGUMENT],
-  ["--help",  "-h", GetoptLong::OPTIONAL_ARGUMENT],
+  ["--help",  "-h", GetoptLong::NO_ARGUMENT],
+  ["--trace",  "-t", GetoptLong::REQUIRED_ARGUMENT],
   ["--version", "-v", GetoptLong::NO_ARGUMENT]
 )
 
@@ -56,6 +57,15 @@ opts.each {|opt, arg|
   $opts[opt.sub(/^-+/, "")] = arg
 }
 
+if ($opts["trace"])
+  set_trace_func proc { |event, file, line, id, binding, classname|
+    if classname.to_s == $opts["trace"]
+      printf "TRACE: %8s %s:%-2d %10s %8s\n", event, File.basename(file), line, id, classname
+    end
+  }
+end
+
+
 if ($opts["version"])
   puts "rbot #{$version}"
   exit 0
index 4f28310b60f640cd721eb0398302fb16e92eed98..dd1d1538a143826f4e010501655ba027a17f91e8 100644 (file)
@@ -1,6 +1,6 @@
 class CalPlugin < Plugin
   def help(plugin, topic="")
-    "cal [options] => show current calendar [unix cal options]"
+    "cal [month year] => show current calendar [optionally specify month and year]"
   end
   def cal(m, params)
     if params.has_key?(:month)
index 1ef2baf7fdfff8cfcb0e7d596ffff97267557560..246f253ca3ba8dfad4a072fbc035729c2a2d9cb1 100644 (file)
@@ -17,6 +17,15 @@ class NickServPlugin < Plugin
     end
   end
   
+  def genpasswd
+    # generate a random password
+    passwd = ""
+    8.times do
+      passwd += (rand(26) + (rand(2) == 0 ? 65 : 97) ).chr
+    end
+    return passwd
+  end
+
   def initialize
     super
     # this plugin only wants to store strings!
@@ -29,49 +38,34 @@ class NickServPlugin < Plugin
       end
     end
   end
-  
-  def privmsg(m)
-    return unless m.params
-    
-    case m.params
-    when (/^password\s*(\S*)\s*(.*)$/)
-      nick = $1
-      passwd = $2
-      @registry[nick] = passwd
-      m.okay
-    when (/^register$/)
-      passwd = genpasswd
-      @bot.sendmsg "PRIVMSG", "NickServ", "REGISTER " + passwd
-      @registry[@bot.nick] = passwd
-      m.okay
-    when (/^register\s*(\S*)\s*(.*)$/)
-      passwd = $1
-      email = $2
-      @bot.sendmsg "PRIVMSG", "NickServ", "REGISTER " + passwd + " " + email
-      @registry[@bot.nick] = passwd
-      m.okay
-    when (/^register\s*(.*)\s*$/)
-      passwd = $1
-      @bot.sendmsg "PRIVMSG", "NickServ", "REGISTER " + passwd
-      @registry[@bot.nick] = passwd
+
+  def password(m, params)
+    @registry[params[:nick]] = params[:passwd]
+    m.okay
+  end
+  def nick_register(m, params)
+    passwd = params[:passwd] ? params[:passwd] : genpasswd
+    message = "REGISTER #{passwd}"
+    message += " #{params[:email]}" if params[:email]
+    @bot.sendmsg "PRIVMSG", "NickServ", message
+    @registry[@bot.nick] = passwd
+    m.okay
+  end
+  def listnicks(m, params)
+    if @registry.length > 0
+      @registry.each {|k,v|
+        @bot.say m.sourcenick, "#{k} => #{v}"
+      }
+    else
+      m.reply "none known"
+    end
+  end
+  def identify(m, params)
+    if @registry.has_key?(@bot.nick)
+      @bot.sendmsg "PRIVMSG", "NickServ", "IDENTIFY #{@registry[@bot.nick]}"
       m.okay
-    when (/^listnicks$/)
-      if @bot.auth.allow?("config", m.source, m.replyto)
-        if @registry.length > 0
-          @registry.each {|k,v|
-            @bot.say m.sourcenick, "#{k} => #{v}"
-          }
-        else
-          m.reply "none known"
-        end
-      end
-    when (/^identify$/)
-      if @registry.has_key?(@bot.nick)
-        @bot.sendmsg "PRIVMSG", "NickServ", "IDENTIFY " + @registry[@bot.nick]
-        m.okay
-      else
-        m.reply "I dunno the nickserv password for the nickname #{@bot.nick} :("
-      end
+    else
+      m.reply "I dunno the nickserv password for the nickname #{@bot.nick} :("
     end
   end
   
@@ -86,14 +80,10 @@ class NickServPlugin < Plugin
     end
   end
 
-  def genpasswd
-    # generate a random password
-    passwd = ""
-    8.times do
-      passwd += (rand(26) + (rand(2) == 0 ? 65 : 97) ).chr
-    end
-    return passwd
-  end
 end
 plugin = NickServPlugin.new
-plugin.register("nickserv")
+plugin.map 'nickserv password :nick :passwd'
+plugin.map 'nickserv register :passwd :email', :action => 'nick_register',
+           :defaults => {:passwd => false, :email => false}
+plugin.map 'nickserv listnicks'
+plugin.map 'nickserv identify'
index 2d11c2df8cca7004d6664c961c4ed832b9aa5d53..ce338e3be8b46739923a17d5444cb29e105ec7d5 100644 (file)
@@ -1,21 +1,23 @@
-70 say
 100 auth
-50 part
+90 quit
 85 config
 80 nick
+80 nickserv
+80 http
+70 opmeh
+70 say
+50 part
 50 join
+15 delquote
 12 msginsult
+12 remind+
+5 rmlart
+5 rmpraise
 5 keycmd
 5 lart
 5 addlart
-10 rmlart
 5 addpraise
-10 rmpraise
-5 addquote
-12 remind+
-5 getquote
-90 quit
 5 remind
 5 keyword
-15 delquote
-70 opmeh
+5 addquote
+5 getquote
index e881774f7cff9e381b283fdf6514fbe0f9d43645..e93af811e51b00129db9bc9862bf0b58d09ed155 100644 (file)
@@ -22,6 +22,7 @@ module Irc
       @config['server.port'] = 6667
       @config['server.password'] = false
       @config['server.bindhost'] = false
+      @config['server.reconnect_wait'] = 5
       @config['irc.nick'] = "rbot"
       @config['irc.user'] = "rbot"
       @config['irc.join_channels'] = ""
index e211ca5309703ad9c61c84acddc43b310e2af099..51f323b37d83c15fb6fe49b8a481f03237796ccb 100644 (file)
@@ -81,7 +81,7 @@ class IrcBot
   # create a new IrcBot with botclass +botclass+
   def initialize(botclass)
     unless FileTest.directory? Config::DATADIR
-      puts "no data directory '#{Config::DATADIR}' found, did you run install.rb?"
+      puts "data directory '#{Config::DATADIR}' not found, did you install.rb?"
       exit 2
     end
     
@@ -97,11 +97,11 @@ class IrcBot
       FileUtils.cp_r Config::DATADIR+'/templates', botclass
     end
     
-    Dir.mkdir("#{botclass}/logs") if(!File.exist?("#{botclass}/logs"))
+    Dir.mkdir("#{botclass}/logs") unless File.exist?("#{botclass}/logs")
 
     @startup_time = Time.new
     @config = Irc::BotConfig.new(self)
-    @timer = Timer::Timer.new
+    @timer = Timer::Timer.new(1.0) # only need per-second granularity
     @registry = BotRegistry.new self
     @timer.add(@config['core.save_every']) { save } if @config['core.save_every']
     @channels = Hash.new
@@ -111,6 +111,8 @@ class IrcBot
     @lang = Irc::Language.new(@config['core.language'])
     @keywords = Irc::Keywords.new(self)
     @auth = Irc::IrcAuth.new(self)
+
+    Dir.mkdir("#{botclass}/plugins") unless File.exist?("#{botclass}/plugins")
     @plugins = Irc::Plugins.new(self, ["#{botclass}/plugins"])
 
     @socket = Irc::IrcSocket.new(@config['server.name'], @config['server.port'], @config['server.bindhost'], @config['server.sendq_delay'], @config['server.sendq_burst'])
@@ -203,7 +205,7 @@ class IrcBot
 
       if(@config['irc.join_channels'])
         @config['irc.join_channels'].split(", ").each {|c|
-          puts "autojoining channel #{c}"
+          debug "autojoining channel #{c}"
           if(c =~ /^(\S+)\s+(\S+)$/i)
             join $1, $2
           else
@@ -278,24 +280,21 @@ class IrcBot
       raise "failed to connect to IRC server at #{@config['server.name']} #{@config['server.port']}: " + e
     end
     @socket.puts "PASS " + @config['server.password'] if @config['server.password']
-    @socket.puts "NICK #{@nick}\nUSER #{@config['server.user']} 4 #{@config['server.name']} :Ruby bot. (c) Tom Gilbert"
+    @socket.puts "NICK #{@nick}\nUSER #{@config['irc.user']} 4 #{@config['server.name']} :Ruby bot. (c) Tom Gilbert"
   end
 
   # begin event handling loop
   def mainloop
-    socket_timeout = 0.2
-    reconnect_wait = 5
-    
     while true
       connect
+      @timer.start
       
       begin
         while true
-          if @socket.select socket_timeout
+          if @socket.select
             break unless reply = @socket.gets
             @client.process reply
           end
-          @timer.tick
         end
       rescue => e
         puts "connection closed: #{e}"
@@ -307,7 +306,7 @@ class IrcBot
       @socket.clearq
       
       puts "waiting to reconnect"
-      sleep reconnect_wait
+      sleep @config['server.reconnect_wait']
     end
   end
   
index 358577369a752a253947a3e84d8a050ea6457290..af605e371962eedd69bfd19480751d2153d8d875 100644 (file)
@@ -2,6 +2,7 @@ module Irc
 
   require 'socket'
   require 'thread'
+  require 'rbot/timer'
 
   # wrapped TCPSocket for communication with the server.
   # emulates a subset of TCPSocket functionality
@@ -23,9 +24,14 @@ module Irc
     # host::   optional local host to bind to (ruby 1.7+ required)
     # create a new IrcSocket
     def initialize(server, port, host, sendq_delay=2, sendq_burst=4)
+      @timer = Timer::Timer.new
+      @timer.add(0.2) do
+        spool
+      end
       @server = server.dup
       @port = port.to_i
       @host = host
+      @spooler = false
       @lines_sent = 0
       @lines_received = 0
       if sendq_delay
@@ -60,21 +66,17 @@ module Irc
       @qthread = false
       @qmutex = Mutex.new
       @sendq = Array.new
-      if (@sendq_delay > 0)
-        @qthread = Thread.new { spooler }
-      end
     end
 
     def sendq_delay=(newfreq)
       debug "changing sendq frequency to #{newfreq}"
       @qmutex.synchronize do
         @sendq_delay = newfreq
-        if newfreq == 0 && @qthread
+        if newfreq == 0
           clearq
-          Thread.kill(@qthread)
-          @qthread = false
-        elsif(newfreq != 0 && !@qthread)
-          @qthread = Thread.new { spooler }
+          @timer.stop
+        else
+          @timer.start
         end
       end
     end
@@ -98,9 +100,7 @@ module Irc
     def gets
       reply = @sock.gets
       @lines_received += 1
-      if(reply)
-        reply.strip!
-      end
+      reply.strip! if reply
       debug "RECV: #{reply.inspect}"
       reply
     end
@@ -108,42 +108,41 @@ module Irc
     def queue(msg)
       if @sendq_delay > 0
         @qmutex.synchronize do
-          # debug "QUEUEING: #{msg}"
           @sendq.push msg
         end
+        @timer.start
       else
         # just send it if queueing is disabled
         self.puts(msg)
       end
     end
 
-    def spooler
-      while true
-        spool
-        sleep 0.2
-      end
-    end
-
     # pop a message off the queue, send it
     def spool
-      unless @sendq.empty?
-        now = Time.new
-        if (now >= (@last_send + @sendq_delay))
-          # reset burst counter after @sendq_delay has passed
-          @burst = 0
-          debug "in spool, resetting @burst"
-        elsif (@burst >= @sendq_burst)
-          # nope. can't send anything
-          return
-        end
-        @qmutex.synchronize do
-          debug "(can send #{@sendq_burst - @burst} lines, there are #{@sendq.length} to send)"
-          (@sendq_burst - @burst).times do
-            break if @sendq.empty?
-            puts_critical(@sendq.shift)
-          end
+      if @sendq.empty?
+        @timer.stop
+        return
+      end
+      now = Time.new
+      if (now >= (@last_send + @sendq_delay))
+        # reset burst counter after @sendq_delay has passed
+        @burst = 0
+        debug "in spool, resetting @burst"
+      elsif (@burst >= @sendq_burst)
+        # nope. can't send anything, come back to us next tick...
+        @timer.start
+        return
+      end
+      @qmutex.synchronize do
+        debug "(can send #{@sendq_burst - @burst} lines, there are #{@sendq.length} to send)"
+        (@sendq_burst - @burst).times do
+          break if @sendq.empty?
+          puts_critical(@sendq.shift)
         end
       end
+      if @sendq.empty?
+        @timer.stop
+      end
     end
 
     def clearq
@@ -160,7 +159,7 @@ module Irc
     end
 
     # Wraps Kernel.select on the socket
-    def select(timeout)
+    def select(timeout=nil)
       Kernel.select([@sock], nil, nil, timeout)
     end
 
index 1a66b7d3250ef4fe120a7daa4b1f888818368b74..8d9dcfc9f9736ca5ba25243b4eb1997b9571f5ad 100644 (file)
@@ -185,10 +185,10 @@ module Irc
             
             begin
               plugin_string = IO.readlines(@tmpfilename).join("")
-              puts "loading module: #{@tmpfilename}"
+              debug "loading module: #{@tmpfilename}"
               plugin_module.module_eval(plugin_string)
             rescue StandardError, NameError, LoadError, SyntaxError => err
-              puts "plugin #{@tmpfilename} load failed: " + err
+              puts "warning: plugin #{@tmpfilename} load failed: " + err
               puts err.backtrace.join("\n")
             end
           }
index 64b060bae65814a4cfe01e5f6f645fa15e6b5e72..64324b6af8faf1e6471cb812487ae1fd91e869b0 100644 (file)
@@ -4,11 +4,11 @@ module Timer
   class Action
     
     # when this action is due next (updated by tick())
-    attr_accessor :in
+    attr_reader :in
     
     # is this action blocked? if so it won't be run
     attr_accessor :blocked
-    
+
     # period:: how often (seconds) to run the action
     # data::   optional data to pass to the proc
     # once::   optional, if true, this action will be run once then removed
@@ -22,11 +22,29 @@ module Timer
       @func = func
       @data = data
       @once = once
+      @last_tick = Time.new
+    end
+
+    def tick
+      diff = Time.new - @last_tick
+      @in -= diff
+      @last_tick = Time.new
+    end
+
+    def inspect 
+      "#<#{self.class}:#{@period}s:#{@once ? 'once' : 'repeat'}>"
+    end
+
+    def due?
+      @in <= 0
     end
 
     # run the action by calling its proc
     def run
       @in += @period
+      # really short duration timers can overrun and leave @in negative,
+      # for these we set @in to @period
+      @in = @period if @in <= 0
       if(@data)
         @func.call(@data)
       else
@@ -39,14 +57,19 @@ module Timer
   # timer handler, manage multiple Action objects, calling them when required.
   # The timer must be ticked by whatever controls it, i.e. regular calls to
   # tick() at whatever granularity suits your application's needs.
-  # Alternatively you can call run(), and the timer will tick itself, but this
-  # blocks so you gotta do it in a thread (remember ruby's threads block on
-  # syscalls so that can suck).
+  # 
+  # Alternatively you can call run(), and the timer will spawn a thread and
+  # tick itself, intelligently shutting down the thread if there are no
+  # pending actions.
   class Timer
-    def initialize
-      @timers = Array.new
+    def initialize(granularity = 0.1)
+      @granularity = granularity
+      @timers = Hash.new
       @handle = 0
       @lasttime = 0
+      @should_be_running = false
+      @thread = false
+      @next_action_time = 0
     end
     
     # period:: how often (seconds) to run the action
@@ -57,10 +80,11 @@ module Timer
     def add(period, data=nil, &func)
       @handle += 1
       @timers[@handle] = Action.new(period, data, &func)
+      start_on_add
       return @handle
     end
 
-    # period:: how often (seconds) to run the action
+    # period:: how long (seconds) until the action is run
     # data::   optional data to pass to the action's proc
     # func::   associate a block with add() to perform the action
     # 
@@ -68,12 +92,13 @@ module Timer
     def add_once(period, data=nil, &func)
       @handle += 1
       @timers[@handle] = Action.new(period, data, true, &func)
+      start_on_add
       return @handle
     end
 
     # remove action with handle +handle+ from the timer
     def remove(handle)
-      @timers.delete_at(handle)
+      @timers.delete(handle)
     end
     
     # block action with handle +handle+
@@ -85,39 +110,92 @@ module Timer
     def unblock(handle)
       @timers[handle].blocked = false
     end
-    
+
     # you can call this when you know you're idle, or you can split off a
     # thread and call the run() method to do it for you.
     def tick 
-      if(@lasttime != 0)
-        diff = (Time.now - @lasttime).to_f
-        @lasttime = Time.now
-        @timers.compact.each { |timer|
-          timer.in = timer.in - diff
-        }
-        @timers.compact.each { |timer|
-          if (!timer.blocked)
-            if(timer.in <= 0)
-              if(timer.run)
-                # run once
-                @timers.delete(timer)
-              end
-            end
-          end
-        }
-      else
+      if(@lasttime == 0)
         # don't do anything on the first tick
         @lasttime = Time.now
+        return
       end
+      @next_action_time = 0
+      diff = (Time.now - @lasttime).to_f
+      @lasttime = Time.now
+      @timers.each { |key,timer|
+        timer.tick
+        next if timer.blocked
+        if(timer.due?)
+          if(timer.run)
+            # run once
+            @timers.delete(key)
+          end
+        end
+        if @next_action_time == 0 || timer.in < @next_action_time
+          @next_action_time = timer.in
+        end
+      }
     end
 
-    # the timer will tick() itself. this blocks, so run it in a thread, and
-    # watch out for blocking syscalls
+    # for backwards compat - this is a bit primitive
     def run(granularity=0.1)
       while(true)
         sleep(granularity)
         tick
       end
     end
+
+    def running?
+      @thread && @thread.alive?
+    end
+
+    # return the number of seconds until the next action is due, or 0 if
+    # none are outstanding - will only be accurate immediately after a
+    # tick()
+    def next_action_time
+      @next_action_time
+    end
+
+    # start the timer, it spawns a thread to tick the timer, intelligently
+    # shutting down if no events remain and starting again when needed.
+    def start
+      return if running?
+      @should_be_running = true
+      start_thread unless @timers.empty?
+    end
+
+    # stop the timer from ticking
+    def stop
+      @should_be_running = false
+      stop_thread
+    end
+    
+    private
+    
+    def start_on_add
+      if running?
+        stop_thread
+        start_thread
+      elsif @should_be_running
+        start_thread
+      end
+    end
+    
+    def stop_thread
+      return unless running?
+      @thread.kill
+    end
+    
+    def start_thread
+      return if running?
+      @thread = Thread.new do
+        while(true)
+          tick
+          exit if @timers.empty?
+          sleep(@next_action_time)
+        end
+      end
+    end
+
   end
 end