Skip to content

Commit

Permalink
Allow a new server Railtie block:
Browse files Browse the repository at this point in the history
- This is similar to other railties blocks (such as `console`,
  `tasks` ...). The goal of this block is to allow the application
  or a railtie to load code after the server start.

  The use case can be to fire the webpack or react server in
  development or start some job worker like sidekiq or resque.

  Right now, all these tasks needs to be done in a separate
  shell and gem maintainer needs to add documentation on
  how to run their libraries if another program needs to run
  next to the Rails server.

  This feature can be used like this:

  ```ruby
    class SuperRailtie < Rails::Railtie
      server do
        WebpackServer.run
      end
    end
  ```
  • Loading branch information
Edouard-chin authored and rafaelfranca committed Nov 2, 2020
1 parent 4dd61b4 commit 868866c
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 1 deletion.
18 changes: 17 additions & 1 deletion railties/CHANGELOG.md
@@ -1,3 +1,20 @@
* Added `Railtie#server` hook called when Rails starts a server.
This is useful in case your application or a library needs to run
another process next to the Rails server. This is quite common in development
for instance to run the Webpack or the React server.

It can be used like this:

```ruby
class MyRailtie < Rails::Railtie
server do
WebpackServer.run
end
end
```

*Edouard Chin*

* Remove deprecated `rake dev:cache` tasks.

*Rafael Mendonça França*
Expand Down Expand Up @@ -94,7 +111,6 @@

*Eileen M. Uchitelle*, *John Crepezzi*


* Accept params from url to prepopulate the Inbound Emails form in Rails conductor.

*Chris Oliver*
Expand Down
11 changes: 11 additions & 0 deletions railties/lib/rails/application.rb
Expand Up @@ -322,6 +322,12 @@ def generators(&blk)
self.class.generators(&blk)
end

# Sends any server called in the instance of a new application up
# to the +server+ method defined in Rails::Railtie.
def server(&blk)
self.class.server(&blk)
end

# Sends the +isolate_namespace+ method up to the class method.
def isolate_namespace(mod)
self.class.isolate_namespace(mod)
Expand Down Expand Up @@ -536,6 +542,11 @@ def run_console_blocks(app) #:nodoc:
super
end

def run_server_blocks(app) #:nodoc:
railties.each { |r| r.run_server_blocks(app) }
super
end

# Returns the ordered railties for this application considering railties_order.
def ordered_railties #:nodoc:
@ordered_railties ||= begin
Expand Down
7 changes: 7 additions & 0 deletions railties/lib/rails/engine.rb
Expand Up @@ -470,6 +470,13 @@ def load_generators(app = self)
self
end

# Invoke the server registered hooks.
# Check <tt>Rails::Railtie.server</tt> for more info.
def load_server(app = self)
run_server_blocks(app)
self
end

def eager_load!
# Already done by Zeitwerk::Loader.eager_load_all. We need this guard to
# easily provide a compatible API for both zeitwerk and classic modes.
Expand Down
1 change: 1 addition & 0 deletions railties/lib/rails/generators/rails/app/app_generator.rb
Expand Up @@ -143,6 +143,7 @@ def config_when_updating
@config_target_version = Rails.application.config.loaded_config_version || "5.0"

config
configru

unless cookie_serializer_config_exist
gsub_file "config/initializers/cookies_serializer.rb", /json(?!,)/, "marshal"
Expand Down
Expand Up @@ -3,3 +3,4 @@
require_relative "config/environment"

run Rails.application
Rails.application.load_server
25 changes: 25 additions & 0 deletions railties/lib/rails/railtie.rb
Expand Up @@ -108,6 +108,23 @@ module Rails
# Since filenames on the load path are shared across gems, be sure that files you load
# through a railtie have unique names.
#
# == Run another program when the Rails server starts
#
# In development, it's very usual to have to run another process next to the Rails Server. In example
# you might want to start the Webpack or React server. Or maybe you need to run your job scheduler process
# like Sidekiq. This is usually done by opening a new shell and running the program from here.
#
# Rails allow you to specify a +server+ block which will get called when a Rails server starts.
# This way, your users don't need to remember to have to open a new shell and run another program, making
# this less confusing for everyone.
# It can be used like this:
#
# class MyRailtie < Rails::Railtie
# server do
# WebpackServer.start
# end
# end
#
# == Application and Engine
#
# An engine is nothing more than a railtie with some initializers already set. And since
Expand Down Expand Up @@ -152,6 +169,10 @@ def generators(&blk)
register_block_for(:generators, &blk)
end

def server(&blk)
register_block_for(:server, &blk)
end

def abstract_railtie?
ABSTRACT_RAILTIES.include?(name)
end
Expand Down Expand Up @@ -246,6 +267,10 @@ def run_tasks_blocks(app) #:nodoc:
each_registered_block(:rake_tasks) { |block| instance_exec(app, &block) }
end

def run_server_blocks(app) #:nodoc:
each_registered_block(:server) { |block| block.call(app) }
end

private
# run `&block` in every registered block in `#register_block_for`
def each_registered_block(type, &block)
Expand Down
26 changes: 26 additions & 0 deletions railties/test/application/server_test.rb
Expand Up @@ -42,6 +42,32 @@ def teardown
end
end

test "run +server+ blocks after the server starts" do
skip "PTY unavailable" unless available_pty?

File.open("#{app_path}/config/boot.rb", "w") do |f|
f.puts "ENV['BUNDLE_GEMFILE'] = '#{Bundler.default_gemfile}'"
f.puts 'require "bundler/setup"'
end

add_to_config(<<~CODE)
server do
puts 'Hello world'
end
CODE

primary, replica = PTY.open
pid = nil

Bundler.with_original_env do
pid = Process.spawn("bin/rails server -b localhost", chdir: app_path, in: replica, out: primary, err: replica)
assert_output("Hello world", primary)
assert_output("Listening", primary)
ensure
kill(pid) if pid
end
end

private
def kill(pid)
Process.kill("TERM", pid)
Expand Down
16 changes: 16 additions & 0 deletions railties/test/railties/railtie_test.rb
Expand Up @@ -170,6 +170,22 @@ class MyTie < Rails::Railtie
assert $ran_block
end

test "server block is executed when MyApp.load_server is called" do
$ran_block = false

class MyTie < Rails::Railtie
server do
$ran_block = true
end
end

require "#{app_path}/config/environment"

assert_not $ran_block
Rails.application.load_server
assert $ran_block
end

test "runner block is executed when MyApp.load_runner is called" do
$ran_block = false

Expand Down

0 comments on commit 868866c

Please sign in to comment.