ddコマンドのラッパー「ddr」をRubyで書いてみた
さくらインターネットの田中社長が、ddコマンドの知られざる(?)機能をブログでご紹介されていました。ddコマンドって、ディスクイメージを流しこむとか、パーティションの引っ越しをする、というような実行時間のかかる作業に使うことがありませんか。どのぐらいコピーが進んだのかよく分からなくて、ああ、UNIXは本当に寡黙なシステム(問題なく実行できている限り何も言わない)だなと思ったりします。
実は、manを見れば書いてあることだったらしいですが、ddのプロセスにUSR1シグナルを送れば途中経過が表示可能です。以下、田中社長のブログから引用。
[root@wwwxxxxu ~]# dd if=/dev/zero of=/dev/null & [1] 31092 [root@wwwxxxxu ~]# kill -USR1 31092 7764899+0 records in 7764898+0 records out 3975627776 bytes (4.0 GB) copied, 6.02001 seconds, 660 MB/s [root@wwwxxxxu ~]# kill -USR1 31092 25123042+0 records in 25123041+0 records out 12862996992 bytes (13 GB) copied, 17.8137 seconds, 722 MB/s [root@wwwxxxxu ~]# kill -USR1 31092 43986419+0 records in 43986418+0 records out 22521046016 bytes (23 GB) copied, 31.3739 seconds, 718 MB/s [root@wwwxxxxu ~]# kill 31092 [1]+ Terminated dd if=/dev/zero of=/dev/null [root@wwwxxxxu ~]#
という感じです。
で、この機能を元にプログレスバーを表示するスクリプトを使ってらっしゃるということです。10年来のPerlユーザーというお話を聞いてますので、きっとPerlスクリプトなのでしょう。私はこれをRubyで書いてみようと思いました。
Ruby用のプログレスバーといえば、高林哲さん(migemoやNamazuの作者、あるいは「バッドノウハウ」という言葉の生みの親として知られていて、今はグーグルにお勤めのはず)が書かれた、ProgressBarというgemがあり、これが使えそうです。使い方は、以下のような感じです。
[Mac:ken]/Users/ken% gem install progressbar Successfully installed progressbar-0.9.0 1 gem installed [Mac:ken]/Users/ken% irb ruby-1.9.2-p0 > require 'progressbar' => true ruby-1.9.2-p0 > p = ProgressBar.new("test", 100) => #<ProgressBar:0/100> ruby-1.9.2-p0 > 100.times{sleep(0.1); p.inc}; p.finish | ETA: --:--:-- test: 100% |oooooooooooooooooooooooooooooooooooooooooo| Time: 00:00:38 => 2010-12-12 18:08:05 0900
しかし、ddの対象となるデータのコピー進ちょくとなると、ちょっと面倒そうです。count=で渡されるブロック数から逆算するか、あらかじめ入力ファイルのサイズを知っておくなどしなくてはなりません。いずれにしても条件を分けないといけません。
まあ、プログレスバーは置いておいて、ともかく、進ちょくの統計情報を表示するだけでも便利そうということで、以下のように書いてみました。
#!/usr/bin/env ruby # # dd command wrapper # pid = Process.spawn('dd', *ARGV) Thread.new do loop do sleep 1 info = IO.popen("kill -SIGINFO #{pid}") puts info.readlines puts end end Process.waitpid(pid, 0)
実行結果は以下のような感じです。
[Mac:ken]/Users/ken/code/ddr% ./ddr if=/dev/zero of=/dev/null count=3000000 544077+0 records in 544076+0 records out 278566912 bytes transferred in 1.002903 secs (277760577 bytes/sec) 1089204+0 records in 1089203+0 records out 557671936 bytes transferred in 2.007118 secs (277847112 bytes/sec) 1633654+0 records in 1633653+0 records out 836430336 bytes transferred in 3.010989 secs (277792540 bytes/sec) 2179604+0 records in 2179603+0 records out 1115956736 bytes transferred in 4.014571 secs (277976572 bytes/sec) 2726554+0 records in 2726554+0 records out 1395995648 bytes transferred in 5.018349 secs (278178262 bytes/sec) 3000000+0 records in 3000000+0 records out 1536000000 bytes transferred in 5.523351 secs (278092040 bytes/sec) [Mac:ken]/Users/ken/code/ddr%
こういうのって、やってみるといろいろと勉強になります。やってること自体は単純ですが、Mac OS Xの実装ではddに送るべきシグナルは「USR1」ではなく「SIGINFO」でした。LinuxではUSR1ですね。FreeBSDはどうなんでしょう。
最初、Process.spawnではなく、Process.forkを使ったのですが、このあたりはプラットフォーム依存で、Ujihisaさんのこのブログエントリが参考になります。
waitpidがうまく子プロセス終了を待ってくれなくて、Rubyのkernelが終了してしまいます。何か変だと思って、結局waitpidについても調べたのですが、すると当然のように各種UNIXのCレベルでのAPIがたくさん出てきて、なるほど、Rubyではそれらをちょっと抽象化しているように見えて、ほぼそのまま見せていたのか、というのも寄り道的に勉強になりました。プロセスの終了を待つにも、単に直近に生成した子プロセスを待つものから、子プロセスをグループとして待つもの、直接pidを指定して待つものなどがあるのですね。
とか何とかもっともらしい話を書いていますが、「そうだ、Rubyを使ってddのラッパーコマンド、ddrを作ろう……」と思いついたときにやりたかったのは、以下のようなことだったりします。
#!/usr/bin/env ruby # -*- coding: utf-8 -*- # # dd command wrapper # ddr = %w(← ↑ ↓ →) pid = Process.spawn('dd', *ARGV) Thread.new do loop do sleep 0.5 puts ddr.map{|arr| arr = " " if rand > 0.5; arr}.join(" ") puts " Perfect!! " if rand > 0.9 end end Process.waitpid(pid, 0)
これの出力は、
[Mac:ken]/Users/ken/code/ddr% ./ddr if=/dev/zero of=/dev/null count=9000000 ↓ Perfect!! ← → ← → ↓ → Perfect!! ↑ ↓ → ← ↑ ← → ← → Perfect!! ← ↑ ↓ ← ↑ ↓ → ↑ ↓ ← ↑ ← ↓ → ↓ ← ↑ ↓ _
という感じです。次々と流れでてくるステップが、DDR(Dance Dance Revolution)っぽくないですか……。スミマセン、スミマセン。
で、コピーしたバイト数を「23MB Combo!」と表示しようとして気付いたのですが、上のスクリプトは意図したことと違う動きをしています。IO#popenで標準出力がちゃんとキャプチャできてなくて、単に画面に出てきているだけです。open3とかSTYとか、それらしきライブラリがあったりしてかすかに試したり、IOバッファのflushの問題? など、いろいろ調べてみたのですが、そんなことしてる場合じゃないと気付いて、そろそろ仕事に戻ります……。
あ、念のため。ddの途中経過を見るのは、田中社長のブログのコメントにありましたが「CTRL-T」を叩くのが正解だと思います。30分の転送処理がうっかりCTRL-Cで中断! みたいな恐怖感はちょっぴりなくもないですが、これで十分ですよね。
コメント
西村賢
ですよね(たぶん)、コメントありがとうございます!