最初の Android 4.0 端末である Galaxy Nexus は、 今までの Android 端末と異なり USB Storage として使うことができない。 PC とデータをやりとりしたいときは MTP (Media Transfer Protocol) を使えということらしい。 従来の Android 端末は、 PC と USB ケーブルで接続することによって、 Android 端末のストレージ (microSD カードなど) を USB Storage として PC からアクセスすることができた。
ただし、 PC と Android 双方から同じストレージを同時に読み書きすると破綻するので、 PC からアクセスするときは、 Android 端末からそのストレージを一時的に切り離す (umount する) 実装になっている。 このため、 ストレージに Android が必要とするデータがあると、 切り離したときに問題が起きる。 例えば着信音 (着メロ) を microSD カード上に置いていると、 切り離しているときに電話がかかってきても、 その着信音を鳴せない (Nexus S などだと、 設定した着信音がアクセスできない場合は、 デフォルトの着信音に切り替わる)。
MTP はブロックデバイス単位でなくファイル単位でストレージを管理する。 したがって PC との接続時でも Android 端末からストレージ全体がアクセスできなくなるという問題がない。 しかも MTP は Windows Vista 以降なら標準でサポートしている。 と、 このように (Samsung と Google は) 考えて、 Galaxy Nexus では USB Storage の代わりに MTP を使うようにしたのだろう (Samsung の一つ前の世代の Android 端末である Galaxy S でも MTP が使える)。 なお、 今後登場する Android 4.0 (Ice Cream Sandwich, ICS) 端末で USB Storage 機能が廃止されるかどうかは不明。 少なくとも Nexus S を ICS にシステムアップデートしても、 従来通り USB Storage を使うことができた。
一見妥当のように見える MTP の採用だが、 実際に使ってみると問題が多い。 ストレートに言えば 「全く使い物にならない」。 Windows XP だと Windows Media Player 10 以降のバージョンをインストールしないと MTP が使えないし、 Windows 以外だと、 標準で MTP をサポートしている OS はほとんどない。 しかも Windows でも、 MTP うんぬん以前に Samsung 端末用の USB ドライバを入手するのが大変。 以前は SAMSUNG_USB_Driver_for_Mobile_Phones.exe が単体で配布されていたようだが、 今は Kies をインストールしないと入手できない (-_-メ)。
さんざん苦労して MTP が使えるようにしても、 データ転送速度が極めて遅い (まだ実装がこなれてないため?)。 さらに、 普通のブロックデバイス (あるいはファイルシステム) として OS から見えないため、 MTP 対応を謳っていない限り、 普通のバックアップアップツールでは扱えないことが多い。 数MB 程度のファイルを一つ二つコピーする程度であればさほど問題無いが、 バックアップなど大量のデータを転送したい場合は不向き。
私の場合、 Galaxy Nexus のストレージ (/mnt/sdcard パーティション, 実際には /data/media を FUSE で /mnt/sdcard に mount している) を毎日 PC (Linux マシン) へバックアップしたい。 全部で数GB の大きさがあるので、 全データを毎回コピーするのは非現実的。 そこで rsync backup for Android アプリを使ってみた。 このアプリは、 内部に rsync と ssh コマンドを持っていて、 rsync を以下のような形で実行している:
/data/data/eu.kowalczuk.rsync4android/files/rsync -vHrltD \ --chmod=Du+rwx,go-rwx,Fu+rw,go-rw --no-perms --delete-after \ -e '/data/data/eu.kowalczuk.rsync4android/files/ssh -y -p 22 \ -i /data/data/eu.kowalczuk.rsync4android/files/dss_key' \ /mnt/sdcard/ sengoku@senri.gcd.org:~/android/sdcard/
Galaxy Nexus には有線LAN が無いので WiFi 経由で通信することになるが、 PC 同士で rsync する場合と全く同じで、とても簡単。 Android も Linux なのだから、 MTP みたいな得体の知れないものを無理に使うより、 使い慣れた rsync の方が断然 (・∀・)イイ!!
とはいえ、 使ってると WiFi の通信の遅さが気になってきた。 「USBテザリング」 を使えば USB ケーブル経由で TCP/IP 通信を行なうこともできるが、 rsync でデータ転送を行なうためだけにテザリングするのは牛刀な感じが否めない。 また、 テザリングの場合 Android の default route が 3G 回線へ向いてしまうので、 特定のアドレス (プライベート IP アドレス) を USB へ向ける route 設定が必要。
そもそも Android 端末と PC とは adb (Android Debug Bridge) でデータをやりとりしているのだから、 rsync も adb 上で行ないたいと思うのが人情というもの。
前フリが長くなったが、ここからが本題。
PC 上で adb コマンドを使うと、 任意のコマンドを Android 上で実行できる。 例えば、 Android 上で tar コマンドを実行することによって Android 上のディレクトリを PC へコピーできる:
senri:~ $ adb shell 'tar cf - /mnt/sdcard/media 2>/dev/null' | tar xvf - tar: Record size = 8 blocks drwxrwxr-x root/sdcard_rw 0 2010-07-15 10:39 mnt/sdcard/media/ drwxrwxr-x root/sdcard_rw 0 2010-07-15 10:39 mnt/sdcard/media/audio/ drwxrwxr-x root/sdcard_rw 0 2012-01-08 00:22 mnt/sdcard/media/audio/notifications/ ...
「tar cf - /mnt/sdcard/media 2>/dev/null」 が Android 上で実行するコマンド。 STDERR (「2>」, 標準エラー出力) を /dev/null へリダイレクトしているのは、 tar が STDERR へ出力したエラーメッセージが、 転送データ (STDOUT へ出力される) と混じるのを防ぐため。
このように Android 上で実行したコマンドの出力は、 PC 側へ伝わるが、 その逆、 PC 上で実行したコマンドの出力は Android へは伝わらない。 例えば次のように実行しても、 文字列 「test」 は Android 側に伝わらず、 cat コマンドは何も出力しない:
senri:~ $ echo test | adb shell cat
これは、 PC 側で実行した adb コマンドにおいて、 adb の STDIN (標準入力) から入力されたデータ (上記の例だと、 文字列 「test」) を、 Android 側へ送る実装になっていないため。 以下のパッチをあてることにより adb の STDIN を、 そのまま Android 上で実行したコマンドの STDIN へ伝えるようにすることができる:
--- system/core/adb/commandline.c.org 2012-01-29 09:28:25.000000000 +0900 +++ system/core/adb/commandline.c 2012-01-29 08:48:54.987004744 +0900 @@ -270,6 +270,35 @@ free(buf); } +static void *raw_read_thread(void *x) +{ + int fd, fdi; + unsigned char buf[1024]; + int r; + + int *fds = (int*) x; + fd = fds[0]; + fdi = fds[1]; + free(fds); + + for(;;) { + /* fdi is really the client's stdin, so use read, not adb_read here */ + D("raw_read_thread(): pre unix_read(fdi=%d,...)\n", fdi); + r = unix_read(fdi, buf, 1024); + D("raw_read_thread(): post unix_read(fdi=%d,...)\n", fdi); + if(r == 0) break; + if(r < 0) { + if(errno == EINTR) continue; + break; + } + r = adb_write(fd, buf, r); + if(r <= 0) { + break; + } + } + return 0; +} + static void *stdin_read_thread(void *x) { int fd, fdi; @@ -1022,6 +1051,12 @@ D("interactive shell loop. buff=%s\n", buf); fd = adb_connect(buf); if(fd >= 0) { + adb_thread_t thr; + int *fds; + fds = malloc(sizeof(int) * 2); + fds[0] = fd; + fds[1] = 0; + adb_thread_create(&thr, raw_read_thread, fds); D("about to read_and_dump(fd=%d)\n", fd); read_and_dump(fd); D("read_and_dump() done.\n");
raw_read_thread は、 adb コマンドの STDIN から入力されたデータを、 そのまま Android へ送るスレッド。 system/core/adb/commandline.c で定義されていた stdin_read_thread を若干書き換えただけ。
で、上記パッチをあてた adb を試してみると...:
senri:~ $ echo test | adb shell cat test test
ありゃ? なんで 「test」 が二度出力されるんだ?
Android 側では adbd (デーモン) が動いていて、 PC 側で 「adb shell コマンド列」 を実行すると、 それを受けて adbd は、 Android 上で 「/system/bin/sh -c コマンド列」 を実行する。 一般にシェル (この場合は /system/bin/sh) は、 対話 (interactive) モードだと STDIN から入力されたデータを、 そのまま STDOUT へエコーバック (echo back) する。 「test」 が二度出力されたのは、 エコーバックによる出力と、 cat コマンドの出力なのだろう。
でも、 なんで対話モードになってしまっているのだろう? フツー、 「-c」 オプション付でシェルを実行するときは、 非対話モードにするものだと思うのだけど... と思って adbd のソースを読むと、 「-c」 オプション付で /system/bin/sh を実行するときまで、 擬似端末 (Pseudo Terminal, pty) を使っていた。 これってバグじゃないのか? と思いつつ、 「-c」 オプション付の時は疑似端末を使わずに /system/bin/sh を実行するパッチを書いてみた:
--- system/core/adb/services.c.org 2012-01-29 09:28:25.000000000 +0900 +++ system/core/adb/services.c 2012-01-31 14:38:28.735416305 +0900 @@ -275,46 +275,54 @@ return -1; #else /* !HAVE_WIN32_PROC */ char *devname; - int ptm; + int fds[2]; - ptm = unix_open("/dev/ptmx", O_RDWR); // | O_NOCTTY); - if(ptm < 0){ - printf("[ cannot open /dev/ptmx - %s ]\n",strerror(errno)); - return -1; - } - fcntl(ptm, F_SETFD, FD_CLOEXEC); - - if(grantpt(ptm) || unlockpt(ptm) || - ((devname = (char*) ptsname(ptm)) == 0)){ - printf("[ trouble with /dev/ptmx - %s ]\n", strerror(errno)); - adb_close(ptm); - return -1; + if (arg1) { + if (adb_socketpair(fds)) { + printf("[ cannot open socketpair - %s ]\n",strerror(errno)); + return -1; + } + } else { + fds[0] = unix_open("/dev/ptmx", O_RDWR); // | O_NOCTTY); + if(fds[0] < 0){ + printf("[ cannot open /dev/ptmx - %s ]\n",strerror(errno)); + return -1; + } + fcntl(fds[0], F_SETFD, FD_CLOEXEC); + + if(grantpt(fds[0]) || unlockpt(fds[0]) || + ((devname = (char*) ptsname(fds[0])) == 0)){ + printf("[ trouble with /dev/ptmx - %s ]\n", strerror(errno)); + adb_close(fds[0]); + return -1; + } } *pid = fork(); if(*pid < 0) { printf("- fork failed: %s -\n", strerror(errno)); - adb_close(ptm); + adb_close(fds[0]); + if (arg1) adb_close(fds[1]); return -1; } if(*pid == 0){ - int pts; - setsid(); - pts = unix_open(devname, O_RDWR); - if(pts < 0) { - fprintf(stderr, "child failed to open pseudo-term slave: %s\n", devname); - exit(-1); - } - - dup2(pts, 0); - dup2(pts, 1); - dup2(pts, 2); + if (!arg1) { + fds[1] = unix_open(devname, O_RDWR); + if(fds[1] < 0) { + fprintf(stderr, "child failed to open pseudo-term slave: %s\n", devname); + exit(-1); + } + } + + dup2(fds[1], 0); + dup2(fds[1], 1); + dup2(fds[1], 2); - adb_close(pts); - adb_close(ptm); + adb_close(fds[1]); + adb_close(fds[0]); // set OOM adjustment to zero char text[64]; @@ -335,7 +343,8 @@ // Let the child do it itself, as sometimes the parent starts // running before the child has a /proc/pid/oom_adj. // """adb: unable to open /proc/644/oom_adj""" seen in some logs. - return ptm; + if (arg1) adb_close(fds[1]); + return fds[0]; } #endif /* !HAVE_WIN32_PROC */ }
「-c」 オプション付の時は 「arg1」 が非0 になるので、 「if (arg1) { ... }」 において、 adb_socketpair(fds) を呼び出して socketpair を用意する。 「arg1」 が 0 の時は、 unix_open("/dev/ptmx", O_RDWR) によって疑似端末が用意されるので、 「-c」 オプション無しの時はエコーバックが行なわれる。
adbd は /sbin/adbd にあって、 Android の場合 /sbin は initramfs のものがそのまま使われるので、 /sbin/adbd を書き換えても再起動すると元に戻ってしまう。 initramfs を書き換える (つまり boot.img を書き換える) とシステムアップデートの時に面倒なので、 以下のスクリプトを起動時に走らせることにした:
if [ -x /data/cust/sbin/adbd -a ! -f /sbin/adbd.org ]; then mount -oremount,rw null / mv /sbin/adbd /sbin/adbd.org cp -a /data/cust/sbin/adbd /sbin/ mount -oremount,ro null / stop adbd start adbd fi
起動時にスクリプトを実行するには、 /system/etc/install-recovery.sh に書き加えておけばよい。 Galaxy Nexus の場合、 もともとこのファイルは存在しないが、 従来の Android と同様、 このファイルが存在すれば起動時に実行される。
以上で、 adb 経由で rsync が使えるようになる。 例えば以下のように実行すると、 Android 上の /mnt/sdcard を、 PC 上の ~/android/sdcard へ丸ごとバックアップできる:
senri:~ $ rsync --delete -av -e adb shell:/mnt/sdcard ~/android/ receiving incremental file list sent 511 bytes received 210692 bytes 46934.00 bytes/sec total size is 5859784477 speedup is 27744.80
本来 rsync は ssh を呼び出して remote host との通信路を確保するが、 「-e」 オプションで ssh の代わりに使うコマンドを指定できる。 上記の例だと、 rsync は adb を以下のような引数をつけて呼び出す:
adb shell rsync --server --sender -vlogDtpre.iLsf . /mnt/sdcard