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,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