アプリケーションが終了するときに、子プロセスも終了させる方法

とある実行プロセス内で実行した別プロセスを親プロセスが死んだ段階で子プロセスも終了させたいというユースケースは結構あるかと思います。

Linuxではプロセスグループというものがあって、そのグループに属しているプロセスは親プロセスが死んだら子プロセスも一緒に終了してくれます。

Windowsにはプロセスグループに相当するものとしてジョブオブジェクトというものがあります。

同じジョブに属しているプロセスは親プロセスが死んだら一緒に子プロセスも死んでくれますが、プログラムでジョブにプロセスを登録してあげる必要があります。

C#での登録の仕方は以下のサイトに記述があります。今回はこれをGoで書いてみようと思います。

qiita.com

Goで書いてみると以下のような感じになりました。以下のコードの例では notepad を3つ立ち上げて 3秒後に終了するという単純なプログラムになっています。

大まかな流れとしては以下のような感じです。

  • CreateJobObject でジョブを作成
  • JOBOBJECT_BASIC_LIMIT_INFORMATION 構造体の LimitFlagsJOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE を設定した JOBOBJECT_EXTENDED_LIMIT_INFORMATION 構造体を作成
  • SetInformationJobObject を作成した ジョブと JOBOBJECT_EXTENDED_LIMIT_INFORMATION 構造体で実行
  • AssignProcessToJobObject に 子プロセスのプロセスを渡して登録する
package main

import (
    "os"
    "os/exec"
    "time"
    "unsafe"

    "golang.org/x/sys/windows"
)

type JobObject struct {
    Handle windows.Handle
}

func CreateAsKillOnJobClose() (*JobObject, error) {
    job, err := windows.CreateJobObject(nil, nil)
    if err != nil {
        return nil, err
    }

    info := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{
        BasicLimitInformation: windows.JOBOBJECT_BASIC_LIMIT_INFORMATION{
            LimitFlags: windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
        },
    }

    _, err = windows.SetInformationJobObject(
        job,
        windows.JobObjectExtendedLimitInformation,
        uintptr(unsafe.Pointer(&info)),
        uint32(unsafe.Sizeof(info)))

    if err != nil {
        panic(err)
    }

    return &JobObject{job}, nil
}

func (obj *JobObject) CloseHandle() error {
    return windows.CloseHandle(obj.Handle)
}

func (obj *JobObject) AssignProcess(p *os.Process) error {
    type process struct {
        Pid    int
        Handle uintptr
    }

    if err := windows.AssignProcessToJobObject(
        obj.Handle,
        windows.Handle((*process)(unsafe.Pointer(p)).Handle)); err != nil {
        return err
    }

    return nil
}

func main() {
    cmds := make([]*exec.Cmd, 0)
    for i := 0; i < 3; i++ {
        cmd := exec.Command("notepad.exe")
        if err := cmd.Start(); err != nil {
            panic(err)
        }
        cmds = append(cmds, cmd)
    }

    jobObj, err := CreateAsKillOnJobClose()
    if err != nil {
        panic(err)
    }
    defer jobObj.CloseHandle()

    for _, cmd := range cmds {
        jobObj.AssignProcess(cmd.Process)
    }

    time.Sleep(3 * time.Second)
}

懸念

yotiky.hatenablog.com

上記の記事にも書かれているように親プロセスが終了すると子プロセスは強制終了させられるとのことで、ちゃんと終了したい場合は WM_CLOSE メッセージを送らないといけないかもとのことでした。

私のコード例ではそのあたりの処理が抜けているので、ちゃんとする場合はメッセージを送るような処理を入れる必要があるんでしょうね・・・