2014/08/24

OpenRestyとLapisでSupervisorctlを実行できるシンプルなウェブアプリ作成

周りのマークアップエンジニアがgit使いこなしていて、平気でブランチを切り替えたりして開発サーバー上で作業して貰っています。ただし、ブランチ切り変えになると、たまにアプリリスタートも必要で、マークアップじゃ一人で作業したいブランチを開発サーバーに反映できない場合があります。

Plack::Loader::Shotgunを使ってアプリさえ実行すれば、こういった問題をよりやすく解決できる気もしますが、一応、Nginx OpenRestyを使ってみたかったので、試しにウェブインタフェースを使ってプロセスをリスタートできるアプリを作っちゃいました。

*OpenRestyについてはOpenRestyの公式ページを参考にしてください

**フレームワークはLapisというOpenResty上で動くLuaのウェブフレームワークです。おしゃんてぃでおすすめです。

最近process管理のため主にSupervisordを使っています。周りの人がそれをrootを使って実行しガチなんですが、やっぱり開発環境といってもウェブのユーザがrootのプロセスが実行できたら怖いですし、nginx自体も普段nobodyユーザによって実行されるので、まずSupervisordをnobodyユーザとして実行しました。 そこでnobodyがアクセスできるディレクトリを作って、以下のlapis app.luaを作成しました:
--app.lua
local lapis = require("lapis")
local app_helpers = require("lapis.application")
local validate = require("lapis.validate")
local cjson = require("cjson")

local capture_errors = app_helpers.capture_errors

local app = lapis.Application()
app:enable("etlua")
app.layout = false

validate.validate_functions.alphanumeric = function(input)
     return string.match(input, "^[%w_%-]+$"), "must be alphanumeric"
end

-- たたいたコマンドのSTDOUTパージング
function app:parse_status(line)
    local parts = {}
    for word in line:gmatch("%S+") do table.insert(parts, word) end

    local status = {
        ["name"] = parts[1],
        ["status"] = parts[2],
    }
    if status["status"] == 'RUNNING' then
        status["pid"] = string.gsub(parts[4], ",", "")
        status["uptime"] = parts[#parts]
    elseif status["status"] == 'STOPPED' then
        status["uptime"] = string.format("%s %s %s", parts[3], parts[4], parts[5])
    end

    return status
end

-- トップページにstatusの結果をテーブルで表示したいので、結果をselfにいれるとテンプレートで使えるようになる
app:get("/", function(self)
    local handle = io.popen("/usr/bin/supervisorctl status" .. " 2>&1")

    self.supervisor_status = {}
    for line in handle:lines() do
        table.insert(
            self.supervisor_status,
            self.app:parse_status(line)
        )
    end

    handle:close()

    return { render = 'index' }
end)

-- 最低限のvalidationとリスタートをかける処理
app:post("/:app_name/restart/", capture_errors(function(self)
    validate.assert_valid(self.params, {
        { "app_name", exists = true, alphanumeric = true }
    })

    local app_name = self.params.app_name
    local handle = io.popen("/usr/bin/supervisorctl restart " .. app_name .. " 2>&1")

    self.message = {}
    for line in handle:lines() do
        table.insert(self.message, line)
    end

    return cjson.encode(self.message)
end))

return app
.

出来上がったものはこんな感じ:

2014/07/27

Amazon SNSを使ってSESメールのホワイトリスト管理

ご無沙汰です。

メールマガジンって色々大変ですよね?私がその大変さを実感したのが最近ばかりのことですが、今日はAmazonのSimple Notification Serviceを使ってホワイトリストの管理を簡単にできる方法を紹介させてもらいたいと思います。

SESを使ってメールを送信した時に、ユーザのメールアドレスが存在しなかったため送信完了できなかった場合(Bounce)や、ユーザが「迷惑メール」のボタンを押した(Complaint)場合、Amazonからエラーの詳細が書いてあるメールを送ってもらうのがデフォルトの設定かと思いますが、それ以外にもSNSの通信を送ってもらうこともできます。

そしてSNSにはHTTPSのインタフェースもあるので、簡単なAPIを立ち上げて、BounceとComplaintを自動的にブラックリスト化をすることが意外と簡単です。

SNS Topicの作成の仕方や送信先(endpoint)の認証の仕方が丁寧にドキュメントに書いてありますが、BounceとComplaintの場合どんなメッセージが書いてくるかというと、こんな感じです:

POST / HTTP/1.1
x-amz-sns-message-type: Notification
x-amz-sns-message-id: 22b80b92-fdea-4c2c-8f9d-bdfb0c7bf324
x-amz-sns-topic-arn: arn:aws:sns:us-east-1:123456789012:MyTopic
x-amz-sns-subscription-arn: arn:aws:sns:us-east-1:123456789012:MyTopic:c9135db0-26c4-47ec-8998-413945fb5a96
Content-Length: 773
Content-Type: text/plain; charset=UTF-8
Host: example.com
Connection: Keep-Alive
User-Agent: Amazon Simple Notification Service Agent

{
  "Type" : "Notification",
  "MessageId" : "22b80b92-fdea-4c2c-8f9d-bdfb0c7bf324",
  "TopicArn" : "arn:aws:sns:us-east-1:123456789012:MyTopic",
  "Message" : "{\\"notificationType\\":\\"Bounce\\",\\"bounce\\":{\\"bounceSubType\\":\\"General\\",\\"bounceType\\":\\"Permanent\\",\\"reportingMTA\\":\\"dsn; a8-41.smtp-out.amazonses.com\\",   \\"bouncedRecipients\\":[{\\"status\\":\\"5.1.1\\",\\"action\\":\\"failed\\",\\"diagnosticCode\\":\\"smtp; 554 5.1.1 <recipient@example.com>: Recipient address rejected: User unknown\\",      \\"emailAddress\\":\\"recipient@example.com\\"}],\\"timestamp\\":\\"2014-07-27T09:39:22.070Z\\",\\"feedbackId\\":\\"00000147773054e9-96ae8a22-c833-4967-874c-d56accc9fd2d-000000\\"},\\"mail\\":{\\"timestamp\\":\\"2014-07-27T09:39:18.000Z\\",\\"source\\":\\"noreply@example.come\\",\\"messageId\\":\\"0000014777304606-b5a16bd1-5a20-40ef-993b-0395f14de101-000000\\",\\"destination\\":         [\\"recipient@example.com\\"]}}",
  "Timestamp" : "2012-05-02T00:54:06.655Z",
  "SignatureVersion" : "1",
  "Signature" : "EXAMPLEw6JRNwm1LFQL4ICB0bnXrdB8ClRMTQFGBqwLpGbM78tJ4etTwC5zU7O3tS6tGpey3ejedNdOJ+1fkIp9F2/LmNVKb5aFlYq+9rk9ZiPph5YlLmWsDcyC5T+Sy9/umic5S0UQc2PEtgdpVBahwNOdMW4JPwk0kAJJztnc=",
  "SigningCertURL" : "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem",
  "UnsubscribeURL" : "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:123456789012:MyTopic:c9135db0-26c4-47ec-8998-413945fb5a96"
  }

リクエストbodyのJSONなのですが、JSONなのにContent-Typeが「text/plain」のでご注意ください。そして、今回の話題のブラックリストに入れたいメールアドレスがMessageの項目に入っています。エスケープされているJSONなので、これもデコード必要ですね。

私はPlack::Requestからrawのリクエストbodyを取得して、デコードしてみました。


if ($request->headers->header('x-amz-sns-message-type') eq "Notification") {
        my $raw_body = $request->raw_body;
        $raw_body =~ s/\\\\/\\/g;

        my $decoder = JSON::XS->new->utf8;
        my $json = $decoder->decode($raw_body) or die "could not decode json";
        my $message = $decoder->decode($json->{Message}) or die "could not decode json";

        my @addresses;
        if ($message->{notificationType} eq "Bounce") {
            my $bounced_recipients = $message->{bounce}->{bouncedRecipients};
            @addresses = map { $_->{emailAddress} } @$bounced_recipients;

        } elsif ($message->{notificationType} eq "Complaint") {
            my $complaining_recipients = $message->{complaint}->{complainedRecipients};
            @addresses = map { $_->{emailAddress} } @$complaining_recipients;

        }

        for my $address (@addresses) {
            #対象メールアドレスをブラックリストに追加
        }
}


意外と簡単でした!これでばっちり!

2013/07/11

VimからCorona SDKのシミュレーターを起動する方法

最近アプリ作りたくて、フレームワーク色々いじってきたところです。JavaやC++とかを使うとちゃんとしたIDEがないと困るかと思いますが、Eclipseって使うとなんかだるいですよね。。。 まぁ、とにかく気軽にスクリプト言語でかけるフレームワークを探しているところCorona SDKを見つけました。Lua全くわかんないというのがありましたが、どっちかというとC++を習うほど大変じゃないですし、なんとvimでの開発環境を構築するのが簡単すぎて、ほぼ一目惚れでした。

SDKをインストールができたら、以下をvimrcに張っておくぐらいで十分:


map  :!/Applications/CoronaSDK/Corona\ Terminal -project %:p -skin iPhone<CR>


*Mac上のCorona Terminalのデファルトパスの場合です

main.luaがvimで開いた時、を押すことによってコロナのiPhoneシミュレーターを起動できるので、とても便利です。

2013/05/29

[技術メモ] CentOS 5.8でOpenSSLを更新

先月、hachioji.pmでinstagramのAPIをたたいて画像を表示する機能を作ったのですが、ローカルで問題なく動かすことができたのに、レンタルサーバーにアップしたら、IO::Socket::SSL

Client side SNI not supported for this openssl

ってエラで動かなくて、びっくりしました。(汗)
具体的になにをインストールしたらいいかわからなくて、そこでちょっと挫折して、直すのに大分さぼっちゃいましたが、次回のため、一応どう対応したかをメモっときましょう。

僕が使ってるCentOS 5.8にデファルトで入ってるOpenSSLのバージョンが0.9.8ですが、IO::Socket::SSLのドキュメントによるとOpenSSL 1.0以上が必要そうです:

Support for SNI on the client side was added somewhere in the OpenSSL 0.9.8 series, but only with 1.0 a bug was fixed when the server could not decide about its hostname. Therefore client side SNI is only supported with OpenSSL 1.0 or higher in IO::Socket::SSL.

OpenSSLを更新するにはAxivoのRepository Packageをいれるとyum installでできます。

rpm -ivh --nosignature http://rpm.axivo.com/redhat/axivo-release-5-1.noarch.rpm


それができたら、インストールするのにopenssl.x86_64が必要なので、それが入ってること確認できたら、以下のコマンドで更新:

yum remove openssl.i686
yum --enablerepo=axivo update openssl


詳しくはこちら

2013/04/21

英語圏の知らない英語、第1回:justの使い方

このブログなんですが、日本語で書き続けるべきか、母語の英語で書けばいいのか、色々検討していたのですが、やっぱり日本語で書きたいと決めました。確かに英語で書けば自分の言いたいことをもっとはっきり言えるはずですが、日本語で書くと「わかりづらい」とか「面白くない」とかを理由にして英語に切り替えれば、諦めたということになると思ったからです。
最近、Google I/Oのカンファレンスのビデオを見てて「The Myth of the Genius Programmer」というtalkを見ました。その中で「人は周りに評価されることで上手になるから、どんなに下手でも早めに自分のコードをgithubなど人が見えるところに投稿するといい。」というメッセージが載っています。
このブログってまさにそのためのもので、初心者の自分のコードを人に見せることで恥ずかしくても、とにかくプライドを捨てて投稿しています。
そして人間の言語も似てるものです。話せば話す程上手くなるので、間違いを気にせず、だんだんコミュニケーションをとればいいですよね。
でもその中で、もう一つの問題が出てしまいます。誰もこのブログ読んでませんw そのためにもうちょっとブログの範囲を広げられればいいと思って、英語や言語取得などについても少し書いてみようと思います。東京に住んでいる外国人として、和製英語で誤解が発生しているのを何回も経験したことあるので、和製英語と英語圏の使ってる英語の使い分けについてちょっと説明させていただきたいと思います。

さて、今日のword of the dayとして「just」を話題にしたいと思います。
昨日街歩いていたら、床屋の看板に「just cut 10 min」を見かけました。この文章は日本人にどうとられるか確実にわかりませんが、日本語ではジャストっていうは「丁度いい」という意味で使われているので、これはおそらく「10分でお似合いの髪型にしてあげます」的なことを伝えたかったでしょうと思います。ただ、英語の上記の文章ですと英語圏の人なら「カットのみ 10分」に取られます。なぜならば、英語では「just」というのは「ただの〜・〜だけ・〜に過ぎない」という意味になります。一般的にネガティブな意味になっているので、そんな看板をみて「その床屋さんでシャンプーはしてくれないのかな」とか思う人も出てしまうでしょう。
英語で上記の文章を書き直せば、「the perfect cut in under ten minutes」が正解ですかね。
確かに英語で「just right」「just perfect」という言い方もあります。こちらはネガティブではなくて、日本語のジャストに近い意味を持っています。おそらく日本語のジャストの由来がこのjust right(丁度いい)ですが、「いいことだらけで悪いことがまざっていない」という意味でjust(だけ)が使えるわけだと思います。
おまけによく見られる和製英語を英語圏に通じそうな英語に翻訳してみました:

和製英語 英語
ジャストカット perfect haircut
ジャストフィット close fitting / snug
ジャストタイミング perfect timing
9時ジャスト exactly 9 o'clock

※実は形容動詞のjustが「だけ」意外の意味がもう一つあります。それは「ぎりぎり」って意味です。例えば、「just in time」(ぎりぎり間に合った)、「just married」(結婚したばかり)とか様々な表現に出たりします。

2013/03/18

TDD Bootcamp Tokyo 2013

16日にTDD Boot Camp Tokyoに参加してきました。
PHPer、Rubyist、Javaer(? ...Javaやってる人って言い方なんかありますか) はやっぱり多かったですが、4番目のテーブルでC#、Scala、Objective-Cなど様々な言語も出てきました。

僕は今職場のプロジェクトでPHPを使っていますので、PHPのテストフレームワークでも勉強しようかなと思っていましたが、やっぱり終末になるとPHPは書きたくないです!
日本語のドキュメントがないせいかわからないけど、そもそも僕が使おうと思っていたPHPフレームワーク(Testify.php)があまり人気がないようです・・・

というわけで、Perlのイベントで知り合ったハッカーとペアプロをして、Test::Moreを使って参加しました。日本語キーボードに慣れてしまった僕とVimを普段利用しない彼という事情でちょっと辛かったのもありましたが、楽しかったです。
機能を実装する前にまずテストを書くという手段がなかなか面白いし、意外とやりやすかったです。

そして最後にじゃんけんで買ってGithubのTシャーツを貰いました。^^


2012/12/02

シンプルなAndroid RSSリーダーを作ってみました

もう12月ですね。ということはアドベントカレンダーの季節になりました♪

子供の時、アドベントカレンダー(チョコレートが入っているほう)でずいぶん楽しんでましたので、大人になった自分もこの季節を楽しめるようにPerl Advent Calendar(http://www.perladvent.org)のRSSリーダーを作成してみました。

RSSのXMLの構成はこんな構成

<entry>
  <title>タイトル</title>
  <id>URL</id>
  <summary>要約</summary>
  <updated>更新時間</updated>
</entry>

なので、以下のようなPOJOを作成しました。


public class Entry implements Serializable {   //後でIntentのBundleに入れるようにSerializableを実装
 
 private String title;
 private String link;
 private String summary;
 private String updated;
 
 public void setTitle(String title) {
  this.title = title;
 }
 
 public String getTitle() {
  return this.title;
 }
 
 //...長いので、linkとsummaryのゲッターとセッターを略します
 
 public void setUpdated(String updated) {
  String dateOnly = updated.substring(0, 10); 
  this.updated = dateOnly;
 }
 
 public String getUpdated() {
  return this.updated;
 }
}


SAXパーサーを使うことにしました。ハンドラークラスを使ってstartElementメソッドで適当なタグ名を見つけたらフラグを立って、中身のテキストを全部引っ張ってきて、最後にendElementメソッドでフラグを消すという訳ですが、なぜかSAXの仕様でcharactersのメソッドが何回も呼ばれることもあるらしくて、たまにテキストが2・3回読み込まれてて、困りました・・・



長い分を読み込むためにcharactersを複数回呼ぶことが必要と思うので、あまりいい解決方法ではないかと思いますけど、文字を追加してくれるbuilder.appendを呼ぶ直前にbuilder.setLength(0)でリセットすることでとりあえず解決できました・・・


builder.setLength(0);
builder.append(ch, start, length);



奇麗に表示できました。



ハンドラークラスはこんな感じになっています:


public class RSSHandler extends DefaultHandler {
 private ArrayList entries;
 private Entry currentEntry;
 private StringBuilder builder;
 
 boolean inTitle;
 boolean inLink;
 boolean inSummary;
 boolean inUpdated;
 boolean inEntry;
 
 public ArrayList getEntries() {
  return this.entries;
 }
 
 @Override
 public void startDocument() throws SAXException {
  super.startDocument();
  entries = new ArrayList();
  builder = new StringBuilder();
 }

 @Override
 public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
  super.startElement(uri, localName, name, attributes);
  if (localName.equalsIgnoreCase(ENTRY)) {
   this.currentEntry = new Entry();
   inEntry = true;
  }
  if (localName.equalsIgnoreCase(TITLE)) {
   inTitle = true;
  }
  if (localName.equalsIgnoreCase(LINK)) {
   inLink = true;
  }
  if (localName.equalsIgnoreCase(SUMMARY)) {
   inSummary = true;
  }
  if (localName.equalsIgnoreCase(UPDATED)) {
   inUpdated = true;
  }
 }
 
 @Override
 public void characters(char[] ch, int start, int length) throws SAXException {
  super.characters(ch, start, length);
  
  if (inEntry) {
   if (inTitle) {
    builder.setLength(0);
    builder.append(ch, start, length);
   }
   if (inLink) {
    builder.setLength(0);
    builder.append(ch, start, length);
   }
   if (inSummary) {
    builder.append(ch, start, length);
   }
   if (inUpdated) {
    builder.setLength(0);
    builder.append(ch, start, length);
   }
  }
 }
 
 @Override
 public void endElement(String uri, String localName, String name) throws SAXException {
  super.endElement(uri, localName, name);
  if (this.currentEntry != null) {
   if (localName.equalsIgnoreCase(TITLE)) {
    currentEntry.setTitle(builder.toString());
    inTitle = false;
   } else if (localName.equalsIgnoreCase(LINK)) {
    currentEntry.setLink(builder.toString());
    inLink = false;
   } else if (localName.equalsIgnoreCase(SUMMARY)) {
    currentEntry.setSummary(builder.toString());
    inSummary = false;
   } else if (localName.equalsIgnoreCase(UPDATED)) {
    currentEntry.setUpdated(builder.toString());
    inUpdated = false;
   } 
    
   if (localName.equalsIgnoreCase(ENTRY)) {
    entries.add(currentEntry);
    inEntry = false;
   }
  }
 }
}


そしてAsyncTaskでSAXを呼ぶことにして、出来上がったArrayListを返すことにしました。


@Override
 protected ArrayList doInBackground(Void... params) {

  try {
   URL url = new URL(xmlLocation);
   SAXParserFactory spf = SAXParserFactory.newInstance();
   SAXParser sp = spf.newSAXParser();
  
   XMLReader xr = sp.getXMLReader();
   RSSHandler handler = new RSSHandler();
   xr.setContentHandler(handler);     //ハンドラークラスを設定
   
   xr.parse(new InputSource(url.openStream()));
   
   entries = handler.getEntries();
  } catch (MalformedURLException e) {
   Log.e("LoadFeedData", "MalformedUrlException: ", e);
  } catch (Exception e) {
   Log.e("LoadFeedData", "Parsing exception", e);
  }
  return entries;
 }


後は、ArrayAdapterに渡して、そこで項目ごとにListViewにデーターを入れるだけです。

ちなみに、詳細はViewはこんな感じに表示してみました。コードはHighlighted Syntaxで読んだほうが楽なので、結局ブラウザーで読みたくて「ブラウザーで表示」みたいなボタンも用意しておきました(汗)



以前にJSONデータを読み込んでListViewで表示するようなアプリは作ったことはあったんですが、XMLは初めてですので、勉強になりました。SAX Parserの仕様をもっと深く勉強する必要があると思いますけど、一応、このRSSリーダーでアドベントカレンダーを読んでおきます。