appcast 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +5 -0
- data/Manifest.txt +14 -0
- data/README.txt +48 -0
- data/Rakefile +24 -0
- data/bin/mongrel_appcast +241 -0
- data/lib/appcast.rb +54 -0
- data/lib/appcast/client.rb +67 -0
- data/lib/appcast/handlers.rb +82 -0
- data/lib/appcast/message.rb +54 -0
- data/lib/database.sqlite +0 -0
- data/test/appcast.yml +9 -0
- data/test/client_test.rb +90 -0
- data/test/message_test.rb +15 -0
- data/test/test_helper.rb +10 -0
- metadata +109 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
README.txt
|
4
|
+
Rakefile
|
5
|
+
bin/mongrel_appcast
|
6
|
+
lib/appcast.rb
|
7
|
+
lib/appcast/client.rb
|
8
|
+
lib/appcast/handlers.rb
|
9
|
+
lib/appcast/message.rb
|
10
|
+
lib/database.sqlite
|
11
|
+
test/appcast.yml
|
12
|
+
test/client_test.rb
|
13
|
+
test/message_test.rb
|
14
|
+
test/test_helper.rb
|
data/README.txt
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
appcast
|
2
|
+
by FIX (your name)
|
3
|
+
FIX (url)
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
FIX (describe your package)
|
8
|
+
|
9
|
+
== FEATURES/PROBLEMS:
|
10
|
+
|
11
|
+
* FIX (list of features or problems)
|
12
|
+
|
13
|
+
== SYNOPSIS:
|
14
|
+
|
15
|
+
FIX (code sample of usage)
|
16
|
+
|
17
|
+
== REQUIREMENTS:
|
18
|
+
|
19
|
+
* FIX (list of requirements)
|
20
|
+
|
21
|
+
== INSTALL:
|
22
|
+
|
23
|
+
* FIX (sudo gem install, anything else)
|
24
|
+
|
25
|
+
== LICENSE:
|
26
|
+
|
27
|
+
(The MIT License)
|
28
|
+
|
29
|
+
Copyright (c) 2007 Rick Olson
|
30
|
+
|
31
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
32
|
+
a copy of this software and associated documentation files (the
|
33
|
+
'Software'), to deal in the Software without restriction, including
|
34
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
35
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
36
|
+
permit persons to whom the Software is furnished to do so, subject to
|
37
|
+
the following conditions:
|
38
|
+
|
39
|
+
The above copyright notice and this permission notice shall be
|
40
|
+
included in all copies or substantial portions of the Software.
|
41
|
+
|
42
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
43
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
44
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
45
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
46
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
47
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
48
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
$: << 'lib'
|
6
|
+
require 'appcast'
|
7
|
+
|
8
|
+
Hoe.new('appcast', Appcast::VERSION) do |p|
|
9
|
+
p.rubyforge_name = 'appcast'
|
10
|
+
p.author = ['Tobi Lütke', 'Rick Olson']
|
11
|
+
p.email = 'technoweenie@gmail.com'
|
12
|
+
# p.summary = 'FIX'
|
13
|
+
# p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
|
14
|
+
# p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
|
15
|
+
p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
|
16
|
+
p.extra_deps << 'mongrel'
|
17
|
+
p.extra_deps << 'builder'
|
18
|
+
p.extra_deps << 'activesupport'
|
19
|
+
p.extra_deps << 'activerecord'
|
20
|
+
p.test_globs << 'test/**/*_test.rb'
|
21
|
+
p.bin_files = ['mongrel_appcast']
|
22
|
+
end
|
23
|
+
|
24
|
+
# vim: syntax=Ruby
|
data/bin/mongrel_appcast
ADDED
@@ -0,0 +1,241 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'yaml'
|
5
|
+
require 'mongrel'
|
6
|
+
require 'appcast'
|
7
|
+
require 'etc'
|
8
|
+
require 'cgi_multipart_eof_fix' rescue nil
|
9
|
+
|
10
|
+
module Mongrel
|
11
|
+
class Start < GemPlugin::Plugin "/commands"
|
12
|
+
include Mongrel::Command::Base
|
13
|
+
|
14
|
+
def configure
|
15
|
+
options [
|
16
|
+
["-d", "--daemonize", "Run daemonized in the background", :@daemon, false],
|
17
|
+
['-p', '--port PORT', "Which port to bind to", :@port, 3000],
|
18
|
+
['-a', '--address ADDR', "Address to bind to", :@address, "0.0.0.0"],
|
19
|
+
['-l', '--log FILE', "Where to write log messages", :@log_file, "log/appcast.log"],
|
20
|
+
['-P', '--pid FILE', "Where to write the PID", :@pid_file, "log/appcast.pid"],
|
21
|
+
['-n', '--num-procs INT', "Number of processors active before clients denied", :@num_procs, 1024],
|
22
|
+
['-t', '--timeout TIME', "Timeout all requests after 100th seconds time", :@timeout, 0],
|
23
|
+
['-c', '--chdir PATH', "Change to dir before starting (will be expanded)", :@cwd, Dir.pwd],
|
24
|
+
['-B', '--debug', "Enable debugging mode", :@debug, false],
|
25
|
+
['-C', '--config PATH', "Use a config file", :@config_file, nil],
|
26
|
+
['-S', '--script PATH', "Load the given file as an extra config script", :@config_script, nil],
|
27
|
+
['-G', '--generate PATH', "Generate a config file for use with -C", :@generate, nil],
|
28
|
+
['', '--user USER', "User to run as", :@user, nil],
|
29
|
+
['', '--group GROUP', "Group to run as", :@group, nil],
|
30
|
+
['', '--prefix PATH', "URL prefix for Rails app", :@prefix, nil],
|
31
|
+
['', '--stats-path PATH', "Path for the stats handler", :@stats_path, '/'],
|
32
|
+
['', '--queue-path PATH', "Path for the queue handler", :@queue_path, '/queue'],
|
33
|
+
['', '--messages-path PATH', "Path for the messages handler", :@messages_path, '/messages']
|
34
|
+
]
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate
|
38
|
+
@cwd = File.expand_path(@cwd)
|
39
|
+
valid_dir? @cwd, "Invalid path to change to during daemon mode: #@cwd"
|
40
|
+
|
41
|
+
# Change there to start, then we'll have to come back after daemonize
|
42
|
+
Dir.chdir(@cwd)
|
43
|
+
|
44
|
+
valid?(@prefix[0].chr == "/" && @prefix[-1].chr != "/", "Prefix must begin with / and not end in /") if @prefix
|
45
|
+
valid_dir? File.dirname(@log_file), "Path to log file not valid: #@log_file"
|
46
|
+
valid_dir? File.dirname(@pid_file), "Path to pid file not valid: #@pid_file"
|
47
|
+
valid_exists? @config_file, "Config file not there: #@config_file" if @config_file
|
48
|
+
valid_dir? File.dirname(@generate), "Problem accessing directory to #@generate" if @generate
|
49
|
+
valid_user? @user if @user
|
50
|
+
valid_group? @group if @group
|
51
|
+
|
52
|
+
return @valid
|
53
|
+
end
|
54
|
+
|
55
|
+
def run
|
56
|
+
# Config file settings will override command line settings
|
57
|
+
settings = { :host => @address, :port => @port, :cwd => @cwd,
|
58
|
+
:log_file => @log_file, :pid_file => @pid_file, :daemon => @daemon,
|
59
|
+
:includes => ["mongrel"], :config_script => @config_script,
|
60
|
+
:num_processors => @num_procs, :timeout => @timeout, :debug => @debug,
|
61
|
+
:user => @user, :group => @group, :prefix => @prefix, :config_file => @config_file,
|
62
|
+
:stats_path => @stats_path, :queue_path => @queue_path, :messages_path => @messages_path,
|
63
|
+
:db => { 'adapter' => nil, 'database' => 'database.sqlite'}
|
64
|
+
}
|
65
|
+
|
66
|
+
if @generate
|
67
|
+
STDERR.puts "** Writing config to \"#@generate\"."
|
68
|
+
open(@generate, "w") {|f| f.write(settings.to_yaml) }
|
69
|
+
STDERR.puts "** Finished. Run \"mongrel_appcast -C #@generate\" to use the config file."
|
70
|
+
exit 0
|
71
|
+
end
|
72
|
+
|
73
|
+
if @config_file
|
74
|
+
settings.merge! YAML.load_file(@config_file)
|
75
|
+
STDERR.puts "** Loading settings from #{@config_file} (they override command line)." unless settings[:daemon]
|
76
|
+
end
|
77
|
+
|
78
|
+
if settings[:db]['adapter'].nil?
|
79
|
+
begin
|
80
|
+
require 'sqlite'
|
81
|
+
settings[:db]['adapter'] ||= 'sqlite'
|
82
|
+
rescue LoadError
|
83
|
+
end
|
84
|
+
end
|
85
|
+
if settings[:db]['adapter'].nil?
|
86
|
+
begin
|
87
|
+
require 'sqlite3'
|
88
|
+
settings[:db]['adapter'] ||= 'sqlite3'
|
89
|
+
rescue LoadError
|
90
|
+
end
|
91
|
+
end
|
92
|
+
if settings[:db]['adapter'].nil?
|
93
|
+
STDERR.puts "** No default adapter selected. sqlite and sqlite3 are not available."
|
94
|
+
exit 1
|
95
|
+
end
|
96
|
+
|
97
|
+
config = Mongrel::Configurator.new(settings) do
|
98
|
+
if defaults[:daemon]
|
99
|
+
if File.exist? defaults[:pid_file]
|
100
|
+
log "!!! PID file #{defaults[:pid_file]} already exists. Mongrel could be running already. Check your #{defaults[:log_file]} for errors."
|
101
|
+
log "!!! Exiting with error. You must stop mongrel and clear the .pid before I'll attempt a start."
|
102
|
+
exit 1
|
103
|
+
end
|
104
|
+
|
105
|
+
daemonize
|
106
|
+
log "Daemonized, any open files are closed. Look at #{defaults[:pid_file]} and #{defaults[:log_file]} for info."
|
107
|
+
log "Settings loaded from #{@config_file} (they override command line)." if @config_file
|
108
|
+
end
|
109
|
+
|
110
|
+
log "Starting Mongrel listening at #{defaults[:host]}:#{defaults[:port]}"
|
111
|
+
|
112
|
+
listener do
|
113
|
+
Appcast.configure_database defaults[:db], defaults[:log_file], defaults[:debug] ? 'DEBUG' : nil
|
114
|
+
uri defaults[:stats_path], :handler => Appcast::StatsHandler.new
|
115
|
+
uri defaults[:queue_path], :handler => Appcast::QueueHandler.new
|
116
|
+
uri defaults[:messages_path], :handler => Appcast::MessageHandler.new
|
117
|
+
log "Appcast v#{Appcast::VERSION} started."
|
118
|
+
|
119
|
+
if defaults[:config_script]
|
120
|
+
log "Loading #{defaults[:config_script]} external config script"
|
121
|
+
run_config(defaults[:config_script])
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
config.run
|
127
|
+
config.log "Mongrel available at #{settings[:host]}:#{settings[:port]}"
|
128
|
+
|
129
|
+
if config.defaults[:daemon]
|
130
|
+
config.write_pid_file
|
131
|
+
else
|
132
|
+
config.log "Use CTRL-C to stop."
|
133
|
+
end
|
134
|
+
|
135
|
+
config.join
|
136
|
+
|
137
|
+
if config.needs_restart
|
138
|
+
if RUBY_PLATFORM !~ /mswin/
|
139
|
+
cmd = "ruby #{__FILE__} start #{original_args.join(' ')}"
|
140
|
+
config.log "Restarting with arguments: #{cmd}"
|
141
|
+
config.stop
|
142
|
+
config.remove_pid_file
|
143
|
+
|
144
|
+
if config.defaults[:daemon]
|
145
|
+
system cmd
|
146
|
+
else
|
147
|
+
STDERR.puts "Can't restart unless in daemon mode."
|
148
|
+
exit 1
|
149
|
+
end
|
150
|
+
else
|
151
|
+
config.log "Win32 does not support restarts. Exiting."
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def Mongrel::send_signal(signal, pid_file)
|
158
|
+
pid = open(pid_file).read.to_i
|
159
|
+
print "Sending #{signal} to Mongrel at PID #{pid}..."
|
160
|
+
begin
|
161
|
+
Process.kill(signal, pid)
|
162
|
+
rescue Errno::ESRCH
|
163
|
+
puts "Process does not exist. Not running."
|
164
|
+
end
|
165
|
+
|
166
|
+
puts "Done."
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
class Stop < GemPlugin::Plugin "/commands"
|
171
|
+
include Mongrel::Command::Base
|
172
|
+
|
173
|
+
def configure
|
174
|
+
options [
|
175
|
+
['-c', '--chdir PATH', "Change to dir before starting (will be expanded).", :@cwd, "."],
|
176
|
+
['-f', '--force', "Force the shutdown (kill -9).", :@force, false],
|
177
|
+
['-w', '--wait SECONDS', "Wait SECONDS before forcing shutdown", :@wait, "0"],
|
178
|
+
['-P', '--pid FILE', "Where the PID file is located.", :@pid_file, "log/mongrel.pid"]
|
179
|
+
]
|
180
|
+
end
|
181
|
+
|
182
|
+
def validate
|
183
|
+
@cwd = File.expand_path(@cwd)
|
184
|
+
valid_dir? @cwd, "Invalid path to change to during daemon mode: #@cwd"
|
185
|
+
|
186
|
+
Dir.chdir @cwd
|
187
|
+
|
188
|
+
valid_exists? @pid_file, "PID file #@pid_file does not exist. Not running?"
|
189
|
+
return @valid
|
190
|
+
end
|
191
|
+
|
192
|
+
def run
|
193
|
+
if @force
|
194
|
+
@wait.to_i.times do |waiting|
|
195
|
+
exit(0) if not File.exist? @pid_file
|
196
|
+
sleep 1
|
197
|
+
end
|
198
|
+
|
199
|
+
Mongrel::send_signal("KILL", @pid_file) if File.exist? @pid_file
|
200
|
+
else
|
201
|
+
Mongrel::send_signal("TERM", @pid_file)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
|
207
|
+
class Restart < GemPlugin::Plugin "/commands"
|
208
|
+
include Mongrel::Command::Base
|
209
|
+
|
210
|
+
def configure
|
211
|
+
options [
|
212
|
+
['-c', '--chdir PATH', "Change to dir before starting (will be expanded)", :@cwd, '.'],
|
213
|
+
['-s', '--soft', "Do a soft restart rather than a process exit restart", :@soft, false],
|
214
|
+
['-P', '--pid FILE', "Where the PID file is located", :@pid_file, "log/mongrel.pid"]
|
215
|
+
]
|
216
|
+
end
|
217
|
+
|
218
|
+
def validate
|
219
|
+
@cwd = File.expand_path(@cwd)
|
220
|
+
valid_dir? @cwd, "Invalid path to change to during daemon mode: #@cwd"
|
221
|
+
|
222
|
+
Dir.chdir @cwd
|
223
|
+
|
224
|
+
valid_exists? @pid_file, "PID file #@pid_file does not exist. Not running?"
|
225
|
+
return @valid
|
226
|
+
end
|
227
|
+
|
228
|
+
def run
|
229
|
+
if @soft
|
230
|
+
Mongrel::send_signal("HUP", @pid_file)
|
231
|
+
else
|
232
|
+
Mongrel::send_signal("USR2", @pid_file)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
|
239
|
+
if not Mongrel::Command::Registry.instance.run ARGV
|
240
|
+
exit 1
|
241
|
+
end
|
data/lib/appcast.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Appcast is a simple message server somewhat similar to Amazon's SQS but based
|
4
|
+
# around simple REST principles
|
5
|
+
#
|
6
|
+
# There is no UI for the server. All the communication is done using HTTP verbs.
|
7
|
+
#
|
8
|
+
# == Adding messages
|
9
|
+
#
|
10
|
+
# You can add messages to the server using simple HTTP post
|
11
|
+
#
|
12
|
+
# curl http://localhost:3000/queue/products/updates -d 'id:5454'
|
13
|
+
# => 201 Created
|
14
|
+
# => Location: http://localhost:3000/messages/1
|
15
|
+
#
|
16
|
+
# To delete a message you can use
|
17
|
+
#
|
18
|
+
# curl http://localhost:3000/messages/1 -x DELETE
|
19
|
+
# => 200 OK
|
20
|
+
#
|
21
|
+
# You can get new messages from a namespace with simple GET requests
|
22
|
+
# note that you will always receive all descendent messages. if you posted your messages
|
23
|
+
# to /products/updates
|
24
|
+
#
|
25
|
+
# curl http://localhost:3000/queue/products
|
26
|
+
# => 200 OK
|
27
|
+
# => XML feed of new messages
|
28
|
+
#
|
29
|
+
# curl http://localhost:3000/queue/products?limit=1&lock_for=300
|
30
|
+
# => 200 OK
|
31
|
+
# => XML feed with one message which will be locked for 5 minutes during which you should hangle and delete it.
|
32
|
+
#
|
33
|
+
# Once you receive messages you can DELETE or PUT to their location parameter to remove the
|
34
|
+
# message from the server or clear any locks on the message so that it will show up again
|
35
|
+
# in GET queries.
|
36
|
+
|
37
|
+
require 'appcast/message'
|
38
|
+
require 'appcast/handlers'
|
39
|
+
|
40
|
+
module Appcast
|
41
|
+
VERSION = '1.0.0'
|
42
|
+
|
43
|
+
def self.configure_database(config, log_file, log_level = nil)
|
44
|
+
raise "Missing database config" if config.empty?
|
45
|
+
ActiveRecord::Base.logger = Logger.new(log_file)
|
46
|
+
ActiveRecord::Base.logger.level = Logger.const_get((log_level || "INFO").upcase)
|
47
|
+
ActiveRecord::Base.establish_connection(config)
|
48
|
+
ActiveRecord::Base.default_timezone = :utc
|
49
|
+
|
50
|
+
unless config['adapter'] !~ /sqlite/ || File.exists?(config['database'])
|
51
|
+
Appcast::Message.create_table
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'net/http'
|
3
|
+
require 'uri'
|
4
|
+
require 'active_support'
|
5
|
+
|
6
|
+
|
7
|
+
module Appcast
|
8
|
+
class ClientError < StandardError; end
|
9
|
+
class Client
|
10
|
+
class Message
|
11
|
+
attr_reader :connection
|
12
|
+
attr_accessor :id, :name, :content
|
13
|
+
|
14
|
+
def initialize(connection, attributes = {})
|
15
|
+
@connection = connection
|
16
|
+
attributes.each do |key, value|
|
17
|
+
send("#{key}=", value)
|
18
|
+
end if attributes
|
19
|
+
end
|
20
|
+
|
21
|
+
def location=(value)
|
22
|
+
self.id = value.to_s.scan(/\d+$/).first
|
23
|
+
end
|
24
|
+
|
25
|
+
def location
|
26
|
+
"/messages/#{id}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def destroy
|
30
|
+
connection.delete(URI.escape(location)).code == '200'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :connection
|
35
|
+
|
36
|
+
# Symbolized option hash
|
37
|
+
def initialize(options = {})
|
38
|
+
raise ClientError, "No :host option given." unless options.key?(:host)
|
39
|
+
@connection = Net::HTTP.new(options[:host], options[:port])
|
40
|
+
end
|
41
|
+
|
42
|
+
def create(name, content)
|
43
|
+
response = connection.post(URI.escape("/queue/#{name}"), content)
|
44
|
+
Message.new(connection, :id => response["Location"].scan(/\d+$/).first, :name => name, :content => content)
|
45
|
+
end
|
46
|
+
|
47
|
+
def list(name, options = {})
|
48
|
+
params = options.inject([]) do |memo, (key, value)|
|
49
|
+
memo << "#{key}=#{value}" if [:limit, :lock_for].include?(key)
|
50
|
+
memo
|
51
|
+
end
|
52
|
+
params = params.empty? ? '' : "?#{params * "&"}"
|
53
|
+
|
54
|
+
response = connection.get(URI.escape("/queue/#{name}#{params}"))
|
55
|
+
messages = Hash.from_xml(response.body)['messages']
|
56
|
+
return [] if messages.nil? || !messages.key?('message')
|
57
|
+
messages = messages['message']
|
58
|
+
if messages.respond_to?(:collect!)
|
59
|
+
messages.collect! do |hash|
|
60
|
+
Message.new(connection, hash)
|
61
|
+
end
|
62
|
+
else
|
63
|
+
[Message.new(connection, messages)]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'mongrel'
|
3
|
+
|
4
|
+
class Mongrel::HttpRequest
|
5
|
+
def method; params[Mongrel::Const::REQUEST_METHOD]; end
|
6
|
+
def path; params[Mongrel::Const::PATH_INFO]; end
|
7
|
+
def uri; params[Mongrel::Const::REQUEST_URI]; end
|
8
|
+
def query_params; @query_params ||= Mongrel::HttpRequest.query_parse(params['QUERY_STRING']); end
|
9
|
+
end
|
10
|
+
|
11
|
+
module Appcast
|
12
|
+
class BaseHandler < Mongrel::HttpHandler
|
13
|
+
def initialize
|
14
|
+
@guard = Mutex.new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Stats handler which just displays some
|
19
|
+
# basic stats on whats going on in the server
|
20
|
+
#
|
21
|
+
class StatsHandler < BaseHandler
|
22
|
+
LINES = "%-30s %-8s\n"
|
23
|
+
|
24
|
+
def process(request, response)
|
25
|
+
response.start do |h, out|
|
26
|
+
h['Content-Type'] = 'text/plain'
|
27
|
+
@guard.synchronize do
|
28
|
+
Message.stats.each do |k, v|
|
29
|
+
out.write(LINES % [k.strip, v])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class QueueHandler < BaseHandler
|
37
|
+
def process(request, response)
|
38
|
+
case request.method
|
39
|
+
when 'POST'
|
40
|
+
msg = nil
|
41
|
+
@guard.synchronize { msg = Message.create(:name => request.path, :content => request.body.read ) }
|
42
|
+
response.start(201) { |head,out| head['Location'] = msg.location; out.write(msg.to_xml) }
|
43
|
+
puts "Added message at #{request.path}"
|
44
|
+
when 'GET'
|
45
|
+
msgs = nil
|
46
|
+
@guard.synchronize { msgs = Message.get(request.path, request.query_params['limit'] || 10, request.query_params['lock_for'] || false) }
|
47
|
+
response.start { |head,out| out.write(msgs.to_xml(:root => 'messages')) }
|
48
|
+
end
|
49
|
+
rescue => e
|
50
|
+
puts e.message
|
51
|
+
puts e.backtrace.join("\n")
|
52
|
+
response.start(500) {}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class MessageHandler < BaseHandler
|
57
|
+
ID_SCANNER = /\/(\d+)$/.freeze
|
58
|
+
|
59
|
+
def process(request, response)
|
60
|
+
id = request.path.scan(ID_SCANNER)[0].first
|
61
|
+
|
62
|
+
case request.method
|
63
|
+
when 'DELETE'
|
64
|
+
@guard.synchronize { Message.destroy(id) }
|
65
|
+
response.start(200) {}
|
66
|
+
when 'PUT'
|
67
|
+
@guard.synchronize { Message.find(id).unlock }
|
68
|
+
response.start(200) {}
|
69
|
+
when 'GET'
|
70
|
+
msg = nil
|
71
|
+
@guard.synchronize { msg = Message.find(id) }
|
72
|
+
response.start { |h,out| h['Location'] = msg.location; out.write(msg.to_xml) }
|
73
|
+
end
|
74
|
+
rescue ActiveRecord::RecordNotFound
|
75
|
+
response.start(404) { |h,out| out.write("Unknown message with id #{id}") }
|
76
|
+
rescue => e
|
77
|
+
puts e.message
|
78
|
+
puts e.backtrace.join("\n")
|
79
|
+
response.start(500) {}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'active_record'
|
3
|
+
require 'builder'
|
4
|
+
|
5
|
+
module Appcast
|
6
|
+
class Message < ActiveRecord::Base
|
7
|
+
def self.create_table
|
8
|
+
connection.create_table :messages, :force => true do |t|
|
9
|
+
t.column :name, :string
|
10
|
+
t.column :content, :text
|
11
|
+
t.column :locked_until, :datetime
|
12
|
+
t.column :created_at, :datetime
|
13
|
+
end
|
14
|
+
|
15
|
+
connection.add_index :messages, :name
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.get(path, limit, lock_seconds)
|
19
|
+
now = Time.now.utc
|
20
|
+
|
21
|
+
returning records = find(:all, :conditions => ["name LIKE ? AND (locked_until IS NULL OR locked_until < ?)", "#{path}%", now], :order => 'id', :limit => limit) do
|
22
|
+
if lock_seconds and !records.empty?
|
23
|
+
update_all(['locked_until = ?', now + lock_seconds.to_f], ['id IN (?)', records.collect(&:id)])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def unlock
|
29
|
+
update_attribute :locked_until, nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def location
|
33
|
+
"/messages/#{id}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_xml(options = {})
|
37
|
+
options[:only] = [:name, :content]
|
38
|
+
options[:methods] = [:location]
|
39
|
+
super
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.stats
|
43
|
+
s = connection.select_all("SELECT name, count(*) as count FROM messages GROUP by name").collect(&:values)
|
44
|
+
s.unshift '--------------------------------------------'
|
45
|
+
if recent = find(:first, :order => 'id ASC')
|
46
|
+
s.unshift ['Most recent', recent.created_at.to_s(:short)]
|
47
|
+
end
|
48
|
+
s.unshift ['Total', count]
|
49
|
+
s
|
50
|
+
end
|
51
|
+
|
52
|
+
validates_presence_of :name, :content
|
53
|
+
end
|
54
|
+
end
|
data/lib/database.sqlite
ADDED
Binary file
|
data/test/appcast.yml
ADDED
data/test/client_test.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
require 'appcast/client'
|
3
|
+
|
4
|
+
context "Client" do
|
5
|
+
setup do
|
6
|
+
@client = Appcast::Client.new(:host => 'test.host')
|
7
|
+
end
|
8
|
+
|
9
|
+
specify "should initialize with http object" do
|
10
|
+
@client.connection.class.should == Net::HTTP
|
11
|
+
end
|
12
|
+
|
13
|
+
specify "should require http host option" do
|
14
|
+
assert_raises Appcast::ClientError do
|
15
|
+
Appcast::Client.new
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
specify "should create message" do
|
20
|
+
@client.connection.expects(:post).with("/queue/test", "foobar").returns({'Location' => '/messages/55'})
|
21
|
+
message = @client.create('test', 'foobar')
|
22
|
+
message.class.should == Appcast::Client::Message
|
23
|
+
message.id.should == '55'
|
24
|
+
message.name.should == 'test'
|
25
|
+
message.content.should == 'foobar'
|
26
|
+
end
|
27
|
+
|
28
|
+
specify "should list messages" do
|
29
|
+
mock_response = OpenStruct.new
|
30
|
+
mock_response.body = <<-ENDXML
|
31
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
32
|
+
<messages>
|
33
|
+
<message><content>test 1</content><name>/test</name><location>/messages/1</location></message>
|
34
|
+
<message><content>test 2</content><name>/test</name><location>/messages/2</location></message>
|
35
|
+
</messages>
|
36
|
+
ENDXML
|
37
|
+
|
38
|
+
@client.connection.expects(:get).with("/queue/test").returns(mock_response)
|
39
|
+
|
40
|
+
messages = @client.list 'test'
|
41
|
+
messages.size == 2
|
42
|
+
messages[0].content.should == 'test 1'
|
43
|
+
messages[0].id.should == '1'
|
44
|
+
messages[1].content.should == 'test 2'
|
45
|
+
messages[1].id.should == '2'
|
46
|
+
end
|
47
|
+
|
48
|
+
specify "should list message" do
|
49
|
+
mock_response = OpenStruct.new
|
50
|
+
mock_response.body = <<-ENDXML
|
51
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
52
|
+
<messages>
|
53
|
+
<message><content>test 1</content><name>/test</name><location>/messages/1</location></message>
|
54
|
+
</messages>
|
55
|
+
ENDXML
|
56
|
+
|
57
|
+
@client.connection.expects(:get).with("/queue/test").returns(mock_response)
|
58
|
+
|
59
|
+
messages = @client.list 'test'
|
60
|
+
messages.size == 1
|
61
|
+
messages[0].content.should == 'test 1'
|
62
|
+
messages[0].id.should == '1'
|
63
|
+
end
|
64
|
+
|
65
|
+
specify "should list no messages" do
|
66
|
+
mock_response = OpenStruct.new(:body => %(<?xml version="1.0" encoding="UTF-8"?><messages></messages>))
|
67
|
+
@client.connection.expects(:get).with("/queue/test").returns(mock_response)
|
68
|
+
|
69
|
+
messages = @client.list 'test'
|
70
|
+
messages.size == 0
|
71
|
+
end
|
72
|
+
|
73
|
+
specify "should list messages with limit parameter" do
|
74
|
+
@client.connection.expects(:get).with("/queue/test?limit=5").returns \
|
75
|
+
OpenStruct.new(:body => %(<?xml version="1.0" encoding="UTF-8"?><messages></messages>))
|
76
|
+
@client.list 'test', :limit => 5
|
77
|
+
end
|
78
|
+
|
79
|
+
specify "should list messages with lock_for parameter" do
|
80
|
+
@client.connection.expects(:get).with("/queue/test?lock_for=5").returns \
|
81
|
+
OpenStruct.new(:body => %(<?xml version="1.0" encoding="UTF-8"?><messages></messages>))
|
82
|
+
@client.list 'test', :lock_for => 5, :ignored => 7
|
83
|
+
end
|
84
|
+
|
85
|
+
specify "should delete message" do
|
86
|
+
message = Appcast::Client::Message.new(Object.new, :id => 5)
|
87
|
+
message.connection.expects(:delete).with("/messages/5").returns(OpenStruct.new(:code => '200'))
|
88
|
+
message.destroy.should == true
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
require 'appcast/message'
|
3
|
+
|
4
|
+
config = YAML.load_file(File.join(File.dirname(__FILE__), 'appcast.yml'))
|
5
|
+
ActiveRecord::Base.establish_connection(config['db'])
|
6
|
+
ActiveRecord::Base.default_timezone = :utc
|
7
|
+
Appcast::Message.create_table unless File.exists?(File.join(File.dirname(__FILE__), config['db']['database']))
|
8
|
+
|
9
|
+
context "Message" do
|
10
|
+
specify "should return correct location" do
|
11
|
+
msg = Appcast::Message.new
|
12
|
+
msg.expects(:id).returns(5)
|
13
|
+
msg.location.should == '/messages/5'
|
14
|
+
end
|
15
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.2
|
3
|
+
specification_version: 1
|
4
|
+
name: appcast
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 1.0.0
|
7
|
+
date: 2007-08-26 00:00:00 -05:00
|
8
|
+
summary: The author was too lazy to write a summary
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: technoweenie@gmail.com
|
12
|
+
homepage: http://www.zenspider.com/ZSS/Products/appcast/
|
13
|
+
rubyforge_project: appcast
|
14
|
+
description: The author was too lazy to write a description
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- "Tobi L\xC3\xBCtke"
|
31
|
+
- Rick Olson
|
32
|
+
files:
|
33
|
+
- History.txt
|
34
|
+
- Manifest.txt
|
35
|
+
- README.txt
|
36
|
+
- Rakefile
|
37
|
+
- bin/mongrel_appcast
|
38
|
+
- lib/appcast.rb
|
39
|
+
- lib/appcast/client.rb
|
40
|
+
- lib/appcast/handlers.rb
|
41
|
+
- lib/appcast/message.rb
|
42
|
+
- lib/database.sqlite
|
43
|
+
- test/appcast.yml
|
44
|
+
- test/client_test.rb
|
45
|
+
- test/message_test.rb
|
46
|
+
- test/test_helper.rb
|
47
|
+
test_files:
|
48
|
+
- test/test_helper.rb
|
49
|
+
- test/client_test.rb
|
50
|
+
- test/message_test.rb
|
51
|
+
rdoc_options:
|
52
|
+
- --main
|
53
|
+
- README.txt
|
54
|
+
extra_rdoc_files:
|
55
|
+
- History.txt
|
56
|
+
- Manifest.txt
|
57
|
+
- README.txt
|
58
|
+
executables:
|
59
|
+
- mongrel_appcast
|
60
|
+
extensions: []
|
61
|
+
|
62
|
+
requirements: []
|
63
|
+
|
64
|
+
dependencies:
|
65
|
+
- !ruby/object:Gem::Dependency
|
66
|
+
name: mongrel
|
67
|
+
version_requirement:
|
68
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">"
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: 0.0.0
|
73
|
+
version:
|
74
|
+
- !ruby/object:Gem::Dependency
|
75
|
+
name: builder
|
76
|
+
version_requirement:
|
77
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: 0.0.0
|
82
|
+
version:
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: activesupport
|
85
|
+
version_requirement:
|
86
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: 0.0.0
|
91
|
+
version:
|
92
|
+
- !ruby/object:Gem::Dependency
|
93
|
+
name: activerecord
|
94
|
+
version_requirement:
|
95
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">"
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: 0.0.0
|
100
|
+
version:
|
101
|
+
- !ruby/object:Gem::Dependency
|
102
|
+
name: hoe
|
103
|
+
version_requirement:
|
104
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: 1.3.0
|
109
|
+
version:
|