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