github.com/chenbh/concourse/v6@v6.4.2/worker/runtime/killer.go (about) 1 package runtime 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "syscall" 8 "time" 9 10 "github.com/containerd/containerd" 11 "github.com/containerd/containerd/cio" 12 "github.com/containerd/containerd/runtime/v2/runc/options" 13 "github.com/containerd/typeurl" 14 ) 15 16 const ( 17 // GracefulSignal is the signal sent to processes when giving them the 18 // opportunity to shut themselves down by their own means. 19 // 20 GracefulSignal = syscall.SIGTERM 21 22 // UngracefulSignal is the signal sent to the init process in the pid 23 // namespace to force its shutdown. 24 // 25 UngracefulSignal = syscall.SIGKILL 26 27 // GracePeriod is the duration by which a graceful killer would let a 28 // set of processes finish by themselves before going ungraceful. 29 // 30 GracePeriod = 10 * time.Second 31 ) 32 33 type KillBehaviour bool 34 35 const ( 36 KillGracefully KillBehaviour = false 37 KillUngracefully KillBehaviour = true 38 ) 39 40 //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . Killer 41 42 // Killer terminates tasks. 43 // 44 type Killer interface { 45 // Kill terminates a task either with a specific behaviour. 46 // 47 Kill( 48 ctx context.Context, 49 task containerd.Task, 50 behaviour KillBehaviour, 51 ) error 52 } 53 54 // killer terminates the processes exec'ed in a task. 55 // 56 // Only processes created through `task.Exec` are targetted to receive the the 57 // first signals it delivers. 58 // 59 type killer struct { 60 gracePeriod time.Duration 61 processKiller ProcessKiller 62 } 63 64 // KillerOpt is a functional option that modifies the behavior of a killer. 65 // 66 type KillerOpt func(k *killer) 67 68 // WithProcessKiller modifies the default process killer used by the task 69 // killer. 70 // 71 func WithProcessKiller(f ProcessKiller) KillerOpt { 72 return func(k *killer) { 73 k.processKiller = f 74 } 75 } 76 77 // WithGracePeriod configures the grace period used when waiting for a process 78 // to be gracefully finished. 79 // 80 func WithGracePeriod(p time.Duration) KillerOpt { 81 return func(k *killer) { 82 k.gracePeriod = p 83 } 84 } 85 86 func NewKiller(opts ...KillerOpt) *killer { 87 k := &killer{ 88 gracePeriod: GracePeriod, 89 processKiller: NewProcessKiller(), 90 } 91 92 for _, opt := range opts { 93 opt(k) 94 } 95 96 return k 97 } 98 99 // Kill delivers a signal to each exec'ed process in the task. 100 // 101 func (k killer) Kill(ctx context.Context, task containerd.Task, behaviour KillBehaviour) error { 102 switch behaviour { 103 case KillGracefully: 104 success, err := k.gracefullyKill(ctx, task) 105 if err != nil { 106 return fmt.Errorf("graceful kill: %w", err) 107 } 108 if !success { 109 err := k.ungracefullyKill(ctx, task) 110 if err != nil { 111 return fmt.Errorf("ungraceful kill: %w", err) 112 } 113 } 114 case KillUngracefully: 115 err := k.ungracefullyKill(ctx, task) 116 if err != nil { 117 return fmt.Errorf("ungraceful kill: %w", err) 118 } 119 } 120 return nil 121 } 122 123 func (k killer) ungracefullyKill(ctx context.Context, task containerd.Task) error { 124 err := k.killTaskExecedProcesses(ctx, task, UngracefulSignal) 125 if err != nil { 126 return fmt.Errorf("ungraceful kill task execed processes: %w", err) 127 } 128 129 return nil 130 } 131 132 func (k killer) gracefullyKill(ctx context.Context, task containerd.Task) (bool, error) { 133 err := k.killTaskExecedProcesses(ctx, task, GracefulSignal) 134 switch { 135 case errors.Is(err, ErrGracePeriodTimeout): 136 return false, nil 137 case err != nil: 138 return false, fmt.Errorf("kill task execed processes: %w", err) 139 } 140 141 return true, nil 142 } 143 144 // killTaskProcesses delivers a signal to every live process that has been 145 // created through a `task.Exec`. 146 // 147 func (k killer) killTaskExecedProcesses(ctx context.Context, task containerd.Task, signal syscall.Signal) error { 148 procs, err := taskExecedProcesses(ctx, task) 149 if err != nil { 150 return fmt.Errorf("task execed processes: %w", err) 151 } 152 153 err = k.killProcesses(ctx, procs, signal) 154 if err != nil { 155 return fmt.Errorf("kill procs: %w", err) 156 } 157 158 return nil 159 } 160 161 // taskProcesses retrieves a task's processes. 162 // 163 func taskExecedProcesses(ctx context.Context, task containerd.Task) ([]containerd.Process, error) { 164 pids, err := task.Pids(context.Background()) 165 if err != nil { 166 return nil, fmt.Errorf("pid listing: %w", err) 167 } 168 169 procs := []containerd.Process{} 170 for _, pid := range pids { 171 if pid.Info == nil { // init 172 continue 173 } 174 175 // the protobuf message has a "catch-all" field for `pid.Info`, 176 // thus, we need to unmarshal the message ourselves. 177 // 178 info, err := typeurl.UnmarshalAny(pid.Info) 179 if err != nil { 180 return nil, fmt.Errorf("proc details unmarshal: %w", err) 181 } 182 183 pinfo, ok := info.(*options.ProcessDetails) 184 if !ok { 185 return nil, fmt.Errorf("unknown proc detail type") 186 } 187 188 proc, err := task.LoadProcess(ctx, pinfo.ExecID, cio.Load) 189 if err != nil { 190 return nil, fmt.Errorf("load process: %w", err) 191 } 192 193 procs = append(procs, proc) 194 } 195 196 return procs, nil 197 } 198 199 // killProcesses takes care of delivering a termination signal to a set of 200 // processes and waiting for their statuses. 201 // 202 func (k killer) killProcesses(ctx context.Context, procs []containerd.Process, signal syscall.Signal) error { 203 204 // TODO - this could (probably *should*) be concurrent 205 // 206 207 for _, proc := range procs { 208 err := k.processKiller.Kill(ctx, proc, signal, k.gracePeriod) 209 if err != nil { 210 return fmt.Errorf("proc kill: %w", err) 211 } 212 } 213 214 return nil 215 }