株式会社クリアコード > ククログ

ククログ


片手でのキーボード入力を支援するソフトウェアについて

はじめに

怪我などで手首などを負傷してしまうと、満足に両手でのタイピングが行えない事態に陥ることがあります。
今回は、そんなときでも怪我した側の手にあまり負担をかけずに入力するための方法を紹介します。*1

Half KeyboardもしくはHalf QWERTY

片手だけで入力できるようにしようというアイデアはそう目新しいものではなく、昔からあるようです。

Half KeyboardもしくはHalf QWERTYなどで検索するといろいろでてくるので興味があれば調べてみるのもよいでしょう。
専用のキーボードが製品化されていたり、ソフトウェアで特定のキーが押されたらレイアウトを変更するというものもあります。

Mirrorboardの場合は、キー配列の定義を差し替えることでCapsLockを押したらキーレイアウトが左右反転するようになっています。

xhkによる片手キーボード入力

専用のキーボードの購入は敷居が高いので、ソフトウェアによる片手入力を試してみましょう。
今回そのうちの一つであるxhkを使ってみます。

xhkは次の手順でインストールできます。

% git clone https://github.com/kbingham/xhk.git
% cd xhk
% sudo apt install build-essential autoconf automake pkg-config
% sudo apt install libx11-dev libxi-dev libxtst-dev
% ./autogen.sh
% ./configure
% make
% sudo make install

使用するには、xhkコマンドを実行するだけです。(最初から反転した状態にするには -m オプションを指定します。)

% xhk

xhkを起動後、SPACEキーを入力するとレイアウトが左右反転するようになっています。

通常のキーレイアウト

SPACEキーを押したときのキーレイアウト

上が通常のキーレイアウトで、下がSPACEキーを押したときのキーレイアウトです。
SPACEキーを押すと、QのキーはPの位置にというように左右反転した位置に配置されます。そのため本来左手で押していたキーも右手で打てるようになっています。
(このキー配列の画像は、http://www.keyboard-layout-editor.com/ で作成しました。あらかじめ選択できる候補にHalf Keyboardがなかったので、フィードバック しておきました。)

例えば、「わたしの」(WATASHINO)と右手だけでローマ字入力したい場合には次のようにキーを押すと入力できます。

キー入力 印字内容
SPACE+O W
SPACE+; A
SPACE+Y T
SPACE+; A
SPACE+L S
H H
I I
N N
O O

入力したい文章によってはキーレイアウトを左右反転させるためにSPACEキーを頻繁に打つことになるので、右手をそれなりに酷使することになります。*2

なお、xhkがパッケージとしてインストールできると導入しやすくなって嬉しいだろうということで、パッケージ化の作業を進めやすくするためにフィードバックしておきました。またドキュメントの記述がやや古かったのでそちらも報告しておきました。

まとめ

今回は、片手でのキーボード入力を支援するソフトウェアのひとつであるxhkを紹介しました。
もし、片手でなんとかキーボード入力しないといけない事態になったら、選択肢の一つとして試してみるのもよいかもしれません。

*1 怪我をしたほうの手に負担をかけないことが主目的なので、怪我をしていないほうの手にはどうしても負荷がかかります。

*2 慣れないうちはキーレイアウトの変更に頭がついていかずにかなり打ち間違えたりします。

2018-11-21

ownCloud/Nextcloud関連プロダクトの翻訳に参加してみよう

最近ownCloud日本語翻訳チームのコーディネーターになってしまいました、結城です。

当社では長らく、業務のスケジュール管理にGoogleカレンダーを使っていていました。しかし、フリーソフトウェアを推進するという社の理念からすると、せっかくownCloudのような自由なライセンスの実装があるのにそちらを使っていないというのは望ましくありません*1。そこで最近、ownCloudの自社運用を開始しました。

ownCloudおよびその派生版のNextcloudは、DropboxやGoogle Driveなどのようなオンラインストレージを自社でホスティングするためのソフトウェアですが、アプリ(プラグイン)で様々な機能を追加でき、中でもカレンダー機能はGoogleカレンダーと似た感覚で使う事ができます。

ただ、実際に運用してみると細かいところで不満や不具合が見つかりました。そのため、当社では現在の所全社的に、業務と並行してそういった問題を解決していくためのフィードバックに取り組んでいます。

この記事ではその一つとして、翻訳への取り組みの紹介を兼ね、Transifexを使った翻訳作業の実際の流れや注意点を解説します。

翻訳に参加するまでの流れ

ownCloudのカレンダーを利用していると、イベントの繰り返し条件の設定画面に未訳箇所があるという点が目につきました。そこで、まずはできる所からという事で、翻訳に参加してみる事にしました。

OSS Gateワークショップでは「公式サイトからフィードバック先を見付けよう」という方針を取っていますが、今回は横着して、まずGoogleで「ownCloud translate」と検索してみました。すると、以下のページが最上位にヒットしました。

これはownCloud公式サイト上にある、翻訳コミュニティへの参加を呼びかけるドキュメントです。これによると、ownCloudの翻訳を行うにはTransifex上のownCloudチームに参加するように書かれています。

これにはまずTransifexのサイト上でアカウントを作成する必要があります*2が、この時「やりたいこと」の項目で「既存のプロジェクトに参加」を選択します。

(スクリーンショット:目的の選択時に「既存のプロジェクトに参加」を選ぶ)

その後、使用言語としてJapanese (ja)Japanese (Japan) (ja_JP)をそれぞれ追加します。Japanese (ja)は一般的な意味での「日本語」を、Japanese (Japan) (ja_JP)はその中でも特に「日本地域で使われる日本語」を指します*3

(スクリーンショット:言語の選択時に「Japanese (ja)」と「Japanese (Japan) (ja_JP)」を選ぶ)

アカウントができたらプロジェクト選択の画面に遷移しますが、この画面は一旦閉じて、ownCloudチームのダッシュボードを訪問します。

(スクリーンショット:ownCloudチームのダッシュボード)

「チームに参加」というボタンが表示されていますので、これをクリックすると、翻訳プロジェクトに参加する言語の選択が表示されます。「Japanese (Japan)」を選択して「参加」ボタンを押して下さい。

(スクリーンショット:言語選択の画面)

送信が完了すると、「Japanese (Japan) (ja_JP) 語に参加しました。 」という表示が出て、参加申請の承認待ちの状態になります。この後、プロジェクトの管理者がユーザーのプロフィールや活動実績などから判断して参加を承認してくれれば、晴れて翻訳作業に参加できるようになります。

実際の翻訳作業の進め方

参加申請が承認された後で再びownCloudチームのダッシュボードを訪れると、画面の左側にメニューが出るようになっており、また同時に、画面右側のボタンが「翻訳」に変化しています。

(スクリーンショット:参加承認後のダッシュボードの画面)

「翻訳」ボタンをクリックすると、言語とリソース(翻訳対象のアプリケーション)を選択する画面に遷移しますので、画面の指示に従ってそれぞれを選択すれば、翻訳の編集画面に遷移します。

(スクリーンショット:翻訳開始時のナビゲーション)

翻訳の編集画面(Transifexエディタ)では、以下の流れで翻訳を行います。

(スクリーンショット:翻訳作業の流れ)

  1. 未翻訳の文字列を選択
  2. 未翻訳の文字列一覧から、翻訳したい物を選択
  3. 選択した文字列が「未翻訳の文字列」欄に表示された事を確認
  4. 「翻訳文を入力してください」欄に訳文を入力
  5. 「翻訳を保存」ボタンで訳文を保存

保存された訳文は「レビュー待ち」という状態になり、レビュー権限がある人のレビューを通過すれば、その訳文は実際の製品のリリース版に含まれるようになります。

誤訳をしないために気をつける事

ただ、この時には淡々と機械的に訳していけば良いというわけではありません。翻訳対象の文字列はフレーズ単位で切り出されている事が多いため、同じ単語でも文脈によって適切な訳は変わってきます

例えば、本記事執筆時点では、カレンダーの予定の編集画面でリマインダーを詳細設定しようとすると、以下のような奇妙な選択肢があります。

(スクリーンショット:誤訳の例)

「秒」と「時間」の間に「最低」という語が登場しています。これは英語では「min」(minutesの略)となっていた部分で、正しい訳は「分」です。「min」を文脈を踏まえないまま「minimum(最低、最小)」の略と誤解してしてしまったために発生した誤訳という事になります。このような事が発生しないよう、特に短いフレーズを翻訳する時は、実際に運用中のownCloudの画面上でその文字列がどのように表示されているかをまず確認する必要があります。

とはいえ、短い文字列が画面内のどこに登場しているかを適切に探り当てるのはなかなか大変です。そこでヒントになるのが、訳文の入力欄の下にある「文脈」タブの中の「出現場所」という情報です。

(スクリーンショット:「文脈」タブ内の「出現場所」)

リソースによってはこのメタ情報が登録されている場合があり、これを参考にすると、原文が登場している部分の前後のソースコードを一緒に見る事ができます。この事例でもjs/app/controllers/valarmcontroller.js:61が出現位置であると記載されており、実際の当該箇所を見ると、前後の他の文字列やtimeUnitReminder〜という名前付けから、「これは最大・最小という文脈ではなく、リマインダーの時間の単位の文脈である」という事が読み取れます。

(スクリーンショット:「min」の出現場所)

文脈を確認しやすくするFirefoxアドオンの使用

いざ文脈を確認しようとした時に躓く点があります。それは、出現位置の情報はリンクになっておらず、Transifexのサイト上からは辿れないという事です。

筆者が調べた範囲では、これらの「出現位置」の情報はプロジェクト運営者がリソースを登録した時に任意の形式で付与するメタ情報で、Transifexのシステムには統合されていないようでした。そのため、対応するプロジェクトのソースコードがどこで公開されているかは自分で調べなくてはいけません。また、場所が分かったとしても、Transifexの画面を表示しているブラウザとソースコード管理ツールや端末の画面とを行ったり来たりする羽目になるため、作業効率が良いとは言い難いです。

そのため、翻訳の作業を補助するためのツールとして、Firefox用のアドオンを開発・公開することにしました。

Firefoxにこのアドオンをインストールすると、Transifexのページ上での右クリックメニューから「この翻訳の出現場所を開く」を選択するだけで、画面上に記載された「出現場所」の情報を自動的に検出し、GitHubなどのWebサイト上のリポジトリの対応する位置を新しいタブで開けるようになります。

(スクリーンショット:「Transifex Open Occurrences(出現場所を開く)」が追加したコンテキストメニュー項目)

Transifex上のプロジェクトとソースコードが公開されているWebサイトとの関連付けは、手動で作成したリストに基づいています。本記事執筆時点では、ownCloudチームの各プロジェクトのほとんどのリソースに対応する情報はあらかじめビルトインの情報として登録済みとなっています。

翻訳が難しいケース

前述のような単純な誤訳は元の語の出現位置を確認すれば防げますが、それだけではどうにもならない場面もあります。例えば、本記事執筆時点ではカレンダーの予定の編集画面でリマインダーの詳細設定と繰り返しの詳細設定において以下のような訳があります。

(スクリーンショット:同じ原文が異なる文脈で登場している例)

リマインダーの設定では「前」の次の選択肢が「回数で指定」となっていますが、これは原文ではそれぞれ「before」と「after」になっています。何故このような訳があてられているかというと、繰り返しの設定で終了条件を設定する箇所の「after N event(s)」の「after」に対して設定された「回数で指定」という訳文が使われているためです。

この問題は、多くのソフトウェアで表示文字列の国際化に使われている仕組みである「gettext」(およびそれを参考にした各言語のライブラリ)の仕様上の欠点に由来します。gettextは原文をそのままコードの中に埋め込んでおき、原文の文字列そのものをキーとして訳文を対応付けるという仕組みです。そのため、同じフレーズの原文が複数の異なる文脈で使われていると、訳文もそれに引きずられて、それぞれの文脈に対して同じ物しか設定できないのです。

このような問題が起こらないよう、gettext形式で国際化に対応するソフトウェアを作成する場合、可能な限り同じ原文が異なる文脈で使われないように気をつける必要があります。あるいは、gettext形式ではなく、同じ原文に対して異なるコンテキスト情報を持たせられる仕組み*4に切り替えるという方法もあります。この事例も本来的には、そのようにカレンダーアプリの実装を訂正することが望ましいと言えます。

翻訳コミュニティへの参加案内の後半を見ると、翻訳の元になる原文の側を変更したい場合は各プロジェクトのGitHub上のIssue Trackerに報告をするよう書かれていましたので、本件についても改善案を起票してみました。また、暫定的ではありますがそれぞれの場面に共用できる訳語として「後」をあてるように変更しました(これなら「前/後」「後 N 回」となるため、まだ違和感は少ないでしょう)。

用語を統一しよう

誤訳とは違った部分で気をつけなければいけない点として、「用語の統一」があります。同じ英単語に対して同じ文脈でも複数の訳語(同義語や、「ユーザ」と「ユーザー」のような音引きの有無の違いなど)がある場合、それぞれの箇所で異なる訳語を選んでしまうと混乱の元になります。

Transifexの翻訳画面で訳語の入力欄の下の「用語集」タブを選択すると、原文に含まれている各単語について、これまでに登録された訳語を一覧表示することができます。

(スクリーンショット:用語集を表示した所)

原文に用語集に登録された単語が含まれていて、訳文でそれに対応する訳語が使われていないと、編集画面上に警告が表示されます。その場合は用語集を確認して、なるべくそこに載っている訳語を使うようにしましょう。また、新登場の語句に新しい訳語を割り当てた場合には、その情報を用語集に忘れずに登録しておきましょう。

とはいえ、このシステムも完璧ではないため、杓子定規に警告が表示されてしまう場合もあります。例えば「Delete」という単語について「削除する」という訳語が割り当てられている時に、ボタンのラベル文字列としては一般的には「削除」と体言止めする事が多いですが、そのように訳すと「削除する」という訳語が含まれていないと判定されてしまう場合があります。

(スクリーンショット:訳語の警告が出ている様子)

また、原文で「event(s)」のように単数形か複数形かあやふやな物を括弧書きで表している場合に、訳語を「回」とあてると「括弧書きに対応する部分が無い」という警告がなされる場合もあります。文脈によっては直訳ではなく意訳した方が分かりやすく自然になる場合もあるという事も考慮すると、警告の内容が常に絶対的に正しいとは限りませんので、あくまで意図しないミスを防ぐための目安として考えるに留めると良いでしょう。

レビューする側にも回ってみよう

前述の通り、Transifexでの翻訳は訳文を保存した段階では「レビュー待ち」の状態となっており、レビュー権限がある人のチェックを経て初めて実際のリリースに反映されます。

今回翻訳に参加してみた所、ownCloudの日本語の翻訳については未訳の項目はそれほど多くなく、それよりも「レビュー待ち」の項目が数百件といった単位で存在している事が目につきました。これはレビューが滞っている事の顕れで、このままではどれだけ翻訳を行ってもその結果が実際のリリースに反映されないという事になってしまいます。このような場合には、レビューする側として協力することで、プロジェクトを円滑に進める手助けができるはずです。

筆者はFirefoxの翻訳コミュニティの活動の情報も耳に入れていた事から、上記のような翻訳の注意点についてはいくつか知見がありましたので、レビュアーに立候補してみる事にしました。

先のownCloud公式サイト内の翻訳協力者向けの情報を見ると、Transifexではユーザーの権限にいくつかの種類がある事と、議論はownCloud Centralというフォーラムで行うよう案内されていました。そこで、翻訳のレビュアーの自薦や他薦を受け付けているスレッドはないかと思い「translate」という単語でフォーラム内を検索してみましたが、残念ながらそれに特化した既存のスレッドはすぐには見付けられませんでした。

何か他の手段で連絡を取れないものかと思い関連情報を辿っていったところ、上記のページからリンクされているTransifexのサイト内でのユーザーの各権限の説明に、「管理者またはコーディネーターの権限がある人は、ユーザーの権限の変更とレビューができる」という旨の情報が書かれていました。そこで日本語の翻訳に関わっているユーザーの一覧を見てみたところ、「レビュアー」は一人もおらず「コーディネーター」は日本語話者らしき方が一人だけいるという状況でした。そこで、唯一のコーディネーターであった方宛にTransifex上で以下のメッセージを送ってみました。

Subject: コーディネーター/レビュワーへの自薦

はじめまして。Piro / 結城洋志と申します。
大変不躾な申し出で恐縮ですが、ownCloudプロジェクトの日本語の翻訳についてレビュー権限のあるロールを与えていただく事はできませんでしょうか?
最近自社でownCloudの運用を始めた際に、Calendarに一部未訳箇所があったことに気付き、翻訳に参加しようと思い立ちました。
その際、現在レビュー待ちの翻訳が非常に多い状態であることと、日本語のレビューを行える方がguitarmasakiさんお一人であるらしい事に気がつきました。
自分はownCloudの翻訳には今日からようやく関わり始めた状況で、貢献量もまだまだ微々たる物ではあります。
しかしながら、個人的にMozilla Firefoxの(主にアドオンの)コミュニティで長く活動しており、GUIの翻訳についてはある程度の知見があると自負しております。
ですので、ownCloudの日本語翻訳レビューにも貢献が可能ではないかと考えております。
以上、急な申し出で失礼かとは存じますが、ご検討いただけましたら幸いです。
https://docs.transifex.com/teams/understanding-user-roles/ を見たところ、言語ごとのユーザーのロール変更はコーディネーターの方に権限があるとの事でしたので、メッセージにて連絡させていただいております。)

すると早速お返事を頂けて、ロールをコーディネーターに引き上げていただく事ができました。

これで未レビュー部分のレビューを進める事ができるようになります。レビュー権限がある状態で翻訳画面を開くと、訳文の保存ボタンの横に「レビュー」というボタンが表示されるようになります。

(スクリーンショット:レビュー権限があるユーザーで翻訳画面を見た様子)

前述の翻訳上の注意点をチェックした上で「レビュー」ボタンを押せば、その翻訳はレビュー済みとなります。

まとめ

以上、Transifexを使ったownCloudの翻訳作業の進め方と、レビュー権限を得るための自薦の様子をご紹介しました。

今回の一連の過程でレビュー権限を得る事となりましたが、依然としてアクティブなレビュアーの数が少ない状態である事には変わりありません。そのため、独断ながらリスクヘッジとして、個人的に信頼のおける方や推薦を頂いた方など何名かのロールをレビュアーに引き上げさせていただきました。
現在はこの体制で未レビュー分の消化を進めていますが、文脈の正しさの検証をきちんとやるにはそれなりの手間がかかるため、すべて消化するまでにはまだまだ時間がかかりそうです。
レビュアー側として翻訳に協力できると自負される方がいらっしゃいましたら、Transifex上のpiroorユーザー宛に自薦のご連絡を頂けましたら幸いです。

また、Transifex上での翻訳作業を補助するFirefox用アドオンのTransifex Open Occurrences(出現場所を開く)は、ownCloud以外のプロジェクトで広く利用できる設計となっていますが、他のプロジェクトで使うためには「出現場所」を実際のリポジトリに紐付けるための情報を手動で登録する必要があります。
他のプロジェクトでお使いになる場合には、次の参加者の方が手動での登録をしなくても良くなるように、ビルトインの情報の追加手順を参考にしてマージリクエスト(GitHubでいうプルリクエスト)をお送り下さい。

「OSS開発に参加する」というと何か特別な事のように思う人もいるかも知れませんが、基本的には

  • ownCloudのカレンダーに未訳のままの箇所があるという問題があるので、翻訳に協力して解決する。
  • 翻訳に使うWebサービスが使いにくいという問題があるので、補助するツールを開発して解決する。
  • 翻訳のレビューが滞っていてせっかくの翻訳が反映されにくくなっているという問題があるので、レビュー作業に協力して解決する。

といった要領で、何か問題に遭遇した時にそこで諦めず、その時に自分の取れる手段で問題の解決を図っているという事になります。
皆さんも普段使っているソフトウェアやサービスで「ここ、何かおかしいな」「どうにかならないのかな」と感じた時には、ぜひとも、そのソフトウェアやサービスに適切にフィードバックする方法を調べて、問題を見過ごさずに解決を図るようにしてみて下さい。
(実際にフィードバックしてみるのは不安があるという方は、OSS Gateのワークショップへの参加も検討してみて下さい。)

*1 自由なライセンスの実装を積極的に使っていかないと、品質が改善されず、いつまでも「使い物にならない」状態が放置され、フリーソフトウェアがますます使われないという事になってしまうからです。

*2 Transifexのサイト上でアカウントを作成する以外にも、GitHubやGoogle、Twitterなどの認証情報を参照する形でもアカウントを作成できます。

*3 「_JP」の部分が地域を表しています。これは、「イギリス英語(en_GB)」と「アメリカ英語(en_US)」のように、分類上は同じ言語でも地域によって単語の綴りや使われ方に差異がある場合を想定した表記です。

*4 C言語やDjangoではpgettext、Rubyではp_()などがそれにあたります。

2018-11-16

PhabricatorでのFirefoxへのパッチ投稿方法

以前の記事で、Firefoxへのパッチ投稿の一手段としてMozReviewという仕組みがあることと、その使用方法を紹介しました。しかし、その後すぐにMozillaのコードレビューシステムがPhabricator完全移行してしまい、MozReviewの運用は止まってしまったようです。記事公開時点ではまだMozReviewを使う開発者が大半のように見えていたため、MozReviewの運用停止が近づいているということを把握できていませんでした。

今回は改めてPhabricatorでのパッチ投稿を実践してみたため、その方法を紹介します。

セットアップ

本記事で紹介するセットアップ方法はMozilla Phabricator User Guideを元にしています。詳細はそちらを参照して下さい。

事前に用意するもの
  • BMO(bugzilla.mozilla.org)のアカウント
    • まだBMOアカウントを取得していない場合は https://bugzilla.mozilla.org/createaccount.cgi で作成します。
    • Real nameにはYour Name [:ircnick]のような形で末尾にニックネームを付けておきましょう。
      • Phabricator上ではこのニックネームがユーザー名として表示されます。
      • ただし、既に存在するニックネームは使用できません。
    • Phabricatorにアクセスするためには、Bugzilla側で2段階認証を有効化しておく必要があります。
  • mozilla-centralのワーキングコピー
    • Firefoxへのフィードバックの仕方:Windows編等を参考に、hg clone https://hg.mozilla.org/mozilla-centralで取得して下さい。
    • 本記事ではバージョン管理システムとしてMercurialを使用する場合のみを対象とします
      • PhabricatorはGitでも使用できるようですが、本記事では対象としていません。
Phabricatorへのログイン確認

MozillaのPhabricatorは https://phabricator.services.mozilla.com/ でアクセスできます。
Loginボタンを押して、ログインを試みます。

Phablicatorログイン

認証はBugzilla側で行われるため、以下のようなボタンが表示されます。

Bugzilla認証

最初のアクセス時には、Bugzilla側での認証成功後、Phabricator上でのアカウント登録を促されます。

Phabricatorユーザー登録

アカウントの登録を完了させて、Phabricatorを使用できる状態にしておきましょう。

Arcanistおよびmoz-phabのセットアップ

パッチをコマンドラインから投稿するためには、ArchanistというPhablicatorのコマンドラインツールと、そのラッパーコマンドであるmoz-phabをインストール必要があります。

Archanist

Archanistのインストール方法は以下に記載されています。

Ubuntuの場合は比較的簡単で、依存パッケージをインストール後、ArchanistのGitリポジトリをcloneして、パスを通します。

$ sudo apt install php php-curl
$ mkdir somewhere/
$ cd somewhere/
$ git clone https://github.com/phacility/libphutil.git
$ git clone https://github.com/phacility/arcanist.git
$ export PATH="$PATH:/somewhere/arcanist/bin/"

Windowsの場合のインストール方法は以下に記載されています。

こちらについては筆者の手元で検証できていないため、本記事では割愛します。

Archanistのインストールが完了したら、APIキーを設定します。mozilla-centralのソースディレクトリ下で以下のコマンドを実行します。

$ arc install-certificate

ブラウザでAPIキーが表示されますので、そのAPIキーをコマンドラインにコピー&ペーストすると、APIキーが取り込まれます。
APIキーは以下のようなJSON形式で~/.arcrcに書き込まれます。

{
  "hosts": {
    "https://phabricator.services.mozilla.com/api/": {
      "token": "xxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    }
  }
}
moz-phab

moz-phabのインストール方法は、moz-phabREADME.mdに記載されていますので、そちらを参照して下さい。

単にmoz-phabコマンドをパスの通ったディレクトリにコピーし、実行権限を付けるだけで良いようです。

レビューリクエストの作成

以前の記事で紹介したように、Firefoxで何かパッチを投稿したい場合は、全てBugzilla上の該当Bugを起点に作業を行います。何か新機能を追加したり、不具合修正を行いたい場合は、まず該当するBugが既に存在するかどうかを確認し、無い場合は新たに自分で新しいBugをfileします。

該当Bugでソースコードに対して何か変更を行って、いざPhabricatorにパッチを投稿したいという状況になった場合、まずはMercurialで手元のリポジトリに変更をコミットします。

$ hg commit

このとき、コミットメッセージの形式に注意しましょう。具体的には以下のような形式にする必要があります。

Bug [Bug番号] - [Bugの概要(一行)]

以下、Bugの詳細についての記述...

Mercurialでリモートリポジトリにpushする際、上記のコミットメッセージのBug番号から自動的にBugzillaの該当Bugにパッチが投稿されます。

また、末尾にr?ircnickという形式でレビュアーを指定すると、push後に自動的に該当レビュアーにレビューリクエストを投げることもできます。このレビュアーの指定は、パッチを送信した後にPhabricatorのWeb UIから行うこともできますので、必ずしもコミットメッセージに含める必要はありません。

以下に、筆者が実際にパッチを投稿した際のコミットメッセージを示します。

Bug 1502786 - Break cycle between PureOmxPlatformLayer and OmxDataDecoder r?jya

OmxDataDecoder, OmxPromiseLayer and PureOmxPlatformLayer consist
circular reference by RefPtr, and no one sever the reference. As a
result their refcount never decrease to 0.
This commit sever it at PureOmxPlatformLayer::Shutdown() which is
called by OmxDataDecoder.

詳細な議論はBug番号から辿ることができるため、コミットメッセージには必ずしも詳細な記述は必要ないようです。有った方が好ましいとは思いますが、慣れていない場合には、まずはBug番号と一行サマリを適切に記載することに注力すると良いでしょう。

ローカルリポジトリへのコミットが完了したら、リモートリポジトリにsubmitします。

$ moz-phab submit

submitが完了した後、先ほどコミットした内容をhg exportで確認してみると、以下のようにDifferential Revision:という行が追加されていることに気が付きます。

# HG changeset patch
# User Takuro Ashie <ashie@clear-code.com>
# Date 1541472583 -32400
#      Tue Nov 06 11:49:43 2018 +0900
# Node ID 25c8e78baa9aa8189ca7026d7ac7868c69d483f3
# Parent  9f9a9234959f114825f58beee0cffbab82d0bb29
Bug 1502786 - Break cycle between PureOmxPlatformLayer and OmxDataDecoder r?jya

OmxDataDecoder, OmxPromiseLayer and PureOmxPlatformLayer consist
circular reference by RefPtr, and no one sever the reference. As a
result their refcount never decrease to 0.
This commit sever it at PureOmxPlatformLayer::Shutdown() which is
called by OmxDataDecoder.

Differential Revision: https://phabricator.services.mozilla.com/D10028

...

この行はレビュー結果を受けてパッチを修正する際に必要になります。また、この行に記載されているURLをブラウザで開くと、Phabricator上でレビューリクエストを参照することができます。以後、レビュアーとのやりとりはこのページで行うことになります。

パッチの修正

レビュアーによってパッチがレビューされ、Phabricator上で修正箇所を指摘されたら、パッチを修正して再度Phabricatorにsubmitすることになります。この際、同一のレビューリクエストに対する修正であることを指定するために、先ほどと同じDifferential Revisionをコミットメッセージに含めてhg commitし、moz-phab submitします。

Mercurialでのパッチ管理方法は本記事のスコープ外のため割愛しますが、パッチ(コミット)が1つのみで、ローカルリポジトリに過去のバージョンが不要である場合、もっとも簡単な修正方法はhg commit --amendで前回のコミットをやり直す方法でしょう。この方法の場合、コミットメッセージは特に修正しなければ前回のままとなりますので、Differential Revisionも前回と同じものが使用されます。ローカルリポジトリの修正は上書きされてしまいますが、リモートリポジトリ上では過去のバージョンも管理され、その差分を確認することもできます。

Phabricator diffリビジョン

修正をsubmitしたら、Phabricator上でレビュアーのコメントに返信をします。この際も、最後にSubmitボタンを押すことを忘れないで下さい。なお、MozReviewの時とは違い、Phabricator上での会話が自動的にBugzillaにも投稿されるという機能は無いようです。

レビューが通ったら

レビュアーによってパッチに問題ないと判断された場合、以下のようにAcceptedのマークが付きます。

Phabricator Accepted

この状態になったら、パッチのランドが可能になります。Mozilla Phabricator User GuideのLanding Patchesの項によると、パッチのランドにはLandoというシステムを使うことを強く推奨するとなっていますが、mozilla-centralへのコミット権限が無い場合、このシステムを使用することはできません。実際に試してみたところ、以下のように弾かれてしまいました(筆者の権限はLevel 1)。

Landoエラー

コミット権限が無い場合は、これまでと同様に、Bugzilla側で「Keywords」欄にcheckin-neededというキーワードを付加しておいて、権限のある開発者にコミットしてもらえば良いようです。この際、Bugzilla側ではレビュー承認済みであるr=ではなくレビューリクエスト中であるr?のマークのままになっていることがあるようですが、Phabricator側でAcceptedになっていれば、構わずcheckin-neededにしてしまって問題無いようです。

Accept後のパッチ修正

単にAcceptされただけであればそのままランドしてしまえば良いだけですが、場合によっては「Acceptするけど、こことここだけは修正しておいてね」と言われる場合があります。この場合はAcceptedのマークは付きますが、パッチは修正して再度送信する必要があります。すると、マークが以下のように変わります。

Phabricator Other Diff Accepted

この場合、修正版のパッチを再度レビューしてもらう必要があるのか疑問に思うところでしょう。結論から言えば、特にレビューしてもらう必要は無いようです。自分で修正できたと判断すれば、そのままランドしてしまうことができます。ただし、指摘された箇所は全て「Done」にチェックを入れておきましょう。

Phabricator Doneフラグ

まとめ

PhabricatorでのFirefoxへのパッチ投稿方法について紹介しました。

なお、本記事内で紹介した実例はBug 1502786: Memory leaks in OpenMAX PDMになります。以前Firefox本体にフィードバックしたOpenMAX IL対応パッチにバグがあることを発見したので、その修正を再度フィードバックしています。

元となるOpenMAX対応パッチについては、特にレビュアーを指定せずにとりあえずMozReviewで上げてみただけだったのですが、Mozillaの開発者の目に止まって勝手にレビューされ、本体にマージされるところまで進みました。やはりコードレビューシステムで登録しておいた方が開発者としてもレビューが捗るのかもしれませんね。

2018-11-15

Debianで医用画像を閲覧するためのアプリケーション三選

はじめに

最近骨折を経験した林です。
病院を紹介してもらうときに、紹介状とともに医用画像を含むメディア(CD-ROM)を渡されることがあります。
メディアには医用画像を閲覧するための専用のビューワーが付属していたりしますが、そのビューワーがWindowsでのみ動作するアプリケーションだったりするとそのままではDebianで閲覧できません。
そこで、今回はDebianでこのような医用画像(いわゆるDICOMと呼ばれるフォーマットのもの)を閲覧する方法をいくつか紹介します。

閲覧時のサンプルの画像には日本画像医療システム工業会:DICOMの世界を利用しました。

aeskulap

Aeskulap - DICOM Viewer

2005年から開発されているDICOMビューワーです。
公式には2007年の0.2.1が最新ですが、Debianではベータ版である0.2.2-beta2がパッケージ化されています。
使ってみたことはないのですが、ネットワークを経由してPACSとよばれるノードからDICOM画像を検索する機能も備えているようです。

画像によっては埋め込まれている患者名の扱いによって落ちる不具合があります。この点についてはすでにアップストリームにバグ報告しています。

[Aeskulap-users] Stack smashing bug when invalid patient name parsing

インストール方法
% sudo apt install aeskulap
起動方法

aeskulapコマンドを実行するとアプリケーションを起動できます。

% aeskulap
医用画像の閲覧方法

メニューの[ファイル]-[開く]から対象のファイルを開くと閲覧できます。拡大といった操作はサポートされていません。

aeskulapによる閲覧

pixelmed-apps

PixelMed Publishing, LLCによって開発されているJava DICOM ToolkitにDICOMビューワーが含まれています。
公式には20181018が最新ですが、Debianでは20150917がpixelmed-appsとしてパッケージ化されています。
DICOMビューワーのほかにもいくつかツールがバンドルされています。

インストール方法
% sudo apt install pixelmed-apps
起動方法

pixelmed-appsパッケージに含まれているdicomimageviewerコマンドを実行するとアプリケーションを起動できます。

% dicomimageviewer
医用画像の閲覧方法

[Local]タブの[File]ボタンをクリックして対象のファイルを開くと閲覧できます。拡大といった操作はサポートされていません。

dicomimageviewerによる閲覧

DICOMscope

OFFISによって開発されているDICOMビューワーがDICOMscopeです。
公式には3.5.1が最新ですが、Debianでは3.6.0のパッケージが提供されています。

インストール方法
% sudo apt install dicomscope
起動方法

dicomscopeコマンドを実行するとアプリケーションを起動できます。

% dicomscope
医用画像の閲覧方法

[Load image file]ボタンをクリックして対象のファイルを開くと閲覧できます。Zoom機能があるので、細部を拡大してみてみたいときにはおすすめです。

DICOMscopeによる閲覧

まとめ

今回は、Debianで医用画像を閲覧するためのアプリケーションを三つ紹介しました。
もし医用画像を閲覧する機会があれば(ないほうがいいのですが)ぜひ使ってみてください。

2018-11-13

macOS版FirefoxのIPCとドラッグ&ドロップ周りのバグを調査する

クリアコードの結城です。

先日、動画でバグ報告する方法の中で例として挙げていたmacOS版Firefoxのサイドバーの操作に関するバグですが、最終的には原因の特定とパッチの提出にまで至る事ができました。このエントリでは当該バグの調査の過程を辿りながら、macOS上のFirefoxにおいてマルチプロセスが絡む部分やドラッグ&ドロップに関わる部分のデバッグをどのように進めたかの一例をご紹介します。

問題の概要

当該バグはツリー型タブというアドオンに寄せられた報告に端を発しています。このアドオンはFirefoxのサイドバー内でタブバーの代替となるUIを提供するという物で、項目をクリックするとタブが切り替わり、ドラッグ&ドロップでタブを並べ替える事もできます。このサイドバーパネル上において「セッション復元後にまだ読み込まれていないタブをドラッグしようとするとドラッグできず、また、それ以後サイドバー内で一切のドラッグ操作が行えなくなる」という現象が発生する、というのが報告の要旨でした。

報告者の方と連絡を取りながら調査を進めた結果、この問題はmacOSでのみ発生するらしいという事と、アドオン側で可能と思われる対策を講じても現象を回避できない、Firefox自体のバグに起因する問題であるらしいという事が分かりました。当該アドオンの実装を参考に最小のテストケースを作成して検証した結果、確かに現象を再現できたため、Firefox自体のバグとしてBugzillaに報告しました。またその過程において、上記の再現手順の他に以下の事も事が分かりました。

  • サイドバーの内容(=アドオンが提供するコンテンツ)が別プロセスで動作している時だけ現象が再現する。
  • 既にセッションが完全に復元された状態のタブでは、現象は再現しない。
  • 現象発生時は、dragstartイベントは発生するが、その後に発生するはずのdragoverdragenterdragleavedragendの各イベントは発生しない。

調査前の時点での見立て

本格的に調査を始める前に、この時点で分かっていた情報から、Firefoxの内部で何が起こりどうしてこの問題が起こっているのかという事について、いくつかの仮説を立てました。

  • 仮説1:タブを切り替えてセッション復元処理が行われる時に、親プロセスと子プロセス群との間の「繋ぎ替え」が発生し、サイドバーのプロセスが「アクティブな子プロセス」でなくなるせいで、サイドバーのプロセスから親プロセスにドラッグ操作が伝わらなくなる。
  • 仮説2:タブを切り替えてセッション復元処理が行われる時に、ドラッグ&ドロップの処理に対して何らかの割り込みをかけるせいで、「何らかのフラグ」が内部で立ってしまい、以後ドラッグ&ドロップのイベントが発生しなくなる。

ただ、そもそも一連の処理に関わる実装がどのような設計になっているのか、「繋ぎ替え」や「フラグ」とここで呼んでいるような物が実際にあるのかどうかも、この時点では不明なままでした。これらはあくまで、過去の経験に基づく推測での見立てとなります。

また、筆者はmacOSのネイティブアプリ開発の知見を全く持っていないという問題もありました。筆者はWindows版のFirefoxの低レイヤ部分の調査は若干経験しているものの、macOSネイティブの開発経験は皆無です。今回のように特定プラットフォームでしか発生しない問題は、そのプラットフォーム向けのコード(WindowsではWin32 API、LinuxではGTK+、macOSではCocoa APIを使う部分)を調査する必要が出てきます。調査対象の事を全く知らない状態での手探りの調査は泥沼化しがちですので、これは大きなリスクです。

そのリスクを回避するために「macOSアプリでのドラッグ&ドロップはどのように実装するのか」という事を先に勉強してから調査に臨むというやり方もあります。しかしながら、今回の調査に必要な知識がその知識体系のどのあたりに存在しているかは全く予想が付きませんし、そこまでガッツリとmacOSでの開発を学びたいという訳でもありません*1。そもそもこれは「解決できれば儲け物だが、自身がmacOSユーザーではないので究極的には直らなくても困らない問題」という性質の物でしたので、失敗に終わったとしてもそれほどダメージは無いと言えます。ですので、知見の不足には一旦目を瞑って、Mozilla製品でのクロスプラットフォームな開発の知識だけを元に調査していく事にしました。

調査の出発点を探す

全く手がかりがない所から調査を行うのは、Firefoxほどの規模のソフトウェアではほぼ不可能です。そこで、ここまでに分かっていた事からヒントになりそうな要素はないかを検討してみました。

成功ケースと失敗ケースの両方の再現手順がはっきりしているという事は、その両者で起こっている事を子細に比較していけば、現象が起こる原因が分かる可能性は高いと考えられます。一連の操作の中で「共通してこのモジュールのこのメソッドが呼ばれている」という部分が特定できれば、そこを手がかりにして調査範囲を広げていけるはずです。

(ある一点を起点にして、成功ケースにまで辿り着くルート上のどこかで、失敗ケースへと分岐するポイントがあるはず)

ここで着目したのは、正常な場合でも異常な場合でもDOMのdragstartイベントは発行されているという点でした。Firefoxの内部ではこういったDOMイベントの名前は定数で定義されていますので、その定数を伴ってイベントを初期化したり発行したりしていそうな所を特定できれば、そこを起点に調査を進められます。

"dragstart"という文字列でざっとソースコード内を検索してみた所、dragstartを定数として定義しているらしい箇所がすぐに見つかりました。ここで、このイベント名は他の箇所からeDragStartという定数名で参照されているらしいという事が分かりました。

次に、eDragStartという定数名でソースコード内を検索してみた所、自動テスト用に擬似的にイベントを生成しているらしい箇所に混ざって、EventStateManagerというモジュールの中でイベントを生成しているらしい箇所が1箇所だけ見つかりました。この処理が含まれているのはEventStateManager::GenerateDragGesture()というメソッドでしたので、メソッド名から見ても、どうやらドラッグ&ドロップの操作が行われた時には必ずここを通過すると考えて良さそうです。

同様にeDragOverという定数名でソースコード内を検索した結果、成功ケースにおける「dragoverイベントが発行されるのはここ」という場所も特定できました*2

ところで、今回の問題は厳密に言うと、以下の2つの現象が起こっています。

  • セッション復元後にまだ読み込まれていないタブをドラッグしようとするとドラッグできない。(最初の問題)
  • それ以後、サイドバー内で一切のドラッグ操作が行えなくなる。(2回目以降の問題)

2つの現象は「タブをドラッグできない」「どちらもdragstartイベントだけが発行される」という症状は共通していますが、原因が同じかどうかは分かりません。ただ、再現が容易なのは2つ目の現象の方(1つ目の現象が再現したら、以後は何度やっても2つ目の現象が起こる事になるので)なので、先にこちらの方から原因を調べる事にしました。こちらの原因が分かれば、今度はそれが「その原因となっている状況を引き起こした犯人=1つ目の現象の原因」を特定する手がかりとなります。

低レベルのログを収集する

Firefoxには環境変数を指定する事でモジュール単位の低レベルのログを収集する機能があるため、成功ケースと失敗ケースの両方についてこのモジュールのログを取得して比較すれば、何か分かるかも知れません。この方法であれば、子プロセスが出力したログもそれぞれ個別に収集する事ができます。

低レベルのログを収集するには、macOSではターミナルを使います。Firefoxのアプリケーションファイルはファイルシステム上はフォルダ(ディレクトリ)になっているので、以下のようにして環境変数を設定して実行します。

$ MOZ_LOG=timestamp,sync,nsCocoaWidget:5 MOZ_LOG_FILE=~/debug.log /Applications/Nightly.app/Contents/MacOS/firefox

環境変数MOZ_LOGには、ログを取得したいモジュール名とログレベルを:で繋げたものを、,区切りのリストとして指定します。このようにしてFirefoxを起動すると、メインプロセスのログがMOZ_LOG_FILEで指定した名前のファイルに出力され、子プロセスのログはdebug.log.child-1のような名前で同じ位置に出力されます。ログ出力が始まった状態から、ターミナルの新しいタブでtail -F debug.log*と実行すれば、それぞれのログに出力される内容をリアルタイムで見る事もできます。

しかし、先程特定した「成功時と失敗で共通して呼ばれているモジュール」であったEventStateManager.cppのソース内をlogで検索してみても、ログ出力を行っているらしきコードは見つかりませんでした。また、dragoverイベントが発行される契機になっている処理の方についてはnsCocoaWidgetというログモジュールでログを収集できましたが、こちらは成功ケースのログしか出力されず、しかも知りたい核心部分の処理が全て終わった後の時点のログでした。これではログを取っても調査の役には立ちません。

モジュール単位でのログが役に立たないとなると、何か別の方法で「成功ケースと失敗ケースのそれぞれで、内部的に何が起こっているのか」を調べる必要があります。

デバッグビルドとデバッガを使用した調査

このような場面で使えるツールの1つがデバッガです。問題が発生するケース・期待通りに動くケースで必ずこの行を通る、という事がはっきり分かっている場合には、デバッガ上でそこにブレークポイントを仕掛けておく事で、処理を一時停止して各変数の値を詳細に見る事ができます。また、そこから1行ずつ処理を進めて流れをじっくり追うという事(ステップ実行)もできます。

デバッグビルドの準備

ただ、デバッガを使うにはmacOS版Firefoxのデバッグ用ビルドが必要です。オフィシャルに公開されているバイナリは無い様子でしたので、「Firefox macOS debug」と検索して出てきたMDNのmacOSでのデバッグ手順の解説ビルド手順の解説を見ながら自分でデバッグビルドを作成することにしました。

  • macOS版FirefoxのビルドのためにはXcodeが必要です。しかし、調査に使える環境はmacOS 10.12 Sierraだったため、App Storeにある最新のmacOS向けのXcodeはインストールできませんでした。そこで「Xcode for old macOS」などのキーワードで検索して見つかった情報を参考に、開発者向けのページからXcodeの旧バージョンをダウンロードしてインストールしました。
  • Homebrewなどのツール類については、解説にある通りbootstrap.pyを使って一括インストールする事ができました。
    • 初期設定時に選択するビルド対象は、Artifact Buildではないデスクトップ版Firefoxとしました。Artifact Buildとは、ビルド済みバイナリをダウンロードしてきて使う事によって、ビルドに要する時間を短縮できるという特殊なビルドです。しかし、今回はバイナリ部分をデバッグ用にビルドしたいので、Artifact Buildは不適当という事になります。
  • 既にNightlyで現象を再現できていたので、cloneするリポジトリはNightlyに対応する https://hg.mozilla.org/mozilla-central/ を使いました。

解説の通りに進めてデバッグビルドができた時点で*3./mach runでテスト実行してみました。最適化がなされないためなのかデバッグビルドだからなのか、一挙手一投足がもたつく程に非常に低速ではありますが、一応動く事は動いているため、準備が整った事にして次の行程に進みます。

ブレークポイントの設定とステップ実行

macOSでのデバッグ手順解説に記載の通りに準備した上で、Xcodeの「Product」メニューから「Run」を選ぶと、デバッグビルドのFirefoxが起動します。

(Xcodeでブレークポイントを設定した様子)

ブレークポイントを設定するためには、まずXcodeの左ペインのフォルダーアイコンをクリックし、ソースコードのファイル一覧から「ここで処理を止めたい」という処理が含まれているファイル(今回は dom/events/EventStateManager.cpp )を選択します。 右上のペインにソースコードが表示されますので、止めたい処理の行番号をクリックします。すると、行番号の部分に青いマークが付きます。これをブレークポイントといい、Xcode経由で起動したデバッグビルドのFirefoxの内部で処理がこの行に到達すると、処理がその場所で止まってXcodeに制御が移るようになります。

ブレークポイントで処理が止まった後は、左のペインの内容が関数呼び出しのスタック表示に切り替わり、ソースコードが表示されているペインの左下ペインには停止位置での各変数の値が表示されます。

(ブレークポイントで処理が停止した様子)

この時には、ツールバー上の各ボタンで以下の操作を行えます。

  • Deactivate breakpoints:全てのブレークポイントを一時的に無効化する。もう一度クリックすると、ブレークポイントを有効化する。
  • Continue program execution:次のブレークポイントまで処理を一気に進める(ブレークポイントでの一時停止を解いて、通常の実行に戻す)
  • Step over:処理をソースコード上で1行分次に進める(ステップ実行)。
  • Step into:関数の呼び出し行において、関数の中に入る(呼ばれた関数の1行目からステップ実行を行う)。
  • Step out:関数の外に出る(現在ステップ実行中の処理を関数の最終行またはreturnに到達するまで進めて、呼び出し元の関数で元の関数が呼ばれている行からステップ実行を行う)。

変数の値を調べても有用な情報を見付けられなかった場合には、ステップ実行を繰り返したり、別の位置にブレークポイントを設定したり、ブレークポイントを削除*4したりして、解析を続けていきます。同じ箇所で「期待通りの結果が得られている時の内部状態」と「問題が起こっている時の内部状態」を詳細に比較していけば、「何が原因で、処理が期待と異なる方向に進んでいってしまったのか」を明らかにできるというわけです。

ただ、実際には各変数の値はツリー構造になっていて、奥の奥の方に原因が潜んでいるという場合も多々あるため、余程「ここをピンポイントで調べたい」という事が事前にはっきり分かっている場合でもない限りは、この方法で一発で原因を見付けるというのは難しいです。今回の調査でも、闇雲に調べるだけでは残念ながら決定打となる情報に辿り着く事はできませんでした。

期待外れだったのは、成功ケースで実際にdragoverイベントが発行されている場面の詳細を調べられなかったという点です。この処理はCocoaに対して登録しておくイベントハンドラのような関数の中にあり、Cocoaから通知されたイベントをトリガーに実行されるため、ここにブレークポイントを置いても(関数呼び出しのスタックが切れてしまっているので)デバッガでは処理の呼び出し元を辿る事ができないのです。「成功ケースと失敗ケースで明らかに異なる部分」が目の前にあるにも関わらず、そこからは何の情報も得られないという、非常に残念な結末でした。

ただ、ここまでの調査過程で以下の事は分かりました。

(プロセス間でのドラッグデータの受け渡しの様子の図)

  • 成功ケースでは親プロセス側で認識できたドラッグデータ(アドオンで定義したdragstartイベントのリスナにおいて、event.dataTransfer.setData()で追加したデータ)の個数が1以上になっているが、失敗ケースでは0になっている。子プロセス側で設定したはずのドラッグデータが親プロセスからは認識されていない。そのため、EventStateManager::DoDefaultDragStart()falseを返して終了する形となっており、ドラッグセッションを開始するnsDragService::InvokeDragSessionWithImage()が呼ばれていない。
  • このドラッグデータはEventStateManager::DetermineDragTargetAndDefaultData()nsContentAreaDragDrop::GetDragData()DragDataProducer::Produce()TabParent::AddInitialDnDDataTo()TabParentmInitialDataTransferItemsというメンバ変数の値を取得する形で初期化されている。
  • TabParentmInitialDataTransferItemsの値は、TabParent::RecvInvokeDragSession()で設定されている。このメソッドは、プロセス間通信でPBrowser::Msg_InvokeDragSession__IDというメッセージを受け取った時に実行されている
    • この時のIPC周りのコードは自動生成されているため、デバッガでブレークポイントを設定できない。
  • PBrowser::Msg_InvokeDragSession__IDというメッセージは、子プロセス側でnsDragServiceProxy::InvokeDragSessionImpl()が呼ばれた時に(IPC周りの自動生成されたコードを経由して)親プロセスに向けて送出される模様。

以上の通り、成功するケースでは子プロセス側のInvokeDragSessionと親プロセス側のInvokeDragSessionが両方とも期待通りに処理されるのに対し、失敗するケースでは親プロセス側のInvokeDragSessionに到達する前に(ドラッグデータが0個という事で)処理が終わってしまっている、という事が分かりました。という事は、失敗ケースでは「子プロセスが送ったメッセージを何らかの理由で親プロセスが受け取れていない」か、もしくは「子プロセスがそもそもそのメッセージを送出していない」かのどちらかであるという事が言えます。

標準出力・標準エラー出力に現れるメッセージの監視

状況の把握のためにPBrowser::Msg_InvokeDragSession__IDというメッセージやその他のメッセージを親プロセスが受け取れているかどうかを調べたいと思ってコードを見ていると、TabParent::RecvInvokeDragSession()呼び出し元箇所mozilla::ipc::LogMessageForProtocol("PBrowserParent",...というコードがある事に気付きました。関数の定義を調べてみた所、これはデバッグビルドの実行時にMOZ_IPC_MESSAGE_LOGという環境変数にPBrowserParentまたは1が設定されている時に、fputsで標準エラー出力にログメッセージを出力するという物であることが分かりました。このログメッセージが現れるかどうかを調べれば、上記のIPCのメッセージを親プロセスが受け取っているかどうかが分かります。

fputsで直接標準出力や標準エラー出力に出力されたメッセージは、MOZ_LOGで収集するログには出力されません。ではどこを見ればよいかというと、Xcodeの右下のペインです。標準出力や標準エラー出力に出力されたメッセージは、ここで確認する事ができます。

デバッグ実行時の環境変数は、Xcodeのメニューの「Product」→「Scheme」→「Edit Scheme」→「Run」→「Environment Variables」で設定できます。ここでMOZ_IPC_MESSAGE_LOGPBrowserParentを設定して再度デバッグ実行して確認した所、成功ケースでは上記のログメッセージが出るのに対し失敗ケースでは出なかったという事から、親プロセス側がPBrowser::Msg_InvokeDragSession__IDというメッセージを受け取れていないという事が分かりました。

そうなると今度は、子プロセス側がIPCのメッセージを送出しているにも関わらず親プロセスが受け取れていない(IPCの仕組みの中での問題)のか、それとも子プロセス側でnsBaseDragService::InvokeDragSession()が実行されていないかのどちらかという点が問題になります。

任意のログを任意のタイミングで出力させる

ここで子プロセス側の処理に対してデバッガでブレークポイントを設定できればよかったのですが、弊社調査環境では何故か、MDNのマルチプロセス有効時のデバッグに関する説明通りに設定しているにも関わらず、設定したブレークポイントで処理を止める事ができないという状況でした。処理の流れを追いたい肝心のモジュールは前述した通りMOZ_LOGでの指定でログを出力してくれないため、これでは調査のしようがありません。

そのため、ここで初めてカスタムログに頼る事にしました。

MOZ_LOGで出力されるログは既存の物以外に、全く新しくログを出力する事もできます。C++製コンポーネントの開発に関わる人向けにMOZ_LOGで出力可能なログをC++製のモジュール内で定義する手順の説明が用意されていますので、これを参考に、EventStateManager.cppの冒頭で以下のようなログモジュールを定義するようにしました。

using mozilla::LogLevel;
static mozilla::LazyLogModule sEventStateManagerLog("EventStateManager");

その上で、ここまでに調査した範囲の中で特定していたドラッグを開始するかどうかの判定を何段階も行っている関数において、returnする直前にMOZ_LOG(sEventStateManagerLog, LogLevel::Info, ("DoDefaultDragStart - no drag service"));のような行を追加して、ログを見ればどのreturnで関数が終了したのかを分かるようにしました。

このようにして収集したログを調べて分かったのは、2回目以降の失敗ケースにおいては「現在進行中のドラッグセッションがまだあるならば、新たなドラッグセッションは開始しない」という判断の結果親プロセス側でドラッグセッションが開始されていない、という事でした。

つまり、1回目の失敗ケースから2回目以降の失敗ケースにかけての間では以下の事が起こっていたと考えられます。

  • 成功ケースや1回目の失敗ケースでは、子プロセス側では通常通りにドラッグセッションが開始されている。
  • その処理が親プロセスに引き継がれるまでの処理のどこかに問題があって、「子プロセスはドラッグセッションが開始されているが、親プロセスはドラッグセッションが始まっていない(終了した)と認識している」状態が発生した。
  • 本来であれば子プロセス側のドラッグセッションを終了させるための処理が行われるはずなのに、この一連の処理がどこかの時点で中断されてしまったために、子プロセス側のドラッグセッショが終了処理が呼ばれず、ゾンビドラッグセッションが残留してしまっている。
  • このゾンビドラッグセッションがあるせいで、親プロセスでドラッグセッションが開始されなくなっている。

ここまでで分かった事をまとめて、1回目の失敗ケースの原因を探る

ここまでの調査で、2回目以降の失敗ケースは「1回目の失敗ケースにおいて、子プロセス側のドラッグセッションが開始されたにも関わらず、親プロセス側ではドラッグセッションが開始されなかったために、子プロセス側で残ってしまったゾンビドラッグセッション」が原因で発生している事が分かりました。

そうなると今度は、1回目の失敗ケースで何故ゾンビドラッグセッションができてしてしまうのか、何故親プロセス側でドラッグセッションを開始できなかったのか(処理が中断されたのか)、という事を明らかにしなくてはいけません。

現時点までで、1回目の失敗ケースでは子プロセスから親プロセスへドラッグデータがきちんと引き渡されているという事が分かっています。そうなると、「ドラッグデータは受け取れているが、ドラッグセッションを開始できない」という状況が発生する条件は、一体何なのでしょうか?

以上の事を念頭に置きながら関連モジュールのコードを眺めていると、各プラットフォームで共通のドラッグセッション開始のための処理であるnsBaseDragService::InvokeDragSession()から呼び出されるmacOS版固有のnsDragService::InvokeDragSessionImpl()の実装の冒頭に、何らかの条件が満たされなかった時にNS_ERROR_FAILUREというエラーコードを返却するというコードがある事に気がつきました。

さらにその先を読み進めていくとbeginDraggingSessionWithItemsというCocoaのAPIを呼んでいる箇所があります。調べてみると、beginDraggingSessionWithItemsはまさにドラッグセッションを開始するためのCocoaのAPIであると書かれています。

ここまで分かってやっと、失敗ケースでdragoverイベントを発行するためのコードが何故呼ばれないかの謎が解けました。このコードはCocoaでのドラッグセッションが進行中である場合に呼ばれるdraggingUpdateというイベントハンドラの中にありますが、ドラッグセッションが開始されていなければ当然これらのイベントハンドラも呼ばれないという訳です。つまり最初から、beginDraggingSessionWithItemsの呼び出しに至るまでのコードパスを調査すればよかったのでした。Cocoa APIの知識を持たない状態で調査を始めたために、ひどく遠回りをしてしまった事になります。

ともあれ、これで調査が必要な範囲はだいぶ絞り込めました。今度はbeginDraggingSessionWithItemsが呼ばれなくなるパターンの分岐に絞ってMOZ_LOGを仕込んで再びログを収集してみた所、1回目の失敗ケースはまさに先程見付けたNS_ERROR_FAILUREを返す分岐に入っている、という事が分かりました。

その分岐に入る条件はgLastDragViewというグローバル変数が空である事で、これは意味としては、「どのビューでドラッグが開始されたか分からなければ、ドラッグセッションを開始せずにエラーを返す」という事です。

そこでこの変数に値を代入している箇所を検索した所、変数の初期化時を除くと、Cocoa API用のmouseDraggedというイベントハンドラの中でのみ値を設定している事が分かりました。具体的には、4728行目で「そのビュー自身」を代入し、イベントの処理を挟んだわずか10行後にnilを再代入しているという状況でした。そこでこの前後に絞ってMOZ_LOGをさらに仕込んで調査した所、成功ケースと失敗ケースでは非同期処理の実行順が異なっており、そのせいで失敗ケースでは、ドラッグが開始されたビューが分かるより前にドラッグセッションを開始しようとしてドラッグセッションを開始できずにいるらしいという事が分かりました。

詳しい人の意見を聞いてみる

Cocoa APIまわりの非同期処理の話になると、ますますこちらに知見がないため、これ以上の調査は難しく思えます。そこで一旦、識者に意見を求めてみる事にしました。

調査対象にしているファイルのコミット履歴を見ると、複数人が関与している様子が窺えます。この時に注目するのは、コミットした人やパッチを書いた人ではなく、パッチのレビューを行った人です。

パッチが投入されるまでの流れを見ると分かりますが、パッチのコミット自体はcheckin-neededという目印に沿って作業担当者が行っているだけなので、コミットした人は必ずしもそのモジュールのエキスパートという訳ではありません。また、パッチを書いた人もたまたま関わっただけの協力者という立場である可能性があります。それらに比べると、レビューはある程度の知見がないとできないため、複数のパッチでレビュー担当者として指名されている人ほど、信頼できる識者である可能性が高くなります。

Bugzillaではコメントを追記する時に「needinfo」という情報を設定できます。これはバグの報告者など他の人に情報を求める、つまり質問するときに使われる機能です。「Need more information from...」というラベルのチェックボックスをONにして、質問先の種別を「other」にし、上記の方法であたりを付けた識者の人のメールアドレスを入力して投稿すれば、これで「質問されている」という事がその人に通知されます。

数日待った所、その方が反応して下さり、「それぞれのケースのスタックトレースを収集してみてはどうか」というようなコメントを頂けました。

スタックトレースはXcodeのメイン画面の左ペインに表示されますが、これをそのままコピーする事はできません。文字列としてコピーできる形でスタックトレースを取得するには、ブレークポイントで処理を止めた状態でXcodeのウィンドウ右下のペインのコンソールの「(lldb)」と表示されている箇所(これが実はプロンプトになっています)にbtというコマンドを入力して実行すると、コンソール内にスタックトレースの情報が出力されます。これを選択してコピー&ペーストすれば、スタックトレースを容易に収集できます。

この方法でスタックトレースを収集してじっくり比較してみた所、成功ケースではCocoa APIのmouseDraggedハンドラからそのまま呼び出しが続いているのに対し、失敗ケースではそうなっておらず、RefreshDriverTimer::Tickなどのメソッドの呼び出しが親となっていたという事が分かりました。

(1つのイベントループの中で処理が完結する場合は成功する) (1つのイベントループで処理が完結しなかった場合は失敗する)

成功ケースでも失敗ケースでもmouseDraggedから全ての処理が始まっているとばかり思い込んでいたため、これは盲点でした。

どうやら、何らかの条件に合致した場合には、ドラッグ時のイベントの処理が実際には後のイベントループに回されており、その場合は必要な情報であるgLastDragViewが既に失われてしまっているのでドラッグセッションを開始できない、という事がこの問題の根本的な発生原因である模様です。このような状況は、元々同期処理で書かれていた物を後から非同期処理の形に改修したというような場面でよく見られる物です。Firefoxは現在、体感的なパフォーマンスの向上のための改良が続いていますので、その中でそういった事が起こる事は十分にあり得そうです。

パッチの作成

ともかく、原因が分かったことでようやく問題を修正する段階に移れます。

ここまでの調査で分かった原因からは、以下のような改修案が考えられます。

  1. gLastDragViewをすぐにnilで破棄してしまわずに、ドラッグイベントの処理が終了するまで待ってから破棄するようにする。
  2. gLastDragViewに相当する情報をイベントの情報の一部として引き回す、または保持しておき、後のイベントループで続きが処理される時にその情報を参照するようにする。

1の方法は非常に単純なやり方です。確実にこの問題は直りますが、gLastDragViewがメモリ上に保持される期間が長くなるため、メモリリークや、ドラッグ開始操作として認識されて欲しくない物が誤ってドラッグ開始操作として扱われてしまうといった新しい別の問題の原因になる恐れがあります。

2は、そういった副作用の恐れがない安全な方法に思えます。一般的にはこちらの方針で改修を行う事が望ましいと言えるでしょう。ただ、今回はこの方針を取るのがためらわれる理由があります。それは、gLastDragViewを設定している箇所や参照している箇所がmacOS固有の実装の中であるのに対し、設定箇所から参照箇所までの間に通過する処理はほとんど全てクロスプラットフォームな実装であるという点です。クロスプラットフォームな実装の中に特定プラットフォーム向けのコードを入れるという変更はあまり行儀が良いとは言えず、また、その中には非同期処理の基盤的な実装も含まれていたため、変更の影響が想像以上に広い範囲に及ぶ恐れがあります。副作用の恐れがないはずの方法の方が、実際にはリスクが大きいという困った状況です。

以上の検討結果を踏まえ、1の方が調査が必要な範囲は狭い(macOS固有の実装だけに変更が閉じる)と考えられたため、まずはその前提で影響範囲を調査・検討しました。その結果、想定外のタイミングでドラッグセッションの開始処理が呼ばれてしまう事は理論上あり得ないという結論に至ったため、実際に1の方針でパッチを作成しました。しかし安全であると言い切れる確証は持てなかったため、前のコメントで紹介して頂けた別のこの件に詳しい方にneedinfoで意見を求めてみる事にしました。

時間はかかったものの無事にその方にパッチを見て頂けて、方針はこれで問題ないというコメントと、追加で修正が必要な箇所(コメント形式で書かれた説明文が実装と食い違うようになるため、それらも併せて更新する必要がある)の指摘を頂きました。現在は、その指摘に基づいて更新したパッチを再提出してレビューを待っているという状況です。この後特に問題がなければ、パッチが取り込まれるまでの流れなどで紹介ている通りに進行してマージに至ると期待されます。

まとめ

以上、プロセス間通信やmacOSでのドラッグ&ドロップといった箇所の実装を調査して、バグの原因を特定しパッチを提出するまでの一通りの経緯をご紹介しました。

最終的なパッチは実質的には1行削除・2行追加しただけの内容ですが、そこに辿り着くまでの調査にはかなりの時間を要する結果となってしまいました。要因の1つに、Cocoa APIを使ったmacOSネイティブアプリにおけるドラッグ&ドロップの実装の一般的な作法を知らないまま調査を進めた事がある事は否めず、その部分の調査をスキップする選択をしたという調査初期の判断ミスが悔やまれます。

この記事で述べた情報そのものが直接的に役に立つ場面はあまり無いと思われますが、未知の部分が大きい調査対象に取り組む際の様々なアプローチの仕方や、調査方針の見直しのタイミングなど、メタな部分で知見を得るための資料、あるいは「しくじり先生」的な反面教師として参考にしていただけると、この紆余曲折の記録も無駄にはならないのではないかと思っております。

OSSの利点の1つとして、今回のように、不可解なトラブルに遭遇した場合でも第三者の立場で詳細な調査を実施できる余地があるという点が挙げられます。本体の開発チームは通常のリリースに向けての作業に注力している事が多く、今回の問題のように希な条件下でしか発生しない・一般のユーザーに与える影響の小さい問題の解決はどうしても後回しになってしまいがちです*5。しかし、その問題に遭遇した当事者にとってはまさに今直面している問題で、ともすれば死活問題ともなり得ます。そのような場合でもただ待つだけ以上の事ができるというのは、OSSならではの事と言えるでしょう。

当社は、そのようにOSS開発により積極的に関われるようになりたい人を支援するOSS Gateという取り組みを支援しています。このエントリを見て「普段使っているOSSに自分でもフィードバックできるのか! 自分もやってみたい!」と新鮮な驚きを感じた方や、「そうそう、こんな感じで調査するんだよね。このやり方が分からなくて困ってる人を手助けできればいいんだけど……」とお考えの方は、ぜひワークショップへの参加をご検討下さい。

また、当社ではFirefoxやThunderbirdの他、Fluentd、Groonga等のOSSの法人利用において発生する様々なトラブルや不具合について有償でのサポートサービスを提供しており、このエントリに記載しているようなソースコードレベルでの調査も承っております。業務上でのOSSの利用でお困りの場合、お問い合わせフォームよりお問い合わせ下さい。

*1 そこで学ぶ知識を今後も活用していける目処があるなら話は別ですが……

*2 ただ、こちらはCocoa(macOSでGUIアプリを実装する際に使われるAPIセット)のイベントハンドラにあたる関数で、単にCocoaでの`draggingUpdate`というイベントを`dragover`というDOMイベントにマッピングするためのものでしかありません。後述しますが、この事が後の調査を難航させる1つの原因になりました。

*3 使用した検証機ではフルビルドに4〜5時間程を要しました。

*4 ブレークポイントを左クリックすると、そのブレークポイントだけ一時的に無効化できます。右クリックして「Delete Breakpoint」を選択すると、ブレークポイントを削除できます。

*5 とはいえ、今回取り組んだBugはpriorityがP3(中程度)と設定されており、それなりに重要な問題と認識されてはいたようです。

2018-10-17

グループポリシーでエンタープライズの証明書を配布した場合、IE(Edge)では閲覧できるページがFirefoxではエラーで閲覧できない場合があります

Firefox ESR52以降のバージョンは、security.enterprise_roots.enabledtrueに設定しておく事で、Active Directoryのグループポリシー機能を使って配布された証明書をFirefoxに自動的にインポートできます *1。また、Firefox ESR60ではグループポリシー経由である程度の設定の制御も行えるようになりました。以前のバージョンのFirefoxでは証明書のインポートや管理者による設定の制御にはCCK2や独自形式の設定ファイルなどを別途用意する必要がありましたが、Firefox ESR52以降では設定も証明書の管理もActive Directoryのグループポリシーに一元化できるようになったという事で、Internet Explorer(およびその後継ブラウザであるEdge)をメインで運用しつつFirefoxも併用するという使い方をしやすくなってきていると言えます。

ただ、そのために、「IEではこれで問題なく閲覧できるのに、Firefoxでは何故かエラーになる」という状況が顕在化しやすくなっています。組織内のWebサイトにSSL/TLSで接続できるようにするために証明書をグループポリシーで配布した場合に、IE(Edge)では期待通りにそのWebサイトを閲覧できるのにも関わらず、Firefoxでは何故か証明書のエラーになって閲覧できない場合がある、というのもその一つです。

この現象が発生する原因として典型的なのは、配布されている証明書自体に問題があるというものです。

証明書の安全性がFirefoxの求める要件を満たしていない場合

Firefoxをアップデートしたら急に、今まで問題なく閲覧できていたWebサイトで「安全でない接続」というエラーが表示されるようになり、閲覧できなくなってしまった。という場合、これはFirefoxの許容する証明書の安全性の水準が見直された事が原因となっている可能性があります

「この水準の安全性が確保されていれば妥当な証明書として認める」という基準は、IE/EdgeとFirefoxで異なります。また同じFirefoxであっても、バージョンによってその基準が見直される事があります。Firefoxのアップデート直後からSSL/TLSに関するトラブルが急増したという場合には、Firefoxの各バージョンのリリースノートや、開発者向けリリースノートの「セキュリティ」の見出し配下を確認し、問題が起こるようになったバージョンにおいて何らかのアナウンスがなされていないかを確認してみて下さい。

GoogleやYahooなどの一般的なWebサイトを閲覧しようとして「安全でない」というエラーが表示されるようになったというケースも、証明書の安全性が低い事が原因である場合が多いです。これは、以下の例のような「SSLロガー」や「SSLプロキシ」と言われるようなネットワーク機器やサービスを使用している場合によく見られるトラブルです。

この種の機器やサービスは、使用にあたっては原理上必ず、各クライアントに専用の認証局証明書をインポートする必要があります。この認証局証明書がFirefoxの求める安全性の水準を満たさなくなると、SSL/TLSを使用したあらゆるWebサイトで「安全でない接続」の問題が起こるようになります。
(いわゆるSSLプロキシがある場合の通信)
導入手順の一環として流されがちなためか、「証明書をインポートして使っている」という事実を忘れてしまいやすい模様ですので、殊更注意が必要です。

認証局証明書として本来は不正な証明書を、気付かずに使っている場合

証明書の安全性の水準に問題がなくても「安全でない接続」のエラーが発生する場面がもう1つあります。それは、認証局証明書(CA*2証明書、あるいはルート証明書)として使えないはずの証明書をエンタープライズの証明書として配布してしまっている場合です。

具体的には、実験や組織内での一時的な使用のために作られる事の多い、「サーバーの署名用」ではあるが「認証局証明所用」ではない自己署名証明書をエンタープライズの証明書として配布している場合に問題が起こります。実際に、各種の証明書エラーの様子を確認できるbadssl.comというサイトにおいて自己署名証明書を使ったサイトの例で使われている証明書の詳細な情報を見ると、

  • Subject(この証明書の識別子と証明する対象):C=US, ST=California, L=San Francisco, O=BadSSL, CN=*.badssl.com
  • Issuer(この証明書の署名者):C=US, ST=California, L=San Francisco, O=BadSSL, CN=*.badssl.com
  • X509v3 extensions(拡張属性), X509v3 Basic Constraints(基本の制約事項):CA:FALSE(認証局ではない

と、まさにそのような形式になっています。

このような証明書を使っているWebサイトを訪問した場合には、必ず「安全でない接続」のエラーが表示され、内容を閲覧するためにはそのサイトをセキュリティの例外として登録しなくてはなりません。しかしながら一般的に、セキュリティの例外はユーザー個々人が手動で操作して行う必要があります。組織内で多くのユーザーが共通して使用するWebサーバーでこの種の証明書を使っている場合*3、全ユーザーに手動で例外登録をさせるというのは非現実的です。そこで取られる事があるのが、この証明書自体をエンタープライズの証明書として配布するという方法です。

この証明書がエンタープライズの証明書として読み込まれているクライアントPCでは、IE/Edgeで当該Webサイトを訪問すると、

  1. そのWebサイトが送ってくる証明書の署名者を確認する。
  2. エンタープライズの証明書に登録されている証明書(Webサイトが送ってきた証明書と同じ物)が見つかる。
  3. 見つかった証明書が認証局証明書として使われ、Webサイトが送ってきた証明書が正しく認証局証明書によって発行された物であると判定される。

という形で検証され、安全な接続であるとしてWebサイトのコンテンツがそのまま表示される結果になります。

Firefoxでsecurity.enterprise_roots.enabledtrueに設定されていている場合も、当然IE/Edgeと同様の検証が行われて当該Webサイトの内容が警告無しに表示できるはず……と思ってしまう所ですが、実際にはそうはなりません。Firefoxでは、

  1. そのWebサイトが送ってくる証明書の署名者を確認する。
  2. エンタープライズの証明書に登録されている証明書(Webサイトが送ってきた証明書と同じ物)が見つかる。
  3. 見つかった証明書にCA:TRUE(認証局証明書であるというメタ情報)が無いため、認証局証明書ではないと判定される。
  4. 1の証明書の署名者の認証局証明書が見つからないため、証明書の妥当性を検証できず、エラーになる。

という形で検証が行われるため、安全な接続ではないと判定され、Webサイトのコンテンツの代わりにエラーページが表示される結果になります*4

ユーザーが手動操作でFirefoxの証明書マネージャから認証局証明書をインポートする場合、本来であれば、用途が認証局証明書でない証明書は「この証明書は認証局の証明書ではないため、認証局の一覧には追加できません。」というエラーメッセージが出て拒絶されるため、誤って認証局に登録してしまうという事も起こりません。しかしながら、グループポリシー経由でエンタープライズの証明書として配布された物を security.enterprise_roots.enabled の機能でインポートする場合には、このチェックが行われません。また、厄介な事に、そうしてエンタープライズの証明書として一度認識されてしまうと、この証明書を証明書マネージャから手動操作で認証局証明書として再インポートしようとした場合にも、用途のチェックがスキップされてインポートに成功してしまうという性質があります。

この問題がそもそも発生しないようにするためには、以下のどちらかの方法を取るしかありません。

  • サーバーの証明書を再発行する。その時に、認証局証明書としての用途も併せて設定することで、認証局証明書で、且つサーバーの証明書という状態にしておく。
  • 認証局証明書を新たに作成し、その認証局で署名された証明書としてサーバーの証明書を再発行する。エンタープライズの証明書としては、認証局証明書の方を配布する。

一般的な認証局で発行してもらう証明書には適切な用途情報が設定されていますが、組織内でのみ使用するために独自に証明書を発行する場合、この点のチェックがなおざりになりがちです。証明書の用途は適切に使い分けるように気をつけましょう。

まとめ

以上、Active Directoryのグループポリシー機能を使って配布したエンタープライズの証明書をIE/EdgeとFirefoxで共用する場合に起こりがちなトラブルについて解説しました。

当社では、Firefoxの法人での運用における不明点の調査、ご要望に合わせた適切な設定のご案内、本体に含まれない特別な運用を実現するアドオンの開発などを広く承っております。Firefoxの運用でお困りのシステム管理・運用ご担当者さまは、お問い合わせフォームよりお気軽にお問い合わせ下さい。

*1 なお、この機能はActive Directoryを運用していない環境でも、Windowsのレジストリを編集することで動作の検証が可能です

*2 Certificate Authority=認証局

*3 本来であればそのような運用を取るべきではありません。

*4 これはIE/EdgeとFirefoxで証明書の取り扱いのポリシーの厳格さに差があるためで、どちらかが正しくてどちらかが間違っているという事はありません。IE/Edgeは証明書の用途に関する検証がルーズなので、そちらを基準にして運用していると支障が生じる場合がある、という事です。

2018-10-02

fcitx-mozcの入力モードを外部プロセスから制御する

はじめに

何年か前に、とある案件で「fcitx-mozcの入力モードを自分のアプリケーションから制御したい」というご要望をお客様から承りました。そのときに、fcitx-dbus-statusというFcitx用アドオンを実装して、このご要望にお応えしました。

今回はこのアドオンについて紹介します。

fcitx-dbus-statusとは

fcitx-dbus-statusはFcitxで動作している入力メソッドのステータスをD-Bus経由で取得したり、変更したりできるようにするためのFcitxアドオンです。dbus-sendコマンドやdbus-monitorコマンドを使えば、シェルスクリプトで入力メソッドのステータスを制御したり監視したりすることもできます。

主にMozcを想定して開発しましたが、他の入力メソッドでも使用することができます。

インストール方法

おそらくどのディストリビューションでもfcitx-dbus-statusのパッケージは用意されていないでしょうから、自分でビルドしてインストールする必要があります。

例えばUbuntu 18.04では以下のような手順でインストールすることができます。

$ sudo apt install g++ cmake fcitx-libs-dev libdbus-1-dev fcitx-module-dbus
$ git clone git@github.com:clear-code/fcitx-dbus-status.git
$ cd fcitx-dbus-status
$ mkdir build
$ cd build
$ cmake ..
$ make
$ sudo make install

インストールが完了したら、アドオンが確実に読み込まれるようにシステムを再起動した方が良いでしょう。

使用方法

scriptsディレクトリ以下にMozc用のサンプルスクリプトがあります。

たとえばMozcの入力モードを全角カタカナに変更したい場合には以下のコマンドを実行します。

$ ./scripts/set-mozc-composition-mode.sh katakana

現在のモードを取得したい場合は以下のコマンドを実行します。

$ ./scripts/get-mozc-composition-mode.sh

モードの変更をリアルタイムで検知したい場合は以下のコマンドを実行します。

$ ./scripts/monitor-status.sh

まとめ

fcitx-dbus-statusについて紹介しました。なお、ステータス変更のリアルタイム検知を実現するにはFcitx側の変更も必要だったので、事前にFcitxのメーリングリストで実装方針を作者の方に相談した上で、パッチを提案しています。この修正は既に取り込まれており、最近のLinuxディストリビューションではこの変更が含まれたバージョンを利用できるようです。

2018-09-06

YoctoのWeston上で日本語入力

はじめに

これまでにも何度か紹介していますが、クリアコードではGecko(Firefox)を組み込み機器向けに移植する取り組みを行っています。

その後、課題として残っていたWebRTCも無事に動作するようになり、主だった機能は主ターゲットであるRZ/G1M上で動作するようになっています。

今回は趣向を変えて、同環境上で少しだけ日本語入力を検証してみた経過を紹介します。

uimでの日本語入力の様子

Yoctoでの日本語入力事情

Yoctoの主要なレイヤを概観してみたところ、IMフレームワークとしてはuim、日本語変換エンジンとしてはAnthyが見つかりました。

逆に、これ以外のIMフレームワークや日本語変換エンジンのレシピを見つけることはできませんでしたので、今回はこれを使用してみます(uim-skkでも良いと思いますが、一般向けにはやや紹介しづらいので、今回はAnthyのみを対象とします)。

さて、ウィンドウシステムがX11であれば、おそらく上記レシピをそのままビルドするだけで使用できるでしょう。ですが、今回の対象はYoctoのcore-image-westonであり、ウィンドウシステムはWayland/Westonです。uimにWaylandサポートが入ったのは1.8.7からで、上記レシピは1.8.6ですから、恐らくこれを普通にビルドするだけでは日本語入力できるようにはならないでしょう。実際に試してみましたが、やはりアプリケーションがクラッシュして起動できないという結果となりました。

レシピの修正

上記仮説が正しければ、単にuimを最新版に上げるだけでWeston上でも動作するでしょう。ですが、実際にuimのバージョンを上げてビルドを試してみたところ、同レシピに含まれるパッチがそのままでは当たらないなどの問題が発生しました。そちらを修正するのも難しくはないでしょうが、まずは手っ取り早く動くか動かないかを確認したかったため、uim-1.5.6のまま最低限のパッチを最新のuimからバックポートしてみることにしました。以下がmeta-openembeddedに対するパッチです。

commit f3b0e042986a83a767a967ec352c731037693d98
Author: Takuro Ashie <ashie@clear-code.com>
Date:   Fri Aug 17 13:16:21 2018 +0900

    uim: First aid to work with Wayland

diff --git a/meta-oe/recipes-support/uim/uim/0001-Fix-the-problem-that-the-candidate-window-is-not-sho.patch b/meta-oe/recipes-support/uim/uim/0001-Fix-the-problem-that-the-candidate-window-is-not-sho.patch
new file mode 100644
index 0000000..6ebeb21
--- /dev/null
+++ b/meta-oe/recipes-support/uim/uim/0001-Fix-the-problem-that-the-candidate-window-is-not-sho.patch
@@ -0,0 +1,26 @@
+From f266ff2b59bc3b0cd732c62683a1df9672114c1d Mon Sep 17 00:00:00 2001
+From: Konosuke Watanabe <konosuke@media.mit.edu>
+Date: Sat, 20 Feb 2016 12:30:35 +0900
+Subject: [PATCH] Fix the problem that the candidate window is not shown in
+ GTK3 environment.
+
+---
+ gtk2/immodule/uim-cand-win-gtk.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/gtk2/immodule/uim-cand-win-gtk.c b/gtk2/immodule/uim-cand-win-gtk.c
+index 1bfe759c..41590d06 100644
+--- a/gtk2/immodule/uim-cand-win-gtk.c
++++ b/gtk2/immodule/uim-cand-win-gtk.c
+@@ -225,7 +225,7 @@ uim_cand_win_gtk_init (UIMCandWinGtk *cwin)
+ 
+   gtk_widget_set_size_request(cwin->num_label, DEFAULT_MIN_WINDOW_WIDTH, -1);
+   gtk_window_set_default_size(GTK_WINDOW(cwin), DEFAULT_MIN_WINDOW_WIDTH, -1);
+-  gtk_window_set_resizable(GTK_WINDOW(cwin), FALSE);
++  gtk_window_set_resizable(GTK_WINDOW(cwin), TRUE);
+ }
+ 
+ static void
+-- 
+2.17.1
+
diff --git a/meta-oe/recipes-support/uim/uim/0001-gtk3-support-Wayland-backend.patch b/meta-oe/recipes-support/uim/uim/0001-gtk3-support-Wayland-backend.patch
new file mode 100644
index 0000000..e40caeb
--- /dev/null
+++ b/meta-oe/recipes-support/uim/uim/0001-gtk3-support-Wayland-backend.patch
@@ -0,0 +1,71 @@
+From 06558e571967f3cb989bdb550d1dea05247cc21d Mon Sep 17 00:00:00 2001
+From: Kouhei Sutou <kou@clear-code.com>
+Date: Sat, 30 Dec 2017 21:15:50 +0900
+Subject: [PATCH] gtk3: support Wayland backend
+
+GitHub: fix #71
+
+Debian: 810739
+
+Reported by Thibaut Girka. Thanks!!!
+---
+ gtk2/immodule/gtk-im-uim.c   | 16 ++++++++++++++++
+ gtk2/immodule/key-util-gtk.c |  8 +++++++-
+ 2 files changed, 23 insertions(+), 1 deletion(-)
+
+diff --git a/gtk2/immodule/gtk-im-uim.c b/gtk2/immodule/gtk-im-uim.c
+index ac2918ce..066e5f5b 100644
+--- a/gtk2/immodule/gtk-im-uim.c
++++ b/gtk2/immodule/gtk-im-uim.c
+@@ -535,6 +535,22 @@ layout_candwin(IMUIMContext *uic)
+     gdk_window_get_geometry(uic->win, &x, &y, &width, &height, &depth);
+ #endif
+     gdk_window_get_origin(uic->win, &x, &y);
++    {
++      GtkWindow *window = NULL;
++      GdkWindow *gdk_window = uic->win;
++      while (gdk_window) {
++        gpointer user_data;
++        gdk_window_get_user_data(gdk_window, &user_data);
++        if (user_data && GTK_IS_WINDOW(user_data)) {
++          window = user_data;
++          break;
++        }
++        gdk_window = gdk_window_get_parent(gdk_window);
++      }
++      if (window) {
++        gtk_window_set_transient_for(GTK_WINDOW(uic->cwin), window);
++      }
++    }
+     uim_cand_win_gtk_layout(uic->cwin, x, y, width, height);
+   }
+ }
+diff --git a/gtk2/immodule/key-util-gtk.c b/gtk2/immodule/key-util-gtk.c
+index 27abd834..bd029e73 100644
+--- a/gtk2/immodule/key-util-gtk.c
++++ b/gtk2/immodule/key-util-gtk.c
+@@ -319,6 +319,7 @@ im_uim_init_modifier_keys()
+ #ifdef GDK_WINDOWING_X11
+   int i, k = 0;
+   int min_keycode, max_keycode, keysyms_per_keycode = 0;
++  GdkDisplay *gdk_display;
+   Display *display;
+   GSList *mod1_list, *mod2_list, *mod3_list, *mod4_list, *mod5_list; 
+   XModifierKeymap *map;
+@@ -329,7 +330,12 @@ im_uim_init_modifier_keys()
+ 
+   mod1_list = mod2_list = mod3_list = mod4_list = mod5_list = NULL;
+ 
+-  display = GDK_DISPLAY_XDISPLAY(gdk_display_get_default());
++  gdk_display = gdk_display_get_default();
++  if (!GDK_IS_X11_DISPLAY(gdk_display)) {
++    /* TODO: We may need to something for Wayland. */
++    return;
++  }
++  display = GDK_DISPLAY_XDISPLAY(gdk_display);
+   map = XGetModifierMapping(display);
+   XDisplayKeycodes(display, &min_keycode, &max_keycode);
+   sym = XGetKeyboardMapping(display, min_keycode,
+-- 
+2.17.1
+
diff --git a/meta-oe/recipes-support/uim/uim_1.8.6.bb b/meta-oe/recipes-support/uim/uim_1.8.6.bb
index 271718e..e7241f1 100644
--- a/meta-oe/recipes-support/uim/uim_1.8.6.bb
+++ b/meta-oe/recipes-support/uim/uim_1.8.6.bb
@@ -4,17 +4,19 @@ LICENSE = "BSD-3-Clause & LGPLv2+"
 LIC_FILES_CHKSUM = "file://COPYING;md5=32463fd29aa303fb2360faeeae17256b"
 SECTION = "inputmethods"
 
-SRC_URI = "http://uim.googlecode.com/files/uim-${PV}.tar.bz2"
+SRC_URI = "https://github.com/uim/uim/releases/download/uim-${PV}/uim-${PV}.tar.bz2"
 
 SRC_URI_append_class-target = " file://uim-module-manager.patch \
     file://0001-fix-bug-for-cross-compile.patch \
     file://0001-Add-support-for-aarch64.patch \
+    file://0001-gtk3-support-Wayland-backend.patch \
+    file://0001-Fix-the-problem-that-the-candidate-window-is-not-sho.patch \
 "
 SRC_URI[md5sum] = "ecea4c597bab1fd4ba98ea84edcece59"
 SRC_URI[sha256sum] = "7b1ea803c73f3478917166f04f67cce6e45ad7ea5ab6df99b948c17eb1cb235f"
 
 DEPENDS = "anthy fontconfig libxft libxt glib-2.0 ncurses intltool"
-DEPENDS_append_class-target = " intltool-native gtk+ gtk+3 uim-native takao-fonts"
+DEPENDS_append_class-target = " intltool-native gtk+3 uim-native takao-fonts"
 
 RDEPENDS_uim = "libuim0 libedit"
 RDEPENDS_uim-anthy = "takao-fonts anthy libanthy0 glibc-utils glibc-gconv-euc-jp"
@@ -31,6 +33,7 @@ EXTRA_OECONF += "--disable-emacs \
     --without-canna \
     --without-mana \
     --without-eb \
+    --without-gtk2 \
 "
 
 CONFIGUREOPTS_remove_class-target = "--disable-silent-rules"

なお、当時のmeta-openembeddedのレシピではuimのソースコードをダウンロードすることが出来なったので、上記パッチにはその修正も含まれています。この点については既にOpenEmbeddedプロジェクトに報告済みで、修正が取り込まれています

ビルド方法

ベースのブートイメージのビルド方法はこれまでと同様ですので割愛します。 これにuimを追加するには、以下の設定をconf/local.confに追加してcore-image-westonを再作成します。

IMAGE_INSTALL_append = " uim uim-common uim-gtk3 uim-anthy "

動作設定

Anthyを既定のIMとするには、以下の内容で設定ファイル~/.uimを作成します。

(define default-im-name 'anthy)

firefoxを起動する際に、環境変数GTK_IM_MODULE=uimをセットすることでuimが使えるようになります。

$ WAYLAND_DISPLAY=wayland-0 GTK_IM_MODULE=uim firefox

まとめ

YoctoのWeston上での日本語入力について、現在の検証状況を紹介しました。こんな記事を書いている暇があったらとっととアップストリームのuimをバージョンアップしてしまいたいところではありますが、すぐには作業に取りかかれないため、まずは社内Wikiの情報を切り貼りして公開してみました。

つづき: 2018-11-08
2018-08-31

ibus-daemonの--mem-profileオプションは非推奨になりました

はじめに

IBusの1.5.19からは、ibus-daemon--mem-profileオプションは非推奨になりました。 今回はその経緯について紹介します。

修正の経緯

IBusはGLibを外部ライブラリーの一つとして使っています。 このGLibですが、2.46からメモリプロファイリングに関する機能が動作しなくなりました。*1

これまでibus-daemonでは--mem-profileというオプションを指定するとメモリプロファイリングの機能が有効になり、 SIGUSR2を送りつけることでメモリ使用量に関するレポートを標準エラーに出力することができるようになっていました。 この機能を実現するために、ibus-daemonではGLibが提供する以下のAPIを使っていました。

しかし、GLib 2.46以降ではこれらの機能は非推奨となり使えません。

とはいえ、GLib 2.46というのはやや古く、これより前のものを採用しているのはUbuntu 14.04(trusty)あたりのリリースバージョンです。*2 そういった古めの環境でわざわざ最新のIBusを採用するというのは考えにくいため、IBusではGLib 2.46以前をサポートしないようにしました。

GLib 2.46以降の環境でibus-daemon--mem-profileオプションを指定した場合には警告するようになっています。

まとめ

今回はibus-daemon--mem-profileオプションが非推奨になった経緯を紹介しました。

*1 GLibとGIOとでメモリー確保の仕組みに互換性がないため。

*2 Ubuntu 14.04ではGLib 2.40が採用されている。

2018-08-14

不具合をフィードバックする時に、不具合の様子を動画で説明する

一般的に、バグ報告は「再現手順」「期待される結果」「実際の結果」をセットで説明する事が望ましいと言われます。報告をする側にとっては自明の事でも、報告を受ける側にとってはそうではないという事は多々あり、そういった情報を過不足なく伝えるための指針と言えるでしょう。

しかしながら、特定のタイミングで操作した場合にのみ再現するというような不具合だと、言葉で説明しきるのは難しい場合があります。また、日本人が英語でフィードバックするというように自分の得意でない言語を使う場面では、説明自体がそもそも困難な場合もあります。そのような場合に便利なのが「スクリーンキャスト(screencast)」です。

スクリーンキャストとは、自分が操作しているPCの画面全体またはその一部の様子を動画にした物の事です。ゲームの実況配信などもスクリーンキャストの一種と言えます。今回、Firefoxにおいてドラッグ操作と同時にタブの復元が行われると、以後のドラッグ操作のイベントが発行されなくなる、という不具合の報告を行うにあたり、現象の再現のための操作のタイミングがシビアだったため、念のためスクリーンキャストの形式でも現象発生時の様子を報告することにしました。同様の形でフィードバックをしたい人は参考にしてみてください。

スクリーンキャストの録画方法

以下は、OS標準の機能や標準添付のアプリケーションなどによる手軽なスクリーンキャスト撮影の方法をWindows、macOS、Linuxの各プラットフォームごとに例示します。文字を入れるなどの高度な編集を行いたい場合は、動画編集ツールを別途組み合わせて使ったり、より高機能なスクリーンキャスト撮影用アプリを使ったりする事をおすすめします。

Windowsの場合

Windows 10 Creators Update(バージョン1703)以降では、「ゲームバー」という機能を標準状態で使えます。これはその名の通りゲームアプリのための機能で、前述の例のようなゲーム実況を行えるよう、アプリケーションのウィンドウ単位でのスクリーンキャスト機能を含んでいます。1つのウィンドウの中で完結する現象であれば、これを使って説明用のスクリーンキャストを作成できます。

スクリーンキャストを撮影したいウィンドウにフォーカスがある状態で、キーボードのWindowsキーを押しながら「G」キーを押すと、以下のような画面が表示されます。

ゲームバーが表示された様子

ゲームバー上部の「Allow gaming features」のチェックボックスにチェックを入れると一旦ゲームバーが消えますので、もう一度ゲームバーを開きなおします。すると、先ほどまでは使用できなかったゲームアプリ向け機能を使えるようになっています。左上の「Game capturing」という領域にあるボタンがスクリーンキャスト用の機能です。

有効化されたスクリーンキャスト機能

中央の「●」アイコンのボタンをクリックすると、即座に録画が始まります。録画中は画面の右上の方に以下のような小さなツールバーが表示され、「■」アイコンのボタンをクリックすればその時点で録画が終了します。

スクリーンキャスト録画中に表示されるツールバー

録画が終了すると、撮影された動画が「ビデオ」内の「キャプチャ」フォルダ(C:\Users\(ユーザー名)\Videos\Capturesの位置)に保存されます。

保存されたスクリーンキャスト

macOSの場合

macOSでは、標準状態で添付されているアプリの「QuickTime Player」にスクリーンキャスト撮影機能が含まれています。スクリーンキャストを撮影する準備が整ったら、QuickTime Playerを起動します。

アプリケーション一覧からQuickTime Playerを選択している様子

QuickTime Playerを起動したら、メニューの「ファイル」から「新規画面収録」を選択すればスクリーンキャスト録画用のウィンドウが開かれます。

メニューの「新規画面収録」を選択している様子 画面収録の操作用ウィンドウ

赤い「●」アイコンのボタンの横にある「⌄」マークをクリックすると、カーソルを含めるかどうかなどのオプションを設定できます。準備ができたら「●」アイコンのボタンをクリックします。すると、画面上のどの領域の様子を動画として録画するかを訪ねるメッセージが表示されます。

録画する領域を訪ねるメッセージ

デスクトップ全体を録画すると動画が大きくなりすぎますので、現象を説明するのに必要最小限の領域を設定するのがおすすめです。画面上をドラッグすると、矩形選択の要領で録画対象の領域を設定できます。

録画領域が設定された状態

録画領域が決まったら、「収録を開始」ボタンをクリックすればすぐに録画が始まります。録画中は画面上部のメニューバーに「■」アイコンのボタンが表示されており、これをクリックすると録画が終了します。

録画を終了するボタン

録画された動画のプレビューが表示されますので、後は任意のファイル名で保存すれば*1スクリーンキャストは完成です。

Linuxの場合

Linuxディストリビューションのデスクトップ環境は、標準で録画機能を備えている例は今のところ無いようですが、スクリーンキャスト録画用のアプリケーションは簡単に導入できます。ここではその例としてUbuntu 16.04LTSでKazamを使う例を紹介します。

sudo apt install kazamでパッケージをインストールして、端末上でkazamコマンドを実行するかメニューから「Kazam」を選択すると、Kazamが起動します。

Kazamが起動した様子

Kazamは画面全体の録画、アプリケーションのウィンドウ単位での録画、指定した領域の録画のそれぞれに対応しています。「Window」をクリックすると、録画対象にするウィンドウをクリックで選択する画面に切り替わります。「Area」をクリックすると、録画領域を矩形で選択する画面に切り替わります。矩形選択の場合は、範囲を選択した状態でEnterキーを押すと選択が確定されます。

録画領域を設定している様子

録画対象が決まったら、メインウィンドウの「Capture」ボタンをクリックすると録画が始まります*2。Kazamが起動している間はデスクトップ上部のバーにカメラのアイコンが表示され、これをクリックしてメニューから「Finish recording」を選択すると録画を終了できます。

録画を終了する様子

録画を終了すると、動画の取り扱いを選択するダイアログが表示されます。

録画した動画の取り扱いの選択

ここから動画編集アプリを起動して加工工程に移る事もできますが、録画した物を無加工でアップロードするだけであれば、「Save for later」を選択して動画ファイルを保存します。

スクリーンキャストの公開

FirefoxのBugzilla(bugzilla.mozilla.org)ではbugに任意のファイルを添付できますので、録画したスクリーンキャストをそのまま添付・アップロードする事も可能です。

メーリングリストへの投稿の場合のように、動画そのものを添付できない*3ケースでは、別途YouTubeのような動画共有サイトに投稿してそのURLを記載するという方法をとると良いでしょう。その場合、動画の共有範囲を「公開」とするか、またはURLを知っている人なら誰でも閲覧できるように設定しておく必要があります。

まとめ

以上、各プラットフォームでのスクリーンキャストの録画手順を簡単に紹介しました。

ところで、キーボード入力に関連する報告では「ここでこのキーを押した」という情報もスクリーンキャストに含めたくなるでしょう。そのような場合には、Linuxのデスクトップ環境であればscreenkeyというツールを使ってキー入力の様子を可視化できます

障害報告のフィードバックは分かりやすい言葉で書かれているに超した事はありませんが、「言葉で説明しなければならない物」ではありません。スクリーンキャストや、あるいはスクリーンショットのように、「言葉で説明するより見た方が早い」物については、むしろ積極的に画像や動画を活用して障害発生時の様子を伝えた方が良いでしょう。皆さんも是非、様々な手段を駆使してより分かりやすいフィードバックを行うように工夫してみてください。

*1 初期状態では「.mov」形式になります。

*2 初期状態では5秒間のカウントダウン後に録画が始まります。

*3 メールに動画を添付すると、そのファイルが全受信者に複製されて届き、各受信者の通信帯域やローカルディスクを圧迫する事になるため、メールへの大きなファイルの添付は避ける事が望ましいです。

つづき: 2018-10-17
2018-07-31

2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
タグ: