summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorTom Gilbert <tom@linuxbrit.co.uk>2005-07-28 23:55:59 +0000
committerTom Gilbert <tom@linuxbrit.co.uk>2005-07-28 23:55:59 +0000
commit8caeee3853ef66dd0e326ff17906f9ca544b8a35 (patch)
tree03faa650efa4ca6ed11bcc4faec1f2ee4ab3a9dc /lib
parentda8e3efa6400c25f4e572c4187a15a37c72af6b8 (diff)
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
Diffstat (limited to 'lib')
-rw-r--r--lib/rbot/config.rb1
-rw-r--r--lib/rbot/ircbot.rb21
-rw-r--r--lib/rbot/ircsocket.rb71
-rw-r--r--lib/rbot/plugins.rb4
-rw-r--r--lib/rbot/timer.rb136
5 files changed, 155 insertions, 78 deletions
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