2012年やったこと - Mozc 編

いままで仕事で書いたコード (Chromium とか Mozc とか) については基本的に言及しないようにしていたんですが,ソースコードが公開されている内容についてはまあいいかということでまとめてみます (もちろんソースコードが公開されていないことについても色々やってますが).とりあえず Mozc 編*1.順序は概ね時系列.

Visual C++ 2010 対応

ちまちまとコンパイルエラーに対処.ビルドスクリプトを vcbuild に加えて msbuild にも対応させたぐらい.あとは gyp にも問題を見つけたので一件パッチを upstream.

1.3.975.10x にてリリース.

SHOW_INFOLIST_IMMEDIATELY compatibility flag

Windows 環境でのバグ.
Meadow 3.0 や NTEmacs 22 で Mozc を利用していると,用例ウィンドウが表示されないというバグ.Mozc 開発チーム内で発見されて回ってきたもの.
これの原因がなんというか脱力系.もともと用例ウィンドウは「最後の UI 更新から 500 ミリ秒経って何のアクションも起きていなければ表示する」というロジックで動いていた.ところが,Meadow 3.0 や NTEmacs 22 はポーリングを行っていて,より短い間隔で IME の UI 更新通知を送ってくる (実際には更新する必要がないケースでも).これをトリガーとして候補ウィンドウの再レイアウトが行われるため,永遠に「最後の UI 更新から 500 ミリ秒経過」という条件が成立しない.
これらのアプリケーションではディレイなしに用例ウィンドウを表示するようにして対処.

1.3.975.102 にてリリース.

「半角空白を入力」が動作しないバグの修正

たまには変換エンジン側のバグも直してみようキャンペーン第一弾.
カスタムキーマップにて「半角空白を入力」「全角空白を入力」というコマンドが選択可能であるにも関わらず,特定条件でうまく動作しないという問題,の修正.

1.4.1003.10x にてリリース.

キャンセル後 IME を無効化

たまには変換エンジン側のバグも直してみようキャンペーン第二弾.
ATOK と MS-IME のデフォルト設定の違いのひとつに,IME をオフにしたときに未確定の入力文字列を確定させるかそれともキャンセルさせるか,というものがある (MS-IME では確定,ATOK の ATOK キーマップではキャンセルさせるのがデフォルト).
これまでのバージョンの Mozc では MS-IME にあわせて確定させていたのだけど,やっぱり ATOK スタイルが良いという人もいる.そういう人のためにカスタマイズの幅を増やしてみたという変更.
といっても,これ以上コンフィグオプションを増やしたくはないよねということで,「キャンセル後 IME を無効化」というキーコマンド側を追加することで対処してみた.この方法だと,ATOK キー設定に自然に対応させられるのもよし.

ATOK (キーマップ) 派の人向けに実装しただけのつもりが,なぜか vim 方面の人に受けるという不思議な経験もした.

バージョン 1.4.1003.10x にてリリース.

サロゲートペア対応

クライアント側と変換エンジン側両方の修正.
Windows クライアントに関しては,実際サロゲートペア対応をサボっていたのでその対応を粛々と.再変換とか確定取り消し周りが地味にめんどくさかった.
変換エンジン側は,UTF-8 を UCS2 に変換して処理している箇所がいくつか残っていたのでその残党狩り.
この変更のおかげで,後の Unicode 絵文字対応がそこそこ楽になる.
バージョン 1.4.1003.10x にてリリース.

Mozc Issue 14

uim-mozc の開発者の人からのレポートで,Protocol Buffers のライブラリと動的リンクしていると,起動時にエラーになるので静的リンクに変更して欲しい,というもの.レポートから 1 年半以上放置されていたので見かねて修正.
ちなみに Debian/Ubuntu/Fedora/Vineはこの辺まるっと無視して今も動的リンクしていたはず.ほんと静的リンク嫌いますよね……

1.4.1003.10x にてリリース.

GCC 4.7 環境での warning つぶし

Linux 向け修正.
タイトル通り.
1.4.1003.10x にてリリース.

ibus_engine_delete_surrounding_text 対応

Linux の IBus 環境向け変更.IBus の比較的新しい API である ibus_engine_delete_surrounding_text を使用して,確定取り消し・再変換を実装するというもの.これ以前は Windows のようにバックスペースキーイベントを送っていた.
ちょうど Firefox 側に関連バグが登録されていたのだけど,Mozc 側の問題ということで対処できた感じになる.

1.4.1003.10x にてリリース.

gtest なしでのビルドのサポート

openSUSE のパッケージで,Mozc が gtest に依存するのを独自パッチをあててまで回避しているっぽかったので,そういうことをしなくて良いように上流側で対処してみた,というもの.
1.4.1033.10x にてリリース.

compiler_specific.h 導入

コンパイラ依存コードの便利マクロ集.Chromium の同名のファイルにインスパイアされたもの.この頃から Clang 対応を考え始める.

1.5.1053.10x にてリリース

cURL 依存の排除

Linux 向け変更.
OSS Mozc ではネットワーク機能を一切有効にしていないにもかかわらず,ビルドに cURL が必要という状態だったので,cURL なしでビルドできるようにしたというもの.
1.5.1053.10x にてリリース

QTBUG-25536 対策

Qt QString::toUcs4() がサロゲートペア領域の文字に対してうまく動かない,という問題に対する Mozc 側のワークアラウンド.

const uint kData[] = { 0x10000 };
QString test = QString::fromUcs4(kData, 1);
const QVector<uint> ucs4s = test.toUcs4();
if (ucs4s.size() == 1) {
    qDebug() << "correct";
} else {
    qDebug() << "wrong";
}

このコード,最新の Qt 5.0.0 でも正しく動かない.ので,Mozc から QString::toUcs4() の呼び出しを削除する方向で対処.

1.5.1053.10x にてリリース

X11 Selection monitor

Linux 向け.
ibus-mozc が GtkTextView でしか再変換ができなかったのを,もうちょっと対応環境を増やした,という変更.
諸悪の根源は,Gtk IM Module の get_surrounding_text がしょぼいことである.この API,カーソル周辺文字列 + カーソル位置しか取得できない.アンカー位置が取得できないため,現在選択されているテキストというのが分からない状態で再変換を実装しなければならない.
今回とった workaround は,xcb-xfixes を利用して,X11 のセレクションイベントをモニターするというもの.セレクションイベント経由で現在選択されているテキストを記録しておいて,get_surrounding_text で取得された周辺文字列 + カーソル位置と比較し,矛盾がないように選択領域を再構築する.
xcb とかはじめて使ったのでなかなかにおもしろかった.

あと,このコードを書いているときに Linux 版 Firefox にサロゲートペア絡みのバグを見つけたので,Bugzilla に報告したところ,Mozilla Japan の中野さんに直して頂けました.感謝.

1.6.1187.102 にてリリース.

Mac OS X Lion でのビルド対応

対応したつもりになって 1 年近く放置してしまったバグの対処.

1.6.1187.102 にてリリース.

scim-mozc is removed

予告通り scim-mozc をリポジトリから削除.
直接の原因は内部のメンテナの不在.
まあ今更 SCIM もないでしょう.(Tizen を除く)

1.6.1187.102 にてリリース.

Debian-specific files are removed

Mozc のリポジトリに存在する Debian パッケージ定義ファイルがメンテナンスされていないので削除した,というもの.
ちょうど Debian 本家でメンテナンスされているパッケージ定義ファイルとの乖離が激しくなってきたところだったというのも理由のひとつ.

1.6.1187.102 にてリリース.

IBus 1.4.1 未満のサポート停止

IBUS_CHECK_VERSION マクロがあちこちに散らばっていたので,一カ所にまとめた上,旧バージョンのサポートと停止.だいぶコードが綺麗になる.

1.6.1187.102 にてリリース.

Clang 対応開始

Clang revision 159409 を使用して発生する warning を全て解消 (または suppress).
1.6.1187.102 にてリリース.

Visual C++ 2012 対応

ちなちまとコンパイルエラーに対処.
1.6.1187.102 にてリリース.

#include <Windows.h> 撲滅運動

実に多くのヘッダファイルが不必要に #include <Windows.h> していることに気付きあちこち改良した,というもの.logging.h が <Windows.h> に依存するとかどうかと思う.

これでビルドがちょっとは速くなってくれるといいなぁと思いつつやったものの,目立っては高速化しなかった.残念.
1.6.1187.102 にてリリース.

Ack-less IPC

Windows の IPC レイヤの改善.いままでの IPC クライアントは,
1. メッセージ送信
2. 応答受信
3. Ack 送信
という手順を踏んでいたのを,3. でいきなり名前付きパイプを切断するように変更.切断イベントはどちらにしろサーバ側で検知可能なので,これを Ack イベントの代用とする.
実際これでレイテンシが数パーセント減ったのだから儲けものである.

1.6.1187.102 にてリリース.

StringPiece の移植

Mozc の portable library に StringPiece がないのが気になっていたので,えいやっと用意した一品.StringPiece については hamaji さんの解説記事 とか参照のこと.

1.6.1187.102 にてリリース.

テンキーにあるコンマに対応

Apple 日本語キーボードのテンキーには,ピリオドキーだけでなくコンマキーもあるのですが,これに対応できていなかったという問題の修正.
Mac では kVK_JIS_KeypadComma を使えば良いのですが,困ったのが Windows.
なんと Brazilian キーボードにはテンキーにコンマがあるらしく,その仮想キーコードは VK_ABNT_C2 (0xC2) とのこと.実際,Apple 日本語キーボードを Windows 7 につなげてテンキーのコンマを押してもこの仮想キーコードが返ってきました.互換性への執念恐るべし.
https://code.google.com/p/mozc/source/browse/trunk/src/win32/ime/ime_keyevent_handler.cc?r=124#253

  // The numpad comma on the Apple Japanese 109 keyboard is somehow mapped into
  // VK_ABNT_C2, which is only defined in kbd.h.
  // See also http://blogs.msdn.com/b/michkap/archive/2006/10/07/799605.aspx
  // See also b/6639635.
  KeyEvent::COMMA,                // 0xC2: VK_ABNT_C2

1.6.1187.102 にてリリース.


2012年他にやったこと - Mozc 編 - NyaRuRuが地球にいたころに続く.


Chromium に関しては 2012年やったこと - Chromium 編 - NyaRuRuが地球にいたころ に続く.

*1:ここでは OSS Mozc に限らず,いわゆる code-name Mozc についての活動

apt-get upgrade gcc considered harmful

いまの仕事に就いたことで触れることのできた興味深いソフトウェア開発手法のひとつに,成果物に関するものであれば何であれ"ソースコード"のバージョン管理システムに関連づけるというものがある.使用するライブラリは言うに及ばず,ビルドに使用するコンパイラのバージョンまでもを,ソースコードと同列にバージョン管理するわけだ.
このような環境で仕事を続けることしばし,ふと気付けば,多くの Linux ディストリビューションで喧伝されるうたい文句,「依存関係を賢く管理し,多数のユーザーによってテストされ,コマンド一発でインストールもアップデートもできる多数の開発ツール!」,というものは酷く色あせて聞こえるようになっていた.
むしろ逆だ.ソースコードバージョン管理システムとは異なるコマンドによってコンパイラやライブラリのバージョンが変化する!? そんなものは悪い冗談にしか聞こえない.そこにはコミットログも存在しなければ,コミット前のスモークテストも存在しない.ブランチを切ることもできなければ,不具合を見つけてもソースコードのようにはロールバックすることもできない.


などというようなことを『継続的デリバリー』を読みつつうなずいていた年末.いやー,本に書いてあるようなお手本運用を間近で見つつ仕事できるってのは刺激になっていいですねー.それではみなさま,来年も良いビルドを.
継続的デリバリー 信頼できるソフトウェアリリースのためのビルド・テスト・デプロイメントの自動化

サービスを最小特権で実行する

今回は Windows サービスを作成する上でのセキュリティ上のポイントを軽く紹介する.
Windows Vista では,サービスをより安全に実行するために Service Control Manager (SCM) の改善が行われている.ポイントとなるのは,必要特権リストの指定が可能になったこと,および制限された SID を割り当てられるようになったことだ.
たとえば,特定ファイルを物理メモリ上に保持し続けるサービスを作りたいとする.この処理をサービスにする必要があるのは,それが特権を必要とするからだ.VirtualLock API でロック可能なメモリ領域は通常 30 ページに制限されており,SetProcessWorkingSetSize API でその制限を拡大するには,SE_INC_BASE_PRIORITY_NAME 特権が必要である.しかし,単純に System アカウントで動くサービスを作ったのでは,余分な特権まで有効にされてしまう.これは最小特権の原則に反する.
ここで,以下のように ChangeServiceConfig2 API を利用することで,サービスに付与される特権を制限できる.

SERVICE_REQUIRED_PRIVILEGES_INFO privileges_info = {};
privileges_info.pmszRequiredPrivileges = SE_INC_BASE_PRIORITY_NAME _T("\0");
ChangeServiceConfig2(service_handle, SERVICE_CONFIG_REQUIRED_PRIVILEGES_INFO,
                     &privileges_info);

また,このサービスはファイルやレジストリに書き込む必要がない.そこで,以下のように設定することで,意図せずファイルやレジストリに書き込もうとしたときに失敗するように構成できる.

SERVICE_SID_INFO sid_info = {};
sid_info.dwServiceSidType = SERVICE_SID_TYPE_RESTRICTED;
ChangeServiceConfig2(service_handle, SERVICE_CONFIG_SERVICE_SID_INFO,
                     &sid_info);

このように設定したサービスを起動し,Process Explorer でセキュリティ情報を見てみる.
f:id:NyaRuRu:20121015010639p:plain
Restricted SID が設定されていること,"NT AUTHORITY\WRITE RESTRICTED" SID が設定されていること,特権が大幅に削除されていることなどがわかる.
実際,OS 標準のサービスは概ね最小特権で実行されている一方で,(残念ながら Microsoft Office を含む) OS 非標準のサービスは,デフォルトの特権を持ったまま動作しているものがほとんどだ.例えば,Office 2010 に付属する ImeDictUpdateService (Microsoft IME Dictionary Update) のセキュリティ属性は以下のようになる.
f:id:NyaRuRu:20121015010658p:plain
デフォルトで付与される全ての特権を持ったまま実行されていることが分かる.これが本当に IME の辞書アップデートサービスであるなら,SeDebugPrivilege (スーパー特権のひとつ.プロセスのセキュリティ設定を無視して,(保護されたプロセスをのぞく) すべてのプロセスを開くことができる.) や,SeTimeZonePrivilege (文字通りタイムゾーンを設定するための特権) をはじめとした,多くの特権を削除しても恐らく動作は可能だろう.

AppContainer 導入による Windows 開発への影響

Windows 8 では,AppContainer と呼ばれる新たな Sandbox メカニズムが導入される.Windows ストアアプリ (旧名: Windows Metro アプリ) や,Immersive モードの Internet Explorerレンダリングプロセスなどが AppContainer で動作するプロセスの代表格だ.Windows ストアアプリ制作者にとって AppContainer が重要になるのはもちろんのこと,Windows ストアアプリ内で動作する必要のある IMEアクセシビリティ系ツールの制作者も,AppContainer について理解する必要がでてくる.

AppContainer プロセスを確認する

Process Explorer 15.23 を用いて,AppContainer プロセスの特徴を確認してみよう.以下は Windows 8 付属の「ミュージック」アプリを起動し,Process Explorer にて表示してみたところだ.
まず目に付く点として,WWAHost.exe プロセスの Integrity 欄に AppContainer と表示されていることが挙げられる.
f:id:NyaRuRu:20121008185343p:plain
次に WWAHost.exe プロセスのプロパティを表示し,Security Tab を選択してみる.プロセスのグループ一覧に,AppContainer と Capability という見慣れないグループがある.
f:id:NyaRuRu:20121008185402p:plain
前者は Windows ストアアプリごとに割り当てられるユニークな SID である.Android 環境でアプリケーションごとに User ID が割り当てられるのと似ている.後者の Capability は,この Windows ストアアプリに許可された動作と対応している.「ミュージック」アプリは,

  • インターネット接続へのアクセス
  • ミュージックライブラリへのアクセス

が許可されているようだ.
このように,AppContainer は,従来のトークン/SIDによるアクセス制御の拡張として実装されていることが分かる.例としては,Windows ストアアプリごとに SID が付与されることから,ある Windows ストアアプリにだけ,特定ディレクトリ以下へのアクセスを許すようなポリシーを設定することも可能である.

あるプロセスが AppContainer に属しているかどうかをプログラム的に判定する

あるプロセスが AppContainer に属しているかどうかをプログラム的に判定するには,プロセスのトークンハンドルに対して GetTokenInformation API を使用する.Windows 8 (SDK) 以降では TokenIsAppContainer フラグが使用でき,ここで 0 が返ってこなければプロセスは AppContainer で動作している.

あるプロセスが Immersive Mode かどうかをプログラム的に判定する

あるプロセスが没入型 UI を使用しているかどうかの判定には IsImmersiveProcess API を用いる.この用途で GetTokenInformation/TokenIsAppContainer フラグを使用するべきではない.なぜか?
それは Windows ストアアプリ風アプリケーション全てが AppContainer 内で動作するわけではないからだ.ブラウザという大きな例外がある.
Metro スタイル対応デスクトップ ブラウザーの開発」というホワイトペーパーに解説されているとおり,Windows 8 対応のブラウザには以下の 3 種類が存在する.

  1. Windows Store app: AppContainer 内で動作する
  2. Desktop browsers: Windows 7 までと同様のモデルで動作する.Win32 API にフルアクセスが可能で,JIT コンパイルや様々なマルチプロセス技術が利用可能.
  3. New experience enabled desktop browser: Windows ストアアプリ風の没入型 UI を備えつつ,Win32 API にフルアクセスが可能で,JIT コンパイルや様々なマルチプロセス技術が利用可能.

この 3 つ目が問題だ.結果として,没入型 UI かどうかの判定と AppContainer で動いているかどうかの判定は分けて考える必要がある.

AppContainer の SID を取得する

プロセスのトークンハンドルに対して GetTokenInformation API を使用する.Windows 8 (SDK) 以降では TokenAppContainerSid フラグが使用でき,ここで返ってきたものが AppContainer の SID である.

AppContainer プロセスのオブジェクト名前空間

引き続き,Process Explorer で AppContainer の動作を調べてみる.「View」→「Lower Pane View」から「Handles」を選択し,プロセス内で開かれているハンドル情報を調べてみよう.
開かれているハンドル名を見ていくと,セッション名前空間にさらに AppContainer SID が接頭辞として付加されていることに気付く.
f:id:NyaRuRu:20121008185545p:plain

\Sessions\1\AppContainerNamedObjects\S-1-15-2-43664394-2685677502-394080391-3933305958-4167273977-1510959782-2102270723\RPC Control\OLE36E12F064548B659D96055B91BF5

これに対応すると考えられる GetAppContainerNamedObjectPath API の Remarks が興味深い.この Remarks は,アクセシビリティ系など,別プロセス内で動作するツールに向けて書かれたものだ.

For assistive technology tools that work across Windows Store apps and desktop applications and have features that get loaded in the context of Windows Store apps, at times it may be necessary for the in-context feature to synchronize with the tool. Typically such synchronization is accomplished by establishing a named object in the user's session. Windows Store apps pose a challenge for this mechanism because, by default, named objects in the user's or global session are not accessible to Windows Store apps. We recommend that you update assistive technology tools to use UI Automation APIs or Magnification APIs to avoid such pitfalls. In the interim, it may be necessary to continue using named objects.

GetAppContainerNamedObjectPath

デフォルトでは Windows Store アプリからは従来の名前付きオブジェクトにアクセスできないものの,アクセス権を設定しさえすればデスクトップアプリと Windows Store アプリの間で名前付きオブジェクトを共有することが依然として可能であることが示唆されている.なお,これは暫定的な措置であり,本来は UI Automation API や拡大鏡 API を用いて対処して欲しいようだ.

All Application Packages グループを利用したアクセス制御

実は,Windows 8 では ALL APPLICATION PACKAGES と呼ばれる特殊なグループが追加されている.このグループへのアクセス許可を設定することで,Windows Store アプリに一括して動作の許可や拒否を設定することができる.
これについては,ファイルシステムの例がわかりやすい.%SystemRoot% や %ProgramFiles% のセキュリティ設定を見てみよう.Windows 8 では,デフォルトで ALL APPLICATION PACKAGES グループに対する「読み取りと実行」「フォルダーの内容の一覧表示」「読み取り」アクセスが許可されている.
f:id:NyaRuRu:20121008185631p:plain
仮に IME を開発するのであれば,仮に AppContainer 内で動作する場合であっても,ACL 的にはこれらのディレクトリ (以下) に配置された設定ファイルや辞書データは自由に読み取りできるものと考えられる.
また,試したわけではないが,AppContainer 内で動作する Windows Store アプリであっても,%ProgramFiles% 以下にインストールされたアプリケーション一覧や置かれたファイルの内容を取得することぐらいは可能なのかもしれない.これらの領域にセキュリティやプライバシーに関わるデータが置かれる場合は,悪意のある Windows Store アプリに読み取られる恐れがないか改めて確認されたい.
さて,ALL APPLICATION PACKAGES だが,ファイルオブジェクト以外にも当然利用されているようだ.
以下は,TSF が利用していると思われる Mutex オブジェクトのセキュリティ情報である.
f:id:NyaRuRu:20121008185714p:plain
ALL APPLICATION PACKAGES に同期とクエリ許可が与えられていることが分かる.このように,DACL を設定することで,Windows Store アプリとデスクトップアプリの間で名前付きオブジェクトを共有することが可能になるのだろう.
Windows Store アプリとデスクトップアプリの間のプロセス間通信についてもいずれいくつか書いてみたい.

参考

Google Chrome をデバッグする (1)

Google Chrome のデイリーリリースバージョンは,炭鉱にて危険をいち早く察知するカナリアにたとえられ Canary 版と呼ばれている.
(炭鉱で戦うものたちの熱い物語については『炭鉱の庭師』を参照されたい)
Google Chrome Canary 版は以下のページからインストールできる.
https://tools.google.com/dlpage/chromesxs
Canary 版と安定版は,インストール先フォルダから使用するプロファイルまで異なる別アプリケーションである (Side-by-side インストールと呼ばれている).両者を同時に起動することももちろん可能だ.また,Canary 版であっても Google Update の対象になることから,一度インストールしてしまえば後は自動で trunk を追いかけてくれる.
実は,Canary 版にはもうひとつ大きな特徴がある.それは,ビルドの副産物であるデバッグシンボルが公開されていることだ.ここで,デバッグシンボルが公開されていることで何が可能になるかについて,数回にわたって紹介してみたい.

シンボルサーバの登録

Windows エコシステムでのシンボルサーバの登録には,環境変数 _NT_SYMBOL_PATH を使用するのが一般的だ.Microsoft のシンボルサーバと Chrome のシンボルサーバを登録する場合,_NT_SYMBOL_PATH は次のように設定することになる.

_NT_SYMBOL_PATH=srv*c:\SymCache*http://msdl.microsoft.com/download/symbols;srv*C:\SymCache*http://chromium-browser-symsrv.commondatastorage.googleapis.com

"C:\SymCache" は別の名前に変更することも可能である.このフォルダにダウンロードされたシンボルデータがキャッシュされるので,なるべく高速なドライブを設定すると良い.
次に,よく利用する Sysinternals のツール群についてもシンボルパスを設定する.
f:id:NyaRuRu:20120930014804p:plain
Process Explorer と Process Monitor それぞれで,Options から Configure Symbols を選択する.Dbghelp.dll path については,Windows SDK 付属のものを選ぶとよい.64-bit 環境では x64 版の Dbghelp.dll を選択しよう.Symbol paths については先ほど同様に以下を設定する.

srv*c:\SymCache*http://msdl.microsoft.com/download/symbols;srv*C:\SymCache*http://chromium-browser-symsrv.commondatastorage.googleapis.com

Process Explorer を利用したハングアップ解析

Google Chrome Canary 版が無反応になったなら,Process Explorer を起動してみよう.デバッグシンボルが完備されていればその場でスレッドのコールスタックを見るのも容易である.(ただし,ここで最も慎重な次の一手はプロセスダンプをとることだ)
状況の保全のため,プロセスの全スレッドを中断させてみよう.一般的に,Chrome のウィンドウが無反応になったときには Chrome のブラウザプロセスの問題である.Process Explorer の "Process" 欄を何度かクリックして,表示モードをプロセスツリーに変更しよう.ここでルートに当たる chrome プロセスがブラウザプロセスである.(別の識別法として,プロセスの起動引数に --type オプションが付いていないプロセスを選ぶという方法もある)
f:id:NyaRuRu:20120930014912p:plain
ブラウザプロセスを見つけたら,コンテキストメニューから Suspend を選択する.これでブラウザプロセスの動作が中断する.動作を中断させたら,コンテキストメニューから Properties を選択し,Threads タブに移動する.
f:id:NyaRuRu:20120930014932p:plain
ここでスレッドを選択し Stack ボタンをクリックすると,該当スレッドのコールスタックが表示される.
f:id:NyaRuRu:20120930014952p:plain
コールスタックを表示すると,非同期でシンボルのダウンロードが始まる.しばらくすると,Chrome のどういった関数が呼び出されていたのかが表示されるだろう.UI スレッドを探し出し,そのコールスタックを探し当てたら,それがハングアップの現場である.

Process Monitor を利用した I/O モニタリング

Process Monitor を利用することで,以下のような動作のモニタリングが可能になる.

  • 任意のファイルアクセス
  • 任意のレジストリアクセス
  • 任意のプロセス起動/スレッドイベント/DLL のロードとアンロード
  • 任意のネットワークアクセス

シンボルがセットされていると,これらの各イベント発生時のコールスタックが取得できるようになる.また,Tools メニューの各 Summary 項目から,選択したイベントの集計やクロス集計も可能である.例えば,プロセス起動時から終了時までの間にあるファイルへ行われた全てのファイルアクセスについて,コールスタックごとにその割合を分析するといったことが可能だ.

f:id:NyaRuRu:20120930015009p:plain
Chrome を起動してあるページをブラウズし,終了するまでの間に行われた約 2000 回のファイル読み取りアクセスを,コールスタックを用いてブレイクダウンしている図

Process Monitor を用いた解析テクニックについては,例えば次の記事などが参考になる.

次回,API モニタ編に続く.

SSD なら動作を変えるアプリケーションを作る

Windows 7 以降の OS では,SSD 上のボリュームに対してデフラグがスケジューリングされません.これと同様に,ドライブ特性に応じた動作戦略の変更をアプリケーションでも行いたいこともあるでしょう.そのような場合にアプリケーションに組み込めるように,簡単なサンプルコードを書いてみます.
元ネタは,
Windows 7 Disk Defragmenter User Interface Overview - The Storage Team at Microsoft - File Cabinet Blog - Site Home - TechNet Blogs
で解説されているアルゴリズムです.

以下,物理ドライブ "\\.\PhysicalDrive0" について,"no seek penalty" かどうかと,"nominal media rotation rate" かどうかの取得を行います.なお,前者はディスクポートドライバの動作に依存するため,Windows 7 以降でなければ使用できませんが,管理者権限なしで動作するというメリットがあります.後者は ATA8-ACS に対応したデバイスであれば,Windows 7 より前の OS でも動作しますが,動作には管理者権限が必要です.
上記記事に解説されているように,これらの関数を組み合わせて使ってもよいですし,どちらか一方のみを使いある程度の false negative は諦めるという手もあります.

#include <Windows.h>
#include <WinIoCtl.h>
#include <Ntddscsi.h>
#include <Setupapi.h>

#include <iostream>
#include <string>

using std::wstring;
using std::wcout;
using std::endl;

// Returns S_OK if |physical_drive_path| has no seek penalty.
// Returns S_FALSE otherwise.
// Returns E_FAIL if fails to retrieve the status.
// |physical_drive_path| should be something like
// "\\\\.\\PhysicalDrive0".
HRESULT HasNoSeekPenalty(const wstring& physical_drive_path) {
    // We do not need write permission.
  const HANDLE handle = ::CreateFileW(
      physical_drive_path.c_str(), FILE_READ_ATTRIBUTES, 
      FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
      OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  if (handle == INVALID_HANDLE_VALUE) {
    return E_FAIL;
  }

  STORAGE_PROPERTY_QUERY query_seek_penalty = {
    StorageDeviceSeekPenaltyProperty,  // PropertyId
    PropertyStandardQuery,             // QueryType,
  };
  DEVICE_SEEK_PENALTY_DESCRIPTOR query_seek_penalty_desc = {};
  DWORD returned_query_seek_penalty_size = 0;
  const BOOL query_seek_penalty_result = DeviceIoControl(
    handle, IOCTL_STORAGE_QUERY_PROPERTY,
    &query_seek_penalty, sizeof(query_seek_penalty),
    &query_seek_penalty_desc,
    sizeof(query_seek_penalty_desc),
    &returned_query_seek_penalty_size, NULL);
  CloseHandle(handle);
  if (!query_seek_penalty_result) {
    // failed to retrieve data.
    return E_FAIL;
  }

  return !query_seek_penalty_desc.IncursSeekPenalty ?
      S_OK : S_FALSE;
}

// Returns S_OK if |physical_drive_path| has nominal media
// rotation rate in terms of ATA8-ACS specification.
// http://www.t13.org/Documents/UploadedDocuments/docs2007/D1699r4-ATA8-ACS.pdf#Page=179
// Returns S_FALSE otherwise.
// Returns E_FAIL if fails to retrieve the status.
// |physical_drive_path| should be something like
// "\\\\.\\PhysicalDrive0".
HRESULT HasNominalMediaRotationRate(
    const wstring& physical_drive_path) {
  // In order to use IOCTL_ATA_PASS_THROUGH,
  // We *do* need read/write permission, which means
  // that the caller has admin privilege.
  const HANDLE handle = CreateFileW(
    physical_drive_path.c_str(),
    GENERIC_READ | GENERIC_WRITE, 
    FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
    OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  if (handle == INVALID_HANDLE_VALUE) {
    return E_FAIL;
  }

  struct ATAIdentifyDeviceQuery {
    ATA_PASS_THROUGH_EX header;
    WORD data[256];
  };

  ATAIdentifyDeviceQuery id_query = {};
  id_query.header.Length = sizeof(id_query.header);
  id_query.header.AtaFlags = ATA_FLAGS_DATA_IN;
  id_query.header.DataTransferLength =
      sizeof(id_query.data);
  id_query.header.TimeOutValue = 3;  // sec
  id_query.header.DataBufferOffset =
      sizeof(id_query.header);
  id_query.header.CurrentTaskFile[6] =
      0xec;  // ATA IDENTIFY DEVICE command

  DWORD retval_size = 0;
  const BOOL result = DeviceIoControl( 
    handle, IOCTL_ATA_PASS_THROUGH,
    &id_query, id_query.header.DataTransferLength,
    &id_query, id_query.header.DataTransferLength,
    &retval_size, NULL);
  if (!result) {
    return E_FAIL;
  }
  const int kNominalMediaRotRateWordIndex = 217;
  // RPM == 1 means this is non-rotate device
  return id_query.data[kNominalMediaRotRateWordIndex] == 1 ?
      S_OK : S_FALSE;
}

int main() {
  const wstring kDrive = L"\\\\.\\PhysicalDrive0";

  switch (HasNoSeekPenalty(kDrive)) {
    case S_OK:
      wcout << kDrive << L" has no seek penalty." << endl;
      break;
    case S_FALSE:
      wcout << kDrive << L" has seek penalty." << endl;
      break;
    default:
      wcout << L"failed to retrieve the status." << endl;
      break;
  }
 
  switch (HasNominalMediaRotationRate(kDrive)) {
    case S_OK:
      wcout << kDrive << L" has no seek penalty." << endl;
      break;
    case S_FALSE:
      wcout << kDrive << L" has seek penalty." << endl;
      break;
    default:
      wcout << L"failed to retrieve the status." << endl;
      break;
  } 

  return 0;
}

さて,"C:\Windows\System32\calc.exe" のようなパス名から,そのファイルが存在する物理ドライブ名を取得するにはどうすればよいでしょうか?
これには,

  1. GetVolumePathName API を利用してパスからマウントポイントを取得する
  2. GetVolumeNameForVolumeMountPoint API を利用して,論理ボリューム名を取得する
  3. IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS IO Control を利用して,物理ドライブの番号を取得する

という手順を踏みます.後は,得られた番号を "\\.\PhysicalDrive" の末尾に付ければ,物理ボリューム名が判明します.
なお,ダイナミックボリュームでは,複数の物理ドライブにまたがった論理ボリュームが作成可能なことに注意してください.以下の関数は,指定されたパスに対応する物理ドライブ番号を返します.

vector<int> GetExtentsFromPath(const wstring& path) {
  vector<int> extents;

  wchar_t mount_point[1024];
  if (!GetVolumePathNameW(
        path.c_str(), mount_point, ARRAYSIZE(mount_point))) {
    return extents;
  }

  wchar_t volume_name[1024];
  if (!GetVolumeNameForVolumeMountPointW(
          mount_point, volume_name, ARRAYSIZE(volume_name))) {
    return extents;
  }

  wstring volume = volume_name;

  // remove trailing '\\'
  volume.resize(volume.size() - 1);

  // We do not need write permission (nor admin rights).
  const HANDLE volume_handle = CreateFileW(
      volume.c_str(), FILE_READ_ATTRIBUTES, 
      FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
      FILE_ATTRIBUTE_NORMAL, NULL);

  VOLUME_DISK_EXTENTS initial_buffer = {};
  DWORD returned_size = 0;
  const BOOL get_volume_disk_result = DeviceIoControl(
      volume_handle, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,
      NULL, 0, &initial_buffer, sizeof(initial_buffer),
      &returned_size, NULL);
  const DWORD query_size_error = GetLastError();
  if (get_volume_disk_result != FALSE &&
      initial_buffer.NumberOfDiskExtents == 1) {
    extents.push_back(initial_buffer.Extents[0].DiskNumber);
    return extents;
  }
  if (query_size_error != ERROR_MORE_DATA) {
    return extents;
  }

  const size_t buffer_size =
      sizeof(initial_buffer.NumberOfDiskExtents) + 
      sizeof(initial_buffer.Extents) *
      initial_buffer.NumberOfDiskExtents;
  char* underlaying_buffer = new char[buffer_size];
  VOLUME_DISK_EXTENTS* query_buffer =
      reinterpret_cast<VOLUME_DISK_EXTENTS *>(
          &underlaying_buffer[0]);
  const BOOL devide_ioc_result = DeviceIoControl(
      volume_handle, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,
      NULL, 0, 
      query_buffer, buffer_size, &returned_size, NULL);
  const DWORD device_detail_result_error = ::GetLastError();
  if (!!devide_ioc_result) {
    for (DWORD i = 0;
         i < query_buffer->NumberOfDiskExtents; ++i) {
      extents.push_back(query_buffer->Extents[i].DiskNumber);
    }
  }
  delete[] underlaying_buffer;
  CloseHandle(volume_handle);
  return extents;
}

幸いなことに,上記処理は管理者権限なしでも動作します.Windows 7 以降では,管理者に昇格する必要なしに,SSD 上で動作しているらしいかどうかをアプリケーションが判別できるということになります.

Metro スタイルアプリケーションと IME

Metro では,AppContainer という特殊な環境でアプリケーションが実行されます.IME DLL は *1,対象の Metro スタイルアプリケーションプロセスに読み込まれ,AppContainer の管理下で動作することが求められます.

実際,Microsoft は Windows 8 Release Preview の公開に合わせ,Guidelines and checklist for IME development (Metro style apps) というガイドラインの提供を開始しました*2.同ドキュメントには,AppContainer 内で IME の機能を実装する上で,次のようなケースに注意せよとあります.

  • 辞書ファイルの置き場所
  • インターネットを利用したアップデート
  • 学習機能
  • プロセス間での(設定や学習)情報の共有

同ドキュメントには,IME の学習機能や,プロセス間での(設定や学習)情報の共有機能の実装方法として,その AppContainer で可能であればウェブサービスを経由してこれらの機能を実装するよう書かれています.Mac や Android のように IME プロセスを分離してくれていれば,IME 単体に別のアクセスコントロールを適用することも可能だったのでしょうが……

なお,Metro アプリケーションとデスクトップアプリケーションでは,利用可能なテクノロジに違いがあります.詳細については API reference for Metro style apps から辿ることができます.Metro 対応 IME では IMM32 ではなく TSF を使う必要があることはガイドラインで明記されていましたが,その他の API についてははっきりとは書かれていません.公開されると言われている Metro 対応 IME サンプルでも読まないとはっきりしない予感がします.
なお,こうして作られた IME DLL は,従来のデスクトップアプリケーションに読み込まれても動作する必要があります.まさに Run Anywhere,Universal Binary というわけです.

*1:Metro スタイルアプリケーションは,TSF のみのサポートとなりますから,IME というより Text Input Processor; TIP DLL と呼ぶ方が適切かもしれませんが

*2:一方で,予定されていた Metro アプリ用 IME サンプルに関する情報は[http://social.msdn.microsoft.com/Forums/en-US/winappswithnativecode/thread/6abc8b91-110c-4d60-b5b7-e113144902d9:title=削除されました].