all repos — erl @ main

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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package command

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

	"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
	}

	signal := syscall.SIGINT
	cmd.out.Info("[sending %s to command]\n", signal)
	var target int
	if cmd.SysProcAttr != nil && cmd.SysProcAttr.Pgid != 0 {
		target = cmd.SysProcAttr.Pgid
	} else {
		target = -cmd.Process.Pid
	}
	err := syscall.Kill(target, signal)
	if err != nil {
		return fmt.Errorf("error killing command: %v", err)
	}

	exited := make(chan struct{})
	go func() {
		if cmd.Process != nil {
			err := cmd.Wait()
			if err != nil {
				exErr := new(exec.ExitError)
				if errors.As(err, &exErr) {
					cmd.out.Fail("[error waiting for command: %s]\n", err)
				}
			}
		}
		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 = syscall.Kill(target, syscall.SIGKILL)
			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.SysProcAttr = &syscall.SysProcAttr{
		Setpgid: true,
		Pgid:    0,
	}
	cmd.Stdout = cmd.opts.Stdout
	cmd.Stderr = cmd.opts.Stderr
}