diff options
-rw-r--r-- | ChangeLog | 11 | ||||
-rwxr-xr-x | bin/rbot | 14 | ||||
-rw-r--r-- | data/rbot/plugins/cal.rb | 2 | ||||
-rw-r--r-- | data/rbot/plugins/nickserv.rb | 92 | ||||
-rw-r--r-- | data/rbot/templates/levels.rbot | 22 | ||||
-rw-r--r-- | lib/rbot/config.rb | 1 | ||||
-rw-r--r-- | lib/rbot/ircbot.rb | 21 | ||||
-rw-r--r-- | lib/rbot/ircsocket.rb | 71 | ||||
-rw-r--r-- | lib/rbot/plugins.rb | 4 | ||||
-rw-r--r-- | lib/rbot/timer.rb | 136 |
10 files changed, 232 insertions, 142 deletions
@@ -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 @@ -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 diff --git a/data/rbot/plugins/cal.rb b/data/rbot/plugins/cal.rb index 4f28310b..dd1d1538 100644 --- a/data/rbot/plugins/cal.rb +++ b/data/rbot/plugins/cal.rb @@ -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) diff --git a/data/rbot/plugins/nickserv.rb b/data/rbot/plugins/nickserv.rb index 1ef2baf7..246f253c 100644 --- a/data/rbot/plugins/nickserv.rb +++ b/data/rbot/plugins/nickserv.rb @@ -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' diff --git a/data/rbot/templates/levels.rbot b/data/rbot/templates/levels.rbot index 2d11c2df..ce338e3b 100644 --- a/data/rbot/templates/levels.rbot +++ b/data/rbot/templates/levels.rbot @@ -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 diff --git a/lib/rbot/config.rb b/lib/rbot/config.rb index e881774f..e93af811 100644 --- a/lib/rbot/config.rb +++ b/lib/rbot/config.rb @@ -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'] = "" diff --git a/lib/rbot/ircbot.rb b/lib/rbot/ircbot.rb index e211ca53..51f323b3 100644 --- a/lib/rbot/ircbot.rb +++ b/lib/rbot/ircbot.rb @@ -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 diff --git a/lib/rbot/ircsocket.rb b/lib/rbot/ircsocket.rb index 35857736..af605e37 100644 --- a/lib/rbot/ircsocket.rb +++ b/lib/rbot/ircsocket.rb @@ -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 diff --git a/lib/rbot/plugins.rb b/lib/rbot/plugins.rb index 1a66b7d3..8d9dcfc9 100644 --- a/lib/rbot/plugins.rb +++ b/lib/rbot/plugins.rb @@ -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 } diff --git a/lib/rbot/timer.rb b/lib/rbot/timer.rb index 64b060ba..64324b6a 100644 --- a/lib/rbot/timer.rb +++ b/lib/rbot/timer.rb @@ -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 |