From 8caeee3853ef66dd0e326ff17906f9ca544b8a35 Mon Sep 17 00:00:00 2001 From: Tom Gilbert Date: Thu, 28 Jul 2005 23:55:59 +0000 Subject: Thu Jul 28 23:45:26 BST 2005 Tom Gilbert * 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 --- lib/rbot/config.rb | 1 + lib/rbot/ircbot.rb | 21 ++++---- lib/rbot/ircsocket.rb | 71 +++++++++++++------------- lib/rbot/plugins.rb | 4 +- lib/rbot/timer.rb | 136 +++++++++++++++++++++++++++++++++++++++----------- 5 files changed, 155 insertions(+), 78 deletions(-) (limited to 'lib') 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 -- cgit v1.2.3