対策
どういう症状?
↓のダイアログが表示されて、動画が見れない。
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>
もうちょっと詳しい説明
↓のページで詳しく解説されてます。
"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を見付けたらそこで読みこみを打ち切って、次に進むようにすればさらに時間を短縮できるかもしれません。
パフォーマンス改善
お、おそい…
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からガシガシデータを抜きだしていく泥臭いアプローチで行く事に決めました。
一応検討したのは…
- Hashを作る部分をObjective-Cにしてその部分はソースを公開しない。ソースは公開するけど、ソースからビルドしたい人は自分でECSのアカウント作ってくださいというスタンス
- 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タグの下にぶらさがっているタグを抜きだす
ただ、件の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タグの下にぶらさがっているタグを抜きだす
この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のままにしてあるけど、そのうち直そう。
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とか散々書いたあとでこの事実に気付いた。
…ええ、直しましたとも。