-
Notifications
You must be signed in to change notification settings - Fork 2
/
exec.go
144 lines (132 loc) · 4.01 KB
/
exec.go
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
// Package mute implements functions to execute other programs muting std streams if required
// license: MIT, see LICENSE for details.
package mute
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"strings"
"syscall"
)
// ExitErrExec is exit code when failed to execute the command
const ExitErrExec = 127
// execContext is the details of an executed command
type execContext struct {
Cmd string
ExitCode int
StdoutText *string
StderrText *string
Error error
}
// Target is the struct to specify what to exec, when to mute and where to print otherwise
type Target struct {
Cmd string
Args []string
Conf *Conf
OutWriter io.Writer
ErrWriter io.Writer
BufPreAlloc int // initial size (bytes) of the buffer for stdout/stderr
}
// Exec runs the target command muting the output when matched the configuration
// executes a command, checks the exit codes and matches stdout with patterns,
// and writes the stdout/sterr when configuration did not match.
// Return the exit code of cmd, and an error if any.
// Panics on empty Cmd.
func (t *Target) Exec() (int, error) {
if t.Cmd == "" {
panic("target cmd is empty")
}
crt := cmdCriteria(t.Cmd, t.Conf)
ctx := execCmd(t.Cmd, t.Args, t.BufPreAlloc)
if !matchesCriteria(crt, ctx.ExitCode, ctx.StdoutText) {
fmt.Fprintf(t.OutWriter, "%v", *ctx.StdoutText)
fmt.Fprintf(t.ErrWriter, "%v", *ctx.StderrText)
}
return ctx.ExitCode, ctx.Error
}
// execCmd runs the command with args and returns a pointer to an execContext
func execCmd(cmd string, args []string, bufPreAlloc int) *execContext {
var stdoutBuffer, stderrBuffer bytes.Buffer
if bufPreAlloc > 0 {
stdoutBuffer.Grow(bufPreAlloc)
stderrBuffer.Grow(bufPreAlloc)
}
var stdoutStr, stderrStr string
var cmdExitCode int
var err error
var ctx = execContext{Cmd: cmd}
var sigs = make(chan os.Signal, 1)
execCmd := exec.Command(cmd, args...)
execCmd.Stdout = &stdoutBuffer
execCmd.Stderr = &stderrBuffer
go func() {
sig := <-sigs
if execCmd.Process != nil { // signal may arrive before cmd starts
execCmd.Process.Signal(sig)
}
}()
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
if err = execCmd.Run(); err != nil {
switch e := err.(type) {
case *exec.ExitError:
cmdExitCode = e.ExitCode()
default:
cmdExitCode = ExitErrExec
}
}
stdoutStr = stdoutBuffer.String()
stdoutBuffer.Reset()
stderrStr = stderrBuffer.String()
stderrBuffer.Reset()
ctx.ExitCode = cmdExitCode
ctx.StdoutText = &stdoutStr
ctx.StderrText = &stderrStr
ctx.Error = err
return &ctx
}
// matchesCriteria indicates if results of an exec matches a given Criteria
// to decide if a program should be muted or not, its exit code and stdout/stderr is matched
// against the configured Criteria. This function helps to decide on mute or not
func matchesCriteria(criteria *Criteria, code int, stdout *string) bool {
for _, crt := range *criteria {
if crt.IsEmpty() {
continue
}
if len(crt.ExitCodes) < 1 || codesContain(crt.ExitCodes, code) {
if len(crt.StdoutPatterns) < 1 || stdoutMatches(crt.StdoutPatterns, stdout) {
return true
}
}
}
return false
}
// cmdCriteria returns the Criteria that the cmd should be matched against from the Conf
// Each command is matched against a criteria. The Conf has Criterias
// either per command or a default one that is used for all commands.
// cmdCriteria finds the corresponding Criterian from a Conf that the cmd
// should be checked against
func cmdCriteria(cmd string, conf *Conf) *Criteria {
matched := ""
for key := range conf.Commands {
if len(key) > len(matched) && strings.HasPrefix(cmd, key) {
matched = key
}
}
if matched == "" { // no command specific criteria matched cmd
return &conf.Default
}
criteria := conf.Commands[matched]
return &criteria
}
// stdoutMatches checks if string matches any of the specified StdoutPattern regex patterns
func stdoutMatches(patterns []*StdoutPattern, stdout *string) bool {
for _, p := range patterns {
if p.Regexp.MatchString(*stdout) {
return true
}
}
return false
}