spring 0.0.1

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