Help us understand the problem. What is going on with this article?

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

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

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

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

どんな感じ?

こんな感じ。

pst.gif

対応OS

  • Mac OS(Catalinaで動いたことは確認できている)
  • Linux

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

画面構成

画面は全部で以下の3つがあります。

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

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

使い方

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

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

image.png

実装のお話

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

プロセス周り

プロセス一覧は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の優れたところはGridFlexといったレイアウトを簡単に作れるところです。
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つの画面のそれぞれの縦幅を渡せばあとはよしなにやってくれます。めっちゃ簡単。
更に、gridgridに格納することもできるので、縦2分割して右側を横2分割する、といったレイアウトも簡単につくれます。とても便利。

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

作ってみた感想

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

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

余談

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

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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away