VagrantにChefでFluentd+elasticsearch+Kibana3をデプロイ

VagrantにChefでFluentdとelasticsearchとKibana3をインストールして動かしてみます。

ChefとBerkshelfのインストール

まずこのプロジェクトのファイルを置くディレクトリを作成します。

$ mkdir sandbox
$ cd sandbox

以下の内容でGemfileを作成します。

source "https://rubygems.org"
 
gem "knife-solo"
gem "berkshelf"

インストールします。

$ bundle install --path vendor/bundle

Chefリポジトリの作成

Chefのリポジトリを作成します。

$ bundle exec knife solo init chef-sandbox
$ cd chef-sandbox

Berkshelfで必要なcookbookをインストール

以下の内容でBerksfileを作成します。

source "http://api.berkshelf.com"
 
cookbook 'td-agent', :git => 'https://github.com/treasure-data/chef-td-agent.git'
cookbook 'elasticsearch', :git => 'https://github.com/elasticsearch/cookbook-elasticsearch.git'
cookbook 'kibana', :git => 'https://github.com/realityforge/chef-kibana.git'
cookbook 'timezone-ii', :git => 'https://github.com/L2G/timezone-ii.git'

cookbookを取得します。

$ bundle exec berks vendor

独自のCookbookの作成

独自のCookbookを作成します。

$ bundle exec knife cookbook create base

インストール時にlibcurl-devが必要になるのとFluentdの設定ファイルを独自のものに変更する為に、cookbooks/base/recipes/default.rbを以下の内容に変更します。

package "libcurl4-openssl-dev" do
  action :install
end
 
include_recipe "td-agent::default"
 
begin
  t = resources("template[/etc/td-agent/td-agent.conf]")
  t.source "td-agent.conf.erb"
  t.cookbook "base"
rescue Chef::Exception::ResourceNotFound
  Chef::Log.warn "could not find template /ect/td-agent/td-agent.conf"
end

独自のFluentdの設定ファイのテンプレートを作成します。cookbooks/base/templates/default/td-agent.conf.erbを以下の内容(必要に応じて変更)で作成します。

<% if node['td_agent']['includes'] %>
include conf.d/*.conf 
<% end %>
<% if node['td_agent']['default_config'] %>
####
## Output descriptions:
##
 
# Treasure Data (http://www.treasure-data.com/) provides cloud based data
# analytics platform, which easily stores and processes data from td-agent.
# FREE plan is also provided.
# @see http://docs.fluentd.org/articles/http-to-td
#
# This section matches events whose tag is td.DATABASE.TABLE
<match td.*.*>
  type tdlog
  apikey <%= node['td_agent']['api_key'] %>
 
  auto_create_table
  buffer_type file
  buffer_path /var/log/td-agent/buffer/td
</match>
 
## match tag=debug.** and dump to console
<match debug.**>
  type stdout
</match>
 
####
## Source descriptions:
##
 
## built-in TCP input
## @see http://docs.fluentd.org/articles/in_forward
<source>
  type forward
  port 24224
</source>
 
## built-in UNIX socket input
#<source>
#  type unix
#</source>
 
# HTTP input
# POST http://localhost:8888/<tag>?json=<json>
# POST http://localhost:8888/td.myapp.login?json={"user"%3A"me"}
# @see http://docs.fluentd.org/articles/in_http
<source>
  type http
  port 8888
</source>
 
## live debugging agent
<source>
  type debug_agent
  bind 127.0.0.1
  port 24230
</source>
<% end %>
 
<match myapp.**>
  index_name adminpack
  type_name http
  type elasticsearch
  include_tag_key true
  tag_key @log_name
  host localhost
  port 9200
  logstash_format true
  flush_interval 5s
</match>

ロールの作成

Chefのロールを作成します。

roles/td-agent.rbを以下の内容で作成します。

name "td-agent"
description "Base role applied to td-agent nodes."
run_list(
  "recipe[apt]",
  "recipe[base::default]",
  "recipe[td-agent]"
)
override_attributes "td_agent" => {
                      "plugins" => [ "elasticsearch" ],
                      "platform" => "ubuntu"
                    }

roles/elasticsearch.rbを以下の内容で作成します。

name "elasticsearch"
description "Base role applied to elasticsearch nodes."
run_list(
  "recipe[java]",
  "recipe[elasticsearch]"
)
override_attributes "java" => {
                        "install_flavor" => "openjdk",
                        "jdk_version" => "7" }

roles/kibana.rbを以下の内容で作成します。

name "kibana"
description "Base role applied to kibana nodes."
run_list(
  "recipe[kibana::default]",
  "recipe[kibana::apache]",
)
default_attributes "kibana" => {
                      "version" => "3",
                    }

VagrantとVirtualBoxをインストール

Oracle VM VirtualBox – Downloads | Oracle Technology Network | OracleからVirtualBoxをダウンロードしてインストールします。

Download Vagrant – VagrantからVagrantをダウンロードしてインストールします。

Vagrant Boxの作成

Vagrant Boxを作成します。代わりにあとでVagrantfileで指定してもいいです。

$ vagrant box add opscode-ubuntu-12.04 https://opscode-vm-bento.s3.amazonaws.com/vagrant/opscode_ubuntu-12.04_provisionerless.box

vagrant-omnibusプラグインのインストール

Vagrantに自動的にChefをインストールするためのプラグインvagrant-omnibusをインストールします。

$ vagrant plugin install vagrant-omnibus

Vagrantfileの作成

Vagrantfileのひな形を作成します。

$ vagrant init opscode-ubuntu-12.04

Vagrantfileの

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|

という行のあとに

  config.omnibus.chef_version = :latest
 
  config.vm.provision :chef_solo do |chef|
    chef.cookbooks_path = ["./cookbooks", "./berks-cookbooks"]
    chef.roles_path = ["./roles"]
    chef.add_role("td-agent")
    chef.add_role("elasticsearch")
    chef.add_role("kibana")
 
    chef.run_list = [
      "role[td-agent]",
      "role[elasticsearch]",
      "role[kibana]",
      "recipe[timezone-ii]",
    ]
 
    chef.json = {
      :tz => "Asia/Tokyo",
    }
  end
 
  config.vm.network "forwarded_port", guest: 80, host: 8080
  config.vm.network "forwarded_port", guest: 9200, host: 9200
  config.vm.network "forwarded_port", guest: 8888, host: 8888

を挿入します。

Vagrantを実行

$ vagrant up

でvagrantを実行します。

動作確認

http://localhost:8080/index.html#/dashboard/file/logstash.json
にアクセスします。以下の様なKibanaの画面が表示されるはずです。

Voila Capture 2014 04 25 05 50 34 午後

次に
http://localhost:8888/myapp.test?json={%22event%22:%22access%22}
にアクセスします。これは、Fluentdにhttpでログを作成しているので、表示は何も行われません。

再び
http://localhost:8080/index.html#/dashboard/file/logstash.json
にアクセスします。すると、以下の様に一つログが表示されているのが確認できるはずです。ログの反映はタイムラグがあるので、表示されていない場合は数秒置いてリロードしてみてください。
Voila Capture 2014 04 25 05 54 26 午後

以上で動作確認までできました。

Objective-CのFunctional Reactive ProgrammingフレームワークReactiveCocoaを試してみる

今、一部で話題のReactive Programmingですが、Objective-CにもReactiveCocoaというフレームワークが存在します。ということでReactiveCocoaを試しに使ってみます。

GitHubにソースコードを置きました。

iOSアプリのシングルビュー構成のプロジェクトを作成します。

今回はReactiveCocoaはCocoaPodsでインストールします。Podfileを作成してpod ‘ReactiveCocoa’と記述します。

まず、テキストフィールドへの入力によってボタンの表示・非表示を行ってみます。
StoryboardでビューにUITextFieldを二つとUIButtonを一つ配置し、コントローラのヘッダにプロパティを作成します。

@property (weak, nonatomic) IBOutlet UITextField *usernameTextField;
@property (weak, nonatomic) IBOutlet UITextField *passwordTextField;
@property (weak, nonatomic) IBOutlet UIButton *logInButton;

コントローラのviewDidLoadで以下のように記述します。

    [self.usernameTextField.rac_textSignal subscribeNext:^(NSString *text) {
        self.logInButton.hidden = (text.length == 0);
    }];

UITextFieldのカテゴリメソッドrac_textSignalでtextの変更のシグナルのRACSignalが取得できます。RACSignalとはその名の通りイベントのシグナルです。
シグナルが正常に発生するとsubscribeNext:で登録したブロックが実行されます。ログインボタンのhiddenにテキスト長が0かの真偽値を設定しています。これでusernameのtextが入力されていない場合にlogInButtonが非表示になり、値を入力するとlogInButtonが表示されます。

今度はusernameとpasswordに入力するとログインボタンを表示するように変更してみます。先ほどのコードを削除して以下のコードを記述します。

    [[RACSignal combineLatest:@[self.usernameTextField.rac_textSignal, self.passwordTextField.rac_textSignal] reduce:^(NSString *username, NSString *password) {
        return @(username.length == 0 || password.length == 0);
    }] subscribeNext:^(NSNumber *hidden) {
        self.logInButton.hidden = hidden.boolValue;
    }];

二つのUITextFieldのシグナルを[RACSignal combineLatest:reduce:]でまとめて、一つのシグナルとしてあつかいます。combineLatestで二つのシグナルを登録して、reduceで二つのテキストの長さのいずれかが0の場合にYES(のNSNumber)を返しています。
これでusernameとpasswordのtextが入力されていない場合にlogInButtonが非表示になり、どちらともに値を入力するとlogInButtonが表示されます。

次に非同期処理としてTwitterからユーザ名を取得してみます。
Storyboardにボタンとラベルを追加して、コントローラのヘッダのプロパティにバインドします。

@property (weak, nonatomic) IBOutlet UIButton *twitterButton;
@property (weak, nonatomic) IBOutlet UILabel *twitterLabel;

viewDidLoadには以下のように記述します。

    @weakify(self);
    [[self.twitterButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *button) {
        [[[[TwitterManager sharedManager] requestAccessToAccounts] sequenceNext:^{
            return [[TwitterManager sharedManager] fetchName];
        }] subscribeNext:^(NSString *name) {
            @strongify(self);
            dispatch_async(dispatch_get_main_queue(), ^{
                self.twitterLabel.text = name;
            });
        }          error:^(NSError *error) {
            @strongify(self);
            dispatch_async(dispatch_get_main_queue(), ^{
                self.twitterLabel.text = error.localizedDescription;
            });
        }];
    }];

rac_signalForControlEvents:でボタンのイベントのシグナルを取得して、subscribeNext:でボタンのイベントで実行するブロックを登録しています。
TwitterManagerのrequestAccessToAccountsを実行すると、これもシグナルを返します。それが成功すると、fetchNameを実行して、さらにシグナルを返しています。そのシグナルが成功するとtwitterLabel.textに名前を設定し、エラーが発生するとエラー内容を設定しています。

TwitterManagerでの非同期処理の記述の仕方は以下のようになります。

+ (TwitterManager *)sharedManager {
    static TwitterManager *_instance = nil;
 
    @synchronized (self) {
        if (_instance == nil) {
            _instance = [[self alloc] init];
        }
    }
 
    return _instance;
}
 
- (id)init {
    self = [super init];
    if (self) {
        self.accountStore = [[ACAccountStore alloc] init];
        self.accountType = [self.accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
    }
    return self;
}
 
- (RACSignal *)requestAccessToAccounts {
    RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id <RACSubscriber> subscriber) {
        [self.accountStore requestAccessToAccountsWithType:self.accountType options:nil completion:^(BOOL granted, NSError *error) {
            if (granted) {
                NSArray *accounts = [self.accountStore accountsWithAccountType:self.accountType];
                if (accounts.count > 0) {
                    self.account = accounts[0];
                    [subscriber sendNext:nil];
                } else {
                    [subscriber sendError:[NSError errorWithDomain:@"ERRORDOMAIN" code:0 userInfo:nil]];
                }
            } else {
                [subscriber sendError:(error ? error : [NSError errorWithDomain:@"ERRORDOMAIN" code:0 userInfo:nil])];
            }
 
            [subscriber sendCompleted];
        }];
 
        return [RACDisposable disposableWithBlock:nil];
    }];
 
    return signal;
}
 
- (RACSignal *)fetchName {
    @weakify(self);
    RACSignal *signal = [RACSignal startWithScheduler:[RACScheduler scheduler] subjectBlock:^(RACSubject *subject) {
        @strongify(self);
        NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"https://api.twitter.com/1.1/users/show.json?screen_name=%@", self.account.username]];
        SLRequest *request = [SLRequest requestForServiceType:SLServiceTypeTwitter requestMethod:SLRequestMethodGET URL:url parameters:nil];
        request.account = self.account;
        [request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
            if (error) {
                [subject sendError:error];
            } else {
                NSError *subError;
                NSDictionary *status = [NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingMutableLeaves error:&subError];
                if (subError) {
                    [subject sendError:subError];
                } else if (status[@"name"]) {
                    [subject sendNext:status[@"name"]];
                } else {
                    [subject sendError:[NSError errorWithDomain:@"ERRORDOMAIN" code:2 userInfo:nil]];
                }
            }
 
            [subject sendCompleted];
        }];
    }];
 
    return signal;
}

requestAccessToAccountsは[RACSignal createSignal:]でシグナルを作成して返しています。リクエストが成功すると[subscriber sendNext:status[@”name”]]でシグナルを送り、エラーの場合はsendError:でエラーを送ります。完了するとsendCompletedを送ります。RACSignalではsubscribeNext:やsubscribeError:以外にもsubscribeCompletedで完了通知を受けとることができます。return [RACDisposable disposableWithBlock:];でキャンセルする時の処理を記述して返します。今回はキャンセル処理は無いのでnilを渡しています。(createSignal:って非同期処理に利用していいのかな?このあたり良く理解していないです。)

fetchNameでは[RACSignal startWithScheduler:subjectBlock:]でシグナルを作成して返しています。RACDisposableを返さない事以外はcreateSignal:とほぼ同様のようです。

ということで、ReactiveCocoaを試してみましたが、非Reactive Programmingとはかなり記述が変わってきますね。ReativeCocoaを使いこなせばUIイベントの処理や非同期処理が記述しやすくなる感触が得られました。

UIViewのローカライズ作業をMethod Swizzlingで簡単にする

iOSでStoryboardやXIBのUILabelやUIButtonなどのローカライズを行うことに関してEZ-NET: UIView のローカライズ作業を簡単にするという記事がありました。その記事ではサブクラス化してローカライズを行っているのですが、サブクラスでなくてカテゴリーで実装することもできるんじゃないかと思ってやってみました。

まずMethod Swizzlingを行うクラスを作成します。

#import <Foundation/Foundation.h>
 
@interface MethodSwizzling : NSObject
+ (void)swizzleMethod:(Class)aClass from:(SEL)originalSelector to:(SEL)newSelector;
@end
 
#import <objc/runtime.h>
#import "MethodSwizzling.h"
 
@implementation MethodSwizzling {}
+ (void)swizzleMethod:(Class)aClass from:(SEL)originalSelector to:(SEL)newSelector {
    Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
    Method newMethod = class_getInstanceMethod(aClass, newSelector);
 
    if (class_addMethod(aClass, originalSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) {
        class_replaceMethod(aClass, newSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, newMethod);
    }
}
@end

そしてUILabelやUIButtonのカテゴリーを作成します。

@interface UILabel (Localize)
- (void)localizedAwakeFromNib;
@end
 
@implementation UILabel (Localize)
+ (void)load {
    [MethodSwizzling swizzleMethod:[UILabel class] from:@selector(awakeFromNib) to:@selector(localizedAwakeFromNib)];
}
 
- (void)localizedAwakeFromNib {
    self.text = NSLocalizedString(self.text, nil);
 
    [self localizedAwakeFromNib];
}
@end
 
@interface UIButton (Localize)
- (void)localizedAwakeFromNib;
@end
 
@implementation UIButton (Localize)
+ (void)load {
    [MethodSwizzling swizzleMethod:[UIButton class] from:@selector(awakeFromNib) to:@selector(localizedAwakeFromNib)];
}
 
- (void)localizedAwakeFromNib {
    [self setTitle:NSLocalizedString(self.titleLabel.text, nil) forState:UIControlStateNormal];
 
    [self localizedAwakeFromNib];
}
@end

Method SwizzlingでawakeFromNibの代わりにlocalizedAwakeFromNibが実行されるように変更しました。
これでUILabelやUIButtonに設定した文字列がローカライズされて表示されます。

問題が出るかもしれないけど、一応こういう方法でも可能だということで。

Evernoteにファイルを保存するフォルダアクションの作り方

指定したフォルダに入れたファイルをEvernoteにクリップするEverDesktopというアプリが公開されました。850円なので躊躇される方もいるかと思うので、無料で似たような機能を作成する方法を解説します。

まず機能を作成する前に確認。
ファイルをクリップするだけならDockのEvernoteアイコンにファイルをドラッグ&ドロップするだけで可能です。
Evernote Icon

さて、フォルダにファイルを入れてクリップするためにはフォルダアクションを利用します。フォルダアクションというのは、指定のフォルダにファイルやフォルダが追加されたら動作する機能です。

フォルダアクションを簡単に作るにはAutomatorを利用します。

Automatorを起動します。起動したらフォルダアクションを選択します。

Voila_Capture7.png

フォルダアクションを作成するための画面が表示されます。フォルダアクションを付けるフォルダを選択します。

Voila_Capture8.png

AppleScriptを利用して機能を作成するので、「AppleScript を実行」というアクションをドラッグして追加します。
Voila_Capture9.png

追加すると以下のようになります。

Voila_Capture10.png

そこへ以下のAppleScriptスクリプトをペーストします。

on run {input, parameters}
	tell application "Evernote"
		repeat with x in input
			try
				create note from file x
			on error error_message number error_number
				display alert "Send to Evernote Failed" message "Error:     " & error_message & "
" & "Error Number:  " & error_number as warning
			end try
		end repeat
	end tell
	return input
end run

※このスクリプトはAutomator.appを使って、Evernoteにファイルを転送する – fav Apple!から流用させてもらいました。ありがとうございました。

Voila_Capture11.png

名前を付けて保存すると完成です。
クリップしたいファイルを最初に指定したフォルダに入れるとEvernoteにクリップされます。

また、以下のスクリプトを利用すると複数のファイルを一緒にフォルダに入れた時に1つのノートにクリップされます。

on run {input, parameters}
	tell application "Evernote"
		try
			create note title "Mixed note" with text "" attachments input
		on error error_message number error_number
			display alert "Send to Evernote Failed" message "Error:     " & error_message & "
" & "Error Number:  " & error_number as warning
		end try
	end tell
	return input
end run

EverDesktopにはクリップしたファイルを日付ごとに仕分けする機能がありますが、このAppleScriptスクリプトをカスタマイズすることで同様の機能を作成することも可能です。挑戦してみてください。

Storyboardで起動時にNavigation Controllerのビューコントローラを構成する

Storyboardを利用している時に、起動時にNavigation Controllerの子ビューコントローラの構成を変更してみます。

まずStoryboardを以下のように作成します。
NewImage

Initial view controllerをUINavigationController、FirstViewControllerをUINavigationControllerに接続、SecondViewControllerを別個作成しています。
SecondViewControllerには以下のようにIdentifierにSecondViewControllerを指定しています。
Identifier

次にApplication Delegateの
– (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
メソッドを以下のように記述します。

UINavigationController *navigationController = (UINavigationController *)self.window.rootViewController;
UIViewController *secondViewController = [navigationController.storyboard instantiateViewControllerWithIdentifier:@"SecondViewController"];
[navigationController pushViewController:secondViewController animated:NO];
return YES;

StoryboardのInitial view controllerであるUINavigationControllerがwindowのrootViewControllerにセットされているので、それを取り出します。コントローラからStoryboardを取り出し、instantiateViewControllerWithIdentifierメソッドで指定したコントローラをインスタンス化します。上記コードではSecondViewControllerをインスタンス化しています。そのSecondViewControllerをUINavigationControllerにpushして、表示しています。

アプリを実行すると

second_view.png
このように、SecondViewControllerのビューが表示されます。

このようにinstantiateViewControllerWithIdentifierでインスタンス化することで、Segueで指定していないビューコントローラを利用することができます。

カスタムStoryboardセグウェイを作成する

Storyboardではカスタマイズしたセグウェイを利用することができます。

UINavigationControllerでフリップで画面を切り替えるセグウェイを作ってみます。

まず、UIStoryboardSegueを継承したFlipSegueクラスを作成します。
次に、FlipSegueクラスに以下のコードを追加します。

- (void)perform {
    UIViewController *sourceViewController = (UIViewController *)self.sourceViewController;
    UIViewController *destinationViewController = (UIViewController *)self.destinationViewController;
    [UIView transitionWithView:sourceViewController.navigationController.view
        duration:0.2
        options:UIViewAnimationOptionTransitionFlipFromLeft
        animations:^{
            [sourceViewController.navigationController pushViewController:destinationViewController animated:NO];
        }
        completion:nil];
}

ソースビューからデスティネーションビューにフリップアニメーションを行いつつ、Navigation Controllerでコントローラの切り替えをアニメーション無しで行っています。
そして、StoryboardでSegueをCustomに変更してFlipSegueを指定します。
NewImage

TestFlight SDKを利用する

簡単にAdhocアプリを配布できるTestFlightからSDKがリリースされました。

  • セッション
  • クラッシュリポート
  • アプリ内での質問
  • チェックポイント
  • リモートロギング
  • アプリ内アップデート

といった機能があるようです。

早速組み込んでみたいと思います。

SDKのページからSDKをダウンロードして解凍します。

libTestFlight.aとTestFlight.hをXcodeプロジェクトに追加します。TestFlight.hを利用するファイルでimportするのですが、簡単に行うにはprojectname_Prefix.pchファイルの#ifdef __OBJC__セクションに#import “TestFlight.h”を記述します。プリプロセッサマクロで#ifdef ADHOCのように分岐するといいと思います。

http://testflightapp.com/dashboard/team/にアクセスしてeditをクリックしてチームトークンを取得します。取得したら
-(BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[TestFlight takeOff:@”チームトークン”];
}
と記述します。チームトークンのところは書き換えてください。

結果はすべてビルドページで閲覧できます。

クラッシュリポートを有効にするにはビルドセッティングでDeployment Post Processing,Strip Debug Symbols During Copy,Strip Linked ProductをNoに設定します。アプリがクラッシュするとビルドページに表示され、スタックとレースなどを確認することができます。

チェックポイントを設定するには[TestFlight passCheckpoint:@”CHECKPOINT_NAME”];とします。チェックポイントを通過したデバイスの割合をビルドページで確認できます。

フィードバックを行えるビューを表示するには[TestFlight openFeedbackView];とします。フィードバックが送信されるとビルドページで確認して、返信することができます。

Safari ExtensionsでHello World!

Safari 5にSafari Extensionsが追加されました。
これでSafariの追加機能が簡単に作成できるようになりました。

ただ、デフォルトでは有効になっていないので、開発メニューを表示して、「機能拡張を有効にする」をONにする必要があります。
機能拡張はSafari Extensionsの訳。

Extensionを開発するにはSafariの開発メニューの「機能拡張ビルダーを表示」でビルダーを表示して作成します。

_Users_r_izumita_Library_Application-Support_Evernote_data_51783_content_p15815_652abdb61f4812f3c50d0d1758d5b584.jpeg

+ボタンをクリックすると新規機能拡張と機能拡張を追加が表示されます。
新規の方をクリック。Saveパネルが表示されるので名前を付けます。
名前はHelloWorld。
HelloWorld.safariextensionというフォルダが作成される。中にはInfo.plistがあります。

_Users_r_izumita_Library_Application-Support_Evernote_data_51783_content_p15815_fe8fa9f111a7be394bceecff9425d50a.jpeg

デベロッパ証明書が必要なので
http://developer.apple.com/safari/certificates/index.action
から取得。取得するにはSafari Developer ProgramにJoinが必要(無料)。

_Users_r_izumita_Library_Application-Support_Evernote_data_51783_content_p15815_df47871c4f12e292fe50d8a1d9596c4f.jpeg

_Users_r_izumita_Library_Application-Support_Evernote_data_51783_content_p15815_13a8dfc0795525f875cf349f5687f4e3.jpeg

キーチェーンアクセスアプリを起動。
キーチェーンアクセスメニューの証明書アシスタントの「認証局に証明書を要求…」を実行。
ユーザのメールアドレスと通称に自分の情報を記述。要求の処理をディスクに保存に設定。
証明書を保存。

Safariに戻ってContinue。

_Users_r_izumita_Library_Application-Support_Evernote_data_51783_content_p15815_ea00616e41fbda0068ba5c7d95582756.jpeg

証明書を選択してGenerate。

_Users_r_izumita_Library_Application-Support_Evernote_data_51783_content_p15815_78f2ce0fda59d80ebbfbb40af0cef4ac.jpeg

_Users_r_izumita_Library_Application-Support_Evernote_data_51783_content_p15815_646c3909e7bd69b7e8d78074b66a2da3.png

ダウンロードしてキーチェーンに追加。
無事Safari Developerになりました。

_Users_r_izumita_Library_Application-Support_Evernote_data_51783_content_p15815_ec4cf493986b97b884218a815baf7192.png

ではではHelloWorldを表示するために…
HelloWorld.safariextensionフォルダにhelloworld.htmlファイルを作成。内容は以下。

<!DOCTYPE html>
<html>
<head>
<title>Hello World</title>
</head>
<body>Hello World!</body>
</html>

Extensionsバーにボタンを表示作成するために、Extensions Builderの新規バーをクリック。

_Users_r_izumita_Library_Application-Support_Evernote_data_51783_content_p15815_00f56d9d58d3bcc16005eacfd1e0b465.jpeg

こんなかんじで入力。
Extensions Buildreのインストールボタンをクリック。すると…

_Users_r_izumita_Library_Application-Support_Evernote_data_51783_content_p15815_c1344b6e0cb1760318f20488381b3a70.jpeg

Extensionバーが表示されてHello World!があります。
このHello World!という表示はhelloworld.htmlのBodyが表示されています。ので、BodyをHello Safari Extension Bar!に変更してExtensions Builderの再度読み込むをクリックすると

_Users_r_izumita_Library_Application-Support_Evernote_data_51783_content_p15815_f7778bfaf542985ba91466f36ef81bf0.jpeg

になりました。

どんなExtensionが現れるかたのしみです。
自分は何つくろうかな。

Snow LeopardにMacPortsでApache+MySQL+PHPのWebアプリケーション開発環境を作成する

MacPortsでApache+MySQL+PHPをインストールしてWebアプリケーション開発環境を作成します。

Mac OS X 10.6 Snow LeopardにMacPortsとDeveloperToolsをインストールしていることを前提にしています。

Apacheのインストールと設定

MacPortsでApacheをインストールします。

$ sudo port install apache2


インストールの最後に以下の文が表示されます。

###########################################################
# A startup item has been generated that will aid in
# starting apache2 with launchd. It is disabled
# by default. Execute the following command to start it,
# and to cause it to launch at startup:
#
# sudo launchctl load -w /Library/LaunchDaemons/org.macports.apache2.plist
###########################################################

Apacheはlaunchdで開始するようになっています。デフォルトでは無効になっているので、sudo launchctl load -w /Library/LaunchDaemons/org.macports.apache2.plistを実行して有効にします。有効にするとApacheが起動します。また、OSを再起動してもApacheが自動的に起動されるようになります。

Apacheの動作確認のため、ブラウザでlocalhostもしくはシステム環境設定の共有で指定したコンピュータ名.localにアクセスします。正しく動作していれば「It works!」と表示されます。

開発環境なので、Apacheのドキュメントルートを~/Sitesフォルダに、実行ユーザを自分のユーザアカウントに変更します(ルートを変更せずに#Include conf/extra/httpd-userdir.confの#を削って有効にすれば、ユーザごとにSitesフォルダが使用できるようになります。)。さらに、機能や設定を自由に変更できるようにします。

/opt/local/apache2/conf/httpd.confファイルの
User wwwをUser yourusername
DocumentRoot “/opt/local/apache2/htdocs”をDocumentRoot “/Users/yourusername/Sites”

<Directory “/opt/local/apache2/htdocs”>を<Directory “/Users/yourusername/Sites”>

(上記Directory内の)Options Indexes FollowSymLinksをOptions All、AllowOverride NoneをAllowOverride All

に変更します。

これらの変更を以下のコマンドでApacheに適用します。

$ sudo /opt/local/apache2/bin/apachectl graceful


再度localhostにアクセスすると、/Users/yourusername/Sitesが表示されます。

MySQLのインストールと設定

続いてMySQLをインストールします。

$ sudo port install mysql5-server

起動する前にデータベースの設定をしておきます。my.cnfを設置して編集します。

$ sudo cp /opt/local/share/mysql5/mysql/my-medium.cnf /opt/local/etc/mysql5/my.cnf


[client]
default-character-set=utf8
[mysqld]
datadir=/Users/yourusername/local/var/mysql
language=/opt/local/share/mysql5/mysql/japanese
default-character-set=utf8
skip-character-set-client-handshake
[mysqldump]
default-character-set=utf8
[mysql]
default-character-set=utf8

MacPortsでインストールしたときに以下の文が表示されます。

###########################################################
# A startup item has been generated that will aid in
# starting mysql5-server with launchd. It is disabled
# by default. Execute the following command to start it,
# and to cause it to launch at startup:
#
# sudo launchctl load -w /Library/LaunchDaemons/org.macports.mysql5.plist
###########################################################
******************************************************
* In order to setup the database, you might want to run
* sudo -u mysql mysql_install_db5
* if this is a new install
******************************************************

これまでMySQLをインストールしたことがない場合はsudo -u mysql mysql_install_db5を実行します。sudo launchctl load -w /Library/LaunchDaemons/org.macports.mysql5.plistを実行して起動します。mysql5を実行するかSequel ProでMySQLサーバに接続して、動作しているか確認します。ソケットは/opt/local/var/run/mysql5/mysqld.sockです。

PHPのインストールと設定

PHP5.2系をインストールします。以下の設定が可能です。

$ port variants php52
php52 has the variants:
   apache: Add Apache 1 web server module
     * conflicts with apache2 no_web
[+]apache2: Add Apache 2.2 web server module
     * conflicts with apache no_web
   darwin_10: Platform variant, selected automatically
   dbase: Add dBase file format support
   debug: Enable debug support (useful to analyze a PHP-related core dump)
   fastcgi: Add FastCGI web server binary
     * conflicts with no_web
   gmp: Add GNU MP multiprocessing functions
   imap: Add IMAP protocol support
   ipc: Add semaphore, shared memory and IPC functions
   macosx: Platform variant, selected automatically
   macports_snmp: Add SNMP support using MacPorts SNMP
     * conflicts with snmp
   mssql: Add MS-SQL server support
   mysql4: Add MySQL 4 support
     * conflicts with mysql5
   mysql5: Add MySQL 5 support
     * conflicts with mysql4
   no_web: Don't include any web server support
     * conflicts with apache apache2 fastcgi
   oracle: Add Oracle oci8 database functions with the Oracle Instant Client
   pcntl: Add process control functions
   pear: Add PEAR
   postgresql82: Add postgresql82 support
     * conflicts with postgresql83
   postgresql83: Add postgresql83 support
     * conflicts with postgresql82
   pspell: Add pspell spell-checking functions
   readline: Add GNU readline functions
   snmp: Add SNMP support using Apple SNMP
     * conflicts with macports_snmp
   sockets: Add socket communication functions
   sqlite: Add SQLite support
   suhosin: Add Suhosin patch
   t1lib: Add PostScript Type 1 font support with t1lib
   tidy: Add Tidy support
   universal: Build for multiple architectures


とりあえずApache2 / MySQL / PEARを指定しますが、たとえばSQLiteのサポートが必要であれば+sqliteを指定したり、必要なものを指定します。また、PHP 5.3系を利用する場合はphp52ではなくphp5ポートを利用し、+mysqlではなくphp5-mysqlポートをインストールします。

$ sudo port install php52 +apache2+mysql5+pear
To customize php, copy
/opt/local/etc/php5/php.ini-dist (if this is a development server) or
/opt/local/etc/php5/php.ini-recommended (if this is a production server) to
/opt/local/etc/php5/php.ini and then make changes.
 
If this is your first install, you need to activate PHP in your web server.
 
To enable PHP in Apache, run
  cd /opt/local/apache2/modules
  /opt/local/apache2/bin/apxs -a -e -n "php5" libphp5.so

指示通りにPHP設定ファイルを配置して、PHPをApacheに組み込みます。

$ sudo cp /opt/local/etc/php5/php.ini-dist /opt/local/etc/php5/php.ini
$ sudo /opt/local/apache2/bin/apxs -a -e -n "php5" libphp5.so

/opt/local/apache/conf/httpd.confを編集します。
DirectoryIndexにindex.phpを追加します。

<IfModule dir_module>
    DirectoryIndex index.html index.php
</IfModule>


httpd.confの最後に以下を追加します。

Include conf/extra/mod_php.conf

Apacheに設定を反映させます。

$ sudo /opt/local/apache2/bin/apachectl graceful

PHPの動作をテストするために、~/Sites/info.phpファイルを以下の内容で作成します。

<?php phpinfo(); ?>


http://localhost/info.phpにアクセスして、PHPの動作を確認します。PHPの情報が表示されればインストール成功です。

Cucumber+WebratでPHPアプリのテストをする

sudo gem install cucumber mechanize rspec webratPHP – cucumber – GitHub

を参考にCucumberでPHPアプリケーションの受け入れテストを作成・実行してみます。さらにこのチュートリアルでは日本語でテストを記述してみます。

Cucumberのインストール

Ruby 1.9.1をすでにインストールしてある環境です。まずはCucumber・Webrat・mechanize・rspecをインストールします。

$ sudo sudo gem install cucumber mechanize rspec webrat

ディレクトリの作成

テストするPHPアプリのトップディレクトリに以下のディレクトリを作成します。

features
features/support
features/step_definitions

環境設定ファイルの作成

features/support/env.rbを以下の内容で作成します。RSpecやWebratを読み込んで、各種設定を行っています。Seleniumを利用するといった場合はこのファイルを編集して設定を変更します。

# RSpec
require 'spec/expectations'
 
# Webrat
require 'webrat'
 
require 'test/unit/assertions'
World(Test::Unit::Assertions)
 
Webrat.configure do |config|
  config.mode = :mechanize
end
 
World do
  session = Webrat::Session.new
  session.extend(Webrat::Methods)
  session.extend(Webrat::Matchers)
  session
end

Cucumberのコマンドライン引数の設定

Cucumberのコマンドライン引数のデフォルトを設定するために以下の内容でルートディレクトリにcucumber.ymlを作成します。–language jaを指定することで日本語でテストを記述できるようになります。以下のdefaultではコマンドラインに結果が出力されますが、結果をHTMLで出力する設定を記述しておき、テスト実行時に適宜設定を指定することができます。

default: --language ja features

ステップの作成

features/step_definitionsディレクトリにwebrat_steps.rbとresult_steps.rbの2つのテストステップファイルを作成します。

features/step_definitions/webrat_steps.rb
Webratでページにアクセスしたりボタンをクリックしたりできるステップを作成します。

# -*- encoding: UTF-8 -*-
 
前提 /^(.*)ページを表示している$/ do |path|
  @response = visit "http://localhost/php_with_cucumber#{path}"
end
 
もし /^(.*)ページを表示する$/ do |path|
  @response = visit "http://localhost/php_with_cucumber#{path}"
end
 
もし /^"(.*)"ボタンをクリックする$/ do |button|
  @response = click_button(button)
end
 
もし /^"(.*)"リンクをクリックする$/ do |link|
  @response = click_link(link)
end
 
もし /^"(.*)""(.*)"と入力する$/ do |field, value|
  @response = fill_in(field, :with => value) 
end
 
もし /^"(.*)"から"(.*)"を選択する$/ do |field, value|
  @response = select(value, :from => field) 
end
 
もし /^"(.*)"をチェックする$/ do |field|
  @response = check(field) 
end
 
もし /^"(.*)"のチェックを外す$/ do |field|
  @response = uncheck(field) 
end
 
もし /^"(.*)"を選択する$/ do |field|
  @response = choose(field)
end
 
もし /^パスが"(.*)"のファイルを"(.*)"に添付する $/ do |path, field|
  @response = attach_file(field, path)
end

ページの表示ではhttp://localhost/php_with_cucumberを直に書き込んで、テストを記述するfeatureファイルではアドレスを省略できるようにしています。

features/step_definitions/result_steps.rb
結果の検査を行うステップを作成します。

# -*- encoding: UTF-8 -*-
 
ならば /^"(.*)"と表示される$/ do |text|
  response_body.to_s.force_encoding("UTF-8").should =~ /#{text}/m
end
 
ならば /^"(.*)"と表示されない$/ do |text|
  response_body.to_s.force_encoding("UTF-8").should_not =~ /#{text}/m
end
 
ならば /^(\w+)メッセージが表示さる$/ do |message_type|
  @response.should have_xpath("//*[@class='#{message_type}']")
end
 
ならば /^(.*)リクエストが失敗する/ do |_|
  @response.should_not be_successful
end
 
ならば /ページ読み込みが成功する/ do
  @response.code.should == "200"
end

今回のテストでは使用しないステップも含まれています。実際に動作するか検証してませんが、こんな感じで作成するということで参考にしてください。

テスト対象のコード

実際にはテストを記述してコードを作成していくかもしれませんが、今回はすでに作成した以下のPHPコードをindex.phpとして保存してテストしてみます。機能はテキストエリアにテキストを入力してSubmitをクリックすると、入力したテキストが表示されるというものです。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja">
 
<head>
    <title>PHP with Cucumber</title>
</head>
<body>
    <h1>PHP with Cucumber</h1>
 
    <?php if(!isset($_GET['text']) or empty($_GET['text'])) print '<p>テキストを入力してください。</p>'; ?>
 
    <form action="/php_with_cucumber/index.php" method="get">
        <p><textarea name="text" rows="10" cols="100"></textarea></p>
        <p><input type="submit" value="Submit" /></p>
    </form>
 
    <p class="printed_text"><?php if(isset($_GET['text'])) print $_GET['text']; ?></p>
 
    <p><a href="./">リセット</a></p>
</body>
</html>

テストの作成

テストを作成します。featuresディレクトリにshow.featureファイルを作成します。ページの表示・結果の表示・表示のリセットをテストします。

機能: トップページの表示とサブミット
 
  シナリオ: トップページの表示
    もし /ページを表示する
    ならば ""と表示される
    かつ "テキストを入力してください。"と表示される
 
  シナリオ: テキストの入力と結果の表示してリセットする
    前提 /ページを表示している
    もし "text"に"テスト用のテキストです。"と入力する
    かつ "submit"ボタンをクリックする
    ならば "テスト用のテキストです。"と表示される
    かつ "テキストを入力してください。"と表示されない
    もし "リセット"リンクをクリックする
    ならば "テキストを入力してください。"と表示される

もっと機能があれば機能ごとにファイルを分割します。またこの例では、テスト内容は不十分ですので、ステップやシナリオを追加して試してみてください。

テストの実行

ではテストを実行してみます。コマンドラインでルートディレクトリに移動してcucumberを実行します。

$ cucumber
Using the default profile...
機能: トップページの表示とサブミット
 
  シナリオ: トップページの表示                                # features/show.feature:3
    もし /ページを表示する                                 # features/step_definitions/webrat_steps.rb:7
    ならば ""と表示される # features/step_definitions/result_steps.rb:3
    かつ "テキストを入力してください。"と表示される                    # features/step_definitions/result_steps.rb:3
 
  シナリオ: テキストの入力と結果の表示してリセットする     # features/show.feature:8
    前提 /ページを表示している                # features/step_definitions/webrat_steps.rb:3
    もし "text"に"テスト用のテキストです。"と入力する # features/step_definitions/webrat_steps.rb:19
    かつ "submit"ボタンをクリックする         # features/step_definitions/webrat_steps.rb:11
    ならば "テスト用のテキストです。"と表示される      # features/step_definitions/result_steps.rb:3
    かつ "テキストを入力してください。"と表示されない    # features/step_definitions/result_steps.rb:7
    もし "リセット"リンクをクリックする           # features/step_definitions/webrat_steps.rb:15
    ならば ページ読み込みが成功する              # features/step_definitions/result_steps.rb:19
    かつ "テキストを入力してください。"と表示される     # features/step_definitions/result_steps.rb:3
 
2 scenarios (2 passed)
11 steps (11 passed)
0m0.029s

すべてのシナリオがパスしました。ここで、たとえば「ならば “テスト用のテキストです。”と表示される」を「ならば “テストのテキストです。”と表示される」に変更してCucumberを実行してみると、11 steps (1 failed, 4 skipped, 6 passed)となります。コマンドラインではカラーで表示されるので、真っ赤な画面だったらfailedで、緑だったらpassedです。

おまけ

作成したファイルの書庫: php_with_cucumber.zip

最終的なディレクトリ・ファイル構成

.
|-- cucumber.yml
|-- features
|   |-- show.feature
|   |-- step_definitions
|   |   |-- result_steps.rb
|   |   `-- webrat_steps.rb
|   `-- support
|       `-- env.rb
`-- index.php