zeus 0.2.6 → 0.3.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.
data/.zeus.rb ADDED
@@ -0,0 +1,11 @@
1
+ Zeus::Server.define! do
2
+
3
+ stage :zeus do
4
+ action { require 'zeus' ; require 'rspec' ; require 'rspec/core/runner' }
5
+
6
+ command :spec, :s do
7
+ RSpec::Core::Runner.autorun
8
+ end
9
+ end
10
+
11
+ end
data/lib/thrud.rb CHANGED
@@ -45,7 +45,7 @@ class Thrud
45
45
  def help(taskname = nil)
46
46
  if taskname && task = task_for_name(taskname)
47
47
  arity = task.arity(self)
48
- puts <<-BANNER
48
+ Zeus.ui.info <<-BANNER
49
49
  Usage:
50
50
  zeus #{taskname} #{arity == -1 ? "[ARGS]" : ""}
51
51
 
@@ -61,7 +61,7 @@ BANNER
61
61
  " zeus %-14s # %s" % [task.method_name, task.desc]
62
62
  }
63
63
 
64
- puts <<-BANNER
64
+ Zeus.ui.info <<-BANNER
65
65
  Global Commands:
66
66
  zeus help # show this help menu
67
67
  zeus help [COMMAND] # show help for a specific command
data/lib/zeus.rb CHANGED
@@ -22,4 +22,14 @@ module Zeus
22
22
  @ui = ui
23
23
  end
24
24
 
25
+ def self.after_fork(&b)
26
+ @after_fork ||= []
27
+ @after_fork << b
28
+ end
29
+
30
+ def self.run_after_fork!
31
+ @after_fork.map(&:call) if @after_fork
32
+ @after_fork = []
33
+ end
34
+
25
35
  end
data/lib/zeus/cli.rb CHANGED
@@ -65,16 +65,16 @@ module Zeus
65
65
 
66
66
  begin
67
67
  require definition_file
68
- Zeus::Server.acceptors.each do |acc|
69
- desc acc.name, (acc.description || "#{acc.name} task defined in .zeus.rb")
70
- define_method(acc.name) { |*args|
71
- Zeus::Client.run(acc.name, args)
72
- }
73
- map acc.aliases => acc.name
74
- end
75
68
  rescue LoadError
76
69
  end
77
70
 
71
+ Zeus::Server.acceptors.each do |acc|
72
+ desc acc.name, (acc.description || "#{acc.name} task defined in zeus definition file")
73
+ define_method(acc.name) { |*args|
74
+ Zeus::Client.run(acc.name, args)
75
+ }
76
+ map acc.aliases => acc.name
77
+ end
78
78
 
79
79
  end
80
80
  end
data/lib/zeus/client.rb CHANGED
@@ -63,7 +63,11 @@ module Zeus
63
63
  def handle_winch
64
64
  @winch.read(1)
65
65
  set_winsize
66
- Process.kill("WINCH", pid) if pid
66
+ begin
67
+ Process.kill("WINCH", pid) if pid
68
+ rescue Errno::ESRCH
69
+ exit # the remote process died. Just quit.
70
+ end
67
71
  end
68
72
 
69
73
  def handle_stdin(buffer)
@@ -72,8 +76,7 @@ module Zeus
72
76
  begin
73
77
  Process.kill(SIGNALS[signal], pid)
74
78
  rescue Errno::ESRCH
75
- # we're trying to kill a process that died. Just quit.
76
- exit
79
+ exit # the remote process died. Just quit.
77
80
  end
78
81
  }
79
82
  @master << input
data/lib/zeus/server.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  require 'json'
2
2
  require 'socket'
3
+ require 'forwardable'
3
4
 
4
5
  module Zeus
5
6
  class Server
7
+ extend Forwardable
6
8
 
7
9
  autoload :Stage, 'zeus/server/stage'
8
10
  autoload :Acceptor, 'zeus/server/acceptor'
@@ -19,7 +21,7 @@ module Zeus
19
21
  end
20
22
 
21
23
  def self.acceptors
22
- @@definition.acceptors
24
+ defined?(@@definition) ? @@definition.acceptors : []
23
25
  end
24
26
 
25
27
  def initialize
@@ -58,29 +60,19 @@ module Zeus
58
60
  File.unlink(Zeus::SOCKET_NAME)
59
61
  end
60
62
 
61
- module ChildProcessApi
62
63
 
63
- def __CHILD__close_parent_sockets
64
- monitors.each(&:close_parent_socket)
65
- end
66
-
67
- def __CHILD__pid_has_ppid(pid, ppid)
68
- @process_tree_monitor.__CHILD__send_pid("#{pid}:#{Process.ppid}")
69
- end
70
-
71
- def __CHILD__pid_has_feature(pid, feature)
72
- @process_tree_monitor.__CHILD__send_feature("#{pid}:#{feature}")
73
- end
74
-
75
- def __CHILD__register_acceptor(io)
76
- @acceptor_registration_monitor.__CHILD__register_acceptor(io)
77
- end
64
+ # Child process API
65
+ def __CHILD__close_parent_sockets
66
+ monitors.each(&:close_parent_socket)
67
+ end
78
68
 
79
- def __CHILD__find_acceptor_for_command(command)
80
- @acceptor_registration_monitor.__CHILD__find_acceptor_for_command(command)
81
- end
69
+ def_delegators :@acceptor_registration_monitor,
70
+ :__CHILD__register_acceptor,
71
+ :__CHILD__find_acceptor_for_command
82
72
 
83
- end ; include ChildProcessApi
73
+ def_delegators :@process_tree_monitor,
74
+ :__CHILD__pid_has_ppid,
75
+ :__CHILD__pid_has_feature
84
76
 
85
77
  end
86
78
  end
@@ -36,6 +36,13 @@ module Zeus
36
36
  ARGV.replace(arguments)
37
37
 
38
38
  @action.call
39
+ ensure
40
+ dnw, dnr = File.open("/dev/null", "w+"), File.open("/dev/null", "r+")
41
+ $stdin.reopen(dnw)
42
+ $stdout.reopen(dnr)
43
+ $stderr.reopen(dnr)
44
+ terminal.close
45
+ exit 0
39
46
  end
40
47
 
41
48
  private
@@ -70,8 +77,8 @@ module Zeus
70
77
 
71
78
  def postfork_action! # TODO :refactor
72
79
  ActiveRecord::Base.establish_connection rescue nil
73
- ActiveSupport::DescendantsTracker.clear rescue nil
74
- ActiveSupport::Dependencies.clear rescue nil
80
+ # ActiveSupport::DescendantsTracker.clear rescue nil
81
+ # ActiveSupport::Dependencies.clear rescue nil
75
82
  end
76
83
 
77
84
  end
@@ -4,7 +4,7 @@ require 'socket'
4
4
  # See Zeus::Server::ClientHandler for relevant documentation
5
5
  module Zeus
6
6
  class Server
7
- module ErrorStateAcceptor
7
+ module AcceptorErrorState
8
8
  attr_accessor :error
9
9
 
10
10
  def print_error(io, error = @error)
@@ -16,7 +16,7 @@ module Zeus
16
16
 
17
17
  def run
18
18
  register_with_client_handler(Process.pid)
19
- Zeus.ui.as_zeus "starting error-state acceptor `#{@name}`"
19
+ Zeus.ui.info "starting error-state acceptor `#{@name}`"
20
20
 
21
21
  Thread.new do
22
22
  loop do
@@ -29,112 +29,101 @@ module Zeus
29
29
  def close_child_socket ; end
30
30
  def close_parent_socket ; @listener.close ; end
31
31
 
32
+ REATTEMPT_HANDSHAKE = 204
33
+
32
34
  def initialize(acceptor_commands, server)
33
35
  @server = server
34
36
  @acceptor_commands = acceptor_commands
35
37
  @listener = UNIXServer.new(Zeus::SOCKET_NAME)
36
38
  @listener.listen(10)
37
39
  rescue Errno::EADDRINUSE
38
- Zeus.ui.error "Zeus appears to be already running in this project. If not, remove .zeus.sock and try again."
40
+ Zeus.ui.error "Zeus appears to be already running in this project. If not, remove #{Zeus::SOCKET_NAME} and try again."
39
41
  exit 1
40
42
  end
41
43
 
44
+ private
45
+
46
+ # client clienthandler acceptor
47
+ # 1 ----------> | {command: String, arguments: [String]}
48
+ # 2 ----------> | Terminal IO
49
+ # 3 -----------> | Terminal IO
50
+ # 4 -----------> | Arguments (json array)
51
+ # 5 <----------- | pid
52
+ # 6 <--------- | pid
42
53
  def handle_server_connection
43
54
  s_client = @listener.accept
44
55
 
45
- # 1
46
- data = JSON.parse(s_client.readline.chomp)
56
+ data = JSON.parse(s_client.readline.chomp) # step 1
47
57
  command, arguments = data.values_at('command', 'arguments')
48
58
 
49
- # 2
50
- client_terminal = s_client.recv_io
59
+ client_terminal = s_client.recv_io # step 2
51
60
 
52
61
  Thread.new {
53
- loop do
54
- pid = fork { handshake_client_to_acceptor(s_client, command, arguments, client_terminal) ; exit }
55
- Process.wait(pid)
56
- break unless $?.exitstatus == REATTEMPT_HANDSHAKE
62
+ # This is a little ugly. Gist: Try to handshake the client to the acceptor.
63
+ # If the acceptor is not booted yet, this will hang until it is, then terminate with
64
+ # REATTEMPT_HANDSHAKE. We catch that exit code and try once more.
65
+ begin
66
+ loop do
67
+ pid = fork { handshake_client_to_acceptor(s_client, command, arguments, client_terminal) ; exit }
68
+ Process.wait(pid)
69
+ break if $?.exitstatus != REATTEMPT_HANDSHAKE
70
+ end
71
+ ensure
72
+ client_terminal.close
73
+ s_client.close
57
74
  end
58
75
  }
59
76
  end
60
77
 
61
- REATTEMPT_HANDSHAKE = 204
78
+ def handshake_client_to_acceptor(s_client, command, arguments, client_terminal)
79
+ unless @acceptor_commands.include?(command.to_s)
80
+ msg = "no such command `#{command}`."
81
+ return exit_with_message(s_client, client_terminal, msg)
82
+ end
83
+
84
+ unless acceptor = send_io_to_acceptor(client_terminal, command) # step 3
85
+ wait_for_acceptor(s_client, client_terminal, command)
86
+ exit REATTEMPT_HANDSHAKE
87
+ end
62
88
 
63
- NoSuchCommand = Class.new(Exception)
64
- AcceptorNotBooted = Class.new(Exception)
65
- ApplicationLoadFailed = Class.new(Exception)
89
+ Zeus.ui.info "accepting connection for #{command}"
90
+
91
+ acceptor.socket.puts arguments.to_json # step 4
92
+ pid = acceptor.socket.readline.chomp.to_i # step 5
93
+ s_client.puts pid # step 6
94
+ s_client.close
95
+ end
66
96
 
67
97
  def exit_with_message(s_client, client_terminal, msg)
68
98
  s_client << "0\n"
69
99
  client_terminal << "[zeus] #{msg}\n"
70
100
  client_terminal.close
71
101
  s_client.close
102
+ exit 1
72
103
  end
73
104
 
74
- def wait_for_acceptor(s_client, client_terminal, command, msg)
105
+ def wait_for_acceptor(s_client, client_terminal, command)
75
106
  s_client << "0\n"
76
- client_terminal << "[zeus] #{msg}\n"
77
-
78
- regmsg = {type: 'wait', command: command}
107
+ client_terminal << "[zeus] waiting for `#{command}` to finish booting...\n"
79
108
 
80
109
  s, r = UNIXSocket.pair
81
-
110
+ s << {type: 'wait', command: command}.to_json << "\n"
82
111
  @server.__CHILD__register_acceptor(r)
83
- s << "#{regmsg.to_json}\n"
84
-
85
- s.readline # wait
86
- s.close
87
112
 
88
- exit REATTEMPT_HANDSHAKE
113
+ s.readline # wait until acceptor is booted
89
114
  end
90
115
 
91
- # client clienthandler acceptor
92
- # 1 ----------> | {command: String, arguments: [String]}
93
- # 2 ----------> | Terminal IO
94
- # 3 -----------> | Terminal IO
95
- # 4 -----------> | Arguments (json array)
96
- # 5 <----------- | pid
97
- # 6 <--------- | pid
98
- def handshake_client_to_acceptor(s_client, command, arguments, client_terminal)
99
- # 3
100
- unless @acceptor_commands.include?(command.to_s)
101
- return exit_with_message(
102
- s_client, client_terminal,
103
- "no such command `#{command}`.")
104
- end
105
- acceptor = @server.__CHILD__find_acceptor_for_command(command)
106
- unless acceptor
107
- wait_for_acceptor(
108
- s_client, client_terminal, command,
109
- "waiting for `#{command}` to finish booting...")
110
- end
111
- usock = UNIXSocket.for_fd(acceptor.socket.fileno)
112
- if usock.closed?
113
- wait_for_acceptor(
114
- s_client, client_terminal, command,
115
- "waiting for `#{command}` to finish reloading dependencies...")
116
- end
117
- begin
118
- usock.send_io(client_terminal)
119
- rescue Errno::EPIPE
120
- wait_for_acceptor(
121
- s_client, client_terminal, command,
122
- "waiting for `#{command}` to finish reloading dependencies...")
123
- end
124
-
125
-
126
- Zeus.ui.info "accepting connection for #{command}"
127
-
128
- # 4
129
- acceptor.socket.puts arguments.to_json
130
-
131
- # 5
132
- pid = acceptor.socket.readline.chomp.to_i
133
-
134
- # 6
135
- s_client.puts pid
116
+ def send_io_to_acceptor(io, command)
117
+ return false unless acceptor = @server.__CHILD__find_acceptor_for_command(command)
118
+ return false unless usock = UNIXSocket.for_fd(acceptor.socket.fileno)
119
+ usock.send_io(io)
120
+ io.close
121
+ return acceptor
122
+ rescue Errno::EPIPE
123
+ return false
136
124
  end
137
125
 
126
+
138
127
  end
139
128
  end
140
129
  end
@@ -1,4 +1,5 @@
1
1
  require 'open3'
2
+ require 'pathname'
2
3
 
3
4
  module Zeus
4
5
  class Server
@@ -13,41 +14,69 @@ module Zeus
13
14
 
14
15
  def initialize(&change_callback)
15
16
  @change_callback = change_callback
16
- @io_in, @io_out, _ = Open3.popen2e(WRAPPER_PATH)
17
- @watched_files = {}
17
+ @io_in, @io_out, _ = open_wrapper
18
+ @givenpath_to_realpath = {}
19
+ @realpath_to_givenpath = {}
18
20
  @buffer = ""
19
21
  end
20
22
 
23
+ # The biggest complicating factor here is that ruby doesn't fully resolve
24
+ # symlinks in paths, but FSEvents does. We resolve all paths fully with
25
+ # Pathname#realpath, and keep mappings in both directions.
26
+ # It's conceivable that the same file would be required by two different paths,
27
+ # so we keep an array and fire callbacks for all given paths matching a real
28
+ # path when a change is detected.
29
+ def watch(given)
30
+ return false if @givenpath_to_realpath[given]
31
+
32
+ real = realpath(given)
33
+ @givenpath_to_realpath[given] = real
34
+ @realpath_to_givenpath[real] ||= []
35
+ @realpath_to_givenpath[real] << given
36
+
37
+ @io_in.puts real
38
+ true
39
+ end
40
+
41
+ private
42
+
43
+ def realpath(file)
44
+ Pathname.new(file).realpath.to_s
45
+ rescue Errno::ENOENT
46
+ file
47
+ end
48
+
49
+ def open_wrapper
50
+ Open3.popen2e(WRAPPER_PATH)
51
+ end
52
+
21
53
  def handle_changed_files
22
54
  50.times { read_and_notify_files }
23
55
  rescue Errno::EAGAIN
24
56
  end
25
57
 
26
58
  def read_and_notify_files
27
- lines = @io_out.read_nonblock(30000)
59
+ lines = @io_out.read_nonblock(1000)
28
60
  files = lines.split("\n")
29
61
  files[0] = "#{@buffer}#{files[0]}" unless @buffer == ""
30
62
  unless lines[-1] == "\n"
31
63
  @buffer = files.pop
32
64
  end
33
65
 
34
- files.each do |file|
35
- file_did_change(file)
66
+ files.each do |real|
67
+ file_did_change(real)
36
68
  end
37
69
  end
38
70
 
39
- def watch(file)
40
- return false if @watched_files[file]
41
- @watched_files[file] = true
42
- @io_in.puts file
43
- true
71
+ def file_did_change(real)
72
+ realpaths_for_givenpath(real).each do |given|
73
+ Zeus.ui.info("Dependency change at #{given}")
74
+ @change_callback.call(given)
75
+ end
44
76
  end
45
77
 
46
- private
47
-
48
- def file_did_change(file)
49
- Zeus.ui.info("Dependency change at #{file}")
50
- @change_callback.call(file)
78
+ def realpaths_for_givenpath(real)
79
+ @realpath_to_givenpath[real] || []
51
80
  end
52
81
 
53
82
  end
@@ -27,17 +27,23 @@ module Zeus
27
27
 
28
28
  $0 = "zeus #{process_type}: #{@name}"
29
29
 
30
- Zeus.ui.as_zeus("starting #{process_type} `#{@name}`")
30
+ Zeus.ui.info("starting #{process_type} `#{@name}`")
31
31
  trap("INT") {
32
- Zeus.ui.as_zeus("killing #{process_type} `#{@name}`")
32
+ Zeus.ui.info("killing #{process_type} `#{@name}`")
33
33
  exit 0
34
34
  }
35
35
 
36
+ new_features = $LOADED_FEATURES - previously_loaded_features
37
+ $previously_loaded_features = $LOADED_FEATURES.dup
36
38
  Thread.new {
37
- $LOADED_FEATURES.each { |f| notify_feature(f) }
39
+ new_features.each { |f| notify_feature(f) }
38
40
  }
39
41
  end
40
42
 
43
+ def previously_loaded_features
44
+ defined?($previously_loaded_features) ? $previously_loaded_features : []
45
+ end
46
+
41
47
  def kill_pid_on_exit(pid)
42
48
  currpid = Process.pid
43
49
  at_exit { Process.kill(9, pid) if Process.pid == currpid rescue nil }
@@ -57,6 +63,9 @@ module Zeus
57
63
  @pid = fork {
58
64
  before_setup
59
65
  setup_forked_process(close_parent_sockets)
66
+
67
+ Zeus.run_after_fork!
68
+
60
69
  after_setup
61
70
  runloop!
62
71
  }
@@ -1,43 +1,12 @@
1
1
  module Zeus
2
2
  class Server
3
3
  class ProcessTree
4
- class Node
5
- attr_accessor :pid, :children, :features
6
- def initialize(pid)
7
- @pid, @children, @features = pid, [], {}
8
- end
9
-
10
- def add_child(node)
11
- self.children << node
12
- end
13
-
14
- def add_feature(feature)
15
- self.features[feature] = true
16
- end
17
-
18
- def has_feature?(feature)
19
- self.features[feature] == true
20
- end
21
-
22
- def inspect
23
- "(#{pid}:#{features.size}:[#{children.map(&:inspect).join(",")}])"
24
- end
25
-
26
- end
27
-
28
- def inspect
29
- @root.inspect
30
- end
31
4
 
32
5
  def initialize
33
6
  @root = Node.new(Process.pid)
34
7
  @nodes_by_pid = {Process.pid => @root}
35
8
  end
36
9
 
37
- def node_for_pid(pid)
38
- @nodes_by_pid[pid.to_i] ||= Node.new(pid.to_i)
39
- end
40
-
41
10
  def process_has_parent(pid, ppid)
42
11
  curr = node_for_pid(pid)
43
12
  base = node_for_pid(ppid)
@@ -49,20 +18,9 @@ module Zeus
49
18
  node.add_feature(feature)
50
19
  end
51
20
 
52
- def kill_node(node)
53
- @nodes_by_pid.delete(node.pid)
54
- # recall that this process explicitly traps INT -> exit 0
55
- Process.kill("INT", node.pid)
56
- end
57
-
58
21
  def kill_nodes_with_feature(file, base = @root)
59
22
  if base.has_feature?(file)
60
- if base == @root.children[0] || base == @root
61
- Zeus.ui.error "One of zeus's dependencies changed. Not killing zeus. You may have to restart the server."
62
- return false
63
- end
64
23
  kill_node(base)
65
- return true
66
24
  else
67
25
  base.children.dup.each do |node|
68
26
  if kill_nodes_with_feature(file, node)
@@ -73,6 +31,46 @@ module Zeus
73
31
  end
74
32
  end
75
33
 
34
+ private
35
+
36
+ def node_for_pid(pid)
37
+ @nodes_by_pid[pid.to_i] ||= Node.new(pid.to_i)
38
+ end
39
+
40
+ def kill_node(node)
41
+ if node == @root.children[0] || node == @root
42
+ Zeus.ui.error "One of zeus's dependencies changed. Not killing zeus. You may have to restart the server."
43
+ return false
44
+ end
45
+ @nodes_by_pid.delete(node.pid)
46
+ node.kill
47
+ end
48
+
49
+ class Node
50
+ attr_accessor :pid, :children, :features
51
+ def initialize(pid)
52
+ @pid, @children, @features = pid, [], {}
53
+ end
54
+
55
+ def kill
56
+ # recall that this process explicitly traps INT -> exit 0
57
+ Process.kill("INT", pid)
58
+ end
59
+
60
+ def add_child(node)
61
+ self.children << node
62
+ end
63
+
64
+ def add_feature(feature)
65
+ self.features[feature] = true
66
+ end
67
+
68
+ def has_feature?(feature)
69
+ self.features[feature] == true
70
+ end
71
+
72
+ end
73
+
76
74
  end
77
75
  end
78
76
  end
@@ -9,34 +9,55 @@ module Zeus
9
9
  def close_child_socket ; @__CHILD__sock.close ; end
10
10
  def close_parent_socket ; @sock.close ; end
11
11
 
12
- def initialize(file_monitor)
13
- @tree = ProcessTree.new
12
+ def initialize(file_monitor, tree=ProcessTree.new)
13
+ @tree = tree
14
14
  @file_monitor = file_monitor
15
15
 
16
- @sock, @__CHILD__sock = Socket.pair(:UNIX, :DGRAM)
16
+ @sock, @__CHILD__sock = open_socketpair
17
17
  end
18
18
 
19
19
  def kill_nodes_with_feature(file)
20
20
  @tree.kill_nodes_with_feature(file)
21
21
  end
22
22
 
23
+ module ChildProcessApi
24
+ def __CHILD__pid_has_ppid(pid, ppid)
25
+ @__CHILD__sock.send("#{PID_TYPE}:#{pid}:#{ppid}", 0)
26
+ rescue Errno::ENOBUFS
27
+ sleep 0.2
28
+ retry
29
+ end
30
+
31
+ def __CHILD__pid_has_feature(pid, feature)
32
+ @__CHILD__sock.send("#{FEATURE_TYPE}:#{pid}:#{feature}", 0)
33
+ rescue Errno::ENOBUFS
34
+ sleep 0.2
35
+ retry
36
+ end
37
+ end ; include ChildProcessApi
38
+
39
+
40
+ private
41
+
23
42
  def handle_messages
24
43
  50.times { handle_message }
25
44
  rescue Errno::EAGAIN
26
45
  end
27
46
 
28
47
  def handle_message
29
- data = @sock.recv_nonblock(1024)
48
+ data = @sock.recv_nonblock(4096)
30
49
  case data[0]
31
50
  when FEATURE_TYPE
32
51
  handle_feature_message(data[1..-1])
33
52
  when PID_TYPE
34
53
  handle_pid_message(data[1..-1])
35
- else
36
- raise "Unrecognized message"
37
54
  end
38
55
  end
39
56
 
57
+ def open_socketpair
58
+ Socket.pair(:UNIX, :DGRAM)
59
+ end
60
+
40
61
  def handle_pid_message(data)
41
62
  data =~ /(\d+):(\d+)/
42
63
  pid, ppid = $1.to_i, $2.to_i
@@ -51,22 +72,6 @@ module Zeus
51
72
  end
52
73
 
53
74
 
54
- module ChildProcessApi
55
- def __CHILD__send_pid(message)
56
- @__CHILD__sock.send(PID_TYPE + message, 0)
57
- rescue Errno::ENOBUFS
58
- sleep 0.2
59
- retry
60
- end
61
-
62
- def __CHILD__send_feature(message)
63
- @__CHILD__sock.send(FEATURE_TYPE + message, 0)
64
- rescue Errno::ENOBUFS
65
- sleep 0.2
66
- retry
67
- end
68
- end ; include ChildProcessApi
69
-
70
75
  end
71
76
  end
72
77
  end
@@ -16,7 +16,7 @@ Zeus::Server.define! do
16
16
  stage :default_bundle do
17
17
  action { Bundler.require(:default) }
18
18
 
19
- stage :dev do
19
+ stage :development_environment do
20
20
  action do
21
21
  Bundler.require(:development)
22
22
  Rails.env = ENV['RAILS_ENV'] = "development"
@@ -57,18 +57,30 @@ Zeus::Server.define! do
57
57
  end
58
58
  end
59
59
 
60
- stage :test do
60
+ stage :test_environment do
61
61
  action do
62
- Rails.env = ENV['RAILS_ENV'] = "test"
63
62
  Bundler.require(:test)
63
+
64
+ Rails.env = ENV['RAILS_ENV'] = 'test'
64
65
  require APP_PATH
66
+
67
+ $rails_rake_task = 'yup' # lie to skip eager loading
65
68
  Rails.application.require_environment!
69
+ $rails_rake_task = nil
70
+
71
+ test = File.join(ROOT_PATH, 'test')
72
+ $LOAD_PATH.unshift(test) unless $LOAD_PATH.include?(test)
73
+ $LOAD_PATH.unshift(ROOT_PATH) unless $LOAD_PATH.include?(ROOT_PATH)
66
74
  end
67
75
 
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
76
+ stage :test_helper do
77
+ action { require 'test_helper' }
78
+
79
+ command :testrb do
80
+ (r = Test::Unit::AutoRunner.new(true)).process_args(ARGV) or
81
+ abort r.options.banner + " tests..."
82
+ exit r.run
83
+ end
72
84
  end
73
85
 
74
86
  end
data/lib/zeus/ui.rb CHANGED
@@ -6,16 +6,8 @@ module Zeus
6
6
  @debug = ENV['DEBUG']
7
7
  end
8
8
 
9
- def as_zeus(msg)
10
- tell_me("[zeus] #{msg}",:purple)
11
- end
12
-
13
9
  def info(msg)
14
- tell_me(msg, nil) if !@quiet
15
- end
16
-
17
- def confirm(msg)
18
- tell_me(msg, :green) if !@quiet
10
+ tell_me(msg, :magenta) if !@quiet
19
11
  end
20
12
 
21
13
  def warn(msg)
@@ -26,37 +18,37 @@ module Zeus
26
18
  tell_me(msg, :red)
27
19
  end
28
20
 
29
- def be_quiet!
30
- @quiet = true
21
+ def debug(msg)
22
+ tell_me(msg, nil) if debug?
31
23
  end
32
24
 
33
- def debug?
34
- # needs to be false instead of nil to be newline param to other methods
35
- !!@debug && !@quiet
25
+ def be_quiet!
26
+ @quiet = true
36
27
  end
37
28
 
38
29
  def debug!
39
30
  @debug = true
40
31
  end
41
32
 
42
- def debug(msg)
43
- tell_me(msg, nil) if debug?
33
+ def debug?
34
+ !!@debug && !@quiet
44
35
  end
45
36
 
46
37
  private
38
+
47
39
  def tell_me(msg, color = nil)
40
+ puts make_message(msg, color)
41
+ end
42
+
43
+ def make_message(msg, color)
48
44
  msg = case color
49
- when :red ; "\x1b[31m#{msg}\x1b[0m"
50
- when :green ; "\x1b[32m#{msg}\x1b[0m"
51
- when :yellow ; "\x1b[33m#{msg}\x1b[0m"
52
- when :purple ; "\x1b[35m#{msg}\x1b[0m"
53
- else ; msg
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
+ when :magenta ; "\x1b[35m#{msg}\x1b[0m"
49
+ else ; msg
54
50
  end
55
- if msg[-1] == "\n"
56
- puts msg
57
- else
58
- puts "#{msg}\n"
59
- end
51
+ msg[-1] == "\n" ? msg : "#{msg}\n"
60
52
  end
61
53
 
62
54
 
data/lib/zeus/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Zeus
2
- VERSION = "0.2.6"
2
+ VERSION = "0.3.0"
3
3
  end
data/spec/cli_spec.rb ADDED
@@ -0,0 +1,67 @@
1
+ require 'zeus'
2
+
3
+ module Zeus
4
+ describe CLI do
5
+
6
+ let(:ui) { stub(debug!: nil) }
7
+
8
+ before do
9
+ Zeus::UI.stub(new: ui)
10
+ end
11
+
12
+ describe "#help" do
13
+ it "prints a generic help menu" do
14
+ ui.should_receive(:info).with(/Global Commands.*zeus help.*show this help menu/m)
15
+ run_with_args("help")
16
+ end
17
+
18
+ it "prints a usage menu per command" do
19
+ ui.should_receive(:info).with(/Usage:.*zeus version.*version information/m)
20
+ run_with_args("help", "version")
21
+ end
22
+ end
23
+
24
+ describe "#start" do
25
+ it "starts the zeus server"
26
+ it "uses the rails template file if the project is missing a config file but looks like rails"
27
+ it "prints an error and exits if there is no config file and the project doesn't look like rails"
28
+ end
29
+
30
+ describe "#version" do
31
+ STRING_INCLUDING_VERSION = %r{#{Regexp.escape Zeus::VERSION}}
32
+
33
+ it "prints the version and exits" do
34
+ ui.should_receive(:info).with(STRING_INCLUDING_VERSION)
35
+ run_with_args("version")
36
+ end
37
+
38
+ it "has aliases" do
39
+ ui.should_receive(:info).with(STRING_INCLUDING_VERSION).twice
40
+ run_with_args("--version")
41
+ run_with_args("-v")
42
+ end
43
+
44
+ end
45
+
46
+ describe "#init" do
47
+ it "currently only generates a rails file, even if the project doesn't look like rails"
48
+ it "prints an error and exits if the project already has a zeus config"
49
+ end
50
+
51
+ describe "generated tasks" do
52
+ it "displays generated tasks in the help menu" do
53
+ ui.should_receive(:info).with(/spec/)
54
+ run_with_args("help")
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def run_with_args(*args)
61
+ ARGV.replace(args)
62
+ Zeus::CLI.start
63
+ end
64
+
65
+ end
66
+ end
67
+
@@ -0,0 +1,88 @@
1
+ require 'socket'
2
+ require 'tempfile'
3
+ require 'fileutils'
4
+ require 'securerandom'
5
+
6
+ require 'zeus'
7
+
8
+ module Zeus::Server::FileMonitor
9
+ describe FSEvent do
10
+
11
+ let(:fsevent) { FSEvent.new() { } }
12
+
13
+ it 'registers files to be watched' do
14
+ _, io_out = stub_open_wrapper!
15
+
16
+ fsevent.watch("/a/b/c.rb")
17
+ io_out.readline.chomp.should == "/a/b/c.rb"
18
+ end
19
+
20
+ it 'only registers a file with the wrapper script once' do
21
+ _, io_out = stub_open_wrapper!
22
+
23
+ files = ["a", "a", "b", "a", "b", "c", "d", "a"]
24
+ files.each { |f| fsevent.watch(f) }
25
+
26
+ files.uniq.each do |file|
27
+ io_out.readline.chomp.should == file
28
+ end
29
+ end
30
+
31
+ it 'passes changed files to a callback' do
32
+ io_in, io_out = stub_open_wrapper!
33
+
34
+ # to prove that very long filenames aren't truncated anywhere:
35
+ filename = SecureRandom.hex(4000) + ".rb"
36
+
37
+ results = []
38
+ fsevent = FSEvent.new { |f| results << f }
39
+
40
+ io_in.puts filename
41
+ fsevent.stub(realpaths_for_givenpath: [filename])
42
+ # test that the right socket is used, and it's ready for reading.
43
+ IO.select([fsevent.datasource])[0].should == [io_out]
44
+
45
+ Zeus.ui.should_receive(:info).with(%r{#{filename}})
46
+ fsevent.on_datasource_event
47
+ results[0].should == filename
48
+ end
49
+
50
+
51
+ it 'closes sockets not necessary in child processes' do
52
+ io_in, io_out = stub_open_wrapper!
53
+ fsevent.close_parent_socket
54
+
55
+ io_in.should be_closed
56
+ io_out.should be_closed
57
+ end
58
+
59
+ it 'integrates with the wrapper script to detect changes' do
60
+ results = []
61
+ callback = ->(path){ results << path }
62
+ fsevent = FSEvent.new(&callback)
63
+
64
+ file = Tempfile.new('fsevent-test')
65
+
66
+ fsevent.watch(file.path)
67
+
68
+ Zeus.ui.should_receive(:info).with(%r{#{file.path}})
69
+
70
+ FileUtils.touch(file.path)
71
+ IO.select([fsevent.datasource], [], [], 3)[0] # just wait for the data to appear
72
+ fsevent.on_datasource_event
73
+ results[0].should == file.path
74
+
75
+ file.unlink
76
+ end
77
+
78
+ private
79
+
80
+ def stub_open_wrapper!
81
+ io_in, io_out = Socket.pair(:UNIX, :STREAM)
82
+ FSEvent.any_instance.stub(open_wrapper: [io_in, io_out])
83
+
84
+ [io_in, io_out]
85
+ end
86
+
87
+ end
88
+ end
@@ -0,0 +1,50 @@
1
+ require 'zeus'
2
+
3
+ class Zeus::Server
4
+ describe ProcessTreeMonitor do
5
+
6
+ let(:file_monitor) { stub }
7
+ let(:tree) { stub }
8
+ let(:monitor) { ProcessTreeMonitor.new(file_monitor, tree) }
9
+
10
+ it "closes sockets not useful to forked processes" do
11
+ parent, child = stub, stub
12
+ ProcessTreeMonitor.any_instance.stub(open_socketpair: [parent, child])
13
+ parent.should_receive(:close)
14
+ monitor.close_parent_socket
15
+ end
16
+
17
+ it "closes sockets not useful to the master process" do
18
+ parent, child = stub, stub
19
+ ProcessTreeMonitor.any_instance.stub(open_socketpair: [parent, child])
20
+ child.should_receive(:close)
21
+ monitor.close_child_socket
22
+ end
23
+
24
+ it "kills nodes with a feature that changed" do
25
+ tree.should_receive(:kill_nodes_with_feature).with("rails")
26
+ monitor.kill_nodes_with_feature("rails")
27
+ end
28
+
29
+ it "passes process inheritance information to the tree" do
30
+ IO.select([monitor.datasource], [], [], 0).should be_nil
31
+ monitor.__CHILD__pid_has_ppid(1, 2)
32
+ IO.select([monitor.datasource], [], [], 0.5).should_not be_nil
33
+ tree.should_receive(:process_has_parent).with(1, 2)
34
+ monitor.on_datasource_event
35
+ end
36
+
37
+ it "passes process feature information to the tree" do
38
+ IO.select([monitor.datasource], [], [], 0).should be_nil
39
+ monitor.__CHILD__pid_has_feature(1, "rails")
40
+ IO.select([monitor.datasource], [], [], 0.5).should_not be_nil
41
+ tree.should_receive(:process_has_feature).with(1, "rails")
42
+ file_monitor.should_receive(:watch).with("rails")
43
+ monitor.on_datasource_event
44
+ end
45
+
46
+ private
47
+
48
+ end
49
+ end
50
+
@@ -0,0 +1,65 @@
1
+ require 'zeus'
2
+
3
+ class Zeus::Server
4
+
5
+ describe ProcessTree do
6
+
7
+ ROOT_PID = Process.pid
8
+ CHILD_1 = ROOT_PID + 1
9
+ CHILD_2 = ROOT_PID + 2
10
+ GRANDCHILD_1 = ROOT_PID + 3
11
+ GRANDCHILD_2 = ROOT_PID + 4
12
+
13
+ let(:process_tree) { ProcessTree.new }
14
+
15
+ before do
16
+ build_tree
17
+ add_features
18
+ end
19
+
20
+ it "doesn't kill the root node" do
21
+ Zeus.ui.should_receive(:error).with(/not killing zeus/i)
22
+ Process.should_not_receive(:kill)
23
+ process_tree.kill_nodes_with_feature("zeus")
24
+ end
25
+
26
+ it "kills a node that has a feature" do
27
+ expect_kill(CHILD_2)
28
+ process_tree.kill_nodes_with_feature("rails")
29
+ end
30
+
31
+ it "kills multiple nodes at the same level with a feature" do
32
+ expect_kill(GRANDCHILD_1)
33
+ expect_kill(GRANDCHILD_2)
34
+ process_tree.kill_nodes_with_feature("model")
35
+ end
36
+
37
+ private
38
+
39
+ def expect_kill(pid)
40
+ Process.should_receive(:kill).with("INT", pid)
41
+ end
42
+
43
+ def build_tree
44
+ process_tree.process_has_parent(CHILD_1, ROOT_PID)
45
+ process_tree.process_has_parent(CHILD_2, CHILD_1)
46
+ process_tree.process_has_parent(GRANDCHILD_1, CHILD_2)
47
+ process_tree.process_has_parent(GRANDCHILD_2, CHILD_2)
48
+ end
49
+
50
+ def add_features
51
+ [CHILD_2, GRANDCHILD_1, GRANDCHILD_2].each do |pid|
52
+ process_tree.process_has_feature(pid, "rails")
53
+ end
54
+
55
+ process_tree.process_has_feature(GRANDCHILD_1, "model")
56
+ process_tree.process_has_feature(GRANDCHILD_2, "model")
57
+
58
+ [ROOT_PID, CHILD_1, CHILD_2, GRANDCHILD_1, GRANDCHILD_2].each do |pid|
59
+ process_tree.process_has_feature(pid, "zeus")
60
+ end
61
+ end
62
+
63
+ end
64
+
65
+ end
data/spec/ui_spec.rb ADDED
@@ -0,0 +1,54 @@
1
+ require 'zeus'
2
+
3
+ module Zeus
4
+ describe UI do
5
+
6
+ let(:ui) {
7
+ ui = UI.new
8
+ # override this method to return the result rather than printing it.
9
+ def ui.tell_me(msg, color)
10
+ return make_message(msg, color)
11
+ end
12
+ ui
13
+ }
14
+
15
+ it "prints errors in red, regardless of verbosity level" do
16
+ ui.error("error").should == "\x1b[31merror\x1b[0m\n"
17
+ ui.be_quiet!
18
+ ui.error("error").should == "\x1b[31merror\x1b[0m\n"
19
+ end
20
+
21
+ it "prints warnings in yellow, regardless of verbosity level" do
22
+ ui.warn("warning").should == "\x1b[33mwarning\x1b[0m\n"
23
+ ui.be_quiet!
24
+ ui.warn("warning").should == "\x1b[33mwarning\x1b[0m\n"
25
+ end
26
+
27
+ it "prints info messages in magenta, but not if quiet-mode is set" do
28
+ ui.info("info").should == "\x1b[35minfo\x1b[0m\n"
29
+ ui.be_quiet!
30
+ ui.info("info").should == nil
31
+ end
32
+
33
+ it "doesn't print debug messages by default" do
34
+ ui.debug("debug").should == nil
35
+ end
36
+
37
+ it "prints debug messages if debug-mode is set" do
38
+ ui.debug!
39
+ ui.debug("debug").should == "debug\n"
40
+ end
41
+
42
+ it "sets debug if ENV['DEBUG']" do
43
+ ENV['DEBUG'] = "yup"
44
+ ui.debug?.should be_true
45
+ end
46
+
47
+ it "doesn't print debug messages if both quiet-mode and debug-mode are set" do
48
+ ui.be_quiet!
49
+ ui.debug!
50
+ ui.debug("debug").should == nil
51
+ end
52
+
53
+ end
54
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zeus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-08-03 00:00:00.000000000 Z
12
+ date: 2012-08-08 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: Zeus preloads pretty much everything you'll ever want to use in development.
15
15
  email:
@@ -20,11 +20,11 @@ extensions: []
20
20
  extra_rdoc_files: []
21
21
  files:
22
22
  - .gitignore
23
+ - .zeus.rb
23
24
  - Gemfile
24
25
  - LICENSE
25
26
  - README.md
26
27
  - Rakefile
27
- - TODO.md
28
28
  - bin/zeus
29
29
  - ext/fsevents-wrapper/fsevents-wrapper
30
30
  - ext/fsevents-wrapper/main.m
@@ -47,6 +47,11 @@ files:
47
47
  - lib/zeus/templates/rails.rb
48
48
  - lib/zeus/ui.rb
49
49
  - lib/zeus/version.rb
50
+ - spec/cli_spec.rb
51
+ - spec/server/file_monitor/fsevent_spec.rb
52
+ - spec/server/process_tree_monitor_spec.rb
53
+ - spec/server/process_tree_spec.rb
54
+ - spec/ui_spec.rb
50
55
  - zeus.gemspec
51
56
  homepage: http://github.com/burke/zeus
52
57
  licenses: []
@@ -72,4 +77,10 @@ rubygems_version: 1.8.11
72
77
  signing_key:
73
78
  specification_version: 3
74
79
  summary: Zeus is an alpha-quality application preloader with terrible documentation.
75
- test_files: []
80
+ test_files:
81
+ - spec/cli_spec.rb
82
+ - spec/server/file_monitor/fsevent_spec.rb
83
+ - spec/server/process_tree_monitor_spec.rb
84
+ - spec/server/process_tree_spec.rb
85
+ - spec/ui_spec.rb
86
+ has_rdoc:
data/TODO.md DELETED
@@ -1,14 +0,0 @@
1
- ## TODO (roughly prioritized)
2
-
3
- * Make sure that when a command process's connection is dropped, it is killed
4
- * less leaky handling of at_exit pid killing
5
- * Refactor, refactor, refactor...
6
- * Support other frameworks?
7
- * Figure out how to run full test suites without multiple env loads
8
-
9
- ## Ideas (not quite TODOs)
10
-
11
- * (maybe) Start the preloader as a daemon transparently when any command is run, then wait for it to finish
12
- * Support inotify on linux
13
-
14
-