2012年12月3日月曜日

ホストオンリー接続でインターネットに接続する

僕の自宅のネット環境はWiMAXにWindowsのノートPCなのですが、Linux上で動くウェブアプリを開発することが多いので、普段はVMware Player内でUbuntuを動かして、WindowsからSSHやSambaでUbuntuに接続して作業をしています。

このときに問題となるのが、VMware Player内のゲストOSをどのようにして他のマシンとネットワークで接続するかです。接続のしかたには「ブリッジ接続」「NAT接続」「ホストオンリー接続」の3種類がありますが、それぞれ一長一短があります。ゲストOSをネットワークに直接接続してホストOSと同列に扱うブリッジ接続が、ネットワーク構成がシンプルでよいように思われますが、WiMAXのようなモバイルルータを使っていると、バッテリーの寿命がネットワークの寿命になってしまうので好ましくありません。そこで、今回はホストOSとゲストOSを独立したネットワークで接続するホストオンリー接続を試してみることにしました。

環境

  • ホストOS: Windows XP Professional SP3
  • ゲストOS: Ubuntu 12.10
  • 仮想化環境: VMware Player 5.0.0
  • ネット環境: WiMAX (モバイルルーターに有線LANで接続)

問題

ホストオンリー接続の最大の問題は、そのままではゲストOSはインターネットに接続できないということです。典型的なのは、以下のような構成でしょう。

実線は同じセグメントのネットワークを、破線は別のセグメントに属するが通信可能であることを示しています。このままでは、ゲストOSはインターネットに接続できません。

解決法

Windows XPには「インターネット接続の共有」 (ICS) という機能があります。この機能を利用すれば、別のネットワーク同士を結びつけることができます。Windowsにルーターと似たような機能を持たせられるわけです。しかし、この機能は相当くせ者です。設定は単純なのに1時間ほど格闘させられました(;´Д`)

まず、「コントロールパネル」の「管理ツール」から

  • Application Layer Gateway Service
  • Network Location Awareness (NLA)
  • Windows Firewall / Internet Connection Sharing (ICS)

の3つのサービスが常に起動する設定になっていることを確認します(参考: Windows上のVMware Playerでホストオンリー(Host-Only)接続を用いDHCPを使わずゲストOSから外部ネットに繋ぐ)。

続いて、VMnet1とゲストOSのネットワークアダプタに、同じネットワークのIPアドレスを割り当てます。VMnet1というアダプタは、ホストオンリー接続でゲストOSと接続する際に使われる仮想のアダプタです。ここでは、VMnet1に192.168.10.1を、ゲストOSのイーサネットアダプタeth0に192.168.10.2を割り当てることとします。

また、一度ゲストOSをシャットダウンし、仮想マシンの設定で「ホスト・オンリー」を選択します。

そしていよいよ、ローカルエリア接続のプロパティから「詳細設定」タブを開き、「ネットワークのほかのユーザーに、このコンピュータのインターネット接続をとおしての接続を許可する」というところにチェックを入れます。もし複数のアダプタがあるならVMnet1を選択します。

これで設定は終了となるはずです。しかし、何も意識せずに行うと恐らく

インターネット接続の共有を有効にすると、このコンピュータのLANアダプタが使用するIPアドレスは192.168.0.1に設定されます。このコンピュータは、ネットワーク上のほかのコンピュータに接続できなくなる可能性があります。ネットワーク上のほかのコンピュータが静的IPアドレスを持っている場合は、IPアドレスを自動的に取得するように設定する必要があります。インターネット接続の共有を有効にしますか?

というメッセージが表示され、「はい」をクリックすると続いて

インターネット接続の共有を有効にするときに、エラーが発生しました。

インターネット接続の共有を有効にできません。 LAN接続は、IPアドレスの自動指定で必要なIPアドレスを使って既に構成されています。

というエラーが出て先へ進めません。

僕はここではまりました。

試行錯誤の末、結局次のようなネットワークを作ろうとすればよいのだという結論に至りました。どうやら、ICSを有効にすると、IPアドレスが192.168.0.1の専用のネットワークアダプタがこっそりと追加されるようです。そのため、WiMAXルータのIPアドレスの変更を余儀なくされました。

また、ICSを有効にする際には、 一度ネットワークアダプタを無効にする必要がある らしいというのも分かりました。無効にしないと上記のエラーが発生します。そんなの知らねえよヽ(`Д´#)ノ

さらに、 ICSを有効にするとVMnet1のIPアドレスが192.168.0.1に勝手に変えられてしまう ので、元の値に戻さなければならいようです・・・。戻したら何か問題が起こるのではないかと不安ですが、どうやらそれはないようです。

細かな手順を書いてもかえって複雑になるだけなので、それはしません。みなさん、どうぞがんばってください><

2012年11月6日火曜日

Refinery CMSでデバッグしようとするとエラーになる

Refinery CMS 2.0.8で新しくプロジェクトを作ったあと、debuggerを使ってデバッグをしようとすると、エラーになってしまうことがあります。この投稿では、その問題の解決方法を紹介します。

環境

  • Ubutnu 12.04
  • ruby 1.9.3p0 (2011-10-30 revision 33570) [i686-linux]
  • refinerycms 2.0.8
  • bundler 1.2.1
  • debugger 1.2.1

問題

$ refinerycms sample

を実行して新しいプロジェクトを作成し、

$ cd sample

Gemfileを次のように編集し、

# Deploy with Capistrano
# gem 'capistrano'

# To use debugger
gem 'debugger'  # <=ここをコメントアウト

# Refinery CMS
gem 'refinerycms', '~> 2.0.0'

いつものように

$ bundle install

すると、次のようなエラーが出てしまいます。

$ rails s
/home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/debugger-1.2.1/lib/ruby-debug/interface.rb:55:in `block (2 levels) in initialize': uninitialized constant Debugger::LocalInterface::Readline (NameError)
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/debugger-1.2.1/lib/ruby-debug/interface.rb:53:in `each'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/debugger-1.2.1/lib/ruby-debug/interface.rb:53:in `block in initialize'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/debugger-1.2.1/lib/ruby-debug/interface.rb:52:in `open'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/debugger-1.2.1/lib/ruby-debug/interface.rb:52:in `initialize'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/debugger-1.2.1/lib/ruby-debug/processor.rb:61:in `new'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/debugger-1.2.1/lib/ruby-debug/processor.rb:61:in `initialize'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/debugger-1.2.1/lib/ruby-debug.rb:9:in `new'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/debugger-1.2.1/lib/ruby-debug.rb:9:in `<module:Debugger>'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/debugger-1.2.1/lib/ruby-debug.rb:8:in `<top (required)>'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/debugger-1.2.1/lib/debugger.rb:4:in `require'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/debugger-1.2.1/lib/debugger.rb:4:in `<top (required)>'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/bundler-1.2.1/lib/bundler/runtime.rb:68:in `require'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/bundler-1.2.1/lib/bundler/runtime.rb:68:in `block (2 levels) in require'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/bundler-1.2.1/lib/bundler/runtime.rb:66:in `each'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/bundler-1.2.1/lib/bundler/runtime.rb:66:in `block in require'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/bundler-1.2.1/lib/bundler/runtime.rb:55:in `each'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/bundler-1.2.1/lib/bundler/runtime.rb:55:in `require'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/bundler-1.2.1/lib/bundler.rb:128:in `require'
        from /srv/samba/share/sample/config/application.rb:13:in `<top (required)>'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/railties-3.2.8/lib/rails/commands.rb:53:in `require'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/railties-3.2.8/lib/rails/commands.rb:53:in `block in <top (required)>'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/railties-3.2.8/lib/rails/commands.rb:50:in `tap'
        from /home/jharai/.rvm/gems/ruby-1.9.3-p0/gems/railties-3.2.8/lib/rails/commands.rb:50:in `<top (required)>'
        from script/rails:6:in `require'
        from script/rails:6:in `<main>'

このように、サーバが起動せずデバッグも行えません。

原因

この問題は、作成されたRailsアプリケーションがBundlerの管理下にあるため、readlineをロードできていないのが原因です。

詳細は、以前私が似たような問題に遭遇した際に投稿したissue "Could not find a JavaScript runtime." error during fresh installation に書かれています。

解決方法

次のようにGemfileに一行加えて、Bundlerにreadlineを使うことを伝えます。

# Deploy with Capistrano
# gem 'capistrano'

# To use debugger
gem 'debugger'
gem 'rb-readline' # <=加える

# Refinery CMS
gem 'refinerycms', '~> 2.0.0'

これで、

$ bundle install

すると、サーバが起動してデバッグも行えるようになります。

$ rails s
=> Booting WEBrick
=> Rails 3.2.8 application starting in development on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server
[1, 10] in /srv/samba/share/sample/config/environment.rb
   1  # Load the rails application
   2  require File.expand_path('../application', __FILE__)
   3
   4  debugger
   5  # Initialize the rails application
=> 6  Sample::Application.initialize!
/srv/samba/share/sample/config/environment.rb:6
Sample::Application.initialize!
(rdb:1) 

ちなみに、Refinery CMS 1で開発していたときは、この問題は起こりませんでした。原因はよく分かりません・・・。

関連

2012年9月30日日曜日

Mongoidで、空のフィールドを省略する

最近、RailsでMongoDBを使ったウェブアプリを作っています。MongoDBってお手軽で便利ですね。複数の階層を持った普通のJSON形式のオブジェクトをひとまとまりのデータとして管理できるので、正規化された複数のテーブルを外部キーで参照し合うようなRDBMS特有のややこしさがありません。それでいて、MongoDBはSQLのような高度なクエリも実行できます。

NoSQLはスケールアウトと結びつけて語られることが多いですが、 MongoDBは仮にスケールアウトできなかったとしても十分に価値あるDBMSだと感じています。

さて、RubyからMongoDBを使いたい場合、もちろんドライバだけで利用する方法もあります。しかし、RDBMSにO/Rマッパーがあるように、MongoDBにはODM(Object-Document Mapper)というものが存在します。RubyにおけるODMではMongoidMongoMapperの二つが有名です。が、どうやら最近はMongoidの方が人気があるようです。

僕もMongoidで開発をしています。

Mongoidで空のフィールドを省略する

ここからが本題。今回、Rubyは1.9.3を、Mongoidは2.2.5を使っています(Mongoidはちょっと古いです)。

今、ブログの記事を管理するソフトウェアを作っているとしましょう。Articleクラスでは、ブログの記事を個別に扱っています。

class Article
  include Mongoid::Document
  include Mongoid::Timestamps

  field :title, type: String
  field :text, type: String
  
  attr_accessible :title, :text

  validates_presence_of :title
end

Mongoidでは、fieldメソッドを用いてオブジェクトとドキュメントのフィールド間のマッピングを設定します。 このように書くと、Articleクラスにtitletextというアクセサが作られ、それぞれがMongoDBのArticleドキュメントのtitle, textフィールドに対応するようになります。見た目がずいぶんActiveRecordに似ていますが、それもそのはず、Mongoidは裏側ではRailsのActiveModelを多用しており、コールバックやバリデーションなどの機能はそれによって実現されているのです。

さて、例に戻りましょう。ここで各ブログ記事に写真を添えたいということになりました。一つの記事に複数の写真を載せられるようにしたいので、photosというフィールドを作り、記事に載せる写真のIDの配列を格納できるようにします(ここでは、写真の記事内での配置については考えないことにします)。

class Article
  include Mongoid::Document
  include Mongoid::Timestamps

  field :title, type: String
  field :text, type: String
  field :photos, type: Array
  
  attr_accessible :title, :text, :photos

  validates_presence_of :title
end

ここでちょっとした問題が発生します。すでにデータベースに格納されているドキュメントには、次のようにphotosフィールドはありません。

{ "_id" : ObjectId("4f24cd8f1d41c80cf4000002"), "title" : "記事1", "text" : "記事本文1", "updated_at" : ISODate("2012-01-29T04:39:43Z"), "created_at" : ISODate("2012-01-29T04:39:43Z") }

一方、これから作成するドキュメントには、写真がない記事にもphotos: []というフィールドが加わることになります。つまり、「写真がない」という同じ状態は、2通りの表現の仕方でデータベース側に格納されることになります。

これには、大きな実害はありません。Mongoidでは次のようにして、クラスのフィールドに対応するフィールドがドキュメントに存在しない場合のデフォルト値を設定することができます。

field :photos, type: Array, default: []

こうすることで、ドキュメントにphotosが存在しない場合であってもphotosが空の配列の場合であっても、article.photoは空の配列を返してくれるようになります(ちなみに、defaultが指定されていない場合、対応するフィールドがドキュメントに存在しないとarticle.photonilを返します)。これで、アプリケーション側は一件落着です。

しかし、依然としてデータベース内には2つの状態が残ります。この問題はどう考えればよいでしょうか?

a. 無視する

実害がないのであれば、わざわざ何かを変える必要はありません。ということで、無視します。

これは十分理にかなった考え方だと思いますが、プログラムの規模が大きく複雑になり、多くの人が開発に携わるようになると、誤解のもとになるかもしれません。

b. 全てのドキュメントにphotosフィールドを追加する

過去に作成されたものも含めて全てのドキュメントにphotosフィールドを追加します。これは、最も美しく、意思疎通の面でも誤解の余地がないベストな考え方だと思います。

ただ、すでに膨大な件数の記事を管理している場合、余計なフィールドが増えるために、データサイズが少し増えるかもしれません。

c. 空のフィールドは省略する

これが今回のメインです。

photosフィールドが空配列である状態を認めません。つまり、photosフィールドが存在する場合は必ず配列には一つ以上の要素が含まれているようにし、配列が空になる場合はphotosフィールドそのものを削除するようにします。

これは、各ドキュメントごとにフィールドを自由に追加、削除できるMongoDBならではの解決法といえるでしょう。

ただ、調べた限りではMongoidでこれを直接実現する方法はないようです。

そこで、Mongoidのソースコードを読んで次のような実装法を思いつきました。

class Article
  include Mongoid::Document
  include Mongoid::Timestamps

  field :title, type: String
  field :text, type: String
  field :photos, type: Array, default: []
  
  attr_accessible :title, :text, :photos

  validates_presence_of :title

  around_save :omit_empty_field

  def omit_empty_field
    if photos.empty?
      atomic_unsets.push(:photos) # photosを$unset
      reset_photos! # photosが再度$setされるのを防ぐ
      yield
      # 次のアクセスのために、値を元に戻す。
      # ActiveModel::Dirtyの値がおかしくならないよう、ここで行う。
      self.photos = []
    else
      yield
    end
  end

  protected :omit_empty_field
end

save時のコールバックでphotosが空かどうか調べ、空の場合には$unset(削除)を行うタスクを追加します。基本はそれだけなのですが、それだけでは不都合が生じるので、いくつか補助的な処理を行います。

フィールドが一つだけの場合は、これでよいでしょう。汎用性を持たせたい場合は、もっと別の書き方が必要になると思います。

2012年8月14日火曜日

東京アメッシュの再生を高速化する

東京近辺に住んでいる人は、雨の様子を確認するのに東京アメッシュを使うことが多いと思う。

このサイトのいいところは、東京近辺の雨の様子を高解像度で表示できることと、過去2時間の様子を5分単位でさかのぼって確認できること。特に二つ目の機能は、今後の雨の降りを予想する上で欠かすことのできない、とっても便利なものだと思う。僕も、アメッシュを開けば必ず「再生」ボタンを押す。

ただ、不満な点もある。デフォルトでは再生の速度がとてもゆっくりで、過去2時間分を再生しようとすると30秒近くかかってしまう。

そこで、再生を高速化するGreasemonkeyスクリプト「amesh_quickplay」を作った。このスクリプトが有効な状態で再生すると、過去2時間分が7, 8秒程度で再生されるようになる。

FirefoxとChromeで動作確認済み。

amesh_quickplayのダウンロードはこちら

// ==UserScript==
// @name           amesh_quickplay
// @namespace      amesh.quickplay
// @description    amesh_quickplay
// @include        http://tokyo-ame.jwa.or.jp/
// ==/UserScript==
var executeBrowserContext = function(funcOrString) {
    var code = "javascript:(" + encodeURIComponent(funcOrString.toString()) + ")();";
    location.href = code;
}
executeBrowserContext(function() {
    animation.interval = 0;
});

2012年7月23日月曜日

ブログ始める

何となく、ブログを始めてみたいなあと思った。
まあ、以前はてなで書いてたんだけど。