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,111 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'eventmachine'
|
4
|
+
|
5
|
+
module Simultaneous
|
6
|
+
class Client
|
7
|
+
|
8
|
+
|
9
|
+
attr_reader :domain
|
10
|
+
|
11
|
+
def initialize(domain, connection_string, &block)
|
12
|
+
@connection = Simultaneous::Connection.new(connection_string)
|
13
|
+
@domain = domain
|
14
|
+
@callbacks = []
|
15
|
+
@socket = nil
|
16
|
+
connect
|
17
|
+
end
|
18
|
+
|
19
|
+
def handler
|
20
|
+
handler = Class.new(EventMachine::Connection) do
|
21
|
+
include EventMachine::Protocols::LineText2
|
22
|
+
|
23
|
+
def client=(client); @client = client end
|
24
|
+
def client; @client end
|
25
|
+
|
26
|
+
def connection_completed
|
27
|
+
# $stdout.puts "Client connection completed\\n"
|
28
|
+
end
|
29
|
+
|
30
|
+
def receive_line(line)
|
31
|
+
client.receive(line)
|
32
|
+
end
|
33
|
+
|
34
|
+
# def unbind
|
35
|
+
# $stdout.puts "Client Connection closed\\n"
|
36
|
+
# client.reconnect!
|
37
|
+
# end
|
38
|
+
end
|
39
|
+
handler
|
40
|
+
end
|
41
|
+
|
42
|
+
def reconnect!
|
43
|
+
@socket = nil
|
44
|
+
connect
|
45
|
+
end
|
46
|
+
|
47
|
+
def close
|
48
|
+
@socket.close_connection_after_writing if @socket
|
49
|
+
end
|
50
|
+
|
51
|
+
def run(command)
|
52
|
+
command.domain = self.domain
|
53
|
+
send(command.dump)
|
54
|
+
end
|
55
|
+
|
56
|
+
def send(message)
|
57
|
+
connection do |c|
|
58
|
+
c.send_data(message)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def connection(&callback)
|
63
|
+
callback.call(@socket) if @socket
|
64
|
+
end
|
65
|
+
|
66
|
+
def connect
|
67
|
+
event_machine do
|
68
|
+
# EventMachine.connect(*Simultaneous.parse_connection(@connection_string), handler) do |conn|
|
69
|
+
@connection.async_socket(handler) do |conn|
|
70
|
+
conn.client = self
|
71
|
+
@socket = conn
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def receive(data)
|
77
|
+
if data == ""
|
78
|
+
notify! if @message
|
79
|
+
else
|
80
|
+
@message ||= Simultaneous::BroadcastMessage.new
|
81
|
+
@message << data
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def notify!
|
86
|
+
if @message.valid? and @message.domain == @domain
|
87
|
+
subscribers[@message.event].each do |subscriber|
|
88
|
+
subscriber.call(@message)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
@message = nil
|
92
|
+
end
|
93
|
+
|
94
|
+
def subscribers
|
95
|
+
@subscribers ||= Hash.new { |hash, key| hash[key] = [] }
|
96
|
+
end
|
97
|
+
|
98
|
+
def on_event(event, &block)
|
99
|
+
subscribers[event.to_s] << block
|
100
|
+
end
|
101
|
+
|
102
|
+
def event_machine(&block)
|
103
|
+
if EM.reactor_running?
|
104
|
+
block.call
|
105
|
+
else
|
106
|
+
Thread.new { EM.run }
|
107
|
+
EM.next_tick { block.call }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Simultaneous
|
4
|
+
module Command
|
5
|
+
class ClientEvent < CommandBase
|
6
|
+
|
7
|
+
def initialize(domain, event, data)
|
8
|
+
@domain, @event, @data = domain, event, data
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
message = Simultaneous::BroadcastMessage.new({
|
13
|
+
:domain => @domain,
|
14
|
+
:event => @event,
|
15
|
+
:data => @data
|
16
|
+
})
|
17
|
+
Simultaneous::Server.broadcast(message.to_event)
|
18
|
+
end
|
19
|
+
def debug
|
20
|
+
"ClientEvent: #{@domain}:#{@event} #{@data.inspect}\n"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module Simultaneous
|
6
|
+
module Command
|
7
|
+
class Fire < CommandBase
|
8
|
+
attr_reader :task_uid, :task_gid
|
9
|
+
|
10
|
+
def initialize(task, params={})
|
11
|
+
super
|
12
|
+
@task_uid = Process.euid
|
13
|
+
@task_gid = Process.egid
|
14
|
+
end
|
15
|
+
|
16
|
+
def niceness
|
17
|
+
@task.niceness
|
18
|
+
end
|
19
|
+
|
20
|
+
def binary
|
21
|
+
@task.binary
|
22
|
+
end
|
23
|
+
|
24
|
+
def binary_file
|
25
|
+
@task.binary.split(" ").first
|
26
|
+
end
|
27
|
+
|
28
|
+
def cmd
|
29
|
+
%(#{binary} #{Simultaneous.to_arguments(@params)})
|
30
|
+
end
|
31
|
+
|
32
|
+
def valid?
|
33
|
+
exists? && permitted?
|
34
|
+
end
|
35
|
+
|
36
|
+
def permitted?
|
37
|
+
raise PermissionsError, "'#{binary_file}' does not belong to user '#{ENV["USER"]}'" unless File.stat(binary_file).uid == task_uid
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
def exists?
|
42
|
+
raise FileNotFoundError, "'#{binary_file}'" unless File.exists?(binary_file)
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def env
|
47
|
+
@task.env.merge({
|
48
|
+
Simultaneous::ENV_CONNECTION => Simultaneous.connection,
|
49
|
+
Simultaneous::ENV_TASK_NAME => task_name,
|
50
|
+
Simultaneous::ENV_DOMAIN => domain
|
51
|
+
})
|
52
|
+
end
|
53
|
+
|
54
|
+
def run
|
55
|
+
if valid?
|
56
|
+
pid = fork do
|
57
|
+
## set up the environment so that the task can access the F&F server
|
58
|
+
env.each { | k, v | ENV[k] = v }
|
59
|
+
## TODO: figure out how to pass a logfile path to this
|
60
|
+
daemonize(cmd, @task.logfile)
|
61
|
+
Process.setpriority(Process::PRIO_PROCESS, 0, niceness) if niceness > 0
|
62
|
+
## change to the UID of the originating thread if necessary
|
63
|
+
Process::UID.change_privilege(task_uid) unless Process.euid == task_uid
|
64
|
+
File.umask(0022)
|
65
|
+
Dir.chdir(@task.pwd)
|
66
|
+
exec(cmd)
|
67
|
+
end
|
68
|
+
Process.detach(pid) if pid
|
69
|
+
# don't return the PID as it's actually wrong (the daemonize call forks again so our original
|
70
|
+
# PID is at least 1 out)
|
71
|
+
"OK"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def debug
|
76
|
+
"Fire :#{namespaced_task_name}: #{cmd}\n"
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# The following adapted from Daemons.daemonize
|
82
|
+
def daemonize(app_name = nil, logfile_name = nil)
|
83
|
+
srand # Split rand streams between spawning and daemonized process
|
84
|
+
safefork and exit # Fork and exit from the parent
|
85
|
+
|
86
|
+
# Detach from the controlling terminal
|
87
|
+
unless sess_id = Process.setsid
|
88
|
+
raise RuntimeException.new('cannot detach from controlling terminal')
|
89
|
+
end
|
90
|
+
|
91
|
+
# Prevent the possibility of acquiring a controlling terminal
|
92
|
+
trap 'SIGHUP', 'IGNORE'
|
93
|
+
exit if pid = safefork
|
94
|
+
|
95
|
+
$0 = app_name if app_name
|
96
|
+
|
97
|
+
Dir.chdir "/" # Release old working directory
|
98
|
+
File.umask 0000 # Ensure sensible umask
|
99
|
+
|
100
|
+
# Make sure all file descriptors are closed
|
101
|
+
ObjectSpace.each_object(IO) do |io|
|
102
|
+
unless [STDIN, STDOUT, STDERR].include?(io)
|
103
|
+
begin
|
104
|
+
unless io.closed?
|
105
|
+
io.close
|
106
|
+
end
|
107
|
+
rescue ::Exception
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
redirect_io(logfile_name)
|
113
|
+
|
114
|
+
return sess_id
|
115
|
+
end
|
116
|
+
|
117
|
+
def redirect_io(logfile_name)
|
118
|
+
begin; STDIN.reopen "/dev/null"; rescue ::Exception; end
|
119
|
+
|
120
|
+
if logfile_name
|
121
|
+
FileUtils.mkdir_p(File.dirname(logfile_name))
|
122
|
+
begin
|
123
|
+
STDOUT.reopen logfile_name, "a"
|
124
|
+
File.chmod(0644, logfile_name)
|
125
|
+
STDOUT.sync = true
|
126
|
+
rescue ::Exception
|
127
|
+
begin; STDOUT.reopen "/dev/null"; rescue ::Exception; end
|
128
|
+
end
|
129
|
+
else
|
130
|
+
begin; STDOUT.reopen "/dev/null"; rescue ::Exception; end
|
131
|
+
end
|
132
|
+
|
133
|
+
begin; STDERR.reopen STDOUT; rescue ::Exception; end
|
134
|
+
STDERR.sync = true
|
135
|
+
end
|
136
|
+
|
137
|
+
def safefork
|
138
|
+
tryagain = true
|
139
|
+
|
140
|
+
while tryagain
|
141
|
+
tryagain = false
|
142
|
+
begin
|
143
|
+
if pid = fork
|
144
|
+
return pid
|
145
|
+
end
|
146
|
+
rescue Errno::EWOULDBLOCK
|
147
|
+
sleep 5
|
148
|
+
tryagain = true
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Simultaneous
|
4
|
+
module Command
|
5
|
+
class Kill < CommandBase
|
6
|
+
|
7
|
+
def initialize(task_name, signal="TERM")
|
8
|
+
@task_name, @signal = task_name.to_sym, signal
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
Simultaneous::Server.kill(namespaced_task_name, @signal)
|
13
|
+
end
|
14
|
+
def debug
|
15
|
+
"Kill :#{namespaced_task_name}: #{@signal}\n"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Simultaneous
|
4
|
+
module Command
|
5
|
+
class SetPid < CommandBase
|
6
|
+
|
7
|
+
attr_reader :task_name, :pid
|
8
|
+
|
9
|
+
def initialize(task_name, pid)
|
10
|
+
@task_name, @pid = task_name.to_sym, pid.to_i
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
Simultaneous::Server.pids[namespaced_task_name] = @pid
|
15
|
+
end
|
16
|
+
|
17
|
+
def debug
|
18
|
+
"SetPid :#{namespaced_task_name}: #{@pid}\n"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Simultaneous
|
4
|
+
module Command
|
5
|
+
class TaskComplete < CommandBase
|
6
|
+
def initialize(task_name)
|
7
|
+
@task_name = task_name
|
8
|
+
end
|
9
|
+
|
10
|
+
def run
|
11
|
+
Simultaneous::Server.task_complete(namespaced_task_name)
|
12
|
+
end
|
13
|
+
def debug
|
14
|
+
"TaskComplete :#{namespaced_task_name}\n"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Simultaneous
|
4
|
+
module Command
|
5
|
+
SEPARATOR = "||".freeze
|
6
|
+
|
7
|
+
def self.load(command)
|
8
|
+
Marshal.load(command)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.allowed?(cmd)
|
12
|
+
allowed_commands.include?(cmd.class)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.allowed_commands
|
16
|
+
@allowed_commands ||= self.constants.map { |c| self.const_get(c) }.select do |k|
|
17
|
+
k.respond_to?(:ancestors) && k.ancestors.include?(CommandBase)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class CommandBase
|
22
|
+
attr_reader :tag, :cmd, :params, :task
|
23
|
+
|
24
|
+
def initialize(task, params={})
|
25
|
+
@task, @params = task, merge_params(task.params, params)
|
26
|
+
@task_name = task.name
|
27
|
+
end
|
28
|
+
|
29
|
+
def dump
|
30
|
+
Marshal.dump(self)
|
31
|
+
end
|
32
|
+
|
33
|
+
def run
|
34
|
+
# overridden in subclasses
|
35
|
+
end
|
36
|
+
|
37
|
+
def domain=(domain)
|
38
|
+
@domain = domain
|
39
|
+
end
|
40
|
+
|
41
|
+
def domain
|
42
|
+
@domain ||= ""
|
43
|
+
end
|
44
|
+
|
45
|
+
def task_name
|
46
|
+
@task_name.to_s
|
47
|
+
end
|
48
|
+
|
49
|
+
def namespaced_task_name
|
50
|
+
"#{domain}/#{task_name}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def merge_params(task_params, call_params)
|
54
|
+
params = task_params.to_a.inject({}) do |hash, (key, value)|
|
55
|
+
hash[key.to_s] = value; hash
|
56
|
+
end
|
57
|
+
call_params.each do |key, value|
|
58
|
+
params[key.to_s] = value
|
59
|
+
end if call_params
|
60
|
+
params
|
61
|
+
end
|
62
|
+
|
63
|
+
def debug()
|
64
|
+
"#{self.class.name.split("::").last} :#{@task_name}\n"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
autoload :Fire, "simultaneous/command/fire"
|
70
|
+
autoload :Kill, "simultaneous/command/kill"
|
71
|
+
autoload :SetPid, "simultaneous/command/set_pid"
|
72
|
+
autoload :ClientEvent, "simultaneous/command/client_event"
|
73
|
+
autoload :TaskComplete, "simultaneous/command/task_complete"
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Simultaneous
|
4
|
+
class Connection
|
5
|
+
TCP_CONNECTION_MATCH = %r{^([^/]+):(\d+)}
|
6
|
+
|
7
|
+
def self.tcp(host, port)
|
8
|
+
"#{host}:#{port}"
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :options
|
12
|
+
|
13
|
+
def initialize(connection_string, options = {})
|
14
|
+
@options = options
|
15
|
+
@tcp = false
|
16
|
+
if connection_string =~ TCP_CONNECTION_MATCH
|
17
|
+
@tcp = true
|
18
|
+
@host, @port = $1, $2.to_i
|
19
|
+
else
|
20
|
+
@socket = connection_string
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def tcp?
|
25
|
+
@tcp
|
26
|
+
end
|
27
|
+
|
28
|
+
def unix?
|
29
|
+
!@tcp
|
30
|
+
end
|
31
|
+
|
32
|
+
def start_server(handler, &block)
|
33
|
+
if tcp?
|
34
|
+
EventMachine::start_server(@host, @port, handler, &block)
|
35
|
+
else
|
36
|
+
EventMachine::start_server(@socket, handler, &block)
|
37
|
+
set_socket_permissions(@socket)
|
38
|
+
EM.add_shutdown_hook {
|
39
|
+
FileUtils.rm(@socket) if @socket and File.exist?(@socket)
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def async_socket(handler, &block)
|
45
|
+
if tcp?
|
46
|
+
EventMachine.connect(@host, @port, handler, &block)
|
47
|
+
else
|
48
|
+
EventMachine.connect(@socket, handler, &block)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def sync_socket
|
53
|
+
socket = open_sync_socket
|
54
|
+
if block_given?
|
55
|
+
begin
|
56
|
+
yield(socket)
|
57
|
+
socket.flush
|
58
|
+
socket.close_write
|
59
|
+
ensure
|
60
|
+
socket.close if socket rescue nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
socket
|
64
|
+
end
|
65
|
+
|
66
|
+
def open_sync_socket
|
67
|
+
if tcp?
|
68
|
+
TCPSocket.new(@host, @port)
|
69
|
+
else
|
70
|
+
UNIXSocket.new(@socket)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def set_socket_permissions(socket)
|
75
|
+
if File.exist?(socket)
|
76
|
+
File.chmod(0770, socket)
|
77
|
+
File.chown(nil, options[:gid], socket) if options[:gid]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|