-
Notifications
You must be signed in to change notification settings - Fork 21.8k
Introduce explicit rails server handler option #32058
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
r? @schneems (@rails-bot has picked a reviewer for you, use r? to override) |
The previous error was:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 to adding the argument validation. But I don't understand why it follows that we should turn the server positional arg into an option?
|
||
msg = "Could not find handler '#{handler}'. ".dup | ||
msg << "Maybe you meant #{suggestions.first} or #{suggestions.second}?\n" | ||
msg << "Run `rails server --help` for more options." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're on Ruby 2.3 now, so lets use the squiggly heredoc <<~
HANDLERS = %w[cgi fastcgi webrick lsws scgi thin puma unicorn] | ||
|
||
def self.message(handler) | ||
suggestions = HANDLERS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Think this might be clearer as:
suggestions = HANDLERS.sort_by { |h| levenshstein_distance(handler, h) }.map(&:inspect)
Don't think the first(2)
makes sense since we use first
and second
below — there's only 8 things in the array, we aren't saving anything with it anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only subtle difference with .map(&:inspect)
is that the message contains double quotes instead of single ones.
|
||
# Hard-coding a bunch of handlers here as we don't have a public way of | ||
# querying them from the Rack::Handler registry. | ||
HANDLERS = %w[cgi fastcgi webrick lsws scgi thin puma unicorn] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we might prefer %w( … )
@@ -122,6 +148,8 @@ class ServerCommand < Base # :nodoc: | |||
desc: "Runs server as a Daemon." | |||
class_option :environment, aliases: "-e", type: :string, | |||
desc: "Specifies the environment to run this server under (development/test/production).", banner: :name | |||
class_option :handler, aliases: "-x", type: :string, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know Rack uses handler
, but it reads a tad too generic here for me — especially if the shorthand is -x
. How about -r
for runner? E.g. the thing to run the rails server.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
--using
? Runner seems particularly problematic given rails runner
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@matthewd yeah, definitely thought that -r
is on sketchy ground because of that clash. I like --using
. 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
--using
sounds good to me too.
|
||
def test_handler_mistype_in_positional_argument | ||
run_command(["tink"]) | ||
assert_match(/Could not find handler 'tink'. Maybe you meant 'thin' or 'cgi'/, output) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't think attr_reader :output
and @output
makes sense. We can just do:
assert_match(/Could not find handler 'tink'. Maybe you meant 'thin' or 'cgi'/, run_command(["tink"]))
Then it'll match our test/application/test_runner_test.rb tests too.
|
||
def test_handler_mistype | ||
run_command(["-x", "tin"]) | ||
assert_match(/Could not find handler 'tin'. Maybe you meant 'thin' or 'cgi'/, output) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How deterministic is it that cgi
will be the other option here? I don't think we'd want intermittent test failures here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The handlers list is hard-coded, as we don't have a clean way to query the handlers from rack/lib/rack/handler.rb. If the order of the HANDLERS
doesn't change often, the test should be stable.
@kaspth we removed the positional parameter for (Specifically, given that we can only load a handler that's available in the Gemfile, and that we automatically load the first handler that's available in the Gemfile, it seems pretty rare to me for people to have cause to name a handler at all.) |
.map! { |s| "'#{s}'" } | ||
|
||
msg = "Could not find handler '#{handler}'. ".dup | ||
msg << "Maybe you meant #{suggestions.first} or #{suggestions.second}?\n" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAICS this could end up suggesting exactly what they typed, which seems not-good. If handler
is in HANDLERS
, maybe we should instead suggest that they're missing a Gemfile entry.
@matthewd sure, but that was for the environment, no? I thought Anyway, you're right that passing it manually is probably a minority case, so lets go with the deprecation — and |
ccc1d5b
to
c5b01cb
Compare
I have changed the option to |
c5b01cb
to
a80f003
Compare
This is probably for another time, but… I just realized that now we're on 2.3+ we could leverage the unless HANDLERS.include?(options[:using])
DidYouMean::SpellChecker.new(HANDLERS).correct(options[:using])
end
We could also remove Rails' Levenshtein distance method from other places. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Getting real close!
Could not find server "#{handler}". Maybe you meant #{suggestions.first} or #{suggestions.second}? | ||
Run `rails server --help` for more options. | ||
MSG | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doing argv validation seems like it should a concern of the command and not the server itself. Both include Rails::Command::Behavior
and rails server --help
seem to allude as much.
Could we move it to the command if we do this?
capturing_missing_handler do
server.start
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried that as well – it's still messy because levenshtein_distance
is a private class method:
def suggest_rack_server(handler)
if handler.in?(HANDLERS)
<<~MSG
Could not load server "#{handler}". Maybe you need the add it to the Gemfile?
gem "#{handler}"
Run `rails server --help` for more options.
MSG
else
suggestions = HANDLERS.sort_by { |h| self.class.send(:levenshtein_distance, handler, h) }.map(&:inspect)
<<~MSG
Could not find server "#{handler}". Maybe you meant #{suggestions.first} or #{suggestions.second}?
Run `rails server --help` for more options.
MSG
end
end
# The '-h' option calls exit before @options is set. | ||
# If we call 'options' with it unset, we get double help banners. | ||
puts "Exiting" unless @options && options[:daemonize] | ||
unless missing_handler |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm guessing the stopgap to inlining the HandlerSuggestion module in the command is this unless
. But I don't fully understand why we need it? Isn't it fine for the server to say Exiting
in the off case that a handler is missing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, the stopgap is the levenstein_distance
method being private and defined in the Rails::Command::Behavior
module which carries command specific helpers. Decided to at least isolate it. I'd love to revisit the levenshtein_distance
usage or swap it for the algo in DidYouMean
, but I'd like to do it in a follow-up change, as it will have to touch the generator commands as well.
The guard here is for my personal aesthetic reasons. 😅 This is the message with the trailing Exiting
:
Could not load server "unicorn". Maybe you need the add it to the Gemfile?
gem "unicorn"
Run `rails server --help` for more options.
Exiting
The Exiting
line seems redundant to me, especially with "Run rails server --help for more options."
as a final suggestion. Maybe the user mistyped an option and triggered this?
Can an option be to complicate the existing guard a bit and move the missing_handler
check there?
# ...
ensure
# The '-h' option calls exit before @options is set.
# If we call 'options' with it unset, we get double help banners.
puts "Exiting" unless @options && options[:daemonize] || missing_handler
end
@@ -132,7 +170,17 @@ class ServerCommand < Base # :nodoc: | |||
def initialize(args = [], local_options = {}, config = {}) | |||
@original_options = local_options | |||
super | |||
@server = self.args.shift | |||
@server = if using |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally I'd probably extract the message into a method like below, but it's probably fine like this :)
@server = deprecate_positional_using(using) || options[:using]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 I think I can extract it in a separate method.
@@ -132,7 +170,17 @@ class ServerCommand < Base # :nodoc: | |||
def initialize(args = [], local_options = {}, config = {}) | |||
@original_options = local_options | |||
super | |||
@server = self.args.shift | |||
@server = if using | |||
ActiveSupport::Deprecation.warn(<<-MSG.squish) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use <<~
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It still needs the squish; deprecation messages should not be hard-wrapped on output.
print_boot_information | ||
trap(:INT) { exit } | ||
create_tmp_directories | ||
setup_dev_caching | ||
log_to_stdout if options[:log_stdout] | ||
|
||
super | ||
rescue LoadError |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should make sure to only rescue a LoadError for "rack/handler/#{options[:using]}"
; if something else is wrong, the original exception will do a better job of describing the issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rack::Handler.get
can raise either LoadError
or NameError
, if the Puma constant does not exist after the rack/handler/puma
require, for example. I think I can override the Rack::Server#server
method to isolate the error catch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can not deal with this by explicitly calling Rack::Handler.get
before starting server?
I am also concerned that other errors by LoadError
will be wrapped by this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can, I'll do the changes accordingly.
a80f003
to
48b0796
Compare
@kaspth, I think I'll be able to get the printing out of the As for moving the handler suggestion logic entirely into the |
railties/CHANGELOG.md
Outdated
|
||
Now: | ||
|
||
$ bin/rails dbconsole -u thin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bin/rails server
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! 😅
Run `rails server --help` for more options. | ||
MSG | ||
else | ||
suggestions = HANDLERS.sort_by { |h| levenshtein_distance(handler, h) }.map(&:inspect) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious. Is the user using the server
option so much that mistyped check is needed?
I understand that mistakenly specify server as env. But will not it solve the necessity of option to specify server?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@y-yagi doubt it's used often. This tries to give you more context on what actually happens when you pass a positional argument. I'm setting the suggestions here by suggestion (rim-shot). 😅 Anyway, I think it's quite easy to setup the suggestions and we can follow the example in more command options that accept a fixed set of values.
48b0796
to
d2c9309
Compare
Friends, I have moved the printing out of |
Dir.chdir(Rails.application.root) | ||
|
||
server = Rails::Server.new(server_options) | ||
handler = begin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can wrap this in a capturing method.
return | ||
end | ||
|
||
begin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here. The purpose of the Exiting
message is to show up, for example, when you CTRL-C
/terminate a running server.
|
||
def print_boot_information(handler) | ||
use_puma = handler.to_s == "Rack::Handler::Puma" | ||
url = "on #{options[:SSLEnable] ? 'https' : 'http'}://#{host}:#{port}" unless use_puma |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
url
can be nil
here, if we're not on Puma.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like where you took this in this round, Genadi! Hadn't thought of extracting all the printing to the command.
Also don't be afraid to say if you've had enough review rounds and would rather focus on wrapping it up. Then we'll find a way to cut it.
I've been using this case to ease back into the command infrastructure and what exactly the responsibility of a command should be. So if it feels like it's playing around a little bit — that's because it is! 😄
def handler | ||
server | ||
rescue LoadError, NameError | ||
raise MissingError |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about:
def serveable? # :nodoc:
server
true
rescue LoadError, NameError
false
end
Then in the command:
unless server.serveable?
say HandlerSuggestion.message(using) # We ought to use `say` now that we're in the command.
return
end
Hell, we could even get server.start
to yield to a finalizer block when it exits. Then it'd be:
server.start do |exited_successfully|
if exited_successfully
say "Exiting" unless options[:daemon]
else
say HandlerSuggestion.message(using)
end
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh. The last suggestion won't work because of print_boot_information
. Maybe there's still something useful we can dig out of the suggestion though? 😊
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kasper, I have tried something similar in the last round. I wasn't able to extract all the params out of print_boot_information
, because using
can be nil
, or empty if we coerce it, but that still won't give us a server name to print.
Run `rails server --help` for more options. | ||
MSG | ||
else | ||
suggestions = HANDLERS.sort_by { |h| levenshtein_distance(handler, h) }.map(&:inspect) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this being private
is what stops us from removing this HandlerSuggestion
then lets make it public. It's marked as :doc:
anyway, so it's already public API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's also a class method, so I'd have to call it through self.class
. We go through lots of hoops to use it now, so I'll extract it in a Rails::Command::Spellchecker
module as we already blew up the PR a bit. 😅
url = "on #{ssl ? 'https' : 'http'}://#{host}:#{port}" unless use_puma | ||
|
||
puts <<~MSG | ||
=> Booting #{ActiveSupport::Inflector.demodulize(handler)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this just use using
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
=> Booting Puma
, where using
is usually puma
. But yeah, we can save this argument pass.
|
||
def print_boot_information(handler, ssl) | ||
use_puma = handler.to_s == "Rack::Handler::Puma" | ||
url = "on #{ssl ? 'https' : 'http'}://#{host}:#{port}" unless use_puma |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's let the server build this URL in a nodoc'ed served_url
, so we can do server.served_url
below.
Then we can keep use_puma?
@@ -132,27 +155,34 @@ class ServerCommand < Base # :nodoc: | |||
def initialize(args = [], local_options = {}, config = {}) | |||
@original_options = local_options | |||
super | |||
@server = self.args.shift |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be better and clearer to do this than the @_using
construct:
super
deprecate_positional_using if @using
@using ||= options[:using]
@log_stdout = …
Don't worry Kasper I'm having fun as well. 😀 |
@@ -9,7 +9,7 @@ | |||
require "rails/dev_caching" | |||
|
|||
module Rails | |||
class Server < ::Rack::Server | |||
class Server < ::Rack::Server #:nodoc: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could technically be a backward incompatible change, but:
- I don't think we should list
Rails::Server
in the documentation at all. - This change aims Rails 6, so it should be okay.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- We have listed it in the past, so it's technically public API.
- Just because it's a major doesn't mean we should just make undue backwards incompatible changes willy-nilly.
I am learning towards the Rails::Server published as public API being a mistake and not being intended. I don't think any of the other Railties command helpers are public.
What does a GitHub search for Rails::Server
say? It might corroborate the above 😊
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we find there's no real usage of Rails::Server
on GitHub, I'm 👍 on # :nodoc:
(but do add the space so it matches the command nodoc 🤓)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really like how this turned out 👍
Feels right that the command handles the error printing and the server is more focused on its own boot.
Some minor nits left, then I'll merge.
server.start | ||
|
||
if server.serveable? | ||
print_boot_information(server.server, server.served_url) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's put a newline after this.
def rack_server_suggestion(server) | ||
if server.in?(RACK_SERVERS) | ||
<<~MSG | ||
Could not load server "#{server}". Maybe you need the add it to the Gemfile? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you need to
<<~MSG | ||
Could not find server "#{server}". Maybe you meant #{suggestions.first} or #{suggestions.second}? | ||
Run `rails server --help` for more options. | ||
MSG |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For another time: I could totally see us enriching Thor's argument/option with a :suggestions
to get messages like this for free. E.g.:
class_option :using, aliases: "-u", type: :string, suggestions: RACK_SERVERS,
desc: "Specifies the Rack server used to run the application (thin/puma/webrick).", banner: :name
trap(:INT) { exit } | ||
create_tmp_directories | ||
setup_dev_caching | ||
log_to_stdout if options[:log_stdout] | ||
|
||
super | ||
ensure | ||
# The '-h' option calls exit before @options is set. | ||
# If we call 'options' with it unset, we get double help banners. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍, dig that we got rid of these comments.
@@ -9,7 +9,7 @@ | |||
require "rails/dev_caching" | |||
|
|||
module Rails | |||
class Server < ::Rack::Server | |||
class Server < ::Rack::Server #:nodoc: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we find there's no real usage of Rails::Server
on GitHub, I'm 👍 on # :nodoc:
(but do add the space so it matches the command nodoc 🤓)
Oh, needs a commit squash and rebase too! Normally we'd want 1 commit, but I'll grant you a commit for the spellchecker change first, then one for the server changes after that if you want. 1 commit is fine too 👍 |
245c603
to
dd8b60e
Compare
I mistype `rails server production` instead of `rails server -e production` expecting to lunch a server in the production environment all the time. However, the signature of `rails server --help` is: ``` Usage: rails server [puma, thin etc] [options] ``` This means that the `production` argument is being interpreted as a Rack server handler like Puma, Thin or Unicorn. Should we argue for the `rails server production`? I'm not sure of the reasons, but the `rails console production` behavior was deprecated in: rails#29358, so parity with the existing `rails console production` usage may not hold anymore. In any case, this PR introduces an explicit option for the Rack servers configuration. The option is called `--using` (or `-u` for short) to avoid the `rails server --server` tantrum. The new interface of `rails server` is: ``` Usage: rails server [using] [options] Options: -p, [--port=port] # Runs Rails on the specified port - defaults to 3000. -b, [--binding=IP] # Binds Rails to the specified IP - defaults to 'localhost' in development and '0.0.0.0' in other environments'. -c, [--config=file] # Uses a custom rackup configuration. # Default: config.ru -d, [--daemon], [--no-daemon] # Runs server as a Daemon. -e, [--environment=name] # Specifies the environment to run this server under (development/test/production). -u, [--using=name] # Specifies the Rack server used to run the application (thin/puma/webrick). -P, [--pid=PID] # Specifies the PID file. # Default: tmp/pids/server.pid -C, [--dev-caching], [--no-dev-caching] # Specifies whether to perform caching in development. [--early-hints], [--no-early-hints] # Enables HTTP/2 early hints. ``` As a bonus, if you mistype the server to use, you'll get an auto-correction message: ``` $ rails s tin Could not find handler "tin". Maybe you meant "thin" or "cgi"? Run `rails server --help` for more options. ```
dd8b60e
to
a951729
Compare
Kasper, I have searched for
This means that we cannot match |
Fine with me! Really glad how this turned out 👏 |
Thanks for the review! 🙇♂️ |
Did some work on the server command a0061d2...1d97b8f and realized I forgot to backport this to 5-2-stable 🤭 |
- Because just passing the server argument to this command is deprecated in #32058
Recover all my page |
I mistype
rails server production
instead ofrails server -e production
expecting to lunch a server in the production environmentall the time. However, the signature of
rails server --help
is:This means that the
production
argument is being interpreted as a Rackserver handler like Puma, Thin or Unicorn.
Should we argue for the
rails server production
? I'm not sure of thereasons, but the
rails console production
behavior was deprecated in:#29358, so parity with the existing
rails console production
usage may not hold anymore.In any case, this PR introduces an explicit option for the Rack servers
configuration. I call them
--handlers
(or-x
for short, as-h
isalready as the shortcut for
--help
) to avoid therails server --server
tantrum.The new interface of
rails server
is:As a bonus, if you mistype the handler, you'll get an auto-correction
message: