zeus 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []