Permalink
Browse files

initial rubuildius commit, read tools/rubuildius/README

  • Loading branch information...
1 parent 346abe2 commit 23f4753ff2192fce1885607413f005a3d59664e1 @bandresen bandresen committed Jan 24, 2008
View
26 tools/rubuildius/README
@@ -0,0 +1,26 @@
+=== rubuildius zsh hack ===
+
+ based on matzbot and
+ plugins/git.rb is the only modified file
+
+Currently most files are hardcoded for my box
+
+== relevant information
+
+* interesting files:
+ + bin/*
+ + matzbot/plugins/git.rb
+ + repo/rubinius/HEAD [empty-dir]
+
+* everything needs to be in ~/continious
+
+* continious/repo/rubinius/HEAD should contain a git checkout.
+ + this one will be updated and then cloned to speed things up and not eat bandwidth
+
+* I launch it like this:
+ + ./launch.rb -u rubinius -n rubuildius -m 'rubinius_build_bot' -c rubinius
+
+* gets usr2 signal via cronjob
+ + */5 * * * * kill -USR2 `cat HOME/continuous/matzbot/matzbot.pid`
+
+* occasionally I run bin/cleanup.zsh to get save some space
View
15 tools/rubuildius/bin/cleanup.zsh
@@ -0,0 +1,15 @@
+#!/bin/zsh
+
+setopt extendedglob
+
+cd $1
+
+integer ndirs
+dirs=(*~HEAD(/N))
+(( ndirs = ${#dirs} ))
+
+# if there are more than 20 dirs, delete the last 10 of them.
+# be sure to never delete HEAD accidently.
+[[ $ndirs -gt 20 ]] && rm -rf *~HEAD(D/om[-11,-1])
+
+exit 0
View
64 tools/rubuildius/bin/rubinius.zsh
@@ -0,0 +1,64 @@
+#!/bin/zsh
+
+trap '' HUP
+trap '' ABRT
+
+[[ -z $1 ]] && exit
+unset RUBYOPT # so that rubinius builds
+
+root=~/continuous
+#pastie=$root/bin/pastie.rb
+pastie=nopaste
+rubinius=$root/repo/rubinius
+head=$rubinius/HEAD
+thischeckout=$rubinius/$1
+
+cd $head
+git pull &>/dev/null
+
+rm -rf $thischeckout &>/dev/null
+git clone $head $thischeckout &>/dev/null
+cd $thischeckout
+git checkout $1 2>/dev/null
+
+
+rake build &>build_log
+# did it build?
+if [[ $? -ne 0 ]]; then
+ echo "build failed! `cat build_log | $pastie`"
+ exit 1
+fi
+
+# version and stuff
+./shotgun/rubinius -v -e 'true' &>ci_log
+# a bit of a workaround because ci_log can exec
+./bin/ci -f m &>>ci_log
+
+#sleep 5
+#old=$(stat -c '%Y' ci_log)
+#sleep 10
+
+#while [[ $old != $(stat -c '%Y' ci_log) ]]; do
+# old=$(stat -c '%Y' ci_log)
+# sleep 5
+#done
+
+tail -1 ci_log | grep expectations &>/dev/null
+
+# did it pass the spec?
+if [[ $? -ne 0 ]]; then
+ echo "bin/ci failed! `cat -v ci_log | sed -e 's/\^\[\[0;3[14]m//' -e 's/\^\[\[0m//' | $pastie`"
+else
+ failures=$(tail -1 ci_log | cut -d' ' -f 5)
+ if [[ $failures -gt 0 ]]; then
+ echo "$(tail -1 ci_log); $(cat -v ci_log | sed -e 's/\^\[\[0;3[14]m//' -e 's/\^\[\[0m//' | $pastie)"
+ else
+ echo "$(tail -1 ci_log)"
+ fi
+fi
+
+# save some space, until it gets cleaned.
+#rake distclean &>/dev/null
+#rm -rf .git &>/dev/null
+
+exit 0
View
61 tools/rubuildius/matzbot/README
@@ -0,0 +1,61 @@
+**********************
+ Plugin API Howto
+ (so we don't forget)
+**********************
+
+Typical plugin:
+
+ module MatzBot::Commands
+ needs_gem 'hpricot' => [ :tinyurl, :get_tinyurl ]
+
+ def tinyurl(data)
+ say "Here it is! #{get_tinyurl(data.first)}"
+ rescue
+ say "No, no that's not gonna work."
+ end
+
+ private
+ def get_tinyurl(url)
+ return if url.empty?
+
+ res = Net::HTTP.post_form(URI.parse('http://tinyurl.com/create.php'), { 'url' => url })
+ doc = Hpricot(res.body)
+ ((doc/:blockquote).last/:b).innerHTML
+ end
+ end
+
+- New methods added to MatzBot::Commands show up as commands
+
+- If you want to add methods which aren't bot commands, make them protected or private.
+
+- Methods must accept one argument, an array of words said after the command.
+
+- If you need to require a gem, use needs_gem like above. Pass it the gem name and all
+ the methods that depend on it.
+
+- If you need to keep around data, use the `session' hash.
+ session[:my_plugin] = true
+ session[:my_plugin]
+
+- You can use `reply' like say, except it will prefix your command with the user's name.
+ <defunkt> hey bot
+ <matzbot> defunkt: hey # => using reply 'hey'
+
+- You can use `pm' to send a private message to whoever made a request of you. Use it like `say.'
+
+- The above plugin would be triggered like this:
+ <irc_dude> tinyurl http://myurl.com
+ <matzbot> Here it is! http://tinyurl.com/fz3
+
+- You get these methods in the Commands API:
+ - puts(string to say)
+ - reply(string to say)
+ - pm(sting to pm) [your bot must be ident'd on most networks for this to work]
+ - action(string to emote)
+ - config -- hash of runtime configuration options
+ - socket -- direct access to the socket, if you need it
+
+- Put plugins in ~/.matzbot and name them whatever.rb.
+
+=> Evan Weaver && Chris Wanstrath
+>> %w[ http://blog.evanweaver.com http://errtheblog.com ]
View
7 tools/rubuildius/matzbot/cron.rb
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+
+matzdir = "~/matzbot/"
+
+if `ps -A | grep \`cat #{matzdir}matzbot.pid\` | wc`.to_i.zero?
+ system("cd #{matzdir}; rm matzbot.pid; ./launch.rb -d -w `cat the_password`")
+end
View
69 tools/rubuildius/matzbot/launch.rb
@@ -0,0 +1,69 @@
+#!/usr/bin/env ruby
+
+$:.unshift "lib"
+require 'rubygems'
+require 'choice'
+require_gem 'choice', '>= 0.1.2'
+require 'matzbot'
+
+Choice.options do
+ header ""
+ header "Specific options:"
+
+ options :server => {
+ :short => '-s', :long => '--server SERVER',
+ :desc => ["The irc server to connect to.", "(default irc.freenode.org)"], :default => 'irc.freenode.org'
+ },
+
+ :port => {
+ :short => '-p', :long => '--port PORT', :desc => ["The irc server's port.", "(default 6667)"],
+ :default => 6667, :cast => Integer
+ },
+
+ :nick => {
+ :short => '-n', :long => '--nick NICK', :desc => ["The irc bot's nick.", "(default matz)"],
+ :default => 'matz'
+ },
+
+ :password => {
+ :short => '-w', :long => '--password PASSWORD', :desc => "Your nick's password, if registered."
+ },
+
+ :channel => {
+ :short => '-c', :long => '--channel CHANNEL', :desc => ["The channel to join on connect -- without a hash.", "(default polymorphs)"],
+ :default => 'polymorphs'
+ },
+
+ :user => {
+ :short => '-u', :long => '--user USER', :desc => ["The irc bot's username.", "(default matz)"], :default => 'matz'
+ },
+
+ :name => {
+ :short => '-m', :long => '--name NAME', :desc => ["The irc bot's name.", "(default Matz)"], :default => 'Matz'
+ },
+
+ :daemonize => {
+ :short => '-d', :long => '--daemon',
+ :desc => ["Run as a daemon - pass it nothing, start, stop, or restart.", "If you pass nothing, `start' is assumed."]
+ },
+
+ :only_when_addressed => {
+ :short => '-a', :long => '--addressed',
+ :desc => ["Only speak if being addressed.", "(default false)"]
+ }
+end
+
+MatzBot::Session.load unless Choice.choices[:daemonize] == 'stop'
+
+require 'daemon'
+
+if Choice.choices[:daemonize]
+ MatzBot::Daemonize.daemonize(Choice.choices.merge('daemonize' => Choice.choices[:daemonize] == 'stop' ? 'stop' : 'start'))
+else
+ MatzBot::Daemonize.store
+ begin
+ MatzBot::Client.start(Choice.choices)
+ ensure
+ MatzBot::Session.save
+ end
+end
View
156 tools/rubuildius/matzbot/lib/client.rb
@@ -0,0 +1,156 @@
+require 'socket'
+
+module MatzBot
+ module Client
+ extend self
+
+ attr_accessor :config, :socket, :last_nick, :authorized
+
+ def start(options)
+ self.config ||= {}
+ self.config.merge! Hash[*options.map { |k,v| [k.intern, v] }.flatten]
+
+ connect!
+ main_loop
+ end
+
+ def connect!
+ log "Connecting to #{config[:server]}:#{config[:port]}..."
+ self.socket = TCPSocket.new(config[:server], config[:port])
+
+ socket.puts "USER #{config[:user]} #{config[:nick]} #{config[:name]} :#{config[:name]} \r\n"
+ socket.puts "NICK #{config[:nick]} \r\n"
+
+ socket.puts "PRIVMSG NickServ :IDENTIFY #{config[:password]}" if config[:password]
+
+ # channel might not have a # in front of it, so add it
+ config[:channel] = config[:channel][/^#/] ? config[:channel] : '#' + config[:channel]
+ join config[:channel]
+ end
+
+ def reconnect!
+ socket.close
+ self.socket = nil
+ start
+ end
+
+ def main_loop
+ while true
+ if IO.select([socket])
+ react_to socket.gets
+ end
+ end
+ #socket.each do |line|
+ # react_to line # its own method so we can change it on the fly, without hurting the loop
+ #end
+ end
+
+ def react_to(line)
+ begin
+ MatzBot.reload!
+ rescue Exception => bang
+ say "Class load error #{bang.class} (#{caller[1]}), #{bang}."
+ end
+
+ self.authorized = false # not authorized
+
+ info = grab_info(line) # grabs the info from an PRIVMSG
+ puts line # puts to the console
+
+ pong(line) if line[0..3] == "PING" # keep-alive
+
+ if info && info.last # only called if grabbing the info was successful
+ log_message info # logs in a friendly format, in chat.txt
+ execute(info.last, info.first) if info
+ elsif has_error?(line)
+ reconnect!
+ end
+ end
+
+ def has_error?(line)
+ log "Error from server: #{line}" and return true if line[/^ERROR/]
+ end
+
+ def execute(cmd, nick)
+ data = cmd.split
+ return false unless data && data.first && authorize?(data)
+
+ self.last_nick = nick
+
+ data.join(' ').split(' then ').each do |command|
+ # callbacks
+ filters(:listen).each do |filter|
+ filter.call(command) if command
+ end if filters(:listen).size.nonzero?
+
+ command = command.split(' ')
+ command.shift if command.first =~ /^#{config[:nick]}/i
+
+ if Commands.methods.include? command.first and !(EmptyModule.methods.include? command.first)
+ Commands.send(command.first, command[1..-1])
+ #else
+ # say "no command #{command}"
+ end
+ end
+ rescue Exception => bang
+ say "Command error #{bang.class}, #{bang}."
+ say " #{bang.backtrace.first}"
+ end
+
+ def say(message)
+ Commands.say message
+ end
+
+ def filters(type)
+ Commands.send(:filters, type)
+ end
+
+ def pong(line)
+ line[0..3] = "PONG"
+ socket.puts "#{line}"
+ puts "#{line}"
+ Commands.poll(line) if Commands.methods.include? 'poll'
+ end
+
+ def grab_info(text)
+ # The following is the format of what the bot recieves:
+ # :kyle!~kyle@X-24735511.lmdaca.adelphia.net PRIVMSG #youngcoders :for the most part
+ # :nick!~ident@host PRIVMSG #channel :message
+ text =~ /^\:(.+)\!\~?(.+)\@(.+) PRIVMSG \#?(\w+) \:(.+)/ ? [$1, $2, $3, $4, $5] : false
+ end
+
+ def authorize?(data)
+ if self.config[:only_when_addressed] and data.first != "#{self.config[:nick]}:"
+ return false
+ end
+
+ command, password = data.first, data[1]
+ if Commands.protected_instance_methods(false).include? command
+ self.authorized = config[:password] && password == config[:password]
+ data.delete_at(1)
+ authorized
+ else true
+ end
+ end
+
+ def join(channel, quit_prev = true)
+ socket.puts "PART #{config[:channel]}" if quit_prev
+ socket.puts "JOIN #{channel} \r\n"
+ config[:channel] = channel
+ end
+
+ def log_message(array)
+ log "<#{array[0]}> : #{array[4]}"
+ end
+
+ def log(string)
+ File.open("chat.txt", "a") do |f|
+ f.puts "[#{Time.new.strftime "%m/%d/%Y %I:%M %p PST"}] #{string} \n"
+ end
+ end
+ end
+end
+
+module EmptyModule
+end
+
View
140 tools/rubuildius/matzbot/lib/commands.rb
@@ -0,0 +1,140 @@
+require 'set'
+require 'yaml'
+
+module MatzBot
+ module Commands
+ extend self
+
+ def help(data)
+ config[:hidden_methods] ||= []
+ hidden = config[:hidden_methods].map { |m| m.to_s }
+ if is_admin?
+ commands = protected_instance_methods(false) - hidden
+ command = :pm
+ else
+ commands = public_instance_methods(false) - hidden
+ command = :say
+ end
+
+ send(command, "Commands I know: \0037 #{commands.sort * ', '}")
+ end
+
+ def action(data)
+ data = [data].flatten
+ socket.puts "PRIVMSG #{config[:channel]} :\01ACTION #{data * ' '}\01"
+ sleep 1
+ end
+
+ def say(data, channel = config[:channel])
+ data = data.join(" ") if data.is_a?(Array)
+ data.split("\n").each do |message|
+ message = filters(:say).inject(message) do |string, filter|
+ filter.call(string)
+ end if filters(:say).size.nonzero?
+ while message
+ fragment, message = message[0..450], message[450..-1]
+ socket.puts "PRIVMSG #{channel} :#{fragment}"
+ end
+ sleep 1
+ end
+ nil
+ end
+ alias :puts :say
+ private :puts
+
+ def save!(data)
+ Session.save
+ puts "Saved session data. ;)"
+ end
+
+ protected
+ def quit(data)
+ socket.puts "QUIT :#{data.shift}"
+ exit
+ end
+
+ def update_nick(data)
+ socket.puts "NICK :#{nick = data.shift}"
+ config[:nick] = nick
+ end
+
+ private
+ def session
+ Session.session
+ end
+
+ def reply(string)
+ say "#{Client.last_nick}: #{string}" if Client.last_nick
+ end
+
+ def pm(data, nick = Client.last_nick)
+ say(data, nick)
+ end
+
+ # filter :say => :drunk_say
+ # filter :listen => :google_translate
+ def filter(type_and_methods)
+ type = type_and_methods.keys.first
+ Array(type_and_methods[type]).each do |method|
+ filters(type) <<
+ case method
+ when Symbol, String then proc { |message| send(method, message) }
+ when Proc then method
+ end
+ end
+ end
+
+ def filters(type)
+ $filters ||= {}
+ $filters[type.to_sym] ||= Set.new
+ end
+
+ def reload_filters!
+ $filters.clear if $filters
+ end
+
+ def help_method(options = {})
+ # help_method :svn => [ :add_repo, :clear_repos ]
+ options.each do |method, wraps|
+ define_method(method) do |*data|
+ wraps.map! { |m| m.to_s }
+ command = :pm
+ unless is_admin?
+ wraps -= protected_instance_methods(false)
+ command = :say
+ end
+ send(command, "Commands for `#{method}': #{wraps.sort * ', '}")
+ end
+ config[:hidden_methods] ||= Set.new
+ config[:hidden_methods] += wraps.map { |m| m.to_s }
+ end
+ end
+
+ def socket
+ Client.socket
+ end
+
+ def config
+ Client.config ||= {}
+ end
+
+ def is_admin?
+ Client.authorized
+ end
+
+ def needs_gem(hash)
+ # needs_gem 'hpricot' => [ :method1, :method2 ]
+ config[:failed_methods] ||= Set.new
+ gem = ''
+ hash.keys.each { |gem| require gem }
+ rescue LoadError
+ config[:failed_methods] += Array(hash[gem])
+ end
+
+ def method_added(method)
+ config[:failed_methods] ||= Set.new
+ remove_method method if config[:failed_methods].include?(method)
+ end
+ end
+end
+
View
76 tools/rubuildius/matzbot/lib/daemon.rb
@@ -0,0 +1,76 @@
+# http://www.bigbold.com/snippets/posts/show/2265
+# (but hacked)
+require 'fileutils'
+
+module MatzBot
+ module Daemonize
+ extend self
+
+ WorkingDirectory = File.expand_path(FileUtils.pwd)
+
+ def pid_fn
+ File.join(WorkingDirectory, "matzbot.pid")
+ end
+
+ def daemonize(config)
+ case config[:daemonize]
+ when 'start'
+ start(config)
+ when 'stop'
+ stop
+ when 'restart'
+ stop
+ start(config)
+ else
+ puts "Invalid command. Please specify start, stop or restart."
+ exit
+ end
+ end
+
+ def start(config)
+ fork do
+ Process.setsid
+ exit if fork
+ store(Process.pid)
+ Dir.chdir WorkingDirectory
+ File.umask 0000
+ STDIN.reopen "/dev/null"
+ STDOUT.reopen "/dev/null", "a"
+ STDERR.reopen STDOUT
+ trap("TERM") do
+ MatzBot::Session.save
+ exit
+ end
+ begin
+ Client.start(config)
+ ensure
+ stop
+ end
+ end
+ end
+
+ def stop
+ unless File.file? pid_fn
+ puts "Pid file not found. Is the daemon started?"
+ exit
+ end
+ pid = recall
+ FileUtils.rm pid_fn
+ puts "Process killed."
+ Process.kill("TERM", pid) rescue nil
+ end
+
+ def store(pid=Process.pid)
+ if File.file? pid_fn
+ puts "** Already started with PID #{File.read(pid_fn)}"
+ exit!
+ else
+ File.open(pid_fn, 'w') { |f| f << pid }
+ end
+ end
+
+ def recall
+ IO.read(pid_fn).to_i rescue nil
+ end
+ end
+end
View
64 tools/rubuildius/matzbot/lib/matzbot.rb
@@ -0,0 +1,64 @@
+$:.unshift File.dirname(__FILE__)
+require 'session'
+require 'client'
+require 'commands'
+
+module MatzBot
+ extend self
+
+ @loaded_plugins ||= {}
+
+ def reload!
+ load File.join(File.expand_path(File.dirname(__FILE__)), 'session.rb')
+ load File.join(File.expand_path(File.dirname(__FILE__)), 'client.rb')
+ reload_plugins!
+ end
+
+ def reload_plugins!
+ commands_file = File.join(File.expand_path(File.dirname(__FILE__)), 'commands.rb')
+ if [commands_file, plugin_files].flatten.any? { |f| plugin_changed? f }
+ Commands.send(:reload_filters!)
+ MatzBot.send(:remove_const, :Commands)
+ load commands_file
+ load_plugins
+ end
+ end
+
+ def load_plugins
+ plugin_files.each do |file|
+ puts "Loading #{file}..."
+ begin
+ touch_plugin(file)
+ load file
+ rescue Object => e
+ puts "Unable to load #{file}, disabled."
+ p e
+ end
+ end
+ end
+
+ def plugin_files
+ [dot_matz_bot, 'plugins'].map do |directory|
+ next unless File.exists? directory
+ Dir[File.join(directory, '*.rb')]
+ end.flatten.compact
+ end
+
+ def dot_matz_bot
+ @dot_matz_bot ||=
+ if PLATFORM =~ /win32/
+ dot_matz_bot = ENV['HOMEDRIVE'] + ENV['HOMEPATH']
+ dot_matz_bot = File.join(home, 'MatzBot')
+ else
+ dot_matz_bot = File.join(File.expand_path("~/matzbot/"), "configuration")
+ end
+ end
+
+ def plugin_changed?(file)
+ touch_plugin(file) if !@loaded_plugins[file] || @loaded_plugins[file] < File.mtime(file)
+ end
+
+ def touch_plugin(file)
+ @loaded_plugins[file] = File.mtime(file)
+ end
+end
View
57 tools/rubuildius/matzbot/lib/session.rb
@@ -0,0 +1,57 @@
+require 'fileutils'
+
+module MatzBot
+ module Session
+ extend self
+
+ def session_file
+ File.join(home_dir, 'session.yml')
+ end
+
+ def home_dir
+ MatzBot.dot_matz_bot
+ end
+
+ def session
+ $session ||= {}
+ end
+
+ def save
+ FileUtils.mkdir(home_dir) unless File.exists? home_dir
+
+ File.open(session_file, 'w') do |f|
+ session_marshalled = session.dup
+ marshalled = {}
+
+ session_marshalled.each do |key, value|
+ callback(:session_before_save, key)
+ begin
+ marshalled[key] = Marshal.dump(value)
+ rescue
+ # don't dump things we cant marshal
+ end
+ end
+ # save
+ f.puts Marshal.dump(marshalled)
+ end
+ rescue
+ puts "Problem saving session file!"
+ end
+
+ def load
+ if File.exist? session_file
+ Marshal.load(open(session_file).read).each do |key, value|
+ session[key] = Marshal.load(value)
+ puts "Loaded session key: #{key}."
+ callback(:session_after_load, key)
+ end
+ end
+ end
+
+ def callback(callback, key)
+ if MatzBot::Commands.methods(false).include?(method = "#{callback}_#{key}")
+ MatzBot::Commands.send(method)
+ end
+ end
+ end
+end
View
56 tools/rubuildius/matzbot/plugins/git.rb
@@ -0,0 +1,56 @@
+module MatzBot::Commands
+
+ require 'open-uri'
+ require 'rexml/document'
+
+ GIT_URL = 'http://git.rubini.us/?p=code;a=atom'
+
+ hup_proc = lambda {
+ trap("HUP", "IGNORE")
+ trap("HUP", hup_proc)
+ }
+ trap("HUP", hup_proc)
+
+ abrt_proc = lambda {
+ trap("ABRT", "IGNORE")
+ trap("ABRT", abrt_proc)
+ }
+ trap("ABRT", abrt_proc)
+
+ def update_git
+ data = open(GIT_URL).read
+
+ doc = REXML::Document.new(data)
+
+ last_hash = session[:git_last_hash]
+ person = nil
+ top_hash = nil
+
+ REXML::XPath.each(doc, "//entry") do |entry|
+ title = REXML::XPath.first(entry, "./title")
+ link = REXML::XPath.first(entry, "./link")
+ name = REXML::XPath.first(entry, "./author/name")
+ hash = link.attributes['href'].split("=").last
+
+ top_hash = hash if top_hash.nil?
+
+ break if hash == last_hash
+
+ # we need to put the hast already in now, otherwise it might run the build twice.
+ session[:git_last_hash] = top_hash
+
+ person = name.text
+ build = IO.popen("~/continuous/bin/rubinius.zsh #{hash}", "r+") { |p| p.read }
+ unless build.empty?
+ say "#{person}: #{hash[0..8]}; #{build}"
+ #build.split("\n").map{|x| say " * " << x}
+ end
+
+ break # only run it for the very last commit
+ end
+ end
+
+ Signal.trap("USR2") do
+ update_git
+ end
+end
View
0 tools/rubuildius/repo/rubinius/HEAD/.empty_dir
No changes.

0 comments on commit 23f4753

Please sign in to comment.