simultaneous 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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