Goでプロセス監視のTUIツールを作ったら便利だった件

ども、バナナとナタデココにハマっているゴリラです。つまり食物繊維大好きゴリラ。

最近なぜかプロセスをkillすることが多くて、毎度コマンド打つの面倒だったので2日くらいかけてTUIツールを作ってみました。

<a href="https://github.com/skanehira/pst"><img src="https://github-link-card.s3.ap-northeast-1.amazonaws.com/skanehira/pst.png" width="460px"></a>

今日はそのツールの紹介と実装の話をしていきます。

# どんな感じ？
こんな感じ。

![pst.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66178/da9e2129-1f84-76ee-5510-ec73a5f6568b.gif)

## 対応OS
- Mac OS（Catalinaで動いたことは確認できている）
- Linux

一応Windowsでも動くはずですが、動作確認していないのでダメだったらごめんなさい。
Windowsで動いたとしてもプロセスの詳細情報は見れないですが、今後対応予定なので、お待ち頂ければと思います。

## 画面構成
画面は全部で以下の３つがあります。

- processes（現在動いているプロセス一覧）
- process info（選択したプロセスの詳細情報）
- process tree（選択したプロセスのツリー）

画面に加えて、プロセス一覧を絞り込むための入力欄があります。自分のPCでは大体400〜くらいのプロセスが動いているので、絞り込みの機能は必須でした。

## 使い方
作ったばっかりなので、最低限の機能（個人的に）しか実装していなくて
- プロセスの絞り込み
- プロセスの強制終了（SIG_KILL）
くらいしかないです。

プロセスの矯正終了はプロセス一覧とプロセスツリーのパネルで`K`を押下すると確認ダイアログがポップするので、Killを選択するとプロセスを強制終了します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66178/64ba7991-f91a-e68d-fda1-bc81e909871f.png)


## 実装のお話
今回の実装にあたり、主に以下のライブラリを使いました。

- [go-ps](https://github.com/mitchellh/go-ps)
- [tview](https://github.com/rivo/tview)

### プロセス周り
プロセス一覧は`go-ps`で取得し、pidを使って`ps`コマンドで詳細情報を取っています。
プロセスを取得するにはシステムコールを使用しますが、OSごとに取れる情報が異なる上、システムコールのインターフェイスも異なります。
システムコールのこと調べても良くわからなかった上、OSの差分吸収はとても面倒だったので最低限pid、ppidとコマンドを取得できるgo-psを使いました。

欲を言えば、もっとプロセスの細かい情報を取得したかったのですが、そこか諦めて外部コマンドに頼ることにしました。。
システムコールを学ぶにはC言語から学んだ方が良いらしいので、ちょっと勉強しようかな…

話は戻して、pidとppidが取れるので、どのプロセスが子プロセスを持っているのか判定できます。
そこでプロセスツリーを作れると思い、そのインターフェイスを実装しました。

プロセスの構造体はとてもシンプルで次になっています。

```go
type Process struct {
	Pid   int
	PPid  int
	Cmd   string
	Child []Process
}
```

抜粋ですが`Child` に子プロセスを次のようにして格納しています。
ちなみに、pidが0のプロセスはシステムコールで取得できますが、こちらは`ps`コマンドで情報を取れない上、
すべてのプロセスの大本なので表示する意味がないためskipするようにしています。

```go
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`コマンドを使って取ってきてそのまま表示させています。

```go
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](https://github.com/skanehira/docui)を作ったときは`gocui`というライブラリを使っていましたが、こちらはメンテされていない上、ダイアログといったコンポーネント画面を作るのはめんどくさいです。

一応、コンポーネントを簡単に作れる[gocui-component](https://github.com/skanehira/gocui-component)というライブラリを作ったのですが、`tview`が圧倒的に高機能かつ便利だったので採用しました。

tviewの優れたところは`Grid`や`Flex`といったレイアウトを簡単に作れるところです。
TUIはウェブと違って結構画面周りはめんどくさいので、こういった画面のサポートが強力なライブラリはすごくありがたいです。

抜粋ですが、画面のレイアウトはこんな感じで実装しています。

```go
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`は縦幅を定義する関数で、縦に３つ分けたいなら３つの画面のそれぞれの縦幅を渡せばあとはよしなにやってくれます。めっちゃ簡単。
更に、`grid`を`grid`に格納することもできるので、縦２分割して右側を横２分割する、といったレイアウトも簡単につくれます。とても便利。

まだぼくも良くわかっていないところもあるので、きちんと理解したら`tview`の使い方に関する記事を書こうと思います。

## 作ってみた感想
プロセスやシステムコール周りについて調べて少しだけ理解した（つもり）ので良い収穫でした。
OSのような低レイヤー周りはやっぱり奥が深くて難しいですが、面白いです。
最近30日でOSを作る本が結構良いらしいので、ちょっと買って勉強しようかなと思います。

そして、このツールは意外と便利なのでぜひ使ってみてください。感想お待ちしています。

## 余談
Macのシステムコールについて調べたら良さげの資料を見つけたので、一応共有しておきます。

https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/sysctl.3.html