diff --git a/bin/console b/bin/console index 4c8257ce4358..a85f2f8207f7 100755 --- a/bin/console +++ b/bin/console @@ -4,6 +4,7 @@ require 'bundler/setup' require 'irb' require 'rubocop' +require 'rubocop/server' ARGV.clear diff --git a/changelog/new_integrate_rubocop_server.md b/changelog/new_integrate_rubocop_server.md new file mode 100644 index 000000000000..6191148cf32a --- /dev/null +++ b/changelog/new_integrate_rubocop_server.md @@ -0,0 +1 @@ +* [#10706](https://github.com/rubocop/rubocop/pull/10706): Integrate rubocop-daemon to add server options. ([@koic][]) diff --git a/codespell.txt b/codespell.txt index c2af630952be..d8d7bcd5bd7a 100644 --- a/codespell.txt +++ b/codespell.txt @@ -1,4 +1,5 @@ ba +creat enviromnent filetest fo diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 37822b509b5b..42d33096ab3d 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -5,6 +5,7 @@ ** xref:usage/basic_usage.adoc[Basic Usage] ** xref:usage/autocorrect.adoc[Autocorrect] ** xref:usage/caching.adoc[Caching] +** xref:usage/server.adoc[Server] * xref:configuration.adoc[Configuration] * xref:cops.adoc[Cops] * xref:formatters.adoc[Formatters] diff --git a/docs/modules/ROOT/pages/integration_with_other_tools.adoc b/docs/modules/ROOT/pages/integration_with_other_tools.adoc index e3711e5bf374..b64ed3802d84 100644 --- a/docs/modules/ROOT/pages/integration_with_other_tools.adoc +++ b/docs/modules/ROOT/pages/integration_with_other_tools.adoc @@ -7,7 +7,7 @@ to do autocorrection for you. In these cases, `rubocop` ends up getting called r which may result in some slowness, as `rubocop` has to require its entire environment on each call. -You can alleviate some of that boot time by using +You can alleviate some of that boot time by using xref:server.adoc["Server"] or https://github.com/fohte/rubocop-daemon[rubocop-daemon]. `rubocop-daemon` is a wrapper around `rubocop` that loads everything into a daemonized process so that subsequent runs save on that boot time after the first execution. Please see the diff --git a/docs/modules/ROOT/pages/usage/server.adoc b/docs/modules/ROOT/pages/usage/server.adoc new file mode 100644 index 000000000000..8eb9f339e5dc --- /dev/null +++ b/docs/modules/ROOT/pages/usage/server.adoc @@ -0,0 +1,84 @@ += Server Mode + +You can reduce the RuboCop boot time by using the `--server` option. + +The server option speeds up the launch of the `rubocop` command by serverizing +the process that loaded the RuboCop runtime production files (i.e. `require 'rubocop'`). + +NOTE: The feature cannot be used on JRuby and Windows that do not support fork. + +== Run with Server + +There are two ways to enable server. + +- `rubocop --server` ... If server process has not started yet, +start server process and execute inspection with server. +- `rubocop --start-server` ... Just start server process. + +When server is started, it outputs the host and port. + +```console +% rubocop --start-server +RuboCop server starting on 127.0.0.1:55772. +``` + +NOTE: The `rubocop` command is executed using server process if server is started. +Whenever server process is not running, it will load the RuboCop runtime files and execute. +(same behavior as 1.30 and lower) + +If server is already running, just display the PID. The new server will not start. + +```console +% rubocop --start-server +RuboCop server (16060) is already running. +``` + +A started server process name is `rubocop --server` with project directory path. + +```console +% ps aux | grep 'rubocop --server' +user 16060 0.0 0.0 5078568 2264 ?? S 7:54AM 0:00.00 rubocop --server /Users/user/src/github.com/rubocop/rubocop +user 16337 0.0 0.0 5331560 2396 ?? S 23:51PM 0:00.00 rubocop --server /Users/user/src/github.com/rubocop/rubocop-rails +``` + +== Restart Server + +The started server does not reload the configuration file. +You will need to restart server when you upgrade RuboCop or change +the RuboCop configuration. + +```console +% rubocop --restart-server +RuboCop server starting on 127.0.0.1:55822. +``` + +This may be supported so that no reboot is required in future. + +== Command Line Options + +These are options for server operations. + +* `--server` ... If server process has not started yet, start the +server process and execute inspection with server. You can specify +the server host and port with the $RUBOCOP_SERVER_HOST and +the $RUBOCOP_SERVER_PORT environment variables. +* `--no-server` ... If server process has been started, stop the +server process and execute inspection with server. +* `--restart-server` ... Restart server process. +* `--start-server` ... Start server process. +* `--stop-server` ... Stop server process. +* `--server-status` ... Show server status. + +== Environment Variables + +You can change the startup host and port of server process with +environment variables. + +* `$RUBOCOP_SERVER_HOST` +* `$RUBOCOP_SERVER_PORT` + +The following is an example: + +```console +% RUBOCOP_SERVER_PORT=98989 rubocop --start-server +``` diff --git a/exe/rubocop b/exe/rubocop index faf692600f0b..a772e9a7dead 100755 --- a/exe/rubocop +++ b/exe/rubocop @@ -3,13 +3,21 @@ $LOAD_PATH.unshift("#{__dir__}/../lib") -require 'rubocop' -require 'benchmark' +require 'rubocop/server' +server_cli = RuboCop::Server::CLI.new +exit_status = server_cli.run +exit exit_status if server_cli.exit? -cli = RuboCop::CLI.new -result = 0 +if RuboCop::Server.running? + exit_status = RuboCop::Server::ClientCommand::Exec.new.run +else + require 'rubocop' + require 'benchmark' -time = Benchmark.realtime { result = cli.run } + cli = RuboCop::CLI.new -puts "Finished in #{time} seconds" if cli.options[:debug] || cli.options[:display_time] -exit result + time = Benchmark.realtime { exit_status = cli.run } + + puts "Finished in #{time} seconds" if cli.options[:debug] || cli.options[:display_time] +end +exit exit_status diff --git a/lib/rubocop/daemon.rb b/lib/rubocop/daemon.rb deleted file mode 100644 index 13660d0ce739..000000000000 --- a/lib/rubocop/daemon.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -# -# This code is based on https://github.com/fohte/rubocop-daemon. -# -# Copyright (c) 2018 Hayato Kawai -# -# The MIT License (MIT) -# -# https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt -# -module RuboCop - module Daemon - TIMEOUT = 20 - - autoload :CLI, 'rubocop/daemon/cli' - autoload :Cache, 'rubocop/daemon/cache' - autoload :ClientCommand, 'rubocop/daemon/client_command' - autoload :Helper, 'rubocop/daemon/helper' - autoload :Server, 'rubocop/daemon/server' - autoload :ServerCommand, 'rubocop/daemon/server_command' - autoload :SocketReader, 'rubocop/daemon/socket_reader' - - def self.running? - Cache.dir.exist? && Cache.pid_path.file? && Cache.pid_running? - end - - def self.wait_for_running_status!(expected) - start_time = Time.now - while Daemon.running? != expected - sleep 0.1 - next unless Time.now - start_time > TIMEOUT - - warn "running? was not #{expected} after #{TIMEOUT} seconds!" - exit 1 - end - end - end -end - -require 'rubocop/daemon/errors' diff --git a/lib/rubocop/daemon/cli.rb b/lib/rubocop/daemon/cli.rb deleted file mode 100644 index 0117ebef6849..000000000000 --- a/lib/rubocop/daemon/cli.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -require 'optparse' - -# -# This code is based on https://github.com/fohte/rubocop-daemon. -# -# Copyright (c) 2018 Hayato Kawai -# -# The MIT License (MIT) -# -# https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt -# -module RuboCop - module Daemon - class CLI - def self.new_parser(&_block) - OptionParser.new do |opts| - yield(opts) - end - end - - def run(argv = ARGV) - parser.order!(argv) - return if argv.empty? - - create_subcommand_instance(argv) - rescue OptionParser::InvalidOption => e - warn "error: #{e.message}" - exit 1 - rescue UnknownClientCommandError => e - warn "rubocop-daemon: #{e.message}. See 'rubocop-daemon --help'." - exit 1 - end - - def parser - @parser ||= self.class.new_parser do |opts| - opts.banner = 'usage: rubocop-daemon []' - end - end - - private - - def create_subcommand_instance(argv) - subcommand, *args = argv - find_subcommand_class(subcommand).new(args).run - end - - def find_subcommand_class(subcommand) - case subcommand - when 'exec' then ClientCommand::Exec - when 'restart' then ClientCommand::Restart - when 'start' then ClientCommand::Start - when 'status' then ClientCommand::Status - when 'stop' then ClientCommand::Stop - else - raise UnknownClientCommandError, "#{subcommand.inspect} is not a rubocop-daemon command" - end - end - end - end -end diff --git a/lib/rubocop/daemon/client_command.rb b/lib/rubocop/daemon/client_command.rb deleted file mode 100644 index 1b7c2857cf08..000000000000 --- a/lib/rubocop/daemon/client_command.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -# -# This code is based on https://github.com/fohte/rubocop-daemon. -# -# Copyright (c) 2018 Hayato Kawai -# -# The MIT License (MIT) -# -# https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt -# -module RuboCop - module Daemon - module ClientCommand - autoload :Base, 'rubocop/daemon/client_command/base' - autoload :Exec, 'rubocop/daemon/client_command/exec' - autoload :Restart, 'rubocop/daemon/client_command/restart' - autoload :Start, 'rubocop/daemon/client_command/start' - autoload :Status, 'rubocop/daemon/client_command/status' - autoload :Stop, 'rubocop/daemon/client_command/stop' - end - end -end diff --git a/lib/rubocop/daemon/client_command/restart.rb b/lib/rubocop/daemon/client_command/restart.rb deleted file mode 100644 index b028197fc312..000000000000 --- a/lib/rubocop/daemon/client_command/restart.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -# -# This code is based on https://github.com/fohte/rubocop-daemon. -# -# Copyright (c) 2018 Hayato Kawai -# -# The MIT License (MIT) -# -# https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt -# -module RuboCop - module Daemon - module ClientCommand - class Restart < Base - def run - parser.parse(@argv) - - ClientCommand::Stop.new([]).run - ClientCommand::Start.new(@argv).run - end - - private - - def parser - @parser ||= CLI.new_parser do |p| - p.banner = 'usage: rubocop-daemon restart' - - p.on('-p', '--port [PORT]') { |v| @options[:port] = v } - end - end - end - end - end -end diff --git a/lib/rubocop/daemon/client_command/start.rb b/lib/rubocop/daemon/client_command/start.rb deleted file mode 100644 index 111fbdcfa49f..000000000000 --- a/lib/rubocop/daemon/client_command/start.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -# -# This code is based on https://github.com/fohte/rubocop-daemon. -# -# Copyright (c) 2018 Hayato Kawai -# -# The MIT License (MIT) -# -# https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt -# -module RuboCop - module Daemon - module ClientCommand - class Start < Base - def run - if Daemon.running? - warn 'rubocop-daemon: server is already running.' - return - end - - Cache.acquire_lock do |locked| - unless locked - # Another process is already starting the daemon, - # so wait for it to be ready. - Daemon.wait_for_running_status!(true) - exit 0 - end - - parser.parse(@argv) - Server.new(@options.fetch(:no_daemon, false)).start(@options.fetch(:port, 0)) - end - end - - private - - def parser - @parser ||= CLI.new_parser do |p| - p.banner = 'usage: rubocop-daemon start [options]' - - p.on('-p', '--port [PORT]') { |v| @options[:port] = v } - p.on('--no-daemon', 'Starts server in foreground with debug information') { @options[:no_daemon] = true } - end - end - end - end - end -end diff --git a/lib/rubocop/options.rb b/lib/rubocop/options.rb index 43f66dc19a6f..98193afe8058 100644 --- a/lib/rubocop/options.rb +++ b/lib/rubocop/options.rb @@ -63,6 +63,7 @@ def define_options add_check_options(opts) add_cache_options(opts) + add_server_options(opts) add_output_options(opts) add_autocorrection_options(opts) add_config_generation_options(opts) @@ -201,6 +202,16 @@ def add_cache_options(opts) end end + def add_server_options(opts) + section(opts, 'Server Options') do + option(opts, '--[no-]server') + option(opts, '--restart-server') + option(opts, '--start-server') + option(opts, '--stop-server') + option(opts, '--server-status') + end + end + def add_additional_modes(opts) section(opts, 'Additional Modes') do option(opts, '-L', '--list-target-files') @@ -569,7 +580,17 @@ module OptionsHelp 'parallel. Default is true.'], stdin: ['Pipe source from STDIN, using FILE in offense', 'reports. This is useful for editor integration.'], - init: 'Generate a .rubocop.yml file in the current directory.' + init: 'Generate a .rubocop.yml file in the current directory.', + server: ['If server process has not started yet, start the', + 'server process and execute inspection with server.', + 'Default is false.', + 'You can specify server host and port with', + 'the $RUBOCOP_SERVER_HOST and the $RUBOCOP_SERVER_PORT', + 'environment variables.'], + restart_server: 'Restart server process.', + start_server: 'Start server process.', + stop_server: 'Stop server process.', + server_status: 'Show server status.' }.freeze end end diff --git a/lib/rubocop/server.rb b/lib/rubocop/server.rb new file mode 100644 index 000000000000..3e8051209b58 --- /dev/null +++ b/lib/rubocop/server.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require_relative 'platform' + +# +# This code is based on https://github.com/fohte/rubocop-daemon. +# +# Copyright (c) 2018 Hayato Kawai +# +# The MIT License (MIT) +# +# https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt +# +module RuboCop + # The bootstrap module for server. + # @api private + module Server + TIMEOUT = 20 + + autoload :CLI, 'rubocop/server/cli' + autoload :Cache, 'rubocop/server/cache' + autoload :ClientCommand, 'rubocop/server/client_command' + autoload :Helper, 'rubocop/server/helper' + autoload :Core, 'rubocop/server/core' + autoload :ServerCommand, 'rubocop/server/server_command' + autoload :SocketReader, 'rubocop/server/socket_reader' + + class << self + def support_server? + RUBY_ENGINE == 'ruby' && !RuboCop::Platform.windows? + end + + def running? + return false unless support_server? # Never running. + + Cache.dir.exist? && Cache.pid_path.file? && Cache.pid_running? + end + + def wait_for_running_status!(expected) + start_time = Time.now + while Server.running? != expected + sleep 0.1 + next unless Time.now - start_time > TIMEOUT + + warn "running? was not #{expected} after #{TIMEOUT} seconds!" + exit 1 + end + end + end + end +end + +require_relative 'server/errors' diff --git a/lib/rubocop/daemon/cache.rb b/lib/rubocop/server/cache.rb similarity index 86% rename from lib/rubocop/daemon/cache.rb rename to lib/rubocop/server/cache.rb index b5d0ee8335bf..983de5b0f39a 100644 --- a/lib/rubocop/daemon/cache.rb +++ b/lib/rubocop/server/cache.rb @@ -12,14 +12,18 @@ # https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt # module RuboCop - module Daemon + module Server + # Caches the states of server process. + # @api private class Cache + GEMFILE_NAMES = %w[Gemfile gems.rb].freeze + class << self # Searches for Gemfile or gems.rb in the current dir or any parent dirs def project_dir current_dir = Dir.pwd while current_dir != '/' - return current_dir if %w[Gemfile gems.rb].any? do |gemfile| + return current_dir if GEMFILE_NAMES.any? do |gemfile| File.exist?(File.join(current_dir, gemfile)) end @@ -30,11 +34,11 @@ def project_dir end def project_dir_cache_key - @project_dir_cache_key ||= project_dir[1..-1].tr('/', '+') + @project_dir_cache_key ||= project_dir[1..].tr('/', '+') end def dir - cache_path = File.expand_path('~/.cache/rubocop-daemon') + cache_path = File.expand_path('~/.cache/rubocop_cache/server') Pathname.new(File.join(cache_path, project_dir_cache_key)).tap do |d| d.mkpath unless d.exist? end diff --git a/lib/rubocop/server/cli.rb b/lib/rubocop/server/cli.rb new file mode 100644 index 000000000000..6b49309d26d1 --- /dev/null +++ b/lib/rubocop/server/cli.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'optparse' +require 'rainbow' + +# +# This code is based on https://github.com/fohte/rubocop-daemon. +# +# Copyright (c) 2018 Hayato Kawai +# +# The MIT License (MIT) +# +# https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt +# +module RuboCop + module Server + # The CLI is a class responsible of handling server command line interface logic. + # @api private + class CLI + # Same exit status value as `RuboCop::CLI`. + STATUS_SUCCESS = 0 + STATUS_ERROR = 2 + + SERVER_OPTIONS = %w[ + --server --no-server --server-status --restart-server --start-server --stop-server + ].freeze + EXCLUSIVE_OPTIONS = (SERVER_OPTIONS - %w[--server --no-server]).freeze + + def initialize + @exit = false + end + + def run(argv = ARGV) + unless Server.support_server? + return error('RuboCop server is not supported by this Ruby.') if use_server_option?(argv) + + return STATUS_SUCCESS + end + + deleted_server_arguments = delete_server_argument_from(argv) + + if deleted_server_arguments.size >= 2 + return error("#{deleted_server_arguments.join(', ')} cannot be specified together.") + end + + server_command = deleted_server_arguments.first + + if EXCLUSIVE_OPTIONS.include?(server_command) && argv.count >= 2 + return error("#{server_command} cannot be combined with other options.") + end + + run_command(server_command) + + STATUS_SUCCESS + end + + def exit? + @exit + end + + private + + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength: + def run_command(server_command) + case server_command + when '--server' + Server::ClientCommand::Start.new.run unless Server.running? + when '--no-server' + Server::ClientCommand::Stop.new.run if Server.running? + when '--restart-server' + @exit = true + Server::ClientCommand::Restart.new.run + when '--start-server' + @exit = true + Server::ClientCommand::Start.new.run + when '--stop-server' + @exit = true + Server::ClientCommand::Stop.new.run + when '--server-status' + @exit = true + Server::ClientCommand::Status.new.run + end + end + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength: + + def delete_server_argument_from(all_arguments) + SERVER_OPTIONS.each_with_object([]) do |server_option, server_arguments| + server_arguments << all_arguments.delete(server_option) + end.compact + end + + def use_server_option?(argv) + (argv & SERVER_OPTIONS).any? + end + + def error(message) + @exit = true + warn Rainbow(message).red + + STATUS_ERROR + end + end + end +end diff --git a/lib/rubocop/server/client_command.rb b/lib/rubocop/server/client_command.rb new file mode 100644 index 000000000000..7486f2a00b25 --- /dev/null +++ b/lib/rubocop/server/client_command.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# +# This code is based on https://github.com/fohte/rubocop-daemon. +# +# Copyright (c) 2018 Hayato Kawai +# +# The MIT License (MIT) +# +# https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt +# +module RuboCop + module Server + # @api private + module ClientCommand + autoload :Base, 'rubocop/server/client_command/base' + autoload :Exec, 'rubocop/server/client_command/exec' + autoload :Restart, 'rubocop/server/client_command/restart' + autoload :Start, 'rubocop/server/client_command/start' + autoload :Status, 'rubocop/server/client_command/status' + autoload :Stop, 'rubocop/server/client_command/stop' + end + end +end diff --git a/lib/rubocop/daemon/client_command/base.rb b/lib/rubocop/server/client_command/base.rb similarity index 73% rename from lib/rubocop/daemon/client_command/base.rb rename to lib/rubocop/server/client_command/base.rb index d1c8cd340cfa..dfd1d5740c46 100644 --- a/lib/rubocop/daemon/client_command/base.rb +++ b/lib/rubocop/server/client_command/base.rb @@ -13,16 +13,15 @@ # https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt # module RuboCop - module Daemon + module Server module ClientCommand + # Abstract base class for server client command. + # @api private class Base - def initialize(argv) - @argv = argv.dup - @options = {} + def run + raise NotImplementedError end - def run; end - private def send_request(command:, args: [], body: '') @@ -30,13 +29,13 @@ def send_request(command:, args: [], body: '') socket.puts [Cache.token_path.read, Dir.pwd, command, *args].shelljoin socket.write body socket.close_write - STDOUT.write socket.read(4096) until socket.eof? + $stdout.write socket.read(4096) until socket.eof? end end def check_running_server - Daemon.running?.tap do |running| - warn 'rubocop-daemon: server is not running.' unless running + Server.running?.tap do |running| + warn 'RuboCop server is not running.' unless running end end diff --git a/lib/rubocop/daemon/client_command/exec.rb b/lib/rubocop/server/client_command/exec.rb similarity index 52% rename from lib/rubocop/daemon/client_command/exec.rb rename to lib/rubocop/server/client_command/exec.rb index 45abffe2fb77..9620217751f2 100644 --- a/lib/rubocop/daemon/client_command/exec.rb +++ b/lib/rubocop/server/client_command/exec.rb @@ -10,36 +10,33 @@ # https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt # module RuboCop - module Daemon + module Server module ClientCommand + # This class is a client command to execute server process. + # @api private class Exec < Base def run - args = parser.parse(@argv) ensure_server! Cache.status_path.delete if Cache.status_path.file? send_request( command: 'exec', - args: args, - body: $stdin.tty? ? '' : $stdin.read, + args: ARGV.dup, + body: $stdin.tty? ? '' : $stdin.read ) - exit_with_status! + status end private - def parser - @parser ||= CLI.new_parser do |p| - p.banner = 'usage: rubocop-daemon exec [options] [files...] [-- [rubocop-options]]' + def status + unless Cache.status_path.file? + raise "rubocop server: Could not find status file at: #{Cache.status_path}" end - end - - def exit_with_status! - raise "rubocop-daemon: Could not find status file at: #{Cache.status_path}" unless Cache.status_path.file? status = Cache.status_path.read - raise "rubocop-daemon: '#{status}' is not a valid status!" if (status =~ /^\d+$/).nil? + raise "rubocop server: '#{status}' is not a valid status!" if (status =~ /^\d+$/).nil? - exit status.to_i + status.to_i end end end diff --git a/lib/rubocop/server/client_command/restart.rb b/lib/rubocop/server/client_command/restart.rb new file mode 100644 index 000000000000..5d185cf104c0 --- /dev/null +++ b/lib/rubocop/server/client_command/restart.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# +# This code is based on https://github.com/fohte/rubocop-daemon. +# +# Copyright (c) 2018 Hayato Kawai +# +# The MIT License (MIT) +# +# https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt +# +module RuboCop + module Server + module ClientCommand + # This class is a client command to restart server process. + # @api private + class Restart < Base + def run + ClientCommand::Stop.new.run + ClientCommand::Start.new.run + end + end + end + end +end diff --git a/lib/rubocop/server/client_command/start.rb b/lib/rubocop/server/client_command/start.rb new file mode 100644 index 000000000000..74391cda83c1 --- /dev/null +++ b/lib/rubocop/server/client_command/start.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# +# This code is based on https://github.com/fohte/rubocop-daemon. +# +# Copyright (c) 2018 Hayato Kawai +# +# The MIT License (MIT) +# +# https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt +# +module RuboCop + module Server + module ClientCommand + # This class is a client command to start server process. + # @api private + class Start < Base + def run + if Server.running? + warn "RuboCop server (#{Cache.pid_path.read}) is already running." + return + end + + Cache.acquire_lock do |locked| + unless locked + # Another process is already starting server, + # so wait for it to be ready. + Server.wait_for_running_status!(true) + exit 0 + end + + Server::Core.new.start( + ENV.fetch('RUBOCOP_SERVER_HOST', '127.0.0.1'), + ENV.fetch('RUBOCOP_SERVER_PORT', 0) + ) + end + end + end + end + end +end diff --git a/lib/rubocop/daemon/client_command/status.rb b/lib/rubocop/server/client_command/status.rb similarity index 53% rename from lib/rubocop/daemon/client_command/status.rb rename to lib/rubocop/server/client_command/status.rb index e948a21e568b..8f73c641de01 100644 --- a/lib/rubocop/daemon/client_command/status.rb +++ b/lib/rubocop/server/client_command/status.rb @@ -10,24 +10,16 @@ # https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt # module RuboCop - module Daemon + module Server module ClientCommand + # This class is a client command to show server process status. + # @api private class Status < Base def run - parser.parse(@argv) - - if Daemon.running? - puts 'rubocop-daemon is running.' + if Server.running? + puts "RuboCop server (#{Cache.pid_path.read}) is running." else - puts 'rubocop-daemon is not running.' - end - end - - private - - def parser - @parser ||= CLI.new_parser do |p| - p.banner = 'usage: rubocop-daemon status' + puts 'RuboCop server is not running.' end end end diff --git a/lib/rubocop/daemon/client_command/stop.rb b/lib/rubocop/server/client_command/stop.rb similarity index 59% rename from lib/rubocop/daemon/client_command/stop.rb rename to lib/rubocop/server/client_command/stop.rb index e2e6186e1893..ee28b1423083 100644 --- a/lib/rubocop/daemon/client_command/stop.rb +++ b/lib/rubocop/server/client_command/stop.rb @@ -10,23 +10,20 @@ # https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt # module RuboCop - module Daemon + module Server module ClientCommand + # This class is a client command to stop server process. + # @api private class Stop < Base def run return unless check_running_server - parser.parse(@argv) - send_request(command: 'stop') - Daemon.wait_for_running_status!(false) - end - - private - - def parser - @parser ||= CLI.new_parser do |p| - p.banner = 'usage: rubocop-daemon stop' + pid = fork do + send_request(command: 'stop') + Server.wait_for_running_status!(false) end + + Process.waitpid(pid) end end end diff --git a/lib/rubocop/daemon/server.rb b/lib/rubocop/server/core.rb similarity index 55% rename from lib/rubocop/daemon/server.rb rename to lib/rubocop/server/core.rb index b3fb25af23ee..fd42e015e92a 100644 --- a/lib/rubocop/daemon/server.rb +++ b/lib/rubocop/server/core.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'socket' -require 'shellwords' require 'securerandom' # @@ -14,41 +13,54 @@ # https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt # module RuboCop - module Daemon - class Server - attr_reader :verbose - + module Server + # The core of server process. It starts TCP server and perform socket communication. + # @api private + class Core def self.token @token ||= SecureRandom.hex(4) end - def initialize(verbose) - @verbose = verbose - end - def token self.class.token end - def start(port) + def start(host, port) + $PROGRAM_NAME = "rubocop --server #{Cache.project_dir}" + require 'rubocop' - start_server(port) + start_server(host, port) + + demonize if server_mode? + end + + private + + def demonize Cache.write_port_and_token_files(port: @server.addr[1], token: token) - Process.daemon(true) unless verbose - Cache.write_pid_file do - read_socket(@server.accept) until @server.closed? + + pid = fork do + Process.daemon(true) + Cache.write_pid_file do + read_socket(@server.accept) until @server.closed? + end end + + Process.waitpid(pid) end - private + def server_mode? + true + end + + def start_server(host, port) + @server = TCPServer.open(host, port) - def start_server(port) - @server = TCPServer.open('127.0.0.1', port) - puts "Server listen on port #{@server.addr[1]}" if verbose + puts "RuboCop server starting on #{@server.addr[3]}:#{@server.addr[1]}." end def read_socket(socket) - SocketReader.new(socket, verbose).read! + SocketReader.new(socket).read! rescue InvalidTokenError socket.puts 'token is not valid.' rescue ServerStopRequest @@ -56,7 +68,7 @@ def read_socket(socket) rescue UnknownServerCommandError => e socket.puts e.message rescue Errno::EPIPE => e - p e if verbose + warn e.inspect rescue StandardError => e socket.puts e.full_message ensure diff --git a/lib/rubocop/daemon/errors.rb b/lib/rubocop/server/errors.rb similarity index 77% rename from lib/rubocop/daemon/errors.rb rename to lib/rubocop/server/errors.rb index 8c5956dbac0a..36a46ca2cb3a 100644 --- a/lib/rubocop/daemon/errors.rb +++ b/lib/rubocop/server/errors.rb @@ -10,11 +10,14 @@ # https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt # module RuboCop - module Daemon - class GemfileNotFound < StandardError; end + module Server + # @api private class InvalidTokenError < StandardError; end + + # @api private class ServerStopRequest < StandardError; end - class UnknownClientCommandError < StandardError; end + + # @api private class UnknownServerCommandError < StandardError; end end end diff --git a/lib/rubocop/daemon/helper.rb b/lib/rubocop/server/helper.rb similarity index 85% rename from lib/rubocop/daemon/helper.rb rename to lib/rubocop/server/helper.rb index 4b71be2a5899..468e8919f535 100644 --- a/lib/rubocop/daemon/helper.rb +++ b/lib/rubocop/server/helper.rb @@ -10,7 +10,9 @@ # https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt # module RuboCop - module Daemon + module Server + # This module has a helper memthod for `RuboCop::Server::SocketReader`. + # @api private module Helper def self.redirect(stdin: $stdin, stdout: $stdout, stderr: $stderr, &_block) old_stdin = $stdin.dup diff --git a/lib/rubocop/daemon/server_command.rb b/lib/rubocop/server/server_command.rb similarity index 57% rename from lib/rubocop/daemon/server_command.rb rename to lib/rubocop/server/server_command.rb index 4bb9c919945c..56ff0ba9efaa 100644 --- a/lib/rubocop/daemon/server_command.rb +++ b/lib/rubocop/server/server_command.rb @@ -10,11 +10,12 @@ # https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt # module RuboCop - module Daemon + module Server + # @api private module ServerCommand - autoload :Base, 'rubocop/daemon/server_command/base' - autoload :Exec, 'rubocop/daemon/server_command/exec' - autoload :Stop, 'rubocop/daemon/server_command/stop' + autoload :Base, 'rubocop/server/server_command/base' + autoload :Exec, 'rubocop/server/server_command/exec' + autoload :Stop, 'rubocop/server/server_command/stop' end end end diff --git a/lib/rubocop/daemon/server_command/base.rb b/lib/rubocop/server/server_command/base.rb similarity index 80% rename from lib/rubocop/daemon/server_command/base.rb rename to lib/rubocop/server/server_command/base.rb index ad56bb220cac..a02ea814e893 100644 --- a/lib/rubocop/daemon/server_command/base.rb +++ b/lib/rubocop/server/server_command/base.rb @@ -10,9 +10,13 @@ # https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt # module RuboCop - module Daemon + module Server module ServerCommand + # Abstract base class for server command. + # @api private class Base + # Common functionality for working with subclasses of this class. + # @api private module Runner def run validate_token! @@ -23,6 +27,7 @@ def run end def self.inherited(child) + super child.prepend Runner end diff --git a/lib/rubocop/daemon/server_command/exec.rb b/lib/rubocop/server/server_command/exec.rb similarity index 79% rename from lib/rubocop/daemon/server_command/exec.rb rename to lib/rubocop/server/server_command/exec.rb index d9427e2e5ea7..d9825debf4ff 100644 --- a/lib/rubocop/daemon/server_command/exec.rb +++ b/lib/rubocop/server/server_command/exec.rb @@ -10,8 +10,10 @@ # https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt # module RuboCop - module Daemon + module Server module ServerCommand + # This class is a server command to execute `rubocop` command using `RuboCop::CLI.new#run`. + # @api private class Exec < Base def run Cache.status_path.delete if Cache.status_path.file? @@ -19,7 +21,7 @@ def run # We must pass the --color option to preserve this behavior. @args.unshift('--color') unless %w[--color --no-color].any? { |f| @args.include?(f) } status = RuboCop::CLI.new.run(@args) - # This status file is read by `rubocop-daemon exec` and `rubocop-daemon-wrapper`, + # This status file is read by `rubocop --server` (`RuboCop::Server::Clientcommand::Exec`). # so that they use the correct exit code. # Status is 1 when there are any issues, and 0 otherwise. Cache.write_status_file(status) diff --git a/lib/rubocop/daemon/server_command/stop.rb b/lib/rubocop/server/server_command/stop.rb similarity index 79% rename from lib/rubocop/daemon/server_command/stop.rb rename to lib/rubocop/server/server_command/stop.rb index e91822c3e6c4..20c7ebef6d5d 100644 --- a/lib/rubocop/daemon/server_command/stop.rb +++ b/lib/rubocop/server/server_command/stop.rb @@ -10,8 +10,10 @@ # https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt # module RuboCop - module Daemon + module Server module ServerCommand + # This class is a server command to stop server process. + # @api private class Stop < Base def run raise ServerStopRequest diff --git a/lib/rubocop/daemon/socket_reader.rb b/lib/rubocop/server/socket_reader.rb similarity index 78% rename from lib/rubocop/daemon/socket_reader.rb rename to lib/rubocop/server/socket_reader.rb index 253b1b188f6e..250b2cfaab7e 100644 --- a/lib/rubocop/daemon/socket_reader.rb +++ b/lib/rubocop/server/socket_reader.rb @@ -10,14 +10,15 @@ # https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt # module RuboCop - module Daemon + module Server + # This class sends the request read from the socket to server. + # @api private class SocketReader Request = Struct.new(:header, :body) Header = Struct.new(:token, :cwd, :command, :args) - def initialize(socket, verbose) + def initialize(socket) @socket = socket - @verbose = verbose end def read! @@ -26,7 +27,7 @@ def read! Helper.redirect( stdin: StringIO.new(request.body), stdout: @socket, - stderr: @socket, + stderr: @socket ) do create_command_instance(request).run end @@ -36,10 +37,6 @@ def read! def parse_request(content) raw_header, *body = content.lines - if @verbose - puts raw_header.to_s - puts "STDIN: #{body.size} lines" if body.any? - end Request.new(parse_header(raw_header), body.join) end @@ -52,11 +49,7 @@ def parse_header(header) def create_command_instance(request) klass = find_command_class(request.header.command) - klass.new( - request.header.args, - token: request.header.token, - cwd: request.header.cwd, - ) + klass.new(request.header.args, token: request.header.token, cwd: request.header.cwd) end def find_command_class(command) diff --git a/spec/rubocop/cli/options_spec.rb b/spec/rubocop/cli/options_spec.rb index ef14a8d882b2..c3ce56400594 100644 --- a/spec/rubocop/cli/options_spec.rb +++ b/spec/rubocop/cli/options_spec.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'open3' + RSpec.describe 'RuboCop::CLI options', :isolated_environment do # rubocop:disable RSpec/DescribeClass subject(:cli) { RuboCop::CLI.new } @@ -71,6 +73,149 @@ end end + if RuboCop::Server.support_server? + context 'when supporting server' do + describe '--server' do + before do + create_file('.rubocop.yml', <<~YAML) + AllCops: + NewCops: enable + YAML + create_file('example.rb', '"hello"') + end + + after do + `ruby -I . "#{rubocop}" --stop-server` + end + + it 'starts server and inspects' do + options = '--server --only Style/FrozenStringLiteralComment,Style/StringLiterals' + output = `ruby -I . "#{rubocop}" #{options}` + expect(output).to match( + /RuboCop server starting on \d+\.\d+\.\d+\.\d+:\d+\.\nInspecting 1 file/ + ) + end + end + + describe '--no-server' do + before do + create_file('.rubocop.yml', <<~YAML) + AllCops: + NewCops: enable + YAML + create_file('example.rb', '"hello"') + end + + it 'starts server and inspects' do + options = '--no-server --only Style/FrozenStringLiteralComment,Style/StringLiterals' + output = `ruby -I . "#{rubocop}" #{options}` + expect(output).not_to match(/RuboCop server starting on \d+\.\d+\.\d+\.\d+:\d+\./) + expect(output).to include(<<~RESULT) + Inspecting 1 file + C + + Offenses: + + example.rb:1:1: C: [Correctable] Style/FrozenStringLiteralComment: Missing frozen string literal comment. + "hello" + ^ + example.rb:1:1: C: [Correctable] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols. + "hello" + ^^^^^^^ + + 1 file inspected, 2 offenses detected, 2 offenses autocorrectable + RESULT + end + end + + describe '--start-server' do + after do + `ruby -I . "#{rubocop}" --stop-server` + end + + it 'start server process and displays an information message' do + output = `ruby -I . "#{rubocop}" --start-server` + expect(output).to match(/RuboCop server starting on \d+\.\d+\.\d+\.\d+:\d+\./) + end + end + + describe '--stop-server' do + before do + `ruby -I . "#{rubocop}" --start-server` + end + + it 'stops server process and displays an information message' do + output = `ruby -I . "#{rubocop}" --stop-server` + expect(output).to eq '' + end + end + + describe '--restart-server' do + before do + `ruby -I . "#{rubocop}" --start-server` + end + + after do + `ruby -I . "#{rubocop}" --stop-server` + end + + it 'restart server process and displays an information message' do + output = `ruby -I . "#{rubocop}" --restart-server` + expect(output).to match(/RuboCop server starting on \d+\.\d+\.\d+\.\d+:\d+\./) + end + end + + describe '--server-status' do + context 'when server is not runnning' do + it 'displays server status' do + output = `ruby -I . "#{rubocop}" --server-status` + expect(output).to match(/RuboCop server is not running./) + end + end + + context 'when server is runnning' do + before do + `ruby -I . "#{rubocop}" --start-server` + end + + after do + `ruby -I . "#{rubocop}" --stop-server` + end + + it 'displays server status' do + output = `ruby -I . "#{rubocop}" --server-status` + expect(output).to match(/RuboCop server \(\d+\) is running./) + end + end + end + end + else + context 'when not supporting server' do + describe 'no server options' do + it 'displays an warning message' do + stdout, stderr, status = Open3.capture3("ruby -I . \"#{rubocop}\"") + expect(stdout).to eq(<<~RESULT) + Inspecting 0 files + + + 0 files inspected, no offenses detected + RESULT + expect(stderr).not_to include("RuboCop server is not supported by this Ruby.\n") + expect(status.exitstatus).to eq 0 + end + end + + describe '--start-server' do + it 'displays an warning message' do + stdout, stderr, status = Open3.capture3("ruby -I . \"#{rubocop}\" --start-server") + expect(stdout).to eq '' + expect(stderr).to include("RuboCop server is not supported by this Ruby.\n") + expect(status.exitstatus).to eq 2 + end + end + end + end + describe '--list-target-files' do context 'when there are no files' do it 'prints nothing with -L' do diff --git a/spec/rubocop/options_spec.rb b/spec/rubocop/options_spec.rb index 19dbf10ddd16..75e35b5ecf02 100644 --- a/spec/rubocop/options_spec.rb +++ b/spec/rubocop/options_spec.rb @@ -83,6 +83,18 @@ def abs(path) parameter AllCops: CacheRootDirectory and the $RUBOCOP_CACHE_ROOT environment variable. + Server Options: + --[no-]server If server process has not started yet, start the + server process and execute inspection with server. + Default is false. + You can specify server host and port with + the $RUBOCOP_SERVER_HOST and the $RUBOCOP_SERVER_PORT + environment variables. + --restart-server Restart server process. + --start-server Start server process. + --stop-server Stop server process. + --server-status Show server status. + Output Options: -f, --format FORMATTER Choose an output formatter. This option can be specified multiple times to enable diff --git a/spec/rubocop/server/cli_spec.rb b/spec/rubocop/server/cli_spec.rb new file mode 100644 index 000000000000..555a25aa5984 --- /dev/null +++ b/spec/rubocop/server/cli_spec.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Server::CLI, :isolated_environment do + subject(:cli) { described_class.new } + + include_context 'cli spec behavior' + + if RuboCop::Server.support_server? + before do + allow_any_instance_of(RuboCop::Server::Core).to receive(:server_mode?).and_return(false) # rubocop:disable RSpec/AnyInstance + end + + after do + RuboCop::Server::ClientCommand::Stop.new.run + end + + context 'when using `--server` option' do + it 'returns exit status 0 and display an information message' do + create_file('example.rb', <<~RUBY) + # frozen_string_literal: true + + x = 0 + puts x + RUBY + expect(cli.run(['--server', '--format', 'simple', 'example.rb'])).to eq(0) + expect(cli.exit?).to be(false) + expect($stdout.string).to start_with 'RuboCop server starting on ' + expect($stderr.string).to eq '' + end + end + + context 'when using `--no-server` option' do + it 'returns exit status 0' do + create_file('example.rb', <<~RUBY) + # frozen_string_literal: true + + x = 0 + puts x + RUBY + expect(cli.run(['--no-server', '--format', 'simple', 'example.rb'])).to eq(0) + expect(cli.exit?).to be(false) + expect($stdout.string).to eq '' + expect($stderr.string).to eq '' + end + end + + context 'when using `--start-server` option' do + it 'returns exit status 0 and display an information message' do + expect(cli.run(['--start-server'])).to eq(0) + expect(cli.exit?).to be(true) + expect($stdout.string).to start_with 'RuboCop server starting on ' + expect($stderr.string).to eq '' + end + end + + context 'when using `--stop-server` option' do + it 'returns exit status 0 and display a warning message' do + expect(cli.run(['--stop-server'])).to eq(0) + expect(cli.exit?).to be(true) + expect($stdout.string).to eq '' + expect($stderr.string).to eq "RuboCop server is not running.\n" + end + end + + context 'when using `--restart-server` option' do + it 'returns exit status 0 and display an information and a warning messages' do + expect(cli.run(['--restart-server'])).to eq(0) + expect(cli.exit?).to be(true) + expect($stdout.string).to start_with 'RuboCop server starting on ' + expect($stderr.string).to eq "RuboCop server is not running.\n" + end + end + + context 'when using `--server-status` option' do + it 'returns exit status 0 and display an information message' do + expect(cli.run(['--server-status'])).to eq(0) + expect(cli.exit?).to be(true) + expect($stdout.string).to eq "RuboCop server is not running.\n" + expect($stderr.string).to eq '' + end + end + + context 'when not using any server options' do + it 'returns exit status 0' do + create_file('example.rb', <<~RUBY) + # frozen_string_literal: true + + x = 0 + puts x + RUBY + expect(cli.run(['--format', 'simple', 'example.rb'])).to eq(0) + expect(cli.exit?).to be(false) + expect($stdout.string.blank?).to be(true) + expect($stderr.string.blank?).to be(true) + end + end + + context 'when using multiple server options' do + it 'returns exit status 2 and display an error message' do + create_file('example.rb', <<~RUBY) + # frozen_string_literal: true + + x = 0 + puts x + RUBY + expect(cli.run(['--server', '--no-server', '--format', 'simple', 'example.rb'])).to eq(2) + expect(cli.exit?).to be(true) + expect($stdout.string).to eq '' + expect($stderr.string).to eq "--server, --no-server cannot be specified together.\n" + end + end + + context 'when using exclusive `--restart-server` option' do + it 'returns exit status 2 and display an error message' do + expect(cli.run(['--restart-server', '--format', 'simple'])).to eq(2) + expect(cli.exit?).to be(true) + expect($stdout.string).to eq '' + expect($stderr.string).to eq "--restart-server cannot be combined with other options.\n" + end + end + + context 'when using exclusive `--start-server` option' do + it 'returns exit status 2 and display an error message' do + expect(cli.run(['--start-server', '--format', 'simple'])).to eq(2) + expect(cli.exit?).to be(true) + expect($stdout.string).to eq '' + expect($stderr.string).to eq "--start-server cannot be combined with other options.\n" + end + end + + context 'when using exclusive `--stop-server` option' do + it 'returns exit status 2 and display an error message' do + expect(cli.run(['--stop-server', '--format', 'simple'])).to eq(2) + expect(cli.exit?).to be(true) + expect($stdout.string).to eq '' + expect($stderr.string).to eq "--stop-server cannot be combined with other options.\n" + end + end + + context 'when using exclusive `--server-status` option' do + it 'returns exit status 2 and display an error message' do + expect(cli.run(['--server-status', '--format', 'simple'])).to eq(2) + expect(cli.exit?).to be(true) + expect($stdout.string).to eq '' + expect($stderr.string).to eq "--server-status cannot be combined with other options.\n" + end + end + else + context 'when using `--server` option' do + it 'returns exit status 2 and display an error message' do + expect(cli.run(['--server', '--format', 'simple'])).to eq(2) + expect(cli.exit?).to be(true) + expect($stdout.string).to eq '' + expect($stderr.string).to eq "RuboCop server is not supported by this Ruby.\n" + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 37e55257f834..3ab94ba6a78e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,6 +5,7 @@ Rainbow.enabled = false require 'rubocop' +require 'rubocop/server' require 'rubocop/cop/internal_affairs' require 'webmock/rspec'