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/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'
@@ -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
@@ -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
@@ -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