すぎゃーんメモ

2014-07-19

ターミナル操作の記録(ttyrec)からGIFアニメを生成するツールを作った

ttyrec で録画したデータを使ってターミナル上で再生しつつ、そのスクリーンキャプチャを使ってアニメーションGIFを生成するツールをGoで作ってみた。

Mac, Linux Desktopで動作。再生速度はオプションで変更可能。

f:id:sugyan:20140719013354g:image

f:id:sugyan:20140719013353g:image


背景

percolを使ってターミナル操作を早く、便利に。 - すぎゃーんメモ のような記事を書いたりする際に、ターミナル操作を録画してGIFアニメにしたい需要があり。

そういった用途に使える汎用のデスクトップ録画ツールとしてはLICEcapやGifzoなどがある。

これらはWindows, OSXあたりが対象で、Linuxの場合はまた他のツールがあるらしい(よく知らない)。

汎用的なキャプチャツールでも良いけれど、いちいち録画対象のターミナルのウィンドウを指定するのも面倒だし、ターミナル専用のツールがあっても良いのではないか。

ttyrec というツールが、古くから存在する。これを使うとコマンドからターミナル操作を録画・再生することができる。

これで各コマを再生しつつターミナルスクリーンショットを撮り、それらを繋げてGIFアニメにする、というツールも既に幾つか存在する。

これらは大抵 ImageMagick に依存しており、importコマンドを利用してスクリーンショットを撮ったり、convertコマンドGIFを生成したりしている。言語C/C++, Pythonで、対象はOSXLinux両方だったり片方だけだったり。


ところで Go には標準パッケージとしてGIF画像を扱うものが用意されている。

これを使えばImageMagickに依存することなく前述のようなツールが作れるのでは? しかもPure Goで書けるはずだからクロスコンパイルして各環境用のバイナリを配布することも可能なはず!

ということで作ってみた次第。

(まぁターミナルの録画を撮りたい需要がある人なら各言語の実行環境用意したりImageMagickインストールしたりするのに障壁は無いでしょうけども)


実装

ttyrecordの読み取り

ttyrecは自分でインストールしてもらうとして(RPMパッケージが用意されていたりhomebrewでも入るし そこは問題ないはず)。

それで撮って得られたバイナリデータは、

  • コマの時刻と、出力内容の長さを格納したヘッダ
  • それに続く出力内容の中身

がただ順番で書き込まれているだけなので、ひたすら順番に読み取っていく。


再生

読み取った内容を元に、各時刻の差分を計算したりしつつ ただターミナル上に出力していくだけで、再生できる。


キャプチャ

コマを再生しつつ、TMPDIR以下にスクリーンショットを放り込んでいく。が、ここは結構面倒なところで。


OSXでは、

という手順でスクリーンショットを撮る。実行中のターミナルアプリケーションがTerminal.appなのかiTerm.appなのか、はたまた別のアプリケーションなのか?は、とりあえずは$TERM_PROGRAM環境変数で二択の判別だけしているけれど 他に良い方法ないだろうか…

あと、Retina display上で撮ると高い解像度デカい画像が作られてしまうので、そこからさらにsipsコマンドを使ってdpi情報を取得して調整したり。


Linux というかX Window Systemでは、xwdというコマンドで指定したWINDOWIDの画面をダンプできる、ということなのでそれを使うことにした。

ただこれはxwdフォーマットという独自のビットマップ形式で保存されるので、それを読み取って画像データとして得るためのdecoderは自分で書いた。


OSX/Linux各環境で分けられるかというとそうでもなく、Mac OS X上でも XQuartz を使ってX Window Systemを立ち上げてその上でターミナル動かしたりするので、出来る限りどちらにも対応できるようにした、つもり。

f:id:sugyan:20140719090422g:image

これはXQuartz上のxtermで撮ったもの。


xwdでは余計な部分が撮られることなくて済むのだけど、OSXscreencaptureではどうしても「ウィンドウ全体」が撮られるため、タイトルバーなどが含まれてしまう。この部分が何故かコマによって白飛びしてしまったりするし、出来れば除外したいのだけど…

少し調べた限りでは良い方法は無さそうだった。決め打ちで上20pixelくらい削ったりしようにも アプリケーションによって または単一タブのときと複数タブのときとでバーの高さが変わったりするだろうし 逆にフルスクリーンモードときは勝手に消えるからその場合も考慮しないといけないし…


キャプチャからのGIF生成

1コマずつ再生しつつ スクリーンショット取得->画像ファイル読み取り とやっていると再生終了まで時間がかかってしまうので、とりあえず撮るだけ撮って保存だけしておき、一通り再生し終わったらそれらを読み込んでGIF生成にとりかかる、というようにしている。

ここで早速 pipeline and cancellation並行性パターンの勉強 - すぎゃーんメモ で勉強した内容が役立った。CPU使う処理なので並行化でそこまで劇的に早くなるというわけでもないけど、runtime.GOMAXPROCS(runtime.NumCPU())指定してコア数ぶん動かすことで可能な限りの処理をしてくれるようになった、と思う。


TODO

Windowsのことは完全に無視して作った(確認できる環境も持ってないし…)けど、コマンドプロンプトスクリーンショットを簡単に撮る方法はあるのだろうか?

Linux Desktopも自分では普段使ってないからあまり詳しく知らない、他にもサポートすべきものはあるのかな。

バイナリ配布するならttyrec自体もGoで再実装して使えるようにしておいた方がよかったりするのだろうか?

スピード調整はできるようにしたけど、「この前半3コマは削りたい」「このtypoの部分を削りたい」みたいな場面は出てくると思うので、一度各コマの時刻と出力内容概要をファイル出力して それを編集して使うことで各コマのタイミング調整や削除をできるようにしたいな、と思っている。

2014-07-17

pipeline and cancellation並行性パターンの勉強

Go Concurrency Patterns: Pipelines and cancellation - The Go Blog を読んでいて、なかなか理解するまで苦しんだので復習がてら自分でもコードを書いて確かめてみた。


お題

複数の入力データそれぞれに対して重い処理を行い、結果として返ってくる値をまとめて取得する。

途中でエラーが発生したら直ちに処理を中止して終了する。


コード

いちばん簡単な例

エラーを考慮しない場合。

package main

import (
	"fmt"
	"log"
	"math/rand"
	"time"
)

func init() {
	log.SetFlags(log.Lmicroseconds)
	rand.Seed(time.Now().UnixNano())
}

func doSomething(id int) string {
	wait := rand.Intn(1000)
	time.Sleep(time.Millisecond * time.Duration(wait)) // something heavy
	return fmt.Sprintf("%02d-%03d", id, wait)
}

func getAllData() (results []string) {
	for i := 0; i < 100; i++ {
		value := doSomething(i)
		log.Println("got", value)
		results = append(results, value)
	}
	return results
}

func main() {
	data := getAllData()
	log.Println("Finished.", data)
}

こんなかんじ。100ループで毎回数百ミリ秒かかる処理(本当はCPUぶん回すような処理だったり)をして、その結果をひとつずつ繋げていって結果が格納されたsliceを返す。


$ go run example.go
22:57:33.931461 got 00-955
22:57:34.706984 got 01-774
22:57:35.204441 got 02-497

...

22:58:23.020169 got 97-116
22:58:23.528240 got 98-507
22:58:24.178353 got 99-649
22:58:24.178596 Finished. [00-955 01-774 02-497 ...

当然ながら順番に1個ずつ処理していくのでとても時間かかる。


エラー処理を加える

doSomethingの中で、もしくはその前に繰り返し処理の内部でエラーが起こりうる、とする。適当に100分の1くらいの確率で起こることにして それぞれの関数errorも返すよう変更

var errUnfortunate1 = errors.New("unfortunate error 1")
var errUnfortunate2 = errors.New("unfortunate error 2")

func doSomething(id int) (string, error) {
	wait := rand.Intn(1000)
	time.Sleep(time.Millisecond * time.Duration(wait)) // something heavy
	if rand.Intn(100) == 0 {
		return "", errUnfortunate1
	}
	return fmt.Sprintf("%02d-%03d", id, wait), nil
}

func getAllData() (results []string, err error) {
	for i := 0; i < 100; i++ {
		if rand.Intn(100) == 0 {
			return nil, errUnfortunate2
		}
		value, err := doSomething(i)
		if err != nil {
			return nil, err
		}
		log.Println("got", value)
		results = append(results, value)
	}
	return results, nil
}

func main() {
	data, err := getAllData()
	if err != nil {
		log.Println("Failed!", err)
		return
	}
	log.Println("Finished.", date)
}

関数の返り値からエラーチェックして 何かあればすぐにgetAllDataを抜けてmain内で出力して終了するようになっている。


並行化その1 channel化

処理を並行で行うための準備として、goroutineとchannelを使った形に変えていく。

まずは入力を送ってくれるchannelを作って返す関数を作り、そこからrangeで読み取るようにしてみる。エラー処理を無視すると

func getDataChannel() <-chan string {
	c := make(chan string)
	go func() {
		for i := 0; i < 100; i++ {
			value, _ := doSomething(i)
			log.Println("got", value)
			c <- value
		}
		close(c)
	}()
	return c
}

func getAllData() (results []string, err error) {
	c := getDataChannel()
	for value := range c {
		results = append(results, value)
	}
	return results, nil
}

こんなかんじ。


並行化その2 重い処理を並行に

引き続きエラー処理を無視したままだけど、doSomething部分をgoroutineに。

単純に即時関数で囲んで並行化するだけだと処理が終わる前にcが閉じてしまったりmainまで終了してしまったりするので、sync.WaitGroupを使って全部おわるまで待つ。

import (
...
	"sync"
)

func getDataChannel() <-chan string {
	c := make(chan string)
	go func() {
		var wg sync.WaitGroup
		for i := 0; i < 100; i++ {
			wg.Add(1)
			go func(id int) {
				value, _ := doSomething(id)
				log.Println("got", value)
				c <- value
				wg.Done()
			}(i)
		}
		wg.Wait()
		close(c)
	}()
	return c
}

早く終わったものから順にどんどんデータが送られて、全部おわるまで待ってからcがcloseされる。

エラーが何もなければこれで良いのだけど、、


並行化その3 エラー処理1

まずはdoSomethingで返ってくるerrUnfortunate1を捕捉。

これはgoroutine内で起こり得るので関数の返り値としては使いづらい。ので、返ってくるvalueとともにstructに含めてchannelに送るようにする

type result struct {
	value string
	err   error
}

func getDataChannel() <-chan result {
	c := make(chan result)
	go func() {
		var wg sync.WaitGroup
		for i := 0; i < 100; i++ {
			wg.Add(1)
			go func(id int) {
				value, err := doSomething(id)
				log.Println("got", value, err)
				c <- result{value: value, err: err}
				wg.Done()
			}(i)
		}
		wg.Wait()
		close(c)
	}()
	return c
}

func getAllData() (results []string, err error) {
	c := getDataChannel()
	for r := range c {
		if r.err != nil {
			return nil, r.err
		}
		results = append(results, r.value)
	}
	return results, nil
}

受け取る側のrangeループ内でresult.errをチェックして、エラーを検出したらそこで終了。

これだけではまだまだ問題あるのだけど とりあえずここではこれで捕捉できたことにする


並行化その4 エラー処理2

次に、繰り返し処理の内部で起こり得るerrUnfortunate2を捕捉する。

goroutineでは返り値をとれないので、ループする部分をfunc() error {}()即時関数で囲むことで取得する。得たerrorを送る手段としてerror用のchannelを用意し、resultを送るchannelと一緒に返して使ってもらうようにする

func getDataChannel() (<-chan result, <-chan error) {
	c := make(chan result)
	errc := make(chan error)
	go func() {
		var wg sync.WaitGroup
		err := func() error {
			for i := 0; i < 100; i++ {
				if rand.Intn(100) == 0 {
					return errUnfortunate2
				}
				wg.Add(1)
				go func(id int) {
					value, err := doSomething(id)
					log.Println("got", value, err)
					c <- result{value: value, err: err}
					wg.Done()
				}(i)
			}
			return nil
		}()
		wg.Wait()
		close(c)
		errc <- err
	}()
	return c, errc
}

func getAllData() (results []string, err error) {
	c, errc := getDataChannel()
	for r := range c {
		results = append(results, r.value)
		if r.err != nil {
			return nil, r.err
		}
	}
	err = <-errc
	if err != nil {
		return
	}
	return results, nil
}

errorが起きようと起きまいと即時関数が終了した後にcはcloseされるのでrange cループが終了し、その後にerrcから即時関数の返り値として得たerrorを取得してチェックすることができる。

これまた問題があるけど一応捕捉はできた。


並行化その5 中断されたことを知らせる

ここまでだと、errUnfortunate1が起きたときにはcがcloseすることもなく走ってる処理が続くし、errUnfortunate2のときにも走ってるもの待ってからcloseすることになってしまったり、まだ正しく中断できているとは言えない。

並行化して走っている処理たちに中断されたことを知らせるために、もう一つchannelを用意してそれを使って判定するようにする。

func getDataChannel(done <-chan struct{}) (<-chan result, <-chan error) {
	c := make(chan result)
	errc := make(chan error)
	go func() {
		var wg sync.WaitGroup
		err := func(walkFunc func(int) error) (err error) {
			for i := 0; i < 100; i++ {
				time.Sleep(time.Millisecond * 50)
				if rand.Intn(100) == 0 {
					return errUnfortunate2
				}
				err = walkFunc(i)
				if err != nil {
					return
				}
			}
			return nil
		}(func(id int) error {
			wg.Add(1)
			go func() {
				value, err := doSomething(id)
				log.Println("got", value, err)
				select {
				case c <- result{value: value, err: err}:
					log.Println("sent.")
				case <-done:
					log.Println("not sent.")
				}
				wg.Done()
			}()
			select {
			case <-done:
				return errors.New("canceled")
			default:
				return nil
			}
		})
		wg.Wait()
		close(c)
		errc <- err
	}()
	return c, errc
}

func getAllData() (results []string, err error) {
	done := make(chan struct{})
	defer close(done)
	c, errc := getDataChannel(done)
	for r := range c {
		results = append(results, r.value)
		if r.err != nil {
			return nil, r.err
		}
	}
	err = <-errc
	if err != nil {
		return
	}
	return results, nil
}

getAllData側で用意したdonechannelは、deferによって関数を抜けるときにcloseする。これをgetDataChannelに渡しておいて、そちらではselectを使って処理を分岐させることができる。doneが閉じていればそちら側が実行されるのでdoSomethingから値が返ってきてもcには送信されないし、ループを実行するwalkFuncは"canceled"なエラーを受け取りループを中断するようになる。


並行化その6 完成形?

中断したときsync.WaitGroupで全部終わるまでWaitするのはブロックする必要ないのでgoroutineにする(deferでも良いかも?)。でもrangeでcが閉じるまで待っていては結局errcからすぐには受け取れないのでこちらもselectを使う。

あと、errcは送る前に受け取り側が終了してしまっていると書き込みがブロックされる可能性があるのでバッファリングしておく必要がある、のでmakeの第2引数で1以上を指定しておく。

func getDataChannel(done <-chan struct{}) (<-chan result, <-chan error) {
	c := make(chan result)
	errc := make(chan error, 1)
	go func() {
		var wg sync.WaitGroup
		err := func(walkFunc func(int) error) (err error) {
			for i := 0; i < 100; i++ {
				time.Sleep(time.Millisecond * 50)
				if rand.Intn(100) == 0 {
					return errUnfortunate2
				}
				err = walkFunc(i)
				if err != nil {
					return
				}
			}
			return nil
		}(func(id int) error {
			wg.Add(1)
			go func() {
				log.Println("start", id)
				value, err := doSomething(id)
				log.Println("got", value, err)
				select {
				case c <- result{value: value, err: err}:
				case <-done:
				}
				wg.Done()
			}()
			select {
			case <-done:
				return errors.New("canceled")
			default:
				return nil
			}
		})
		go func() {
			wg.Wait()
			close(c)
		}()
		errc <- err
	}()
	return c, errc
}

func getAllData() (results []string, err error) {
	done := make(chan struct{})
	defer close(done)
	c, errc := getDataChannel(done)

Loop:
	for {
		select {
		case r, ok := <-c:
			if !ok {
				break Loop
			}
			results = append(results, r.value)
			if r.err != nil {
				return nil, r.err
			}
		case err = <-errc:
			if err != nil {
				return
			}
		}
	}
	return results, nil
}

これで、並行かつ エラー時には即座に処理が中断されて余計なデータ送受信などもなく後始末もできるようになった。


goroutineの起動数を制限

とはいえ上記の方法だと入力受け取るたびにどんどんgoroutineを起動することになりメモリ使用量などマズいことになり得る。

ので、並行に走らせる数を制限させる別のパターンを用意する。

まず、処理の結果を送るchannelを返していたgetDataChannelを、"入力"を送るchannelを返すだけのものに変更する。

func getInputChannel(done <-chan struct{}) (<-chan int, <-chan error) {
	ids := make(chan int)
	errc := make(chan error, 1)
	go func() {
		defer close(ids)
		err := func(walkFunc func(int) error) (err error) {
			for i := 0; i < 100; i++ {
				time.Sleep(time.Millisecond * 50)
				if rand.Intn(100) == 0 {
					return errUnfortunate2
				}
				err = walkFunc(i)
				if err != nil {
					return
				}
			}
			return nil
		}(func(id int) error {
			select {
			case <-done:
				return errors.New("canceled")
			case ids <- id:
			}
			return nil
		})
		errc <- err
	}()
	return ids, errc
}

こんなかんじ、doneが閉じてない限りは入力データとなるidを送りつづける。

で、その入力channelを受け取って出力に結果を流すworker的なものを別に作る。

func worker(ids <-chan int, c chan<- result, done <-chan struct{}) {
	for id := range ids {
		value, err := doSomething(id)
		log.Println("got", value, err)
		select {
		case c <- result{value: value, err: err}:
		case <-done:
			return
		}
	}
}

単純に入力が流れてくる限りdoSomethingな処理をして、doneが閉じていない限りはcにresultを送りつづける。役割がハッキリしている。

んで、あとはこれをgoroutineで起動させて受け取るだけ。ただし終了するのを待ってからcをcloseしてやる必要はある。

func getAllData() (results []string, err error) {
	done := make(chan struct{})
	defer close(done)
	ids, errc := getInputChannel(done)

	var wg sync.WaitGroup
	c := make(chan result)
	wg.Add(1)
	go func() {
		worker(ids, c, done)
		wg.Done()
	}()
	go func() {
		wg.Wait()
		close(c)
	}()

Loop:
	for {
		select {
		case r, ok := <-c:
			if !ok {
				break Loop
			}
			results = append(results, r.value)
			if r.err != nil {
				return nil, r.err
			}
		case err = <-errc:
			if err != nil {
				return
			}
		}
	}
	return results, nil
}

この形で呼び出されるworkerは任意の数のgoroutineで並行起動してもそれぞれが「入力を受け取り出力を送る」という役目をこなすだけなので上手く動作してくれる。

	var wg sync.WaitGroup
	c := make(chan result)
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			worker(ids, c, done)
			wg.Done()
		}()
	}
	go func() {
		wg.Wait()
		close(c)
	}()

最終形

というわけで最終的に出来上がったのが以下。ちゃんと納得できるかたちで http://blog.golang.org/pipelines/bounded.go と同じような形にできたので大丈夫だと思う。

package main

import (
	"errors"
	"fmt"
	"log"
	"math/rand"
	"runtime"
	"sync"
	"time"
)

func init() {
	log.SetFlags(log.Lmicroseconds)
	rand.Seed(time.Now().UnixNano())
}

var errUnfortunate1 = errors.New("unfortunate error 1")
var errUnfortunate2 = errors.New("unfortunate error 2")

type result struct {
	value string
	err   error
}

func doSomething(id int) (string, error) {
	wait := rand.Intn(1000)
	time.Sleep(time.Millisecond * time.Duration(wait)) // something heavy
	if rand.Intn(100) == 0 {
		return "", errUnfortunate1
	}
	return fmt.Sprintf("%02d-%03d", id, wait), nil
}

func getInputChannel(done <-chan struct{}) (<-chan int, <-chan error) {
	ids := make(chan int)
	errc := make(chan error, 1)
	go func() {
		defer close(ids)
		err := func(walkFunc func(int) error) (err error) {
			for i := 0; i < 100; i++ {
				time.Sleep(time.Millisecond * 50)
				if rand.Intn(100) == 0 {
					return errUnfortunate2
				}
				err = walkFunc(i)
				if err != nil {
					return
				}
			}
			return nil
		}(func(id int) error {
			select {
			case <-done:
				return errors.New("canceled")
			case ids <- id:
			}
			return nil
		})
		errc <- err
	}()
	return ids, errc
}

func worker(ids <-chan int, c chan<- result, done <-chan struct{}) {
	for id := range ids {
		value, err := doSomething(id)
		log.Println("got", value, err)
		select {
		case c <- result{value: value, err: err}:
			log.Println("sent")
		case <-done:
			log.Println("not sent")
			return
		}
	}
}

func getAllData() (results []string, err error) {
	done := make(chan struct{})
	defer close(done)
	ids, errc := getInputChannel(done)

	var wg sync.WaitGroup
	c := make(chan result)
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			worker(ids, c, done)
			wg.Done()
		}()
	}
	go func() {
		wg.Wait()
		close(c)
	}()

Loop:
	for {
		select {
		case r, ok := <-c:
			if !ok {
				break Loop
			}
			results = append(results, r.value)
			if r.err != nil {
				return nil, r.err
			}
		case err = <-errc:
			if err != nil {
				return
			}
		}
	}
	return results, nil
}

func main() {
	defer func() {
		// 異常に大きな数のgoroutineが起動しっぱなしでないか確かめる
		time.Sleep(time.Millisecond * 2000)
		log.Println(runtime.NumGoroutine())
	}()
	data, err := getAllData()
	if err != nil {
		log.Println("Failed!", err)
		return
	}
	log.Println("Finished.", data)
}

まとめ

なかなか処理の流れが複雑なかんじがして「なんでこんな書き方するの」「ここがエラーになったらどうなるの」とか悩んだけど、書きながら読んでるうちにようやく「あー、だからこうするのか」「確かに、こうしようと思ったらこういう形になるよねー」って納得できた。

とはいえスラスラとこういうのが書ける気はまだしないけど…。

2014-07-03

はじめて、の次のGo

はじめてのGo - すぎゃーんメモ の続き?的な。

入門で終わらずにもうちょっとマトモに何か作れるようになりたい、ということで、以前にRubyで書いた「げんきだしてbot」をGoで書いてみた。

成果物のリポジトリはこちら。

「げんきだしてbot」とは

まぁUser streamからネガティヴな発言を拾ってきて「げんきだして!」とリプライを送るだけのTwitter botです。


ファイル分割

すべて同一のmainパッケージ内で書くけど、機能ごとにファイルを分けてみた。

typeやfuncの定義が色んなところに分散していても、どうせgodef使って定義元にジャンプできるので問題ない。


設定値の保存/読取

前回は go-pit を使って設定値を読み書きしていたけど、簡単に自分で書いてみよう、と自前で用意してみた。

~/.config/以下に適当にディレクトリ掘ってini形式ファイルaccess tokenを保存。JSONYAMLなどの構造化データもいいけど、Goで扱う場合 それほど複雑なデータ構造を必要としないならiniファイルくらいがお手軽で良いなーという印象を持った。

HOMEディレクトリの取得の際に最初はos/userパッケージを使っていたけど、これがどうもクロスコンパイルするときの障壁になるようなので避けることにした。


Stream API

普通にOAuth認証でのリクエストまでは問題ないとして、Stream API接続したときのレスポンスを継続して受け続けるために。

などを参考に。

bufio.NewScanner(resp.Body)で作った*bufio.Scannerに対してScanをかけることで 新たなデータが流れてきたときbyte列を取得できるようだ。

ただこのときclient側から接続を切ろうとすると接続時のConnectionを維持している必要がありそうで、そのへんは面倒そうなのでここでは省略した。


正規表現

Ruby版では「疲れた」に対しては「疲れてるの?げんきだして!」と返信するが「お疲れさま」などには反応しないように/疲(?!れ(?:様|さ(?:ま|ん)))/のような正規表現を使って判定している。

が、これをこのままGoで使おうとするとinvalid or unsupported Perl syntax: `(?!`とエラーを吐かれてしまう。(?!pattern)、"negative look-ahead"否定先読みというのかな、これはGoではサポートされていないらしい。注意。


テスト

いちおうどんな発言に対してどんな返信をすべきか、または返信すべきでないか、を確かめるためにテストも書いておいた。

あまり書きやすいかんじはしないけど、とりあえず書いておけばgo testコマンドで簡単にテストを実行できるので安心できる。


Goroutineで簡単非同期

リプライを送る際にちょっとsleepで間を空けたり、またそのPOSTの送信で処理がブロックされる。Streamの受け取りは止めずにリプライ処理をするために、ここで初めてGoroutineを使用。

		if mention != nil {
			go func() {
				time.Sleep(time.Second * time.Duration(rand.Int31n(5)+5))
				tweet, err := bot.client.Mention(mention)
				if err != nil {
					log.Println(err)
				} else {
					log.Printf("tweeted: %s", tweet.Text)
				}
			}()
		}

Mentionメソッド自体は普通に返り値、エラーを戻す関数として定義しておいて、それを呼ぶ側をfunc(){...}()で囲んでGoroutineとして呼んでやればいいかな、と。

ここでは並行処理同士での協調や調整は必要ないはずなのでChannelとか難しいことは考えない。


クロスコンパイル

Goは簡単に他の環境用のバイナリを生成できる(前述の通りos/userのようなcgoを使うパッケージを含んでいるとハマったりするけれど)、ということで試してみた。

Mac OS上で作ったバイナリさくらVPSで動かしているCentOS上で動くことが確認できた。感動。

drone.iowerckerを使ってバイナリ配布とかも後でチャレンジしてみよう。

Windowsについては試せる環境もっていないので無視で…。


まとめ

まだまだ慣れないけれど 少しずつGoを書く楽しさは感じはじめてる。

やっぱりツールが充実していてすぐに文法エラーを検出できたり定義を調べたり出来るのは強いなーと思う。

もうちょい色んなパッケージやライブラリソース読んだりして良い書き方とか身に付けたい。


次は何を作ってみようか

2014-06-11

percolを使ってターミナル操作を早く、便利に。

先日、shell勉強会で「zawを使ってシェル操作を快適に」というお話を聴いて、自分ももう少しそのあたりの環境を整えよう、と思い立った。


自分が使う選択をしたのは zaw ではなく percol

軽く調べてみたかんじでは zawってのは設定してある(もしくは自作する)決められたsourceを使って決められた操作を行うもので、zshに密接に結び付いているツールで。percolはそういうのではなく純粋に「入力をフィルタリングする」だけのツールなので、パイプなどを使って各コマンドと組み合わせることで色々な使い方ができそう。

f:id:sugyan:20140611204115g:image


percolの導入

Python製のツールなので、sudo pip install percolとかで入る。

READMEに書いてある通り、~/.percol.d/rc.pyに設定ファイルを用意することでプロンプトやキーマップをカスタマイズできる。

自分はできるだけEmacsの helm と同じような操作感にするため こんなかんじにして使ってる。

percol.view.PROMPT  = ur"<green>Input:</green> %q"
percol.view.RPROMPT = ur"[%i/%I]"

percol.import_keymap({
    "C-f" : lambda percol: percol.command.forward_char(),
    "C-b" : lambda percol: percol.command.backward_char(),
    "C-p" : lambda percol: percol.command.select_previous(),
    "C-n" : lambda percol: percol.command.select_next(),
    "C-h" : lambda percol: percol.command.delete_backward_char(),
    "C-d" : lambda percol: percol.command.delete_forward_char(),
    "C-k" : lambda percol: percol.command.kill_end_of_line(),
    "C-a" : lambda percol: percol.command.beginning_of_line(),
    "C-e" : lambda percol: percol.command.end_of_line(),
    "C-v" : lambda percol: percol.command.select_next_page(),
    "M-v" : lambda percol: percol.command.select_previous_page(),
    "C-j" : lambda percol: percol.finish(),
    "C-g" : lambda percol: percol.cancel(),
})

ちなみに、現在これと同様なツールとしてgoで書かれたpecoというものが作られているので、もしかしたら将来的にこちらに移行するかもしれない。


percolでコマンド履歴の検索・絞り込み

READMEに書いてあるけれど。

シェルのhistoryをpercolに流し込むことで より素早く簡単にコマンド履歴を遡れる。

zshの場合はfc -l -n 1で全履歴を表示できる、のかな。

function percol_select_history() {
    local tac
    if which tac > /dev/null; then
        tac="tac"
    else
        tac="tail -r"
    fi
    BUFFER=$(fc -l -n 1 | eval $tac | percol --query "$LBUFFER")
    CURSOR=$#BUFFER             # move cursor
    zle -R -c                   # refresh
}
zle -N percol_select_history
bindkey '^R' percol_select_history

と設定を書いておけば、Ctrl+Rでの履歴検索をanything likeに快適に行うことができる。

あと、重複したコマンド履歴は必要ないので

setopt hist_ignore_all_dups

も指定しておいた。

f:id:sugyan:20140611204116g:image


percolでディレクトリ移動

の記事の通り、「一度でもcdしたことのあるディレクトリに効率よくcdする」ために、 autojumpz を使わずともpercolでディレクトリ訪問履歴を絞り込んで選択して移動、ということができる。

自分はしばらくzを使ってきてた けど、percol使ったほうが全然便利だわ!


上記参照記事ではchpwd_functionsに履歴の記録を仕込んでいるけれど、z.shを使っていれば履歴データが~/.zに残っているので 折角なのでそれを使うことにした。

function percol_select_directory() {
    local tac
    if which tac > /dev/null; then
        tac="tac"
    else
        tac="tail -r"
    fi
    local dest=$(_z -r 2>&1 | eval $tac | percol --query "$LBUFFER" | awk '{ print $2 }')
    if [ -n "${dest}" ]; then
        cd ${dest}
    fi
    zle reset-prompt
}
zle -N percol_select_directory
bindkey "^X^J" percol_select_directory

_z -rでランキング順?にsortされて最近訪問したディレクトリ一覧が出力されるので、それをpipeでpercolに渡して選択。

f:id:sugyan:20140611204117g:image


percolでtmuxのwindow選択・切り替え

これもREADMEに書いてあるけれど。

bind b split-window "tmux lsw | percol --initial-index $(tmux lsw | awk '/active.$/ {print NR-1}') | cut -d':' -f 1 | xargs tmux select-window -t"

のようにtmux.confに書いておくと、tmuxの現在のsessionで開いているwindowをpercolで絞り込み、選択して切り替えができる。

デフォルトで"w"キーにbindされているtmux choose-windowでも似たようなことが出来るのだけど、window数が多いときにpercolならインクリメンタルに絞り込みができるのと、tmux split-windowを使うことで現在のwindowが隠れることなく選択操作ができるようになるのが利点だと思う。


ちなみに、tmux list-windowsで表示される情報はwindow nameやpane情報だったりするのだけど、percolで絞り込むときなどは特に「そのwindowはどのディレクトリで作業しているものか」などもあった方が嬉しいので、

tmux list-windows -F '#{window_index}: #{window_name}#{window_flags} (#{window_panes} panes) #{pane_current_path} #{?window_active, (active),}

のように#{pane_current_path}を含むformatを指定して表示させるようにした。最終的にtmux.confには

bind-key C-t split-window -c '#{pane_current_path}' "tmux list-windows -F '#{window_index}: #{window_name}#{window_flags} (#{window_panes} panes) #{pane_current_path} #{?window_active, (active),}' | percol --initial-index $(tmux lsw | awk '/active.$/ {print NR-1}') | cut -d':' -f 1 | xargs tmux select-window -t"

と(長いw)。

f:id:sugyan:20140611204118g:image


あと、window nameには通常は実行中のコマンド名が表示されているのだけれど、sshで他hostにログインしている場合はただ"ssh"とだけ出るのではなく そのhost情報などを表示するようにした方が分かりやすいし誤操作を防げそう。ということで

など参考にしつつ、自分ではsshコマンドをラップするシェルスクリプトを用意した。

#!/bin/sh
if [ -n "$TMUX" ]; then
    local_command='tmux rename-window $(echo "%r@%n(%h:%p)")'
fi

command -p ssh -o PermitLocalCommand=yes -o LocalCommand="${local_command}" "$@"

if [ -n "$TMUX" ]; then
    tmux set-window-option -u automatic-rename
fi

tmux上でsshを叩いたときだけ、接続先のhost名などの情報を含むwindow nameに更新し、終了したら元通りに。tmux rename-windowで変更した場合はautomatic-renameオプションがoffになるようなので、それを戻してあげるのが正しい復元方法だと思う。


percolでweechatのbuffer選択

IRC環境として weechatを使っている けれど、これのchannel選択なんかもpercolでできると嬉しい。

の記事を真似てみようとした。

WeeChat はデフォルトでFifo pluginが有効になっていて、起動中のweechatに対して任意のコマンドを送ることができる。

しかし内部の情報を取得するようなことはできなそうで、やろうとするとそういった機能を持つpluginを作るなり導入するなりする必要がありそうだった。

苦肉の策として、「コマンド送信によって現在のchannel(buffer)一覧情報ファイル保存させ、そのファイルを読み込む」という方法で一覧を取得することにした。

for fifo in $HOME/.weechat/weechat_fifo_*; do
    echo '*/mute layout store' > $fifo
    echo '*/mute save weechat' > $fifo
done

のようにFIFO pipeに対しlayout storesaveを送ることで、現在のbuffer情報~/.weechat以下に保存される(毎回その結果がweechatのbufferに出力されるのを防ぐためにmuteコマンド経由で)。

あとはそれで保存された設定ファイルを読み込んでpercolに渡し、選択結果を使ったbuffer切り替えコマンドを再度FIFO pipeに流せばよい。

BUFFER=$(grep 'default.buffer' $HOME/.weechat/weechat.conf | cut -d'"' -f2 | percol | cut -d';' -f3)
if [ -n "$BUFFER" ]; then
    for fifo in $HOME/.weechat/weechat_fifo_*; do
        echo '*/mute buffer' $BUFFER > $fifo
    done
fi

これでだいぶ捗る。

f:id:sugyan:20140611204119g:image


参照

など

2014-05-27

Google Doodleのルービックキューブを解くChrome拡張を作った

http://www.google.com/doodles/rubiks-cube のやつ。

f:id:sugyan:20140527222637p:image

とっくに旬を過ぎていて今さら感あるけど、、

って自分で言ったし。

この拡張を入れた状態で https://www.google.com/logos/2014/rubiks/rubiks.html を開いてキューブをクリックしてスタートすると左上に"solve"ボタンが現れ、それを押すと勝手に完成させてくれる。


デフォルトの初期状態に対しては31手で解く。(ちなみに https://gstatic.com/logos/2014/rubiks/iframe/index.html を開いてdeveloper toolsでcube.twistQueue.historyみてみると分かるけど、デフォルトの初期状態は40手まわして作られている。solveCube()と叩くとそれを遡って復元してくれる。)


キューブの解を求める方法は色々ありそうで よく知らないけど、とりあえず https://github.com/cubing/jsss のものを適当に使わせていただいた。だいたいどんな状態に対しても30手くらいで解を示してくれるっぽい。


JavaScriptだけで完結するしブックマークレットとかでも出来るかな?と思ったけど https://www.google.com/logos/2014/rubiks/rubiks.html からスタートすると https://gstatic.com/logos/2014/rubiks/iframe/index.html をiframeで開く、という仕組み上 ムリそうだったのでextensionで実現することにした。


しかしcubeの状態を知ろうとしてもChrome Extensionのcontent_scriptsは"isolated world"で実行されるため、開いているページでブラウザが実行しているJavaScriptの変数や関数を直接弄ることはできない。

なので対象page上で任意のコードを実行させるために、文字列化した関数を含むscript要素を拡張側から挿入させるなどの方法があるようだ。


これを使ってcubeの状態が変更されるごとにそれを適当なDOM要素に埋め込み、解を計算する処理を行いたいときにはそのDOM要素から値を読み込む、という方法でデータを受け渡しするようにしてみた。その他のイベント処理なんかはwindow.postMessageとか使って適当にやりとりしつつ。



いやーしかしこんなの考えたエルノー・ルービック先生すごいし 様々な解法やアルゴリズム考え出したヒトたちすごい。