-
Notifications
You must be signed in to change notification settings - Fork 71
/
pubsub.rb
236 lines (200 loc) · 5.63 KB
/
pubsub.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
#
# 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