From b32547e16e187e7b80b4862afeeb910f93879af6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A9=20Dupuis?= Date: Tue, 24 Sep 2024 15:42:18 -0700 Subject: [PATCH] Add enhanced default bin/dev Fixes #52459 The current default `bin/dev` execs the Rails server directly and doesn't support Procfiles. Therefore, gems like `jsbundling-rails`, `tailwindcss-rails`, and soon `solid_queue` need to overwrite the `bin/dev` script with their own version. With this new version, gems can skip overwriting the `bin/dev` script and add to the Procfile instead. It spawns Foreman in the background to allow breakpoints in the Rails process without Foreman eating the inputs or interleaving its output. --- .../commands/devserver/devserver_command.rb | 92 +++++++++++++++++++ .../generators/rails/app/templates/bin/dev.tt | 37 +++++++- 2 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 railties/lib/rails/commands/devserver/devserver_command.rb diff --git a/railties/lib/rails/commands/devserver/devserver_command.rb b/railties/lib/rails/commands/devserver/devserver_command.rb new file mode 100644 index 0000000000000..1d445ee6e0787 --- /dev/null +++ b/railties/lib/rails/commands/devserver/devserver_command.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +module Rails + module Command + class DevserverCommand < Base # :nodoc: + DEFAULT_PORT = 3000 + PROCFILE = "Procfile.dev" + + class_option :verbose, aliases: "-v", type: :boolean, default: false, + desc: "Show all process outputs." + option :port, aliases: "-p", type: :numeric, + desc: "Run Rails on the specified port - defaults to 3000.", banner: :port + + desc "devserver", "Start everything" + def perform + exec_rails unless procfile_exists? + + Signal.trap("INT") { } + + install_foreman + + begin + start_foreman + start_rails + ensure + stop + end + end + + private + attr_reader :foreman_pgid, :pid + + def start_foreman + @foreman_pgid = + Process.spawn( + env, + *%W(foreman start -f Procfile.dev -m all=1,web=0 --env /dev/null), + in: "/dev/null", + out: (verbose ? STDOUT : "/dev/null"), + err: (verbose ? STDOUT : "/dev/null"), + pgroup: true, + ).then(&Process.method(:getpgid)) + end + + def install_foreman + if system(env, *%w(gem list --no-installed --exact --silent foreman)) + puts "Installing foreman..." + system(env, *%w(gem install foreman)) + end + end + + def start_rails + @pid = Process.spawn(*%W(bundle exec rails server --port=#{port})) + Process.wait(pid) + @pid = false + end + + def exec_rails + Process.exec(env, *%W(bundle exec rails server --port=#{port})) + end + + def stop + Process.kill("INT", -foreman_pgid) if foreman_pgid + Process.wait(-foreman_pgid) if foreman_pgid + + begin + Process.kill("KILL", pid) if pid + rescue Errno::ESRCH + end + end + + def procfile_exists? + File.exist?(Rails::Command.application_root.join(PROCFILE)) + end + + def env + { + "BUNDLER_SETUP" => nil, + "RUBYOPT" => nil, + } + end + + def verbose + options[:verbose] + end + + def port + options[:port] || ENV.fetch("PORT", DEFAULT_PORT).to_i + end + end + end +end diff --git a/railties/lib/rails/generators/rails/app/templates/bin/dev.tt b/railties/lib/rails/generators/rails/app/templates/bin/dev.tt index 89df11bc7ead3..1f32ec200b1de 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/dev.tt +++ b/railties/lib/rails/generators/rails/app/templates/bin/dev.tt @@ -1 +1,36 @@ -exec "./bin/rails", "server", *ARGV +# !/usr/bin/env ruby + +ENV["PORT"] ||= "3000" +VERBOSE = ARGV.include?("--verbose") || ARGV.include?("-v") || ENV["VERBOSE"] + +PROCFILE = 'Procfile.dev' +Process.exec(*%w[bundle exec rails server], *ARGV) unless File.exist?(PROCFILE) + +Signal.trap("INT") { } + +if system(*%w[gem list --no-installed --exact --silent foreman]) + puts "Installing foreman..." + system(*%w[gem install foreman]) +end + +begin + foreman_pid = Process.spawn( + *%w[foreman start -f Procfile.dev -m all=1,web=0 --env /dev/null], + in: "/dev/null", + out: (VERBOSE ? STDOUT : "/dev/null"), + err: (VERBOSE ? STDOUT : "/dev/null"), + pgroup: true, + ) + foreman_pgid = Process.getpgid(foreman_pid) + pid = Process.spawn(*%w[bundle exec rails server]) + + Process.wait(pid) + pid = nil +ensure + Process.kill("INT", -foreman_pgid) + Process.wait(-foreman_pgid) + begin + Process.kill("KILL", pid) if pid + rescue Errno::ESRCH + end +end