@ledsun blog

無味の味は佳境に入らざればすなわち知れず

Ractorを試す 改訂版

macOShttps://github.com/ko1/ruby/tree/ractorソースコードをビルドする方法を改善しました。

インストール先

コンパイルしたRubyをmake installしたら「PCの環境が壊れるかも」とビビっていたところ、 id:hanachin さんに ./configure --prefix="$HOME/.rbenv/versions/ractor" するとrbenvの1実行環境として扱えると教えてもらいました。

OpenSSL

makeのログを見てたら、次のようなエラーも出ていました。

*** Following extensions are not compiled:
openssl:
    Could not be configured. It will not be installed.
    /Users/shigerunakajima/ractor/ext/openssl/extconf.rb:99: OpenSSL library could not be found. You might want to use --with-openssl-dir=<dir> option to specify the prefix where OpenSSL is installed.
    Check ext/openssl/mkmf.log for more details.
*** Fix the problems, then remove these directories and try again if you want.

--with-openssl-dir=/usr/local/opt/openssl@1.1/オプションをつけて、HomebrewでインストールしたOpenSSLのディレクトリを指定します。

完成形

CFLAGS=-Wno-error=shorten-64-to-32 ./configure --prefix="$HOME/.rbenv/versions/ractor" --with-openssl-dir=/usr/local/opt/openssl@1.1/
make
make install

まだ、次のようなエラーが表示されます。

installing bundled gems:            /Users/shigerunakajima/.rbenv/versions/ractor/lib/ruby/gems/2.8.0
Traceback (most recent call last):
    6: from ./tool/rbinstall.rb:957:in `<main>'
    5: from ./tool/rbinstall.rb:957:in `each'
    4: from ./tool/rbinstall.rb:960:in `block in <main>'
    3: from ./tool/rbinstall.rb:889:in `block in <main>'
    2: from ./tool/rbinstall.rb:889:in `foreach'
    1: from ./tool/rbinstall.rb:894:in `block (2 levels) in <main>'
./tool/rbinstall.rb:818:in `load_gemspec': [/Users/shigerunakajima/ractor/.bundle/gems/minitest-5.14.0/minitest.gemspec] isn't a Gem::Specification (NilClass instead). (TypeError)
make: *** [do-install-all] Error 1

Rubyコマンドのインストールはできているみたいです。

~ ls ~/.rbenv/versions/ractor/bin/ruby
/Users/shigerunakajima/.rbenv/versions/ractor/bin/ruby*

プログラムの実行だけなら、minitestはなくてもよさそうなので、先に進みます。

動作確認

システムのRubyではRactorを使ったRubyスクリプトの実行に失敗します。

~ ruby --version
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin18]
~ ruby sum_1_to_10.rb
Traceback (most recent call last):
sum_1_to_10.rb:2:in `<main>': uninitialized constant Ractor (NameError)

ractor版を使うと実行できました。

~ rbenv shell ractor
~ ruby --version
ruby 2.8.0dev (2020-04-08T16:31:17Z ractor 2658fc668d) [x86_64-darwin18]
~ ruby sum_1_to_10.rb
55

Ractorをつかうプログラムの練習 2つのイベントを待つ

Ractorをつかって、ユーザー入力とクロックを両方待つプログラムを書いてみます。 練習中であり、Ractorを使ったイケているソースコードではありません。

次のプログラムを実装します。 1秒毎にカウントアップします。 ユーザーがEnterキーを押したらカウンターをリセットします。

完成形

# ユーザー入力を待つRactor
input = Ractor.new do
  loop do
    gets
    Ractor.yield :reset
  end
end

# クロックイベントを発生するRactor
clock = Ractor.new do
  loop do
    Ractor.yield nil
    sleep 1
  end
end

# 初期値
val = 0
loop do
  # ユーザー入力とクロックを両方待ちます。
  _, flag = Ractor.select input, clock

  # イベントがユーザー入力だったら値をリセットします。
  val = 0 if flag == :reset

  # カウントアップします。
  val += 1
  p val
end

失敗作

処女作

ユーザー入力があったときだけ、カウンターから帰ってくる値を0に戻さそうと思って書きました。

input = Ractor.new do
  loop do
    gets
    Ractor.yield 0
  end
end

counter = Ractor.new do
  loop do
    Ractor.yield Ractor.recv + 1
    sleep 1
  end
end

counter << 0
loop do
  _, v = Ractor.select input, counter
  p v
  counter << v
end

実際はこんな感じにうごきます。

~ docker run --rm -it -v (pwd):/ractor wakaba260/ruby-ractor-dev ruby ractor/input_and_timer.rb
1
2

0
3
1
4

リセットされた値とされていない値が両方出力されます。 Ractorはキューを持っているので、counter << vした値は上書きされることなく保存されています。

debounceを実装

最新の値だけがほしいので、debounceが実装できれば良さそうです。

input = Ractor.new do
  loop do
    gets
    Ractor.yield 0
  end
end

counter = Ractor.new do
  v = 0
  loop do

    v = Ractor.recv
    Ractor.yield v + 1
    sleep 1
  end
end

debounce = Ractor.new do
  prev = Time.now.to_i
  loop do
    v = Ractor.recv

    now = Time.now.to_i
    if now - prev > 0.1
      p v
      prev = now
    end
  end
end

counter << 0
debounce << 0
loop do
  _, v = Ractor.select input, counter
  counter << v
  debounce << v
end

実際には、これも期待通りには動きません。 毎回counter << vしているので、counterのキューに全部の値が貯まります。

どうやらcounterで1秒に1加算しているのがよくなさそうです。 counterを1秒毎にイベントを発火するclockにし、mainで加算しました。

感想

Ractorインスタンスからグローバルな値を変更できないので、Ractor.yieldを使って値を親に返す必要があります。 その代わり、レールに乗りさせすれば、Ractor.selectを使って、イベントの待ち合わせを簡単に書けます。

スレッドで書くと次のようになります。

queue = Queue.new

input = Thread.new do
  loop do
    gets
    queue << :reset
  end
end

clock = Thread.new do
  loop do
    queue << nil
    sleep 1
  end
end

val = 0
loop do
  flag = queue.pop
  val = 0 if flag == :reset
  val += 1
  p val
end

今回の使い方からは「最初からキューを持っているスレッド」というイメージを持ちました。

Ractorを使ってプログラムを書く練習    

注意:Ractorならではとか、計算効率の良さとか考えていません。

1から10までを足す

# 最終結果送り先のRactor
CR = Ractor.current
r = Ractor.new { CR << Ractor.recv }

1.upto(10) do |i|
  # 数を足すRactorを作ってポインタを置き換える
  r = Ractor.new(r, i) do |next_r, i|
    # 担当の整数を足して次のRactorに送る
    next_r << Ractor.recv + i
  end
end

r << 0 # 計算開始。初期値を送る
p Ractor.recv # 結果を出力

<<でRactorインスタンスに値を送ります。 Ractor.recvで値を受け取ります。 並列に動いていません。

10個のRactorを並列に動かす

# 10個のRactorを作る
ractor_list = (1..10).map do |i|
  Ractor.new i do |i|
    i
  end
end

# 10個のRactorの終わりを待つ
until ractor_list.empty?
  # いずれかのRactorの結果を待つ
  r, val = Ractor.select(*ractor_list)
  p val

  # 実行の終わったRactorは、もう待たない
  ractor_list.delete r
end

Ractor.selectでRactorインスタンスの終了を待ちます。 Ractor.newに渡したブロックの戻り値が得られます。

参考

https://github.com/ko1/ruby/blob/ractor/ractor.ja.md

Ractorを試す

Ruby3 さみっと online - connpass を見ていました。 Ractorの発表を見て使ってみたくなったので環境を整えてみます。

ビルドする

https://github.com/ko1/ruby/tree/ractorソースコードをビルドします。

git clone git@github.com:ko1/ruby.git ractor
cd ractor
git checkout ractor
autconf
./configure
make
ractor.c:1105:13: error: implicit conversion loses integer precision: 'VALUE' (aka 'unsigned long') to 'uint32_t' (aka 'unsigned int') [-Werror,-Wshorten-64-to-32]
    r->id = ractor_next_id();

が出ます。

Mac で ruby-1.9.3-p385 がビルドできない問題を素早く解決する - Qiita を参考にして環境変数CFLAGS=-Wno-error=shorten-64-to-32を指定します。

CFLAGS=-Wno-error=shorten-64-to-32 ./configure
make

コンパイルは成功しました。 rubyコマンドを実行するとrubygems.rbが見つからずにエラーになります。

~ ./ruby
Traceback (most recent call last):
    1: from <internal:gem_prelude>:one:in `<internal:gem_prelude>'
<internal:gem_prelude>:one:in `require': cannot load such file -- rubygems.rb (LoadError)

make installも実行する必要があるようです。 PCの環境が壊れるかもと考えると億劫です。

Dockerイメージを使う

id:wakaba260yen さんがDockerイメージを用意していてくれました。 使ってみます。

~ docker run --rm  wakaba260/ruby-ractor-dev ruby -e 'r = Ractor.new(42) { p _1 }; r.take'
42

たとえば ring.rb を実行するには次のように実行します。

~ docker run --rm -v (pwd):/ractor wakaba260/ruby-ractor-dev ruby ractor/ring.rb
:setup_ok
1
:fin

参考資料

http://atdot.net/~ko1/activities/2020_ruby3summit.pdf

ruby/ractor.ja.md at ractor · ko1/ruby · GitHub

自宅作業環境の整備

新型コロナウイルスの影響で在宅勤務しています。 3月3日から、少しずつ作業環境を整備しています。

ノイズキャンセリングヘッドホン

オフィス作業時代に、作業集中用に使っていたものをそのまま使っています。

オンラインミーティングしている相手の声が聞き取りにくい時や、自宅内の騒音が大きいときに使っています。 騒音の音が自分には聞こえなくなるのですが、オンラインミーティングの相手には流れているかもしれないのが、気になる点です。

www.sony.jp

モバイルディスプレイ 15,000円

作業に使っているノートPCに外付けディスプレイが欲しかったので買いました。

決め手は値段です。 自宅作業を始めた初期に買ったので、なるべく安いものを選びました。 今考えると、もう少し大きな据置ディスプレイを買ってもよかったかもしれません。

モバイルディスプレイの利点は作業終了後に片付けやすい点です。

同梱のUSBケーブルが2, 3日で壊れたので、追加でUSBケーブルを買いました。

肘置き 4,500円

自宅作業だと右の肩と背中が痛くなるので、対策で買いました。 使ってみたら、机と同じ高さに固定なのは予想より気になります。 1,000円ちょっと追加して、高さが調整できるタイプにすればよかったです。

ないよりはマシですが、これだけで、肩、背中の疲労がなくなることはないです。 予算ができたら、机と椅子を整備しようと思います。

USBマイク 5,000円

PC搭載のマイクだとキーボードのタッチ音を拾いやすいのと、自宅内の雑音も拾ってしまいます。 ヘッドホンにもマイクがついていたのですがBluetooth接続のノイズが乗るので微妙でした。

外付けの単一指向性マイクを買いました。 比較的安くて、アームやポップガードまでついているので、最初の1個目として良さそうと思い選びました。

どれぐらいの効果があるかは、自分が聞く側でないので、わかりません。 録音して試した感じでは、PC搭載のマイクよりはマシだと思います。 ヘッドセットのマイクだと飲み物を飲む音も拾うので、ヘッドセットより外部マイクの方がいいのかなあ?と思っています。

コンデンサマイクよりダイナミックマイクの方が単一指向性が強いらしいので、ダイナミックマイクの方が良いのかもしれません。 よくわかりません。

天板 2,000円

天板の広さ78x29cm、高さ80cm本棚の上で作業していました。 もう少し広い作業スペースがほしかったので、アイリスオーヤマの化粧板を買いました。 本棚の上に載せて天板にしています。

送料込みで2,000円、安い!

item.rakuten.co.jp

新型コロナウイルスの患者数を予測する

東京都 新型コロナウイルス陽性患者発表詳細 - データセット - 東京都オープンデータカタログサイト から患者の情報を取ってきます。 グーグルスプレッドシートをつかってグラフを書きます。

f:id:ledsun:20200329151852p:plain
新型コロナウイルスの患者数推移

24〜28日がほぼ直線になっていることが読み取れます。 赤線の部分です。 この部分だけをグラフにします。

f:id:ledsun:20200329152050p:plain
 3/24-3/28の傾向から予測する患者数

このまま線形に推移すると仮定してトレンドラインを引きます。赤線の部分です。

  • 4/1には500人
  • 4/11には1000人
  • 5月には2000人

と、予測できます。

グラフ作成に使ったスプレッドシートです。 20200329の患者数推移予測 - Google スプレッドシート

都内の感染症指定医療機関で何が起こっているのか(忽那賢志) - 個人 - Yahoo!ニュース

無症状の患者、すでに改善した患者、軽症の患者については隔離を行わずに自宅療養とすべきと考えます。

すべての感染者を隔離し、封じ込めを狙うフェーズはすでに過ぎており、今は中等症〜重症例の医療を必要とする患者を医療機関で診療すべき段階に来ています。

は、本当にそうなのだろうと思います。 つまり、

  1. 感染者を隔離できない
  2. 感染者増加ペースはおさまらない
  3. 今後もいまの外出自粛が続く

のでしょう。気長に頑張りたいと思います。

新型コロナウイルスの患者数増加ペースが指数関数的増加を少し下回る

3月27日が40人増加で、指数関数的増加を少し下回っています。 指数関数的増加をしていたら累計で400人台前半、28日には400人を超えている見込みでした。 少し希望が見えて来ました。 なるべく家族以外との接触を控えて、増加ペースの鈍化に協力していきたいところです。

f:id:ledsun:20200328075259p:plain
東京都の新型コロナウイルス「COVID-19」感染者数推移

都内の最新感染動向 | 東京都 新型コロナウイルス感染症対策サイトより

日テレのグラフはちょっと過激に感じすぎる気がしたので、変えました。

新型コロナウイルスの勢い衰えず

感染者数は、3/22に勢いが衰えたかと思いきや、見事な指数関数的増加中。この先1〜2週間では収まらなさそうです。

f:id:ledsun:20200326101430p:plain
東京都の新型コロナウイルス「COVID-19」感染者数推移

データとグラフで見る「新型コロナウイルス」感染状況(国内版)|日テレNEWS24 より

RubyMineのターミナルが壊れた話

RubyMineのターミナルのシェルがおかしいです。

パスから/usr/local/binが消えてしまった。rbenvが動かせません

rbenvが動かせません。

シェルにはfish-shellを使っています。 fish-shellの起動時に、次のエラーメッセージが出ます。

~/.config/fish/config.fish (line 1): 
rbenv init -|psub
^
in command substitution
        called on line 39 of file ~/.config/fish/config.fish
from sourcing file ~/.config/fish/config.fish
        called on line 185 of file /usr/local/Cellar/fish/3.1.0/share/fish/config.fish
in function '.' with arguments '/Users/shigerunakajima/.config/fish/config.fish'
        called on line 20 of file /Applications/RubyMine.app/Contents/plugins/terminal/fish/config.fish
from sourcing file /Applications/RubyMine.app/Contents/plugins/terminal/fish/config.fish
        called during startup
~/.config/fish/config.fish: Unknown error while evaluating command substitution
from sourcing file ~/.config/fish/config.fish
        called on line 185 of file /usr/local/Cellar/fish/3.1.0/share/fish/config.fish
in function '.' with arguments '/Users/shigerunakajima/.config/fish/config.fish'
        called on line 20 of file /Applications/RubyMine.app/Contents/plugins/terminal/fish/config.fish
from sourcing file /Applications/RubyMine.app/Contents/plugins/terminal/fish/config.fish
        called during startup
Welcome to fish, the friendly interactive shell

~/.config/fish/config.fish の39行目の status --is-interactive; and source (rbenv init -|psub) でエラーが出ています。

これはなんのためのコードでしょうか? rbenv init - は、つぎのようなrbenvのPATHなどを初期化するシェルスクリプトを出力します。

set -gx PATH '/Users/shigerunakajima/.rbenv/shims' $PATH
set -gx RBENV_SHELL fish
source '/usr/local/Cellar/rbenv/1.1.2/libexec/../completions/rbenv.fish'
command rbenv rehash 2>/dev/null
function rbenv
  set command $argv[1]
  set -e argv[1]

  switch "$command"
  case rehash shell
    source (rbenv "sh-$command" $argv|psub)
  case '*'
    command rbenv "$command" $argv
  end
end

psubというのはコマンドの実行結果を名前付きパイプ(仮想ファイルみたいなもの?)に書き込むfishのコマンドです。

たとえば、次のようにうごきます。

~ echo (rbenv init -|psub)
/var/folders/3m/82sq4tnn62d59h7h6wzn6w340000gn/T//.psub.s8MwzlfDwx

この出力された名前付きパイプをsourceすると、rbenvが初期化されます。

結局 /usr/local/bin/rbenv にパスが通ってないからエラーになるようです。

PATHに/usr/local/binを追加してみましょう。

fishらしく set -U fish_user_paths /usr/local/bin $fish_user_paths でユニバーサルなPATHに追加してみても、変わりません。

~/.config/fish/config.fish の39行目の直前で set -g fish_user_paths "/usr/local/bin /usr/local/opt/ruby/bin" $fish_user_paths してPATHに追加してみます。変わりません。

~/.config/fish/config.fish の39行目の直前で set PATH /usr/local/bin $PATH すると、うごきました!

config.fishなのにfish-shellのコマンドが効かない、エラーが出ないのに効果が出ないのは、不思議な感じがします。

蛇足 RubyMineのPATHどこからくるのか?(不明)

path_helperについて。Mac OSX 版 - それマグで! によると、 mac のパス設定は path_helper というのがあり、path_helper は /etc/paths のをPATHに反映するらしい。

/etc/pathsを確認してみます。

~ cat /etc/paths
/usr/local/bin
/usr/bin
/bin
/usr/local/sbin
/usr/sbin
/sbin

/usr/local/bin が入っている。RubyMineがpath_helperを使わないのかもしれません。

RubyMineの設定をみてみます。

f:id:ledsun:20200304043810p:plain
RubyMineの設定

システムの環境変数を使うって有るし、/usr/local/bin も入っています・・・

Rubyのコンパイルに挑戦した記録

OpenSSLがみつからない

GithubのREADMEに従って

git clone git@github.com:ruby/ruby.git

して

./configure
make
make check

すると、次のような警告メッセージがでました。

*** Following extensions are not compiled:
openssl:
    Could not be configured. It will not be installed.
    /Users/shigerunakajima/ruby/ext/openssl/extconf.rb:97: OpenSSL library could not be found. You might want to use --with-openssl-dir=<dir> option to specify the prefix where OpenSSL is installed.
    Check ext/openssl/mkmf.log for more details.
*** Fix the problems, then remove these directories and try again if you want.

OpenSSLが見つからないみたいです。

brew info opensslすると

For compilers to find openssl@1.1 you may need to set:
  set -gx LDFLAGS "-L/usr/local/opt/openssl@1.1/lib"
  set -gx CPPFLAGS "-I/usr/local/opt/openssl@1.1/include"

と表示されたので、この設定をして

./configure
make clean
make

しました。 該当の警告メッセージは消えました。

テスト失敗

make check

すると、今度は次のテストが失敗しました。

1)
Socket.getnameinfo using IPv6 using a 3 element Array as the first argument using NI_NUMERICHOST as the flag returns an Array containing the numeric hostname and service name FAILED
Expected ["::ffff:127.0.0.1", "ftp"] == ["::1", "ftp"]
to be truthy but was false
/Users/shigerunakajima/ruby/spec/ruby/library/socket/socket/getnameinfo_spec.rb:111:in `block (6 levels) in <top (required)>'
/Users/shigerunakajima/ruby/spec/ruby/library/socket/socket/getnameinfo_spec.rb:65:in `<top (required)>'

該当のテストコードは https://github.com/ruby/ruby/blob/master/spec/ruby/library/socket/socket/getnameinfo_spec.rb#L109-L113 です。

describe 'using NI_NUMERICHOST as the flag' do
  it 'returns an Array containing the numeric hostname and service name' do
    Socket.getnameinfo(@addr, Socket::NI_NUMERICHOST).should == [ip_address, 'ftp']
  end
end

ソースコード追っかけて要約すると、このテストコードは

Socket.getnameinfo([Socket::AF_INET6, 21, 'localhost'], Socket::NI_NUMERICHOST)

[":1", "ftp"] を返すことを期待しています。

irbを使ってためしてみます。

irb(main):012:0> Socket.getnameinfo([Socket::AF_INET6, 21, 'localhost'], Socket::NI_NUMERICHOST)
=> ["::ffff:127.0.0.1", "ftp"]

なるほど["::ffff:127.0.0.1", "ftp"]が返ってきています。 それでテストが失敗しているようです。

ネットワークを変えると

ネットワークをLANからスマートフォンテザリングに変更して試してみます。

irb(main):003:0> Socket.getnameinfo([Socket::AF_INET6, 21, 'localhost'], Socket:
:NI_NUMERICHOST)
=> ["::1", "ftp"]

ネットワーク環境依存しているみたいです。

2019年のふりかえりと2020の目標

全体的に

私事に掛かるリソースが増えたので、残されたリソースでどうやりくりしたものか工夫した一年でした。

認知度

認知度に関しては、自分の能力以上に認知度が上がっても辛そうなので、一番最初にあきらめました。 その結果が、登壇数0です。

認知度を上げて、期待値に答えるように自分をストレッチするやり方もあると思います。 そのためのリソースを捻出しにくい状況だったので、やりませんでした。

技術力

業務で新しいツールを使って引き出しは増えました。

実感としては技術力が上がった感はありません。 今更使えるツールが増えてもそんなに技術力は上がらなさそうです。 ツールを使うこと自体は案件中にキャッチアップできるので、予め使えるようになっていることのメリットが感じられません。

理解しているレイヤーを増やすようなアプローチが必要かな?と思っています。 「作って理解するOS」を読み始めました。まだ途中です。

2020年には、実装するところまでやりきりたいです。 2017年に「RubyでつくるRuby 」を読んだときは、考え方を応用してできることがぐっと増えたので、同じような効果を期待しています。

影響力

javascriptingの運営で、自分以外の人がPullRequestを送りやすくするにはどうすればいいか?と工夫し始めました。

2020年は、リアルな人間関係、具体的に職場で「ミドルクラスのエンジニアをシニアエンジニアにする」方法にチャレンジしたいと思っています。

職業プログラマを初めて4,5年経つと一人で仕事できるようになります。 ここではこの辺をミドルクラスとします。 ミドルクラスのエンジニアが、学習をやめてしまうのか、学習の仕方を身につけられていないのか、伸び止まる現象をみることがあります。 たぶん「プログラマ35歳定年説」というのは、この現象のことかな?と思います。

2020年は、この壁を乗り越えるサポートの仕方を見つけようと思います。

本当はユーザーが多いOSSを作ったりとか、新しい開発思想をまとめたりとかできるといいと思うんですが、現状の僕が1番影響力を発揮できそうなのは、この辺をじゃないか?と思っています。

その他

会社員としてのもう一つの目標は「上司を上手く使う」です。 上司にどう動いてもらえると効果的なのかを上手く説明できるようになりたいです。

あとは健康。健康は大事です。2019年の本厄は無事乗り切りました。後厄も大過なくやり過ごしたいものです。

登壇

今年は0回です。 去年の5回に比べるだいぶ減りました。

同人誌執筆

techbookfest.org

Ruby並行並列大全」という同人誌を書きました。 Rubyの標準ライブラリでできる並行処理、並列処理を一通り網羅して説明した本です。 在庫はだいぶ残っているのですが、電子版も公開していないですし、委託販売もしていないというよくわからない事態になっています。 次の販売予定は決まっていません。

貰った感想では「concurrent-rubyeventmachineのようなライブラリの使いかた解説がほしい」というものがありました。 それらのライブラリをバリバリ使うようになったら書いてみたいと思います。

とはいえ、2020年は同人誌執筆を一旦お休みするつもりです。

OSS活動

javascriptingのメンテナンス

github.com

Nodeコミュニティのはworkshopperという、CLIJavaScriptプログラミング入門のCLIプログラムがあります。 そのうち一番入門よりのjavascriptingのメンテナンス権を5年位前から持っていました。 今年は、やる気を出してメンテナンスしはじめました。

いまさらソースコードに手を入れる必要は殆どないので、ソースコードの修正よりは、issueの整理に力を入れていました。 issueを閉じて1ページに収めました。 軽微な修正をしたり、議論が止まっているissueを閉じました。 また、残ったissueにはlabelをつけて、プロジェクトの状況をわかりやすくしました。

その結果、イタリア、ブラジル、ペルーあたりからPullRequestをもらいました。 javascriptingは問題文や回答文が多言語化されています。 機能への貢献より、文章を翻訳するために、その言語を使える人たちに協力してもらう必要があります。 協力してもらいやすい環境ができたのは良かったと思います。

workshopperは日本では完全に下火です。 英語圏でもあまり使われていないように思います。 Node.jsのブームが一巡したからではないかと思います。 一方で、ブラジル、ペルーでは、まだ需要があることがわかりました。 興味深いです。

いつかペルーのエンジニアと会ってみたいものです。

creekにPullRequest

github.com

xlsxファイルフォーマットでは、日本語の「ふりがな」が扱えます。 Excelで日本語を入力するとIMEの変換情報から「ふりがな」を取得して、自動的にxlsx内に埋め込みます。 便利なのだと思うのですが、creekというxlsxファイルのパーサーは、意図せずにセルの値に「ふりがな」をくっつけて返していました。 修正しました。

ふりがなに関するxlsxファイルフォーマットの機能に、英語圏の開発者に気づけというのは無茶な話でね。

ハック

autodocでOpenAPIドキュメント生成

github.com

autodocというAPIサーバーの使用例ドキュメントをRequest Specから生成するGemがあります。 これを元に、Request SpecからOpenAPIドキュメントを生成する、Railsアプリケーションのコントローラーファイルを生成する機能を追加しました。 APIの詳細をあとから書くために、コントローラーファイルを生成しています。 ファイルを修正してから、特定のコントローラーを呼び出すとOpenAPIドキュメントを生成します。

API定義の集め方やコントローラーを呼び出す使いかたがイマイチ気に入っていないのでPull Requestにしていません。

lhalibのRuby 2.0対応

github.com

RubyLZHファイルを解凍でるGemがあります。 C拡張ライブラリです。 Ruby 2.0でRubyのC拡張APIが変わりました。 その変更に対応していないため、Ruby 2.0以降ではGemのインストールに失敗します。 C拡張のAPIを修正して、Gemインストール可能にしました。

RubyGems.org | your community gem hostで公開されているgemはないので、Pull Requestは作成していません。

自作プログラム

github.com

「GemfileやGemfile.lockファイルを解析すれば、どんなGemが使われているのか調べられそう」と思って作ったRubyスクリプトです。

github.com

「bootstrap-select-railsというGemをTurbolinks 5に対応するハックが上手くうごかない」という情報を聞きつけて作成した サンプルアプリケーションです。

github.com

管理職業務を少し軽減するスクリプトです。

読書

エンジニアの知的生産術

学習モデルの考え方が参考になりました。

作って理解するOS

コンピューターサイエンス再入門として楽しく読んでいます。 大学の授業でやったけど理解があやふやなところを復習しています。 なんとなく理解が不安なところを理解しているかどうか確認できるので、満足感が高いです。

解説が丁寧なので、この本だけ読んでいればまあまあ理解が進むので楽です。 一方でページ数が多いので、読むのには時間がかかります。 5分の1ぐらい読みました。引き続き読んでいこうと思います。

「やさしさ」という技術

結論は当たり前の話なんですけど、どう論理的に說明するかという本です。 途中まで読んで、今すぐに役立つ本ではないかなあ?というところでした。

理論と事例でわかる自己肯定感

booth.pm

自己肯定感が低いときは「これでよい」だけではダメで、どっかでギャンブルして「とても良い」を感じる必要がある、といった記述があり、目から鱗でした。

英語

TOEICの結果です。

受験日 点数
2019/1/1 675
2019/3/10 755
2019/09/29 710

コンスタントに700点取れているのは良い点です。 もう少し目に見えて成果が欲しいのですが、英語の勉強を日常習慣に組み込まないと、なんともならない感じです。

Qiita

ledsun - Qiita

Tips的な内容はブログ読者より扱っている技術に興味がある人に届くと良さそうなので、 ブログでなくQiitaに書くことにしています。

12件。 2018年の16件に比べ少し減りました。

SameSite cookieについての勉強メモ

発端

Google Chromeの開発コンソールを有効にして、Webアプリケーションを開発しているときに次のような警告メッセージが表示されました。 これは一体どういう意味で、何に注意すればいいのでしょうか?

f:id:ledsun:20191202095526p:plain
Google Chromeの表示する警告

A cookie associated with a cross-site resource at http://pubannotation.org/ was set without the SameSite attribute. A future release of Chrome will only deliver cookies with cross-site requests if they are set with SameSite=None and Secure. You can review cookies in developer tools under Application>Storage>Cookies and see more details at https://www.chromestatus.com/feature/5088147346030592 and https://www.chromestatus.com/feature/5633521622188032.

Cookies default to SameSite=Lax - Chrome Platform Status を見ろと書いてあるので、みてみましょう。

Cookies default to SameSite=Lax

Treat cookies as SameSite=Lax by default if no SameSite attribute is specified.

CookiesをデフォルトでSameSite=Laxとして扱うと書いてあります。 SameSite=Lax とは何でしょうか?

SameSite=Lax

SameSite=Laxを検索するとCross-Site Request Forgery is dead!で紹介されています。 CSRF対策のようです。

日本語の解説もあります。

CSRF

対策したい問題がわからないと、対応策も理解できません。 CSRFはどのような攻撃でしたっけ?

安全なウェブサイトの作り方:IPA 独立行政法人 情報処理推進機構にあるPDF資料「安全なウェブサイトの作り方」のP30に「CSRF(クロスサイト・リクエスト・フォージェリ)」の解説があります。

あるサイトAへのログイン状態を維持したまま、悪意あるサイトB上のサイトAへのリンクをクリックした際に、サイトAがログインしたユーザーから意図したの操作だと誤認してしまう問題です。

よくある対応は、次のようなものです

  1. 変更が必要な処理はHTTPリクエストのメソッドをPOSTにする
  2. フォーム上にテンポラリーなトークンをhiddenパラメーターとして埋め込んでおく
  3. サーバーはHTTPリクエストに有効なトークンが含まれているか検証する

これによりユーザーのリクエストが、正規のフォームからのものであることが確認できます。

ふたたびSameSite=Lax

既知の対応策があるにも関わらずSameSite=Laxが道入されるのはなぜでしょうか? Cross-Site Request Forgery is dead! にProblemという章があります。

The trouble is though that these both put some kind of requirement on the site to implement and maintain the solution.

訳せば

問題は、これらの両方がソリューションを実装および維持するためにサイトに何らかの要件を課すことです。

そりゃまあ、Webサイト構築時に何らかの工夫をするよりは、ブラウザが対応してくれた方が嬉しいですよね。 で、具体的にはどのように使えばよいのでしょうか?

SameSite属性は次のように書きます。

Set-Cookie: sess=abc123; path=/; SameSite=lax

SameSite属性には次の2つの値があります。

  • Strict
  • LaxSameSite

SameSite=Strict

SameSite=Strict モードでは、オリジンが異なるサイトにクッキーを一切送信しません。他のサイトにあるリンクをクリックして対象の遷移してきた場合でも、送信しません。 つまり、ユーザーがログイン済みでも、クッキーを送信しないので、再度ログインを要求します。

このモードは、AmazonFacebookのように、認証クッキーが次の2段階に分かれているサイトでつかうことを想定しています。

  • ログイン用のクッキー
  • 決済や情報更新用のクッキー

後者のクッキーを SameSite=Strict モードにすることで、CSRF攻撃を確実に防ぎます。

SameSite=Lax

SameSite=Laxモードでは、 GET, HEAD, OPTIONS and TRACE メソッドのときはクッキーを送ります。 ログイン済みのユーザが、他サイトのリンクをクリックして遷移してきた場合は、クッキーが送信されるので、サーバーはログイン済みとして扱います。

この場合、サイトは変更を行うリクエストにはPOSTメソッドをつかうように実装します。 POSTメソッドをつかっている限り、CSRF攻撃を防ぎます。

Chromeの出す警告の意味

以上の知識を踏まえて、Chromeの警告の意味を考えてみましょう。

A cookie associated with a cross-site resource at http://pubannotation.org/ was set without the SameSite attribute. A future release of Chrome will only deliver cookies with cross-site requests if they are set with SameSite=None and Secure. You can review cookies in developer tools under Application>Storage>Cookies and see more details at https://www.chromestatus.com/feature/5088147346030592 and https://www.chromestatus.com/feature/5633521622188032.

翻訳すると

http://pubannotation.org/のクロスサイトリソースに関連付けられたCookieは、 SameSite属性なしで設定されました。 Chromeの今後のリリースでは、「SameSite = None」と「Secure」が設定されている場合にのみ、クロスサイトリクエストでCookieを配信します。開発者ツールのCookieを[アプリケーション]> [ストレージ]> [Cookies]で確認し、https://www.chromestatus.com/feature/5088147346030592およびhttps://www.chromestatus.com/feature/5633521622188032で詳細を確認できます。

この警告がでている時、ブラウザはXMLHttpRequestを使って自サイト以外のサイト(http://pubannotation.org/)からデータを取得しています。 このリクエストに対して、「クロスサイトリソースに関連付けられたCookie」と言っています。 特に、Chrome 80からは明示的に SameSite=NoneSecure が設定されていないクッキーはクロスオリジンのサイトには送信しなくなるという警告です。

このアプリケーションの用途ではクッキーを送信する必要はありません。 しかし http://pubannotation.org/ へのHTTPリクエストのレスポンスには Set-Cookieヘッダーがついています。 それで、Google Chromeは親切に教えてくれました。

Secure フラグ

ついでに警告に一緒に出ているSecureフラグはTough Cookies によると、次の形式で指定します。

Set-Cookie: sess=123; path=/; Secure

その意味は

The Secure flag is another optional flag that can be included in a Set-Cookie header that instructs the browser that the cookie must only ever be sent over a secure connection.

翻訳すると

ブラウザはセキュア(https)でない接続ではクッキーの情報を送りません。

蛇足

Cookiesの仕様、クソむずい・・・

https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-5.3 を見れば網羅できるのでしょうか?

RSpecでSharedExamplesを使ったときのエラー表示の現状

RSpecで、SharedExamplesを使うと、失敗したテストの行番号の代わりに [1:2:2:3:1] みたいなのが表示されて、どのテストを見ればいいのかわかりにくくなるのが辛いです。 2015年からInclude line numbers as well as index in output · Issue #2119 · rspec/rspec-coreがあります。

https://github.com/rspec/rspec-core/issues/2119#issuecomment-173664887 の辺りを ざっくり要約すると

SharedExampleを使ったテストが失敗した時に、見たい場所はspecを定義している場所とexampleを定義している場所の2箇所ある。 どっちを見たいかは人によって変わる。 specを定義している場所を表示すると、それはそれで不便。 Summary output of shared failed examples is useless when examples defined in external file · Issue #793 · rspec/rspec-coreで議論済み。 かと、言って両方を表示するのはSummaryとしては冗長すぎて、それも不便でしょ?

今週の作業メモ

JavaScripting

runコマンドのバグ

https://github.com/workshopper/javascripting/issues/218 javascripting run hogohoge.jsを実行するとたしかに[object Object]が表示されます。 javascriptingではrunコマンドは使っていません。

https://github.com/workshopper/workshopper-adventure/blob/master/index.js#L264 workshopper-adventure 経由です。 アンドキュメントなコマンド発見して実行して上手く動かないと言われるのは心外だが、不親切なのも確か。 こういうときはどうするのがいいのかなあ? コマンドをworkshopperに投げる前に検査してUsage表示するとか?

helpコマンドでrunコマンドでの説明が出てくるのか・・・全然アンドキュメントじゃないな

~ bin/javascripting help

 # JAVASCRIPTING

 ## javascripting のワークショップに対して質問がありますか?

  エキスパートチームはあなたの質問を待っています。あなたもNode.jsのマスター
  になれるようにここに質問(New Issue)をしてみてください:

     https://github.com/nodeschool/discussions/issues

  どんな質問でもどうぞ!

  一般的なNode.jsのヘルプが必要であればNode.jsの日本のGoogleグループがありま
  す:

     https://groups.google.com/forum/#!forum/nodejs_jp

 ## javascriptingのバグが見つかった場合または貢献したい場合:

  javascriptingのリポジトリはここにあります:

     https://github.com/sethvincent/javascripting.git

  バグの報告やPullリクエストは日本語でいつでもどうぞ!

 ## 使い方

  javascripting .....................
  ワークショップを選択する対話的メニューを表示します。

  javascripting list ................
  登録されているすべてのワークショップの名称一覧を表示します。

  javascripting select NAME ......... ワークショップを選択します。

  javascripting current .............
  現在選択されているワークショップの名称を表示します。

  javascripting print ...............
  現在選択されているワークショップのガイドを表示します。

  javascripting next ................
  現在選択されているワークショップの次の未修了のワークショップのガイドを表示
  します。

  javascripting reset ...............
  ワークショップの進捗状況をリセットします。

  javascripting run program.js ......
  あなたが作成したプログラムを実行します。

  javascripting verify program.js ...
  あなたが作成したプログラムが正しいかを検証します。

  javascripting -l <language> .......
  システムで使用する言語を指定されたものに変更します。

ソースコードリーディング

javascriptingソースコードからjavascriptingworkshopperの役割分担を読みときます。 https://github.com/workshopper/javascripting/blob/master/index.jsworkshopper-adventureインスタンスを読み込んでいます。 サブコマンドによる分岐の全体的な制御フローは workshopper-adventure が担当します。

https://github.com/workshopper/javascripting/blob/master/index.js#L8 で、 menu.json に含まれる problem の一覧を読む。

https://github.com/workshopper/javascripting/blob/master/index.js#L12-L14problemごとに require('problems/introduction') な処理をする関数を作る。

https://github.com/workshopper/javascripting/blob/master/problems/introduction/index.jsrequire("../../lib/problem")(__dirname) なので 実質的に https://github.com/workshopper/javascripting/blob/master/lib/problem.js を実行している。

createProblem 関数で作った exercise を引数にして https://github.com/workshopper/workshopper-adventure/blob/master/index.js#L345 が呼ばれます。

この辺は VSCodeデバッグしながら追いかけています。 https://code.visualstudio.com/docs/nodejs/nodejs-debugging

{
  // IntelliSense を使用して利用可能な属性を学べます。
  // 既存の属性の説明をホバーして表示します。
  // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "program": "${workspaceFolder}/bin/javascripting",
      "args": ["verify", "index.js"]
    }
  ]
}

修正案

https://github.com/workshopper/javascripting/blob/master/lib/problem.js ではrunメソッドも呼んでいる。この辺をいじると、クリーンな修正ができるのだろうか?

デバッグするとたしかに run メソッドは呼ばれています。ただし、出力文字列はこで作っているわけでは無さそうです。 https://github.com/workshopper/javascripting/blob/master/lib/problem.js#L53-L55 workshopper-adventure が出力文字列は作っていそう、javascripting 側から上書きする方法があるのかな?

対処

単に run メソッドの定義を消せばよいだけだった。こんなに簡単な話だったとはね、ふふ

~ bin/javascripting run index.js
run is not required for this exercise.

github.com

今週の作業メモ

spring

Rails 5.2.3でrails consoleを実行すると、プロンプトが出る前で止まります。

~ bundle exec rails --version
Rails 5.2.3
~ bundle exec rails c
^CTraceback (most recent call last):
    15: from bin/rails:3:in `<main>'
    14: from bin/rails:3:in `load'
    13: from /Users/shigerunakajima/circular_dependency_detected_job/bin/spring:15:in `<top (required)>'
    12: from /Users/shigerunakajima/circular_dependency_detected_job/bin/spring:15:in `require'
    11: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/binstub.rb:11:in `<top (required)>'
    10: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/binstub.rb:11:in `load'
     9: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/bin/spring:49:in `<top (required)>'
     8: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client.rb:30:in `run'
     7: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/command.rb:7:in `call'
     6: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/rails.rb:24:in `call'
     5: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/command.rb:7:in `call'
     4: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/run.rb:35:in `call'
     3: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/run.rb:42:in `warm_run'
     2: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/run.rb:62:in `run'
     1: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/run.rb:117:in `verify_server_version'
/Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/run.rb:117:in `gets': Interrupt

https://github.com/rails/spring/blob/v2.1.0/lib/spring/client/run.rb#L117 ここのserver(どうもUnixソケットらしい)の読み込みで止まっている。

試しに、Gemfileからspringを消してみると、期待通りにプロンプトが表示されます。

~ bundle exec rails c
Loading development environment (Rails 5.2.3)
irb(main):001:0> 

@p_ck_ 曰く

springが悪さをするのに疲れ果てて最近は DISABLE_SPRING=1 してます 似たような状況のときは bin/spring stopをしたら直っていたので毎回それをしていた記憶があります

Bingo!

bootsnap

ruby 2.7-preview1でRails 6.0.0.rc2の rails --version を実行すると、エラーが起きます。

~ rails --version
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
Traceback (most recent call last):
        36: from bin/rails:3:in `<main>'
        35: from bin/rails:3:in `load'
        34: from /Users/shigerunakajima/my_first_rails_6/bin/spring:15:in `<top (required)>'
        33: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        32: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        31: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/lib/spring/binstub.rb:11:in `<top (required)>'
        30: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/lib/spring/binstub.rb:11:in `load'
        29: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/bin/spring:49:in `<top (required)>'
        28: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/lib/spring/client.rb:30:in `run'
        27: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/lib/spring/client/command.rb:7:in `call'
        26: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/lib/spring/client/rails.rb:28:in `call'
        25: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/lib/spring/client/rails.rb:28:in `load'
        24: from /Users/shigerunakajima/my_first_rails_6/bin/rails:8:in `<top (required)>'
        23: from /Users/shigerunakajima/my_first_rails_6/bin/rails:8:in `require_relative'
        22: from /Users/shigerunakajima/my_first_rails_6/config/boot.rb:4:in `<top (required)>'
        21: from /Users/shigerunakajima/my_first_rails_6/config/boot.rb:4:in `require'
        20: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/setup.rb:30:in `<top (required)>'
        19: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap.rb:30:in `setup'
        18: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/compile_cache.rb:9:in `setup'
        17: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:50:in `require_relative'
        16: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.0.rc2/lib/active_support/dependencies.rb:322:in `require'
        15: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.0.rc2/lib/active_support/dependencies.rb:288:in `load_dependency'
        14: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.0.rc2/lib/active_support/dependencies.rb:322:in `block in require'
        13: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:32:in `require'
        12: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `require_with_bootsnap_lfi'
        11: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register'
        10: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
         9: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
         8: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/compile_cache/iseq.rb:1:in `<top (required)>'
         7: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.0.rc2/lib/active_support/dependencies.rb:322:in `require'
         6: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.0.rc2/lib/active_support/dependencies.rb:288:in `load_dependency'
         5: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.0.rc2/lib/active_support/dependencies.rb:322:in `block in require'
         4: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:32:in `require'
         3: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `require_with_bootsnap_lfi'
         2: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register'
         1: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
/Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require': no implicit conversion of String into Integer (TypeError)

エラーが起きているのはこの辺 https://github.com/Shopify/bootsnap/blob/v1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb#L17-L24

エラーが起きるときの path の値は "bootsnap/bootsnap"です。 require_without_bootsnap は require の alias_methodなので、 require "bootsnap/bootsnap"で再現します。

~ irb
irb(main):001:0> require "bootsnap/bootsnap"
Traceback (most recent call last):
        7: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/bin/irb:23:in `<main>'
        6: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/bin/irb:23:in `load'
        5: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
        4: from (irb):1
        3: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:34:in `require'
        2: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:130:in `rescue in require'
        1: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:130:in `require'
TypeError (no implicit conversion of String into Integer)

これでRuby 2.7 + Rails 6ではなくRuby 2.7 + bootsnapの問題だと絞り込めました。bootsnapのissueを漁ると https://github.com/Shopify/bootsnap/issues/258 が 発見できます。 https://github.com/Shopify/bootsnap/issues/258#issuecomment-518726704 をみるとmasterブランチでは修正済みのようです。

https://github.com/Shopify/bootsnap/commit/a163c7cddfccd68277f6ea43b1831378509817c9 パッチの作者は須藤さんや!さすがや、俺の遥かに前を行っているッ!!! Ruby2.7からはRubyのリビジョン番号が数値からgitのSHAハッシュに変わったので、数値前提のソースコードが壊れたらしい。なるほど・・・そういう変更もあるのか。 bootsnap.c なので、bootsnapはC拡張なのねえ。

ていうかBootsnapって、なんなんだろう?名前しか知らないや

READMEによりますと、Rubyプログラムの起動を早くするためのgemです。大きなRailsアプリケーションだと、50%以上の高速化の実績があります。主な手法としては Kernel#require を上書きして、キャッシュファイルをつかうことで、読み込むrubyスクリプトの検索を速くします。 今回エラーが出ていた https://github.com/Shopify/bootsnap/blob/v1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb#L17-L24 は、まさにこのKernel#require を上書きしている部分!

もう一つの最適化手法にiSeq(コンパイル済みのRubyバイトコード)のキャッシュがあります。MessagePack形式でファイルに保存しておくことで、Ruby本体による「ファイルの読み込み〜コンパイル」を「ファイルの読み込み」だけにできます。iSeqはコンパイルしたRubyのバージョン(だけでなくリビジョン単位)によって変わるので、キャッシュが有効かどうか判断するには、Rubyのリビジョン番号が必要です。このリビジョン番号を取るところがRuby 2.7で動かなく成っていたのの修正が https://github.com/Shopify/bootsnap/commit/a163c7cddfccd68277f6ea43b1831378509817c9 なるほどー!

jQueryUI ダイアログ

jQueryUIダイアログには、閉じた時にその前に選択していた要素を再選択する機能があるようです。

Upon closing a dialog, focus is automatically returned to the element that had focus when the dialog was opened.

https://api.jqueryui.com/dialog/

なるほど・・・

closeするときに this.opener をfocusします。 https://github.com/jquery/jquery-ui/blob/master/ui/widgets/dialog.js#L224

this.opnerは開いたときの document.activeElement なのかな? https://github.com/jquery/jquery-ui/blob/master/ui/widgets/dialog.js#L273

$.ui.safeActiveElement は、IEのエラーを出なくした document.activeElement 取得関数です。 https://github.com/jquery/jquery-ui/blob/74f8a0ac952f6f45f773312292baef1c26d81300/ui/safe-active-element.js#L12