spring 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. data/.gitignore +17 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +29 -0
  5. data/Rakefile +10 -0
  6. data/bin/spring +5 -0
  7. data/lib/spring.rb +98 -0
  8. data/lib/spring/application.rb +109 -0
  9. data/lib/spring/application_manager.rb +79 -0
  10. data/lib/spring/application_watcher.rb +40 -0
  11. data/lib/spring/commands.rb +97 -0
  12. data/lib/spring/env.rb +30 -0
  13. data/lib/spring/server.rb +59 -0
  14. data/lib/spring/sid.rb +19 -0
  15. data/lib/spring/version.rb +3 -0
  16. data/spring.gemspec +21 -0
  17. data/test/acceptance/app_test.rb +198 -0
  18. data/test/apps/rails-3-2/.gitignore +15 -0
  19. data/test/apps/rails-3-2/Gemfile +5 -0
  20. data/test/apps/rails-3-2/README.rdoc +261 -0
  21. data/test/apps/rails-3-2/Rakefile +7 -0
  22. data/test/apps/rails-3-2/app/assets/images/rails.png +0 -0
  23. data/test/apps/rails-3-2/app/assets/javascripts/application.js +15 -0
  24. data/test/apps/rails-3-2/app/assets/javascripts/posts.js.coffee +3 -0
  25. data/test/apps/rails-3-2/app/assets/stylesheets/application.css +13 -0
  26. data/test/apps/rails-3-2/app/assets/stylesheets/posts.css.scss +3 -0
  27. data/test/apps/rails-3-2/app/assets/stylesheets/scaffolds.css.scss +69 -0
  28. data/test/apps/rails-3-2/app/controllers/application_controller.rb +3 -0
  29. data/test/apps/rails-3-2/app/controllers/posts_controller.rb +83 -0
  30. data/test/apps/rails-3-2/app/helpers/application_helper.rb +2 -0
  31. data/test/apps/rails-3-2/app/helpers/posts_helper.rb +2 -0
  32. data/test/apps/rails-3-2/app/mailers/.gitkeep +0 -0
  33. data/test/apps/rails-3-2/app/models/.gitkeep +0 -0
  34. data/test/apps/rails-3-2/app/models/post.rb +3 -0
  35. data/test/apps/rails-3-2/app/views/layouts/application.html.erb +14 -0
  36. data/test/apps/rails-3-2/app/views/posts/_form.html.erb +21 -0
  37. data/test/apps/rails-3-2/app/views/posts/edit.html.erb +6 -0
  38. data/test/apps/rails-3-2/app/views/posts/index.html.erb +23 -0
  39. data/test/apps/rails-3-2/app/views/posts/new.html.erb +5 -0
  40. data/test/apps/rails-3-2/app/views/posts/show.html.erb +10 -0
  41. data/test/apps/rails-3-2/config.ru +4 -0
  42. data/test/apps/rails-3-2/config/application.rb +62 -0
  43. data/test/apps/rails-3-2/config/boot.rb +6 -0
  44. data/test/apps/rails-3-2/config/database.yml +25 -0
  45. data/test/apps/rails-3-2/config/environment.rb +5 -0
  46. data/test/apps/rails-3-2/config/environments/development.rb +37 -0
  47. data/test/apps/rails-3-2/config/environments/production.rb +67 -0
  48. data/test/apps/rails-3-2/config/environments/test.rb +37 -0
  49. data/test/apps/rails-3-2/config/initializers/backtrace_silencers.rb +7 -0
  50. data/test/apps/rails-3-2/config/initializers/inflections.rb +15 -0
  51. data/test/apps/rails-3-2/config/initializers/mime_types.rb +5 -0
  52. data/test/apps/rails-3-2/config/initializers/secret_token.rb +7 -0
  53. data/test/apps/rails-3-2/config/initializers/session_store.rb +8 -0
  54. data/test/apps/rails-3-2/config/initializers/wrap_parameters.rb +14 -0
  55. data/test/apps/rails-3-2/config/locales/en.yml +5 -0
  56. data/test/apps/rails-3-2/config/routes.rb +60 -0
  57. data/test/apps/rails-3-2/db/migrate/20121109171227_create_posts.rb +9 -0
  58. data/test/apps/rails-3-2/db/schema.rb +22 -0
  59. data/test/apps/rails-3-2/db/seeds.rb +7 -0
  60. data/test/apps/rails-3-2/lib/assets/.gitkeep +0 -0
  61. data/test/apps/rails-3-2/lib/tasks/.gitkeep +0 -0
  62. data/test/apps/rails-3-2/log/.gitkeep +0 -0
  63. data/test/apps/rails-3-2/public/404.html +26 -0
  64. data/test/apps/rails-3-2/public/422.html +26 -0
  65. data/test/apps/rails-3-2/public/500.html +25 -0
  66. data/test/apps/rails-3-2/public/favicon.ico +0 -0
  67. data/test/apps/rails-3-2/public/index.html +241 -0
  68. data/test/apps/rails-3-2/public/robots.txt +5 -0
  69. data/test/apps/rails-3-2/script/rails +6 -0
  70. data/test/apps/rails-3-2/test/fixtures/.gitkeep +0 -0
  71. data/test/apps/rails-3-2/test/fixtures/posts.yml +7 -0
  72. data/test/apps/rails-3-2/test/functional/.gitkeep +0 -0
  73. data/test/apps/rails-3-2/test/functional/posts_controller_test.rb +49 -0
  74. data/test/apps/rails-3-2/test/integration/.gitkeep +0 -0
  75. data/test/apps/rails-3-2/test/performance/browsing_test.rb +12 -0
  76. data/test/apps/rails-3-2/test/test_helper.rb +13 -0
  77. data/test/apps/rails-3-2/test/unit/.gitkeep +0 -0
  78. data/test/apps/rails-3-2/test/unit/helpers/posts_helper_test.rb +4 -0
  79. data/test/apps/rails-3-2/test/unit/post_test.rb +7 -0
  80. data/test/apps/rails-3-2/vendor/assets/javascripts/.gitkeep +0 -0
  81. data/test/apps/rails-3-2/vendor/assets/stylesheets/.gitkeep +0 -0
  82. data/test/apps/rails-3-2/vendor/plugins/.gitkeep +0 -0
  83. data/test/helper.rb +8 -0
  84. data/test/unit/application_watcher_test.rb +54 -0
  85. metadata +215 -0
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in spring.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Jon Leighton
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # Spring
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'spring'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install spring
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.test_files = FileList["test/{acceptance,unit}/*_test.rb"]
7
+ t.verbose = true
8
+ end
9
+
10
+ task default: :test
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
4
+ require 'spring'
5
+ Spring.run(ARGV)
@@ -0,0 +1,98 @@
1
+ require "rbconfig"
2
+ require "socket"
3
+ require "pty"
4
+ require "io/console"
5
+
6
+ require "spring/version"
7
+ require "spring/sid"
8
+ require "spring/env"
9
+ require "spring/commands"
10
+
11
+ class Spring
12
+ SERVER_COMMAND = [
13
+ File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME')),
14
+ "-r", "bundler/setup",
15
+ File.expand_path("../spring/server.rb", __FILE__)
16
+ ]
17
+
18
+ def self.run(args)
19
+ exit new.run(args)
20
+ end
21
+
22
+ attr_reader :env
23
+
24
+ def initialize
25
+ @env = Env.new
26
+ end
27
+
28
+ def server_running?
29
+ env.socket_path.exist?
30
+ end
31
+
32
+ def boot_server
33
+ # Boot the server into the process group of the current session.
34
+ # This will cause it to be automatically killed once the session
35
+ # ends (i.e. when the user closes their terminal).
36
+ Process.spawn(*SERVER_COMMAND, pgroup: SID.pgid)
37
+ sleep 0.1 until server_running?
38
+ end
39
+
40
+ def run(args)
41
+ boot_server unless server_running?
42
+
43
+ socket = UNIXSocket.open(env.socket_name)
44
+ socket.write rails_env_for(args.first)
45
+ socket.close
46
+
47
+ socket = UNIXSocket.open(env.socket_name)
48
+
49
+ socket.send_io STDOUT
50
+ socket.send_io STDERR
51
+ socket.send_io stdin_slave
52
+
53
+ socket.puts args.length
54
+
55
+ args.each do |arg|
56
+ socket.puts arg.length
57
+ socket.write arg
58
+ end
59
+
60
+ # FIXME: receive exit status from server
61
+ socket.read
62
+ true
63
+ rescue Errno::ECONNRESET
64
+ false
65
+ ensure
66
+ socket.close
67
+ end
68
+
69
+ private
70
+
71
+ def rails_env_for(command_name)
72
+ command = Spring.command(command_name)
73
+
74
+ if command.respond_to?(:env)
75
+ command.env
76
+ else
77
+ ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
78
+ end
79
+ end
80
+
81
+ # FIXME: need to make special chars (e.g. arrow keys) work
82
+ def stdin_slave
83
+ master, slave = PTY.open
84
+ master.raw!
85
+
86
+ Thread.new {
87
+ until STDIN.closed?
88
+ # This makes special chars work, but has some weird side-effects that
89
+ # I need to figure out.
90
+ # master.write STDIN.getch
91
+
92
+ master.write STDIN.read(1)
93
+ end
94
+ }
95
+
96
+ slave
97
+ end
98
+ end
@@ -0,0 +1,109 @@
1
+ require "spring/application_watcher"
2
+ require "spring/commands"
3
+ require "set"
4
+
5
+ class Spring
6
+ class << self
7
+ attr_accessor :application_watcher
8
+ end
9
+
10
+ self.application_watcher = ApplicationWatcher.new
11
+
12
+ class Application
13
+ WATCH_INTERVAL = 0.2
14
+
15
+ attr_reader :manager, :watcher
16
+
17
+ def initialize(manager, watcher = Spring.application_watcher)
18
+ @manager = manager
19
+ @watcher = watcher
20
+ @setup = Set.new
21
+
22
+ @stdout = IO.new(STDOUT.fileno)
23
+ @stderr = IO.new(STDERR.fileno)
24
+ @stdin = IO.new(STDIN.fileno)
25
+ end
26
+
27
+ def start
28
+ require "./config/application"
29
+
30
+ # The test environment has config.cache_classes = true set by default.
31
+ # However, we don't want this to prevent us from performing class reloading,
32
+ # so this gets around that.
33
+ Rails::Application.initializer :initialize_dependency_mechanism, group: :all do
34
+ ActiveSupport::Dependencies.mechanism = :load
35
+ end
36
+
37
+ require "./config/environment"
38
+
39
+ watcher.add_files $LOADED_FEATURES
40
+ watcher.add_files ["Gemfile", "Gemfile.lock"].map { |f| "#{Rails.root}/#{f}" }
41
+ watcher.add_globs Rails.application.paths["config/initializers"].map { |p| "#{Rails.root}/#{p}/*.rb" }
42
+
43
+ run
44
+ end
45
+
46
+ def run
47
+ loop do
48
+ watch_application
49
+
50
+ client = manager.recv_io
51
+ client.autoclose = false # prevent GC closing the FD
52
+
53
+ serve UNIXSocket.for_fd(client.fileno)
54
+ end
55
+ end
56
+
57
+ def watch_application
58
+ until IO.select([manager], [], [], WATCH_INTERVAL)
59
+ exit if watcher.stale?
60
+ end
61
+ end
62
+
63
+ def serve(client)
64
+ redirect_output(client) do
65
+ args_length = client.gets.to_i
66
+ args = args_length.times.map { client.read(client.gets.to_i) }
67
+ command = Spring.command(args.shift)
68
+
69
+ setup command
70
+
71
+ ActionDispatch::Reloader.cleanup!
72
+ ActionDispatch::Reloader.prepare!
73
+
74
+ Process.wait(fork { command.call(args) })
75
+ end
76
+ ensure
77
+ client.puts
78
+ client.close
79
+ end
80
+
81
+ # The command might need to require some files in the
82
+ # main process so that they are cached. For example a test command wants to
83
+ # load the helper file once and have it cached.
84
+ #
85
+ # FIXME: The watcher.add_files will reset the watcher, which may mean that
86
+ # previous changes to already-loaded files are missed.
87
+ def setup(command)
88
+ return if @setup.include?(command.class)
89
+ @setup << command.class
90
+
91
+ if command.respond_to?(:setup)
92
+ command.setup
93
+ watcher.add_files $LOADED_FEATURES # loaded features may have changed
94
+ end
95
+ end
96
+
97
+ def redirect_output(socket)
98
+ STDOUT.reopen socket.recv_io
99
+ STDERR.reopen socket.recv_io
100
+ STDIN.reopen socket.recv_io
101
+
102
+ yield
103
+ ensure
104
+ STDOUT.reopen @stdout
105
+ STDERR.reopen @stderr
106
+ STDIN.reopen @stdin
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,79 @@
1
+ require "socket"
2
+ require "spring/application"
3
+ require "mutex_m"
4
+
5
+ class Spring
6
+ class ApplicationManager
7
+ include Mutex_m
8
+
9
+ attr_reader :pid, :child, :env
10
+
11
+ def initialize(env)
12
+ super()
13
+ @env = env
14
+ end
15
+
16
+ def start
17
+ start_child
18
+ start_wait_thread
19
+ end
20
+
21
+ def restart
22
+ # Restarting is a background operation. If it fails, we don't want
23
+ # any terminal output. The user will see the output when they next
24
+ # try to run a command.
25
+ start_child(true)
26
+ end
27
+
28
+ def alive?
29
+ @pid
30
+ end
31
+
32
+ def run(client)
33
+ @client = client
34
+
35
+ synchronize do
36
+ start unless alive?
37
+
38
+ begin
39
+ child.send_io @client
40
+ rescue Errno::EPIPE
41
+ # EPIPE indicates child has died but has not been collected by the wait thread yet
42
+ start
43
+ child.send_io @client
44
+ end
45
+ end
46
+ ensure
47
+ @client.close
48
+ @client = nil
49
+ end
50
+
51
+ private
52
+
53
+ def start_child(silence = false)
54
+ @child, child_socket = UNIXSocket.pair
55
+ @pid = fork {
56
+ [STDOUT, STDERR].each { |s| s.reopen('/dev/null', 'w') } if silence
57
+ @client.close if @client
58
+ ENV['RAILS_ENV'] = ENV['RACK_ENV'] = env
59
+ Application.new(child_socket).start
60
+ }
61
+ child_socket.close
62
+ end
63
+
64
+ def start_wait_thread
65
+ @wait_thread = Thread.new {
66
+ Thread.current.abort_on_exception = true
67
+
68
+ while alive?
69
+ _, status = Process.wait2(pid)
70
+ @pid = nil
71
+
72
+ # In the forked child, this will block forever, so we won't
73
+ # return to the next iteration of the loop.
74
+ synchronize { restart if !alive? && status.success? }
75
+ end
76
+ }
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,40 @@
1
+ class Spring
2
+ class ApplicationWatcher
3
+ attr_reader :mtime, :files, :globs
4
+
5
+ def initialize
6
+ @files = []
7
+ @globs = []
8
+ @mtime = nil
9
+ end
10
+
11
+ def add_files(new_files)
12
+ files.concat new_files.select { |f| File.exist?(f) }
13
+ files.uniq!
14
+ reset
15
+ end
16
+
17
+ def add_globs(new_globs)
18
+ globs.concat new_globs
19
+ reset
20
+ end
21
+
22
+ def reset
23
+ @mtime = compute_mtime
24
+ end
25
+
26
+ def stale?
27
+ mtime < compute_mtime
28
+ end
29
+
30
+ private
31
+
32
+ def compute_mtime
33
+ expanded_files.map { |f| File.mtime(f).to_f }.max || 0
34
+ end
35
+
36
+ def expanded_files
37
+ files + Dir["{#{globs.join(",")}}"]
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,97 @@
1
+ # TODO: Need a way for applications to add their own commands.
2
+
3
+ class Spring
4
+ @commands = {}
5
+
6
+ class << self
7
+ attr_reader :commands
8
+ end
9
+
10
+ def self.register_command(name, klass)
11
+ commands[name] = klass.new
12
+ end
13
+
14
+ def self.command(name)
15
+ commands.fetch name
16
+ end
17
+
18
+ module Commands
19
+ class Test
20
+ def env
21
+ "test"
22
+ end
23
+
24
+ def setup
25
+ $LOAD_PATH.unshift "test"
26
+ require "test_helper"
27
+ end
28
+
29
+ def call(args)
30
+ ARGV.replace args
31
+ require File.expand_path(args.first)
32
+ end
33
+ end
34
+ Spring.register_command "test", Test
35
+
36
+ class RSpec
37
+ def env
38
+ "test"
39
+ end
40
+
41
+ def setup
42
+ $LOAD_PATH.unshift "spec"
43
+ require "spec_helper"
44
+ end
45
+
46
+ def call(args)
47
+ ::RSpec::Core::Runner.run(args)
48
+ end
49
+ end
50
+ Spring.register_command "rspec", RSpec
51
+
52
+ class Rake
53
+ def setup
54
+ require "rake"
55
+ end
56
+
57
+ def call(args)
58
+ ARGV.replace args
59
+ ::Rake.application.run
60
+ end
61
+ end
62
+ Spring.register_command "rake", Rake
63
+
64
+ class Console
65
+ def setup
66
+ require "rails/commands/console"
67
+ end
68
+
69
+ def call(args)
70
+ ARGV.replace args
71
+ ::Rails::Console.start(::Rails.application)
72
+ end
73
+ end
74
+ Spring.register_command "console", Console
75
+
76
+ class Generate
77
+ def setup
78
+ Rails.application.load_generators
79
+ end
80
+
81
+ def call(args)
82
+ ARGV.replace args
83
+ require "rails/commands/generate"
84
+ end
85
+ end
86
+ Spring.register_command "generate", Generate
87
+
88
+ class Runner
89
+ def call(args)
90
+ Object.const_set(:APP_PATH, Rails.root.join('config/application'))
91
+ ARGV.replace args
92
+ require "rails/commands/runner"
93
+ end
94
+ end
95
+ Spring.register_command "runner", Runner
96
+ end
97
+ end