blob: ffb036de1075714c283719df8ee8385ca8ab0140 [file] [log] [blame]
#
# Whimsy pubsub support: watches for updates to the whimsy repository,
# fetches the changes and deploys them.
#
# For usage instructions, try
#
# ruby pubsub.rb --help
#
require 'optparse'
require 'ostruct'
require 'etc'
require 'net/http'
require 'json'
require 'fileutils'
# extract script name
script = File.basename(__FILE__, '.rb')
#
### option parsing
#
options = OpenStruct.new
options.remote = 'https://gitbox.apache.org/repos/asf/whimsy.git'
options.local = '/srv/whimsy'
options.pidfile = "/var/run/#{script}.pid"
options.streamURL = 'http://pubsub.apache.org:2069/git/'
options.puppet = false
optionparser = OptionParser.new do |opts|
opts.on '-u', '--user id', "Optional user to run #{script} as" do |user|
options.user = user
end
opts.on '-g', '--group id', "Optional group to run #{script} as" do |group|
options.group = group
end
opts.on '-p', '--pidfile path', "Optional pid file location" do |path|
options.pidfile = path
end
opts.on '-d', '--daemonize', "Run as daemon" do
options.daemonize = true
end
opts.on '--puppet', "Use puppet agent to update" do
options.puppet = true
end
opts.on '-s', '--stream', "StreamURL" do |url|
options.streamURL = url
end
opts.on '-r', '--remote', "Git Clone URL" do |url|
options.streamURL = url
end
opts.on '-c', '--clone', "Git Clone Directory" do |path|
options.local = path
end
opts.on '--stop', "Kill the currently running #{script} process" do
options.kill = true
end
end
optionparser.parse!
# Check for required tools
if options.puppet and `which puppet 2>/dev/null`.empty?
STDERR.puts 'puppet not found in path; exiting'
exit 1
end
%w(git rake).each do |tool|
if `which #{tool} 2>/dev/null`.empty?
STDERR.puts "#{tool} not found in path; exiting"
exit 1
end
end
#
### process management
#
# Either kill old process, or start a new one
if options.kill
if File.exist? options.pidfile
Process.kill 'TERM', File.read(options.pidfile).to_i
File.delete options.pidfile if File.exist? options.pidfile
exit 0
end
else
# optionally daemonize
Process.daemon if options.daemonize
# Determine if pidfile is writable
if File.exist? options.pidfile
writable = File.writable? options.pidfile
else
writable = File.writable? File.dirname(options.pidfile)
end
# PID file management
if writable
File.write options.pidfile, Process.pid.to_s
at_exit { File.delete options.pidfile if File.exist? options.pidfile }
else
STDERR.puts "EACCES: Skipping creation of pidfile #{options.pidfile}"
end
end
# Optionally change user/group
if Process.uid == 0
Process::Sys.setgid Etc.getgrnam(options.group).gid if options.group
Process::Sys.setuid Etc.getpwnam(options.user).uid if options.user
end
# Perform initial clone
if not Dir.exist? options.local
FileUtils.mkdir_p File.basename(options.local)
system('git', 'clone', options.remote, options.local)
end
#
# Monitor PubSub endpoint (see https://infra.apache.org/pypubsub.html)
#
PROJECT = File.basename(options.remote, '.git')
# prime the pump
restartable = false
notification_queue = Queue.new
notification_queue.push 'project' => PROJECT
ps_thread = Thread.new do
begin
uri = URI.parse(options.streamURL)
Net::HTTP.start(uri.host, uri.port) do |http|
request = Net::HTTP::Get.new uri.request_uri
http.request request do |response|
body = ''
response.read_body do |chunk|
# Looks like the service only sends \n terminators now
if chunk =~ /\r?\n$|\0$/
notification = JSON.parse(body + chunk.chomp("\0"))
body = ''
if notification['stillalive']
restartable = true
elsif notification['push']
notification_queue << notification['push']
elsif notification['commit']
notification_queue << notification['commit']
elsif notification['svnpubsub']
next
else
STDERR.puts '*** unexpected notification ***'
STDERR.puts notification.inspect
end
else
body += chunk
end
end
end
end
rescue Errno::ECONNREFUSED => e
restartable = true
STDERR.puts e
sleep 3
rescue Exception => e
STDERR.puts e
STDERR.puts e.backtrace
end
end
#
# Process queued requests
#
begin
mtime = File.mtime(__FILE__)
while ps_thread.alive?
notification = notification_queue.pop
next unless notification['project'] == PROJECT
notification_queue.clear
if options.puppet
# Update using puppet. If puppet fails, it may be due to puppet already
# running; in which case it may not have picked up this update. So try
# again in 30, 60, 90, and 120 seconds, for a total of five minutes.
4.times do |i|
break if system('puppet', 'agent', '-t')
sleep 30 * (i+1)
end
else
# update git directories in the foreground
Dir.chdir(options.local) do
before = `git log --oneline -1`
system('git', 'fetch', 'origin')
system('git', 'clean', '-df')
system('git', 'reset', '--hard', 'origin/master')
if File.exist? 'Rakefile' and `git log --oneline -1` != before
system('rake', 'update')
end
end
end
break if mtime != File.mtime(__FILE__)
end
rescue SignalException => e
STDERR.puts e
restartable = false
rescue Exception => e
if ps_thread.alive?
STDERR.puts e
STDERR.puts e.backtrace
restartable = false
end
end
#
# restart
#
if restartable
STDERR.puts 'restarting'
# relaunch script after a one second delay
sleep 1
exec RbConfig.ruby, __FILE__, *ARGV
end