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.
@@ -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
- acceptor :generate, ".zeus.dev_generate.sock" do
27
+ command :generate, :g do
28
28
  require 'rails/commands/generate'
29
29
  end
30
- acceptor :runner, ".zeus.dev_runner.sock" do
30
+
31
+ command :runner, :r do
31
32
  require 'rails/commands/runner'
32
33
  end
33
- acceptor :console, ".zeus.dev_console.sock" do
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
- acceptor :server, ".zeus.dev_server.sock" do
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
- acceptor :rake, ".zeus.dev_rake.sock" do
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
- acceptor :testrb, ".zeus.test_testrb.sock" do
67
- forkpoint testrb: acceptor(".zeus.test_testrb.sock") {
68
- (r = Test::Unit::AutoRunner.new(true)).process_args(ARGV) or
69
- abort r.options.banner + " tests..."
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