2016/09/13

Minna no Go Gengo: A Summary / Review in English (chapter 1)

My copy of Minna no Go Gengo, the Golang book with the cover everyone loves, came in the mail today!

Just last week, I was talking to some gophers based in Germany and when I mentioned that a coworker of mine recently published a chapter in a book on Go, one of the guys immediately asked "Is it the one with the gophers and robot on the cover?". Since the book only exists in Japanese I was surprised that he knew about it, but I guess I shouldn't be too suprised. The cover is just too good not to share, am I right?

So I thought i'd write a quick review / summary of each chapter in case there are some gophers out there who are interested in knowing what's in the book. I've just barely begun reading, but as the title, Minna no Go Gengo (Everyone's Golang), suggests, it covers a number of topics for a range of different skill levels from how to get started for absolute beginners to more advanced topics like reflection. Each chapter is written by a different author, all of whom are well-known OSS contributors here in Japan.

The first chapter is the most beginner friendly, but also contains some stellar tips about how to write Go code in a Go-like way.

How to start writing Go code for team development.

Author: Matsuki Masayuki (aka @Songmu)

This chapter starts out with the essentials: how to install Go, an introduction to some of the core command-line tools used in Go development as well as suggestions for some useful third party tools like ghq, peco and glide). It's super concise and does a great job of covering the essentials without being too verbose.

For anyone who's written Go code before, the meat of the chapter, though is in the style guide which highlights some differences between writing programs using scripting languages like Ruby and Perl vs writing in Go. For example:

  • Avoid using regexp: use the strings package wherever possible instead. Why? They can be really slow, sometimes even slower than Perl regexp... which is pretty bad for a pre-compiled program.
  • Avoid maps. Because Go is a strongly typed language it is better to use structs. Also maps are not thread-safe. If you need to use a map alongside concurrency embed one in a struct alongside a sync.RWMutex.
  • Don't overuse concurrency. While Go is great for concurrency overusing it not only makes programs harder to read, but also increases the likelihood of race conditions.
  • Use the -ldflags and -tag options to embed useful information in a binary when using go build.
  • runtime.NumGoRoutine and runtime.ReadMemStats are useful monitoring metrics for web servers and other long running programs. golang-stats-api-handler is a useful library that provides an api interface to the go runtime package.

Hardly an exhaustive list, as this chapter is packed with useful info for people who are transitioning to Go from other languages and gives a good introduction to how to get into a Go mindset. I am looking forward to reading and writing up on the remaining chapters.

2016/08/07

Using OpenResty's access_by_lua and the satisfy any directive

So recently I learned that OpenResty's access_by_lua_block and the satisfy any directive don't play nice together. To be honest I didn't have a very compelling reason to use an access_by_lua_block to begin with. Ideally I would have used a set_by_lua_block, but subrequests using ngx.location.capture are disabled by it since it is a blocking function.

Still I felt a bit conflicted about whether I should be using access_by_lua or rewrite_by_lua, since technically all I really wanted to do was set a variable (to print in the access logs) and am neither authenticating or rewriting. Using either one seems like a hacky workaround.

As it turns out, rewrite_by_lua is the much safer option if you use any authentication directives. Take for example this situation:

  • I need to set a variable using an external microservice and decide to do so with ngx.location.capture
  • It's a private API with complex authentication rules (ie a combination of IP blocking and basic auth)

So something like this:


server {
  set $my_special_variable "0"; # fallback value if no response from microservice
  access_by_lua_block {    
    local res = ngx.location.capture("/microservice")
    if res then
      ngx.var.my_special_variable = res.body
    end
  }

  location ~ /private/api/endpoint {
    satisfy any;
    allow [some ip address];
    deny all;

    auth basic "unauthorized";
    auth_basic_user_file /etc/nginx/.htpasswd;

    proxy_pass http://my_backend_server;
  }
}
.

Unfortunately the access_by_lua_block counts as a satisfied condition for the satisfy any; directive and suddenly my API is opened up to the world: neither the correct IP or basic auth is required to access it anymore. Everyone can access it. Huge security risk to say the least.

So it still feels bit hacky since the subrequest to the microservice isn't involved doing any uri rewriting to speak of, but rewrite_by_lua_block is definitely the better option in this situation. Glad I caught this technicality early on.

2016/07/31

Testing Out Google's Natural Language API

Since today is the last day of the Google Natural Language API's free public beta I thought I'd give it a little spin. One of the applications for the API listed on google's promotional page was analyzing product reviews... which reminded me that late last year I made a Slack webhook that extracts Google Play and App Store reviews and happen to still have a stockpile of those laying around in a database of mine, so what better sample data to use for this experiment?

Apple and Google provide rating data (1 to 5 stars) so I know what percentages of the users gave unfavourable reviews, but it would be a good idea to try to narrow down some of the things users are complaining about. Perhaps that's something we can tackle with this API? Let's try it.

My sample data is stored in a database with the following structure:


sqlite> .schema
CREATE TABLE review (
  id INT(11) NOT NULL,
  title VARCHAR(255) NULL,
  content TEXT,
  rating TINYINT(3) NOT NULL DEFAULT 0,
  device_type TINYINT(3) NOT NULL,
  device_name VARCHAR(255) NULL,
  author_name VARCHAR(255) NULL,
  author_uri VARCHAR(255) NULL,
  created DATETIME NULL,
  updated DATETIME NOT NULL,
  acquired DATETIME NOT NULL,
  PRIMARY KEY (id, device_type)
);
CREATE INDEX updated_idx on review(updated);
CREATE INDEX rating_idx on review(rating);
.

At this point I don't really care about platform (although I certainly could further break down my user samples by device type if I was so inclined), so I'm just going to collect the comments from users who gave a distinctly bad rating (of 1 or 2 stars) to feed to the API with a simple query like this:


SELECT content FROM review WHERE rating < 3;
.

If I throw the main body of the review text at Google's API and see if it'll come up with some salient keywords (and how often they are brought up) perhaps it'll give us a better clue what it is the users are complaining about.

So first we need to authenticate with the API.

Creating a service key file for authentication is straight forward enough, so I'll just link to the documentation here and once you have one of those all you need to do is use the gcloud command to authenticate and print an access-token.


$ gcloud auth activate-service-account --key-file=kinmedai-cb03d32572c2.json
Activated service account credentials for: [user@projectname.iam.gserviceaccount.com]
$ gcloud auth print-access-token
[[output omitted]]
.

Now that I'm ready to access the API, I decided to create a script in Go to do the dirty work for me. So the first step is to use the sample json payload and response body data from the getting started docs to generate structs in Go. Writing structs by hand is a pain, so I used JSON to Go to generate the bulk of it and then tweaked it a bit like so:

The request structure:


type EntityRequest struct {
  EncodingType string                `json:"encodingType"`
  Document     EntityRequestDocument `json:"document"`
}

type EntityRequestDocument struct {
  TypeName string `json:"type"`
  Content  string `json:"content"`
  Language string `json:"language"`
}
.

And the response structure:


type EntityResponse struct {
  Entities []DetectedEntity `json:"entities"`
  Language string           `json:"language"`
}

type DetectedEntity struct {
  Name       string          `json:"name"`
  EntityType string          `json:"type"`
  Salience   float64         `json:"salience"`
  Mentions   []EntityMention `json:"mentions"`
  Metadata   struct {
    WikipediaUrl string `json:wikipedia_url"`
  } `json:"metadata"`
}

type EntityMention struct {
  Text struct {
    Content     string `json:"content"`
    BeginOffset string `json:"beginOffset"`
  } `json:"text"`
}
.

Now that I know what kind of data I'll be dealing with I can start building my request.


func createEntityRequests() []*EntityRequest {
  dbh := getDBH()
  rows, err := dbh.Query(`SELECT content FROM review WHERE rating < 3`)
  if err != nil {
    log.Fatal(err)
  }

  var entities []*EntityRequest

  for rows.Next() {
    var comment string
    err = rows.Scan(&comment)
    if err != nil {
      log.Fatal(err)
    }

    // Google Play lets users submit ratings with no comments (stars only ratings) so skip those
    if len(comment) == 0 {
      continue
    }

    entityRequest := &EntityRequest{
      EncodingType: "UTF8",
      Document: EntityRequestDocument{
        TypeName: "PLAIN_TEXT",
        Content:  comment,
        Language: "JA",
      },
    }
    entities = append(entities, entityRequest)
  }

  return entities
}
.

Next I'll need to create a function that posts to the entities analysis API. Again, the quickstart docs summarize this process very clearly, but it's a basic HTTP post request with a json payload and the access token we got from gcloud set in the Authorization header.

I'm planning on passing the token directly from standard input so I can pipe my script with gcloud, but more on that later. First the request:


func postEntity(accessToken string, entityRequest *EntityRequest) []byte {
  jsonEntity, _ := json.Marshal(entityRequest)
  req, err := http.NewRequest("POST", ENTITIES_URL, bytes.NewBuffer(jsonEntity))
  if err != nil {
    log.Fatal(err)
  }
  req.Header.Set("Content-Type", "application/json")
  req.Header.Set("Authorization", "Bearer "+accessToken)

  client := &http.Client{}
  res, err := client.Do(req)
  if err != nil {
    log.Fatal(err)
  }
  defer res.Body.Close()

  body, err := ioutil.ReadAll(res.Body)

  if res.StatusCode != http.StatusOK || err != nil {
    log.Fatal(res.Status)
    log.Fatal(err)
  }

  return body
}
.

Now you might have noticed that this is not the most efficient way to get feedback since I am sending one request per review. At the moment I only have 220 reviews for my test data, so it's not a big deal, but if I was actually planning on using this on any regular kind of basis it could potentially be very expensive (and also slow) to do it this way. Since we don't need to associate any other of the data with the content we could potentially amalgamate several reviews into one body of text and send the data in a batch.

However at this point I'm not even 100% sure this experiment is going to produce any kind of meaningful result, so for the time being I'm going to analyze each review individually. Better to make sure it works before spending time optimizing it, right?

Anyway, let's piece the rest of the script together. I know I'm going to have to pass the script my API access token as well as the path to the db (I'm using sqlite3), so my interface is going to look something like this:


$ gcloud auth print-access-token | go run kinmedai.go -d /path/to/sqlitedbname.db
.

So for my main block I'm going to grab my two parameters via stdin and flags and build the request payloads. Then I'll post each payload to the API and parse any entities from the http response body into the detectedEntities map that'll be used to count how many times a specific term was referenced:


func main() {
  flag.Parse()

  var accessToken string
  fmt.Scan(&accessToken)

  entityRequests := createEntityRequests()
  detectedEntities := make(map[string]int)

  for i := 0; i < len(entityRequests); i++ {
    var entityResponse EntityResponse
    body := postEntity(accessToken, entityRequests[i])
    json.Unmarshal(body, &entityResponse)

    for j := 0; j < len(entityResponse.Entities); j++ {
      entity := entityResponse.Entities[j]
      detectedEntities[entity.Name] = detectedEntities[entity.Name] + 1
    }
  }

  for k, v := range detectedEntities {
    fmt.Printf("%s: %d\n", k, v)
  }
}
.

Yikes! That's a lot of for loops! But as I said I'm still in the experimental phase so I'm not going to worry about how fast this runs just yet.

And the output looks something like this (did I mention I was parsing Japanese reviews?):


Wi-Fi: 2
GOOGLE: 1
TT: 1
DL: 1
2MWXZH4T: 1
アンインストール: 2
ぼく友: 1
ガラポン: 1
2.7.2: 1
GYH7AFSY: 1
某都: 1
Fuck this shit I: 1
掛布: 1
星飛雄馬: 1
ガチャ: 12
ガチャゲー: 1
C8YENNZG: 1
やめた: 3
ゴミゲー: 1
めちゃ運: 1
っ・ω: 1
ゴールド: 1
ガチャ.: 1
間違いない(* ̄ー ̄: 1
WB5Z2JQX: 1
甲子園: 3
平安: 1
パワプロ: 1
.

So it looks like the results are a bit hit and miss. Things like invitation codes, emoji, etc that probably shouldn't be actual keywords are showing up as entities. However it looks like at least 15 (if you count "ガチャ", "ガチャ.", "ガチャゲー" and "ガラポン" as the same result) out of our sample of 220 users are complaining about the gacha system in this particular app.

I can't exactly say this is a groundbreaking discovery or anything. Having read most of the reviews for the app over the last few months (since my webhook delivers new reviews to my team's slack daily) I pretty much knew that many of the complaints hinged around users not getting the drops they wanted, but I guess this helps quantify it a little better?

Either way it looks like Google's entity search is mostly based around pinpointing terms that can be found on Wikipedia... whereas for an app like the one I run it'd be more useful to do a keyword search for game specific terminology if the goal is to pinpoint features the users might be frustrated about.

But now that I've seen what this API is capable of I'm already starting to think of better applications for this technology (like analyzing followers' tweets to see what other games and manga they are talking about).

I skipped over some details (such as connecting to the db and other things that don't really pertain to using the API), but I've uploaded the script to a gist so feel free to reference it in its entirety in case I lost you at any point.

2014/12/25

ペッパー開発体験ワークショップ行ってみた

この間、今年のqiitaのアドベントカレンダーに「Pepper」カレンダーのをみて、「ペッパーってそもそもまだ販売されてないし、買えたとしても滅茶苦茶高いでしょ?」って思って、実際誰がペッパーの開発しているのか見たくてクリックしてみたのですが。なんと秋葉原で無料で開発体験ができるスペースがあること知りました。(今年はもうほぼ終了と思いますが、来年もやるようです)

丁度今仕事休みで暇ですし、無料ですから行ってみました!

ペッパーくんといいツーショット撮ってもらいましたね!


※以下のブログは主に「ペッパー体験ワークショップに興味あるけど、よくわからない」って人向けです。開発方法について特になにも書いてないです。


ワークショップの名前自体に「開発」って単語が入っていますが、実際「基本編」って書いてあるセッションは少なくとも非エンジニアも全然参加できます。Aldebaranさんのソフト(Choreographe)を使って、ドラッグアンドドロップ操作などでペッパーくんのプログラム作れますので、興味ある人はぜひ友達など連れてみてください。
プログラミングがやりたい方はむしろ「上級編」や「ハッカソン」などに参加したほうが良さそうです。

ChoreographeのUIがこういう感じです:

体験スペースではノートパソコンはテーブルごとに用意してあるので、特になにも持っていかなくてもOKなのですが、自分のノートパソコンの持ち込む場合、事前にChoreographeをインストールすることができます。
ダウンロード方法が大変わかりづらいのですが、Aldebaranのコミュニティーサイトでアカウント作ってから、このページに「Software」のタブが現れます。

アカウント作成が面倒な場合、体験スペースにインストーラーが入っているUSBもありますので、早めに行ってUSBからインストールしてもOKだと思います。

公式サイトにワークショップの内容についてあまり書いてないので、ワークショップの内容をざっくり紹介させてください:


【12/15】Pepper開発体験ワークショップ(SDK基本編 #1)

こういったものやりました:

  • ポーズとジェスチャー&timelineを使ってタイミングを合わせる
  • 音声合成機能をつかって喋らせる
  • 発話認識とSwitch Caseを使った反応
  • タッチパネル認識

ダンスのデモも見せてもらいました:


【12/23】Pepper開発体験ワークショップ(SDK基本編 #2)

こういったものやりました:

  • タブレットに画像や動画を表示する
  • Move toやMove around機能で体を動かせる
  • 顔追跡
  • 喋りながらジェスチャーをつける

基本的に時間あまりないので、項目ごとにとても簡単なプログラムを作る余裕しかないですが、まあまあ楽しいです。
最後にペッパーくんにこんなのやってもらいました:

機会あれば、ぜひ皆さんも行ってみてください

2014/12/08

Riot API: 画像のバッチダウンロード

この記事はRiot APIのAdvent Calendar 2014の8日目の記事です。

前回はプロフィールアイコンのurlをCDNから取得するところまでデモしましたが、最新の画像をバッチで一気にダウンロード、使いやすくなるようにファイルをローカルで保存するのをやってみます。


1)チャンピオンのリストを取得

バージョン取得などは前回とほぼ同じので、説明を飛ばします:

#!/usr/bin/env python

import os
import requests
import shutil

DD_URL = 'http://ddragon.leagueoflegends.com'

res = requests.get(DD_URL + '/realms/na.json')
res.raise_for_status()

version = res.json()['n']['champion']

champion_info_url= DD_URL + '/cdn/' + version + '/data/en_US/champion.json'
res = requests.get(champion_info_url)
res.raise_for_status()

data = res.json()['data']
 

ブラウザでチャンピオンjsonデーターを確認するとわかり安いと思いますが、全チャンピオンの名前や基本パラメーターが入っているhash tableを'data'に取得できました。

2)保存先のディレクトリーを用意する

今後ダウンロードをする画像を保存先ディレクトリがあるかどうか確認して、なければつくりましょう。


output_dir = os.path.dirname(os.path.realpath(__file__)) + '/champion'

try:
    os.stat(output_dir)
except:
    os.mkdir(output_dir)
 

3)キャラクターをループで回して、画像を保存しておく

DataDragonではチャンピオンの名前をファイル名として使われています:
http://ddragon.leagueoflegends.com/cdn/4.20.1/img/champion/Aatrox.png

チャンピオンの名前わかれば取得しやすいでしょうけど、たとえば、Riot APIで最近のゲーム履歴(/game/by-summoner/SUMMONER_ID/recent)取得した場合、利用したチャンピオンのkey(ID)しか返ってこないので、名前だけだとかなり不便ですね。
そのため、画像をダウンロードしたら、ファイル名をチャンピオンの名前じゃなくて、チャンピオンのkeyをつけましょう。


champion_img_base = DD_URL + "/cdn/" + version + "/img/champion/"

for name in data:
    img_url = champion_img_base + name + ".png"
    filename = data[name]['key'] + ".png"

    res = requests.get(img_url, stream=True)
    if res.status_code == 200:
        with open(output_dir + '/' + filename, 'wb') as f:
            res.raw.decode_content = True
            shutil.copyfileobj(res.raw, f)
 

以上!
ダウンロードできたかどうかを確認すると:


ls champion/
1.png 106.png 114.png 122.png 14.png 161.png 21.png 25.png 28.png 34.png 40.png 45.png 55.png 61.png 7.png 79.png 85.png 96.png
10.png 107.png 115.png 126.png 143.png 17.png 22.png 254.png 29.png 35.png 41.png 48.png 56.png 62.png 72.png 8.png 86.png 98.png
101.png 11.png 117.png 127.png 15.png 18.png 222.png 26.png 3.png 36.png 412.png 5.png 57.png 63.png 74.png 80.png 89.png 99.png
102.png 110.png 119.png 13.png 150.png 19.png 23.png 266.png 30.png 37.png 42.png 50.png 58.png 64.png 75.png 81.png 9.png
103.png 111.png 12.png 131.png 154.png 2.png 236.png 267.png 31.png 38.png 429.png 51.png 59.png 67.png 76.png 82.png 90.png
104.png 112.png 120.png 133.png 157.png 20.png 238.png 268.png 32.png 39.png 43.png 53.png 6.png 68.png 77.png 83.png 91.png
105.png 113.png 121.png 134.png 16.png 201.png 24.png 27.png 33.png 4.png 44.png 54.png 60.png 69.png 78.png 84.png 92.png
  

全部揃ってありますね!これで新しいチャンピオンが追加された場合でもスクリプトを動かすだけですぐに画像をダウンロードできそうですね。

2014/12/06

Riot API: プロフィールアイコンの取得

この記事はRiot APIのAdvent Calendarの6日目の投稿です。

Riot APIで取得できるJSONデータをみるとなんかワクワクして、LOLKingなどMobafireみたいなかっこいいサイト作りたくなりますよね?
でももちろん、ウェブサイトを作った場合に、最新のアイコンなどの画像も必要になりますので、今回はPythonをつかって自分のプロフィールアイコンのurlを取得するところまでデモしたいと思います。

Riot Gamesのアセット・リポジトリはData Dragonというサービスに通じてアクセスできます。
Riot APIと別のチームが管理しているらしいので、アップデート直後にData Dragonへの反映が遅れたり、 データーが一致しない可能性もありますが、それでも最新の画像の取得には一番便利なツールになります。

ではプロフィールアイコンの取得をやりましょう

1) バージョン取得

画像データーはバッチでアップロードされていて、一番最初にダウンロードしたい画像のバージョンを取得する必要があります。 自分のアカウントはNorth Americaサーバーにありますので、naのjsonフィードを使います。

# datadragon.py
import requests    # pip install requests

DD_URL = 'http://ddragon.leagueoflegends.com'

version_path = '/realms/na.json'
res = requests.get(DD_URL + version_path)
res.raise_for_status()      # catch non 2xx status

print(res.text)
  

プリントの結果はこんな感じです:


{"n":{"item":"4.20.1","rune":"4.17.1","mastery":"4.17.1","summoner":"4.20.1","champion":"4.20.1",            "profileicon":"4.20.1","language":"4.20.1"},"v":"4.20.1","l":"en_US","cdn":"http:\/\/ddragon.leagueoflegends.  com\/cdn","dd":"4.17.1","lg":"0.152.55","css":"0.152.55","profileiconmax":28,"store":null}
  

今回はプロフィールアイコンのバージョンが取得したいと思いますので、こういうふうにprofileiconのバージョン番号だけ取得しましょう:


version = res.json()['n']['profileicon']
  

2) 自分のプロフィールアイコンIDを取得

次に自分のプロフィールアイコンIDを取得します。今回はデータードラゴンじゃなくて、通常のRiotAPIを使いましょう。


API_URL = 'https://na.api.pvp.net/api/lol/na'
summoner_by_name_path = "/summoner/by-name/laouji";
profile_url = API_URL + "/v1.4" + summoner_by_name_path + "?api_key=" + API_KEY

res = requests.get(profile_url)
res.raise_for_status()
 

res.textの中身を確認すると:


"laouji":{"id":46048341,"name":"laouji","profileIconId":607,"summonerLevel":23,"revisionDate":1411808085000}}
 

3)画像のURLを組み合わせる

APIコールの結果に必要なprofileIconIdありましたので、画像のurlを組み合わせるのが簡単です:


def icon_url ( version, icon_id ):
    url = DD_URL + '/cdn/' + version + '/img/profileicon/' + str(icon_id) + '.png'
    return url

icon_id = res.json()['laouji']['profileIconId']
icon_url = icon_url(version, icon_id)
 

自分の場合はこれでした:

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
.

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