ども、バナナとナタデココにハマっているゴリラです。つまり食物繊維大好きゴリラ。
最近なぜかプロセスをkillすることが多くて、毎度コマンド打つの面倒だったので2日くらいかけてTUIツールを作ってみました。
今日はそのツールの紹介と実装の話をしていきます。
どんな感じ?
こんな感じ。
対応OS
- Mac OS(Catalinaで動いたことは確認できている)
- Linux
一応Windowsでも動くはずですが、動作確認していないのでダメだったらごめんなさい。
Windowsで動いたとしてもプロセスの詳細情報は見れないですが、今後対応予定なので、お待ち頂ければと思います。
画面構成
画面は全部で以下の3つがあります。
- processes(現在動いているプロセス一覧)
- process info(選択したプロセスの詳細情報)
- process tree(選択したプロセスのツリー)
画面に加えて、プロセス一覧を絞り込むための入力欄があります。自分のPCでは大体400〜くらいのプロセスが動いているので、絞り込みの機能は必須でした。
使い方
作ったばっかりなので、最低限の機能(個人的に)しか実装していなくて
- プロセスの絞り込み
- プロセスの強制終了(SIG_KILL)
くらいしかないです。
プロセスの矯正終了はプロセス一覧とプロセスツリーのパネルでK
を押下すると確認ダイアログがポップするので、Killを選択するとプロセスを強制終了します。
実装のお話
今回の実装にあたり、主に以下のライブラリを使いました。
プロセス周り
プロセス一覧はgo-ps
で取得し、pidを使ってps
コマンドで詳細情報を取っています。
プロセスを取得するにはシステムコールを使用しますが、OSごとに取れる情報が異なる上、システムコールのインターフェイスも異なります。
システムコールのこと調べても良くわからなかった上、OSの差分吸収はとても面倒だったので最低限pid、ppidとコマンドを取得できるgo-psを使いました。
欲を言えば、もっとプロセスの細かい情報を取得したかったのですが、そこか諦めて外部コマンドに頼ることにしました。。
システムコールを学ぶにはC言語から学んだ方が良いらしいので、ちょっと勉強しようかな…
話は戻して、pidとppidが取れるので、どのプロセスが子プロセスを持っているのか判定できます。
そこでプロセスツリーを作れると思い、そのインターフェイスを実装しました。
プロセスの構造体はとてもシンプルで次になっています。
type Process struct {
Pid int
PPid int
Cmd string
Child []Process
}
抜粋ですがChild
に子プロセスを次のようにして格納しています。
ちなみに、pidが0のプロセスはシステムコールで取得できますが、こちらはps
コマンドで情報を取れない上、
すべてのプロセスの大本なので表示する意味がないためskipするようにしています。
pids := make(map[int]Process)
for _, proc := range processes {
// skip pid 0
if proc.Pid() == 0 {
continue
}
pids[proc.Pid()] = Process{
Pid: proc.Pid(),
PPid: proc.PPid(),
Cmd: proc.Executable(),
}
}
// add child processes
for _, p := range processes {
if p.Pid() == p.PPid() {
continue
}
if proc, ok := pids[p.PPid()]; ok {
proc.Child = append(proc.Child, pids[p.Pid()])
pids[p.PPid()] = proc
}
}
プロセスの詳細情報はこんな感じでps
コマンドを使って取ってきてそのまま表示させています。
func (p *ProcessManager) Info(pid int) (string, error) {
// TODO implements windows
if runtime.GOOS == "windows" {
return "", nil
}
if pid == 0 {
return "", nil
}
buf := bytes.Buffer{}
cmd := exec.Command("ps", "-o", "pid,ppid,%cpu,%mem,lstart,user,command", "-p", strconv.Itoa(pid))
cmd.Stdout = &buf
cmd.Stderr = &buf
if err := cmd.Run(); err != nil {
return "", err
}
return buf.String(), nil
}
-o
を使用することでフォーマットを自由にカスタマイズできるので、必要そうな情報だけピックアップしました。
どんな情報が取れるかはman ps
のkeywordの部分を参照して下さい。
-p
はプロセスIDを指定することでそのプロセスIDの情報だけ取得できます。
こういった出力をカスタマイズできるコマンドはGoでツールを作るときにありがたいです。
ちなみに、Windowsはpsコマンドがない(はず…)ので一旦TODOにしています。Windows機購入したので届いてから対応しようと思います。
画面周り
画面周りはtview
を使いました。以前DockerのTUI Clientを作ったときはgocui
というライブラリを使っていましたが、こちらはメンテされていない上、ダイアログといったコンポーネント画面を作るのはめんどくさいです。
一応、コンポーネントを簡単に作れるgocui-componentというライブラリを作ったのですが、tview
が圧倒的に高機能かつ便利だったので採用しました。
tviewの優れたところはGrid
やFlex
といったレイアウトを簡単に作れるところです。
TUIはウェブと違って結構画面周りはめんどくさいので、こういった画面のサポートが強力なライブラリはすごくありがたいです。
抜粋ですが、画面のレイアウトはこんな感じで実装しています。
infoGrid := tview.NewGrid().SetRows(0, 0).
AddItem(g.ProcInfoView, 0, 0, 1, 1, 0, 0, true).
AddItem(g.ProcessTreeView, 1, 0, 1, 1, 0, 0, true)
grid := tview.NewGrid().SetRows(1, 0).
SetColumns(30, 0).
AddItem(g.FilterInput, 0, 0, 1, 1, 0, 0, true).
AddItem(g.ProcessManager, 1, 0, 1, 1, 0, 0, true).
AddItem(infoGrid, 1, 1, 1, 1, 0, 0, true)
g.Pages = tview.NewPages().
AddAndSwitchToPage("main", grid, true)
grid
を作って、AddItem
を使って画面をgrid
に格納する事ができます。SetColumns
は縦幅を定義する関数で、縦に3つ分けたいなら3つの画面のそれぞれの縦幅を渡せばあとはよしなにやってくれます。めっちゃ簡単。
更に、grid
をgrid
に格納することもできるので、縦2分割して右側を横2分割する、といったレイアウトも簡単につくれます。とても便利。
まだぼくも良くわかっていないところもあるので、きちんと理解したらtview
の使い方に関する記事を書こうと思います。
作ってみた感想
プロセスやシステムコール周りについて調べて少しだけ理解した(つもり)ので良い収穫でした。
OSのような低レイヤー周りはやっぱり奥が深くて難しいですが、面白いです。
最近30日でOSを作る本が結構良いらしいので、ちょっと買って勉強しようかなと思います。
そして、このツールは意外と便利なのでぜひ使ってみてください。感想お待ちしています。
余談
Macのシステムコールについて調べたら良さげの資料を見つけたので、一応共有しておきます。