From 868866c1fd7f6c79fd48d90febb1fd02a83380e1 Mon Sep 17 00:00:00 2001 From: Edouard Chin Date: Thu, 30 Jul 2020 14:15:18 +0200 Subject: [PATCH] Allow a new `server` Railtie block: - 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 ``` --- railties/CHANGELOG.md | 18 ++++++++++++- railties/lib/rails/application.rb | 11 ++++++++ railties/lib/rails/engine.rb | 7 +++++ .../generators/rails/app/app_generator.rb | 1 + .../rails/app/templates/config.ru.tt | 1 + railties/lib/rails/railtie.rb | 25 ++++++++++++++++++ railties/test/application/server_test.rb | 26 +++++++++++++++++++ railties/test/railties/railtie_test.rb | 16 ++++++++++++ 8 files changed, 104 insertions(+), 1 deletion(-) diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 419911dce6..1839372ebe 100644 --- a/railties/CHANGELOG.md +++ b/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* @@ -94,7 +111,6 @@ *Eileen M. Uchitelle*, *John Crepezzi* - * Accept params from url to prepopulate the Inbound Emails form in Rails conductor. *Chris Oliver* diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 8738e4b93e..48b812403b 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -322,6 +322,12 @@ module Rails 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) @@ -536,6 +542,11 @@ module Rails 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 diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index 52d1943fea..ed96b1c8b4 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -470,6 +470,13 @@ module Rails self end + # Invoke the server registered hooks. + # Check Rails::Railtie.server 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. diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 6c99d54314..8101d518b3 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -143,6 +143,7 @@ module Rails @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" diff --git a/railties/lib/rails/generators/rails/app/templates/config.ru.tt b/railties/lib/rails/generators/rails/app/templates/config.ru.tt index 441e6ff0c3..4a3c09a688 100644 --- a/railties/lib/rails/generators/rails/app/templates/config.ru.tt +++ b/railties/lib/rails/generators/rails/app/templates/config.ru.tt @@ -3,3 +3,4 @@ require_relative "config/environment" run Rails.application +Rails.application.load_server diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb index baf1c16747..1e74b22cfb 100644 --- a/railties/lib/rails/railtie.rb +++ b/railties/lib/rails/railtie.rb @@ -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 @@ -152,6 +169,10 @@ module Rails register_block_for(:generators, &blk) end + def server(&blk) + register_block_for(:server, &blk) + end + def abstract_railtie? ABSTRACT_RAILTIES.include?(name) end @@ -246,6 +267,10 @@ module Rails 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) diff --git a/railties/test/application/server_test.rb b/railties/test/application/server_test.rb index 1aa7003906..9813cc62c4 100644 --- a/railties/test/application/server_test.rb +++ b/railties/test/application/server_test.rb @@ -42,6 +42,32 @@ module ApplicationTests 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) diff --git a/railties/test/railties/railtie_test.rb b/railties/test/railties/railtie_test.rb index b9725ca0ad..845f500ce3 100644 --- a/railties/test/railties/railtie_test.rb +++ b/railties/test/railties/railtie_test.rb @@ -170,6 +170,22 @@ module RailtiesTest 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