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,83 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'rack/async'
|
4
|
+
|
5
|
+
module Simultaneous
|
6
|
+
module Rack
|
7
|
+
# A Rack handler that allows you to create a HTML5 Server-Sent Events
|
8
|
+
# endpoint. Relies on EventMachine to easily handle multiple
|
9
|
+
# open connections simultaneously
|
10
|
+
#
|
11
|
+
# To use, first create an instance of the EventSource class:
|
12
|
+
#
|
13
|
+
# messenger = Simultaneous::Rack::EventSource.new
|
14
|
+
#
|
15
|
+
# Then map this onto a URL in your application, e.g. in a RackUp file
|
16
|
+
#
|
17
|
+
# app = ::Rack::Builder.new do
|
18
|
+
# map "/messages" do
|
19
|
+
# run messenger.app
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
# run app
|
23
|
+
#
|
24
|
+
# In your web-page, set up an EventSource using the new APIs
|
25
|
+
#
|
26
|
+
# source = new EventSource('/messages');
|
27
|
+
# source.addEventListener('message', function(e) {
|
28
|
+
# alert(e.data);
|
29
|
+
# }, false);
|
30
|
+
#
|
31
|
+
#
|
32
|
+
# Then when you want to send a messages to all your clients you
|
33
|
+
# use your (Ruby) EventSource instance like so:
|
34
|
+
#
|
35
|
+
# messenger.deliver("Hello!")
|
36
|
+
#
|
37
|
+
# IMPORTANT:
|
38
|
+
#
|
39
|
+
# This will only work when run behind Thin or some other, EventMachine
|
40
|
+
# driven webserver. See <https://github.com/matsadler/rack-async> for more
|
41
|
+
# info.
|
42
|
+
#
|
43
|
+
class EventSource
|
44
|
+
|
45
|
+
def initialize
|
46
|
+
@lock = Mutex.new
|
47
|
+
@timer = nil
|
48
|
+
@clients = []
|
49
|
+
end
|
50
|
+
|
51
|
+
def app
|
52
|
+
::Rack::Async.new(self)
|
53
|
+
end
|
54
|
+
|
55
|
+
def call(env)
|
56
|
+
stream = env['async.body']
|
57
|
+
stream.errback { cleanup!(stream) }
|
58
|
+
|
59
|
+
@lock.synchronize { @clients << stream }
|
60
|
+
|
61
|
+
[200, {"Content-type" => "text/event-stream"}, stream]
|
62
|
+
end
|
63
|
+
|
64
|
+
def deliver_event(event)
|
65
|
+
send(event.to_sse)
|
66
|
+
end
|
67
|
+
|
68
|
+
def deliver(data)
|
69
|
+
send("data: #{data}\n\n")
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def send(message)
|
75
|
+
@clients.each { |client| client << message }
|
76
|
+
end
|
77
|
+
|
78
|
+
def cleanup!(connection)
|
79
|
+
@lock.synchronize { @clients.delete(connection) }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'eventmachine'
|
4
|
+
|
5
|
+
module Simultaneous
|
6
|
+
class Server < EM::Connection
|
7
|
+
|
8
|
+
def self.channel
|
9
|
+
@channel ||= EM::Channel.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.start(connection_string = Simultaneous.connection, options = {})
|
13
|
+
Simultaneous.connection = connection_string
|
14
|
+
connection = Simultaneous::Connection.new(connection_string, options)
|
15
|
+
@server = connection.start_server(self)
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def self.broadcast(data)
|
20
|
+
channel << data
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.receive_data(data)
|
24
|
+
command = Simultaneous::Command.load(data)
|
25
|
+
run(command)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.run(command)
|
29
|
+
if Command.allowed?(command)
|
30
|
+
puts command.debug if $debug
|
31
|
+
command.run
|
32
|
+
else
|
33
|
+
raise PermissionsError, "'#{command.class}' is not an approved command"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.set_pid(task_name, pid)
|
38
|
+
pids[task_name] = pid.to_i
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.get_pid(task)
|
42
|
+
pids[task.name]
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.pids
|
46
|
+
@pids ||= {}
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.kill(task_name, signal="TERM")
|
50
|
+
pid = pids[task_name]
|
51
|
+
Process.kill(signal, pid) unless pid == 0
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.task_complete(task_name)
|
55
|
+
pid = pids.delete(task_name)
|
56
|
+
end
|
57
|
+
|
58
|
+
def channel
|
59
|
+
Simultaneous::Server.channel
|
60
|
+
end
|
61
|
+
|
62
|
+
def post_init
|
63
|
+
@sid = channel.subscribe { |m| send_data "#{m}\n" }
|
64
|
+
end
|
65
|
+
|
66
|
+
def receive_data(data)
|
67
|
+
Simultaneous::Server.receive_data(data)
|
68
|
+
end
|
69
|
+
|
70
|
+
def unbind
|
71
|
+
channel.unsubscribe @sid if @sid
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Simultaneous
|
2
|
+
module Task
|
3
|
+
|
4
|
+
def self.task_name
|
5
|
+
ENV[Simultaneous::ENV_TASK_NAME]
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.pid
|
9
|
+
$$
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.included(klass)
|
13
|
+
Simultaneous.client = Simultaneous::TaskClient.new
|
14
|
+
Simultaneous.set_pid(self.task_name, pid) if task_name
|
15
|
+
at_exit {
|
16
|
+
begin
|
17
|
+
Simultaneous.task_complete(self.task_name)
|
18
|
+
rescue Errno::ECONNREFUSED
|
19
|
+
rescue Errno::ENOENT
|
20
|
+
end
|
21
|
+
|
22
|
+
# Simultaneous.client.close
|
23
|
+
}
|
24
|
+
rescue Errno::ECONNREFUSED
|
25
|
+
rescue Errno::ENOENT
|
26
|
+
# server isn't running but we don't want this to stop our script
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def simultaneous_event(event, message)
|
31
|
+
Simultaneous.send_event(event, message)
|
32
|
+
rescue Errno::ECONNREFUSED
|
33
|
+
rescue Errno::ENOENT
|
34
|
+
# server isn't running but we don't want this to stop our script
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
module Simultaneous
|
6
|
+
class TaskClient
|
7
|
+
attr_reader :domain
|
8
|
+
|
9
|
+
def initialize(domain = Simultaneous.domain, connection_string=Simultaneous.connection)
|
10
|
+
@domain = domain
|
11
|
+
@connection = Simultaneous::Connection.new(connection_string)
|
12
|
+
end
|
13
|
+
|
14
|
+
def run(command)
|
15
|
+
command.domain = self.domain
|
16
|
+
send(command.dump)
|
17
|
+
end
|
18
|
+
|
19
|
+
def send(command)
|
20
|
+
connect do |connection|
|
21
|
+
connection.send(command, 0)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def connect
|
26
|
+
@connection.sync_socket do |socket|
|
27
|
+
yield(socket)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def close
|
32
|
+
end
|
33
|
+
|
34
|
+
def on_event(event)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Simultaneous
|
4
|
+
class TaskDescription
|
5
|
+
attr_reader :name, :binary, :options, :params, :env
|
6
|
+
|
7
|
+
# options:
|
8
|
+
# :nice
|
9
|
+
# :logfile - defaults to PWD/log/task_name.log
|
10
|
+
#
|
11
|
+
#
|
12
|
+
# name, path_to_binary, options, default_parameters, env
|
13
|
+
def initialize(name, path_to_binary, options={}, default_parameters={}, env={})
|
14
|
+
@name, @binary, @params, @options, @env = name, path_to_binary, default_parameters, options, env
|
15
|
+
end
|
16
|
+
|
17
|
+
def niceness
|
18
|
+
(options[:nice] || options[:niceness] || 0)
|
19
|
+
end
|
20
|
+
|
21
|
+
def logfile
|
22
|
+
(options[:logfile] || options[:log] || default_log_file)
|
23
|
+
end
|
24
|
+
|
25
|
+
def pwd
|
26
|
+
(options[:pwd] || default_pwd)
|
27
|
+
end
|
28
|
+
|
29
|
+
def default_log_file
|
30
|
+
File.expand_path(File.join(Dir.pwd, "log", "#{name}-task.log"))
|
31
|
+
end
|
32
|
+
|
33
|
+
def default_pwd
|
34
|
+
"/"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/simultaneous.rb
ADDED
@@ -0,0 +1,265 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
require 'eventmachine'
|
5
|
+
|
6
|
+
module Simultaneous
|
7
|
+
VERSION = "0.2.0"
|
8
|
+
|
9
|
+
DEFAULT_CONNECTION = "/tmp/simultaneous-server.sock"
|
10
|
+
DEFAULT_PORT = 9999
|
11
|
+
DEFAULT_HOST = 'localhost'
|
12
|
+
|
13
|
+
ENV_CONNECTION = "SIMULTANEOUS_CONNECTION"
|
14
|
+
ENV_DOMAIN = "SIMULTANEOUS_DOMAIN"
|
15
|
+
ENV_TASK_NAME = "SIMULTANEOUS_TASK_NAME"
|
16
|
+
|
17
|
+
class Error < ::StandardError; end
|
18
|
+
class PermissionsError < Error; end
|
19
|
+
class FileNotFoundError < Error; end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
|
23
|
+
def server_binary
|
24
|
+
File.expand_path("../../bin/simultaneous-server", __FILE__)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Registers a task and makes it available for easy launching using #fire
|
28
|
+
#
|
29
|
+
# @param [Symbol] task_name
|
30
|
+
# the name for the task. This should be unique
|
31
|
+
#
|
32
|
+
# @param [String] path_to_binary
|
33
|
+
# the path to the executable that should be run when this task is launched
|
34
|
+
#
|
35
|
+
# @param [Hash] options
|
36
|
+
# A hash of options for the task. Available options are:
|
37
|
+
# :niceness: the niceness value of the process, >=0
|
38
|
+
# :logfile: the location of the processes log file to which all io will be redirected
|
39
|
+
# :pwd: directory that the task should work in
|
40
|
+
#
|
41
|
+
# @param [Fixnum] niceness
|
42
|
+
# the niceness value of the process >= 0. The higher this value the 'nicer' the launched
|
43
|
+
# process will be (a high nice value results in a low priority task).
|
44
|
+
# On UNIX systems the max, nicest, value is 20
|
45
|
+
#
|
46
|
+
# @param [Hash] default_params
|
47
|
+
# A Hash of parameters that should be passed to every invocation of the task.
|
48
|
+
# These will be converted to command line parameters
|
49
|
+
# { "setting" => "value", "output" => "destination"}
|
50
|
+
# gives the parameters
|
51
|
+
# --setting=value --output=destination
|
52
|
+
# @see Simultaneous::Utilities#to_arguments
|
53
|
+
#
|
54
|
+
# @param [Hash] env
|
55
|
+
# A Hash of values to add to the task's ENV settings
|
56
|
+
#
|
57
|
+
def add_task(task_name, path_to_binary, options={}, default_params={}, env={})
|
58
|
+
tasks[task_name] = TaskDescription.new(task_name, path_to_binary, options, default_params, env)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Launches the given task
|
62
|
+
#
|
63
|
+
# @param [Symbol] task_name the name of the task to launch
|
64
|
+
# @param [Hash] params parameters to pass to the executable
|
65
|
+
def fire(task_name, params={})
|
66
|
+
task = tasks[task_name]
|
67
|
+
command = Command::Fire.new(task, params)
|
68
|
+
client.run(command)
|
69
|
+
end
|
70
|
+
|
71
|
+
def client=(client)
|
72
|
+
@client.close if @client
|
73
|
+
@client = client
|
74
|
+
end
|
75
|
+
|
76
|
+
def client
|
77
|
+
@client ||= \
|
78
|
+
begin
|
79
|
+
client = \
|
80
|
+
if ::EM.reactor_running?
|
81
|
+
Client.new(domain, connection)
|
82
|
+
else
|
83
|
+
TaskClient.new(domain, connection)
|
84
|
+
end
|
85
|
+
# make sure that new client is hooked into all listeners
|
86
|
+
event_listeners.each do |event, blocks|
|
87
|
+
blocks.each do |block|
|
88
|
+
client.on_event(event, &block)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
client
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def event_listeners
|
96
|
+
@event_listeners ||= Hash.new { |hash, key| hash[key] = [] }
|
97
|
+
end
|
98
|
+
|
99
|
+
def on_event(event, &block)
|
100
|
+
event_listeners[event] << block
|
101
|
+
client.on_event(event, &block) if client
|
102
|
+
end
|
103
|
+
|
104
|
+
def reset_client!
|
105
|
+
@client = nil
|
106
|
+
end
|
107
|
+
|
108
|
+
def reset!
|
109
|
+
reset_client!
|
110
|
+
@tasks = nil
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns the path to the binary for the given task
|
114
|
+
#
|
115
|
+
# @param [Symbol] task_name the name of the task
|
116
|
+
# @return [String] the path of the task's binary
|
117
|
+
def binary(task_name)
|
118
|
+
tasks[task_name].binary
|
119
|
+
end
|
120
|
+
|
121
|
+
# Gets the TaskDescription of a task
|
122
|
+
#
|
123
|
+
# @param [Symbol] task_name the name of the task to get
|
124
|
+
def [](task_name)
|
125
|
+
tasks[task_name]
|
126
|
+
end
|
127
|
+
|
128
|
+
def tasks
|
129
|
+
@tasks ||= {}
|
130
|
+
end
|
131
|
+
|
132
|
+
def connection=(connection)
|
133
|
+
reset_client!
|
134
|
+
@connection = connection
|
135
|
+
end
|
136
|
+
|
137
|
+
def domain=(domain)
|
138
|
+
reset_client!
|
139
|
+
@domain = domain
|
140
|
+
end
|
141
|
+
|
142
|
+
def connection
|
143
|
+
@connection ||= (ENV[Simultaneous::ENV_CONNECTION] || Simultaneous::DEFAULT_CONNECTION)
|
144
|
+
end
|
145
|
+
|
146
|
+
def domain
|
147
|
+
@domain ||= (ENV[Simultaneous::ENV_DOMAIN] || "domain#{$$}")
|
148
|
+
end
|
149
|
+
|
150
|
+
# Used by the {Simultaneous::Daemon} module to set the correct PID for a given task
|
151
|
+
def map_pid(task_name, pid)
|
152
|
+
command = Command::SetPid.new(task_name, pid)
|
153
|
+
client.run(command)
|
154
|
+
end
|
155
|
+
|
156
|
+
alias_method :set_pid, :map_pid
|
157
|
+
|
158
|
+
def send_event(event, data)
|
159
|
+
command = Command::ClientEvent.new(domain, event, data)
|
160
|
+
client.run(command)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Sends a running task the TERM signal
|
164
|
+
def term(task_name)
|
165
|
+
kill(task_name, "TERM")
|
166
|
+
end
|
167
|
+
|
168
|
+
# Sends a running task the INT signal
|
169
|
+
def int(task_name)
|
170
|
+
kill(task_name, "INT")
|
171
|
+
end
|
172
|
+
|
173
|
+
# Sends a running task an arbitrary signal
|
174
|
+
#
|
175
|
+
# @param [Symbol] task_name the name of the task to send the signal
|
176
|
+
# @param [String] signal the signal to send
|
177
|
+
#
|
178
|
+
# @see Signal#list for a full list of signals available
|
179
|
+
def kill(task_name, signal="TERM")
|
180
|
+
command = Command::Kill.new(task_name, signal)
|
181
|
+
client.run(command)
|
182
|
+
end
|
183
|
+
|
184
|
+
def task_complete(task_name)
|
185
|
+
command = Command::TaskComplete.new(task_name)
|
186
|
+
client.run(command)
|
187
|
+
end
|
188
|
+
|
189
|
+
def to_arguments(params={})
|
190
|
+
params.keys.sort { |a, b| a.to_s <=> b.to_s }.map do |key|
|
191
|
+
%(--#{key}=#{to_parameter(params[key])})
|
192
|
+
end.join(" ")
|
193
|
+
end
|
194
|
+
|
195
|
+
# Maps objects to command line parameters suitable for parsing by Thor
|
196
|
+
# @see https://github.com/wycats/thor
|
197
|
+
def to_parameter(obj)
|
198
|
+
case obj
|
199
|
+
when String
|
200
|
+
obj.inspect
|
201
|
+
when Array
|
202
|
+
obj.map { |o| to_parameter(o) }.join(' ')
|
203
|
+
when Hash
|
204
|
+
obj.map do |k, v|
|
205
|
+
"#{k}:#{to_parameter(obj[k])}"
|
206
|
+
end.join(' ')
|
207
|
+
when Numeric
|
208
|
+
obj
|
209
|
+
else
|
210
|
+
to_parameter(obj.to_s)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
TCP_CONNECTION_MATCH = %r{^([^/]+):(\d+)}
|
215
|
+
# Convert connection string into an argument array suitable for passing
|
216
|
+
# to EM.connect or EM.server
|
217
|
+
# e.g.
|
218
|
+
# "/path/to/socket.sock" #=> ["/path/to/socket.sock"]
|
219
|
+
# "localhost:9999" #=> ["localhost", 9999]
|
220
|
+
#
|
221
|
+
def parse_connection(connection_string)
|
222
|
+
if connection_string =~ TCP_CONNECTION_MATCH
|
223
|
+
[$1, $2.to_i]
|
224
|
+
else
|
225
|
+
[connection_string]
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def client_connection(connection_string)
|
230
|
+
if connection_string =~ TCP_CONNECTION_MATCH
|
231
|
+
TCPSocket.new($1, $2.to_i)
|
232
|
+
else
|
233
|
+
UNIXSocket.new(connection_string)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
protected
|
238
|
+
|
239
|
+
# Catch method missing to enable launching of tasks by direct name
|
240
|
+
# e.g.
|
241
|
+
# Simultaneous.add_task(:process_things, "/usr/bin/process")
|
242
|
+
# launch this task:
|
243
|
+
# Simultaneous.process_things
|
244
|
+
#
|
245
|
+
def method_missing(method, *args, &block)
|
246
|
+
if tasks.key?(method)
|
247
|
+
fire(method, *args, &block)
|
248
|
+
else
|
249
|
+
super
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
extend ClassMethods
|
255
|
+
|
256
|
+
autoload :Connection, "simultaneous/connection"
|
257
|
+
autoload :Server, "simultaneous/server"
|
258
|
+
autoload :Client, "simultaneous/client"
|
259
|
+
autoload :TaskClient, "simultaneous/task_client"
|
260
|
+
autoload :Task, "simultaneous/task"
|
261
|
+
autoload :TaskDescription, "simultaneous/task_description"
|
262
|
+
autoload :BroadcastMessage, "simultaneous/broadcast_message"
|
263
|
+
autoload :Command, "simultaneous/command"
|
264
|
+
autoload :Rack, "simultaneous/rack"
|
265
|
+
end
|