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