🦍

簡易なバックグラウンドプロセス管理ツールを実装してみた

はじめに

ども、最近Claude Codeにすべてをかけているゴリラです。
Claude Codeくんを使って開発していく中で、ときたまバックグラウンドでプロセスを実行したいときがあります。

例えばnpm run devで開発サーバーを起動して、playwright mcpで画面の確認をしようとすると、
Claude Codeくんは普通にnpm run dev &を実行しようとします。

しかしそれだとバックグラウンドで実行できないので、簡易な管理ツールをCaude Codeくんに作ってもらいました。

README.mdをClaude Codeくんに食わせてバックグラウンドでプロセスを動かしたいときはこれを使ってねって依頼するとこうふうにいい感じにやってくれます。

Image from Gyazo

作ったもの

見えないところでプロセスが動くということで ghost(幽霊) という名前にしてみました。

https://github.com/skanehira/ghost

主な機能としては以下のようになっています。

  • プロセスの起動・停止
  • プロセスの状態確認
  • プロセスの標準・エラー出力の確認
  • TUI画面

使い方はこんな感じです。

# sleep 10 をバックグラウンドで実行
$ ghost run sleep 10

# タスク一覧
$ ghost list

# タスク停止
$ ghost stop {taskid}

# TUI起動
$ ghost

アーキテクチャ

同じようなツールとして以下のものがありますが、どちらもdaemonを常駐してプロセスを管理させる設計になっています。

ghostはdaemonを使わず、シンプルにsqliteにプロセスの管理情報を持たせて、コマンド実行時にプロセスの実行状態と同期させています。
プロセスの厳密の管理ができないというデメリットがありますが、今回の用途ではこれで充分です。
プロセスの出力に関してはファイルに書き出すようにしていて、必要なときに中身を参照できるようにしています。

┌────────────┐                    ┌────────────┐
│ ghost CLI  │ ──── spawn ──────▶ │ background │
│ (clap)     │                    │  process   │
└────────────┘                    └────────────┘
     │                                    │
     │ write                        write │
     ▼                                    ▼
┌────────────┐                    ┌────────────┐
│ SQLite DB  │                    │ Log Files  │
│ (tasks.db) │                    │ (*.log)    │
├────────────┤                    └────────────┘
│ Task table │                           │
│ PID info   │                      read │
│ Status     │                           ▼
└────────────┘                    ┌────────────┐
     ▲                            │   ghost    │
     │ read/update                │    TUI     │
     └────────────────────────────│ (ratatui)  │
                                  └────────────┘

またpueueとtask-spoolerと大きな違いとしてghostはTUI画面を用意しています。
やはり時代はTUIですね。

プロセス管理

受け取ったコマンドをバックグラウンドで実行する処理はこの部分ですが、ポイントはsetsid(2)を実行している部分です。

https://github.com/skanehira/ghost/blob/f3dfcab4c19907b28754a7f30abd470b37b88d39/src/app/process.rs#L44-L62

setsid(2)を理解するにはまずセッションとプロセスグループについて理解する必要があります。

プロセスグループは関連するプロセスの集合でシグナルを一括で送信できる単位として機能します。
セッションは、1つ以上のプロセスグループの集合です。

setsid(2)を使うと、子プロセスが新しいセッションとプロセスグループのリーダーになり、実行しているghostとターミナルから分離されます。
プロセスグループリーダーなると、孫以降がそのプロセスグループにまとめられるので、プロセスグループをkillすると孫以降のプロセスをまとめてkillできます。

実行前の状態:
┌─────────────────────────────────┐
│ Terminal Session (SID=1000)     │
│ ┌─────────────────────────────┐ │
│ │ Shell (PID=1000, PGID=1000) │ │
│ │ └─ ghost (PID=2000)         │ │
│ └─────────────────────────────┘ │
│ terminal: /dev/pts/0            │
└─────────────────────────────────┘

setsid()実行後:
┌─────────────────────────────────┐  ┌────────────────────────────┐
│ Terminal Session (SID=1000)     │  │ New Session (SID=2001)     │
│ ┌─────────────────────────────┐ │  │ ┌────────────────────────┐ │
│ │ Shell (PID=1000, PGID=1000) │ │  │ │ Task (PID=2001)        │ │
│ └─────────────────────────────┘ │  │ │ PGID=2001              │ │
│ terminal: /dev/pts/0            │  │ └────────────────────────┘ │
└─────────────────────────────────┘  │ terminal: nothing          │
                                     └────────────────────────────┘

シーケンス図にするとこんな感じです。

Vibe Codingについて

がっつりAIに実装してもらったものはこれが初めてだけど、そこまで詰まることなく開発できたかなという印象です。
アーキテクチャの壁打ちから一緒にやりましたが、自分の知らないことも多々あり勉強になることも多かったです。

実装はAIを使ったほうが速かったんですが、TUIの部分に関してはUIの調整が難しかったです。例えばborderの位置がズレていたりします。
画像を貼って指示してもなかなか想定通りのUIにできなかったので、そういった部分は手修正しました。

こういう小さなツールを作るとき、もうClaude Codeなしでは作れない体になってしまったのかもしれないです。ぴえん

さいごに

プロセス管理についてあんまりよくわかっていなかったので、今回の実装でちょっと理解できたのはよかったです。
また、RustではTUIを作ったのも初めてだけど、ratatuiは割と直感的な設計になっていてコードがわかりやすかったです。

Discussion