kurumi-bioの雑記帳

プログラミング、パソコン、ペット、 犬、お出かけ

初心者のGo言語 -37- <CommandContext>

こんにちは、kurumi-bioです。
第8回目のexecパッケージ(標準ライブラリー)の学習です。

環境

  • Windows
    OSバージョン:Windows11 Home 22H2
    Go言語のバージョン:go version go1.20.3 windows/amd64
  • Linux
    OSバージョン:openSUSE Leap 15.4
    Go言語のバージョン:go version go1.20.3 linux/amd64

CommandContext関数

func CommandContext(ctx context.Context, name string, arg ...string) *Cmd

関数の説明 CommandContext は Command に似ていますが、コンテキストが含まれています。

コマンドが単独で完了する前にコンテキストが完了した場合、提供されたコンテキストは (cmd.Cancel または os.Process.Kill を呼び出して) プロセスを中断するために使用されます。

CommandContext は、コマンドの Cancel 関数を設定して、その Process で Kill メソッドを呼び出し、その WaitDelay を未設定のままにします。呼び出し元は、コマンドを開始する前にこれらのフィールドを変更することで、キャンセルの動作を変更できます。

[contextパッケージ] WithDeadline関数

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

関数の説明 WithDeadline は、期限が d までに調整された親コンテキストのコピーを返します。親の期限がすでに d より早い場合、WithDeadline(parent, d) は意味的に親と同等です。返されたコンテキストの Done チャネルは、期限が切れたとき、返されたキャンセル関数が呼び出されたとき、または親コンテキストの Done チャネルが閉じられたときのいずれか早い方で閉じられます。

このコンテキストをキャンセルすると、関連付けられているリソースが解放されるため、コードは、このコンテキストで実行されている操作が完了したらすぐにキャンセルを呼び出す必要があります。

[contextパッケージ] WithTimeout関数

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

関数の説明 WithTimeout は WithDeadline(parent, time.Now().Add(timeout)) を返します。

このコンテキストをキャンセルすると、それに関連付けられているリソースが解放されるため、コードは、このコンテキストで実行されている操作が完了したらすぐにキャンセルを呼び出す必要があります。

◆テストコード

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("Start")
    time.Sleep(3 * time.Second)
    fmt.Println("End")
}

コードの説明 exec.CommandContext()関数を試すためのテストコードです。
time.Sleep()関数を使って、3秒間一時停止します。
起動時に標準出力に"Start"と出力し、3秒後に"End"と出力します。
予め'go build sleep.go`で実行可能ファイルを作成しておきます。

◆テストコード

package main

import (
    "context"
    "fmt"
    "io"
    "os"
    "os/exec"
    "strconv"
    "time"
)

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintln(os.Stderr, "Timeoutまでの秒数を指定してください。")
        os.Exit(1)
    }

    i, _ := strconv.Atoi(os.Args[1])
    t := time.Duration(i)

    ctx, cancel := context.WithTimeout(context.Background(), t*time.Second)
    defer cancel()

    c := exec.CommandContext(ctx, "./sleep.exe")
    o, e := c.StdoutPipe()
    if e != nil {
        fmt.Fprintf(os.Stderr, "%v\n", os.NewSyscallError("Cmd.StdoutPipe", e))
        os.Exit(1)
    }

    if err := c.Start(); err != nil {
        fmt.Fprintf(os.Stderr, "%v\n", os.NewSyscallError("Cmd.Start", err))
        os.Exit(1)
    }

    b, _ := io.ReadAll(o)
    fmt.Printf("%s\n", b)

    if err := c.Wait(); err != nil {
        fmt.Fprintf(os.Stderr, "%v\n", os.NewSyscallError("Cmd.Wait", err))
        os.Exit(1)
    }
}

コードの説明 起動時の引数をcontext.WithTimeout()関数のタイムアウト値に指定してコンテキストを生成しています。このコンテキストをexec.CommandContext()関数の引数に渡していますので、 ./sleep.exeモジュールの実行時間が、起動時の引数を超えるとプロセスが中断されます。

◆実行結果(Windows)

実行結果の説明 タイムアウト値に2秒を指定して実行しました。
Sleep.exeの処理時間が3秒かかるので、処理中にプロセスが中断したため"End"が出力されませんでした。
さらに、Cmd.Wait()関数のerr戻り値の値がexit status 1となっていました。

◆実行結果(Windows)

実行結果の説明 タイムアウト値に4秒を指定して実行しました。
Sleep.exeの処理時間を超えていたためプロセスが中断されないで"End"が出力されました。

◆実行結果(Linux)

実行結果の説明 Linuxで実行した場合は、中断時にsignal: killedと表示されました。
Windowsより分かりやすいエラー内容でした。

◆テストコード

package main

import (
    "context"
    "fmt"
    "io"
    "os"
    "os/exec"
    "strconv"
    "time"
)

func main() {
    const layout = "2006-01-02 15:04:05"

    if len(os.Args) != 2 {
        fmt.Fprintln(os.Stderr, "期限までの秒数を指定してください。")
        os.Exit(1)
    }
    i, _ := strconv.Atoi(os.Args[1])
    t := time.Duration(i)

    n := time.Now()
    d := n.Add(t * time.Second)
    fmt.Printf("開始時刻:[%s]\n", n.Format(layout))
    fmt.Printf("期限時刻:[%s]\n", d.Format(layout))

    ctx, cancel := context.WithDeadline(context.Background(), d)
    defer cancel()

    c := exec.CommandContext(ctx, "./sleep.exe")
    o, e := c.StdoutPipe()
    if e != nil {
        fmt.Fprintf(os.Stderr, "%v\n", os.NewSyscallError("Cmd.StdoutPipe", e))
        os.Exit(1)
    }

    if err := c.Start(); err != nil {
        fmt.Fprintf(os.Stderr, "%v\n", os.NewSyscallError("Cmd.Start", err))
        os.Exit(1)
    }

    b, _ := io.ReadAll(o)
    fmt.Printf("%s\n", b)

    if err := c.Wait(); err != nil {
        fmt.Printf("中断時刻:[%s]\n", time.Now().Format(layout))
        fmt.Fprintf(os.Stderr, "%v\n", os.NewSyscallError("Cmd.Wait", err))
        os.Exit(1)
    }
    fmt.Printf("終了時刻:[%s]\n", time.Now().Format(layout))
}

コードの説明 期限(Time)が来たら処理を中断するcontext.WithDeadline()関数を使用してコンテキストを生成しています。 今回は、起動時に指定した秒数が経過したら処理を中断するようにしています。 実行すると、開始時刻と期限時刻(開始時刻+起動時の引数秒)を標準出力に出力します。
処理が中断または完了すると、その時の時刻を標準出力に出力します。

◆実行結果(Windows)

実行結果の説明 期限を2秒後にして実行しました。
期限時刻が開始時刻の2秒後になっており、2秒後に処理が中断しexit status 1が出力されました。

◆実行結果(Windows)

実行結果の説明 期限を4秒後にして実行しました。
期限時刻が開始時刻の4秒後になっていますが、sleep.exeの処理が終わった3秒後に完了しています。

最後までご覧いただきありがとうございます