@IT編集部の西村賢がRuby/Rails関連を中心に書いています。

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で中断! みたいな恐怖感はちょっぴりなくもないですが、これで十分ですよね。

Comment(2)

コメント

spawnとても便利です!

西村賢

ですよね(たぶん)、コメントありがとうございます!

コメントを投稿する