Class: Spring::Application

Inherits:
Object
  • Object
show all
Defined in:
lib/spring/application.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(manager, original_env) ⇒ Application

Returns a new instance of Application.



8
9
10
11
12
13
14
15
16
17
# File 'lib/spring/application.rb', line 8

def initialize(manager, original_env)
  @manager      = manager
  @original_env = original_env
  @spring_env   = Env.new
  @mutex        = Mutex.new
  @waiting      = 0
  @preloaded    = false
  @state        = :initialized
  @interrupt    = IO.pipe
end

Instance Attribute Details

#managerObject (readonly)

Returns the value of attribute manager.



6
7
8
# File 'lib/spring/application.rb', line 6

def manager
  @manager
end

#original_envObject (readonly)

Returns the value of attribute original_env.



6
7
8
# File 'lib/spring/application.rb', line 6

def original_env
  @original_env
end

#spring_envObject (readonly)

Returns the value of attribute spring_env.



6
7
8
# File 'lib/spring/application.rb', line 6

def spring_env
  @spring_env
end

#watcherObject (readonly)

Returns the value of attribute watcher.



6
7
8
# File 'lib/spring/application.rb', line 6

def watcher
  @watcher
end

Instance Method Details

#app_envObject



30
31
32
# File 'lib/spring/application.rb', line 30

def app_env
  ENV['RAILS_ENV']
end

#app_nameObject



34
35
36
# File 'lib/spring/application.rb', line 34

def app_name
  spring_env.app_name
end

#connect_databaseObject



249
250
251
# File 'lib/spring/application.rb', line 249

def connect_database
  ActiveRecord::Base.establish_connection if defined?(ActiveRecord::Base)
end

#disconnect_databaseObject



245
246
247
# File 'lib/spring/application.rb', line 245

def disconnect_database
  ActiveRecord::Base.remove_connection if defined?(ActiveRecord::Base)
end

#exitObject



212
213
214
215
216
217
# File 'lib/spring/application.rb', line 212

def exit
  state :exiting
  manager.shutdown(:RDWR)
  exit_if_finished
  sleep
end

#exit_if_finishedObject



219
220
221
222
223
# File 'lib/spring/application.rb', line 219

def exit_if_finished
  @mutex.synchronize {
    Kernel.exit if exiting? && @waiting == 0
  }
end

#exiting?Boolean

Returns:

  • (Boolean)


50
51
52
# File 'lib/spring/application.rb', line 50

def exiting?
  @state == :exiting
end

#initialized?Boolean

Returns:

  • (Boolean)


62
63
64
# File 'lib/spring/application.rb', line 62

def initialized?
  @state == :initialized
end

#invoke_after_fork_callbacksObject



234
235
236
237
238
# File 'lib/spring/application.rb', line 234

def invoke_after_fork_callbacks
  Spring.after_fork_callbacks.each do |callback|
    callback.call
  end
end

#loaded_application_featuresObject



240
241
242
243
# File 'lib/spring/application.rb', line 240

def loaded_application_features
  root = Spring.application_root_path.to_s
  $LOADED_FEATURES.select { |f| f.start_with?(root) }
end

#log(message) ⇒ Object



38
39
40
# File 'lib/spring/application.rb', line 38

def log(message)
  spring_env.log "[application:#{app_env}] #{message}"
end

#preloadObject



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/spring/application.rb', line 72

def preload
  log "preloading app"

  begin
    require "spring/commands"
  ensure
    start_watcher
  end

  require Spring.application_root_path.join("config", "application")

  # config/environments/test.rb will have config.cache_classes = true. However
  # we want it to be false so that we can reload files. This is a hack to
  # override the effect of config.cache_classes = true. We can then actually
  # set config.cache_classes = false after loading the environment.
  Rails::Application.initializer :initialize_dependency_mechanism, group: :all do
    ActiveSupport::Dependencies.mechanism = :load
  end

  require Spring.application_root_path.join("config", "environment")

  Rails.application.config.cache_classes = false
  disconnect_database

  @preloaded = :success
rescue Exception => e
  @preloaded = :failure
  watcher.add e.backtrace.map { |line| line.match(/^(.*)\:\d+\:in /)[1] }
  raise e unless initialized?
ensure
  watcher.add loaded_application_features
  watcher.add Spring.gemfile, "#{Spring.gemfile}.lock"

  if defined?(Rails)
    watcher.add Rails.application.paths["config/initializers"]
    watcher.add Rails.application.paths["config/database"]
  end
end

#preload_failed?Boolean

Returns:

  • (Boolean)


46
47
48
# File 'lib/spring/application.rb', line 46

def preload_failed?
  @preloaded == :failure
end

#preloaded?Boolean

Returns:

  • (Boolean)


42
43
44
# File 'lib/spring/application.rb', line 42

def preloaded?
  @preloaded
end


271
272
273
274
275
# File 'lib/spring/application.rb', line 271

def print_exception(stream, error)
  first, rest = error.backtrace.first, error.backtrace.drop(1)
  stream.puts("#{first}: #{error} (#{error.class})")
  rest.each { |line| stream.puts("\tfrom #{line}") }
end

#runObject



111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/spring/application.rb', line 111

def run
  state :running
  manager.puts

  loop do
    IO.select [manager, @interrupt.first]

    if terminating? || watcher_stale? || preload_failed?
      exit
    else
      serve manager.recv_io(UNIXSocket)
    end
  end
end

#serve(client) ⇒ Object



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/spring/application.rb', line 126

def serve(client)
  log "got client"
  manager.puts

  stdout, stderr, stdin = streams = 3.times.map { client.recv_io }
  [STDOUT, STDERR].zip([stdout, stderr]).each { |a, b| a.reopen(b) }

  preload unless preloaded?

  args, env = JSON.load(client.read(client.gets.to_i)).values_at("args", "env")
  command   = Spring.command(args.shift)

  connect_database
  setup command

  if Rails.application.reloaders.any?(&:updated?)
    ActionDispatch::Reloader.cleanup!
    ActionDispatch::Reloader.prepare!
  end

  pid = fork {
    Process.setsid
    STDIN.reopen(stdin)
    IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") }
    trap("TERM", "DEFAULT")

    ARGV.replace(args)
    $0 = command.process_title

    # Delete all env vars which are unchanged from before spring started
    original_env.each { |k, v| ENV.delete k if ENV[k] == v }

    # Load in the current env vars, except those which *were* changed when spring started
    env.each { |k, v| ENV[k] ||= v }

    # requiring is faster, and we don't need constant reloading in this process
    ActiveSupport::Dependencies.mechanism = :require
    Rails.application.config.cache_classes = true

    connect_database
    srand

    invoke_after_fork_callbacks
    shush_backtraces

    command.call
  }

  disconnect_database
  [STDOUT, STDERR].each { |stream| stream.reopen(spring_env.log_file) }

  log "forked #{pid}"
  manager.puts pid

  # Wait in a separate thread so we can run multiple commands at once
  Thread.new {
    @mutex.synchronize { @waiting += 1 }

    _, status = Process.wait2 pid
    log "#{pid} exited with #{status.exitstatus}"

    streams.each(&:close)
    client.puts(status.exitstatus)
    client.close

    @mutex.synchronize { @waiting -= 1 }
    exit_if_finished
  }

rescue Exception => e
  log "exception: #{e}"
  manager.puts unless pid

  if streams && !e.is_a?(SystemExit)
    print_exception(stderr, e)
    streams.each(&:close)
  end

  client.puts(1) if pid
  client.close
end

#setup(command) ⇒ Object

The command might need to require some files in the main process so that they are cached. For example a test command wants to load the helper file once and have it cached.



228
229
230
231
232
# File 'lib/spring/application.rb', line 228

def setup(command)
  if command.setup
    watcher.add loaded_application_features # loaded features may have changed
  end
end

#shush_backtracesObject

This feels very naughty



254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/spring/application.rb', line 254

def shush_backtraces
  Kernel.module_eval do
    old_raise = Kernel.method(:raise)
    remove_method :raise
    define_method :raise do |*args|
      begin
        old_raise.call(*args)
      ensure
        if $!
          lib = File.expand_path("..", __FILE__)
          $!.backtrace.reject! { |line| line.start_with?(lib) }
        end
      end
    end
  end
end

#start_watcherObject



66
67
68
69
70
# File 'lib/spring/application.rb', line 66

def start_watcher
  @watcher = Spring.watcher
  @watcher.on_stale { state! :watcher_stale }
  @watcher.start
end

#state(val) ⇒ Object



19
20
21
22
23
# File 'lib/spring/application.rb', line 19

def state(val)
  return if exiting?
  log "#{@state} -> #{val}"
  @state = val
end

#state!(val) ⇒ Object



25
26
27
28
# File 'lib/spring/application.rb', line 25

def state!(val)
  state val
  @interrupt.last.write "."
end

#terminateObject



208
209
210
# File 'lib/spring/application.rb', line 208

def terminate
  state! :terminating
end

#terminating?Boolean

Returns:

  • (Boolean)


54
55
56
# File 'lib/spring/application.rb', line 54

def terminating?
  @state == :terminating
end

#watcher_stale?Boolean

Returns:

  • (Boolean)


58
59
60
# File 'lib/spring/application.rb', line 58

def watcher_stale?
  @state == :watcher_stale
end