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 +11 -0
- data/lib/thrud.rb +2 -2
- data/lib/zeus.rb +10 -0
- data/lib/zeus/cli.rb +7 -7
- data/lib/zeus/client.rb +6 -3
- data/lib/zeus/server.rb +13 -21
- data/lib/zeus/server/acceptor.rb +9 -2
- data/lib/zeus/server/acceptor_error_state.rb +2 -2
- data/lib/zeus/server/client_handler.rb +57 -68
- data/lib/zeus/server/file_monitor/fsevent.rb +44 -15
- data/lib/zeus/server/forked_process.rb +12 -3
- data/lib/zeus/server/process_tree.rb +40 -42
- data/lib/zeus/server/process_tree_monitor.rb +27 -22
- data/lib/zeus/templates/rails.rb +19 -7
- data/lib/zeus/ui.rb +18 -26
- data/lib/zeus/version.rb +1 -1
- data/spec/cli_spec.rb +67 -0
- data/spec/server/file_monitor/fsevent_spec.rb +88 -0
- data/spec/server/process_tree_monitor_spec.rb +50 -0
- data/spec/server/process_tree_spec.rb +65 -0
- data/spec/ui_spec.rb +54 -0
- metadata +15 -4
- data/TODO.md +0 -14
data/.zeus.rb
ADDED
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
|
-
|
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
|
-
|
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
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
|
-
|
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
|
-
#
|
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
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
80
|
-
|
81
|
-
|
69
|
+
def_delegators :@acceptor_registration_monitor,
|
70
|
+
:__CHILD__register_acceptor,
|
71
|
+
:__CHILD__find_acceptor_for_command
|
82
72
|
|
83
|
-
|
73
|
+
def_delegators :@process_tree_monitor,
|
74
|
+
:__CHILD__pid_has_ppid,
|
75
|
+
:__CHILD__pid_has_feature
|
84
76
|
|
85
77
|
end
|
86
78
|
end
|
data/lib/zeus/server/acceptor.rb
CHANGED
@@ -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
|
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.
|
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
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
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
|
105
|
+
def wait_for_acceptor(s_client, client_terminal, command)
|
75
106
|
s_client << "0\n"
|
76
|
-
client_terminal << "[zeus]
|
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
|
-
|
113
|
+
s.readline # wait until acceptor is booted
|
89
114
|
end
|
90
115
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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, _ =
|
17
|
-
@
|
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(
|
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 |
|
35
|
-
file_did_change(
|
66
|
+
files.each do |real|
|
67
|
+
file_did_change(real)
|
36
68
|
end
|
37
69
|
end
|
38
70
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
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.
|
30
|
+
Zeus.ui.info("starting #{process_type} `#{@name}`")
|
31
31
|
trap("INT") {
|
32
|
-
Zeus.ui.
|
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
|
-
|
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 =
|
12
|
+
def initialize(file_monitor, tree=ProcessTree.new)
|
13
|
+
@tree = tree
|
14
14
|
@file_monitor = file_monitor
|
15
15
|
|
16
|
-
@sock, @__CHILD__sock =
|
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(
|
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
|
data/lib/zeus/templates/rails.rb
CHANGED
@@ -16,7 +16,7 @@ Zeus::Server.define! do
|
|
16
16
|
stage :default_bundle do
|
17
17
|
action { Bundler.require(:default) }
|
18
18
|
|
19
|
-
stage :
|
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 :
|
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
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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,
|
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
|
30
|
-
|
21
|
+
def debug(msg)
|
22
|
+
tell_me(msg, nil) if debug?
|
31
23
|
end
|
32
24
|
|
33
|
-
def
|
34
|
-
|
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
|
43
|
-
|
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
|
50
|
-
when :green
|
51
|
-
when :yellow
|
52
|
-
when :
|
53
|
-
else
|
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
|
-
|
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
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.
|
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-
|
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
|
-
|