visionmedia-swarm 0.0.1
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.md +5 -0
- data/Manifest +39 -0
- data/Rakefile +18 -0
- data/Readme.md +69 -0
- data/bin/swarm +30 -0
- data/jspec/server.rb +4 -0
- data/jspec/spec.dom.html +27 -0
- data/jspec/spec.rhino.js +8 -0
- data/jspec/spec.safe.js +12 -0
- data/jspec/spec.server.html +16 -0
- data/jspec/spec.swarm.js +35 -0
- data/lib/public/images/chrome.png +0 -0
- data/lib/public/images/chrome.sm.png +0 -0
- data/lib/public/images/gecko.png +0 -0
- data/lib/public/images/gecko.sm.png +0 -0
- data/lib/public/images/konqueror.png +0 -0
- data/lib/public/images/konqueror.sm.png +0 -0
- data/lib/public/images/msie.png +0 -0
- data/lib/public/images/msie.sm.png +0 -0
- data/lib/public/images/presto.png +0 -0
- data/lib/public/images/presto.sm.png +0 -0
- data/lib/public/images/webkit.png +0 -0
- data/lib/public/images/webkit.sm.png +0 -0
- data/lib/public/jquery.js +19 -0
- data/lib/public/jquery.rest.js +27 -0
- data/lib/public/jquery.smart-poll.js +40 -0
- data/lib/public/safe.js +2 -0
- data/lib/public/swarm.css +0 -0
- data/lib/public/swarm.html +28 -0
- data/lib/public/swarm.js +92 -0
- data/lib/swarm.rb +31 -0
- data/lib/swarm/agent.rb +78 -0
- data/lib/swarm/job.rb +68 -0
- data/lib/swarm/swarm.rb +185 -0
- data/spec/agent_spec.rb +57 -0
- data/spec/job_spec.rb +45 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/swarm_spec.rb +142 -0
- data/swarm.gemspec +47 -0
- metadata +170 -0
data/lib/swarm.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# (The MIT License)
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2009 TJ Holowaychuk <tj@vision-media.ca>
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
# a copy of this software and associated documentation files (the
|
|
7
|
+
# 'Software'), to deal in the Software without restriction, including
|
|
8
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
# the following conditions:
|
|
12
|
+
#
|
|
13
|
+
# The above copyright notice and this permission notice shall be
|
|
14
|
+
# included in all copies or substantial portions of the Software.
|
|
15
|
+
#
|
|
16
|
+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
20
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
21
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
22
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
require 'json'
|
|
25
|
+
require 'dm-core'
|
|
26
|
+
require 'dm-validations'
|
|
27
|
+
require 'sinatra/base'
|
|
28
|
+
require 'user-agent'
|
|
29
|
+
require 'swarm/swarm'
|
|
30
|
+
require 'swarm/job'
|
|
31
|
+
require 'swarm/agent'
|
data/lib/swarm/agent.rb
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
|
|
2
|
+
class Agent
|
|
3
|
+
|
|
4
|
+
#--
|
|
5
|
+
# Mixins
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
include DataMapper::Resource
|
|
9
|
+
|
|
10
|
+
#--
|
|
11
|
+
# Properties
|
|
12
|
+
#++
|
|
13
|
+
|
|
14
|
+
property :id, Serial
|
|
15
|
+
property :address, String, :length => 5..128, :nullable => false
|
|
16
|
+
property :string, String, :index => true, :length => 5..255, :nullable => false
|
|
17
|
+
property :busy, Boolean, :default => false
|
|
18
|
+
|
|
19
|
+
#--
|
|
20
|
+
# Associations
|
|
21
|
+
#++
|
|
22
|
+
|
|
23
|
+
has n, :jobs, :through => Resource
|
|
24
|
+
|
|
25
|
+
#--
|
|
26
|
+
# Instance methods
|
|
27
|
+
#++
|
|
28
|
+
|
|
29
|
+
##
|
|
30
|
+
# Initialize with _string_or_attrs_.
|
|
31
|
+
|
|
32
|
+
def initialize string_or_attrs
|
|
33
|
+
self.attributes = String === string_or_attrs ?
|
|
34
|
+
{ :string => string_or_attrs } :
|
|
35
|
+
string_or_attrs
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# Check if _other_ equals self. Supports
|
|
40
|
+
# DataMapper join support when checking AgentJob
|
|
41
|
+
# equality, as well as against other Agent instances via
|
|
42
|
+
# their #string value.
|
|
43
|
+
|
|
44
|
+
def == other
|
|
45
|
+
AgentJob === other ?
|
|
46
|
+
id == other.agent_id :
|
|
47
|
+
super
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# Check if the agent is busy.
|
|
52
|
+
|
|
53
|
+
def busy?
|
|
54
|
+
busy
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
#--
|
|
58
|
+
# Class methods
|
|
59
|
+
#++
|
|
60
|
+
|
|
61
|
+
##
|
|
62
|
+
# Unique agents.
|
|
63
|
+
|
|
64
|
+
def self.unique
|
|
65
|
+
all.inject [] do |agents, agent|
|
|
66
|
+
agents << agent unless agents.include? agent
|
|
67
|
+
agents
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# Unique available agents.
|
|
73
|
+
|
|
74
|
+
def self.available
|
|
75
|
+
unique.delete_if { |agent| agent.busy? }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
end
|
data/lib/swarm/job.rb
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
|
|
2
|
+
class Job
|
|
3
|
+
|
|
4
|
+
#--
|
|
5
|
+
# Mixins
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
include DataMapper::Resource
|
|
9
|
+
|
|
10
|
+
#--
|
|
11
|
+
# Properties
|
|
12
|
+
#++
|
|
13
|
+
|
|
14
|
+
property :id, Serial
|
|
15
|
+
property :address, String, :length => 5..128, :nullable => false
|
|
16
|
+
property :source, Text, :lazy => false
|
|
17
|
+
property :results, Object, :lazy => false, :default => []
|
|
18
|
+
property :active, Boolean, :default => true
|
|
19
|
+
|
|
20
|
+
#--
|
|
21
|
+
# Associations
|
|
22
|
+
#++
|
|
23
|
+
|
|
24
|
+
has n, :agents, :through => Resource
|
|
25
|
+
|
|
26
|
+
#--
|
|
27
|
+
# Instance methods
|
|
28
|
+
#++
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# Check if the job is active.
|
|
32
|
+
|
|
33
|
+
def active?
|
|
34
|
+
active
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# Job JSON.
|
|
39
|
+
|
|
40
|
+
def to_json
|
|
41
|
+
{ :id => id,
|
|
42
|
+
:source => source,
|
|
43
|
+
:results => results,
|
|
44
|
+
:address => address,
|
|
45
|
+
:active => active }.to_json
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
#--
|
|
49
|
+
# Class methods
|
|
50
|
+
#++
|
|
51
|
+
|
|
52
|
+
##
|
|
53
|
+
# Return active job for _agent_.
|
|
54
|
+
|
|
55
|
+
def self.for agent
|
|
56
|
+
active.find do |job|
|
|
57
|
+
not agent.jobs.include?(job) || job.agents.any? { |other| agent.string == other.string }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
##
|
|
62
|
+
# Return array of active jobs.
|
|
63
|
+
|
|
64
|
+
def self.active
|
|
65
|
+
all :active => true
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
end
|
data/lib/swarm/swarm.rb
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
|
|
2
|
+
class Swarm < Sinatra::Base
|
|
3
|
+
|
|
4
|
+
#--
|
|
5
|
+
# Constants
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
VERSION = '0.0.1'
|
|
9
|
+
|
|
10
|
+
#--
|
|
11
|
+
# Settings
|
|
12
|
+
#++
|
|
13
|
+
|
|
14
|
+
enable :methodoverride
|
|
15
|
+
disable :raise_errors
|
|
16
|
+
|
|
17
|
+
#--
|
|
18
|
+
# Helpers
|
|
19
|
+
#++
|
|
20
|
+
|
|
21
|
+
error do
|
|
22
|
+
env['ACCEPT'] =~ /json/ ?
|
|
23
|
+
json(:status => 0, :message => env['sinatra.error'].to_s) :
|
|
24
|
+
env['sinatra.error'].to_s
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
##
|
|
28
|
+
# Log _string_ to stderr.
|
|
29
|
+
|
|
30
|
+
def log string
|
|
31
|
+
$stderr.puts string
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
##
|
|
35
|
+
# Halts with status of 200 and JSON _object_.
|
|
36
|
+
# Sets content type to application/json and jsonifies the object.
|
|
37
|
+
|
|
38
|
+
def json object
|
|
39
|
+
content_type :json
|
|
40
|
+
object[:status] ||= 1 if object.is_a? Hash
|
|
41
|
+
halt 200, object.to_json
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
# User agent.
|
|
46
|
+
|
|
47
|
+
def user_agent
|
|
48
|
+
env['HTTP_USER_AGENT']
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
##
|
|
52
|
+
# Remote address.
|
|
53
|
+
|
|
54
|
+
def address
|
|
55
|
+
env['REMOTE_ADDR']
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
##
|
|
59
|
+
# Path to public _file_.
|
|
60
|
+
|
|
61
|
+
def public_path_for file
|
|
62
|
+
File.dirname(__FILE__) + '/../public/' + file
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
#--
|
|
66
|
+
# Agent
|
|
67
|
+
#++
|
|
68
|
+
|
|
69
|
+
##
|
|
70
|
+
# Send public static files.
|
|
71
|
+
|
|
72
|
+
get '/public/*' do |file|
|
|
73
|
+
send_file public_path_for(file)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
##
|
|
77
|
+
# Bind a user agent. Reponds with swarm.html
|
|
78
|
+
# which then continues to poll for jobs.
|
|
79
|
+
|
|
80
|
+
get '/bind/?' do
|
|
81
|
+
agent = Agent.new :string => user_agent, :address => address
|
|
82
|
+
raise 'invalid user agent' unless agent.save
|
|
83
|
+
log "bound: #{agent.inspect}"
|
|
84
|
+
log " #{agent.string.inspect}"
|
|
85
|
+
send_file public_path_for('swarm.html')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
##
|
|
89
|
+
# Get a job.
|
|
90
|
+
#
|
|
91
|
+
# * Loads the current agent
|
|
92
|
+
# * Finds an active job
|
|
93
|
+
# * Marks agent as busy
|
|
94
|
+
# * Responds with job json.
|
|
95
|
+
#
|
|
96
|
+
|
|
97
|
+
get '/job/?' do
|
|
98
|
+
if agent = Agent.first(:string => user_agent, :address => address)
|
|
99
|
+
if job = Job.for(agent)
|
|
100
|
+
agent.update :busy => true
|
|
101
|
+
log "running job: #{job.id} with #{agent}"
|
|
102
|
+
json job
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
raise 'failed to find active job'
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
##
|
|
109
|
+
# Get job _id_ as json.
|
|
110
|
+
|
|
111
|
+
get '/job/:id.json' do |id|
|
|
112
|
+
if job = Job.get(id)
|
|
113
|
+
json job
|
|
114
|
+
end
|
|
115
|
+
raise "failed to load job #{id}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
#--
|
|
119
|
+
# Client
|
|
120
|
+
#++
|
|
121
|
+
|
|
122
|
+
##
|
|
123
|
+
# Get DOM results for job _id_.
|
|
124
|
+
|
|
125
|
+
get '/job/:id/?' do |id|
|
|
126
|
+
if job = Job.get(id)
|
|
127
|
+
p job
|
|
128
|
+
p job.results
|
|
129
|
+
'TODO: show table / update progress by polling job/:id.json'
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
#--
|
|
134
|
+
# Server
|
|
135
|
+
#++
|
|
136
|
+
|
|
137
|
+
##
|
|
138
|
+
# Push job :source.
|
|
139
|
+
|
|
140
|
+
post '/job/?' do
|
|
141
|
+
if js = params[:source]
|
|
142
|
+
job = Job.create :source => js, :address => address
|
|
143
|
+
log "pushed job: %d %0.3fkb %s" % [job.id, js.length / 1024, job.address]
|
|
144
|
+
json :message => "job #{job.id} created", :id => job.id
|
|
145
|
+
end
|
|
146
|
+
raise ':source required'
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
##
|
|
150
|
+
# Update job _id_ with results.
|
|
151
|
+
|
|
152
|
+
put '/job/:id/?' do |id|
|
|
153
|
+
if agent = Agent.first(:string => user_agent, :address => address, :busy => true)
|
|
154
|
+
if job = Job.get(id)
|
|
155
|
+
job.results << params
|
|
156
|
+
agent.jobs << job
|
|
157
|
+
agent.update :busy => false
|
|
158
|
+
job.save
|
|
159
|
+
log "updated job: #{job.id} #{params.inspect} with #{agent}"
|
|
160
|
+
halt 200
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
raise "failed to update job #{id}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
#--
|
|
167
|
+
# Class methods
|
|
168
|
+
#++
|
|
169
|
+
|
|
170
|
+
##
|
|
171
|
+
# Return array of agent instances for _names_.
|
|
172
|
+
|
|
173
|
+
def self.agents_for *names
|
|
174
|
+
unless names.empty?
|
|
175
|
+
Agent.available.select do |agent|
|
|
176
|
+
names.any? do |name|
|
|
177
|
+
agent.name.to_s.downcase == name.to_s.downcase if agent.name
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
else
|
|
181
|
+
Agent.available
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
end
|
data/spec/agent_spec.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
|
|
2
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
|
3
|
+
|
|
4
|
+
describe Agent do
|
|
5
|
+
before :each do
|
|
6
|
+
DataMapper.auto_migrate!
|
|
7
|
+
@safari = Agent.create :string => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en) AppleWebKit/526.9 (KHTML, like Gecko) Version/4.0dp1 Safari/526.8'
|
|
8
|
+
@chrome = Agent.create :string => 'Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.202.2 Safari/532.0'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
describe "#string" do
|
|
12
|
+
describe "should still work with DataMapper" do
|
|
13
|
+
it "when using .new" do
|
|
14
|
+
Agent.new('foo').string.should == 'foo'
|
|
15
|
+
Agent.new(:string => 'foo').string.should == 'foo'
|
|
16
|
+
Agent.new(:address => 'foo').address.should == 'foo'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "when using .create" do
|
|
20
|
+
Agent.create(:string => 'foo').string.should == 'foo'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "should #string=" do
|
|
24
|
+
agent = Agent.new 'foo'
|
|
25
|
+
agent.string = 'bar'
|
|
26
|
+
agent.string.should == 'bar'
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe "#busy?" do
|
|
32
|
+
it "should default to false" do
|
|
33
|
+
Agent.create(:string => 'test').should_not be_busy
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "should check if the agent is busy" do
|
|
37
|
+
Agent.create(:string => 'test', :busy => true).should be_busy
|
|
38
|
+
Agent.create(:string => 'test', :busy => false).should_not be_busy
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe ".unique" do
|
|
43
|
+
it "should return all unique agents available" do
|
|
44
|
+
Agent.stub!(:all).and_return [@chrome, @safari, @chrome, @chrome]
|
|
45
|
+
Agent.unique.should == [@chrome, @safari]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe ".available" do
|
|
50
|
+
it "should return all available / unique agents" do
|
|
51
|
+
Agent.stub!(:all).and_return [@chrome, @safari, @chrome, @chrome]
|
|
52
|
+
Agent.available.should == [@chrome, @safari]
|
|
53
|
+
@chrome.busy = true
|
|
54
|
+
Agent.available.should == [@safari]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/spec/job_spec.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
|
|
2
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
|
3
|
+
|
|
4
|
+
describe Job do
|
|
5
|
+
before :each do
|
|
6
|
+
DataMapper.auto_migrate!
|
|
7
|
+
@active_job = Job.create :source => 'alert("foo")', :address => '127.0.0.1'
|
|
8
|
+
@active_job2 = Job.create :source => 'alert("bar")', :address => '127.0.0.1'
|
|
9
|
+
@inactive_job = Job.create :source => 'alert("baz")', :address => '127.0.0.1', :active => false
|
|
10
|
+
@safari = Agent.create :string => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en) AppleWebKit/526.9 (KHTML, like Gecko) Version/4.0dp1 Safari/526.8', :address => '1.1.1.1'
|
|
11
|
+
@safari2 = Agent.create :string => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en) AppleWebKit/526.9 (KHTML, like Gecko) Version/4.0dp1 Safari/526.8', :address => '2.2.2.2'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe "#to_json" do
|
|
15
|
+
it "should convert a job to JSON" do
|
|
16
|
+
json = @active_job.to_json
|
|
17
|
+
json.should include('"source":"alert')
|
|
18
|
+
json.should include('"results":[]')
|
|
19
|
+
json.should include('"active":true')
|
|
20
|
+
json.should include('"address":"127.0.0.1"')
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe ".active" do
|
|
25
|
+
it "should return active jobs" do
|
|
26
|
+
Job.active.should == [@active_job, @active_job2]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe ".for" do
|
|
31
|
+
it "should return an job for the agent passed" do
|
|
32
|
+
@safari.jobs << @active_job
|
|
33
|
+
Job.for(@safari).should == @active_job2
|
|
34
|
+
@safari.jobs << @active_job2
|
|
35
|
+
Job.for(@safari).should be_nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "should only return a job that has not been provided to a similar agent" do
|
|
39
|
+
@safari.jobs << @active_job << @active_job2
|
|
40
|
+
@safari.save
|
|
41
|
+
Job.for(@safari).should be_nil
|
|
42
|
+
Job.for(@safari2).should be_nil
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|