zeus 0.1.0 → 0.2.0.beta1
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/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
|