対策

どういう症状?

↓のダイアログが表示されて、動画が見れない。

Adobe Flash Playerの設定
ローカル記憶領域
res.nimg.jpはコンピュータに情報を保存する許可を要求しています。

リクエスト容量: 1MB
使用中の容量: 82KB

直し方

フラッシュプレイヤーを一旦アンインストールして、インストールしなおす時にローカル記憶領域を無制限にすればOKらしいです。
しかし、インストールしなおすの面倒じゃね?と思ったので、直接設定を書き換える方法でやってみました。コマンドラインの操作に慣れてる人ならこっちの方が簡単かも。

プレイヤーの設定が記録されてる、settings.solというファイルに設定を直に書き込めばOK。ファイルはバイナリなので、スクリプトで一旦テキストにして、終ったらバイナリに戻します。(settings.solをこのエントリに添付できれば話が速いのだけど、はてなPlusじゃないし、しかたないね。)

1. s2x.py をダウンロード

s2x.py - http://osflash.org/s2x

2. settings.solからsettings.xmlを作る
  # python s2x.py -x settings.sol

settings.solファイルは以下のパスにあります。

~/Library/Preferences/Macromedia/Flash Player/macromedia.com/support/flashplayer/sys/#res.nimg.jp

3. settings.xmlをアップデートする

↓みたいに、klimitの値を1000にします。

<?xml version="1.0" encoding="utf-8"?>
<solx sol_name="res.nimg.jp/settings" std_author="iceeLyne" std_version="0.75">
	<data name="allow" type="boolean" value="false"/>
	<data name="always" type="boolean" value="false"/>
	<data name="klimit" type="number" value="1000.0"/>
</solx>
4. settings.xmlをsettings.solに戻す
  # python s2x.py -s settings.xml

もうちょっと詳しい説明

↓のページで詳しく解説されてます。

"Flashの「ローカル記憶領域」をSOLで直接変更する方法 - ubuntu flash sol s2x.py"
http://www.watanet.org/~chihiro/index.cgi/linux/ubuntu/20100214_ubuntu-flash-plugin-local-storage-settings-issue.html

[Ruby] NSURLRequestとか

新たな問題

最初にアイテムを追加する時はアイコンをいわゆるスピナーにしておいて、非同期に画像を読みにいって、順次入れかえていくという挙動を実現してみました*1

最初にやったのは、Ruby側でThreadを立てておいて、アイテムを追加する度にQueueを経由でそのスレッドに画像読みこみタスクを渡すという方法です。

これで、カートを読みにいく動作がアイコンの読み込みでブロックされる事もなくなり、スピードアップするだろうと期待したのですが、なんだか返って遅くなったような気が…

測ってみると、ダウンロードがメインスレッドで実行してた時の10倍くらい遅くなってます。

家のネットワークでは、以下のページを最後まで読むのに1.7秒くらい、それから画像のURLを抜きだして画像そのものを読むのに0.4秒くらいかかります。
http://www.amazon.co.jp/gp/aw/d.html?a=4798023809
ところが、別スレッドで動かすとそれが18秒と4秒くらいになってしまったのです。

NSURLRequest

ネットでいろいろ調べると、そもそもRubyCocoaのアプリでTreadを使ってGUIをアップデートするのはお勧めではないらしい。

http://www.unfitforprint.com/articles/2008/02/14/working-with-threads-in-rubycocoa

を見ると"never call anything GUI-related from a separate thread,"とか書いてあるし…
バックグラウンドでダウンロードしたい時はNSURLConnectionがお勧めらしい。

MacRubyチュートリアルに丁度NSURLConnectionを使ったコードがあったので、それを丸ごと真似して以下のようにやってみました。

class DataRequest < OSX::NSObject
  include OSX

  def get(url, &blk)
    @buf = NSMutableData.new
    @blk = blk
    req = NSURLRequest.requestWithURL(NSURL.URLWithString(url))
    NSURLConnection.alloc.initWithRequest_delegate_(req, self)
  end
  
  def connection_didReceiveResponse(conn, resp)
    @buf.setLength(0)
  end
  
  def connection_didReceiveData(conn, data)
    @buf.appendData(data)
  end

  def connection_didFailWithError(conn, err)
    NSLog "Request failed"
  end
  
  def connectionDidFinishLoading(conn)
    @blk.call(@buf)
  end
end

これを以下の様に使います。

  def get(url, &blk)
    DataRequest.new.get(url, &blk)
  end

  def set_icon_async(asin)
    if(self.smallimage)
      self.icon = image(self.smallimage)
    else
      self.icon = SPINNER
      get("http://www.amazon.co.jp/gp/aw/d.html?a=#{asin}") do |data|
        body = NSString.alloc.initWithData_encoding_(data, NSShiftJISStringEncoding)
        if(/<img src="([^\"]+\.jpg)"/ =~ body) 
          get($1) do |data|
            self.smallimage = data.bytes.bytestr(data.length)
            self.icon = NSImage.alloc.initWithData(data)
            self.smallimage = nil if self.icon.nil?      
          end
        end
      end
    end
  end

getメソッド自体はHTTPのリクエストを出すだけだしてすぐ終了します。そしてダウンロードが終ったところで、元のスレッド上で指定ブロックが呼ばれるという動作です。

試してみたところ2秒ほどでダウンロードが終るようになりました。

画像のURLはの割と最初の方に記述されているので、URLを見付けたらそこで読みこみを打ち切って、次に進むようにすればさらに時間を短縮できるかもしれません。

クラッシュする

NSURLConnectionに切り替えてからアプリがクラッシュするようになってしまいました。再現しないのでデバッグするのも難しいです。

どうしたものやら。

*1: ちなみにスピナーの画像は/Applications/iWeb.app/Contents/Resources/Widgets/RSS Feed.iwdgt/Indeterminate-Spinner-Gray-Fast.gifのを使っています。

パフォーマンス改善

お、おそい…

Amazonカート整理アプリもそろそろまがりなりにも使える形になってきたので、試しに自分のアカウントで使ってみた。

結果。おそい…

試しに、400個くらいある今は買わないのアイテムをショッピングカートの方に移動してみた。そしたらGUIの表示が
全然更新されない。かたまってしまう。

これは予想外。

まとめて移動させてみた

ソースを見直してみたら、二つのNSArrayControllerの間でデータを移動するにに、一つずつ動かしているのに気付いた。
まとめて動かすようにしたら改善するかもしれない。

    from_list = @cart.active_items
    to_list = @cart.saved_items
    to_list.addObjects(from_list.selectedObjects)
    from_list.removeObjectsAtArrangedObjectIndexes(from_list.selectionIndexes)

こんな感じ

***move 421 items...
took 12.4783091545105 sec

おお、終了しなかった時にくらべれば大分マシだ。

NSImageViewのimageURLをやめた

今まで表紙画像はNSImageViewのimageURLにURLをbindさせてたのだけど、ひょっとしたら
移動の度にこれが毎回作りなおされて遅くなっているのかもしれない、と思って試しに
NSImageを作って直接imageの方にbindしてみた。

  image_path = "/images/I/41R5gj5VRFL._SL110_.jpg"
  response = Net::HTTP.start("ec3.images-amazon.com").get(image_path)
  data = OSX::NSData.dataWithRubyString(response.body)
  cover_image = OSX::NSImage.alloc.initWithData(data)

こんな感じ。実際に既にDataがある場合は流用するようにしている。

***move 421 items...
took 0.708032846450806 sec

これでよし。

この結果は実験用にテキトーに書き換えた状態での値。ちゃんと使えるようにするには
かなりリファクタリングしなければならない。

そうはいっても、方向性が見えてまずは一安心。

追記

遅くなってたのは、二つのリスト間でアイテムを移動するときに、addObjectとremoveObjectで一個ずつやってたのが原因。
removeObjectは毎回配列をスキャンするので、配列の長さNに対して一つのアイテムを移動させる処理がO(N)になってしまう。配列全体を移動させるならO(N^2)だ。それは遅い訳だ。

豚キムチのはずが…

夕飯に豚キムチなべを作ろうと思って材料を買ってきたのだが、肝心のキムチを買いわすれていた事に気付いた。

わざわざ買いにもどるのも面倒なので、そのまま他の材料だけで鍋にしてみたら…

「意外とおいしいじゃん」

豚、ニラ、もやし、しめじ鍋、味噌と豆板醤風味。ダシは中華スープの元。

別にキムチ要らなくね?

でも、名前がないのが難点だな。昨日何たべたっけ?という時すぐに思い出せないかもしれない、とかそういう意味で。

そうか、あらゆる意味でキムチのない豚キムチなんだから、豚キムチ(キム抜き)だと思っておけばいいのか。

[Ruby] XPathとか

結局HTML Scrapingですか?

Amazon ECSのシークレットキー(だっけ?うろおぼえ)を入れたままだと、アプリが完成しても公開できない。どうしよう?
いろいろ考えたのだけど、当面はHTMLからガシガシデータを抜きだしていく泥臭いアプローチで行く事に決めました。

一応検討したのは…

  1. Hashを作る部分をObjective-Cにしてその部分はソースを公開しない。ソースは公開するけど、ソースからビルドしたい人は自分でECSのアカウント作ってくださいというスタンス
  2. APIプロシキを使わせてもらう(and/or 自分でも立てる)

という代替案。
1の方は、リバースエンジニアリングされたら簡単にキーをひっこぬかれるだろう、という事でやめた。キーを知られてしまう事がどの程度問題なのかがよくわからないので。前にも書いたな。オープンソースのライセンス的な事は別に問題にならないと思われる。その気になれば別の実行ファイルにすればいいわけだし。プロキシーをアプリに同梱するみたいなイメージでやれば問題なし。

2の方の問題はプライバシー。プロキシーのサーバーにショッピングカートの中身が送られてしまうというのが気になる。個人を特定する情報はIPだけしか送られないので、そんなに問題ないかなと思うけれど、やっぱり気にする人は気にするかもしれない。世の中には「本屋さんで買うのが恥しい本をAmazonで買ってます」という人が意外に多いみたいなので*1

問題になるかどうかそれ自体よりも、その注意事項を説明する事で「なんか面倒くさそう…」という印象を与えてしまうのがいやなので、この方法はパス。

HTML Scrapingしちゃう事のダウンサイドですが、サーバーに発行するリクエストの量はたいした事ないので*2、アプリ側のパフォーマンス意外の面ではあんまり気にする必要はなさそうです。実はそれもやってみたらたいして変らない事が判明。

XPath便利だよXPath

実は最近仕事でXMLを処理する機会があったので、XPathの使い方を覚えておきたいな、と思ってたところです。で、今回のお題を使って練習。
XPathについては、だいたいの事は知ってたので、細かい点を以下の文書で調べたらだいたい用が足りました。
http://dret.net/lectures/xml-fall06/xpath-chapter.pdf

アイテムの抜きだし

もともと、ショッピングカートのページから各アイテムのへのリンクを抜きだす処理は、けっこうごちゃごちゃになっていました。
まずは元々どんな感じでやってたかを説明しますと…

カートの中のアイテムのIDは

  <a href="http://www.amazon.co.jp/exec/obidos/ASIN/4877832068/ほにゃららら">ここにタイトル</a>

みたいな感じで出現します。この中のASINの次にくる4877832068とかがアイテムのIDなわけです。

なのでページからリンクを全部抜きだして、URLが↑のパターンにマッチするものだけを抜きだせばいい、という方針でやっていました。

ただ、カートのページのアイテムの中にはカートの中のアイテム意外にもアイテムのリンクがたくさんあります(最近チェックした商品とか)。それらと本当に見分けられるかどうかが心配でした。一応パッと見た感じだと、大丈夫そうなんですが…

それと、カートの最初のページには、カートの中の商品(以下Activeといいます)と「今はかわない」にした商品(以下Saved)の、両方のリンクがあります。これらを見分ける安定した方法が欲しいところ。

そこで、注目したのが

<a name="1" />

みたいなリンク。こんな感じのnameの指定が、必ずActiveの商品の直前に置かれています(数字は1番目のアイテムは1、2番目は2という感じで変化)。またSavedの商品には

<a name="s1" />

というnameのアンカーがあります。

Nokogiri#search("a")して

  • まず目的のnameのaタグがくるまでループ
  • nameが見付かったらその種類応じて、次に来るリンクをActiveまたはSavedのアイテムのリンクと認識する

というアプローチを取っていました。

   def parse
     next_item = nil
     @page.parser.search("a").each do |a|
       name = a.get_attribute("name") || ""
       url = a.get_attribute("href") || ""
       case name
       when /^\d+$/
         next_item = :active
         next
       when /^s\d+$/
         next_item = :saved
         next
       end
       if not next_item.nil?
         url =~ %r{/ASIN/([^/]+)/}
         if(next_item == :active)
           @active_items.push AmazonOrganizer::Item.new($1)
         else
           @saved_items.push AmazonOrganizer::Item.new($1)
         end
         next_item = nil
         next
       end
    end
  end

うげー。まあ、どう書いてもある程度ぐだぐだなのはしかたがない。
仕事柄ステートマシンには馴染があるので、こういうコードにはあまり抵抗がなかったり…

問題点

上のやり方の問題点は、価格の情報をとってくるのが難しいところ。
価格は

<b class="price">¥ 3,360</b><br />

こんな感じで入ってます。珍しくclassが設定されてるので、拾うのは簡単なんですが()、これのある位置をどう見付けるかが問題。
上のアイテムを検出する方法はあくまでaタグを見付けるループの処理でしかないので、見付かった位置から「次にでてくるclassがpriceのbタグ」というのを見付けるのが難しいわけです。

XPathラクラク

実は上では書いてませんが、カートはtableで構成されてて、各アイテムはtrタグの中にまとまってます。↓みたいな感じ

<tr>
  <td>
    <a name="1"/>
    <input name="saveForLater.1" alt="今は買わない" />
  </td>
  <td>
    <a href="http://www.amazon.co.jp/exec/obidos/ASIN/4877832068/ほにゃららら">ここにタイトル</a>
  </td>
  <td>
    <b class="price">¥ 3,360</b><br />
  </td>
</tr>

見易いように大分はしょってますが、構造はこういう形。
なので、以下の事ができればリンクも、タイトルも、価格も抜き出す事ができそうです。

  • アイテムを格納しているtrのノードを順番に処理する
  • trタグの下にぶらさがっているタグを抜きだす

まさにXPathが得意としそうな感じではありませんか*3

ただ、件のname付きaタグは名前が短すぎてちょっと使いづらそうなので、trを見付けてくる目印としてinputタグを使います。nameがsaveForLater.とかmoveToCart.sで始まっているinputがターゲットです。その親の親ノードが問題のtrだという見付け方にしておきます。

実際のコードはこんな感じ

  def parse_item(key)
    item_rows = @page.parser.xpath("//input[starts-with(@name, '#{key}')]/../..")
    item_rows.collect do |row|
      link_to_item = row.xpath("descendant::a[starts-with(@href, 'http://www.amazon.co.jp/exec/obidos/ASIN')]")
      url = link_to_item.xpath("@href").to_s
      asin = %r{/ASIN/([^/]+)/}.match(url)[1]
      title = Iconv.conv("UTF-8", "SJIS", link_to_item.xpath("text()").to_s)

      price_string = row.xpath("descendant::b[@class='price']/text()").to_s
      price = /\d+/.match(price_string.sub(",", ""))[0].to_i

      AmazonOrganizer::Item.new(asin, {:title => title, :price => price})
    end
  end

  def parse
    @active_items += parse_item("saveForLater.")
    @saved_items += parse_item("moveToCart.s")
  end

まず以下の部分がtrタグの一覧をひっぱってきてるところです。@pageはWWW::Mechanize::Pageのインスタンス。parserてのがNokogiriを返してきます。

    item_rows = @page.parser.xpath("//input[starts-with(@name, '#{key}')]/../..")

"../.."というはちょっと汚いかもしれませんね。ancestor方向にtrを探しにいくべきかもしれません。
ま、それはそうと、これだけで、

  • アイテムを格納しているtrのノードを順番に処理する

のところは何とかなってしまいそうなわけです。すごい楽。

  • trタグの下にぶらさがっているタグを抜きだす

の部分も相対パスXPathを使えば楽々。

このtrタグの中にあるlinkならアイテムのリンクだとわかってるわけですから、安心して使えるわけです。

こんな感じ。

      link_to_item = row.xpath("descendant::a[starts-with(@href, 'http://www.amazon.co.jp/exec/obidos/ASIN')]")
      url = link_to_item.xpath("@href").to_s
      asin = %r{/ASIN/([^/]+)/}.match(url)[1]
      title = Iconv.conv("UTF-8", "SJIS", link_to_item.xpath("text()").to_s)

AmazonのHTMLはSJISなのでタイトルはUTF-8に変換してやる必要があります。

priceの方も簡単にとりだせます。

      price_string = row.xpath("descendant::b[@class='price']/text()").to_s

XPath便利。

しかし、我に返るとなんかまた何をいまさら的なエントリーですね。これも。XPathっていつごろからあるんでしたっけ?
いや、いいの。自分にとってはニュースなので書いた。反省はしてない。

Unit Test

けっこううろ覚えだけど、Unit Testをどうするかで試行錯誤した記憶がある。

基本の方向性としてはMochaを使ってMechanizeの偽物をつくって、あらかじめセーブしておいたHTMLをパーズしたNokogiriのインスタンスを返すという方針。

# Mechanizeの偽物
class DummyAgent
  include Mocha::API
  
  def initialize(testcase)
    @testcase_path = File.dirname(__FILE__) + "/" + testcase
    @page = nil
  end

  attr_reader :page

  # urlに対応する、セーブ済みhtmlファイルを@testcase_pathからreadする
  def get_html(url)
    # ...
  end

  def get(url)
    parser = Nokogiri::HTML.parse(get_html(url), nil, "SJIS")
    @page = stub(:parser => parser, :forms => make_dummy_forms(parser, url), 
                 :form_with => stub_everything)
  end
end

この準備ができてれば、以下のような感じで

class TC_AmazonTest < Test::Unit::TestCase
  def setup_amazon_stub(testcase)
    agent = DummyAgent.new(testcase)
    WWW::Mechanize.expects(:new).returns(agent)
  end

  def test_active_list
    setup_amazon_stub("testcase_dir1")
    amazon = Amazon.new()

    # Mechanizeを使ってあれこれする
    page = amazon.get(AmazonOrganizer::Amazon::URL_SHOPPING_CART)

    # ここがNokogiriを使う処理 (この時NokogiriはテストケースのHTMLで作成されてる)
    page.parse
  end
end

ま、実際のコードとはちょっと違いますが、だいたいこんな感じでうまくテストできてます。

Xpathを持ち出すまでもない時

既に書いた通り、もともとアイテムのIDはHTMLから抜きだしていたのだけど、それ意外の情報はこのIDをもとにAPIから得ていた。
その情報とは

  • タイトル
  • 価格
  • 画像 (smallimage)

で、タイトルと価格はカートから取れるけど、画像だけはやっぱり別の方法で調達する必要がある。
どうすればいいか?

もちろん普通に商品紹介ページを開いて、そこに張られている画像のURLを取ってくればいい。
でも、気付いたのですよ、それを楽にやる方法に。

それは、「モバイル向けページ」

例えば http://www.amazon.co.jp/gp/aw/d.html?a=4798023809

実はPC向けページだとsmallイメージではなくて中サイズのイメージが張られてるので、そっちを使うしかないかな
と思っていた。でも、モバイルページなら都合よく小さいサイズのイメージが貼ってあるではないですか。

という事で

    Net::HTTP.start("www.amazon.co.jp") do |http|
      response = http.get("/gp/aw/d.html?a=#{@asin}")
    end
    page = Nokogiri::HTML.parse(response.body, nil, "SJIS")
    @attr[:smallimage] = page.xpath("//img[contains(@src, '.jpg')]/@src").to_s

楽々。

でもさ、後で気付いたのだけど、さすがにこれはやりすぎだろう。モバイルのページには.jpgは一つしか貼ってない。だから正規表現で十分抜きだせるハズだ。

  /<img src="([^"]+.jpg)">/.match(response.body)[1]

とかそんな感じ(試してないけど)。
まだXpathのままにしてあるけど、そのうち直そう。

*1:イマイチ理解できない考え方だけどなぁ…。オンラインに個人情報つきでバッチリ記録が残る方が嫌じゃないですか?

*2:1アイテムにつき一回だけ、結果はキャッシュされる

*3:という事を考えた時点ではXPathを使った事が一度もなかったので、具体的にどうやるかはわかってなかったのだけど… 結果的には正解でした。

Keychainとか

近況

全然更新してなかったけど、実はちょっとずつ作業は進んでたり。進捗を書いてみます。

  • Keychainからパスワードひろってくるようにした (ソースの中にパスワードハードコードしてたのを直しました)
  • Amazon ECSを使うのをやめた (これも、自分のECSアカウント情報を埋めこまないで済ますため)

あれ?少いな。
その経過でいろいろ覚えたのでいいんだけど…

RubyCocoaにおけるKeychainの使い方

で、このエントリーではKeychainについて書こうと思います。

結論からいうと、KeychainをObjective-CでラップしてそれをRubyから使う形にした。

最初はRubyからBridgeSupport経由で直にSecurity frameworkを使おうと思ったんですよ。
丁度↓こんなドンピシャリなものも見付かったし

RubyCocoa: Store User Password in Keychain

何だよ、結局ググってんじゃん。というのは置いといて、この記事を参考にして一応パスワードを取ってくる事はできました。ありがたや。

BridgeSupportファイルを生成したり、Rubyからframework読みこんだり、やった事なかった事をいろいろできたので、これはこれで良い経験になりました。

  state, *data = OSX::SecKeychainFindInternetPassword(
                                                      nil,
                                                      server.length, server,
                                                      0, nil,
                                                      account.length, account,
                                                      0, "",
                                                      0,
                                                      OSX::KSecProtocolTypeHTTPS,
                                                      nil)
  # data[1]に入ってる文字列は普通のCの文字列ではないので、長さ分で来ってやる必要がある
  password = data[1].slice(0, data[0]-1)

こんな感じでOK (うろ覚えで書いてるので、ちょっと間違ってるかもしれん)。

一つ注意する必要があるのは、gen_bridge_metadataで作ったBridgeSupportそのままだと、パスワードを受けとれないという事。
passwordDataの型がvoid**だから。

  <arg type='^^v'/>

  <arg type='^*'/>

にすればOK

パスワード読めれば十分って人ならこれでOKかもしれません。

しかし問題が…

  • パスワード文字列がリークするんでは?(たぶん)
  • サーチとかやろうとすると大変な事になる

前者については、このアプリではそんなに問題にならないかな。パスワードはamazonにアクセスする度に取得しなおすとしても、基本的に一回アプリを起動したらサインオンした状態を維持するつもりだから、そんなに何どもパスワードを読むわけではないしね。

どっちかというとメモリリークよりも、用の済んだパスワード情報をいつまでもメモリに置いておくのがよろしくない、という方が問題かもしれません。

問題だったのは後者の方。このアプリではアカウント名(メールアドレス)をKeychainからサーチしてきたかったので…
Safariユーザーの場合、Amazonにサインインしてパスワードを記憶させてれば、Keychainにアカウント情報がもう入ってるはずです。それをサーチしてきて、そのまま使えるようにしたかった。絶対必要という機能ではないけど、こっちの方がタイプしまちがえる事もなくて便利だし、やっぱりやりたい。
という事でサーチする方法を調べてみた。

BridgeSupport良くわからない

やりたい事は簡単で、サーバーがwww.amazon.co.jpになってるインタネットパスワードの一覧を取ってきて、そのアカウント情報のリストを返すって事だけ。

しかし、その為にはサーチ結果を一旦SecKeychainSearchRefというものに入れられて、そこからKeychainItemに一つずつコピーしてアトリビュートを抜きだしていかなければならないらしい。もちろん用が済んだオブジェクトはフリーしてやる必要がある。

このSecKeychainSearchRefとかKeychainItemとかをちゃんとBridgeSupportでラップして使いこなす自信がありません。
いやむしろ、ちょっと自作BridgeSupportをロードしようとしたらBUS Errorとか言われてややトラウマ気味に…

ひょっとしたらもしかしたらできるかもしれないけど、BridgeSupportをマスターする気力がありませんでした。

余談だけど、日本語のGoogleでBridgeSupportを検索すると「bridgesupportファイルの仕様がよくわからない」という記事がトップヒットになって笑ってしまった。まったく同感だ!

ちなみに↑の記事で問題になっているBridgeSupportファイルの中に書かれているハナモゲラな記号は"Objective-C runtime encoding types"というらしい。man BridgeSupport(1)を良く読むと書いてある。で、ググったらADCに説明が見付かった。でも、これだけ見ても全然わかりませんが(^^;

結局サーチはどうやるの?

サーチするコードはこんな感じ

#import <Cocoa/Cocoa.h>
#import <Security/Security.h>

@interface Keychain : NSObject {
  SecKeychainRef keychain;
}

static const char server[] = "www.amazon.co.jp";

static inline NSUInteger len(NSString* str) {
  return [str lengthOfBytesUsingEncoding: NSUTF8StringEncoding];
}

static inline const char* cstr(NSString* str) {
  return [str UTF8String];
}

@implementation Keychain
- (Keychain*) init {
  keychain = NULL;
}

- (NSArray*) getAccounts {
  SecKeychainAttribute attributes[1];
  SecKeychainAttributeList list;
  OSErr result;
  SecKeychainSearchRef search;
  SecKeychainItemRef item;

  SecKeychainAttributeInfo attrInfo;
  UInt32 tag = kSecAccountItemAttr;
  SecKeychainAttributeList *list2 = NULL;
  NSMutableArray *ary;
  NSString *buf;

  
  attributes[0].tag = kSecServerItemAttr;
  attributes[0].data = "www.amazon.co.jp";
  attributes[0].length = strlen(attributes[0].data);
  
  list.count = 1;
  list.attr = attributes;

  // ここがサーチしてるところ
  result = SecKeychainSearchCreateFromAttributes(keychain, kSecInternetPasswordItemClass, &list, &search);
  if(result != noErr) {
    NSLog(@"Error SecKeychainSearchCreateFromAttributes: %d", result);
    return NULL;
  }

  attrInfo.count = 1;
  attrInfo.tag = &tag;
  attrInfo.format = NULL;

  ary = [NSMutableArray arrayWithCapacity: 1];

  while((result = SecKeychainSearchCopyNext(search, &item)) == noErr && &item) {
    // ここがアトリビュートをコピーしてるところ
    result = SecKeychainItemCopyAttributesAndData(item, &attrInfo, NULL, &list2, NULL, NULL);
    if(result != noErr) {
      NSLog(@"Error SecKeychainSearchCreateFromAttributes: %d", result);
      return NULL;
    }

    buf = [[NSString alloc] initWithBytes: (const void*)list2->attr[0].data 
			    length: list2->attr[0].length encoding:NSASCIIStringEncoding];
    [ary addObject: buf];
    SecKeychainItemFreeAttributesAndData(list2, NULL);
    CFRelease(item);
  }
  /*  err(result, "SecKeychainSearchCopyNext"); */

  CFRelease(search);
  return ary;
}
@end

実際のコードからいろいろ切り貼りしてきたので、そのままでは動かないかもしれませんが…

コメントあんまりないのでわかりにくいかもしれないですが、要はSecKeychainSearchCreateFromAttributesがサーチする関数。で結果がSearchRefなるものに入ってるので、それをひとつずつKeychainItemにコピーし、それからまた別の関数読んで欲しいアトリビュートをコピーしてくるという手順です。めんどくセー。素直に全部のアトリビュートが入ったHash作ってそのArrayを返すってわけにはいかんのか?(もともとのAPIは大規模なディレクトリサービスをサーチするような事を想定しているのかな?パソコン上で個人のパスワード管理するにはオーバースペック気味。)

実はこれ、最初にAPIの実験用としてCで書いたんですよ。それを参考にBridgeSupportを調整してRubyで↑のロジックを書くつもりだった。
しかし、そのやり方だとどっかで壁にぶつかりそうという気がしてならなかった。

Objective-Cのメソッドなら特に何もしないでもRubyから呼べるらしいので、ラッパーを作ってしまった方が楽なのでは?そう思ってObjective-Cで書き直したわけです。

XCodeのプロジェクトに組込むには?

Objective-Cで所望の動作が実現できるのはわかったけど、RubyCocoaのアプリの中で使うにはどうすれば?

やってみたら別に悩むところなんかまったくなくて、以下の様に簡単に

  • Keychain.hとKeychain.mを適当に作って、プロジェクトに追加
  • frameworksにSecurityフレームワークを追加

こんだけ。超適当な説明ですいません。でも本当に簡単。これで上記のObjective-Cコードが、以下の様な形で使えちゃう

keychain = OSX::Keychain.alloc.init
accounts = keychain.getAccounts.to_ary.collect {|item| item.to_s}

とこんな感じで使える筈。
NSStringのNSArrayが返ってきてしまうので、変換しないといけないのがやや面倒ですが… Ruby側でKeychain#get_accountsというメソッドを定義して、そこで変換すればいいかも。

class OSX::Keychain
  def get_accounts
    getAccounts.to_ary.collect {|item| item.to_s}
  end
end

とか。こういうのを定義したKeychain.rbを作ってプロジェクトに追加すればOK。

せっかくなので、他にもいくつかの機能をつけて、下みたいな事ができるようにもしてみてた。こういうのはRubyでやると本当楽。

  keychain.add_internet_password("https://www.amazon.co.jp/", "email@test.com", "password")

Unit Testとか

Unit TestはXCodeとは別にemacsで全部やってるので、そっちの環境でObjective-Cで書いたKeychainをテストできるようにしなければならない。
これも、結局RubyでUnit Testを書けば、Objective-Cのコードのテストができてしまうという非常にラクラクな状況になる事がわかった。

なんか、だいたい以下の様なMakefileを用意

.PHONY : all
all : runtest
	./runtest
objc_path = ../AmazonOrganizer
CFLAGS += -I $(objc_path)

runtest : runtest.o Keychain.o
	gcc -o $@ $^ -framework Cocoa -framework RubyCocoa -framework Security

runtest.o : runtest.m
Keychain.o : $(objc_path)/Keychain.h $(objc_path)/Keychain.m
	gcc -c $(objc_path)/Keychain.m

runtest.mはこんだけ

#import <Cocoa/Cocoa.h>
#import <RubyCocoa/RBRuntime.h>
int main(int argc, const char *argv[])
{
  return RBApplicationMain("runtest.rb", argc, argv);
}

runtest.rbは以下の形。これは、Objective-Cを混ぜる前に使ってたテスト実行様のスクリプトをそのまま使えてます。

require 'test/unit/autorunner'

Dir.chdir(File.dirname(__FILE__))

$LOAD_PATH.push("../lib")
$LOAD_PATH.push("../AmazonOrganizer")

runner = Test::Unit::AutoRunner.new(true)
runner.run

これで、Testコードの中でOSX::Keychainをフツーに呼びだせてしまうわけです。

「わけです」とか偉そうに解説してますけど、RubyCocoaをずっと使っている人には常識なんでしょうね。
いや、しかし、自分は今日知ったので自分的にはニュースだ。これすごい!!便利!!!

ログインキーチェーン(普段実際に使ってるキーチェーン)でテストするのは嫌だったので、テスト用にキーチェーンを作るコードなんかも書いてみた。いい感じ。

Keychainですよ。KeyChainじゃなくて!

key chainじゃなくて、keychainで一つの単語。

KeyChain.mとかKeyChain.hとか@key_chainとか散々書いたあとでこの事実に気付いた。

…ええ、直しましたとも。