Skip to content
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

Integrate rubocop-daemon #10706

Merged
merged 2 commits into from Jun 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/console
Expand Up @@ -4,6 +4,7 @@
require 'bundler/setup'
require 'irb'
require 'rubocop'
require 'rubocop/server'

ARGV.clear

Expand Down
1 change: 1 addition & 0 deletions 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][])
1 change: 1 addition & 0 deletions codespell.txt
@@ -1,4 +1,5 @@
ba
creat
enviromnent
filetest
fo
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/nav.adoc
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/integration_with_other_tools.adoc
Expand Up @@ -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
Expand Down
84 changes: 84 additions & 0 deletions 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
```
22 changes: 15 additions & 7 deletions exe/rubocop
Expand Up @@ -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
23 changes: 22 additions & 1 deletion lib/rubocop/options.rb
Expand Up @@ -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)
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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
53 changes: 53 additions & 0 deletions 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'
101 changes: 101 additions & 0 deletions lib/rubocop/server/cache.rb
@@ -0,0 +1,101 @@
# frozen_string_literal: true

require 'pathname'

#
# 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
# 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 GEMFILE_NAMES.any? do |gemfile|
File.exist?(File.join(current_dir, gemfile))
end

current_dir = File.expand_path('..', current_dir)
end
# If we can't find a Gemfile, just use the current directory
Dir.pwd
end

def project_dir_cache_key
@project_dir_cache_key ||= project_dir[1..].tr('/', '+')
end

def dir
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
end

def port_path
dir.join('port')
end

def token_path
dir.join('token')
end

def pid_path
dir.join('pid')
end

def lock_path
dir.join('lock')
end

def status_path
dir.join('status')
end

def pid_running?
Process.kill(0, pid_path.read.to_i) == 1
rescue Errno::ESRCH
false
end

def acquire_lock
lock_file = File.open(lock_path, File::CREAT)
# flock returns 0 if successful, and false if not.
flock_result = lock_file.flock(File::LOCK_EX | File::LOCK_NB)
yield flock_result != false
ensure
lock_file.flock(File::LOCK_UN)
lock_file.close
end

def write_port_and_token_files(port:, token:)
port_path.write(port)
token_path.write(token)
end

def write_pid_file
pid_path.write(Process.pid)
yield
ensure
dir.rmtree
end

def write_status_file(status)
status_path.write(status)
end
end
end
end
end