zeus 0.4.5 → 0.4.6
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/README.md +2 -1
- data/Rakefile +9 -3
- data/bin/zeus +4 -0
- data/docs/acceptor_registration.md +14 -0
- data/docs/client_server_handshake.md +25 -0
- data/lib/zeus.rb +19 -7
- data/lib/zeus/client.rb +36 -36
- data/lib/zeus/client/winsize.rb +28 -0
- data/lib/zeus/error_printer.rb +16 -0
- data/lib/zeus/plan.rb +18 -0
- data/lib/zeus/plan/acceptor.rb +38 -0
- data/lib/zeus/plan/node.rb +66 -0
- data/lib/zeus/plan/stage.rb +50 -0
- data/lib/zeus/server.rb +17 -17
- data/lib/zeus/server/acceptor.rb +7 -24
- data/lib/zeus/server/acceptor_registration_monitor.rb +2 -10
- data/lib/zeus/server/client_handler.rb +14 -37
- data/lib/zeus/server/command_runner.rb +36 -16
- data/lib/zeus/server/file_monitor.rb +2 -1
- data/lib/zeus/server/file_monitor/fsevent.rb +6 -1
- data/lib/zeus/server/stage.rb +49 -40
- data/lib/zeus/server/stage/error_state.rb +42 -0
- data/lib/zeus/server/stage/feature_notifier.rb +38 -0
- data/lib/zeus/version.rb +1 -1
- data/spec/error_printer_spec.rb +27 -0
- data/spec/integration_spec.rb +31 -12
- metadata +14 -4
- data/lib/zeus/dsl.rb +0 -154
- data/lib/zeus/server/forked_process.rb +0 -98
data/README.md
CHANGED
@@ -27,7 +27,7 @@ Install the gem.
|
|
27
27
|
|
28
28
|
Q: "I should put it in my `Gemfile`, right?"
|
29
29
|
|
30
|
-
A:
|
30
|
+
A: You can, but running `bundle exec zeus` instead of `zeus` can add precious seconds to a command that otherwise would take 200ms. Zeus was built to be run from outside of bundler.
|
31
31
|
|
32
32
|
## Usage
|
33
33
|
|
@@ -57,6 +57,7 @@ Check [Rails tests](https://github.com/burke/zeus-extended-testcases) if you cha
|
|
57
57
|
|
58
58
|
## Thanks...
|
59
59
|
|
60
|
+
* To [Michael Grosser](http://github.com/grosser) for various contributions
|
60
61
|
* To [Stefan Penner](http://github.com/stefanpenner) for discussion and various contributions.
|
61
62
|
* To [Samuel Kadolph](http://github.com/samuelkadolph) for doing most of the cross-process pseudoterminal legwork.
|
62
63
|
* To [Jesse Storimer](http://github.com/jstorimer) for spin, part of the inspiration for this project
|
data/Rakefile
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
#!/usr/bin/env rake
|
2
2
|
require "bundler/gem_tasks"
|
3
3
|
|
4
|
-
require 'rspec'
|
5
|
-
task :
|
6
|
-
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
task :spec do
|
6
|
+
raise "tests do not work with bundle exec" if defined?(Bundler)
|
7
|
+
desc "Run specs under spec/"
|
8
|
+
RSpec::Core::RakeTask.new do |t|
|
9
|
+
t.pattern = 'spec/**/*_spec.rb'
|
10
|
+
end
|
7
11
|
end
|
12
|
+
|
13
|
+
task default: :spec
|
data/bin/zeus
CHANGED
@@ -0,0 +1,14 @@
|
|
1
|
+
# Acceptor Registration
|
2
|
+
|
3
|
+
When an acceptor is booted, it registers itself with the master process through UNIX Sockets. Specifically, it talks to the `AcceptorRegistrationMonitor`.
|
4
|
+
|
5
|
+
Here's an overview of the registration process:
|
6
|
+
|
7
|
+
1. `AcceptorRegistrationMonitor` creates a socketpair for Acceptor registration (`S_REG`)
|
8
|
+
2. When an `Acceptor` is started, it:
|
9
|
+
1. Creates a new socketpair for communication with the master process (`S_ACC`)
|
10
|
+
2. Sends one side of `S_ACC` over `S_REG` to the master.
|
11
|
+
3. Sends its pid and then a newline character over `S_REG`.
|
12
|
+
3. `AcceptorRegistrationMonitor` receives first the IO and then the pid, and stores them for later reference.
|
13
|
+
|
14
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Client/Server handshake
|
2
|
+
|
3
|
+
This takes place in `lib/zeus/server/client_handler.rb`, `lib/zeus/client.rb`, and `lib/zeus/server/acceptor.rb`.
|
4
|
+
|
5
|
+
The model is kind of convoluted, so here's an explanation of what's happening with all these sockets:
|
6
|
+
|
7
|
+
## Running a command
|
8
|
+
1. ClientHandler has a UNIXServer (`SVR`) listening.
|
9
|
+
2. ClientHandler has a socketpair with the acceptor referenced by the command (see `docs/acceptor_registration.md`) (`S_ACC`)
|
10
|
+
3. When clienthandler receives a connection (`S_CLI`) on `SVR`:
|
11
|
+
1. ClientHandler sends `S_CLI` over `S_ACC`, so the acceptor can communicate with the server's client.
|
12
|
+
2. ClientHandler sends a JSON-encoded array of `arguments` over `S_ACC`
|
13
|
+
3. Acceptor sends the newly-forked worker PID over `S_ACC` to clienthandler.
|
14
|
+
4. ClientHandler forwards the pid to the client over `S_CLI`.
|
15
|
+
|
16
|
+
|
17
|
+
## A sort of network diagram
|
18
|
+
client clienthandler acceptor
|
19
|
+
1 ----------> | {command: String, arguments: [String]}
|
20
|
+
2 ----------> | Terminal IO
|
21
|
+
3 -----------> | Terminal IO
|
22
|
+
4 -----------> | Arguments (json array)
|
23
|
+
5 <----------- | pid
|
24
|
+
6 <--------- | pid
|
25
|
+
|
data/lib/zeus.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
|
+
require 'zeus/version'
|
2
|
+
require 'zeus/ui'
|
3
|
+
require 'zeus/plan'
|
4
|
+
require 'zeus/server'
|
5
|
+
require 'zeus/client'
|
6
|
+
require 'zeus/error_printer'
|
7
|
+
require 'zeus/cli'
|
8
|
+
|
1
9
|
module Zeus
|
2
10
|
SOCKET_NAME = '.zeus.sock'
|
3
11
|
|
4
|
-
autoload :UI, 'zeus/ui'
|
5
|
-
autoload :CLI, 'zeus/cli'
|
6
|
-
autoload :DSL, 'zeus/dsl'
|
7
|
-
autoload :Server, 'zeus/server'
|
8
|
-
autoload :Client, 'zeus/client'
|
9
|
-
autoload :VERSION, 'zeus/version'
|
10
|
-
|
11
12
|
class ZeusError < StandardError
|
12
13
|
def self.status_code(code)
|
13
14
|
define_method(:status_code) { code }
|
@@ -32,4 +33,15 @@ module Zeus
|
|
32
33
|
@after_fork = []
|
33
34
|
end
|
34
35
|
|
36
|
+
def self.thread_with_backtrace_on_error(&b)
|
37
|
+
Thread.new {
|
38
|
+
begin
|
39
|
+
b.call
|
40
|
+
rescue => e
|
41
|
+
ErrorPrinter.new(e).write_to($stdout)
|
42
|
+
end
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
35
46
|
end
|
47
|
+
|
data/lib/zeus/client.rb
CHANGED
@@ -11,14 +11,18 @@ require "json"
|
|
11
11
|
require "pty"
|
12
12
|
require "socket"
|
13
13
|
|
14
|
+
require 'zeus/client/winsize'
|
15
|
+
|
14
16
|
module Zeus
|
15
17
|
class Client
|
18
|
+
include Winsize
|
16
19
|
|
17
20
|
attr_accessor :pid
|
18
21
|
|
19
22
|
SIGNALS = {
|
20
23
|
"\x03" => "INT",
|
21
|
-
"\x1C" => "QUIT"
|
24
|
+
"\x1C" => "QUIT",
|
25
|
+
"\x1A" => "TSTP",
|
22
26
|
}
|
23
27
|
SIGNAL_REGEX = Regexp.union(SIGNALS.keys)
|
24
28
|
|
@@ -29,62 +33,55 @@ module Zeus
|
|
29
33
|
def run(command, args)
|
30
34
|
maybe_raw do
|
31
35
|
PTY.open do |master, slave|
|
36
|
+
@exit_status, @es2 = IO.pipe
|
32
37
|
@master = master
|
33
38
|
set_winsize
|
34
|
-
|
35
|
-
@winch = make_winch_channel
|
39
|
+
make_winch_channel
|
36
40
|
@pid = connect_to_server(command, args, slave)
|
37
41
|
|
38
|
-
|
39
|
-
begin
|
40
|
-
while ready = select([@winch, @master, $stdin])[0]
|
41
|
-
handle_winch if ready.include?(@winch)
|
42
|
-
handle_stdin(buffer) if ready.include?($stdin)
|
43
|
-
handle_master(buffer) if ready.include?(@master)
|
44
|
-
end
|
45
|
-
rescue EOFError
|
46
|
-
end
|
42
|
+
select_loop!
|
47
43
|
end
|
48
44
|
end
|
49
45
|
end
|
50
46
|
|
51
47
|
private
|
52
48
|
|
49
|
+
def select_loop!
|
50
|
+
buffer = ""
|
51
|
+
while ready = select([winch, @master, $stdin, @exit_status])[0]
|
52
|
+
handle_winch if ready.include?(winch)
|
53
|
+
handle_stdin(buffer) if ready.include?($stdin)
|
54
|
+
handle_master(buffer) if ready.include?(@master)
|
55
|
+
handle_exit if ready.include?(@exit_status)
|
56
|
+
end
|
57
|
+
rescue EOFError
|
58
|
+
end
|
59
|
+
|
60
|
+
def handle_exit
|
61
|
+
exit @exit_status.readline.chomp.to_i
|
62
|
+
end
|
63
|
+
|
53
64
|
def connect_to_server(command, arguments, slave, socket_path = Zeus::SOCKET_NAME)
|
54
65
|
socket = UNIXSocket.new(socket_path)
|
55
66
|
socket << {command: command, arguments: arguments}.to_json << "\n"
|
56
67
|
socket.send_io(slave)
|
68
|
+
socket.send_io(@es2)
|
57
69
|
slave.close
|
58
70
|
|
59
71
|
pid = socket.readline.chomp.to_i
|
72
|
+
trap("CONT") { Process.kill("CONT", @pid) }
|
73
|
+
pid
|
60
74
|
rescue Errno::ENOENT, Errno::ECONNREFUSED, Errno::ECONNRESET
|
61
75
|
# we need a \r at the end because the terminal is in raw mode.
|
62
|
-
# we need to reset the cursor to position 0
|
63
76
|
Zeus.ui.error "Zeus doesn't seem to be running, try 'zeus start`\r"
|
64
77
|
exit 1
|
65
78
|
end
|
66
79
|
|
67
|
-
def make_winch_channel
|
68
|
-
winch, winch_ = IO.pipe
|
69
|
-
trap("WINCH") { winch_ << "\0" }
|
70
|
-
winch
|
71
|
-
end
|
72
|
-
|
73
|
-
def handle_winch
|
74
|
-
@winch.read(1)
|
75
|
-
set_winsize
|
76
|
-
begin
|
77
|
-
Process.kill("WINCH", pid) if pid
|
78
|
-
rescue Errno::ESRCH
|
79
|
-
exit # the remote process died. Just quit.
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
80
|
def handle_stdin(buffer)
|
84
81
|
input = $stdin.readpartial(4096, buffer)
|
85
82
|
input.scan(SIGNAL_REGEX).each { |signal|
|
86
83
|
begin
|
87
|
-
|
84
|
+
send_signal(signal, pid)
|
88
85
|
rescue Errno::ESRCH
|
89
86
|
exit # the remote process died. Just quit.
|
90
87
|
end
|
@@ -92,12 +89,17 @@ module Zeus
|
|
92
89
|
@master << input
|
93
90
|
end
|
94
91
|
|
95
|
-
def
|
96
|
-
|
92
|
+
def send_signal(signal, pid)
|
93
|
+
if SIGNALS[signal] == "TSTP"
|
94
|
+
Process.kill("STOP", pid)
|
95
|
+
Process.kill("TSTP", Process.pid)
|
96
|
+
else
|
97
|
+
Process.kill(SIGNALS[signal], pid)
|
98
|
+
end
|
97
99
|
end
|
98
100
|
|
99
|
-
def
|
100
|
-
$stdout
|
101
|
+
def handle_master(buffer)
|
102
|
+
$stdout << @master.readpartial(4096, buffer)
|
101
103
|
end
|
102
104
|
|
103
105
|
def maybe_raw(&b)
|
@@ -110,5 +112,3 @@ module Zeus
|
|
110
112
|
|
111
113
|
end
|
112
114
|
end
|
113
|
-
|
114
|
-
__FILE__ == $0 and Zeus::Client.run
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Zeus
|
2
|
+
class Client
|
3
|
+
module Winsize
|
4
|
+
|
5
|
+
attr_reader :winch
|
6
|
+
|
7
|
+
def set_winsize
|
8
|
+
$stdout.tty? and @master.winsize = $stdout.winsize
|
9
|
+
end
|
10
|
+
|
11
|
+
def make_winch_channel
|
12
|
+
@winch, winch_ = IO.pipe
|
13
|
+
trap("WINCH") { winch_ << "\0" }
|
14
|
+
end
|
15
|
+
|
16
|
+
def handle_winch
|
17
|
+
@winch.read(1)
|
18
|
+
set_winsize
|
19
|
+
begin
|
20
|
+
Process.kill("WINCH", pid) if pid
|
21
|
+
rescue Errno::ESRCH
|
22
|
+
exit # the remote process died. Just quit.
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Zeus
|
2
|
+
class ErrorPrinter
|
3
|
+
attr_reader :error
|
4
|
+
def initialize(error)
|
5
|
+
@error = error
|
6
|
+
end
|
7
|
+
|
8
|
+
def write_to(io)
|
9
|
+
io.puts "#{error.backtrace[0]}: #{error.message} (#{error.class})"
|
10
|
+
error.backtrace[1..-1].each do |line|
|
11
|
+
io.puts "\tfrom #{line}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
data/lib/zeus/plan.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
require 'zeus/plan/node'
|
4
|
+
require 'zeus/plan/stage'
|
5
|
+
require 'zeus/plan/acceptor'
|
6
|
+
|
7
|
+
module Zeus
|
8
|
+
module Plan
|
9
|
+
class Evaluator
|
10
|
+
def stage(name, &b)
|
11
|
+
stage = Plan::Stage.new(name)
|
12
|
+
stage.root = true
|
13
|
+
stage.instance_eval(&b)
|
14
|
+
stage
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Zeus
|
2
|
+
module Plan
|
3
|
+
|
4
|
+
class Acceptor < Node
|
5
|
+
|
6
|
+
attr_reader :name, :aliases, :description, :action
|
7
|
+
def initialize(name, aliases, description, &b)
|
8
|
+
super(name)
|
9
|
+
@description = description
|
10
|
+
@aliases = aliases
|
11
|
+
@action = b
|
12
|
+
end
|
13
|
+
|
14
|
+
# ^ configuration
|
15
|
+
# V later use
|
16
|
+
|
17
|
+
def commands
|
18
|
+
[name, *aliases].map(&:to_s)
|
19
|
+
end
|
20
|
+
|
21
|
+
def acceptors
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_process_object(server)
|
26
|
+
Zeus::Server::Acceptor.new(server).tap do |stage|
|
27
|
+
stage.name = @name
|
28
|
+
stage.aliases = @aliases
|
29
|
+
stage.action = @action
|
30
|
+
stage.description = @description
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Zeus
|
2
|
+
module Plan
|
3
|
+
|
4
|
+
class Node
|
5
|
+
attr_reader :name, :stages, :features
|
6
|
+
attr_accessor :pid, :root
|
7
|
+
|
8
|
+
def initialize(name)
|
9
|
+
@name = name
|
10
|
+
@stages = []
|
11
|
+
@features = Set.new # hash might be faster than ruby's inane impl of set.
|
12
|
+
end
|
13
|
+
|
14
|
+
def stage_has_feature(name, file)
|
15
|
+
node_for_name(name).features << file
|
16
|
+
end
|
17
|
+
|
18
|
+
def stage_has_pid(name, pid)
|
19
|
+
node_for_name(name).pid = pid
|
20
|
+
end
|
21
|
+
|
22
|
+
def kill_nodes_with_feature(file)
|
23
|
+
if features.include?(file)
|
24
|
+
if root
|
25
|
+
Zeus.ui.error "One of zeus's dependencies changed. Not killing zeus. You may have to restart the server."
|
26
|
+
else
|
27
|
+
kill!
|
28
|
+
end
|
29
|
+
else
|
30
|
+
stages.each do |child|
|
31
|
+
child.kill_nodes_with_feature(file)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# We send STOP before actually killing the processes here.
|
37
|
+
# This is to prevent parents from respawning before all the children
|
38
|
+
# are killed. This prevents a race condition.
|
39
|
+
def kill!
|
40
|
+
Process.kill("STOP", pid) if pid
|
41
|
+
# Protected methods don't work with each(&:m) notation.
|
42
|
+
stages.each { |stage| stage.kill! }
|
43
|
+
old_pid = pid
|
44
|
+
self.pid = nil
|
45
|
+
Process.kill("KILL", old_pid) if old_pid
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def node_for_name(name)
|
51
|
+
@nodes_by_name ||= __nodes_by_name
|
52
|
+
@nodes_by_name[name]
|
53
|
+
end
|
54
|
+
|
55
|
+
def __nodes_by_name
|
56
|
+
nodes = {name => self}
|
57
|
+
stages.each do |child|
|
58
|
+
nodes.merge!(child.__nodes_by_name)
|
59
|
+
end
|
60
|
+
nodes
|
61
|
+
end ; protected :__nodes_by_name
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Zeus
|
2
|
+
module Plan
|
3
|
+
|
4
|
+
class Stage < Node
|
5
|
+
|
6
|
+
attr_reader :actions
|
7
|
+
def initialize(name)
|
8
|
+
super(name)
|
9
|
+
@actions = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def action(&b)
|
13
|
+
@actions << b
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def desc(desc)
|
18
|
+
@desc = desc
|
19
|
+
end
|
20
|
+
|
21
|
+
def stage(name, &b)
|
22
|
+
@stages << Plan::Stage.new(name).tap { |s| s.instance_eval(&b) }
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def command(name, *aliases, &b)
|
27
|
+
@stages << Plan::Acceptor.new(name, aliases, @desc, &b)
|
28
|
+
@desc = nil
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
# ^ configuration
|
33
|
+
# V later use
|
34
|
+
|
35
|
+
def acceptors
|
36
|
+
stages.map(&:acceptors).flatten
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_process_object(server)
|
40
|
+
Zeus::Server::Stage.new(server).tap do |stage|
|
41
|
+
stage.name = @name
|
42
|
+
stage.stages = @stages.map { |stage| stage.to_process_object(server) }
|
43
|
+
stage.actions = @actions
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|