
第二版がでます。1章結構書き直しました。特にGo Modulesなど。

レビューに参加しました


kibelaのCLI。
30を超えている。特に使っているものが以下
ワンコマンドで実行、もしくはCI上で実行するが、中身としては以下のような手順で実施している。
github.com/Songmu/godzil
/) 派./cmd/myapp/ 派 ← ぼくはこっちツールやライブラリで使い分ける人もいるようですが、僕は一律、 cmd/ を切ってそれ以下にコードを配置している。
package main 及び、 main.main() はボイラープレートのみにpackage main 及び、 main.main() はボイラープレートのみにos.Exit するのはボイラープレートのみ。自分では書いてはいけないgo run ./cmd/myapp/main.go とかできる
gore -pkg . が異常に便利ですgo get してもツールが入らない分かりづらさgo get github.com/Songmu/go-hoge/cmd/hogefunc() main 変遷func main() {
os.Exit((&ghch.CLI{ErrStream: os.Stderr, OutStream: os.Stdout}).Run(os.Args[1:]))
}
Run(argv []string) int メソッドを呼び出すRun() がintを返すのでその値で os.Exit するfunc main() {
log.SetFlags(0)
err := kibelasync.Run(os.Args[1:], os.Stdout, os.Stderr)
if err != nil && err != flag.ErrHelp {
log.Println(err)
exitCode := 1
if ecoder, ok := err.(interface{ ExitCode() int }); ok {
exitCode = ecoder.ExitCode()
}
os.Exit(exitCode)
}
}
err := kibelasync.Run(os.Args[1:], os.Stdout, os.Stderr)
CLI 型を作るのは止めた
Run だけPublicインターフェースにしておけばいいCLI 型を公開する必要はないif err != nil && err != flag.ErrHelp {
内部で flagSet作るときに flag.NewFlagSet("myapp", flag.ContinueOnError) にしているので、 flag.ErrHelp がここまで上がってくることがある。その場合は正常終了としている。
flagパッケージのデフォルトのflagSet(= flag.CommandLine) はExitOnErrorになってる(flag parseエラーでos.Exit(2)が呼ばれる)けど、これはEasy Usageのためのやつだと認識していて、基本的には使わない主義。
エラーがあったら基本終了コード1だけど、errの中から終了コードが取り出せる場合はそれで終了する。
exitCode := 1
if ecoder, ok := err.(interface{ ExitCode() int }); ok {
exitCode = ecoder.ExitCode()
}
os.Exit(exitCode)
ExitCode() int がimplされていたらそこから終了コードを取る。結構そういうinterfaceをimplしているパッケージはある。例えばurfave/cli にもそういうinterfaceが定義されている。他にも github.com/Code-Hex/exit とか。
// github.com/urfave/cli/errors.go
type ExitCoder interface {
error
ExitCode() int
}
err.(interface{ ExitCode() int }) とassertionすれば良いだけなので、外部パッケージに依存しなくていいのもポイント。
とはいえあまり、ちゃんとExitCodeを綿密に定義することは少ないが。
コマンドラッパーとはコマンドを引数で受け取ってそれを実行しつつ別のこともやってくれるもの。
プロジェクトで独自のラッパーシェルを作ることも多いでしょう
テストにコケる度にシーザーが死ぬ仕組みを作りました by moznion
https://moznion.hatenadiary.com/entry/20130305/1362467136
% cpanm App::WithSound
% with-sound /path/to/cmd
コマンド実行中に音を出す、コマンド失敗したら別の音を出す
簡単!
import "os/exec"
cmd := exec.CommandContext("/path/to/command", "option...")
// 同期的呼び出し
err := cmd.Run()
// バックグランド呼び出し
cmd.Start()
// do something
cmd.Wait()
*os.File じゃなくてio.Reader/Writerになっているのが良い
*os.Filecmd.Output/CombinedOutput などもあるけど取り上げませんbytes.Buffer が基本。io.Reader/Writerをimplしてる便利なやつ。
var outbuf, errbuf bytes.Buffer
cmd.Stdin = &outbuf
cmd.Stderr = &errbuf
err := cmd.Run()
fmt.Println("stdout: " + outbuf.String())
fmt.Println("stderr: " + errbuf.String())
io.MultiWriter で複数Writerに書き出せる
var outbuf, errbuf, mergedBuf bytes.Buffer
outWtr := io.MultiWriter(&outbuf, &mergedBuf)
errWtr := io.MultiWriter(&errbuf, &mergedBuf)
cmd.Stdin = outWtr
cmd.Stderr = errWtr
err := cmd.Run()
fmt.Println("stdout: " + outbuf.String())
fmt.Println("stderr: " + errbuf.String())
fmt.Println("merged: " + mergedBuf.String())
cmd.StdoutPipe() で読み出しパイプを作り、io.TeeReader で別の場所に書き出したあと、io.Copy を使ってos.Stdoutに書き戻している。
pipe, err := cmd.StdoutPipe()
var buf bytes.Buffer
reader := io.TeeReader(pipe, buf)
err := cmd.Start()
_, err := io.Copy(os.Stdout, reader)
err := cmd.Wait()
fmt.Println(buf.String())
(MultiWriterを使っても同様のことは実現できるけど説明のため)
golang.org/x/text/transform.Transformer が便利
import "github.com/Songmu/timestamper"
import "golang.org/x/text/transform"
pipe, err := cmd.StdoutPipe()
defer pipe.Close()
reader := transform.NewReader(pipe, timestamper.New())
err := cmd.Start()
_, err := io.Copy(os.Stdout, reader)
err := cmd.Wait()
コマンドの標準出力もエラー出力もOSの出力にそれぞれ出しつつ、標準出力とエラー出力の内容はそれぞれ変数に保持し、マージ出力も保持しマージ出力にはタイムスタンプを付与する。
みたいなこともできるようになります。horensoは内部的にはそういうことをやっている。
backtickやsystem()などの同期呼び出し方法はあるが、バックグラウンドで呼び出そうとすると大変。
pipe開いて, forkしてpipeつないで、不必要なFDはちゃんと閉じる
my @command = ('path/to/command');
pipe my $rpipe, $wpipe;
unless (my $pid = fork) {
if (defined $pid) {
# child process
close $rpipe; close $wpipe;
open STDERR, '>&', $wpipe;
open STDOUT, '>&', $wpipe;
exec @command;
die "exec(2) failed:$!";
} else {
close $rpipe; close $wpipe;
die "fork(2) failed:$!";
}
} else {
# main process
close $wpipe;
while (my $log <$wpipe>) {
# do something
}
close $wpipe;
}
bytes.Bufferio.MultiWriterio.TeeReaderio.Copygolang.org/x/text/transform.Transformerexec.Command(...).Env = append(os.Environ(), "key=val") とかで渡す
os.Environ() が必要なことに注意os.Entenv("key") とかで取得するcmd.Command("/path/to/cmd", info) とかで渡すos.Args[len(os.Args)-1] とかで取るそうそう超えないが、機械的にコマンド組み立てたりしてて油断してると超えることも
getconf ARG_MAX
MAX_ARG_STRLEN runtime定数
コマンド プロンプトで使用できる文字列の最大長は 8191 文字です。
CreateProcess を呼び出す場合だと、32737文字が上限(UNICODE)
The maximum length of this string is 32,768 characters
:thinking:
一時ファイルを作って、そのファイルパスを環境変数などで通知する
r, w := io.Pipe()
cmd := exec.Command(...)
cmd.Stdin = r
cmd.Start()
// wに書き込む処理
fmt.Fprint(w, ...)
os.Stdin から読み出せば良い
標準入力だけでは足りない場合
r, w, _ := os.Pipe()
cmd := exec.Command(...)
cmd.ExtraFiles = []*os.File{r}
cmd.Start()
// wに書き込む処理
// 3から始まる(0:Stdin, 1:Stdout, 2:Stderr)
pipe := os.NewFile(uintptr(3), "pipe")
逆に、入出力を逆にすることもできる。
ちょっと駆け足になってしまったのでこのあたり理解深めたければ
「Goならわかるシステムプログラミング」
がおすすめです。
kill - terminate a process
The command kill sends the specified signal to the specified process or process group
-- man 1 kill
killコマンドとはプロセスまたはプロセスグループにシグナルを送るものである。
プロセスはシグナルをトラップし、ハンドリングすることができる。以下のようなことも可能。
ただしSIGKILLとSIGSTOPはハンドリングできない。
プロセスを停止させたい場合はまずはSIGTERMで正常終了を促すのがお作法。それでも止まらない場合はSIGKILLを送る。
標準に exec.CommandContext がある
ctx, cancel := context.WithCancel(context.Background())
cmd := command.CommandContext(ctx, "sleep", "100")
err := cmd.Start()
cancel() // <- 停止
err = cmd.Wait()
ctx, cancel := context.WithCancel(context.Background(), time.Second*10)
defer cancel()
cmd := command.CommandContext(ctx, "sleep", "100")
err := cmd.Run() // <- 10秒で停止
func (c *Cmd) Start() 抜粋
select {
case <-c.ctx.Done():
c.Process.Kill() // <- 内部的に p.Signal(Kill) 呼び出してるいる…!
case <-c.waitDone:
}
sh -c の場合2000 pts/0 Ss 0:00 -main
2001 pts/0 S+ 0:00 \_ sh -c echo; sleep 100000000
2002 pts/0 S+ 0:00 \_ sleep 100000000
exec.CommandContext(ctx, "sh", "-c", "echo; sleep 100000000") の場合
sh のプロセス(この場合PID2001)のみにシグナルが送られそれが停止するsleep (PID2002)は生き残り、孤児プロセスとなってinitに引き取られるsleep だとそれほど害はないが、シェル経由で呼び出したコマンドが暴走した場合、
sh を止めたとしても暴走プロセスは止まらないため、リソースを食い続けることになる。
など
https://github.com/golang/go/issues/21135


いろいろな提案がされたものの、Russ Cox氏直々にClose。「標準じゃないパッケージでやればいいよね」
一定時間を超えたコマンドをタイムアウトさせ、その際コマンドを正しく終了させる。
import "github.com/Songmu/timeout"
cmd := exec.Command("/path/to/command")
tio := &timeout.Timeout{
Cmd: cmd,
Duration: 10 * time.Second,
KillAfter: 5 * time.Second,
Signal syscall.SIGTERM, // Default SIGTERM on Unix
}
err := tio.RunContext(context.Background())
% go get github.com/Songmu/timeout/cmd/go-timeout
% go-timeout 30 /path/to/command
https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html
![]()
coreutilsに入っている、一定実行時間をコマンドをタイムアウトさせるコマンド。
% timeout 30 /path/to/command
Macだと brew install coreutils で入る。cronなどで便利。バッチスキー(バッチが好きな人)の道具箱に常備されている。(他の便利道具に setlock, sotflimit 等がある)
このあたりは、GNU timeoutとだいたい同じことをやっている
それぞれアプローチが異なる。
setpgid (0, 0)man 2 kill)If pid equals 0, then sig is sent to every process in the process group of the calling process.
/* timeout.c */
static int
send_sig (pid_t where, int sig)
{
/* If sending to the group, then ignore the signal,
so we don't go into a signal loop. Note that this will ignore any of the
signals registered in install_cleanup(), that are sent after we
propagate the first one, which hopefully won't be an issue. Note this
process can be implicitly multithreaded due to some timer_settime()
implementations, therefore a signal sent to the group, can be sent
multiple times to this process. */
if (where == 0)
signal (sig, SIG_IGN);
return kill (where, sig);
}
呼び出しプロセス をプロセスグループリーダーにしている。
func (tio *Timeout) getCmd() *exec.Cmd {
if tio.Cmd.SysProcAttr == nil {
tio.Cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
}
return tio.Cmd
}
そしてそのプロセスグループに対してシグナルを送っている(後述)
func Kill(pid int, signum syscall.Signal) (err error)
cmd.Process.Signal(sig os.Signal) ではプロセスグループにシグナルを送れないので、syscall.Kill にpidの負値を渡す。
また、Process.Signal() は os.Signal を受け取るが、 syscall.Kill() が受け取るのは syscall.Signal である点も注意。
syssig, ok := sig.(syscall.Signal)
err := syscall.Kill(-cmd.Process.Pid, syssig)
これで完璧か?
シグナルを送ったあとに、更にSIGCONTを送っている。これはどういうことか。
/* The normal case is the job has remained in our
newly created process group, so send to all processes in that. */
if (!foreground)
{
send_sig (0, sig);
if (sig != SIGKILL && sig != SIGCONT)
{
send_sig (monitored_pid, SIGCONT);
send_sig (0, SIGCONT);
}
}
→SIGCONTを送ることで強制的に再開させる
これで正しくプロセスを停止させられるようになりました。
func (tio *Timeout) getCmd() *exec.Cmd {
if tio.Cmd.SysProcAttr == nil {
tio.Cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
}
return tio.Cmd
}
func (tio *Timeout) terminate() error {
sig := tio.signal()
syssig, ok := sig.(syscall.Signal)
if !ok || tio.Foreground {
return tio.Cmd.Process.Signal(sig)
}
err := syscall.Kill(-tio.Cmd.Process.Pid, syssig)
if err != nil {
return err
}
if syssig != syscall.SIGKILL && syssig != syscall.SIGCONT {
return syscall.Kill(-tio.Cmd.Process.Pid, syscall.SIGCONT)
}
return nil
}
WaitStatus.ExitStatus() はシグナルを受けたときには常時-1になるこのサンプルでは、 w.Signaled() で判定して、シグナルを受けた場合は、 シグナル番号に128を足した値を返却している。ただ、この128を加算するルールもあくまでシェルのルールであるため、厳密とは言えないかもしれない。
err := cmd.Wait()
exitCode, signaled := resolveExitCode(err)
func resolveExitCode(err error) (int, bool) {
if err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
if w.Signaled() {
return int(w.Signal())+128, true
}
return status.ExitStatus(), false
}
}
// The exit codes in some platforms aren't integer. e.g. plan9.
return -1, false
}
return 0, false // 正常終了
}
https://github.com/Songmu/wrapcommander
WaitStatus.Signaled が存在しない
→ wrapcommanderはそのあたりをいい感じに解決してくれます
var est *wrapcommander.ExitStatus = wrapcommander.ResolveExitStatus(err)
cmd := exec.Command("/path/to/cmd")
cmd.Run()
exitCode := cmd.ProcessState.ExitCode()
これもシグナル終了かどうかを取りたい場合は、以下のように syscall.WaitStatus を取り出してください。
st, ok := cmd.Process.Sys().(syscall.WaitStatus)