1
2
3
4
5
6
7
8
9
10
11
12
13
14 package bpm
15
16 import (
17 "context"
18 "fmt"
19 "os"
20 "os/exec"
21 "sync"
22 "syscall"
23
24 "github.com/shirou/gopsutil/process"
25
26 ctrl "sigs.k8s.io/controller-runtime"
27 )
28
29 var log = ctrl.Log.WithName("background-process-manager")
30
31 type NsType string
32
33 const (
34 MountNS NsType = "mnt"
35
36
37 IpcNS NsType = "ipc"
38 NetNS NsType = "net"
39 PidNS NsType = "pid"
40
41
42 )
43
44 var nsArgMap = map[NsType]string{
45 MountNS: "m",
46
47
48 IpcNS: "i",
49 NetNS: "n",
50 PidNS: "p",
51
52
53 }
54
55 const (
56 pausePath = "/usr/local/bin/pause"
57 nsexecPath = "/usr/local/bin/nsexec"
58
59 DefaultProcPrefix = "/proc"
60 )
61
62
63 type ProcessPair struct {
64 Pid int
65 CreateTime int64
66 }
67
68
69 type BackgroundProcessManager struct {
70 deathSig *sync.Map
71 identifiers *sync.Map
72 }
73
74
75 func NewBackgroundProcessManager() BackgroundProcessManager {
76 return BackgroundProcessManager{
77 deathSig: &sync.Map{},
78 identifiers: &sync.Map{},
79 }
80 }
81
82
83 func (m *BackgroundProcessManager) StartProcess(cmd *ManagedProcess) error {
84 var identifierLock *sync.Mutex
85 if cmd.Identifier != nil {
86 lock, _ := m.identifiers.LoadOrStore(*cmd.Identifier, &sync.Mutex{})
87
88 identifierLock = lock.(*sync.Mutex)
89
90 identifierLock.Lock()
91 }
92
93 err := cmd.Start()
94 if err != nil {
95 log.Error(err, "fail to start process")
96 return err
97 }
98
99 pid := cmd.Process.Pid
100 procState, err := process.NewProcess(int32(cmd.Process.Pid))
101 if err != nil {
102 return err
103 }
104 ct, err := procState.CreateTime()
105 if err != nil {
106 return err
107 }
108
109 pair := ProcessPair{
110 Pid: pid,
111 CreateTime: ct,
112 }
113
114 channel, _ := m.deathSig.LoadOrStore(pair, make(chan bool, 1))
115 deathChannel := channel.(chan bool)
116
117 log := log.WithValues("pid", pid)
118
119 go func() {
120 err := cmd.Wait()
121 if err != nil {
122 err, ok := err.(*exec.ExitError)
123 if ok {
124 status := err.Sys().(syscall.WaitStatus)
125 if status.Signaled() && status.Signal() == syscall.SIGTERM {
126 log.Info("process stopped with SIGTERM signal")
127 }
128 } else {
129 log.Error(err, "process exited accidentally")
130 }
131 }
132
133 log.Info("process stopped")
134
135 deathChannel <- true
136 m.deathSig.Delete(pair)
137
138 if identifierLock != nil {
139 identifierLock.Unlock()
140 m.identifiers.Delete(*cmd.Identifier)
141 }
142 }()
143
144 return nil
145 }
146
147
148 func (m *BackgroundProcessManager) KillBackgroundProcess(ctx context.Context, pid int, startTime int64) error {
149 log := log.WithValues("pid", pid)
150
151 p, err := os.FindProcess(int(pid))
152 if err != nil {
153 log.Error(err, "unreachable path. `os.FindProcess` will never return an error on unix")
154 return err
155 }
156
157 procState, err := process.NewProcess(int32(pid))
158 if err != nil {
159
160 return nil
161 }
162 ct, err := procState.CreateTime()
163 if err != nil {
164 log.Error(err, "fail to read create time")
165
166 return nil
167 }
168 if startTime != ct {
169 log.Info("process has already been killed", "startTime", ct, "expectedStartTime", startTime)
170
171 return nil
172 }
173
174 ppid, err := procState.Ppid()
175 if err != nil {
176 log.Error(err, "fail to read parent id")
177
178 return nil
179 }
180 if ppid != int32(os.Getpid()) {
181 log.Info("process has already been killed", "ppid", ppid)
182
183 return nil
184 }
185
186 err = p.Signal(syscall.SIGTERM)
187
188 if err != nil && err.Error() != "os: process already finished" {
189 log.Error(err, "error while killing process")
190 return err
191 }
192
193 pair := ProcessPair{
194 Pid: pid,
195 CreateTime: ct,
196 }
197 channel, ok := m.deathSig.Load(pair)
198 if ok {
199 deathChannel := channel.(chan bool)
200 select {
201 case <-deathChannel:
202 case <-ctx.Done():
203 return ctx.Err()
204 }
205 }
206
207 log.Info("Successfully killed process")
208 return nil
209 }
210
211
212 func DefaultProcessBuilder(cmd string, args ...string) *ProcessBuilder {
213 return &ProcessBuilder{
214 cmd: cmd,
215 args: args,
216 nsOptions: []nsOption{},
217 pause: false,
218 identifier: nil,
219 ctx: context.Background(),
220 }
221 }
222
223
224 type ProcessBuilder struct {
225 cmd string
226 args []string
227
228 nsOptions []nsOption
229
230 pause bool
231 localMnt bool
232
233 identifier *string
234
235 ctx context.Context
236 }
237
238
239 func GetNsPath(pid uint32, typ NsType) string {
240 return fmt.Sprintf("%s/%d/ns/%s", DefaultProcPrefix, pid, string(typ))
241 }
242
243
244 func (b *ProcessBuilder) SetNS(pid uint32, typ NsType) *ProcessBuilder {
245 return b.SetNSOpt([]nsOption{{
246 Typ: typ,
247 Path: GetNsPath(pid, typ),
248 }})
249 }
250
251
252 func (b *ProcessBuilder) SetNSOpt(options []nsOption) *ProcessBuilder {
253 b.nsOptions = append(b.nsOptions, options...)
254
255 return b
256 }
257
258
259 func (b *ProcessBuilder) SetIdentifier(id string) *ProcessBuilder {
260 b.identifier = &id
261
262 return b
263 }
264
265
266 func (b *ProcessBuilder) EnablePause() *ProcessBuilder {
267 b.pause = true
268
269 return b
270 }
271
272 func (b *ProcessBuilder) EnableLocalMnt() *ProcessBuilder {
273 b.localMnt = true
274
275 return b
276 }
277
278
279 func (b *ProcessBuilder) SetContext(ctx context.Context) *ProcessBuilder {
280 b.ctx = ctx
281
282 return b
283 }
284
285 type nsOption struct {
286 Typ NsType
287 Path string
288 }
289
290
291 type ManagedProcess struct {
292 *exec.Cmd
293
294
295
296 Identifier *string
297 }
298