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.
@@ -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