PXEブートによるPCの有効利用

PXEブートの環境を見直し、私なりの工夫を加えた結果、以前より保守しやすい環境を構築できたが、その内容はDHCP、tftp、syslinux、gPXEと多岐に渡るので今後、それぞれのテーマに分けてメモって行くことにする。ここでは何故そのような見直しが必要となった背景だけ書いておく(ここには技術的な内容に関する記述はない)。

私は、古くなったが捨てられずにいるPCも含めて6台ほどのPCを持っている。メールやMS Officeを中心に使うWindowのPCs(Windowsしかドライバの無い地デジカードの利用も含む)、外出用のノートPC、家族用のPC(仕事用のPCとは共有したくないので)、サーバ用のPC(RAID構成の大容量ファイルサーバ、兼仮想マシンサーバ、兼××、兼○○、…)、そして古くなった半分ジャンクになりかけてりるPCが2台、という感じになる。(その他にも常用している仮想マシンが数台ある。仕事用(物理)PCに何かあった場合にほぼ同じ環境で利用できるバックアップ用の仮想マシン、怪しいソフトのダウンロートや試験をするための仮想マシン、銀行などの口座にアクセスするためのセキュリティを高めた専用の仮想マシンなどである。)それぞれのPCにはHDDがありWindowsなり、LinuxなりのOSがインストールされている。大体はそんな感じで使い分けている訳で(PCの稼働率を無視すれば)普段は特段の問題もない。一時的に必要なPCが出てきた場合は仮想マシンを仕立てれば済んでしまう。

とは言っても、一時的に他のOSで立ち上げたり、他の用途で使ったりする必要が出てくる。例えば、WindowsのPCのバックアップを取る場合である。勿論、Windowsにも気の利いたバックアップツールは用意されているが、Linuxで起動してWindowsの入ったHDDを(もしくはシステムパーティションを)丸ごとイメージバックアップする方法以上に完璧で、かつ容易な方法ないと思う。また、ハードウェアに依存した環境で使う場合。例えば、普段はWindowsを動かしているが、そのPCにあるハードウェアをLinuxでも利用したい場合。そして、特定の環境を状況によって色々なPCで利用したい場合。例えば、最近はAndroidの開発環境(Ubuntuベース)を使っているが、机の前にいる時にはWindowsマシンの中でvmplayerを使って仮想マシン環境で動かしている。vmplayerであればホストのUSBのポートへもアクセスできるので実機(REGZA Phone)の接続も可能となる。とは言っても仮想マシンではなく実マシンで動かしたい時は、Windowsをシャットダウンして、AndroidをインストールしているUbuntuを直接ブートする。また、Windowsで他の重い作業をしている時は、家族のPC(普段は勿論Windowsを動かしている)を拝借して、そちらでUbuntuをブートして使う。また、外出中は(vmplayerではなく)VMware Serverの中の仮想マシンとして起動して、リモートコンソールから使う、といった感じになる。こう書くとAndroidSDKをインストールしたUbuntuを各マシン毎に何種類も用意しているように思うかも知れないが、全て“同じ”Ubuntuをブートして使っている(そのため、同時に複数のマシンでブートすることは出来ないが)。そして(実マシン、仮想マシンの区別なく)どのマシンでブートしても全く同一の環境で作業できる。

こういった事を可能にしてくれるのがPXEブートであり、更にiSCSIと組み合わせると複数PCによる同一環境の使い回しもできるようになる。上の個々のケースについては既にこのブログでもメモして来ているので新規にどうこうと言うことではない。ただ、今まではそれぞれの用途に対してパッチワーク的に構築してきたので、OSを起動するマシンを変更する場合はDHCPサーバの設定を変更したり、syslinuxの設定を変更したりで、“あれ、このマシンであのOSをブートさせる時はどこを直せばよかったんだっけ?”と自分でも迷ってしまうこともあった(そうは初中、変更するわけでもないので忘れてしまう)。そこで、これらの環境をまとめ、PXEブートする時にメニュー選択できるように環境を整えた。今、私の環境でPXEブートすると、次のような画面になる:

“Memory Test”はOSをブートするのではなくMemory Testプログラムを起動実行する。syslinuxに付属するmt86plus(旧memtest86+)を起動しているだけ。
“gPXE”を選択すると、gPXEを使って再ブートに入りiSCSIでのブートが可能となる。ここで、どのiSCSIディスクを使ってブートするのかメニュー化したかったのだが、現バージョン(1.0.1)のgPXEに対してパラメータを渡すことができないようなので、ここだけはマシン依存になってしまう(詳細はgPXEの設定の所で)。今のところはAndroidの開発環境をブートするのに使っている。
VMware Remote Desktop”はシンクライアントとしてPCを立ち上げて、サーバ上で動いている仮想マシンのリモートコンソールとして使う。捨てられずに置いてある昔のPCのための設定である。
Ubuntu 10.04 Desktop i386”、“Ubuntu 10.04 Desktop amd64”は(iSCSIを使わず)ディスクレス形態でUbuntuをブートする。ちょうどUbuntuのライブCDを起動したのと同じ状態をPXEでネットブートする形になる。この設定があると普段はWindowsを動かしているPCでも気軽にUbuntuを起動することができ、マシンの保守などのために利用する。Windowsマシンのバックアップ取る場合はLinuxをブートしてHDDを丸ごと(もしくはシステム・パーティションを)イメージとしてバックアップするのに使う。また、HDDからブートできない!とかいう場合にも使う。
“Install Ubuntu i386”、“Install Ubuntu amd64”はUbuntuのインストールに使う。この様に設定しておけば、いちいちインストール用のDVDを焼かずにBIOSからPXEブートを選ぶだけでUbuntuのインストールが可能となる(ただGUI画面ではくCUI画面でのインストールになるが)。

単にこういったメニューを作りました!だけではここでメモって置く必要もない。こういったメニューの保守をできるだけ容易にするように色々と工夫してみた。それらをテーマ毎に分割してメモっていくことにする。
↓ 各テーマごとのメモ
1:syslinuxの設定ファイルに関する工夫

ddの使い方メモ

空(ゼロの詰まった)の巨大ファイルを作る

/dev/zeroを入力として使う。空のディスクイメージ・ファイル(ファイルの中身がHDDのデータになっているもの)の作成等に使う。
例:64GiBのファイルを作成する

adsaria@ubuntu$ dd if=/dev/zero bs=1G count=64 of=fff

bsの大きさは使っている計算機のメモリサイズで決める。余り大きいとスワップが発生して、異常に時間がかかる(またbsの上限もあるので注意、詳しくはここ)。小さ過ぎると入出力回数が増えて、やはり時間がかかる。

空(本当に空)の巨大ファイルを作る

見かけの容量だけを持ったファイルを作る。ディスクイメージ・ファイルを作る時などに便利。ただし、実際にファイルの中身が書かれる時にHDDを割り当てするので書き込みに時間がかかったり、思わぬエラーが発生する可能性もあるので注意。
例:64GiBのファイルを作成する。

adsaria@ubuntu$ dd if=/dev/null bs=1 seek=64G of=fff
0+0 records in
0+0 records out
0 bytes (0 B) copied, 4.866e-06 s, 0.0 kB/s
adsaria@ubuntu$ ls -ls fff
0 -rw-r--r-- 1 adsaria adsaria 68719476736 2011-01-25 13:16 fff

実際にデータは書き込まないので瞬時に終わる。“ls -ls”で見ると実質的にはサイズがゼロになっていることが確認できる。

ファイルの大きさを目的の大きさまで“見かけ上”大きくする

これもディスクイメージ・ファイルなどの大きさを変更したい時に便利。MBR(つまりパーティション情報)だけを取ってきて、後はこの方法でサイズを大きくする、とか。
例:41バイトのファイルを1MiBに変更する。

adsaria@ubuntu$ ls -ls fff
4 -rw-r--r-- 1 adsaria adsaria 41 2011-01-25 13:12 fff
adsaria@ubuntu$ hd fff
00000000  31 32 33 34 35 36 37 38  39 30 31 32 33 34 35 36  |1234567890123456|
00000010  37 38 39 30 31 32 33 34  35 36 37 38 39 30 31 32  |7890123456789012|
00000020  33 34 35 36 37 38 39 30  0a                       |34567890.|
00000029
adsaria@ubuntu$ dd if=/dev/null of=fff bs=1 seek=1M
0+0 records in
0+0 records out
0 bytes (0 B) copied, 5.358e-06 s, 0.0 kB/s
adsaria@ubuntu$ ls -ls fff
4 -rw-r--r-- 1 adsaria adsaria 1048576 2011-01-25 13:12 fff
adsaria@ubuntu$ hd fff
00000000  31 32 33 34 35 36 37 38  39 30 31 32 33 34 35 36  |1234567890123456|
00000010  37 38 39 30 31 32 33 34  35 36 37 38 39 30 31 32  |7890123456789012|
00000020  33 34 35 36 37 38 39 30  0a 00 00 00 00 00 00 00  |34567890........|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00100000
adsaria@ubuntu$ 

見かけの大きさだけを変更するので、“ls -ls”で見ると実際のファイルの大きさは変わっていない事を確認できる。

ファイルの特定の場所からデータを上書きする

seekとconv=notruncを使う。ディスクイメージ・ファイルの中の特定のパーティションのデータだけを変更したい場合などに便利。
例:元のファイルの11バイト目(10バイト目の次)から4バイトを別のデータで置き換える。

adsaria@ubuntu$ ls -l fff
-rw-r--r-- 1 adsaria adsaria 41 2011-01-25 12:38 fff
adsaria@ubuntu$ cat fff
1234567890123456789012345678901234567890
adsaria@ubuntu$ echo -n abcd | dd of=fff bs=1 seek=10 conv=notrunc
4+0 records in
4+0 records out
4 bytes (4 B) copied, 2.1531e-05 s, 186 kB/s
adsaria@ubuntu$ ls -l fff
-rw-r--r-- 1 adsaria adsaria 41 2011-01-25 12:39 fff
adsaria@ubuntu$ cat fff
1234567890abcd56789012345678901234567890
adsaria@ubuntu$

読み出しエラーのあるHDDから無理やり読み出す

conv=noerror,sync を使う。noerrorはエラーが発生しても処理を続行するためのもの。syncは入力エラーがあった場合にその部分にNULLを埋め込み出力するためのもの。これでエラーを起こすHDDからデータを救出できる“かも”。

例:

root@ubuntu# dd if=/dev/sdb of=hdd.img bs=1M conv=noerror,sync

ただし、bs(ブロックサイズ)に大きな値を指定すると、例え1セクタ(256バイト)の読み出しエラーだけでもbsで指定したサイズのデータがNULLになってしまうので注意。上の例では1MiBがNULLになってしまう。(エラーを起こすHDDが手元にないので未確認)しかし、bsのサイズを余り小さくすると読み出しに気が遠くなるほど時間が必要になるので注意。
サルベージしたファイルはスーパーブロック、MFT等が壊れていなければmountして(mountの方法はここ)、fsckchkdskで修復して使える場合もある。

Android SDKのSampleSyncAdapterの使い方

Androidのアプリケーションの真髄はクラウドと連携する機能だと思う。認証機能であったり、データの同期機能であったり。そういった意味ではSDKのサンプル、SampleSyncAdapterは初心者としては読破しておかなければならないプログラムだろう。ところが、“Account Managerについて”にも書いたが、SampleSyncAdapterは初心者には解り難い。認証や同期、サービスやプロバイダが絡み合っているアプリケーションである、と言う事もある。しかし何よりデバイスにインストールしても何のアクティビティも起動してくれない、というのが初心者には分かり辛いところだろう。私はこのアプリケーションをどうやって利用するのか中々解らなかった。しかし、“よく分からないから飛ばしちゃえ”という性格でもないのでネチネチと調べてみた。で、ようやく動かすことができた。

実際に、このサンプルをどの様に使うかはこの後にメモっておくが、まず、SampleSyncAdapterの構造についてチョットだけ触れておく。

SampleSyncAdapterって?

まず、SampleSyncAdapterはAndroidManifest.xmlを見ると2つのサービス(AuthenticationServiceとSyncService)と1つのアクティビティ、AuthenticatorActivityから構成されているが分かる。普通のアプリケーションであれば、起動するとメイン・アクティビティが画面に表示されるが、そのためにはactivityに次のインテント・フィルタが記述されていなければならない。

<intent-filter>
	<action android:name="android.intent.action.MAIN" />
	<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

しかし、このインテント・フィルタが記述されていないので、アプリケーションをインストールしてもAuthenticatorActivityは起動されないし、ランチャーにも表示されない。本来、インテント・フィルタが記述されている部分には、

<!--
    No intent-filter here! This activity is only ever launched by
    someone who explicitly knows the class name
-->

というコメントがある。つまり、このアクティビティは別のもの(someone)から起動されるということ。“別のもの”ってなに? この説明が書いていないので分からなかった。ちなみに、無理やりaction.MAINとcategory.LAUNCHERのインテント・フィルタを追加すると、アプリケーションの実行と同時にAuthenticatorActivity起動されるし、ランチャーにもアイコンが表示される。AuthenticatorActivityが起動すると画面にユーザ名とパスワードを入力するダイアログが表示される。

さて、調べていくと“別のもの”とはAccountManagerのようだ。AccountManagerはAndroid OSの基本的な機能の一つで、最初からOSに組み込まれている。では、AccountManagerはどうやって(何が原因で)AuthenticatorActivityを呼び出すのか? サンプルの中にAuthenticatorActivityとは別にAuthenticator.javaというクラスがあり、このクラスがAuthenticatorActivityを使ってユーザからGUI画面を使ってユーザ名とパスワードを入力するようにAccountManagerに依頼している。Authenticatorの幾つかのメソッドではAuthenticatorActivityを呼び出すためのインテントを作成して、それをBundleに梱包してAccountManagerに返している。なお、AuthenticatorはAccountManagerに対するプラグイン拡張モジュールである(“Account Managerについて”参照)。

つまり、まず認証を必要としているアプリケーションがある。それがAccountManagerを呼び出す。AccountManagerはアカウントのタイプに応じたAuthenticatorを呼び出す。Authenticatorでは実際の認証を行うが、認証の際にユーザ名が空だったりパスワードが指定されていなかったり、またはトークンの期限が切れているなど条件で再度ユーザからユーザ名とパスワードの入力が必要と判断した場合、“認証が成功した”という情報の代わりにAuthenticatorActivityを呼び出すインテントを作成してAccountManagerに返す。そこでAccountManagerはAuthenticatorActivityを呼び出す。と言うのが大まかな流れとなる。

それでは“認証を必要としているアプリケーション”とは? SampleSyncAdapterサインプルではSync Serviceとなっている。SyncAdapter.javaのonPerformSyncメソッドでAccountManagerのblockingGetAuthTokenメソッドを呼び出してる。つまり、クラウド上のデータと同期をとる時に(当たり前だか)認証している。では、SyncAdapterは誰に呼ばれるのか? SyncService.javaでサービスの本体としてSyncAdapterのインスタンスを作成し、他のアプリケーションからのバインドに対するリスナーとしてonBindを定義している。つまり、このonBindを通してSyncAdapterをサービスの機能として利用できるようになる(“AndroidのRemote Serviceについて”参照)。

では、最後に、誰がSync Serviceを呼び出すのか? これはイベントの発生よってAndroid OS自身が呼び出す(ACTION_AUTHENTICATOR_INTENTというインテントを使ってSyncServiceを起動する)。イベントとはユーザがGUIを使って明示的にデータの同期を要求したり、一定時間が経って自動的に同期をしたり、等などだろう。

以上で、データの同期に関わる認証のシステム構成が大体分かったのだか、サンプルSampleSyncAdapterを理解する上で、もう一つ知っておきたいことがある。何のデータを同期するのか? と、どこにあるデータを同期するのか? だ。マニフェストにもあるとおり、このサンプルで唯一GUIを持っているのがAuthenticatorActivityなのだが、これは単にユーザにユーザ名とパスワードを入力してもらうだけのものだ。では、同期したデータはどうやって観るのか?

このサンプルはAndroidに標準でついている“Contacts(連絡先)”アプリケーションと連携して使うようになっている。つまり、このサンプルのメインのGUIは“連絡先”だった。これが分からなくて1週間近くもマゴマゴしていた。そして、このアプリケーションが扱う(そしてクラウドと同期する)データは連絡先のデータだった。

このサンプルでは、同期する先は“http://samplesyncadapter.appspot.com/”というサイトを利用するようになっている。このサイトはサンプルの試験用に誰でもデータを登録することができる。(一方で、誰でも他人のデータを書き換えたり消去したりもできる。)このサイトはGoogle App Engineで実装されているようだ。サンプルのディレクトリに“samplesyncadapter_server”というサブディレクトリがあるが、ここにはApp Engineで使うPythonのプログラムが入っている。従って、App EngineのSDKをダウンロードして、このPythonのサンプルプログラム実行すれば、ローカル(http://localhost:8080)でもサーバのデモができる。

SampleSyncAdapterの使い方

まず、このサンプルをインストールしていない状態とした状態を比較するために、サンプルをインストールしない状態でMENUの[Settings(設定)]から設定メニューを出し、[Accounts & Sync(アカウントと同期)]を選択する。そして画面下にある[Add account(アカウントを追加)]ボタンを押す。するとアカウントのタイプ(種類)として“Corporate(コーポレート)”と“Google”の2つが表示されている。

一旦、ホームに戻して、サンプルプログラムをインストールする(Eclipseを使っているのであれば[run]ボタンをクリックする)。この状態ではデバイスには何の変化もない。そこで程と同様に[MENU]-[Settings(設定)]-[Accounts & Sync(アカウントと同期)]-[Add account(アカウントを追加)]と進むと次の様にアカウントのタイプとして“SamplesyncAdapter”が増えていることが確認できる。

さて、ここからアカウントを指定するのだが、その前にクラウド上にアカウントと連絡先のデータを作っておく必要がある。インターネット・ブラウザから“http://samplesyncadapter.appspot.com/users”をアクセスする。すると、現在この試験サイトの登録されている“連絡先”の一覧が出てくる。ページの一番下までスクロールする。

[Insert More]というリンクをクリックして自分専用の試験アカウント(連絡先データ)を作成する。世界中の人がこの試験サイトを使っているので、できるだけカブらないようなHandle名にする。

ここでは順次、次の3つのデータを登録してゆく:

  • myssatest1 Tanaka Minoru
  • myssatest2 Suzuki Sigeru
  • myssatest3 Satoh Kazuko

左側の“myssatest[1-3]”はHandle名でこれがSamplesyncAdapterではユーザ名となる。名前やStatus、電話番号は適当に入力する。データを入力したら[Submit]ボタンを押す。すると一覧のページ戻り、一番下までスクロールすると今入力したデータが登録されていることがわかる。(ただし、登録するとHandle名とは別に内部的なID(ユニークな番号)が自動的に割り当てられるので必ずしも“最後”にリストされているとは限らない。見当たらない場合はページ内検索をしてみる。)

同様にあと2名分のデータを登録する。



(割り振られる内部的なIDにより、必ずしも入力した順番に並ぶとは限らない。)
さて、ここでは“私”は“Tanaka Minoru”だとする。そこで“myssatest1”をユーザ名として使って認証できるようにパスワードを設定する。ブラウザに直接“http://samplesyncadapter.appspot.com/add_credentials”を指定すると、パスワード設定画面になる。任意のパスワードを入力して[Submit]をクリックする。

次に“私(Tanaka Minoru)”の友達として“Suzuki Sigeru"と“Satoh Kazuko”を登録する。“Minoru_Tanaka”の右側の[Friends]をクリックする。

[Add More]のリンクをクリックして友達を登録する。


“お友達”として登録したいHandle名を順次追加してゆく。

以上で、クラウド上のデータの準備は完了である。

バイスの設定に戻る。[MENU]-[Settings(設定)]-[Accounts & Sync(アカウントと同期)]-[Add account(アカウントを追加)]と進みリストされている[SamplesyncAdapter]をタッチする。するとAuthenticatorActivityのユーザ名&パスワード入力画面が出てくる。ここではユーザ名として“myssatest1”、パスワードとして“hirakegoma”を入力して[Sign in]をタッチする。

管理対象アカウントとして“myssatest1”が追加され、同期が“ON”になっていることを確認する。

ホーム画面に戻り、ランチャーからContacts(連絡)を起動する。

この例では、このデバイスにはまだ1件も連絡先を登録していないので、説明文が現れている(既に連絡先が登録されていても以下の操作は同じ)。[MENU]ボタンを押してメニューを表示して“Display options(表示オプション)”をタッチする。

すると“myssatest1”のデータを連絡先として表示するか指定するチェックボタン(“All Contacts”)があるのチェックをしてから[Done]にタッチする。すると、連絡先の画面に戻り、クラウド上で登録したデータが表示されていることが確認できる。



上の例ではLocale(言語)を日本語にしていないので電話番号の表示がちょっと変だが、Localeを“ja_JP”に設定すれば、ちゃんと表示される。

これでこのサンプルの挙動が分かるのでコードを追うのも楽になる。

なお、このサンプルプログラムはあくまでもサンプルであり実用レベルではない。そのため、デバイス側でデータを変更して、それでクラウド上のデータを更新する、ということは出来ない。クラウドからの一方通行になっている。従って同期の確認をするのであれば、ブラウザを使ってクラウド上のデータを変更して、それがデバイス側で更新されていることを確認することになる。

Account Managerについて

Android SDKのサンプルSampleSyncAdapterを読んでみたのだが、結構初心者には難解なコードだった。SampleSyncAdapter自身はAndroidが提供する2つの機能を使って実現している。1つはインターネット上のサービスなどにログインするアカウントの管理機能を提供するAccount Managerと、もう一つはデータの同期を実現するSynchronization Manager。サンプルプログラムとしてはこれらの2つの機能を組み合わせた方が現実的なサンプルになるのだが、両方の機能の詳細を理解していない初心者には難解になってしまう。

補足:2010/10/17
このサンプルがとっつき難い理由として、どうやって動かすか解らない、という面もあった。 SampleSyncAdapterの動かし方については“Android SDKのSampleSyncAdapterの使い方”に詳しく書いておいた。

そこで、2つの機能を切り分けて別々に独自のサンプルを作ってみることにした。先ずは、Account Managerから。

Account ManagerはAndroid端末内でユーザの持つオンライン・アカウントを一元的に管理する機能。ユーザは一度IDとパスワードを入力すれば、以降はイチイチ、ID・パスワードを入力することなく、ワンクリックでアクセスできるようになる。

勿論、サービスが異なれば認証の仕方も異なるので、AccountManagerにはauthenticatorモジュールをプラグインできるようになっている。サービスごとのauthenticatorを追加することでGoogleだけでなくFacebookMicrosoft Exchangeのサービスにも対応する。

また、一般に多くのサービスで“認証トークン”を扱えるが、Account Managerでもトークンを生成・管理できる。トークンはサービスへアクセスするたびに実際のパスワードを送信せず、(最初の認証の後は)一定期間有効なトークンを送ることで、認証できるようにする。ただし、トークンを無効にするタイミングはサービスに任せられる。

サービスにアクセスするアプリケーションは一般に次のような手順を踏む(Androidの開発ページより):

  1. AccountManager.get(Context)メソッドを使ってインスタンスを獲得する。
  2. getAccountsByType(String)などを使って利用可能なアカウントのリストを獲得する。特定のアプリケーションが必要とするのは通常は特定のタイプのアカウントに限定される。この“アカウントタイプ”とはauthenticatorを識別する。例えば、Googleのサービスであれば“com.google”というアカウントタイプとしてauthenticatorを指定する。アカウントタイプはauthenticatorに依存し、アプリケーションは利用するauthenticatorのアカウントタイプを知っていなければならない。(アカウントタイプの他にアカウントフィーチャもあるが、初歩的な利用ということでここでは割愛した。)
  3. getAccountsByType(String)などを使って取得したアカウントのリストは通常、ユーザに提示して、その一つを選択してもらう。もし利用したいアカウントがリストに無ければ、addAccountメソッドを使ってユーザに新たにアカウントを作るように促す。
  4. 重要:前回の選択したアカウントをアプリケーションが再利用する際には、そのアカウントがgetAccountsByTypeメソッドで返されるリストにまた存在していることを確認しなければならない。既にデバイスに存在しないアカウントを使うと未定義エラーとなる。
  5. getAuthTokenメソッドや関連ヘルパを使って、選択したアカウントの認証トークンを要求する。
  6. 認証トークンと使ってサービスを要求する。認証トークンの形式、サービス要求のフォーマット、プロトコルなどはアクセスしようとしているサービスに依存する。アプリケーションはネットワークやプロトコルのライブラリを利用することになるだろう。
  7. 重要:認証エラーでサービス要求が失敗した場合は、入手したトークンは期限切れであり、サーバではもう受け入れられない可能性がある。アプリケーションはinvalidateAuthTokenメソッドを呼び出してキャッシュされたトークンを取り除かなければならない。それを行わないとサービス要求は失敗し続けることになる。現在もっている認証トークンを無効化した後に、上の“認証トークンの要求”を再実行する。それでも認証が失敗する場合は、“本格的な”認証の失敗として、ユーザに取るべきアクションを訪ねることになる。

Account Managerを使ったサンプル

いきなりSDKのサンプル、SampleSyncAdapterのようなことをやってもわからないので、次のような非常に単純なサンプルを作ってみた。このサンプルでやりたいことは次のようなことである。

  1. googleアカウントの情報を使って、Googleカレンダへアクセスするためのトークンを取得する。
  2. 取得した認証トークンを使ってGoogleアカウントにアクセスする。
  3. (認証が通れば)ログインした状態でGoogleカレンダーのWebページをGETする。

これを実現するために次のような手順を踏んだ:

  1. 前準備:Googleアカウント(Gmailのアカウント)を取得する。そして、そのID(Gmailアドレス)とパスワードをAndroidバイスの[設定]−[アカウントと同期]からデバイスGoogleアカウントを登録する。以上を設定してからアンプルアプリケーションを起動する。
  2. サンプルアプリケーションでは、アカウントマネージャのインスタンスを獲得(AccountManager.get(this))する。
  3. Googleサービスに関連したアカウントのリストを取得する(getAccountsByType("com.google"))。本来でここで取得したリストを画面に表示して、どのアカウント(Gmailアドレス)を使うかユーザが選択するが、ここでは1つのアカウントしか登録していないので、決め打ちでリストの先頭のアカウントを使う(accounts[0])。
  4. アカウントの情報と“認証トークンタイプ”としてGoogleカレンダーを表す“cl”を使って、認証トークンを取得する(getAuthToken(accounts[0], "cl", false, new GetAuthTokenCallback(), null))。このメソッドは非同期のメソッドなので、トークンを取得した時に呼び出されるCallbackを指定している。Callbackを指定せずに、getAuthTokenの直後にgetResultメソッドを呼び出せばブロック型の利用ができるらしい。またblockingGetAuthTokenメソッドを使ってもブロック型でトークンを獲得できる。(ブロック型=トークンが獲得できるまでメソッドからは制御が返ってこない。)
  5. トークンが取得できると、Callback(GetAuthTokenCallbackインスタンスのrunメソッドが呼び出されるので、そこで得られたトークンを使ってGoogleアカウントを呼び出すURLを生成して、HttpGetする。

基本は押さえたと思ったのだが、上手く行かない。認証トークンを取得するところまでは上手く行くのだが、そのトークンを使ってGoogle Accountへアクセスすると、“The page you requested is invalid(指定されたページは無効です)”画面が返って来てしまう。トークンの入手方法に問題があるのか(=トークン自身が無効なのか)、トークンを使ったGoogle Accountへのアクセスに問題あるのか。色々とやってみたが2日程進展がないので、一旦、ここまでの成果をメモっておくこととした。(このサンプルを作るにあたって次のWebページを参考にした:http://blog.notdot.net/2010/05/Authenticating-against-App-Engine-from-an-Android-app

なお、getAuthTokenメソッドの引数、authTokenTypeについて一言書いておく。authTokenTypeはその名前の通り認証トークンの型を指定する。authTokenTypeはauthenticator依存、つまり認証の種類ごとにことなる。この例ではGoogleアカウントサービスのauthenticatorに対して、“cl”型を指定しているが、これはgoogleカレンダーとして認識されるようだ。色々と試してみると、次のような文字列がGoogleアカウントのauthenticatorでは次のような文字列を認識していると思われる。(Androidのauthenticatorでこれらの文字列を認識していることは分かるのだが、これらを使って実際にサービスにアクセスするところまでは至っていない。)

サンプルプログラム

メイン・アクティビティ:

package com.example.android.test_accountmanager_1;

import java.io.IOException;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;

public class TestAccountManager1 extends Activity {
	
	static final String TAG="+++ TestAccountManager1";
	AccountManager mAccountManager = null;

	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
	}

	@Override
	protected void onResume() {
		super.onResume();
        
		Account[] accounts = null;
		if (mAccountManager == null) mAccountManager = AccountManager.get(this);
		accounts = mAccountManager.getAccountsByType("com.google");
		for (Account ac : accounts) Log.d(TAG, ac.toString());
		AccountManagerFuture<Bundle> accountManagerFuture = 
			mAccountManager.getAuthToken(accounts[0], 
        			"cl", 
//					"android", 
//					"ah",		// Google AppEngine 
//					"cl", 		// Google Calendar
//					"mail", 	// Gmail
//					"reader", 	// Google Reader
//					"talk", 	// Gtalk
//					"youtube",	// YouTube 
					false, new GetAuthTokenCallback(), null);

/* 以下は同期型(ブロック型)のgetAuthTokenを使った例。
		try {
			token = mAccountManager.blockingGetAuthToken(accounts[0], "cl", false);
		} catch (OperationCanceledException e) {
			e.printStackTrace();
		} catch (AuthenticatorException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		Log.d(TAG, "Token = " + token);
*/
	}
    
	private class GetAuthTokenCallback implements AccountManagerCallback<Bundle> {

		static final String TAG="+++ GetAuthTokenCallback";
		@Override
		public void run(AccountManagerFuture<Bundle> arg0) {
			Bundle bundle;
			try {
				bundle = arg0.getResult();
				Intent intent = (Intent) bundle.get(AccountManager.KEY_INTENT);
				if (intent != null) {
					Log.d(TAG, "User Input required");
					startActivity(intent);
				} else {
					Log.d(TAG, "Token = " + bundle.getString(AccountManager.KEY_AUTHTOKEN));
					loginGoogle(bundle.getString(AccountManager.KEY_AUTHTOKEN));
				}
			} catch (OperationCanceledException e) {
				e.printStackTrace();
			} catch (AuthenticatorException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
    	
		private void loginGoogle(String token) {
			DefaultHttpClient http_client = new DefaultHttpClient();
			HttpGet http_get = new HttpGet(
					// TokenAuthの他にも Login、ServiceLgoin、ClientLoginがあるがどれもNG
					"https://www.google.com/accounts/TokenAuth?auth=" 
					+ token
					+ "&continue=http://www.google.com/calendar/"
					);
			HttpResponse response = null;
			try {
				response = http_client.execute(http_get);
			} catch (ClientProtocolException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
			if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
				try {
					String entity = EntityUtils.toString(response.getEntity());
					Log.d(TAG, entity);
					if (entity.contains("The page you requested is invalid")) {
						Log.d(TAG, "The page you requested is invalid");
						mAccountManager.invalidateAuthToken("com.google", token);
					}
				} catch (IllegalStateException e) {
					e.printStackTrace();
				} catch (IOException e) {
					e.printStackTrace();
				}
			} else
				Log.d(TAG, "Login failure");
		}
	}
}

マニフェスト(uses-permissionの追加が必要):

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
	package="com.example.android.test_accountmanager_1"
	android:versionCode="1" android:versionName="1.0">
	<uses-permission android:name="android.permission.GET_ACCOUNTS" />
	<uses-permission android:name="android.permission.USE_CREDENTIALS"/>
	<uses-permission android:name="android.permission.INTERNET"/>
	<uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
	<application android:icon="@drawable/icon" android:label="@string/app_name">
		<activity android:name=".TestAccountManager1" android:label="@string/app_name">
			<intent-filter>
				<action android:name="android.intent.action.MAIN" />
				<category android:name="android.intent.category.LAUNCHER" />
			</intent-filter>
		</activity>
	</application>
</manifest> 

【追記:2011/03/25】
tknringさんのご指摘で android.permission.MANAGE_ACCOUNTS を追加。

乳酸菌の効果的な摂取についての考察

初めに断っておくが、私は医療関係者でもないし、医学を学んだことがあるわけでもない。あくまで推論の範囲での話。

ちまたでは多くの種類の乳酸菌食品(ヨーグルトや乳酸菌飲料)があふれている。使われている乳酸菌も色々で、整腸を目的としたものから、ピロリ菌退治まで多種多様だ。私も最近乳酸菌飲料を飲み始めた。飲み始めると“なんとなく”体の調子がよくなったような気がするので、だた漫然と飲んでいたのだ。が、Wikipediaとかで乳酸菌を調べているうちに、漫然と飲んでいてはお金の無駄ではないかと思い始めた。

しかし“正しい飲み方”を探しても“空腹時に食べる(飲む)”と“寝起きに食べる(飲む)”という程度のことしかなかった。でも、胃の構造とかを調べれば、もう少し効果的な摂取方法があるのではないか?と思った。

基本的に乳酸菌は胃酸に弱い。胃酸は塩酸であり強酸性なので殆どの菌が胃で死滅する。そのため、健康が保たれているわけだが。乳酸菌も例外ではないようで、長時間、胃酸に浸かっていると死滅するそうだ。ただ、胃酸に強い乳酸菌も見つけられていて、“生きたまま腸まで届く”といううたい文句の乳酸菌もある。とはいうものの、胃酸に浸かっていれば相当数が死んで、確率的に他製品よりは多い生き残りが腸まで達する、ということだと思う。

つまり、乳酸菌を胃酸に曝さない、もしくは胃酸に曝す時間を短くするのが、効果的な乳酸菌の取り方となる。そこで、まず胃の構造を調べた。

“胃の構造”でググるといくつも出てくる。

色々なところから断片的な情報を集めてくると次のようになる。

  • 胃の内容積は空腹時には50ml程度に縮んでいる。(満腹時には1800ml程度まで膨らむ)
  • 胃に食物が入ることにより、それが刺激となって胃液が分泌される。
  • 消化中は、胃の出口は幽門括約筋で閉じていている。

ということが分かる。調べた限りのことから次のようなことが類推できる。

  • 空腹時は幽門が開いているのではないか。
  • 幽門が閉じるのは胃酸が放出される時、すなわち食べ物の刺激はある時。
  • 胃が食べ物の刺激を感知するのは、(1)固形物のように幽門を通らないものが胃に入った時、(2)胃が膨らんだ場合、のどちらか、もしくは両方と思われる。

胃の出口の幽門は胃カメラの検査で分かるが、幽門が開いている状態でも細い胃カメラも通り抜ける際に刺激があるほど狭い。だから、ある程度の大きさの固形物はここを通り抜けられないので、胃に溜まって刺激となる、と考えられる。それと大彎部に溜った食べ物が胃を押し広げて刺激となる、と類推できる。

以上から、乳酸菌(飲料)は次のような条件で摂取することが必要となるのではないか。

  • 空腹時に摂取する:空腹時と言っても色々な状態がある。空腹感が強いと食べ物を見ただけでも胃酸の分泌が始まるので良くない。食後3時間程度経って胃が空になり、かつ空腹感もない時間帯が狙い目となる。あと、摂取する乳酸菌の目的によっても注意が必要だろう。整腸目的で単に腸まで届けばいい、というのであれば寝起きに飲むというのも効果的だが、ピロリ菌退治が目的で、ある程度の時間、乳酸菌を(胃酸の無い)胃に残留させたいのであれば、寝起きのような食前は避けるべきだろう。私は夕食を食べた3時間後〜就寝前に夜食代わりとして飲むようにした。(ダイエット目的では夜食代わりというのは抵抗があるかも知れないが、少量の乳酸菌飲料のカロリーなどたかが知れている。大量に摂取すれば別だが。)
  • 乳酸菌飲料を摂取する:胃に刺激を与えないために乳酸菌食品ではなく、乳酸菌“飲料”を摂取する。ヨーグルトの場合は飲み込む前に十分柔らかくしてから飲み込む。
  • 室温で摂取する:冷やしたものは胃に刺激を与える要因ともなりかねないので“念のため”室温にしてから飲む。(胃は温度を感知できないので、大丈夫なのかも知れないが。ただ、脳が熱いと感じないのと、胃が刺激を感知しないのは別の話ではないか。)
  • 1回(1口)の摂取量は最大50mlとする:空腹時の胃の容積は50mlなので、それを超えて摂取すると胃が膨らんで胃に刺激を与えることになってしまう。ヤクルトが50mlというのは、こうしたことも考えてなのか、たまたまなのか。ヤクルト以外の乳酸菌飲料はそれよりもかなり多い。私は、100ml程度の乳酸菌飲料を時間をかけて舐めるようにチョロチョロと摂取している。(最近では500mlのパックで売られている乳酸菌飲料もある。が、これをこの方法でゆっくり摂取すると、軽いダンピング症状になる。乳酸菌飲料は基本的にブドウ糖などの糖分が高めなので、それが一気に十二指腸に達するためだろう。500mlもの量になれば結構なカロリーにもなる。これを繰り返すと逆に健康にも悪そうだ。50〜100ml程度がちょうどいいのかも。もしくは500mlを10日に分けて飲むとか。)

以上の方法が本当に効果があるかどうかは分からないが、以前より、少ない量でも調子が良いような気もする。本来であれば、乳酸菌飲料を開発している企業が調査して、科学的に証明された形でユーザに提示すべきではないかと思う。もっとも企業にとっては売れる量が少なくなっては迷惑な話かも知れないが。

Android SDKのサンプルを読む:BackupRestoreActivity

Android SDKに入っているサンプルを読む。今回はBackupRestoreActivity。
BackupRestoreActivityはAndroidのBackup Serviceを利用するサンプル。Backup ServiceはAPI Level 8から追加された機能でアプリケーションの持つデータを“クラウド”上に保管するサービス。この機能を使うことで、アプリケーションを一時的に削除(アンインストール)した後、再インストールしても以前のデータを利用できる。これは、端末を買い換えてアプリケーションを再インストールした場合にも利用できる。
http://developer.android.com/intl/ja/guide/topics/data/backup.html
開発ガイドのサンプルでは、ゲームの最高得点のデータを保存するサンプルを挙げている。
SDKのサンプル、BackupRestoreActivityには4つのJavaファイルがあるが、BackupRestoreActivity.javaがバックアップサービスを利用するアプリケーションの本体。他の3つ、ExampleAgent.java、FileHelperExampleAgent.java、MultiRecordExampleAgent.javaはバックアップサービスを利用する際に必要となる“Agent”で、同時には3つは必要ない。エージェントの実装方法のバリエーションを示してるだけで、どれか1つのエージェントを選択して利用する。(マニフェストの“android:backupAgent”属性で指定する。)
BackupRestoreActivityはAndroidのバックアップサービスを利用するが、このサンプルではデータを“クラウド”に保存することは出来ない。クラウドを使った保存にはアプリケーションをGoogleに登録してBackup Service Keyを取得・設定する必要がある。BackupRestoreActivityでは、その代わりにデバイス(AVDであれシミュレータ)が提供する場所にデータを保存している(/data/backup/com.android.internal.backup.LocalTransport というディレクトリにアクティビティのパッケージ名を使ったファイルが作られ、そこに保存される)。

バックアップサービスの概要

バックアップサービスの利用方法は簡単だ。バックアップサービスを利用したいアプリケーションに、BackupAgentクラスもしくはBackupAgentHelperクラスを継承したサブクラスを作成し、その抽象メソッドであるonBackupとonRestoreを実装する。onBackupにはそのアプリケーション固有のデータを集めてバックアップサービスへ渡す機能を実装する。onRestoreはその逆。アプリケーション本体からバックアップのサービスを呼び出すにはBackupManagerクラスのdataChangedメソッドを呼び出す。ただし、dataChangedメソッドを呼び出しても、直ぐにはバックアップが実行されない。バックアップの“要求”を出すだけで、実際のバックアップは“あるタイミングで”バックアップサービスを要求している他のアプリケーションとまとめて実行されるとある。が、どういうタイミングかは上記のページには見当たらなかった。そのタイミングで実際にバックアップが行われる時にエージェントのonBackupが呼び出される。

dataChangedで直ちにバックアップを開始しないのには合理的な理由がある。アプリケーションがバックアップすべきデータを頻繁に更新する場合(基本的にはデータが更新されるとdataChangedを呼び出すのだが)、その度にクラウドにアクセスしてデータを保存しているでは負担が大き過ぎる。そこで、データの更新がある度にdataChangedで複数の更新の要求を出すが、実際にバックアップが実行される時点での最新のデータだけをクラウドに保存するようになっている。

リストアに関しては、基本的にはアプリケーションが何かメソッドを呼び出すような必要はない。アプリケーションのインストール時にシステムが判断して、初回の実行時にonRestoreを呼び出してくれる。(アプリケーションが主体的にリストアを要求する場合は、バックアップマネージャのrequestRestoreメソッドを呼び出す。)

BackupRestoreActivityについて

サンプルとしてはSDKのサンプル(BackupRestoreActivity)よりも、開発ページのサンプルの方が分かりやすいと思う。なぜならBackupRestoreActivityのエージェントはちょっと凝りすぎているので。
エージェント(BackupAgentのサブクラス)のonBackupで実際に保存したいデータをファイルなどから読み込んできてバックアップサービスへ渡している。しかし、単純にファイルからデータを持ってくるだけでなく“以前のバックアップに比べて内容が新しくなっていた場合のみ”データを保存する。ここで“以前のバックアップに比べて内容が新しくなっていた場合”をどう判断するか、だ。Webの開発ページのサンプルは、バックアップしたいファイルの更新時間が、以前のバックアップより新しくなっていれば更新する、という方法だ。解りやすい。
しかし、SDKのBackupRestoreActivityはバックアップサービスで(クラウドに)保存するデータと同じデータをローカルに保存して、そのローカルに保存した値に比べてデータの値が変更している場合は(クラウドに)バックアップする、という方法。つまり、クラウドとローカルに同じデータを保存しようとしているので、最初読むとなんだかよく解らない。とくに“バックアップって何”というのを勉強しようとしている人が読むと解らなくなってしまう。私も最初読んで、このコードは一体何をやっているのだろう、と理解に苦しんだ。Webのサンプルを見てスッキリした感じだ。

onBackupの引数

onBackupは次のような引数になっている。
onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)
第1引数と第3引数が“ParcelFileDescriptor”となっている。これは何?
リファレンスを見るとParcel#readFileDescriptor()というメソッドで返されるファイル・ディスクプリタということ。では、Parcelは? ParcelはIBinderでプロセス間を受け渡しされるオブジェクト。IBinderはAndroidのプロセス間通信でデータの受け渡しに使われるインタフェース。
つまり、oldStateやnewStateは“ファイルディスクプリタ”なので任意のデータを読み書きできる。(基本的にoldStateは読みだし専用だが)newStateに書かれたデータはAndroidのIPCを通して別のプロセス(多分、バックアップサービスのプロセス)が読んでいるということ。onBackupではこのoldState、newStateを使ってバックアップしているデータの“状態”をデバイスで管理している、らしい。oldState、newStateには任意の情報を書き込むことが出来きるが、その内容の意味解釈はアプリケーションに任される。開発ページのサンプルではバックアップするデータのあるローカルファイルの“更新日時”を書き込んでいる。SDKのサンプルではバックアップするデータと同じデータを書き込んでいる。
真ん中の第2引数 dataを通して実際に保存するデータをバックアップサービスへ渡す。書き込むデータはGoogle得意のkey/valueペアからなるエンティティの形式となる。まず、writeEntityHeaderでキーを書き込み、writeEntityDataでバリュー(もしくはその一部)を書き込む。エンティティのバリュー部分は複数のwriteEntityDataで複数の塊に分けて書き込むことができる(ただし、その合計の大きさはwriteEntityHeaderで指定した大きさでなければならない)。

買ってみたい文房具

バイモ11フラット
フリクションボールノック
アリシス
テープカッター直線美
ペンカット
ジェットストリーム4&1
ハリナックス(ハンディタイプ)
ニーモシネ ノートパッド ホルダー付
カードメモ
スコッチ チタンコート シザーズ1468