zeus 0.0.1 → 0.1.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/README.md CHANGED
@@ -1,24 +1,55 @@
1
1
  # Zeus
2
2
 
3
- TODO: Write a gem description
3
+ ## What?
4
4
 
5
- ## Installation
5
+ Zeus preloads your app so that your normal development tasks such as `console`, `server`, `generate`, and tests are faster.
6
+
7
+ ## Why?
8
+
9
+ Because waiting 25 seconds sucks, but waiting 0.4 seconds doesn't.
6
10
 
7
- Add this line to your application's Gemfile:
11
+ ## When?
8
12
 
9
- gem 'zeus'
13
+ Not yet. Zeus is nowhere near production-ready yet. Use only if you really like broken things.
10
14
 
11
- And then execute:
15
+ ## Ugly bits
16
+
17
+ * Probably crashes a lot
18
+ * Creates a bunch of sockets
19
+ * Uses an obscene number of file descriptors
20
+
21
+ ## Installation
12
22
 
13
- $ bundle
23
+ Install the gem.
14
24
 
15
- Or install it yourself as:
25
+ gem install zeus
16
26
 
17
- $ gem install zeus
27
+ Copy `examples/rails.rb` to `{your app}/.zeus.rb`
18
28
 
19
29
  ## Usage
20
30
 
21
- TODO: Write usage instructions here
31
+ Start the server:
32
+
33
+ zeus start
34
+
35
+ Run some commands:
36
+
37
+ zeus console
38
+ zeus server
39
+ zeus testrb -Itest -I. test/unit/omg_test.rb
40
+ zeus generate model omg
41
+ zeus rake -T
42
+ zeus runner omg.rb
43
+
44
+ ## TODO (roughly prioritized)
45
+
46
+ * Kill process when files are detected to have changed
47
+ * Handle client/server without requiring a unix socket for each acceptor (1 shared socket)
48
+ * Make the code less terrible
49
+ * Figure out how to run full test suites without multiple env loads
50
+ * Support other frameworks?
51
+ * Use fsevents instead of kqueue to reduce the obscene number of file descriptors.
52
+ * Support epoll on linux
22
53
 
23
54
  ## Contributing
24
55
 
data/bin/zeus ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ zeus = File.expand_path("../../lib/", __FILE__)
4
+ $:.unshift(zeus) unless $:.include?(zeus)
5
+
6
+ if ARGV[0] == "start"
7
+ require 'zeus/server'
8
+ require './.zeus.rb'
9
+ Zeus::Server.run
10
+ else
11
+ require 'zeus/client'
12
+ Zeus::Client.run
13
+ end
data/examples/rails.rb ADDED
@@ -0,0 +1,78 @@
1
+ require 'socket'
2
+
3
+ Zeus::Server.define! do
4
+ stage :boot do
5
+
6
+ action do
7
+ ENV_PATH = File.expand_path('../config/environment', __FILE__)
8
+ BOOT_PATH = File.expand_path('../config/boot', __FILE__)
9
+ APP_PATH = File.expand_path('../config/application', __FILE__)
10
+ ROOT_PATH = File.expand_path('..', __FILE__)
11
+
12
+ require BOOT_PATH
13
+ require 'rails/all'
14
+ end
15
+
16
+ stage :default_bundle do
17
+ action { Bundler.require(:default) }
18
+
19
+ stage :dev do
20
+ action do
21
+ Bundler.require(:development)
22
+ ENV['RAILS_ENV'] = "development"
23
+ require APP_PATH
24
+ Rails.application.require_environment!
25
+ end
26
+
27
+ acceptor :generate, ".zeus.dev_generate.sock" do
28
+ require 'rails/commands/generate'
29
+ end
30
+ acceptor :runner, ".zeus.dev_runner.sock" do
31
+ require 'rails/commands/runner'
32
+ end
33
+ acceptor :console, ".zeus.dev_console.sock" do
34
+ require 'rails/commands/console'
35
+ Rails::Console.start(Rails.application)
36
+ end
37
+
38
+ acceptor :server, ".zeus.dev_server.sock" do
39
+ require 'rails/commands/server'
40
+ server = Rails::Server.new
41
+ Dir.chdir(Rails.application.root)
42
+ server.start
43
+ end
44
+
45
+ stage :prerake do
46
+ action do
47
+ require 'rake'
48
+ load 'Rakefile'
49
+ end
50
+
51
+ acceptor :rake, ".zeus.dev_rake.sock" do
52
+ Rake.application.run
53
+ end
54
+
55
+ end
56
+ end
57
+
58
+ stage :test do
59
+ action do
60
+ ENV['RAILS_ENV'] = "test"
61
+ Bundler.require(:test)
62
+ require APP_PATH
63
+ Rails.application.require_environment!
64
+ end
65
+
66
+ acceptor :testrb, ".zeus.test_testrb.sock" do
67
+ forkpoint testrb: acceptor(".zeus.test_testrb.sock") {
68
+ (r = Test::Unit::AutoRunner.new(true)).process_args(ARGV) or
69
+ abort r.options.banner + " tests..."
70
+ exit r.run
71
+ }
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,78 @@
1
+ require "io/console"
2
+ require "json"
3
+ require "pty"
4
+ require "socket"
5
+
6
+ module Zeus
7
+ class Client
8
+
9
+ SIGNALS = {
10
+ "\x03" => "TERM",
11
+ "\x1C" => "QUIT"
12
+ }
13
+ SIGNAL_REGEX = Regexp.union(SIGNALS.keys)
14
+
15
+ def self.maybe_raw(&b)
16
+ if $stdout.tty?
17
+ $stdout.raw(&b)
18
+ else
19
+ b.call
20
+ end
21
+ end
22
+
23
+ def self.run
24
+ maybe_raw do
25
+ PTY.open do |master, slave|
26
+ $stdout.tty? and master.winsize = $stdout.winsize
27
+ winch, winch_ = IO.pipe
28
+ trap("WINCH") { winch_ << "\0" }
29
+
30
+ case ARGV.shift
31
+ when 'testrb', 't'
32
+ socket = UNIXSocket.new(".zeus.test_testrb.sock")
33
+ when 'console', 'c'
34
+ socket = UNIXSocket.new(".zeus.dev_console.sock")
35
+ when 'server', 's'
36
+ socket = UNIXSocket.new(".zeus.dev_server.sock")
37
+ when 'rake'
38
+ socket = UNIXSocket.new(".zeus.dev_rake.sock")
39
+ when 'runner', 'r'
40
+ socket = UNIXSocket.new(".zeus.dev_runner.sock")
41
+ when 'generate', 'g'
42
+ socket = UNIXSocket.new(".zeus.dev_generate.sock")
43
+ end
44
+ socket.send_io(slave)
45
+ socket << ARGV.to_json << "\n"
46
+ slave.close
47
+
48
+ pid = socket.gets.strip.to_i
49
+
50
+ begin
51
+ buffer = ""
52
+
53
+ while ready = select([winch, master, $stdin])[0]
54
+ if ready.include?(winch)
55
+ winch.read(1)
56
+ $stdout.tty? and master.winsize = $stdout.winsize
57
+ Process.kill("WINCH", pid)
58
+ end
59
+
60
+ if ready.include?($stdin)
61
+ input = $stdin.readpartial(4096, buffer)
62
+ input.scan(SIGNAL_REGEX).each { |signal|
63
+ Process.kill(SIGNALS[signal], pid)
64
+ }
65
+ master << input
66
+ end
67
+
68
+ if ready.include?(master)
69
+ $stdout << master.readpartial(4096, buffer)
70
+ end
71
+ end
72
+ rescue EOFError
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,20 @@
1
+ module Process
2
+
3
+ def self.killall_descendants(sig, base=Process.pid)
4
+ descendants(base).each do |pid|
5
+ begin
6
+ Process.kill(sig, pid)
7
+ rescue Errno::ESRCH
8
+ end
9
+ end
10
+ end
11
+
12
+ def self.descendants(base=Process.pid)
13
+ descendants = Hash.new{|ht,k| ht[k]=[k]}
14
+ Hash[*`ps -eo pid,ppid`.scan(/\d+/).map{|x|x.to_i}].each{|pid,ppid|
15
+ descendants[ppid] << descendants[pid]
16
+ }
17
+ descendants[base].flatten - [base]
18
+ end
19
+ end
20
+
@@ -0,0 +1,298 @@
1
+ require 'json'
2
+ require 'socket'
3
+
4
+ require 'rb-kqueue'
5
+ require 'zeus/process'
6
+
7
+ module Zeus
8
+ module Server
9
+ def self.define!(&b)
10
+ @@root = Stage.new("(root)")
11
+ @@root.instance_eval(&b)
12
+ @@files = {}
13
+ end
14
+
15
+ def self.pid_has_file(pid, file)
16
+ @@files[file] ||= []
17
+ @@files[file] << pid
18
+ end
19
+
20
+ def self.killall_with_file(file)
21
+ pids = @@files[file]
22
+ @@process_tree.kill_nodes_with_feature(file)
23
+ end
24
+
25
+ TARGET_FD_LIMIT = 8192
26
+
27
+ def self.configure_number_of_file_descriptors
28
+ limit = Process.getrlimit(Process::RLIMIT_NOFILE)
29
+ if limit[0] < TARGET_FD_LIMIT && limit[1] >= TARGET_FD_LIMIT
30
+ Process.setrlimit(Process::RLIMIT_NOFILE, TARGET_FD_LIMIT)
31
+ else
32
+ puts "\x1b[33m[zeus] Warning: increase the max number of file descriptors. If you have a large project, this max cause a crash in about 10 seconds.\x1b[0m"
33
+ end
34
+ end
35
+
36
+ def self.notify(event)
37
+ if event.flags.include?(:delete)
38
+ # file was deleted, so we need to close and reopen it.
39
+ event.watcher.disable!
40
+ begin
41
+ @@queue.watch_file(event.watcher.path, :write, :extend, :rename, :delete, &method(:notify))
42
+ rescue Errno::ENOENT
43
+ lost_files << event.watcher.path
44
+ end
45
+ end
46
+ puts "\x1b[37m[zeus] dependency change: #{event.watcher.path}\x1b[0m"
47
+ killall_with_file(event.watcher.path)
48
+ end
49
+
50
+ def self.run
51
+ $0 = "zeus master"
52
+ configure_number_of_file_descriptors
53
+ trap("INT") { exit 0 }
54
+ at_exit { Process.killall_descendants(9) }
55
+
56
+ $r_features, $w_features = IO.pipe
57
+ $w_features.sync = true
58
+
59
+ $r_pids, $w_pids = IO.pipe
60
+ $w_pids.sync = true
61
+
62
+ @@process_tree = ProcessTree.new
63
+ @@root_stage_pid = @@root.run
64
+
65
+ @@queue = KQueue::Queue.new
66
+
67
+ lost_files = []
68
+
69
+ @@file_watchers = {}
70
+ loop do
71
+ @@queue.poll
72
+
73
+ # TODO: It would be really nice if we could put the queue poller in the select somehow.
74
+ # --investigate kqueue. Is this possible?
75
+ rs, _, _ = IO.select([$r_features, $r_pids], [], [], 1)
76
+ rs.each do |r|
77
+ case r
78
+ when $r_pids ; handle_pid_message(r.readline)
79
+ when $r_features ; handle_feature_message(r.readline)
80
+ end
81
+ end if rs
82
+ end
83
+
84
+ end
85
+
86
+ class ProcessTree
87
+ class Node
88
+ attr_accessor :pid, :children, :features
89
+ def initialize(pid)
90
+ @pid, @children, @features = pid, [], {}
91
+ end
92
+
93
+ def add_child(node)
94
+ self.children << node
95
+ end
96
+
97
+ def add_feature(feature)
98
+ self.features[feature] = true
99
+ end
100
+
101
+ def has_feature?(feature)
102
+ self.features[feature] == true
103
+ end
104
+
105
+ def inspect
106
+ "(#{pid}:#{features.size}:[#{children.map(&:inspect).join(",")}])"
107
+ end
108
+
109
+ end
110
+
111
+ def inspect
112
+ @root.inspect
113
+ end
114
+
115
+ def initialize
116
+ @root = Node.new(Process.pid)
117
+ @nodes_by_pid = {Process.pid => @root}
118
+ end
119
+
120
+ def node_for_pid(pid)
121
+ @nodes_by_pid[pid.to_i] ||= Node.new(pid.to_i)
122
+ end
123
+
124
+ def process_has_parent(pid, ppid)
125
+ curr = node_for_pid(pid)
126
+ base = node_for_pid(ppid)
127
+ base.add_child(curr)
128
+ end
129
+
130
+ def process_has_feature(pid, feature)
131
+ node = node_for_pid(pid)
132
+ node.add_feature(feature)
133
+ end
134
+
135
+ def kill_node(node)
136
+ @nodes_by_pid.delete(node.pid)
137
+ # recall that this process explicitly traps INT -> exit 0
138
+ Process.kill("INT", node.pid)
139
+ end
140
+
141
+ def kill_nodes_with_feature(file, base = @root)
142
+ if base.has_feature?(file)
143
+ if base == @root.children[0] || base == @root
144
+ puts "\x1b[31mOne of zeus's dependencies changed. Not killing zeus. You may have to restart the server.\x1b[0m"
145
+ return false
146
+ end
147
+ kill_node(base)
148
+ return true
149
+ else
150
+ base.children.dup.each do |node|
151
+ if kill_nodes_with_feature(file, node)
152
+ base.children.delete(node)
153
+ end
154
+ end
155
+ return false
156
+ end
157
+ end
158
+
159
+ end
160
+
161
+ def self.handle_pid_message(data)
162
+ data =~ /(\d+):(\d+)/
163
+ pid, ppid = $1.to_i, $2.to_i
164
+ @@process_tree.process_has_parent(pid, ppid)
165
+ end
166
+
167
+ def self.handle_feature_message(data)
168
+ data =~ /(\d+):(.*)/
169
+ pid, file = $1.to_i, $2
170
+ @@process_tree.process_has_feature(pid, file)
171
+ return if @@file_watchers[file]
172
+ begin
173
+ @@file_watchers[file] = true
174
+ @@queue.watch_file(file.chomp, :write, :extend, :rename, :delete, &method(:notify))
175
+ # rescue Errno::EMFILE
176
+ # exit 1
177
+ rescue Errno::ENOENT
178
+ puts "No file found at #{file.chomp}"
179
+ end
180
+ end
181
+
182
+ class Stage
183
+ attr_reader :pid
184
+ def initialize(name)
185
+ @name = name
186
+ @stages, @actions = [], []
187
+ end
188
+
189
+ def action(&b)
190
+ @actions << b
191
+ end
192
+
193
+ def stage(name, &b)
194
+ @stages << Stage.new(name).tap { |s| s.instance_eval(&b) }
195
+ end
196
+
197
+ def acceptor(name, socket, &b)
198
+ @stages << Acceptor.new(name, socket, &b)
199
+ end
200
+
201
+ # There are a few things we want to accomplish:
202
+ # 1. Running all the actions (each time this stage is killed and restarted)
203
+ # 2. Starting all the substages (and restarting them when necessary)
204
+ # 3. Starting all the acceptors (and restarting them when necessary)
205
+ def run
206
+ @pid = fork {
207
+ $0 = "zeus spawner: #{@name}"
208
+ pid = Process.pid
209
+ $w_pids.puts "#{pid}:#{Process.ppid}\n"
210
+ puts "\x1b[35m[zeus] starting spawner `#{@name}`\x1b[0m"
211
+ trap("INT") {
212
+ puts "\x1b[35m[zeus] killing spawner `#{@name}`\x1b[0m"
213
+ exit 0
214
+ }
215
+
216
+ @actions.each(&:call)
217
+
218
+ $LOADED_FEATURES.each do |f|
219
+ $w_features.puts "#{pid}:#{f}\n"
220
+ end
221
+
222
+ pids = {}
223
+ @stages.each do |stage|
224
+ pids[stage.run] = stage
225
+ end
226
+
227
+ loop do
228
+ begin
229
+ pid = Process.wait
230
+ rescue Errno::ECHILD
231
+ raise "Stage `#{@name}` has no children. All terminal nodes must be acceptors"
232
+ end
233
+ if (status = $?.exitstatus) > 0
234
+ exit status
235
+ else # restart the stage that died.
236
+ stage = pids[pid]
237
+ pids[stage.run] = stage
238
+ end
239
+ end
240
+
241
+ }
242
+ end
243
+
244
+ end
245
+
246
+ class Acceptor
247
+ attr_reader :pid
248
+ def initialize(name, socket, &b)
249
+ @name = name
250
+ @socket = socket
251
+ @action = b
252
+ end
253
+
254
+ def run
255
+ @pid = fork {
256
+ $0 = "zeus acceptor: #{@name}"
257
+ pid = Process.pid
258
+ $w_pids.puts "#{pid}:#{Process.ppid}\n"
259
+ $LOADED_FEATURES.each do |f|
260
+ $w_features.puts "#{pid}:#{f}\n"
261
+ end
262
+ puts "\x1b[35m[zeus] starting acceptor `#{@name}`\x1b[0m"
263
+ trap("INT") {
264
+ puts "\x1b[35m[zeus] killing acceptor `#{@name}`\x1b[0m"
265
+ exit 0
266
+ }
267
+
268
+ File.unlink(@socket) rescue nil
269
+ server = UNIXServer.new(@socket)
270
+ loop do
271
+ ActiveRecord::Base.clear_all_connections! # TODO : refactor
272
+ client = server.accept
273
+ child = fork do
274
+ ActiveRecord::Base.establish_connection # TODO :refactor
275
+ ActiveSupport::DescendantsTracker.clear
276
+ ActiveSupport::Dependencies.clear
277
+
278
+ terminal = client.recv_io
279
+ arguments = JSON.load(client.gets.strip)
280
+
281
+ client << $$ << "\n"
282
+ $stdin.reopen(terminal)
283
+ $stdout.reopen(terminal)
284
+ $stderr.reopen(terminal)
285
+ ARGV.replace(arguments)
286
+
287
+ @action.call
288
+ end
289
+ Process.detach(child)
290
+ client.close
291
+ end
292
+ }
293
+ end
294
+
295
+ end
296
+
297
+ end
298
+ end
data/lib/zeus/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Zeus
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
data/zeus.gemspec CHANGED
@@ -4,8 +4,8 @@ require File.expand_path('../lib/zeus/version', __FILE__)
4
4
  Gem::Specification.new do |gem|
5
5
  gem.authors = ["Burke Libbey"]
6
6
  gem.email = ["burke@libbey.me"]
7
- gem.description = %q{OMGZEUS: Write a gem description}
8
- gem.summary = %q{OMGZEUS: Write a gem summary}
7
+ gem.description = %q{Zeus preloads pretty much everything you'll ever want to use in development.}
8
+ gem.summary = %q{Zeus is an alpha-quality application preloader with terrible documentation.}
9
9
  gem.homepage = ""
10
10
 
11
11
  gem.files = `git ls-files`.split($\)
@@ -14,4 +14,6 @@ Gem::Specification.new do |gem|
14
14
  gem.name = "zeus"
15
15
  gem.require_paths = ["lib"]
16
16
  gem.version = Zeus::VERSION
17
+
18
+ gem.add_dependency "rb-kqueue-burke", "~> 0.1.0"
17
19
  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.0.1
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,12 +9,24 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-07-25 00:00:00.000000000 Z
13
- dependencies: []
14
- description: ! 'OMGZEUS: Write a gem description'
12
+ date: 2012-07-27 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rb-kqueue-burke
16
+ requirement: &70349438039760 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.1.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70349438039760
25
+ description: Zeus preloads pretty much everything you'll ever want to use in development.
15
26
  email:
16
27
  - burke@libbey.me
17
- executables: []
28
+ executables:
29
+ - zeus
18
30
  extensions: []
19
31
  extra_rdoc_files: []
20
32
  files:
@@ -23,7 +35,12 @@ files:
23
35
  - LICENSE
24
36
  - README.md
25
37
  - Rakefile
38
+ - bin/zeus
39
+ - examples/rails.rb
26
40
  - lib/zeus.rb
41
+ - lib/zeus/client.rb
42
+ - lib/zeus/process.rb
43
+ - lib/zeus/server.rb
27
44
  - lib/zeus/version.rb
28
45
  - zeus.gemspec
29
46
  homepage: ''
@@ -49,5 +66,5 @@ rubyforge_project:
49
66
  rubygems_version: 1.8.11
50
67
  signing_key:
51
68
  specification_version: 3
52
- summary: ! 'OMGZEUS: Write a gem summary'
69
+ summary: Zeus is an alpha-quality application preloader with terrible documentation.
53
70
  test_files: []