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.
@@ -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
@@ -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