少し前にAdvent Calendarのネタでswiftfsと言うツールを作ったのですが、Goでデーモンプログラムをどう書けば良いのかよくわからなかった。以下の記事を参考にしたけど、そもそもfork()しちゃいかんとなると、なかなか難しいですね。
で、swiftfsは以下のような実装にしました。syscall使うとWindowsとかで動かなくなってしまうとか、そもそも長いとか、あまり良い実装ではないように思います。Go的にもっとい良い方法があったら教えて下さい。コード全文は末尾とGistにあります。
親プロセス、子プロセス(デーモン本体)共通
func main() {
var child *bool = flag.Bool("child", false, "Run as a child process")
flag.Parse()
fork()できないので、親プロセス、子プロセスの判別をflagで行います。コマンドに--childが付いていれば子プロセスと判断。
親プロセス
// 子プロセスとのパイプを作っておく
r, w, err := os.Pipe()
if err != nil {
return err
}
cmd := exec.Command(os.Args[0], args...)
cmd.ExtraFiles = []*os.File{w}
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
パイプを使って親プロセスと子プロセスの間でプロセス間通信をします。cmd.ExtraFilesに適当なos.File(この場合はw)を渡しておくと、実行したプロセス側のファイルディスクリプタに割り当てられます。割り当てられるのは3番からです。
子プロセスの起動完了を親側で待ったり、初期化処理でエラーになったことを親側で知るためにこうしました。親側では適切な終了コードとともにプログラムを終了できます。そして
if err = cmd.Start(); err != nil {
return err
}
cmd.Start()で子プロセスを起動します。argsに--childをセットしてあるので、起動されたプロセスはchildMain()が実行されます。
// パイプから子プロセスの起動状態を取得する
var status int = DAEMON_START
go func() {
buf := make([]byte, 1)
r.Read(buf)
if int(buf[0]) == DAEMON_SUCCESS {
status = int(buf[0])
} else {
status = DAEMON_FAIL
}
}()
// 子プロセスの起動を30秒待つ
i := 0
for i < 60 {
if status != DAEMON_START {
break
}
time.Sleep(500 * time.Millisecond)
i++
}
// 親プロセス終了
if status == DAEMON_SUCCESS {
return nil
} else {
return fmt.Errorf("Child failed to start")
}
パイプの読み込み側(変数r)には子プロセス側から何かしら状態が返ってきます。それを待つgoroutineを作り、ステータスの変更を待ちます。もし子プロセスがシグナルなどを受け取って異常終了しても、タイムアウトして適切に(エラーで)終了できるようにしてます。
子プロセス
func childMain() {
// 初期化処理があればここに
var err error
// 子プロセスの起動状態を親プロセスに通知する
pipe := os.NewFile(uintptr(3), "pipe")
if pipe != nil {
defer pipe.Close()
if err == nil {
pipe.Write([]byte{DAEMON_SUCCESS})
} else {
pipe.Write([]byte{DAEMON_FAIL})
}
}
これはサンプルなので初期化処理は省いています。初期化が終わった後、子プロセス側はファイルディスクリプタ3番に自身の起動状態を書き込みます。ここは単純にint型で状態を表しています。
コード全文
package main
import (
"flag"
"fmt"
"log"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
)
const (
DAEMON_START = 1 + iota
DAEMON_SUCCESS
DAEMON_FAIL
)
func main() {
var child *bool = flag.Bool("child", false, "Run as a child process")
flag.Parse()
if !*child {
// parent
if err := parentMain(); err != nil {
log.Fatalf("Error occurred [%v]", err)
os.Exit(1)
} else {
os.Exit(0)
}
} else {
// child
childMain()
}
}
func parentMain() (err error) {
args := []string{"--child"}
args = append(args, os.Args[1:]...)
// 子プロセスとのパイプを作っておく
r, w, err := os.Pipe()
if err != nil {
return err
}
cmd := exec.Command(os.Args[0], args...)
cmd.ExtraFiles = []*os.File{w}
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err = cmd.Start(); err != nil {
return err
}
// パイプから子プロセスの起動状態を取得する
var status int = DAEMON_START
go func() {
buf := make([]byte, 1)
r.Read(buf)
if int(buf[0]) == DAEMON_SUCCESS {
status = int(buf[0])
} else {
status = DAEMON_FAIL
}
}()
// 子プロセスの起動を30秒待つ
i := 0
for i < 60 {
if status != DAEMON_START {
break
}
time.Sleep(500 * time.Millisecond)
i++
}
// 親プロセス終了
if status == DAEMON_SUCCESS {
return nil
} else {
return fmt.Errorf("Child failed to start")
}
}
func childMain() {
// 初期化処理があればここに
var err error
// 子プロセスの起動状態を親プロセスに通知する
pipe := os.NewFile(uintptr(3), "pipe")
if pipe != nil {
defer pipe.Close()
if err == nil {
pipe.Write([]byte{DAEMON_SUCCESS})
} else {
pipe.Write([]byte{DAEMON_FAIL})
}
}
// SIGCHILDを無視する
signal.Ignore(syscall.SIGCHLD)
// STDOUT, STDIN, STDERRをクローズ
syscall.Close(0)
syscall.Close(1)
syscall.Close(2)
// プロセスグループリーダーになる
syscall.Setsid()
// Umaskをクリア
syscall.Umask(022)
// / にchdirする
syscall.Chdir("/")
// main loop
for {
time.Sleep(1000 * time.Millisecond)
}
}