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 +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
|
-
|