3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

listen が Encoding::CompatibilityError で死ぬ(Windows はつらいよ)

Posted at

概要

Windows の日本語環境では,パス名に非 ASCII 文字が含まれるとき,ディレクトリー監視を行うライブラリー listen がエラーを吐くことがある,という話。

listen は gem の形で配布されているライブラリーで,指定したディレクトリー以下を監視し,ファイルの変更,追加,削除があったらそのつど指定の処理を実行してくれる。

主要な OS で動作する。非常によく使われているようで,これに依存する gem も多い。

算譜

以下のようなファイルが存在するとする。

C:\でれくとり\ふぁいる.txt

問題の再現コードは以下のとおり。

gem "listen", "3.2.0"
require "listen"

target_dir = "C:/でれくとり"

listener = Listen.to(target_dir) do |modified, added, removed|
  puts "[modified]", modified
  puts "[added]", added
  puts "[removed]", removed
  puts
end

listener.start
sleep

C:\でれくとり というディレクトリーを監視して,ファイルの変更,追加,削除があったらそのパスを画面に表示するだけのもの。

Listen.to メソッドに監視対象のディレクトリーのパスを渡している。
短い時間サイクルごとに,(そのサイクル内で)ファイルの変更,追加,削除があったら,ブロックを実行してくれる。
時間サイクル内で複数のファイルが追加されたり,といったこともあるので,modified, added, removed はすべて配列である。該当ファイルが無ければ空配列になる。

オプションで動作を細かく指定することもできる。

Listen.to を実行しただけでは監視は始まらない。start メソッドで監視が始まる。

sleep(引数が無いので永遠に眠る)を置くことによって,Ctrl+C で止めるまでずっと監視が継続するようになる。
sleep が無いと一瞬でスクリプトが終了してしまう。

現象

さて,前節のスクリプトを Windows の日本語環境で起動すると,以下のような例外が発生する。

C:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/listen-3.2.0/lib/listen/record/entry.rb:38:in `join': incompatible character encodings: UTF-8 and Windows-31J (Encoding::CompatibilityError)

つまり,UTF-8 と Windows 31J1 の文字列を結合しようとして失敗する例のアレ2

例外は listen の内部で起こっている。
ユーザーが多く,継続的にメンテナンスされている印象の listen だが,どうしたことだろう。

原因

例外が発生した箇所は ここ

抜粋すると

def sys_path
  # Use full path in case someone uses chdir
  ::File.join(*[@root, @relative, @name].compact)
end

となっている。この File.join で例外が発生した。

今の場合,@root が UTF-8 で,@name が Windows 31J になっていた。
これを File.join で繋ごうとしたので「できるかっ!」と怒られたんである。

@root が UTF-8 だったのは,おそらく Listen.to に UTF-8 の文字列を渡したから。
@name のほうは listen が自分でディレクトリーを探索して拾ってきたわけだけど,場合によって Windows 31J だったり UTF-8 だったりするようだ。

そういう,文字コードがバラバラな可能性を考えずに繋ごうとしているわけね。

対策

とりあえず,応急処置としては,sys_pathFile.join を書き換えることになる。
いわゆるモンキーパッチ3というやつですな。

いずれの場合も,使われている箇所が内部なので,refinement のような生ぬるい手段は取れない。メソッドをもろに上書きしてやるのだ。ふふ。
どうせ今回作っていたのはライブラリーとかではなく小さなアプリケーションなので,書き換えたところで「思わぬ副作用」を心配する必要もない。

で,どういうわけかその時の私は sys_path を書き換えるという発想が無く,当然のように File.join を書き換えようとした。

File.join のエイリアスを作っておき,新しい File.join は引数の文字コードを揃えてから古いほうに丸投げしてやればいいだろう,と。

特異メソッドのエイリアスを作る方法がなかなか分からなかったけれど,特異クラス内でふつうに alias すればいいんだった。

つまり,以下のようなコードを追加した。

class << File
  alias _join join

  def join(*args)
    _join(*args.map{ |x| x.encode(Encoding::UTF_8) })
  end
end

引数を全部 UTF-8 に変換している。

これで解決した。エラーは出なくなった。

結語

Windows の日本語環境では,誰かの作ったライブラリー内で Encoding::CompatibilityError が出て死ぬ,ということがときどき起こる。たいがい UTF-8 と Windows 31J の文字列を結合しようとしたのが原因だ。

そういうときは,例外の発生箇所を調べてそこに至る各文字列の文字コードを調べていくことになる。めんどくさい。

たぶんライブラリー開発者のほとんどは macOS か Linux なのだ。そこでは非 UTF-8 文字列が不意に紛れ込むことが起こらないのだろう。
実際,文字コード絡みの issue を立てても「再現しない」「直せない」「直せたのかよく分からん」と言われることがある(それらより放置されることのほうが多いかも?)。

  1. Windows 31J は CP932 とか Codepage 32 とも呼ばれる。平たく言えば「Windows のシフト JIS」のこと。

  2. Windows Rubyist にはおなじみのアレである。

  3. 今年(2019 年)の春に亡くなった 著名漫画家 ではない。猿の股引きでもない。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?