zeus 0.2.6 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
-