simultaneous 0.2.0
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/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
|