彷徨えるフジワラ

年がら年中さまよってます

非 ASCII 文字出力により、Windows 上での Mercurial 実行が中断される問題

時間の無い方向けに、先に要点をまとめておきます(笑)。

Windows 環境で以下の条件が成立する場合、 Mercurial が非 ASCII 文字列を表示しようとすると、『表示失敗』扱いでコマンド実行が中断されます。

  • Windows 標準のコマンドプロンプト(cmd.exe)を使用していて:
    • chcp コマンドで、システム標準ではないコードページを設定(日本語 Windows の場合は 932 以外)し、且つ
    • フォント設定を『ラスタフォント』以外に設定している
  • 標準のコマンドプロンプト以外のターミナルソフトを使用している
    • TCC/LEConEmu *1で障害の発生が確認されています
    • ターミナルソフトの文字コード設定が Shift-JIS (cp932) なら大丈夫かも?

標準のコマンドプロンプトを、わざわざ前者のような設定で使用している人は少ないと思いますが、後者のようなサードパーティー製のターミナルソフトを使用してる人は少なくないと思いますので、注意してください。

環境変数による言語設定により、Mercurial 自身の出力するメッセージに関しては ASCII 文字列に限定できますが、コミットログ等で非 ASCII 文字列が使用されている場合は、非 ASCII 文字列の出力を抑止できない可能性があります。

このような場合は、Mercurial の出力を一旦ファイルにリダイレクトしてから、リダイレクト先のファイルを参照するようにしてください。

ちなみにこの問題の原因は、後述するように Python とターミナルソフトの組み合わせによるものなので、同様の問題は Mercurial 以外の Pythonn プログラムでも発生します。

以下、この問題に関する詳細ですので、技術的な話に興味のない方は、読み飛ばしてください(笑)

事の起こりは、コミットログに日本語(というか非 ASCII 文字)を使用しているリポジトリからの hg convert が正常終了しない、という報告でした。

Mercurial では、コミットログやコミットユーザ、ブランチ名、タグ名といった履歴に関するメタデータは、UTF-8 文字列として保持しており、画面出力の際は以下の指定に応じて文字コード変換を行います(詳細はこちらのエントリを参照)

但し、hg convert 実行時に限っては、上記の指定に関わらず、一時的に UTF-8 指定とみなして内部処理を行っています。これは、別形式リポジトリにアクセスする際に、連携用ライブラリ/外部コマンドを使用する都合からです。

ところが、メッセージ類は一時的に UTF-8 で出力される一方で、hg convert の実行経過情報として出力される「変換元リビジョンのコミットログ」出力は、元の文字コード指定に従って変換されたものが出力されます(下記コードの recode() 部分)

        # convert log message to local encoding without using
        # tolocal() because the encoding.encoding convert()
        # uses is 'utf-8'
        self.ui.status("%d %s\n" % (num, recode(desc)))

そのため、文字コード設定次第では、複数の文字コードが出力に混在する状態になるわけです。

以前、「Windows 上でのパスワード入力で、プロンプト部分だけが文字化けする」という下記の障害を修正した際に、WindowsAPI が非 ASCII なバイト列を上手く扱えないケースがあることを知ったので、文字コード混在時に出力を上手く捌けていないのでは?という仮定から調査を開始しました。

ところが実際に動作確認してみると、文字コードが混在するような出力をさせても、特に問題なく hg convert 実行が完了します。

2バイト目にバックスラッシュを含むような、一番問題を起こしそうな文字を出力するようなケースでも、期待に反して無事実行が完了してしまいました。

--quiet オプション指定や、出力リダイレクトを試してもらったところ、問題が発生する環境でも、無事 hg convert 実行が完了するとのことなので、非 ASCII 文字列の画面出力が原因であることは間違いないのですが……

試しに Mercurial の障害管理システムで類似事例が無いか調べてみたところ……ありました

障害が発生している環境について確認したところ、標準のコマンドプロンプト以外のターミナルソフトを使用していたとのことでしたが:

標準のコマンドプロンプトでも、以下の手順で再現できました。

  1. chcp 65001 でコードページを変更(cp65001 は UTF-8
  2. タイトルバーでの右クリック ⇒ 「プロパティ」でダイアログ表示
  3. 「フォント」タブで、「ラスターフォント」以外を選択
  4. Mercurial で非 ASCII 文字列を出力(例: LANGUAGE=ja な状態で hg version --traceback 実行)

Mercurial への障害報告の内容をざっと見る限りでは、どうやら Python の実装が原因となっているらしく、以下のような過程で、コマンド実行が中断されるようです。

  1. 指定されたバイト列を、PythonWriteConsoleOutputA() で書き出す
  2. コードページ/フォント設定次第で、非 ASCII なバイト列の出力で、WriteConsoleOutputA()指定バイト列のバイト数とは異なる値を返す
  3. Python 側では、要求バイト数と書き出しバイト数の差から、「書き出しエラー」とみなす
  4. エラー発生により、ファイルオブジェクト(= 標準出力)を close 扱いにする
  5. 次の標準出力への書き出しが、「close 済みファイルへの書き出し」扱いになる
  6. 想定外の I/O 例外により、コマンド実行が中断される

WriteConsoleOutputA() API の仕様や、ターミナルソフト実装における規約等を確認してないので、詳細に関しては何とも言えませんが、書き出し要求バイト数ではなく、表示した文字数(or バイト数)ベースで、戻り値を算出しているのかな?

条件が揃った場合は、コマンドプロンプトの組み込みコマンド dir も "The system cannot write to the specified device." というエラーで中断されてしまいます……

UNIX 的な感覚だと、書き出し指定バイト値と成功時の戻り値が異なるのには、ちょっと違和感がありますね。まぁ、ファイル出力と画面出力で API が違う時点で、UNIX 的には違和感てんこ盛りですが(笑)。

*1:ConEmu の Build 130913 〜 131129 のどこかの時点で問題が解消されたとの利用者の報告あり