-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathmodule.go
146 lines (119 loc) · 3.48 KB
/
module.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
145
146
package caddyexec
import (
"context"
"fmt"
"os"
"os/exec"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyevents"
"go.uber.org/zap"
)
func init() {
caddy.RegisterModule(Handler{})
}
// Handler implements an event handler that runs a command/program.
// By default, commands are run in the background so as to not
// block the Caddy goroutine.
type Handler struct {
// The command to execute.
Command string `json:"command,omitempty"`
// Arguments to the command. Placeholders are expanded
// in arguments, so use caution to not introduce any
// security vulnerabilities with the command.
Args []string `json:"args,omitempty"`
// The directory in which to run the command.
Dir string `json:"dir,omitempty"`
// How long to wait for the command to terminate
// before forcefully closing it. Default: 30s
Timeout caddy.Duration `json:"timeout,omitempty"`
// If true, runs the command in the foreground,
// which will block and wait for output. Only
// do this if you know the command will finish
// quickly! Required if you want to abort the
// event.
Foreground bool `json:"foreground,omitempty"`
// If the command exits with any of these codes, the
// event will be signaled to abort with the error.
// Must be running in the foreground to apply.
AbortCodes []int `json:"abort_codes,omitempty"`
logger *zap.Logger
}
// CaddyModule returns the Caddy module information.
func (Handler) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "events.handlers.exec",
New: func() caddy.Module { return new(Handler) },
}
}
// Provision sets up the module.
func (eh *Handler) Provision(ctx caddy.Context) error {
eh.logger = ctx.Logger(eh)
if eh.Timeout <= 0 {
eh.Timeout = caddy.Duration(30 * time.Second)
}
if len(eh.AbortCodes) > 0 && !eh.Foreground {
return fmt.Errorf("must run commands in foreground to apply abort codes")
}
return nil
}
// Handle handles the event.
func (eh *Handler) Handle(ctx context.Context, e caddyevents.Event) error {
repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
// expand placeholders in command args;
// notably, we do not expand placeholders
// in the command itself for safety reasons
expandedArgs := make([]string, len(eh.Args))
for i := range eh.Args {
expandedArgs[i] = repl.ReplaceAll(eh.Args[i], "")
}
var cancel context.CancelFunc
if eh.Timeout > 0 {
ctx, cancel = context.WithTimeout(ctx, time.Duration(eh.Timeout))
}
cmd := exec.CommandContext(ctx, eh.Command, expandedArgs...)
cmd.Dir = eh.Dir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if eh.Foreground {
if cancel != nil {
defer cancel()
}
err := cmd.Run()
exitCode := cmd.ProcessState.ExitCode()
for _, abortCode := range eh.AbortCodes {
if exitCode == abortCode {
return fmt.Errorf("%w: %v", caddyevents.ErrAborted, err)
}
}
return err
}
go func() {
if cancel != nil {
defer cancel()
}
if err := cmd.Run(); err != nil {
eh.logger.Error("background command failed", zap.Error(err))
}
}()
return nil
}
// UnmarshalCaddyfile parses the module's Caddyfile config. Syntax:
//
// exec <command> <args...>
func (eh *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
if !d.NextArg() {
return d.ArgErr()
}
eh.Command = d.Val()
eh.Args = d.RemainingArgs()
}
return nil
}
// Interface guards
var (
_ caddyfile.Unmarshaler = (*Handler)(nil)
_ caddy.Provisioner = (*Handler)(nil)
)