zeus 0.1.0 → 0.2.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +24 -6
- data/bin/zeus +34 -4
- data/lib/tiny_thor.rb +96 -0
- data/lib/zeus.rb +16 -1
- data/lib/zeus/cli.rb +72 -0
- data/lib/zeus/client.rb +6 -17
- data/lib/zeus/dsl.rb +88 -0
- data/lib/zeus/init.rb +17 -0
- data/lib/zeus/server.rb +82 -244
- data/lib/zeus/server/acceptor.rb +81 -0
- data/lib/zeus/server/acceptor_registration_monitor.rb +44 -0
- data/lib/zeus/server/client_handler.rb +88 -0
- data/lib/zeus/server/file_monitor.rb +57 -0
- data/lib/zeus/server/process_tree_monitor.rb +98 -0
- data/lib/zeus/server/stage.rb +57 -0
- data/{examples → lib/zeus/templates}/rails.rb +11 -11
- data/lib/zeus/ui.rb +56 -0
- data/lib/zeus/version.rb +1 -1
- data/zeus.gemspec +1 -1
- metadata +20 -9
@@ -0,0 +1,44 @@
|
|
1
|
+
module Zeus
|
2
|
+
class Server
|
3
|
+
class AcceptorRegistrationMonitor
|
4
|
+
|
5
|
+
def datasource ; @reg_monitor ; end
|
6
|
+
def on_datasource_event ; handle_registration ; end
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
# note: if these aren't ivars, they go out of scope, get GC'd,
|
10
|
+
# and cause the UNIXSockets to quit working... often in perplexing ways.
|
11
|
+
@s, @r = Socket.pair(:UNIX, :DGRAM)
|
12
|
+
@reg_monitor = UNIXSocket.for_fd(@s.fileno)
|
13
|
+
@reg_acceptor = UNIXSocket.for_fd(@r.fileno)
|
14
|
+
@acceptors = []
|
15
|
+
end
|
16
|
+
|
17
|
+
AcceptorStub = Struct.new(:pid, :socket, :commands, :description)
|
18
|
+
|
19
|
+
def handle_registration
|
20
|
+
io = @reg_monitor.recv_io
|
21
|
+
|
22
|
+
data = JSON.parse(io.readline.chomp)
|
23
|
+
pid = data['pid'].to_i
|
24
|
+
commands = data['commands']
|
25
|
+
description = data['description']
|
26
|
+
|
27
|
+
@acceptors.reject!{|ac|ac.commands == commands}
|
28
|
+
@acceptors << AcceptorStub.new(pid, io, commands, description)
|
29
|
+
end
|
30
|
+
|
31
|
+
def find_acceptor_for_command(command)
|
32
|
+
@acceptors.detect { |acceptor|
|
33
|
+
acceptor.commands.include?(command)
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def acceptor_registration_socket
|
38
|
+
@reg_acceptor
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
require 'zeus/server/acceptor'
|
5
|
+
|
6
|
+
module Zeus
|
7
|
+
class Server
|
8
|
+
# The model here is kind of convoluted, so here's an explanation of what's
|
9
|
+
# happening with all these sockets:
|
10
|
+
#
|
11
|
+
# #### Acceptor Registration
|
12
|
+
# 1. ClientHandler creates a socketpair for Acceptor registration (S_REG)
|
13
|
+
# 2. When an acceptor is spawned, it:
|
14
|
+
# 1. Creates a new socketpair for communication with clienthandler (S_ACC)
|
15
|
+
# 2. Sends one side of S_ACC over S_REG to clienthandler.
|
16
|
+
# 3. Sends a JSON-encoded hash of `pid`, `commands`, and `description`. over S_REG.
|
17
|
+
# 3. ClientHandler received first the IO and then the JSON hash, and stores them for later reference.
|
18
|
+
#
|
19
|
+
# #### Running a command
|
20
|
+
# 1. ClientHandler has a UNIXServer (SVR) listening.
|
21
|
+
# 2. ClientHandler has a socketpair with the acceptor referenced by the command (see Registration) (S_ACC)
|
22
|
+
# 3. When clienthandler received a connection (S_CLI) on SVR:
|
23
|
+
# 1. ClientHandler sends S_CLI over S_ACC, so the acceptor can communicate with the server's client.
|
24
|
+
# 2. ClientHandler sends a JSON-encoded array of `arguments` over S_ACC
|
25
|
+
# 3. Acceptor sends the newly-forked worker PID over S_ACC to clienthandler.
|
26
|
+
# 4. ClientHandler forwards the pid to the client over S_CLI.
|
27
|
+
#
|
28
|
+
class ClientHandler
|
29
|
+
SERVER_SOCK = ".zeus.sock"
|
30
|
+
|
31
|
+
def datasource ; @server ; end
|
32
|
+
def on_datasource_event ; handle_server_connection ; end
|
33
|
+
|
34
|
+
def initialize(registration_monitor)
|
35
|
+
@reg_monitor = registration_monitor
|
36
|
+
begin
|
37
|
+
@server = UNIXServer.new(SERVER_SOCK)
|
38
|
+
@server.listen(10)
|
39
|
+
rescue Errno::EADDRINUSE
|
40
|
+
Zeus.ui.error "Zeus appears to be already running in this project. If not, remove .zeus.sock and try again."
|
41
|
+
exit 1
|
42
|
+
# ensure
|
43
|
+
# @server.close rescue nil
|
44
|
+
# File.unlink(SERVER_SOCK)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def handle_server_connection
|
49
|
+
s_client = @server.accept
|
50
|
+
fork { handshake_client_to_acceptor(s_client) }
|
51
|
+
end
|
52
|
+
|
53
|
+
# client clienthandler acceptor
|
54
|
+
# 1 ----------> | {command: String, arguments: [String]}
|
55
|
+
# 2 ----------> | Terminal IO
|
56
|
+
# 3 -----------> | Terminal IO
|
57
|
+
# 4 -----------> | Arguments (json array)
|
58
|
+
# 5 <----------- | pid
|
59
|
+
# 6 <--------- | pid
|
60
|
+
def handshake_client_to_acceptor(s_client)
|
61
|
+
# 1
|
62
|
+
data = JSON.parse(s_client.readline.chomp)
|
63
|
+
command, arguments = data.values_at('command', 'arguments')
|
64
|
+
|
65
|
+
# 2
|
66
|
+
client_terminal = s_client.recv_io
|
67
|
+
|
68
|
+
# 3
|
69
|
+
acceptor = @reg_monitor.find_acceptor_for_command(command)
|
70
|
+
usock = UNIXSocket.for_fd(acceptor.socket.fileno)
|
71
|
+
# TODO handle nothing found
|
72
|
+
usock.send_io(client_terminal)
|
73
|
+
|
74
|
+
puts "accepting connection for #{command}"
|
75
|
+
|
76
|
+
# 4
|
77
|
+
acceptor.socket.puts arguments.to_json
|
78
|
+
|
79
|
+
# 5
|
80
|
+
pid = acceptor.socket.readline.chomp.to_i
|
81
|
+
|
82
|
+
# 6
|
83
|
+
s_client.puts pid
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'rb-kqueue'
|
2
|
+
|
3
|
+
module Zeus
|
4
|
+
class Server
|
5
|
+
class FileMonitor
|
6
|
+
|
7
|
+
TARGET_FD_LIMIT = 8192
|
8
|
+
|
9
|
+
def initialize(&change_callback)
|
10
|
+
configure_file_descriptor_resource_limit
|
11
|
+
@queue = KQueue::Queue.new
|
12
|
+
@watched_files = {}
|
13
|
+
@deleted_files = []
|
14
|
+
@change_callback = change_callback
|
15
|
+
end
|
16
|
+
|
17
|
+
def process_events
|
18
|
+
@queue.poll
|
19
|
+
end
|
20
|
+
|
21
|
+
def watch(file)
|
22
|
+
return if @watched_files[file]
|
23
|
+
@watched_files[file] = true
|
24
|
+
@queue.watch_file(file, :write, :extend, :rename, :delete, &method(:file_did_change))
|
25
|
+
rescue Errno::ENOENT
|
26
|
+
Zeus.ui.debug("No file found at #{file}")
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def file_did_change(event)
|
32
|
+
Zeus.ui.info("Dependency change at #{event.watcher.path}")
|
33
|
+
resubscribe_deleted_file(event) if event.flags.include?(:delete)
|
34
|
+
@change_callback.call(event.watcher.path)
|
35
|
+
end
|
36
|
+
|
37
|
+
def configure_file_descriptor_resource_limit
|
38
|
+
limit = Process.getrlimit(Process::RLIMIT_NOFILE)
|
39
|
+
if limit[0] < TARGET_FD_LIMIT && limit[1] >= TARGET_FD_LIMIT
|
40
|
+
Process.setrlimit(Process::RLIMIT_NOFILE, TARGET_FD_LIMIT)
|
41
|
+
else
|
42
|
+
puts "\x1b[33m[zeus] Warning: increase the max number of file descriptors. If you have a large project, this max cause a crash in about 10 seconds.\x1b[0m"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def resubscribe_deleted_file(event)
|
47
|
+
event.watcher.disable!
|
48
|
+
begin
|
49
|
+
@queue.watch_file(event.watcher.path, :write, :extend, :rename, :delete, &method(:file_did_change))
|
50
|
+
rescue Errno::ENOENT
|
51
|
+
@deleted_files << event.watcher.path
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Zeus
|
2
|
+
class Server
|
3
|
+
class ProcessTreeMonitor
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@tree = ProcessTree.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def kill_nodes_with_feature(file)
|
10
|
+
@tree.kill_nodes_with_feature(file)
|
11
|
+
end
|
12
|
+
|
13
|
+
def process_has_feature(pid, file)
|
14
|
+
@tree.process_has_feature(pid, file)
|
15
|
+
end
|
16
|
+
|
17
|
+
def process_has_parent(pid, ppid)
|
18
|
+
@tree.process_has_parent(pid, ppid)
|
19
|
+
end
|
20
|
+
|
21
|
+
class ProcessTree
|
22
|
+
class Node
|
23
|
+
attr_accessor :pid, :children, :features
|
24
|
+
def initialize(pid)
|
25
|
+
@pid, @children, @features = pid, [], {}
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_child(node)
|
29
|
+
self.children << node
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_feature(feature)
|
33
|
+
self.features[feature] = true
|
34
|
+
end
|
35
|
+
|
36
|
+
def has_feature?(feature)
|
37
|
+
self.features[feature] == true
|
38
|
+
end
|
39
|
+
|
40
|
+
def inspect
|
41
|
+
"(#{pid}:#{features.size}:[#{children.map(&:inspect).join(",")}])"
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
def inspect
|
47
|
+
@root.inspect
|
48
|
+
end
|
49
|
+
|
50
|
+
def initialize
|
51
|
+
@root = Node.new(Process.pid)
|
52
|
+
@nodes_by_pid = {Process.pid => @root}
|
53
|
+
end
|
54
|
+
|
55
|
+
def node_for_pid(pid)
|
56
|
+
@nodes_by_pid[pid.to_i] ||= Node.new(pid.to_i)
|
57
|
+
end
|
58
|
+
|
59
|
+
def process_has_parent(pid, ppid)
|
60
|
+
curr = node_for_pid(pid)
|
61
|
+
base = node_for_pid(ppid)
|
62
|
+
base.add_child(curr)
|
63
|
+
end
|
64
|
+
|
65
|
+
def process_has_feature(pid, feature)
|
66
|
+
node = node_for_pid(pid)
|
67
|
+
node.add_feature(feature)
|
68
|
+
end
|
69
|
+
|
70
|
+
def kill_node(node)
|
71
|
+
@nodes_by_pid.delete(node.pid)
|
72
|
+
# recall that this process explicitly traps INT -> exit 0
|
73
|
+
Process.kill("INT", node.pid)
|
74
|
+
end
|
75
|
+
|
76
|
+
def kill_nodes_with_feature(file, base = @root)
|
77
|
+
if base.has_feature?(file)
|
78
|
+
if base == @root.children[0] || base == @root
|
79
|
+
puts "\x1b[31mOne of zeus's dependencies changed. Not killing zeus. You may have to restart the server.\x1b[0m"
|
80
|
+
return false
|
81
|
+
end
|
82
|
+
kill_node(base)
|
83
|
+
return true
|
84
|
+
else
|
85
|
+
base.children.dup.each do |node|
|
86
|
+
if kill_nodes_with_feature(file, node)
|
87
|
+
base.children.delete(node)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
return false
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Zeus
|
2
|
+
class Server
|
3
|
+
class Stage
|
4
|
+
|
5
|
+
attr_accessor :name, :stages, :actions
|
6
|
+
attr_reader :pid
|
7
|
+
def initialize(server)
|
8
|
+
@server = server
|
9
|
+
end
|
10
|
+
|
11
|
+
# There are a few things we want to accomplish:
|
12
|
+
# 1. Running all the actions (each time this stage is killed and restarted)
|
13
|
+
# 2. Starting all the substages (and restarting them when necessary)
|
14
|
+
# 3. Starting all the acceptors (and restarting them when necessary)
|
15
|
+
def run
|
16
|
+
@pid = fork {
|
17
|
+
$0 = "zeus spawner: #{@name}"
|
18
|
+
pid = Process.pid
|
19
|
+
@server.w_pid "#{pid}:#{Process.ppid}"
|
20
|
+
puts "\x1b[35m[zeus] starting spawner `#{@name}`\x1b[0m"
|
21
|
+
trap("INT") {
|
22
|
+
puts "\x1b[35m[zeus] killing spawner `#{@name}`\x1b[0m"
|
23
|
+
exit 0
|
24
|
+
}
|
25
|
+
|
26
|
+
@actions.each(&:call)
|
27
|
+
|
28
|
+
pids = {}
|
29
|
+
@stages.each do |stage|
|
30
|
+
pids[stage.run] = stage
|
31
|
+
end
|
32
|
+
|
33
|
+
$LOADED_FEATURES.each do |f|
|
34
|
+
@server.w_feature "#{pid}:#{f}"
|
35
|
+
end
|
36
|
+
|
37
|
+
loop do
|
38
|
+
begin
|
39
|
+
pid = Process.wait
|
40
|
+
rescue Errno::ECHILD
|
41
|
+
raise "Stage `#{@name}` has no children. All terminal nodes must be acceptors"
|
42
|
+
end
|
43
|
+
if (status = $?.exitstatus) > 0
|
44
|
+
exit status
|
45
|
+
else # restart the stage that died.
|
46
|
+
stage = pids[pid]
|
47
|
+
pids[stage.run] = stage
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -24,18 +24,20 @@ Zeus::Server.define! do
|
|
24
24
|
Rails.application.require_environment!
|
25
25
|
end
|
26
26
|
|
27
|
-
|
27
|
+
command :generate, :g do
|
28
28
|
require 'rails/commands/generate'
|
29
29
|
end
|
30
|
-
|
30
|
+
|
31
|
+
command :runner, :r do
|
31
32
|
require 'rails/commands/runner'
|
32
33
|
end
|
33
|
-
|
34
|
+
|
35
|
+
command :console, :c do
|
34
36
|
require 'rails/commands/console'
|
35
37
|
Rails::Console.start(Rails.application)
|
36
38
|
end
|
37
39
|
|
38
|
-
|
40
|
+
command :server, :s do
|
39
41
|
require 'rails/commands/server'
|
40
42
|
server = Rails::Server.new
|
41
43
|
Dir.chdir(Rails.application.root)
|
@@ -48,7 +50,7 @@ Zeus::Server.define! do
|
|
48
50
|
load 'Rakefile'
|
49
51
|
end
|
50
52
|
|
51
|
-
|
53
|
+
command :rake do
|
52
54
|
Rake.application.run
|
53
55
|
end
|
54
56
|
|
@@ -63,12 +65,10 @@ Zeus::Server.define! do
|
|
63
65
|
Rails.application.require_environment!
|
64
66
|
end
|
65
67
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
exit r.run
|
71
|
-
}
|
68
|
+
command :testrb do
|
69
|
+
(r = Test::Unit::AutoRunner.new(true)).process_args(ARGV) or
|
70
|
+
abort r.options.banner + " tests..."
|
71
|
+
exit r.run
|
72
72
|
end
|
73
73
|
|
74
74
|
end
|
data/lib/zeus/ui.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
module Zeus
|
2
|
+
class UI
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@quiet = false
|
6
|
+
@debug = ENV['DEBUG']
|
7
|
+
end
|
8
|
+
|
9
|
+
def info(msg)
|
10
|
+
tell_me(msg, nil) if !@quiet
|
11
|
+
end
|
12
|
+
|
13
|
+
def confirm(msg)
|
14
|
+
tell_me(msg, :green) if !@quiet
|
15
|
+
end
|
16
|
+
|
17
|
+
def warn(msg)
|
18
|
+
tell_me(msg, :yellow)
|
19
|
+
end
|
20
|
+
|
21
|
+
def error(msg)
|
22
|
+
tell_me(msg, :red)
|
23
|
+
end
|
24
|
+
|
25
|
+
def be_quiet!
|
26
|
+
@quiet = true
|
27
|
+
end
|
28
|
+
|
29
|
+
def debug?
|
30
|
+
# needs to be false instead of nil to be newline param to other methods
|
31
|
+
!!@debug && !@quiet
|
32
|
+
end
|
33
|
+
|
34
|
+
def debug!
|
35
|
+
@debug = true
|
36
|
+
end
|
37
|
+
|
38
|
+
def debug(msg)
|
39
|
+
tell_me(msg, nil) if debug?
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def tell_me(msg, color = nil)
|
44
|
+
msg = case color
|
45
|
+
when :red ; "\x1b[31m#{msg}\x1b[0m"
|
46
|
+
when :green ; "\x1b[32m#{msg}\x1b[0m"
|
47
|
+
when :yellow ; "\x1b[33m#{msg}\x1b[0m"
|
48
|
+
else ; msg
|
49
|
+
end
|
50
|
+
puts msg
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|