Closed18

RuboCop公式のServer Modeを試す

inomotoinomoto

https://www.wantedly.com/companies/wantedly/post_articles/431073

  • Deamonize
    • rubocop コマンドを実行するたびに process を起動するのではなく、process を常駐させる

え、なに、ついに公式できちゃったの?オレオレでゴニョゴニョと書いていた人としては試さざるを得ない

inomotoinomoto

ひとまず、ぱっと思いつく知りたいことは以下

  • Docker環境でいい感じにできるか
  • Server側(Dockerコンテナ内)とClient側でバージョンが違っても動くか
    • ホストマシンにプロジェクトのGemfileのすべてをインストールせずに動かせるか
  • vscodeでいい感じに動かせるか
inomotoinomoto

まずは対象のプロジェクトを適当にでっち上げる。
なんかrubyでdocker composeな感じでとりあえずrubocopが入ってればok。

リポジトリ
https://github.com/cumet04/sbox_rubocop-server-on-docker

inomotoinomoto

はい。適当なsinatraなやつ。
https://github.com/cumet04/sbox_rubocop-server-on-docker/commit/0ed6386fcff6c7430c7859ff110cc1d88af241df

docker-compose.yml
version: "3"

services:
  app:
    build:
      context: app
    command: bundle exec ruby main.rb -o 0.0.0.0
    volumes:
      - ./app:/app
      - bundle:/usr/local/bundle
    ports:
      - 4567:4567

volumes:
  bundle:
app/Dockerfile
FROM ruby:3.1.2

RUN mkdir /app
WORKDIR /app

COPY Gemfile Gemfile.lock .
RUN bundle install
app/Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

gem "sinatra"
gem "sinatra-reloader"
gem "puma"
app/main.rb
require 'sinatra'
require "sinatra/reloader"

get '/' do
  'Hello world!'
end

ちなみに、ruby3系からはWEBrickがruby標準に無い関係で、明示的にthinなりpumaなり入れないといけないらしい。知らんかった。

inomotoinomoto

ではserver modeで適当に遊んで挙動を確認する。
コンテナに入ってちょっと触ってみた

root@b6d6d4555a18:/app# rubocop --start-server
RuboCop server starting on 127.0.0.1:41393.
root@b6d6d4555a18:/app# rubocop --start-server
RuboCop server (27) is already running.
root@b6d6d4555a18:/app# rubocop --stop-server
root@b6d6d4555a18:/app# rubocop --start-server
RuboCop server starting on 127.0.0.1:42167.

なにもしない場合、ポート番号は固定されないらしい。

inomotoinomoto

いやその前に速度確認しろよw

念のためstop serverしてから、serverなし、serverあり(一回目)、serverあり(二回目)でtimeしてみた。
realだけ抜き出すと下記。

  • serverなし: 0.677s
  • serverあり(1): 0.645s
  • serverあり(2): 0.242s

※原理的に早いはずなのは知っているし、ベンチする気もないので一回ずつしか試してない

以下生ログ。

root@b6d6d4555a18:/app# rubocop --stop-server
root@b6d6d4555a18:/app# time rubocop .
Inspecting 2 files
..

2 files inspected, no offenses detected

real    0m0.677s
user    0m0.617s
sys     0m0.132s
root@b6d6d4555a18:/app# time rubocop --server
RuboCop server starting on 127.0.0.1:46801.
Inspecting 2 files
..

2 files inspected, no offenses detected

real    0m0.645s
user    0m0.616s
sys     0m0.102s
root@b6d6d4555a18:/app# time rubocop --server
Inspecting 2 files
..

2 files inspected, no offenses detected

real    0m0.242s
user    0m0.059s
sys     0m0.019s
inomotoinomoto

ところで、 --start-serverしたら裏でプロセスを起動するだけしてコマンドは終了してしまう。
ドキュメントを見てもforegroundで起動しっぱなしにするみたいなモードは無いようにみえる。

これdocker環境だとどこにserverプロセスを差し込むか悩むな。

inomotoinomoto

ん?いやごちゃごちゃ考えなくてもdocker compose execでやればよいのでは?

host-machine$ time docker compose exec app rubocop --server
RuboCop server starting on 127.0.0.1:44725.
Inspecting 2 files
..

2 files inspected, no offenses detected

________________________________________________________
Executed in  709.86 millis    fish           external
   usr time   46.85 millis  205.00 micros   46.65 millis
   sys time   29.34 millis  205.00 micros   29.13 millis

host-machine$ time docker compose exec app rubocop --server
Inspecting 2 files
..

2 files inspected, no offenses detected

________________________________________________________
Executed in  287.50 millis    fish           external
   usr time   21.98 millis  200.00 micros   21.79 millis
   sys time   45.12 millis  200.00 micros   44.92 millis

まぁ普通に動きますな...
現実的にチームに入れるならゴチャゴチャ書くよりはシンプルにこれのがいいな...

inomotoinomoto

適当にrubocopのバージョンを1.31以上にして、docker compose execすればそれだけでいいっぽい結論が出かかっているが、一応最初にイメージしてたあたりの挙動も試したい。

inomotoinomoto

適当にdocker composeの中にrubocop server用のコンテナを立てる。

version: "3"

x-app-base: &app-base
  build:
    context: app
  volumes:
    - ./app:/app
    - bundle:/usr/local/bundle

services:
  base:
    <<: *app-base
    command: bundle install

  app:
    <<: *app-base
    command: bundle exec ruby main.rb -o 0.0.0.0
    ports:
      - 4567:4567

  rubocop-server:
    <<: *app-base
    command: bash -c 'rubocop --start-server; sleep infinity'
    environment:
      RUBOCOP_SERVER_HOST: 0.0.0.0
      RUBOCOP_SERVER_PORT: 45678
    ports:
      - 45678:45678

volumes:
  bundle:

少々ごちゃついているが、x-app-basebaseserviceの構成は、同じRuby環境で複数のコンテナを立ち上げる開発環境のための豆テクです。
volumesで/usr/local/bundleしつつserviceのコマンドでbundle installすることで、Gemfileの中身が変わった場合の再bundle時に以前のvolumeの中身をcache的に使えるというハック。なのでDockerfileの中ではRUN bundle installしてません。

で、rubocop-serverserviceを適当に作る。単にrubocop --start-serverを起動しただけでは一瞬で終了してしまうので、sleep infinityで起動しっぱなしに。

inomotoinomoto

で、適当にホストでgem install rubocopしたあとに env RUBOCOP_SERVER_HOST=localhost RUBOCOP_SERVER_PORT=45678 rubocop -- serverなどとやってみたが...

Address already in use - bind(2) for "localhost" port 45678 (Errno::EADDRINUSE)

などと、こちらでも別途のserverを起動しようとしてしまう。
そもそも、既存のserverに接続専用としてシンクライアント的にrubocopを起動するオプションは無いっぽい。

inomotoinomoto

一応、本体のコードを読んで仕様を確かめてみる。

まず実行ファイル?的なものの該当コードがこれ。
https://github.com/rubocop/rubocop/blob/d5d2fe1b37624b43b055507e3e1329f747e7812d/exe/rubocop#L11-L12

RuboCop::Server.running?の定義を追ってみると、こう。
https://github.com/rubocop/rubocop/blob/d5d2fe1b37624b43b055507e3e1329f747e7812d/lib/rubocop/server.rb#L33-L37

めちゃめちゃcacheとかpidとか見てるがな。同一ホストで動いてる想定しか存在してない。せやな。

inomotoinomoto

というわけで、docker compose execする以外に手段は無い。少なくとも現時点では。

まぁ下手にホスト側から実行できて、dockerを介さない分オーバーヘッドが減って早くなるみたいな結果が出てしまうと歯止めが効かなくなってしまうので、ここで止めてくれて助かったということにしておこう。

inomotoinomoto

一応vscodeもためそう。

inomotoinomoto

結論からすると、以下の準備をすればできる。

bin/rubocopを用意

プロジェクトにrubocopのwrapperを用意する。

bin/rubocop
#!/bin/bash

cd $(dirname $0)/..

OPTION=$(test -p /dev/stdin && echo '-T')
docker compose exec $OPTION app rubocop --server $@

docker-compose.ymlがあるディレクトリに移動したあと、execしている。
なお標準入力を使う場合でも使わない場合でもいい感じに動くようにすべく、-Tオプションの有無を制御している。

とはいえ、ホストでshellから起動する場合でもファイルパスはコンテナ内基準で指定する必要がある(あくまでrubocop本体はコンテナ内のファイルを見るため)。モノレポなどでリポジトリルートとコンテナ内のWORKDIRが違う場合は注意が必要。

rubocopのextensionをセットアップする

misogi.ruby-rubocop拡張を入れ、下記をsettings.jsonに追記する:

  "ruby.rubocop.executePath": "./bin/",
  "[ruby]": {
    "editor.defaultFormatter": "misogi.ruby-rubocop"
  },

※lintだけでいいならeditor.defaultFormatterの設定はいらない

ちなみに、この方法を使う場合は、下記の理由により通常のRuby extension (rebornix.Ruby) のlinter/formatter設定で使うことはできず、misogi.ruby-rubocopを使う必要があります。

  • rebornix.Rubyはrubocopに渡すファイルパスをホスト上の絶対パスで渡す仕様になっている
    • コンテナ内のファイルパスと食い違ってしまい、うまく動かない
    • misogi.ruby-rubocopは相対パスで渡すので問題ない
    • bin/rubocop内でsed "s|$PWD/||"とかで置き換えてもいいけど、そこまでしなくても上記拡張を使うだけで済む
  • rebornix.Rubyはformatterのパスを指定できず、カスタムのbin/rubocopを指定できない

これだけ準備してしまえばいい感じにrubocopライフが送れる。

このスクラップは2022/09/12にクローズされました