オープンソースこねこね

Webプログラミングなどについてあれこれ。

IntelliJからMacの辞書(Dictionary.app)を開くプラグインをつくった

Macの辞書アプリはかなり便利で、何かキーワードを選択している状態でCTRL+CMD+Dのショートカットを押すとそのキーワードで辞書を引いてくれる。 主に英語ドキュメントなどにある分からない英単語を調べるのに使うのだけど、メインに使っているエディタのAtomIntelliJ IDEAだとなんでかこのキーバインドが効かなくて残念な思いをしてた。そこでしばらく前にAtomから辞書を引けるようにしたパッケージとして以下を作った。

github.com

そしてここ最近Go言語をIntelliJ IDEAで書くようになったので、こっちでも辞書を引けるようにしたくて、さきほどちょちょいと作ってみた。

github.com

実装にあたってAtom版もIntelliJ版も両方ともすでに存在していたDashのプラグインを参考にした。 これにはエディタの選択領域からテキストを抽出して、外部コマンドとしてDashを起動するというコードある。 私がやったことは、そのあたりを参考元からを切りだしてDictionary.appを叩くように変えただけのもの。よって細かいところなどにはまるで気を使っていない実装なのだけど、個人的に使う分には必要十分に動くのでこれで満足している(^o^)

なお、Atomの方はパッケージの登録までしているのでAtomの設定画面から直接インストールできるが、IntelliJの方はJarをGithubのリリースページにおいただけなので、インストールするにはJarをダウンロードしてローカルファイルからインストールする必要があります。

sshのラッパーコマンドを作った

GoでSSHコマンドに便利機能を追加したコマンドを作りました。

2017/04/11 追記: この記事の内容は古くなっています!

2017/04/11時点の最新の仕様は、次の記事を参照してください

SSHラッパーコマンドEsshのv1.0.0をリリースしました - オープンソースこねこね

追記ここまで。以下は2015/11時点の古い情報となります。

f:id:kohkimakimoto:20151115163938g:plain

github.com

機能として

  • Lua~/.ssh/configに相当する設定を書ける。
  • zshの補完機能を使って、接続先一覧を出す。
  • サーバ接続時にフックを仕込める。自分はスクリーンの色を変えるの使っている。
  • 複数のリモートサーバにまとめてコマンドを実行する。

といったところです。詳細はリポジトリのREADMEを見ていだければと思います。 ビルド済みバイナリをリリースページにおいてあるので、インストールはダウンロードして解凍してパスの通ったディレクトリに配置すればOKです。

~/.ssh/configを上書きするので~/.ssh/configのバックアップを取っておくのをお忘れなく。

https://github.com/kohkimakimoto/zssh/releases/latest

使い方

zsshsshのラッパーコマンドになっているのでsshコマンドと同様に使えます。zsshを実行するとsshコマンドで実際の処理を実行する前に、設定ファイル~/.ssh/zssh.luaを読み込んで~/.ssh/configを生成するしくみになっています。

~/.ssh/zssh.luaに以下のような設定を書いておくと

Host "web01.localhost" {
    ForwardAgent = "yes",
    HostName = "192.168.0.11",
    Port = "22",
    User = "kohkimakimoto",
    -- 小文字で始まる設定は、~/.ssh/configに出力されない。descriptionはzsh補完の説明文に使用される(後述)
    description = "my web01 server",
}

Host "web02.localhost" {
    ForwardAgent = "yes",
    HostName = "192.168.0.12",
    Port = "22",
    User = "kohkimakimoto",
    description = "my web02 server",
}

zssh実行時に次のような~/.ssh/configを自動生成して上書きします。

Host web01.localhost
    ForwardAgent yes
    HostName 192.168.0.11
    Port 22
    User kohkimakimoto

Host web02.localhost
    ForwardAgent yes
    HostName 192.168.0.12
    Port 22
    User kohkimakimoto

これで、sshコマンドと同様に以下のようにしてサーバにSSH接続できます。

zssh web01.localhost

ZSH補完

zsh補完をサポートしているので、以下のコードを~/.zshrcに書いておくと

eval "$(zssh --zsh-completion)"

上に貼り付けたアニメgifのようにサーバが説明文付きで候補にでるようになります。

フック

hooksの設定でサーバ接続時と切断時にローカルでLuaのコードを実行できます。 os.executeでコマンドを実行できるので、以下のようにするとMacのターミナルの色を変更できます。

Host "web01.localhost" {
    HostName = "192.168.0.11",
    Port = "22",
    User = "kohkimakimoto",
    ForwardAgent = "yes",
    description = "my web01 server",

    -- フックの設定
    hooks = {
        before = function()
            -- This is an example to change screen color to red.
            os.execute("osascript -e 'tell application \"Terminal\" to set current settings of first window to settings set \"Red Sands\"'")
        end,
        after = function()
            -- This is an example to change screen color to black.
            os.execute("osascript -e 'tell application \"Terminal\" to set current settings of first window to settings set \"Pro\"'")
        end,
    }
}

ちなみに、ターミナルの色を変える方法は以前にも書きましたので、よければそちらも参照ください。

http://kohkimakimoto.hatenablog.com/entry/2015/04/02/211232

マクロ

コマンドを複数サーバにまとめて実行できます。

Host "web01.localhost" {
    HostName = "192.168.0.11",
    Port = "22",
    User = "kohkimakimoto",
    ForwardAgent = "yes",
    description = "my web01 server",
    tags = {
        role = "web"
    },
}

Host "web02.localhost" {
    HostName = "192.168.0.12",
    Port = "22",
    User = "kohkimakimoto",
    ForwardAgent = "yes",
    description = "my web02 server",
    tags = {
        role = "web"
    },
}

Macro "example" {
    -- 並列実行するか?
    parallel = true,
    -- 実行前に確認プロンプトを出す。
    confirm = "Are you OK?",
    -- zsh補完時の説明
    description = "example macro",
    -- 実行先サーバを指定。Host設定のtagsで設定したタグを指定する。指定しないとローカルでの実行になる。
    on = {role = "web"},
    -- ttyを使うか? tail -f でログを監視するときなどはtrueに
    tty = false,
    -- コマンドの内容
    command = [[
        ls -la
    ]],
}

マクロ名を指定して実行できます。

$ zssh example

Goでgraceful restartに対応したデーモンプロセスをつくる

Goで書かれたWebアプリのプロセスをデーモンにしたり、ダウンタイムなしでデプロイできるようにするための情報をいろいろ調べていたのですが、どうもSupervisorなど外部のツールを使ったりするのが定番か、herokuなどのPaaSにまかせてしまうという手法が多いようです。 でも個人的にGoの一番気に入っているところは依存のない独立した単一バイナリを生成できる点なので、Webアプリもなるべく外部に依存せずにどうにかできないものかと調査していました。

さてGoのデーモン化とgracefulのライブラリはそれぞれいくつかあるのですが、両方に対応したライブラリは見つからず、組み合わせたサンプルなども見当たらなかったので、各種ライブラリのソースを読んだりしながら、最終的に以下のものを取り上げてみました

この2つを組み合わせてgraceful restartできるデーモンを作ろうとしたわけですが、上記のライブラリではdaemonziseもgracefulも内部的にforkの代わりにos.StartProcessで外部コマンドとして自分自身を再起動し、環境変数を使って状態の制御をおこなうということをやっています。一緒に使うと、その辺が干渉してうまく動かない。。。

で、いろいろワークアラウンドをいれて、ひと通り動くようになったのが以下のコードです。やっていることはコード内のコメントを参照してください。

package main

import (
    "flag"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "os"
    "path/filepath"
    "strconv"
    "time"

    "github.com/VividCortex/godaemon"
    "github.com/facebookgo/grace/gracehttp"
    "github.com/kardianos/osext"
)

var (
    logfile = flag.String("l", "goserver.log", "log file")
    pidfile = flag.String("p", "goserver.pid", "pid file")
)

func main() {
    flag.Parse()

    // os.Args[0]を絶対パスに書き換える。
    // デーモナイズ後、カレントディレクトリが変わるので、
    // 相対パスのままだとgracehttpがos.StartProcessするとき自分自身を指し示すファイルパスを解決できない。
    bin, err := osext.Executable()
    if err != nil {
        log.Fatal(err)
    }
    os.Args[0] = bin

    // pidとlogのファイルのパスも絶対パスにする。
    logfilepath, err := filepath.Abs(*logfile)
    if err != nil {
        log.Fatal(err)
    }
    pidfilepath, err := filepath.Abs(*pidfile)
    if err != nil {
        log.Fatal(err)
    }

    // デーモナイズする
    // "LISTEN_FDS"があるときはgraceful restart時なので、スキップさせる。
    if os.Getenv("LISTEN_FDS") == "" {
        godaemon.MakeDaemon(&godaemon.DaemonAttr{})
    }

    // 以下のロジックはデーモナイズ後の状態で実行される。

    // ログの出力先をファイルに。
    f, err := os.OpenFile(logfilepath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()
    log.SetOutput(f)

    // pidファイルのプロセスIDを書き込む
    pid := os.Getpid()
    if err = ioutil.WriteFile(pidfilepath, []byte(strconv.Itoa(pid)), 0666); err != nil {
        log.Fatal(err)
    }
    log.Printf("Generated pidfile %s (%d)\n", pidfilepath, pid)

    // WebAppサーバ
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(5 * time.Second)  // 動作検証用に5秒リクエストをつかみっぱなしにする(restartしても切れないことを見る)
        fmt.Fprintf(w, "hello world!")
    })
    server := &http.Server{Addr: ":1234", Handler: mux}

    // graceful restartをサポートして起動
    gracehttp.Serve(server)
}

github.com/kardianos/osextは実行ファイルの絶対パスを取得するための便利ライブラリです。

これをビルドし起動した後、graceful restartするには-USR2シグナルを送る。

kill -USR2 5678

シャットダウンもリクエストが途中で切れないgraceful shutdownになる。

kill 5678

動作させてみたところうまく動いているようではあるが、os.Args[0]を書き換えたりしてキモいコードになっている。 daemonizeとgracefulをまとめて、ルーティングとmiddlewareまわりの構造を提供してくれる軽量フレームワークがあったらいいなあ、と妄想しています。

iOSでステータスバーの色が写真取得すると黒に戻る件についての対処

ステータスバーの文字の色を

[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];

で白に変えられる。ところがUIImagePickerControllerで写真アルバムから写真を取得すると、このステータスバーの文字色が黒に戻ってしまう。調べたらstackoverflowが引っかかり、以下のワークアラウンドで回避できることがわかったのでメモ。

まずUIImagePickerControllerのデリゲートを設定し

UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.delegate = self;

そして以下のデリゲートメソッドを実装する。

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
}

UIImagePickerControllerはUINavigationControllerの子クラスなので、UINavigationControllerのデリゲートメソッドであるwillShowViewControllerを実装しておく。ここに色を白にするコードを入れておいてデリゲートにセットしておけばOKというわけです。

以下、stackoverflowの元記事です。

ios7 - UIImagePickerController breaks status bar appearance - Stack Overflow

Goについての雑感

最近Golangコマンドラインツールを書いているので、ちょっと思うところを書いておく。

気に入っているところ

シングルバイナリをクロスプラットフォームで生成できる

とくにコマンドラインツールを作ってみると、この特性がすごく気持ちよい。シングルバイナリはインストールが楽という利点があり、特に今はgithubのリリースページにバイナリを置いたり、https://bintray.com/とかを使うことでファイルをHTTPでダウンロードすることができ、導入の敷居がすごく下がる。アンインストールもファイルを消せばいいだけなので気楽だ。

ところで自分の場合、RubyPythonなど自分がメインで使っていない言語の処理系を必要とするツールだと、まずその処理系を「正しく」インストールする方法を調べるのに気を使ったりしてしまう。もちろん本当のところは目的のツールが単に動けばいいだけなので、Macにプリインストールされている処理系を使えばいいのだけど、どうせ使うんだったら、Linuxサーバ上のものと同じ環境にしたいとか、メジャーな方法を使いたいとかでrbenvを調べたりごにょごにょ環境周りをいじって時間を浪費することが多くて、しょんぼりする。

そのへんの事情から開放してくれて他に依存が無いのですぐに使えるし、すぐに捨てられるというシングルバイナリは思いの外、快適だったりする。

静的型付けのコンパイル言語

今はIntelliJでGoを書いているので静的型な言語だとIDEの補完や定義先へのジャンプがかなり強力に作用する。ちょっと作業環境が重いと感じることもあるが、許容できる。プライベートで書いているプログラムだと、ちょっと変数の名前の付け方やディレクトリ構成が気に食わないとか、気分でコード全体をもりっと変えることがよくあって、そういうときもコンパイラのチェックがあるので、ある程度のコードの正当性を担保してくれるのがよい。

また、型推論のためコードの見た目はLLっぽいのも気に入っている。

OSSですでに周辺ライブラリがいろいろある、導入も簡単

Githubで検索すれば必要なライブラリは大抵あるし、それを使ったサンプルコードも検索すれば大体出てくる。

困っているところ

GOPATHとimport

もういろんなところで言及されているので詳細は省くが、GoはGOPATHというパスを起点にプロジェクトのコードと依存する外部パッケージのコードを同列にフラットに管理するという独特の方式を採用している。これのせいで明確に困ることがあって、

GithubのGo言語プロジェクトにPull Requestを送るときのimport問題 | SOTA

のようにforkしたプロジェクトだと問題になる。また自分の作業環境だと、依存物をプロジェクトのディレクトリ配下に収められないのでctagsでタグ生成する範囲が広がりすぎる問題がある。で、この辺は以前書いたようにdirenvGomを使ってGOPATHをプロジェクトごとに複数、切り替えて対応している。

Goの開発環境 - オープンソースこねこね

幸いIntelliJもGOPATHを複数設定し、プロジェクトごとに切り替えることができるのでその機能を使っている。 とはいえちょっとややこしくて、非標準のツールに頼っているため悩ましいところではある。

プラグイン的なものが作れない

Goは動的に他のビルド済みのGoのコードをロードすることができないので、後からプロダクトに機能追加できるようにする、いわゆるプラグイン的なものはつくれない。

考えていること

単一で完結するコマンドラインツールは今後Goで書く。

普通のWebアプリだったら依然PHPを使うと思う。