毎日使うブラウザから、気軽にコマンドを呼び出せたら素敵でない?そんなことを思い、kamakura.go #2でGoを使った方法を考えてみました。TL;DRという方は、発表スライドをご覧ください。
ポイント
今回のポイントは、Chromeとネイティブアプリで通信することです。幸い、ChromeはNative Messaging Host APIという、拡張機能とネイティブアプリで通信する機能を持っています。
利用するために、以下の3つの作業を行いました。しかし、Goと関係ないのでここでは取り上げません。
- Native Messaging Hostの登録
- 拡張機能の登録
- Native Messaging Hostの配置
詳しくは、Googleのドキュメントを参照してください。
拡張機能とネイティブアプリの通信方法
通信は標準入力と標準出力を介して行われます。メッセージはJSONでシリアライズされており、その先頭に32ビットのメッセージ長が付加されます。
今回は、このプロトコルに従って通信するための小さなライブラリをGoで書きました。Native Messaging APIを介して、Chrome拡張機能と通信できます。
ライブラリのデモとして、ブラウザからRubyインタプリタを実行してみました。デモの流れは、次のとおりです。
- Rubyスクリプトのテキストを選択する
- 右クリックからRubyインタプリタを実行する
- 実行結果をアラートダイアログが表示される
Goのソースコードは、以下のように短いものです。
package chrome
import (
"encoding/binary"
"io"
)
var byteOrder binary.ByteOrder = binary.LittleEndian
func Post(msg []byte, writer io.Writer) error {
// Post message length in native byte order
header := make([]byte, 4)
byteOrder.PutUint32(header, (uint32)(len(msg)))
if n, err := writer.Write(header); err != nil || n != len(header) {
return err
}
// Post message body
if n, err := writer.Write(msg); err != nil || n != len(msg) {
return err
}
return nil
}
func Receive(reader io.Reader) ([]byte, error) {
// Read message length in native byte order
var length uint32
if err := binary.Read(reader, byteOrder, &length); err != nil {
return nil, err
}
// Return if no message
if length == 0 {
return nil, nil
}
// Read message body
received := make([]byte, length)
if n, err := reader.Read(received); err != nil || n != len(received) {
return nil, err
}
return received, nil
}
まとめ
毎日利用するブラウザから、ターミナルへテキストをコピー&ペーストすることは手間です。そんな手間を最小にするための方法の一つを、Goで知ることができました。
Goには、バイナリデータを扱う標準パッケージが用意されています。bytesやencoding/binaryを学ぶ、よい題材でした。
最後に、kamakura.goは、Goについて学ぶ良い機会でした。鎌倉 小町通で、焼き鳥エンジニアから色々勉強させて頂きました。。。
サンプルコード
package main
import (
"os"
"os/exec"
"log"
"encoding/json"
"github.com/lhside/chrome-go"
)
type Message struct {
Text, Result string
}
func main() {
// Receive message
msg, err := chrome.Receive(os.Stdin)
if err != nil {
log.Fatal(err)
}
message := &Message{}
if err := json.Unmarshal(msg, message); err != nil {
log.Fatal(err)
}
log.Println(message.Text)
// Execute message text as ruby script
cmd:=exec.Command("ruby", "-e", message.Text)
result, err := cmd.CombinedOutput()
if err != nil {
log.Fatal(err)
}
log.Printf("%s\n", result)
// Post message with the result
message.Result = string(result)
body, err := json.Marshal(message)
if err != nil {
log.Fatal(err)
}
err = chrome.Post(body, os.Stdout)
if err != nil {
log.Fatal(err)
}
}