appcast 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|