all repos — erl @ b63d8d33dc0b786fdc9937770645d4c6e914a9dc

Execute Reload Loop

command/command.go (view raw)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package command

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"strings"

	"alin.ovh/erl/output"
)

type Command interface {
	Start() error
	Wait() error
	Stop(context.Context) error
}

type Cmd struct {
	*exec.Cmd

	name string
	args []string

	out output.Output

	opts Options
}

type Options struct {
	Output io.Writer
	Stdout io.Writer
	Stderr io.Writer
}

func New(name string, args []string, options Options) *Cmd {
	if options.Stdout == nil {
		options.Stdout = os.Stdout
	}
	if options.Stderr == nil {
		options.Stderr = os.Stderr
	}
	if options.Output == nil {
		options.Output = os.Stderr
	}

	return &Cmd{
		name: name,
		args: args,
		opts: options,

		out: output.New(options.Output),
	}
}

func (cmd *Cmd) Stop(ctx context.Context) error {
	if cmd.Cmd == nil || cmd.Process == nil || cmd.ProcessState != nil {
		return nil
	}

	err := cmd.Process.Signal(os.Interrupt)
	if err != nil {
		return fmt.Errorf("error killing command: %v", err)
	}

	exited := make(chan struct{})
	go func() {
		cmd.Wait()
		exited <- struct{}{}
		close(exited)
	}()

	for {
		select {
		case <-ctx.Done():
			err := ctx.Err()
			if !errors.Is(err, context.DeadlineExceeded) {
				return err
			}

			cmd.out.Fail("[command not stopped, killing]\n")
			err = cmd.Process.Kill()
			if err != nil && !errors.Is(err, os.ErrProcessDone) {
				return fmt.Errorf("error killing command: %v", err)
			}

			return nil
		case <-exited:
			return nil
		}
	}
}

func (cmd *Cmd) Exited() bool {
	return cmd.Cmd != nil && cmd.ProcessState != nil && cmd.ProcessState.Exited()
}

func (cmd *Cmd) Wait() error {
	err := cmd.Cmd.Wait()
	var exitErr *exec.ExitError
	if err != nil && !errors.As(err, &exitErr) {
		return err
	}

	switch {
	case !cmd.Exited():
		cmd.out.Info("[command stopped]\n")
	case cmd.ProcessState.Success():
		cmd.out.Success("[command finished]\n")
	default:
		cmd.out.Fail("[command exited with code %d]\n", cmd.ProcessState.ExitCode())
	}

	return nil
}

func (cmd *Cmd) Start() error {
	cmd.makeCommand()
	cmd.out.Success("[running: %s]\n", strings.Join(append([]string{cmd.name}, cmd.args...), " "))
	err := cmd.Cmd.Start()
	if err != nil {
		return fmt.Errorf("error starting command: %v", err)
	}

	return err
}

func (cmd *Cmd) makeCommand() {
	cmd.Cmd = exec.Command(cmd.name, cmd.args...)
	cmd.Stdout = cmd.opts.Stdout
	cmd.Stderr = cmd.opts.Stderr
}