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 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: No. 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.
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 :default do
6
- RSpec::Core::Runner.run(Dir.glob("spec/**/*_spec.rb"))
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
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ if defined?(Bundler)
4
+ puts "\x1b[34mDon't run Zeus with `bundle exec`. It's unnecessarily slow.\x1b[0m"
5
+ end
6
+
3
7
  require 'zeus'
4
8
  Zeus::CLI.start
@@ -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
+
@@ -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
+
@@ -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
- buffer = ""
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
- Process.kill(SIGNALS[signal], pid)
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 handle_master(buffer)
96
- $stdout << @master.readpartial(4096, buffer)
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 set_winsize
100
- $stdout.tty? and @master.winsize = $stdout.winsize
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
@@ -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