golang版Exping「pexpo」を書きました。


追記 2017/09/21:http pingも打てるように機能を追加しました。kwskはイカ。

golang版Exping「pexpo」にhttp pingの追加とその実装。

pexpo

https://github.com/nnao45/pexpo

どういうツールなのか。

題名にもあるような、Exping的なものをgolangでterminal上で動くアプリに描き下ろしたものです。
とても軽く、IPv6も対応しており、pingのintervalも500msまでならしっかりと打てます。
windows,Mac,Linux等プラットフォームを選ばず動きますし、色も変な色になりません。

なぜ作ったのか。

筆者は普段ネットワークエンジニアをやっており、
Expingを愛用しているのですが、
他のPING監視ツールも含めもっとこうだったらな、なポイントがいくつかありました。

  • Expingは実装上、ping-listのホストの全部を事前にメモリ予約してping発射するので監視ホストを多くしすぎると(例えば100台)重い、というかそもそもメモリ不足で動かない。
  • Expingやその他のツールはIPv6が対応していない(なぜ・・・笑)。
  • UIはExpingのあのピピピピピピってスクロールするのが最高(TUIの他のツールだとホストが多いと画面から切れて見えなくなる)なので他のPingツールのUIがどうも気に入らない(超個人的な理由)。
  • 殆どのツールがWindowsでしか動かない(visual studio使ってたり)、Linuxでしか動かない(linuxのpingコマンドを直接スクリプトから叩いてたり)等、環境依存が激しい。
  • 今!NOW!誰が死んでるのかどのツールもわかりにくい(これ重要)。
  • そんな実装難しくないだろ・・・・ログくらい残してくれ・・・。
  • 一時停止出来なくて止めるとロスのカウントが初期化されるTUIアプリしかないんだが・・・。
  • ping-listくらいテキストファイルとかで手軽に自由に管理させてよ〜。
  • ping-listで無駄な縛り(IPアドレス以外にもホスト名書かないと駄目)とかやめて笑

という個人的な理由で作り上げたものでございます。

どういう実装なの。

ざっくりとまとめました。

  • Windowsでも動きます!!!Windows版とかありません!!!
  • 軽くするため+個人的にいつも書いてるgolangで書く。
  • GUIだとプラットフォーム依存が半端じゃ無いのでTUIで書く(というかgoは(ry )。TUIのエンジンはpeco等マルチプラットフォームでの実装例が多く、windows対応範囲が多いtermbox-goを使わせていただいております。
  • pingはプラットフォームに依存させない+IPv6対応+速くするためにgo-fastpingを使わせていただいております。
  • スクロールはさせるが重くなる+メモリ溢れ等恐いので、スクロールして見えなくなったping結果は割り切って全て捨ててます(ログには書き出す)。
  • ping結果がスクロールするので、たとえ20000台のホストがいたとしても、○×判定は見えるようにしてます。
  • 構造的にシンプルに、ping-listから1行1行読み出してfastping()に渡しているだけなのでたとえ300000台のホストがいても重くなる事はありません。でも最速ping interval 500msでちゃんと打てます。
  • しっかりEsc、Crtl+Cで即時抜けれる、更にCrtl+Sで即時一時停止→再開できる(意外とこれ大変なのよ笑)
  • ping-listも-fでファイルパスを指定出来ます。

ここから開発苦労話

半分愚痴や雑多な内容なのでgolangでのTUI開発に興味ないひとはアディオス!
なお、苦悩と苦労を重ねて無理やり実装してる部分も多いので、ご指摘などもお待ちしております。。。。

golangのTUIに関する情報が世の中にまっっっっっっっっったくもって存在しない(笑)

世にはtermbox-go以外にもgocuitermui等のちょ~~かっちょいいTUIエンジンがあるものの、いざ作ろうとなるとほぼほぼ情報は使うTUIエンジンのソースとexampleディレクトリ下の実装コードのみ(stack overflowにさえもほぼ情報はない)。今回のpexpoのような「動的に描写した文字列が上にスクロールする」なんてニッチな使い方はtermboxの_example下にはおろか、Google先生も知らない。今golangでTUI開発してる人って地下帝国で秘密裏に開発しているの?
・・・つまり描きたい描写はスクラッチでお前が考えろな環境である(今回の場合pythonのncursesでの実装をアルゴリズムベースでgolangに移植するとか、そんなことをやった)。

GolangのTUI開発の勉強会とか作ってみたいなと思ったりしてるけど・・・同志求む。

「記憶させる」ジレンマ。

追記:2017/09/19 0:55
そういえばイカのように関数に変数を閉じ込めて変数に関数を代入する形でやれば、グローバル変数をカウントアップさせるなんて要らない事が判明したのでこの部分はその後めちゃくちゃ修正した。

func intCounter() func(int) int {
    counter := 0
    return func(x int) int {
        counter++
        x = counter
        return x
        }
}
var i int // i is counter
fi := intCounter() // fi is having i counter value.
for {
        i = fi(i)
        なんかの関数(i)
}

みたいなね。
別にpexpoの機能には全くかかわらない部分だけど、まぁこれでモヤモヤは消えました。
尚可読性(ry

pexpoみたいな「pingのloss回数を記憶する」という動作をさせる際、ただ単純に「1回だけ処理をするだけの関数」であれば、変数なりなんなりに記憶させればいいが、今回のように「関数はfor文で何回でも呼び出されるが、結果は記憶させてカウントアップしたい」時の場合そうはいかない。変数の中身を毎回初期化させるわけにはいかないからだ。これを実現させる最も安価な実装は「ファイルに追記する」なんだが、最速500ms毎にファイルIOを発生なんかさせたくない・・・な所で、自分はグローバル変数+グローバルでbytes.bufferに結果を書き込んで、読み込むようにしている。おかげで可読性がお亡くなりになったのは言うまでもない・・・。もっと良い書き方があるのかな(結局なんでもかんでも全部の繰り返しの処理をmain()内で書くのが正解だったんだろうけど、それはそれで可読性が上がるか?と言われると微妙)、と思いつつも今更なのでやらない。

for文の中にgoroutineやfor文があって、その中にもgoroutineやfor文があって....

基本的にTUIアプリはキー割り込みが必須なのは言うまでもない(それがないとアプリを終了できません)。つまり、常にキー割り込みを監視させる関数を書くことが必須となり、自然とgoroutineで並行処理させる必要がある。更にキー割り込みは「for文を一時停止させる」「アプリ全体の処理を終わらせる」等の動きになってくるため、自然にforとgoroutineだらけになるので無理がある実装にするとすぐ重くなる(pexpoのような500msの間にping打って描写してスクロールしてログに書いて・・・等レスポンス重要なアプリは特に)上に、キー割り込みの差込口(要はselect文やその中のcase文の書く位置)をちゃんと書かないと想定したとおりにアプリが止まったり終了しなくなってしまうので、自ずと、TUIアプリを書く=goroutineやchannelの理解が一定以上求められるといえる(筆者があるとは一度も言っていない)。

因みにキー割り込みのお約束を書いておくがここに書いてあるように「キー割り込みを監視するgoroutine」と「キー入力をチャンネルで受け取るプロセス」は切り離して並列に走らせ、チャンネルでやり取りするというスタイルがどうやら安定して動作します(筆者経験談)。

私はただ、初期化したい画面の箇所だけ初期化したいのよ。

termboxにはfunc Clear(fg, bg Attribute) errorという画面全体を文字色背景色を指定して初期化する関数があるが、TUIを作っていると、これだとすぐに柔軟性に欠けてしまう。
pexpoだと「pingの結果の画面はスクロールさせるために打つたびに初期化したいが、pingのlossのカウントまで初期化されてしまうと、打つたびに今打ってるホスト以外の全てのlossカウントがゼロになっちゃう」等、うだうだ言ったが要は画面の左半分だけ初期化したい!がtermbox.Clear()では出来ないという状況である。
pexpoではtermboxにあるexampleの「fill関数」が便利だったのでそれを使って部分的に画面の初期化をすることでこの問題を解決している。

func fill(x, y, w, h int, cell termbox.Cell) {
    for ly := 0; ly < h; ly++ {
        for lx := 0; lx < w; lx++ {
            termbox.SetCell(x+lx, y+ly, cell.Ch, cell.Fg, cell.Bg)
        }
    }
}
/*ping-list clear*/
fill(JUDGE_X+1, DRAW_UP_Y, 1, maxY-4, termbox.Cell{Ch: ' '})
fill(HOST_X+1, DRAW_UP_Y, COLUMN-1, maxY-4, termbox.Cell{Ch: ' '})
fill(RTT_X+1, DRAW_UP_Y, COLUMN-1, maxY-4, termbox.Cell{Ch: ' '})
fill(DES_X+1, DRAW_UP_Y, COLUMN-1, maxY-4, termbox.Cell{Ch: ' '})

ふっふっふ・・・お気づきだろうか?そう!!ただその座標をスペースで埋めているだけである!!! \あっかり~ん/
(gocuiとかはそこらへんは「ウィンドウ」ってオブジェクトを別途持って解決しているみたいですね)

スクロールの実装について

追記:2017/09/25現在、実はbytes.bufferに書き出すのがどうも気に入らなく(ファイルに見立てるために改行コードを文末に入れるのがあんまり・・・)、[]stringappend()する形でイカを実現していますが、大枠は変わってません。まぁコードを見ればわかりますが、もうbytesパッケージさえ使ってません笑

もし、ここまで読んでくれている人がいるとするときっと「能書きはいいからどうやって画面をスクロールさせてるのか教えろ」と思っているかもしれないので、とりあえず隠さず書いておきます。

STEP① とりあえずy座標を1個ずつズラす。

スクロールさせるまでは図のように、
pingを実行して回数毎にy座標を+1して書くだけ。簡単だね☆

STEP② スクロールの上限まで来たら、その上限で結果を書き続けるように分岐。

termboxの関数を使ってmaxX, maxY := termbox.Size()だとかなんとかでターミナルのサイズを取得したら、どこまでスクロールさせたいか決まると思うので(maxYとか、maxY-1とか、maxY-yとか?)、そこまでスクロールが来たらif文で分岐させてスクロール止めて、そのmaxYとかmaxY-1とかmaxY-yとかで結果を表示するようにする。

STEP③ 画面に描写しておいた結果をどっかに保存しておいて、回数分yをズラす。

ここがややこしいかもしれません。STEP②でもそれっぽい画面にはなりますが、これだと画面の一番下がチカチカなってる謎のTUIとなりますのでスクロールさせねばなりません。
pexpoでは、実はSTEP②にあるy、y+1,y+2...の結果をbytes.bufferに溜めておいてあります。

これを、スクロールしきった時点で、次の回からテキストファイルのように一行一行読み込んで、読み込む度にスクロールしきった箇所からy座標を1つずらして表示させてます。

これをひたすら繰り返しているといった感じです。
ちょっぴりややこしいですが、まぁそんな感じで実装してます。

愚直に実装するしかない。

コードを書いている間、頭が一日に十回は爆発したので上に書いたように、描きたいアニメーションになるようにまずは図でイメージを固めるのが肝要だと思いました まる

最後に

ここまで書けるようになった上でタメになった書籍を記します。
ただ、自分が書きたい部分のアルゴリズムは最後は自分で詰めるしかないので、そこはしっかり頭に置いておいて書いていかねばというところでした。

プログラミング言語Go・・・とりあえず読んでおいて損はない本。やりたいアルゴリズムの土台は大体これを読めばある。
みんなのGo言語・・・価格対効果が高すぎるマストバイな参考書。テストやマルチプラットフォーム対応させる秘訣、Goらしい書き方等、独学の人が得にくいGoを書く上の「お作法」がぎゅっと詰まっている。
Effective Go・・・基礎から高度でわかりにくいけど良くGoプログラマが対面する問題について(reflectや埋め込みやチャンネルのチャンネルとか)わかりやすく書いている。