summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog11
-rwxr-xr-xbin/rbot14
-rw-r--r--data/rbot/plugins/cal.rb2
-rw-r--r--data/rbot/plugins/nickserv.rb92
-rw-r--r--data/rbot/templates/levels.rbot22
-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
10 files changed, 232 insertions, 142 deletions
diff --git a/ChangeLog b/ChangeLog
index 30d2390c..fcb65aac 100644
--- 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
diff --git a/bin/rbot b/bin/rbot
index 962f3d0d..f65cb949 100755
--- 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
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