simultaneous 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +6 -0
- data/LICENSE +20 -0
- data/README +0 -0
- data/Rakefile +152 -0
- data/bin/simultaneous-console +139 -0
- data/bin/simultaneous-server +60 -0
- data/lib/simultaneous/broadcast_message.rb +63 -0
- data/lib/simultaneous/client.rb +111 -0
- data/lib/simultaneous/command/client_event.rb +24 -0
- data/lib/simultaneous/command/fire.rb +155 -0
- data/lib/simultaneous/command/kill.rb +19 -0
- data/lib/simultaneous/command/set_pid.rb +22 -0
- data/lib/simultaneous/command/task_complete.rb +18 -0
- data/lib/simultaneous/command.rb +75 -0
- data/lib/simultaneous/connection.rb +81 -0
- data/lib/simultaneous/rack.rb +83 -0
- data/lib/simultaneous/server.rb +74 -0
- data/lib/simultaneous/task.rb +37 -0
- data/lib/simultaneous/task_client.rb +37 -0
- data/lib/simultaneous/task_description.rb +37 -0
- data/lib/simultaneous.rb +265 -0
- data/simultaneous.gemspec +96 -0
- data/test/helper.rb +14 -0
- data/test/tasks/example.rb +22 -0
- data/test/test_client.rb +24 -0
- data/test/test_command.rb +72 -0
- data/test/test_connection.rb +91 -0
- data/test/test_faf.rb +43 -0
- data/test/test_message.rb +81 -0
- data/test/test_server.rb +242 -0
- data/test/test_task.rb +21 -0
- metadata +145 -0
@@ -0,0 +1,96 @@
|
|
1
|
+
## This is the rakegem gemspec template. Make sure you read and understand
|
2
|
+
## all of the comments. Some sections require modification, and others can
|
3
|
+
## be deleted if you don't need them. Once you understand the contents of
|
4
|
+
## this file, feel free to delete any comments that begin with two hash marks.
|
5
|
+
## You can find comprehensive Gem::Specification documentation, at
|
6
|
+
## http://docs.rubygems.org/read/chapter/20
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.specification_version = 2 if s.respond_to? :specification_version=
|
9
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
10
|
+
s.rubygems_version = '1.3.5'
|
11
|
+
|
12
|
+
## Leave these as is they will be modified for you by the rake gemspec task.
|
13
|
+
## If your rubyforge_project name is different, then edit it and comment out
|
14
|
+
## the sub! line in the Rakefile
|
15
|
+
s.name = 'simultaneous'
|
16
|
+
s.version = '0.2.0'
|
17
|
+
s.date = '2011-11-03'
|
18
|
+
s.rubyforge_project = 'simultaneous'
|
19
|
+
|
20
|
+
## Make sure your summary is short. The description may be as long
|
21
|
+
## as you like.
|
22
|
+
s.summary = "Simultaneous is the background task launcher used by Spontaneous CMS"
|
23
|
+
s.description = "Simultaneous is designed for the very specific use case of a small set of users collaborating on editing a single website. Because of that it is optimised for infrequent invocation of very long running publishing tasks and provides an event based messaging system that allows launched tasks to communicate back to the CMS web-server and for that server to then fire off update messages through HTML5 Server-Sent Events."
|
24
|
+
|
25
|
+
## List the primary authors. If there are a bunch of authors, it's probably
|
26
|
+
## better to set the email to an email list or something. If you don't have
|
27
|
+
## a custom homepage, consider using your GitHub URL or the like.
|
28
|
+
s.authors = ["Garry Hill"]
|
29
|
+
s.email = 'garry@magnetised.info'
|
30
|
+
s.homepage = 'http://spontaneouscms.org'
|
31
|
+
|
32
|
+
## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
|
33
|
+
## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
|
34
|
+
s.require_paths = %w[lib]
|
35
|
+
|
36
|
+
## If your gem includes any executables, list them here.
|
37
|
+
s.executables = ["simultaneous-server", "simultaneous-console"]
|
38
|
+
|
39
|
+
## Specify any RDoc options here. You'll want to add your README and
|
40
|
+
## LICENSE files to the extra_rdoc_files list.
|
41
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
42
|
+
s.extra_rdoc_files = %w[README LICENSE]
|
43
|
+
|
44
|
+
## List your runtime dependencies here. Runtime dependencies are those
|
45
|
+
## that are needed for an end user to actually USE your code.
|
46
|
+
s.add_dependency('eventmachine', [">= 1.0.0.beta.4", "< 2.0"])
|
47
|
+
s.add_dependency('rack', [">= 1.0", "< 2.0"])
|
48
|
+
s.add_dependency('rack-async', [">= 0.0.1", "< 2.0"])
|
49
|
+
|
50
|
+
## List your development dependencies here. Development dependencies are
|
51
|
+
## those that are only needed during development
|
52
|
+
s.add_development_dependency('rr', ["~> 1.0.4"])
|
53
|
+
|
54
|
+
## Leave this section as-is. It will be automatically generated from the
|
55
|
+
## contents of your Git repository via the gemspec task. DO NOT REMOVE
|
56
|
+
## THE MANIFEST COMMENTS, they are used as delimiters by the task.
|
57
|
+
# = MANIFEST =
|
58
|
+
s.files = %w[
|
59
|
+
Gemfile
|
60
|
+
LICENSE
|
61
|
+
README
|
62
|
+
Rakefile
|
63
|
+
bin/simultaneous-console
|
64
|
+
bin/simultaneous-server
|
65
|
+
lib/simultaneous.rb
|
66
|
+
lib/simultaneous/broadcast_message.rb
|
67
|
+
lib/simultaneous/client.rb
|
68
|
+
lib/simultaneous/command.rb
|
69
|
+
lib/simultaneous/command/client_event.rb
|
70
|
+
lib/simultaneous/command/fire.rb
|
71
|
+
lib/simultaneous/command/kill.rb
|
72
|
+
lib/simultaneous/command/set_pid.rb
|
73
|
+
lib/simultaneous/command/task_complete.rb
|
74
|
+
lib/simultaneous/connection.rb
|
75
|
+
lib/simultaneous/rack.rb
|
76
|
+
lib/simultaneous/server.rb
|
77
|
+
lib/simultaneous/task.rb
|
78
|
+
lib/simultaneous/task_client.rb
|
79
|
+
lib/simultaneous/task_description.rb
|
80
|
+
simultaneous.gemspec
|
81
|
+
test/helper.rb
|
82
|
+
test/tasks/example.rb
|
83
|
+
test/test_client.rb
|
84
|
+
test/test_command.rb
|
85
|
+
test/test_connection.rb
|
86
|
+
test/test_faf.rb
|
87
|
+
test/test_message.rb
|
88
|
+
test/test_server.rb
|
89
|
+
test/test_task.rb
|
90
|
+
]
|
91
|
+
# = MANIFEST =
|
92
|
+
|
93
|
+
## Test files will be grabbed from the file list. Make sure the path glob
|
94
|
+
## matches what you actually use.
|
95
|
+
s.test_files = s.files.select { |path| path =~ /^test\/test_.*\.rb/ }
|
96
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
|
5
|
+
$:.unshift(File.expand_path("../../../lib", __FILE__))
|
6
|
+
|
7
|
+
require 'simultaneous'
|
8
|
+
|
9
|
+
class MyTask
|
10
|
+
include Simultaneous::Task
|
11
|
+
|
12
|
+
def run
|
13
|
+
puts ARGV[0]
|
14
|
+
# 10.times do |i|
|
15
|
+
# puts i
|
16
|
+
# sleep(1)
|
17
|
+
# end
|
18
|
+
simultaneous_event("example", "done")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
MyTask.new.run
|
data/test/test_client.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
describe Simultaneous::Client do
|
4
|
+
it "should send right command to server" do
|
5
|
+
EM.run {
|
6
|
+
task = Simultaneous.add_task(:publish, "/publish", {:param1 => "value1", :param2 => "value2"}, 12)
|
7
|
+
command = Object.new
|
8
|
+
mock(command).domain=("faf.org")
|
9
|
+
mock(command).dump { "dumpedcommand" }
|
10
|
+
mock(Simultaneous::Command::Fire).new(task, {:param2 => "value3"}) { command }
|
11
|
+
|
12
|
+
Simultaneous.domain = "faf.org"
|
13
|
+
Simultaneous.connection = SOCKET
|
14
|
+
Simultaneous::Server.start
|
15
|
+
|
16
|
+
mock(Simultaneous::Server).receive_data("dumpedcommand") { EM.stop }
|
17
|
+
|
18
|
+
Thread.new {
|
19
|
+
pid = Simultaneous.publish({:param2 => "value3"})
|
20
|
+
}.join
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
describe Simultaneous::Command do
|
4
|
+
before do
|
5
|
+
Simultaneous.domain = "example.net"
|
6
|
+
@task = Simultaneous::TaskDescription.new(:publish, "/publish", 9, {:param1 => "value1", :param2 => "value2"})
|
7
|
+
end
|
8
|
+
it "should serialize and deserialize correctly" do
|
9
|
+
task = Simultaneous::TaskDescription.new(:publish, "/publish", 9, {:param1 => "value1", :param2 => "value2"})
|
10
|
+
cmd = Simultaneous::Command::CommandBase.new(task, {"param2" => "newvalue2", :param3 => "value3"})
|
11
|
+
cmd2 = Simultaneous::Command.load(cmd.dump)
|
12
|
+
task2 = cmd2.task
|
13
|
+
task2.binary.must_equal task.binary
|
14
|
+
task2.params.must_equal task.params
|
15
|
+
task2.name.must_equal task.name
|
16
|
+
cmd.params.must_equal({"param1" => "value1", "param2" => "newvalue2", "param3" => "value3"})
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should only run scripts belonging to the same user as the ruby process" do
|
20
|
+
stub(File).exist?("/publish") { true }
|
21
|
+
stub(File).exists?("/publish") { true }
|
22
|
+
stat = Object.new
|
23
|
+
stub(stat).uid { Process.uid + 1 }
|
24
|
+
stub(File).stat("/publish") { stat }
|
25
|
+
cmd = Simultaneous::Command::Fire.new(@task)
|
26
|
+
lambda { cmd.run }.must_raise(Simultaneous::PermissionsError)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should not raise an error if the binary belongs to this process" do
|
30
|
+
stat = Object.new
|
31
|
+
stub(stat).uid { Process.uid }
|
32
|
+
stub(File).stat("/publish") { stat }
|
33
|
+
cmd = Simultaneous::Command::Fire.new(@task)
|
34
|
+
cmd.permitted?
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should raise an error if the binary belongs to this process" do
|
38
|
+
stat = Object.new
|
39
|
+
uid = Process.uid
|
40
|
+
stub(stat).uid { uid }
|
41
|
+
stub(File).stat("/publish") { stat }
|
42
|
+
stub(Process).euid { uid + 1 }
|
43
|
+
cmd = Simultaneous::Command::Fire.new(@task)
|
44
|
+
lambda { cmd.permitted? }.must_raise(Simultaneous::PermissionsError)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should give error if binary doesn't exist" do
|
48
|
+
stub(File).exist?("/publish") { false }
|
49
|
+
stub(File).exists?("/publish") { false }
|
50
|
+
stub(File).owned?("/publish") { true }
|
51
|
+
cmd = Simultaneous::Command::Fire.new(@task)
|
52
|
+
lambda { cmd.run }.must_raise(Simultaneous::FileNotFoundError)
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should raise error if command isn't one of the approved list" do
|
56
|
+
cmd = Object.new
|
57
|
+
mock(cmd).run.times(0)
|
58
|
+
lambda { Simultaneous::Server.run(cmd) }.must_raise(Simultaneous::PermissionsError)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should work with binaries involving a command" do
|
62
|
+
task = Simultaneous::TaskDescription.new(:publish, "/publish all")
|
63
|
+
stat = Object.new
|
64
|
+
stub(stat).uid { Process.uid }
|
65
|
+
stub(File).stat("/publish") { stat }
|
66
|
+
stub(File).exist?("/publish") { true }
|
67
|
+
stub(File).exists?("/publish") { true }
|
68
|
+
cmd = Simultaneous::Command::Fire.new(task)
|
69
|
+
cmd.valid?
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
describe Simultaneous::Connection do
|
4
|
+
|
5
|
+
after do
|
6
|
+
# FileUtils.rm( "/tmp/socket.sock") if File.exist?( "/tmp/socket.sock")
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should allow me to generate a connection string from host & port values" do
|
10
|
+
Simultaneous::Connection.tcp("localhost", 999).must_equal "localhost:999"
|
11
|
+
end
|
12
|
+
it "should be correct recognise TCP connections" do
|
13
|
+
%w(127.0.0.1:9999 localhost:1234 123.239.23.1:9999 host.domain.com:9999).each do |c|
|
14
|
+
Simultaneous::Connection.new(c).tcp?.must_be :==, true
|
15
|
+
Simultaneous::Connection.new(c).unix?.must_be :==, false
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should be correct recognise UNIX connections" do
|
20
|
+
%w(/path/to/socket.sock socket file.sock).each do |c|
|
21
|
+
Simultaneous::Connection.new(c).tcp?.must_be :==, false
|
22
|
+
Simultaneous::Connection.new(c).unix?.must_be :==, true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should open an EventMachine TCP server connection" do
|
27
|
+
conn = Simultaneous::Connection.new("127.0.0.1:9999")
|
28
|
+
handler = Object.new
|
29
|
+
mock(EventMachine).start_server("127.0.0.1", 9999, handler)
|
30
|
+
conn.start_server(handler)
|
31
|
+
end
|
32
|
+
it "should open an EventMachine TCP client connection" do
|
33
|
+
conn = Simultaneous::Connection.new("127.0.0.1:9999")
|
34
|
+
handler = Object.new
|
35
|
+
mock(EventMachine).connect("127.0.0.1", 9999, handler)
|
36
|
+
conn.async_socket(handler)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should open an synchronous TCP client connection" do
|
40
|
+
conn = Simultaneous::Connection.new("127.0.0.1:9999")
|
41
|
+
socket = Object.new
|
42
|
+
mock(socket).write("string")
|
43
|
+
mock(socket).flush
|
44
|
+
mock(socket).close_write
|
45
|
+
mock(socket).close
|
46
|
+
mock(TCPSocket).new("127.0.0.1", 9999) { socket }
|
47
|
+
conn.sync_socket do |s|
|
48
|
+
s.write("string")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should open an EventMachine Unix server connection" do
|
53
|
+
conn = Simultaneous::Connection.new("/tmp/socket.sock")
|
54
|
+
handler = Object.new
|
55
|
+
mock(EventMachine).start_server("/tmp/socket.sock", handler)
|
56
|
+
conn.start_server(handler)
|
57
|
+
end
|
58
|
+
it "should open an EventMachine Unix client connection" do
|
59
|
+
conn = Simultaneous::Connection.new("/tmp/socket.sock")
|
60
|
+
handler = Object.new
|
61
|
+
mock(EventMachine).connect("/tmp/socket.sock", handler)
|
62
|
+
conn.async_socket(handler)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should open an synchronous Unix client connection" do
|
66
|
+
conn = Simultaneous::Connection.new("/tmp/socket.sock")
|
67
|
+
socket = Object.new
|
68
|
+
mock(socket).write("string")
|
69
|
+
mock(socket).flush
|
70
|
+
mock(socket).close_write
|
71
|
+
mock(socket).close
|
72
|
+
mock(UNIXSocket).new("/tmp/socket.sock") { socket }
|
73
|
+
conn.sync_socket do |s|
|
74
|
+
s.write("string")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should set the correct permissions on the EventMachine server socket" do
|
79
|
+
socket = "/tmp/socket.sock"
|
80
|
+
gid = "789"
|
81
|
+
FileUtils.rm(socket) if File.exist?(socket)
|
82
|
+
mock(File).chmod(0770, socket)
|
83
|
+
mock(File).chown(nil, gid, socket)
|
84
|
+
handler = Module.new
|
85
|
+
mock(EventMachine).start_server(socket, handler) { FileUtils.touch(socket) }
|
86
|
+
conn = Simultaneous::Connection.new(socket, {:gid => gid})
|
87
|
+
conn.start_server(handler)
|
88
|
+
FileUtils.rm(socket) if File.exist?(socket)
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
data/test/test_faf.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
describe Simultaneous do
|
4
|
+
it "should translate a hash to command line arguments" do
|
5
|
+
Simultaneous.to_arguments({
|
6
|
+
:param1 => "value1",
|
7
|
+
:param2 => "value2",
|
8
|
+
:array => [1, 2, "3 of 4"],
|
9
|
+
:hash => {:name => "Fred", :age => 23}
|
10
|
+
}).must_equal %(--array=1 2 "3 of 4" --hash=name:"Fred" age:23 --param1="value1" --param2="value2")
|
11
|
+
end
|
12
|
+
it "should enable mapping of task to a binary" do
|
13
|
+
Simultaneous.add_task(:publish, "/path/to/binary")
|
14
|
+
Simultaneous.binary(:publish).must_equal "/path/to/binary"
|
15
|
+
Simultaneous[:publish].binary.must_equal "/path/to/binary"
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should enable setting of a niceness value for the task" do
|
19
|
+
Simultaneous.add_task(:publish1, "/path/to/binary", {:niceness => 10})
|
20
|
+
Simultaneous[:publish1].niceness.must_equal 10
|
21
|
+
Simultaneous.add_task(:publish2, "/path/to/binary", {:nice => 12})
|
22
|
+
Simultaneous[:publish2].niceness.must_equal 12
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
it "should enable launching a task by its name" do
|
27
|
+
Simultaneous.add_task(:publish, "/path/to/binary")
|
28
|
+
args = {:param1 => "param1", :param2 => "param2"}
|
29
|
+
mock(Simultaneous).fire(:publish, args)
|
30
|
+
Simultaneous.publish(args)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should enable setting of path to socket" do
|
34
|
+
Simultaneous.connection = "/tmp/something"
|
35
|
+
Simultaneous.connection.must_equal "/tmp/something"
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should enable setting of domain" do
|
39
|
+
Simultaneous.domain = "domain_name"
|
40
|
+
Simultaneous.domain.must_equal "domain_name"
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
describe Simultaneous::BroadcastMessage do
|
4
|
+
|
5
|
+
describe "when parsing input" do
|
6
|
+
before do
|
7
|
+
@message = Simultaneous::BroadcastMessage.new
|
8
|
+
end
|
9
|
+
it "should begin as invalid" do
|
10
|
+
@message.valid?.wont_be :==, true
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should recognise the domain: header" do
|
14
|
+
@message << "domain: name"
|
15
|
+
@message.domain.must_equal "name"
|
16
|
+
@message.valid?.wont_be :==, true
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should recognise the event: header" do
|
20
|
+
@message << "event: name"
|
21
|
+
@message.event.must_equal "name"
|
22
|
+
@message.valid?.wont_be :==, true
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should recognise the data: header" do
|
26
|
+
@message << "data: a"
|
27
|
+
@message.data.must_equal "a"
|
28
|
+
@message << "data: b"
|
29
|
+
@message.data.must_equal "a\nb"
|
30
|
+
@message << "data: c"
|
31
|
+
@message.data.must_equal "a\nb\nc"
|
32
|
+
@message.valid?.wont_be :==, true
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should ignore comments" do
|
36
|
+
@message << ": a"
|
37
|
+
@message.data.must_equal ""
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should ignore blank lines" do
|
41
|
+
@message << ""
|
42
|
+
@message.data.must_equal ""
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "when data has been parsed" do
|
47
|
+
before do
|
48
|
+
@message = Simultaneous::BroadcastMessage.new
|
49
|
+
@message.event = "event"
|
50
|
+
@message.domain = "domain"
|
51
|
+
@message.data = "line 1\nline 2"
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should be valid" do
|
55
|
+
@message.valid?.must_equal true
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should serialise to a SSE-friendly format" do
|
59
|
+
@message.to_event.must_equal((<<-SRC).gsub(/^ */, ''))
|
60
|
+
domain: domain
|
61
|
+
event: event
|
62
|
+
data: line 1
|
63
|
+
data: line 2
|
64
|
+
|
65
|
+
SRC
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "when initialising" do
|
70
|
+
it "should accept values at initialisation" do
|
71
|
+
message = Simultaneous::BroadcastMessage.new({
|
72
|
+
:domain => "domain",
|
73
|
+
:event => "event",
|
74
|
+
:data => "line 1\nline 2"
|
75
|
+
})
|
76
|
+
message.domain.must_equal "domain"
|
77
|
+
message.event.must_equal "event"
|
78
|
+
message.data.must_equal "line 1\nline 2"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|